viewport-lib 0.19.0

3D viewport rendering library
Documentation
// Refractive distortion pass for `ScatterVolume`.
//
// Each refractive volume renders one instanced screen-space quad whose
// vertex shader projects the volume's world bounding box and emits a
// 2-triangle rectangle covering its on-screen footprint. The fragment
// shader samples the previously copied scene colour at a UV offset derived
// from the local density gradient and writes the distorted result back to
// the HDR target with replace blend, so the scatter pass that follows
// integrates absorption and in-scattering on top of the shimmered scene.

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 GpuRefractionVolume {
    shape_pack: vec4<u32>,
    p0:         vec4<f32>,
    p1:         vec4<f32>,
    params:     vec4<f32>, // strength, threshold, noise_scale, time
};

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

@group(2) @binding(0) var scene_src:     texture_2d<f32>;
@group(2) @binding(1) var scene_sampler: sampler;
@group(2) @binding(2) var opaque_depth:  texture_depth_2d;

struct VsOut {
    @builtin(position) clip_pos: vec4<f32>,
    @location(0)       ndc_xy:   vec2<f32>,
};

fn hash31(p: vec3<f32>) -> f32 {
    let h = dot(p, vec3<f32>(127.1, 311.7, 74.7));
    return fract(sin(h) * 43758.5453123);
}

fn value_noise_3d(p: vec3<f32>) -> f32 {
    let pi = floor(p);
    let pf = fract(p);
    let w = pf * pf * (3.0 - 2.0 * pf);
    let n000 = hash31(pi);
    let n100 = hash31(pi + vec3<f32>(1.0, 0.0, 0.0));
    let n010 = hash31(pi + vec3<f32>(0.0, 1.0, 0.0));
    let n110 = hash31(pi + vec3<f32>(1.0, 1.0, 0.0));
    let n001 = hash31(pi + vec3<f32>(0.0, 0.0, 1.0));
    let n101 = hash31(pi + vec3<f32>(1.0, 0.0, 1.0));
    let n011 = hash31(pi + vec3<f32>(0.0, 1.0, 1.0));
    let n111 = hash31(pi + vec3<f32>(1.0, 1.0, 1.0));
    let nx00 = mix(n000, n100, w.x);
    let nx10 = mix(n010, n110, w.x);
    let nx01 = mix(n001, n101, w.x);
    let nx11 = mix(n011, n111, w.x);
    let nxy0 = mix(nx00, nx10, w.y);
    let nxy1 = mix(nx01, nx11, w.y);
    return mix(nxy0, nxy1, w.z);
}

fn project(p: vec3<f32>) -> vec4<f32> {
    return camera.view_proj * vec4<f32>(p, 1.0);
}

@vertex
fn vs_main(@builtin(vertex_index) vi: u32) -> VsOut {
    var bmin: vec3<f32>;
    var bmax: vec3<f32>;
    if vol.shape_pack.x == 0u {
        bmin = vol.p0.xyz;
        bmax = vol.p1.xyz;
    } else {
        let r = vol.p0.w;
        bmin = vol.p0.xyz - vec3<f32>(r);
        bmax = vol.p0.xyz + vec3<f32>(r);
    }
    var corners: array<vec3<f32>, 8>;
    corners[0] = vec3<f32>(bmin.x, bmin.y, bmin.z);
    corners[1] = vec3<f32>(bmax.x, bmin.y, bmin.z);
    corners[2] = vec3<f32>(bmin.x, bmax.y, bmin.z);
    corners[3] = vec3<f32>(bmax.x, bmax.y, bmin.z);
    corners[4] = vec3<f32>(bmin.x, bmin.y, bmax.z);
    corners[5] = vec3<f32>(bmax.x, bmin.y, bmax.z);
    corners[6] = vec3<f32>(bmin.x, bmax.y, bmax.z);
    corners[7] = vec3<f32>(bmax.x, bmax.y, bmax.z);

    var ss_min = vec2<f32>(1.0, 1.0);
    var ss_max = vec2<f32>(-1.0, -1.0);
    var any_behind = false;
    for (var i = 0u; i < 8u; i = i + 1u) {
        let c = project(corners[i]);
        if c.w <= 1e-4 {
            any_behind = true;
        } else {
            let nx = c.x / c.w;
            let ny = c.y / c.w;
            ss_min = min(ss_min, vec2<f32>(nx, ny));
            ss_max = max(ss_max, vec2<f32>(nx, ny));
        }
    }
    if any_behind {
        ss_min = vec2<f32>(-1.0, -1.0);
        ss_max = vec2<f32>(1.0, 1.0);
    } else {
        ss_min = clamp(ss_min, vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, 1.0));
        ss_max = clamp(ss_max, vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, 1.0));
    }

    var p: vec2<f32>;
    if vi == 0u      { p = vec2<f32>(ss_min.x, ss_min.y); }
    else if vi == 1u { p = vec2<f32>(ss_max.x, ss_min.y); }
    else if vi == 2u { p = vec2<f32>(ss_min.x, ss_max.y); }
    else if vi == 3u { p = vec2<f32>(ss_max.x, ss_min.y); }
    else if vi == 4u { p = vec2<f32>(ss_max.x, ss_max.y); }
    else             { p = vec2<f32>(ss_min.x, ss_max.y); }

    var out: VsOut;
    out.clip_pos = vec4<f32>(p, 0.0, 1.0);
    out.ndc_xy = p;
    return out;
}

fn ray_box(p0: vec3<f32>, p1: vec3<f32>, o: vec3<f32>, d: vec3<f32>) -> vec2<f32> {
    let inv = 1.0 / d;
    let t0 = (p0 - o) * inv;
    let t1 = (p1 - o) * inv;
    let tmin = min(t0, t1);
    let tmax = max(t0, t1);
    let t_enter = max(max(tmin.x, tmin.y), tmin.z);
    let t_exit  = min(min(tmax.x, tmax.y), tmax.z);
    return vec2<f32>(t_enter, t_exit);
}

fn ray_sphere(c: vec3<f32>, r: f32, o: vec3<f32>, d: vec3<f32>) -> vec2<f32> {
    let oc = o - c;
    let a = dot(d, d);
    let b = 2.0 * dot(oc, d);
    let cc = dot(oc, oc) - r * r;
    let disc = b * b - 4.0 * a * cc;
    if disc < 0.0 { return vec2<f32>(1.0, 0.0); }
    let sq = sqrt(disc);
    let t0 = (-b - sq) / (2.0 * a);
    let t1 = (-b + sq) / (2.0 * a);
    return vec2<f32>(t0, t1);
}

fn opaque_distance(ndc_xy: vec2<f32>, depth: f32, ray_dir: vec3<f32>) -> f32 {
    if depth >= 1.0 {
        return 1e30;
    }
    let ndc = vec4<f32>(ndc_xy, depth, 1.0);
    let world_h = camera.inv_view_proj * ndc;
    let world_p = world_h.xyz / world_h.w;
    return dot(world_p - camera.eye_pos, ray_dir);
}

@fragment
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
    let ndc_xy = in.ndc_xy;
    let near_h = camera.inv_view_proj * vec4<f32>(ndc_xy, 0.0, 1.0);
    let far_h  = camera.inv_view_proj * vec4<f32>(ndc_xy, 1.0, 1.0);
    let near_p = near_h.xyz / near_h.w;
    let far_p  = far_h.xyz  / far_h.w;
    let ray_dir = normalize(far_p - near_p);
    let eye = camera.eye_pos;

    let dims = textureDimensions(opaque_depth, 0);
    let uv = vec2<f32>(ndc_xy.x * 0.5 + 0.5, 1.0 - (ndc_xy.y * 0.5 + 0.5));
    let coord = vec2<i32>(
        clamp(i32(uv.x * f32(dims.x)), 0, i32(dims.x) - 1),
        clamp(i32(uv.y * f32(dims.y)), 0, i32(dims.y) - 1),
    );
    let depth = textureLoad(opaque_depth, coord, 0);
    let t_opaque = opaque_distance(ndc_xy, depth, ray_dir);

    var hit: vec2<f32>;
    if vol.shape_pack.x == 0u {
        hit = ray_box(vol.p0.xyz, vol.p1.xyz, eye, ray_dir);
    } else {
        hit = ray_sphere(vol.p0.xyz, vol.p0.w, eye, ray_dir);
    }
    let t_enter = max(hit.x, 0.0);
    let t_exit  = min(hit.y, t_opaque);
    if t_enter >= t_exit { discard; }

    let strength    = vol.params.x;
    let threshold   = vol.params.y;
    let noise_scale = max(vol.params.z, 1e-4);
    let time        = vol.params.w;

    // Sample the noise field near the volume centre along the ray. The
    // density gradient there sets the per-pixel distortion vector and the
    // scalar density gates the threshold check.
    let t_mid = mix(t_enter, t_exit, 0.5);
    let p     = eye + ray_dir * t_mid;
    let warp  = vec3<f32>(
        sin(time * 0.71),
        cos(time * 0.83),
        sin(time * 0.59 + 1.7),
    );
    let q   = p * noise_scale + warp;
    let eps = 0.35;
    let n_x0 = value_noise_3d(q - vec3<f32>(eps, 0.0, 0.0));
    let n_x1 = value_noise_3d(q + vec3<f32>(eps, 0.0, 0.0));
    let n_y0 = value_noise_3d(q - vec3<f32>(0.0, eps, 0.0));
    let n_y1 = value_noise_3d(q + vec3<f32>(0.0, eps, 0.0));
    let n_z0 = value_noise_3d(q - vec3<f32>(0.0, 0.0, eps));
    let n_z1 = value_noise_3d(q + vec3<f32>(0.0, 0.0, eps));
    let grad = vec3<f32>(n_x1 - n_x0, n_y1 - n_y0, n_z1 - n_z0);
    let mid_density = (n_x0 + n_x1 + n_y0 + n_y1 + n_z0 + n_z1) / 6.0;
    if mid_density < threshold { discard; }

    // Project the world gradient into view space to drive a 2D screen-space
    // offset. Flip Y so screen-up matches the texture-up convention.
    let g_view = (camera.view * vec4<f32>(grad, 0.0)).xyz;
    let offset = vec2<f32>(g_view.x, -g_view.y) * strength;
    let sample_uv = clamp(uv + offset, vec2<f32>(0.0), vec2<f32>(1.0));
    let sampled = textureSampleLevel(scene_src, scene_sampler, sample_uv, 0.0).rgb;
    return vec4<f32>(sampled, 1.0);
}