diff --git a/examples/files.json b/examples/files.json
index 0bd680b2a98e5c..8698b75f0a973e 100644
--- a/examples/files.json
+++ b/examples/files.json
@@ -460,6 +460,7 @@
"webgpu_postprocessing_traa",
"webgpu_postprocessing_transition",
"webgpu_postprocessing",
+ "webgpu_postprocessing_direct",
"webgpu_procedural_texture",
"webgpu_reflection",
"webgpu_reflection_blurred",
diff --git a/examples/screenshots/webgpu_postprocessing_direct.jpg b/examples/screenshots/webgpu_postprocessing_direct.jpg
new file mode 100644
index 00000000000000..68e817a99075e5
Binary files /dev/null and b/examples/screenshots/webgpu_postprocessing_direct.jpg differ
diff --git a/examples/screenshots/webgpu_xr_native_layers.jpg b/examples/screenshots/webgpu_xr_native_layers.jpg
index 1683140fb19f3a..bbe02c985711a7 100644
Binary files a/examples/screenshots/webgpu_xr_native_layers.jpg and b/examples/screenshots/webgpu_xr_native_layers.jpg differ
diff --git a/examples/screenshots/webgpu_xr_rollercoaster.jpg b/examples/screenshots/webgpu_xr_rollercoaster.jpg
index 4af5da58da9d26..f2c63332607f44 100644
Binary files a/examples/screenshots/webgpu_xr_rollercoaster.jpg and b/examples/screenshots/webgpu_xr_rollercoaster.jpg differ
diff --git a/examples/webgpu_postprocessing_direct.html b/examples/webgpu_postprocessing_direct.html
new file mode 100644
index 00000000000000..bfb35f655d870f
--- /dev/null
+++ b/examples/webgpu_postprocessing_direct.html
@@ -0,0 +1,128 @@
+
+
+
+ three.js webgpu - direct post-processing
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Inline post-processing.
+
+
+
+
+
+
+
+
diff --git a/examples/webgpu_xr_cubes.html b/examples/webgpu_xr_cubes.html
index 44e78e833af689..54a8f2fec1b150 100644
--- a/examples/webgpu_xr_cubes.html
+++ b/examples/webgpu_xr_cubes.html
@@ -43,7 +43,7 @@
const controllerModelFactory = new XRControllerModelFactory();
let container;
- let camera, scene, raycaster, renderer;
+ let camera, scene, raycaster, renderer, renderPipeline;
let room;
@@ -152,6 +152,7 @@
}
renderer = newRenderer;
+ renderPipeline = new THREE.DirectRenderPipeline( renderer );
setupControllers();
}
@@ -317,7 +318,7 @@
}
- renderer.render( scene, camera );
+ renderPipeline.render( scene, camera );
}
diff --git a/examples/webgpu_xr_native_layers.html b/examples/webgpu_xr_native_layers.html
index 78315245482533..1586f41729162a 100644
--- a/examples/webgpu_xr_native_layers.html
+++ b/examples/webgpu_xr_native_layers.html
@@ -46,7 +46,7 @@
import { HTMLMesh } from 'three/addons/interactive/HTMLMesh.js';
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
- let camera, scene, renderer;
+ let camera, scene, renderer, renderPipeline;
let controller1, controller2;
let controllerGrip1, controllerGrip2;
@@ -191,6 +191,7 @@
document.body.appendChild( renderer.domElement );
renderer.setAnimationLoop( render );
renderer.xr.enabled = true;
+ renderPipeline = new THREE.DirectRenderPipeline( renderer );
//
@@ -633,8 +634,6 @@
timer.update();
- renderer.xr.renderLayers( );
-
handleController( controller1 );
handleController( controller2 );
@@ -712,7 +711,7 @@
}
- renderer.render( scene, camera );
+ renderPipeline.render( scene, camera );
}
diff --git a/examples/webgpu_xr_rollercoaster.html b/examples/webgpu_xr_rollercoaster.html
index fe488cf0ae1752..0695c26c3c4886 100644
--- a/examples/webgpu_xr_rollercoaster.html
+++ b/examples/webgpu_xr_rollercoaster.html
@@ -36,7 +36,7 @@
import { setupWebGLXRFallback } from 'three/addons/webxr/WebGLXRFallback.js';
let mesh, material, geometry;
- let renderer;
+ let renderer, renderPipeline;
const rendererParameters = { antialias: true, outputBufferType: THREE.UnsignedByteType, multiview: false };
@@ -79,6 +79,7 @@
}
renderer = newRenderer;
+ renderPipeline = new THREE.DirectRenderPipeline( renderer );
}
@@ -298,7 +299,7 @@
//
- renderer.render( scene, camera );
+ renderPipeline.render( scene, camera );
prevTime = time;
diff --git a/src/Three.WebGPU.Nodes.js b/src/Three.WebGPU.Nodes.js
index 702dafbf079a34..dcb8e7c5ccd184 100644
--- a/src/Three.WebGPU.Nodes.js
+++ b/src/Three.WebGPU.Nodes.js
@@ -9,6 +9,7 @@ export { default as BundleGroup } from './renderers/common/BundleGroup.js';
export { default as QuadMesh } from './renderers/common/QuadMesh.js';
export { default as PMREMGenerator } from './renderers/common/extras/PMREMGenerator.js';
export { default as RenderPipeline } from './renderers/common/RenderPipeline.js';
+export { default as DirectRenderPipeline } from './renderers/common/DirectRenderPipeline.js';
export { default as PostProcessing } from './renderers/common/PostProcessing.js';
export { default as ReadbackBuffer } from './renderers/common/ReadbackBuffer.js';
import * as RendererUtils from './renderers/common/RendererUtils.js';
diff --git a/src/Three.WebGPU.js b/src/Three.WebGPU.js
index e4f6a448e6be6e..5d7825fda1bf15 100644
--- a/src/Three.WebGPU.js
+++ b/src/Three.WebGPU.js
@@ -12,6 +12,7 @@ export { default as BundleGroup } from './renderers/common/BundleGroup.js';
export { default as QuadMesh } from './renderers/common/QuadMesh.js';
export { default as PMREMGenerator } from './renderers/common/extras/PMREMGenerator.js';
export { default as RenderPipeline } from './renderers/common/RenderPipeline.js';
+export { default as DirectRenderPipeline } from './renderers/common/DirectRenderPipeline.js';
export { default as PostProcessing } from './renderers/common/PostProcessing.js';
export { default as ReadbackBuffer } from './renderers/common/ReadbackBuffer.js';
import * as RendererUtils from './renderers/common/RendererUtils.js';
diff --git a/src/renderers/common/DirectRenderPipeline.js b/src/renderers/common/DirectRenderPipeline.js
new file mode 100644
index 00000000000000..da2e3c265cb8b4
--- /dev/null
+++ b/src/renderers/common/DirectRenderPipeline.js
@@ -0,0 +1,233 @@
+import { output, renderOutput, uniform } from '../../nodes/TSL.js';
+import { ColorManagement } from '../../math/ColorManagement.js';
+import { NoToneMapping } from '../../constants.js';
+import RenderPipeline from './RenderPipeline.js';
+
+/**
+ * An alternative render pipeline that applies output processing directly in
+ * material shaders. This avoids the intermediate framebuffer and output pass
+ * used by {@link Renderer}, but changes blending and is not compatible with
+ * materials that sample the framebuffer, such as transmissive materials.
+ *
+ * ```js
+ * const renderPipeline = new DirectRenderPipeline( renderer );
+ * renderPipeline.render( scene, camera );
+ * ```
+ *
+ * Note: This module can only be used with `WebGPURenderer`.
+ *
+ * @augments RenderPipeline
+ */
+class DirectRenderPipeline extends RenderPipeline {
+
+ /**
+ * Constructs a direct render pipeline.
+ *
+ * @param {Renderer} renderer - A reference to the renderer.
+ * @param {Node} outputNode - An optional output node.
+ */
+ constructor( renderer, outputNode = output ) {
+
+ super( renderer, outputNode );
+
+ /**
+ * This flag can be used for type testing.
+ *
+ * @type {boolean}
+ * @readonly
+ * @default true
+ */
+ this.isDirectRenderPipeline = true;
+
+ /**
+ * The context node used to apply output processing in material shaders.
+ *
+ * @private
+ * @type {?ContextNode}
+ * @default null
+ */
+ this._contextNode = null;
+
+ /**
+ * The renderer context node wrapped by this pipeline.
+ *
+ * @private
+ * @type {?ContextNode}
+ * @default null
+ */
+ this._rendererContextNode = null;
+
+ /**
+ * Cached node representations of solid scene backgrounds.
+ *
+ * @private
+ * @type {WeakMap}
+ */
+ this._backgroundNodes = new WeakMap();
+
+ }
+
+ /**
+ * Renders the scene with output processing applied directly in material shaders.
+ *
+ * @param {Object3D} scene - The scene or object to render.
+ * @param {Camera} camera - The camera.
+ */
+ render( scene, camera ) {
+
+ const renderer = this.renderer;
+
+ this._update();
+
+ const backgroundNode = this._getBackgroundNode( scene );
+ const currentBackgroundNode = backgroundNode !== null ? scene.backgroundNode : null;
+ const currentContextNode = renderer.contextNode;
+ const outputRenderTarget = renderer.getOutputRenderTarget();
+ const useDirectXRRenderTarget = outputRenderTarget !== null && renderer.xr.isPresenting === true &&
+ renderer.backend.isWebGPUBackend === true && renderer.isOutputTarget === true;
+
+ let samples;
+ let depthBuffer;
+ let toneMapping;
+ let outputColorSpace;
+
+ if ( backgroundNode !== null ) scene.backgroundNode = backgroundNode;
+
+ renderer.contextNode = this._contextNode;
+
+ if ( useDirectXRRenderTarget ) {
+
+ samples = outputRenderTarget.samples;
+ depthBuffer = outputRenderTarget.depthBuffer;
+
+ outputRenderTarget.samples = renderer.samples;
+ outputRenderTarget.depthBuffer = true;
+
+ }
+
+ try {
+
+ for ( const callback of this._contextData.onBeforePipelineCallbacks ) callback();
+
+ toneMapping = renderer.toneMapping;
+ outputColorSpace = renderer.outputColorSpace;
+
+ renderer.toneMapping = NoToneMapping;
+ renderer.outputColorSpace = ColorManagement.workingColorSpace;
+
+ if ( renderer.xr.enabled ) {
+
+ renderer.xr.renderLayers();
+
+ }
+
+ renderer.render( scene, camera );
+
+ } finally {
+
+ if ( toneMapping !== undefined ) renderer.toneMapping = toneMapping;
+ if ( outputColorSpace !== undefined ) renderer.outputColorSpace = outputColorSpace;
+
+ if ( backgroundNode !== null && scene.backgroundNode === backgroundNode ) scene.backgroundNode = currentBackgroundNode;
+
+ if ( renderer.contextNode === this._contextNode ) renderer.contextNode = currentContextNode;
+
+ if ( useDirectXRRenderTarget ) {
+
+ outputRenderTarget.samples = samples;
+ outputRenderTarget.depthBuffer = depthBuffer;
+
+ }
+
+ for ( const callback of this._contextData.onAfterPipelineCallbacks ) callback();
+
+ }
+
+ }
+
+ /**
+ * Returns a node representation of a solid scene background so it receives
+ * the same inline output processing as material fragments.
+ *
+ * @private
+ * @param {Object3D} scene - The scene or object to render.
+ * @return {?UniformNode} The background node.
+ */
+ _getBackgroundNode( scene ) {
+
+ if ( scene.isScene !== true || scene.backgroundNode != null || scene.background?.isColor !== true ) return null;
+
+ const environmentBlendMode = this.renderer.xr.getEnvironmentBlendMode();
+
+ if ( environmentBlendMode === 'additive' || environmentBlendMode === 'alpha-blend' ) return null;
+
+ let backgroundNode = this._backgroundNodes.get( scene.background );
+
+ if ( backgroundNode === undefined ) {
+
+ backgroundNode = uniform( scene.background );
+ this._backgroundNodes.set( scene.background, backgroundNode );
+
+ }
+
+ return backgroundNode;
+
+ }
+
+ /**
+ * Updates the state of the module.
+ *
+ * @private
+ */
+ _update() {
+
+ if ( this._rendererContextNode !== this.renderer.contextNode ) {
+
+ this._rendererContextNode = this.renderer.contextNode;
+ this.needsUpdate = true;
+
+ }
+
+ super._update();
+
+ }
+
+ /**
+ * Updates the context used to process material output directly.
+ *
+ * @private
+ */
+ _updateContext() {
+
+ super._updateContext();
+
+ const pipelineOutputNode = this.outputNode;
+ const toneMapping = this._toneMapping;
+ const outputColorSpace = this._outputColorSpace;
+ const outputColorTransform = this.outputColorTransform;
+
+ this._contextNode = this._rendererContextNode.context( {
+
+ ...this._contextData,
+
+ getOutput: ( materialOutputNode, builder ) => {
+
+ const renderer = builder.renderer;
+ const renderTarget = renderer.getRenderTarget();
+
+ if ( renderer.isOutputTarget === false && renderTarget?._hasExternalTextures !== true ) return materialOutputNode;
+
+ output.assign( materialOutputNode );
+
+ return outputColorTransform === true ?
+ renderOutput( pipelineOutputNode, toneMapping, outputColorSpace ) :
+ pipelineOutputNode;
+
+ }
+ } );
+
+ }
+
+}
+
+export default DirectRenderPipeline;
diff --git a/src/renderers/common/RenderPipeline.js b/src/renderers/common/RenderPipeline.js
index a41c67425e45ed..028805d8498c83 100644
--- a/src/renderers/common/RenderPipeline.js
+++ b/src/renderers/common/RenderPipeline.js
@@ -168,6 +168,46 @@ class RenderPipeline {
}
+ /**
+ * Updates the context data.
+ *
+ * @private
+ */
+ _updateContext() {
+
+ const toneMapping = this._toneMapping;
+ const outputColorSpace = this._outputColorSpace;
+
+ const contextData = {
+ renderPipeline: this,
+ renderPipelineState: {
+ viewOffsetOwner: null
+ },
+ onBeforePipelineCallbacks: [],
+ onAfterPipelineCallbacks: []
+ };
+
+ let outputNode = this.outputNode;
+
+ if ( this.outputColorTransform === true ) {
+
+ outputNode = renderOutput( outputNode, toneMapping, outputColorSpace );
+
+ } else {
+
+ contextData.toneMapping = toneMapping;
+ contextData.outputColorSpace = outputColorSpace;
+
+ }
+
+ this._contextData = contextData;
+
+ this._quadMesh.material.contextNode = context( contextData );
+ this._quadMesh.material.fragmentNode = outputNode;
+ this._quadMesh.material.needsUpdate = true;
+
+ }
+
/**
* Updates the state of the module.
*
@@ -191,36 +231,7 @@ class RenderPipeline {
if ( this.needsUpdate === true ) {
- const toneMapping = this._toneMapping;
- const outputColorSpace = this._outputColorSpace;
-
- const contextData = {
- renderPipeline: this,
- renderPipelineState: {
- viewOffsetOwner: null
- },
- onBeforePipelineCallbacks: [],
- onAfterPipelineCallbacks: []
- };
-
- let outputNode = this.outputNode;
-
- if ( this.outputColorTransform === true ) {
-
- outputNode = renderOutput( outputNode, toneMapping, outputColorSpace );
-
- } else {
-
- contextData.toneMapping = toneMapping;
- contextData.outputColorSpace = outputColorSpace;
-
- }
-
- this._contextData = contextData;
-
- this._quadMesh.material.contextNode = context( contextData );
- this._quadMesh.material.fragmentNode = outputNode;
- this._quadMesh.material.needsUpdate = true;
+ this._updateContext();
this.needsUpdate = false;
diff --git a/src/renderers/common/XRManager.js b/src/renderers/common/XRManager.js
index 62529f08dc2d57..587cfece51b4ff 100644
--- a/src/renderers/common/XRManager.js
+++ b/src/renderers/common/XRManager.js
@@ -17,6 +17,7 @@ import { Mesh } from '../../objects/Mesh.js';
import { warn, warnOnce } from '../../utils.js';
import { renderOutput } from '../../nodes/display/RenderOutputNode.js';
import { RenderTarget } from '../../core/RenderTarget.js';
+import { context } from '../../nodes/core/ContextNode.js';
const _cameraLPos = /*@__PURE__*/ new Vector3();
const _cameraRPos = /*@__PURE__*/ new Vector3();
@@ -1125,13 +1126,15 @@ class XRManager extends EventDispatcher {
// Apply ToneMapping and OutputColorSpace directly in the material shader
- contextNode = currentContextNode.context( {
+ contextNode = context( {
getOutput: ( outputNode ) => {
return renderOutput( outputNode, renderer.toneMapping, renderer.outputColorSpace );
- }
+ },
+
+ ... currentContextNode.getFlowContextData()
} );