Jelajahi Sumber

MeshSurfaceSampler: Initial commit.

- Removes GeometryUtils random point selection.
- Adds MeshSurfaceSampler for incremental, weighted surface sampling.
- Adds 'webgl / instancing / scatter' example with InstancedMesh.
- Adds 'Flower.glb' sample for the example.
Don McCurdy 5 tahun lalu
induk
melakukan
88abe877e5

+ 1 - 0
examples/files.js

@@ -50,6 +50,7 @@ var files = {
 		"webgl_instancing_dynamic",
 		"webgl_instancing_interactive",
 		"webgl_instancing_raycast",
+		"webgl_instancing_scatter",
 		"webgl_interactive_buffergeometry",
 		"webgl_interactive_cubes",
 		"webgl_interactive_cubes_gpu",

+ 181 - 0
examples/js/math/MeshSurfaceSampler.js

@@ -0,0 +1,181 @@
+/**
+ * @author donmccurdy / https://www.donmccurdy.com/
+ */
+
+/**
+ * Utility class for sampling weighted random points on the surface of a mesh.
+ *
+ * Building the sampler is a one-time O(n) operation. Once built, any number of
+ * random samples may be selected in O(logn) time. Memory usage is O(n).
+ *
+ * References:
+ * - http://www.joesfer.com/?p=84
+ * - https://stackoverflow.com/a/4322940/1314762
+ */
+THREE.MeshSurfaceSampler = ( function () {
+
+	var _face = new THREE.Triangle();
+
+	function MeshSurfaceSampler ( geometry ) {
+
+		if ( ! geometry.isBufferGeometry || geometry.attributes.position.itemSize !== 3 ) {
+
+			throw new Error( 'THREE.MeshSurfaceSampler: Requires BufferGeometry triangle mesh.' );
+
+		}
+
+		if ( geometry.index ) {
+
+			console.warn( 'THREE.MeshSurfaceSampler: Converting geometry to non-indexed BufferGeometry.' );
+
+			geometry = geometry.toNonIndexed();
+
+		}
+
+		this.geometry = geometry;
+
+		this.positionAttribute = this.geometry.getAttribute( 'position' );
+		this.weightAttribute = null;
+
+		this.distribution = null;
+
+	};
+
+	MeshSurfaceSampler.prototype = {
+
+		constructor: MeshSurfaceSampler,
+
+		setWeightAttribute: function ( name ) {
+
+			this.weightAttribute = name ? this.geometry.getAttribute( name ) : null;
+
+			return this;
+
+		},
+
+		build: function () {
+
+			var positionAttribute = this.positionAttribute;
+			var weightAttribute = this.weightAttribute;
+
+			var faceWeights = new Float32Array( positionAttribute.count / 3 );
+
+			// Accumulate weights for each mesh face.
+
+			for ( var i = 0; i < positionAttribute.count; i += 3 ) {
+
+				var faceWeight = 1;
+
+				if ( weightAttribute ) {
+
+					faceWeight = weightAttribute.getX( i )
+						+ weightAttribute.getX( i + 1 )
+						+ weightAttribute.getX( i + 2 );
+
+				}
+
+				_face.a.fromBufferAttribute( positionAttribute, i );
+				_face.b.fromBufferAttribute( positionAttribute, i + 1 );
+				_face.c.fromBufferAttribute( positionAttribute, i + 2 );
+				faceWeight *= _face.getArea();
+
+				faceWeights[ i / 3 ] = faceWeight;
+
+			}
+
+			// Store cumulative total face weights in an array, where weight index
+			// corresponds to face index.
+
+			this.distribution = new Float32Array( positionAttribute.count / 3 );
+
+			var cumulativeTotal = 0;
+
+			for ( var i = 0; i < faceWeights.length; i ++ ) {
+
+				cumulativeTotal += faceWeights[ i ];
+
+				this.distribution[ i ] = cumulativeTotal;
+
+			}
+
+			return this;
+
+		},
+
+		sample: function ( targetPosition, targetNormal ) {
+
+			var cumulativeTotal = this.distribution[ this.distribution.length - 1 ];
+
+			var faceIndex = this.binarySearch( Math.random() * cumulativeTotal );
+
+			return this.sampleFace( faceIndex, targetPosition, targetNormal );
+
+		},
+
+		binarySearch: function ( x ) {
+
+			var dist = this.distribution;
+			var start = 0;
+			var end = dist.length - 1;
+
+			var index = -1;
+
+			while ( start <= end ) {
+
+				var mid = Math.floor( ( start + end ) / 2 );
+
+				if ( dist[ mid - 1 ] <= x && dist[ mid ] > x ) {
+
+					index = mid;
+
+					break;
+
+				} else if ( x < dist[ mid ] ) {
+
+					end = mid - 1;
+
+				} else {
+
+					start = mid + 1;
+
+				}
+
+			}
+
+			return index;
+
+		},
+
+		sampleFace: function ( faceIndex, targetPosition, targetNormal ) {
+
+			var u = Math.random();
+			var v = Math.random();
+
+			if ( u + v > 1 ) {
+
+				u = 1 - u;
+				v = 1 - v;
+
+			}
+
+			_face.a.fromBufferAttribute( this.positionAttribute, faceIndex * 3 );
+			_face.b.fromBufferAttribute( this.positionAttribute, faceIndex * 3 + 1 );
+			_face.c.fromBufferAttribute( this.positionAttribute, faceIndex * 3 + 2 );
+
+			targetPosition
+				.set( 0, 0, 0 )
+				.addScaledVector( _face.a, u )
+				.addScaledVector( _face.b, v )
+				.addScaledVector( _face.c, 1 - ( u + v ) );
+
+			_face.getNormal( targetNormal );
+
+			return this;
+
+		}
+
+	};
+
+	return MeshSurfaceSampler;
+
+} )();

+ 0 - 264
examples/js/utils/GeometryUtils.js

@@ -26,270 +26,6 @@ THREE.GeometryUtils = {
 
 	},
 
-	// Get random point in triangle (via barycentric coordinates)
-	// 	(uniform distribution)
-	// 	http://www.cgafaq.info/wiki/Random_Point_In_Triangle
-
-	randomPointInTriangle: function () {
-
-		var vector = new THREE.Vector3();
-
-		return function ( vectorA, vectorB, vectorC ) {
-
-			var point = new THREE.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 THREE.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 = THREE.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 ] = THREE.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 THREE.Vector3();
-		vB = new THREE.Vector3();
-		vC = new THREE.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 += THREE.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 ] = THREE.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 ] = THREE.GeometryUtils.randomPointInTriangle( vA, vB, vC );
-
-		}
-
-		return result;
-
-	},
-
-	// Get triangle area (half of parallelogram)
-	// http://mathworld.wolfram.com/TriangleArea.html
-
-	triangleArea: function () {
-
-		var vector1 = new THREE.Vector3();
-		var vector2 = new THREE.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.' );

+ 187 - 0
examples/jsm/math/MeshSurfaceSampler.js

@@ -0,0 +1,187 @@
+/**
+ * @author donmccurdy / https://www.donmccurdy.com/
+ */
+
+import {
+	Triangle
+} from "../../../build/three.module.js";
+
+/**
+ * Utility class for sampling weighted random points on the surface of a mesh.
+ *
+ * Building the sampler is a one-time O(n) operation. Once built, any number of
+ * random samples may be selected in O(logn) time. Memory usage is O(n).
+ *
+ * References:
+ * - http://www.joesfer.com/?p=84
+ * - https://stackoverflow.com/a/4322940/1314762
+ */
+var MeshSurfaceSampler = ( function () {
+
+	var _face = new Triangle();
+
+	function MeshSurfaceSampler ( geometry ) {
+
+		if ( ! geometry.isBufferGeometry || geometry.attributes.position.itemSize !== 3 ) {
+
+			throw new Error( 'THREE.MeshSurfaceSampler: Requires BufferGeometry triangle mesh.' );
+
+		}
+
+		if ( geometry.index ) {
+
+			console.warn( 'THREE.MeshSurfaceSampler: Converting geometry to non-indexed BufferGeometry.' );
+
+			geometry = geometry.toNonIndexed();
+
+		}
+
+		this.geometry = geometry;
+
+		this.positionAttribute = this.geometry.getAttribute( 'position' );
+		this.weightAttribute = null;
+
+		this.distribution = null;
+
+	};
+
+	MeshSurfaceSampler.prototype = {
+
+		constructor: MeshSurfaceSampler,
+
+		setWeightAttribute: function ( name ) {
+
+			this.weightAttribute = name ? this.geometry.getAttribute( name ) : null;
+
+			return this;
+
+		},
+
+		build: function () {
+
+			var positionAttribute = this.positionAttribute;
+			var weightAttribute = this.weightAttribute;
+
+			var faceWeights = new Float32Array( positionAttribute.count / 3 );
+
+			// Accumulate weights for each mesh face.
+
+			for ( var i = 0; i < positionAttribute.count; i += 3 ) {
+
+				var faceWeight = 1;
+
+				if ( weightAttribute ) {
+
+					faceWeight = weightAttribute.getX( i )
+						+ weightAttribute.getX( i + 1 )
+						+ weightAttribute.getX( i + 2 );
+
+				}
+
+				_face.a.fromBufferAttribute( positionAttribute, i );
+				_face.b.fromBufferAttribute( positionAttribute, i + 1 );
+				_face.c.fromBufferAttribute( positionAttribute, i + 2 );
+				faceWeight *= _face.getArea();
+
+				faceWeights[ i / 3 ] = faceWeight;
+
+			}
+
+			// Store cumulative total face weights in an array, where weight index
+			// corresponds to face index.
+
+			this.distribution = new Float32Array( positionAttribute.count / 3 );
+
+			var cumulativeTotal = 0;
+
+			for ( var i = 0; i < faceWeights.length; i ++ ) {
+
+				cumulativeTotal += faceWeights[ i ];
+
+				this.distribution[ i ] = cumulativeTotal;
+
+			}
+
+			return this;
+
+		},
+
+		sample: function ( targetPosition, targetNormal ) {
+
+			var cumulativeTotal = this.distribution[ this.distribution.length - 1 ];
+
+			var faceIndex = this.binarySearch( Math.random() * cumulativeTotal );
+
+			return this.sampleFace( faceIndex, targetPosition, targetNormal );
+
+		},
+
+		binarySearch: function ( x ) {
+
+			var dist = this.distribution;
+			var start = 0;
+			var end = dist.length - 1;
+
+			var index = -1;
+
+			while ( start <= end ) {
+
+				var mid = Math.floor( ( start + end ) / 2 );
+
+				if ( dist[ mid - 1 ] <= x && dist[ mid ] > x ) {
+
+					index = mid;
+
+					break;
+
+				} else if ( x < dist[ mid ] ) {
+
+					end = mid - 1;
+
+				} else {
+
+					start = mid + 1;
+
+				}
+
+			}
+
+			return index;
+
+		},
+
+		sampleFace: function ( faceIndex, targetPosition, targetNormal ) {
+
+			var u = Math.random();
+			var v = Math.random();
+
+			if ( u + v > 1 ) {
+
+				u = 1 - u;
+				v = 1 - v;
+
+			}
+
+			_face.a.fromBufferAttribute( this.positionAttribute, faceIndex * 3 );
+			_face.b.fromBufferAttribute( this.positionAttribute, faceIndex * 3 + 1 );
+			_face.c.fromBufferAttribute( this.positionAttribute, faceIndex * 3 + 2 );
+
+			targetPosition
+				.set( 0, 0, 0 )
+				.addScaledVector( _face.a, u )
+				.addScaledVector( _face.b, v )
+				.addScaledVector( _face.c, 1 - ( u + v ) );
+
+			_face.getNormal( targetNormal );
+
+			return this;
+
+		}
+
+	};
+
+	return MeshSurfaceSampler;
+
+} )();
+
+export { MeshSurfaceSampler };

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

@@ -31,270 +31,6 @@ var GeometryUtils = {
 
 	},
 
-	// 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.' );

TEMPAT SAMPAH
examples/models/gltf/Flower/Flower.glb


+ 8 - 0
examples/models/gltf/Flower/README.md

@@ -0,0 +1,8 @@
+# Flower
+
+Model by [Kenney](https://twitter.com/KenneyNL), from [Nature Pack](https://www.kenney.nl/assets/nature-pack). CC0 1.0.
+
+Modifications by [Don McCurdy](https://donmccurdy.com/):
+
+- Split stem and blossom meshes.
+- Color adjustments.

+ 288 - 0
examples/webgl_instancing_scatter.html

@@ -0,0 +1,288 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - instancing - scatter</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<link type="text/css" rel="stylesheet" href="main.css">
+	</head>
+	<body>
+
+		<script type="module">
+
+			import * as THREE from '../build/three.module.js';
+
+			import { MeshSurfaceSampler } from './jsm/math/MeshSurfaceSampler.js';
+			import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';
+			import Stats from './jsm/libs/stats.module.js';
+			import { GUI } from './jsm/libs/dat.gui.module.js';
+
+			var camera, scene, renderer, stats;
+
+			var api = {
+
+				count: 2000,
+				distribution: 'random',
+				resample: resample,
+				surfaceColor: 0xFFF784,
+				backgroundColor: 0xE39469,
+
+			};
+
+			var stemMesh, blossomMesh;
+			var stemGeometry, blossomGeometry;
+			var stemMaterial, blossomMaterial;
+
+			var sampler;
+			var count = api.count;
+			var ages = new Float32Array( count );
+			var dummy = new THREE.Object3D();
+
+			var _sPosition = new THREE.Vector3();
+			var _sNormal = new THREE.Vector3();
+
+			// var surfaceGeometry = new THREE.BoxBufferGeometry( 10, 10, 10 ).toNonIndexed();
+			var surfaceGeometry = new THREE.TorusKnotBufferGeometry( 10, 3, 100, 16 ).toNonIndexed();
+			var surfaceMaterial = new THREE.MeshLambertMaterial( { color: api.surfaceColor, wireframe: false } );
+			var surface = new THREE.Mesh( surfaceGeometry, surfaceMaterial );
+
+			// Source: https://gist.github.com/gre/1650294
+			var easeOutCubic = function ( t ) { return ( -- t ) * t * t + 1; };
+
+			var loader = new GLTFLoader();
+
+			loader.load( './models/gltf/Flower/Flower.glb', function ( gltf ) {
+
+				var _stemMesh = gltf.scene.getObjectByName('Stem');
+				var _blossomMesh = gltf.scene.getObjectByName('Blossom');
+
+				stemGeometry = _stemMesh.geometry;
+				blossomGeometry = _blossomMesh.geometry;
+
+				var defaultTransform = new THREE.Matrix4()
+					.makeRotationX( Math.PI )
+					.multiply( new THREE.Matrix4().makeScale( 7, 7, 7 ) )
+
+				stemGeometry.applyMatrix( defaultTransform );
+				blossomGeometry.applyMatrix( defaultTransform );
+
+				stemMaterial = _stemMesh.material;
+				blossomMaterial = _blossomMesh.material;
+
+				// Assign random colors to the blossoms.
+				var _color = new THREE.Color();
+				var color = new Float32Array( count * 3 );
+				var blossomPalette = [ 0xF20587, 0xF2D479, 0xF2C879, 0xF2B077, 0xF24405 ];
+
+				for ( var i = 0; i < count; i ++ ) {
+
+					_color.setHex( blossomPalette[ Math.floor( Math.random() * blossomPalette.length ) ] );
+					_color.toArray( color, i * 3 );
+
+				}
+
+				blossomGeometry.setAttribute( 'color', new THREE.InstancedBufferAttribute( color, 3 ) );
+				blossomMaterial.vertexColors = THREE.VertexColors;
+
+				stemMesh = new THREE.InstancedMesh( stemGeometry, stemMaterial, count );
+				blossomMesh = new THREE.InstancedMesh( blossomGeometry, blossomMaterial, count );
+
+				resample();
+
+				init();
+				animate();
+
+			} );
+
+			function init() {
+
+				camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
+				camera.position.set( 25, 25, 25 );
+				camera.lookAt( 0, 0, 0 );
+
+				//
+
+				scene = new THREE.Scene();
+				scene.background = new THREE.Color( api.backgroundColor );
+
+				var pointLight = new THREE.PointLight( 0xAA8899, 0.75 );
+				pointLight.position.set( 50, -25, 75 );
+				scene.add( pointLight );
+
+				scene.add( new THREE.HemisphereLight() );
+
+				//
+
+				scene.add( stemMesh );
+				scene.add( blossomMesh );
+
+				scene.add( surface );
+
+				//
+
+				var gui = new GUI();
+				gui.add( api, 'count', 0, count ).onChange( function () {
+
+					stemMesh.count = api.count;
+					blossomMesh.count = api.count;
+
+				} );
+
+				// gui.addColor( api, 'backgroundColor' ).onChange( function () {
+
+				// 	scene.background.setHex( api.backgroundColor );
+
+				// } );
+
+				// gui.addColor( api, 'surfaceColor' ).onChange( function () {
+
+				// 	surfaceMaterial.color.setHex( api.surfaceColor );
+
+				// } );
+
+				gui.add( api, 'distribution' ).options( [ 'random', 'weighted' ] ).onChange( resample );
+				gui.add( api, 'resample' );
+
+				//
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				document.body.appendChild( renderer.domElement );
+
+				//
+
+				stats = new Stats();
+				document.body.appendChild( stats.dom );
+
+				//
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+			}
+
+			function resample() {
+
+				var vertexCount = surface.geometry.getAttribute( 'position' ).count;
+
+				console.info( 'Sampling ' + count + ' points from a surface with ' + vertexCount + ' vertices...' );
+
+				//
+
+				console.time( '.build()' );
+
+				sampler = new MeshSurfaceSampler( surface.geometry )
+					.setWeightAttribute( api.distribution === 'weighted' ? 'uv' : null )
+					.build();
+
+				console.timeEnd( '.build()' );
+
+				//
+
+				console.time( '.sample()' );
+
+				for ( var i = 0; i < count; i ++ ) {
+
+					ages[ i ] = Math.random();
+
+					resampleParticle( i );
+
+				}
+
+				console.timeEnd( '.sample()' );
+
+				stemMesh.instanceMatrix.needsUpdate = true;
+				blossomMesh.instanceMatrix.needsUpdate = true;
+
+			}
+
+			function resampleParticle ( i ) {
+
+				sampler.sample( _sPosition, _sNormal );
+				_sNormal.add( _sPosition );
+
+				dummy.position.copy( _sPosition );
+				dummy.scale.set( ages[ i ], ages[ i ], ages[ i ] );
+				dummy.lookAt( _sNormal );
+				dummy.updateMatrix();
+
+				stemMesh.setMatrixAt( i, dummy.matrix );
+				blossomMesh.setMatrixAt( i, dummy.matrix );
+
+			}
+
+			function updateParticle ( i,  ) {
+
+				ages[ i ] += 0.005;
+
+				if ( ages[ i ] >= 1 ) {
+
+					ages[ i ] = 0.001;
+
+					resampleParticle( i );
+
+					return;
+
+				}
+
+				var scale = Math.abs( easeOutCubic( ( ages[ i ] > 0.5 ? 1 - ages[ i ] : ages[ i ] ) * 2 ) );
+
+				stemMesh.getMatrixAt( i, dummy.matrix );
+				dummy.matrix.decompose( dummy.position, dummy.quaternion, dummy.scale );
+				dummy.scale.set( scale, scale, scale );
+				dummy.updateMatrix();
+
+				stemMesh.setMatrixAt( i, dummy.matrix );
+				blossomMesh.setMatrixAt( i, dummy.matrix );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function animate() {
+
+				requestAnimationFrame( animate );
+
+				render();
+
+				stats.update();
+
+			}
+
+			function render() {
+
+				if ( stemMesh && blossomMesh ) {
+
+					var time = Date.now() * 0.001;
+
+					scene.rotation.x = Math.sin( time / 4 );
+					scene.rotation.y = Math.sin( time / 2 );
+
+					for ( var i = 0; i < api.count; i ++ ) {
+
+						updateParticle( i );
+
+					}
+
+					stemMesh.instanceMatrix.needsUpdate = true;
+					blossomMesh.instanceMatrix.needsUpdate = true;
+
+				}
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>

+ 1 - 0
utils/modularize.js

@@ -111,6 +111,7 @@ var files = [
 	{ path: 'math/ConvexHull.js', dependencies: [], ignoreList: [] },
 	{ path: 'math/ImprovedNoise.js', dependencies: [], ignoreList: [] },
 	{ path: 'math/Lut.js', dependencies: [], ignoreList: [] },
+	{ path: 'math/MeshSurfaceSampler.js', dependencies: [], ignoreList: [] },
 	{ path: 'math/SimplexNoise.js', dependencies: [], ignoreList: [] },
 
 	{ path: 'misc/CarControls.js', dependencies: [], ignoreList: [] },