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