pub const BLOOM: &str = "// Bloom chain: prefilter, downsample, upsample. One fragment per compile,\n// selected by a define so each variant declares exactly the resources it binds\n// (Metal and DXIL indices are assigned in declaration order, so an unused\n// declaration would shift the live ones):\n//\n// BLOOM_PREFILTER - threshold the scene into bloom mip 0, Karis-weighted.\n// BLOOM_DOWNSAMPLE - 13-tap reduce of the previous mip into the next.\n// BLOOM_UPSAMPLE - 9-tap tent expand, additively blended onto the coarser\n// mip by the pipeline blend state.\n//\n// Pairs with `fullscreen_vertex` in fullscreen.slang.\n\n{POST_COMMON}\n\n[[vk::binding(0, 0)]] Sampler2D<float4> src;\n\n#ifdef BLOOM_PREFILTER\n// Layout must match `PostProcessParams` in render_types.rs (24 B); the\n// prefilter reads only the threshold, knee and exposure.\nstruct PostProcessParams\n{\n float bloom_intensity;\n float bloom_threshold;\n float bloom_knee;\n float exposure;\n float vignette;\n float lut_strength;\n};\n\n[[vk::push_constant]]\nConstantBuffer<PostProcessParams> post;\n#endif\n\nfloat bloom_luma(float3 c)\n{\n return dot(c, float3(0.2126, 0.7152, 0.0722));\n}\n\n// Karis weight: attenuate bright 2x2 groups so a single firefly does not\n// dominate the downsample. Applied only on the first (prefilter) downsample.\nfloat karis_weight(float3 c)\n{\n return 1.0 / (1.0 + bloom_luma(c));\n}\n\n// 13-tap downsample (Jimenez/CoD). When `karis` is set, the 13 taps are grouped\n// into five overlapping 2x2 boxes and luma-weighted.\nfloat3 downsample_13(float2 uv, float2 texel, bool karis)\n{\n float3 a = src.Sample(uv + texel * float2(-2.0, -2.0)).rgb;\n float3 b = src.Sample(uv + texel * float2( 0.0, -2.0)).rgb;\n float3 c = src.Sample(uv + texel * float2( 2.0, -2.0)).rgb;\n float3 d = src.Sample(uv + texel * float2(-2.0, 0.0)).rgb;\n float3 e = src.Sample(uv + texel * float2( 0.0, 0.0)).rgb;\n float3 f = src.Sample(uv + texel * float2( 2.0, 0.0)).rgb;\n float3 g = src.Sample(uv + texel * float2(-2.0, 2.0)).rgb;\n float3 h = src.Sample(uv + texel * float2( 0.0, 2.0)).rgb;\n float3 i = src.Sample(uv + texel * float2( 2.0, 2.0)).rgb;\n float3 j = src.Sample(uv + texel * float2(-1.0, -1.0)).rgb;\n float3 k = src.Sample(uv + texel * float2( 1.0, -1.0)).rgb;\n float3 l = src.Sample(uv + texel * float2(-1.0, 1.0)).rgb;\n float3 m = src.Sample(uv + texel * float2( 1.0, 1.0)).rgb;\n\n if (karis)\n {\n float3 g0 = (a + b + d + e) * (0.125 / 4.0);\n float3 g1 = (b + c + e + f) * (0.125 / 4.0);\n float3 g2 = (d + e + g + h) * (0.125 / 4.0);\n float3 g3 = (e + f + h + i) * (0.125 / 4.0);\n float3 g4 = (j + k + l + m) * (0.5 / 4.0);\n g0 *= karis_weight(g0);\n g1 *= karis_weight(g1);\n g2 *= karis_weight(g2);\n g3 *= karis_weight(g3);\n g4 *= karis_weight(g4);\n return g0 + g1 + g2 + g3 + g4;\n }\n\n float3 result = e * 0.125;\n result += (a + c + g + i) * 0.03125;\n result += (b + d + f + h) * 0.0625;\n result += (j + k + l + m) * 0.125;\n return result;\n}\n\n#if defined(BLOOM_PREFILTER)\n\n// Upper bound on a single bloom source sample. IBL skyboxes can be many orders\n// of magnitude brighter than surface shading (e.g. an EV24 HDR); without a\n// clamp one ultra-bright texel blurs out and washes the whole frame white.\nstatic const float BLOOM_CLAMP = 16.0;\n\n// Quadratic soft-knee threshold (CoD/Unity): pixels above `threshold`\n// contribute fully, pixels within `knee` below it ramp in smoothly.\nfloat3 prefilter_threshold(float3 c, float threshold, float knee)\n{\n float br = max(c.r, max(c.g, c.b));\n float kn = max(knee, 1e-4);\n float soft = clamp(br - threshold + kn, 0.0, 2.0 * kn);\n soft = (soft * soft) / (4.0 * kn);\n float contrib = max(soft, br - threshold) / max(br, 1e-4);\n return c * max(contrib, 0.0);\n}\n\n[shader(\"fragment\")]\nfloat4 bloom_prefilter_fragment([[vk::location(0)]] float2 uv : TEXCOORD0) : SV_Target\n{\n float2 texel = 1.0 / combined_size(src);\n float3 c = downsample_13(uv, texel, true);\n // Exposure applies before the threshold so the soft-knee cut tracks the\n // exposed scene the composite pass tonemaps, not the raw HDR radiance.\n c *= post.exposure;\n c = min(c, float3(BLOOM_CLAMP));\n c = prefilter_threshold(c, post.bloom_threshold, post.bloom_knee);\n return float4(c, 1.0);\n}\n\n#elif defined(BLOOM_DOWNSAMPLE)\n\n[shader(\"fragment\")]\nfloat4 bloom_downsample_fragment([[vk::location(0)]] float2 uv : TEXCOORD0) : SV_Target\n{\n float2 texel = 1.0 / combined_size(src);\n return float4(downsample_13(uv, texel, false), 1.0);\n}\n\n#elif defined(BLOOM_UPSAMPLE)\n\n// 9-tap tent upsample filter (weights 1 2 1 / 2 4 2 / 1 2 1, /16).\nfloat3 upsample_tent(float2 uv, float2 texel)\n{\n float3 sum = float3(0.0);\n sum += src.Sample(uv + texel * float2(-1.0, -1.0)).rgb * 1.0;\n sum += src.Sample(uv + texel * float2( 0.0, -1.0)).rgb * 2.0;\n sum += src.Sample(uv + texel * float2( 1.0, -1.0)).rgb * 1.0;\n sum += src.Sample(uv + texel * float2(-1.0, 0.0)).rgb * 2.0;\n sum += src.Sample(uv + texel * float2( 0.0, 0.0)).rgb * 4.0;\n sum += src.Sample(uv + texel * float2( 1.0, 0.0)).rgb * 2.0;\n sum += src.Sample(uv + texel * float2(-1.0, 1.0)).rgb * 1.0;\n sum += src.Sample(uv + texel * float2( 0.0, 1.0)).rgb * 2.0;\n sum += src.Sample(uv + texel * float2( 1.0, 1.0)).rgb * 1.0;\n return sum * (1.0 / 16.0);\n}\n\n[shader(\"fragment\")]\nfloat4 bloom_upsample_fragment([[vk::location(0)]] float2 uv : TEXCOORD0) : SV_Target\n{\n float2 texel = 1.0 / combined_size(src);\n return float4(upsample_tent(uv, texel), 1.0);\n}\n\n#else\n#error \"bloom.slang: define BLOOM_PREFILTER, BLOOM_DOWNSAMPLE, or BLOOM_UPSAMPLE\"\n#endif\n";Expand description
bloom.slang.