09 Texts - inanevin/LinaVG GitHub Wiki

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();
LinaVG::Text::GetCallbacks().fontTextureBind = std::bind(&GLBackend::BindFontTexture, m_renderingBackend, std::placeholders::_1);
LinaVG::Text::GetCallbacks().fontTextureCreate = std::bind(&GLBackend::CreateFontTexture, m_renderingBackend, std::placeholders::_1, std::placeholders::_2);
LinaVG::Text::GetCallbacks().fontTextureBufferData =std::bind(&GLBackend::BufferFontTextureAtlas, m_renderingBackend, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4, std::placeholders::_5);
LinaVG::Text::GetCallbacks().fontTextureBufferEnd = std::bind(&GLBackend::BufferEnded, m_renderingBackend);

When loading a font, LinaVG will adjust atlas configuration based on LinaVG::Config. It might ask you to create atlas textures, then buffer some glyph data into those textures given glyph size and offsets. By setting up above callbacks you can tie those actions to your own rendering backend.

Above code initializes the text state, so don't forget to terminate it before your app exits.

LinaVG::Text::Terminate();

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);

LoadFont functions returns the created font instance. When you want to draw using this font, make sure to set the TextOptions.font (or SDFTextOptions.font) parameter to the returned pointer.

You are responsible for deleting the font pointers when you are done with LinaVG & terminated the system.

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.

Traditional Texts

Rendering simple texts is straight forward. You can call DrawTextNormal method to do so.

LINAVG_API void DrawTextNormal(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

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 setting the target font to use. If your application uses a single font, you can omit this. If you don't set the font property in TextOptions, it will always use the latest loaded font.

If you set a font handle, you have to make sure that that font was loaded as non-SDF (LoadFont -> loadAsSDF = false), otherwise the system will complain and won't draw.

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 stand for signed-distance-field text rendering. It's an alternative to bitmaps, and produce much higher quality results. The thing with the SDFs is that they will look the same quality even when you scale, zoom or transform them. It's a great method, especially to render high quality UI texts such as big titles, or 3D in-game texts. I would definitely suggest taking a look at this Valve paper for more information on them.

In regards to LinaVG, rendering SDF texts is pretty similar to normal texts. Only thing you have to do is to make sure you load your font as SDF:

titleFont = LinaVG::LoadFont("Resources/Fonts/SourceSansPro-Regular.ttf", true, 65);

Then, instead of TextOptions, you need to use SDFTextOptions, which is a subclass of TextOptions. So it includes all the functionality mentioned above such as colors, gradients, drop shadows, character & line spacing, alignment and wrapping. Additionally, it offers thickness, softness and outline parameters. You also need to use the DrawTextSDF function this time.

startPos.y += 90;
startPos.x                       = screenSize.x * 0.05f;
const float    beforeSDFStartPos = startPos.y;
SDFTextOptions sdfOpts;
sdfOpts.font         = textDemoSDFFont;
sdfOpts.sdfThickness = 0.55f;
lvgDrawer.DrawTextSDF("An SDF text.", startPos, sdfOpts, rotateAngle, 1);

startPos.y += 50;
sdfOpts.sdfThickness  = 0.6f;
sdfOpts.color.start   = Vec4(1, 0, 0, 1);
sdfOpts.color.end     = Vec4(0, 0, 1, 1);
lvgDrawer.DrawTextSDF("Thicker SDF text", startPos, sdfOpts, rotateAngle, 1);

startPos.y += 50;
sdfOpts.sdfThickness = 0.7f;
sdfOpts.sdfSoftness  = 2.0f;
sdfOpts.color        = Vec4(0.1f, 0.8f, 0.1f, 1.0f);
lvgDrawer.DrawTextSDF("Smoother text", startPos, sdfOpts, rotateAngle, 1);

startPos.y += 50;
sdfOpts.color               = Vec4(1, 1, 1, 1);
sdfOpts.sdfThickness        = 0.6f;
sdfOpts.sdfSoftness         = 0.5f;
sdfOpts.sdfOutlineThickness = 0.1f;
sdfOpts.sdfOutlineColor     = Vec4(0, 0, 0, 1);
lvgDrawer.DrawTextSDF("Outlined SDF text", startPos, sdfOpts, rotateAngle, 1);

startPos.y += 50;
sdfOpts.sdfThickness        = 0.8f;
sdfOpts.sdfSoftness         = 0.5f;
sdfOpts.sdfOutlineThickness = 0.3f;
sdfOpts.sdfOutlineColor     = Vec4(0, 0, 0, 1);
lvgDrawer.DrawTextSDF("Thicker outline.", startPos, sdfOpts, rotateAngle, 1);

startPos.y += 50;
sdfOpts.sdfDropShadowThickness = 0.6f;
sdfOpts.dropShadowOffset       = Vec2(5, 5);
sdfOpts.sdfOutlineThickness    = 0.0f;
sdfOpts.sdfThickness           = 0.6f;
lvgDrawer.DrawTextSDF("Drop shadow.", startPos, sdfOpts, rotateAngle, 1);

startPos.y = beforeSDFStartPos;
startPos.x += 550;
sdfOpts.sdfDropShadowThickness = 0.0f;
sdfOpts.sdfOutlineThickness    = 0.0f;
sdfOpts.sdfThickness           = 0.6f;
sdfOpts.wrapWidth              = 150;
sdfOpts.alignment              = TextAlignment::Right;
LinaVG::DrawTextSDF("This is an SDF, wrapped and right aligned text.", startPos, sdfOpts, rotateAngle, 1);

startPos.y = beforeSDFStartPos;
startPos.x += 300;
sdfOpts.wrapWidth      = 150;
sdfOpts.alignment      = TextAlignment::Right;
sdfOpts.newLineSpacing = 15;
lvgDrawer.DrawTextSDF("Same, but a higher new line spacing.", startPos, sdfOpts, rotateAngle, 1);

sdfOpts.sdfThickness           = 0.6f;
sdfOpts.wrapWidth              = 150;
sdfOpts.alignment              = TextAlignment::Right;
lvgDrawer.DrawTextSDF("This is an SDF, wrapped and right aligned text.", startPos, sdfOpts, rotateAngle, 1);

startPos.y = beforeSDFStartPos;
startPos.x += 300;
sdfOpts.wrapWidth      = 150;
sdfOpts.alignment      = TextAlignment::Right;
sdfOpts.newLineSpacing = 15;
lvgDrawer.DrawTextSDF("Same, but a higher new line spacing.", startPos, sdfOpts, rotateAngle, 1);

image

As you can see, you can play with the sdfXXX parameters to customize the thickness and softness of the SDF texts. A thickness value of 0.5 will draw the text very similary to a non-SDF text. Depending on the font, below 0.5f might make it invisible, and 1.0f might make it a full-quad. Softness determines the blur of the text, and is rather kept small, such as around 0.2f and 0.4f.

One non-intuitive thing about SDF texts is drop shadows. You would still use the drop shadow parameters given in the parent class TextOptions, however SDF texts have an additional parameter, called sdfDropShadowThickness, which acts as the sdfThickness parameter but for the drop-shadow text.

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;
LinaVG::Config.textCachingSDFEnabled = 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.