Scene Depth
App: apps/scene_depth_demo/
Demonstrates scene depth in the mesh ABI (engine PLM-263 / ADR 0089). A mesh shader —
typically a water surface — can now read the depth of the opaque scene behind and under it at
set 0 binding 5, the enabler for shoreline blend, intersection foam, and
depth-tinted colour. With
Graphics.opaqueCaptureEnabled(true) the engine copies
the opaque-phase depth into a separate image after the opaque phase; a transparent draw
(blendMode = "alpha") then samples that copy — so it reads the scene behind it without touching
the live depth attachment it is being tested against.
This mirrors Refraction (opaque colour at binding 6, A#3) and Planar Reflection (reflection at binding 7, A#4): the same opaque-capture machinery, one more global feed on the mesh set-0 ABI — but this one carries depth.
Run from root
Section titled “Run from root”% ./plume3d scene_depth_demoFour opaque colour bands sit at four different depths (near→far, left→right); one transparent
quad floats in front of them. The quad reads the opaque depth behind each pixel and paints a
shoreline: foam-white where a band sits close behind the surface, grading to deep blue
where only the cleared background is behind it. Because the bands are at different depths, the
shoreline tracks across the quad — that varying gradient is the proof the shader reads the actual
per-pixel scene depth (a constant / default read would make the whole quad uniform). The demo
captures a screenshot (scene_depth.png) shortly after startup, then exits.
The mesh ABI (set 0 binding 5)
Section titled “The mesh ABI (set 0 binding 5)”A mesh shader reads scene depth through the canonical mesh ABI — #include "plume3d.slang" — with
two accessors:
// Raw device depth [0,1] at a screen pixel — pass the fragment's SV_Position.xy.float plume3d_sceneDepthLoad(int2 px);
// Linearize a raw device depth to an eye-space distance. The mesh ABI carries no near/far,// so the caller passes its own camera planes.float plume3d_linearizeDepth(float raw, float nearZ, float farZ);Read it with the fragment’s screen pixel coordinate — the integer SV_Position.xy, not a
normalized UV:
#include "plume3d.slang"
[shader("fragment")]float4 fragment(VSOutput input) : SV_Target { int2 px = int2(input.PositionCS.xy); // screen pixel float d = plume3d_sceneDepthLoad(px); // raw opaque-scene depth [0,1] behind this pixel
// "Water depth" = how far the opaque scene is behind the surface → shoreline blend. float waterZ = 0.3; float gap = saturate((d - waterZ) * 2.2); float3 col = lerp(float3(0.90, 0.97, 1.0), // foam (shallow) float3(0.05, 0.28, 0.55), // deep water gap); return float4(col, 1.0);}Binding 5 is a Texture2D<float> read via .Load() (a texel fetch), not a combined sampler:
a D32 depth image sampled through a SamplerState generates an undeclared split sampler in MSL on
MoltenVK, so .Load is the safe form (the same reason the post pass reads depth this way). The
binding is always declared in the shared ABI (the PLM-178 superset idiom) — a mesh shader that
never reads it is unaffected, and Slang strips the unused resource.
The gate — opaqueCaptureEnabled
Section titled “The gate — opaqueCaptureEnabled”Scene depth rides the same gate as opaque colour (A#3). Each frame in draw(), enable capture,
draw the opaque geometry, then the transparent surface, and add a post effect so the scene renders
offscreen (opaque capture needs the offscreen path — a passthrough present is fine):
Graphics.opaqueCaptureEnabled(true) // split opaque → copy → transparent (colour AND depth)for (b in _bands) Graphics.drawMesh(b) // opaque phase (pass 1) → writes the depth bufferGraphics.drawMesh(_quad) // transparent phase (pass 2) → reads binding 5Graphics.addPostEffect("shaders/present", []) // passthrough present drives the offscreen pathThe transparent quad is classified into the second (post-copy) phase by its blend mode:
_quad = Graphics.newMesh(q, "trianglestrip", _water)_quad.blendMode = "alpha" // transparent → renders after the opaque depth copyWith capture off (or no post effect) binding 5 is the 1×1 default depth, so the quad reads a single flat value and shows no banding — inert, never a crash.
How it works
Section titled “How it works”A water surface draws in the transparent phase of the same scene pass that owns the depth buffer,
so it cannot sample the live depth attachment directly — that is depth-attachment feedback
(reading the attachment being depth-tested against). The engine solves it exactly as A#3 solved it for
colour: after the opaque phase (only when opaque capture is on) it copies the scene depth into a
separate opaqueDepth_ image and transitions it to read-only; the transparent phase samples the
copy at binding 5. There is no extra cost when opaque capture is off — the binding stays on the
1×1 default and no copy runs.
The Water pack enabler
Section titled “The Water pack enabler”Reading the opaque scene depth from a mesh shader is the primitive a stylized water surface is built on. It composes with the render foundations already shipped:
- Waves — the global frame + wind uniform (A#1) animate the surface.
- Shoreline / foam / depth tint — this (binding 5): fade toward foam where the opaque scene is close behind the surface, tint deeper water by the depth gap.
- Refraction — opaque colour at binding 6 (A#3): sample the scene behind the surface and distort it.
- Reflection — planar reflection at binding 7 (A#4): mirror the scene above the plane.
- No new Wren surface. This is a render-foundation (mesh-ABI) capability, not a scripting change:
the gate is the existing
Graphics.opaqueCaptureEnabled, the accessors live in the sharedplume3d.slangABI, and existing shaders are unaffected (the binding is additive and inert unless read). plume3d_sceneDepthLoadreads raw device depth ([0,1], Vulkan clip z). For an eye-space distance (metric shoreline width, fog-by-depth), pass your camera’s near/far toplume3d_linearizeDepth.- The post ABI (
plume3d_post.slang) has exposed sampleable scene depth since A#2 — see the post effects + scene depth guide and the depth-edge outline in the Toon pack. PLM-263 brings the same read to mesh shaders, so a transparent surface can use it mid-scene.
See Graphics.opaqueCaptureEnabled. Screenshot-verified
(the shoreline tracks the bands’ varying depth).