concinnity-device 0.18.68

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
Documentation
// src/metal/shaders/raymarch_volumetric_template.metal
//
// Volumetric variant of the raymarch template. Appended to a user's
// volumetric fragment shader at compile time (after the helpers + the
// user's `sampleVolume` definition). Authoring contract:
//
//     VolumeSample sampleVolume(float3 p, constant SdfParams& params, float time);
//
// where `VolumeSample` is declared in `raymarch_helpers.metal`. The user
// shader does NOT need `map` / `shade`; the Metal compiler's dead-code
// elimination strips the unused engine helpers (`sdfNormal`, `coneRaymarch`)
// along with their `map` calls.
//
// March + integration: same back-face bounding-box rasterisation as the
// surface template, but at the pixel stage we step linearly from box
// entry to exit (`max_steps` samples), accumulate Beer-Lambert
// transmittance front-to-back, and add per-step in-scattered sun light
// plus emission. Output is alpha-blended over the rasterised scene
// (SRC_ALPHA / ONE_MINUS_SRC_ALPHA at the pipeline level); no depth write.
//
// V1 limitations:
//   * No self-shadowing - the in-scatter term assumes uniform sun
//     illumination through the medium. Adding a secondary short march
//     toward the sun per step would buy directional shadowing at ~32x
//     density-sample cost; deferred.
//   * No per-pixel scene-depth clamp - the march extends across the
//     full bounding box. Sized so the box does not intersect rasterised
//     geometry the volume is meant to render behind. Adding a
//     scene-depth read + per-pixel `t_exit = min(t_exit, scene_t)` is a
//     well-bounded follow-up.

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

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

vertex VolVertexOut raymarch_volumetric_vertex(
    VolVertexIn v [[stage_in]],
    constant RaymarchView& view [[buffer(0)]],
    constant SdfVolumeUniforms& vol [[buffer(1)]]
) {
    float3 wp = v.pos * float3(vol.extent) + float3(vol.centre);
    VolVertexOut o;
    o.position = view.vp * float4(wp, 1.0);
    o.world_pos = wp;
    return o;
}

struct VolFragOut {
    float4 color [[color(0)]];
};

fragment VolFragOut raymarch_volumetric_fragment(
    VolVertexOut 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)]],
    texture2d<float> scene_color [[texture(4)]],
    sampler depth_samp [[sampler(0)]],
    sampler shadow_samp [[sampler(1)]],
    sampler cube_samp [[sampler(2)]],
    sampler scene_samp [[sampler(3)]]
) {
    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` is on the far side of the box; the slab
    // test gives both enter + exit and handles the camera-inside-box case.
    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);
    float t_exit = min(box_t.y, vol.max_distance);
    if (t_enter >= t_exit) {
        discard_fragment();
    }

    uint step_count = uint(max(vol.max_steps, 1));
    float step_size = (t_exit - t_enter) / float(step_count);

    // Sun radiance for the in-scatter term. No self-shadow march in V1.
    float3 sun_radiance = float3(0.0, 0.0, 0.0);
    if (lights.num_directional > 0) {
        sun_radiance = float3(lights.directional[0].color)
                     * lights.directional[0].intensity;
    }

    float transmittance = 1.0;
    float3 luminance = float3(0.0, 0.0, 0.0);

    for (uint i = 0u; i < step_count; ++i) {
        float t = t_enter + (float(i) + 0.5) * step_size;
        float3 p = cam + ray_dir * t;

        VolumeSample vs = sampleVolume(p, vol.params, view.time);
        if (vs.density <= 0.0) {
            continue;
        }

        float opt_depth = vs.density * step_size;
        float step_T = exp(-opt_depth);

        // Energy in-scattered inside this slab = (1 - step_T) * radiance.
        // Radiance = single-scatter from the sun (modulated by the per-point
        // scattering albedo) plus self-emission.
        float3 step_radiance = sun_radiance * vs.scattering + vs.emission;
        luminance += transmittance * step_radiance * (1.0 - step_T);
        transmittance *= step_T;

        // Early-out once the medium is effectively opaque.
        if (transmittance < 0.005) {
            break;
        }
    }

    float alpha = 1.0 - transmittance;
    if (alpha < 0.005) {
        discard_fragment();
    }

    VolFragOut o;
    o.color = float4(luminance, alpha);
    return o;
}