3d-tiles-renderer
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
688 lines (458 loc) • 16.2 kB
JavaScript
import { MathUtils, Triangle, BufferGeometry, BufferAttribute, Mesh, Vector4 } from 'three';
const SPLIT_VALUE = 0;
const vertNames = [ 'a', 'b', 'c' ];
const _vec = /* @__PURE__ */ new Vector4();
const _v0 = /* @__PURE__ */ new Vector4();
const _v1 = /* @__PURE__ */ new Vector4();
const _v2 = /* @__PURE__ */ new Vector4();
// Class for clipping geometry using the results from a "split operation"
export class GeometryClipper {
constructor() {
// the list of attributes to use in the geometry being clipped, such as
// [ 'position', 'normal', 'uv' ]
this.attributeList = null;
// internal
this.splitOperations = [];
this.trianglePool = new ClipTrianglePool();
}
forEachSplitPermutation( callback ) {
const { splitOperations } = this;
const runPermutations = ( index = 0 ) => {
if ( index >= splitOperations.length ) {
callback();
return;
}
splitOperations[ index ].keepPositive = true;
runPermutations( index + 1 );
splitOperations[ index ].keepPositive = false;
runPermutations( index + 1 );
};
runPermutations();
}
// Takes an operation that returns a value for the given vertex passed to the callback. Triangles
// are clipped along edges where the interpolated value is equal to 0. The polygons on the positive
// side of the operation are kept if "keepPositive" is true.
// callback( geometry, i0, i1, i2, barycoord );
addSplitOperation( callback, keepPositive = true ) {
this.splitOperations.push( {
callback,
keepPositive,
} );
}
// Removes all split operations
clearSplitOperations() {
this.splitOperations.length = 0;
}
// clips an object hierarchy
clipObject( object ) {
const result = object.clone();
const toRemove = [];
result.traverse( c => {
if ( c.isMesh ) {
c.geometry = this.clip( c ).geometry;
const triCount = c.geometry.index ? c.geometry.index.count / 3 : c.attributes.position.count / 3;
if ( triCount === 0 ) {
toRemove.push( c );
}
}
} );
toRemove.forEach( m => {
m.removeFromParent();
} );
return result;
}
// Returns a new mesh that has been clipped by the split operations. Range indicates the range of
// elements to include when clipping.
clip( mesh, range = null ) {
// TODO: support multimaterial
const result = this.getClippedData( mesh, range );
return this.constructMesh( result.attributes, result.index, mesh );
}
// Appends the clip operation data to the given "target" object so multiple ranges can be appended.
// The "target" object is returned with an "index" field, "vertexIsClipped" field, and series of arrays
// in "attributes".
// attributes - set of attribute arrays
// index - triangle indices referencing vertices in attributes
// vertexIsClipped - array indicating whether a vertex is on a clipped edge
getClippedData( mesh, range = null, target = {} ) {
const { trianglePool, splitOperations, attributeList } = this;
// source geometry
const sourceGeometry = mesh.geometry;
const position = sourceGeometry.attributes.position;
const index = sourceGeometry.index;
// vertex hash data
let nextIndex = 0;
const vertToNewIndexMap = {};
// initialize the result
target.index = target.index || [];
target.vertexIsClipped = target.vertexIsClipped || [];
target.attributes = target.attributes || {};
// initialize the attributes to the set in the attribute list or all if set to null
for ( const key in sourceGeometry.attributes ) {
if ( attributeList !== null ) {
if ( attributeList instanceof Function && ! attributeList( key ) ) {
continue;
} else if ( Array.isArray( attributeList ) && ! attributeList.includes( key ) ) {
continue;
}
}
target.attributes[ key ] = [];
}
// iterate over each group separately to retain the group information
let start = 0;
let count = index ? index.count : position.count;
if ( range !== null ) {
start = range.start;
count = range.count;
}
// run the clip operations
for ( let i = start, l = start + count; i < l; i += 3 ) {
// get the indices
let i0 = i + 0;
let i1 = i + 1;
let i2 = i + 2;
if ( index ) {
i0 = index.getX( i0 );
i1 = index.getX( i1 );
i2 = index.getX( i2 );
}
// get the original triangle
const tri = trianglePool.get();
tri.initFromIndices( i0, i1, i2 );
// iterate over each triangle and clip it
let triangles = [ tri ];
for ( let s = 0; s < splitOperations.length; s ++ ) {
const { keepPositive, callback } = splitOperations[ s ];
const result = [];
for ( let t = 0; t < triangles.length; t ++ ) {
const tri = triangles[ t ];
const { indices, barycoord } = tri;
tri.clipValues.a = callback( sourceGeometry, indices.a, indices.b, indices.c, barycoord.a, mesh.matrixWorld );
tri.clipValues.b = callback( sourceGeometry, indices.a, indices.b, indices.c, barycoord.b, mesh.matrixWorld );
tri.clipValues.c = callback( sourceGeometry, indices.a, indices.b, indices.c, barycoord.c, mesh.matrixWorld );
this.splitTriangle( tri, ! keepPositive, result );
}
triangles = result;
}
// append the triangles to the result
for ( let t = 0, l = triangles.length; t < l; t ++ ) {
const tri = triangles[ t ];
pushTriangle( tri, sourceGeometry );
}
trianglePool.reset();
}
return target;
function pushTriangle( tri, geometry ) {
for ( let i = 0; i < 3; i ++ ) {
const hash = tri.getVertexHash( i, geometry );
if ( ! ( hash in vertToNewIndexMap ) ) {
vertToNewIndexMap[ hash ] = nextIndex;
nextIndex ++;
tri.getVertexData( i, geometry, target.attributes );
target.vertexIsClipped.push( tri.clipValues[ vertNames[ i ] ] === SPLIT_VALUE );
}
const index = vertToNewIndexMap[ hash ];
target.index.push( index );
}
}
}
// Takes the set of resultant data and constructs a mesh
constructMesh( attributes, index, sourceMesh ) {
const sourceGeometry = sourceMesh.geometry;
// new geometry
const geometry = new BufferGeometry();
const indexBuffer = attributes.position.length / 3 > 65535 ? new Uint32Array( index ) : new Uint16Array( index );
geometry.setIndex( new BufferAttribute( indexBuffer, 1, false ) );
for ( const key in attributes ) {
const attr = sourceGeometry.getAttribute( key );
const cons = new attr.array.constructor( attributes[ key ] );
const newAttr = new BufferAttribute( cons, attr.itemSize, attr.normalized );
newAttr.gpuType = attr.gpuType;
geometry.setAttribute( key, newAttr );
}
// new mesh
const result = new Mesh( geometry, sourceMesh.material.clone() );
result.position.copy( sourceMesh.position );
result.quaternion.copy( sourceMesh.quaternion );
result.scale.copy( sourceMesh.scale );
return result;
}
// Splits the given triangle
splitTriangle( tri, keepNegative, target ) {
const { trianglePool } = this;
// TODO: clean up, add scratch variables, optimize
const edgeIndices = [];
const edges = [];
const lerpValues = [];
// Find all points to clip
for ( let i = 0; i < 3; i ++ ) {
const v = vertNames[ i ];
const nv = vertNames[ ( i + 1 ) % 3 ];
const pValue = tri.clipValues[ v ];
const npValue = tri.clipValues[ nv ];
// if the uv values span across the halfway divide
if ( ( pValue < SPLIT_VALUE ) !== ( npValue < SPLIT_VALUE ) || pValue === SPLIT_VALUE ) {
edgeIndices.push( i );
edges.push( [ v, nv ] );
if ( pValue === npValue ) {
// avoid NaN here which can occur with mapLinear when pValue and npValue are the same value
lerpValues.push( 0 );
} else {
lerpValues.push( MathUtils.mapLinear( SPLIT_VALUE, pValue, npValue, 0, 1 ) );
}
}
}
if ( edgeIndices.length !== 2 ) {
// if we don't have two intersection points then this triangle must fall on
// one side of the bounds.
const minBound = Math.min(
tri.clipValues.a,
tri.clipValues.b,
tri.clipValues.c,
);
if ( ( minBound < SPLIT_VALUE ) === keepNegative ) {
target.push( tri );
}
} else if ( edgeIndices.length === 2 ) {
// TODO: how can we determine which triangles actually need to be added here ahead of time
const tri0 = trianglePool.get().initFromTriangle( tri );
const tri1 = trianglePool.get().initFromTriangle( tri );
const tri2 = trianglePool.get().initFromTriangle( tri );
// If the points lie on edges that are immediately after one another then we have to split the
// triangle differently.
const sequential = ( ( edgeIndices[ 0 ] + 1 ) % 3 ) === edgeIndices[ 1 ];
if ( sequential ) {
tri0.lerpVertexFromEdge( tri, edges[ 0 ][ 0 ], edges[ 0 ][ 1 ], lerpValues[ 0 ], 'a' );
tri0.copyVertex( tri, edges[ 0 ][ 1 ], 'b' );
tri0.lerpVertexFromEdge( tri, edges[ 1 ][ 0 ], edges[ 1 ][ 1 ], lerpValues[ 1 ], 'c' );
tri0.clipValues.a = SPLIT_VALUE;
tri0.clipValues.c = SPLIT_VALUE;
tri1.lerpVertexFromEdge( tri, edges[ 0 ][ 0 ], edges[ 0 ][ 1 ], lerpValues[ 0 ], 'a' );
tri1.copyVertex( tri, edges[ 1 ][ 1 ], 'b' );
tri1.copyVertex( tri, edges[ 0 ][ 0 ], 'c' );
tri1.clipValues.a = SPLIT_VALUE;
tri2.lerpVertexFromEdge( tri, edges[ 0 ][ 0 ], edges[ 0 ][ 1 ], lerpValues[ 0 ], 'a' );
tri2.lerpVertexFromEdge( tri, edges[ 1 ][ 0 ], edges[ 1 ][ 1 ], lerpValues[ 1 ], 'b' );
tri2.copyVertex( tri, edges[ 1 ][ 1 ], 'c' );
tri2.clipValues.a = SPLIT_VALUE;
tri2.clipValues.b = SPLIT_VALUE;
} else {
tri0.lerpVertexFromEdge( tri, edges[ 0 ][ 0 ], edges[ 0 ][ 1 ], lerpValues[ 0 ], 'a' );
tri0.lerpVertexFromEdge( tri, edges[ 1 ][ 0 ], edges[ 1 ][ 1 ], lerpValues[ 1 ], 'b' );
tri0.copyVertex( tri, edges[ 0 ][ 0 ], 'c' );
tri0.clipValues.a = SPLIT_VALUE;
tri0.clipValues.b = SPLIT_VALUE;
tri1.lerpVertexFromEdge( tri, edges[ 0 ][ 0 ], edges[ 0 ][ 1 ], lerpValues[ 0 ], 'a' );
tri1.copyVertex( tri, edges[ 0 ][ 1 ], 'b' );
tri1.lerpVertexFromEdge( tri, edges[ 1 ][ 0 ], edges[ 1 ][ 1 ], lerpValues[ 1 ], 'c' );
tri1.clipValues.a = SPLIT_VALUE;
tri1.clipValues.c = SPLIT_VALUE;
tri2.copyVertex( tri, edges[ 0 ][ 1 ], 'a' );
tri2.copyVertex( tri, edges[ 1 ][ 0 ], 'b' );
tri2.lerpVertexFromEdge( tri, edges[ 1 ][ 0 ], edges[ 1 ][ 1 ], lerpValues[ 1 ], 'c' );
tri2.clipValues.c = SPLIT_VALUE;
}
// Save the triangles that sit on the right side of the split
let minBound, negativeSide;
minBound = Math.min( tri0.clipValues.a, tri0.clipValues.b, tri0.clipValues.c );
negativeSide = minBound < SPLIT_VALUE;
if ( negativeSide === keepNegative ) {
target.push( tri0 );
}
minBound = Math.min( tri1.clipValues.a, tri1.clipValues.b, tri1.clipValues.c );
negativeSide = minBound < SPLIT_VALUE;
if ( negativeSide === keepNegative ) {
target.push( tri1 );
}
minBound = Math.min( tri2.clipValues.a, tri2.clipValues.b, tri2.clipValues.c );
negativeSide = minBound < SPLIT_VALUE;
if ( negativeSide === keepNegative ) {
target.push( tri2 );
}
}
}
}
// Pool of reusable triangles
class ClipTrianglePool {
constructor() {
this.pool = [];
this.index = 0;
}
get() {
if ( this.index >= this.pool.length ) {
const tri = new ClipTriangle();
this.pool.push( tri );
}
const res = this.pool[ this.index ];
this.index ++;
return res;
}
reset() {
this.index = 0;
}
}
// Triangle class that stores the values to clip along, barycoord values for clipping, and the
// original indices that the barycoord are interpolated between.
class ClipTriangle {
constructor() {
this.indices = {
a: - 1,
b: - 1,
c: - 1,
};
this.clipValues = {
a: - 1,
b: - 1,
c: - 1,
};
this.barycoord = new Triangle();
}
// returns a hash for the given [0, 2] index based on attributes of the referenced geometry
getVertexHash( index, geometry ) {
const { barycoord, indices } = this;
const vn = vertNames[ index ];
const bc = barycoord[ vn ];
// If the barycoord value is specifying a single vertex then return a quick hash
if ( bc.x === 1 ) {
return indices[ vertNames[ 0 ] ];
} else if ( bc.y === 1 ) {
return indices[ vertNames[ 1 ] ];
} else if ( bc.z === 1 ) {
return indices[ vertNames[ 2 ] ];
} else {
// Construct a hash based on all the interpolated attributes
const { attributes } = geometry;
let result = '';
for ( const name in attributes ) {
const attr = attributes[ name ];
readInterpolatedAttribute( attr, indices.a, indices.b, indices.c, bc, _vec );
// normalize values if needed
if ( name === 'normal' || name === 'tangent' || name === 'bitangent' ) {
_vec.normalize();
}
// construct the hash
switch ( attr.itemSize ) {
case 4:
result += hashVertex( _vec.x, _vec.y, _vec.z, _vec.w );
break;
case 3:
result += hashVertex( _vec.x, _vec.y, _vec.z );
break;
case 2:
result += hashVertex( _vec.x, _vec.y );
break;
case 1:
result += hashVertex( _vec.x );
break;
}
result += '|';
}
return result;
}
}
// Accumulate the vertex data in the given attribute arrays
getVertexData( index, geometry, target ) {
const { barycoord, indices } = this;
const vn = vertNames[ index ];
const bc = barycoord[ vn ];
const { attributes } = geometry;
for ( const name in attributes ) {
// skip saving the data if we have no fields for it
if ( ! target[ name ] ) {
continue;
}
const attr = attributes[ name ];
const arr = target[ name ];
readInterpolatedAttribute( attr, indices.a, indices.b, indices.c, bc, _vec );
// normalize values if needed
if ( name === 'normal' || name === 'tangent' || name === 'bitangent' ) {
_vec.normalize();
}
// append the data
switch ( attr.itemSize ) {
case 4:
arr.push( _vec.x, _vec.y, _vec.z, _vec.w );
break;
case 3:
arr.push( _vec.x, _vec.y, _vec.z );
break;
case 2:
arr.push( _vec.x, _vec.y );
break;
case 1:
arr.push( _vec.x );
break;
}
}
}
// Copy the indices from a target triangle
initFromTriangle( other ) {
return this.initFromIndices(
other.indices.a,
other.indices.b,
other.indices.c,
);
}
// Set the indices for the given
initFromIndices( i0, i1, i2 ) {
this.indices.a = i0;
this.indices.b = i1;
this.indices.c = i2;
this.clipValues.a = - 1;
this.clipValues.b = - 1;
this.clipValues.c = - 1;
this.barycoord.a.set( 1, 0, 0 );
this.barycoord.b.set( 0, 1, 0 );
this.barycoord.c.set( 0, 0, 1 );
return this;
}
// Lerp the given vertex along to the provided edge of the provided triangle
lerpVertexFromEdge( other, e0, e1, alpha, targetVertex ) {
this.clipValues[ targetVertex ] = MathUtils.lerp( other.clipValues[ e0 ], other.clipValues[ e1 ], alpha );
this.barycoord[ targetVertex ].lerpVectors( other.barycoord[ e0 ], other.barycoord[ e1 ], alpha );
}
// Copy a vertex from the provided triangle
copyVertex( other, fromVertex, targetVertex ) {
this.clipValues[ targetVertex ] = other.clipValues[ fromVertex ];
this.barycoord[ targetVertex ].copy( other.barycoord[ fromVertex ] );
}
}
// Read a vertex from the given attribute interpolated between the indices
function readInterpolatedAttribute( attribute, i0, i1, i2, barycoord, target ) {
_v0.fromBufferAttribute( attribute, i0 );
_v1.fromBufferAttribute( attribute, i1 );
_v2.fromBufferAttribute( attribute, i2 );
target
.set( 0, 0, 0, 0 )
.addScaledVector( _v0, barycoord.x )
.addScaledVector( _v1, barycoord.y )
.addScaledVector( _v2, barycoord.z );
switch ( attribute.itemSize ) {
case 3:
_vec.w = 0;
break;
case 2:
_vec.w = 0;
_vec.z = 0;
break;
case 1:
_vec.w = 0;
_vec.z = 0;
_vec.y = 0;
break;
}
return target;
}
// Hash the provided numbers
export function hashVertex( ...args ) {
const scalar = 1e5;
const additive = 0.5;
let result = '';
for ( let i = 0, l = args.length; i < l; i ++ ) {
result += ~ ~ ( args[ i ] * scalar + additive );
if ( i !== l - 1 ) {
result += '_';
}
}
return result;
}