瀏覽代碼

JSM: Convert utils/

Don McCurdy 6 年之前
父節點
當前提交
7201d6d1bf

+ 657 - 0
examples/jsm/utils/BufferGeometryUtils.js

@@ -0,0 +1,657 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ */
+
+import {
+	BufferAttribute,
+	BufferGeometry,
+	InterleavedBuffer,
+	InterleavedBufferAttribute,
+	Vector2,
+	Vector3
+} from "../../../build/three.module.js";
+
+var BufferGeometryUtils = {
+
+	computeTangents: function ( geometry ) {
+
+		var index = geometry.index;
+		var attributes = geometry.attributes;
+
+		// based on http://www.terathon.com/code/tangent.html
+		// (per vertex tangents)
+
+		if ( index === null ||
+			 attributes.position === undefined ||
+			 attributes.normal === undefined ||
+			 attributes.uv === undefined ) {
+
+			console.warn( 'THREE.BufferGeometry: Missing required attributes (index, position, normal or uv) in BufferGeometry.computeTangents()' );
+			return;
+
+		}
+
+		var indices = index.array;
+		var positions = attributes.position.array;
+		var normals = attributes.normal.array;
+		var uvs = attributes.uv.array;
+
+		var nVertices = positions.length / 3;
+
+		if ( attributes.tangent === undefined ) {
+
+			geometry.addAttribute( 'tangent', new BufferAttribute( new Float32Array( 4 * nVertices ), 4 ) );
+
+		}
+
+		var tangents = attributes.tangent.array;
+
+		var tan1 = [], tan2 = [];
+
+		for ( var i = 0; i < nVertices; i ++ ) {
+
+			tan1[ i ] = new Vector3();
+			tan2[ i ] = new Vector3();
+
+		}
+
+		var vA = new Vector3(),
+			vB = new Vector3(),
+			vC = new Vector3(),
+
+			uvA = new Vector2(),
+			uvB = new Vector2(),
+			uvC = new Vector2(),
+
+			sdir = new Vector3(),
+			tdir = new Vector3();
+
+		function handleTriangle( a, b, c ) {
+
+			vA.fromArray( positions, a * 3 );
+			vB.fromArray( positions, b * 3 );
+			vC.fromArray( positions, c * 3 );
+
+			uvA.fromArray( uvs, a * 2 );
+			uvB.fromArray( uvs, b * 2 );
+			uvC.fromArray( uvs, c * 2 );
+
+			var x1 = vB.x - vA.x;
+			var x2 = vC.x - vA.x;
+
+			var y1 = vB.y - vA.y;
+			var y2 = vC.y - vA.y;
+
+			var z1 = vB.z - vA.z;
+			var z2 = vC.z - vA.z;
+
+			var s1 = uvB.x - uvA.x;
+			var s2 = uvC.x - uvA.x;
+
+			var t1 = uvB.y - uvA.y;
+			var t2 = uvC.y - uvA.y;
+
+			var r = 1.0 / ( s1 * t2 - s2 * t1 );
+
+			sdir.set(
+				( t2 * x1 - t1 * x2 ) * r,
+				( t2 * y1 - t1 * y2 ) * r,
+				( t2 * z1 - t1 * z2 ) * r
+			);
+
+			tdir.set(
+				( s1 * x2 - s2 * x1 ) * r,
+				( s1 * y2 - s2 * y1 ) * r,
+				( s1 * z2 - s2 * z1 ) * r
+			);
+
+			tan1[ a ].add( sdir );
+			tan1[ b ].add( sdir );
+			tan1[ c ].add( sdir );
+
+			tan2[ a ].add( tdir );
+			tan2[ b ].add( tdir );
+			tan2[ c ].add( tdir );
+
+		}
+
+		var groups = geometry.groups;
+
+		if ( groups.length === 0 ) {
+
+			groups = [ {
+				start: 0,
+				count: indices.length
+			} ];
+
+		}
+
+		for ( var i = 0, il = groups.length; i < il; ++ i ) {
+
+			var group = groups[ i ];
+
+			var start = group.start;
+			var count = group.count;
+
+			for ( var j = start, jl = start + count; j < jl; j += 3 ) {
+
+				handleTriangle(
+					indices[ j + 0 ],
+					indices[ j + 1 ],
+					indices[ j + 2 ]
+				);
+
+			}
+
+		}
+
+		var tmp = new Vector3(), tmp2 = new Vector3();
+		var n = new Vector3(), n2 = new Vector3();
+		var w, t, test;
+
+		function handleVertex( v ) {
+
+			n.fromArray( normals, v * 3 );
+			n2.copy( n );
+
+			t = tan1[ v ];
+
+			// Gram-Schmidt orthogonalize
+
+			tmp.copy( t );
+			tmp.sub( n.multiplyScalar( n.dot( t ) ) ).normalize();
+
+			// Calculate handedness
+
+			tmp2.crossVectors( n2, t );
+			test = tmp2.dot( tan2[ v ] );
+			w = ( test < 0.0 ) ? - 1.0 : 1.0;
+
+			tangents[ v * 4 ] = tmp.x;
+			tangents[ v * 4 + 1 ] = tmp.y;
+			tangents[ v * 4 + 2 ] = tmp.z;
+			tangents[ v * 4 + 3 ] = w;
+
+		}
+
+		for ( var i = 0, il = groups.length; i < il; ++ i ) {
+
+			var group = groups[ i ];
+
+			var start = group.start;
+			var count = group.count;
+
+			for ( var j = start, jl = start + count; j < jl; j += 3 ) {
+
+				handleVertex( indices[ j + 0 ] );
+				handleVertex( indices[ j + 1 ] );
+				handleVertex( indices[ j + 2 ] );
+
+			}
+
+		}
+
+	},
+
+	/**
+	 * @param  {Array<BufferGeometry>} geometries
+	 * @param  {Boolean} useGroups
+	 * @return {BufferGeometry}
+	 */
+	mergeBufferGeometries: function ( geometries, useGroups ) {
+
+		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 BufferGeometry();
+
+		var offset = 0;
+
+		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
+
+			mergedGeometry.userData.mergedUserData = mergedGeometry.userData.mergedUserData || [];
+			mergedGeometry.userData.mergedUserData.push( geometry.userData );
+
+			if ( useGroups ) {
+
+				var count;
+
+				if ( isIndexed ) {
+
+					count = geometry.index.count;
+
+				} else if ( geometry.attributes.position !== undefined ) {
+
+					count = geometry.attributes.position.count;
+
+				} else {
+
+					return null;
+
+				}
+
+				mergedGeometry.addGroup( offset, count, i );
+
+				offset += count;
+
+			}
+
+		}
+
+		// merge indices
+
+		if ( isIndexed ) {
+
+			var indexOffset = 0;
+			var mergedIndex = [];
+
+			for ( var i = 0; i < geometries.length; ++ i ) {
+
+				var index = geometries[ i ].index;
+
+				for ( var j = 0; j < index.count; ++ j ) {
+
+					mergedIndex.push( index.getX( j ) + indexOffset );
+
+				}
+
+				indexOffset += geometries[ i ].attributes.position.count;
+
+			}
+
+			mergedGeometry.setIndex( 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<BufferAttribute>} attributes
+	 * @return {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 i = 0; i < attributes.length; ++ i ) {
+
+			array.set( attributes[ i ].array, offset );
+
+			offset += attributes[ i ].array.length;
+
+		}
+
+		return new BufferAttribute( array, itemSize, normalized );
+
+	},
+
+	/**
+	 * @param {Array<BufferAttribute>} attributes
+	 * @return {Array<InterleavedBufferAttribute>}
+	 */
+	interleaveAttributes: function ( attributes ) {
+
+		// Interleaves the provided attributes into an InterleavedBuffer and returns
+		// a set of InterleavedBufferAttributes for each attribute
+		var TypedArray;
+		var arrayLength = 0;
+		var stride = 0;
+
+		// calculate the the length and type of the interleavedBuffer
+		for ( var i = 0, l = attributes.length; i < l; ++ i ) {
+
+			var attribute = attributes[ i ];
+
+			if ( TypedArray === undefined ) TypedArray = attribute.array.constructor;
+			if ( TypedArray !== attribute.array.constructor ) {
+
+				console.warn( 'AttributeBuffers of different types cannot be interleaved' );
+				return null;
+
+			}
+
+			arrayLength += attribute.array.length;
+			stride += attribute.itemSize;
+
+		}
+
+		// Create the set of buffer attributes
+		var interleavedBuffer = new InterleavedBuffer( new TypedArray( arrayLength ), stride );
+		var offset = 0;
+		var res = [];
+		var getters = [ 'getX', 'getY', 'getZ', 'getW' ];
+		var setters = [ 'setX', 'setY', 'setZ', 'setW' ];
+
+		for ( var j = 0, l = attributes.length; j < l; j ++ ) {
+
+			var attribute = attributes[ j ];
+			var itemSize = attribute.itemSize;
+			var count = attribute.count;
+			var iba = new InterleavedBufferAttribute( interleavedBuffer, itemSize, offset, attribute.normalized );
+			res.push( iba );
+
+			offset += itemSize;
+
+			// Move the data for each attribute into the new interleavedBuffer
+			// at the appropriate offset
+			for ( var c = 0; c < count; c ++ ) {
+
+				for ( var k = 0; k < itemSize; k ++ ) {
+
+					iba[ setters[ k ] ]( c, attribute[ getters[ k ] ]( c ) );
+
+				}
+
+			}
+
+		}
+
+		return res;
+
+	},
+
+	/**
+	 * @param {Array<BufferGeometry>} geometry
+	 * @return {number}
+	 */
+	estimateBytesUsed: function ( geometry ) {
+
+		// Return the estimated memory used by this geometry in bytes
+		// Calculate using itemSize, count, and BYTES_PER_ELEMENT to account
+		// for InterleavedBufferAttributes.
+		var mem = 0;
+		for ( var name in geometry.attributes ) {
+
+			var attr = geometry.getAttribute( name );
+			mem += attr.count * attr.itemSize * attr.array.BYTES_PER_ELEMENT;
+
+		}
+
+		var indices = geometry.getIndex();
+		mem += indices ? indices.count * indices.itemSize * indices.array.BYTES_PER_ELEMENT : 0;
+		return mem;
+
+	},
+
+	/**
+	 * @param {BufferGeometry} geometry
+	 * @param {number} tolerance
+	 * @return {BufferGeometry>}
+	 */
+	mergeVertices: function ( geometry, tolerance = 1e-4 ) {
+
+		tolerance = Math.max( tolerance, Number.EPSILON );
+
+		// Generate an index buffer if the geometry doesn't have one, or optimize it
+		// if it's already available.
+		var hashToIndex = {};
+		var indices = geometry.getIndex();
+		var positions = geometry.getAttribute( 'position' );
+		var vertexCount = indices ? indices.count : positions.count;
+
+		// next value for triangle indices
+		var nextIndex = 0;
+
+		// attributes and new attribute arrays
+		var attributeNames = Object.keys( geometry.attributes );
+		var attrArrays = {};
+		var morphAttrsArrays = {};
+		var newIndices = [];
+		var getters = [ 'getX', 'getY', 'getZ', 'getW' ];
+
+		// initialize the arrays
+		for ( var i = 0, l = attributeNames.length; i < l; i ++ ) {
+			var name = attributeNames[ i ];
+
+			attrArrays[ name ] = [];
+
+			var morphAttr = geometry.morphAttributes[ name ];
+			if ( morphAttr ) {
+
+				morphAttrsArrays[ name ] = new Array( morphAttr.length ).fill().map( () => [] );
+
+			}
+
+		}
+
+		// convert the error tolerance to an amount of decimal places to truncate to
+		var decimalShift = Math.log10( 1 / tolerance );
+		var shiftMultiplier = Math.pow( 10, decimalShift );
+		for ( var i = 0; i < vertexCount; i ++ ) {
+
+			var index = indices ? indices.getX( i ) : i;
+
+			// Generate a hash for the vertex attributes at the current index 'i'
+			var hash = '';
+			for ( var j = 0, l = attributeNames.length; j < l; j ++ ) {
+
+				var name = attributeNames[ j ];
+				var attribute = geometry.getAttribute( name );
+				var itemSize = attribute.itemSize;
+
+				for ( var k = 0; k < itemSize; k ++ ) {
+
+					// double tilde truncates the decimal value
+					hash += `${ ~ ~ ( attribute[ getters[ k ] ]( index ) * shiftMultiplier ) },`;
+
+				}
+
+			}
+
+			// Add another reference to the vertex if it's already
+			// used by another index
+			if ( hash in hashToIndex ) {
+
+				newIndices.push( hashToIndex[ hash ] );
+
+			} else {
+
+				// copy data to the new index in the attribute arrays
+				for ( var j = 0, l = attributeNames.length; j < l; j ++ ) {
+
+					var name = attributeNames[ j ];
+					var attribute = geometry.getAttribute( name );
+					var morphAttr = geometry.morphAttributes[ name ];
+					var itemSize = attribute.itemSize;
+					var newarray = attrArrays[ name ];
+					var newMorphArrays = morphAttrsArrays[ name ];
+
+					for ( var k = 0; k < itemSize; k ++ ) {
+
+						var getterFunc = getters[ k ];
+						newarray.push( attribute[ getterFunc ]( index ) );
+
+						if ( morphAttr ) {
+
+							for ( var m = 0, ml = morphAttr.length; m < ml; m ++ ) {
+
+								newMorphArrays[ m ].push( morphAttr[ m ][ getterFunc ]( index ) );
+
+							}
+
+						}
+
+					}
+
+				}
+
+				hashToIndex[ hash ] = nextIndex;
+				newIndices.push( nextIndex );
+				nextIndex ++;
+
+			}
+
+		}
+
+		// Generate typed arrays from new attribute arrays and update
+		// the attributeBuffers
+		const result = geometry.clone();
+		for ( var i = 0, l = attributeNames.length; i < l; i ++ ) {
+
+			var name = attributeNames[ i ];
+			var oldAttribute = geometry.getAttribute( name );
+			var attribute;
+
+			var buffer = new oldAttribute.array.constructor( attrArrays[ name ] );
+			if ( oldAttribute.isInterleavedBufferAttribute ) {
+
+				attribute = new BufferAttribute( buffer, oldAttribute.itemSize, oldAttribute.itemSize );
+
+			} else {
+
+				attribute = geometry.getAttribute( name ).clone();
+				attribute.setArray( buffer );
+
+			}
+
+			result.addAttribute( name, attribute );
+
+			// Update the attribute arrays
+			if ( name in morphAttrsArrays ) {
+
+				for ( var j = 0; j < morphAttrsArrays[ name ].length; j ++ ) {
+
+					var morphAttribute = geometry.morphAttributes[ name ][ j ].clone();
+					morphAttribute.setArray( new morphAttribute.array.constructor( morphAttrsArrays[ name ][ j ] ) );
+					result.morphAttributes[ name ][ j ] = morphAttribute;
+
+				}
+
+			}
+
+		}
+
+		// Generate an index buffer typed array
+		var cons = Uint8Array;
+		if ( newIndices.length >= Math.pow( 2, 8 ) ) cons = Uint16Array;
+		if ( newIndices.length >= Math.pow( 2, 16 ) ) cons = Uint32Array;
+
+		var newIndexBuffer = new cons( newIndices );
+		var newIndices = null;
+		if ( indices === null ) {
+
+			newIndices = new BufferAttribute( newIndexBuffer, 1 );
+
+		} else {
+
+			newIndices = geometry.getIndex().clone();
+			newIndices.setArray( newIndexBuffer );
+
+		}
+
+		result.setIndex( newIndices );
+
+		return result;
+
+	}
+
+};
+
+export { BufferGeometryUtils };

+ 307 - 0
examples/jsm/utils/GeometryUtils.js

@@ -0,0 +1,307 @@
+/**
+ * @author mrdoob / http://mrdoob.com/
+ * @author alteredq / http://alteredqualia.com/
+ */
+
+import {
+	Mesh,
+	Vector3
+} from "../../../build/three.module.js";
+
+var GeometryUtils = {
+
+	// Merge two geometries or geometry and geometry from object (using object's transform)
+
+	merge: function ( geometry1, geometry2, materialIndexOffset ) {
+
+		console.warn( 'THREE.GeometryUtils: .merge() has been moved to Geometry. Use geometry.merge( geometry2, matrix, materialIndexOffset ) instead.' );
+
+		var matrix;
+
+		if ( geometry2 instanceof Mesh ) {
+
+			geometry2.matrixAutoUpdate && geometry2.updateMatrix();
+
+			matrix = geometry2.matrix;
+			geometry2 = geometry2.geometry;
+
+		}
+
+		geometry1.merge( geometry2, matrix, materialIndexOffset );
+
+	},
+
+	// Get random point in triangle (via barycentric coordinates)
+	// 	(uniform distribution)
+	// 	http://www.cgafaq.info/wiki/Random_Point_In_Triangle
+
+	randomPointInTriangle: function () {
+
+		var vector = new Vector3();
+
+		return function ( vectorA, vectorB, vectorC ) {
+
+			var point = new Vector3();
+
+			var a = Math.random();
+			var b = Math.random();
+
+			if ( ( a + b ) > 1 ) {
+
+				a = 1 - a;
+				b = 1 - b;
+
+			}
+
+			var c = 1 - a - b;
+
+			point.copy( vectorA );
+			point.multiplyScalar( a );
+
+			vector.copy( vectorB );
+			vector.multiplyScalar( b );
+
+			point.add( vector );
+
+			vector.copy( vectorC );
+			vector.multiplyScalar( c );
+
+			point.add( vector );
+
+			return point;
+
+		};
+
+	}(),
+
+	// Get random point in face (triangle)
+	// (uniform distribution)
+
+	randomPointInFace: function ( face, geometry ) {
+
+		var vA, vB, vC;
+
+		vA = geometry.vertices[ face.a ];
+		vB = geometry.vertices[ face.b ];
+		vC = geometry.vertices[ face.c ];
+
+		return GeometryUtils.randomPointInTriangle( vA, vB, vC );
+
+	},
+
+	// Get uniformly distributed random points in mesh
+	// 	- create array with cumulative sums of face areas
+	//  - pick random number from 0 to total area
+	//  - find corresponding place in area array by binary search
+	//	- get random point in face
+
+	randomPointsInGeometry: function ( geometry, n ) {
+
+		var face, i,
+			faces = geometry.faces,
+			vertices = geometry.vertices,
+			il = faces.length,
+			totalArea = 0,
+			cumulativeAreas = [],
+			vA, vB, vC;
+
+		// precompute face areas
+
+		for ( i = 0; i < il; i ++ ) {
+
+			face = faces[ i ];
+
+			vA = vertices[ face.a ];
+			vB = vertices[ face.b ];
+			vC = vertices[ face.c ];
+
+			face._area = GeometryUtils.triangleArea( vA, vB, vC );
+
+			totalArea += face._area;
+
+			cumulativeAreas[ i ] = totalArea;
+
+		}
+
+		// binary search cumulative areas array
+
+		function binarySearchIndices( value ) {
+
+			function binarySearch( start, end ) {
+
+				// return closest larger index
+				// if exact number is not found
+
+				if ( end < start )
+					return start;
+
+				var mid = start + Math.floor( ( end - start ) / 2 );
+
+				if ( cumulativeAreas[ mid ] > value ) {
+
+					return binarySearch( start, mid - 1 );
+
+				} else if ( cumulativeAreas[ mid ] < value ) {
+
+					return binarySearch( mid + 1, end );
+
+				} else {
+
+					return mid;
+
+				}
+
+			}
+
+			var result = binarySearch( 0, cumulativeAreas.length - 1 );
+			return result;
+
+		}
+
+		// pick random face weighted by face area
+
+		var r, index,
+			result = [];
+
+		var stats = {};
+
+		for ( i = 0; i < n; i ++ ) {
+
+			r = Math.random() * totalArea;
+
+			index = binarySearchIndices( r );
+
+			result[ i ] = GeometryUtils.randomPointInFace( faces[ index ], geometry );
+
+			if ( ! stats[ index ] ) {
+
+				stats[ index ] = 1;
+
+			} else {
+
+				stats[ index ] += 1;
+
+			}
+
+		}
+
+		return result;
+
+	},
+
+	randomPointsInBufferGeometry: function ( geometry, n ) {
+
+		var i,
+			vertices = geometry.attributes.position.array,
+			totalArea = 0,
+			cumulativeAreas = [],
+			vA, vB, vC;
+
+		// precompute face areas
+		vA = new Vector3();
+		vB = new Vector3();
+		vC = new Vector3();
+
+		// geometry._areas = [];
+		var il = vertices.length / 9;
+
+		for ( i = 0; i < il; i ++ ) {
+
+			vA.set( vertices[ i * 9 + 0 ], vertices[ i * 9 + 1 ], vertices[ i * 9 + 2 ] );
+			vB.set( vertices[ i * 9 + 3 ], vertices[ i * 9 + 4 ], vertices[ i * 9 + 5 ] );
+			vC.set( vertices[ i * 9 + 6 ], vertices[ i * 9 + 7 ], vertices[ i * 9 + 8 ] );
+
+			totalArea += GeometryUtils.triangleArea( vA, vB, vC );
+
+			cumulativeAreas.push( totalArea );
+
+		}
+
+		// binary search cumulative areas array
+
+		function binarySearchIndices( value ) {
+
+			function binarySearch( start, end ) {
+
+				// return closest larger index
+				// if exact number is not found
+
+				if ( end < start )
+					return start;
+
+				var mid = start + Math.floor( ( end - start ) / 2 );
+
+				if ( cumulativeAreas[ mid ] > value ) {
+
+					return binarySearch( start, mid - 1 );
+
+				} else if ( cumulativeAreas[ mid ] < value ) {
+
+					return binarySearch( mid + 1, end );
+
+				} else {
+
+					return mid;
+
+				}
+
+			}
+
+			var result = binarySearch( 0, cumulativeAreas.length - 1 );
+			return result;
+
+		}
+
+		// pick random face weighted by face area
+
+		var r, index,
+			result = [];
+
+		for ( i = 0; i < n; i ++ ) {
+
+			r = Math.random() * totalArea;
+
+			index = binarySearchIndices( r );
+
+			// result[ i ] = GeometryUtils.randomPointInFace( faces[ index ], geometry, true );
+			vA.set( vertices[ index * 9 + 0 ], vertices[ index * 9 + 1 ], vertices[ index * 9 + 2 ] );
+			vB.set( vertices[ index * 9 + 3 ], vertices[ index * 9 + 4 ], vertices[ index * 9 + 5 ] );
+			vC.set( vertices[ index * 9 + 6 ], vertices[ index * 9 + 7 ], vertices[ index * 9 + 8 ] );
+			result[ i ] = GeometryUtils.randomPointInTriangle( vA, vB, vC );
+
+		}
+
+		return result;
+
+	},
+
+	// Get triangle area (half of parallelogram)
+	// http://mathworld.wolfram.com/TriangleArea.html
+
+	triangleArea: function () {
+
+		var vector1 = new Vector3();
+		var vector2 = new Vector3();
+
+		return function ( vectorA, vectorB, vectorC ) {
+
+			vector1.subVectors( vectorB, vectorA );
+			vector2.subVectors( vectorC, vectorA );
+			vector1.cross( vector2 );
+
+			return 0.5 * vector1.length();
+
+		};
+
+	}(),
+
+	center: function ( geometry ) {
+
+		console.warn( 'THREE.GeometryUtils: .center() has been moved to Geometry. Use geometry.center() instead.' );
+		return geometry.center();
+
+	}
+
+};
+
+export { GeometryUtils };

+ 67 - 0
examples/jsm/utils/MathUtils.js

@@ -0,0 +1,67 @@
+/**
+ * @author WestLangley / http://github.com/WestLangley
+ * @author thezwap / http://github.com/thezwap
+ */
+
+
+
+var MathUtils = {
+
+    setQuaternionFromProperEuler: function ( q, a, b, c, order ) {
+
+        // Intrinsic Proper Euler Angles - see https://en.wikipedia.org/wiki/Euler_angles
+
+        // rotations are applied to the axes in the order specified by 'order'
+        // rotation by angle 'a' is applied first, then by angle 'b', then by angle 'c'
+        // angles are in radians
+
+        var cos = Math.cos;
+        var sin = Math.sin;
+
+        var c2 = cos( b / 2 );
+        var s2 = sin( b / 2 );
+
+        var c13 = cos( ( a + c ) / 2 );
+        var s13 = sin( ( a + c ) / 2 );
+
+        var c1_3 = cos( ( a - c ) / 2 );
+        var s1_3 = sin( ( a - c ) / 2 );
+
+        var c3_1 = cos( ( c - a ) / 2 );
+        var s3_1 = sin( ( c - a ) / 2 );
+
+        if ( order === 'XYX' ) {
+
+            q.set( c2 * s13, s2 * c1_3, s2 * s1_3, c2 * c13 );
+
+        } else if ( order === 'YZY' ) {
+
+            q.set( s2 * s1_3, c2 * s13, s2 * c1_3, c2 * c13 );
+
+        } else if ( order === 'ZXZ' ) {
+
+            q.set( s2 * c1_3, s2 * s1_3, c2 * s13, c2 * c13 );
+
+        } else if ( order === 'XZX' ) {
+
+            q.set( c2 * s13, s2 * s3_1, s2 * c3_1, c2 * c13 );
+
+        } else if ( order === 'YXY' ) {
+
+            q.set( s2 * c3_1, c2 * s13, s2 * s3_1, c2 * c13 );
+
+        } else if ( order === 'ZYZ' ) {
+
+            q.set( s2 * s3_1, s2 * c3_1, c2 * s13, c2 * c13 );
+
+        } else {
+
+            console.warn( 'THREE.MathUtils: .setQuaternionFromProperEuler() encountered an unknown order.' );
+
+        }
+
+    }
+
+};
+
+export { MathUtils };

+ 46 - 0
examples/jsm/utils/SceneUtils.js

@@ -0,0 +1,46 @@
+/**
+ * @author alteredq / http://alteredqualia.com/
+ */
+
+import {
+	Group,
+	Matrix4,
+	Mesh
+} from "../../../build/three.module.js";
+
+var SceneUtils = {
+
+	createMultiMaterialObject: function ( geometry, materials ) {
+
+		var group = new Group();
+
+		for ( var i = 0, l = materials.length; i < l; i ++ ) {
+
+			group.add( new Mesh( geometry, materials[ i ] ) );
+
+		}
+
+		return group;
+
+	},
+
+	detach: function ( child, parent, scene ) {
+
+		child.applyMatrix( parent.matrixWorld );
+		parent.remove( child );
+		scene.add( child );
+
+	},
+
+	attach: function ( child, scene, parent ) {
+
+		child.applyMatrix( new Matrix4().getInverse( parent.matrixWorld ) );
+
+		scene.remove( child );
+		parent.add( child );
+
+	}
+
+};
+
+export { SceneUtils };

+ 208 - 0
examples/jsm/utils/ShadowMapViewer.js

@@ -0,0 +1,208 @@
+/**
+ * @author arya-s / https://github.com/arya-s
+ *
+ * This is a helper for visualising a given light's shadow map.
+ * It works for shadow casting lights: THREE.DirectionalLight and THREE.SpotLight.
+ * It renders out the shadow map and displays it on a HUD.
+ *
+ * Example usage:
+ *	1) Include <script src='examples/js/utils/ShadowMapViewer.js'><script> in your html file
+ *
+ *	2) Create a shadow casting light and name it optionally:
+ *		var light = new THREE.DirectionalLight( 0xffffff, 1 );
+ *		light.castShadow = true;
+ *		light.name = 'Sun';
+ *
+ *	3) Create a shadow map viewer for that light and set its size and position optionally:
+ *		var shadowMapViewer = new ShadowMapViewer( light );
+ *		shadowMapViewer.size.set( 128, 128 );	//width, height  default: 256, 256
+ *		shadowMapViewer.position.set( 10, 10 );	//x, y in pixel	 default: 0, 0 (top left corner)
+ *
+ *	4) Render the shadow map viewer in your render loop:
+ *		shadowMapViewer.render( renderer );
+ *
+ *	5) Optionally: Update the shadow map viewer on window resize:
+ *		shadowMapViewer.updateForWindowResize();
+ *
+ *	6) If you set the position or size members directly, you need to call shadowMapViewer.update();
+ */
+
+import {
+	DoubleSide,
+	LinearFilter,
+	Mesh,
+	MeshBasicMaterial,
+	OrthographicCamera,
+	PlaneBufferGeometry,
+	Scene,
+	ShaderMaterial,
+	Texture,
+	UniformsUtils,
+	UnpackDepthRGBAShader
+} from "../../../build/three.module.js";
+
+var ShadowMapViewer = function ( light ) {
+
+	//- Internals
+	var scope = this;
+	var doRenderLabel = ( light.name !== undefined && light.name !== '' );
+	var userAutoClearSetting;
+
+	//Holds the initial position and dimension of the HUD
+	var frame = {
+		x: 10,
+		y: 10,
+		width: 256,
+		height: 256
+	};
+
+	var camera = new OrthographicCamera( window.innerWidth / - 2, window.innerWidth / 2, window.innerHeight / 2, window.innerHeight / - 2, 1, 10 );
+	camera.position.set( 0, 0, 2 );
+	var scene = new Scene();
+
+	//HUD for shadow map
+	var shader = UnpackDepthRGBAShader;
+
+	var uniforms = new UniformsUtils.clone( shader.uniforms );
+	var material = new ShaderMaterial( {
+		uniforms: uniforms,
+		vertexShader: shader.vertexShader,
+		fragmentShader: shader.fragmentShader
+	} );
+	var plane = new PlaneBufferGeometry( frame.width, frame.height );
+	var mesh = new Mesh( plane, material );
+
+	scene.add( mesh );
+
+
+	//Label for light's name
+	var labelCanvas, labelMesh;
+
+	if ( doRenderLabel ) {
+
+		labelCanvas = document.createElement( 'canvas' );
+
+		var context = labelCanvas.getContext( '2d' );
+		context.font = 'Bold 20px Arial';
+
+		var labelWidth = context.measureText( light.name ).width;
+		labelCanvas.width = labelWidth;
+		labelCanvas.height = 25;	//25 to account for g, p, etc.
+
+		context.font = 'Bold 20px Arial';
+		context.fillStyle = 'rgba( 255, 0, 0, 1 )';
+		context.fillText( light.name, 0, 20 );
+
+		var labelTexture = new Texture( labelCanvas );
+		labelTexture.magFilter = LinearFilter;
+		labelTexture.minFilter = LinearFilter;
+		labelTexture.needsUpdate = true;
+
+		var labelMaterial = new MeshBasicMaterial( { map: labelTexture, side: DoubleSide } );
+		labelMaterial.transparent = true;
+
+		var labelPlane = new PlaneBufferGeometry( labelCanvas.width, labelCanvas.height );
+		labelMesh = new Mesh( labelPlane, labelMaterial );
+
+		scene.add( labelMesh );
+
+	}
+
+
+	function resetPosition () {
+
+		scope.position.set( scope.position.x, scope.position.y );
+
+	}
+
+	//- API
+	// Set to false to disable displaying this shadow map
+	this.enabled = true;
+
+	// Set the size of the displayed shadow map on the HUD
+	this.size = {
+		width: frame.width,
+		height: frame.height,
+		set: function ( width, height ) {
+
+			this.width = width;
+			this.height = height;
+
+			mesh.scale.set( this.width / frame.width, this.height / frame.height, 1 );
+
+			//Reset the position as it is off when we scale stuff
+			resetPosition();
+
+		}
+	};
+
+	// Set the position of the displayed shadow map on the HUD
+	this.position = {
+		x: frame.x,
+		y: frame.y,
+		set: function ( x, y ) {
+
+			this.x = x;
+			this.y = y;
+
+			var width = scope.size.width;
+			var height = scope.size.height;
+
+			mesh.position.set( - window.innerWidth / 2 + width / 2 + this.x, window.innerHeight / 2 - height / 2 - this.y, 0 );
+
+			if ( doRenderLabel ) labelMesh.position.set( mesh.position.x, mesh.position.y - scope.size.height / 2 + labelCanvas.height / 2, 0 );
+
+		}
+	};
+
+	this.render = function ( renderer ) {
+
+		if ( this.enabled ) {
+
+			//Because a light's .shadowMap is only initialised after the first render pass
+			//we have to make sure the correct map is sent into the shader, otherwise we
+			//always end up with the scene's first added shadow casting light's shadowMap
+			//in the shader
+			//See: https://github.com/mrdoob/three.js/issues/5932
+			uniforms.tDiffuse.value = light.shadow.map.texture;
+
+			userAutoClearSetting = renderer.autoClear;
+			renderer.autoClear = false; // To allow render overlay
+			renderer.clearDepth();
+			renderer.render( scene, camera );
+			renderer.autoClear = userAutoClearSetting;	//Restore user's setting
+
+		}
+
+	};
+
+	this.updateForWindowResize = function () {
+
+		if ( this.enabled ) {
+
+			 camera.left = window.innerWidth / - 2;
+			 camera.right = window.innerWidth / 2;
+			 camera.top = window.innerHeight / 2;
+			 camera.bottom = window.innerHeight / - 2;
+			 camera.updateProjectionMatrix();
+
+			 this.update();
+		}
+
+	};
+
+	this.update = function () {
+
+		this.position.set( this.position.x, this.position.y );
+		this.size.set( this.size.width, this.size.height );
+
+	};
+
+	//Force an update to set position/size
+	this.update();
+
+};
+
+ShadowMapViewer.prototype.constructor = ShadowMapViewer;
+
+export { ShadowMapViewer };

+ 548 - 0
examples/jsm/utils/SkeletonUtils.js

@@ -0,0 +1,548 @@
+/**
+ * @author sunag / http://www.sunag.com.br
+ */
+
+import {
+	AnimationClip,
+	AnimationMixer,
+	Euler,
+	Matrix4,
+	Quaternion,
+	QuaternionKeyframeTrack,
+	SkeletonHelper,
+	Vector2,
+	Vector3,
+	VectorKeyframeTrack
+} from "../../../build/three.module.js";
+
+'use strict';
+
+var SkeletonUtils = {
+
+	retarget: function () {
+
+		var pos = new Vector3(),
+			quat = new Quaternion(),
+			scale = new Vector3(),
+			bindBoneMatrix = new Matrix4(),
+			relativeMatrix = new Matrix4(),
+			globalMatrix = new Matrix4();
+
+		return function ( target, source, options ) {
+
+			options = options || {};
+			options.preserveMatrix = options.preserveMatrix !== undefined ? options.preserveMatrix : true;
+			options.preservePosition = options.preservePosition !== undefined ? options.preservePosition : true;
+			options.preserveHipPosition = options.preserveHipPosition !== undefined ? options.preserveHipPosition : false;
+			options.useTargetMatrix = options.useTargetMatrix !== undefined ? options.useTargetMatrix : false;
+			options.hip = options.hip !== undefined ? options.hip : "hip";
+			options.names = options.names || {};
+
+			var sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ),
+				bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ),
+				bindBones,
+				bone, name, boneTo,
+				bonesPosition, i;
+
+			// reset bones
+
+			if ( target.isObject3D ) {
+
+				target.skeleton.pose();
+
+			} else {
+
+				options.useTargetMatrix = true;
+				options.preserveMatrix = false;
+
+			}
+
+			if ( options.preservePosition ) {
+
+				bonesPosition = [];
+
+				for ( i = 0; i < bones.length; i ++ ) {
+
+					bonesPosition.push( bones[ i ].position.clone() );
+
+				}
+
+			}
+
+			if ( options.preserveMatrix ) {
+
+				// reset matrix
+
+				target.updateMatrixWorld();
+
+				target.matrixWorld.identity();
+
+				// reset children matrix
+
+				for ( i = 0; i < target.children.length; ++ i ) {
+
+					target.children[ i ].updateMatrixWorld( true );
+
+				}
+
+			}
+
+			if ( options.offsets ) {
+
+				bindBones = [];
+
+				for ( i = 0; i < bones.length; ++ i ) {
+
+					bone = bones[ i ];
+					name = options.names[ bone.name ] || bone.name;
+
+					if ( options.offsets && options.offsets[ name ] ) {
+
+						bone.matrix.multiply( options.offsets[ name ] );
+
+						bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
+
+						bone.updateMatrixWorld();
+
+					}
+
+					bindBones.push( bone.matrixWorld.clone() );
+
+				}
+
+			}
+
+			for ( i = 0; i < bones.length; ++ i ) {
+
+				bone = bones[ i ];
+				name = options.names[ bone.name ] || bone.name;
+
+				boneTo = this.getBoneByName( name, sourceBones );
+
+				globalMatrix.copy( bone.matrixWorld );
+
+				if ( boneTo ) {
+
+					boneTo.updateMatrixWorld();
+
+					if ( options.useTargetMatrix ) {
+
+						relativeMatrix.copy( boneTo.matrixWorld );
+
+					} else {
+
+						relativeMatrix.getInverse( target.matrixWorld );
+						relativeMatrix.multiply( boneTo.matrixWorld );
+
+					}
+
+					// ignore scale to extract rotation
+
+					scale.setFromMatrixScale( relativeMatrix );
+					relativeMatrix.scale( scale.set( 1 / scale.x, 1 / scale.y, 1 / scale.z ) );
+
+					// apply to global matrix
+
+					globalMatrix.makeRotationFromQuaternion( quat.setFromRotationMatrix( relativeMatrix ) );
+
+					if ( target.isObject3D ) {
+
+						var boneIndex = bones.indexOf( bone ),
+							wBindMatrix = bindBones ? bindBones[ boneIndex ] : bindBoneMatrix.getInverse( target.skeleton.boneInverses[ boneIndex ] );
+
+						globalMatrix.multiply( wBindMatrix );
+
+					}
+
+					globalMatrix.copyPosition( relativeMatrix );
+
+				}
+
+				if ( bone.parent && bone.parent.isBone ) {
+
+					bone.matrix.getInverse( bone.parent.matrixWorld );
+					bone.matrix.multiply( globalMatrix );
+
+				} else {
+
+					bone.matrix.copy( globalMatrix );
+
+				}
+
+				if ( options.preserveHipPosition && name === options.hip ) {
+
+					bone.matrix.setPosition( pos.set( 0, bone.position.y, 0 ) );
+
+				}
+
+				bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
+
+				bone.updateMatrixWorld();
+
+			}
+
+			if ( options.preservePosition ) {
+
+				for ( i = 0; i < bones.length; ++ i ) {
+
+					bone = bones[ i ];
+					name = options.names[ bone.name ] || bone.name;
+
+					if ( name !== options.hip ) {
+
+						bone.position.copy( bonesPosition[ i ] );
+
+					}
+
+				}
+
+			}
+
+			if ( options.preserveMatrix ) {
+
+				// restore matrix
+
+				target.updateMatrixWorld( true );
+
+			}
+
+		};
+
+	}(),
+
+	retargetClip: function ( target, source, clip, options ) {
+
+		options = options || {};
+		options.useFirstFramePosition = options.useFirstFramePosition !== undefined ? options.useFirstFramePosition : false;
+		options.fps = options.fps !== undefined ? options.fps : 30;
+		options.names = options.names || [];
+
+		if ( ! source.isObject3D ) {
+
+			source = this.getHelperFromSkeleton( source );
+
+		}
+
+		var numFrames = Math.round( clip.duration * ( options.fps / 1000 ) * 1000 ),
+			delta = 1 / options.fps,
+			convertedTracks = [],
+			mixer = new AnimationMixer( source ),
+			bones = this.getBones( target.skeleton ),
+			boneDatas = [],
+			positionOffset,
+			bone, boneTo, boneData,
+			name, i, j;
+
+		mixer.clipAction( clip ).play();
+		mixer.update( 0 );
+
+		source.updateMatrixWorld();
+
+		for ( i = 0; i < numFrames; ++ i ) {
+
+			var time = i * delta;
+
+			this.retarget( target, source, options );
+
+			for ( j = 0; j < bones.length; ++ j ) {
+
+				name = options.names[ bones[ j ].name ] || bones[ j ].name;
+
+				boneTo = this.getBoneByName( name, source.skeleton );
+
+				if ( boneTo ) {
+
+					bone = bones[ j ];
+					boneData = boneDatas[ j ] = boneDatas[ j ] || { bone: bone };
+
+					if ( options.hip === name ) {
+
+						if ( ! boneData.pos ) {
+
+							boneData.pos = {
+								times: new Float32Array( numFrames ),
+								values: new Float32Array( numFrames * 3 )
+							};
+
+						}
+
+						if ( options.useFirstFramePosition ) {
+
+							if ( i === 0 ) {
+
+								positionOffset = bone.position.clone();
+
+							}
+
+							bone.position.sub( positionOffset );
+
+						}
+
+						boneData.pos.times[ i ] = time;
+
+						bone.position.toArray( boneData.pos.values, i * 3 );
+
+					}
+
+					if ( ! boneData.quat ) {
+
+						boneData.quat = {
+							times: new Float32Array( numFrames ),
+							values: new Float32Array( numFrames * 4 )
+						};
+
+					}
+
+					boneData.quat.times[ i ] = time;
+
+					bone.quaternion.toArray( boneData.quat.values, i * 4 );
+
+				}
+
+			}
+
+			mixer.update( delta );
+
+			source.updateMatrixWorld();
+
+		}
+
+		for ( i = 0; i < boneDatas.length; ++ i ) {
+
+			boneData = boneDatas[ i ];
+
+			if ( boneData ) {
+
+				if ( boneData.pos ) {
+
+					convertedTracks.push( new VectorKeyframeTrack(
+						".bones[" + boneData.bone.name + "].position",
+						boneData.pos.times,
+						boneData.pos.values
+					) );
+
+				}
+
+				convertedTracks.push( new QuaternionKeyframeTrack(
+					".bones[" + boneData.bone.name + "].quaternion",
+					boneData.quat.times,
+					boneData.quat.values
+				) );
+
+			}
+
+		}
+
+		mixer.uncacheAction( clip );
+
+		return new AnimationClip( clip.name, - 1, convertedTracks );
+
+	},
+
+	getHelperFromSkeleton: function ( skeleton ) {
+
+		var source = new SkeletonHelper( skeleton.bones[ 0 ] );
+		source.skeleton = skeleton;
+
+		return source;
+
+	},
+
+	getSkeletonOffsets: function () {
+
+		var targetParentPos = new Vector3(),
+			targetPos = new Vector3(),
+			sourceParentPos = new Vector3(),
+			sourcePos = new Vector3(),
+			targetDir = new Vector2(),
+			sourceDir = new Vector2();
+
+		return function ( target, source, options ) {
+
+			options = options || {};
+			options.hip = options.hip !== undefined ? options.hip : "hip";
+			options.names = options.names || {};
+
+			if ( ! source.isObject3D ) {
+
+				source = this.getHelperFromSkeleton( source );
+
+			}
+
+			var nameKeys = Object.keys( options.names ),
+				nameValues = Object.values( options.names ),
+				sourceBones = source.isObject3D ? source.skeleton.bones : this.getBones( source ),
+				bones = target.isObject3D ? target.skeleton.bones : this.getBones( target ),
+				offsets = [],
+				bone, boneTo,
+				name, i;
+
+			target.skeleton.pose();
+
+			for ( i = 0; i < bones.length; ++ i ) {
+
+				bone = bones[ i ];
+				name = options.names[ bone.name ] || bone.name;
+
+				boneTo = this.getBoneByName( name, sourceBones );
+
+				if ( boneTo && name !== options.hip ) {
+
+					var boneParent = this.getNearestBone( bone.parent, nameKeys ),
+						boneToParent = this.getNearestBone( boneTo.parent, nameValues );
+
+					boneParent.updateMatrixWorld();
+					boneToParent.updateMatrixWorld();
+
+					targetParentPos.setFromMatrixPosition( boneParent.matrixWorld );
+					targetPos.setFromMatrixPosition( bone.matrixWorld );
+
+					sourceParentPos.setFromMatrixPosition( boneToParent.matrixWorld );
+					sourcePos.setFromMatrixPosition( boneTo.matrixWorld );
+
+					targetDir.subVectors(
+						new Vector2( targetPos.x, targetPos.y ),
+						new Vector2( targetParentPos.x, targetParentPos.y )
+					).normalize();
+
+					sourceDir.subVectors(
+						new Vector2( sourcePos.x, sourcePos.y ),
+						new Vector2( sourceParentPos.x, sourceParentPos.y )
+					).normalize();
+
+					var laterialAngle = targetDir.angle() - sourceDir.angle();
+
+					var offset = new Matrix4().makeRotationFromEuler(
+						new Euler(
+							0,
+							0,
+							laterialAngle
+						)
+					);
+
+					bone.matrix.multiply( offset );
+
+					bone.matrix.decompose( bone.position, bone.quaternion, bone.scale );
+
+					bone.updateMatrixWorld();
+
+					offsets[ name ] = offset;
+
+				}
+
+			}
+
+			return offsets;
+
+		};
+
+	}(),
+
+	renameBones: function ( skeleton, names ) {
+
+		var bones = this.getBones( skeleton );
+
+		for ( var i = 0; i < bones.length; ++ i ) {
+
+			var bone = bones[ i ];
+
+			if ( names[ bone.name ] ) {
+
+				bone.name = names[ bone.name ];
+
+			}
+
+		}
+
+		return this;
+
+	},
+
+	getBones: function ( skeleton ) {
+
+		return Array.isArray( skeleton ) ? skeleton : skeleton.bones;
+
+	},
+
+	getBoneByName: function ( name, skeleton ) {
+
+		for ( var i = 0, bones = this.getBones( skeleton ); i < bones.length; i ++ ) {
+
+			if ( name === bones[ i ].name )
+
+				return bones[ i ];
+
+		}
+
+	},
+
+	getNearestBone: function ( bone, names ) {
+
+		while ( bone.isBone ) {
+
+			if ( names.indexOf( bone.name ) !== - 1 ) {
+
+				return bone;
+
+			}
+
+			bone = bone.parent;
+
+		}
+
+	},
+
+	findBoneTrackData: function ( name, tracks ) {
+
+		var regexp = /\[(.*)\]\.(.*)/,
+			result = { name: name };
+
+		for ( var i = 0; i < tracks.length; ++ i ) {
+
+			// 1 is track name
+			// 2 is track type
+			var trackData = regexp.exec( tracks[ i ].name );
+
+			if ( trackData && name === trackData[ 1 ] ) {
+
+				result[ trackData[ 2 ] ] = i;
+
+			}
+
+		}
+
+		return result;
+
+	},
+
+	getEqualsBonesNames: function ( skeleton, targetSkeleton ) {
+
+		var sourceBones = this.getBones( skeleton ),
+			targetBones = this.getBones( targetSkeleton ),
+			bones = [];
+
+		search : for ( var i = 0; i < sourceBones.length; i ++ ) {
+
+			var boneName = sourceBones[ i ].name;
+
+			for ( var j = 0; j < targetBones.length; j ++ ) {
+
+				if ( boneName === targetBones[ j ].name ) {
+
+					bones.push( boneName );
+
+					continue search;
+
+				}
+
+			}
+
+		}
+
+		return bones;
+
+	}
+
+};
+
+export { SkeletonUtils };

+ 195 - 0
examples/jsm/utils/UVsDebug.js

@@ -0,0 +1,195 @@
+/*
+ * @author zz85 / http://github.com/zz85
+ * @author WestLangley / http://github.com/WestLangley
+ * @author Mugen87 / https://github.com/Mugen87
+ *
+ * tool for "unwrapping" and debugging three.js geometries UV mapping
+ *
+ * Sample usage:
+ *	document.body.appendChild( UVsDebug( new THREE.SphereBufferGeometry( 10, 10, 10, 10 ) );
+ *
+ */
+
+import {
+	Vector2
+} from "../../../build/three.module.js";
+
+var UVsDebug = function ( geometry, size ) {
+
+	// handles wrapping of uv.x > 1 only
+
+	var abc = 'abc';
+	var a = new Vector2();
+	var b = new Vector2();
+
+	var uvs = [
+		new Vector2(),
+		new Vector2(),
+		new Vector2()
+	];
+
+	var face = [];
+
+	var canvas = document.createElement( 'canvas' );
+	var width = size || 1024; // power of 2 required for wrapping
+	var height = size || 1024;
+	canvas.width = width;
+	canvas.height = height;
+
+	var ctx = canvas.getContext( '2d' );
+	ctx.lineWidth = 2;
+	ctx.strokeStyle = 'rgba( 0, 0, 0, 1.0 )';
+	ctx.textAlign = 'center';
+
+	// paint background white
+
+	ctx.fillStyle = 'rgba( 255, 255, 255, 1.0 )';
+	ctx.fillRect( 0, 0, width, height );
+
+	if ( geometry.isGeometry ) {
+
+		var faces = geometry.faces;
+		var uvSet = geometry.faceVertexUvs[ 0 ];
+
+		for ( var i = 0, il = uvSet.length; i < il; i ++ ) {
+
+			var face = faces[ i ];
+			var uv = uvSet[ i ];
+
+			face[ 0 ] = face.a;
+			face[ 1 ] = face.b;
+			face[ 2 ] = face.c;
+
+			uvs[ 0 ].copy( uv[ 0 ] );
+			uvs[ 1 ].copy( uv[ 1 ] );
+			uvs[ 2 ].copy( uv[ 2 ] );
+
+			processFace( face, uvs, i );
+
+		}
+
+	} else {
+
+		var index = geometry.index;
+		var uvAttribute = geometry.attributes.uv;
+
+		if ( index ) {
+
+			// indexed geometry
+
+			for ( var i = 0, il = index.count; i < il; i += 3 ) {
+
+				face[ 0 ] = index.getX( i );
+				face[ 1 ] = index.getX( i + 1 );
+				face[ 2 ] = index.getX( i + 2 );
+
+				uvs[ 0 ].fromBufferAttribute( uvAttribute, face[ 0 ] );
+				uvs[ 1 ].fromBufferAttribute( uvAttribute, face[ 1 ] );
+				uvs[ 2 ].fromBufferAttribute( uvAttribute, face[ 2 ] );
+
+				processFace( face, uvs, i );
+
+			}
+
+		} else {
+
+			// non-indexed geometry
+
+			for ( var i = 0, il = uvAttribute.count; i < il; i += 3 ) {
+
+				face[ 0 ] = i;
+				face[ 1 ] = i + 1;
+				face[ 2 ] = i + 2;
+
+				uvs[ 0 ].fromBufferAttribute( uvAttribute, face[ 0 ] );
+				uvs[ 1 ].fromBufferAttribute( uvAttribute, face[ 1 ] );
+				uvs[ 2 ].fromBufferAttribute( uvAttribute, face[ 2 ] );
+
+				processFace( face, uvs, i );
+
+			}
+
+		}
+
+	}
+
+	return canvas;
+
+	function processFace( face, uvs, index ) {
+
+		// draw contour of face
+
+		ctx.beginPath();
+
+		a.set( 0, 0 );
+
+		for ( var j = 0, jl = uvs.length; j < jl; j ++ ) {
+
+			var uv = uvs[ j ];
+
+			a.x += uv.x;
+			a.y += uv.y;
+
+			if ( j === 0 ) {
+
+				ctx.moveTo( uv.x * width, ( 1 - uv.y ) * height );
+
+			} else {
+
+				ctx.lineTo( uv.x * width, ( 1 - uv.y ) * height );
+
+			}
+
+		}
+
+		ctx.closePath();
+		ctx.stroke();
+
+		// calculate center of face
+
+		a.divideScalar( uvs.length );
+
+		// label the face number
+
+		ctx.font = '12pt Arial bold';
+		ctx.fillStyle = 'rgba( 0, 0, 0, 1.0 )';
+		ctx.fillText( index, a.x * width, ( 1 - a.y ) * height );
+
+		if ( a.x > 0.95 ) {
+
+			// wrap x // 0.95 is arbitrary
+
+			ctx.fillText( index, ( a.x % 1 ) * width, ( 1 - a.y ) * height );
+
+		}
+
+		//
+
+		ctx.font = '8pt Arial bold';
+		ctx.fillStyle = 'rgba( 0, 0, 0, 1.0 )';
+
+		// label uv edge orders
+
+		for ( j = 0, jl = uvs.length; j < jl; j ++ ) {
+
+			var uv = uvs[ j ];
+			b.addVectors( a, uv ).divideScalar( 2 );
+
+			var vnum = face[ j ];
+			ctx.fillText( abc[ j ] + vnum, b.x * width, ( 1 - b.y ) * height );
+
+			if ( b.x > 0.95 ) {
+
+				// wrap x
+
+				ctx.fillText( abc[ j ] + vnum, ( b.x % 1 ) * width, ( 1 - b.y ) * height );
+
+			}
+
+		}
+
+	}
+
+};
+
+export { UVsDebug };

+ 13 - 3
utils/modularize.js

@@ -4,23 +4,33 @@
 
 var fs = require( 'fs' );
 
-var srcFolder = '../examples/js/';
-var dstFolder = '../examples/jsm/';
+var srcFolder = __dirname + '/../examples/js/';
+var dstFolder = __dirname + '/../examples/jsm/';
 
 var files = [
 	{ path: 'controls/OrbitControls.js', ignoreList: [] },
 	{ path: 'controls/MapControls.js', ignoreList: [] },
 	{ path: 'controls/TrackballControls.js', ignoreList: [] },
 	// { path: 'controls/TransformControls.js', ignoreList: [] },
+
 	{ path: 'exporters/GLTFExporter.js', ignoreList: [ 'AnimationClip', 'Camera', 'Geometry', 'Material', 'Mesh', 'Object3D', 'RGBFormat', 'Scenes', 'ShaderMaterial', 'VertexColors' ] },
 	{ path: 'exporters/MMDExporter.js', ignoreList: [] },
 	{ path: 'exporters/OBJExporter.js', ignoreList: [] },
 	{ path: 'exporters/PLYExporter.js', ignoreList: [] },
 	{ path: 'exporters/STLExporter.js', ignoreList: [] },
 	{ path: 'exporters/TypedGeometryExporter.js', ignoreList: [] },
+
 	{ path: 'loaders/GLTFLoader.js', ignoreList: [ 'NoSide', 'Matrix2', 'DDSLoader' ] },
 	{ path: 'loaders/OBJLoader.js', ignoreList: [] },
-	{ path: 'loaders/MTLLoader.js', ignoreList: [] }
+	{ path: 'loaders/MTLLoader.js', ignoreList: [] },
+
+	{ path: 'utils/BufferGeometryUtils.js', ignoreList: [] },
+	{ path: 'utils/GeometryUtils.js', ignoreList: [] },
+	{ path: 'utils/MathUtils.js', ignoreList: [] },
+	{ path: 'utils/SceneUtils.js', ignoreList: [] },
+	{ path: 'utils/ShadowMapViewer.js', ignoreList: [ 'DirectionalLight', 'SpotLight' ] },
+	{ path: 'utils/SkeletonUtils.js', ignoreList: [] },
+	{ path: 'utils/UVsDebug.js', ignoreList: [ 'SphereBufferGeometry' ] },
 ];
 
 for ( var i = 0; i < files.length; i ++ ) {

+ 0 - 0
examples/js/utils/ldraw/packLDrawModel.js → utils/packLDrawModel.js