Просмотр исходного кода

BatchedMesh: add support for frustum culling per batched geometry (#27120)

* BatchedMeh: add support for frustum culling batched elements

* comments

* Update frustum culling support

* BatchedMesh: add "perObjectFrustumCulled"

* Reduce the number of world matrix transform applications for frustum culling

* Update copy function

* Update toJSON

* Fix serialization
Garrett Johnson 1 год назад
Родитель
Сommit
7863b733b3
2 измененных файлов с 197 добавлено и 11 удалено
  1. 186 11
      examples/jsm/objects/BatchedMesh.js
  2. 11 0
      src/core/Object3D.js

+ 186 - 11
examples/jsm/objects/BatchedMesh.js

@@ -6,10 +6,17 @@ import {
 	MathUtils,
 	Matrix4,
 	Mesh,
-	RGBAFormat
+	RGBAFormat,
+	Box3,
+	Sphere,
+	Frustum,
+	WebGLCoordinateSystem,
+	WebGPUCoordinateSystem,
+	Vector3,
 } from 'three';
 
 const ID_ATTR_NAME = 'batchId';
+const _matrix = new Matrix4();
 const _identityMatrix = new Matrix4();
 const _zeroScaleMatrix = new Matrix4().set(
 	0, 0, 0, 0,
@@ -17,6 +24,11 @@ const _zeroScaleMatrix = new Matrix4().set(
 	0, 0, 0, 0,
 	0, 0, 0, 1,
 );
+const _projScreenMatrix = new Matrix4();
+const _frustum = new Frustum();
+const _box = new Box3();
+const _sphere = new Sphere();
+const _vector = new Vector3();
 
 // @TODO: SkinnedMesh support?
 // @TODO: Future work if needed. Move into the core. Can be optimized more with WEBGL_multi_draw.
@@ -62,12 +74,15 @@ class BatchedMesh extends Mesh {
 		super( new BufferGeometry(), material );
 
 		this.isBatchedMesh = true;
+		this.perObjectFrustumCulled = true;
+		this.frustumCulled = false;
 
 		this._drawRanges = [];
 		this._reservedRanges = [];
 
 		this._visible = [];
 		this._active = [];
+		this._bounds = [];
 
 		this._maxGeometryCount = maxGeometryCount;
 		this._maxVertexCount = maxVertexCount;
@@ -82,9 +97,6 @@ class BatchedMesh extends Mesh {
 		// Local matrix per geometry by using data texture
 		this._matricesTexture = null;
 
-		// @TODO: Calculate the entire binding box and make frustumCulled true
-		this.frustumCulled = false;
-
 		this._initMatricesTexture();
 
 	}
@@ -260,6 +272,7 @@ class BatchedMesh extends Mesh {
 		let lastRange = null;
 		const reservedRanges = this._reservedRanges;
 		const drawRanges = this._drawRanges;
+		const bounds = this._bounds;
 		if ( this._geometryCount !== 0 ) {
 
 			lastRange = reservedRanges[ reservedRanges.length - 1 ];
@@ -345,6 +358,13 @@ class BatchedMesh extends Mesh {
 			start: hasIndex ? reservedRange.indexStart : reservedRange.vertexStart,
 			count: - 1
 		} );
+		bounds.push( {
+			boxInitialized: false,
+			box: new Box3(),
+
+			sphereInitialized: false,
+			sphere: new Sphere()
+		} );
 
 		// set the id for the geometry
 		const idAttribute = this.geometry.getAttribute( ID_ATTR_NAME );
@@ -444,6 +464,30 @@ class BatchedMesh extends Mesh {
 
 		}
 
+		// store the bounding boxes
+		const bound = this._bounds[ id ];
+		if ( geometry.boundingBox !== null ) {
+
+			bound.box.copy( geometry.boundingBox );
+			bound.boxInitialized = true;
+
+		} else {
+
+			bound.boxInitialized = false;
+
+		}
+
+		if ( geometry.boundingSphere !== null ) {
+
+			bound.sphere.copy( geometry.boundingSphere );
+			bound.sphereInitialized = true;
+
+		} else {
+
+			bound.sphereInitialized = false;
+
+		}
+
 		// set drawRange count
 		const drawRange = this._drawRanges[ id ];
 		const posAttr = geometry.getAttribute( 'position' );
@@ -474,6 +518,99 @@ class BatchedMesh extends Mesh {
 
 	}
 
+	// get bounding box and compute it if it doesn't exist
+	getBoundingBoxAt( id, target ) {
+
+		const active = this._active;
+		if ( active[ id ] === false ) {
+
+			return this;
+
+		}
+
+		// compute bounding box
+		const bound = this._bounds[ id ];
+		const box = bound.box;
+		const geometry = this.geometry;
+		if ( bound.boxInitialized === false ) {
+
+			box.makeEmpty();
+
+			const index = geometry.index;
+			const position = geometry.attributes.position;
+			const drawRange = this._drawRanges[ id ];
+			for ( let i = drawRange.start, l = drawRange.start + drawRange.count; i < l; i ++ ) {
+
+				let iv = i;
+				if ( index ) {
+
+					iv = index.getX( iv );
+
+				}
+
+				box.expandByPoint( _vector.fromBufferAttribute( position, iv ) );
+
+			}
+
+			bound.boxInitialized = true;
+
+		}
+
+		target.copy( box );
+		return target;
+
+	}
+
+	// get bounding sphere and compute it if it doesn't exist
+	getBoundingSphereAt( id, target ) {
+
+		const active = this._active;
+		if ( active[ id ] === false ) {
+
+			return this;
+
+		}
+
+		// compute bounding sphere
+		const bound = this._bounds[ id ];
+		const sphere = bound.sphere;
+		const geometry = this.geometry;
+		if ( bound.sphereInitialized === false ) {
+
+			sphere.makeEmpty();
+
+			this.getBoundingBoxAt( id, _box );
+			_box.getCenter( sphere.center );
+
+			const index = geometry.index;
+			const position = geometry.attributes.position;
+			const drawRange = this._drawRanges[ id ];
+
+			let maxRadiusSq = 0;
+			for ( let i = drawRange.start, l = drawRange.start + drawRange.count; i < l; i ++ ) {
+
+				let iv = i;
+				if ( index ) {
+
+					iv = index.getX( iv );
+
+				}
+
+				_vector.fromBufferAttribute( position, iv );
+				maxRadiusSq = Math.max( maxRadiusSq, sphere.center.distanceToSquared( _vector ) );
+
+			}
+
+			sphere.radius = Math.sqrt( maxRadiusSq );
+			bound.sphereInitialized = true;
+
+		}
+
+		target.copy( sphere );
+		return target;
+
+	}
+
 	optimize() {
 
 		throw new Error( 'BatchedMesh: Optimize function not implemented.' );
@@ -568,12 +705,20 @@ class BatchedMesh extends Mesh {
 		super.copy( source );
 
 		this.geometry = source.geometry.clone();
+		this.perObjectFrustumCulled = source.perObjectFrustumCulled;
 
 		this._drawRanges = source._drawRanges.map( range => ( { ...range } ) );
 		this._reservedRanges = source._reservedRanges.map( range => ( { ...range } ) );
 
 		this._visible = source._visible.slice();
 		this._active = source._active.slice();
+		this._bounds = source._bounds.map( bound => ( {
+			boxInitialized: bound.boxInitialized,
+			box: bound.box.clone(),
+
+			sphereInitialized: bound.sphereInitialized,
+			sphere: bound.sphere.clone()
+		} ) );
 
 		this._maxGeometryCount = source._maxGeometryCount;
 		this._maxVertexCount = source._maxVertexCount;
@@ -600,7 +745,7 @@ class BatchedMesh extends Mesh {
 
 	}
 
-	onBeforeRender( _renderer, _scene, _camera, geometry ) {
+	onBeforeRender( _renderer, _scene, camera, geometry, material/*, _group*/ ) {
 
 		// the indexed version of the multi draw function requires specifying the start
 		// offset in bytes.
@@ -611,16 +756,48 @@ class BatchedMesh extends Mesh {
 		const multiDrawStarts = this._multiDrawStarts;
 		const multiDrawCounts = this._multiDrawCounts;
 		const drawRanges = this._drawRanges;
+		const perObjectFrustumCulled = this.perObjectFrustumCulled;
+
+		// prepare the frustum
+		if ( perObjectFrustumCulled ) {
+
+			_projScreenMatrix
+				.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse )
+				.multiply( this.matrixWorld );
+			_frustum.setFromProjectionMatrix(
+				_projScreenMatrix,
+				_renderer.isWebGPURenderer ? WebGPUCoordinateSystem : WebGLCoordinateSystem
+			);
+
+		}
 
 		let count = 0;
 		for ( let i = 0, l = visible.length; i < l; i ++ ) {
 
 			if ( visible[ i ] ) {
 
-				const range = drawRanges[ i ];
-				multiDrawStarts[ count ] = range.start * bytesPerElement;
-				multiDrawCounts[ count ] = range.count;
-				count ++;
+				// determine whether the batched geometry is within the frustum
+				let culled = false;
+				if ( perObjectFrustumCulled ) {
+
+					// get the bounds in camera space
+					this.getMatrixAt( i, _matrix );
+
+					// get the bounds
+					this.getBoundingBoxAt( i, _box ).applyMatrix4( _matrix );
+					this.getBoundingSphereAt( i, _sphere ).applyMatrix4( _matrix );
+					culled = ! _frustum.intersectsBox( _box ) || ! _frustum.intersectsSphere( _sphere );
+
+				}
+
+				if ( ! culled ) {
+
+					const range = drawRanges[ i ];
+					multiDrawStarts[ count ] = range.start * bytesPerElement;
+					multiDrawCounts[ count ] = range.count;
+					count ++;
+
+				}
 
 			}
 
@@ -628,8 +805,6 @@ class BatchedMesh extends Mesh {
 
 		this._multiDrawCount = count;
 
-		// @TODO: Implement frustum culling for each geometry
-
 		// @TODO: Implement geometry sorting for transparent and opaque materials
 
 	}

+ 11 - 0
src/core/Object3D.js

@@ -722,11 +722,22 @@ class Object3D extends EventDispatcher {
 		if ( this.isBatchedMesh ) {
 
 			object.type = 'BatchedMesh';
+			object.perObjectFrustumCulled = this.perObjectFrustumCulled;
+
 			object.drawRanges = this._drawRanges;
 			object.reservedRanges = this._reservedRanges;
 
 			object.visible = this._visible;
 			object.active = this._active;
+			object.bounds = this._bounds.map( bound => ( {
+				boxInitialized: bound.boxInitialized,
+				boxMin: bound.box.min.toArray(),
+				boxMax: bound.box.max.toArray(),
+
+				sphereInitialized: bound.sphereInitialized,
+				sphereRadius: bound.sphere.radius,
+				sphereCenter: bound.sphere.center.toArray()
+			} ) );
 
 			object.maxGeometryCount = this._maxGeometryCount;
 			object.maxVertexCount = this._maxVertexCount;