TileCreasedNormalsPlugin.js 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import { BufferAttribute } from 'three';
  2. /**
  3. * A plugin for `3d-tiles-renderer` that computes creased vertex normals for the
  4. * geometry of each loaded tile: smooth normals everywhere except where faces meet
  5. * at an angle greater than the crease angle. Useful for photogrammetry tile sets
  6. * like Google Photorealistic 3D Tiles which come without vertex normals.
  7. *
  8. * The normals are computed in a Web Worker so tile processing doesn't block the
  9. * main thread. Tiles are displayed once their normals are ready.
  10. *
  11. * ```js
  12. * tiles.registerPlugin( new TileCreasedNormalsPlugin( { creaseAngle: Math.PI / 6 } ) );
  13. * ```
  14. *
  15. * @three_import import { TileCreasedNormalsPlugin } from 'three/addons/misc/TileCreasedNormalsPlugin.js';
  16. */
  17. class TileCreasedNormalsPlugin {
  18. /**
  19. * Constructs a new plugin.
  20. *
  21. * @param {Object} [options] - The configuration options.
  22. * @param {number} [options.creaseAngle=Math.PI/3] - The crease angle in radians.
  23. */
  24. constructor( { creaseAngle = Math.PI / 3 } = {} ) {
  25. /**
  26. * The crease angle in radians.
  27. *
  28. * @type {number}
  29. */
  30. this.creaseAngle = creaseAngle;
  31. this._requestId = 0;
  32. this._pending = new Map();
  33. const workerCode = `
  34. ${ computeCreasedNormals.toString() }
  35. onmessage = ( { data } ) => {
  36. const { id, positions, creaseAngle } = data;
  37. const normals = computeCreasedNormals( positions, creaseAngle );
  38. postMessage( { id, positions, normals }, [ positions.buffer, normals.buffer ] );
  39. };
  40. `;
  41. this._worker = new Worker( URL.createObjectURL( new Blob( [ workerCode ] ) ) );
  42. this._worker.onmessage = ( { data } ) => {
  43. this._pending.get( data.id )( data );
  44. this._pending.delete( data.id );
  45. };
  46. }
  47. /**
  48. * Called by the tiles renderer for each loaded tile model. The tile is
  49. * displayed once the returned promise resolves.
  50. *
  51. * @param {Object3D} scene - The tile model.
  52. * @return {Promise} A promise that resolves when all geometries have creased normals.
  53. */
  54. processTileModel( scene ) {
  55. const promises = [];
  56. scene.traverse( ( mesh ) => {
  57. if ( mesh.geometry ) {
  58. promises.push( this._processMesh( mesh ) );
  59. }
  60. } );
  61. return Promise.all( promises );
  62. }
  63. _processMesh( mesh ) {
  64. const geometry = mesh.geometry.index ? mesh.geometry.toNonIndexed() : mesh.geometry;
  65. const positions = geometry.attributes.position.array;
  66. const id = this._requestId ++;
  67. this._worker.postMessage( { id, positions, creaseAngle: this.creaseAngle }, [ positions.buffer ] );
  68. return new Promise( ( resolve ) => {
  69. this._pending.set( id, ( { positions, normals } ) => {
  70. geometry.setAttribute( 'position', new BufferAttribute( positions, 3 ) );
  71. geometry.setAttribute( 'normal', new BufferAttribute( normals, 3 ) );
  72. mesh.geometry = geometry;
  73. resolve();
  74. } );
  75. } );
  76. }
  77. /**
  78. * Called by the tiles renderer when the plugin is unregistered or the
  79. * tiles renderer is disposed.
  80. */
  81. dispose() {
  82. this._worker.terminate();
  83. }
  84. }
  85. // Computes creased normals for non-indexed triangle positions. The function is
  86. // self-contained so it can be serialized into the worker.
  87. function computeCreasedNormals( positions, creaseAngle ) {
  88. const creaseDot = Math.cos( creaseAngle );
  89. const hashMultiplier = ( 1 + 1e-10 ) * 1e2;
  90. const vertexCount = positions.length / 3;
  91. const faceCount = vertexCount / 3;
  92. // compute the normal of each face
  93. const faceNormals = new Float64Array( faceCount * 3 );
  94. for ( let f = 0; f < faceCount; f ++ ) {
  95. const f9 = 9 * f;
  96. const ax = positions[ f9 + 0 ], ay = positions[ f9 + 1 ], az = positions[ f9 + 2 ];
  97. const bx = positions[ f9 + 3 ], by = positions[ f9 + 4 ], bz = positions[ f9 + 5 ];
  98. const cx = positions[ f9 + 6 ], cy = positions[ f9 + 7 ], cz = positions[ f9 + 8 ];
  99. const v1x = cx - bx, v1y = cy - by, v1z = cz - bz;
  100. const v2x = ax - bx, v2y = ay - by, v2z = az - bz;
  101. const nx = v1y * v2z - v1z * v2y;
  102. const ny = v1z * v2x - v1x * v2z;
  103. const nz = v1x * v2y - v1y * v2x;
  104. const invLength = 1 / ( Math.sqrt( nx * nx + ny * ny + nz * nz ) || 1 );
  105. faceNormals[ 3 * f + 0 ] = nx * invLength;
  106. faceNormals[ 3 * f + 1 ] = ny * invLength;
  107. faceNormals[ 3 * f + 2 ] = nz * invLength;
  108. }
  109. // assign an id to each vertex, sharing the id between vertices with the same
  110. // quantized position via an open-addressed hash table (slots hold id + 1, 0 means empty)
  111. const vertexIds = new Int32Array( vertexCount );
  112. const quantized = new Int32Array( vertexCount * 3 );
  113. let tableSize = 1;
  114. while ( tableSize < vertexCount * 2 ) tableSize <<= 1;
  115. const tableMask = tableSize - 1;
  116. const table = new Int32Array( tableSize );
  117. let uniqueCount = 0;
  118. for ( let i = 0; i < vertexCount; i ++ ) {
  119. const i3 = 3 * i;
  120. const qx = ~ ~ ( positions[ i3 + 0 ] * hashMultiplier );
  121. const qy = ~ ~ ( positions[ i3 + 1 ] * hashMultiplier );
  122. const qz = ~ ~ ( positions[ i3 + 2 ] * hashMultiplier );
  123. let slot = ( Math.imul( qx, 73856093 ) ^ Math.imul( qy, 19349663 ) ^ Math.imul( qz, 83492791 ) ) & tableMask;
  124. while ( true ) {
  125. const id = table[ slot ];
  126. if ( id === 0 ) {
  127. const q3 = 3 * uniqueCount;
  128. quantized[ q3 + 0 ] = qx;
  129. quantized[ q3 + 1 ] = qy;
  130. quantized[ q3 + 2 ] = qz;
  131. table[ slot ] = uniqueCount + 1;
  132. vertexIds[ i ] = uniqueCount ++;
  133. break;
  134. }
  135. const q3 = 3 * ( id - 1 );
  136. if ( quantized[ q3 + 0 ] === qx && quantized[ q3 + 1 ] === qy && quantized[ q3 + 2 ] === qz ) {
  137. vertexIds[ i ] = id - 1;
  138. break;
  139. }
  140. slot = ( slot + 1 ) & tableMask;
  141. }
  142. }
  143. // bucket the faces surrounding each unique vertex position
  144. const bucketOffsets = new Int32Array( uniqueCount + 1 );
  145. for ( let i = 0; i < vertexCount; i ++ ) bucketOffsets[ vertexIds[ i ] + 1 ] ++;
  146. for ( let i = 0; i < uniqueCount; i ++ ) bucketOffsets[ i + 1 ] += bucketOffsets[ i ];
  147. const bucketFaces = new Int32Array( vertexCount );
  148. const bucketCursors = bucketOffsets.slice( 0, uniqueCount );
  149. for ( let f = 0; f < faceCount; f ++ ) {
  150. const f3 = 3 * f;
  151. bucketFaces[ bucketCursors[ vertexIds[ f3 + 0 ] ] ++ ] = f;
  152. bucketFaces[ bucketCursors[ vertexIds[ f3 + 1 ] ] ++ ] = f;
  153. bucketFaces[ bucketCursors[ vertexIds[ f3 + 2 ] ] ++ ] = f;
  154. }
  155. // average the normals of the faces surrounding each vertex if they are within the
  156. // provided crease threshold
  157. const normalArray = new Float32Array( vertexCount * 3 );
  158. for ( let f = 0; f < faceCount; f ++ ) {
  159. const f3 = 3 * f;
  160. const nx = faceNormals[ f3 + 0 ];
  161. const ny = faceNormals[ f3 + 1 ];
  162. const nz = faceNormals[ f3 + 2 ];
  163. for ( let n = 0; n < 3; n ++ ) {
  164. const i = f3 + n;
  165. const id = vertexIds[ i ];
  166. let sumX = 0, sumY = 0, sumZ = 0;
  167. for ( let k = bucketOffsets[ id ], end = bucketOffsets[ id + 1 ]; k < end; k ++ ) {
  168. const o3 = 3 * bucketFaces[ k ];
  169. const ox = faceNormals[ o3 + 0 ];
  170. const oy = faceNormals[ o3 + 1 ];
  171. const oz = faceNormals[ o3 + 2 ];
  172. if ( nx * ox + ny * oy + nz * oz > creaseDot ) {
  173. sumX += ox;
  174. sumY += oy;
  175. sumZ += oz;
  176. }
  177. }
  178. const invLength = 1 / ( Math.sqrt( sumX * sumX + sumY * sumY + sumZ * sumZ ) || 1 );
  179. normalArray[ 3 * i + 0 ] = sumX * invLength;
  180. normalArray[ 3 * i + 1 ] = sumY * invLength;
  181. normalArray[ 3 * i + 2 ] = sumZ * invLength;
  182. }
  183. }
  184. return normalArray;
  185. }
  186. export { TileCreasedNormalsPlugin };
粤ICP备19079148号