diff --git a/examples/files.json b/examples/files.json index 4509551e4d2365..0b69f8e9829a20 100644 --- a/examples/files.json +++ b/examples/files.json @@ -386,7 +386,8 @@ "webgpu_instancing_morph", "webgpu_texturegrad", "webgpu_volume_cloud", - "webgpu_volume_perlin" + "webgpu_volume_perlin", + "webgpu_renderbundle" ], "webaudio": [ "webaudio_orientation", diff --git a/examples/jsm/renderers/common/RenderBundle.js b/examples/jsm/renderers/common/RenderBundle.js new file mode 100644 index 00000000000000..e84c0ad0de8d5c --- /dev/null +++ b/examples/jsm/renderers/common/RenderBundle.js @@ -0,0 +1,18 @@ +class RenderBundle { + + constructor( scene, camera ) { + + this.scene = scene; + this.camera = camera; + + } + + clone() { + + return Object.assign( new this.constructor(), this ); + + } + +} + +export default RenderBundle; diff --git a/examples/jsm/renderers/common/RenderBundles.js b/examples/jsm/renderers/common/RenderBundles.js new file mode 100644 index 00000000000000..66045184139de4 --- /dev/null +++ b/examples/jsm/renderers/common/RenderBundles.js @@ -0,0 +1,38 @@ +import ChainMap from './ChainMap.js'; +import RenderBundle from './RenderBundle.js'; + +class RenderBundles { + + constructor() { + + this.lists = new ChainMap(); + + } + + get( scene, camera ) { + + const lists = this.lists; + const keys = [ scene, camera ]; + + let list = lists.get( keys ); + + if ( list === undefined ) { + + list = new RenderBundle( scene, camera ); + lists.set( keys, list ); + + } + + return list; + + } + + dispose() { + + this.lists = new ChainMap(); + + } + +} + +export default RenderBundles; diff --git a/examples/jsm/renderers/common/RenderList.js b/examples/jsm/renderers/common/RenderList.js index e34f736c7e7c6d..29c185225e99a4 100644 --- a/examples/jsm/renderers/common/RenderList.js +++ b/examples/jsm/renderers/common/RenderList.js @@ -57,6 +57,7 @@ class RenderList { this.opaque = []; this.transparent = []; + this.bundles = []; this.lightsNode = new LightsNode( [] ); this.lightsArray = []; @@ -71,6 +72,8 @@ class RenderList { this.opaque.length = 0; this.transparent.length = 0; + this.bundles.length = 0; + this.lightsArray.length = 0; this.occlusionQueryCount = 0; @@ -135,6 +138,12 @@ class RenderList { } + pushBundle( group ) { + + this.bundles.push( group ); + + } + pushLight( light ) { this.lightsArray.push( light ); diff --git a/examples/jsm/renderers/common/Renderer.js b/examples/jsm/renderers/common/Renderer.js index 1dec53e008a386..421bf1ca6da381 100644 --- a/examples/jsm/renderers/common/Renderer.js +++ b/examples/jsm/renderers/common/Renderer.js @@ -15,6 +15,7 @@ import ClippingContext from './ClippingContext.js'; import { Scene, Frustum, Matrix4, Vector2, Vector3, Vector4, DoubleSide, BackSide, FrontSide, SRGBColorSpace, NoColorSpace, NoToneMapping, LinearFilter, LinearSRGBColorSpace, RenderTarget, HalfFloatType, RGBAFormat } from 'three'; import { NodeMaterial } from '../../nodes/Nodes.js'; import QuadMesh from '../../objects/QuadMesh.js'; +import RenderBundles from './RenderBundles.js'; const _scene = new Scene(); const _drawingBufferSize = new Vector2(); @@ -87,6 +88,7 @@ class Renderer { this._bindings = null; this._objects = null; this._pipelines = null; + this._bundles = null; this._renderLists = null; this._renderContexts = null; this._textures = null; @@ -111,6 +113,7 @@ class Renderer { this._renderObjectFunction = null; this._currentRenderObjectFunction = null; + this._currentRenderBundle = null; this._handleObjectFunction = this._renderObjectDirect; @@ -171,6 +174,7 @@ class Renderer { this._bindings = new Bindings( backend, this._nodes, this._textures, this._attributes, this._pipelines, this.info ); this._objects = new RenderObjects( this, this._nodes, this._geometries, this._pipelines, this._bindings, this.info ); this._renderLists = new RenderLists(); + this._bundles = new RenderBundles(); this._renderContexts = new RenderContexts(); // @@ -326,6 +330,81 @@ class Renderer { } + _renderBundle( bundle, sceneRef, lightsNode ) { + + const { object, camera, renderList } = bundle; + + const renderContext = this._currentRenderContext; + const renderContextData = this.backend.get( renderContext ); + + // + + const renderBundle = this._bundles.get( object, camera ); + + const renderBundleData = this.backend.get( renderBundle ); + if ( renderBundleData.renderContexts === undefined ) renderBundleData.renderContexts = new Set(); + + // + + const renderBundleNeedsUpdate = renderBundleData.renderContexts.has( renderContext ) === false || object.needsUpdate === true; + + renderBundleData.renderContexts.add( renderContext ); + + if ( renderBundleNeedsUpdate ) { + + if ( renderContextData.renderObjects === undefined || object.needsUpdate === true ) { + + const nodeFrame = this._nodes.nodeFrame; + + renderContextData.renderObjects = []; + renderContextData.renderBundles = []; + renderContextData.scene = sceneRef; + renderContextData.camera = camera; + renderContextData.renderId = nodeFrame.renderId; + + renderContextData.registerBundlesPhase = true; + + } + + this._currentRenderBundle = renderBundle; + + const opaqueObjects = renderList.opaque; + + if ( opaqueObjects.length > 0 ) this._renderObjects( opaqueObjects, camera, sceneRef, lightsNode ); + + this._currentRenderBundle = null; + + // + + object.needsUpdate = false; + + } else { + + const renderContext = this._currentRenderContext; + const renderContextData = this.backend.get( renderContext ); + + for ( let i = 0, l = renderContextData.renderObjects.length; i < l; i ++ ) { + + const renderObject = renderContextData.renderObjects[ i ]; + + this._nodes.updateBefore( renderObject ); + + // + + renderObject.object.modelViewMatrix.multiplyMatrices( camera.matrixWorldInverse, renderObject.object.matrixWorld ); + renderObject.object.normalMatrix.getNormalMatrix( renderObject.object.modelViewMatrix ); + + this._nodes.updateForRender( renderObject ); + this._bindings.updateForRender( renderObject ); + + this.backend.draw( renderObject, this.info ); + + } + + } + + } + render( scene, camera ) { if ( this._initialized === false ) { @@ -456,7 +535,6 @@ class Renderer { if ( camera.parent === null && camera.matrixWorldAutoUpdate === true ) camera.updateMatrixWorld(); - // let viewport = this._viewport; @@ -564,8 +642,10 @@ class Renderer { const opaqueObjects = renderList.opaque; const transparentObjects = renderList.transparent; + const bundles = renderList.bundles; const lightsNode = renderList.lightsNode; + if ( bundles.length > 0 ) this._renderBundles( bundles, sceneRef, lightsNode ); if ( opaqueObjects.length > 0 ) this._renderObjects( opaqueObjects, camera, sceneRef, lightsNode ); if ( transparentObjects.length > 0 ) this._renderObjects( transparentObjects, camera, sceneRef, lightsNode ); @@ -1194,6 +1274,25 @@ class Renderer { } + if ( object.static === true ) { + + const baseRenderList = renderList; + + // replace render list + renderList = this._renderLists.get( object, camera ); + + renderList.begin(); + + baseRenderList.pushBundle( { + object, + camera, + renderList, + } ); + + renderList.finish(); + + } + const children = object.children; for ( let i = 0, l = children.length; i < l; i ++ ) { @@ -1204,6 +1303,16 @@ class Renderer { } + _renderBundles( bundles, sceneRef, lightsNode ) { + + for ( const bundle of bundles ) { + + this._renderBundle( bundle, sceneRef, lightsNode ); + + } + + } + _renderObjects( renderList, camera, scene, lightsNode ) { // process renderable objects @@ -1397,8 +1506,25 @@ class Renderer { // + if ( this._currentRenderBundle !== null && this._currentRenderBundle.needsUpdate === true ) { + + const renderObjectData = this.backend.get( renderObject ); + + renderObjectData.bundleEncoder = undefined; + renderObjectData.lastPipelineGPU = undefined; + + } + this.backend.draw( renderObject, this.info ); + if ( this._currentRenderBundle !== null ) { + + const renderContextData = this.backend.get( this._currentRenderContext ); + + renderContextData.renderObjects.push( renderObject ); + + } + } _createObjectPipeline( object, material, scene, camera, lightsNode, passId ) { diff --git a/examples/jsm/renderers/webgpu/WebGPUBackend.js b/examples/jsm/renderers/webgpu/WebGPUBackend.js index 1c0e8e6de1b54d..f28e1bc41e0426 100644 --- a/examples/jsm/renderers/webgpu/WebGPUBackend.js +++ b/examples/jsm/renderers/webgpu/WebGPUBackend.js @@ -453,6 +453,13 @@ class WebGPUBackend extends Backend { const renderContextData = this.get( renderContext ); const occlusionQueryCount = renderContext.occlusionQueryCount; + if ( renderContextData.renderBundles !== undefined && renderContextData.renderBundles.length > 0 ) { + + renderContextData.registerBundlesPhase = false; + renderContextData.currentPass.executeBundles( renderContextData.renderBundles ); + + } + if ( occlusionQueryCount > renderContextData.occlusionQueryIndex ) { renderContextData.currentPass.endOcclusionQuery(); @@ -791,9 +798,22 @@ class WebGPUBackend extends Backend { const pipelineGPU = this.get( pipeline ).pipeline; const currentSets = contextData.currentSets; - // pipeline + const renderObjectData = this.get( renderObject ); + + const { bundleEncoder, renderBundle, lastPipelineGPU } = renderObjectData; + + const renderContextData = this.get( context ); + + if ( renderContextData.registerBundlesPhase === true && bundleEncoder !== undefined && lastPipelineGPU === pipelineGPU ) { + + renderContextData.renderBundles.push( renderBundle ); + return; + + } - const passEncoderGPU = contextData.currentPass; + const passEncoderGPU = this.renderer._currentRenderBundle ? this.createBundleEncoder( context, renderObject ) : contextData.currentPass; + + // pipeline if ( currentSets.pipeline !== pipelineGPU ) { @@ -905,6 +925,16 @@ class WebGPUBackend extends Backend { } + + if ( this.renderer._currentRenderBundle ) { + + const renderBundle = passEncoderGPU.finish(); + renderObjectData.lastPipelineGPU = pipelineGPU; + renderObjectData.renderBundle = renderBundle; + renderObjectData.bundleEncoder = passEncoderGPU; + + } + } // cache key @@ -1160,6 +1190,12 @@ class WebGPUBackend extends Backend { } + createBundleEncoder( renderContext, renderObject ) { + + return this.pipelineUtils.createBundleEncoder( renderContext, renderObject ); + + } + // bindings createBindings( bindings ) { diff --git a/examples/jsm/renderers/webgpu/utils/WebGPUPipelineUtils.js b/examples/jsm/renderers/webgpu/utils/WebGPUPipelineUtils.js index d7816f4f72e71a..c75b4644996167 100644 --- a/examples/jsm/renderers/webgpu/utils/WebGPUPipelineUtils.js +++ b/examples/jsm/renderers/webgpu/utils/WebGPUPipelineUtils.js @@ -23,6 +23,27 @@ class WebGPUPipelineUtils { } + _getSampleCount( renderObjectContext ) { + + let sampleCount = this.backend.utils.getSampleCount( renderObjectContext ); + + if ( sampleCount > 1 ) { + + // WebGPU only supports power-of-two sample counts and 2 is not a valid value + sampleCount = Math.pow( 2, Math.floor( Math.log2( sampleCount ) ) ); + + if ( sampleCount === 2 ) { + + sampleCount = 4; + + } + + } + + return sampleCount; + + } + createRenderPipeline( renderObject, promises ) { const { object, material, geometry, pipeline } = renderObject; @@ -102,22 +123,11 @@ class WebGPUPipelineUtils { const primitiveState = this._getPrimitiveState( object, geometry, material ); const depthCompare = this._getDepthCompare( material ); const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context ); - let sampleCount = utils.getSampleCount( renderObject.context ); - - if ( sampleCount > 1 ) { - - // WebGPU only supports power-of-two sample counts and 2 is not a valid value - sampleCount = Math.pow( 2, Math.floor( Math.log2( sampleCount ) ) ); - - if ( sampleCount === 2 ) { - sampleCount = 4; - - } - - } + const sampleCount = this._getSampleCount( renderObject.context ); const pipelineDescriptor = { + label: 'renderPipeline', vertex: Object.assign( {}, vertexModule, { buffers: vertexBuffers } ), fragment: Object.assign( {}, fragmentModule, { targets } ), primitive: primitiveState, @@ -162,6 +172,35 @@ class WebGPUPipelineUtils { } + createBundleEncoder( renderContext, renderObject ) { + + const backend = this.backend; + const { utils, device } = backend; + + const renderContextData = backend.get( renderContext ); + const renderObjectData = backend.get( renderObject ); + + const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderContext ); + const colorFormat = utils.getCurrentColorFormat( renderContext ); + const sampleCount = this._getSampleCount( renderObject.context ); + + const descriptor = { + label: 'renderBundleEncoder', + colorFormats: [ colorFormat ], + depthStencilFormat, + sampleCount + }; + + const bundleEncoder = device.createRenderBundleEncoder( descriptor ); + + renderObjectData.bundleEncoder = bundleEncoder; + renderContextData.currentSets = { attributes: {} }; + renderContextData._renderBundleViewport = renderContext.width + '_' + renderContext.height; + + return bundleEncoder; + + } + createComputePipeline( pipeline, bindings ) { const backend = this.backend; diff --git a/examples/screenshots/webgpu_renderbundle.jpg b/examples/screenshots/webgpu_renderbundle.jpg new file mode 100644 index 00000000000000..f64103bd9943e6 Binary files /dev/null and b/examples/screenshots/webgpu_renderbundle.jpg differ diff --git a/examples/webgpu_renderbundle.html b/examples/webgpu_renderbundle.html new file mode 100644 index 00000000000000..90658f2bae6a49 --- /dev/null +++ b/examples/webgpu_renderbundle.html @@ -0,0 +1,318 @@ + + +
+