Needle Engine

Changes between version 3.25.5 and 3.26.0-beta
Files changed (9) hide show
  1. src/engine/engine_physics.ts +5 -3
  2. src/engine/engine_three_utils.ts +4 -2
  3. src/engine/engine_utils.ts +4 -2
  4. src/engine-components/ui/EventSystem.ts +56 -32
  5. src/engine-components/LODGroup.ts +2 -1
  6. src/engine-components/OrbitControls.ts +1 -0
  7. src/engine-components/ui/PointerEvents.ts +12 -10
  8. src/engine-components/ui/Raycaster.ts +16 -7
  9. src/engine-components/Renderer.ts +13 -0
src/engine/engine_physics.ts CHANGED
@@ -8,7 +8,8 @@
8
8
  const debugPhysics = getParam("debugphysics");
9
9
  const layerMaskHelper: Layers = new Layers();
10
10
 
11
- declare type IgnoreCallback = (obj: Object3D) => void | boolean | "continue in children";
11
+ export declare type RaycastTestObjectReturnType = void | boolean | "continue in children";
12
+ export declare type RaycastTestObjectCallback = (obj: Object3D) => RaycastTestObjectReturnType;
12
13
 
13
14
  export class RaycastOptions {
14
15
  ray: Ray | undefined = undefined;
@@ -27,7 +28,7 @@
27
28
  /** if defined it's called per object before tested for intersections.
28
29
  * 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)
29
30
  * */
30
- testObject?: IgnoreCallback = undefined;
31
+ testObject?: RaycastTestObjectCallback = undefined;
31
32
 
32
33
  screenPointFromOffset(ox: number, oy: number) {
33
34
  if (this.screenPoint === undefined) this.screenPoint = new Vector2();
@@ -234,8 +235,9 @@
234
235
  const testResult = options.testObject?.(obj);
235
236
  if (testResult === false) continue;
236
237
  const checkObject = testResult !== "continue in children";
237
- if (checkObject)
238
+ if (checkObject) {
238
239
  raycaster.intersectObject(obj, false, results);
240
+ }
239
241
 
240
242
  if (options.recursive) {
241
243
  this.intersect(raycaster, obj.children, results, options);
src/engine/engine_three_utils.ts CHANGED
@@ -47,8 +47,10 @@
47
47
 
48
48
 
49
49
  const _tempVecs = new CircularBuffer(() => new Vector3(), 100);
50
- export function getTempVector() {
51
- return _tempVecs.get();
50
+ export function getTempVector(value?: Vector3) {
51
+ const vec = _tempVecs.get();
52
+ if(value instanceof Vector3) vec.copy(value);
53
+ return vec;
52
54
  }
53
55
 
54
56
 
src/engine/engine_utils.ts CHANGED
@@ -51,14 +51,16 @@
51
51
  return new URLSearchParams(globalThis.location?.search);
52
52
  }
53
53
 
54
+ // bit strange that we have to pass T in here as well but otherwise the type parameter is stripped it seems
55
+ type Param<T extends string> = string | boolean | number | T;
56
+
54
57
  /** Checks if a url parameter exists.
55
58
  * Returns true if it exists but has no value (e.g. ?help)
56
59
  * Returns false if it does not exist
57
60
  * Returns false if it's set to 0 e.g. ?debug=0
58
61
  * Returns the value if it exists e.g. ?message=hello
59
62
  */
60
- export function getParam(paramName: string): string | boolean | number {
61
-
63
+ export function getParam<T extends string>(paramName: T): Param<T> {
62
64
  if (saveParams && !requestedParams.includes(paramName))
63
65
  requestedParams.push(paramName);
64
66
  const urlParams = getUrlParams();
src/engine-components/ui/EventSystem.ts CHANGED
@@ -1,10 +1,10 @@
1
- import { RaycastOptions } from "../../engine/engine_physics.js";
1
+ import { RaycastOptions, RaycastTestObjectReturnType } from "../../engine/engine_physics.js";
2
2
  import { Behaviour, Component, GameObject } from "../Component.js";
3
3
  import { WebXR } from "../webxr/WebXR.js";
4
4
  import { ControllerEvents, WebXRController } from "../webxr/WebXRController.js";
5
5
  import * as ThreeMeshUI from 'three-mesh-ui'
6
6
  import { Context } from "../../engine/engine_setup.js";
7
- import { type IPointerEventHandler, PointerEventData } from "./PointerEvents.js";
7
+ import { type IPointerEventHandler, PointerEventData, hasPointerEventComponent } from "./PointerEvents.js";
8
8
  import { ObjectRaycaster, Raycaster } from "./Raycaster.js";
9
9
  import { InputEvents, NEPointerEvent, PointerType } from "../../engine/engine_input.js";
10
10
  import { Object3D } from "three";
@@ -98,19 +98,6 @@
98
98
  console.warn("Added an ObjectRaycaster to the scene because no raycaster was found. Skinnedmeshes will be ignored for better performance");
99
99
  }
100
100
  }
101
-
102
- if (isDevEnvironment()) {
103
- const foundProblematicObjects: string[] = [];
104
- for (const rc of this.raycaster) {
105
- if (rc instanceof ObjectRaycaster) {
106
- if (rc.ignoreSkinnedMeshes === false) {
107
- foundProblematicObjects.push(rc.gameObject.name);
108
- }
109
- }
110
- }
111
- if (foundProblematicObjects.length > 0)
112
- console.warn("Found ObjectRaycaster that doesn't ignore skinned meshes. This might cause performance issues. Consider enabling \"ignoreSkinnedMeshes\" on the ObjectRaycaster components in your scene", foundProblematicObjects);
113
- }
114
101
  }
115
102
 
116
103
  register(rc: Raycaster) {
@@ -180,7 +167,7 @@
180
167
  const controllerRcOpts = new RaycastOptions();
181
168
  this._selectUpdateFn ??= (_ctrl: WebXRController) => {
182
169
  controllerRcOpts.ray = _ctrl.getRay();
183
- const rc = this.performRaycast(controllerRcOpts, _ctrl.selectionClick);
170
+ const rc = this.performRaycast(controllerRcOpts);
184
171
  if (!rc) return;
185
172
  const opts = new PointerEventData(this.context.input);
186
173
  opts.inputSource = _ctrl;
@@ -252,7 +239,7 @@
252
239
  const options = new RaycastOptions();
253
240
  options.screenPoint = this.context.input.getPointerPositionRC(pointerId)!;
254
241
 
255
- const hits = this.performRaycast(options, data.isClicked);
242
+ const hits = this.performRaycast(options);
256
243
  if (!hits) return;
257
244
 
258
245
  if (debug && data.isClicked) {
@@ -291,29 +278,66 @@
291
278
 
292
279
  private readonly _sortedHits: THREE.Intersection[] = [];
293
280
 
294
- private performRaycast(opts: RaycastOptions | null, isClick: boolean): THREE.Intersection[] | null {
281
+ /**
282
+ * cache for objects that we want to raycast against. It's cleared before each call to performRaycast invoking raycasters
283
+ */
284
+ private readonly _testObjectsCache = new Map<Object3D, boolean>();
285
+ /**
286
+ * Checks if an object that we encounter has an event component and if it does, we add it to our objects cache
287
+ * If it doesnt we tell our raycasting system to ignore it and continue in the child hierarchy
288
+ * We do this to avoid raycasts against objects that are not going to be used by the event system
289
+ * Because there's no component callback to be invoked anyways.
290
+ * This is especially important to avoid expensive raycasts against SkinnedMeshes
291
+ *
292
+ * Further optimizations would be to check what type of event we're dealing with
293
+ * For example if an event component has only an onPointerClick method we don't need to raycast during movement events
294
+ * */
295
+ private shouldRaycastObject = (obj: Object3D): RaycastTestObjectReturnType => {
296
+ // check if the object was seen previously
297
+ if (this._testObjectsCache.has(obj)) {
298
+ // if yes we check if it was previously stored as "YES WE NEED TO RAYCAST THIS"
299
+ const prev = this._testObjectsCache.get(obj)!;
300
+ if (!prev) return "continue in children"
301
+ return true;
302
+ }
303
+ else {
304
+ // the object was not yet seen so we test if it has an event component
305
+ const hasEventComponent = hasPointerEventComponent(obj);
306
+ if (hasEventComponent) {
307
+ // console.log("YES RAYCAST", obj.name)
308
+ // it has an event component: we add it and all its children to the cache
309
+ this._testObjectsCache.set(obj, true);
310
+ obj.traverse((o) => {
311
+ this._testObjectsCache.set(o, true);
312
+ })
313
+ return true;
314
+ }
315
+ this._testObjectsCache.set(obj, false);
316
+ return "continue in children"
317
+ }
318
+ }
319
+
320
+ /** the raycast filter is always overriden */
321
+ private performRaycast(opts: RaycastOptions | null): THREE.Intersection[] | null {
295
322
  if (!this.raycaster) return null;
323
+
296
324
  this._sortedHits.length = 0;
325
+
326
+ if (!opts) opts = new RaycastOptions();
327
+
328
+ // we clear the cache of previously seen objects
329
+ this._testObjectsCache.clear();
330
+ opts.testObject = this.shouldRaycastObject;
331
+
297
332
  for (const rc of this.raycaster) {
298
333
  if (!rc.activeAndEnabled) continue;
299
334
 
300
- // TODO: it would be better to filter out the objects that actually have components with callback methods either themselves or in their parents(?)
301
- let didIgnoreSkinnedMeshes: boolean | undefined = undefined;;
302
- if (rc instanceof ObjectRaycaster) {
303
- didIgnoreSkinnedMeshes = rc.ignoreSkinnedMeshes;
304
- if (!isClick) rc.ignoreSkinnedMeshes = true;
305
- }
306
335
  const res = rc.performRaycast(opts);
307
336
 
308
- if (rc instanceof ObjectRaycaster) {
309
- if (didIgnoreSkinnedMeshes !== undefined) {
310
- rc.ignoreSkinnedMeshes = didIgnoreSkinnedMeshes;
311
- }
337
+ if (res && res.length > 0) {
338
+ // console.log(res.length, res.map(r => r.object.name));
339
+ this._sortedHits.push(...res);
312
340
  }
313
-
314
-
315
- if (res && res.length > 0)
316
- this._sortedHits.push(...res);
317
341
  }
318
342
  this._sortedHits.sort((a, b) => {
319
343
  return a.distance - b.distance;
src/engine-components/LODGroup.ts CHANGED
@@ -105,13 +105,14 @@
105
105
  }
106
106
  if (debug)
107
107
  console.log("LEVEL", object.name, dist);
108
+ handler.autoUpdate = false;
108
109
  this.onAddLodLevel(handler, object, lod.model.distance);
109
110
  }
110
111
  }
111
112
  }
112
113
  }
113
114
 
114
- update() {
115
+ onAfterRender() {
115
116
  if (!this.gameObject) return;
116
117
  if (!this._lodsHandler) return;
117
118
  const cam = this.context.mainCamera;
src/engine-components/OrbitControls.ts CHANGED
@@ -530,6 +530,7 @@
530
530
  // we dont want to check invisible objects
531
531
  if (!obj.visible) return;
532
532
  if (useForAutoFit(obj) === false) return;
533
+ if(obj.type === "TransformControlsGizmo" || obj.type === "TransformControlsPlane") return;
533
534
  // ignore Box3Helpers
534
535
  if (obj instanceof Box3Helper) allowExpanding = false;
535
536
  if (obj instanceof GridHelper) allowExpanding = false;
src/engine-components/ui/PointerEvents.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { GameObject } from "../Component.js";
1
2
  import { Input, NEPointerEvent } from "../../engine/engine_input.js";
2
3
  import { Object3D } from "three";
3
4
 
@@ -91,13 +92,14 @@
91
92
  * @internal tests if the object has any PointerEventComponent used by the EventSystem
92
93
  * This is used to skip raycasting on objects that have no components that use pointer events
93
94
  */
94
- // export function hasPointerEventComponent(obj: Object3D) {
95
- // const res = GameObject.foreachComponent(obj, comp => {
96
- // const handler = comp as IPointerEventHandler;
97
- // if (handler.onPointerDown || handler.onPointerUp || handler.onPointerEnter || handler.onPointerExit || handler.onPointerClick)
98
- // return true;
99
- // return undefined;
100
- // });
101
- // if (res === true) return true;
102
- // return false;
103
- // }
95
+ export function hasPointerEventComponent(obj: Object3D) {
96
+ const res = GameObject.foreachComponent(obj, comp => {
97
+ const handler = comp as IPointerEventHandler;
98
+ if (handler.onPointerDown || handler.onPointerUp || handler.onPointerEnter || handler.onPointerExit || handler.onPointerClick)
99
+ return true;
100
+ // undefined means continue
101
+ return undefined;
102
+ }, false);
103
+ if (res === true) return true;
104
+ return false;
105
+ }
src/engine-components/ui/Raycaster.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { serializable } from "../../engine/engine_serialization.js";
1
2
  import { RaycastOptions } from "../../engine/engine_physics.js";
2
3
  import { Behaviour, Component } from "../Component.js";
3
4
  import { EventSystem } from "./EventSystem.js";
@@ -27,6 +28,7 @@
27
28
  private targets: THREE.Object3D[] | null = null;
28
29
  private raycastHits: THREE.Intersection[] = [];
29
30
 
31
+ @serializable()
30
32
  ignoreSkinnedMeshes = false;
31
33
 
32
34
  start(): void {
@@ -38,14 +40,21 @@
38
40
  opts ??= new RaycastOptions();
39
41
  opts.targets = this.targets;
40
42
  opts.results = this.raycastHits;
41
- opts.testObject = obj => {
42
- if (this.ignoreSkinnedMeshes && obj instanceof SkinnedMesh) {
43
- return "continue in children";
44
- }
45
- return true;
46
- };
43
+ const orig = opts.testObject;
44
+ if (this.ignoreSkinnedMeshes) {
45
+ opts.testObject = obj => {
46
+ // if we are set to ignore skinned meshes, we return false for them
47
+ if (obj instanceof SkinnedMesh) {
48
+ return "continue in children";
49
+ }
50
+ // call the original testObject function
51
+ if (orig) return orig(obj);
52
+ // otherwise allow raycasting
53
+ return true;
54
+ };
55
+ }
47
56
  const hits = this.context.physics.raycast(opts);
48
- // console.log(this.context.alias, hits);
57
+ opts.testObject = orig;
49
58
  return hits;
50
59
  }
51
60
  }
src/engine-components/Renderer.ts CHANGED
@@ -14,11 +14,14 @@
14
14
  import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
15
15
  import { isLocalNetwork } from "../engine/engine_networking_utils.js";
16
16
  import { showBalloonWarning } from "../engine/debug/index.js";
17
+ import { Gizmos } from "../engine/engine_gizmos.js";
18
+ import { getTempVector } from "../engine/engine_three_utils.js";
17
19
 
18
20
  // for staying compatible with old code
19
21
  export { InstancingUtil } from "../engine/engine_instancing.js";
20
22
 
21
23
  const debugRenderer = getParam("debugrenderer");
24
+ const debugskinnedmesh = getParam("debugskinnedmesh");
22
25
  const suppressInstancing = getParam("noInstancing");
23
26
  const debugInstancing = getParam("debuginstancing");
24
27
  const debugProgressiveLoading = getParam("debugprogressive");
@@ -757,7 +760,17 @@
757
760
  super.awake();
758
761
  // disable skinned mesh occlusion because of https://github.com/mrdoob/three.js/issues/14499
759
762
  this.allowOcclusionWhenDynamic = false;
763
+ // If we don't do that here the bounding sphere matrix used for raycasts will be wrong. Not sure *why* this is necessary
764
+ this.gameObject.parent?.updateWorldMatrix(false, true);
760
765
  }
766
+ onBeforeRender(): void {
767
+ super.onBeforeRender();
768
+
769
+ if (debugskinnedmesh && this.gameObject instanceof SkinnedMesh && this.gameObject.boundingSphere) {
770
+ const tempCenter = getTempVector(this.gameObject.boundingSphere.center).applyMatrix4(this.gameObject.matrixWorld);
771
+ Gizmos.DrawWireSphere(tempCenter, this.gameObject.boundingSphere.radius, "red");
772
+ }
773
+ }
761
774
  }
762
775
 
763
776
  export enum ShadowCastingMode {