Nimbin[12]?SDL & Graphics / sdl_runtime_compiler / commits / d2ead71

sdl_runtime_compiler git · main

SDL3 game for running and compiling code at runtime

sdl3 c++ compiler dlopen cmake · first commit 2026-04-19 · last commit 2026-07-03 (3 months ago) · synced 3 days ago · upstream: git.ide3.de/hsnr/sdl-runtime-compiler

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.gz

Add text sparkel tail With Claude

Christian Immanuel · 2026-06-01 10:24 · d2ead71f270b7f10f00c64034f568516cab35d4b

 src/text_3d/text_3d.cpp | 165 +++++++++++++++++++++++++++++++++++++++++++++++-
 src/text_3d/text_3d.hpp |  11 ++++
 2 files changed, 174 insertions(+), 2 deletions(-)

diff --git a/src/text_3d/text_3d.cpp b/src/text_3d/text_3d.cpp
index 0774b94..311d3e5 100644
--- a/src/text_3d/text_3d.cpp
+++ b/src/text_3d/text_3d.cpp
@@ -12,6 +12,70 @@
 #include "utils/globals.hpp"
 
 
+static SDL_FColor
+aiGlintColor(Text3dAI::State s)
+{
+    switch (s) {
+        case Text3dAI::State::Attack:  return {1.0f, 0.85f, 0.75f, 1.0f};
+        case Text3dAI::State::Flee:    return {1.0f, 1.0f,  0.80f, 1.0f};
+        case Text3dAI::State::Stunned: return {0.85f, 0.85f, 0.95f, 1.0f};
+        case Text3dAI::State::Idle:    return {1.0f, 1.0f,  1.0f,  1.0f};
+    }
+    return {1.0f, 1.0f, 1.0f, 1.0f};
+}
+
+static Vec3 v_norm(const Vec3& v)
+{
+    double l = SDL_sqrt(v.x*v.x + v.y*v.y + v.z*v.z);
+    return (l < 1e-9) ? Vec3{0,0,1} : Vec3{v.x/l, v.y/l, v.z/l};
+}
+
+// Camera-facing filled disc (triangle fan). Same vertex layout as the text mesh.
+static void
+append_disc(DynArray<float>& v, DynArray<int>& idx,
+            const Vec3& c, const Vec3& right, const Vec3& up,
+            const Vec3& camN, double radius)
+{
+    constexpr int SEG = 10;
+    const int base = static_cast<int>(v.size()) / 9;
+
+    auto push = [&](const Vec3& p) {
+        v.push_back((float)p.x);    v.push_back((float)p.y);    v.push_back((float)p.z);
+        v.push_back((float)camN.x); v.push_back((float)camN.y); v.push_back((float)camN.z);
+        v.push_back(1.0f);          v.push_back(0.0f);          v.push_back(0.0f);
+    };
+
+    push(c);
+    for (int i = 0; i < SEG; ++i) {
+        double ang = static_cast<double>(i) * (2.0 * SDL_PI_D / SEG);
+        double dx = SDL_cos(ang) * radius, dy = SDL_sin(ang) * radius;
+        push(Vec3{ c.x + right.x*dx + up.x*dy,
+                   c.y + right.y*dx + up.y*dy,
+                   c.z + right.z*dx + up.z*dy });
+    }
+    for (int i = 0; i < SEG; ++i) {
+        idx.push_back(base + 0);
+        idx.push_back(base + 1 + i);
+        idx.push_back(base + 1 + ((i + 1) % SEG));
+    }
+}
+
+// Fast integer hash → float in [0,1). Used for all per-sparkle randomness.
+static double hash01(uint32_t x)
+{
+    x ^= x >> 16; x *= 0x7feb352dU;
+    x ^= x >> 15; x *= 0x846ca68bU;
+    x ^= x >> 16;
+    return (x & 0x00ffffffU) / static_cast<double>(0x01000000);
+}
+
+static SDL_FColor mix_col(SDL_FColor a, SDL_FColor b, float k)
+{
+    return { a.r + (b.r - a.r) * k,
+             a.g + (b.g - a.g) * k,
+             a.b + (b.b - a.b) * k, 1.0f };
+}
+
 
 Text3d::Text3d(const std::string& txt,
                float dpth, float scle,
@@ -41,6 +105,8 @@ Text3d::~Text3d()
     // Renderer owns the GL context; only delete if buffers were created.
     if (gpu_vbo || gpu_ebo)
         GlRenderer::free_MeshBuffers(gpu_vbo, gpu_ebo);
+    if (gpu_vbo_sparkle || gpu_ebo_sparkle)
+        GlRenderer::free_MeshBuffers(gpu_vbo_sparkle, gpu_ebo_sparkle);
 }
 
 
@@ -172,7 +238,6 @@ Text3d::draw(GlRenderer& gl, const Camera& camera, const Light& light) const
     gl.set_Light(light);
     gl.set_MVP(model, mvp);
     gl.set_NormalMat(model);
-
     gl.set_FaceShade(color.r, color.g, color.b, back_dim, front_tint);
 
     if (glow_enabled && glow_strength > 0.0f)
@@ -180,7 +245,7 @@ Text3d::draw(GlRenderer& gl, const Camera& camera, const Light& light) const
     else
         gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f);
 
-    // Persistent VBO: upload only when the mesh changed.
+    // Draw the actual word FIRST, while MVP/normal/face-shade are still correct.
     gl.draw_MeshCached(gpu_vbo, gpu_ebo, gpu_index_count, gpu_dirty,
                        packed_verts.data(), (int)packed_verts.size() / 9,
                        packed_indices.data(), (int)packed_indices.size());
@@ -188,4 +253,100 @@ Text3d::draw(GlRenderer& gl, const Camera& camera, const Light& light) const
 
     gl.set_Emissive(0.0f, 0.0f, 0.0f, 0.0f);
     gl.set_FaceShade(0.0f, 0.0f, 0.0f, 1.0f, false);
+
+    // ── AI sparkle tail ──────────────────────────────────────────────────
+    if (ai.state != Text3dAI::State::Idle) {
+        const Uint64 now = SDL_GetTicks();
+
+        // Sample the word's path into a history ring → the trailing tail.
+        if (sparkle_trail_count == 0) {
+            for (int i = 0; i < SPARKLE_TRAIL; ++i) sparkle_trail[i] = pos;
+            sparkle_trail_count = SPARKLE_TRAIL;
+            sparkle_last_ms     = now;
+        } else if (now - sparkle_last_ms >= 33) {              // ~30 Hz
+            for (int i = SPARKLE_TRAIL - 1; i > 0; --i)
+                sparkle_trail[i] = sparkle_trail[i - 1];
+            sparkle_trail[0] = pos;
+            sparkle_last_ms  = now;
+        }
+
+        const double t     = static_cast<double>(now) * 0.001;
+        const Vec3   fwd    = v_norm(camera.fwd);
+        const Vec3   right  = v_norm(camera.right);
+        const Vec3   up     = v_norm(Vec3{ right.y*fwd.z - right.z*fwd.y,
+                                           right.z*fwd.x - right.x*fwd.z,
+                                           right.x*fwd.y - right.y*fwd.x });
+        const Vec3   camN   = { -fwd.x, -fwd.y, -fwd.z };
+        const double sz     = SDL_max(half_extent, 0.25);
+        const uint32_t wseed = static_cast<uint32_t>(body.hover_phase * 10037.0) | 1u;
+
+        static const SDL_FColor PAL[] = {
+            {1.00f, 0.72f, 0.10f, 1.0f},  // gold
+            {1.00f, 0.25f, 0.65f, 1.0f},  // magenta-pink
+            {0.20f, 0.80f, 1.00f, 1.0f},  // cyan
+            {0.45f, 1.00f, 0.30f, 1.0f},  // lime
+            {1.00f, 0.45f, 0.10f, 1.0f},  // orange
+            {0.65f, 0.40f, 1.00f, 1.0f},  // violet
+        };
+        constexpr int PALN = sizeof(PAL) / sizeof(PAL[0]);
+
+        DynArray<float> sv[PALN];
+        DynArray<int>   si[PALN];
+
+        constexpr int PER_NODE = 2;
+        for (int j = 0; j < sparkle_trail_count; ++j) {
+            const double age  = (sparkle_trail_count > 1)
+                              ? double(j) / double(sparkle_trail_count - 1) : 0.0;
+            const Vec3&  node = sparkle_trail[j];
+
+            for (int s = 0; s < PER_NODE; ++s) {
+                uint32_t h = wseed ^ (uint32_t(j) * 2654435761u) ^ (uint32_t(s) * 40503u);
+                if (hash01(h) < 0.6) continue;               // sparser → calmer
+
+                double phase = hash01(h ^ 0xA5A5u) * 6.28318;
+                double tw    = SDL_pow(SDL_max(0.0, SDL_sin(t * 1.6 + phase)), 3.0);  // slower, softer
+                if (tw < 0.04) continue;
+
+                double spread = sz * (0.6 + age * 1.4);        // older = wider scatter
+                double jx = (hash01(h ^ 0x1111u) - 0.5) * spread;
+                double jy = (hash01(h ^ 0x2222u) - 0.5) * spread;
+
+                // gentle independent wander so each sparkle drifts in place
+                double wamp = sz * 0.22;                        // how far it roams
+                double fx   = 0.6 + hash01(h ^ 0x7777u) * 0.8;  // per-sparkle drift rates
+                double fy   = 0.6 + hash01(h ^ 0x8888u) * 0.8;
+                jx += SDL_sin(t * fx + hash01(h ^ 0x5555u) * 6.28318) * wamp;
+                jy += SDL_cos(t * fy + hash01(h ^ 0x6666u) * 6.28318) * wamp;
+
+                Vec3 ctr = { node.x + right.x*jx + up.x*jy + camN.x*0.03,
+                             node.y + right.y*jx + up.y*jy + camN.y*0.03,
+                             node.z + right.z*jx + up.z*jy + camN.z*0.03 };
+
+                double size_rng = 0.06 + 0.05 * hash01(h ^ 0x3333u);   // smaller + varied
+                double outer    = size_rng * sz * tw * (1.0 - age * 0.6);
+                if (outer < 1e-4) continue;
+
+                int c = int(hash01(h ^ 0x4444u) * PALN) % PALN;
+                append_disc(sv[c], si[c], ctr, right, up, camN, static_cast<float>(outer));
+            }
+        }
+
+        Mat4 ident = buildModelMat4(Vec3{0,0,0}, 0.0f, 0.0f);
+        gl.set_MVP(ident, Mat4::mul(proj, view));
+        gl.set_NormalMat(ident);
+
+        const SDL_FColor glint = aiGlintColor(ai.state);
+        for (int c = 0; c < PALN; ++c) {
+            if (si[c].empty()) continue;
+            SDL_FColor g = mix_col(PAL[c], glint, 0.15f);     // keep only a hint of state
+            gl.set_FaceShade(0.0f, 0.0f, 0.0f, 1.0f, false);  // no lit term → no white-out
+            gl.set_Emissive (g.r, g.g, g.b, 0.85f);           // pure colour, below clip
+            gl.draw_MeshCached(gpu_vbo_sparkle, gpu_ebo_sparkle,
+                               gpu_sparkle_index_count, /*dirty=*/true,
+                               sv[c].data(), (int)sv[c].size() / 9,
+                               si[c].data(), (int)si[c].size());
+        }
+        gl.set_Emissive (0.0f, 0.0f, 0.0f, 0.0f);
+        gl.set_FaceShade(0.0f, 0.0f, 0.0f, 1.0f, false);
+    }
 }
diff --git a/src/text_3d/text_3d.hpp b/src/text_3d/text_3d.hpp
index a08a54a..8a3f81b 100644
--- a/src/text_3d/text_3d.hpp
+++ b/src/text_3d/text_3d.hpp
@@ -97,6 +97,17 @@ struct Text3d final : public GameObject {
     mutable int          gpu_index_count { 0 };
     mutable bool         gpu_dirty { true };
 
+    // Sparkle overlay for AI words: a small star mesh rebuilt every frame.
+    mutable unsigned int gpu_vbo_sparkle { 0 };
+    mutable unsigned int gpu_ebo_sparkle { 0 };
+    mutable int          gpu_sparkle_index_count { 0 };
+
+    // Tail: recent world positions, sampled over time (index 0 = newest).
+    static constexpr int SPARKLE_TRAIL = 14;
+    mutable Vec3   sparkle_trail[SPARKLE_TRAIL];
+    mutable int    sparkle_trail_count { 0 };
+    mutable Uint64 sparkle_last_ms { 0 };
+
 
     bool       loaded{false};
     SDL_FColor color;