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 + + + + + + + + + + +
+ + +
+ three.jsDirect 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() } );