#include "particle_system.hpp" #include #include #include "entities/camera.hpp" #include "entities/game_object.hpp" #include "utils/config.hpp" namespace Nimbin { namespace { // Random point on a unit sphere — used to spread burst velocities evenly. // Uses the "two uniform angles" method; fine for fireworks. Vec3 random_UnitSphere() { double u = Random::unit() * 2.0 - 1.0; double phi = Random::unit() * SDL_PI_D * 2.0; double s = SDL_sqrt(1.0 - u * u); return Vec3{ s * SDL_cos(phi), u, s * SDL_sin(phi) }; } constexpr float PARTICLE_HALF = 0.04f; } // namespace void ParticleSystem::emit_FireworkBurst(const Vec3& center, SDL_FColor color, int count) { _particles.reserve(_particles.size() + static_cast(count)); for (int i = 0; i < count; i++) { Particle p; p.pos = center; // Speed 5..10 m/s in a random direction. Vec3 dir = random_UnitSphere(); double speed = 5.0 + Random::unit() * 5.0; p.vel = dir * speed; // Slight upward bias so the burst reads as an explosion, not implosion. p.vel.y += 1.5; p.color = color; p.age = 0.0; p.lifetime = 2.5 + Random::unit() * 1.5; p.yaw = static_cast(Random::unit() * SDL_PI_D * 2.0); p.spin = static_cast((Random::unit() - 0.5) * 12.0); p.alive = true; _particles.push_back(p); } } void ParticleSystem::update(double dt, const DynArray& solid_colliders, float world_scale) { const double ws = static_cast(world_scale); for (Particle& p : _particles) { if (!p.alive) continue; p.age += dt; if (p.age >= p.lifetime) { p.alive = false; continue; } p.vel.y -= Config::Physics::proj_gravity * dt; p.pos.x += p.vel.x * dt; p.pos.y += p.vel.y * dt; p.pos.z += p.vel.z * dt; p.yaw += p.spin * static_cast(dt); if (p.pos.y < -ws + 0.05 || p.pos.y > ws - 0.05 || p.pos.x < -ws + 0.05 || p.pos.x > ws - 0.05 || p.pos.z < -ws + 0.05 || p.pos.z > ws - 0.05) { p.alive = false; continue; } // Solid colliders — vanish on contact with pults, plates, platforms. // Cheap point-in-AABB check; particles are tiny so this is fine. for (const GameObject* obj : solid_colliders) { if (!obj || !obj->solid) continue; const Vec3 mn = obj->aabb_Min(); const Vec3 mx = obj->aabb_Max(); if (p.pos.x >= mn.x && p.pos.x <= mx.x && p.pos.y >= mn.y && p.pos.y <= mx.y && p.pos.z >= mn.z && p.pos.z <= mx.z) { p.alive = false; break; } } } // Compact: drop dead particles. _particles.erase( std::remove_if(_particles.begin(), _particles.end(), [](const Particle& p) { return !p.alive; }), _particles.end()); } void ParticleSystem::draw(GlRenderer& gl, const Camera& cam, const Light& light) const { if (_particles.empty()) return; Light glow = light; glow.ambient = { 0.7, 0.7, 0.7 }; glow.sun_intensity = 0.3f; glow.flashlight_intensity = 0.0f; Mat4 view = buildViewMat4 (cam); Mat4 proj = gl.get_Projection(); gl.set_Wireframe(false); gl.set_Light (glow); for (const Particle& p : _particles) { // Fade out over the last 30% of lifetime. double tail = p.lifetime * 0.3; double t_left = p.lifetime - p.age; float fade = (t_left < tail) ? static_cast(t_left / tail) : 1.0f; if (fade < 0.0f) fade = 0.0f; Mat4 model = buildModelMat4(p.pos, p.yaw); Mat4 mvp = Mat4::mul(proj, Mat4::mul(view, model)); gl.set_MVP (model, mvp); gl.set_NormalMat(model); gl.set_Color (p.color.r * fade, p.color.g * fade, p.color.b * fade); gl.draw_Box (PARTICLE_HALF, PARTICLE_HALF, PARTICLE_HALF); } } void ParticleSystem::clear() { _particles.clear(); } } // namespace Nimbin