viewport-lib 0.2.1

3D viewport rendering library
Documentation
// Outline shader for two-pass stencil selection effect.
//
// Pass 1 (stencil_write_pipeline): renders the object normally; writes stencil=1
//   wherever the depth test passes. Uses the standard mesh pipeline for this.
//
// Pass 2 (outline_pipeline): renders a slightly expanded version of the object;
//   stencil test = NotEqual(1) so only the "ring" outside the original silhouette
//   is drawn. Depth compare = Always so the ring always renders on top.
//
// Group 0: Camera bind group (same layout as mesh.wgsl, bindings 0–7).
// Group 1: OutlineUniform (model matrix, outline color, pixel_offset).

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 OutlineUniform {
    model: mat4x4<f32>,
    color: vec4<f32>,
    pixel_offset: f32,
    _pad0: f32,
    _pad1: f32,
    _pad2: f32,
};

struct ClipVolumeUB {
    volume_type: u32,
    _pad0: u32, _pad1: u32, _pad2: u32,
    plane_normal: vec3<f32>,
    plane_dist: f32,
    box_center: vec3<f32>,
    _padB0: f32,
    box_half_extents: vec3<f32>,
    _padB1: f32,
    box_col0: vec3<f32>,
    _padB2: f32,
    box_col1: vec3<f32>,
    _padB3: f32,
    box_col2: vec3<f32>,
    _padB4: f32,
    sphere_center: vec3<f32>,
    sphere_radius: f32,
};

@group(0) @binding(0) var<uniform> camera: Camera;
@group(0) @binding(4) var<uniform> clip_planes: ClipPlanes;
@group(0) @binding(6) var<uniform> clip_volume: ClipVolumeUB;
@group(1) @binding(0) var<uniform> outline: OutlineUniform;

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

@vertex
fn vs_main(in: VertexIn) -> @builtin(position) vec4<f32> {
    let world_pos = outline.model * vec4<f32>(in.position, 1.0);
    let clip_pos  = camera.view_proj * world_pos;

    // Perform perspective divide to get NDC position.
    let ndc = clip_pos.xy / clip_pos.w;

    // Compute the object center in NDC for a stable expansion direction.
    let center_world = outline.model * vec4<f32>(0.0, 0.0, 0.0, 1.0);
    let center_clip  = camera.view_proj * center_world;
    let center_ndc   = center_clip.xy / center_clip.w;

    // Direction from object center to this vertex in NDC (screen space).
    let dir = ndc - center_ndc;
    let len = length(dir);

    // Expand outward by pixel_offset pixels.
    // One pixel in NDC = 2.0 / viewport_dimension.
    let vp_w = max(clip_planes.viewport_width, 1.0);
    let vp_h = max(clip_planes.viewport_height, 1.0);
    let px = vec2<f32>(2.0 / vp_w, 2.0 / vp_h);

    // If the vertex is at the center (degenerate), fall back to no expansion.
    var offset_ndc = vec2<f32>(0.0);
    if len > 0.0001 {
        let norm_dir = dir / len;
        offset_ndc = norm_dir * outline.pixel_offset * px;
    }

    // Apply the offset in clip space (multiply back by w).
    return vec4<f32>(
        clip_pos.x + offset_ndc.x * clip_pos.w,
        clip_pos.y + offset_ndc.y * clip_pos.w,
        clip_pos.z,
        clip_pos.w,
    );
}

@fragment
fn fs_main() -> @location(0) vec4<f32> {
    return outline.color;
}