Skip to content

09 Texts

Inan Evin edited this page Nov 3, 2024 · 20 revisions

LinaVG supports both traditional and SDF texts. Both of them share common properties, but API is separated for the sake of clarity.

Prior to text rendering, you have to initialize font loading state and bind callback functions for buffering font atlas data.

LinaVG::Text::Initialize(); // Do this only once for the app's lifetime.

LinaVG::Text m_lvgText; // this is your text object similar to Drawer API. Keep it alive through your app. You can have as many as you want, each one will have its own font and atlas management separately.

m_lvgText.GetCallbacks().atlasNeedsUpdate = std::bind(&GLBackend::AtlasNeedsUpdate, m_renderingBackend, std::placeholders::_1);

LinaVG::Text::Terminate(); // Do this only at app exit.

When loading fonts, LinaVG will call this callback whenever an internal atlas data changes. You need to create a texture in your gfx backend for every different Atlas* this function will pass, and whenever callback is triggered you need to update the data. You can fetch the current atlas data via:

uint8_t* data = atlas->GetData();
size_t dataSz = atlas->GetSize().x * atlas->GetSize().y;

Font Loading

During your app lifetime you can use LoadFont function to load your target fonts. LinaVG uses FreeType for font loading, so you can check their page to learn about the supported formats.

// For loading from file
LinaVGFont* LoadFont(const char* file, bool loadAsSDF, BackendHandle uniqueID, int size = 48, GlyphEncoding* customRanges = nullptr, int customRangesSize = 0);

// For loading from memory
LinaVGFont* LoadFontFromMemory(void* data, size_t dataSize, bool loadAsSDF, BackendHandle uniqueID, int size = 48, GlyphEncoding* customRanges = nullptr, int customRangesSize = 0);

// Fit the font into an appropriate atlas after loading
void AddFontToAtlas(Font* font);

LoadFont functions returns the created font instance. When you want to draw using this font, make sure to set the TextOptions.font parameter to the returned pointer. Before you could draw using the font, you will need to call AddFontToAtlas function to setup an appropriate atlas for your font. This is seperated from LoadFontXXX functions so that you could for example delegate the heavy work of loading the font on a different thread and add it to atlas when joined.

You are responsible for removing the fonts from atlases and deleting the pointers when you are done with the font.

// Before deleting the font make sure we remove it from LinaVG's atlas
LinaVG::Text text;
text.RemoveFontFromAtlas(m_font);
delete m_font;

You can use loadAsSDF parameter to load your fonts as SDF, which will be explained later on. Size parameter determines the height of the character glyphs, meanwhile width is automatically calculated based on size.

LoadFontXXX functions are not thread-safe! So remember to wrap them around mutexes if you are going to be calling them from multiple threads.

Normally, LoadFont function loads all the characters in the ASCII character set (32-128). If your font contains additional characters, you can define a custom range in 32-bit encoding to load those character ranges. Whatever array you pass, each pair of elements will be treated as a range.

//  Additionally loads 5 characters
GlyphEncoding customRanges[] = {0x015F, 0x015F, 0x011F, 0x011F, 0x0411, 0x0411, 0x25c0, 0x25c0, 0x00E7, 0x00E7 };
defaultFont                  = LinaVG::LoadFont("Resources/Fonts/SourceSansPro-Regular.ttf", false, 28, customRanges, 10);

// Additionally loads all characters between 0x015F and 0x00E7
GlyphEncoding customRanges2[] = {0x015F, 0x00E7F};
someOtherFont                 = LinaVG::LoadFont("Resources/Fonts/SourceSansPro-Regular.ttf", false, 28, customRanges2, 2);

Additional glyphs are usually used to render non-ASCII unicode characters. Remember to send your strings in utf-8 encoding, here is a cpp11 example:

lvgDrawer.DrawTextNormal(u8"My test string", ....);

For above C++11 where there is no implicit conversion between u8string and string, you can still encode your string into a wstring using wchars, and convert back to string/const char* and send it to DrawTextXXX functions.

Text Drawing

Drawing texts are pretty straight-forward:

LINAVG_API void DrawText(const LINAVG_STRING& text, const Vec2& position, const TextOptions& opts, float rotateAngle = 0.0f, int drawOrder = 0);

It accepts your text as string, a position, rotateAngle and drawOrder as other shapes & lines. One thing that has not been covered yet is it's style options, which is in its specific struct TextOptions.

TextOptions

EDIT: Drop shadow related options are removed in latest versions of LinaVG. Users can control their own draw order to emulate drop-shadow in user-land code, or do it through their own shaders.

Texts also support almost all other styling methods that are present in shapes. You can use TextOptions struct to determine:

  • Font
  • Flat color
  • Color, flat colors, vertical and horizontal gradients are supported. Radials won't work and will fall back to vertical. Usage is the same as StyleOptions.
  • Character spacing.
  • Maximum width, the text will wrap underneath after reaching this.
  • New line spacing on wrapped texts.
  • Drop shadow options such as color and offset.
  • Text alignment

One thing you gotta keep in mind is making sure to set the target font to use. LinaVG will draw a traditional or an SDF text depending on how the font was loaded.

TextOptions textOpts;
textOpts.font = textDemoFont;
lvgDrawer.DrawTextNormal("This is a normal text.", startPos, textOpts, rotateAngle, 1);

startPos.x += 300;
textOpts.dropShadowOffset = Vec2(2, 2);
lvgDrawer.DrawTextNormal("Drop shadow.", startPos, textOpts, rotateAngle, 1);

startPos.x += 300;
textOpts.color.start = Vec4(1, 0, 0, 1);
textOpts.color.start = Vec4(0, 0, 1, 1);
LinaVG::DrawTextNormal("Gradient color.", startPos, textOpts, rotateAngle, 1);

image

And here are examples of wrapped texts with different alignments:

startPos.x = screenSize.x * 0.05f;
startPos.y += 50;
textOpts.wrapWidth       = 100;
textOpts.dropShadowColor = Vec4(1, 0, 0, 1);
textOpts.color           = Vec4(1, 1, 1, 1);
lvgDrawer.DrawTextNormal("This is a wrapped text with a colored drop shadow.", startPos, textOpts, rotateAngle, 1);

startPos.x += 300;
textOpts.wrapWidth            = 100;
textOpts.alignment            = TextAlignment::Center;
textOpts.dropShadowOffset     = Vec2(0.0f, 0.0f);
textOpts.color.start          = Vec4(0.6f, 0.6f, 0.6f, 1);
textOpts.color.end            = Vec4(1, 1, 1, 1);
textOpts.color.gradientType   = GradientType::Vertical;
const Vec2 size               = LinaVG::CalculateTextSize("Center alignment and vertical gradient.", textOpts);
startPos.x += size.x / 2.0f;
lvgDrawer.DrawTextNormal("Center alignment and vertical gradient.", startPos, textOpts, rotateAngle, 1);

startPos.x += 300;
textOpts.color       = Vec4(0.8f, 0.1f, 0.1f, 1.0f);
textOpts.alignment   = TextAlignment::Right;
const Vec2 size2     = LinaVG::CalculateTextSize("Same, but it's right alignment", textOpts);
startPos.x += size.x;
LinaVG::DrawTextNormal("Same, but it's right alignment", startPos, textOpts, rotateAngle, 1);

image

SDF Texts

SDF texts are drawn using the exact same API. Previously there was options inside TextOptions struct to determine SDF parameters. However SDF rendering falls under user-land code, since it involves shaders. So these are now removed. Now if you try to draw using an SDF font, you will always receive a single buffer for all your SDF draw requests. This might be optimal or not, depending on the user code and how your shaders draw the SDF. If you need different materials, you could always make use of void* userData pointer in TextOptions!

image

Text Caching

If you are drawing around 500 or more texts per-frame, performance might get a small hit (0.5-1 ms~, debug mode) depending on the complexity of your texts. This hit is usually negligible all the way up to 2K or 3K texts on modern platforms running on Release mode, but if you are performance critical you can improve this.

LinaVG::Config.textCachingEnabled = true;

By setting these two configs, before you initialize LinaVG, you can allow the system to cache calculated vertices for your strings. System keeps a map of hashed strings - vertex data on the background, and uses this data for drawing your text on the consecutive calls with the same strings. Of course this will leave a small memory footprint, but it's up to you to decide. My general advice would be anything above 300 visible texts per-frame should be cached, assuming they are static.

Caching dynamic texts (like counters) does not bring any advantages, and it is recommended to skip caching them. You can use the last argument skipCache to skip caching functionality per DrawTextXXX call.

Clone this wiki locally