diff --git a/apps/editor/components/editor/custom-camera-controls.tsx b/apps/editor/components/editor/custom-camera-controls.tsx
index 2c3314cd..ac16b411 100644
--- a/apps/editor/components/editor/custom-camera-controls.tsx
+++ b/apps/editor/components/editor/custom-camera-controls.tsx
@@ -26,6 +26,7 @@ export const CustomCameraControls = () => {
useEffect(() => {
camera.layers.enable(EDITOR_LAYER)
raycaster.layers.enable(EDITOR_LAYER)
+ raycaster.layers.enable(2)
}, [camera, raycaster])
useEffect(() => {
diff --git a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx
index 8719d848..4bebc85b 100644
--- a/packages/viewer/src/components/renderers/zone/zone-renderer.tsx
+++ b/packages/viewer/src/components/renderers/zone/zone-renderer.tsx
@@ -28,7 +28,7 @@ const createWallGradientMaterial = (zoneColor: string) => {
colorNode: baseColor,
opacityNode: finalOpacity,
side: DoubleSide,
- depthWrite: false,
+ depthWrite: true,
depthTest: false,
userData: {
uOpacity: opacity,
@@ -244,7 +244,7 @@ export const ZoneRenderer = ({ node }: { node: ZoneNode }) => {
{/* Wall borders with gradient */}
-
+
)
}
diff --git a/packages/viewer/src/components/viewer/index.tsx b/packages/viewer/src/components/viewer/index.tsx
index ebbae641..a0a81d58 100644
--- a/packages/viewer/src/components/viewer/index.tsx
+++ b/packages/viewer/src/components/viewer/index.tsx
@@ -84,7 +84,7 @@ const Viewer: React.FC = ({ children, selectionManager = 'default'
}}
camera={{ position: [50, 50, 50], fov: 50 }}
>
-
+ {/* */}
diff --git a/packages/viewer/src/components/viewer/post-processing.tsx b/packages/viewer/src/components/viewer/post-processing.tsx
index 42a335eb..5bb2d711 100644
--- a/packages/viewer/src/components/viewer/post-processing.tsx
+++ b/packages/viewer/src/components/viewer/post-processing.tsx
@@ -1,6 +1,6 @@
import { useFrame, useThree } from '@react-three/fiber'
import { useEffect, useRef, useState } from 'react'
-import { Color, UnsignedByteType } from 'three'
+import { Color, Layers, UnsignedByteType } from 'three'
import { outline } from 'three/addons/tsl/display/OutlineNode.js'
import { ssgi } from 'three/addons/tsl/display/SSGINode.js'
import { traa } from 'three/addons/tsl/display/TRAANode.js'
@@ -9,6 +9,8 @@ import {
colorToDirection,
diffuseColor,
directionToColor,
+ float,
+ mix,
mrt,
normalView,
oscSine,
@@ -40,12 +42,24 @@ export const SSGI_PARAMS = {
useTemporalFiltering: true,
}
+const DARK_BG = '#1f2433'
+const LIGHT_BG = '#ffffff'
+
const PostProcessingPasses = () => {
const { gl: renderer, scene, camera } = useThree()
const renderPipelineRef = useRef(null)
const hasPipelineErrorRef = useRef(false)
const [isInitialized, setIsInitialized] = useState(false)
+ const theme = useViewer((s) => s.theme)
+
+ // Background color uniform — updated every frame via lerp, read by the TSL pipeline.
+ // Initialised from the current theme so there's no flash on first render.
+ const initBg = theme === 'dark' ? DARK_BG : LIGHT_BG
+ const bgUniform = useRef(uniform(new Color(initBg)))
+ const bgCurrent = useRef(new Color(initBg))
+ const bgTarget = useRef(new Color())
+
useEffect(() => {
let mounted = true
@@ -111,6 +125,18 @@ const PostProcessingPasses = () => {
return colorToDirection(scenePassNormal.sample(uv))
})
+ // camera.layers.disable(0)
+ // camera.layers.disable(1)
+ // camera.layers.enable(2) // Enable layer 2 for zone rendering
+ const zonePass = pass(scene, camera)
+ // camera.layers.enable(0) // Disable layer 2 for main scene rendering
+ // camera.layers.enable(1) // Disable layer 2 for main scene rendering
+ // camera.layers.disable(2) // Disable layer 2 for main scene rendering
+
+ const layers: Layers = new Layers()
+ layers.enable(2) // Enable layer 2 for zone rendering
+ layers.disable(0)
+ zonePass.setLayers(layers)
// SSGI Pass (cast to PerspectiveCamera for SSGI)
const giPass = ssgi(scenePassColor, scenePassDepth, sceneNormal, camera as any)
@@ -130,10 +156,17 @@ const PostProcessingPasses = () => {
const gi = giPass.rgb
const ao = giPass.a
+ // Background detection via alpha: renderer clears with alpha=0 (setClearAlpha(0) in useFrame),
+ // so background pixels have scenePassColor.a=0 while geometry pixels have output.a=1.
+ // WebGPU only applies clearColorValue to MRT attachment 0 (output), so scenePassColor.a
+ // is the reliable geometry mask — no normals, no flicker.
+ const hasGeometry = scenePassColor.a
+ const contentAlpha = hasGeometry.max(zonePass.a)
+
// Composite: scene * AO + diffuse * GI
const compositePass = vec4(
- add(scenePassColor.rgb.mul(ao), scenePassDiffuse.rgb.mul(gi)),
- scenePassColor.a,
+ add(scenePassColor.rgb.mul(ao), add(zonePass.rgb, scenePassDiffuse.rgb.mul(gi))),
+ contentAlpha,
)
function generateSelectedOutlinePass() {
@@ -194,7 +227,17 @@ const PostProcessingPasses = () => {
: vec4(add(scenePassColor.rgb, selectedOutlinePass.add(hoverOutlinePass)), scenePassColor.a)
// TRAA (Temporal Reprojection Anti-Aliasing) - applied AFTER combining everything
- const finalOutput = traa(compositeWithOutlines, scenePassDepth, scenePassVelocity, camera)
+ const traaOutput = traa(compositeWithOutlines, scenePassDepth, scenePassVelocity, camera)
+
+ // For zone-over-background pixels, scenePassDepth=1.0 (no scene geometry) causes TRAA
+ // to output black. Use hasGeometry to blend: geometry pixels use traaRgb, all others
+ // (zones over background, pure background) use compositePass.rgb directly.
+ const traaRgb = (traaOutput as any).rgb
+ const colorSource = mix(compositePass.rgb, traaRgb, hasGeometry)
+ const finalOutput = vec4(
+ mix(bgUniform.current, colorSource, contentAlpha),
+ float(1),
+ )
const renderPipeline = new RenderPipeline(renderer as unknown as WebGPURenderer)
renderPipeline.outputNode = finalOutput
@@ -219,12 +262,20 @@ const PostProcessingPasses = () => {
}
}, [renderer, scene, camera, isInitialized])
- useFrame(() => {
+ useFrame((_, delta) => {
+ // Animate background colour toward the current theme target (same lerp as AnimatedBackground)
+ bgTarget.current.set(theme === 'dark' ? DARK_BG : LIGHT_BG)
+ bgCurrent.current.lerp(bgTarget.current, Math.min(delta, 0.1) * 4)
+ bgUniform.current.value.copy(bgCurrent.current)
+
if (hasPipelineErrorRef.current || !renderPipelineRef.current) {
return
}
try {
+ // Clear alpha=0 so background pixels in the output MRT attachment (index 0) get a=0,
+ // making scenePassColor.a a reliable geometry mask (geometry pixels write a=1 via output node).
+ ;(renderer as any).setClearAlpha(0)
renderPipelineRef.current.render()
} catch (error) {
hasPipelineErrorRef.current = true