viewport-lib 0.19.0

3D viewport rendering library
Documentation
// Particle mesh shader: instanced mesh draw whose per-instance transform is
// composed in the vertex stage from a live GPU particle.
//
// The mesh vertex buffer is bound on slot 0 with the standard `Vertex` layout
// (position, normal, colour, uv, tangent); only position and uv are consumed
// here. Per-instance data comes from a storage buffer indexed by
// `@builtin(instance_index)`. Unlit; the particle colour multiplies any
// optional albedo sample.

struct Camera {
    view_proj:     mat4x4<f32>,
    eye_pos:       vec3<f32>,
    _pad:          f32,
    forward:       vec3<f32>,
    _pad1:         f32,
    inv_view_proj: mat4x4<f32>,
    view:          mat4x4<f32>,
};

struct ClipPlanes {
    planes:          array<vec4<f32>, 6>,
    count:           u32,
    _pad0:           u32,
    viewport_width:  f32,
    viewport_height: f32,
};

struct MeshDrawUniform {
    align:       u32,        // 0=Identity, 1=Velocity, 2=Random
    has_texture: u32,
    _pad0:       u32,
    _pad1:       u32,
};

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

@group(0) @binding(0) var<uniform>       camera:      Camera;
@group(0) @binding(4) var<uniform>       clip_planes: ClipPlanes;

@group(1) @binding(0) var<uniform>       draw_ub:     MeshDrawUniform;
@group(1) @binding(1) var                albedo:      texture_2d<f32>;
@group(1) @binding(2) var                albedo_samp: sampler;
@group(1) @binding(3) var<storage, read> particles:   array<Particle>;

struct VertexIn {
    @location(0) position: vec3<f32>,
    @location(1) normal:   vec3<f32>,
    @location(2) colour:   vec4<f32>,
    @location(3) uv:       vec2<f32>,
    @location(4) tangent:  vec4<f32>,
};

struct VertexOut {
    @builtin(position) clip_pos:  vec4<f32>,
    @location(0)       colour:    vec4<f32>,
    @location(1)       world_pos: vec3<f32>,
    @location(2)       uv:        vec2<f32>,
};

// PCG-style hash. Same kernel as the emit shader; reused so the random
// rotation derived here is reproducible across draws of the same particle.
fn hash_u32(x: u32) -> u32 {
    var state = x * 747796405u + 2891336453u;
    let word  = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
    return (word >> 22u) ^ word;
}

fn hash_f(x: u32) -> f32 {
    return f32(hash_u32(x)) / 4294967295.0;
}

// Build a 3x3 rotation matrix from a unit quaternion (x, y, z, w).
fn quat_to_mat3(q: vec4<f32>) -> mat3x3<f32> {
    let x = q.x; let y = q.y; let z = q.z; let w = q.w;
    let xx = x * x; let yy = y * y; let zz = z * z;
    let xy = x * y; let xz = x * z; let yz = y * z;
    let wx = w * x; let wy = w * y; let wz = w * z;
    return mat3x3<f32>(
        vec3<f32>(1.0 - 2.0 * (yy + zz),       2.0 * (xy + wz),       2.0 * (xz - wy)),
        vec3<f32>(      2.0 * (xy - wz), 1.0 - 2.0 * (xx + zz),       2.0 * (yz + wx)),
        vec3<f32>(      2.0 * (xz + wy),       2.0 * (yz - wx), 1.0 - 2.0 * (xx + yy)),
    );
}

// Uniform random unit quaternion via Shoemake's parameterisation.
fn random_quat(seed: f32) -> vec4<f32> {
    let bits = bitcast<u32>(seed);
    let u1 = hash_f(bits);
    let u2 = hash_f(bits ^ 0x9E3779B1u);
    let u3 = hash_f(bits ^ 0x85EBCA77u);
    let r1 = sqrt(1.0 - u1);
    let r2 = sqrt(u1);
    let t1 = 6.2831853 * u2;
    let t2 = 6.2831853 * u3;
    return vec4<f32>(r1 * sin(t1), r1 * cos(t1), r2 * sin(t2), r2 * cos(t2));
}

// Shortest-arc rotation that maps +Y to `dir`. `dir` must be unit length.
fn align_y_to(dir: vec3<f32>) -> mat3x3<f32> {
    let up = vec3<f32>(0.0, 1.0, 0.0);
    let c = dot(up, dir);
    if c > 0.9999 {
        return mat3x3<f32>(
            vec3<f32>(1.0, 0.0, 0.0),
            vec3<f32>(0.0, 1.0, 0.0),
            vec3<f32>(0.0, 0.0, 1.0),
        );
    }
    if c < -0.9999 {
        // Antiparallel: 180-degree rotation around any axis orthogonal to up.
        return mat3x3<f32>(
            vec3<f32>(1.0,  0.0,  0.0),
            vec3<f32>(0.0, -1.0,  0.0),
            vec3<f32>(0.0,  0.0, -1.0),
        );
    }
    let axis = cross(up, dir);
    let s = length(axis);
    let k = axis / s;
    // Rodrigues' formula with sin = s, cos = c.
    let K = mat3x3<f32>(
        vec3<f32>( 0.0,   k.z, -k.y),
        vec3<f32>(-k.z,  0.0,   k.x),
        vec3<f32>( k.y, -k.x,  0.0),
    );
    let I = mat3x3<f32>(
        vec3<f32>(1.0, 0.0, 0.0),
        vec3<f32>(0.0, 1.0, 0.0),
        vec3<f32>(0.0, 0.0, 1.0),
    );
    return I + K * s + (K * K) * (1.0 - c);
}

@vertex
fn vs_main(in: VertexIn, @builtin(instance_index) ii: u32) -> VertexOut {
    var out: VertexOut;
    let p = particles[ii];

    if p.lifetime <= 0.0 {
        // Dead slot. Emit a clip-space vertex outside any meaningful frustum so
        // the rasteriser drops every triangle of this instance.
        out.clip_pos  = vec4<f32>(2.0, 2.0, 2.0, 1.0);
        out.colour    = vec4<f32>(0.0);
        out.world_pos = vec3<f32>(0.0);
        out.uv        = vec2<f32>(0.0);
        return out;
    }

    var rot = mat3x3<f32>(
        vec3<f32>(1.0, 0.0, 0.0),
        vec3<f32>(0.0, 1.0, 0.0),
        vec3<f32>(0.0, 0.0, 1.0),
    );
    if draw_ub.align == 1u {
        let speed2 = dot(p.velocity, p.velocity);
        if speed2 > 1e-8 {
            rot = align_y_to(p.velocity / sqrt(speed2));
        }
    } else if draw_ub.align == 2u {
        rot = quat_to_mat3(random_quat(p.spawn_seed));
    }

    let local = rot * (in.position * p.size);
    let world_pos = p.position + local;

    out.clip_pos  = camera.view_proj * vec4<f32>(world_pos, 1.0);
    out.colour    = p.colour;
    out.world_pos = world_pos;
    out.uv        = in.uv;
    return out;
}

@fragment
fn fs_main(in: VertexOut) -> @location(0) vec4<f32> {
    for (var i = 0u; i < clip_planes.count; i = i + 1u) {
        if dot(vec4<f32>(in.world_pos, 1.0), clip_planes.planes[i]) < 0.0 {
            discard;
        }
    }

    var colour = in.colour;
    if draw_ub.has_texture != 0u {
        colour = colour * textureSample(albedo, albedo_samp, in.uv);
    }
    if colour.a <= 0.001 { discard; }
    return colour;
}