From 62f8f5e208da35079d92b6f0ac10fab9c6ea0299 Mon Sep 17 00:00:00 2001 From: Martin Valigursky Date: Mon, 17 Aug 2026 11:55:54 +0200 Subject: [PATCH] Add a scene depth mode to the CameraFrame debug rendering The debug rendering could display the scene colour, the bloom, the SSAO, the vignette and both halves of the depth of field, but not the depth those effects are driven by - so anything depth related had to be inspected with a hand written material sampling uSceneDepthMap, which is what the gaussian splatting examples ended up doing. Adds 'depth', displaying the linear scene depth as a ramp over the camera's clip range. Like every other mode it only displays what the frame already produces: it does not request the depth, so a frame with no depth consumer renders black rather than quietly turning a producer on. That is what the internal depthmissing mode is - the requested mode resolves to it when nothing has published a depth, and a debug build explains the black screen once. The compose pass had no camera until now, so it takes the camera component for the depth encoding defines and the clip range, and sets camera_params only for this mode, leaving the rest of the composition untouched. The screenDepthPS include is inside the depth branch, as declaring the sampler in a frame with no depth bound to it is an error. Co-Authored-By: Claude Opus 5 --- scripts/esm/camera-frame.mjs | 3 +- src/extras/render-passes/camera-frame.js | 7 ++- .../render-passes/frame-pass-camera-frame.js | 15 ++++- .../render-passes/render-pass-compose.js | 63 ++++++++++++++++++- .../render-pass/frag/compose/compose.js | 15 +++++ .../render-pass/frag/compose/compose.js | 15 +++++ 6 files changed, 111 insertions(+), 7 deletions(-) diff --git a/scripts/esm/camera-frame.mjs b/scripts/esm/camera-frame.mjs index 71dd2c502de..f9fa40a9c38 100644 --- a/scripts/esm/camera-frame.mjs +++ b/scripts/esm/camera-frame.mjs @@ -39,7 +39,8 @@ const DebugType = { BLOOM: 'bloom', VIGNETTE: 'vignette', DOFCOC: 'dofcoc', - DOFBLUR: 'dofblur' + DOFBLUR: 'dofblur', + DEPTH: 'depth' }; /** diff --git a/src/extras/render-passes/camera-frame.js b/src/extras/render-passes/camera-frame.js index f0014e8e2ea..e39a09cb31e 100644 --- a/src/extras/render-passes/camera-frame.js +++ b/src/extras/render-passes/camera-frame.js @@ -453,9 +453,12 @@ class CameraFrame { }; /** - * Debug rendering. Set to null to disable. + * Debug rendering, which displays an intermediate value of the frame in place of the composed + * result. This never changes what the frame renders - a mode whose value this frame does not + * generate simply displays nothing: 'depth' renders black when no effect has produced the scene + * depth, and the modes of a disabled effect are ignored. Set to null to disable. * - * @type {null|'scene'|'ssao'|'bloom'|'vignette'|'dofcoc'|'dofblur'} + * @type {null|'scene'|'ssao'|'bloom'|'vignette'|'dofcoc'|'dofblur'|'depth'} */ debug = null; diff --git a/src/extras/render-passes/frame-pass-camera-frame.js b/src/extras/render-passes/frame-pass-camera-frame.js index 45c6aafaa5c..d29c7f5f49f 100644 --- a/src/extras/render-passes/frame-pass-camera-frame.js +++ b/src/extras/render-passes/frame-pass-camera-frame.js @@ -878,7 +878,7 @@ class FramePassCameraFrame extends FramePass { setupComposePass(options) { // create a compose pass, which combines the results of the scene and other passes - this.composePass = new RenderPassCompose(this.device); + this.composePass = new RenderPassCompose(this.device, this.cameraComponent); this.composePass.bloomTexture = this.bloomPass?.bloomTexture; this.composePass.hdrScene = this.hdrFormat !== PIXELFORMAT_RGBA8; this.composePass.taaEnabled = options.taaEnabled; @@ -919,6 +919,19 @@ class FramePassCameraFrame extends FramePass { super.frameUpdate(); + // Whether the depth debug mode has a depth to display. Either producer publishes to the same + // uniform, and both have run by the time the composition does. The mode does not request the + // depth - a debug view never changes what is rendered - so with neither producer it shows black. + const { options, composePass } = this; + const sceneDepthAvailable = options.sceneTextureDepth || options.prepassEnabled; + composePass.sceneDepthAvailable = sceneDepthAvailable; + + Debug.call(() => { + if (composePass.debug === 'depth' && !sceneDepthAvailable) { + Debug.warnOnce('CameraFrame.debug is set to \'depth\', but nothing this camera renders produces the scene depth, so the debug view is black. Enable an effect which consumes the depth (the depth of field, the volumetric fog, TAA, or SSAO in combine mode), or request it with CameraFrame.rendering.sceneDepthMap.'); + } + }); + if (this.sceneDepthTexture) { // the alias of the scene color is not resized by a pass of its own, as it shares its diff --git a/src/extras/render-passes/render-pass-compose.js b/src/extras/render-passes/render-pass-compose.js index 972ad893a82..ac5bda33045 100644 --- a/src/extras/render-passes/render-pass-compose.js +++ b/src/extras/render-passes/render-pass-compose.js @@ -11,6 +11,8 @@ import { composeChunksGLSL } from '../../scene/shader-lib/glsl/collections/compo import { composeChunksWGSL } from '../../scene/shader-lib/wgsl/collections/compose-chunks-wgsl.js'; /** + * @import { CameraComponent } from '../../framework/components/camera/component.js'; + * @import { GraphicsDevice } from '../../platform/graphics/graphics-device.js'; * @import { Texture } from '../../platform/graphics/texture.js'; */ @@ -108,6 +110,8 @@ class RenderPassCompose extends RenderPassShaderQuad { _debug = null; + _sceneDepthAvailable = false; + // track user-provided custom compose chunks _customComposeChunks = new Map([ ['composeDeclarationsPS', ''], @@ -115,9 +119,16 @@ class RenderPassCompose extends RenderPassShaderQuad { ['composeMainEndPS', ''] ]); - constructor(graphicsDevice) { + /** + * @param {GraphicsDevice} graphicsDevice - The graphics device. + * @param {CameraComponent} cameraComponent - The camera this composes the frame of. Only the depth + * debug mode needs it, for the depth encoding the camera renders and its clip range. + */ + constructor(graphicsDevice, cameraComponent) { super(graphicsDevice); + this.cameraComponent = cameraComponent; + // register compose shader chunks ShaderChunks.get(graphicsDevice, SHADERLANGUAGE_GLSL).add(composeChunksGLSL, false); ShaderChunks.get(graphicsDevice, SHADERLANGUAGE_WGSL).add(composeChunksWGSL, false); @@ -144,6 +155,8 @@ class RenderPassCompose extends RenderPassShaderQuad { this.colorEnhanceParamsId = scope.resolve('colorEnhanceParams'); this.colorEnhanceMidtonesId = scope.resolve('colorEnhanceMidtones'); this.composeTargetFlipYId = scope.resolve('composeTargetFlipY'); + this.cameraParams = new Float32Array(4); + this.cameraParamsId = scope.resolve('camera_params'); } set debug(value) { @@ -157,6 +170,37 @@ class RenderPassCompose extends RenderPassShaderQuad { return this._debug; } + /** + * Whether the scene depth this frame renders is available to sample, which the depth debug mode + * displays instead of producing a depth of its own - a debug mode never changes what is rendered. + * + * @type {boolean} + */ + set sceneDepthAvailable(value) { + if (this._sceneDepthAvailable !== value) { + this._sceneDepthAvailable = value; + this._shaderDirty = true; + } + } + + get sceneDepthAvailable() { + return this._sceneDepthAvailable; + } + + /** + * The debug mode the shader is built for. This is the requested mode, except that a request for the + * depth with no depth to sample renders black instead. + * + * @type {string|null} + * @private + */ + get _debugMode() { + if (this._debug === 'depth' && !this._sceneDepthAvailable) { + return 'depthmissing'; + } + return this._debug; + } + set colorLUT(value) { if (this._colorLUT !== value) { this._colorLUT = value; @@ -365,6 +409,12 @@ class RenderPassCompose extends RenderPassShaderQuad { const startHash = hashCode(customChunks.get('composeMainStartPS') ?? ''); const endHash = hashCode(customChunks.get('composeMainEndPS') ?? ''); + // the depth debug mode samples the scene depth, whose encoding varies with what produced it + const debugMode = this._debugMode; + const depthDefines = new Map(); + const depthKey = debugMode === 'depth' ? + ShaderUtils.addScreenDepthChunkDefines(this.cameraComponent.shaderParams, depthDefines) : ''; + const key = `${this.toneMapping}` + `-${gammaCorrectionName}` + @@ -380,7 +430,7 @@ class RenderPassCompose extends RenderPassShaderQuad { `-${this.fringingEnabled ? 'fringing' : 'nofringing'}` + `-${this.taaEnabled ? 'taa' : 'notaa'}` + `-${this.isSharpnessEnabled ? (this._hdrScene ? 'cashdr' : 'cas') : 'nocas'}` + - `-${this._debug ?? ''}` + + `-${debugMode ?? ''}${depthKey}` + `-decl${declHash}-start${startHash}-end${endHash}`; if (this._key !== key) { @@ -404,7 +454,8 @@ class RenderPassCompose extends RenderPassShaderQuad { defines.set('CAS', true); if (this._hdrScene) defines.set('CAS_HDR', true); } - if (this._debug) defines.set('DEBUG_COMPOSE', this._debug); + if (debugMode) defines.set('DEBUG_COMPOSE', debugMode); + depthDefines.forEach((value, name) => defines.set(name, value)); this.shader = ShaderUtils.createShader(this.device, { uniqueName: `ComposeShader-${key}`, @@ -419,6 +470,12 @@ class RenderPassCompose extends RenderPassShaderQuad { execute() { + // the clip range the depth debug mode maps to its ramp, and what the depth chunk linearizes + // with. Only set for that mode, so the rest of the composition leaves the camera state alone. + if (this._debugMode === 'depth') { + this.cameraParamsId.setValue(this.cameraComponent.camera.fillShaderParams(this.cameraParams)); + } + const sceneTex = this.sceneTexture; this.sceneTextureId.setValue(sceneTex); this.sceneTextureInvResValue[0] = 1.0 / sceneTex.width; diff --git a/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose.js b/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose.js index 5abf0187cfc..626e046cf62 100644 --- a/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose.js +++ b/src/scene/shader-lib/glsl/chunks/render-pass/frag/compose/compose.js @@ -17,6 +17,14 @@ export default /* glsl */` #include "composeCasPS" #include "composeColorLutPS" + // The depth debug mode displays a depth some other pass in this frame has already produced - the + // debug modes never turn any rendering on, so the mode is switched to depthmissing when nothing + // did, see RenderPassCompose. That is also why this is included here rather than unconditionally: + // declaring the depth sampler in a frame with no depth to bind to it is an error. + #if DEBUG_COMPOSE == depth + #include "screenDepthPS" + #endif + #include "composeDeclarationsPS" void main() { @@ -94,6 +102,13 @@ export default /* glsl */` result = vec3(dSsao); #elif defined(VIGNETTE) && DEBUG_COMPOSE == vignette result = vec3(dVignette); + #elif DEBUG_COMPOSE == depth + // a linear ramp over the camera clip range + float dDepth = getLinearScreenDepth(uv); + result = vec3(clamp((dDepth - camera_params.z) / (camera_params.y - camera_params.z), 0.0, 1.0)); + #elif DEBUG_COMPOSE == depthmissing + // the depth was asked for while nothing in this frame produces it + result = vec3(0.0); #endif #endif diff --git a/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose.js b/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose.js index a931c097e1e..7efaf487ebd 100644 --- a/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose.js +++ b/src/scene/shader-lib/wgsl/chunks/render-pass/frag/compose/compose.js @@ -18,6 +18,14 @@ export default /* wgsl */` #include "composeCasPS" #include "composeColorLutPS" + // The depth debug mode displays a depth some other pass in this frame has already produced - the + // debug modes never turn any rendering on, so the mode is switched to depthmissing when nothing + // did, see RenderPassCompose. That is also why this is included here rather than unconditionally: + // declaring the depth sampler in a frame with no depth to bind to it is an error. + #if DEBUG_COMPOSE == depth + #include "screenDepthPS" + #endif + #include "composeDeclarationsPS" @fragment @@ -98,6 +106,13 @@ export default /* wgsl */` result = vec3f(dSsao); #elif defined(VIGNETTE) && DEBUG_COMPOSE == vignette result = vec3f(dVignette); + #elif DEBUG_COMPOSE == depth + // a linear ramp over the camera clip range + let dDepth = getLinearScreenDepth(uv); + result = vec3f(clamp((dDepth - uniform.camera_params.z) / (uniform.camera_params.y - uniform.camera_params.z), 0.0, 1.0)); + #elif DEBUG_COMPOSE == depthmissing + // the depth was asked for while nothing in this frame produces it + result = vec3f(0.0); #endif #endif