concinnity-device 0.19.1

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
Documentation
// src/metal/shaders/raymarch_template.metal
//
// Engine-shipped template for raymarched SDF volumes. Appended to the
// user's fragment shader at compile time (after the helpers + the
// user's `map` / `shade` definitions). Provides:
//
//   * `raymarch_vertex` - rasterises the bounding-box proxy (back faces
//     only via the encoder's cull mode). Each output fragment is a
//     candidate for a ray that pierces the box.
//   * `raymarch_fragment` - reconstructs the world-space ray, samples
//     main depth for early-out, calls `coneRaymarch` + the user's
//     `map`, and on hit calls `sdfNormal` + the user's `shade` and
//     applies the engine's PBR + ambient helpers. Writes opaque colour
//     into the bound `hdr_resolve` attachment.
//
// The fragment writes hit depth back into the
// bound depth attachment via `[[depth(less)]]`, so downstream passes
// (water / decal / fog) that sample `hdr_targets.depth_resolve` see the
// raymarched surface and composite correctly. The shader-side cone-
// march early-out still samples the MSAA `hdr_targets.depth` as a
// read-only snapshot, a different texture from the writable
// `depth_resolve` attachment so no aliasing rule applies.

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

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

vertex VertexOut raymarch_vertex(
    VertexIn v [[stage_in]],
    constant RaymarchView& view [[buffer(0)]],
    constant SdfVolumeUniforms& vol [[buffer(1)]]
) {
    // The proxy buffer is a unit cube with positions in [-0.5, 0.5]^3.
    // Scale by the volume's extent (which is the AABB half-widths * 2,
    // see raymarch.rs::build_raymarch_cube_buffers) and translate by
    // centre to land in world space.
    float3 wp = v.pos * float3(vol.extent) + float3(vol.centre);
    VertexOut o;
    o.position = view.vp * float4(wp, 1.0);
    o.world_pos = wp;
    return o;
}

struct RaymarchFragOut {
    float4 color [[color(0)]];
    // `depth(less)` lets the hardware discard fragments whose computed
    // hit depth is behind the already-written depth (so overlapping
    // SDF volumes still resolve correctly in z-order), and writes the
    // hit's reprojected NDC depth into the bound `depth_resolve`
    // attachment so downstream passes that sample depth see the
    // raymarched surface, not the rasterised geometry behind it.
    float depth [[depth(less)]];
};

fragment RaymarchFragOut raymarch_fragment(
    VertexOut in [[stage_in]],
    constant RaymarchView& view [[buffer(0)]],
    constant SdfVolumeUniforms& vol [[buffer(1)]],
    constant RaymarchLights& lights [[buffer(2)]],
    constant RaymarchShadowUniforms& shadow [[buffer(3)]],
    depth2d_ms<float> main_depth [[texture(0)]],
    depth2d_array<float> shadow_map [[texture(1)]],
    texturecube<float> irradiance_cube [[texture(2)]],
    texturecube<float> prefilter_cube [[texture(3)]],
    // Pre-raymarch scene snapshot the blit at the head of
    // the pass populated. User shaders sample this through
    // `sampleSceneRefracted` to get the surface-below-water colour.
    texture2d<float> scene_color [[texture(4)]],
    sampler shadow_samp [[sampler(1)]],
    sampler cube_samp [[sampler(2)]],
    sampler scene_samp [[sampler(3)]]
) {
    // Build the world-space ray from camera through this fragment.
    float3 cam = float3(view.cam_pos);
    float3 ray_dir = normalize(in.world_pos - cam);

    // Clip the ray to the volume's AABB. The vertex shader rasterised
    // the back faces, so `in.world_pos` lies on the far side of the
    // box; using the slab test gives both enter + exit and handles the
    // camera-inside-box case uniformly (t_enter clamps to 0 below).
    float3 box_min = float3(vol.centre) - float3(vol.extent);
    float3 box_max = float3(vol.centre) + float3(vol.extent);
    float2 box_t = rayBox(cam, 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);

    // Sample main depth at this pixel. MSAA depth attachment, read
    // sample 0; the raymarch pass is single-sample and the worst-case
    // distance is "the closest pixel of the rasterised geometry behind
    // this fragment", so picking any sample is conservative-enough for
    // the early-out. Convert depth → world-space distance via inv_vp.
    uint2 px = uint2(in.position.xy);
    float depth_ndc = main_depth.read(px, 0);
    float2 ndc_xy = (in.position.xy / view.viewport) * 2.0 - 1.0;
    // Metal NDC has y-down in clip space (after the projection matrix
    // flip the engine applies), so re-mirror Y to match the inv_vp the
    // CPU built (which inverts the un-flipped projection).
    ndc_xy.y = -ndc_xy.y;
    float4 world = view.inv_vp * float4(ndc_xy, depth_ndc, 1.0);
    world /= max(world.w, 1e-6);
    float t_rasterized = length(world.xyz - cam);

    // Clip to the closest of: bounding-box exit, rasterised depth, the
    // per-volume far-clip. Everything past `t_max` is discarded, so a
    // raymarch behind rasterised geometry pays the rasterisation cost
    // only and the shader bails immediately.
    float t_max = min(box_t.y, min(t_rasterized, vol.max_distance));
    if (t_enter >= t_max) {
        discard_fragment();
    }

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

    float3 hit_pos = cam + ray_dir * hit.t;
    float3 normal = sdfNormal(hit_pos, vol.params, view.time, 0.001);
    // [0, 1] screen UV in Metal's y-down convention, matches the
    // sampler `scene_color.sample` expects directly.
    float2 frag_uv = in.position.xy / view.viewport;
    SdfSurface surf = shade(hit_pos, normal, vol.params, view.time,
                             frag_uv, scene_color, scene_samp);

    // IBL ambient when an EnvironmentMap is bound +
    // CSM cascade-shadowed first directional light. The helpers
    // internally fall back when the gate fails (no IBL → hemispheric
    // ambient; receive_shadows off → no shadow sample).
    float3 view_dir = -ray_dir;
    float3 color = shadeAmbientIbl(
        surf, normal, view_dir,
        view.prefilter_mip_count,
        irradiance_cube, prefilter_cube, cube_samp
    );
    if (lights.num_directional > 0) {
        float shadow_factor = 1.0;
        if (vol.receive_shadows != 0) {
            // `hit.t` is distance along the view ray ~ view-space
            // depth (positive, camera-relative). Good enough for
            // cascade selection without needing the view matrix; the
            // cosine error near screen edges is small for typical
            // SDF volume sizes.
            shadow_factor = sampleSunShadow(
                hit_pos, hit.t, in.position.xy,
                shadow, shadow_map, shadow_samp
            );
        }
        color += shadePbrSun(surf, normal, view_dir,
                              lights.directional[0], shadow_factor);
    }
    // Refraction / transmitted contribution. User shaders
    // that sampled the scene snapshot inside `shade` set this to the
    // colour they want to show through; opaque shaders leave it zero.
    color += surf.transmitted;

    // Reproject the hit position through the camera's view-projection
    // for the NDC depth output. The matrix is the same `view.vp` the
    // vertex shader rasterised the proxy cube with, so the reprojected
    // depth shares the rasterised geometry's depth space exactly.
    float4 hit_clip = view.vp * float4(hit_pos, 1.0);
    RaymarchFragOut o;
    o.color = float4(color, 1.0);
    o.depth = hit_clip.z / max(hit_clip.w, 1e-6);
    return o;
}