Skip to content

Support writing the scene textures from the forward pass - #9166

Merged
mvaligursky merged 2 commits into
mainfrom
mv-scene-texture-write
Aug 13, 2026
Merged

Support writing the scene textures from the forward pass#9166
mvaligursky merged 2 commits into
mainfrom
mv-scene-texture-write

Conversation

@mvaligursky

Copy link
Copy Markdown
Contributor

The scene textures are additional color attachments the scene pass renders alongside the scene color, holding per pixel data - the linear depth to begin with - which the post-processing effects then consume. Their advantage over a depth prepass is that they cost no extra geometry pass, and that blended geometry contributes to them, so gaussian splats can supply a depth the prepass cannot produce at all.

This adds the layer that lets a material write them, and masks off the materials whose shader does not. Nothing enables it: the list of scene textures a camera renders stays empty until a follow up sets up the render target they are attached to, so every generated shader and every render target is unchanged by this on its own.

Changes:

  • CameraShaderParams.sceneTextures holds the ordered names of the scene textures the pass renders, the name at index i going to the color attachment at index i + 1. Each name generates a pair of defines following the same naming as the shader passes - 'depth' gives SCENE_TEXTURE_DEPTH, enabling the write, and {SCENE_TEXTURE_DEPTH_SLOT}, which the preprocessor substitutes into the name of the output written. The value is compared by value on assignment, as the render pass assigns it around every layer step it renders and an equal value must not invalidate the shader variants of every material.
  • A new sceneTexturesPS chunk supplies a write function per scene texture, so the attachment index never appears in the calling code. Custom shaders can use it the way they already use shadowCasterPS, by including the chunk and calling the function. On WebGPU the function takes a pointer to the fragment output, as the outputs there are members of the returned struct.
  • Material.sceneTexturesWrite declares whether a material's shader generates the scene textures, defaulting by material type - true for the opaque StandardMaterial and LitMaterial, false for ShaderMaterial and the particle materials. The additional attachments of a material which does not generate them are masked off using a new getSingleAttachmentBlendState, as an attachment the fragment shader leaves unwritten makes the draw invalid on both backends.
  • RenderPassForward.sceneTextures scopes all of this to the passes rendering to a render target the scene textures are attached to, so that a camera's other passes, for example the one rendering the UI to the output render target, do not write them. The same pass also narrows the camera's clear color to attachment 0, leaving the clear values of the scene textures to whoever owns them.
  • The preprocessor injects the defines before the passes which inspect the source, rather than after. This is required: stripUnusedColorAttachments matches pcFragColorN with a literal index, and would otherwise strip the declaration of an output written through a substituted slot. It is safe for the two passes it now precedes - processArraySize rewrites [KEY] for unbraced int defines while the injection substitutes braced {KEY} keys, which are disjoint patterns, and the injection skips lines containing a preprocessor directive, a set neither of the reordered passes alters.
  • linearDepth is now derived in one expression per material type, instead of being assigned separately by the minimal and the full variant of the standard material options.
  • Unit tests for the new pieces, and for array.equals, which had none.

API Changes:

  • RenderTarget#colorBufferCount - new, the number of color attachments the render target was set up with. The rest of the above is internal (@ignore).

Verified on both backends that the dormant path renders identically. The write path itself was exercised by temporarily forcing a scene texture on, confirming on WebGL2 that the slot is substituted and the output declaration survives the color attachment stripping, and on WebGPU that the write through the pointer compiles and that the generated fragment output struct picks up the additional attachment.

@github-actions

Copy link
Copy Markdown

Build size report

This PR changes the size of the minified bundles.

Bundle Minified Gzip Brotli
playcanvas.min.js 2360.8 KB (+2.2 KB, +0.09%) 606.2 KB (+0.7 KB, +0.12%) 470.8 KB (+0.8 KB, +0.16%)
playcanvas.min.mjs 2358.2 KB (+2.2 KB, +0.09%) 605.3 KB (+0.7 KB, +0.12%) 470.1 KB (+0.4 KB, +0.10%)

@github-actions

Copy link
Copy Markdown

Public API report

This PR changes the public API surface (+1 / −0), per the docs' rules (@ignore / @Private / undocumented are excluded).

Show API diff
+RenderTarget.get colorBufferCount(): number

Informational only — this never fails the build.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds dormant infrastructure for forward-pass scene textures, initially supporting linear depth.

Changes:

  • Adds scene-texture shader defines and GLSL/WGSL write chunks.
  • Adds material write declarations, attachment masking, and render-pass scoping.
  • Adds supporting APIs, preprocessing changes, and unit tests.

Reviewed changes

Copilot reviewed 21 out of 21 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
test/scene/camera-shader-params.test.mjs Tests scene-texture parameters.
test/platform/graphics/blend-state-utils.test.mjs Tests attachment masking.
test/core/array-utils.test.mjs Tests array equality.
src/scene/shader-lib/wgsl/collections/shader-chunks-wgsl.js Registers the WGSL chunk.
src/scene/shader-lib/wgsl/chunks/lit/frag/pass-forward/litForwardMain.js Writes WGSL scene depth.
src/scene/shader-lib/wgsl/chunks/common/frag/scene-textures.js Implements WGSL scene-texture writes.
src/scene/shader-lib/glsl/collections/shader-chunks-glsl.js Registers the GLSL chunk.
src/scene/shader-lib/glsl/chunks/lit/frag/pass-forward/litForwardMain.js Writes GLSL scene depth.
src/scene/shader-lib/glsl/chunks/common/frag/scene-textures.js Implements GLSL scene-texture writes.
src/scene/renderer/render-pass-forward.js Scopes scene textures and clears.
src/scene/renderer/forward-renderer.js Masks unsupported material outputs.
src/scene/particle-system/particle-material.js Disables particle writes.
src/scene/materials/standard-material-options-builder.js Enables forward linear depth.
src/scene/materials/shader-material.js Defaults custom shaders to no writes.
src/scene/materials/material.js Adds scene-texture write policy.
src/scene/materials/lit-material-options-builder.js Enables lit linear depth.
src/scene/constants.js Defines the depth texture name.
src/scene/camera-shader-params.js Generates texture defines and hashing.
src/platform/graphics/render-target.js Exposes color attachment count.
src/platform/graphics/blend-state-utils.js Derives attachment-masked blend states.
src/core/preprocessor.js Reorders define substitution.
Suppressed comments (1)

src/scene/camera-shader-params.js:242

  • Returning the backing array allows mutations to bypass both validation and markDirty(). After hash or defines has been read, params.sceneTextures.push('depth') changes the configured list while leaving both caches stale. Expose an immutable value (for example, freeze the stored copy) or return a defensive snapshot.
    get sceneTextures() {
        return this._sceneTextures;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +663 to +664
const blendState = (attachmentCount > 1 && !material.sceneTexturesWrite) ?
getSingleAttachmentBlendState(material.blendState, attachmentCount) : material.blendState;
Comment on lines +196 to +197
const colorIndex = this.sceneTextures?.length ? 0 : undefined;
this.setClearColor(fullSizeClearRect && step.clearColor ? camera.clearColor : undefined, colorIndex);
Comment on lines +227 to +231
Debug.call(() => {
names.forEach((name) => {
Debug.assert(/^[a-z]\w*$/i.test(name), `Scene texture name can only contain letters, numbers and underscores and start with a letter: ${name}`);
});
});
Comment thread src/scene/materials/material.js
Comment thread src/core/preprocessor.js
Comment on lines +67 to +68
params.sceneTextures = ['depth'];
expect(params.hash).to.equal(hash);
Comment thread src/platform/graphics/render-target.js
Comment thread src/scene/renderer/render-pass-forward.js Outdated
Comment on lines +663 to +664
const blendState = (attachmentCount > 1 && !material.sceneTexturesWrite) ?
getSingleAttachmentBlendState(material.blendState, attachmentCount) : material.blendState;
@mvaligursky
mvaligursky merged commit f06ee5e into main Aug 13, 2026
10 checks passed
@mvaligursky
mvaligursky deleted the mv-scene-texture-write branch August 13, 2026 11:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants