concinnity-device 0.18.66

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
Documentation
// src/metal/shaders/raymarch_shadow.metal
//
// Engine-shipped depth-only template for raymarched SDF shadow casters on
// Metal. Appended to the user's `map` + `shade` definitions (after
// raymarch_helpers.metal) when building the per-volume SHADOW pipeline - note
// the main `raymarch_template.metal` is NOT included, so this library defines
// only the shadow vertex + fragment. The user's `shade` is never called by
// the shadow functions, so it links to nothing and binds no IBL / scene-colour
// resources; only `map` (through `coneRaymarch`) is sampled.
//
// One pipeline per `SdfVolume.cast_shadows == true`. The encoder draws the
// proxy unit cube (back faces only - front-face culling) once per CSM cascade
// into that cascade's `shadow_map` slice, marching the SDF from the light side
// and writing the hit's NDC depth via `[[depth(less)]]`. The cascade slice's
// LESS depth test composites the raymarched caster with the rasterised casters
// already drawn into it - the nearer occluder wins per texel. Mirrors
// src/directx/shaders/raymarch_shadow.hlsl.

// Which cascade this draw targets. Bound at buffer(4); selects
// `shadow.light_vps[idx]` for both the vertex projection and the fragment's
// depth reprojection. Matches `RaymarchShadowCascade` in metal/raymarch.rs.
struct RaymarchShadowCascade {
    uint cascade_idx;
    uint _pad0;
    uint _pad1;
    uint _pad2;
};

struct ShadowVertexIn {
    float3 pos     [[attribute(0)]];
    float3 normal  [[attribute(1)]];
    float3 tangent [[attribute(2)]];
    float3 color   [[attribute(3)]];
    float2 uv      [[attribute(4)]];
};

struct ShadowVertexOut {
    float4 position [[position]];
    float3 world_pos;
};

vertex ShadowVertexOut raymarch_shadow_vertex(
    ShadowVertexIn v [[stage_in]],
    constant SdfVolumeUniforms& vol [[buffer(1)]],
    constant RaymarchShadowUniforms& shadow [[buffer(3)]],
    constant RaymarchShadowCascade& cascade [[buffer(4)]]
) {
    // Unit-cube proxy at ±1; scale by extent + offset by centre to land at
    // the AABB corners. Identical to the main raymarch vertex - only the
    // projection matrix changes to the cascade's light view-projection.
    float3 wp = v.pos * float3(vol.extent) + float3(vol.centre);
    ShadowVertexOut o;
    o.position = shadow.light_vps[cascade.cascade_idx] * float4(wp, 1.0);
    o.world_pos = wp;
    return o;
}

// Depth-only output: the shadow pass binds no colour attachment.
struct ShadowFragOut {
    float depth [[depth(less)]];
};

fragment ShadowFragOut raymarch_shadow_fragment(
    ShadowVertexOut in [[stage_in]],
    constant RaymarchView& view [[buffer(0)]],
    constant SdfVolumeUniforms& vol [[buffer(1)]],
    constant RaymarchLights& lights [[buffer(2)]],
    constant RaymarchShadowUniforms& shadow [[buffer(3)]],
    constant RaymarchShadowCascade& cascade [[buffer(4)]]
) {
    // For a directional sun, `directional[0].direction` is L (surface → light).
    // Incoming light travels along -L, so the shadow ray marches along -L. Match
    // what `shadePbrSun` reads from the same field so SDF shadows line up with
    // the lit-side surface.
    float3 ray_dir = -normalize(float3(lights.directional[0].direction));

    // `in.world_pos` lies on the bbox face farthest from the light (the encoder
    // culls front faces of the proxy cube). Step back toward the light by the
    // bbox bounding-sphere diameter so the slab test below picks up the true
    // front-face entry from outside the box.
    float diag = length(float3(vol.extent)) * 2.5;
    float3 origin = in.world_pos - ray_dir * diag;

    float3 box_min = float3(vol.centre) - float3(vol.extent);
    float3 box_max = float3(vol.centre) + float3(vol.extent);
    float2 box_t = rayBox(origin, ray_dir, box_min, box_max);
    if (box_t.y < max(box_t.x, 0.0)) {
        discard_fragment();
    }
    float t_enter = max(box_t.x, 0.001);
    float t_max = min(box_t.y, vol.max_distance);
    if (t_enter >= t_max) {
        discard_fragment();
    }

    RayHit hit = coneRaymarch(origin, ray_dir, t_enter, t_max, vol, view.time);
    if (!hit.hit) {
        discard_fragment();
    }

    // Reproject the hit through the SAME cascade VP the vertex stage rasterised
    // with, so the depth write shares the rasterised casters' NDC depth space in
    // this slice. The `[[depth(less)]]` contract holds: the hit is bounded by
    // `t_max ≤ box exit`, so its NDC.z ≤ the back face's rasterised NDC.z.
    float3 hit_pos = origin + ray_dir * hit.t;
    float4 hit_clip = shadow.light_vps[cascade.cascade_idx] * float4(hit_pos, 1.0);
    ShadowFragOut o;
    o.depth = hit_clip.z / max(hit_clip.w, 1e-6);
    return o;
}