vtk-pure-rs 0.2.0

Pure Rust visualization toolkit — data structures, filters, I/O, rendering
Documentation
struct SsaoUniforms {
    projection: mat4x4<f32>,
    inv_projection: mat4x4<f32>,
    radius: f32,
    bias: f32,
    intensity: f32,
    num_samples: f32,
    texel_size: vec2<f32>,
    near: f32,
    far: f32,
    samples: array<vec4<f32>, 32>,
};

@group(0) @binding(0)
var<uniform> uniforms: SsaoUniforms;
@group(0) @binding(1)
var depth_tex: texture_depth_2d;
@group(0) @binding(2)
var depth_sampler: sampler;
@group(0) @binding(3)
var ao_tex: texture_2d<f32>;

struct VertexOutput {
    @builtin(position) position: vec4<f32>,
    @location(0) uv: vec2<f32>,
};

@vertex
fn vs_fullscreen(@builtin(vertex_index) idx: u32) -> VertexOutput {
    var out: VertexOutput;
    let x = f32(i32(idx & 1u)) * 4.0 - 1.0;
    let y = f32(i32(idx >> 1u)) * 4.0 - 1.0;
    out.position = vec4<f32>(x, y, 0.0, 1.0);
    out.uv = vec2<f32>((x + 1.0) * 0.5, (1.0 - y) * 0.5);
    return out;
}

// Reconstruct view-space position from depth and UV
fn view_pos_from_depth(uv: vec2<f32>, depth: f32) -> vec3<f32> {
    let ndc = vec4<f32>(uv * 2.0 - 1.0, depth, 1.0);
    let view = uniforms.inv_projection * ndc;
    return view.xyz / view.w;
}

// Pseudo-random hash for noise (avoid banding)
fn hash(p: vec2<f32>) -> f32 {
    let h = dot(p, vec2<f32>(127.1, 311.7));
    return fract(sin(h) * 43758.5453);
}

// SSAO computation
@fragment
fn fs_ssao(in: VertexOutput) -> @location(0) f32 {
    let depth = textureSample(depth_tex, depth_sampler, in.uv);
    if depth >= 1.0 {
        return 1.0; // background, no occlusion
    }

    let view_pos = view_pos_from_depth(in.uv, depth);

    // Approximate normal from depth derivatives
    let px = view_pos_from_depth(in.uv + vec2<f32>(uniforms.texel_size.x, 0.0),
        textureSample(depth_tex, depth_sampler, in.uv + vec2<f32>(uniforms.texel_size.x, 0.0)));
    let py = view_pos_from_depth(in.uv + vec2<f32>(0.0, uniforms.texel_size.y),
        textureSample(depth_tex, depth_sampler, in.uv + vec2<f32>(0.0, uniforms.texel_size.y)));
    let normal = normalize(cross(px - view_pos, py - view_pos));

    // Random rotation per pixel
    let noise_angle = hash(in.uv * 1000.0) * 6.2831853;
    let cs = cos(noise_angle);
    let sn = sin(noise_angle);

    var occlusion = 0.0;
    let ns = u32(uniforms.num_samples);

    for (var i = 0u; i < ns && i < 32u; i = i + 1u) {
        var s = uniforms.samples[i].xyz;
        // Random rotation around normal
        let rotated = vec3<f32>(
            s.x * cs - s.y * sn,
            s.x * sn + s.y * cs,
            s.z,
        );
        // Align sample to surface normal (hemisphere)
        var sample_vec = rotated;
        if dot(sample_vec, normal) < 0.0 {
            sample_vec = -sample_vec;
        }

        let sample_pos = view_pos + sample_vec * uniforms.radius;

        // Project sample to screen space
        let projected = uniforms.projection * vec4<f32>(sample_pos, 1.0);
        let sample_uv = vec2<f32>(
            projected.x / projected.w * 0.5 + 0.5,
            -projected.y / projected.w * 0.5 + 0.5,
        );

        // Sample depth at projected position
        let sample_depth = textureSample(depth_tex, depth_sampler, sample_uv);
        let sample_view = view_pos_from_depth(sample_uv, sample_depth);

        // Range check + occlusion test
        let range_check = smoothstep(0.0, 1.0, uniforms.radius / abs(view_pos.z - sample_view.z));
        if sample_view.z >= sample_pos.z + uniforms.bias {
            occlusion += range_check;
        }
    }

    occlusion = 1.0 - (occlusion / f32(ns)) * uniforms.intensity;
    return clamp(occlusion, 0.0, 1.0);
}

// Bilateral blur (4x4, depth-aware)
@fragment
fn fs_blur(in: VertexOutput) -> @location(0) f32 {
    let center_depth = textureSample(depth_tex, depth_sampler, in.uv);
    var result = 0.0;
    var total_weight = 0.0;

    for (var y = -2; y <= 2; y = y + 1) {
        for (var x = -2; x <= 2; x = x + 1) {
            let offset = vec2<f32>(f32(x), f32(y)) * uniforms.texel_size;
            let sample_uv = in.uv + offset;
            let ao = textureSample(ao_tex, depth_sampler, sample_uv).r;
            let d = textureSample(depth_tex, depth_sampler, sample_uv);
            // Depth-aware weight: reject samples at different depths
            let depth_diff = abs(d - center_depth);
            let weight = exp(-depth_diff * 1000.0);
            result += ao * weight;
            total_weight += weight;
        }
    }

    return result / max(total_weight, 0.001);
}

// Multiply AO onto color (blend state handles multiplication)
@fragment
fn fs_composite(in: VertexOutput) -> @location(0) vec4<f32> {
    let ao = textureSample(ao_tex, depth_sampler, in.uv).r;
    return vec4<f32>(ao, ao, ao, 1.0);
}