sdl_runtime_compiler git · main
SDL3 game for running and compiling code at runtime
C++ 72.3% C 26.2%git clone https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler.gitwget https://git.christianimmanuel.de/sdl-graphics/sdl_runtime_compiler/archive/sdl_runtime_compiler.tar.gzsrc/text/mesh.cpp raw
// text_mesh.cpp
//
// Pipeline:
// FreeType glyph outline → contour polygons (with Bezier flattening)
// → classify contours as outer/hole by containment (not just winding)
// → earcut triangulation per outer+holes group
// → extrude into 3D solid (front face, back face, side walls)
//
// Dependencies: freetype2, earcut.hpp (header-only)
#include "mesh.hpp"
#include "thirdparty/earcut/earcut.hpp"
#include "token.hpp"
#include <SDL3/SDL.h>
#include <algorithm>
#include <array>
#include <ft2build.h>
#include <unordered_map>
namespace
Nimbin
{
#include FT_FREETYPE_H
#include FT_OUTLINE_H
static constexpr int BEZIER_STEPS = 8;
static constexpr float FT_SCALE = 1.0f / 64.0f;
using Pt2 = std::array<float, 2>;
using Ring = DynArray<Pt2>;
using Poly = DynArray<Ring>; // [0]=outer, [1..]=holes
namespace {
struct FaceKey {
std::string path;
int size;
bool operator==(const FaceKey& o) const { return size == o.size && path == o.path; }
};
struct FaceKeyHash {
size_t operator()(const FaceKey& k) const {
return std::hash<std::string>{}(k.path) ^ (std::hash<int>{}(k.size) << 1);
}
};
FT_Library g_ft = nullptr;
std::unordered_map<FaceKey, FT_Face, FaceKeyHash> g_faces;
// Returns a ready-to-use face at the requested pixel size, or nullptr.
FT_Face acquire_Face(const std::string& path, int size) {
if (!g_ft && FT_Init_FreeType(&g_ft)) { g_ft = nullptr; return nullptr; }
FaceKey key{ path, size };
auto it = g_faces.find(key);
if (it != g_faces.end()) {
FT_Set_Pixel_Sizes(it->second, 0, static_cast<FT_UInt>(size)); // cheap; face stays loaded
return it->second;
}
FT_Face face;
if (FT_New_Face(g_ft, path.c_str(), 0, &face)) {
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "FT_New_Face failed: %s", path.c_str());
return nullptr;
}
FT_Set_Pixel_Sizes(face, 0, static_cast<FT_UInt>(size));
g_faces.emplace(key, face);
return face;
}
} // namespace
// ── FreeType outline callbacks ────────────────────────────────────────────────
struct OutlineCtx {
DynArray<Ring> rings;
Ring current;
Pt2 pen{};
void flush() {
if (current.size() >= 3) rings.push_back(std::move(current));
current.clear();
}
};
static int ftMoveTo(const FT_Vector* to, void* user) {
OutlineCtx* c = static_cast<OutlineCtx*>(user);
c->flush();
c->pen = { static_cast<float>(to->x)*FT_SCALE, static_cast<float>(to->y)*FT_SCALE };
c->current.push_back(c->pen);
return 0;
}
static int ftLineTo(const FT_Vector* to, void* user) {
OutlineCtx* c = static_cast<OutlineCtx*>(user);
c->pen = { static_cast<float>(to->x)*FT_SCALE, static_cast<float>(to->y)*FT_SCALE };
c->current.push_back(c->pen);
return 0;
}
static int ftConicTo(const FT_Vector* ctrl, const FT_Vector* to, void* user) {
OutlineCtx* c = static_cast<OutlineCtx*>(user);
float p0x=c->pen[0], p0y=c->pen[1];
float p1x=static_cast<float>(ctrl->x)*FT_SCALE, p1y=static_cast<float>(ctrl->y)*FT_SCALE;
float p2x=static_cast<float>(to->x )*FT_SCALE, p2y=static_cast<float>(to->y )*FT_SCALE;
for (int i=1; i<=BEZIER_STEPS; i++) {
float t=static_cast<float>(i)/static_cast<float>(BEZIER_STEPS), mt=1.0f-t;
c->current.push_back({ mt*mt*p0x+2*mt*t*p1x+t*t*p2x,
mt*mt*p0y+2*mt*t*p1y+t*t*p2y });
}
c->pen = {p2x,p2y};
return 0;
}
static int ftCubicTo(const FT_Vector* c1, const FT_Vector* c2, const FT_Vector* to, void* user) {
OutlineCtx* c = static_cast<OutlineCtx*>(user);
float p0x=c->pen[0], p0y=c->pen[1];
float p1x=static_cast<float>(c1->x)*FT_SCALE, p1y=static_cast<float>(c1->y)*FT_SCALE;
float p2x=static_cast<float>(c2->x)*FT_SCALE, p2y=static_cast<float>(c2->y)*FT_SCALE;
float p3x=static_cast<float>(to->x)*FT_SCALE, p3y=static_cast<float>(to->y)*FT_SCALE;
for (int i=1; i<=BEZIER_STEPS; i++) {
float t=static_cast<float>(i)/static_cast<float>(BEZIER_STEPS), mt=1.0f-t;
c->current.push_back({ mt*mt*mt*p0x+3*mt*mt*t*p1x+3*mt*t*t*p2x+t*t*t*p3x,
mt*mt*mt*p0y+3*mt*mt*t*p1y+3*mt*t*t*p2y+t*t*t*p3y });
}
c->pen = {p3x,p3y};
return 0;
}
static const FT_Outline_Funcs kFTFuncs = { ftMoveTo, ftLineTo, ftConicTo, ftCubicTo, 0, 0 };
// ── Contour helpers ───────────────────────────────────────────────────────────
// Signed area: positive = CCW (outer in FT Y-up), negative = CW (hole)
static float signedArea(const Ring& r) {
float a = 0.0f;
size_t n = r.size();
for (size_t i = 0, j = n-1; i < n; j = i++)
a += (r[j][0]+r[i][0]) * (r[j][1]-r[i][1]);
return a * 0.5f;
}
// Axis-aligned bounding box of a ring
struct AABB { float minx,miny,maxx,maxy; };
static AABB ringAABB(const Ring& r) {
AABB b{ r[0][0],r[0][1],r[0][0],r[0][1] };
for (auto& p : r) {
if (p[0]<b.minx) b.minx=p[0];
if (p[0]>b.maxx) b.maxx=p[0];
if (p[1]<b.miny) b.miny=p[1];
if (p[1]>b.maxy) b.maxy=p[1];
}
return b;
}
// Does AABB A fully contain AABB B?
static bool aabbContains(const AABB& a, const AABB& b) {
return b.minx>=a.minx && b.maxx<=a.maxx && b.miny>=a.miny && b.maxy<=a.maxy;
}
// ── Build polygons: group contours into (outer + its holes) ──────────────────
// FreeType guarantees: CCW = outer contour, CW = hole.
// For glyphs like 'i', 'j', '!', there are multiple separate outer contours.
// Each CW (hole) ring belongs to the outer ring whose AABB contains it.
// If no outer contains it, treat it as its own outer (handles edge cases).
static DynArray<Poly> groupContours(DynArray<Ring>& rings, float pen_x, float scale)
{
struct RingInfo { Ring r; AABB bb; bool is_hole; };
DynArray<RingInfo> info;
info.reserve(rings.size());
for (auto& ring : rings) {
Ring shifted;
shifted.reserve(ring.size());
for (auto& p : ring)
shifted.push_back({ (p[0]+pen_x)*scale, p[1]*scale });
AABB bb = ringAABB(shifted);
bool hole = (signedArea(ring) < 0.0f); // original coords for winding
info.push_back({ std::move(shifted), bb, hole });
}
// Collect outers
DynArray<size_t> outer_ids, hole_ids;
for (size_t i = 0; i < info.size(); i++)
(info[i].is_hole ? hole_ids : outer_ids).push_back(i);
// Each outer starts its own Poly
DynArray<Poly> polys;
for (size_t oi : outer_ids) {
Poly p;
p.push_back(info[oi].r);
polys.push_back(std::move(p));
}
// Assign each hole to the smallest containing outer
for (size_t hi : hole_ids) {
float best_area = 1e18f;
int best_poly = -1;
for (int pi = 0; pi < static_cast<int>(polys.size()); pi++) {
AABB ob = ringAABB(polys[static_cast<size_t>(pi)][0]);
if (aabbContains(ob, info[hi].bb)) {
float a = (ob.maxx-ob.minx)*(ob.maxy-ob.miny);
if (a < best_area) { best_area=a; best_poly=pi; }
}
}
if (best_poly >= 0)
polys[static_cast<size_t>(best_poly)].push_back(info[hi].r);
else {
// No outer contains it — treat as standalone outer (shouldn't happen)
Poly p; p.push_back(info[hi].r);
polys.push_back(std::move(p));
}
}
return polys;
}
// ── Main builder ──────────────────────────────────────────────────────────────
void computeVertexNormals(TextMesh& mesh)
{
mesh.vertex_normals.assign(mesh.verts.size(), Vec3{0,0,0});
for (const MeshTri& t : mesh.tris) {
const Vec3& v0 = mesh.verts[static_cast<size_t>(t.a)];
const Vec3& v1 = mesh.verts[static_cast<size_t>(t.b)];
const Vec3& v2 = mesh.verts[static_cast<size_t>(t.c)];
Vec3 e1 = v1 - v0;
Vec3 e2 = v2 - v0;
Vec3 n{
e1.y * e2.z - e1.z * e2.y,
e1.z * e2.x - e1.x * e2.z,
e1.x * e2.y - e1.y * e2.x
};
// Accumulate weighted by area (len of cross product = 2*area)
mesh.vertex_normals[static_cast<size_t>(t.a)] += n;
mesh.vertex_normals[static_cast<size_t>(t.b)] += n;
mesh.vertex_normals[static_cast<size_t>(t.c)] += n;
}
for (Vec3& n : mesh.vertex_normals) {
double len = SDL_sqrt(n.x*n.x + n.y*n.y + n.z*n.z);
if (len > 0.000001) {
n.x /= len; n.y /= len; n.z /= len;
}
else {
n = {0, 0, -1}; // fallback: face forward
}
}
}
// ── Per-glyph geometry cache ──────────────────────────────────────────────────
// A glyph's tessellated+extruded geometry depends only on (codepoint, scale)
// now that depth is a draw-time Z-scale (mesh built at unit depth, faces ±0.5).
// Build each glyph once; build() just stitches cached glyphs with an x-offset.
namespace {
struct GlyphKey {
uint32_t ch;
int scale_q; // scale quantised → stable float key
bool operator==(const GlyphKey& o) const { return ch == o.ch && scale_q == o.scale_q; }
};
struct GlyphKeyHash {
size_t operator()(const GlyphKey& k) const {
return std::hash<uint32_t>{}(k.ch) * 1000003u ^ static_cast<size_t>(k.scale_q);
}
};
struct GlyphMesh {
TextMesh m; // verts/tris/vertex_normals/is_front, at origin, unit depth
float advance = 0.0f; // scaled world-unit x advance
};
std::unordered_map<GlyphKey, GlyphMesh, GlyphKeyHash> g_glyphs;
// Extrude one glyph's polys into `out` at unit depth (faces at z = ±0.5).
void
extrude_Polys(TextMesh& out, const DynArray<Poly>& polys)
{
constexpr float half_d = 0.5f; // UNIT depth — scaled per-instance at draw
for (const Poly& poly : polys) {
if (poly.empty() || poly[0].size() < 3) continue;
DynArray<uint32_t> indices = mapbox::earcut<uint32_t>(poly);
if (indices.empty()) continue;
DynArray<Pt2> flat;
for (auto& ring : poly) for (auto& p : ring) flat.push_back(p);
int base = static_cast<int>(out.verts.size());
for (auto& p : flat) { // front (z = -0.5)
out.verts.push_back({ (double)p[0], (double)p[1], (double)(-half_d) });
out.is_front.push_back(1.0f);
}
int back_base = static_cast<int>(out.verts.size());
for (auto& p : flat) { // back (z = +0.5)
out.verts.push_back({ (double)p[0], (double)p[1], (double)( half_d) });
out.is_front.push_back(0.0f);
}
for (size_t i = 0; i + 2 < indices.size(); i += 3)
out.tris.push_back({ base + (int)indices[i], base + (int)indices[i+2], base + (int)indices[i+1] });
for (size_t i = 0; i + 2 < indices.size(); i += 3)
out.tris.push_back({ back_base + (int)indices[i], back_base + (int)indices[i+1], back_base + (int)indices[i+2] });
for (const auto& ring : poly) { // side walls
int rn = static_cast<int>(ring.size());
for (int i = 0; i < rn; i++) {
int i1 = (i + 1) % rn;
int wb = static_cast<int>(out.verts.size());
const Pt2& p0 = ring[(size_t)i];
const Pt2& p1 = ring[(size_t)i1];
out.verts.push_back({ (double)p0[0], (double)p0[1], (double)(-half_d) }); out.is_front.push_back(0.0f);
out.verts.push_back({ (double)p1[0], (double)p1[1], (double)(-half_d) }); out.is_front.push_back(0.0f);
out.verts.push_back({ (double)p1[0], (double)p1[1], (double)( half_d) }); out.is_front.push_back(0.0f);
out.verts.push_back({ (double)p0[0], (double)p0[1], (double)( half_d) }); out.is_front.push_back(0.0f);
out.tris.push_back({ wb+0, wb+1, wb+2 });
out.tris.push_back({ wb+0, wb+2, wb+3 });
}
}
}
}
const GlyphMesh*
acquire_Glyph(FT_Face face, uint32_t ch, float scale)
{
GlyphKey key{ ch, (int)SDL_lround((double)scale * 1e7) };
auto it = g_glyphs.find(key);
if (it != g_glyphs.end()) return &it->second;
// SDL_Log("GLYPH MISS '%c' scale_q=%d (cache=%zu)", (char)ch, key.scale_q, g_glyphs.size());
GlyphMesh gm;
FT_UInt gi = FT_Get_Char_Index(face, (FT_ULong)ch);
if (!FT_Load_Glyph(face, gi, FT_LOAD_NO_BITMAP) &&
face->glyph->format == FT_GLYPH_FORMAT_OUTLINE)
{
gm.advance = (float)face->glyph->advance.x * FT_SCALE * scale;
OutlineCtx ctx;
FT_Outline_Decompose(&face->glyph->outline, &kFTFuncs, &ctx);
ctx.flush();
if (!ctx.rings.empty()) {
DynArray<Poly> polys = groupContours(ctx.rings, 0.0f, scale); // origin, scaled
extrude_Polys(gm.m, polys);
computeVertexNormals(gm.m); // per-glyph, once
}
}
// failed load / non-outline → advance 0, no geometry (matches old skip)
auto [ins, ok] = g_glyphs.emplace(std::move(key), std::move(gm));
return &ins->second;
}
} // namespace
bool
TextMesh::build(const std::string& font_path,
const std::string& text, int size, float /*depth*/, float scale)
{
//SPIKE("mesh.build");
verts.clear(); tris.clear(); is_front.clear(); vertex_normals.clear();
FT_Face face = acquire_Face(font_path, size);
if (!face) return false;
float pen_x = 0.0f; // scaled world units
for (const char chc : text) {
const GlyphMesh* g = acquire_Glyph(face, (unsigned char)chc, scale);
if (!g) continue;
const int base = static_cast<int>(verts.size());
for (const Vec3& v : g->m.verts)
verts.push_back({ v.x + (double)pen_x, v.y, v.z }); // z stays unit (±0.5)
for (const Vec3& n : g->m.vertex_normals) vertex_normals.push_back(n);
for (float f : g->m.is_front) is_front.push_back(f);
for (const MeshTri& t : g->m.tris)
tris.push_back({ t.a + base, t.b + base, t.c + base });
pen_x += g->advance;
}
// Centre on bounding box (x only) — unchanged.
if (!verts.empty()) {
float minX = 1e9f, maxX = -1e9f;
for (const Vec3& v : verts) {
float vx = (float)v.x;
if (vx < minX) minX = vx;
if (vx > maxX) maxX = vx;
}
double cx = (double)((minX + maxX) * 0.5f);
for (Vec3& v : verts) v.x -= cx;
}
// Normals are cached per glyph — no whole-word recompute needed.
return !verts.empty();
}
} // namespace Nimbin