Parcourir la source

WebGPURenderer: Improve `ArrayCamera` performance and fixes (#30313)

* wip arraycamera

* Update webgl_camera_array2.html

* updated approach

* Update WebGLBackend.js

* added webgpu support

* update example

* revision

* update camera array

* Update Camera.js

* update

* Update webgpu_camera_array.html

* cleanup

* rev
sunag il y a 11 mois
Parent
commit
0c1185d40e

BIN
examples/screenshots/webgpu_camera_array.jpg


+ 45 - 33
examples/webgpu_camera_array.html

@@ -7,6 +7,7 @@
 		<link type="text/css" rel="stylesheet" href="main.css">
 	</head>
 	<body>
+
 		<script type="importmap">
 			{
 				"imports": {
@@ -22,42 +23,34 @@
 
 			import * as THREE from 'three';
 
+			import Stats from 'three/addons/libs/stats.module.js';
+
 			let camera, scene, renderer;
 			let mesh;
+			let stats;
+
 			const AMOUNT = 6;
 
 			init();
 
 			function init() {
 
-				const ASPECT_RATIO = window.innerWidth / window.innerHeight;
-
-				const WIDTH = ( window.innerWidth / AMOUNT );
-				const HEIGHT = ( window.innerHeight / AMOUNT );
+				const subCameras = [];
 
-				const cameras = [];
+				for ( let i = 0; i < AMOUNT * AMOUNT; i ++ ) {
 
-				for ( let y = 0; y < AMOUNT; y ++ ) {
+					const subCamera = new THREE.PerspectiveCamera( 40, 1, 0.1, 10 );
+					subCamera.viewport = new THREE.Vector4();
 
-					for ( let x = 0; x < AMOUNT; x ++ ) {
-
-						const subcamera = new THREE.PerspectiveCamera( 40, ASPECT_RATIO, 0.1, 10 );
-						subcamera.viewport = new THREE.Vector4( Math.floor( x * WIDTH ), Math.floor( y * HEIGHT ), Math.ceil( WIDTH ), Math.ceil( HEIGHT ) );
-						subcamera.position.x = ( x / AMOUNT ) - 0.5;
-						subcamera.position.y = 0.5 - ( y / AMOUNT );
-						subcamera.position.z = 1.5;
-						subcamera.position.multiplyScalar( 2 );
-						subcamera.lookAt( 0, 0, 0 );
-						subcamera.updateMatrixWorld();
-						cameras.push( subcamera );
-
-					}
+					subCameras.push( subCamera );
 
 				}
 
-				camera = new THREE.ArrayCamera( cameras );
+				camera = new THREE.ArrayCamera( subCameras );
 				camera.position.z = 3;
 
+				updateCameras();
+
 				scene = new THREE.Scene();
 
 				scene.add( new THREE.AmbientLight( 0x999999 ) );
@@ -65,8 +58,8 @@
 				const light = new THREE.DirectionalLight( 0xffffff, 3 );
 				light.position.set( 0.5, 0.5, 1 );
 				light.castShadow = true;
-				light.shadow.camera.zoom = 4; // tighter shadow map
 				light.shadow.bias = - 0.001;
+				light.shadow.camera.zoom = 4; // tighter shadow map
 				scene.add( light );
 
 				const geometryBackground = new THREE.PlaneGeometry( 100, 100 );
@@ -77,7 +70,7 @@
 				background.position.set( 0, 0, - 1 );
 				scene.add( background );
 
-				const geometryCylinder = new THREE.BoxGeometry();
+				const geometryCylinder = new THREE.CylinderGeometry( 0.5, 0.5, 1, 32 );
 				const materialCylinder = new THREE.MeshPhongMaterial( { color: 0xff0000 } );
 
 				mesh = new THREE.Mesh( geometryCylinder, materialCylinder );
@@ -85,7 +78,7 @@
 				mesh.receiveShadow = true;
 				scene.add( mesh );
 
-				renderer = new THREE.WebGPURenderer( { antialias: true } );
+				renderer = new THREE.WebGPURenderer( /*{ forceWebGL: true }*/ );
 				renderer.setPixelRatio( window.devicePixelRatio );
 				renderer.setSize( window.innerWidth, window.innerHeight );
 				renderer.setAnimationLoop( animate );
@@ -96,13 +89,18 @@
 
 				window.addEventListener( 'resize', onWindowResize );
 
+				//
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
 			}
 
-			function onWindowResize() {
+			function updateCameras() {
 
 				const ASPECT_RATIO = window.innerWidth / window.innerHeight;
-				const WIDTH = ( window.innerWidth / AMOUNT );
-				const HEIGHT = ( window.innerHeight / AMOUNT );
+				const WIDTH = window.innerWidth / AMOUNT;
+				const HEIGHT = window.innerHeight / AMOUNT;
 
 				camera.aspect = ASPECT_RATIO;
 				camera.updateProjectionMatrix();
@@ -112,28 +110,42 @@
 					for ( let x = 0; x < AMOUNT; x ++ ) {
 
 						const subcamera = camera.cameras[ AMOUNT * y + x ];
+						subcamera.copy( camera ); // copy fov, aspect ratio, near, far from the root camera
 
-						subcamera.viewport.set(
-							Math.floor( x * WIDTH ),
-							Math.floor( y * HEIGHT ),
-							Math.ceil( WIDTH ),
-							Math.ceil( HEIGHT ) );
-
-						subcamera.aspect = ASPECT_RATIO;
+						subcamera.viewport.set( Math.floor( x * WIDTH ), Math.floor( y * HEIGHT ), Math.ceil( WIDTH ), Math.ceil( HEIGHT ) );
 						subcamera.updateProjectionMatrix();
 
+						subcamera.position.x = ( x / AMOUNT ) - 0.5;
+						subcamera.position.y = 0.5 - ( y / AMOUNT );
+						subcamera.position.z = 1.5 + ( ( x + y ) * .5 );
+						subcamera.position.multiplyScalar( 2 );
+
+						subcamera.lookAt( 0, 0, 0 );
+						subcamera.updateMatrixWorld();
+
 					}
 
 				}
 
+			}
+
+			function onWindowResize() {
+
+				updateCameras();
+
 				renderer.setSize( window.innerWidth, window.innerHeight );
 
 			}
 
 			function animate() {
 
+				mesh.rotation.x += 0.005;
+				mesh.rotation.z += 0.01;
+
 				renderer.render( scene, camera );
 
+				stats.update();
+
 			}
 
 		</script>

+ 1 - 0
src/Three.TSL.js

@@ -93,6 +93,7 @@ export const bypass = TSL.bypass;
 export const cache = TSL.cache;
 export const call = TSL.call;
 export const cameraFar = TSL.cameraFar;
+export const cameraIndex = TSL.cameraIndex;
 export const cameraNear = TSL.cameraNear;
 export const cameraNormalMatrix = TSL.cameraNormalMatrix;
 export const cameraPosition = TSL.cameraPosition;

+ 1 - 0
src/cameras/ArrayCamera.js

@@ -9,6 +9,7 @@ class ArrayCamera extends PerspectiveCamera {
 		this.isArrayCamera = true;
 
 		this.cameras = array;
+		this.index = 0;
 
 	}
 

+ 64 - 3
src/nodes/accessors/Camera.js

@@ -1,9 +1,18 @@
 import { uniform } from '../core/UniformNode.js';
-import { renderGroup } from '../core/UniformGroupNode.js';
+import { renderGroup, sharedUniformGroup } from '../core/UniformGroupNode.js';
 import { Vector3 } from '../../math/Vector3.js';
+import { Fn } from '../tsl/TSLBase.js';
+import { uniformArray } from './UniformArrayNode.js';
 
 /** @module Camera **/
 
+/**
+ * TSL object that represents the current `index` value of the camera if used ArrayCamera.
+ *
+ * @type {UniformNode<uint>}
+ */
+export const cameraIndex = /*@__PURE__*/ uniform( 'uint' ).setGroup( sharedUniformGroup( 'cameraIndex' ) ).vertexStage();
+
 /**
  * TSL object that represents the `near` value of the camera used for the current render.
  *
@@ -23,7 +32,33 @@ export const cameraFar = /*@__PURE__*/ uniform( 'float' ).label( 'cameraFar' ).s
  *
  * @type {UniformNode<mat4>}
  */
-export const cameraProjectionMatrix = /*@__PURE__*/ uniform( 'mat4' ).label( 'cameraProjectionMatrix' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.projectionMatrix );
+export const cameraProjectionMatrix = /*@__PURE__*/ ( Fn( ( { camera } ) => {
+
+	let cameraProjectionMatrix;
+
+	if ( camera.isArrayCamera ) {
+
+		const matrices = [];
+
+		for ( const subCamera of camera.cameras ) {
+
+			matrices.push( subCamera.projectionMatrix );
+
+		}
+
+		const cameraProjectionMatrices = uniformArray( matrices ).setGroup( renderGroup ).label( 'cameraProjectionMatrices' );
+
+		cameraProjectionMatrix = cameraProjectionMatrices.element( cameraIndex ).toVar( 'cameraProjectionMatrix' );
+
+	} else {
+
+		cameraProjectionMatrix = uniform( 'mat4' ).label( 'cameraProjectionMatrix' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.projectionMatrix );
+
+	}
+
+	return cameraProjectionMatrix;
+
+} ).once() )();
 
 /**
  * TSL object that represents the inverse projection matrix of the camera used for the current render.
@@ -37,7 +72,33 @@ export const cameraProjectionMatrixInverse = /*@__PURE__*/ uniform( 'mat4' ).lab
  *
  * @type {UniformNode<mat4>}
  */
-export const cameraViewMatrix = /*@__PURE__*/ uniform( 'mat4' ).label( 'cameraViewMatrix' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.matrixWorldInverse );
+export const cameraViewMatrix = /*@__PURE__*/ ( Fn( ( { camera } ) => {
+
+	let cameraViewMatrix;
+
+	if ( camera.isArrayCamera ) {
+
+		const matrices = [];
+
+		for ( const subCamera of camera.cameras ) {
+
+			matrices.push( subCamera.matrixWorldInverse );
+
+		}
+
+		const cameraViewMatrices = uniformArray( matrices ).setGroup( renderGroup ).label( 'cameraViewMatrices' );
+
+		cameraViewMatrix = cameraViewMatrices.element( cameraIndex ).toVar( 'cameraViewMatrix' );
+
+	} else {
+
+		cameraViewMatrix = uniform( 'mat4' ).label( 'cameraViewMatrix' ).setGroup( renderGroup ).onRenderUpdate( ( { camera } ) => camera.matrixWorldInverse );
+
+	}
+
+	return cameraViewMatrix;
+
+} ).once() )();
 
 /**
  * TSL object that represents the world matrix of the camera used for the current render.

+ 20 - 0
src/renderers/common/RenderObject.js

@@ -376,6 +376,26 @@ class RenderObject {
 
 	}
 
+	/**
+	 * Returns a binding group by group name of this render object.
+	 *
+	 * @param {String} name - The name of the binding group.
+	 * @return {BindGroup?} The bindings.
+	 */
+	getBindingGroup( name ) {
+
+		for ( const bindingGroup of this.getBindings() ) {
+
+			if ( bindingGroup.name === name ) {
+
+				return bindingGroup;
+
+			}
+
+		}
+
+	}
+
 	/**
 	 * Returns the index of the render object's geometry.
 	 *

+ 13 - 39
src/renderers/common/Renderer.js

@@ -1229,9 +1229,19 @@ class Renderer {
 		if ( camera.coordinateSystem !== coordinateSystem ) {
 
 			camera.coordinateSystem = coordinateSystem;
-
 			camera.updateProjectionMatrix();
 
+			if ( camera.isArrayCamera ) {
+
+				for ( const subCamera of camera.cameras ) {
+
+					subCamera.coordinateSystem = coordinateSystem;
+					subCamera.updateProjectionMatrix();
+
+				}
+
+			}
+
 		}
 
 		//
@@ -2596,47 +2606,11 @@ class Renderer {
 	 */
 	_renderObjects( renderList, camera, scene, lightsNode, passId = null ) {
 
-		// process renderable objects
-
 		for ( let i = 0, il = renderList.length; i < il; i ++ ) {
 
-			const renderItem = renderList[ i ];
-
-			const { object, geometry, material, group, clippingContext } = renderItem;
-
-			if ( camera.isArrayCamera ) {
-
-				const cameras = camera.cameras;
-
-				for ( let j = 0, jl = cameras.length; j < jl; j ++ ) {
+			const { object, geometry, material, group, clippingContext } = renderList[ i ];
 
-					const camera2 = cameras[ j ];
-
-					if ( object.layers.test( camera2.layers ) ) {
-
-						const vp = camera2.viewport;
-						const minDepth = ( vp.minDepth === undefined ) ? 0 : vp.minDepth;
-						const maxDepth = ( vp.maxDepth === undefined ) ? 1 : vp.maxDepth;
-
-						const viewportValue = this._currentRenderContext.viewportValue;
-						viewportValue.copy( vp ).multiplyScalar( this._pixelRatio ).floor();
-						viewportValue.minDepth = minDepth;
-						viewportValue.maxDepth = maxDepth;
-						this._currentRenderContext.viewport = true;
-
-						this.backend.updateViewport( this._currentRenderContext );
-
-						this._currentRenderObjectFunction( object, scene, camera2, geometry, material, group, lightsNode, clippingContext, passId );
-
-					}
-
-				}
-
-			} else {
-
-				this._currentRenderObjectFunction( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId );
-
-			}
+			this._currentRenderObjectFunction( object, scene, camera, geometry, material, group, lightsNode, clippingContext, passId );
 
 		}
 

+ 74 - 9
src/renderers/webgl-fallback/WebGLBackend.js

@@ -1035,31 +1035,96 @@ class WebGLBackend extends Backend {
 
 		}
 
-		if ( object.isBatchedMesh ) {
+		const draw = () => {
 
-			if ( object._multiDrawInstances !== null ) {
+			if ( object.isBatchedMesh ) {
 
-				renderer.renderMultiDrawInstances( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount, object._multiDrawInstances );
+				if ( object._multiDrawInstances !== null ) {
 
-			} else if ( ! this.hasFeature( 'WEBGL_multi_draw' ) ) {
+					renderer.renderMultiDrawInstances( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount, object._multiDrawInstances );
 
-				warnOnce( 'THREE.WebGLRenderer: WEBGL_multi_draw not supported.' );
+				} else if ( ! this.hasFeature( 'WEBGL_multi_draw' ) ) {
+
+					warnOnce( 'THREE.WebGLRenderer: WEBGL_multi_draw not supported.' );
+
+				} else {
+
+					renderer.renderMultiDraw( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount );
+
+				}
+
+			} else if ( instanceCount > 1 ) {
+
+				renderer.renderInstances( firstVertex, vertexCount, instanceCount );
 
 			} else {
 
-				renderer.renderMultiDraw( object._multiDrawStarts, object._multiDrawCounts, object._multiDrawCount );
+				renderer.render( firstVertex, vertexCount );
+
+			}
+
+		};
+
+		if ( renderObject.camera.isArrayCamera ) {
+
+			const cameraData = this.get( renderObject.camera );
+			const cameras = renderObject.camera.cameras;
+
+			if ( cameraData.indexesGPU === undefined ) {
+
+				const data = new Uint32Array( [ 0, 0, 0, 0 ] );
+				const indexesGPU = [];
+
+				for ( let i = 0, len = cameras.length; i < len; i ++ ) {
+
+					const bufferGPU = gl.createBuffer();
+
+					data[ 0 ] = i;
+
+					gl.bindBuffer( gl.UNIFORM_BUFFER, bufferGPU );
+					gl.bufferData( gl.UNIFORM_BUFFER, data, gl.DYNAMIC_DRAW );
+
+					indexesGPU.push( bufferGPU );
+
+				}
+
+				cameraData.indexesGPU = indexesGPU; // TODO: Create a global library for this
+				cameraData.cameraIndex = renderObject.getBindingGroup( 'cameraIndex' ).bindings[ 0 ];
 
 			}
 
-		} else if ( instanceCount > 1 ) {
+			const cameraIndexData = this.get( cameraData.cameraIndex );
+			const pixelRatio = this.renderer.getPixelRatio();
+
+			for ( let i = 0, len = cameras.length; i < len; i ++ ) {
+
+				const subCamera = cameras[ i ];
+
+				if ( object.layers.test( subCamera.layers ) ) {
 
-			renderer.renderInstances( firstVertex, vertexCount, instanceCount );
+					const vp = subCamera.viewport;
+
+					gl.viewport(
+						Math.floor( vp.x * pixelRatio ),
+						Math.floor( ( renderObject.context.height - vp.height - vp.y ) * pixelRatio ),
+						Math.floor( vp.width * pixelRatio ),
+						Math.floor( vp.height * pixelRatio )
+					);
+
+					state.bindBufferBase( gl.UNIFORM_BUFFER, cameraIndexData.index, cameraData.indexesGPU[ i ] );
+
+					draw();
+
+				}
+
+			}
 
 		} else {
 
-			renderer.render( firstVertex, vertexCount );
+			draw();
 
 		}
+
 		//
 
 		gl.bindVertexArray( null );

+ 94 - 29
src/renderers/webgpu/WebGPUBackend.js

@@ -1194,69 +1194,134 @@ class WebGPUBackend extends Backend {
 
 		// draw
 
-		if ( object.isBatchedMesh === true ) {
+		const draw = () => {
 
-			const starts = object._multiDrawStarts;
-			const counts = object._multiDrawCounts;
-			const drawCount = object._multiDrawCount;
-			const drawInstances = object._multiDrawInstances;
+			if ( object.isBatchedMesh === true ) {
 
-			for ( let i = 0; i < drawCount; i ++ ) {
+				const starts = object._multiDrawStarts;
+				const counts = object._multiDrawCounts;
+				const drawCount = object._multiDrawCount;
+				const drawInstances = object._multiDrawInstances;
 
-				const count = drawInstances ? drawInstances[ i ] : 1;
-				const firstInstance = count > 1 ? 0 : i;
+				for ( let i = 0; i < drawCount; i ++ ) {
 
-				if ( hasIndex === true ) {
+					const count = drawInstances ? drawInstances[ i ] : 1;
+					const firstInstance = count > 1 ? 0 : i;
 
-					passEncoderGPU.drawIndexed( counts[ i ], count, starts[ i ] / index.array.BYTES_PER_ELEMENT, 0, firstInstance );
+					if ( hasIndex === true ) {
+
+						passEncoderGPU.drawIndexed( counts[ i ], count, starts[ i ] / index.array.BYTES_PER_ELEMENT, 0, firstInstance );
+
+					} else {
+
+						passEncoderGPU.draw( counts[ i ], count, starts[ i ], firstInstance );
+
+					}
+
+				}
+
+			} else if ( hasIndex === true ) {
+
+				const { vertexCount: indexCount, instanceCount, firstVertex: firstIndex } = drawParams;
+
+				const indirect = renderObject.getIndirect();
+
+				if ( indirect !== null ) {
+
+					const buffer = this.get( indirect ).buffer;
+
+					passEncoderGPU.drawIndexedIndirect( buffer, 0 );
 
 				} else {
 
-					passEncoderGPU.draw( counts[ i ], count, starts[ i ], firstInstance );
+					passEncoderGPU.drawIndexed( indexCount, instanceCount, firstIndex, 0, 0 );
 
 				}
 
+				info.update( object, indexCount, instanceCount );
+
+			} else {
+
+				const { vertexCount, instanceCount, firstVertex } = drawParams;
+
+				const indirect = renderObject.getIndirect();
+
+				if ( indirect !== null ) {
+
+					const buffer = this.get( indirect ).buffer;
+
+					passEncoderGPU.drawIndirect( buffer, 0 );
+
+				} else {
+
+					passEncoderGPU.draw( vertexCount, instanceCount, firstVertex, 0 );
+
+				}
+
+				info.update( object, vertexCount, instanceCount );
+
 			}
 
-		} else if ( hasIndex === true ) {
+		};
 
-			const { vertexCount: indexCount, instanceCount, firstVertex: firstIndex } = drawParams;
+		if ( renderObject.camera.isArrayCamera ) {
 
-			const indirect = renderObject.getIndirect();
+			const cameraData = this.get( renderObject.camera );
+			const cameras = renderObject.camera.cameras;
 
-			if ( indirect !== null ) {
+			if ( cameraData.indexesGPU === undefined ) {
 
-				const buffer = this.get( indirect ).buffer;
+				const cameraIndex = renderObject.getBindingGroup( 'cameraIndex' );
+				const bindingsData = this.get( cameraIndex );
+				const indexesGPU = [];
 
-				passEncoderGPU.drawIndexedIndirect( buffer, 0 );
+				const data = new Uint32Array( [ 0, 0, 0, 0 ] );
 
-			} else {
+				for ( let i = 0, len = cameras.length; i < len; i ++ ) {
+
+					data[ 0 ] = i;
 
-				passEncoderGPU.drawIndexed( indexCount, instanceCount, firstIndex, 0, 0 );
+					const bindGroupIndex = this.bindingUtils.createBindGroupIndex( data, bindingsData.layout );
+
+					indexesGPU.push( bindGroupIndex );
+
+				}
+
+				cameraData.indexesGPU = indexesGPU; // TODO: Create a global library for this
+				cameraData.cameraIndex = cameraIndex;
 
 			}
 
-			info.update( object, indexCount, instanceCount );
+			const pixelRatio = this.renderer.getPixelRatio();
 
-		} else {
+			for ( let i = 0, len = cameras.length; i < len; i ++ ) {
 
-			const { vertexCount, instanceCount, firstVertex } = drawParams;
+				const subCamera = cameras[ i ];
 
-			const indirect = renderObject.getIndirect();
+				if ( object.layers.test( subCamera.layers ) ) {
 
-			if ( indirect !== null ) {
+					const vp = subCamera.viewport;
 
-				const buffer = this.get( indirect ).buffer;
+					passEncoderGPU.setViewport(
+						Math.floor( vp.x * pixelRatio ),
+						Math.floor( vp.y * pixelRatio ),
+						Math.floor( vp.width * pixelRatio ),
+						Math.floor( vp.height * pixelRatio ),
+						context.viewportValue.minDepth,
+						context.viewportValue.maxDepth
+					);
 
-				passEncoderGPU.drawIndirect( buffer, 0 );
+					passEncoderGPU.setBindGroup( cameraData.cameraIndex.index, cameraData.indexesGPU[ i ] );
 
-			} else {
+					draw();
 
-				passEncoderGPU.draw( vertexCount, instanceCount, firstVertex, 0 );
+				}
 
 			}
 
-			info.update( object, vertexCount, instanceCount );
+		} else {
+
+			draw();
 
 		}
 

+ 33 - 0
src/renderers/webgpu/utils/WebGPUBindingUtils.js

@@ -293,6 +293,39 @@ class WebGPUBindingUtils {
 
 	}
 
+	/**
+	 * Creates a GPU bind group for the camera index.
+	 *
+	 * @param {Uint32Array} data - The index data.
+	 * @param {GPUBindGroupLayout} layout - The GPU bind group layout.
+	 * @return {GPUBindGroup} The GPU bind group.
+	 */
+	createBindGroupIndex( data, layout ) {
+
+		const backend = this.backend;
+		const device = backend.device;
+
+		const usage = GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST;
+		const index = data[ 0 ];
+
+		const buffer = device.createBuffer( {
+			label: 'bindingCameraIndex_' + index,
+			size: 16, // uint(4) * 4
+			usage: usage
+		} );
+
+		device.queue.writeBuffer( buffer, 0, data, 0 );
+
+		const entries = [ { binding: 0, resource: { buffer } } ];
+
+		return device.createBindGroup( {
+			label: 'bindGroupCameraIndex_' + index,
+			layout,
+			entries
+		} );
+
+	}
+
 	/**
 	 * Creates a GPU bind group for the given bind group and GPU layout.
 	 *

粤ICP备19079148号