Skip to main content

SSR

Constant SSR 

Source
pub const SSR: &str = "// Screen-space reflection resolve: a fullscreen ray-march over the pre-pass\n// G-buffer that writes reflected radiance (.rgb) and the composite weight (.a)\n// the reflection blur + composite then blends over the scene. Single source for\n// every backend; pairs with `fullscreen_vertex` in fullscreen.slang.\n//\n// Compile with -DMAX_PROBES=<n> (the reflection-probe array length the device\n// binds). Vulkan gets set 0 bindings 0-3 plus the forward global set\'s probe\n// bindings (7 + 8) at set 1, which is how the hand-written GLSL bound them;\n// Metal gets texture(0..3) for the screen sources and the prefilter cube,\n// texture(4..4+MAX_PROBES) for the probe cubes, the push constant at buffer(0)\n// and the probe set at buffer(1). DXIL gets t0..t3 / s0..s3 for the screen\n// sources, b0 for the params and b1 for the probe set, and (under\n// SPLIT_PROBE_SAMPLER) the probe cube array at t4.. with its one sampler at s4.\n\n#ifndef MAX_PROBES\n#define MAX_PROBES 8\n#endif\n\n{POST_COMMON}\n\n// Layout matches `SsrParams` in render_types.rs (144 B).\nstruct SsrParams\n{\n    float intensity;\n    float max_distance;\n    float tan_half_fov_y;\n    float aspect;\n    float stride;\n    float thickness;\n    // IBL prefilter cubemap mip count; 0 means no EnvironmentMap is bound and\n    // the cube fallback is skipped (missed rays keep the base shading).\n    float prefilter_mip_count;\n    float _pad;\n    // Camera-to-world transform (the rigid inverse of the view matrix): its 3x3\n    // turns the view-space reflection ray into the world-space direction the\n    // cubemap is sampled with, and its translation column lets the resolve\n    // rebuild the world-space surface position a probe box-projects against.\n    float4x4 inv_view;\n    // Rows of the rotation from world space into the sky cube\'s baked frame;\n    // identity when the sky does not turn.\n    float4 sky_rot[3];\n};\n\n// A world direction in the sky cube\'s own frame; the sky fallback goes through\n// it so a resolved reflection matches the sky the main pass shows.\n#define SKY_DIR(d) float3(dot(params.sky_rot[0].xyz, (d)), \\\n                          dot(params.sky_rot[1].xyz, (d)), \\\n                          dot(params.sky_rot[2].xyz, (d)))\n\n{PROBE_TYPES}\n\n// The push constant leads so it lands on Metal\'s buffer(0), where every post\n// pass puts its params and where the host writes it.\n[[vk::push_constant]]\nConstantBuffer<SsrParams> params;\n\n[[vk::binding(0, 0)]] Sampler2D<float4> scene;\n[[vk::binding(1, 0)]] Sampler2D<float4> gbuffer;\n[[vk::binding(2, 0)]] Sampler2D<float4> rough_tex;\n[[vk::binding(3, 0)]] SamplerCube<float4> prefilter;\n\n// The forward global set, bound here only for its reflection-probe count +\n// per-probe parallax boxes + cube array: a screen-space ray that escapes the\n// frame falls back to the local probe capture instead of the foreign sky cube.\n[[vk::binding(7, 1)]] ConstantBuffer<ProbeSet> probe_set;\n#define PROBE_SET probe_set\n\n// The probe cube array, in whichever form its host can bind.\n//\n// A top-level combined `SamplerCube` array lowers to a texture array plus a\n// sampler array, and D3D12 binds a shader sampler array only through a\n// descriptor table -- a static sampler covers a single register, so twelve of\n// them still leave s4..s11 \"not fully bound\". SPLIT_PROBE_SAMPLER therefore\n// declares the array separated, with one sampler the root signature hands out\n// statically, the same shape the bindless main pass uses for the same array.\n// Metal takes an argument buffer, which is what keeps the array out of the\n// texture namespace slangc numbers by declaration order. Vulkan keeps the\n// combined form on the forward global set\'s binding 8, which is what it binds.\n#if defined(SPLIT_PROBE_SAMPLER)\nTextureCube<float4> probe_cubes[MAX_PROBES];\nSamplerState probe_cube_sampler;\n\nfloat3 probe_cube_sample_bias(uint i, float3 dir, float lod)\n{\n    return probe_cubes[i].SampleBias(probe_cube_sampler, dir, lod).rgb;\n}\n#elif defined(METAL_ABI)\n// Metal reaches the cubes through an argument buffer: a resource array at\n// global scope emits with no [[texture(n)]], and the compiler then places it\n// at whatever slot happens to be unused.\nstruct ProbeCubes\n{\n    TextureCube<float4> probe_cubes[MAX_PROBES];\n};\nParameterBlock<ProbeCubes> probe_cube_set : register(b11);\nSamplerState probe_cube_sampler;\n\nfloat3 probe_cube_sample_bias(uint i, float3 dir, float lod)\n{\n    return probe_cube_set.probe_cubes[i].SampleBias(probe_cube_sampler, dir, lod).rgb;\n}\n#else\n[[vk::binding(8, 1)]] SamplerCube<float4> probe_cubes[MAX_PROBES];\n\nfloat3 probe_cube_sample_bias(uint i, float3 dir, float lod)\n{\n    return probe_cubes[i].SampleBias(dir, lod).rgb;\n}\n#endif\n\nstatic const int   SSR_MAX_STEPS = 48;\nstatic const int   SSR_REFINE    = 5;\n// Surfaces rougher than REFLECTION_ROUGHNESS_CUT get no SSR; glossiness ramps\n// in below it. Locked to concinnity_core::gfx::ssr::REFLECTION_ROUGHNESS_CUT by\n// unit test so the SSR, RT, and composite passes can never disagree on it.\nstatic const float REFLECTION_ROUGHNESS_CUT = 0.6;\n// Dielectric base reflectance (water, glass, polished stone) for the Fresnel.\nstatic const float SSR_F0        = 0.04;\n// UV margin over which a hit near the screen border fades out.\nstatic const float SSR_EDGE_FADE = 0.12;\n\n// Rebuild a view-space position from a UV and its linear (view-space) depth.\n// The inverse of ssr_project; matches ssao_view_pos in the SSAO kernel.\nfloat3 ssr_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// Project a view-space point (z < 0, in front of the camera) to a screen UV.\nfloat2 ssr_project(float3 q, float tan_y, float aspect)\n{\n    float inv = 1.0 / max(-q.z, 1e-4);\n    float2 ndc = float2(q.x * inv / (tan_y * aspect), q.y * inv / tan_y);\n    return float2(ndc.x * 0.5 + 0.5, 1.0 - (ndc.y * 0.5 + 0.5));\n}\n\n{PROBE_COMMON}\n\n[shader(\"fragment\")]\nfloat4 ssr_resolve_fragment([[vk::location(0)]] float2 uv : TEXCOORD0) : SV_Target\n{\n    float3 base = scene.Sample(uv).rgb;\n    float4 c = gbuffer.Sample(uv);\n    float depth = c.a;\n    // Background / sky, or a non-reflecting (too-rough) surface: weight 0 so the\n    // reflection composite keeps the scene there. The resolve does not blend\n    // inline; it writes reflected radiance (.rgb) + composite weight (.a).\n    if (depth <= 0.0)\n    {\n        return float4(base, 0.0);\n    }\n\n    float roughness = rough_tex.Sample(uv).r;\n    // Glossy surfaces reflect sharply; rough ones get nothing.\n    float gloss = saturate((REFLECTION_ROUGHNESS_CUT - roughness) / REFLECTION_ROUGHNESS_CUT);\n    if (gloss <= 0.0)\n    {\n        return float4(base, 0.0);\n    }\n\n    float3 n = normalize(c.xyz);\n    float3 p = ssr_view_pos(uv, depth, params.tan_half_fov_y, params.aspect);\n    float3 v = normalize(-p);                       // p in view space, camera at origin\n    float3 r_dir = reflect(-v, n);                  // reflected ray direction\n\n    // Environment fallback for a missed (or screen-edge) ray, in the reflected\n    // direction at a roughness-keyed mip so a rougher surface reflects a\n    // blurrier environment. With a baked reflection probe this is the local\n    // scene capture (box-projected + blended across covering probes), the same\n    // source the forward IBL specular term uses, rather than the foreign sky\n    // HDR; otherwise it is the IBL prefilter cube. With no EnvironmentMap bound\n    // there is nothing to fall back to, so missed rays keep the base shading.\n    bool ibl = params.prefilter_mip_count > 0.5;\n    float3 env = base;\n    if (ibl)\n    {\n        float3 r_world = mul((float3x3)params.inv_view, r_dir);\n        float lod = roughness * (params.prefilter_mip_count - 1.0);\n        if (probe_set.count > 0u)\n        {\n            // The full inv_view (its translation column carries the camera\n            // position) lifts the view-space surface point p to world space,\n            // which the probe box-projection needs.\n            float3 world_pos = mul(params.inv_view, float4(p, 1.0)).xyz;\n            env = probe_set_specular(world_pos, r_world, lod);\n        }\n        else\n        {\n            env = prefilter.SampleLevel(SKY_DIR(r_world), lod).rgb;\n        }\n    }\n\n    float3 step_v = r_dir * params.stride;\n    float3 q = p;\n    bool hit = false;\n    float2 hit_uv = uv;\n    int steps_taken = SSR_MAX_STEPS;\n    for (int i = 0; i < SSR_MAX_STEPS; i++)\n    {\n        q += step_v;\n        if (q.z >= 0.0) { steps_taken = i; break; }  // crossed the camera plane\n        float2 march_uv = ssr_project(q, params.tan_half_fov_y, params.aspect);\n        if (march_uv.x < 0.0 || march_uv.x > 1.0 || march_uv.y < 0.0 || march_uv.y > 1.0)\n        {\n            steps_taken = i;\n            break;\n        }\n        float scene_depth = gbuffer.Sample(march_uv).a;\n        if (scene_depth <= 0.0) continue;            // sky here - keep marching\n        float diff = (-q.z) - scene_depth;           // > 0: ray is behind the surface\n        if (diff > 0.0 && diff < params.thickness)\n        {\n            // Binary-search refine between the last two samples.\n            float3 lo = q - step_v;\n            float3 hi = q;\n            for (int r = 0; r < SSR_REFINE; r++)\n            {\n                float3 mid = (lo + hi) * 0.5;\n                float2 muv = ssr_project(mid, params.tan_half_fov_y, params.aspect);\n                float sd = gbuffer.Sample(muv).a;\n                if (sd > 0.0 && (-mid.z) - sd > 0.0) hi = mid; else lo = mid;\n            }\n            hit_uv = ssr_project(hi, params.tan_half_fov_y, params.aspect);\n            hit = true;\n            steps_taken = i;\n            break;\n        }\n    }\n\n    // The reflected colour: the screen-space hit (a single sharp tap - the\n    // reflection composite blurs it by roughness), or the environment cube when\n    // the ray missed. A hit near the screen border or at the end of its march\n    // fades toward the environment rather than snapping flat to the base\n    // shading.\n    float3 reflected;\n    if (hit)\n    {\n        float3 hit_color = scene.Sample(hit_uv).rgb;\n        float2 e = smoothstep(float2(0.0), float2(SSR_EDGE_FADE), hit_uv)\n                 * smoothstep(float2(0.0), float2(SSR_EDGE_FADE), float2(1.0) - hit_uv);\n        float edge = e.x * e.y;\n        float march = float(steps_taken) / float(SSR_MAX_STEPS);\n        float dist_fade = 1.0 - smoothstep(0.7, 1.0, march);\n        reflected = lerp(env, hit_color, edge * dist_fade);\n    }\n    else\n    {\n        reflected = env;\n    }\n\n    float ndv = saturate(dot(n, v));\n    float fresnel = SSR_F0 + (1.0 - SSR_F0) * pow(1.0 - ndv, 5.0);\n    float w = saturate(fresnel * gloss * params.intensity);\n    // Reflected radiance (.rgb) + composite weight (.a). The reflection\n    // composite blurs this by surface roughness and blends it over the scene.\n    return float4(reflected, w);\n}\n";
Expand description

ssr.slang.