viewport-lib 0.14.0

3D viewport rendering library
Documentation
// volume_outline_mask.wgsl : renders the visible silhouette of a ray-marched
// volume into the R8 outline mask texture. Uses the same bind group layout as
// volume.wgsl so we can reuse the per-volume VolumeGpuData bind group directly.
//
// The fragment shader runs a simplified ray march: no colour, no lighting, just
// detects whether any visible voxel exists along the ray. Writes solid white
// where the volume is visible; discards otherwise.
//
// Group 0: Camera bind group (view-projection, eye position, clip planes/volumes).
// Group 1: Volume-specific bindings (same layout as volume.wgsl).
// Vertex input: unit cube [0,1]^3 positions (location 0).

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

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

struct VolumeUniform {
    model:           mat4x4<f32>,
    inv_model:       mat4x4<f32>,
    bbox_min:        vec3<f32>,
    step_size:       f32,
    bbox_max:        vec3<f32>,
    opacity_scale:   f32,
    scalar_min:      f32,
    scalar_max:      f32,
    threshold_min:   f32,
    threshold_max:   f32,
    enable_shading:  u32,
    num_clip_planes: u32,
    use_nan_colour:   u32,
    _pad0:           u32,
    nan_colour:       vec4<f32>,
    clip_planes:     array<vec4<f32>, 6>,
};

struct ClipVolumeEntry {
    volume_type: u32,
    _pad_a: u32,
    _pad_b: u32,
    _pad_c: u32,
    center: vec3<f32>,
    radius: f32,
    half_extents: vec3<f32>,
    _pad1: f32,
    col0: vec3<f32>,
    _pad2: f32,
    col1: vec3<f32>,
    _pad3: f32,
    col2: vec3<f32>,
    _pad4: f32,
}

struct ClipVolumeUB {
    count: u32,
    _pad_a: u32,
    _pad_b: u32,
    _pad_c: u32,
    volumes: array<ClipVolumeEntry, 4>,
};

@group(0) @binding(0) var<uniform> camera: Camera;
@group(0) @binding(4) var<uniform> clip_planes_ub: ClipPlanes;
@group(0) @binding(6) var<uniform> clip_volume: ClipVolumeUB;

@group(1) @binding(0) var<uniform> volume: VolumeUniform;
@group(1) @binding(1) var volume_tex: texture_3d<f32>;
@group(1) @binding(2) var volume_nearest_sampler: sampler;
// Bindings 3-5 (colour LUT, opacity LUT, LUT sampler) are declared to match the
// volume bind group layout but are not sampled in this shader.
@group(1) @binding(3) var colour_lut: texture_2d<f32>;
@group(1) @binding(4) var opacity_lut: texture_2d<f32>;
@group(1) @binding(5) var lut_sampler: sampler;

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

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

// Ray-AABB intersection in unit cube space [0,1]^3.
fn intersect_unit_box(ray_origin: vec3<f32>, inv_dir: vec3<f32>) -> vec2<f32> {
    let t0 = (vec3<f32>(0.0) - ray_origin) * inv_dir;
    let t1 = (vec3<f32>(1.0) - ray_origin) * inv_dir;
    let tmin = min(t0, t1);
    let tmax = max(t0, t1);
    let t_near = max(max(tmin.x, tmin.y), tmin.z);
    let t_far  = min(min(tmax.x, tmax.y), tmax.z);
    return vec2<f32>(t_near, t_far);
}

fn clip_ray_plane(
    ray_origin: vec3<f32>,
    ray_dir: vec3<f32>,
    plane_normal: vec3<f32>,
    plane_d: f32,
    t_near: f32,
    t_far: f32,
) -> vec2<f32> {
    let denom = dot(plane_normal, ray_dir);
    let num = -(dot(plane_normal, ray_origin) + plane_d);
    if abs(denom) < 1e-8 {
        if num < 0.0 {
            return vec2<f32>(t_far + 1.0, t_far);
        }
        return vec2<f32>(t_near, t_far);
    }
    let t = num / denom;
    if denom > 0.0 {
        return vec2<f32>(max(t_near, t), t_far);
    } else {
        return vec2<f32>(t_near, min(t_far, t));
    }
}

fn clip_ray_box(
    ray_origin: vec3<f32>,
    ray_dir: vec3<f32>,
    e: ClipVolumeEntry,
    t_near_in: f32,
    t_far_in: f32,
) -> vec2<f32> {
    let d = ray_origin - e.center;
    let local_origin = vec3<f32>(dot(d, e.col0), dot(d, e.col1), dot(d, e.col2));
    let local_dir = vec3<f32>(dot(ray_dir, e.col0), dot(ray_dir, e.col1), dot(ray_dir, e.col2));
    let he = e.half_extents;
    var t_near = t_near_in;
    var t_far  = t_far_in;
    for (var ax = 0u; ax < 3u; ax = ax + 1u) {
        var lo: f32; var la: f32;
        if ax == 0u { lo = local_origin.x; la = local_dir.x; }
        else if ax == 1u { lo = local_origin.y; la = local_dir.y; }
        else { lo = local_origin.z; la = local_dir.z; }
        var he_ax: f32;
        if ax == 0u { he_ax = he.x; }
        else if ax == 1u { he_ax = he.y; }
        else { he_ax = he.z; }
        if abs(la) < 1e-8 {
            if abs(lo) > he_ax { return vec2<f32>(t_far + 1.0, t_far); }
        } else {
            let inv = 1.0 / la;
            var t0 = (-he_ax - lo) * inv;
            var t1 = ( he_ax - lo) * inv;
            if t0 > t1 { let tmp = t0; t0 = t1; t1 = tmp; }
            t_near = max(t_near, t0);
            t_far  = min(t_far,  t1);
        }
    }
    return vec2<f32>(t_near, t_far);
}

fn clip_ray_sphere(
    ray_origin: vec3<f32>,
    ray_dir: vec3<f32>,
    e: ClipVolumeEntry,
    t_near_in: f32,
    t_far_in: f32,
) -> vec2<f32> {
    let oc = ray_origin - e.center;
    let b  = dot(oc, ray_dir);
    let c  = dot(oc, oc) - e.radius * e.radius;
    let discriminant = b * b - c;
    if discriminant < 0.0 {
        return vec2<f32>(t_far_in + 1.0, t_far_in);
    }
    let sqrt_d = sqrt(discriminant);
    let t0 = -b - sqrt_d;
    let t1 = -b + sqrt_d;
    return vec2<f32>(max(t_near_in, t0), min(t_far_in, t1));
}

fn clip_ray_cylinder(
    ray_origin: vec3<f32>,
    ray_dir: vec3<f32>,
    e: ClipVolumeEntry,
    t_near_in: f32,
    t_far_in: f32,
) -> vec2<f32> {
    let axis     = e.col0;
    let half_len = e.half_extents.x;
    let oc       = ray_origin - e.center;
    let d_dot_a  = dot(ray_dir, axis);
    let oc_dot_a = dot(oc, axis);
    let d_perp   = ray_dir - axis * d_dot_a;
    let oc_perp  = oc      - axis * oc_dot_a;
    let qa = dot(d_perp, d_perp);
    let qb = dot(oc_perp, d_perp);
    let qc = dot(oc_perp, oc_perp) - e.radius * e.radius;
    var t_near = t_near_in;
    var t_far  = t_far_in;
    if qa < 1e-12 {
        if qc > 0.0 { return vec2<f32>(t_far + 1.0, t_far); }
    } else {
        let discriminant = qb * qb - qa * qc;
        if discriminant < 0.0 { return vec2<f32>(t_far + 1.0, t_far); }
        let sqrt_d = sqrt(discriminant);
        let inv_a  = 1.0 / qa;
        t_near = max(t_near, (-qb - sqrt_d) * inv_a);
        t_far  = min(t_far,  (-qb + sqrt_d) * inv_a);
    }
    if abs(d_dot_a) < 1e-8 {
        if abs(oc_dot_a) > half_len { return vec2<f32>(t_far + 1.0, t_far); }
    } else {
        let inv_da  = 1.0 / d_dot_a;
        let t_cap0  = (-half_len - oc_dot_a) * inv_da;
        let t_cap1  = ( half_len - oc_dot_a) * inv_da;
        t_near = max(t_near, min(t_cap0, t_cap1));
        t_far  = min(t_far,  max(t_cap0, t_cap1));
    }
    return vec2<f32>(t_near, t_far);
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    let eye_model = (volume.inv_model * vec4<f32>(camera.eye_pos, 1.0)).xyz;
    let frag_model = (volume.inv_model * vec4<f32>(in.world_pos, 1.0)).xyz;
    let ray_dir = normalize(frag_model - eye_model);
    let inv_dir = 1.0 / ray_dir;

    var t_range = intersect_unit_box(eye_model, inv_dir);
    var t_near = max(t_range.x, 0.0);
    var t_far = t_range.y;

    // Clip against per-volume clip planes.
    let inv_model_t = transpose(volume.model);
    for (var i = 0u; i < volume.num_clip_planes; i = i + 1u) {
        let plane_world = volume.clip_planes[i];
        let n_world = plane_world.xyz;
        let d_world = plane_world.w;
        let n_model_raw = (inv_model_t * vec4<f32>(n_world, 0.0)).xyz;
        let n_model_len = length(n_model_raw);
        if n_model_len < 1e-8 { continue; }
        let n_model = n_model_raw / n_model_len;
        let p_on_plane_world = n_world * (-d_world);
        let p_on_plane_model = (volume.inv_model * vec4<f32>(p_on_plane_world, 1.0)).xyz;
        let d_model = -dot(n_model, p_on_plane_model);
        let clipped = clip_ray_plane(eye_model, ray_dir, n_model, d_model, t_near, t_far);
        t_near = clipped.x;
        t_far = clipped.y;
    }

    // Clip against scene clip volumes.
    if clip_volume.count > 0u {
        let eye_world = camera.eye_pos;
        let model3 = mat3x3<f32>(volume.model[0].xyz, volume.model[1].xyz, volume.model[2].xyz);
        let dir_world_raw = model3 * ray_dir;
        let dir_world_len = length(dir_world_raw);
        if dir_world_len > 1e-8 {
            let dir_world = dir_world_raw / dir_world_len;
            let scale = dir_world_len;
            var tw_near = t_near * scale;
            var tw_far  = t_far  * scale;
            for (var i = 0u; i < clip_volume.count; i = i + 1u) {
                let e = clip_volume.volumes[i];
                if e.volume_type == 2u {
                    let r = clip_ray_box(eye_world, dir_world, e, tw_near, tw_far);
                    tw_near = r.x; tw_far = r.y;
                } else if e.volume_type == 3u {
                    let r = clip_ray_sphere(eye_world, dir_world, e, tw_near, tw_far);
                    tw_near = r.x; tw_far = r.y;
                } else if e.volume_type == 4u {
                    let r = clip_ray_cylinder(eye_world, dir_world, e, tw_near, tw_far);
                    tw_near = r.x; tw_far = r.y;
                }
            }
            t_near = tw_near / scale;
            t_far  = tw_far  / scale;
        }
    }

    if t_near >= t_far {
        discard;
    }

    // Simplified ray march: detect the first visible voxel and write to the mask.
    // No colour, no lighting -- just a visibility test.
    let step = volume.step_size;
    let max_steps = 256u;
    var t = t_near;

    for (var i = 0u; i < max_steps; i = i + 1u) {
        if t >= t_far { break; }
        let sample_pos = eye_model + ray_dir * t;
        let raw_scalar = textureSampleLevel(volume_tex, volume_nearest_sampler, sample_pos, 0.0).r;

        // Skip NaN samples.
        if raw_scalar != raw_scalar {
            if volume.use_nan_colour != 0u {
                // NaN voxel with nan_colour enabled counts as visible.
                return vec4<f32>(1.0, 0.0, 0.0, 1.0);
            }
            t = t + step;
            continue;
        }

        // Threshold: skip samples outside the visible range.
        if raw_scalar < volume.threshold_min || raw_scalar > volume.threshold_max {
            t = t + step;
            continue;
        }

        // Sample opacity LUT to confirm the voxel is actually visible.
        let scalar_range = volume.scalar_max - volume.scalar_min;
        let inv_scalar_range = select(1.0 / scalar_range, 1.0, abs(scalar_range) < 1e-10);
        let normalized = clamp((raw_scalar - volume.scalar_min) * inv_scalar_range, 0.0, 1.0);
        let lut_coord = vec2<f32>(normalized, 0.5);
        let sample_opacity = textureSampleLevel(opacity_lut, lut_sampler, lut_coord, 0.0).r
                             * volume.opacity_scale;
        if sample_opacity > 0.001 {
            return vec4<f32>(1.0, 0.0, 0.0, 1.0);
        }

        t = t + step;
    }

    discard;
}