NervLand DevLog #24: Extending support of text rendering

We continue here from our previous devlog article where we implemented the initial support for text rendering in NervLand. Nwow it's time to push it a little further and add the missing features in this tex rendering system.

Youtube video for this article available at:

One of the first additional features I think we could add here is the support to control a text color: that should be fairly easy to add, we simply need to forward the color for a given text as an additional parameter for each glyph.

If you think about it, there is a bit of data duplication here is we store the color of the text in the parameters for each character in this text, but for now I don't think this is really a big deal

I first added the color parameter in the TextDesc structure:

    struct TextDesc {
        String text;
        Vec2f position{0, 0};
        Vec2f size{-1.0F, 18.0F};
        TextAnchor anchor{ANCHOR_BOTTOM_LEFT};
        Vec4f color{1.0F, 1.0F, 1.0F, 1.0F};

And then also realized it was not very cleaver to keep using the TextInfo struct just to also provide the font when I call the TextRenderer::add_text() function, instead I'm now constructing a TextDesc directly and providing the font separated:

auto WGPUTextRenderer::add_text(const TextDesc& input, RefPtr<WGPUFont> font)
    -> U32 {
    auto* eng = WGPUEngine::instance();

    if (font == nullptr) {
        font = eng->get_default_font();

    auto& fctx = get_or_create_font_context(font);
    fctx.dirty = true;

    // Return the index of our text slot:
    return fctx.texts.size() - 1;

OK! Now we can specify the text color we want to use in add_text:

    trdr = eng->create_text_renderer("texts"_sid, {});
    trdr->add_text({.text = "Hello manu and patglqiy:-)!",
                    .position = {1920.0 * 0.5, 1080 * 0.5},
                    .size = {-1.0F, 70.0F},
                    .anchor = ANCHOR_CENTER_CENTER,
                    .color = {1.0, 1.0, 0.0, 1.0}});

Currently we can update the “text content” of each text slot dynamically, but it would also be good to be able to update the other text settings like the position, size or color dynamically too. So let's introduce support for that.

And while I am at it, I think I should also reconsider the way I access a text Slot after it has been created: currently I need the text slot index but also the font index, as the text slots are stored per FontContext: this is a bit of a pain, and I think it would be better to use a single index to access the text slots. Let's think about this a bit 🤔…

Allright, so, I have now removed the Vector<TextDesc> from the FontContext struct, and instead, will use a dedicated Vector to store the text slots. And now also keeping track of the font to use directly in the TextDesc struct (so I had to updated the add_text method again 😅):

    struct TextDesc {
        String text;
        Vec2f position{0, 0};
        Vec2f size{-1.0F, 18.0F};
        TextAnchor anchor{ANCHOR_BOTTOM_LEFT};
        Vec4f color{1.0F, 1.0F, 1.0F, 1.0F};
        RefPtr<WGPUFont> font{nullptr};

And now I have a bunch of additional methods to update the different settings for a given text:

    /** Set a text value using the first font context.*/
    void set_text(U32 slotId, const String& text);

    /** Set a text slot position */
    void set_position(U32 slotId, Vec2f position);

    /** Set a text slot size */
    void set_size(U32 slotId, Vec2f size);

    /** Set a text slot anchor point */
    void set_anchor(U32 slotId, TextAnchor anchor);

    /** Set a text slot color */
    void set_color(U32 slotId, Vec4f color);

Which means I can, for instance, update the text position dynamically:

    static U32 count = 0;
    F32 ctime = (F32)SystemTime::get_current_time();

    trdr->set_text(0, format_string("Fpack: %d", count / 10));
    F32 yoff = (F32)(50.0F * cos(ctime * nv::PI_2));
    trdr->set_position(0, {F32(1920.0 * 0.5F), 1080 * 0.5F + yoff});


Which will produce this nice position update animation:

So far, I have actually only been using an hardcoded projection transformation in our text rendering shader 😅:

    pos = vec4(-1.0 + 2.0*pos.x/1920.0, -1.0 + 2.0*pos.y/1080.0, 0.0, 1.0);
    output.Position = pos;
    // output.Position = proj * pos;
    output.coords = uv;

I tried to read the actual projection matrix I provided from the CPU side, but for some reason this didn't work. Now time to find out why and fix!

Arrff…. 😒, Of course, if you set the far plane to the same value as the near plane, you won't go very far Manu…:

struct OrthoProjection {
    F32 left{0.0F};
    F32 right{1920.0F};
    F32 bottom{0.0F};
    F32 top{1080.0F};
    F32 znear{0.01F};
    F32 zfar{0.01F};

⇒ Fixing that and now updating the shader to use the project matrix:

…OOOooops… the result are not exactly as expected lol:

But that's really no big deal: I just need to flip the top/bottom values when building the projection. OK fixed already ;-).

Currently, we are using instancing to specify the number of characters we want to draw with a given font. But in fact, there is no real need for that, and we could just as well request the rendering of 1 instance with 6*nchars vertices.

I'm not absolutely sure about that but from a few sources that I checked already it seems that instancing could come with a cost, so it should be a good move to get rid of it in this case (?).

⇒ in TextRenderer::update_buffers() we are now updating the number of vertices instead of the number of instances:

    for (auto& fctx : _fontContexts) {
        if (fctx.dirty) {
            drawArgs->instanceCount = 1;
            U32 nverts = update_font_context(fctx) * 6;
            if (nverts != drawArgs->vertexCount) {
                updated = true;
                drawArgs->vertexCount = nverts;

And I updated the shader file accordingly of course:

    // Get the glyh rect that should be rendered for the current instance:
    // let glyph = &glyphs[input.instanceID];
    var charIdx: u32 = input.vertexID/6;
    let glyph = &glyphs[charIdx];

And these are the only changes we need to get the rendering done with a single instance 😎.

A final element I would like to add support for is a rotation angle for the text slots: even if this would only have limited value, it should be simple enough to add it here as a “proof of concept” at least, so let's do it.

OK, working just fine: now I also push an additional vector to store the rotation center point and the angle, and then we transform the points manually before applying the projection matrix in the shader:

    // pos = vec4(-1.0 + 2.0*pos.x/1920.0, -1.0 + 2.0*pos.y/1080.0, 0.0, 1.0);
    // output.Position = pos;
    var point: vec2<f32> = pos.xy - rot.xy;

    // Compute the cos/sin of theta:
    var cost = cos(radians(rot.w));
    var sint = sin(radians(rot.w));
    var rotp = vec2(point.x*cost-point.y*sint, point.x*sint+point.y*cost) + rot.xy;
    pos = vec4(rotp, pos.z, 1.0);

    output.Position = proj * pos;

Here is the rotation effet in use:

  • blog/2023/0726_nvl_devlog24_more_text_rendering_features.txt
  • Last modified: 2023/08/04 18:31
  • (external edit)