Needle Engine

Changes between version 3.24.1 and 3.25.0
Files changed (18) hide show
  1. src/engine-components/AudioSource.ts +18 -48
  2. src/engine-components/AxesHelper.ts +2 -0
  3. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +3 -3
  4. src/engine-components/ui/Button.ts +4 -0
  5. src/engine-components/codegen/components.ts +1 -0
  6. src/engine-components/ContactShadows.ts +26 -9
  7. src/engine/engine_camera.ts +18 -3
  8. src/engine/engine_math.ts +85 -2
  9. src/engine/engine_physics_rapier.ts +1 -1
  10. src/engine/engine_physics.ts +23 -1
  11. src/engine-components/ui/EventSystem.ts +34 -5
  12. src/engine-components/OrbitControls.ts +161 -80
  13. src/engine-components/ui/PointerEvents.ts +18 -0
  14. src/engine-components/ui/Raycaster.ts +14 -0
  15. src/engine/codegen/register_types.ts +2 -0
  16. src/engine-components/webxr/WebARSessionRoot.ts +2 -0
  17. src/engine-components/webxr/WebXR.ts +4 -5
  18. src/engine-components/webxr/WebXRImageTracking.ts +77 -66
src/engine-components/AudioSource.ts CHANGED
@@ -26,63 +26,33 @@
26
26
  }
27
27
 
28
28
 
29
+ let userInteractionRegistered = false;
30
+ function onUserInteraction() {
31
+ userInteractionRegistered = true;
32
+ }
33
+ document.addEventListener('pointerdown', onUserInteraction);
34
+ document.addEventListener('click', onUserInteraction);
35
+ document.addEventListener('dragstart', onUserInteraction);
36
+ document.addEventListener('touchstart', onUserInteraction);
37
+
29
38
  export class AudioSource extends Behaviour {
30
39
 
31
- private static _didCallBeginWaitForUserInteraction: boolean = false;
32
40
  public static get userInteractionRegistered(): boolean {
33
-
34
- if (!AudioSource._didCallBeginWaitForUserInteraction) {
35
- AudioSource._didCallBeginWaitForUserInteraction = true;
36
- AudioSource._beginWaitForUserInteraction();
37
- }
38
- return AudioSource._userInteractionRegistered;
41
+ return userInteractionRegistered;
39
42
  }
40
43
 
41
44
  private static callbacks: Function[] = [];
42
45
  public static registerWaitForAllowAudio(cb: Function) {
43
46
  if (cb !== null) {
44
- if (this._userInteractionRegistered) {
47
+ if (userInteractionRegistered) {
45
48
  cb();
46
49
  return;
47
50
  }
48
51
  if (this.callbacks.indexOf(cb) === -1)
49
52
  this.callbacks.push(cb);
50
- if (!AudioSource._didCallBeginWaitForUserInteraction) {
51
- AudioSource._didCallBeginWaitForUserInteraction = true;
52
- AudioSource._beginWaitForUserInteraction();
53
- }
54
53
  }
55
54
  }
56
55
 
57
- private static _userInteractionRegistered: boolean = false;
58
- private static _beginWaitForUserInteraction(cb: Function | null = null) {
59
- if (this._userInteractionRegistered) {
60
- if (cb) cb();
61
- return;
62
- }
63
- if (cb !== null)
64
- this.registerWaitForAllowAudio(cb);
65
- const callback = () => {
66
- if (fn == undefined) return;
67
- if (AudioSource._userInteractionRegistered) return;
68
- AudioSource._userInteractionRegistered = true;
69
- if (debug) console.log("🔊 registered interaction, can play audio now");
70
- document.removeEventListener('pointerdown', fn);
71
- document.removeEventListener('click', fn);
72
- document.removeEventListener('dragstart', fn);
73
- document.removeEventListener('touchstart', fn);
74
- for (const cb of this.callbacks) {
75
- cb();
76
- }
77
- this.callbacks.length = 0;
78
- };
79
- const fn = callback.bind(this);
80
- document.addEventListener('pointerdown', fn);
81
- document.addEventListener('click', fn);
82
- document.addEventListener('dragstart', fn);
83
- document.addEventListener('touchstart', fn);
84
- }
85
-
86
56
  @serializable(URL)
87
57
  clip: string | MediaStream = "";
88
58
 
@@ -160,7 +130,7 @@
160
130
  private _audioElement: HTMLAudioElement | null = null;
161
131
 
162
132
  public get Sound(): PositionalAudio | null {
163
- if (!this.sound && AudioSource._userInteractionRegistered) {
133
+ if (!this.sound && AudioSource.userInteractionRegistered) {
164
134
  let listener = GameObject.getComponent(this.context.mainCamera, AudioListener) ?? GameObject.findObjectOfType(AudioListener, this.context);
165
135
  if (!listener && this.context.mainCamera) listener = GameObject.addNewComponent(this.context.mainCamera, AudioListener);
166
136
  if (listener?.listener) {
@@ -181,8 +151,8 @@
181
151
  }
182
152
 
183
153
  onEnable(): void {
184
- if (!AudioSource._userInteractionRegistered) {
185
- AudioSource._beginWaitForUserInteraction(() => {
154
+ if (!AudioSource.userInteractionRegistered) {
155
+ AudioSource.registerWaitForAllowAudio(() => {
186
156
  if (this.enabled && !this.destroyed && this.shouldPlay)
187
157
  this.onNewClip(this.clip);
188
158
  });
@@ -211,8 +181,8 @@
211
181
  }
212
182
  break;
213
183
  case "visible":
214
- if (debug) console.log("visible", this.enabled, this.playOnAwake, !this.isPlaying, AudioSource._userInteractionRegistered, this.wasPlaying);
215
- if (this.enabled && this.playOnAwake && !this.isPlaying && AudioSource._userInteractionRegistered && this.wasPlaying) {
184
+ if (debug) console.log("visible", this.enabled, this.playOnAwake, !this.isPlaying, AudioSource.userInteractionRegistered, this.wasPlaying);
185
+ if (this.enabled && this.playOnAwake && !this.isPlaying && AudioSource.userInteractionRegistered && this.wasPlaying) {
216
186
  this.play();
217
187
  }
218
188
  break;
@@ -257,7 +227,7 @@
257
227
 
258
228
  if (debug) console.log(this.name, this.shouldPlay, AudioSource.userInteractionRegistered, this);
259
229
 
260
- if (this.shouldPlay && AudioSource._userInteractionRegistered)
230
+ if (this.shouldPlay && AudioSource.userInteractionRegistered)
261
231
  this.play();
262
232
  });
263
233
  }
@@ -322,7 +292,7 @@
322
292
  /** Play a mediastream */
323
293
  play(clip: string | MediaStream | undefined = undefined) {
324
294
  // use audio source's clip when no clip is passed in
325
- if(!clip && this.clip)
295
+ if (!clip && this.clip)
326
296
  clip = this.clip;
327
297
 
328
298
  // We only support strings and media stream
src/engine-components/AxesHelper.ts CHANGED
@@ -17,6 +17,8 @@
17
17
  if (this.isGizmo && !params.showGizmos) return;
18
18
  if (!this._axes)
19
19
  this._axes = new _AxesHelper(this.length);
20
+ this._axes.layers.disableAll();
21
+ this._axes.layers.set(this.layer);
20
22
  this.gameObject.add(this._axes);
21
23
  const mat: any = this._axes.material;
22
24
  if (mat) {
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -478,16 +478,16 @@
478
478
 
479
479
  onPointerClick(args: PointerEventData) {
480
480
  args.use();
481
- if (!this.target && !this.clip) return;
482
481
 
482
+ if (!this.clip) return;
483
+
483
484
  if (!this.target) {
484
-
485
485
  const newAudioSource = this.gameObject.addNewComponent(AudioSource);
486
486
  if (newAudioSource) {
487
+ this.target = newAudioSource;
487
488
  newAudioSource.spatialBlend = 1;
488
489
  newAudioSource.volume = 1;
489
490
  newAudioSource.loop = false;
490
- this.target = newAudioSource;
491
491
  }
492
492
  }
493
493
 
src/engine-components/ui/Button.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  import { Animator } from "../Animator.js";
8
8
  import { getParam } from "../../engine/engine_utils.js";
9
9
  import { showBalloonMessage } from "../../engine/debug/index.js";
10
+ import { GraphicRaycaster, ObjectRaycaster, Raycaster } from "./Raycaster.js";
10
11
 
11
12
  const debug = getParam("debugbutton");
12
13
 
@@ -176,6 +177,9 @@
176
177
 
177
178
  start() {
178
179
  this._image?.setInteractable(this.interactable);
180
+ if (!this.gameObject.getComponentInParent(Raycaster)) {
181
+ this.gameObject.addComponent(GraphicRaycaster);
182
+ }
179
183
  }
180
184
 
181
185
  onEnable() {
src/engine-components/codegen/components.ts CHANGED
@@ -38,6 +38,7 @@
38
38
  export { Button } from "../ui/Button.js";
39
39
  export { CallInfo } from "../EventList.js";
40
40
  export { Camera } from "../Camera.js";
41
+ export { CameraTargetReachedEvent } from "../OrbitControls.js";
41
42
  export { Canvas } from "../ui/Canvas.js";
42
43
  export { CanvasGroup } from "../ui/CanvasGroup.js";
43
44
  export { CapsuleCollider } from "../Collider.js";
src/engine-components/ContactShadows.ts CHANGED
@@ -4,7 +4,11 @@
4
4
  import { CustomBlending, DoubleSide, Group, Matrix4, MaxEquation, Mesh, MeshBasicMaterial, MeshDepthMaterial, MinEquation, OrthographicCamera, PlaneGeometry, ShaderMaterial, WebGLRenderTarget } from "three";
5
5
  import { HorizontalBlurShader } from 'three/examples/jsm/shaders/HorizontalBlurShader.js';
6
6
  import { VerticalBlurShader } from 'three/examples/jsm/shaders/VerticalBlurShader.js';
7
+ import { getParam } from "../engine/engine_utils.js"
8
+ import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
7
9
 
10
+ const debug = getParam("debugcontactshadows");
11
+
8
12
  // Adapted from https://github.com/mrdoob/three.js/blob/master/examples/webgl_shadow_contact.html.
9
13
 
10
14
  // Improved with
@@ -40,6 +44,7 @@
40
44
  private verticalBlurMaterial?: ShaderMaterial;
41
45
 
42
46
  awake(): void {
47
+ if(debug) console.log("Create ContactShadows on " + this.gameObject.name, this)
43
48
  const textureSize = 512;
44
49
 
45
50
  this.shadowGroup = new Group();
@@ -55,11 +60,17 @@
55
60
 
56
61
  // make a plane and make it face up
57
62
  const planeGeometry = new PlaneGeometry(1, 1).rotateX(Math.PI / 2);
58
- //@ts-ignore
59
- if (this.gameObject.isMesh) {
63
+
64
+ if (this.gameObject instanceof Mesh) {
65
+ if (debug) console.log("ContactShadows: use existing mesh", this.gameObject)
60
66
  this.plane = this.gameObject as any as Mesh;
61
- const mat = this.plane!.material as MeshBasicMaterial;
67
+ // Make sure we clone the material once because it might be used on another object as well
68
+ const mat = this.plane.material = (this.plane.material as MeshBasicMaterial).clone();
62
69
  mat.map = this.renderTarget.texture;
70
+ mat.opacity = this.opacity;
71
+ // mat.transparent = true;
72
+ // mat.depthWrite = false;
73
+ // mat.needsUpdate = true;
63
74
  // When someone makes a custom mesh, they can set these values right on the material.
64
75
  // mat.opacity = this.state.plane.opacity;
65
76
  // mat.transparent = true;
@@ -85,7 +96,9 @@
85
96
  depthWrite: true,
86
97
  stencilWrite: true,
87
98
  colorWrite: false,
88
- })).rotateX(Math.PI).translateY(0.0001);
99
+ }))
100
+ // .rotateX(Math.PI)
101
+ .translateY(-0.0001);
89
102
  this.occluderMesh.renderOrder = -100;
90
103
  this.gameObject.add(this.occluderMesh);
91
104
  }
@@ -159,13 +172,12 @@
159
172
  if (!this.renderTarget || !this.renderTargetBlur ||
160
173
  !this.depthMaterial || !this.shadowCamera ||
161
174
  !this.blurPlane || !this.shadowGroup || !this.plane ||
162
- !this.horizontalBlurMaterial || !this.verticalBlurMaterial)
175
+ !this.horizontalBlurMaterial || !this.verticalBlurMaterial) {
176
+ if(debug)
177
+ console.error("ContactShadows: not initialized yet");
163
178
  return;
179
+ }
164
180
 
165
- //@ts-ignore
166
- if (this.gameObject.isMesh)
167
- this.gameObject.visible = false;
168
-
169
181
  // Idea: shear the shadowCamera matrix to add some light direction to the ground shadows
170
182
  /*
171
183
  const mat = this.shadowCamera.projectionMatrix.clone();
@@ -177,6 +189,11 @@
177
189
  const planeWasVisible = this.plane.visible;
178
190
  this.plane.visible = false;
179
191
 
192
+ if (this.gameObject instanceof Mesh) {
193
+ this.gameObject.visible = false;
194
+ setCustomVisibility(this.gameObject, false);
195
+ }
196
+
180
197
  // remove the background
181
198
  const initialBackground = scene.background;
182
199
  scene.background = null;
src/engine/engine_camera.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ICameraController } from "./engine_types.js";
2
- import { Camera } from "three";
2
+ import { Camera, Object3D } from "three";
3
3
 
4
4
 
5
5
  const $cameraController = Symbol("cameraController");
@@ -11,8 +11,23 @@
11
11
  export function setCameraController(cam: Camera, cameraController: ICameraController, active: boolean) {
12
12
  if (active)
13
13
  cam[$cameraController] = cameraController;
14
- else{
15
- if(cam[$cameraController] === cameraController)
14
+ else {
15
+ if (cam[$cameraController] === cameraController)
16
16
  cam[$cameraController] = null;
17
17
  }
18
+ }
19
+
20
+
21
+ const $autofit = Symbol("camera autofit");
22
+
23
+ export function useForAutoFit(obj: Object3D): boolean {
24
+ // if autofit is not defined we assume it may be included
25
+ if (obj[$autofit] === undefined) return true;
26
+ // otherwise if anything is set except false we assume it should be included
27
+ return obj[$autofit] !== false;
28
+ }
29
+
30
+ export function setAutoFitEnabled(obj: Object3D, enabled: boolean): void {
31
+ obj[$autofit] = enabled;
32
+
18
33
  }
src/engine/engine_math.ts CHANGED
@@ -84,9 +84,92 @@
84
84
  }
85
85
  return true;
86
86
  }
87
+
88
+ easeInOutCubic(x: number) {
89
+ return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
90
+ }
87
91
  };
88
92
 
89
93
  const vectorKeys = ["x", "y", "z", "w"]
90
94
 
91
- const Mathf = new MathHelper();
92
- export { Mathf };
95
+ export const Mathf = new MathHelper();
96
+
97
+
98
+ class LowPassFilter {
99
+ y: number | null;
100
+ s: number | null;
101
+ alpha = 0;
102
+
103
+ constructor(alpha: number) {
104
+ this.setAlpha(alpha);
105
+ this.y = null;
106
+ this.s = null;
107
+ }
108
+
109
+ setAlpha(alpha: number) {
110
+ if (alpha <= 0 || alpha > 1.0) {
111
+ throw new Error();
112
+ }
113
+ this.alpha = alpha;
114
+ }
115
+
116
+ filter(value: number, alpha: number) {
117
+ if (alpha) {
118
+ this.setAlpha(alpha);
119
+ }
120
+ let s: number;
121
+ if (!this.y) {
122
+ s = value;
123
+ } else {
124
+ s = this.alpha * value + (1.0 - this.alpha) * this.s!;
125
+ }
126
+ this.y = value;
127
+ this.s = s;
128
+ return s;
129
+ }
130
+
131
+ lastValue() {
132
+ return this.y;
133
+ }
134
+ }
135
+
136
+ export class OneEuroFilter {
137
+ freq: number;
138
+ minCutOff: number;
139
+ beta: number;
140
+ dCutOff: number;
141
+ x: LowPassFilter;
142
+ dx: LowPassFilter;
143
+ lasttime: number | null;
144
+
145
+ constructor(freq: number, minCutOff = 1.0, beta = 0.0, dCutOff = 1.0) {
146
+ if (freq <= 0 || minCutOff <= 0 || dCutOff <= 0) {
147
+ throw new Error();
148
+ }
149
+ this.freq = freq;
150
+ this.minCutOff = minCutOff;
151
+ this.beta = beta;
152
+ this.dCutOff = dCutOff;
153
+ this.x = new LowPassFilter(this.alpha(this.minCutOff));
154
+ this.dx = new LowPassFilter(this.alpha(this.dCutOff));
155
+ this.lasttime = null;
156
+ }
157
+
158
+ alpha(cutOff: number) {
159
+ const te = 1.0 / this.freq;
160
+ const tau = 1.0 / (2 * Math.PI * cutOff);
161
+ return 1.0 / (1.0 + tau / te);
162
+ }
163
+
164
+ filter(x: number, time: number | null = null) {
165
+ if (this.lasttime && time) {
166
+ this.freq = 1.0 / (time - this.lasttime);
167
+ }
168
+ this.lasttime = time;
169
+ const prevX = this.x.lastValue();
170
+ const dx = !prevX ? 0.0 : (x - prevX) * this.freq;
171
+ const edx = this.dx.filter(dx, this.alpha(this.dCutOff));
172
+ const cutOff = this.minCutOff + this.beta * Math.abs(edx);
173
+ return this.x.filter(x, this.alpha(cutOff));
174
+ }
175
+ }
src/engine/engine_physics_rapier.ts CHANGED
@@ -563,7 +563,7 @@
563
563
  const finalRadius = radius * scale.x;
564
564
  // half height = distance between capsule origin and top sphere origin (not the top end of the capsule)
565
565
  height = Math.max(height, finalRadius * 2);
566
- const hh = (height * .5 * scale.y) - (radius * scale.x);
566
+ const hh = Mathf.clamp((height * .5 * scale.y) - (radius * scale.x), 0, Number.MAX_SAFE_INTEGER);
567
567
  const desc = ColliderDesc.capsule(hh, finalRadius);
568
568
  this.createCollider(collider, desc, center);
569
569
  }
src/engine/engine_physics.ts CHANGED
@@ -8,6 +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";
12
+
11
13
  export class RaycastOptions {
12
14
  ray: Ray | undefined = undefined;
13
15
  cam: Camera | undefined | null = undefined;
@@ -22,6 +24,10 @@
22
24
  /** raw layer mask, use setLayer to set an individual layer active */
23
25
  layerMask: Layers | number | undefined = undefined;
24
26
  ignore: Object3D[] | undefined = undefined;
27
+ /** if defined it's called per object before tested for intersections.
28
+ * 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
+ testObject?: IgnoreCallback = undefined;
25
31
 
26
32
  screenPointFromOffset(ox: number, oy: number) {
27
33
  if (this.screenPoint === undefined) this.screenPoint = new Vector2();
@@ -212,7 +218,8 @@
212
218
 
213
219
  // shoot
214
220
  results.length = 0;
215
- rc.intersectObjects(targets, options.recursive, results);
221
+ this.intersect(this.raycaster, targets, results, options);
222
+ results.sort((a, b) => a.distance - b.distance);
216
223
 
217
224
  // TODO: instead of doing this we should temporerly set these objects to layer 2 during raycasting
218
225
  const ignorelist = options.ignore;
@@ -221,4 +228,19 @@
221
228
  }
222
229
  return results;
223
230
  }
231
+
232
+ private intersect(raycaster: Raycaster, objects: Object3D[], results: Intersection[], options: RaycastOptions) {
233
+ for (const obj of objects) {
234
+ const testResult = options.testObject?.(obj);
235
+ if (testResult === false) continue;
236
+ const checkObject = testResult !== "continue in children";
237
+ if (checkObject)
238
+ raycaster.intersectObject(obj, false, results);
239
+
240
+ if (options.recursive) {
241
+ this.intersect(raycaster, obj.children, results, options);
242
+ }
243
+ }
244
+ return results;
245
+ }
224
246
  }
src/engine-components/ui/EventSystem.ts CHANGED
@@ -92,11 +92,25 @@
92
92
  const res = GameObject.findObjectOfType(Raycaster, this.context);
93
93
  if (!res) {
94
94
  const rc = GameObject.addNewComponent(this.context.scene, ObjectRaycaster);
95
+ rc.ignoreSkinnedMeshes = true;
95
96
  this.raycaster.push(rc);
96
97
  if (isDevEnvironment() || debug)
97
- console.warn("Added an ObjectRaycaster to the scene because no raycaster was found");
98
+ console.warn("Added an ObjectRaycaster to the scene because no raycaster was found. Skinnedmeshes will be ignored for better performance");
98
99
  }
99
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
+ }
100
114
  }
101
115
 
102
116
  register(rc: Raycaster) {
@@ -166,7 +180,7 @@
166
180
  const controllerRcOpts = new RaycastOptions();
167
181
  this._selectUpdateFn ??= (_ctrl: WebXRController) => {
168
182
  controllerRcOpts.ray = _ctrl.getRay();
169
- const rc = this.performRaycast(controllerRcOpts);
183
+ const rc = this.performRaycast(controllerRcOpts, _ctrl.selectionClick);
170
184
  if (!rc) return;
171
185
  const opts = new PointerEventData(this.context.input);
172
186
  opts.inputSource = _ctrl;
@@ -226,7 +240,7 @@
226
240
 
227
241
  data.inputSource = this.context.input;
228
242
  data.pointerId = pointerId;
229
- data.isClicked = pointerEvent.type == InputEvents.PointerUp && this.context.input.getPointerClicked(pointerId)
243
+ data.isClicked = pointerEvent.type == InputEvents.PointerUp && this.context.input.getPointerClicked(pointerId)
230
244
  // using the input type directly instead of input API -> otherwise onMove events can sometimes be getPointerUp() == true
231
245
  data.isDown = pointerEvent.type == InputEvents.PointerDown;
232
246
  data.isUp = pointerEvent.type == InputEvents.PointerUp;
@@ -238,7 +252,7 @@
238
252
  const options = new RaycastOptions();
239
253
  options.screenPoint = this.context.input.getPointerPositionRC(pointerId)!;
240
254
 
241
- const hits = this.performRaycast(options);
255
+ const hits = this.performRaycast(options, data.isClicked);
242
256
  if (!hits) return;
243
257
 
244
258
  if (debug && data.isClicked) {
@@ -277,12 +291,27 @@
277
291
 
278
292
  private readonly _sortedHits: THREE.Intersection[] = [];
279
293
 
280
- private performRaycast(opts: RaycastOptions | null): THREE.Intersection[] | null {
294
+ private performRaycast(opts: RaycastOptions | null, isClick: boolean): THREE.Intersection[] | null {
281
295
  if (!this.raycaster) return null;
282
296
  this._sortedHits.length = 0;
283
297
  for (const rc of this.raycaster) {
284
298
  if (!rc.activeAndEnabled) continue;
299
+
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
+ }
285
306
  const res = rc.performRaycast(opts);
307
+
308
+ if (rc instanceof ObjectRaycaster) {
309
+ if (didIgnoreSkinnedMeshes !== undefined) {
310
+ rc.ignoreSkinnedMeshes = didIgnoreSkinnedMeshes;
311
+ }
312
+ }
313
+
314
+
286
315
  if (res && res.length > 0)
287
316
  this._sortedHits.push(...res);
288
317
  }
src/engine-components/OrbitControls.ts CHANGED
@@ -1,19 +1,20 @@
1
1
  import { Behaviour, GameObject } from "./Component.js";
2
2
  import { Camera } from "./Camera.js";
3
3
  import { LookAtConstraint } from "./LookAtConstraint.js";
4
- import { getWorldPosition, getWorldRotation, setWorldPosition, setWorldRotation, slerp } from "../engine/engine_three_utils.js";
4
+ import { getWorldDirection, getWorldPosition, getWorldRotation, setWorldPosition, setWorldRotation, slerp } from "../engine/engine_three_utils.js";
5
5
  import { RaycastOptions } from "../engine/engine_physics.js";
6
6
  import { serializable } from "../engine/engine_serialization_decorator.js";
7
7
  import { getParam, isMobileDevice } from "../engine/engine_utils.js";
8
8
 
9
- import { Camera as ThreeCamera, Box3, Object3D, PerspectiveCamera, Vector2, Vector3, Box3Helper, GridHelper, Mesh, ShadowMaterial, RGBA_ASTC_10x10_Format } from "three";
9
+ import { Camera as ThreeCamera, Box3, Object3D, PerspectiveCamera, Vector2, Vector3, Box3Helper, GridHelper, Mesh, ShadowMaterial, RGBA_ASTC_10x10_Format, Ray } from "three";
10
10
  import { OrbitControls as ThreeOrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
11
11
  import { type AfterHandleInputEvent, EventSystem, EventSystemEvents } from "./ui/EventSystem.js";
12
12
  import type { ICameraController } from "../engine/engine_types.js";
13
- import { setCameraController } from "../engine/engine_camera.js";
13
+ import { setCameraController, useForAutoFit } from "../engine/engine_camera.js";
14
14
  import { SyncedTransform } from "./SyncedTransform.js";
15
15
  import { tryGetUIComponent } from "./ui/Utils.js";
16
16
  import { GroundProjectedSkybox } from "three/examples/jsm/objects/GroundProjectedSkybox.js";
17
+ import { Mathf } from "../engine/engine_math.js";
17
18
 
18
19
  const freeCam = getParam("freecam");
19
20
  const debugCameraFit = getParam("debugcamerafit");
@@ -22,6 +23,21 @@
22
23
  const disabledKeys = { LEFT: "", UP: "", RIGHT: "", BOTTOM: "" };
23
24
  let defaultKeys: any = undefined;
24
25
 
26
+ export enum OrbitControlsEventsType {
27
+ /** Invoked with a CameraTargetReachedEvent */
28
+ CameraTargetReached = "target-reached",
29
+ }
30
+ export class CameraTargetReachedEvent extends CustomEvent<{ controls: OrbitControls, type: "camera" | "lookat" }> {
31
+ constructor(ctrls: OrbitControls, type: "camera" | "lookat") {
32
+ super(OrbitControlsEventsType.CameraTargetReached, {
33
+ detail: {
34
+ controls: ctrls,
35
+ type: type,
36
+ }
37
+ });
38
+ }
39
+ }
40
+
25
41
  export class OrbitControls extends Behaviour implements ICameraController {
26
42
 
27
43
  get isCameraController(): boolean {
@@ -86,20 +102,35 @@
86
102
 
87
103
  debugLog: boolean = false;
88
104
 
89
- /** The speed at which the camera target and the camera will be lerping to their destinations (if set via script or user input) */
90
- targetLerpSpeed = 5;
105
+ /**
106
+ * @deprecated use `targetLerpDuration` instead
107
+ * ~~The speed at which the camera target and the camera will be lerping to their destinations (if set via script or user input)~~
108
+ * */
109
+ get targetLerpSpeed() { return 5 }
110
+ set targetLerpSpeed(v) { this.targetLerpDuration = 1 / v; }
91
111
 
112
+ /** The duration in seconds it takes for the camera look ad and position lerp to reach their destination (when set via `setCameraTargetPosition` and `setLookTargetPosition`) */
113
+ @serializable()
114
+ targetLerpDuration = 1;
115
+
92
116
  /** When enabled OrbitControls will automatically raycast find a look at target in start */
93
117
  autoTarget: boolean = true;
94
118
 
95
- private _lookTargetPosition!: Vector3;
96
119
  private _controls: ThreeOrbitControls | null = null;
97
120
  private _cameraObject: Object3D | null = null;
98
121
 
99
- private _lerpToTargetPosition: boolean = false;
100
- private _lerpCameraToTarget: boolean = false;
101
- private _cameraTargetPosition: Vector3 | null = null;
122
+ private _lookTargetLerpActive: boolean = false;
123
+ private _lookTargetStartPosition: Vector3 = new Vector3();
124
+ private _lookTargetEndPosition: Vector3 = new Vector3();
125
+ private _lookTargetLerp01: number = 0;
126
+ private _lookTargetLerpDuration: number = 0;
102
127
 
128
+ private _cameraLerpActive: boolean = false;
129
+ private _cameraStartPosition: Vector3 = new Vector3();
130
+ private _cameraEndPosition: Vector3 = new Vector3();
131
+ private _cameraLerp01: number = 0;
132
+ private _cameraLerpDuration: number = 0;
133
+
103
134
  private _inputs: number = 0;
104
135
  private _enableTime: number = 0; // use to disable double click when double clicking on UI
105
136
  private _startedListeningToKeyEvents: boolean = false;
@@ -114,7 +145,6 @@
114
145
 
115
146
  awake(): void {
116
147
  this._didStart = false;
117
- this._lookTargetPosition = new Vector3();
118
148
  this._startedListeningToKeyEvents = false;
119
149
  }
120
150
 
@@ -123,22 +153,22 @@
123
153
  if (this.autoTarget) {
124
154
  if (this._controls) {
125
155
  const camGo = GameObject.getComponent(this.gameObject, Camera);
126
- if (camGo && !this.setFromTargetPosition()) {
156
+ if (camGo && !this.setLookTargetFromConstraint()) {
127
157
  if (this.debugLog)
128
158
  console.log("NO TARGET");
129
159
  const worldPosition = getWorldPosition(camGo.cam);
130
160
  const distanceToCenter = worldPosition.length();
131
161
  const forward = new Vector3(0, 0, -distanceToCenter).applyMatrix4(camGo.cam.matrixWorld);
132
- this.setTarget(forward, true);
162
+ this.setLookTargetPosition(forward, true);
133
163
  }
134
- if (this.autoTarget && !this.setFromTargetPosition()) {
164
+ if (this.autoTarget && !this.setLookTargetFromConstraint()) {
135
165
  const opts = new RaycastOptions();
136
166
  // center of the screen:
137
167
  opts.screenPoint = new Vector2(0, 0);
138
168
  opts.lineThreshold = 0.1;
139
169
  const hits = this.context.physics.raycast(opts);
140
170
  if (hits.length > 0) {
141
- this.setTarget(hits[0].point, true);
171
+ this.setLookTargetPosition(hits[0].point, true);
142
172
  }
143
173
  if (debugCameraFit)
144
174
  console.log("OrbitControls hits", ...hits);
@@ -267,8 +297,8 @@
267
297
  if (this.enableRotate) {
268
298
  this.autoRotate = false;
269
299
  }
270
- this._lerpCameraToTarget = false;
271
- this._lerpToTargetPosition = false;
300
+ this._cameraLerpActive = false;
301
+ this._lookTargetLerpActive = false;
272
302
  }
273
303
  this._inputs = 0;
274
304
 
@@ -278,35 +308,32 @@
278
308
  this.setTargetFromRaycast();
279
309
  }
280
310
 
281
- if (this._lerpToTargetPosition || this._lerpCameraToTarget) {
282
- const step = this.context.time.deltaTime * this.targetLerpSpeed;
311
+ if (this._lookTargetLerpActive || this._cameraLerpActive) {
283
312
 
284
- // confusing naming ahead:
285
- // _targetObject: the target where the camera moves to
286
- // targetPosition: the target where the look target moves to
287
-
288
313
  // lerp the camera
289
- if (this._lerpCameraToTarget && this._cameraTargetPosition && this._cameraObject) {
290
- // setWorldPosition(this._cameraObject, this._cameraTargetPosition);
291
- if (this.useSlerp) {
292
- const position = this._cameraObject?.position;
293
- slerp(position, this._cameraTargetPosition, step);
314
+ if (this._cameraLerpActive && this._cameraObject) {
315
+ this._cameraLerp01 += this.context.time.deltaTime / this._cameraLerpDuration;
316
+ if (this._cameraLerp01 >= 1) {
317
+ this._cameraObject.position.copy(this._cameraEndPosition);
318
+ this._cameraLerpActive = false;
319
+ this.dispatchEvent(new CameraTargetReachedEvent(this, "camera"));
294
320
  }
295
321
  else {
296
- this._cameraObject?.position.lerp(this._cameraTargetPosition, step);
322
+ const t = Mathf.easeInOutCubic(this._cameraLerp01);
323
+ this._cameraObject.position.lerpVectors(this._cameraStartPosition, this._cameraEndPosition, t);
297
324
  }
298
- const minDist = this.autoRotate ? .02 : .001;
299
- const dist = this._cameraObject.position.distanceTo(this._cameraTargetPosition);
300
- if (dist < minDist) {
301
- this._lerpCameraToTarget = false;
302
- }
303
325
  }
304
326
 
305
327
  // lerp the look target
306
- if (this._lerpToTargetPosition) {
307
- this.lerpTarget(this._lookTargetPosition, step);
308
- if (this._lookTargetPosition.distanceTo(this._controls.target) < .00001) {
309
- this._lerpToTargetPosition = false;
328
+ if (this._lookTargetLerpActive) {
329
+ this._lookTargetLerp01 += this.context.time.deltaTime / this._lookTargetLerpDuration;
330
+ if (this._lookTargetLerp01 >= 1) {
331
+ this._controls.target.copy(this._lookTargetEndPosition);
332
+ this._lookTargetLerpActive = false;
333
+ this.dispatchEvent(new CameraTargetReachedEvent(this, "lookat"));
334
+ } else {
335
+ const t = Mathf.easeInOutCubic(this._lookTargetLerp01);
336
+ this._controls.target.lerpVectors(this._lookTargetStartPosition, this._lookTargetEndPosition, t);
310
337
  }
311
338
  }
312
339
  }
@@ -324,76 +351,134 @@
324
351
  this._controls.dampingFactor = this.dampingFactor;
325
352
  this._controls.enablePan = this.enablePan;
326
353
  this._controls.enableRotate = this.enableRotate;
354
+
327
355
  if (typeof smoothcam === "number" || smoothcam === true) {
328
356
  this._controls.enableDamping = true;
329
357
  const factor = typeof smoothcam === "number" ? smoothcam : .99;
330
358
  this._controls.dampingFactor = Math.max(.001, 1 - Math.min(1, factor));
331
359
  }
360
+
361
+ if (!this.allowInterrupt) {
362
+ if (this._lookTargetLerpActive) {
363
+ this._controls.enablePan = false;
364
+ }
365
+ if (this._cameraLerpActive) {
366
+ this._controls.enableRotate = false;
367
+ this._controls.autoRotate = false;
368
+ }
369
+ if (this._lookTargetLerpActive || this._cameraLerpActive) {
370
+ this._controls.enableZoom = false;
371
+ }
372
+ }
332
373
  //@ts-ignore
333
374
  // this._controls.zoomToCursor = this.zoomToCursor;
334
375
  if (!this.context.isInXR) {
335
- if (!freeCam && this.lookAtConstraint?.locked) this.setFromTargetPosition(0, this.lookAtConstraint01);
376
+ if (!freeCam && this.lookAtConstraint?.locked) this.setLookTargetFromConstraint(0, this.lookAtConstraint01);
336
377
  this._controls.update();
337
378
  }
338
379
  }
339
380
 
340
381
  }
341
382
 
342
- /** Moves the camera to position smoothly. @deprecated use `setCameraTargetPosition` */
343
- public setCameraTarget(position?: Vector3 | null, immediate: boolean = false) {
344
- return this.setCameraTargetPosition(position, immediate);
383
+
384
+ /**
385
+ * Sets camera target position and look direction. Does perform a raycast in the forward direction of the passed in object to find an orbit point
386
+ */
387
+ public setCameraAndLookTarget(target: Object3D) {
388
+ if (!target || !(target instanceof Object3D)) return;
389
+ const worldPosition = getWorldPosition(target);
390
+ const forward = getWorldDirection(target);
391
+ this.setTargetFromRaycast(new Ray(worldPosition, forward));
392
+ this.setCameraTargetPosition(worldPosition);
345
393
  }
394
+
346
395
  /** Moves the camera to position smoothly.
347
396
  * @param position The position in local space of the controllerObject to move the camera to. If null the camera will stop lerping to the target.
348
397
  */
349
- public setCameraTargetPosition(position?: Vector3 | null, immediate: boolean = false) {
350
- if (!position) this._lerpCameraToTarget = false;
398
+ public setCameraTargetPosition(position?: Object3D | Vector3 | null, immediateOrDuration: boolean | number = false) {
399
+ if (!position) return;
400
+ if (position instanceof Object3D) {
401
+ position = getWorldPosition(position) as Vector3;
402
+ }
403
+ if (!this._cameraEndPosition) this._cameraEndPosition = new Vector3();
404
+ this._cameraEndPosition.copy(position);
405
+ if (immediateOrDuration === true) {
406
+ this._cameraLerpActive = false;
407
+ this.controllerObject?.position.copy(this._cameraEndPosition);
408
+ }
409
+ else if (this._cameraObject) {
410
+ this._cameraLerpActive = true;
411
+ this._cameraLerp01 = 0;
412
+ this._cameraStartPosition.copy(this._cameraObject?.position);
413
+ if (typeof immediateOrDuration === "number") {
414
+ this._cameraLerpDuration = immediateOrDuration;
415
+ }
416
+ else this._cameraLerpDuration = this.targetLerpDuration;
417
+ }
418
+ }
419
+ /** True while the camera position is being lerped */
420
+ get cameraLerpActive() { return this._cameraLerpActive; }
421
+ /** Call to stop camera position lerping */
422
+ public stopCameraLerp() {
423
+ this._cameraLerpActive = false;
424
+ }
425
+
426
+ /** Moves the camera look-at target to a position smoothly. */
427
+ public setLookTargetPosition(position: Object3D | Vector3 | null = null, immediateOrDuration: boolean = false) {
428
+ if (!this._controls) return;
429
+ if (!position) return
430
+ if (position instanceof Object3D) {
431
+ position = getWorldPosition(position) as Vector3;
432
+ }
433
+ this._lookTargetEndPosition.copy(position);
434
+
435
+ if (immediateOrDuration === true) {
436
+ this._controls.target.copy(this._lookTargetEndPosition);
437
+ }
351
438
  else {
352
- this._lerpCameraToTarget = true;
353
- this._cameraTargetPosition = position.clone();
354
- if (immediate && this._cameraTargetPosition) {
355
- this.controllerObject?.position.copy(this._cameraTargetPosition);
439
+ this._lookTargetLerpActive = true;
440
+ this._lookTargetLerp01 = 0;
441
+ this._lookTargetStartPosition.copy(this._controls.target);
442
+ if (typeof immediateOrDuration === "number") {
443
+ this._lookTargetLerpDuration = immediateOrDuration;
356
444
  }
445
+ else this._lookTargetLerpDuration = this.targetLerpDuration;
357
446
  }
358
447
  }
448
+ /** True while the camera look target is being lerped */
449
+ get lookTargetLerpActive() { return this._lookTargetLerpActive; }
450
+ /** Call to stop camera look target lerping */
451
+ public stopLookTargetLerp() {
452
+ this._lookTargetLerpActive = false;
453
+ }
359
454
 
360
455
  /** Sets the look at target from an assigned lookAtConstraint source by index */
361
- public setFromTargetPosition(index: number = 0, t: number = 1): boolean {
456
+ private setLookTargetFromConstraint(index: number = 0, t: number = 1): boolean {
362
457
  if (!this._controls) return false;
363
458
  const sources = this.lookAtConstraint?.sources;
364
459
  if (sources && sources.length > 0) {
365
460
  const target = sources[index];
366
461
  if (target) {
367
- target.getWorldPosition(this._lookTargetPosition);
368
- this.lerpTarget(this._lookTargetPosition, t);
462
+ target.getWorldPosition(this._lookTargetEndPosition);
463
+ this.lerpLookTarget(this._lookTargetEndPosition, t);
369
464
  return true;
370
465
  }
371
466
  }
372
467
  return false;
373
468
  }
374
469
 
375
- /** Moves the camera look-at target to a position smoothly. */
376
- public setTarget(position: Vector3 | null = null, immediate: boolean = false) {
377
- if (!this._controls) return;
378
- if (position !== null) this._lookTargetPosition.copy(position);
379
- if (immediate)
380
- this._controls.target.copy(this._lookTargetPosition);
381
- else this._lerpToTargetPosition = true;
382
- }
470
+ /** @deprecated use `controls.target.lerp(position, delta)` */
471
+ public lerpTarget(position: Vector3, delta: number) { return this.lerpLookTarget(position, delta); }
383
472
 
384
- public lerpTarget(position: Vector3, delta: number) {
473
+ private lerpLookTarget(position: Vector3, delta: number) {
385
474
  if (!this._controls) return;
386
- this._controls.target.lerp(position, delta);
475
+ if (delta >= 1) this._controls.target.copy(position);
476
+ else this._controls.target.lerp(position, delta);
387
477
  }
388
478
 
389
- public distanceToTarget(position: Vector3): number {
390
- if (!this._controls) return -1;
391
- return this._controls.target.distanceTo(position);
392
- }
393
-
394
- private setTargetFromRaycast() {
479
+ private setTargetFromRaycast(ray?: Ray) {
395
480
  if (!this.controls) return;
396
- const rc = this.context.physics.raycast();
481
+ const rc = ray ? this.context.physics.raycastFromRay(ray) : this.context.physics.raycast();
397
482
  for (const hit of rc) {
398
483
  if (hit.distance > 0 && GameObject.isActiveInHierarchy(hit.object)) {
399
484
 
@@ -404,19 +489,14 @@
404
489
  break;
405
490
  }
406
491
  }
407
- // if (hit.object && hit.object.parent) {
408
- // const par: any = hit.object.parent;
409
- // if (par.isUI) continue;
410
- // }
411
- // console.log("Set target", this.targetPosition, hit.object.name, hit.object);
412
- this._lookTargetPosition.copy(hit.point);
413
- this._lerpToTargetPosition = true;
414
- this._cameraTargetPosition = null;
492
+
493
+ this.setLookTargetPosition(hit.point);
494
+
415
495
  if (this.context.mainCamera) {
416
- this._lerpCameraToTarget = true;
417
496
  const pos = getWorldPosition(this.context.mainCamera);
418
- this._cameraTargetPosition = pos.clone().sub(this.controls.target).add(this._lookTargetPosition);
419
- this._cameraObject?.parent?.worldToLocal(this._cameraTargetPosition);
497
+ const cameraTarget = pos.clone().sub(this.controls.target).add(this._lookTargetEndPosition);
498
+ this._cameraObject?.parent?.worldToLocal(cameraTarget);
499
+ this.setCameraTargetPosition(cameraTarget);
420
500
  }
421
501
  break;
422
502
  }
@@ -449,6 +529,7 @@
449
529
  let allowExpanding = true;
450
530
  // we dont want to check invisible objects
451
531
  if (!obj.visible) return;
532
+ if (useForAutoFit(obj) === false) return;
452
533
  // ignore Box3Helpers
453
534
  if (obj instanceof Box3Helper) allowExpanding = false;
454
535
  if (obj instanceof GridHelper) allowExpanding = false;
@@ -516,7 +597,7 @@
516
597
  controls.maxDistance = distance * 10;
517
598
  controls.minDistance = distance * 0.01;
518
599
 
519
- this.setTarget(center, immediate);
600
+ this.setLookTargetPosition(center, immediate);
520
601
  this.autoTarget = false;
521
602
 
522
603
  // TODO: this doesnt take the Camera component nearClipPlane into account
@@ -528,7 +609,7 @@
528
609
 
529
610
  if (camera.parent) {
530
611
  const cameraLocalPosition = camera.parent!.worldToLocal(controls.target.clone().sub(direction));
531
- this.setCameraTarget(cameraLocalPosition, immediate);
612
+ this.setCameraTargetPosition(cameraLocalPosition, immediate);
532
613
  }
533
614
  else console.error(`Can not fit camera ${camera.name} because it has no parent`)
534
615
 
src/engine-components/ui/PointerEvents.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Input, NEPointerEvent } from "../../engine/engine_input.js";
2
+ import { Object3D } from "three";
2
3
 
3
4
  export interface IInputEventArgs {
4
5
  get used(): boolean;
@@ -83,3 +84,20 @@
83
84
 
84
85
  export interface IPointerEventHandler extends IPointerDownHandler,
85
86
  IPointerUpHandler, IPointerEnterHandler, IPointerMoveHandler, IPointerExitHandler, IPointerClickHandler { }
87
+
88
+
89
+
90
+ /**
91
+ * @internal tests if the object has any PointerEventComponent used by the EventSystem
92
+ * This is used to skip raycasting on objects that have no components that use pointer events
93
+ */
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
+ // }
src/engine-components/ui/Raycaster.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { RaycastOptions } from "../../engine/engine_physics.js";
2
2
  import { Behaviour, Component } from "../Component.js";
3
3
  import { EventSystem } from "./EventSystem.js";
4
+ import { SkinnedMesh } from "three";
4
5
 
5
6
 
6
7
  export class Raycaster extends Behaviour {
@@ -26,6 +27,8 @@
26
27
  private targets: THREE.Object3D[] | null = null;
27
28
  private raycastHits: THREE.Intersection[] = [];
28
29
 
30
+ ignoreSkinnedMeshes = false;
31
+
29
32
  start(): void {
30
33
  this.targets = [this.gameObject];
31
34
  }
@@ -35,6 +38,12 @@
35
38
  opts ??= new RaycastOptions();
36
39
  opts.targets = this.targets;
37
40
  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
+ };
38
47
  const hits = this.context.physics.raycast(opts);
39
48
  // console.log(this.context.alias, hits);
40
49
  return hits;
@@ -45,6 +54,11 @@
45
54
  // eventCamera: Camera | null = null;
46
55
  // ignoreReversedGraphics: boolean = false;
47
56
  // rootRaycaster: GraphicRaycaster | null = null;
57
+
58
+ constructor() {
59
+ super();
60
+ this.ignoreSkinnedMeshes = true;
61
+ }
48
62
  }
49
63
 
50
64
 
src/engine/codegen/register_types.ts CHANGED
@@ -40,6 +40,7 @@
40
40
  import { Button } from "../../engine-components/ui/Button.js";
41
41
  import { CallInfo } from "../../engine-components/EventList.js";
42
42
  import { Camera } from "../../engine-components/Camera.js";
43
+ import { CameraTargetReachedEvent } from "../../engine-components/OrbitControls.js";
43
44
  import { Canvas } from "../../engine-components/ui/Canvas.js";
44
45
  import { CanvasGroup } from "../../engine-components/ui/CanvasGroup.js";
45
46
  import { CapsuleCollider } from "../../engine-components/Collider.js";
@@ -259,6 +260,7 @@
259
260
  TypeStore.add("Button", Button);
260
261
  TypeStore.add("CallInfo", CallInfo);
261
262
  TypeStore.add("Camera", Camera);
263
+ TypeStore.add("CameraTargetReachedEvent", CameraTargetReachedEvent);
262
264
  TypeStore.add("Canvas", Canvas);
263
265
  TypeStore.add("CanvasGroup", CanvasGroup);
264
266
  TypeStore.add("CapsuleCollider", CapsuleCollider);
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -114,6 +114,8 @@
114
114
  hit?.createAnchor?.call(hit, pose.transform)?.then(anchor => {
115
115
  if (this.context.isInAR)
116
116
  this._anchor = anchor;
117
+ }).catch(ex => {
118
+ console.warn("Failed to create anchor", ex);
117
119
  });
118
120
  }
119
121
 
src/engine-components/webxr/WebXR.ts CHANGED
@@ -188,6 +188,9 @@
188
188
  public get IsInVR() { return this._isInVR; }
189
189
  public get IsInAR() { return this._isInAR; }
190
190
 
191
+ /** When enabled */
192
+ allowARPlacementReticle: boolean = true;
193
+
191
194
  private rig!: Object3D;
192
195
  private isInit: boolean = false;
193
196
 
@@ -229,10 +232,6 @@
229
232
 
230
233
  this.context.renderer.xr.enabled = true;
231
234
 
232
- // general WebXR support?
233
- const browserSupportsXR = WebXR.XRSupported;
234
-
235
-
236
235
  // TODO: move the whole buttons positioning out of here and make it configureable from css
237
236
  // better set proper classes so user code can react to it instead
238
237
  // of this hardcoded stuff
@@ -740,7 +739,7 @@
740
739
  }
741
740
 
742
741
  if (this.reticle) {
743
- this.reticle.visible = this.reticleActive;
742
+ this.reticle.visible = this.reticleActive && this._webxr.allowARPlacementReticle;
744
743
  if (this.reticleActive) {
745
744
  if (pose) {
746
745
  const matrix = pose.transform.matrix;
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import { Object3D, Quaternion, Vector3 } from "three";
5
5
  import { CircularBuffer, getParam } from "../../engine/engine_utils.js";
6
6
  import { AssetReference } from "../../engine/engine_addressables.js";
7
- import { showBalloonWarning } from "../../engine/debug/index.js";
7
+ import { showBalloonMessage, showBalloonWarning } from "../../engine/debug/index.js";
8
8
 
9
9
  import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.js";
10
10
  import { USDZExporterContext, USDWriter, imageToCanvas } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
@@ -143,11 +143,15 @@
143
143
  private readonly trackedImageIndexMap: Map<number, WebXRImageTrackingModel> = new Map();
144
144
 
145
145
  private static _imageElements: Map<string, ImageBitmap | null> = new Map();
146
+ private webxr: WebXR | null = null;
146
147
 
147
148
  awake(): void {
148
- if(debug) console.log(this)
149
+ if (debug) console.log(this)
149
150
  if (!this.trackedImages) return;
150
151
  for (const trackedImage of this.trackedImages) {
152
+ if (trackedImage.object?.asset) {
153
+ trackedImage.object.asset.visible = false;
154
+ }
151
155
  if (trackedImage.image) {
152
156
  if (WebXRImageTracking._imageElements.has(trackedImage.image)) {
153
157
  }
@@ -181,10 +185,9 @@
181
185
  }
182
186
  }
183
187
 
184
- private xr: WebXR | null = null;
185
188
 
186
189
  onEnable(): void {
187
- this.xr = GameObject.findObjectOfType(WebXR);
190
+ this.webxr = GameObject.findObjectOfType(WebXR);
188
191
  WebXR.addEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
189
192
  WebXR.addEventListener(WebXREvent.XRStarted, this.onXRStarted);
190
193
  WebXR.addEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
@@ -221,17 +224,71 @@
221
224
  }
222
225
  }
223
226
 
224
- private imageToObjectMap: Map<WebXRImageTrackingModel, { object: GameObject | null, frames: number }> = new Map();
227
+ private onXRStarted = (_: any) => {
228
+ // clear out all frame counters for tracking
229
+ for (const trackedData of this.imageToObjectMap.values()) {
230
+ trackedData.frames = 0;
231
+ }
232
+ };
225
233
 
226
- private onImageTrackingUpdate = (event: any) => {
227
- const images = event.detail as WebXRTrackedImage[];
234
+ private readonly imageToObjectMap = new Map<WebXRImageTrackingModel, { object: GameObject | null, frames: number }>();
235
+ private readonly currentImages: WebXRTrackedImage[] = [];
228
236
 
237
+
238
+ private onXRUpdate = (evt): void => {
239
+ this.currentImages.length = 0;
240
+
241
+ const frame = evt.frame;
242
+ if (!frame) return;
243
+
244
+ if (frame.session && !("getImageTrackingResults" in frame)) {
245
+ const warning = "Image tracking is currently not supported on this device. On Chrome for Android, you can enable the <a href=\"chrome://flags/#webxr-incubations\">chrome://flags/#webxr-incubations</a> flag.";
246
+ console.log(warning);
247
+ showBalloonWarning(warning);
248
+ return;
249
+ }
250
+
251
+ if (frame.session && typeof frame.getImageTrackingResults === "function") {
252
+ const results = frame.getImageTrackingResults();
253
+ if (results.length > 0) {
254
+ const space = this.context.renderer.xr.getReferenceSpace();
255
+ if (space) {
256
+ for (const result of results) {
257
+ const state = result.trackingState;
258
+ const imageIndex = result.index;
259
+ const trackedImage = this.trackedImageIndexMap.get(imageIndex);
260
+ if (trackedImage) {
261
+ const pose = frame.getPose(result.imageSpace, space);
262
+ const imageData = new WebXRTrackedImage(this, trackedImage, result.image, result.measuredSize, state, pose);
263
+ this.currentImages.push(imageData);
264
+ }
265
+ else {
266
+ if (debug) {
267
+ console.warn("No tracked image for index", imageIndex);
268
+ }
269
+ }
270
+ }
271
+ if (this.currentImages.length > 0) {
272
+ try {
273
+ this.dispatchEvent(new CustomEvent("image-tracking", { detail: this.currentImages }));
274
+ if (this.webxr && this.webxr.allowARPlacementReticle) {
275
+ this.webxr.allowARPlacementReticle = false;
276
+ }
277
+ }
278
+ catch (e) {
279
+ console.error(e);
280
+ }
281
+ }
282
+ }
283
+ }
284
+ }
285
+
229
286
  // disable any objects that are no longer tracked
230
287
  for (const [model, object] of this.imageToObjectMap) {
231
288
  if (!object.object || !model) continue;
232
289
  let found = false;
233
- for (const trackedImage of images) {
234
- if (trackedImage.model === model) {
290
+ for (const trackedImage of this.currentImages) {
291
+ if (trackedImage.state === "tracked" && trackedImage.model === model) {
235
292
  found = true;
236
293
  break;
237
294
  }
@@ -240,7 +297,12 @@
240
297
  GameObject.setActive(object.object, false);
241
298
  }
242
299
  }
300
+ }
243
301
 
302
+
303
+ private onImageTrackingUpdate = (event: any) => {
304
+ const images = event.detail as WebXRTrackedImage[];
305
+
244
306
  for (const image of images) {
245
307
  const model = image.model;
246
308
  // don't do anything if we don't have an object to track - can be handled externally through events
@@ -252,15 +314,16 @@
252
314
  this.imageToObjectMap.set(model, trackedData);
253
315
 
254
316
  model.object.loadAssetAsync().then((asset: GameObject | null) => {
255
- if (model.createObjectInstance)
317
+ if (model.createObjectInstance) {
256
318
  asset = GameObject.instantiate(asset);
319
+ }
257
320
 
258
321
  if (asset) {
259
322
  trackedData!.object = asset;
260
323
 
261
324
  // make sure to parent to the WebXR.rig
262
- if (this.xr) {
263
- this.xr.Rig.add(asset);
325
+ if (this.webxr) {
326
+ this.webxr.Rig.add(asset);
264
327
  }
265
328
 
266
329
  image.applyToObject(asset);
@@ -279,8 +342,8 @@
279
342
 
280
343
  if (!trackedData.object) continue;
281
344
 
282
- if (this.xr) {
283
- this.xr.Rig.add(trackedData.object);
345
+ if (this.webxr) {
346
+ this.webxr.Rig.add(trackedData.object);
284
347
  }
285
348
 
286
349
  image.applyToObject(trackedData.object);
@@ -289,56 +352,4 @@
289
352
  }
290
353
  }
291
354
  }
292
-
293
- private onXRStarted = (_: any) => {
294
- // clear out all frame counters for tracking
295
- for (const trackedData of this.imageToObjectMap.values()) {
296
- trackedData.frames = 0;
297
- }
298
- };
299
-
300
- private onXRUpdate = (evt): void => {
301
- const frame = evt.frame;
302
- if (!frame) return;
303
-
304
- if (frame.session && !("getImageTrackingResults" in frame)) {
305
- showBalloonWarning("Image tracking is currently not supported on this device. On Chrome for Android, you can enable the <a href=\"chrome://flags/#webxr-incubations\">chrome://flags/#webxr-incubations</a> flag.");
306
- return;
307
- }
308
-
309
- //@ts-ignore
310
- if (frame.session && typeof frame.getImageTrackingResults === "function") {
311
- //@ts-ignore
312
- const results = frame.getImageTrackingResults();
313
- if (results.length) {
314
- const space = this.context.renderer.xr.getReferenceSpace();
315
- if (space) {
316
- const images: WebXRTrackedImage[] = [];
317
- for (const result of results) {
318
- const imageIndex = result.index;
319
- const trackedImage = this.trackedImageIndexMap.get(imageIndex);
320
- if (trackedImage) {
321
- const pose = frame.getPose(result.imageSpace, space);
322
- const state = result.trackingState;
323
- const imageData = new WebXRTrackedImage(this, trackedImage, result.image, result.measuredSize, state, pose);
324
- images.push(imageData);
325
- }
326
- else {
327
- if (debug) {
328
- console.warn("No tracked image for index", imageIndex);
329
- }
330
- }
331
- }
332
- if (images.length > 0) {
333
- try {
334
- this.dispatchEvent(new CustomEvent("image-tracking", { detail: images }));
335
- }
336
- catch (e) {
337
- console.error(e);
338
- }
339
- }
340
- }
341
- }
342
- }
343
- }
344
355
  }