viewport-lib 0.15.0

3D viewport rendering library
Documentation
// Skinned vertex stage for the standard lit material.
//
// Replaces only the vertex stage of `mesh.wgsl`. The fragment stage in
// `mesh.wgsl` (entry point `fs_main`) is reused unchanged. Vertex output
// layout must stay in sync with `VertexOut` in `mesh.wgsl`.
//
// Skin data lives in storage buffers at group 2: a per-vertex weights buffer
// (joint indices + weights) and a per-instance joint palette. The mesh's
// bind-pose vertex buffer is unchanged. Linear blend skinning is applied to
// position and normal in this shader before the standard model transform.
//
// See `docs/plans/skeletal-animation-plan.md` Phase 5.

// ---------------------------------------------------------------------------
// Bindings (only those the vertex stage needs are declared; the rest of
// `mesh.wgsl`'s bindings remain available to `fs_main` via the shared bind
// group layouts).
// ---------------------------------------------------------------------------

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>,
};

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

struct Object {
    model: mat4x4<f32>,
    colour: vec4<f32>,
    selected: u32,
    wireframe: u32,
    ambient: f32,
    diffuse: f32,
    specular: f32,
    shininess: f32,
    has_texture: u32,
    use_pbr: u32,
    metallic: f32,
    roughness: f32,
    has_normal_map: u32,
    has_ao_map: u32,
    has_attribute: u32,
    scalar_min: f32,
    scalar_max: f32,
    _pad_scalar: u32,
    nan_colour: vec4<f32>,
    use_nan_colour: u32,
    use_matcap: u32,
    matcap_blendable: u32,
    unlit: u32,
    use_face_colour: u32,
    uv_vis_mode: u32,
    uv_vis_scale: f32,
    backface_policy: u32,
    backface_colour: vec4<f32>,
    has_warp: u32,
    warp_scale: f32,
    _pad_warp0: u32,
    _pad_warp1: u32,
    emissive: vec3<f32>,
    _pad_emissive: u32,
    alpha_mode: u32,
    alpha_cutoff: f32,
    has_metallic_roughness_tex: u32,
    has_emissive_tex: u32,
};

@group(1) @binding(0) var<uniform> object: Object;
// Per-vertex storage buffers consumed by the vertex stage. Bindings match
// `mesh.wgsl`'s declarations exactly so the same `object_bgl` works for both
// pipeline variants. Texture bindings (1..5, 7, 11, 12) are read by
// `mesh.wgsl`'s `fs_main` and not declared here.
@group(1) @binding(6) var<storage, read> scalar_buffer: array<f32>;
@group(1) @binding(8) var<storage, read> face_colour_buffer: array<vec4<f32>>;
@group(1) @binding(9) var<storage, read> warp_buffer: array<f32>;

// One entry per vertex: 4 weights, then joint indices packed as two u32s
// (low/high u16 pair). Field order matches `PackedSkinVertex` in
// `src/resources/skin.rs` so the struct is naturally 24 bytes with no
// padding under WGSL std430 layout (vec4<f32> aligns to 16, two u32s pack
// immediately after).
struct SkinVertex {
    weights: vec4<f32>,
    joints_01: u32,
    joints_23: u32,
};

@group(2) @binding(0) var<storage, read> skin_weights: array<SkinVertex>;
@group(2) @binding(1) var<storage, read> skin_palette: array<mat4x4<f32>>;

// ---------------------------------------------------------------------------
// Vertex IO
// ---------------------------------------------------------------------------

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>,
    @builtin(vertex_index) vertex_index: u32,
};

struct VertexOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0) colour:         vec4<f32>,
    @location(1) world_normal:   vec3<f32>,
    @location(2) world_pos:      vec3<f32>,
    @location(3) uv:             vec2<f32>,
    @location(4) world_tangent:  vec4<f32>,
    @location(5) scalar_val:     f32,
    @location(6) is_nan_scalar:  f32,
    @location(7) face_colour:    vec4<f32>,
};

// Unpack one of four 16-bit joint indices from the two packed u32s.
fn unpack_joint(skin: SkinVertex, slot: u32) -> u32 {
    if slot == 0u { return skin.joints_01 & 0xFFFFu; }
    if slot == 1u { return (skin.joints_01 >> 16u) & 0xFFFFu; }
    if slot == 2u { return skin.joints_23 & 0xFFFFu; }
    return (skin.joints_23 >> 16u) & 0xFFFFu;
}

// Blend the four palette matrices for this vertex into a single skinning
// matrix. Zero-weight slots fall out naturally because the matrix is scaled
// by zero before summing.
fn skin_matrix(vertex_index: u32) -> mat4x4<f32> {
    let s = skin_weights[vertex_index];
    let j0 = unpack_joint(s, 0u);
    let j1 = unpack_joint(s, 1u);
    let j2 = unpack_joint(s, 2u);
    let j3 = unpack_joint(s, 3u);
    return skin_palette[j0] * s.weights.x
         + skin_palette[j1] * s.weights.y
         + skin_palette[j2] * s.weights.z
         + skin_palette[j3] * s.weights.w;
}

@vertex
fn vs_main(in: VertexIn) -> VertexOut {
    var out: VertexOut;

    // Warp is applied to the bind-pose position before skinning so a warped
    // vertex follows its joint cleanly. Mirrors mesh.wgsl's vs_main behaviour
    // for warp + skinning composition.
    var bind_local = in.position;
    if object.has_warp != 0u {
        let wi = in.vertex_index * 3u;
        let warp_len = arrayLength(&warp_buffer);
        if wi + 2u < warp_len {
            bind_local += vec3<f32>(
                warp_buffer[wi],
                warp_buffer[wi + 1u],
                warp_buffer[wi + 2u],
            ) * object.warp_scale;
        }
    }

    let skin = skin_matrix(in.vertex_index);
    let local_pos = (skin * vec4<f32>(bind_local, 1.0)).xyz;

    let skin3 = mat3x3<f32>(
        skin[0].xyz,
        skin[1].xyz,
        skin[2].xyz,
    );
    let local_normal = skin3 * in.normal;
    let local_tangent = skin3 * in.tangent.xyz;

    let world_pos = object.model * vec4<f32>(local_pos, 1.0);
    out.clip_pos = camera.view_proj * world_pos;
    out.world_pos = world_pos.xyz;
    out.colour = in.colour;

    let model3 = mat3x3<f32>(
        object.model[0].xyz,
        object.model[1].xyz,
        object.model[2].xyz,
    );
    out.world_normal = normalize(model3 * local_normal);
    out.world_tangent = vec4<f32>(normalize(model3 * local_tangent), in.tangent.w);
    out.uv = in.uv;

    // Per-vertex scalar attribute, guarded by has_attribute + buffer length.
    // Detect NaN before interpolation can corrupt the bit pattern. Mirrors
    // mesh.wgsl::vs_main.
    let buf_len = arrayLength(&scalar_buffer);
    let idx = in.vertex_index;
    let has_attr = object.has_attribute != 0u && buf_len > 0u;
    let safe_idx = min(idx, select(0u, buf_len - 1u, buf_len > 0u));
    let raw_scalar = scalar_buffer[safe_idx];
    out.scalar_val = select(0.0, raw_scalar, has_attr);
    let sv_bits = bitcast<u32>(raw_scalar);
    let sv_is_nan = has_attr
        && (sv_bits & 0x7F800000u) == 0x7F800000u
        && (sv_bits & 0x007FFFFFu) != 0u;
    out.is_nan_scalar = select(0.0, 1.0, sv_is_nan);

    // Per-face RGBA colour (FaceColour attribute kind). Indexed by
    // vertex_index which equals the sequential draw invocation counter for
    // non-indexed face draws.
    let fc_len = arrayLength(&face_colour_buffer);
    let fc_idx = min(idx, select(0u, fc_len - 1u, fc_len > 0u));
    out.face_colour = select(
        vec4<f32>(1.0),
        face_colour_buffer[fc_idx],
        object.use_face_colour != 0u && fc_len > 0u,
    );

    return out;
}