viewport-lib 0.19.0

3D viewport rendering library
Documentation
// Particle emit kernel.
//
// One thread per slot in the particle buffer. Threads whose slot is dead
// (lifetime <= 0) compete for the limited spawn budget via an atomic counter;
// threads whose slot is still alive return immediately. Successful threads
// write a fresh particle drawn from the emitter's spawn shape and velocity
// distribution.

struct Particle {
    position:     vec3<f32>,
    lifetime:     f32,
    velocity:     vec3<f32>,
    max_lifetime: f32,
    colour:       vec4<f32>,
    size:         f32,
    spawn_seed:   f32,
    _pad:         vec2<f32>,
};

struct EmitParams {
    spawn_min:        vec3<f32>,
    spawn_kind:       u32,        // 0=Point, 1=Box, 2=Sphere
    spawn_max:        vec3<f32>,
    spawn_radius:     f32,
    vel_min:          vec3<f32>,
    vel_kind:         u32,        // 0=Fixed, 1=UniformBox, 2=UniformCone
    vel_max:          vec3<f32>,
    cone_half_angle:  f32,
    vel_axis:         vec3<f32>,
    cone_min_speed:   f32,
    colour:           vec4<f32>,
    spawn_count:      u32,
    capacity:         u32,
    rng_seed:         u32,
    size:             f32,
    lifetime_min:     f32,
    lifetime_max:     f32,
    cone_max_speed:   f32,
    _pad:             f32,
};

@group(0) @binding(0) var<uniform>             params:    EmitParams;
@group(1) @binding(0) var<storage, read_write> particles: array<Particle>;
@group(1) @binding(1) var<storage, read_write> emit_remaining: atomic<u32>;

// PCG hash for cheap, decent-quality per-thread randomness.
fn pcg(seed: u32) -> u32 {
    var state = seed * 747796405u + 2891336453u;
    let word  = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
    return (word >> 22u) ^ word;
}

fn rand_f(seed: ptr<function, u32>) -> f32 {
    *seed = pcg(*seed);
    return f32(*seed) / 4294967295.0;
}

@compute @workgroup_size(64)
fn emit_main(@builtin(global_invocation_id) gid: vec3<u32>) {
    let tid = gid.x;
    if tid >= params.capacity { return; }

    let cur = particles[tid];
    if cur.lifetime > 0.0 { return; }

    // Each emitting thread claims one spawn ticket. If we under-claim
    // (counter hit 0) we restore it so the count stays correct.
    let claim = atomicSub(&emit_remaining, 1u);
    if claim == 0u || claim > params.spawn_count {
        atomicAdd(&emit_remaining, 1u);
        return;
    }

    var rng = pcg(params.rng_seed ^ tid);

    let position = spawn_position(&rng);
    let velocity = spawn_velocity(&rng);
    let life     = mix(params.lifetime_min, params.lifetime_max, rand_f(&rng));

    // Stable per-spawn seed used by the mesh draw route for `Random` align
    // rotation. Held untouched by the sim until the slot is recycled.
    let seed_bits = pcg(params.rng_seed ^ (tid * 0x68E31DA4u));
    let spawn_seed = f32(seed_bits) / 4294967295.0;

    var p: Particle;
    p.position     = position;
    p.lifetime     = life;
    p.velocity     = velocity;
    p.max_lifetime = life;
    p.colour       = params.colour;
    p.size         = params.size;
    p.spawn_seed   = spawn_seed;
    particles[tid] = p;
}

fn spawn_position(rng: ptr<function, u32>) -> vec3<f32> {
    if params.spawn_kind == 0u {
        return params.spawn_min;
    } else if params.spawn_kind == 1u {
        return vec3<f32>(
            mix(params.spawn_min.x, params.spawn_max.x, rand_f(rng)),
            mix(params.spawn_min.y, params.spawn_max.y, rand_f(rng)),
            mix(params.spawn_min.z, params.spawn_max.z, rand_f(rng)),
        );
    } else {
        // Sphere: rejection sample inside the unit ball, then scale. Capped at
        // 8 attempts so the validator can prove termination; the success rate
        // is ~52% per try so 8 misses is vanishingly rare.
        var p = vec3<f32>(0.0);
        for (var i = 0u; i < 8u; i = i + 1u) {
            p = vec3<f32>(
                rand_f(rng) * 2.0 - 1.0,
                rand_f(rng) * 2.0 - 1.0,
                rand_f(rng) * 2.0 - 1.0,
            );
            if dot(p, p) <= 1.0 {
                break;
            }
        }
        return params.spawn_min + p * params.spawn_radius;
    }
}

fn spawn_velocity(rng: ptr<function, u32>) -> vec3<f32> {
    if params.vel_kind == 0u {
        return params.vel_min;
    } else if params.vel_kind == 1u {
        return vec3<f32>(
            mix(params.vel_min.x, params.vel_max.x, rand_f(rng)),
            mix(params.vel_min.y, params.vel_max.y, rand_f(rng)),
            mix(params.vel_min.z, params.vel_max.z, rand_f(rng)),
        );
    } else {
        // Cone around `vel_axis`: pick a direction in a spherical cap, then
        // scale by a uniform speed.
        let axis = normalize(params.vel_axis);
        let cos_min = cos(params.cone_half_angle);
        let cos_a   = mix(cos_min, 1.0, rand_f(rng));
        let sin_a   = sqrt(max(0.0, 1.0 - cos_a * cos_a));
        let phi     = rand_f(rng) * 6.2831853;

        // Build an orthonormal basis around `axis`.
        let helper = select(vec3<f32>(1.0, 0.0, 0.0),
                            vec3<f32>(0.0, 1.0, 0.0),
                            abs(axis.x) > 0.9);
        let tangent   = normalize(cross(axis, helper));
        let bitangent = cross(axis, tangent);

        let dir = axis * cos_a
                + tangent   * (sin_a * cos(phi))
                + bitangent * (sin_a * sin(phi));
        let speed = mix(params.cone_min_speed, params.cone_max_speed, rand_f(rng));
        return dir * speed;
    }
}