Skip to main content

COMPOSITE

Constant COMPOSITE 

Source
pub const COMPOSITE: &str = "// Final composite: bloom onto the exposed HDR scene, then either the SDR\n// chain (ACES tonemap, gamma 2.2, FXAA, colour-grading LUT) or the HDR EDR\n// passthrough, plus the vignette, the scene-transition fade, and the G-buffer\n// channel views. Single source for every backend; pairs with\n// `fullscreen_vertex` in fullscreen.slang.\n//\n// Vulkan gets set 0 bindings 0-5 as combined image samplers plus the push\n// constant; Metal gets texture(0..5) + sampler(0..5) and the params at\n// buffer(0). Textures 3-5 are declared but sampled only while a channel view\n// is active, which is also the only time the host binds them.\n\n{POST_COMMON}\n\n// Layout matches `CompositeParams` in render_types.rs (48 B): the\n// `PostProcessParams` fields followed by the fade, the channel-view selector,\n// and the camera far plane.\nstruct CompositeParams\n{\n    float bloom_intensity;\n    float bloom_threshold;\n    float bloom_knee;\n    float exposure;\n    float vignette;\n    float lut_strength;\n    // 0.0 = SDR path (ACES + gamma 2.2 + FXAA + ColorLut into a display-referred\n    // target). 1.0 = HDR EDR path (linear extended-range values; ACES / gamma /\n    // FXAA / LUT are all skipped because they assume a display-referred range).\n    float hdr_output;\n    // Inside the HDR branch: 0.0 = scRGB-linear passthrough, 1.0 = PQ encode\n    // (SMPTE ST 2084) for HDR10 panels.\n    float pq_output;\n    // 1.0 = run the FXAA edge filter on the SDR path; 0.0 = skip it (the Off\n    // anti-aliasing mode). Ignored on the HDR path, which never runs FXAA.\n    float fxaa;\n    // Scene-transition fade to black in [0, 1]. 0 leaves the frame untouched.\n    float fade;\n    // G-buffer channel view selector (ViewMode discriminant): 0 composites the\n    // scene; 3 = normals, 4 = roughness, 5 = occlusion, 6 = depth.\n    uint view_mode;\n    // Camera far plane, normalizing the depth channel view.\n    float far_plane;\n};\n\n[[vk::binding(0, 0)]] Sampler2D<float4> hdr_tex;\n// Bloom mip 0 (half-res). Always bound - when bloom is disabled the sample is\n// skipped, so an unwritten target is never read.\n[[vk::binding(1, 0)]] Sampler2D<float4> bloom_tex;\n// 3D colour-grading LUT. Always bound - a 2x2x2 identity LUT stands in when the\n// world declares no ColorLut, so the grade is a no-op at any lut_strength.\n[[vk::binding(2, 0)]] Sampler3D<float4> lut_tex;\n// G-buffer channel sources for the debug view modes.\n[[vk::binding(3, 0)]] Sampler2D<float4> gbuf_nd_tex;\n[[vk::binding(4, 0)]] Sampler2D<float4> gbuf_rough_tex;\n[[vk::binding(5, 0)]] Sampler2D<float4> ao_tex;\n\n[[vk::push_constant]]\nConstantBuffer<CompositeParams> post;\n\n// SDR reference white in cd/m2 (nits). BT.2408 recommends 203 nits as the HDR\n// mixing reference; it keeps SDR content from looking dim alongside HDR\n// highlights and matches the value mainstream HDR pipelines use as the\n// linear-to-PQ mapping reference.\nstatic const float PQ_SDR_REFERENCE_NITS = 203.0;\n\n// Width of the colour-grading LUT along one axis.\n//\n// The same slangc GetDimensions mis-lowering `combined_size` works around for\n// Sampler2D applies to a top-level Sampler3D, so this is the three-dimensional\n// twin of that pair rather than a plain call.\nvoid combined_dims_3d(Sampler3D<float4> s, out uint w, out uint h, out uint d)\n{\n    __target_switch\n    {\n    case hlsl:\n        __intrinsic_asm \"$0.GetDimensions($2, $3, $4)\";\n    default:\n        s.GetDimensions(w, h, d);\n    }\n}\n\nfloat lut_axis_size(Sampler3D<float4> s)\n{\n    __target_switch\n    {\n    case metal:\n        __intrinsic_asm \"float($0.get_width())\";\n    default:\n        uint w, h, d;\n        combined_dims_3d(s, w, h, d);\n        return float(w);\n    }\n}\n\n// PQ inverse-EOTF (PQ OETF / encode). Constants from SMPTE ST 2084 / ITU-R\n// BT.2100. Maps absolute luminance in cd/m2 (capped at 10000) onto the\n// perceptually-quantized signal in [0, 1] the HDR10 panel expects.\nfloat3 pq_encode(float3 l_nits)\n{\n    const float m1 = 0.1593017578125;     // = 2610 / 16384\n    const float m2 = 78.84375;            // = 2523 / 4096 * 128\n    const float c1 = 0.8359375;           // = 3424 / 4096\n    const float c2 = 18.8515625;          // = 2413 / 4096 * 32\n    const float c3 = 18.6875;             // = 2392 / 4096 * 32\n    float3 l_n = clamp(l_nits * (1.0 / 10000.0), float3(0.0), float3(1.0));\n    float3 lm1 = pow(l_n, float3(m1));\n    return pow((c1 + c2 * lm1) / (1.0 + c3 * lm1), float3(m2));\n}\n\n// Narkowicz 2015 ACES fit - closed-form approximation of the ACES RRT+ODT.\nfloat3 aces_narkowicz(float3 x)\n{\n    const float a = 2.51;\n    const float b = 0.03;\n    const float c = 2.43;\n    const float d = 0.59;\n    const float e = 0.14;\n    return saturate((x * (a * x + b)) / (x * (c * x + d) + e));\n}\n\n// FXAA luma weighting on a display-referred sRGB pixel.\nfloat fxaa_luma(float3 rgb)\n{\n    return dot(rgb, float3(0.299, 0.587, 0.114));\n}\n\n// Scene colour = exposed HDR resolve + bloom. Exposure scales the HDR tap\n// only - the bloom mip already carries the exposure applied in the prefilter.\n// The bloom sample is skipped when bloom is disabled so an uninitialised bloom\n// target is never read.\nfloat3 scene_sample(float2 uv)\n{\n    float3 c = hdr_tex.Sample(uv).rgb * post.exposure;\n    if (post.bloom_intensity > 0.0)\n    {\n        c += bloom_tex.Sample(uv).rgb * post.bloom_intensity;\n    }\n    return c;\n}\n\n// Scene sample -> ACES tonemap -> gamma 2.2 encode.\nfloat3 tonemap(float2 uv)\n{\n    return pow(aces_narkowicz(scene_sample(uv)), float3(1.0 / 2.2));\n}\n\n// Smooth radial corner darkening. `strength` 0 disables it; 1 fully darkens the\n// corners. The squared-distance falloff keeps the centre untouched.\nfloat vignette_factor(float2 uv)\n{\n    float2 d = uv - 0.5;\n    float dist = dot(d, d) * 2.0;\n    return 1.0 - post.vignette * smoothstep(0.25, 1.0, dist);\n}\n\n// Scene-transition fade as a multiplier: 1 = un-faded, 0 = fully black.\nfloat fade_scale()\n{\n    return 1.0 - saturate(post.fade);\n}\n\n// Sample the 3D colour-grading LUT with the tonemapped, display-referred sRGB\n// colour. The half-texel correction maps an input of 0 / 1 to the centres of\n// the first / last texels so trilinear filtering stays accurate edge to edge; a\n// 2x2x2 identity LUT then reproduces the input exactly.\nfloat3 apply_lut(float3 c)\n{\n    float n = lut_axis_size(lut_tex);\n    float3 uvw = saturate(c) * ((n - 1.0) / n) + (0.5 / n);\n    return lut_tex.Sample(uvw).rgb;\n}\n\n// Blend the LUT-graded colour over the input by `lut_strength`. Grading the\n// display-referred LDR result keeps the LUT independent of exposure / tonemap.\nfloat3 grade(float3 c)\n{\n    return lerp(c, apply_lut(c), post.lut_strength);\n}\n\n// One prepass channel, visualized in place of the composited scene.\nfloat3 channel_view(float2 uv)\n{\n    float4 nd = gbuf_nd_tex.Sample(uv);\n    if (post.view_mode == 3u)\n    {\n        // View-space normal; cleared alpha 0 marks \"no geometry\".\n        return (nd.a > 0.0) ? nd.xyz * 0.5 + 0.5 : float3(0.0);\n    }\n    if (post.view_mode == 4u)\n    {\n        return float3(gbuf_rough_tex.Sample(uv).r);\n    }\n    if (post.view_mode == 5u)\n    {\n        return float3(ao_tex.Sample(uv).r);\n    }\n    if (post.view_mode == 6u)\n    {\n        // Linear view depth over far; empty pixels read as the far plane.\n        return (nd.a > 0.0) ? float3(saturate(nd.a / max(post.far_plane, 1e-3)))\n                            : float3(1.0);\n    }\n    return float3(0.0);\n}\n\n[shader(\"fragment\")]\nfloat4 composite_fragment([[vk::location(0)]] float2 uv : TEXCOORD0) : SV_Target\n{\n    // Channel views replace the composited scene with one prepass channel; the\n    // text overlay still draws after in this same pass.\n    if (post.view_mode != 0u)\n    {\n        return float4(channel_view(uv), 1.0);\n    }\n\n    float2 inv_size = 1.0 / combined_size(hdr_tex);\n    // The scene fade rides the vignette multiplier: both are plain scalars on\n    // the output colour, so the HDR path fades in linear light ahead of the\n    // optional PQ encode and the SDR path fades the display-referred result.\n    float vig = vignette_factor(uv) * fade_scale();\n\n    // HDR EDR output: skip ACES + gamma + FXAA + LUT. Two flavours, picked by\n    // `pq_output`:\n    //\n    //   - scRGB linear: the compositor wants linear extended-range values where\n    //     1.0 is SDR reference white and values above drive the panel headroom.\n    //   - HDR10 PQ: the panel decodes via the PQ EOTF, so encode in-shader with\n    //     SDR reference white at 203 nits per BT.2408.\n    //\n    // The vignette applies in linear space ahead of the optional PQ encode,\n    // because it is a multiplicative falloff defined on luminance.\n    if (post.hdr_output > 0.5)\n    {\n        float3 hdr_vig = scene_sample(uv) * vig;\n        if (post.pq_output > 0.5)\n        {\n            return float4(pq_encode(hdr_vig * PQ_SDR_REFERENCE_NITS), 1.0);\n        }\n        return float4(hdr_vig, 1.0);\n    }\n\n    // Composite bloom onto the HDR scene, then ACES tonemap + gamma encode.\n    float3 c = tonemap(uv);\n\n    // FXAA is gated by post.fxaa (off for the Off anti-aliasing mode). When\n    // disabled, grade + vignette the tonemapped centre directly and skip the\n    // neighbour samples the edge filter would otherwise take.\n    if (post.fxaa < 0.5)\n    {\n        return float4(grade(c) * vig, 1.0);\n    }\n\n    // FXAA 3.11-style edge detection on the encoded image. Each neighbour is\n    // composited with bloom and remapped through the same tonemap + gamma so\n    // luma compares stay consistent with the centre sample.\n    float3 n = tonemap(uv + float2(0.0, -inv_size.y));\n    float3 s = tonemap(uv + float2(0.0,  inv_size.y));\n    float3 e = tonemap(uv + float2( inv_size.x, 0.0));\n    float3 w = tonemap(uv + float2(-inv_size.x, 0.0));\n\n    float l_c = fxaa_luma(c);\n    float l_n = fxaa_luma(n);\n    float l_s = fxaa_luma(s);\n    float l_e = fxaa_luma(e);\n    float l_w = fxaa_luma(w);\n\n    float l_min = min(l_c, min(min(l_n, l_s), min(l_e, l_w)));\n    float l_max = max(l_c, max(max(l_n, l_s), max(l_e, l_w)));\n    float l_range = l_max - l_min;\n\n    // Flat regions skip the blur entirely; they are still graded and vignetted.\n    if (l_range < max(0.0312, l_max * 0.125))\n    {\n        return float4(grade(c) * vig, 1.0);\n    }\n\n    // Pick the dominant edge direction (horizontal vs vertical) and step half a\n    // texel along the perpendicular for a 2-tap blur averaged with the centre.\n    float horz_diff = abs(l_n + l_s - 2.0 * l_c) * 2.0 + abs(l_e + l_w - 2.0 * l_c);\n    float vert_diff = abs(l_e + l_w - 2.0 * l_c) * 2.0 + abs(l_n + l_s - 2.0 * l_c);\n    bool horizontal = horz_diff >= vert_diff;\n\n    float2 step_dir = horizontal ? float2(0.0, inv_size.y) : float2(inv_size.x, 0.0);\n    float3 a = tonemap(uv + step_dir * 0.5);\n    float3 b = tonemap(uv - step_dir * 0.5);\n    float3 blended = (c + a + b) * (1.0 / 3.0);\n\n    // Grade the FXAA-resolved colour, then vignette last so it darkens the\n    // final composited result.\n    return float4(grade(blended) * vig, 1.0);\n}\n";
Expand description

composite.slang.