feat: add text rendering with SDF's

This commit is contained in:
2026-10-17 16:00:51 +02:00
parent 7dd5ee7903
commit 67c685a158
12 changed files with 746 additions and 51 deletions
+49
View File
@@ -1,3 +1,4 @@
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
@@ -33,6 +34,7 @@
#include <destrum/Components/CameraComponent.h>
#include <destrum/Graphics/Resources/PrimitiveMeshes.h>
#include <destrum/Graphics/Managers/Renderer2D.h>
#include <destrum/Graphics/Resources/Font.h>
#include <destrum/Components/LightComponent.h>
#include <destrum/Components/Physics/Rigidbody.h>
#include <destrum/Graphics/Material.h>
@@ -478,6 +480,52 @@ namespace {
check(r2d.GetShapes().empty(), "clear must drop all submitted shapes");
}
void testFontAtlasAndLayout()
{
const std::filesystem::path fontPath =
std::filesystem::path{DESTRUM_ENGINE_ASSETS_SRC_DIR} / "fonts" / "Roboto-Medium.ttf";
const Font font = Font::LoadFromFile(fontPath, 32.0f);
const FontGlyph* a = font.GetGlyph(U'A');
const FontGlyph* space = font.GetGlyph(U' ');
check(a != nullptr && a->size.x > 0.0f && a->size.y > 0.0f && a->advance > 0.0f &&
a->uvMax.x > a->uvMin.x && a->uvMax.y > a->uvMin.y,
"printable glyphs must be rasterized into the atlas");
check(space != nullptr && space->size == glm::vec2{0.0f} && space->advance > 0.0f,
"spaces must only advance the pen");
check(font.GetGlyph(U'中') == font.GetGlyph(U'?'), "missing glyphs must fall back to '?'");
const auto& pixels = font.GetAtlasPixels();
const auto [minIt, maxIt] = std::minmax_element(pixels.begin(), pixels.end());
check(!pixels.empty() && *minIt < 128 && *maxIt > 128,
"the atlas must hold a signed distance field with the edge at 128");
const glm::vec2 one = font.MeasureText("A", 64.0f);
const glm::vec2 two = font.MeasureText("A\nAA", 64.0f);
check(close(one.x, a->advance * 2.0f) && close(one.y, font.GetLineHeight() * 2.0f),
"text must be measured at the requested size");
check(close(two.x, (a->advance * 2.0f + font.GetKerning(U'A', U'A')) * 2.0f) &&
close(two.y, one.y * 2.0f),
"multi line text must measure its widest line and every line");
std::vector<PlacedGlyph> placed;
font.Layout("A b", {100.0f, 50.0f}, 32.0f, [&](const PlacedGlyph& glyph) { placed.push_back(glyph); });
check(placed.size() == 2 && placed[0].glyph == a &&
close(placed[0].topLeft.x, 100.0f + a->offset.x) &&
close(placed[0].topLeft.y, 50.0f + font.GetAscender() + a->offset.y),
"layout must skip spaces and start on the baseline below the top left");
const std::string_view utf8 = "\xC3\xA9\xFF";
std::size_t index = 0;
check(Font::DecodeUtf8(utf8, index) == U'é' && index == 2, "utf8 must decode two byte sequences");
check(Font::DecodeUtf8(utf8, index) == 0xFFFD && index == 3, "invalid utf8 must decode to U+FFFD");
auto& r2d = Renderer2D::GetInstance();
r2d.Clear();
r2d.DrawText(font, "not uploaded", {0.0f, 0.0f}, 16.0f, glm::vec4{1.0f});
check(r2d.GetShapes().empty(), "text must not be drawn before the font atlas is uploaded");
}
void testCapsuleColliderDebugLines()
{
GameObject object{"capsule"};
@@ -1179,6 +1227,7 @@ int main()
testColliderCenterOffset();
testCapsuleColliderDebugLines();
testRenderer2DShapes();
testFontAtlasAndLayout();
std::cout << "destrum tests passed\n";
return EXIT_SUCCESS;
}