The scene-pass split that lets refract_data glass refract the emissive data behind
it while the data in FRONT of it stays crisp (spec MESH_PHYSICAL_MATERIALS §3.4,
Phase 3), on both backends.
Two facts force a multi-pass frame. Every Luxar point, line and splat material is
transparent and writes no depth (additive mode has the depth test off altogether), so
a glass drawn after the data cannot tell a lattice row in front of it from one behind
it — and three's WebGL renderTransmissionPass draws only the OPAQUE render list
into the texture a transmissive material samples, so on that backend the glass could
not see the data at all in one pass. The split therefore renders, per frame with a
visible refracting glass:
Pass G — the refracting glass's FRONT faces, depth only, into
DataRefractionSplit.glassDepthTarget (cleared depth 1.0 = "no glass here").
Drawn through depth-only PROXY meshes in a private scene (geometry shared, world
matrix copied — the pick system's pattern), so the glass mesh's own material and
side never matter and the main scene is not traversed a third time.
Pass A — everything except the refracting glass, with every data material in
partition mode 1 (glass-partition.ts): each fragment compares its own window
depth with the glass depth at its pixel and keeps itself only when it is BEHIND the
glass or under no glass at all. Meshes three's own materials draw (physical glass
without the flag, physical opaque surfaces) cannot classify themselves; they are
drawn here in full and excluded from pass C via RENDER_LAYER_UNPARTITIONED.
Blit (WebGL only) — pass A's colour into a copy target: pass B must read it
while writing the HDR target, and a texture cannot be both.
Pass B — only the glass (plus, on WebGL, one screen-space quad textured with
the copy: an OPAQUE object, so three's transmission pass draws it into the
transmission texture and the glass refracts the data), into the HDR target WITHOUT
clearing. On WebGPU three samples the live framebuffer, so the glass alone is
enough. Pass A's depth survives, so an opaque mesh in front still occludes the
glass.
Pass C — the data again in partition mode 2: only the fragments IN FRONT of the
glass survive, and they land crisp on top of it. Modes 1 and 2 are complements of
the one predicate, so every data fragment is drawn exactly once across A and C.
The one-layer depth map represents only the nearest glass front face. Overlapping
refracting glasses can paint over data in front of the farther glass; viewed from
inside a double-sided refracting glass, the front-face proxy writes no depth and
the shell can paint over enclosed data.
Under MSAA the copies are load-bearing, not an optimisation. Three's WebGL
renderer resolves a multisampled target's colour to its texture at the end of every
render() and then INVALIDATES the multisampled colour attachment (the depth
attachment is resolved and kept), so passes B and C would each draw onto undefined
colour. The pass-B quad repaints it from the copy; before pass C a second blit and a
second full-screen quad (leading the opaque list) do the same. The four quad flags —
depthTest: false, depthWrite: false, transparent: false, frustumCulled: false
— are pinned by test. WebGPURenderer's native backend keeps its multisampled colour
between passes; its WebGL2 fallback does not and cannot take the GLSL quad, so that
one combination (?webgpuForceWebgl + MSAA, both diagnostic switches) falls back
to the single pass and says so once.
Passes B and C run with scene.background taken away. Both renderers FORCE a
clear at the start of every render() whose scene has a colour background, whatever
autoClear says (three's WebGLBackground / Background node set forceClear), and
a texture background is redrawn over everything as an opaque mesh — either way the
frame pass A just drew would be gone. Pass A keeps the background, so the frame is
still cleared to the configured colour exactly once.
Everything the split touches is restored in a finally before render() returns —
the partition mode FIRST (an exception must never leave the data materials
discarding), then the glass and unpartitioned meshes' layer masks, the camera mask,
autoClear, the scene background, the renderer's transmissionResolutionScale, and
the quads' membership of the scene — so the pick pass, the environment cube capture,
the blend warm-up and scene disposal never see any of it. The scene holds no lights
today; a future light would need layers.enableAll() to reach pass B.
transmissionResolutionScale (config renderingControls.refraction) applies to
pass B only: three reads it per transmission pass, so Phase 2 glass in pass A keeps
three's default and its pixels are untouched.
The glass depth target is created WITH the one shared DepthTexture every data
material already samples (getGlassDepthTexture), so no material is ever rebound.
The target is PRIMED (bound and cleared) at the start of the first split-capable
frame and after every resize, before any material's first bind in that frame, so the
GPU texture a bind group captures is always the full-size one.
The scene-pass split that lets
refract_dataglass refract the emissive data behind it while the data in FRONT of it stays crisp (spec MESH_PHYSICAL_MATERIALS §3.4, Phase 3), on both backends.Two facts force a multi-pass frame. Every Luxar point, line and splat material is transparent and writes no depth (additive mode has the depth test off altogether), so a glass drawn after the data cannot tell a lattice row in front of it from one behind it — and three's WebGL
renderTransmissionPassdraws only the OPAQUE render list into the texture a transmissive material samples, so on that backend the glass could not see the data at all in one pass. The split therefore renders, per frame with a visible refracting glass:sidenever matter and the main scene is not traversed a third time.glass-partition.ts): each fragment compares its own window depth with the glass depth at its pixel and keeps itself only when it is BEHIND the glass or under no glass at all. Meshes three's own materials draw (physical glass without the flag, physical opaque surfaces) cannot classify themselves; they are drawn here in full and excluded from pass C via RENDER_LAYER_UNPARTITIONED.Under MSAA the copies are load-bearing, not an optimisation. Three's WebGL renderer resolves a multisampled target's colour to its texture at the end of every
render()and then INVALIDATES the multisampled colour attachment (the depth attachment is resolved and kept), so passes B and C would each draw onto undefined colour. The pass-B quad repaints it from the copy; before pass C a second blit and a second full-screen quad (leading the opaque list) do the same. The four quad flags —depthTest: false,depthWrite: false,transparent: false,frustumCulled: false— are pinned by test.WebGPURenderer's native backend keeps its multisampled colour between passes; its WebGL2 fallback does not and cannot take the GLSL quad, so that one combination (?webgpuForceWebgl+ MSAA, both diagnostic switches) falls back to the single pass and says so once.Passes B and C run with
scene.backgroundtaken away. Both renderers FORCE a clear at the start of everyrender()whose scene has a colour background, whateverautoClearsays (three'sWebGLBackground/Backgroundnode setforceClear), and a texture background is redrawn over everything as an opaque mesh — either way the frame pass A just drew would be gone. Pass A keeps the background, so the frame is still cleared to the configured colour exactly once.Everything the split touches is restored in a
finallybeforerender()returns — the partition mode FIRST (an exception must never leave the data materials discarding), then the glass and unpartitioned meshes' layer masks, the camera mask,autoClear, the scene background, the renderer'stransmissionResolutionScale, and the quads' membership of the scene — so the pick pass, the environment cube capture, the blend warm-up and scene disposal never see any of it. The scene holds no lights today; a future light would needlayers.enableAll()to reach pass B.transmissionResolutionScale(configrenderingControls.refraction) applies to pass B only: three reads it per transmission pass, so Phase 2 glass in pass A keeps three's default and its pixels are untouched.The glass depth target is created WITH the one shared
DepthTextureevery data material already samples (getGlassDepthTexture), so no material is ever rebound. The target is PRIMED (bound and cleared) at the start of the first split-capable frame and after every resize, before any material's first bind in that frame, so the GPU texture a bind group captures is always the full-size one.