Skip to main content

PARTICLE_SIMULATE

Constant PARTICLE_SIMULATE 

Source
pub const PARTICLE_SIMULATE: &str = "// GPU particle simulation kernel: single source for every backend.\n//\n// One thread per slot in the per-emitter pool. Each thread ages and integrates\n// whatever particle currently occupies its slot; if the age reaches the\n// lifetime the slot is marked dead. A dead slot then tries to consume one unit\n// of the frame\'s spawn budget (atomically) and respawns with a fresh velocity\n// sampled inside a cone of half-angle acos(spread_cos) around `direction`.\n//\n// `Particle` and `ParticleParams` arrive from the shared PARTICLE_TYPES\n// fragment, which the render half (`particle.slang`) splices too, so the pool\n// this kernel writes and the pool that pass reads have one declaration. Both\n// are locked to the Rust struct by `particle_params_layout_matches_msl` in\n// render_types.rs.\n\n{PARTICLE_TYPES}\n\n[[vk::binding(0, 0)]] RWStructuredBuffer<Particle> pool : register(u0);\n// Remaining spawn budget for this dispatch, as a single counter at element 0.\n[[vk::binding(1, 0)]] RWStructuredBuffer<uint> spawn_counter : register(u1);\n\n// A host difference, not a target one: Vulkan pushes the params and DirectX\n// takes them as root constants at b0 (where a bare push constant lands there),\n// while the Metal encoder writes them to buffer(2).\n#ifdef METAL_BINDINGS\nConstantBuffer<ParticleParams> params : register(b2);\n#else\n[[vk::push_constant]] ConstantBuffer<ParticleParams> params;\n#endif\n\n// Cheap fixed-point hash to a unit float in [0, 1). Mutates `state` so a thread\n// needing several uncorrelated samples advances it between calls.\nfloat prng(inout uint state)\n{\n    state = state * 1664525u + 1013904223u;\n    // Top 24 bits of the hash become the mantissa.\n    return float(state >> 8) * (1.0 / 16777216.0);\n}\n\n// Sample a unit vector inside a cone of half-angle acos(cone_cos) centred on\n// `axis`. The cap is uniformly sampled in solid angle, so the spawn cloud has\n// no axial bunching.\nfloat3 sample_cone(inout uint rng, float3 axis, float cone_cos)\n{\n    // `u` picks a polar angle whose cosine is uniform in [cone_cos, 1].\n    float u = lerp(cone_cos, 1.0, prng(rng));\n    float r = sqrt(max(1.0 - u * u, 0.0));\n    float phi = prng(rng) * 6.2831853;\n    float3 local = float3(r * cos(phi), r * sin(phi), u);\n\n    // Any orthonormal basis around `axis`. Picking the world axis least\n    // parallel to it keeps the cross product well conditioned.\n    float3 up = abs(axis.y) < 0.9 ? float3(0, 1, 0) : float3(1, 0, 0);\n    float3 t = normalize(cross(up, axis));\n    float3 b = cross(axis, t);\n    return normalize(local.x * t + local.y * b + local.z * axis);\n}\n\n[shader(\"compute\")]\n[numthreads(64, 1, 1)]\nvoid particle_simulate(uint3 gid : SV_DispatchThreadID)\n{\n    uint id = gid.x;\n    if (id >= params.max_particles)\n    {\n        return;\n    }\n    Particle pt = pool[id];\n\n    // Age the particle in this slot, if any. A lifetime of 0 flags a dead slot.\n    if (pt.velocity_lifetime.w > 0.0)\n    {\n        pt.position_age.w += params.dt;\n        if (pt.position_age.w >= pt.velocity_lifetime.w)\n        {\n            pt.velocity_lifetime.w = 0.0;\n        }\n        else\n        {\n            pt.velocity_lifetime.xyz =\n                pt.velocity_lifetime.xyz + params.gravity_speed_max.xyz * params.dt;\n            pt.position_age.xyz = pt.position_age.xyz + pt.velocity_lifetime.xyz * params.dt;\n        }\n    }\n\n    // A dead slot claims one unit of the remaining budget. Adding -1 in two\'s\n    // complement returns the pre-subtract value, so only threads that observed\n    // a positive remaining count spawn; threads racing past zero see 0 or a\n    // wrapped very large unsigned number.\n    if (pt.velocity_lifetime.w == 0.0 && params.spawn_budget > 0u)\n    {\n        uint claimed;\n        InterlockedAdd(spawn_counter[0], 0xFFFFFFFFu, claimed);\n        if (claimed > 0u && claimed <= params.spawn_budget)\n        {\n            uint rng = (id * 747796405u) ^ (params.random_seed * 2891336453u);\n            // Warm the RNG so adjacent threads decorrelate; the value is dropped.\n            float warm = prng(rng);\n            float3 dir = sample_cone(\n                rng,\n                normalize(params.direction_speed_min.xyz),\n                params.position_spread.w);\n            float speed = lerp(\n                params.direction_speed_min.w, params.gravity_speed_max.w, prng(rng));\n            float life = lerp(params.lifetime_min, params.lifetime_max, prng(rng));\n            pt.position_age = float4(params.position_spread.xyz, 0.0);\n            pt.velocity_lifetime = float4(dir * speed, max(life, 0.001));\n        }\n    }\n\n    pool[id] = pt;\n}\n";
Expand description

particle_simulate.slang.