Constant PARTICLE_UPDATE_COMP
Source pub const PARTICLE_UPDATE_COMP: &str = "// particle_update.comp \u{2014} GPU particle simulation compute shader\r\n//\r\n// Simulates particles using 10 mathematical engine types, applies force fields,\r\n// damping, gravity, wind, and turbulence. Outputs to a ping-pong SSBO pair.\r\n//\r\n// Dispatch with: glDispatchCompute(ceil(particle_count / 256), 1, 1)\r\n\r\n#version 430\r\nlayout(local_size_x = 256) in;\r\n\r\n// \u{2500}\u{2500} Particle struct (matches GpuParticle in Rust, 80 bytes) \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n\r\nstruct Particle {\r\n vec3 position;\r\n float _pad0;\r\n vec3 velocity;\r\n float _pad1;\r\n vec4 color;\r\n float life;\r\n float max_life;\r\n float size;\r\n uint engine_type;\r\n float seed;\r\n uint flags;\r\n float _reserved0;\r\n float _reserved1;\r\n};\r\n\r\n// \u{2500}\u{2500} Force field struct (matches GpuForceField in Rust, 32 bytes) \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n\r\nstruct ForceField {\r\n vec3 position;\r\n float strength;\r\n uint field_type; // 0=gravity, 1=vortex, 2=repulsion, 3=directional, 4=noise, 5=drag\r\n float radius;\r\n float falloff;\r\n float _pad;\r\n};\r\n\r\n// \u{2500}\u{2500} Buffers \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n\r\nlayout(std430, binding = 0) buffer ParticlesIn { Particle particles_in[]; };\r\nlayout(std430, binding = 1) buffer ParticlesOut { Particle particles_out[]; };\r\nlayout(std430, binding = 2) buffer AliveCounter { uint alive_count; };\r\n\r\n// \u{2500}\u{2500} Uniforms \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n\r\nuniform float u_dt;\r\nuniform float u_time;\r\nuniform uint u_particle_count;\r\nuniform float u_corruption;\r\nuniform float u_damping;\r\nuniform vec3 u_gravity;\r\nuniform vec3 u_wind;\r\nuniform float u_turbulence;\r\nuniform int u_field_count;\r\n\r\n// Force fields (max 16).\r\nuniform vec3 u_field_pos[16];\r\nuniform float u_field_strength[16];\r\nuniform uint u_field_type[16];\r\nuniform float u_field_radius[16];\r\nuniform float u_field_falloff[16];\r\n\r\n// \u{2500}\u{2500} Pseudo-random \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n\r\nfloat hash(float n) {\r\n return fract(sin(n) * 43758.5453123);\r\n}\r\n\r\nfloat hash2(vec2 p) {\r\n return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);\r\n}\r\n\r\nvec3 hash3(float n) {\r\n return vec3(\r\n hash(n),\r\n hash(n + 1.0),\r\n hash(n + 2.0)\r\n ) * 2.0 - 1.0;\r\n}\r\n\r\n// \u{2500}\u{2500} 3D Simplex-like noise for turbulence \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n\r\nfloat noise3d(vec3 p) {\r\n vec3 i = floor(p);\r\n vec3 f = fract(p);\r\n f = f * f * (3.0 - 2.0 * f);\r\n float n = i.x + i.y * 157.0 + i.z * 113.0;\r\n return mix(\r\n mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),\r\n mix(hash(n + 157.0), hash(n + 158.0), f.x), f.y),\r\n mix(mix(hash(n + 113.0), hash(n + 114.0), f.x),\r\n mix(hash(n + 270.0), hash(n + 271.0), f.x), f.y),\r\n f.z\r\n );\r\n}\r\n\r\nvec3 turbulence_force(vec3 pos, float strength, float time) {\r\n return vec3(\r\n noise3d(pos * 0.5 + vec3(time * 0.3, 0.0, 0.0)) - 0.5,\r\n noise3d(pos * 0.5 + vec3(0.0, time * 0.3, 100.0)) - 0.5,\r\n noise3d(pos * 0.5 + vec3(200.0, 0.0, time * 0.3)) - 0.5\r\n ) * strength;\r\n}\r\n\r\n// \u{2500}\u{2500} Engine-specific acceleration \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n\r\nvec3 engine_accel(uint engine_type, vec3 pos, float seed, float corruption, float time) {\r\n vec3 accel = vec3(0.0);\r\n float c = corruption;\r\n\r\n if (engine_type == 0u) {\r\n // Linear: gentle drift with sine modulation.\r\n accel = vec3(\r\n sin(time * 0.5 + seed * 6.28) * 0.3,\r\n cos(time * 0.3 + seed * 3.14) * 0.2,\r\n 0.0\r\n );\r\n }\r\n else if (engine_type == 1u) {\r\n // Lorenz attractor.\r\n float sigma = 10.0 + c * 2.0;\r\n float rho = 28.0 + c * 5.0;\r\n float beta = 2.6667 + c * 0.5;\r\n accel.x = sigma * (pos.y - pos.x);\r\n accel.y = pos.x * (rho - pos.z) - pos.y;\r\n accel.z = pos.x * pos.y - beta * pos.z;\r\n accel *= 0.01;\r\n }\r\n else if (engine_type == 2u) {\r\n // Mandelbrot orbit: z = z\u{b2} + c, mapped to 2D.\r\n float cx = pos.x * 0.03 + seed * 0.5 - 0.5;\r\n float cy = pos.y * 0.03;\r\n float zr = cx, zi = cy;\r\n for (int i = 0; i < 4; i++) {\r\n float nr = zr * zr - zi * zi + cx;\r\n float ni = 2.0 * zr * zi + cy;\r\n zr = nr; zi = ni;\r\n }\r\n accel.x = (zr - pos.x * 0.03) * 0.5;\r\n accel.y = (zi - pos.y * 0.03) * 0.5;\r\n accel *= (1.0 + c * 0.3);\r\n }\r\n else if (engine_type == 3u) {\r\n // Julia set orbit: z = z\u{b2} + c_julia.\r\n float cr = -0.7 + c * 0.1;\r\n float ci = 0.27015 + c * 0.05;\r\n float zr = pos.x * 0.02, zi = pos.y * 0.02;\r\n for (int i = 0; i < 3; i++) {\r\n float nr = zr * zr - zi * zi + cr;\r\n float ni = 2.0 * zr * zi + ci;\r\n zr = nr; zi = ni;\r\n }\r\n accel.x = (zr - pos.x * 0.02) * 0.3;\r\n accel.y = (zi - pos.y * 0.02) * 0.3;\r\n }\r\n else if (engine_type == 4u) {\r\n // Rossler attractor.\r\n float a = 0.2 + c * 0.05;\r\n float b = 0.2 + c * 0.03;\r\n float cc = 5.7 + c * 1.0;\r\n accel.x = -(pos.y + pos.z);\r\n accel.y = pos.x + a * pos.y;\r\n accel.z = b + pos.z * (pos.x - cc);\r\n accel *= 0.01;\r\n }\r\n else if (engine_type == 5u) {\r\n // Aizawa attractor.\r\n float a = 0.95, b = 0.7, cc = 0.6, d = 3.5, e = 0.25, f = 0.1;\r\n a += c * 0.1;\r\n accel.x = (pos.z - b) * pos.x - d * pos.y;\r\n accel.y = d * pos.x + (pos.z - b) * pos.y;\r\n accel.z = cc + a * pos.z - pos.z * pos.z * pos.z / 3.0\r\n - (pos.x * pos.x + pos.y * pos.y) * (1.0 + e * pos.z)\r\n + f * pos.z * pos.x * pos.x * pos.x;\r\n accel *= 0.02;\r\n }\r\n else if (engine_type == 6u) {\r\n // Thomas attractor.\r\n float b = 0.208186 + c * 0.05;\r\n accel.x = sin(pos.y) - b * pos.x;\r\n accel.y = sin(pos.z) - b * pos.y;\r\n accel.z = sin(pos.x) - b * pos.z;\r\n accel *= 0.5;\r\n }\r\n else if (engine_type == 7u) {\r\n // Halvorsen attractor.\r\n float a = 1.89 + c * 0.2;\r\n accel.x = -a * pos.x - 4.0 * pos.y - 4.0 * pos.z - pos.y * pos.y;\r\n accel.y = -a * pos.y - 4.0 * pos.z - 4.0 * pos.x - pos.z * pos.z;\r\n accel.z = -a * pos.z - 4.0 * pos.x - 4.0 * pos.y - pos.x * pos.x;\r\n accel *= 0.005;\r\n }\r\n else if (engine_type == 8u) {\r\n // Chen attractor.\r\n float a = 35.0 + c * 3.0;\r\n float b = 3.0 + c * 0.5;\r\n float cc2 = 28.0 + c * 2.0;\r\n accel.x = a * (pos.y - pos.x);\r\n accel.y = (cc2 - a) * pos.x - pos.x * pos.z + cc2 * pos.y;\r\n accel.z = pos.x * pos.y - b * pos.z;\r\n accel *= 0.003;\r\n }\r\n else if (engine_type == 9u) {\r\n // Dadras attractor.\r\n float a = 3.0, b = 2.7, cc3 = 1.7, d = 2.0, e = 9.0;\r\n a += c * 0.3;\r\n accel.x = pos.y - a * pos.x + b * pos.y * pos.z;\r\n accel.y = cc3 * pos.y - pos.x * pos.z + pos.z;\r\n accel.z = d * pos.x * pos.y - e * pos.z;\r\n accel *= 0.005;\r\n }\r\n\r\n return accel;\r\n}\r\n\r\n// \u{2500}\u{2500} Force field evaluation \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n\r\nvec3 evaluate_force_fields(vec3 pos) {\r\n vec3 total_force = vec3(0.0);\r\n\r\n for (int i = 0; i < u_field_count && i < 16; i++) {\r\n vec3 to_field = u_field_pos[i] - pos;\r\n float dist = length(to_field);\r\n\r\n if (dist > u_field_radius[i] || dist < 0.001) continue;\r\n\r\n float falloff_factor = 1.0;\r\n if (u_field_falloff[i] > 0.0) {\r\n falloff_factor = 1.0 / pow(dist + 0.1, u_field_falloff[i]);\r\n }\r\n\r\n float edge_fade = 1.0 - smoothstep(u_field_radius[i] * 0.7, u_field_radius[i], dist);\r\n vec3 dir = normalize(to_field);\r\n\r\n if (u_field_type[i] == 0u) {\r\n // Gravity: attract/repel along direction.\r\n total_force += dir * u_field_strength[i] * falloff_factor * edge_fade;\r\n }\r\n else if (u_field_type[i] == 1u) {\r\n // Vortex: perpendicular force (swirl).\r\n vec3 perp = normalize(cross(to_field, vec3(0.0, 1.0, 0.0)));\r\n total_force += perp * u_field_strength[i] * falloff_factor * edge_fade;\r\n // Plus weak attraction to maintain orbit.\r\n total_force += dir * u_field_strength[i] * 0.1 * falloff_factor * edge_fade;\r\n }\r\n else if (u_field_type[i] == 2u) {\r\n // Repulsion: push away.\r\n total_force -= dir * abs(u_field_strength[i]) * falloff_factor * edge_fade;\r\n }\r\n else if (u_field_type[i] == 3u) {\r\n // Directional: constant direction (use field_pos as direction).\r\n total_force += normalize(u_field_pos[i]) * u_field_strength[i] * edge_fade;\r\n }\r\n else if (u_field_type[i] == 4u) {\r\n // Noise-based force.\r\n vec3 n = turbulence_force(pos * 0.3 + u_field_pos[i] * 0.1, 1.0, u_time);\r\n total_force += n * u_field_strength[i] * edge_fade;\r\n }\r\n else if (u_field_type[i] == 5u) {\r\n // Drag: resist velocity (applied as deceleration later).\r\n // We encode drag differently \u{2014} the main loop handles it.\r\n }\r\n }\r\n\r\n return total_force;\r\n}\r\n\r\n// \u{2500}\u{2500} Main \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n\r\nvoid main() {\r\n uint id = gl_GlobalInvocationID.x;\r\n if (id >= u_particle_count) return;\r\n\r\n Particle p = particles_in[id];\r\n\r\n // Dead particles pass through unchanged.\r\n if (p.life <= 0.0) {\r\n particles_out[id] = p;\r\n return;\r\n }\r\n\r\n // \u{2500}\u{2500} Engine-specific acceleration \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n vec3 accel = engine_accel(p.engine_type, p.position, p.seed, u_corruption, u_time);\r\n\r\n // \u{2500}\u{2500} External forces \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n accel += u_gravity;\r\n accel += u_wind;\r\n\r\n // Force fields.\r\n if ((p.flags & 1u) != 0u) {\r\n accel += evaluate_force_fields(p.position);\r\n }\r\n\r\n // Turbulence.\r\n if (u_turbulence > 0.0) {\r\n accel += turbulence_force(p.position, u_turbulence, u_time);\r\n }\r\n\r\n // \u{2500}\u{2500} Integration (symplectic Euler) \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n p.velocity += accel * u_dt;\r\n p.velocity *= u_damping;\r\n p.position += p.velocity * u_dt;\r\n\r\n // \u{2500}\u{2500} Lifetime \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n p.life -= u_dt;\r\n float age_frac = 1.0 - clamp(p.life / p.max_life, 0.0, 1.0);\r\n\r\n // Fade alpha over lifetime.\r\n p.color.a = mix(1.0, 0.0, age_frac * age_frac);\r\n\r\n // Shrink over lifetime.\r\n // (size stays constant \u{2014} the render pass uses age_frac for size modulation)\r\n\r\n // \u{2500}\u{2500} Respawn dead particles (infinite mode) \u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\u{2500}\r\n if (p.life <= 0.0) {\r\n // Respawn at random position near origin with same engine type.\r\n float h = hash(float(id) + u_time * 100.0);\r\n float h2 = hash(float(id) + u_time * 100.0 + 1.0);\r\n float h3 = hash(float(id) + u_time * 100.0 + 2.0);\r\n p.position = vec3(\r\n (h - 0.5) * 40.0,\r\n (h2 - 0.5) * 30.0,\r\n (h3 - 0.5) * 10.0\r\n );\r\n p.velocity = vec3(\r\n (hash(float(id) * 3.14) - 0.5) * 0.5,\r\n (hash(float(id) * 2.71) - 0.5) * 0.5,\r\n (hash(float(id) * 1.41) - 0.5) * 0.1\r\n );\r\n p.life = p.max_life;\r\n p.color.a = 1.0;\r\n\r\n // Increment alive counter.\r\n atomicAdd(alive_count, 1u);\r\n } else {\r\n atomicAdd(alive_count, 1u);\r\n }\r\n\r\n particles_out[id] = p;\r\n}\r\n";