viewport-lib 0.14.0

3D viewport rendering library
Documentation
// Projected tetrahedra shader for transparent unstructured volume rendering.
//
// Each tetrahedron is rendered as a screen-space AABB bounding quad (6 vertices,
// 2 triangles). The vertex shader reads tet data from a storage buffer and computes
// the screen-space AABB. The fragment shader performs a ray-tet half-space intersection,
// computes Beer-Lambert thickness opacity, and writes weighted-blended OIT output
// (same targets as mesh_oit.wgsl).
//
// Group 0: Camera uniform (shared camera_bind_group_layout, only bindings 0 and 4 used).
// Group 1: PT uniforms + tet storage buffer + colourmap LUT + sampler.
//
// Opaque-surface clipping: the OIT render pass loads the opaque depth buffer and
// uses LessEqual depth compare without depth writes, so the hardware depth test
// clips bounding-quad fragments behind already-rendered opaque geometry.

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

// Section-view clip planes (matches mesh.wgsl ClipPlanes, binding 4 of camera group).
struct ClipPlanes {
    planes: array<vec4<f32>, 6>,
    count:              u32,
    _pad0:              u32,
    viewport_width:     f32,
    viewport_height:    f32,
};

struct PtUniforms {
    density:       f32,
    scalar_min:    f32,
    scalar_max:    f32,
    threshold_min: f32,
    threshold_max: f32,
    _pad:          f32,
};

// One tetrahedron on the GPU: four vec4 slots (64 bytes, 16-byte aligned).
// v0.xyz = world position of vertex 0,  v0.w = per-tet scalar value.
// v1..v3: world positions (w unused).
struct GpuTet {
    v0: vec4<f32>,
    v1: vec4<f32>,
    v2: vec4<f32>,
    v3: vec4<f32>,
};

struct OitOut {
    @location(0) accum:  vec4<f32>,  // Rgba16Float accumulation buffer
    @location(1) reveal: f32,         // R8Unorm reveal (transmittance) buffer
};

// Group 0: shared with all other passes.
@group(0) @binding(0) var<uniform> camera:      Camera;
@group(0) @binding(4) var<uniform> clip_planes: ClipPlanes;

// Group 1: projected-tet specific.
@group(1) @binding(0) var<uniform>        uniforms:         PtUniforms;
@group(1) @binding(1) var<storage, read>  tets:             array<GpuTet>;
@group(1) @binding(2) var                 colourmap_lut:     texture_2d<f32>;
@group(1) @binding(3) var                 colourmap_sampler: sampler;

struct VsOut {
    @builtin(position)              clip_pos: vec4<f32>,
    // Flat (non-interpolated) tet vertex world positions and scalar.
    @location(0) @interpolate(flat) v0:       vec4<f32>,   // xyz=pos, w=scalar
    @location(1) @interpolate(flat) v1:       vec3<f32>,
    @location(2) @interpolate(flat) v2:       vec3<f32>,
    @location(3) @interpolate(flat) v3:       vec3<f32>,
    // Interpolated NDC XY for ray reconstruction.
    @location(4)                    ndc_xy:   vec2<f32>,
};

// Map vertex_index in [0,5] to one of the 4 AABB corners (two-triangle quad).
// Corner order: 0=min_x/min_y, 1=max_x/min_y, 2=max_x/max_y, 3=min_x/max_y.
fn corner_select(vi: u32) -> vec2<u32> {
    // triangle 0: corners 0,1,3  triangle 1: corners 0,3,2
    // Splits along BL->TR diagonal so both triangles cover the full AABB rectangle.
    let idx = array<u32, 6>(0u, 1u, 3u, 0u, 3u, 2u);
    let c = idx[vi];
    return vec2<u32>(c & 1u, (c >> 1u) & 1u);  // (x_hi, y_hi)
}

@vertex
fn vs_main(
    @builtin(vertex_index)   vi: u32,
    @builtin(instance_index) ti: u32,
) -> VsOut {
    let tet = tets[ti];

    // Project all 4 world-space vertices to clip space, then NDC.
    let c0 = camera.view_proj * vec4<f32>(tet.v0.xyz, 1.0);
    let c1 = camera.view_proj * vec4<f32>(tet.v1.xyz, 1.0);
    let c2 = camera.view_proj * vec4<f32>(tet.v2.xyz, 1.0);
    let c3 = camera.view_proj * vec4<f32>(tet.v3.xyz, 1.0);

    // Perspective divide to NDC. Clamp w to avoid division by zero.
    let eps = 1e-6;
    let w0 = select(c0.w, eps, abs(c0.w) < eps);
    let w1 = select(c1.w, eps, abs(c1.w) < eps);
    let w2 = select(c2.w, eps, abs(c2.w) < eps);
    let w3 = select(c3.w, eps, abs(c3.w) < eps);
    let n0 = c0.xyz / w0;
    let n1 = c1.xyz / w1;
    let n2 = c2.xyz / w2;
    let n3 = c3.xyz / w3;

    // Screen-space AABB of the projected XY positions.
    let min_xy = min(min(n0.xy, n1.xy), min(n2.xy, n3.xy));
    let max_xy = max(max(n0.xy, n1.xy), max(n2.xy, n3.xy));

    // Clamp AABB to clip space [-1, 1] to avoid emitting huge quads
    // for tets that straddle the near plane.
    let clamped_min = max(min_xy, vec2<f32>(-1.0));
    let clamped_max = min(max_xy, vec2<f32>( 1.0));

    // Use the minimum NDC z as the quad depth (closest point = passes depth test).
    let quad_z = min(min(n0.z, n1.z), min(n2.z, n3.z));

    // Select AABB corner from vertex_index.
    let corner = corner_select(vi % 6u);
    let ndc_x = select(clamped_min.x, clamped_max.x, corner.x != 0u);
    let ndc_y = select(clamped_min.y, clamped_max.y, corner.y != 0u);

    var out: VsOut;
    out.clip_pos = vec4<f32>(ndc_x, ndc_y, clamp(quad_z, 0.0, 1.0), 1.0);
    out.v0       = tet.v0;
    out.v1       = tet.v1.xyz;
    out.v2       = tet.v2.xyz;
    out.v3       = tet.v3.xyz;
    out.ndc_xy   = vec2<f32>(ndc_x, ndc_y);
    return out;
}

// ---------------------------------------------------------------------------
// Fragment shader
// ---------------------------------------------------------------------------

// Intersect a ray (eye + t*dir) with one half-space of a tetrahedron face.
// face_a, face_b, face_c: the three face vertices.
// opposite: the tet vertex NOT on this face (used to determine outward direction).
// Updates t_enter / t_exit so the ray segment stays inside the tet.
fn update_slab(
    face_a: vec3<f32>, face_b: vec3<f32>, face_c: vec3<f32>,
    opposite: vec3<f32>,
    eye: vec3<f32>, ray_dir: vec3<f32>,
    t_enter: ptr<function, f32>,
    t_exit:  ptr<function, f32>,
) {
    // Compute face normal from edge cross product.
    var n = cross(face_b - face_a, face_c - face_a);
    // Ensure the normal points OUTWARD (away from the opposite vertex).
    // If dot(n, opposite - face_a) > 0, n is pointing toward opposite (inward),
    // so flip it.
    if dot(n, opposite - face_a) > 0.0 {
        n = -n;
    }
    let denom = dot(n, ray_dir);
    if abs(denom) < 1e-10 {
        return;  // ray parallel to this face
    }
    // t where ray meets this face plane: eye + t*dir on the plane => dot(n, p-face_a)=0
    let t = dot(n, face_a - eye) / denom;
    if denom < 0.0 {
        // Ray moving toward the outside (denom<0 with outward n means ray entering).
        *t_enter = max(*t_enter, t);
    } else {
        // Ray moving outward: exiting this half-space.
        *t_exit = min(*t_exit, t);
    }
}

@fragment
fn fs_main(in: VsOut) -> OitOut {
    // Reconstruct world-space ray direction from NDC position.
    let ndc_near = vec4<f32>(in.ndc_xy, 0.0, 1.0);
    let ndc_far  = vec4<f32>(in.ndc_xy, 1.0, 1.0);
    let world_near_h = camera.inv_view_proj * ndc_near;
    let world_far_h  = camera.inv_view_proj * ndc_far;
    let world_near = world_near_h.xyz / world_near_h.w;
    let world_far  = world_far_h.xyz  / world_far_h.w;
    let ray_dir = normalize(world_far - world_near);

    let eye = camera.eye_pos;

    let p0 = in.v0.xyz;
    let p1 = in.v1;
    let p2 = in.v2;
    let p3 = in.v3;

    // Slab test: intersect ray with all 4 tet half-spaces.
    // update_slab uses the opposite vertex to guarantee an outward-pointing normal,
    // so the result is correct regardless of tet handedness.
    var t_enter: f32 = 0.0;
    var t_exit:  f32 = 1e30;

    update_slab(p1, p2, p3, p0, eye, ray_dir, &t_enter, &t_exit);
    update_slab(p0, p2, p3, p1, eye, ray_dir, &t_enter, &t_exit);
    update_slab(p0, p1, p3, p2, eye, ray_dir, &t_enter, &t_exit);
    update_slab(p0, p1, p2, p3, eye, ray_dir, &t_enter, &t_exit);

    // Section-view clip planes: constrain [t_enter, t_exit] to the kept half-spaces.
    for (var i = 0u; i < clip_planes.count; i++) {
        let plane = clip_planes.planes[i];  // (nx, ny, nz, d); kept when dot(p,n)+d >= 0
        let n_plane = plane.xyz;
        let bias = dot(n_plane, eye) + plane.w;  // signed distance of eye from plane
        let denom = dot(n_plane, ray_dir);
        if abs(denom) < 1e-10 {
            // Ray parallel to plane. Discard if eye is on the clipped side.
            if bias < 0.0 {
                discard;
            }
        } else {
            let t_plane = -bias / denom;
            if denom > 0.0 {
                // Ray moves into the kept half-space at t_plane.
                t_enter = max(t_enter, t_plane);
            } else {
                // Ray exits the kept half-space at t_plane.
                t_exit = min(t_exit, t_plane);
            }
        }
    }

    // Face bias: treat rays that miss the tet by up to FACE_BIAS world units as
    // grazing hits. This fills sub-pixel seam gaps at tet boundaries without
    // moving the bounding quad (which causes wobble when the AABB shifts
    // frame-to-frame during camera motion). The resulting alpha for a bare
    // graze is ~density * FACE_BIAS ~= 5e-4, essentially invisible but stable.
    const FACE_BIAS: f32 = 1e-3;
    if t_enter >= t_exit + FACE_BIAS {
        discard;
    }
    let thickness = max(t_exit - t_enter, 0.0);

    // Beer-Lambert opacity.
    let alpha = 1.0 - exp(-uniforms.density * thickness);

    // Map scalar to [0,1] and sample colourmap LUT.
    let scalar = in.v0.w;

    // Threshold: discard tets outside [threshold_min, threshold_max].
    if scalar < uniforms.threshold_min || scalar > uniforms.threshold_max {
        discard;
    }
    let t_scalar = clamp(
        (scalar - uniforms.scalar_min) / max(uniforms.scalar_max - uniforms.scalar_min, 1e-10),
        0.0, 1.0,
    );
    let colour = textureSample(colourmap_lut, colourmap_sampler, vec2<f32>(t_scalar, 0.5));

    // Weighted-blended OIT (same formula as mesh_oit.wgsl).
    let depth = in.clip_pos.z;
    let w = alpha * max(1e-2, 1.0 - depth);
    var out: OitOut;
    out.accum  = vec4<f32>(colour.rgb * alpha * w, alpha * w);
    out.reveal = alpha;
    return out;
}