Start

Theory

Reference

Examples

← All examples
Rendering · Assets

Text Rendering

Fonts, sizes and color

Bakes the same font at three different pixel heights — each ASGE Font is rasterized at one fixed size, so multiple on-screen sizes means loading it multiple times, each with its own atlas texture.

A headline drawn alongside them continuously cycles through hue space, exercising per-draw color tinting on top of the baked glyph atlas.

No input — purely a visual reference for this subsystem.

RenderingAssets
Text Rendering screenshot

Run it yourself

build just this example
cmake --build --preset windows-debug --target text_demo
# binary lands under bin/ (e.g. bin/Debug/text_demo.exe on Windows)

See Installation for the full build setup.

Source

examples/text_demo/Game.hpp
#pragma once

#include <ASGE/ASGE.hpp>

class TextDemoState final : public asge::game::state::IGameState<int>
{
    // Each Font is baked at one fixed pixel height, so showing multiple
    // sizes on screen means loading the font multiple times -- once per
    // size -- each with its own atlas and uploaded texture.
    std::unique_ptr<asge::media::Font> m_SmallFont;
    std::unique_ptr<asge::media::Font> m_MediumFont;
    std::unique_ptr<asge::media::Font> m_LargeFont;
    std::unique_ptr<asge::video::ITexture> m_SmallAtlas;
    std::unique_ptr<asge::video::ITexture> m_MediumAtlas;
    std::unique_ptr<asge::video::ITexture> m_LargeAtlas;

    float m_HueTime{0.0f}; // drives the color-cycling headline

    void EnsureFontsLoaded(asge::video::IRenderer& inRenderer);

public:
    [[nodiscard]] std::optional<asge::game::state::Transition<int>>
    Update(float inDeltaTime, asge::input::InputState const& inInput) override;
    void Render(asge::video::IRenderer& inRenderer) override;
    void OnSystemEvent(asge::event::SystemEvent const& inSysEvent) override;
};

class TextDemoGame final : public asge::game::Game<int>
{
public:
    explicit TextDemoGame(asge::video::IRenderer& inRenderer, asge::audio::AudioDevice& inAudioDev);

protected:
    [[nodiscard]] std::unique_ptr<StateType> CreateState(int inId) override;
};
examples/text_demo/Game.cpp
#include "Game.hpp"

#include <cmath>
#include <filesystem>

namespace
{
constexpr float HUE_CYCLE_SPEED = 0.6f; // full cycles per ~10 seconds

// Cheap HSV(hue in turns, full sat/value) -> RGB, just enough to cycle the
// headline's color smoothly without needing a real color-space library.
asge::graphics::RGBA_Color HueToColor(float inHueTurns)
{
    float const h = (inHueTurns - std::floor(inHueTurns)) * 6.0f;
    float const x = 1.0f - std::fabs(std::fmod(h, 2.0f) - 1.0f);

    float r = 0.0f, g = 0.0f, b = 0.0f;
    if      (h < 1.0f) { r = 1.0f; g = x;    b = 0.0f; }
    else if (h < 2.0f) { r = x;    g = 1.0f; b = 0.0f; }
    else if (h < 3.0f) { r = 0.0f; g = 1.0f; b = x;    }
    else if (h < 4.0f) { r = 0.0f; g = x;    b = 1.0f; }
    else if (h < 5.0f) { r = x;    g = 0.0f; b = 1.0f; }
    else                { r = 1.0f; g = 0.0f; b = x;    }

    return {
        static_cast<std::uint8_t>(r * 255.0f),
        static_cast<std::uint8_t>(g * 255.0f),
        static_cast<std::uint8_t>(b * 255.0f),
        255
    };
}

std::unique_ptr<asge::media::Font> LoadFont(int inPixelHeight)
{
    // ASGE_TEXT_DEMO_ASSET_DIR is injected by CMakeLists.txt.
    auto const path = std::filesystem::path(ASGE_TEXT_DEMO_ASSET_DIR) / "PTSans-Regular.ttf";

    auto fontResult = asge::media::Font::Load(path, inPixelHeight);
    if (!fontResult)
    {
        fontResult.LogError();
        return nullptr;
    }

    return std::make_unique<asge::media::Font>(std::move(fontResult).Value());
}
}

void TextDemoState::EnsureFontsLoaded(asge::video::IRenderer& inRenderer)
{
    if (!m_SmallFont)
    {
        m_SmallFont = LoadFont(18);
        if (m_SmallFont) m_SmallAtlas = inRenderer.CreateTexture(m_SmallFont->GetAtlasImage());
    }

    if (!m_MediumFont)
    {
        m_MediumFont = LoadFont(28);
        if (m_MediumFont) m_MediumAtlas = inRenderer.CreateTexture(m_MediumFont->GetAtlasImage());
    }

    if (!m_LargeFont)
    {
        m_LargeFont = LoadFont(56);
        if (m_LargeFont) m_LargeAtlas = inRenderer.CreateTexture(m_LargeFont->GetAtlasImage());
    }
}

std::optional<asge::game::state::Transition<int>>
TextDemoState::Update(float inDeltaTime, [[maybe_unused]] asge::input::InputState const& inInput)
{
    m_HueTime += inDeltaTime * (HUE_CYCLE_SPEED / 10.0f);
    return std::nullopt;
}

void TextDemoState::Render(asge::video::IRenderer &inRenderer)
{
    inRenderer.Clear({ 20, 20, 25, 255 });

    EnsureFontsLoaded(inRenderer);

    if (m_SmallFont && m_SmallAtlas)
    {
        inRenderer.DrawString(
            "The quick brown fox jumps over the lazy dog.",
            *m_SmallFont, *m_SmallAtlas, { 40.0f, 70.0f }, { 220, 220, 220, 255 });

        inRenderer.DrawString(
            "Font::GetAtlasImage() ->",
            *m_SmallFont, *m_SmallAtlas, { 460.0f, 400.0f }, { 150, 150, 150, 255 });
    }

    if (m_MediumFont && m_MediumAtlas)
    {
        inRenderer.DrawString(
            "DrawString renders glyphs from a baked atlas",
            *m_MediumFont, *m_MediumAtlas, { 40.0f, 150.0f }, { 120, 200, 255, 255 });
    }

    if (m_LargeFont && m_LargeAtlas)
    {
        // SetColorMod is applied per DrawString call, not baked into the
        // texture -- this line's color keeps changing every frame while
        // reusing the exact same atlas texture as every other draw below.
        inRenderer.DrawString(
            "ASGE", *m_LargeFont, *m_LargeAtlas, { 40.0f, 280.0f }, HueToColor(m_HueTime));

        // The atlas itself, drawn like any other texture -- every glyph
        // DrawString just rendered lives somewhere in this packed bitmap.
        inRenderer.DrawTexture(*m_LargeAtlas, asge::math::Rect{ 460.0f, 410.0f, 150.0f, 150.0f });
    }
}

void TextDemoState::OnSystemEvent([[maybe_unused]] asge::event::SystemEvent const &inSysEvent)
{
}

TextDemoGame::TextDemoGame(asge::video::IRenderer& inRenderer, asge::audio::AudioDevice& inAudioDev)
: Game(inRenderer, inAudioDev)
{
    SetInitialState(0);
}

std::unique_ptr<TextDemoGame::StateType> TextDemoGame::CreateState([[maybe_unused]] int inId)
{
    return std::make_unique<TextDemoState>();
}

Browse the full example on GitHub →