concinnity-render 0.18.67

GPU-free render preparation for the Concinnity engine
Documentation
// Projected (deferred) decal pass: single source for every backend.
//
// Runs after the main HDR pass. The vertex stage draws a unit cube (positions
// in [-0.5, 0.5]^3 in local space) under the decal's model matrix and the
// camera VP, jittered when TAA is on so the rasterised pixel grid matches the
// main pass exactly. The fragment stage reconstructs the world-space sample
// point of each rasterised pixel from the main pass's depth attachment,
// transforms it back into decal-local space, and discards where the point
// falls outside the unit box. Pixels near the top / bottom face along local +Y
// fade out so the stamp shows no hard edge where it meets a tilted or curved
// surface.
//
// The composited colour is alpha-blended into the resolved HDR target by the
// pipeline's blend state (Src.A * Src + (1 - Src.A) * Dst). The pass binds no
// depth attachment: the unit-box test is the depth test.
//
// USE_MSAA is a HOST difference rather than a target one: Vulkan reads the
// multisampled main depth while Metal reads the resolved copy.

#ifndef USE_MSAA
#define USE_MSAA 0
#endif

// Per-frame view inputs, 144 B. Mirrors `DecalView` in each backend's uniforms
// module.
struct DecalView
{
    // The main pass's view-projection (jittered when TAA is on).
    float4x4 vp;
    // Inverse of the same, for the world-space reconstruction.
    float4x4 inv_vp;
    // Attachment width, height in pixels.
    float2 viewport;
    float2 _pad;
};

// Per-decal uniforms, 160 B. Mirrors `DecalParams`.
struct DecalParams
{
    float4x4 model;     // local -> world
    float4x4 inv_model; // world -> local
    float4 tint;        // linear RGB x alpha
    float fade_pow;     // exponent on the soft fade along local +Y
    float _pad0;
    float _pad1;
    float _pad2;
};

// Vulkan binds both as UBOs in the per-frame set (the params through a
// dynamic offset into one MAX_DECALS-slot ring); Metal writes both with
// setBytes at the buffer indices the register numbers name.
[[vk::binding(0, 0)]] ConstantBuffer<DecalView> view : register(b0);
[[vk::binding(1, 0)]] ConstantBuffer<DecalParams> params : register(b1);

// Declaration order is the Metal texture index: the scene depth at 0 and this
// decal's albedo at 1, which is what that host binds. The albedo lives in its
// own Vulkan set so swapping decals rebinds set 1 rather than the per-frame set.
#if USE_MSAA
[[vk::binding(2, 0)]] Texture2DMS<float> scene_depth;
#else
[[vk::binding(2, 0)]] Texture2D<float> scene_depth;
#endif
[[vk::binding(0, 1)]] Sampler2D<float4> decal_tex;

struct DecalVertexIn
{
    // Unit cube vertex; the bound buffer carries 8 corners in [-0.5, 0.5]^3.
    [[vk::location(0)]] float3 pos : POSITION;
};

// The cube is only a rasterisation proxy, so the fragment derives everything it
// needs from the pixel position and carries no varying.
struct DecalVertexOut
{
    float4 position : SV_Position;
};

[shader("vertex")]
DecalVertexOut decal_vertex(DecalVertexIn v)
{
    DecalVertexOut o;
    float4 world = mul(params.model, float4(v.pos, 1.0));
    o.position = mul(view.vp, world);
    return o;
}

float decal_scene_depth(int2 pixel)
{
#if USE_MSAA
    return scene_depth.Load(pixel, 0);
#else
    return scene_depth.Load(int3(pixel, 0));
#endif
}

[shader("fragment")]
float4 decal_fragment(DecalVertexOut i) : SV_Target
{
    // The single-sample depth resolve shares the rasterised pixel grid with the
    // composite target, so integer texel coordinates address it directly.
    int2 pixel = int2(i.position.xy);
    if (pixel.x < 0 || pixel.y < 0 || pixel.x >= int(view.viewport.x)
        || pixel.y >= int(view.viewport.y))
    {
        discard;
    }
    // 1.0 is the cleared / "no geometry" sentinel: the main pass left this
    // pixel empty (the sky writes near-far-plane depth instead). Nothing to
    // project onto.
    float depth = decal_scene_depth(pixel);
    if (depth >= 1.0)
    {
        discard;
    }

    // Reconstruct world space at this pixel through the inverse VP. Every
    // backend rasterises this pass with the framebuffer's Y running downwards
    // (Vulkan through the same negative-height viewport as the main pass), so
    // the NDC flip is shared rather than target-specific.
    float2 ndc_xy = (i.position.xy / view.viewport) * 2.0 - 1.0;
    ndc_xy.y = -ndc_xy.y;
    float4 clip = float4(ndc_xy, depth, 1.0);
    float4 world = mul(view.inv_vp, clip);
    world /= world.w;

    // Decal-local clip against the unit box.
    float4 local = mul(params.inv_model, world);
    float3 ab = abs(local.xyz);
    if (ab.x > 0.5 || ab.y > 0.5 || ab.z > 0.5)
    {
        discard;
    }

    // Soft fade along the projection axis (local +Y) so the stamp shows no hard
    // band where the surface tilts away from the projection plane: alpha rolls
    // off as |local.y| approaches 0.5.
    float fade = saturate(1.0 - (ab.y * 2.0));
    fade = pow(fade, max(params.fade_pow, 1.0));

    // Sample on local X-Z; UV in [0, 1] with V = 0 at the top, matching the
    // rest of the engine's textures.
    float2 uv = local.xz + 0.5;
    uv.y = 1.0 - uv.y;
    float4 tex = decal_tex.Sample(uv);

    return float4(tex.rgb * params.tint.rgb, tex.a * params.tint.a * fade);
}