diff --git a/src/core/BufferGeometry.js b/src/core/BufferGeometry.js index db7bfe67f8b159..cdeff8447f455a 100644 --- a/src/core/BufferGeometry.js +++ b/src/core/BufferGeometry.js @@ -779,11 +779,35 @@ class BufferGeometry extends EventDispatcher { let maxRadiusSq = 0; - for ( let i = 0, il = position.count; i < il; i ++ ) { + const array = position.array; - _vector.fromBufferAttribute( position, i ); + if ( position.isInterleavedBufferAttribute !== true && position.normalized === false && + position.itemSize === 3 && ( array instanceof Float32Array || array instanceof Float64Array ) ) { - maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector ) ); + // fast path: read the typed array directly, avoiding per-vertex getter calls. + // Math.max is used (instead of a comparison) so NaN values propagate to the radius. + + const cx = center.x, cy = center.y, cz = center.z; + + for ( let i = 0, il = position.count * 3; i < il; i += 3 ) { + + const dx = array[ i ] - cx; + const dy = array[ i + 1 ] - cy; + const dz = array[ i + 2 ] - cz; + + maxRadiusSq = Math.max( maxRadiusSq, dx * dx + dy * dy + dz * dz ); + + } + + } else { + + for ( let i = 0, il = position.count; i < il; i ++ ) { + + _vector.fromBufferAttribute( position, i ); + + maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector ) ); + + } } diff --git a/src/math/Box3.js b/src/math/Box3.js index 675f74156954c1..a71453733fae6e 100644 --- a/src/math/Box3.js +++ b/src/math/Box3.js @@ -87,9 +87,37 @@ class Box3 { this.makeEmpty(); - for ( let i = 0, il = attribute.count; i < il; i ++ ) { + const array = attribute.array; - this.expandByPoint( _vector.fromBufferAttribute( attribute, i ) ); + if ( attribute.isInterleavedBufferAttribute !== true && attribute.normalized === false && + attribute.itemSize === 3 && ( array instanceof Float32Array || array instanceof Float64Array ) ) { + + // fast path: read the typed array directly, avoiding per-vertex getter calls. + // Math.min/max are used (instead of comparisons) so NaN values propagate to min/max. + + let minX = + Infinity, minY = + Infinity, minZ = + Infinity; + let maxX = - Infinity, maxY = - Infinity, maxZ = - Infinity; + + for ( let i = 0, il = attribute.count * 3; i < il; i += 3 ) { + + const x = array[ i ], y = array[ i + 1 ], z = array[ i + 2 ]; + + minX = Math.min( minX, x ); maxX = Math.max( maxX, x ); + minY = Math.min( minY, y ); maxY = Math.max( maxY, y ); + minZ = Math.min( minZ, z ); maxZ = Math.max( maxZ, z ); + + } + + this.min.set( minX, minY, minZ ); + this.max.set( maxX, maxY, maxZ ); + + } else { + + for ( let i = 0, il = attribute.count; i < il; i ++ ) { + + this.expandByPoint( _vector.fromBufferAttribute( attribute, i ) ); + + } } diff --git a/test/benchmark/computeBoundingSphere.js b/test/benchmark/computeBoundingSphere.js new file mode 100644 index 00000000000000..6df345c175969f --- /dev/null +++ b/test/benchmark/computeBoundingSphere.js @@ -0,0 +1,131 @@ +/** + * Benchmark: BufferGeometry.computeBoundingSphere() fast path vs the previous + * implementation (per-vertex Vector3.fromBufferAttribute access). + * + * Run with: node test/benchmark/computeBoundingSphere.js + */ + +import { BufferGeometry } from '../../src/core/BufferGeometry.js'; +import { BufferAttribute } from '../../src/core/BufferAttribute.js'; +import { Box3 } from '../../src/math/Box3.js'; +import { Sphere } from '../../src/math/Sphere.js'; +import { Vector3 } from '../../src/math/Vector3.js'; + +const _box = new Box3(); +const _vector = new Vector3(); + +// faithful copy of the previous implementation (no morph targets) + +function computeBoundingSphereOld( geometry ) { + + const sphere = new Sphere(); + const position = geometry.attributes.position; + const center = sphere.center; + + // old Box3.setFromBufferAttribute path + + _box.makeEmpty(); + + for ( let i = 0, il = position.count; i < il; i ++ ) { + + _box.expandByPoint( _vector.fromBufferAttribute( position, i ) ); + + } + + _box.getCenter( center ); + + let maxRadiusSq = 0; + + for ( let i = 0, il = position.count; i < il; i ++ ) { + + _vector.fromBufferAttribute( position, i ); + + maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector ) ); + + } + + sphere.radius = Math.sqrt( maxRadiusSq ); + + return sphere; + +} + +function makeGeometry( vertexCount ) { + + const array = new Float32Array( vertexCount * 3 ); + + for ( let i = 0; i < array.length; i ++ ) { + + array[ i ] = Math.random() * 200 - 100; + + } + + const geometry = new BufferGeometry(); + geometry.setAttribute( 'position', new BufferAttribute( array, 3 ) ); + + return geometry; + +} + +function bench( fn, iterations ) { + + const times = []; + + for ( let i = 0; i < iterations; i ++ ) { + + const start = performance.now(); + fn(); + times.push( performance.now() - start ); + + } + + times.sort( ( a, b ) => a - b ); + + return times[ Math.floor( times.length / 2 ) ]; // median + +} + +const sizes = [ 10_000, 100_000, 1_000_000, 5_000_000 ]; +const iterations = 20; + +console.log( `median of ${ iterations } runs per case\n` ); +console.log( 'vertices'.padStart( 10 ), 'old (ms)'.padStart( 12 ), 'new (ms)'.padStart( 12 ), 'speedup'.padStart( 10 ) ); + +for ( const size of sizes ) { + + const geometry = makeGeometry( size ); + + // correctness check + + const oldSphere = computeBoundingSphereOld( geometry ); + geometry.computeBoundingSphere(); + const newSphere = geometry.boundingSphere; + + if ( Math.abs( oldSphere.radius - newSphere.radius ) > 1e-10 || + oldSphere.center.distanceTo( newSphere.center ) > 1e-10 ) { + + console.error( `MISMATCH at ${ size } vertices: old`, oldSphere, 'new', newSphere ); + process.exit( 1 ); + + } + + // warmup + + for ( let i = 0; i < 3; i ++ ) { + + computeBoundingSphereOld( geometry ); + geometry.computeBoundingSphere(); + + } + + const oldMs = bench( () => computeBoundingSphereOld( geometry ), iterations ); + const newMs = bench( () => geometry.computeBoundingSphere(), iterations ); + + console.log( + String( size ).padStart( 10 ), + oldMs.toFixed( 3 ).padStart( 12 ), + newMs.toFixed( 3 ).padStart( 12 ), + ( oldMs / newMs ).toFixed( 2 ).padStart( 9 ) + 'x' + ); + +}