three
Version:
JavaScript 3D library
418 lines (273 loc) • 15.2 kB
JavaScript
import { Box3, StorageBufferAttribute, Vector4 } from 'three/webgpu';
import { vec2, storage, uniform, uniformArray } from 'three/tsl';
import { MeshoptClusterizer } from '../libs/meshopt_clusterizer.module.js';
import { MeshoptSimplifier } from '../libs/meshopt_simplifier.module.js';
/**
* Utilities to process geometries into clusters (meshlets) of 64 triangles with a chain of simplified
* levels of detail, used by the meshlet drawing pipelines.
*
* @module MeshletGeometryUtils
* @three_import import * as MeshletGeometryUtils from 'three/addons/utils/MeshletGeometryUtils.js';
*/
/**
* The number of triangles of a meshlet. Meshlets are padded with degenerate triangles to this size.
*
* @type {number}
* @constant
*/
export const MESHLET_SIZE = 64;
/**
* Resolves when the meshoptimizer modules are ready to process geometries.
*
* @type {Promise}
*/
export const ready = Promise.all( [ MeshoptClusterizer.ready, MeshoptSimplifier.ready ] );
// simplified levels of detail. levels beyond the first may collapse edges across attribute seams
// ( Permissive ): faceted geometries like buildings have seams at every hard edge, which would
// otherwise block the simplification. small disconnected parts are removed if the error allows it ( Prune )
const LOD_TARGETS = [
{ ratio: 1.0, error: 0.0, weights: [ 0.25, 0.25, 0.25, 0.5, 0.5 ], flags: [] },
{ ratio: 0.55, error: 0.004, weights: [ 0.2, 0.2, 0.2, 0.35, 0.35 ], flags: [ 'RegularizeLight', 'Permissive', 'Prune' ] },
{ ratio: 0.25, error: 0.015, weights: [ 0.12, 0.12, 0.12, 0.2, 0.2 ], flags: [ 'RegularizeLight', 'Permissive', 'Prune' ] },
{ ratio: 0.1, error: 0.05, weights: [ 0.08, 0.08, 0.08, 0.12, 0.12 ], flags: [ 'RegularizeLight', 'Permissive', 'Prune' ] },
{ ratio: 0.04, error: 0.14, weights: [ 0.04, 0.04, 0.04, 0.06, 0.06 ], flags: [ 'Regularize', 'Permissive', 'Prune' ] },
{ ratio: 0.015, error: 0.3, weights: [ 0.02, 0.02, 0.02, 0.03, 0.03 ], flags: [ 'Regularize', 'Permissive', 'Prune' ] }
];
/**
* The maximum number of levels of detail of a geometry.
*
* @type {number}
* @constant
*/
export const MAX_LODS = LOD_TARGETS.length;
// a level which removes less than 15% of the triangles of the previous level is simplified again without
// preserving the topology, allowing a larger error
const MIN_LOD_REDUCTION = 0.85;
const SLOPPY_ERROR_SCALE = 4;
// the clusters are drawn by pulling the vertices with their global index, so the vertices per cluster are
// not limited by a local vertex buffer. faceted geometries share few vertices between triangles, a limit of
// 64 vertices would end meshlets at ~32 triangles and pad the rest with degenerate triangles
const MAX_MESHLET_VERTICES = MESHLET_SIZE * 3;
// returns the components of a vec4 node used by an attribute of the given item size
const swizzle = ( node, itemSize ) => itemSize === 1 ? node.x : itemSize === 2 ? node.xy : itemSize === 3 ? node.xyz : node;
/**
* Builds meshlets of 64 triangles and a chain of simplified levels of detail for the given geometry.
* Identical vertices are welded first and all levels index the same vertices. The vertex data is
* interleaved into one storage buffer and accessed through the returned functions.
*
* @param {BufferGeometry} geometry - The geometry, with position and normal attributes.
* @param {?Object} [group=null] - An optional material group whose triangles are processed independently.
* @return {Object} The meshlet data.
*/
export function createMeshlets( geometry, group = null ) {
const positionAttribute = geometry.attributes.position;
const normalAttribute = geometry.attributes.normal;
const uvAttribute = geometry.attributes.uv;
const sourceCount = positionAttribute.count;
const indexCount = geometry.index !== null ? geometry.index.count : sourceCount;
const start = group !== null ? group.start : 0;
const count = group !== null ? Math.floor( Math.min( group.count, indexCount - start ) / 3 ) * 3 : indexCount;
const referenced = group !== null ? new Uint8Array( sourceCount ) : null;
if ( referenced !== null ) {
for ( let i = start; i < start + count; i ++ ) referenced[ geometry.index !== null ? geometry.index.getX( i ) : i ] = 1;
}
// other attributes are reconstructed per fragment as well, e.g. for custom material nodes
const extras = Object.keys( geometry.attributes )
.filter( ( name ) => name !== 'position' && name !== 'normal' && name !== 'uv' && geometry.attributes[ name ].isInstancedBufferAttribute !== true && geometry.attributes[ name ].itemSize <= 4 )
.map( ( name ) => ( { name, attribute: geometry.attributes[ name ] } ) );
// weld identical vertices, so the clusters share vertices and the simplifier can collapse edges —
// non-indexed geometries would otherwise produce clusters of ~21 triangles and no levels of detail
// the vertices are compared by their values, hashed by the bits of the values ( -0 is stored as 0 )
const keyAttributes = [ positionAttribute, normalAttribute ];
if ( uvAttribute !== undefined ) keyAttributes.push( uvAttribute );
for ( const { attribute } of extras ) keyAttributes.push( attribute );
const keySize = keyAttributes.reduce( ( size, attribute ) => size + attribute.itemSize, 0 );
const keys = new Float32Array( sourceCount * keySize );
const keyBits = new Uint32Array( keys.buffer );
const tableSize = 2 ** Math.ceil( Math.log2( sourceCount * 2 + 1 ) );
const table = new Int32Array( tableSize ).fill( - 1 );
const remap = new Uint32Array( sourceCount );
const sources = [];
for ( let i = 0; i < sourceCount; i ++ ) {
if ( referenced !== null && referenced[ i ] === 0 ) continue;
const offset = i * keySize;
let k = offset;
for ( const attribute of keyAttributes ) {
for ( let c = 0; c < attribute.itemSize; c ++ ) keys[ k ++ ] = attribute.getComponent( i, c ) + 0;
}
let hash = 2166136261;
for ( let c = offset; c < k; c ++ ) hash = Math.imul( hash ^ keyBits[ c ], 16777619 );
let slot = ( hash >>> 0 ) & ( tableSize - 1 );
let index = - 1;
while ( table[ slot ] !== - 1 ) {
const other = sources[ table[ slot ] ] * keySize;
let c = 0;
while ( c < keySize && keys[ other + c ] === keys[ offset + c ] ) c ++;
if ( c === keySize ) {
index = table[ slot ];
break;
}
slot = ( slot + 1 ) & ( tableSize - 1 );
}
if ( index === - 1 ) {
index = sources.length;
sources.push( i );
table[ slot ] = index;
}
remap[ i ] = index;
}
const vertexCount = sources.length;
const positions = new Float32Array( vertexCount * 3 );
const simplifierAttributes = new Float32Array( vertexCount * 5 );
// all vertex data is interleaved into one buffer to stay within the storage buffer limit per shader stage:
// [ position.xyz, uv.x ], [ normal.xyz, uv.y ] and one vec4 per other attribute
const stride = 2 + extras.length;
const vertexArray = new Float32Array( vertexCount * stride * 4 );
for ( let i = 0; i < vertexCount; i ++ ) {
const source = sources[ i ];
for ( let e = 0; e < extras.length; e ++ ) {
const attribute = extras[ e ].attribute;
for ( let c = 0; c < attribute.itemSize; c ++ ) vertexArray[ ( i * stride + 2 + e ) * 4 + c ] = attribute.getComponent( source, c );
}
const x = positionAttribute.getX( source ), y = positionAttribute.getY( source ), z = positionAttribute.getZ( source );
const nx = normalAttribute.getX( source ), ny = normalAttribute.getY( source ), nz = normalAttribute.getZ( source );
const u = uvAttribute !== undefined ? uvAttribute.getX( source ) : 0;
const v = uvAttribute !== undefined ? uvAttribute.getY( source ) : 0;
positions[ i * 3 + 0 ] = x;
positions[ i * 3 + 1 ] = y;
positions[ i * 3 + 2 ] = z;
simplifierAttributes[ i * 5 + 0 ] = nx;
simplifierAttributes[ i * 5 + 1 ] = ny;
simplifierAttributes[ i * 5 + 2 ] = nz;
simplifierAttributes[ i * 5 + 3 ] = u;
simplifierAttributes[ i * 5 + 4 ] = v;
vertexArray.set( [ x, y, z, u, nx, ny, nz, v ], i * stride * 4 );
}
const sourceIndices = new Uint32Array( count );
for ( let i = 0; i < sourceIndices.length; i ++ ) {
sourceIndices[ i ] = remap[ geometry.index !== null ? geometry.index.getX( start + i ) : start + i ];
}
const sourceScale = MeshoptSimplifier.getScale( positions, 3 );
const lods = [];
let indices = sourceIndices;
let previousError = 0;
let totalChunks = 0;
for ( let i = 0; i < LOD_TARGETS.length; i ++ ) {
let error = 0;
if ( i > 0 ) {
const target = LOD_TARGETS[ i ];
// pruning can reduce a level below the target of the next one
const targetIndexCount = Math.min( indices.length, Math.max( 3, Math.floor( sourceIndices.length * target.ratio / 3 ) * 3 ) );
let simplified = MeshoptSimplifier.simplifyWithAttributes( indices, positions, 3, simplifierAttributes, 5, target.weights, null, targetIndexCount, target.error, target.flags );
// if the topology blocks the simplification, the level is simplified without preserving it. the
// error of the level grows accordingly, so the level is only selected if it isn't noticeable
if ( indices.length > targetIndexCount && simplified[ 0 ].length > indices.length * MIN_LOD_REDUCTION ) {
const sloppy = MeshoptSimplifier.simplifySloppy( indices, positions, 3, null, targetIndexCount, target.error * SLOPPY_ERROR_SCALE );
if ( sloppy[ 0 ].length >= 3 && sloppy[ 0 ].length < simplified[ 0 ].length ) simplified = sloppy;
}
if ( simplified[ 0 ].length >= 3 ) {
indices = simplified[ 0 ];
error = previousError + simplified[ 1 ] * sourceScale;
} else {
error = previousError;
}
}
previousError = error;
const meshlets = MeshoptClusterizer.buildMeshlets( indices, positions, 3, MAX_MESHLET_VERTICES, MESHLET_SIZE, 0.25 );
const bounds = MeshoptClusterizer.computeMeshletBounds( meshlets, positions, 3 );
lods.push( { meshlets, bounds, error, numChunks: meshlets.meshletCount, numTriangles: meshlets.meshletCount * MESHLET_SIZE } );
totalChunks += meshlets.meshletCount;
}
const totalTriangles = totalChunks * MESHLET_SIZE;
const indexArray = new Uint32Array( totalTriangles * 3 );
// the bounding sphere and the normal cone of each cluster, interleaved to save storage buffer bindings
const chunkArray = new Float32Array( totalChunks * 8 );
let chunkOffset = 0;
let triangleOffset = 0;
for ( const lod of lods ) {
lod.chunkStart = chunkOffset;
lod.triangleStart = triangleOffset;
for ( let m = 0; m < lod.numChunks; m ++ ) {
const meshlet = MeshoptClusterizer.extractMeshlet( lod.meshlets, m );
const meshletTriangles = meshlet.triangles.length / 3;
// pad to exactly 64 triangles with degenerate triangles
for ( let t = 0; t < MESHLET_SIZE; t ++ ) {
const triangle = triangleOffset + m * MESHLET_SIZE + t;
for ( let k = 0; k < 3; k ++ ) {
indexArray[ triangle * 3 + k ] = t < meshletTriangles ? meshlet.vertices[ meshlet.triangles[ t * 3 + k ] ] : meshlet.vertices[ 0 ];
}
}
const bound = lod.bounds[ m ];
chunkArray[ chunkOffset * 8 + 0 ] = bound.centerX;
chunkArray[ chunkOffset * 8 + 1 ] = bound.centerY;
chunkArray[ chunkOffset * 8 + 2 ] = bound.centerZ;
chunkArray[ chunkOffset * 8 + 3 ] = bound.radius;
// the normal cone of the triangles, the cutoff is 1 if the cone is too wide to cull the cluster
chunkArray[ chunkOffset * 8 + 4 ] = bound.coneAxisX;
chunkArray[ chunkOffset * 8 + 5 ] = bound.coneAxisY;
chunkArray[ chunkOffset * 8 + 6 ] = bound.coneAxisZ;
chunkArray[ chunkOffset * 8 + 7 ] = bound.coneCutoff;
chunkOffset ++;
}
triangleOffset += lod.numTriangles;
}
if ( geometry.boundingSphere === null ) geometry.computeBoundingSphere();
if ( geometry.boundingBox === null ) geometry.computeBoundingBox();
// the bounding sphere of a group is centered like the sphere of the geometry, so the groups of an object share
// the bounds of their instances, and only its radius is fitted to the triangles of the group
const boundingBox = group !== null ? new Box3().setFromArray( positions ) : geometry.boundingBox.clone();
const boundingSphere = geometry.boundingSphere.clone();
if ( group !== null ) {
const center = boundingSphere.center;
let radiusSq = 0;
for ( let i = 0; i < positions.length; i += 3 ) {
const dx = positions[ i ] - center.x, dy = positions[ i + 1 ] - center.y, dz = positions[ i + 2 ] - center.z;
radiusSq = Math.max( radiusSq, dx * dx + dy * dy + dz * dz );
}
boundingSphere.radius = Math.sqrt( radiusSq );
}
// per-geometry values are uniforms, so the batches of all geometries share their shaders and pipelines
const vertexBuffer = storage( new StorageBufferAttribute( vertexArray, 4 ), 'vec4', vertexCount * stride ).toReadOnly();
const strideNode = uniform( stride, 'uint' );
const element = ( vertex, offset ) => vertexBuffer.element( vertex.mul( strideNode ).add( offset ) );
const chunkBuffer = storage( new StorageBufferAttribute( chunkArray, 4 ), 'vec4', totalChunks * 2 ).toReadOnly();
const lodOffsets = LOD_TARGETS.map( ( target, i ) => {
const lod = lods[ i ];
return lod !== undefined ? new Vector4( lod.triangleStart, lod.numTriangles, lod.chunkStart, lod.error ) : new Vector4();
} );
const attributes = new Map( extras.map( ( { name, attribute }, e ) => [ name, { offset: 2 + e, itemSize: attribute.itemSize } ] ) );
const instancedAttributes = Object.keys( geometry.attributes )
.filter( ( name ) => geometry.attributes[ name ].isInstancedBufferAttribute === true && geometry.attributes[ name ].itemSize <= 4 )
.map( ( name ) => ( { name, itemSize: geometry.attributes[ name ].itemSize } ) );
// geometries with the same layout of the vertex data and the instanced attributes can share a draw
const layoutKey = extras.map( ( { name, attribute } ) => name + ':' + attribute.itemSize ).join( ',' ) + '|' +
instancedAttributes.map( ( { name, itemSize } ) => name + ':' + itemSize ).join( ',' );
return {
position: ( vertex ) => element( vertex, 0 ).xyz,
normal: ( vertex ) => element( vertex, 1 ).xyz,
uv: ( vertex ) => vec2( element( vertex, 0 ).w, element( vertex, 1 ).w ),
hasAttribute: ( name ) => attributes.has( name ),
attribute: ( name, vertex ) => swizzle( element( vertex, attributes.get( name ).offset ), attributes.get( name ).itemSize ),
indexBuffer: storage( new StorageBufferAttribute( indexArray, 1 ), 'uint', totalTriangles * 3 ).toReadOnly(),
chunkBounds: ( chunk ) => chunkBuffer.element( chunk.mul( 2 ) ),
chunkCone: ( chunk ) => chunkBuffer.element( chunk.mul( 2 ).add( 1 ) ),
chunkArray,
chunkCount: totalChunks,
lodOffsets: uniformArray( lodOffsets, 'vec4' ),
lodOffsetValues: lodOffsets,
lodTriangleEnds: lods.map( ( lod ) => lod.triangleStart + lod.numTriangles ),
lodCount: lods.length,
maxChunks: lods[ 0 ].numChunks,
instancedAttributes,
// the raw data, for pools of the data of several geometries: the interleaved vertices ( vec4 elements, stride
// elements per vertex ), the vertex indices of the triangles and the offsets of the other attributes
vertexArray,
stride,
indexArray,
attributeLayout: attributes,
layoutKey,
geometry,
boundingSphere,
boundingBox
};
}