viewport-lib 0.14.0

3D viewport rendering library
Documentation
// Volume surface slice shader: samples a 3D volume at each fragment's world position.
//
// The slice surface is any mesh passed as a vertex + index buffer. The fragment shader
// converts the fragment's world position to volume UVW coordinates and samples the
// volume texture, discarding fragments that fall outside the volume bbox.
//
// Group 0: Camera uniform (same layout as all other shaders).
// Group 1: VolumeSurfaceSliceUniform + volume texture + nearest sampler
//          + colourmap LUT texture + linear sampler.

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 VolumeSurfaceSliceUniform {
    model:      mat4x4<f32>,  // offset   0, size 64
    bbox_min:   vec3<f32>,    // offset  64, size 12  (align 16, fits at 64)
    scalar_min: f32,           // offset  76, size  4
    bbox_max:   vec3<f32>,    // offset  80, size 12  (align 16, fits at 80)
    scalar_max: f32,           // offset  92, size  4
    opacity:    f32,           // offset  96, size  4
    // struct size = roundUp(100, 16) = 112 -- matches the Rust repr(C) layout
};

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

@group(1) @binding(0) var<uniform> slice_ub:    VolumeSurfaceSliceUniform;
@group(1) @binding(1) var          vol_tex:     texture_3d<f32>;
@group(1) @binding(2) var          vol_sampler: sampler;
@group(1) @binding(3) var          lut_tex:     texture_2d<f32>;
@group(1) @binding(4) var          lut_sampler: sampler;

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

@vertex
fn vs_main(@location(0) position: vec3<f32>) -> VertexOut {
    var out: VertexOut;
    let world_pos = (slice_ub.model * vec4<f32>(position, 1.0)).xyz;
    out.clip_pos  = camera.view_proj * vec4<f32>(world_pos, 1.0);
    out.world_pos = world_pos;
    return out;
}

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

    // Convert world position to volume UVW [0, 1]^3.
    let extent = slice_ub.bbox_max - slice_ub.bbox_min;
    let uvw = (in.world_pos - slice_ub.bbox_min) / max(extent, vec3<f32>(1e-6));

    // Discard fragments outside the volume bounding box.
    if any(uvw < vec3<f32>(0.0)) || any(uvw > vec3<f32>(1.0)) {
        discard;
    }

    // Load scalar from the 3D volume texture (R32Float is non-filterable).
    let dims   = vec3<f32>(textureDimensions(vol_tex));
    let texel  = vec3<i32>(clamp(uvw * dims, vec3<f32>(0.0), dims - vec3<f32>(1.0)));
    let scalar = textureLoad(vol_tex, texel, 0).r;

    // Normalize scalar and map through the LUT.
    let range = slice_ub.scalar_max - slice_ub.scalar_min;
    let t     = select(0.0, (scalar - slice_ub.scalar_min) / range, range > 0.0);
    let u     = clamp(t, 0.0, 1.0);
    var colour = textureSampleLevel(lut_tex, lut_sampler, vec2<f32>(u, 0.5), 0.0);
    colour.a   = colour.a * slice_ub.opacity;
    return colour;
}