raycastTraverse.js 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import { Matrix4, Sphere, Ray, Vector3 } from 'three';
  2. const _sphere = new Sphere();
  3. const _mat = new Matrix4();
  4. const _vec = new Vector3();
  5. const _vec2 = new Vector3();
  6. const _ray = new Ray();
  7. const _hitArray = [];
  8. function distanceSort( a, b ) {
  9. return a.distance - b.distance;
  10. }
  11. function intersectTileScene( scene, raycaster, intersects ) {
  12. // Don't intersect the box3 helpers because those are used for debugging
  13. scene.traverse( c => {
  14. // We set the default raycast function to empty so three.js doesn't automatically cast against it
  15. Object.getPrototypeOf( c ).raycast.call( c, raycaster, intersects );
  16. } );
  17. }
  18. // Returns the closest hit when traversing the tree
  19. export function raycastTraverseFirstHit( root, group, activeTiles, raycaster ) {
  20. // If the root is active make sure we've checked it
  21. if ( activeTiles.has( root ) ) {
  22. intersectTileScene( root.cached.scene, raycaster, _hitArray );
  23. if ( _hitArray.length > 0 ) {
  24. if ( _hitArray.length > 1 ) {
  25. _hitArray.sort( distanceSort );
  26. }
  27. const res = _hitArray[ 0 ];
  28. _hitArray.length = 0;
  29. return res;
  30. } else {
  31. return null;
  32. }
  33. }
  34. // TODO: can we avoid creating a new array here every time to save on memory?
  35. const array = [];
  36. const children = root.children;
  37. for ( let i = 0, l = children.length; i < l; i ++ ) {
  38. const tile = children[ i ];
  39. const cached = tile.cached;
  40. const groupMatrixWorld = group.matrixWorld;
  41. _mat.copy( groupMatrixWorld );
  42. // if we don't hit the sphere then early out
  43. const sphere = cached.sphere;
  44. if ( sphere ) {
  45. _sphere.copy( sphere );
  46. _sphere.applyMatrix4( _mat );
  47. if ( ! raycaster.ray.intersectsSphere( _sphere ) ) {
  48. continue;
  49. }
  50. }
  51. // TODO: check region?
  52. const boundingBox = cached.box;
  53. const obbMat = cached.boxTransform;
  54. if ( boundingBox ) {
  55. _mat.multiply( obbMat ).invert();
  56. _ray.copy( raycaster.ray );
  57. _ray.applyMatrix4( _mat );
  58. if ( _ray.intersectBox( boundingBox, _vec ) ) {
  59. // account for tile scale
  60. _vec2.setFromMatrixScale( _mat );
  61. const invScale = _vec2.x;
  62. if ( Math.abs( Math.max( _vec2.x - _vec2.y, _vec2.x - _vec2.z ) ) > 1e-6 ) {
  63. console.warn( 'ThreeTilesRenderer : Non uniform scale used for tile which may cause issues when raycasting.' );
  64. }
  65. // if we intersect the box save the distance to the tile bounds
  66. const data = {
  67. distance: Infinity,
  68. tile: null
  69. };
  70. array.push( data );
  71. data.distance = _vec.distanceToSquared( _ray.origin ) * invScale * invScale;
  72. data.tile = tile;
  73. } else {
  74. continue;
  75. }
  76. }
  77. }
  78. // sort them by ascending distance
  79. array.sort( distanceSort );
  80. // traverse until we find the best hit and early out if a tile bounds
  81. // couldn't possible include a best hit
  82. let bestDistanceSquared = Infinity;
  83. let bestHit = null;
  84. for ( let i = 0, l = array.length; i < l; i ++ ) {
  85. const data = array[ i ];
  86. const distanceSquared = data.distance;
  87. if ( distanceSquared > bestDistanceSquared ) {
  88. break;
  89. } else {
  90. const tile = data.tile;
  91. const scene = tile.cached.scene;
  92. let hit = null;
  93. if ( activeTiles.has( tile ) ) {
  94. // save the hit if it's closer
  95. intersectTileScene( scene, raycaster, _hitArray );
  96. if ( _hitArray.length > 0 ) {
  97. if ( _hitArray.length > 1 ) {
  98. _hitArray.sort( distanceSort );
  99. }
  100. hit = _hitArray[ 0 ];
  101. }
  102. } else {
  103. hit = raycastTraverseFirstHit( tile, group, activeTiles, raycaster );
  104. }
  105. if ( hit ) {
  106. const hitDistanceSquared = hit.distance * hit.distance;
  107. if ( hitDistanceSquared < bestDistanceSquared ) {
  108. bestDistanceSquared = hitDistanceSquared;
  109. bestHit = hit;
  110. }
  111. _hitArray.length = 0;
  112. }
  113. }
  114. }
  115. return bestHit;
  116. }
  117. export function raycastTraverse( tile, group, activeTiles, raycaster, intersects ) {
  118. const cached = tile.cached;
  119. const groupMatrixWorld = group.matrixWorld;
  120. _mat.copy( groupMatrixWorld );
  121. // Early out if we don't hit this tile sphere
  122. const sphere = cached.sphere;
  123. if ( sphere ) {
  124. _sphere.copy( sphere );
  125. _sphere.applyMatrix4( _mat );
  126. if ( ! raycaster.ray.intersectsSphere( _sphere ) ) {
  127. return;
  128. }
  129. }
  130. // Early out if we don't this this tile box
  131. const boundingBox = cached.box;
  132. const obbMat = cached.boxTransform;
  133. if ( boundingBox ) {
  134. _mat.multiply( obbMat ).invert();
  135. _ray.copy( raycaster.ray ).applyMatrix4( _mat );
  136. if ( ! _ray.intersectsBox( boundingBox ) ) {
  137. return;
  138. }
  139. }
  140. // TODO: check region
  141. const scene = cached.scene;
  142. if ( activeTiles.has( tile ) ) {
  143. intersectTileScene( scene, raycaster, intersects );
  144. return;
  145. }
  146. const children = tile.children;
  147. for ( let i = 0, l = children.length; i < l; i ++ ) {
  148. raycastTraverse( children[ i ], group, activeTiles, raycaster, intersects );
  149. }
  150. }