nightshade 0.13.3

A cross-platform data-oriented game engine.
Documentation
struct SpriteParticle {
    position: vec4<f32>,
    velocity: vec4<f32>,
    color: vec4<f32>,
    size_lifetime: vec4<f32>,
    emitter_data: vec4<f32>,
    physics: vec4<f32>,
    color_start: vec4<f32>,
    color_end: vec4<f32>,
}

struct SpriteEmitterData {
    position_anchor: vec4<f32>,
    velocity_min: vec4<f32>,
    velocity_max: vec4<f32>,
    lifetime_range: vec4<f32>,
    size_start: vec4<f32>,
    size_end: vec4<f32>,
    gravity_drag: vec4<f32>,
    rotation_range: vec4<f32>,
    rotation_speed_range: vec4<f32>,
    color_start: vec4<f32>,
    color_end: vec4<f32>,
    uv_min_max: vec4<f32>,
    texture_depth: vec4<f32>,
    shape_params: vec4<f32>,
    spawn_count: u32,
    shape_type: u32,
    blend_mode: u32,
    _padding: u32,
}

struct SimParams {
    delta_time: f32,
    time: f32,
    max_particles: u32,
    _padding: u32,
}

struct DrawIndirect {
    vertex_count: u32,
    instance_count: atomic<u32>,
    first_vertex: u32,
    first_instance: u32,
}

@group(0) @binding(0)
var<storage, read_write> particles: array<SpriteParticle>;

@group(0) @binding(1)
var<storage, read> emitters: array<SpriteEmitterData>;

@group(0) @binding(2)
var<uniform> params: SimParams;

@group(0) @binding(3)
var<storage, read_write> free_indices: array<u32>;

@group(0) @binding(4)
var<storage, read_write> free_count: atomic<u32>;

@group(0) @binding(5)
var<storage, read_write> alive_indices: array<u32>;

@group(0) @binding(6)
var<storage, read_write> alive_count: atomic<u32>;

@group(0) @binding(7)
var<storage, read_write> draw_indirect: DrawIndirect;

fn hash(seed: u32) -> u32 {
    var s = seed;
    s = s ^ 2747636419u;
    s = s * 2654435769u;
    s = s ^ (s >> 16u);
    s = s * 2654435769u;
    s = s ^ (s >> 16u);
    s = s * 2654435769u;
    return s;
}

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

fn random_range(seed: ptr<function, u32>, min_val: f32, max_val: f32) -> f32 {
    return min_val + random_float(seed) * (max_val - min_val);
}

@compute @workgroup_size(256)
fn update(@builtin(global_invocation_id) global_id: vec3<u32>) {
    let index = global_id.x;
    if (index >= params.max_particles) {
        return;
    }

    var particle = particles[index];

    let is_alive = particle.size_lifetime.w > 0.0;
    if (!is_alive) {
        return;
    }

    let age = particle.size_lifetime.z;
    let lifetime = particle.size_lifetime.w;
    let new_age = age + params.delta_time;

    if (new_age >= lifetime) {
        particle.size_lifetime.w = 0.0;
        let free_slot = atomicAdd(&free_count, 1u);
        free_indices[free_slot] = index;
        particles[index] = particle;
        return;
    }

    let gravity_x = particle.physics.x;
    let gravity_y = particle.physics.y;
    let drag = particle.physics.z;
    let rotation_speed = particle.physics.w;

    let drag_factor = 1.0 - drag * params.delta_time;
    var velocity = particle.velocity.xy * drag_factor;
    velocity = velocity + vec2<f32>(gravity_x, gravity_y) * params.delta_time;

    let new_position = particle.position.xy + velocity * params.delta_time;

    let new_rotation = particle.position.z + rotation_speed * params.delta_time;

    let life_ratio = new_age / lifetime;

    let size_start_x = particle.size_lifetime.x;
    let size_start_y = particle.size_lifetime.y;
    let size_end_x = particle.emitter_data.x;
    let size_end_y = particle.emitter_data.y;
    let current_size_x = mix(size_start_x, size_end_x, life_ratio);
    let current_size_y = mix(size_start_y, size_end_y, life_ratio);

    let color = mix(particle.color_start, particle.color_end, life_ratio);

    particle.position = vec4<f32>(new_position.x, new_position.y, new_rotation, 1.0);
    particle.velocity = vec4<f32>(velocity.x, velocity.y, current_size_x, current_size_y);
    particle.color = color;
    particle.size_lifetime.z = new_age;

    let alive_slot = atomicAdd(&alive_count, 1u);
    alive_indices[alive_slot] = index;
    atomicAdd(&draw_indirect.instance_count, 1u);

    particles[index] = particle;
}

@compute @workgroup_size(1)
fn reset_counters() {
    atomicStore(&alive_count, 0u);
    atomicStore(&draw_indirect.instance_count, 0u);
}

@compute @workgroup_size(256)
fn spawn(
    @builtin(workgroup_id) workgroup_id: vec3<u32>,
    @builtin(local_invocation_id) local_id: vec3<u32>
) {
    let emitter_index = workgroup_id.x;
    let spawn_index = local_id.x;

    let emitter = emitters[emitter_index];
    if (spawn_index >= emitter.spawn_count) {
        return;
    }

    let old_free_count = atomicSub(&free_count, 1u);
    if (old_free_count == 0u) {
        atomicAdd(&free_count, 1u);
        return;
    }

    let particle_index = free_indices[old_free_count - 1u];

    var seed = hash(particle_index * 1973u + u32(params.time * 10000.0) + spawn_index * 7919u + emitter_index * 6997u);

    var spawn_offset = vec2<f32>(0.0);
    let shape_type = emitter.shape_type;

    if (shape_type == 1u) {
        let radius = emitter.shape_params.x;
        let angle = random_float(&seed) * 6.28318530718;
        let distance = sqrt(random_float(&seed)) * radius;
        spawn_offset = vec2<f32>(cos(angle) * distance, sin(angle) * distance);
    } else if (shape_type == 2u) {
        let half_x = emitter.shape_params.x;
        let half_y = emitter.shape_params.y;
        spawn_offset = vec2<f32>(
            random_range(&seed, -half_x, half_x),
            random_range(&seed, -half_y, half_y)
        );
    }

    let position = emitter.position_anchor.xy + spawn_offset;
    let velocity = vec2<f32>(
        random_range(&seed, emitter.velocity_min.x, emitter.velocity_max.x),
        random_range(&seed, emitter.velocity_min.y, emitter.velocity_max.y)
    );
    let lifetime = random_range(&seed, emitter.lifetime_range.x, emitter.lifetime_range.y);
    let rotation = random_range(&seed, emitter.rotation_range.x, emitter.rotation_range.y);
    let rotation_speed = random_range(&seed, emitter.rotation_speed_range.x, emitter.rotation_speed_range.y);

    var particle: SpriteParticle;
    particle.position = vec4<f32>(position.x, position.y, rotation, 1.0);
    particle.velocity = vec4<f32>(velocity.x, velocity.y, emitter.size_start.x, emitter.size_start.y);
    particle.color = emitter.color_start;
    particle.size_lifetime = vec4<f32>(emitter.size_start.x, emitter.size_start.y, 0.0, lifetime);
    particle.emitter_data = vec4<f32>(
        emitter.size_end.x,
        emitter.size_end.y,
        emitter.texture_depth.x,
        emitter.texture_depth.y
    );
    particle.physics = vec4<f32>(
        emitter.gravity_drag.x,
        emitter.gravity_drag.y,
        emitter.gravity_drag.z,
        rotation_speed
    );
    particle.color_start = emitter.color_start;
    particle.color_end = emitter.color_end;

    particles[particle_index] = particle;
}