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.gzAdd text sparkel tail With Claude
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;