Browse Source

SimplifyModifier: initial commit (#5806) (#9159)

- api: simplify.modify( geometry, reductions ) gives new geometry
Joshua Koo 9 years ago
parent
commit
3376a9b00d
2 changed files with 647 additions and 0 deletions
  1. 461 0
      examples/js/modifiers/SimplifyModifier.js
  2. 186 0
      examples/webgl_modifier_simplification.html

+ 461 - 0
examples/js/modifiers/SimplifyModifier.js

@@ -0,0 +1,461 @@
+/*
+ *	@author zz85 / http://twitter.com/blurspline / http://www.lab4games.net/zz85/blog
+ *
+ *	Simplification Geometry Modifier
+ *    - based on code and technique
+ *	  - by Stan Melax in 1998
+ *	  - Progressive Mesh type Polygon Reduction Algorithm
+ *    - http://www.melax.com/polychop/
+ */
+
+THREE.SimplifyModifier = function() {
+
+};
+
+(function() {
+
+	var cb = new THREE.Vector3(), ab = new THREE.Vector3();
+
+	function pushIfUnique( array, object ) {
+
+		if ( array.indexOf( object ) === -1 ) array.push( object );
+
+	}
+
+	function removeFromArray( array, object ) {
+
+		var 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".
+
+		var edgelength = v.position.distanceTo( u.position );
+		var curvature = 0;
+
+		var sideFaces = [];
+		var i, uFaces = u.faces, il = u.faces.length, face, sideFace;
+
+		// find the "sides" triangles that are on the edge uv
+		for ( i = 0 ; i < il; i ++ ) {
+
+			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 ( i = 0 ; i < il; i ++ ) {
+
+			var minCurvature = 1;
+			face = u.faces[ i ];
+
+			for( var j = 0; j < sideFaces.length; j ++ ) {
+
+				sideFace = sideFaces[ j ];
+				// use dot product of face normals.
+				var 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
+		var borders = 0;
+		if ( sideFaces.length < 2 ) {
+
+			// we add some arbitrary cost for borders,
+			// borders += 10;
+			curvature = 1;
+		}
+
+		var 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 ( var i = 0; i < v.neighbors.length; i ++ ) {
+
+			var 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 ) {
+
+			var 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!
+		var vs = [ this.v1, this.v2, this.v3 ];
+		var v1, v2;
+
+		for( var i = 0 ; i < 3 ; i ++ ) {
+			v1 = vs[ i ];
+			v2 = vs[( i+1) % 3 ];
+
+			if( !v1 || !v2 ) continue;
+			v1.removeIfNonNeighbor( v2 );
+			v2.removeIfNonNeighbor( v1 );
+		}
+
+	}
+
+	function collapse( vertices, faces, u, v ) { // u and v are pointers to vertices of an edge
+
+		// 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;
+
+		}
+
+		var i;
+		var tmpVertices = [];
+
+		for( i = 0 ; i < u.neighbors.length; i ++ ) {
+
+			tmpVertices.push( u.neighbors[ i ] );
+
+		}
+
+
+		// delete triangles on edge uv:
+		for( i = u.faces.length - 1; i >= 0; i -- ) {
+
+			if ( u.faces[ i ].hasVertex( v ) ) {
+
+				removeFace( u.faces[ i ], faces );
+
+			}
+
+		}
+
+		// update remaining triangles to have v instead of u
+		for( 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( i = 0; i < tmpVertices.length; i ++ ) {
+
+			computeEdgeCostAtVertex( tmpVertices[ i ] );
+
+		}
+
+	}
+
+
+
+	function minimumCostEdge( vertices ) {
+
+		// O(n * n) approach. TODO optimize this
+
+		var least = vertices[ 0 ];
+
+		for (var 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
+
+	function Triangle( 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 THREE.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 );
+
+	}
+
+	Triangle.prototype.computeNormal = function() {
+
+		var vA = this.v1.position;
+		var vB = this.v2.position;
+		var vC = this.v3.position;
+
+		cb.subVectors( vC, vB );
+		ab.subVectors( vA, vB );
+		cb.cross( ab ).normalize();
+
+		this.normal.copy( cb );
+
+	};
+
+	Triangle.prototype.hasVertex = function( v ) {
+
+		return v === this.v1 || v === this.v2 || v === this.v3;
+
+	};
+
+	Triangle.prototype.replaceVertex = function( 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();
+
+	};
+
+	function Vertex( v, id ) {
+
+		this.position = v;
+
+		this.id = id; // old index id
+
+		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 candinate for collapsing
+
+	}
+
+	Vertex.prototype.addUniqueNeighbor = function( vertex ) {
+		pushIfUnique(this.neighbors, vertex);
+	}
+
+	Vertex.prototype.removeIfNonNeighbor = function( n ) {
+
+		var neighbors = this.neighbors;
+		var faces = this.faces;
+
+		var offset = neighbors.indexOf( n );
+		if ( offset === -1 ) return;
+		for ( var i = 0; i < faces.length; i ++ ) {
+
+			if ( faces[ i ].hasVertex( n ) ) return;
+
+		}
+
+		neighbors.splice( offset, 1 );
+	}
+
+	THREE.SimplifyModifier.prototype.modify = function( geometry, count ) {
+
+		if ( geometry instanceof THREE.BufferGeometry && !geometry.vertices && !geometry.faces ) {
+			console.log('converting BufferGeometry to Geometry');
+			geometry = new THREE.Geometry().fromBufferGeometry( geometry );
+		}
+
+		geometry.mergeVertices();
+
+		var oldVertices = geometry.vertices; // Three Position
+		var oldFaces = geometry.faces; // Three Face
+
+		var newGeometry = new THREE.Geometry();
+
+		// conversion
+		var vertices = new Array( oldVertices.length ); // Simplify Custom Vertex Struct
+		var faces = new Array( oldFaces.length ); // Simplify Custom Traignle Struct
+
+		var i, il, face;
+
+		//
+		// put data of original geometry in different data structures
+		//
+
+		// add vertices
+		for ( i = 0, il = oldVertices.length; i < il; i ++ ) {
+
+			vertices[ i ] = new Vertex( oldVertices[ i ], i );
+
+		}
+
+		// add faces
+		for ( i = 0, il = oldFaces.length; i < il; i ++ ) {
+
+			face = oldFaces[ i ];
+			faces[ i ] = new Triangle( vertices[ face.a ], vertices[ face.b ], vertices[ face.c ], face.a, face.b, face.c );
+
+		}
+
+		// compute all edge collapse costs
+		for ( i = 0, il = vertices.length; i < il; i ++ ) {
+
+			computeEdgeCostAtVertex( vertices[ i ] );
+
+		}
+
+		var permutation = new Array( vertices.length );
+		var map = new Array( vertices.length );
+
+		var nextVertex;
+
+		var z = count;
+
+		// console.time('z')
+		// console.profile('zz');
+
+		while( z-- ) {
+			nextVertex = minimumCostEdge( vertices );
+			if (!nextVertex) {
+				console.log('no next vertex');
+				break;
+			}
+			collapse( vertices, faces, nextVertex, nextVertex.collapseNeighbor );
+		}
+
+		// console.profileEnd('zz');
+		// console.timeEnd('z')
+
+		// TODO convert to buffer geometry.
+		var newGeo = new THREE.Geometry();
+
+		for ( i = 0; i < vertices.length; i ++ ) {
+
+			var v = vertices[ i ];
+			newGeo.vertices.push( v.position )
+
+		}
+
+		for ( i = 0; i < faces.length; i ++ ) {
+
+			var tri = faces[ i ];
+			newGeo.faces.push( new THREE.Face3(
+				vertices.indexOf(tri.v1),
+				vertices.indexOf(tri.v2),
+				vertices.indexOf(tri.v3)
+			) )
+
+		}
+
+		return newGeo;
+	};
+})()

+ 186 - 0
examples/webgl_modifier_simplification.html

@@ -0,0 +1,186 @@
+<!DOCTYPE html>
+<html lang="en">
+	<head>
+		<title>three.js webgl - modifier - Subdivisions using Loop Subdivision Scheme</title>
+		<meta charset="utf-8">
+		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+		<style>
+			body {
+				font-family: Monospace;
+				background-color: #f0f0f0;
+				margin: 0px;
+				overflow: hidden;
+			}
+		</style>
+	</head>
+	<body>
+
+		<script src="../build/three.js"></script>
+		<script src="js/controls/OrbitControls.js"></script>
+		<script src="js/modifiers/SimplifyModifier.js"></script>
+		<script src="js/libs/stats.min.js"></script>
+
+		<script>
+
+			var container, stats;
+
+			var camera, controls, scene, renderer;
+
+			var cube, mesh, material;
+
+			var boxGeometry = new THREE.BoxGeometry( 80, 80, 80, 4, 4, 4 );
+			var torusGeometry = new THREE.TorusGeometry( 50, 25, 8, 8, Math.PI * 2 );
+			var sphereGeometry = new THREE.SphereGeometry( 50, 15, 15 );
+			var planeGeometry = new THREE.PlaneGeometry( 100, 100, 6, 6 );
+			var textGeometry;
+
+			var loader = new THREE.FontLoader();
+			loader.load( 'fonts/helvetiker_regular.typeface.json', function ( font ) {
+
+					textGeometry = new THREE.TextGeometry( '&', {
+					size: 200,
+					height: 50,
+					curveSegments: 1,
+					font: font
+				} );
+
+			} );
+
+			var modifer = new THREE.SimplifyModifier();
+			var meshes = [];
+			var count = 0;
+
+
+			init();
+			animate();
+
+
+			function addStuff( geometry ) {
+				count ++;
+
+				var simplified = modifer.modify( geometry, geometry.vertices.length * 0.5 | 0 );
+
+				var wireframe = new THREE.MeshBasicMaterial({
+					color: Math.random() * 0xffffff,
+					wireframe: true
+				});
+
+				var materialNormal = new THREE.MeshNormalMaterial({
+					transparent: true,
+					opacity: 0.7
+
+				});
+
+				mesh = THREE.SceneUtils.createMultiMaterialObject( simplified, [
+					material,
+					wireframe,
+					// materialNormal
+				]);
+
+				mesh.position.x = -200;
+				mesh.position.y = count * 150 - 300;
+
+				scene.add( mesh );
+				meshes.push( mesh );
+
+
+				mesh = THREE.SceneUtils.createMultiMaterialObject( geometry, [
+					material,
+					wireframe,
+					// materialNormal
+				]);
+
+				mesh.position.x = 200;
+				mesh.position.y = count * 150 - 300;
+				scene.add( mesh );
+				meshes.push ( mesh );
+
+			}
+
+			function init() {
+
+				container = document.createElement( 'div' );
+				document.body.appendChild( container );
+
+				info = document.createElement( 'div' );
+				info.style.position = 'absolute';
+				info.style.top = '10px';
+				info.style.width = '100%';
+				info.style.textAlign = 'center';
+				info.innerHTML = '50% Vertex Reduction using SimplifyModifier';
+				container.appendChild( info );
+
+				camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 1000 );
+				camera.position.z = 500;
+
+				scene = new THREE.Scene();
+
+				var light = new THREE.PointLight( 0xffffff, 1.5 );
+				light.position.set( 1000, 1000, 2000 );
+				scene.add( light );
+
+				addStuff( planeGeometry );
+				addStuff( boxGeometry );
+				addStuff( sphereGeometry );
+				addStuff( torusGeometry );
+
+
+
+
+
+				renderer = new THREE.WebGLRenderer( { antialias: true } );
+				renderer.setClearColor( 0xf0f0f0 );
+				renderer.setPixelRatio( window.devicePixelRatio );
+				renderer.setSize( window.innerWidth, window.innerHeight );
+				container.appendChild( renderer.domElement );
+
+				stats = new Stats();
+				container.appendChild( stats.dom );
+
+				//
+
+				controls = new THREE.OrbitControls( camera, renderer.domElement );
+
+				window.addEventListener( 'resize', onWindowResize, false );
+
+			}
+
+			function onWindowResize() {
+
+				camera.aspect = window.innerWidth / window.innerHeight;
+				camera.updateProjectionMatrix();
+
+				renderer.setSize( window.innerWidth, window.innerHeight );
+
+			}
+
+			//
+
+			function animate() {
+
+				meshes.forEach( m => {
+					m.rotation.x += 0.01;
+					m.rotation.y += 0.01;
+					m.rotation.z += 0.01;
+				})
+
+				requestAnimationFrame( animate );
+
+				controls.update();
+
+				stats.begin();
+				render();
+				stats.end();
+
+			}
+
+			function render() {
+
+				renderer.render( scene, camera );
+
+			}
+
+		</script>
+
+	</body>
+</html>