Skip to content

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 effect
Graphics.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 scene
Graphics.drawMesh(_water) // transparent surface, samples the opaque copy
Graphics.addPostEffect("shaders/present", []) // passthrough — capture needs the offscreen path
  1. Graphics.opaqueCaptureEnabled(true) turns on the split.
  2. 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.
  3. 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.
  • 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_opaqueColor lives there, not in plume3d_post.slang.
Terminal window
plume3d apps/refraction_demo # four opaque bands behind a transparent quad, then screenshots

The 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.