diff --git a/src/math/FrustumArray.js b/src/math/FrustumArray.js index fd0b4d5e0a796a..4463a21525aff8 100644 --- a/src/math/FrustumArray.js +++ b/src/math/FrustumArray.js @@ -3,7 +3,6 @@ import { Frustum } from './Frustum.js'; import { Matrix4 } from './Matrix4.js'; const _projScreenMatrix = /*@__PURE__*/ new Matrix4(); -const _frustum = /*@__PURE__*/ new Frustum(); /** * FrustumArray is used to determine if an object is visible in at least one camera @@ -25,220 +24,191 @@ class FrustumArray { */ this.coordinateSystem = WebGLCoordinateSystem; + /** + * A pool of frustum instances. It may hold more entries than are + * currently in use; surplus instances are kept for reuse to avoid + * reallocating when array cameras of different lengths are rendered. + * + * @private + * @type {Array} + */ + this._frustums = []; + + /** + * The number of frustums in {@link FrustumArray#_frustums} that are currently + * in use. + * + * @private + * @type {number} + * @default 0 + */ + this._count = 0; + } /** - * Returns `true` if the 3D object's bounding sphere is intersecting any frustum - * from the camera array. + * Computes and caches a frustum for each camera of the given array camera. * - * @param {Object3D} object - The 3D object to test. - * @param {Object} cameraArray - An object with a cameras property containing an array of cameras. - * @return {boolean} Whether the 3D object is visible in any camera. + * @param {ArrayCamera} cameraArray - The array camera whose sub-cameras define the frustums. + * @return {FrustumArray} A reference to this frustum array. */ - intersectsObject( object, cameraArray ) { - - if ( ! cameraArray.isArrayCamera || cameraArray.cameras.length === 0 ) { - - return false; - - } + setFromArrayCamera( cameraArray ) { - for ( let i = 0; i < cameraArray.cameras.length; i ++ ) { + const cameras = cameraArray.cameras; + const frustums = this._frustums; - const camera = cameraArray.cameras[ i ]; + for ( let i = 0; i < cameras.length; i ++ ) { - _projScreenMatrix.multiplyMatrices( - camera.projectionMatrix, - camera.matrixWorldInverse - ); + const camera = cameras[ i ]; - _frustum.setFromProjectionMatrix( - _projScreenMatrix, - camera.coordinateSystem, - camera.reversedDepth - ); + _projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ); - if ( _frustum.intersectsObject( object ) ) { + if ( frustums[ i ] === undefined ) frustums[ i ] = new Frustum(); - return true; // Object is visible in at least one camera - - } + frustums[ i ].setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth ); } - return false; // Not visible in any camera + this._count = cameras.length; + + return this; } /** - * Returns `true` if the given sprite is intersecting any frustum - * from the camera array. + * Returns `true` if the 3D object's bounding sphere is intersecting any cached frustum. * - * @param {Sprite} sprite - The sprite to test. - * @param {Object} cameraArray - An object with a cameras property containing an array of cameras. - * @return {boolean} Whether the sprite is visible in any camera. + * {@link FrustumArray#setFromArrayCamera} must be called once per render before this method. + * + * @param {Object3D} object - The 3D object to test. + * @return {boolean} Whether the 3D object is visible in any camera. */ - intersectsSprite( sprite, cameraArray ) { + intersectsObject( object ) { - if ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) { + const frustums = this._frustums; - return false; + for ( let i = 0; i < this._count; i ++ ) { - } + if ( frustums[ i ].intersectsObject( object ) ) return true; - for ( let i = 0; i < cameraArray.cameras.length; i ++ ) { + } - const camera = cameraArray.cameras[ i ]; + return false; - _projScreenMatrix.multiplyMatrices( - camera.projectionMatrix, - camera.matrixWorldInverse - ); + } - _frustum.setFromProjectionMatrix( - _projScreenMatrix, - camera.coordinateSystem, - camera.reversedDepth - ); + /** + * Returns `true` if the given sprite is intersecting any cached frustum. + * + * {@link FrustumArray#setFromArrayCamera} must be called once per render before this method. + * + * @param {Sprite} sprite - The sprite to test. + * @return {boolean} Whether the sprite is visible in any camera. + */ + intersectsSprite( sprite ) { - if ( _frustum.intersectsSprite( sprite ) ) { + const frustums = this._frustums; - return true; // Sprite is visible in at least one camera + for ( let i = 0; i < this._count; i ++ ) { - } + if ( frustums[ i ].intersectsSprite( sprite ) ) return true; } - return false; // Not visible in any camera + return false; } /** - * Returns `true` if the given bounding sphere is intersecting any frustum - * from the camera array. + * Returns `true` if the given bounding sphere is intersecting any cached frustum. + * + * {@link FrustumArray#setFromArrayCamera} must be called once per render before this method. * * @param {Sphere} sphere - The bounding sphere to test. - * @param {Object} cameraArray - An object with a cameras property containing an array of cameras. * @return {boolean} Whether the sphere is visible in any camera. */ - intersectsSphere( sphere, cameraArray ) { - - if ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) { - - return false; - - } + intersectsSphere( sphere ) { - for ( let i = 0; i < cameraArray.cameras.length; i ++ ) { + const frustums = this._frustums; - const camera = cameraArray.cameras[ i ]; + for ( let i = 0; i < this._count; i ++ ) { - _projScreenMatrix.multiplyMatrices( - camera.projectionMatrix, - camera.matrixWorldInverse - ); - - _frustum.setFromProjectionMatrix( - _projScreenMatrix, - camera.coordinateSystem, - camera.reversedDepth - ); - - if ( _frustum.intersectsSphere( sphere ) ) { - - return true; // Sphere is visible in at least one camera - - } + if ( frustums[ i ].intersectsSphere( sphere ) ) return true; } - return false; // Not visible in any camera + return false; } /** - * Returns `true` if the given bounding box is intersecting any frustum - * from the camera array. + * Returns `true` if the given bounding box is intersecting any cached frustum. + * + * {@link FrustumArray#setFromArrayCamera} must be called once per render before this method. * * @param {Box3} box - The bounding box to test. - * @param {Object} cameraArray - An object with a cameras property containing an array of cameras. * @return {boolean} Whether the box is visible in any camera. */ - intersectsBox( box, cameraArray ) { - - if ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) { - - return false; + intersectsBox( box ) { - } - - for ( let i = 0; i < cameraArray.cameras.length; i ++ ) { - - const camera = cameraArray.cameras[ i ]; - - _projScreenMatrix.multiplyMatrices( - camera.projectionMatrix, - camera.matrixWorldInverse - ); + const frustums = this._frustums; - _frustum.setFromProjectionMatrix( - _projScreenMatrix, - camera.coordinateSystem, - camera.reversedDepth - ); + for ( let i = 0; i < this._count; i ++ ) { - if ( _frustum.intersectsBox( box ) ) { - - return true; // Box is visible in at least one camera - - } + if ( frustums[ i ].intersectsBox( box ) ) return true; } - return false; // Not visible in any camera + return false; } /** - * Returns `true` if the given point lies within any frustum - * from the camera array. + * Returns `true` if the given point lies within any cached frustum. + * + * {@link FrustumArray#setFromArrayCamera} must be called once per render before this method. * * @param {Vector3} point - The point to test. - * @param {Object} cameraArray - An object with a cameras property containing an array of cameras. * @return {boolean} Whether the point is visible in any camera. */ - containsPoint( point, cameraArray ) { + containsPoint( point ) { - if ( ! cameraArray || ! cameraArray.cameras || cameraArray.cameras.length === 0 ) { + const frustums = this._frustums; - return false; + for ( let i = 0; i < this._count; i ++ ) { + + if ( frustums[ i ].containsPoint( point ) ) return true; } - for ( let i = 0; i < cameraArray.cameras.length; i ++ ) { + return false; - const camera = cameraArray.cameras[ i ]; + } - _projScreenMatrix.multiplyMatrices( - camera.projectionMatrix, - camera.matrixWorldInverse - ); + /** + * Copies the values of the given frustum array to this instance. + * + * @param {FrustumArray} frustumArray - The frustum array to copy. + * @return {FrustumArray} A reference to this frustum array. + */ + copy( source ) { + + this.coordinateSystem = source.coordinateSystem; - _frustum.setFromProjectionMatrix( - _projScreenMatrix, - camera.coordinateSystem, - camera.reversedDepth - ); + const frustums = this._frustums; + const sourceFrustums = source._frustums; - if ( _frustum.containsPoint( point ) ) { + for ( let i = 0; i < source._count; i ++ ) { - return true; // Point is visible in at least one camera + if ( frustums[ i ] === undefined ) frustums[ i ] = new Frustum(); - } + frustums[ i ].copy( sourceFrustums[ i ] ); } - return false; // Not visible in any camera + this._count = source._count; + + return this; } @@ -249,7 +219,7 @@ class FrustumArray { */ clone() { - return new FrustumArray(); + return new FrustumArray().copy( this ); } diff --git a/src/math/Matrix4.js b/src/math/Matrix4.js index 985fc5b2e452b3..f30cf4ec4e6764 100644 --- a/src/math/Matrix4.js +++ b/src/math/Matrix4.js @@ -238,7 +238,7 @@ class Matrix4 { */ extractBasis( xAxis, yAxis, zAxis ) { - if ( this.determinant3x3() === 0 ) { + if ( this.determinantAffine() === 0 ) { xAxis.set( 1, 0, 0 ); yAxis.set( 0, 1, 0 ); @@ -288,7 +288,7 @@ class Matrix4 { */ extractRotation( m ) { - if ( m.determinant3x3() === 0 ) { + if ( m.determinantAffine() === 0 ) { return this.identity(); @@ -650,14 +650,17 @@ class Matrix4 { } /** - * Computes and returns the determinant of the upper-left 3x3 submatrix. + * Computes and returns the determinant of the 4x4 matrix, but assumes the + * matrix is affine, saving some computations. * * For affine matrices (like an object's world matrix), this value equals the * full 4x4 {@link Matrix4#determinant} but is cheaper to compute. + * + * Assumes the bottom row is [0, 0, 0, 1]. * - * @return {number} The determinant of the upper-left 3x3 submatrix. + * @return {number} The determinant of the matrix. */ - determinant3x3() { + determinantAffine() { const te = this.elements; @@ -1080,7 +1083,7 @@ class Matrix4 { position.y = te[ 13 ]; position.z = te[ 14 ]; - const det = this.determinant3x3(); + const det = this.determinantAffine(); if ( det === 0 ) { diff --git a/src/objects/BatchedMesh.js b/src/objects/BatchedMesh.js index cb2a9f32847df8..2ef7cb25743882 100644 --- a/src/objects/BatchedMesh.js +++ b/src/objects/BatchedMesh.js @@ -1552,17 +1552,25 @@ class BatchedMesh extends Mesh { const frustum = camera.isArrayCamera ? _frustumArray : _frustum; // prepare the frustum in the local frame - if ( perObjectFrustumCulled && ! camera.isArrayCamera ) { + if ( perObjectFrustumCulled ) { - _matrix - .multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ) - .multiply( this.matrixWorld ); + if ( camera.isArrayCamera ) { - _frustum.setFromProjectionMatrix( - _matrix, - camera.coordinateSystem, - camera.reversedDepth - ); + frustum.setFromArrayCamera( camera ); + + } else { + + _matrix + .multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ) + .multiply( this.matrixWorld ); + + frustum.setFromProjectionMatrix( + _matrix, + camera.coordinateSystem, + camera.reversedDepth + ); + + } } @@ -1588,7 +1596,7 @@ class BatchedMesh extends Mesh { let culled = false; if ( perObjectFrustumCulled ) { - culled = ! frustum.intersectsSphere( _sphere, camera ); + culled = ! frustum.intersectsSphere( _sphere ); } @@ -1645,7 +1653,7 @@ class BatchedMesh extends Mesh { // get the bounds in world space this.getMatrixAt( i, _matrix ); this.getBoundingSphereAt( geometryId, _sphere ).applyMatrix4( _matrix ); - culled = ! frustum.intersectsSphere( _sphere, camera ); + culled = ! frustum.intersectsSphere( _sphere ); } diff --git a/src/renderers/WebGLRenderer.js b/src/renderers/WebGLRenderer.js index 8c70b3bc534e73..cc2f156939863f 100644 --- a/src/renderers/WebGLRenderer.js +++ b/src/renderers/WebGLRenderer.js @@ -1185,7 +1185,7 @@ class WebGLRenderer { if ( scene === null ) scene = _emptyScene; // renderBufferDirect second parameter used to be fog (could be null) - const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant3x3() < 0 ); + const frontFaceCW = ( object.isMesh && object.matrixWorld.determinantAffine() < 0 ); const program = setProgram( camera, scene, geometry, material, object ); diff --git a/src/renderers/common/Renderer.js b/src/renderers/common/Renderer.js index fa6d7720c84e44..6cdf6d1c184c8d 100644 --- a/src/renderers/common/Renderer.js +++ b/src/renderers/common/Renderer.js @@ -948,7 +948,11 @@ class Renderer { const frustum = camera.isArrayCamera ? _frustumArray : _frustum; - if ( ! camera.isArrayCamera ) { + if ( camera.isArrayCamera ) { + + frustum.setFromArrayCamera( camera ); + + } else { _projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ); frustum.setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth ); @@ -1653,7 +1657,11 @@ class Renderer { const frustum = camera.isArrayCamera ? _frustumArray : _frustum; - if ( ! camera.isArrayCamera ) { + if ( camera.isArrayCamera ) { + + frustum.setFromArrayCamera( camera ); + + } else { _projScreenMatrix.multiplyMatrices( camera.projectionMatrix, camera.matrixWorldInverse ); frustum.setFromProjectionMatrix( _projScreenMatrix, camera.coordinateSystem, camera.reversedDepth ); @@ -3094,7 +3102,7 @@ class Renderer { const frustum = camera.isArrayCamera ? _frustumArray : _frustum; - if ( ! object.frustumCulled || frustum.intersectsSprite( object, camera ) ) { + if ( ! object.frustumCulled || frustum.intersectsSprite( object ) ) { if ( this.sortObjects === true ) { @@ -3120,7 +3128,7 @@ class Renderer { const frustum = camera.isArrayCamera ? _frustumArray : _frustum; - if ( ! object.frustumCulled || frustum.intersectsObject( object, camera ) ) { + if ( ! object.frustumCulled || frustum.intersectsObject( object ) ) { const { geometry, material } = object; diff --git a/src/renderers/webgl-fallback/WebGLBackend.js b/src/renderers/webgl-fallback/WebGLBackend.js index 53908d279e9bd5..bd8bd9d075bca7 100644 --- a/src/renderers/webgl-fallback/WebGLBackend.js +++ b/src/renderers/webgl-fallback/WebGLBackend.js @@ -1080,7 +1080,7 @@ class WebGLBackend extends Backend { this._bindUniforms( renderObject.getBindings() ); - const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant3x3() < 0 ); + const frontFaceCW = ( object.isMesh && object.matrixWorld.determinantAffine() < 0 ); state.setMaterial( material, frontFaceCW, hardwareClippingPlanes ); diff --git a/src/renderers/webgpu/WebGPUBackend.js b/src/renderers/webgpu/WebGPUBackend.js index 6b4e767f47fbcb..91ef251aaa118b 100644 --- a/src/renderers/webgpu/WebGPUBackend.js +++ b/src/renderers/webgpu/WebGPUBackend.js @@ -2058,7 +2058,7 @@ class WebGPUBackend extends Backend { const colorFormat = utils.getCurrentColorFormat( renderObject.context ); const depthStencilFormat = utils.getCurrentDepthStencilFormat( renderObject.context ); const primitiveTopology = utils.getPrimitiveTopology( object, material ); - const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant3x3() < 0 ); + const frontFaceCW = ( object.isMesh && object.matrixWorld.determinantAffine() < 0 ); let needsUpdate = false; @@ -2120,7 +2120,7 @@ class WebGPUBackend extends Backend { // meshes with negative scale have a different frontFace render pipeline // descriptor value so the following must be honored in the cache key - const frontFaceCW = ( object.isMesh && object.matrixWorld.determinant3x3() < 0 ); + const frontFaceCW = ( object.isMesh && object.matrixWorld.determinantAffine() < 0 ); return [ material.transparent, material.blending, material.premultipliedAlpha, diff --git a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js index f3551af6dd5b10..764267c51628af 100644 --- a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js +++ b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js @@ -847,7 +847,7 @@ class WebGPUPipelineUtils { let flipSided = ( material.side === BackSide ); - if ( object.isMesh && object.matrixWorld.determinant3x3() < 0 ) flipSided = ! flipSided; + if ( object.isMesh && object.matrixWorld.determinantAffine() < 0 ) flipSided = ! flipSided; descriptor.frontFace = ( flipSided === true ) ? GPUFrontFace.CW : GPUFrontFace.CCW; diff --git a/test/unit/src/math/Matrix4.tests.js b/test/unit/src/math/Matrix4.tests.js index f20cff559d67c5..545f82b6327c8e 100644 --- a/test/unit/src/math/Matrix4.tests.js +++ b/test/unit/src/math/Matrix4.tests.js @@ -467,7 +467,7 @@ export default QUnit.module( 'Maths', () => { } ); - QUnit.test( 'determinant3x3', ( assert ) => { + QUnit.test( 'determinantAffine', ( assert ) => { // for affine matrices (the typical object world matrix), the 3x3 result // equals the full 4x4 determinant since the bottom row is [ 0, 0, 0, 1 ] @@ -479,28 +479,28 @@ export default QUnit.module( 'Maths', () => { // translation + rotation + non-uniform scale a.compose( position, quaternion, new Vector3( 2, 3, 0.5 ) ); - assert.ok( Math.abs( a.determinant3x3() - a.determinant() ) <= eps, 'Affine matrix: Passed!' ); + assert.ok( Math.abs( a.determinantAffine() - a.determinant() ) <= eps, 'Affine matrix: Passed!' ); // reflection (negative scale on one axis flips the winding order) a.compose( position, quaternion, new Vector3( 2, 3, - 0.5 ) ); - assert.ok( a.determinant3x3() < 0, 'Reflection produces a negative determinant!' ); - assert.ok( Math.abs( a.determinant3x3() - a.determinant() ) <= eps, 'Reflection matrix: Passed!' ); + assert.ok( a.determinantAffine() < 0, 'Reflection produces a negative determinant!' ); + assert.ok( Math.abs( a.determinantAffine() - a.determinant() ) <= eps, 'Reflection matrix: Passed!' ); // shear a.multiply( new Matrix4().makeShear( 0.5, 0, 0.2, 0, 0.7, 0 ) ); - assert.ok( Math.abs( a.determinant3x3() - a.determinant() ) <= eps, 'Shear matrix: Passed!' ); + assert.ok( Math.abs( a.determinantAffine() - a.determinant() ) <= eps, 'Shear matrix: Passed!' ); } ); - QUnit.test( 'determinant3x3 (projective matrix)', ( assert ) => { + QUnit.test( 'determinantAffine (projective matrix)', ( assert ) => { // for non-affine (projective) matrices, the bottom row is not [ 0, 0, 0, 1 ] // and so the 3x3 result generally differs from the full 4x4 determinant const a = new Matrix4().makePerspective( - 1, 1, 1, - 1, 1, 100 ); - assert.ok( Math.abs( a.determinant3x3() - a.determinant() ) > eps, 'Passed!' ); + assert.ok( Math.abs( a.determinantAffine() - a.determinant() ) > eps, 'Passed!' ); } );