Skip to main content

SSAO

Constant SSAO 

Source
pub const SSAO: &str = "// Screen-space ambient occlusion (GTAO): the horizon-search kernel and the\n// depth-aware blur that cleans up its noise. One fragment per compile, selected\n// by a define so each variant declares exactly the resources it binds (Metal\n// and DXIL indices are assigned in declaration order, so an unused declaration\n// would shift the live ones):\n//\n//   SSAO_KERNEL - horizon search over the G-buffer -> raw occlusion.\n//   SSAO_BLUR   - depth-aware 5x5 box blur of that raw occlusion.\n//\n// Both read the unified G-buffer pre-pass (rgb = unit view normal, a = linear\n// view depth) and write a single-channel occlusion target. Pairs with\n// `fullscreen_vertex` in fullscreen.slang.\n\n{POST_COMMON}\n\n#if defined(SSAO_KERNEL)\n\n// Layout matches `SsaoParams` in render_types.rs (16 B).\nstruct SsaoParams\n{\n    float radius;\n    float intensity;\n    float tan_half_fov_y;\n    float aspect;\n};\n\n[[vk::binding(0, 0)]] Sampler2D<float4> gbuffer;\n\n[[vk::push_constant]]\nConstantBuffer<SsaoParams> params;\n\nstatic const int   SSAO_SLICES  = 3;\nstatic const int   SSAO_STEPS   = 6;\nstatic const float SSAO_PI      = 3.14159265359;\nstatic const float SSAO_HALF_PI = 1.57079632679;\n// Cap on the kernel\'s UV footprint so geometry right in front of the camera\n// does not blow the search radius out to most of the screen.\nstatic const float SSAO_MAX_UV  = 0.2;\n\n// Rebuild a view-space position from a UV and its linear (view-space) depth.\nfloat3 ssao_view_pos(float2 uv, float depth, float tan_y, float aspect)\n{\n    float2 ndc = float2(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0);\n    return float3(ndc.x * tan_y * aspect, ndc.y * tan_y, -1.0) * depth;\n}\n\n[shader(\"fragment\")]\nfloat ssao_kernel_fragment(\n    [[vk::location(0)]] float2 uv : TEXCOORD0,\n    float4 pixel : SV_Position) : SV_Target\n{\n    float4 c = gbuffer.Sample(uv);\n    float depth = c.a;\n    if (depth <= 0.0)\n    {\n        return 1.0;                        // background - no geometry, fully lit\n    }\n\n    float3 n_vec = normalize(c.xyz);\n    float3 p = ssao_view_pos(uv, depth, params.tan_half_fov_y, params.aspect);\n    float3 v = normalize(-p);              // p is in view space; camera is origin\n\n    // UV-space radius of the world-space search radius at this depth. The\n    // viewport spans 2*tan_half_fov*depth view units vertically.\n    float radius_uv = params.radius / max(2.0 * params.tan_half_fov_y * depth, 1e-4);\n    radius_uv = min(radius_uv, SSAO_MAX_UV);\n\n    // Interleaved gradient noise: a per-pixel slice rotation + step jitter that\n    // trades banding for high-frequency noise the blur pass then cleans up.\n    float ign = frac(52.9829189 * frac(dot(pixel.xy, float2(0.06711056, 0.00583715))));\n\n    float visibility = 0.0;\n    for (int s = 0; s < SSAO_SLICES; s++)\n    {\n        float ang = (float(s) + ign) * (SSAO_PI / float(SSAO_SLICES));\n        float2 dir = float2(cos(ang), sin(ang));\n\n        // Slice plane: spanned by v and the screen direction lifted to view\n        // space. The projected surface normal and both horizons are measured\n        // inside this plane.\n        float3 dir_vs  = normalize(float3(dir, 0.0));\n        float3 plane_n = normalize(cross(dir_vs, v));\n        float3 proj_n  = n_vec - plane_n * dot(n_vec, plane_n);\n        float proj_len = length(proj_n);\n        if (proj_len < 1e-4)\n        {\n            continue;\n        }\n        float3 tangent = cross(plane_n, v);\n        float n = atan2(dot(proj_n, tangent), dot(proj_n, v));\n\n        // Horizon search: march both screen directions, keeping the widest\n        // horizon cosine, distance-attenuated so far occluders fade out.\n        float cos_plus  = -1.0;\n        float cos_minus = -1.0;\n        for (int step = 1; step <= SSAO_STEPS; step++)\n        {\n            float t = (float(step) - 0.5 + ign) / float(SSAO_STEPS);\n            float2 off = dir * radius_uv * t;\n\n            float2 uvp = uv + off;\n            float dp = gbuffer.Sample(uvp).a;\n            if (dp > 0.0)\n            {\n                float3 sp = ssao_view_pos(uvp, dp, params.tan_half_fov_y, params.aspect) - p;\n                float lp = length(sp);\n                float fo = saturate(1.0 - lp / max(params.radius, 1e-4));\n                cos_plus = lerp(cos_plus, max(cos_plus, dot(sp / max(lp, 1e-5), v)), fo);\n            }\n            float2 uvm = uv - off;\n            float dm = gbuffer.Sample(uvm).a;\n            if (dm > 0.0)\n            {\n                float3 sm = ssao_view_pos(uvm, dm, params.tan_half_fov_y, params.aspect) - p;\n                float lm = length(sm);\n                float fo = saturate(1.0 - lm / max(params.radius, 1e-4));\n                cos_minus = lerp(cos_minus, max(cos_minus, dot(sm / max(lm, 1e-5), v)), fo);\n            }\n        }\n\n        // Horizon angles, clamped into the hemisphere around the projected\n        // normal, then the GTAO cosine-weighted arc integral for the slice.\n        float h1 = -acos(clamp(cos_minus, -1.0, 1.0));\n        float h2 =  acos(clamp(cos_plus,  -1.0, 1.0));\n        h1 = n + max(h1 - n, -SSAO_HALF_PI);\n        h2 = n + min(h2 - n,  SSAO_HALF_PI);\n        float sin_n = sin(n);\n        float cos_n = cos(n);\n        float a1 = 0.25 * (-cos(2.0 * h1 - n) + cos_n + 2.0 * h1 * sin_n);\n        float a2 = 0.25 * (-cos(2.0 * h2 - n) + cos_n + 2.0 * h2 * sin_n);\n        visibility += proj_len * (a1 + a2);\n    }\n\n    visibility = saturate(visibility / float(SSAO_SLICES));\n    // `intensity` sharpens the contact darkening; 1.0 is the integrated amount.\n    return pow(visibility, max(params.intensity, 0.0));\n}\n\n#elif defined(SSAO_BLUR)\n\n[[vk::binding(0, 0)]] Sampler2D<float4> ao_raw;\n[[vk::binding(1, 0)]] Sampler2D<float4> gbuffer;\n\n// Depth-aware 5x5 box blur. Weighting each tap by view-depth similarity keeps\n// the noisy GTAO output from bleeding occlusion across silhouette edges.\n[shader(\"fragment\")]\nfloat ssao_blur_fragment([[vk::location(0)]] float2 uv : TEXCOORD0) : SV_Target\n{\n    float2 texel = 1.0 / combined_size(ao_raw);\n    float center_depth = gbuffer.Sample(uv).a;\n    if (center_depth <= 0.0)\n    {\n        return 1.0;\n    }\n    float sum = 0.0;\n    float wsum = 0.0;\n    for (int y = -2; y <= 2; y++)\n    {\n        for (int x = -2; x <= 2; x++)\n        {\n            float2 tap = uv + float2(float(x), float(y)) * texel;\n            float d = gbuffer.Sample(tap).a;\n            // Depth-similarity weight; background taps (d <= 0) drop out.\n            float w = (d > 0.0)\n                ? exp(-abs(d - center_depth) * 8.0 / max(center_depth, 1e-3))\n                : 0.0;\n            sum  += ao_raw.Sample(tap).r * w;\n            wsum += w;\n        }\n    }\n    return (wsum > 1e-4) ? (sum / wsum) : ao_raw.Sample(uv).r;\n}\n\n#else\n#error \"ssao.slang: define SSAO_KERNEL or SSAO_BLUR\"\n#endif\n";
Expand description

ssao.slang.