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
@@ -2,6 +2,7 @@
#define DESTRUM_RENDERER2D_H
#include <cstdint>
#include <string_view>
#include <vector>
#include <glm/glm.hpp>
@@ -9,10 +10,18 @@
#include <destrum/Graphics/ids.h>
#include <destrum/Singleton.h>
// windows.h maps DrawText to DrawTextA/W
#ifdef DrawText
#undef DrawText
#endif
class Font;
enum class Shape2DType : std::uint32_t {
Rect = 0,
Ellipse = 1,
Triangle = 2,
Text = 3,
};
enum class TextureFilter : std::uint32_t {
@@ -41,7 +50,15 @@ struct ShapeStyle {
Texture2D texture{};
};
// Mirrors Shape2D in shape2d.glsl (scalar layout)
struct TextStyle {
glm::vec4 color{1.0f};
// Grows outwards from the glyphs, at most the font's SDF spread
float outline{0.0f};
glm::vec4 outlineColor{0.0f, 0.0f, 0.0f, 1.0f};
};
// Mirrors Shape2D in shape2d.glsl (scalar layout). Text glyphs store the SDF distance
// scale in p0.x and the outline colour in p1/p2.
struct Shape2D {
glm::vec2 center;
glm::vec2 halfSize;
@@ -94,6 +111,20 @@ public:
const glm::vec4& color,
LineCap cap = LineCap::Butt);
// size is the line size in pixels, the font has to be uploaded
void DrawText(
const Font& font,
std::string_view text,
const glm::vec2& topLeft,
float size,
const glm::vec4& color);
void DrawText(
const Font& font,
std::string_view text,
const glm::vec2& topLeft,
float size,
const TextStyle& style);
[[nodiscard]] const std::vector<Shape2D>& GetShapes() const { return shapes; }
void Clear();
@@ -0,0 +1,110 @@
#ifndef DESTRUM_FONT_H
#define DESTRUM_FONT_H
#include <cstdint>
#include <filesystem>
#include <string_view>
#include <unordered_map>
#include <vector>
#include <glm/glm.hpp>
#include <destrum/Graphics/ids.h>
class GfxDevice;
class RenderResources;
struct FontGlyph {
// In atlas pixels, relative to the pen position on the baseline, y down
glm::vec2 offset{0.0f};
glm::vec2 size{0.0f};
float advance{0.0f};
glm::vec2 uvMin{0.0f};
glm::vec2 uvMax{0.0f};
};
struct PlacedGlyph {
const FontGlyph* glyph;
glm::vec2 topLeft;
glm::vec2 size;
};
// Signed distance field glyph atlas, so one atlas renders crisp text at any size
class Font final {
public:
static constexpr float DefaultPixelSize = 48.0f;
static constexpr int SdfSpread = 8;
static Font LoadFromFile(const std::filesystem::path& path, float pixelSize = DefaultPixelSize);
void Upload(GfxDevice& gfxDevice, RenderResources& resources);
[[nodiscard]] const FontGlyph* GetGlyph(char32_t codepoint) const;
[[nodiscard]] float GetKerning(char32_t left, char32_t right) const;
// Calls onGlyph(const PlacedGlyph&) for every visible glyph, in screen pixels
template <typename F>
void Layout(std::string_view text, const glm::vec2& topLeft, float size, F&& onGlyph) const;
[[nodiscard]] glm::vec2 MeasureText(std::string_view text, float size) const;
[[nodiscard]] float GetPixelSize() const { return pixelSize; }
[[nodiscard]] float GetAscender() const { return ascender; }
[[nodiscard]] float GetLineHeight() const { return lineHeight; }
[[nodiscard]] ImageID GetAtlas() const { return atlas; }
[[nodiscard]] glm::uvec2 GetAtlasSize() const { return atlasSize; }
[[nodiscard]] const std::vector<std::uint8_t>& GetAtlasPixels() const { return atlasPixels; }
static char32_t DecodeUtf8(std::string_view text, std::size_t& index);
private:
float pixelSize{0.0f};
float ascender{0.0f};
float lineHeight{0.0f};
std::unordered_map<char32_t, FontGlyph> glyphs;
std::unordered_map<std::uint64_t, float> kerning;
glm::uvec2 atlasSize{0};
std::vector<std::uint8_t> atlasPixels;
ImageID atlas{NULL_IMAGE_ID};
};
template <typename F>
void Font::Layout(std::string_view text, const glm::vec2& topLeft, float size, F&& onGlyph) const
{
const float scale = size / pixelSize;
glm::vec2 pen{topLeft.x, topLeft.y + ascender * scale};
char32_t previous = 0;
for (std::size_t i = 0; i < text.size();) {
const char32_t codepoint = DecodeUtf8(text, i);
if (codepoint == U'\n') {
pen = {topLeft.x, pen.y + lineHeight * scale};
previous = 0;
continue;
}
const FontGlyph* glyph = GetGlyph(codepoint);
if (!glyph) {
continue;
}
if (previous != 0) {
pen.x += GetKerning(previous, codepoint) * scale;
}
previous = codepoint;
if (glyph->size.x > 0.0f && glyph->size.y > 0.0f) {
onGlyph(PlacedGlyph{
.glyph = glyph,
.topLeft = pen + glyph->offset * scale,
.size = glyph->size * scale,
});
}
pen.x += glyph->advance * scale;
}
}
#endif // DESTRUM_FONT_H