Opaque colour capture & refraction
Let a transparent draw — water, glass, heat haze — sample the opaque scene behind it and distort it. The engine renders the opaque scene, copies it into a capture texture, then renders the transparent draws, which read that texture at a screen-space UV and offset it to refract (engine ADR 0075 / PLM-238).
It is opt-in: Graphics.opaqueCaptureEnabled(true) splits
the offscreen scene render into opaque → copy → transparent; off, the scene renders in one pass.
Companion guides: Full-screen post effects & scene depth · Frame clock & wind.
// apps/<app>/shaders/src/water.slang — a MESH shader (the mesh ABI, not the post ABI)#include "plume3d.slang"struct VSOutput { float4 PositionCS : SV_Position; float2 ScreenUV : TEXCOORD0; float4 Color : COLOR0; };
[shader("vertex")]VSOutput vertex(VSInput input) { // VSInput = the shared mesh vertex layout VSOutput o; o.PositionCS = float4(input.Position, 1.0); o.ScreenUV = input.Position.xy * 0.5 + 0.5; // NDC → [0,1] screen UV o.Color = input.Color; return o;}
[shader("fragment")]float4 fragment(VSOutput input) : SV_Target { float2 wobble = float2(sin(input.ScreenUV.y * 30 + plume3d_time() * 3), 0) * 0.03; float4 behind = plume3d_opaqueColor(input.ScreenUV + wobble); // the captured opaque scene return float4(behind.rgb * float3(0.65, 0.82, 1.0), 1.0); // cool water tint}// main.wren — draw the opaque scene, then the transparent surface; enable capture + a post effectGraphics.opaqueCaptureEnabled(true) // split opaque → copy → transparent_water.blendMode = "alpha" // classify the surface as transparent// draw() each frame:for (b in _bands) Graphics.drawMesh(b) // opaque sceneGraphics.drawMesh(_water) // transparent surface, samples the opaque copyGraphics.addPostEffect("shaders/present", []) // passthrough — capture needs the offscreen pathHow it works
Section titled “How it works”Graphics.opaqueCaptureEnabled(true)turns on the split.- On the offscreen path, the engine renders the opaque meshes (+ instanced) into the scene colour, copies that into a separate capture texture, then renders the transparent meshes into a second pass.
- Transparent shaders read the capture through the mesh ABI at set 0 binding 6:
plume3d_opaqueColor(screenUv)— sample with a screen-space UV (fragment position ÷ screen size, or NDC · 0.5 + 0.5 passed from the vertex stage). Offset the UV to refract.
Rules & gotchas
Section titled “Rules & gotchas”- A surface must be transparent to see the opaque scene. Set
mesh.blendMode = "alpha"(or a translucent material) — only the transparent phase samples the capture. An opaque draw would read itself, which is why the phases are split. - Opaque capture requires the offscreen path. Pair it with at least one
Graphics.addPostEffect(a passthrough is fine). Without a post effect, binding 6 stays the 1×1 white default and the surface reads white (no refraction). - Single camera. Like the post stack, capture is single-camera (not split-screen).
- Sample with a screen UV, not the mesh UV. The capture is screen-space; a model-space UV won’t line up with the scene behind the surface.
- It is a colour texture —
Sampler2D+.Sample()(unlike scene depth, read by.Load()). - Use the mesh ABI, not the post ABI. A refraction surface is a mesh shader
(
#include "plume3d.slang");plume3d_opaqueColorlives there, not inplume3d_post.slang.
Verify it works
Section titled “Verify it works”plume3d apps/refraction_demo # four opaque bands behind a transparent quad, then screenshotsThe reference (refraction example): the band boundaries are straight outside the quad and wavy inside it, because the quad samples the captured bands at a wobbled UV. With capture off (or no post effect) the quad reads white instead.
Related
Section titled “Related”- API: Graphics → Opaque colour capture (A#3).
- Example: Refraction (
apps/refraction_demo). - Companion guides: Full-screen post effects & scene depth · Frame clock & wind.