concinnity-device 0.19.9

GPU backends (Metal, Vulkan, DirectX) behind a device facade for Concinnity
Documentation
// src/vulkan/shaders/raymarch_volumetric_template.glsl
//
// Engine-shipped volumetric fragment template for raymarched SDF volumes on
// Vulkan. Appended to the user's fragment shader at compile time (after the
// helpers + the user's `sampleVolume` definition). GLSL port of
// `raymarch_volumetric_fragment` in directx/shaders/raymarch_volumetric_template.hlsl
// and the Metal counterpart: same march + Beer-Lambert integration, same
// alpha-blend-over-scene compositing.
//
// The user shader defines `sampleVolume` instead of `map` / `shade`; glslang
// prunes the unused surface helpers (`sdfNormal`, `coneRaymarch`) along with
// their forward-declared `map` calls, so the volumetric author needs no surface
// stubs.
//
// March + integration: the proxy vertex shader rasterised the bounding-box back
// faces, so each fragment seeds a ray that pierces the box. We step linearly
// from box entry to exit (`vol_max_steps` samples), accumulate Beer-Lambert
// transmittance front-to-back, and add per-step in-scattered sun light plus
// emission. Output alpha-blends over the rasterised scene (SRC_ALPHA /
// ONE_MINUS_SRC_ALPHA at the pipeline level); no depth write, so the medium
// neither occludes itself nor updates the depth buffer downstream passes read.
//
// V1 limitations (match the DirectX + Metal templates):
//   * No self-shadowing -- the in-scatter term assumes uniform sun illumination
//     through the medium.
//   * No per-pixel scene-depth clamp -- the march extends across the full
//     bounding box. Size the box so it doesn't intersect rasterised geometry the
//     volume is meant to render behind.

layout(location = 0) in vec3 v_world_pos;
layout(location = 0) out vec4 out_color;

void main() {
    vec3 cam = rmview.view_cam_pos;
    vec3 ray_dir = normalize(v_world_pos - cam);

    // Clip the ray to the volume's AABB (slab test; handles camera-inside).
    vec3 box_min = vol.vol_centre - vol.vol_extent;
    vec3 box_max = vol.vol_centre + vol.vol_extent;
    vec2 box_t = rayBox(cam, ray_dir, box_min, box_max);
    if (box_t.y < max(box_t.x, 0.0)) {
        discard;
    }
    float t_enter = max(box_t.x, 0.001);
    float t_exit = min(box_t.y, vol.vol_max_distance);
    if (t_enter >= t_exit) {
        discard;
    }

    int step_count = max(vol.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.
    vec3 sun_radiance = vec3(0.0);
    if (lights.num_dir > 0) {
        sun_radiance = lights.dir[0].col.xyz * lights.dir[0].dir_i.w;
    }

    float transmittance = 1.0;
    vec3 luminance = vec3(0.0);

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

        VolumeSample vs = sampleVolume(p, vol.vol_params, rmview.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.
        vec3 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;
    }

    out_color = vec4(luminance, alpha);
}