Needle Engine

Changes between version 3.46.1-beta.4 and 3.47.0-beta
Files changed (7) hide show
  1. src/engine/engine_physics_rapier.ts +28 -7
  2. src/engine/engine_physics.ts +317 -35
  3. src/engine-components/ui/EventSystem.ts +6 -4
  4. src/engine-components/ui/Raycaster.ts +3 -1
  5. src/engine-components/RigidBody.ts +7 -3
  6. src/engine-components/ui/Text.ts +1 -1
  7. src/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js +122 -0
src/engine/engine_physics_rapier.ts CHANGED
@@ -303,6 +303,11 @@
303
303
  filterGroups?: number,
304
304
  /** Return false to ignore this collider */
305
305
  filterPredicate?: (c: ICollider) => boolean,
306
+ /** When enabled the hit object's layer will be tested. If layer 2 is enabled the object will be ignored (Layer 2 == IgnoreRaycast)
307
+ * If not set the raycast will ignore objects in the IgnoreRaycast layer (default: true)
308
+ * @default undefined
309
+ */
310
+ useIgnoreRaycastLayer?: boolean
306
311
  })
307
312
  : null | { point: Vector3, collider: ICollider } {
308
313
 
@@ -320,9 +325,11 @@
320
325
  const hit = this.world?.castRay(ray, maxDistance, solid, options?.queryFilterFlags, options?.filterGroups, undefined, undefined, (c) => {
321
326
  const component = c[$componentKey];
322
327
  if (options?.filterPredicate) return options.filterPredicate(component);
323
-
324
- // ignore objects in the IgnoreRaycast=2 layer
325
- return !component?.gameObject.layers.isEnabled(2);
328
+ if (options?.useIgnoreRaycastLayer !== false) {
329
+ // ignore objects in the IgnoreRaycast=2 layer
330
+ return !component?.gameObject.layers.isEnabled(2);
331
+ }
332
+ return true;
326
333
  });
327
334
  if (hit) {
328
335
  const point = ray.pointAt(hit.toi);
@@ -342,6 +349,11 @@
342
349
  filterGroups?: number,
343
350
  /** Return false to ignore this collider */
344
351
  filterPredicate?: (c: ICollider) => boolean,
352
+ /** When enabled the hit object's layer will be tested. If layer 2 is enabled the object will be ignored (Layer 2 == IgnoreRaycast)
353
+ * If not set the raycast will ignore objects in the IgnoreRaycast layer (default: true)
354
+ * @default undefined
355
+ */
356
+ useIgnoreRaycastLayer?: boolean
345
357
  })
346
358
  : null | { point: Vector3, normal: Vector3, collider: ICollider } {
347
359
 
@@ -359,8 +371,11 @@
359
371
  const hit = this.world?.castRayAndGetNormal(ray, maxDistance, solid, options?.queryFilterFlags, options?.filterGroups, undefined, undefined, (c) => {
360
372
  const component = c[$componentKey];
361
373
  if (options?.filterPredicate) return options.filterPredicate(component);
362
- // ignore objects in the IgnoreRaycast=2 layer
363
- return !component?.gameObject.layers.isEnabled(2);
374
+ if (options?.useIgnoreRaycastLayer !== false) {
375
+ // ignore objects in the IgnoreRaycast=2 layer
376
+ return !component?.gameObject.layers.isEnabled(2);
377
+ }
378
+ return true;
364
379
  });
365
380
  if (hit) {
366
381
  const point = ray.pointAt(hit.toi);
@@ -1165,8 +1180,14 @@
1165
1180
  }
1166
1181
  }
1167
1182
  else if (body instanceof Collider) {
1168
- const wp = getWorldPosition(obj, this._tempPosition);
1169
- const wq = getWorldQuaternion(obj, this._tempQuaternion);
1183
+ if (obj.matrixWorldNeedsUpdate) {
1184
+ obj.updateWorldMatrix(true, false);
1185
+ }
1186
+ obj.matrixWorld.decompose(this._tempPosition, this._tempQuaternion, this._tempScale);
1187
+ const wp = this._tempPosition;
1188
+ const wq = this._tempQuaternion;
1189
+ // const wp = getWorldPosition(obj, this._tempPosition);
1190
+ // const wq = getWorldQuaternion(obj, this._tempQuaternion);
1170
1191
  const collider = body[$componentKey] as ICollider;
1171
1192
  this.tryApplyCenter(collider, wp);
1172
1193
 
src/engine/engine_physics.ts CHANGED
@@ -1,11 +1,14 @@
1
1
  import { getRaycastMesh } from '@needle-tools/gltf-progressive';
2
- import { ArrayCamera, AxesHelper, Box3, BufferGeometry, Camera, type Intersection, Layers, Line, Matrix3, Matrix4, Mesh, Object3D, PerspectiveCamera, Plane, Ray, Raycaster, Sphere, SphereGeometry, Vector2, Vector3 } from 'three'
2
+ import { ArrayCamera, AxesHelper, Box3, BufferGeometry, Camera, type Intersection, Layers, Line, Matrix3, Matrix4, Mesh, Object3D, PerspectiveCamera, Plane, Ray, Raycaster, SkinnedMesh,Sphere, SphereGeometry, Vector2, Vector3 } from 'three'
3
3
  import { GroundedSkybox } from 'three/examples/jsm/objects/GroundedSkybox.js';
4
+ import type { MeshBVH, MeshBVHOptions, StaticGeometryGenerator } from 'three-mesh-bvh';
5
+ import type { GenerateMeshBVHWorker } from 'three-mesh-bvh/src/workers/GenerateMeshBVHWorker.js';
4
6
 
7
+ import { isDevEnvironment } from './debug/index.js';
5
8
  import { Gizmos } from './engine_gizmos.js';
6
9
  import { Context } from './engine_setup.js';
7
10
  import { getTempVector, getWorldPosition } from "./engine_three_utils.js"
8
- import type { Vec2, Vec3, } from './engine_types.js';
11
+ import type { ConstructorConcrete, Vec2, Vec3, } from './engine_types.js';
9
12
  import type { IPhysicsEngine } from './engine_types.js';
10
13
  import { getParam } from "./engine_utils.js"
11
14
 
@@ -62,6 +65,12 @@
62
65
  * Return `false` to ignore the object completely or `"continue in children"` to skip the object but continue to traverse its children (if you do raycast with `recursive` enabled)
63
66
  * */
64
67
  testObject?: RaycastTestObjectCallback;
68
+
69
+ /**
70
+ * Use MeshBVH for raycasting. This is faster than the default threejs raycaster but uses more memory.
71
+ * @default undefined
72
+ */
73
+ useAcceleratedRaycast?: boolean;
65
74
  }
66
75
 
67
76
  export class RaycastOptions implements IRaycastOptions {
@@ -80,6 +89,7 @@
80
89
  layerMask?: Layers | number;
81
90
  ignore?: Object3D[];
82
91
  testObject?: RaycastTestObjectCallback;
92
+ useAcceleratedRaycast?: boolean | undefined;
83
93
 
84
94
  screenPointFromOffset(ox: number, oy: number) {
85
95
  if (this.screenPoint === undefined) this.screenPoint = new Vector2();
@@ -163,53 +173,26 @@
163
173
 
164
174
  private sphereResults: Array<Intersection> = new Array<Intersection>();
165
175
  private sphereMask: Layers = new Layers();
176
+ private readonly sphere: Sphere = new Sphere();
166
177
  /** Test overlapping of a sphere with the threejs geometry. This does not use colliders. This does not return an exact intersection point (intersections returned contain the object and the world position of the object that is being hit)
167
178
  * For a more accurate test use the physics engine's collider overlap test (see sphereOverlapPhysics)
168
179
  * @param spherePos the center of the sphere in world space
169
180
  * @param radius the radius of the sphere
170
181
  * @param traverseChildsAfterHit if false it will stop after the first hit. If true it will continue to traverse and add all hits to the result array
182
+ * @param bvh use MeshBVH for raycasting. This is faster than the default threejs raycaster but uses more memory.
183
+ * @param shouldRaycast optional callback to filter objects. Return `false` to ignore the object completely or `"continue in children"` to skip the object but continue to traverse its children (if you do raycast with `recursive` enabled)
171
184
  */
172
- public sphereOverlap(spherePos: Vector3, radius: number, traverseChildsAfterHit: boolean = true): Array<Intersection> {
185
+ public sphereOverlap(spherePos: Vector3, radius: number, traverseChildsAfterHit: boolean = true, bvh: boolean = false, shouldRaycast: RaycastTestObjectCallback | null = null): Array<Intersection> {
173
186
  this.sphereResults.length = 0;
174
187
  if (!this.context.scene) return this.sphereResults;
175
- const sphere = new Sphere(spherePos, radius);
176
188
  const mask = this.sphereMask;
177
189
  mask.enableAll();
178
190
  mask.disable(2);
179
191
  for (const ch of this.context.scene.children) {
180
- this.onSphereOverlap(ch, sphere, mask, this.sphereResults, traverseChildsAfterHit);
192
+ this.intersectSphere(ch, spherePos, radius, mask, this.sphereResults, traverseChildsAfterHit, bvh, shouldRaycast);
181
193
  }
182
194
  return this.sphereResults.sort((a, b) => a.distance - b.distance);
183
195
  }
184
- private tempBoundingBox: Box3 = new Box3();
185
- private onSphereOverlap(obj: Object3D, sp: Sphere, mask: Layers, results: Array<Intersection>, traverseChildsAfterHit: boolean): void {
186
- if (obj.type === "Mesh" && obj.layers.test(mask) && !Gizmos.isGizmo(obj)) {
187
- const mesh = obj as Mesh;
188
- const geo = mesh.geometry;
189
- if (geo) {
190
- if (!geo.boundingBox)
191
- geo.computeBoundingBox();
192
- if (geo.boundingBox) {
193
- if (mesh.matrixWorldNeedsUpdate) mesh.updateMatrixWorld();
194
- const test = this.tempBoundingBox.copy(geo.boundingBox).applyMatrix4(mesh.matrixWorld);
195
- if (sp.intersectsBox(test)) {
196
- const wp = getWorldPosition(obj);
197
- const dist = wp.distanceTo(sp.center);
198
- const int = new SphereIntersection(obj, dist, wp);
199
- results.push(int);
200
- if (!traverseChildsAfterHit) return;
201
- }
202
- }
203
- }
204
- }
205
- if (obj.children) {
206
- for (const ch of obj.children) {
207
- const len = results.length;
208
- this.onSphereOverlap(ch, sp, mask, results, traverseChildsAfterHit);
209
- if (len != results.length && !traverseChildsAfterHit) return;
210
- }
211
- }
212
- }
213
196
 
214
197
  public raycastFromRay(ray: Ray, options: IRaycastOptions | null = null): Array<Intersection> {
215
198
  const opts = options ?? this.defaultRaycastOptions;
@@ -301,7 +284,7 @@
301
284
  if (ignorelist !== undefined && ignorelist.length > 0) {
302
285
  results = results.filter(r => !ignorelist.includes(r.object));
303
286
  }
304
-
287
+
305
288
  Physics._raycasting--;
306
289
 
307
290
  if (debugPhysics) {
@@ -349,6 +332,9 @@
349
332
  if (!usePrecise && customRaycast(mesh, raycaster, results)) {
350
333
  // did handle raycast
351
334
  }
335
+ else if (options.useAcceleratedRaycast !== false) {
336
+ NEMeshBVH.runMeshBVHRaycast(raycaster, mesh, results, this.context);
337
+ }
352
338
  else {
353
339
  raycaster.intersectObject(obj, false, results);
354
340
  }
@@ -365,6 +351,75 @@
365
351
  }
366
352
  return results;
367
353
  }
354
+
355
+ private tempBoundingBox: Box3 = new Box3();
356
+ private intersectSphere(obj: Object3D, spherePos: Vector3, radius: number, mask: Layers, results: Array<Intersection>, traverseChildsAfterHit: boolean, useBvh: boolean, shouldRaycast: RaycastTestObjectCallback | null): void {
357
+
358
+ let canIntersect = obj && (obj as Mesh).isMesh && obj.layers.test(mask) && !Gizmos.isGizmo(obj);
359
+
360
+ canIntersect &&= obj.visible;
361
+ canIntersect &&= !(obj instanceof Line);
362
+ canIntersect &&= !(obj instanceof GroundedSkybox);
363
+
364
+ if (shouldRaycast) {
365
+ const testResult = shouldRaycast(obj);
366
+ if (testResult === false) {
367
+ return;
368
+ }
369
+ const checkObject = testResult !== "continue in children";
370
+ if (checkObject) {
371
+ return;
372
+ }
373
+ }
374
+
375
+
376
+ if (canIntersect) {
377
+ const mesh = obj as Mesh;
378
+ const geo = mesh.geometry;
379
+ if (geo) {
380
+
381
+ if (useBvh) {
382
+ const sphere = this.sphere;
383
+ sphere.center.copy(spherePos);
384
+ sphere.radius = radius;
385
+ const previousResults = results.length;
386
+ NEMeshBVH.runMeshBVHRaycast(this.sphere, mesh, results, this.context);
387
+ if (previousResults != results.length && !traverseChildsAfterHit) {
388
+ return;
389
+ }
390
+ }
391
+ // Classic sphere intersection test
392
+ else {
393
+ if (!geo.boundingBox)
394
+ geo.computeBoundingBox();
395
+
396
+ if (geo.boundingBox) {
397
+ if (mesh.matrixWorldNeedsUpdate) mesh.updateWorldMatrix(false, false);
398
+ const test = this.tempBoundingBox.copy(geo.boundingBox).applyMatrix4(mesh.matrixWorld);
399
+ const sphere = this.sphere;
400
+ sphere.center.copy(spherePos);
401
+ sphere.radius = radius;
402
+
403
+ if (sphere.intersectsBox(test)) {
404
+ const wp = getWorldPosition(obj);
405
+ const dist = wp.distanceTo(sphere.center);
406
+ const int = new SphereIntersection(obj, dist, wp);
407
+ results.push(int);
408
+ if (!traverseChildsAfterHit) return;
409
+ }
410
+ }
411
+ }
412
+
413
+ }
414
+ }
415
+ if (obj.children) {
416
+ for (const ch of obj.children) {
417
+ const len = results.length;
418
+ this.intersectSphere(ch, spherePos, radius, mask, results, traverseChildsAfterHit, useBvh, shouldRaycast);
419
+ if (len != results.length && !traverseChildsAfterHit) return;
420
+ }
421
+ }
422
+ }
368
423
  }
369
424
 
370
425
  const tempSphere = new Sphere();
@@ -426,3 +481,230 @@
426
481
  mesh["_computeIntersections"] = originalComputeIntersectionsFn;
427
482
  return true;
428
483
  }
484
+
485
+
486
+
487
+ declare module 'three' {
488
+ export interface SkinnedMesh {
489
+ staticGenerator?: StaticGeometryGenerator;
490
+ staticGeometry?: BufferGeometry;
491
+ staticGeometryLastUpdate?: number;
492
+ bvhNeedsUpdate?: boolean;
493
+ }
494
+ export interface Mesh {
495
+ acceleratedRaycast?: any;
496
+ }
497
+ }
498
+
499
+
500
+
501
+ module NEMeshBVH {
502
+ export function runMeshBVHRaycast(method: Raycaster | Sphere, mesh: Mesh, results: Intersection[], context: Pick<Context, "xr">): boolean {
503
+ if (!mesh.geometry) {
504
+ return false;
505
+ }
506
+ // The code below handles generating the mesh bvh structure that is used for raycasting
507
+ // We first try to setup workers so it can run off the main thread
508
+ const geom = mesh.geometry;
509
+
510
+ if ((mesh as SkinnedMesh)?.isSkinnedMesh) {
511
+ const skinnedMesh = mesh as SkinnedMesh;
512
+ const skinnedMeshBVHNeedsUpdate = skinnedMesh.bvhNeedsUpdate;
513
+ if (!skinnedMesh.staticGenerator) {
514
+ loadMeshBVHLibrary();
515
+ if (_StaticGeometryGenerator) {
516
+ skinnedMesh.staticGenerator = new _StaticGeometryGenerator(mesh);
517
+ skinnedMesh.staticGenerator.applyWorldTransforms = false;
518
+ skinnedMesh.staticGeometry = skinnedMesh.staticGenerator.generate();
519
+ geom.boundsTree = _computeBoundsTree?.call(skinnedMesh.staticGeometry);
520
+ skinnedMesh.staticGeometryLastUpdate = performance.now() + Math.random() * 200;
521
+ if (skinnedMesh["autoUpdateMeshBVH"] === undefined)
522
+ skinnedMesh["autoUpdateMeshBVH"] = false;
523
+ }
524
+ }
525
+ else if (geom.boundsTree && (skinnedMesh["autoUpdateMeshBVH"] === true || skinnedMeshBVHNeedsUpdate === true)) {
526
+ // automatically refit the tree every 300ms
527
+ const now = performance.now();
528
+ const timeSinceLastUpdate = now - skinnedMesh.staticGeometryLastUpdate!;
529
+ if (skinnedMeshBVHNeedsUpdate || timeSinceLastUpdate > 100) {
530
+ skinnedMesh.bvhNeedsUpdate = false;
531
+ skinnedMesh.staticGeometryLastUpdate = now;
532
+ skinnedMesh.staticGenerator?.generate(skinnedMesh.staticGeometry);
533
+ geom.boundsTree.refit();
534
+ }
535
+ }
536
+ }
537
+
538
+ else if (!geom.boundsTree) {
539
+
540
+ // Try to generate the bvh on a worker
541
+ if (!didSetupWorker) internalSetupWorker();
542
+
543
+ let canUseWorker = true;
544
+
545
+ if (context.xr) { // < in XR for some reason sometimes geometry (controllers) are broken - maybe this is not exclusive to controller geometry
546
+ canUseWorker = false;
547
+ }
548
+ else if (geom[canUseWorkerSymbol] === false) {
549
+ canUseWorker = false;
550
+ }
551
+ else if (geom.getAttribute('position')["isInterleavedBufferAttribute"]
552
+ || geom.index && geom.index["isInterleavedBufferAttribute"]) {
553
+ canUseWorker = false;
554
+ }
555
+
556
+ // if we have a worker use that
557
+ if (canUseWorker && _GenerateMeshBVHWorker) {
558
+ if (geom[workerTaskSymbol] === undefined) {
559
+
560
+ // get available worker
561
+ let workerInstance: GenerateMeshBVHWorker | null = null;
562
+ // if there are workers available, use one
563
+ if (availableWorkers.length > 0) {
564
+ const available = availableWorkers.shift();
565
+ if (available && !available.running) {
566
+ workerInstance = available;
567
+ }
568
+ }
569
+ // if there are no workers available, create a new one
570
+ else if (workerInstances.length < 1) {
571
+ workerInstance = new _GenerateMeshBVHWorker();
572
+ workerInstances.push(workerInstance);
573
+ }
574
+
575
+ if (workerInstance != null && !workerInstance.running) {
576
+ geom[workerTaskSymbol] = "queued";
577
+ performance.mark("bvh.create.start");
578
+ workerInstance.generate(geom)
579
+ .then(bvh => {
580
+ geom[workerTaskSymbol] = "done";
581
+ geom.boundsTree = bvh;
582
+ })
583
+ .catch(err => {
584
+ geom[workerTaskSymbol] = "failed - " + err?.message;
585
+ geom[canUseWorkerSymbol] = false;
586
+ })
587
+ .finally(() => {
588
+ availableWorkers.push(workerInstance);
589
+ performance.mark("bvh.create.end");
590
+ performance.measure("bvh.create (worker)", "bvh.create.start", "bvh.create.end");
591
+ });
592
+ }
593
+ else {
594
+ // we don't want to generate the BVH on the main thread unless workers are not supported
595
+ // If all workers are currently running we need to run a "slow" raycast
596
+ if (debugPhysics) console.warn("No worker available");
597
+ }
598
+ }
599
+ }
600
+ // Fallback to sync bvh generation if workers are not available
601
+ else if (!isRequestingWorker || !canUseWorker) {
602
+ loadMeshBVHLibrary();
603
+ if (_MeshBVH) {
604
+ performance.mark("bvh.create.start");
605
+ geom.boundsTree = new _MeshBVH(geom as BufferGeometry);
606
+ performance.mark("bvh.create.end");
607
+ performance.measure("bvh.create", "bvh.create.start", "bvh.create.end");
608
+ }
609
+ }
610
+ }
611
+
612
+ if (method instanceof Raycaster) {
613
+ const raycaster = method;
614
+ // Skinned meshes work when we disable applyWorldTransform in the generator (see applyWorldTransforms = false above)
615
+ // We do need to set the accelerated raycast method (bind it once)
616
+ // We could also override it on the prototype - not sure if it's more performant but then it would always run
617
+ const raycastMesh = mesh.raycast;
618
+ if (_acceleratedRaycast && geom.boundsTree) {
619
+ // bind the raycast to the mesh
620
+ if (!mesh.acceleratedRaycast) {
621
+ mesh.acceleratedRaycast = _acceleratedRaycast.bind(mesh) as any;
622
+ }
623
+ mesh.raycast = mesh.acceleratedRaycast;
624
+ }
625
+ const prevFirstHitOnly = raycaster.firstHitOnly;
626
+ raycaster.firstHitOnly = false;
627
+ raycaster.intersectObject(mesh, false, results);
628
+ raycaster.firstHitOnly = prevFirstHitOnly;
629
+ mesh.raycast = raycastMesh;
630
+ return true;
631
+ }
632
+ else if (method instanceof Sphere) {
633
+ const bvh = geom.boundsTree;
634
+ if (bvh) {
635
+ const sphere = method;
636
+ // Gizmos.DrawWireSphere(sphere.center, sphere.radius, 0xdddd00, 1, false);
637
+
638
+ invMat.copy(mesh.matrixWorld).invert();
639
+ sphere.applyMatrix4(invMat);
640
+ const intersects = bvh.intersectsSphere(sphere);
641
+ if (intersects) {
642
+ // console.log(intersects, mesh.name);
643
+ const worldpos = getWorldPosition(mesh);
644
+ const distance = worldpos.distanceTo(sphere.center);
645
+ const intersection = new SphereIntersection(mesh, distance, worldpos);
646
+ results.push(intersection);
647
+ }
648
+ }
649
+ return true;
650
+ }
651
+
652
+
653
+ return false;
654
+ }
655
+
656
+ let didLoadMeshBVHLibrary: boolean = false;
657
+ let _acceleratedRaycast: Function | null = null;
658
+ let _MeshBVH: ConstructorConcrete<MeshBVH> | null = null;
659
+ let _StaticGeometryGenerator: ConstructorConcrete<StaticGeometryGenerator> | null = null;
660
+ let _computeBoundsTree: ((_opt?: MeshBVHOptions) => MeshBVH) | null = null;
661
+
662
+ function loadMeshBVHLibrary() {
663
+ if (didLoadMeshBVHLibrary) return;
664
+ didLoadMeshBVHLibrary = true;
665
+ import("three-mesh-bvh").then(res => {
666
+ _acceleratedRaycast = res.acceleratedRaycast;
667
+ _MeshBVH = res.MeshBVH;
668
+ _StaticGeometryGenerator = res.StaticGeometryGenerator;
669
+ _computeBoundsTree = res.computeBoundsTree;
670
+ }).catch(_err => {
671
+ if (debugPhysics || isDevEnvironment()) {
672
+ console.warn("Failed to load mesh bvh library");
673
+ }
674
+ })
675
+ }
676
+
677
+
678
+ const invMat = new Matrix4();
679
+
680
+ /** True after the worker has been requested for the first time */
681
+ let didSetupWorker: boolean = false;
682
+ /** True while the worker is being requested */
683
+ let isRequestingWorker: boolean = false;
684
+ let _GenerateMeshBVHWorker: ConstructorConcrete<GenerateMeshBVHWorker> | null = null;
685
+
686
+ const workerTaskSymbol = Symbol("Needle:MeshBVH-Worker");
687
+ const canUseWorkerSymbol = Symbol("Needle:MeshBVH-CanUseWorker");
688
+ const workerInstances: GenerateMeshBVHWorker[] = [];
689
+ const availableWorkers: GenerateMeshBVHWorker[] = [];
690
+
691
+ function internalSetupWorker() {
692
+ didSetupWorker = true;
693
+ isRequestingWorker = true;
694
+ // Using local worker. see https://github.com/gkjohnson/three-mesh-bvh/issues/636#issuecomment-2209571751
695
+ import("./physics/workers/mesh-bvh/GenerateMeshBVHWorker.js")
696
+ .then(res => {
697
+ _GenerateMeshBVHWorker = res.GenerateMeshBVHWorker;
698
+ })
699
+ .catch(_err => {
700
+ if (debugPhysics || isDevEnvironment()) {
701
+ console.warn("Failed to setup mesh bvh worker");
702
+ }
703
+ })
704
+ .finally(() => {
705
+ isRequestingWorker = false;
706
+ });
707
+ }
708
+
709
+
710
+ }
src/engine-components/ui/EventSystem.ts CHANGED
@@ -806,19 +806,21 @@
806
806
  if (currentFrame === lu.frame) return;
807
807
  lu.frame = currentFrame;
808
808
  let shouldUpdate = this.needsUpdate || currentFrame < 1;
809
- if (lu.nextUpdate === context.time.frameCount) shouldUpdate = true;
809
+ if (lu.nextUpdate <= currentFrame) shouldUpdate = true;
810
810
  // if (this.needsUpdate) lu.nextUpdate = currentFrame + 3;
811
811
  if (shouldUpdate) {
812
- if (debug)
813
- console.log("Update threemeshui");
812
+ // console.warn(currentFrame, lu.nextUpdate, this.needsUpdate)
813
+ if (debug) console.log("Update threemeshui");
814
814
  this.needsUpdate = false;
815
+ lu.nextUpdate = currentFrame + 60;
815
816
  threeMeshUI.update();
816
817
  }
817
818
  return;
818
819
  }
819
820
  }
820
- this.lastUpdateFrame = [{ context, frame: currentFrame, nextUpdate: currentFrame }];
821
+ this.lastUpdateFrame = [{ context, frame: currentFrame, nextUpdate: currentFrame + 60 }];
821
822
  threeMeshUI.update();
823
+ this.needsUpdate = false;
822
824
  }
823
825
 
824
826
  static updateState(intersect: Object3D, _selectState: boolean): Object3D | null {
src/engine-components/ui/Raycaster.ts CHANGED
@@ -44,6 +44,8 @@
44
44
  opts ??= new RaycastOptions();
45
45
  opts.targets = this.targets;
46
46
  opts.results = this.raycastHits;
47
+ opts.useAcceleratedRaycast = true;
48
+
47
49
  const orig = opts.testObject;
48
50
  if (this.ignoreSkinnedMeshes) {
49
51
  opts.testObject = obj => {
@@ -89,6 +91,6 @@
89
91
  // draw gizmo around ray origin
90
92
  // Gizmos.DrawSphere(rayOrigin, radius, 0x00ff0022);
91
93
 
92
- return this.context.physics.sphereOverlap(rayOrigin, radius);
94
+ return this.context.physics.sphereOverlap(rayOrigin, radius, false, true);
93
95
  }
94
96
  }
src/engine-components/RigidBody.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Matrix4, Object3D, Vector3 } from "three";
1
+ import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
2
2
 
3
3
  import { CollisionDetectionMode, RigidbodyConstraints } from "../engine/engine_physics.types.js";
4
4
  import { serializable } from "../engine/engine_serialization_decorator.js";
@@ -485,9 +485,13 @@
485
485
 
486
486
  private captureVelocity() {
487
487
  const wp = getWorldPosition(this.gameObject);
488
- Rigidbody.tempPosition.copy(wp);
488
+ this.gameObject.matrixWorld.decompose(Rigidbody.tempPosition, tempQuaternion, tempScale);
489
489
  const vel = wp.sub(this._lastPosition);
490
490
  this._lastPosition.copy(Rigidbody.tempPosition);
491
491
  this._smoothedVelocity.lerp(vel, this.context.time.deltaTime / .1);
492
492
  }
493
- }
493
+ }
494
+
495
+ const tempPosition = new Vector3();
496
+ const tempQuaternion = new Quaternion();
497
+ const tempScale = new Vector3();
src/engine-components/ui/Text.ts CHANGED
@@ -121,7 +121,7 @@
121
121
  const overflow = (this.uiObject as any)?._overflow;
122
122
  if (overflow) {
123
123
  overflow._needsUpdate = true;
124
- this.markDirty();
124
+ // the screenspace canvas does force an update, no need to mark dirty here
125
125
  }
126
126
  }
127
127
 
src/engine/physics/workers/mesh-bvh/GenerateMeshBVHWorker.js ADDED
@@ -0,0 +1,122 @@
1
+ import { Box3, BufferAttribute } from 'three';
2
+ import { MeshBVH } from 'three-mesh-bvh';
3
+
4
+ // Modified according to https://github.com/gkjohnson/three-mesh-bvh/issues/636#issuecomment-2209571751
5
+ import { WorkerBase } from "three-mesh-bvh/src/workers/utils/WorkerBase.js";
6
+ import generateMeshBVHWorker from "three-mesh-bvh/src/workers/generateMeshBVH.worker.js?worker";
7
+
8
+ export class GenerateMeshBVHWorker extends WorkerBase {
9
+
10
+ constructor() {
11
+
12
+ super(new generateMeshBVHWorker());
13
+ this.name = 'GenerateMeshBVHWorker';
14
+
15
+ }
16
+
17
+ runTask( worker, geometry, options = {} ) {
18
+
19
+ return new Promise( ( resolve, reject ) => {
20
+
21
+ if (
22
+ geometry.getAttribute( 'position' ).isInterleavedBufferAttribute ||
23
+ geometry.index && geometry.index.isInterleavedBufferAttribute
24
+ ) {
25
+
26
+ throw new Error( 'GenerateMeshBVHWorker: InterleavedBufferAttribute are not supported for the geometry attributes.' );
27
+
28
+ }
29
+
30
+ worker.onerror = e => {
31
+
32
+ reject( new Error( `GenerateMeshBVHWorker: ${ e.message }` ) );
33
+
34
+ };
35
+
36
+ worker.onmessage = e => {
37
+
38
+ const { data } = e;
39
+
40
+ if ( data.error ) {
41
+
42
+ reject( new Error( data.error ) );
43
+ worker.onmessage = null;
44
+
45
+ } else if ( data.serialized ) {
46
+
47
+ const { serialized, position } = data;
48
+ const bvh = MeshBVH.deserialize( serialized, geometry, { setIndex: false } );
49
+ const boundsOptions = Object.assign( {
50
+
51
+ setBoundingBox: true,
52
+
53
+ }, options );
54
+
55
+ // we need to replace the arrays because they're neutered entirely by the
56
+ // webworker transfer.
57
+ geometry.attributes.position.array = position;
58
+ if ( serialized.index ) {
59
+
60
+ if ( geometry.index ) {
61
+
62
+ geometry.index.array = serialized.index;
63
+
64
+ } else {
65
+
66
+ const newIndex = new BufferAttribute( serialized.index, 1, false );
67
+ geometry.setIndex( newIndex );
68
+
69
+ }
70
+
71
+ }
72
+
73
+ if ( boundsOptions.setBoundingBox ) {
74
+
75
+ geometry.boundingBox = bvh.getBoundingBox( new Box3() );
76
+
77
+ }
78
+
79
+ if ( options.onProgress ) {
80
+
81
+ options.onProgress( data.progress );
82
+
83
+ }
84
+
85
+ resolve( bvh );
86
+ worker.onmessage = null;
87
+
88
+ } else if ( options.onProgress ) {
89
+
90
+ options.onProgress( data.progress );
91
+
92
+ }
93
+
94
+ };
95
+
96
+ const index = geometry.index ? geometry.index.array : null;
97
+ const position = geometry.attributes.position.array;
98
+ const transferable = [ position ];
99
+ if ( index ) {
100
+
101
+ transferable.push( index );
102
+
103
+ }
104
+
105
+ worker.postMessage( {
106
+
107
+ index,
108
+ position,
109
+ options: {
110
+ ...options,
111
+ onProgress: null,
112
+ includedProgressCallback: Boolean( options.onProgress ),
113
+ groups: [ ... geometry.groups ],
114
+ },
115
+
116
+ }, transferable.map( arr => arr.buffer ).filter( v => ( typeof SharedArrayBuffer === 'undefined' ) || ! ( v instanceof SharedArrayBuffer ) ) );
117
+
118
+ } );
119
+
120
+ }
121
+
122
+ }