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.
+ 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.
+ 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 ] ) );
+
+ } );
+
+} );