concinnity-core 0.19.2

Runtime vocabulary for the Concinnity engine: GPU layouts, ECS components, registry, CPU kernels
Documentation
// Bloom chain: prefilter, downsample, upsample. One fragment per compile,
// selected by a define so each variant declares exactly the resources it binds
// (Metal and DXIL indices are assigned in declaration order, so an unused
// declaration would shift the live ones):
//
//   BLOOM_PREFILTER  - threshold the scene into bloom mip 0, Karis-weighted.
//   BLOOM_DOWNSAMPLE - 13-tap reduce of the previous mip into the next.
//   BLOOM_UPSAMPLE   - 9-tap tent expand, additively blended onto the coarser
//                      mip by the pipeline blend state.
//
// Pairs with `fullscreen_vertex` in fullscreen.slang.

{POST_COMMON}

[[vk::binding(0, 0)]] Sampler2D<float4> src;

#ifdef BLOOM_PREFILTER
// Layout must match `PostProcessParams` in render_types.rs (24 B); the
// prefilter reads only the threshold, knee and exposure.
struct PostProcessParams
{
    float bloom_intensity;
    float bloom_threshold;
    float bloom_knee;
    float exposure;
    float vignette;
    float lut_strength;
};

[[vk::push_constant]]
ConstantBuffer<PostProcessParams> post;
#endif

float bloom_luma(float3 c)
{
    return dot(c, float3(0.2126, 0.7152, 0.0722));
}

// Karis weight: attenuate bright 2x2 groups so a single firefly does not
// dominate the downsample. Applied only on the first (prefilter) downsample.
float karis_weight(float3 c)
{
    return 1.0 / (1.0 + bloom_luma(c));
}

// 13-tap downsample (Jimenez/CoD). When `karis` is set, the 13 taps are grouped
// into five overlapping 2x2 boxes and luma-weighted.
float3 downsample_13(float2 uv, float2 texel, bool karis)
{
    float3 a = src.Sample(uv + texel * float2(-2.0, -2.0)).rgb;
    float3 b = src.Sample(uv + texel * float2( 0.0, -2.0)).rgb;
    float3 c = src.Sample(uv + texel * float2( 2.0, -2.0)).rgb;
    float3 d = src.Sample(uv + texel * float2(-2.0,  0.0)).rgb;
    float3 e = src.Sample(uv + texel * float2( 0.0,  0.0)).rgb;
    float3 f = src.Sample(uv + texel * float2( 2.0,  0.0)).rgb;
    float3 g = src.Sample(uv + texel * float2(-2.0,  2.0)).rgb;
    float3 h = src.Sample(uv + texel * float2( 0.0,  2.0)).rgb;
    float3 i = src.Sample(uv + texel * float2( 2.0,  2.0)).rgb;
    float3 j = src.Sample(uv + texel * float2(-1.0, -1.0)).rgb;
    float3 k = src.Sample(uv + texel * float2( 1.0, -1.0)).rgb;
    float3 l = src.Sample(uv + texel * float2(-1.0,  1.0)).rgb;
    float3 m = src.Sample(uv + texel * float2( 1.0,  1.0)).rgb;

    if (karis)
    {
        float3 g0 = (a + b + d + e) * (0.125 / 4.0);
        float3 g1 = (b + c + e + f) * (0.125 / 4.0);
        float3 g2 = (d + e + g + h) * (0.125 / 4.0);
        float3 g3 = (e + f + h + i) * (0.125 / 4.0);
        float3 g4 = (j + k + l + m) * (0.5 / 4.0);
        g0 *= karis_weight(g0);
        g1 *= karis_weight(g1);
        g2 *= karis_weight(g2);
        g3 *= karis_weight(g3);
        g4 *= karis_weight(g4);
        return g0 + g1 + g2 + g3 + g4;
    }

    float3 result = e * 0.125;
    result += (a + c + g + i) * 0.03125;
    result += (b + d + f + h) * 0.0625;
    result += (j + k + l + m) * 0.125;
    return result;
}

#if defined(BLOOM_PREFILTER)

// Upper bound on a single bloom source sample. IBL skyboxes can be many orders
// of magnitude brighter than surface shading (e.g. an EV24 HDR); without a
// clamp one ultra-bright texel blurs out and washes the whole frame white.
static const float BLOOM_CLAMP = 16.0;

// Quadratic soft-knee threshold (CoD/Unity): pixels above `threshold`
// contribute fully, pixels within `knee` below it ramp in smoothly.
float3 prefilter_threshold(float3 c, float threshold, float knee)
{
    float br = max(c.r, max(c.g, c.b));
    float kn = max(knee, 1e-4);
    float soft = clamp(br - threshold + kn, 0.0, 2.0 * kn);
    soft = (soft * soft) / (4.0 * kn);
    float contrib = max(soft, br - threshold) / max(br, 1e-4);
    return c * max(contrib, 0.0);
}

[shader("fragment")]
float4 bloom_prefilter_fragment([[vk::location(0)]] float2 uv : TEXCOORD0) : SV_Target
{
    float2 texel = 1.0 / combined_size(src);
    float3 c = downsample_13(uv, texel, true);
    // Exposure applies before the threshold so the soft-knee cut tracks the
    // exposed scene the composite pass tonemaps, not the raw HDR radiance.
    c *= post.exposure;
    c = min(c, float3(BLOOM_CLAMP));
    c = prefilter_threshold(c, post.bloom_threshold, post.bloom_knee);
    return float4(c, 1.0);
}

#elif defined(BLOOM_DOWNSAMPLE)

[shader("fragment")]
float4 bloom_downsample_fragment([[vk::location(0)]] float2 uv : TEXCOORD0) : SV_Target
{
    float2 texel = 1.0 / combined_size(src);
    return float4(downsample_13(uv, texel, false), 1.0);
}

#elif defined(BLOOM_UPSAMPLE)

// 9-tap tent upsample filter (weights 1 2 1 / 2 4 2 / 1 2 1, /16).
float3 upsample_tent(float2 uv, float2 texel)
{
    float3 sum = float3(0.0);
    sum += src.Sample(uv + texel * float2(-1.0, -1.0)).rgb * 1.0;
    sum += src.Sample(uv + texel * float2( 0.0, -1.0)).rgb * 2.0;
    sum += src.Sample(uv + texel * float2( 1.0, -1.0)).rgb * 1.0;
    sum += src.Sample(uv + texel * float2(-1.0,  0.0)).rgb * 2.0;
    sum += src.Sample(uv + texel * float2( 0.0,  0.0)).rgb * 4.0;
    sum += src.Sample(uv + texel * float2( 1.0,  0.0)).rgb * 2.0;
    sum += src.Sample(uv + texel * float2(-1.0,  1.0)).rgb * 1.0;
    sum += src.Sample(uv + texel * float2( 0.0,  1.0)).rgb * 2.0;
    sum += src.Sample(uv + texel * float2( 1.0,  1.0)).rgb * 1.0;
    return sum * (1.0 / 16.0);
}

[shader("fragment")]
float4 bloom_upsample_fragment([[vk::location(0)]] float2 uv : TEXCOORD0) : SV_Target
{
    float2 texel = 1.0 / combined_size(src);
    return float4(upsample_tent(uv, texel), 1.0);
}

#else
#error "bloom.slang: define BLOOM_PREFILTER, BLOOM_DOWNSAMPLE, or BLOOM_UPSAMPLE"
#endif