diff --git a/docs/api/core/BufferGeometry.html b/docs/api/core/BufferGeometry.html index 1ab54208280d25..1e1ce18cc87494 100644 --- a/docs/api/core/BufferGeometry.html +++ b/docs/api/core/BufferGeometry.html @@ -134,8 +134,9 @@

[property:Array groups]

Each group is an object of the form: { start: Integer, count: Integer, materialIndex: Integer } - where start specifies the index of the first vertex in this draw call, count specifies - how many vertices are included, and materialIndex specifies the material array index to use.

+ where start specifies the first element in this draw call – the first vertex for non-indexed geometry, + otherwise the first triangle index. Count specifies how many vertices (or indices) are included, and + materialIndex specifies the material array index to use.

Use [page:.addGroup] to add groups, rather than modifying this array directly. diff --git a/docs/examples/BufferGeometryUtils.html b/docs/examples/BufferGeometryUtils.html new file mode 100644 index 00000000000000..6aa774df026618 --- /dev/null +++ b/docs/examples/BufferGeometryUtils.html @@ -0,0 +1,51 @@ + + + + + + + + + + +

[name]

+ +
+ A class containing utility functions for [page:BufferGeometry BufferGeometry] instances.

+
+ + +

Methods

+ +

[method:null computeTangents]( [param:BufferGeometry geometry] )

+
+ geometry -- A [page:BufferGeometry BufferGeometry] instance, which must have index, position, normal, and uv attributes.

+ + Calculates and adds tangent attribute to a geometry.

+ +
+ +

[method:BufferGeometry mergeBufferGeometries]( [param:Array geometries] )

+
+ geometries -- Array of [page:BufferGeometry BufferGeometry] instances.

+ + Merges a set of geometries into a single instance. All geometries must have compatible attributes. + If merge does not succeed, the method returns null.

+ +
+ +

[method:BufferAttribute mergeBufferAttributes]( [param:Array attributes] )

+
+ attributes -- Array of [page:BufferAttribute BufferAttribute] instances.

+ + Merges a set of attributes into a single instance. All attributes must have compatible properties + and types, and [page:InterleavedBufferAttribute InterleavedBufferAttributes] are not supported. If merge does not succeed, the method + returns null. + +
+ +

Source

+ + [link:https://github.com/mrdoob/three.js/blob/master/examples/js/BufferGeometryUtils.js examples/js/BufferGeometryUtils.js] + + diff --git a/docs/list.js b/docs/list.js index 20ad72b4265431..3cb505b21c3597 100644 --- a/docs/list.js +++ b/docs/list.js @@ -386,6 +386,7 @@ var list = { }, "Utils": { + "BufferGeometryUtils": "examples/BufferGeometryUtils", "SceneUtils": "examples/utils/SceneUtils" } diff --git a/examples/js/BufferGeometryUtils.js b/examples/js/BufferGeometryUtils.js index 18d47320d8168c..6a57f1767c40c0 100644 --- a/examples/js/BufferGeometryUtils.js +++ b/examples/js/BufferGeometryUtils.js @@ -182,6 +182,194 @@ THREE.BufferGeometryUtils = { } + }, + + /** + * @param {Array} geometries + * @return {THREE.BufferGeometry} + */ + mergeBufferGeometries: function ( geometries ) { + + var isIndexed = geometries[ 0 ].index !== null; + + var attributesUsed = new Set( Object.keys( geometries[ 0 ].attributes ) ); + var morphAttributesUsed = new Set( Object.keys( geometries[ 0 ].morphAttributes ) ); + + var attributes = {}; + var morphAttributes = {}; + + var mergedGeometry = new THREE.BufferGeometry(); + + for ( var i = 0; i < geometries.length; ++ i ) { + + var geometry = geometries[ i ]; + + // ensure that all geometries are indexed, or none + + if ( isIndexed !== ( geometry.index !== null ) ) return null; + + // gather attributes, exit early if they're different + + for ( var name in geometry.attributes ) { + + if ( !attributesUsed.has( name ) ) return null; + + if ( attributes[ name ] === undefined ) attributes[ name ] = []; + + attributes[ name ].push( geometry.attributes[ name ] ); + + } + + // gather morph attributes, exit early if they're different + + for ( var name in geometry.morphAttributes ) { + + if ( !morphAttributesUsed.has( name ) ) return null; + + if ( morphAttributes[ name ] === undefined ) morphAttributes[ name ] = []; + + morphAttributes[ name ].push( geometry.morphAttributes[ name ] ); + + } + + // gather .userData + + if ( geometry.userData !== undefined ) { + + mergedGeometry.userData = mergedGeometry.userData || {}; + mergedGeometry.userData.mergedUserData = mergedGeometry.userData.mergedUserData || []; + mergedGeometry.userData.mergedUserData.push( geometry.userData ); + + } + + } + + // merge indices + + if ( isIndexed ) { + + var indexOffset = 0; + var indexList = []; + + for ( var i = 0; i < geometries.length; ++ i ) { + + var index = geometries[ i ].index; + + if ( indexOffset > 0 ) { + + index = index.clone(); + + for ( var j = 0; j < index.count; ++ j ) { + + index.setX( j, index.getX( j ) + indexOffset ); + + } + + } + + indexList.push( index ); + indexOffset += geometries[ i ].attributes.position.count; + + } + + var mergedIndex = this.mergeBufferAttributes( indexList ); + + if ( !mergedIndex ) return null; + + mergedGeometry.index = mergedIndex; + + } + + // merge attributes + + for ( var name in attributes ) { + + var mergedAttribute = this.mergeBufferAttributes( attributes[ name ] ); + + if ( ! mergedAttribute ) return null; + + mergedGeometry.addAttribute( name, mergedAttribute ); + + } + + // merge morph attributes + + for ( var name in morphAttributes ) { + + var numMorphTargets = morphAttributes[ name ][ 0 ].length; + + if ( numMorphTargets === 0 ) break; + + mergedGeometry.morphAttributes = mergedGeometry.morphAttributes || {}; + mergedGeometry.morphAttributes[ name ] = []; + + for ( var i = 0; i < numMorphTargets; ++ i ) { + + var morphAttributesToMerge = []; + + for ( var j = 0; j < morphAttributes[ name ].length; ++ j ) { + + morphAttributesToMerge.push( morphAttributes[ name ][ j ][ i ] ); + + } + + var mergedMorphAttribute = this.mergeBufferAttributes( morphAttributesToMerge ); + + if ( !mergedMorphAttribute ) return null; + + mergedGeometry.morphAttributes[ name ].push( mergedMorphAttribute ); + + } + + } + + return mergedGeometry; + + }, + + /** + * @param {Array} attributes + * @return {THREE.BufferAttribute} + */ + mergeBufferAttributes: function ( attributes ) { + + var TypedArray; + var itemSize; + var normalized; + var arrayLength = 0; + + for ( var i = 0; i < attributes.length; ++ i ) { + + var attribute = attributes[ i ]; + + if ( attribute.isInterleavedBufferAttribute ) return null; + + if ( TypedArray === undefined ) TypedArray = attribute.array.constructor; + if ( TypedArray !== attribute.array.constructor ) return null; + + if ( itemSize === undefined ) itemSize = attribute.itemSize; + if ( itemSize !== attribute.itemSize ) return null; + + if ( normalized === undefined ) normalized = attribute.normalized; + if ( normalized !== attribute.normalized ) return null; + + arrayLength += attribute.array.length; + + } + + var array = new TypedArray( arrayLength ); + var offset = 0; + + for ( var j = 0; j < attributes.length; ++ j ) { + + array.set( attributes[ j ].array, offset ); + + offset += attributes[ j ].array.length; + + } + + return new THREE.BufferAttribute( array, itemSize, normalized ); + } }; diff --git a/src/core/BufferGeometry.js b/src/core/BufferGeometry.js index 1f0f1a0e18ce51..b5a900d9e937c7 100644 --- a/src/core/BufferGeometry.js +++ b/src/core/BufferGeometry.js @@ -794,7 +794,16 @@ BufferGeometry.prototype = Object.assign( Object.create( EventDispatcher.prototy } - if ( offset === undefined ) offset = 0; + if ( offset === undefined ) { + + offset = 0; + + console.warn( + 'THREE.BufferGeometry.merge(): Overwriting original geometry, starting at offset=0. ' + + 'Use BufferGeometryUtils.mergeBufferGeometries() for lossless merge.' + ); + + } var attributes = this.attributes; diff --git a/test/three.example.unit.js b/test/three.example.unit.js index b8c8c011ee64f4..1ddc8693860736 100644 --- a/test/three.example.unit.js +++ b/test/three.example.unit.js @@ -2,6 +2,7 @@ * @author TristanVALCKE / https://github.com/Itee */ +import './unit/example/BufferGeometryUtils.tests'; import './unit/example/exporters/GLTFExporter.tests'; import './unit/example/loaders/GLTFLoader.tests'; import './unit/example/objects/Lensflare.tests'; diff --git a/test/unit/example/BufferGeometryUtils.tests.js b/test/unit/example/BufferGeometryUtils.tests.js new file mode 100644 index 00000000000000..6af45bdfcb431d --- /dev/null +++ b/test/unit/example/BufferGeometryUtils.tests.js @@ -0,0 +1,129 @@ +/** + * @author Don McCurdy / https://www.donmccurdy.com + */ +/* global QUnit */ + +import * as BufferGeometryUtils from '../../../examples/js/BufferGeometryUtils'; + +export default QUnit.module( 'BufferGeometryUtils', () => { + + QUnit.test( 'mergeBufferAttributes - basic', ( assert ) => { + + var array1 = new Float32Array( [ 1, 2, 3, 4 ] ); + var attr1 = new THREE.BufferAttribute( array1, 2, false ); + + var array2 = new Float32Array( [ 5, 6, 7, 8 ] ); + var attr2 = new THREE.BufferAttribute( array2, 2, false ); + + var mergedAttr = THREE.BufferGeometryUtils.mergeBufferAttributes( [ attr1, attr2 ] ); + + assert.smartEqual( Array.from( mergedAttr.array ), [ 1, 2, 3, 4, 5, 6, 7, 8 ], 'merges elements' ); + assert.equal( mergedAttr.itemSize, 2, 'retains .itemSize' ); + assert.equal( mergedAttr.normalized, false, 'retains .normalized' ); + + } ); + + QUnit.test( 'mergeBufferAttributes - invalid', ( assert ) => { + + var array1 = new Float32Array( [ 1, 2, 3, 4 ] ); + var attr1 = new THREE.BufferAttribute( array1, 2, false ); + + var array2 = new Float32Array( [ 5, 6, 7, 8 ] ); + var attr2 = new THREE.BufferAttribute( array2, 4, false ); + + assert.notOk( THREE.BufferGeometryUtils.mergeBufferAttributes( [ attr1, attr2 ] ) ); + + attr2.itemSize = 2; + attr2.normalized = true; + + assert.notOk( THREE.BufferGeometryUtils.mergeBufferAttributes( [ attr1, attr2 ] ) ); + + attr2.normalized = false; + + assert.ok( THREE.BufferGeometryUtils.mergeBufferAttributes( [ attr1, attr2 ] ) ); + + } ); + + QUnit.test( 'mergeBufferGeometries - basic', ( assert ) => { + + var geometry1 = new THREE.BufferGeometry(); + geometry1.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( [ 1, 2, 3 ] ), 1, false ) ); + + var geometry2 = new THREE.BufferGeometry(); + geometry2.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( [ 4, 5, 6 ] ), 1, false ) ); + + var mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries( [ geometry1, geometry2 ] ); + + assert.ok( mergedGeometry, 'merge succeeds' ); + assert.smartEqual( Array.from( mergedGeometry.attributes.position.array ), [ 1, 2, 3, 4, 5, 6 ], 'merges elements' ); + assert.equal( mergedGeometry.attributes.position.itemSize, 1, 'retains .itemSize' ); + + } ); + + QUnit.test( 'mergeBufferGeometries - indexed', ( assert ) => { + + var geometry1 = new THREE.BufferGeometry(); + geometry1.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( [ 1, 2, 3 ] ), 1, false ) ); + geometry1.setIndex( new THREE.BufferAttribute( new Uint16Array( [ 0, 1, 2, 2, 1, 0 ] ), 1, false ) ); + + var geometry2 = new THREE.BufferGeometry(); + geometry2.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( [ 4, 5, 6 ] ), 1, false ) ); + geometry2.setIndex( new THREE.BufferAttribute( new Uint16Array( [ 0, 1, 2 ] ), 1, false ) ); + + var mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries( [ geometry1, geometry2 ] ); + + assert.ok( mergedGeometry, 'merge succeeds' ); + assert.smartEqual( Array.from( mergedGeometry.attributes.position.array ), [ 1, 2, 3, 4, 5, 6 ], 'merges elements' ); + assert.smartEqual( Array.from( mergedGeometry.index.array ), [ 0, 1, 2, 2, 1, 0, 3, 4, 5 ], 'merges indices' ); + assert.equal( mergedGeometry.attributes.position.itemSize, 1, 'retains .itemSize' ); + + } ); + + QUnit.test( 'mergeBufferGeometries - morph targets', ( assert ) => { + + var geometry1 = new THREE.BufferGeometry(); + geometry1.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( [ 1, 2, 3 ] ), 1, false ) ); + geometry1.morphAttributes.position = [ + new THREE.BufferAttribute( new Float32Array( [ 10, 20, 30 ] ), 1, false ), + new THREE.BufferAttribute( new Float32Array( [ 100, 200, 300 ] ), 1, false ) + ]; + + var geometry2 = new THREE.BufferGeometry(); + geometry2.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( [ 4, 5, 6 ] ), 1, false ) ); + geometry2.morphAttributes.position = [ + new THREE.BufferAttribute( new Float32Array( [ 40, 50, 60 ] ), 1, false ), + new THREE.BufferAttribute( new Float32Array( [ 400, 500, 600 ] ), 1, false ) + ]; + + var mergedGeometry = THREE.BufferGeometryUtils.mergeBufferGeometries( [ geometry1, geometry2 ] ); + + assert.ok( mergedGeometry, 'merge succeeds' ); + assert.smartEqual( Array.from( mergedGeometry.attributes.position.array ), [ 1, 2, 3, 4, 5, 6 ], 'merges elements' ); + assert.smartEqual( Array.from( mergedGeometry.morphAttributes.position[ 0 ].array ), [ 10, 20, 30, 40, 50, 60 ], 'merges morph targets' ); + assert.smartEqual( Array.from( mergedGeometry.morphAttributes.position[ 1 ].array ), [ 100, 200, 300, 400, 500, 600 ], 'merges morph targets' ); + assert.equal( mergedGeometry.attributes.position.itemSize, 1, 'retains .itemSize' ); + + } ); + + QUnit.test( 'mergeBufferGeometries - invalid', ( assert ) => { + + var geometry1 = new THREE.BufferGeometry(); + geometry1.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( [ 1, 2, 3 ] ), 1, false ) ); + geometry1.setIndex( new THREE.BufferAttribute( new Uint16Array( [ 0, 1, 2 ] ), 1, false ) ); + + var geometry2 = new THREE.BufferGeometry(); + geometry2.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( [ 4, 5, 6 ] ), 1, false ) ); + + assert.notOk( THREE.BufferGeometryUtils.mergeBufferGeometries( [ geometry1, geometry2 ] ) ); + + geometry2.setIndex( new THREE.BufferAttribute( new Uint16Array( [ 0, 1, 2 ] ), 1, false ) ); + + assert.ok( THREE.BufferGeometryUtils.mergeBufferGeometries( [ geometry1, geometry2 ] ) ); + + geometry2.addAttribute( 'foo', new THREE.BufferAttribute( new Float32Array( [ 1, 2, 3 ] ), 1, false ) ); + + assert.notOk( THREE.BufferGeometryUtils.mergeBufferGeometries( [ geometry1, geometry2 ] ) ); + + } ); + +} );