Просмотр исходного кода

SimplifyModifier: Refactor implementation. (#34021)

Michael Herzog 1 неделя назад
Родитель
Сommit
b56e2135c3

+ 63 - 556
examples/jsm/modifiers/SimplifyModifier.js

@@ -1,25 +1,20 @@
 import {
-	BufferGeometry,
-	Color,
-	Float32BufferAttribute,
-	Vector2,
-	Vector3,
-	Vector4
+	BufferAttribute,
+	BufferGeometry
 } from 'three';
-import * as BufferGeometryUtils from '../utils/BufferGeometryUtils.js';
-
-const _cb = new Vector3(), _ab = new Vector3();
+import { MeshoptSimplifier } from '../libs/meshopt_simplifier.module.js';
+import { mergeVertices } from '../utils/BufferGeometryUtils.js';
 
 /**
  * This class can be used to modify a geometry by simplifying it. A typical use
  * case for such a modifier is automatic LOD generation.
  *
- * The implementation is based on [Progressive Mesh type Polygon Reduction Algorithm](https://web.archive.org/web/20230610044040/http://www.melax.com/polychop/)
- * by Stan Melax in 1998.
+ * The implementation is based on [meshoptimizer]{@link https://github.com/zeux/meshoptimizer}.
+ * If you only need a simplified index buffer, use {@link MeshoptSimplifier} directly.
  *
  * ```js
  * const modifier = new SimplifyModifier();
- * geometry = modifier.modify( geometry );
+ * geometry = await modifier.modify( geometry, count );
  * ```
  *
  * @three_import import { SimplifyModifier } from 'three/addons/modifiers/SimplifyModifier.js';
@@ -27,206 +22,113 @@ const _cb = new Vector3(), _ab = new Vector3();
 class SimplifyModifier {
 
 	/**
-	 * Returns a new, modified version of the given geometry by applying a simplification.
-	 * Please note that the resulting geometry is always non-indexed.
+	 * Returns a new, simplified version of the given geometry. The vertex buffers
+	 * of the result only contain vertices referenced by the simplified index.
 	 *
+	 * @async
 	 * @param {BufferGeometry} geometry - The geometry to modify.
-	 * @param {number} count - The number of vertices to remove.
-	 * @return {BufferGeometry} A new, modified geometry.
+	 * @param {number} count - The approximate number of vertices to remove.
+	 * @return {Promise<BufferGeometry>} A promise that resolves with the new, modified geometry.
 	 */
-	modify( geometry, count ) {
-
-		geometry = geometry.clone();
-
-		// currently morphAttributes are not supported
-		delete geometry.morphAttributes.position;
-		delete geometry.morphAttributes.normal;
-		const attributes = geometry.attributes;
-
-		// this modifier can only process indexed and non-indexed geometries with at least a position attribute
-
-		for ( const name in attributes ) {
-
-			if ( name !== 'position' && name !== 'uv' && name !== 'normal' && name !== 'tangent' && name !== 'color' ) geometry.deleteAttribute( name );
-
-		}
-
-		geometry = BufferGeometryUtils.mergeVertices( geometry );
+	async modify( geometry, count ) {
 
-		//
-		// put data of original geometry in different data structures
-		//
+		await MeshoptSimplifier.ready;
 
-		const vertices = [];
-		const faces = [];
+		// non-indexed geometries must be welded so the simplifier can collapse edges
 
-		// add vertices
+		if ( geometry.getIndex() === null ) geometry = mergeVertices( geometry );
 
+		const index = geometry.getIndex();
 		const positionAttribute = geometry.getAttribute( 'position' );
-		const uvAttribute = geometry.getAttribute( 'uv' );
-		const normalAttribute = geometry.getAttribute( 'normal' );
-		const tangentAttribute = geometry.getAttribute( 'tangent' );
-		const colorAttribute = geometry.getAttribute( 'color' );
-
-		let t = null;
-		let v2 = null;
-		let nor = null;
-		let col = null;
-
-		for ( let i = 0; i < positionAttribute.count; i ++ ) {
-
-			const v = new Vector3().fromBufferAttribute( positionAttribute, i );
-			if ( uvAttribute ) {
-
-				v2 = new Vector2().fromBufferAttribute( uvAttribute, i );
-
-			}
-
-			if ( normalAttribute ) {
-
-				nor = new Vector3().fromBufferAttribute( normalAttribute, i );
-
-			}
 
-			if ( tangentAttribute ) {
+		// the simplifier requires tightly packed positions
 
-				t = new Vector4().fromBufferAttribute( tangentAttribute, i );
+		const positions = new Float32Array( positionAttribute.count * 3 );
 
-			}
+		for ( let i = 0; i < positionAttribute.count; i ++ ) {
 
-			if ( colorAttribute ) {
+			positions[ i * 3 + 0 ] = positionAttribute.getX( i );
+			positions[ i * 3 + 1 ] = positionAttribute.getY( i );
+			positions[ i * 3 + 2 ] = positionAttribute.getZ( i );
 
-				col = new Color().fromBufferAttribute( colorAttribute, i );
+		}
 
-			}
+		const ratio = Math.min( 1, Math.max( 0, 1 - count / positionAttribute.count ) );
+		const targetIndexCount = Math.max( 3, Math.floor( index.count * ratio / 3 ) * 3 );
 
-			const vertex = new Vertex( v, v2, nor, t, col );
-			vertices.push( vertex );
+		// normals and uvs, when present, contribute to the error metric so collapses
+		// that would distort shading or texture seams are penalized
 
-		}
+		const normalAttribute = geometry.getAttribute( 'normal' );
+		const uvAttribute = geometry.getAttribute( 'uv' );
+		const attributeStride = ( normalAttribute ? 3 : 0 ) + ( uvAttribute ? 2 : 0 );
 
-		// add faces
+		let indices;
 
-		let index = geometry.getIndex();
+		if ( attributeStride > 0 ) {
 
-		if ( index !== null ) {
+			const vertexAttributes = new Float32Array( positionAttribute.count * attributeStride );
+			const attributeWeights = [];
 
-			for ( let i = 0; i < index.count; i += 3 ) {
+			if ( normalAttribute ) attributeWeights.push( 0.25, 0.25, 0.25 );
+			if ( uvAttribute ) attributeWeights.push( 0.5, 0.5 );
 
-				const a = index.getX( i );
-				const b = index.getX( i + 1 );
-				const c = index.getX( i + 2 );
+			for ( let i = 0, o = 0; i < positionAttribute.count; i ++ ) {
 
-				const triangle = new Triangle( vertices[ a ], vertices[ b ], vertices[ c ], a, b, c );
-				faces.push( triangle );
+				if ( normalAttribute ) {
 
-			}
+					vertexAttributes[ o ++ ] = normalAttribute.getX( i );
+					vertexAttributes[ o ++ ] = normalAttribute.getY( i );
+					vertexAttributes[ o ++ ] = normalAttribute.getZ( i );
 
-		} else {
+				}
 
-			for ( let i = 0; i < positionAttribute.count; i += 3 ) {
+				if ( uvAttribute ) {
 
-				const a = i;
-				const b = i + 1;
-				const c = i + 2;
+					vertexAttributes[ o ++ ] = uvAttribute.getX( i );
+					vertexAttributes[ o ++ ] = uvAttribute.getY( i );
 
-				const triangle = new Triangle( vertices[ a ], vertices[ b ], vertices[ c ], a, b, c );
-				faces.push( triangle );
+				}
 
 			}
 
-		}
-
-		// compute all edge collapse costs
+			indices = MeshoptSimplifier.simplifyWithAttributes( index.array, positions, 3, vertexAttributes, attributeStride, attributeWeights, null, targetIndexCount, 1 )[ 0 ];
 
-		for ( let i = 0, il = vertices.length; i < il; i ++ ) {
+		} else {
 
-			computeEdgeCostAtVertex( vertices[ i ] );
+			indices = MeshoptSimplifier.simplify( index.array, positions, 3, targetIndexCount, 1 )[ 0 ];
 
 		}
 
-		let nextVertex;
+		// drop vertices that are no longer referenced and rebuild the attributes
 
-		let z = count;
-
-		while ( z -- ) {
-
-			nextVertex = minimumCostEdge( vertices );
-
-			if ( ! nextVertex ) {
-
-				console.log( 'THREE.SimplifyModifier: No next vertex' );
-				break;
-
-			}
-
-			collapse( vertices, faces, nextVertex, nextVertex.collapseNeighbor );
-
-		}
-
-		//
+		const [ remap, unique ] = MeshoptSimplifier.compactMesh( indices ); // rewrites indices in place
 
 		const simplifiedGeometry = new BufferGeometry();
-		const position = [];
-		const uv = [];
-		const normal = [];
-		const tangent = [];
-		const color = [];
-
-		index = [];
-
-		//
-
-		for ( let i = 0; i < vertices.length; i ++ ) {
 
-			const vertex = vertices[ i ];
-			position.push( vertex.position.x, vertex.position.y, vertex.position.z );
-			if ( vertex.uv ) {
+		for ( const name in geometry.attributes ) {
 
-				uv.push( vertex.uv.x, vertex.uv.y );
+			const attribute = geometry.getAttribute( name );
+			const itemSize = attribute.itemSize;
+			const newAttribute = new BufferAttribute( new attribute.array.constructor( unique * itemSize ), itemSize, attribute.normalized );
 
-			}
-
-			if ( vertex.normal ) {
+			// scatter each vertex to its new slot; 0xffffffff marks unreferenced vertices
 
-				normal.push( vertex.normal.x, vertex.normal.y, vertex.normal.z );
+			for ( let i = 0; i < remap.length; i ++ ) {
 
-			}
+				const target = remap[ i ];
 
-			if ( vertex.tangent ) {
+				if ( target === 0xffffffff ) continue;
 
-				tangent.push( vertex.tangent.x, vertex.tangent.y, vertex.tangent.z, vertex.tangent.w );
+				for ( let k = 0; k < itemSize; k ++ ) newAttribute.setComponent( target, k, attribute.getComponent( i, k ) );
 
 			}
 
-			if ( vertex.color ) {
-
-				color.push( vertex.color.r, vertex.color.g, vertex.color.b );
-
-			}
-
-
-			// cache final index to GREATLY speed up faces reconstruction
-			vertex.id = i;
-
-		}
-
-		//
-
-		for ( let i = 0; i < faces.length; i ++ ) {
-
-			const face = faces[ i ];
-			index.push( face.v1.id, face.v2.id, face.v3.id );
+			simplifiedGeometry.setAttribute( name, newAttribute );
 
 		}
 
-		simplifiedGeometry.setAttribute( 'position', new Float32BufferAttribute( position, 3 ) );
-		if ( uv.length > 0 ) simplifiedGeometry.setAttribute( 'uv', new Float32BufferAttribute( uv, 2 ) );
-		if ( normal.length > 0 ) simplifiedGeometry.setAttribute( 'normal', new Float32BufferAttribute( normal, 3 ) );
-		if ( tangent.length > 0 ) simplifiedGeometry.setAttribute( 'tangent', new Float32BufferAttribute( tangent, 4 ) );
-		if ( color.length > 0 ) simplifiedGeometry.setAttribute( 'color', new Float32BufferAttribute( color, 3 ) );
-
-		simplifiedGeometry.setIndex( index );
+		simplifiedGeometry.setIndex( new BufferAttribute( indices, 1 ) );
 
 		return simplifiedGeometry;
 
@@ -234,399 +136,4 @@ class SimplifyModifier {
 
 }
 
-function pushIfUnique( array, object ) {
-
-	if ( array.indexOf( object ) === - 1 ) array.push( object );
-
-}
-
-function removeFromArray( array, object ) {
-
-	const k = array.indexOf( object );
-	if ( k > - 1 ) array.splice( k, 1 );
-
-}
-
-function computeEdgeCollapseCost( u, v ) {
-
-	// if we collapse edge uv by moving u to v then how
-	// much different will the model change, i.e. the "error".
-
-	const edgelength = v.position.distanceTo( u.position );
-	let curvature = 0;
-
-	const sideFaces = [];
-
-	// find the "sides" triangles that are on the edge uv
-	for ( let i = 0, il = u.faces.length; i < il; i ++ ) {
-
-		const face = u.faces[ i ];
-
-		if ( face.hasVertex( v ) ) {
-
-			sideFaces.push( face );
-
-		}
-
-	}
-
-	// use the triangle facing most away from the sides
-	// to determine our curvature term
-	for ( let i = 0, il = u.faces.length; i < il; i ++ ) {
-
-		let minCurvature = 1;
-		const face = u.faces[ i ];
-
-		for ( let j = 0; j < sideFaces.length; j ++ ) {
-
-			const sideFace = sideFaces[ j ];
-			// use dot product of face normals.
-			const dotProd = face.normal.dot( sideFace.normal );
-			minCurvature = Math.min( minCurvature, ( 1.001 - dotProd ) / 2 );
-
-		}
-
-		curvature = Math.max( curvature, minCurvature );
-
-	}
-
-	// crude approach in attempt to preserve borders
-	// though it seems not to be totally correct
-	const borders = 0;
-
-	if ( sideFaces.length < 2 ) {
-
-		// we add some arbitrary cost for borders,
-		// borders += 10;
-		curvature = 1;
-
-	}
-
-	const amt = edgelength * curvature + borders;
-
-	return amt;
-
-}
-
-function computeEdgeCostAtVertex( v ) {
-
-	// compute the edge collapse cost for all edges that start
-	// from vertex v.  Since we are only interested in reducing
-	// the object by selecting the min cost edge at each step, we
-	// only cache the cost of the least cost edge at this vertex
-	// (in member variable collapse) as well as the value of the
-	// cost (in member variable collapseCost).
-
-	if ( v.neighbors.length === 0 ) {
-
-		// collapse if no neighbors.
-		v.collapseNeighbor = null;
-		v.collapseCost = - 0.01;
-
-		return;
-
-	}
-
-	v.collapseCost = 100000;
-	v.collapseNeighbor = null;
-
-	// search all neighboring edges for "least cost" edge
-	for ( let i = 0; i < v.neighbors.length; i ++ ) {
-
-		const collapseCost = computeEdgeCollapseCost( v, v.neighbors[ i ] );
-
-		if ( ! v.collapseNeighbor ) {
-
-			v.collapseNeighbor = v.neighbors[ i ];
-			v.collapseCost = collapseCost;
-			v.minCost = collapseCost;
-			v.totalCost = 0;
-			v.costCount = 0;
-
-		}
-
-		v.costCount ++;
-		v.totalCost += collapseCost;
-
-		if ( collapseCost < v.minCost ) {
-
-			v.collapseNeighbor = v.neighbors[ i ];
-			v.minCost = collapseCost;
-
-		}
-
-	}
-
-	// we average the cost of collapsing at this vertex
-	v.collapseCost = v.totalCost / v.costCount;
-	// v.collapseCost = v.minCost;
-
-}
-
-function removeVertex( v, vertices ) {
-
-	console.assert( v.faces.length === 0 );
-
-	while ( v.neighbors.length ) {
-
-		const n = v.neighbors.pop();
-		removeFromArray( n.neighbors, v );
-
-	}
-
-	removeFromArray( vertices, v );
-
-}
-
-function removeFace( f, faces ) {
-
-	removeFromArray( faces, f );
-
-	if ( f.v1 ) removeFromArray( f.v1.faces, f );
-	if ( f.v2 ) removeFromArray( f.v2.faces, f );
-	if ( f.v3 ) removeFromArray( f.v3.faces, f );
-
-	// TODO optimize this!
-	const vs = [ f.v1, f.v2, f.v3 ];
-
-	for ( let i = 0; i < 3; i ++ ) {
-
-		const v1 = vs[ i ];
-		const v2 = vs[ ( i + 1 ) % 3 ];
-
-		if ( ! v1 || ! v2 ) continue;
-
-		v1.removeIfNonNeighbor( v2 );
-		v2.removeIfNonNeighbor( v1 );
-
-	}
-
-}
-
-function collapse( vertices, faces, u, v ) {
-
-	// Collapse the edge uv by moving vertex u onto v
-
-	if ( ! v ) {
-
-		// u is a vertex all by itself so just delete it..
-		removeVertex( u, vertices );
-		return;
-
-	}
-
-	if ( v.uv ) {
-
-		u.uv.copy( v.uv );
-
-	}
-
-	if ( v.normal ) {
-
-		v.normal.add( u.normal ).normalize();
-
-	}
-
-	if ( v.tangent ) {
-
-		v.tangent.add( u.tangent ).normalize();
-
-	}
-
-	const tmpVertices = [];
-
-	for ( let i = 0; i < u.neighbors.length; i ++ ) {
-
-		tmpVertices.push( u.neighbors[ i ] );
-
-	}
-
-
-	// delete triangles on edge uv:
-	for ( let i = u.faces.length - 1; i >= 0; i -- ) {
-
-		if ( u.faces[ i ] && u.faces[ i ].hasVertex( v ) ) {
-
-			removeFace( u.faces[ i ], faces );
-
-		}
-
-	}
-
-	// update remaining triangles to have v instead of u
-	for ( let i = u.faces.length - 1; i >= 0; i -- ) {
-
-		u.faces[ i ].replaceVertex( u, v );
-
-	}
-
-
-	removeVertex( u, vertices );
-
-	// recompute the edge collapse costs in neighborhood
-	for ( let i = 0; i < tmpVertices.length; i ++ ) {
-
-		computeEdgeCostAtVertex( tmpVertices[ i ] );
-
-	}
-
-}
-
-
-
-function minimumCostEdge( vertices ) {
-
-	// O(n * n) approach. TODO optimize this
-
-	let least = vertices[ 0 ];
-
-	for ( let i = 0; i < vertices.length; i ++ ) {
-
-		if ( vertices[ i ].collapseCost < least.collapseCost ) {
-
-			least = vertices[ i ];
-
-		}
-
-	}
-
-	return least;
-
-}
-
-// we use a triangle class to represent structure of face slightly differently
-
-class Triangle {
-
-	constructor( v1, v2, v3, a, b, c ) {
-
-		this.a = a;
-		this.b = b;
-		this.c = c;
-
-		this.v1 = v1;
-		this.v2 = v2;
-		this.v3 = v3;
-
-		this.normal = new Vector3();
-
-		this.computeNormal();
-
-		v1.faces.push( this );
-		v1.addUniqueNeighbor( v2 );
-		v1.addUniqueNeighbor( v3 );
-
-		v2.faces.push( this );
-		v2.addUniqueNeighbor( v1 );
-		v2.addUniqueNeighbor( v3 );
-
-
-		v3.faces.push( this );
-		v3.addUniqueNeighbor( v1 );
-		v3.addUniqueNeighbor( v2 );
-
-	}
-
-	computeNormal() {
-
-		const vA = this.v1.position;
-		const vB = this.v2.position;
-		const vC = this.v3.position;
-
-		_cb.subVectors( vC, vB );
-		_ab.subVectors( vA, vB );
-		_cb.cross( _ab ).normalize();
-
-		this.normal.copy( _cb );
-
-	}
-
-	hasVertex( v ) {
-
-		return v === this.v1 || v === this.v2 || v === this.v3;
-
-	}
-
-	replaceVertex( oldv, newv ) {
-
-		if ( oldv === this.v1 ) this.v1 = newv;
-		else if ( oldv === this.v2 ) this.v2 = newv;
-		else if ( oldv === this.v3 ) this.v3 = newv;
-
-		removeFromArray( oldv.faces, this );
-		newv.faces.push( this );
-
-
-		oldv.removeIfNonNeighbor( this.v1 );
-		this.v1.removeIfNonNeighbor( oldv );
-
-		oldv.removeIfNonNeighbor( this.v2 );
-		this.v2.removeIfNonNeighbor( oldv );
-
-		oldv.removeIfNonNeighbor( this.v3 );
-		this.v3.removeIfNonNeighbor( oldv );
-
-		this.v1.addUniqueNeighbor( this.v2 );
-		this.v1.addUniqueNeighbor( this.v3 );
-
-		this.v2.addUniqueNeighbor( this.v1 );
-		this.v2.addUniqueNeighbor( this.v3 );
-
-		this.v3.addUniqueNeighbor( this.v1 );
-		this.v3.addUniqueNeighbor( this.v2 );
-
-		this.computeNormal();
-
-	}
-
-}
-
-class Vertex {
-
-	constructor( v, uv, normal, tangent, color ) {
-
-		this.position = v;
-		this.uv = uv;
-		this.normal = normal;
-		this.tangent = tangent;
-		this.color = color;
-
-		this.id = - 1; // external use position in vertices list (for e.g. face generation)
-
-		this.faces = []; // faces vertex is connected
-		this.neighbors = []; // neighbouring vertices aka "adjacentVertices"
-
-		// these will be computed in computeEdgeCostAtVertex()
-		this.collapseCost = 0; // cost of collapsing this vertex, the less the better. aka objdist
-		this.collapseNeighbor = null; // best candidate for collapsing
-
-	}
-
-	addUniqueNeighbor( vertex ) {
-
-		pushIfUnique( this.neighbors, vertex );
-
-	}
-
-	removeIfNonNeighbor( n ) {
-
-		const neighbors = this.neighbors;
-		const faces = this.faces;
-
-		const offset = neighbors.indexOf( n );
-
-		if ( offset === - 1 ) return;
-
-		for ( let i = 0; i < faces.length; i ++ ) {
-
-			if ( faces[ i ].hasVertex( n ) ) return;
-
-		}
-
-		neighbors.splice( offset, 1 );
-
-	}
-
-}
-
 export { SimplifyModifier };

BIN
examples/screenshots/webgl_modifier_simplifier.jpg


+ 24 - 11
examples/webgl_modifier_simplifier.html

@@ -11,6 +11,9 @@
 		<link type="text/css" rel="stylesheet" href="main.css">
 	</head>
 	<body>
+
+		<div id="info"><a href="https://threejs.org" target="_blank" rel="noopener">threejs</a> - Vertex Reduction using SimplifyModifier</div>
+
 		<script type="importmap">
 			{
 				"imports": {
@@ -25,6 +28,7 @@
 			import * as THREE from 'three';
 
 			import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+			import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
 			import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
 			import { SimplifyModifier } from 'three/addons/modifiers/SimplifyModifier.js';
 
@@ -34,14 +38,6 @@
 
 			function init() {
 
-				const info = document.createElement( 'div' );
-				info.style.position = 'absolute';
-				info.style.top = '10px';
-				info.style.width = '100%';
-				info.style.textAlign = 'center';
-				info.innerHTML = '<a href="https://threejs.org" target="_blank" rel="noopener">three.js</a> - Vertex Reduction using SimplifyModifier';
-				document.body.appendChild( info );
-
 				renderer = new THREE.WebGLRenderer( { antialias: true } );
 				renderer.setPixelRatio( window.devicePixelRatio );
 				renderer.setSize( window.innerWidth, window.innerHeight );
@@ -75,14 +71,31 @@
 					const simplified = mesh.clone();
 					simplified.material = simplified.material.clone();
 					simplified.material.flatShading = true;
-					const count = Math.floor( simplified.geometry.attributes.position.count * 0.875 ); // number of vertices to remove
-					simplified.geometry = modifier.modify( simplified.geometry, count );
 
 					simplified.position.x = 3;
 					simplified.rotation.y = - Math.PI / 2;
 					scene.add( simplified );
 
-					render();
+					const params = { ratio: 0.125 };
+
+					async function simplify() {
+
+						const count = Math.floor( mesh.geometry.attributes.position.count * ( 1 - params.ratio ) ); // number of vertices to remove
+
+						const geometry = await modifier.modify( mesh.geometry, count );
+
+						if ( simplified.geometry !== mesh.geometry ) simplified.geometry.dispose();
+
+						simplified.geometry = geometry;
+
+						render();
+
+					}
+
+					simplify();
+
+					const gui = new GUI();
+					gui.add( params, 'ratio', 0.01, 1, 0.01 ).onChange( simplify );
 
 				} );
 

粤ICP备19079148号