Parcourir la source

Examples: Add TileCreasedNormalsPlugin. (#33767)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
mrdoob il y a 3 jours
Parent
commit
bb5ecbc16a

+ 1 - 0
examples/jsm/Addons.js

@@ -149,6 +149,7 @@ export * from './misc/MorphAnimMesh.js';
 export * from './misc/MorphBlendMesh.js';
 export * from './misc/ProgressiveLightMap.js';
 export * from './misc/RollerCoaster.js';
+export * from './misc/TileCreasedNormalsPlugin.js';
 export * from './misc/TubePainter.js';
 export * from './misc/Volume.js';
 export * from './misc/VolumeSlice.js';

+ 270 - 0
examples/jsm/misc/TileCreasedNormalsPlugin.js

@@ -0,0 +1,270 @@
+import { BufferAttribute } from 'three';
+
+/**
+ * A plugin for `3d-tiles-renderer` that computes creased vertex normals for the
+ * geometry of each loaded tile: smooth normals everywhere except where faces meet
+ * at an angle greater than the crease angle. Useful for photogrammetry tile sets
+ * like Google Photorealistic 3D Tiles which come without vertex normals.
+ *
+ * The normals are computed in a Web Worker so tile processing doesn't block the
+ * main thread. Tiles are displayed once their normals are ready.
+ *
+ * ```js
+ * tiles.registerPlugin( new TileCreasedNormalsPlugin( { creaseAngle: Math.PI / 6 } ) );
+ * ```
+ *
+ * @three_import import { TileCreasedNormalsPlugin } from 'three/addons/misc/TileCreasedNormalsPlugin.js';
+ */
+class TileCreasedNormalsPlugin {
+
+	/**
+	 * Constructs a new plugin.
+	 *
+	 * @param {Object} [options] - The configuration options.
+	 * @param {number} [options.creaseAngle=Math.PI/3] - The crease angle in radians.
+	 */
+	constructor( { creaseAngle = Math.PI / 3 } = {} ) {
+
+		/**
+		 * The crease angle in radians.
+		 *
+		 * @type {number}
+		 */
+		this.creaseAngle = creaseAngle;
+
+		this._requestId = 0;
+		this._pending = new Map();
+
+		const workerCode = `
+
+			${ computeCreasedNormals.toString() }
+
+			onmessage = ( { data } ) => {
+
+				const { id, positions, creaseAngle } = data;
+				const normals = computeCreasedNormals( positions, creaseAngle );
+				postMessage( { id, positions, normals }, [ positions.buffer, normals.buffer ] );
+
+			};
+
+		`;
+
+		this._worker = new Worker( URL.createObjectURL( new Blob( [ workerCode ] ) ) );
+		this._worker.onmessage = ( { data } ) => {
+
+			this._pending.get( data.id )( data );
+			this._pending.delete( data.id );
+
+		};
+
+	}
+
+	/**
+	 * Called by the tiles renderer for each loaded tile model. The tile is
+	 * displayed once the returned promise resolves.
+	 *
+	 * @param {Object3D} scene - The tile model.
+	 * @return {Promise} A promise that resolves when all geometries have creased normals.
+	 */
+	processTileModel( scene ) {
+
+		const promises = [];
+
+		scene.traverse( ( mesh ) => {
+
+			if ( mesh.geometry ) {
+
+				promises.push( this._processMesh( mesh ) );
+
+			}
+
+		} );
+
+		return Promise.all( promises );
+
+	}
+
+	_processMesh( mesh ) {
+
+		const geometry = mesh.geometry.index ? mesh.geometry.toNonIndexed() : mesh.geometry;
+		const positions = geometry.attributes.position.array;
+
+		const id = this._requestId ++;
+		this._worker.postMessage( { id, positions, creaseAngle: this.creaseAngle }, [ positions.buffer ] );
+
+		return new Promise( ( resolve ) => {
+
+			this._pending.set( id, ( { positions, normals } ) => {
+
+				geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
+				geometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );
+				mesh.geometry = geometry;
+				resolve();
+
+			} );
+
+		} );
+
+	}
+
+	/**
+	 * Called by the tiles renderer when the plugin is unregistered or the
+	 * tiles renderer is disposed.
+	 */
+	dispose() {
+
+		this._worker.terminate();
+
+	}
+
+}
+
+// Computes creased normals for non-indexed triangle positions. The function is
+// self-contained so it can be serialized into the worker.
+function computeCreasedNormals( positions, creaseAngle ) {
+
+	const creaseDot = Math.cos( creaseAngle );
+	const hashMultiplier = ( 1 + 1e-10 ) * 1e2;
+
+	const vertexCount = positions.length / 3;
+	const faceCount = vertexCount / 3;
+
+	// compute the normal of each face
+	const faceNormals = new Float64Array( faceCount * 3 );
+	for ( let f = 0; f < faceCount; f ++ ) {
+
+		const f9 = 9 * f;
+		const ax = positions[ f9 + 0 ], ay = positions[ f9 + 1 ], az = positions[ f9 + 2 ];
+		const bx = positions[ f9 + 3 ], by = positions[ f9 + 4 ], bz = positions[ f9 + 5 ];
+		const cx = positions[ f9 + 6 ], cy = positions[ f9 + 7 ], cz = positions[ f9 + 8 ];
+
+		const v1x = cx - bx, v1y = cy - by, v1z = cz - bz;
+		const v2x = ax - bx, v2y = ay - by, v2z = az - bz;
+
+		const nx = v1y * v2z - v1z * v2y;
+		const ny = v1z * v2x - v1x * v2z;
+		const nz = v1x * v2y - v1y * v2x;
+
+		const invLength = 1 / ( Math.sqrt( nx * nx + ny * ny + nz * nz ) || 1 );
+		faceNormals[ 3 * f + 0 ] = nx * invLength;
+		faceNormals[ 3 * f + 1 ] = ny * invLength;
+		faceNormals[ 3 * f + 2 ] = nz * invLength;
+
+	}
+
+	// assign an id to each vertex, sharing the id between vertices with the same
+	// quantized position via an open-addressed hash table (slots hold id + 1, 0 means empty)
+	const vertexIds = new Int32Array( vertexCount );
+	const quantized = new Int32Array( vertexCount * 3 );
+
+	let tableSize = 1;
+	while ( tableSize < vertexCount * 2 ) tableSize <<= 1;
+	const tableMask = tableSize - 1;
+	const table = new Int32Array( tableSize );
+
+	let uniqueCount = 0;
+	for ( let i = 0; i < vertexCount; i ++ ) {
+
+		const i3 = 3 * i;
+		const qx = ~ ~ ( positions[ i3 + 0 ] * hashMultiplier );
+		const qy = ~ ~ ( positions[ i3 + 1 ] * hashMultiplier );
+		const qz = ~ ~ ( positions[ i3 + 2 ] * hashMultiplier );
+
+		let slot = ( Math.imul( qx, 73856093 ) ^ Math.imul( qy, 19349663 ) ^ Math.imul( qz, 83492791 ) ) & tableMask;
+
+		while ( true ) {
+
+			const id = table[ slot ];
+
+			if ( id === 0 ) {
+
+				const q3 = 3 * uniqueCount;
+				quantized[ q3 + 0 ] = qx;
+				quantized[ q3 + 1 ] = qy;
+				quantized[ q3 + 2 ] = qz;
+
+				table[ slot ] = uniqueCount + 1;
+				vertexIds[ i ] = uniqueCount ++;
+				break;
+
+			}
+
+			const q3 = 3 * ( id - 1 );
+
+			if ( quantized[ q3 + 0 ] === qx && quantized[ q3 + 1 ] === qy && quantized[ q3 + 2 ] === qz ) {
+
+				vertexIds[ i ] = id - 1;
+				break;
+
+			}
+
+			slot = ( slot + 1 ) & tableMask;
+
+		}
+
+	}
+
+	// bucket the faces surrounding each unique vertex position
+	const bucketOffsets = new Int32Array( uniqueCount + 1 );
+	for ( let i = 0; i < vertexCount; i ++ ) bucketOffsets[ vertexIds[ i ] + 1 ] ++;
+	for ( let i = 0; i < uniqueCount; i ++ ) bucketOffsets[ i + 1 ] += bucketOffsets[ i ];
+
+	const bucketFaces = new Int32Array( vertexCount );
+	const bucketCursors = bucketOffsets.slice( 0, uniqueCount );
+	for ( let f = 0; f < faceCount; f ++ ) {
+
+		const f3 = 3 * f;
+		bucketFaces[ bucketCursors[ vertexIds[ f3 + 0 ] ] ++ ] = f;
+		bucketFaces[ bucketCursors[ vertexIds[ f3 + 1 ] ] ++ ] = f;
+		bucketFaces[ bucketCursors[ vertexIds[ f3 + 2 ] ] ++ ] = f;
+
+	}
+
+	// average the normals of the faces surrounding each vertex if they are within the
+	// provided crease threshold
+	const normalArray = new Float32Array( vertexCount * 3 );
+	for ( let f = 0; f < faceCount; f ++ ) {
+
+		const f3 = 3 * f;
+		const nx = faceNormals[ f3 + 0 ];
+		const ny = faceNormals[ f3 + 1 ];
+		const nz = faceNormals[ f3 + 2 ];
+
+		for ( let n = 0; n < 3; n ++ ) {
+
+			const i = f3 + n;
+			const id = vertexIds[ i ];
+
+			let sumX = 0, sumY = 0, sumZ = 0;
+
+			for ( let k = bucketOffsets[ id ], end = bucketOffsets[ id + 1 ]; k < end; k ++ ) {
+
+				const o3 = 3 * bucketFaces[ k ];
+				const ox = faceNormals[ o3 + 0 ];
+				const oy = faceNormals[ o3 + 1 ];
+				const oz = faceNormals[ o3 + 2 ];
+
+				if ( nx * ox + ny * oy + nz * oz > creaseDot ) {
+
+					sumX += ox;
+					sumY += oy;
+					sumZ += oz;
+
+				}
+
+			}
+
+			const invLength = 1 / ( Math.sqrt( sumX * sumX + sumY * sumY + sumZ * sumZ ) || 1 );
+			normalArray[ 3 * i + 0 ] = sumX * invLength;
+			normalArray[ 3 * i + 1 ] = sumY * invLength;
+			normalArray[ 3 * i + 2 ] = sumZ * invLength;
+
+		}
+
+	}
+
+	return normalArray;
+
+}
+
+export { TileCreasedNormalsPlugin };

+ 2 - 20
examples/webgl_loader_3dtiles.html

@@ -74,7 +74,7 @@
 
 			import * as THREE from 'three';
 			import { DRACOLoader, DRACO_GLTF_CONFIG } from 'three/addons/loaders/DRACOLoader.js';
-			import { toCreasedNormals } from 'three/addons/utils/BufferGeometryUtils.js';
+			import { TileCreasedNormalsPlugin } from 'three/addons/misc/TileCreasedNormalsPlugin.js';
 			import { TilesRenderer, GlobeControls, CAMERA_FRAME } from '3d-tiles-renderer';
 			import { CesiumIonAuthPlugin } from '3d-tiles-renderer/core/plugins';
 			import { GLTFExtensionsPlugin, TilesFadePlugin, UpdateOnChangePlugin } from '3d-tiles-renderer/three/plugins';
@@ -127,28 +127,10 @@
 				const DEG2RAD = Math.PI / 180;
 
 				// tiles
-				class TileCreasedNormalsPlugin {
-
-					processTileModel( scene ) {
-
-						scene.traverse( ( mesh ) => {
-
-							if ( mesh.geometry ) {
-
-								mesh.geometry = toCreasedNormals( mesh.geometry, 30 * DEG2RAD );
-
-							}
-
-						} );
-
-					}
-
-				}
-
 				tiles = new TilesRenderer();
 				tiles.registerPlugin( new CesiumIonAuthPlugin( { apiToken: ION_KEY, assetId: '2275207', autoRefreshToken: true } ) );
 				tiles.registerPlugin( new GLTFExtensionsPlugin( { dracoLoader } ) );
-				tiles.registerPlugin( new TileCreasedNormalsPlugin() );
+				tiles.registerPlugin( new TileCreasedNormalsPlugin( { creaseAngle: 30 * DEG2RAD } ) );
 				tiles.registerPlugin( new TilesFadePlugin() );
 				tiles.registerPlugin( new UpdateOnChangePlugin() );
 				tiles.setCamera( camera );

粤ICP备19079148号