import { BufferGeometry, Float32BufferAttribute, Vector2, Vector3, Vector4 } from 'three'; import * as BufferGeometryUtils from '../utils/BufferGeometryUtils.js'; /** * Simplification Geometry Modifier * - based on code and technique * - by Stan Melax in 1998 * - Progressive Mesh type Polygon Reduction Algorithm * - http://www.melax.com/polychop/ */ const _cb = new Vector3(), _ab = new Vector3(); class SimplifyModifier { 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 geomtries 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 ); // // put data of original geometry in different data structures // const vertices = []; const faces = []; // add vertices 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 ) { t = new Vector4().fromBufferAttribute( tangentAttribute, i ); } if ( colorAttribute ) { col = new THREE.Color().fromBufferAttribute( colorAttribute, i ); } const vertex = new Vertex( v, v2, nor, t, col ); vertices.push( vertex ); } // add faces let index = geometry.getIndex(); if ( index !== null ) { for ( let i = 0; i < index.count; i += 3 ) { const a = index.getX( i ); const b = index.getX( i + 1 ); const c = index.getX( i + 2 ); const triangle = new Triangle( vertices[ a ], vertices[ b ], vertices[ c ], a, b, c ); faces.push( triangle ); } } else { for ( let i = 0; i < positionAttribute.count; i += 3 ) { const a = i; const b = i + 1; const c = i + 2; const triangle = new Triangle( vertices[ a ], vertices[ b ], vertices[ c ], a, b, c ); faces.push( triangle ); } } // compute all edge collapse costs for ( let i = 0, il = vertices.length; i < il; i ++ ) { computeEdgeCostAtVertex( vertices[ i ] ); } let nextVertex; 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 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 ) { uv.push( vertex.uv.x, vertex.uv.y ); } if ( vertex.normal ) { normal.push( vertex.normal.x, vertex.normal.y, vertex.normal.z ); } if ( vertex.tangent ) { tangent.push( vertex.tangent.x, vertex.tangent.y, vertex.tangent.z, vertex.tangent.w ); } 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( '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 ); return simplifiedGeometry; } } 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 candinate 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 };