diff --git a/examples/jsm/modifiers/SimplifyModifier.js b/examples/jsm/modifiers/SimplifyModifier.js index 3c5bdfc6f79ccb..437121fcd706a3 100644 --- a/examples/jsm/modifiers/SimplifyModifier.js +++ b/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} 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 }; diff --git a/examples/jsm/shaders/DepthLimitedBlurShader.js b/examples/jsm/shaders/DepthLimitedBlurShader.js index ef5e8525124e85..b1aa8c2bc6ce84 100644 --- a/examples/jsm/shaders/DepthLimitedBlurShader.js +++ b/examples/jsm/shaders/DepthLimitedBlurShader.js @@ -8,7 +8,10 @@ import { */ /** - * TODO + * A separable Gaussian blur shader that limits blurring across depth + * discontinuities, stopping when the view-space depth difference to a + * neighboring sample exceeds `depthCutoff`. This preserves edges and + * avoids bleeding across them. * * Used by {@link SAOPass}. * diff --git a/examples/jsm/shaders/ExposureShader.js b/examples/jsm/shaders/ExposureShader.js index d61d492f396875..ca66f7affcb270 100644 --- a/examples/jsm/shaders/ExposureShader.js +++ b/examples/jsm/shaders/ExposureShader.js @@ -4,7 +4,7 @@ */ /** - * TODO + * Exposure shader that scales the image color by an exposure factor. * * @constant * @type {ShaderMaterial~Shader} diff --git a/examples/jsm/shaders/FilmShader.js b/examples/jsm/shaders/FilmShader.js index 2f25c5945fb34e..9f9b7fba395859 100644 --- a/examples/jsm/shaders/FilmShader.js +++ b/examples/jsm/shaders/FilmShader.js @@ -4,7 +4,8 @@ */ /** - * TODO + * Film grain shader that adds animated noise to the image, with an optional + * grayscale conversion. * * Used by {@link FilmPass}. * diff --git a/examples/screenshots/webgl_geometry_extrude_shapes.jpg b/examples/screenshots/webgl_geometry_extrude_shapes.jpg index 128e7969747442..443b274ab5a149 100644 Binary files a/examples/screenshots/webgl_geometry_extrude_shapes.jpg and b/examples/screenshots/webgl_geometry_extrude_shapes.jpg differ diff --git a/examples/screenshots/webgl_interactive_lines.jpg b/examples/screenshots/webgl_interactive_lines.jpg index 4577cc278ed962..7d29e6bd7283dd 100644 Binary files a/examples/screenshots/webgl_interactive_lines.jpg and b/examples/screenshots/webgl_interactive_lines.jpg differ diff --git a/examples/screenshots/webgl_modifier_edgesplit.jpg b/examples/screenshots/webgl_modifier_edgesplit.jpg index 7588197ca5b29d..9e7f2b73bc57b6 100644 Binary files a/examples/screenshots/webgl_modifier_edgesplit.jpg and b/examples/screenshots/webgl_modifier_edgesplit.jpg differ diff --git a/examples/screenshots/webgl_modifier_simplifier.jpg b/examples/screenshots/webgl_modifier_simplifier.jpg index 7de4e07804b878..c637fde7003178 100644 Binary files a/examples/screenshots/webgl_modifier_simplifier.jpg and b/examples/screenshots/webgl_modifier_simplifier.jpg differ diff --git a/examples/webgl_geometry_extrude_shapes.html b/examples/webgl_geometry_extrude_shapes.html index 4ae53f0c4f48a0..a2d5f82d062e25 100644 --- a/examples/webgl_geometry_extrude_shapes.html +++ b/examples/webgl_geometry_extrude_shapes.html @@ -20,6 +20,8 @@ +
three.js webgl - geometry extrude shapes
+