Needle Engine

Changes between version 3.21.4 and 3.21.5
Files changed (7) hide show
  1. src/engine-components/ui/Button.ts +4 -0
  2. src/engine/engine_input.ts +1 -7
  3. src/engine/engine_physics_rapier.ts +0 -2
  4. src/engine-components/ui/EventSystem.ts +8 -2
  5. src/engine-components/timeline/PlayableDirector.ts +41 -30
  6. src/engine-components/webxr/WebXR.ts +31 -9
  7. src/engine-components/webxr/WebXRPlaneTracking.ts +2 -2
src/engine-components/ui/Button.ts CHANGED
@@ -182,6 +182,10 @@
182
182
  super.onEnable();
183
183
  }
184
184
 
185
+ onDestroy(): void {
186
+ if (this._isHovered) this.context.input.setCursorNormal();
187
+ }
188
+
185
189
  private _requestedAnimatorTrigger?: string;
186
190
  private *setAnimatorTriggerAtEndOfFrame(requestedTriggerId: string) {
187
191
  this._requestedAnimatorTrigger = requestedTriggerId;
src/engine/engine_input.ts CHANGED
@@ -572,14 +572,8 @@
572
572
  }
573
573
  // moveEvent?: Event;
574
574
  private onMove(evt: NEPointerEvent) {
575
- // when we get a pointer move event then the click state needs to be reset
576
- // this happens for example if we use touch emulation in chrome and click on an object without using the mouse
577
- // since the EventSystem queries `isPointerClicked` and we get an event for onMove we would then falsely emit
578
- // two onPointerClick events for one click
579
575
  const index = evt.button;
580
- this._pointerClick[index] = false;
581
- this._pointerDoubleClick[index] = false;
582
-
576
+
583
577
  const isDown = this.getPointerPressed(index);
584
578
  if (isDown === false && !this.isInRect(evt)) return;
585
579
  if (evt.pointerType === PointerType.Touch && !isDown) return;
src/engine/engine_physics_rapier.ts CHANGED
@@ -804,7 +804,6 @@
804
804
  col.setDensity(1);
805
805
  }
806
806
  rigidbody.recomputeMassPropertiesFromColliders();
807
- console.log(rigidbody.mass())
808
807
  }
809
808
  else {
810
809
  rigidbody.setAdditionalMass(rb.mass, false);
@@ -813,7 +812,6 @@
813
812
  col.setDensity(0);
814
813
  }
815
814
  rigidbody.recomputeMassPropertiesFromColliders();
816
- console.log(rigidbody.mass())
817
815
  }
818
816
 
819
817
  // https://rapier.rs/docs/user_guides/javascript/rigid_bodies#mass-properties
src/engine-components/ui/EventSystem.ts CHANGED
@@ -227,7 +227,7 @@
227
227
 
228
228
  data.inputSource = this.context.input;
229
229
  data.pointerId = pointerId;
230
- data.isClicked = this.context.input.getPointerClicked(pointerId)
230
+ data.isClicked = pointerEvent.type == InputEvents.PointerUp && this.context.input.getPointerClicked(pointerId)
231
231
  // using the input type directly instead of input API -> otherwise onMove events can sometimes be getPointerUp() == true
232
232
  data.isDown = pointerEvent.type == InputEvents.PointerDown;
233
233
  data.isUp = pointerEvent.type == InputEvents.PointerUp;
@@ -403,7 +403,13 @@
403
403
  }
404
404
 
405
405
  // save hovered object
406
- this.hoveredByID.set(args.pointerId, { obj: object, data: args });
406
+ const entry = this.hoveredByID.get(args.pointerId);
407
+ if (!entry)
408
+ this.hoveredByID.set(args.pointerId, { obj: object, data: args });
409
+ else {
410
+ entry.obj = object;
411
+ entry.data = args;
412
+ }
407
413
 
408
414
  // create / update pressed entry
409
415
  if (args.isDown) {
src/engine-components/timeline/PlayableDirector.ts CHANGED
@@ -191,7 +191,7 @@
191
191
  this._isPlaying = false;
192
192
  if (this._isPaused) return;
193
193
  this._isPaused = true;
194
- this.evaluate();
194
+ this.internalEvaluate();
195
195
  this.invokePauseChangedMethodsOnTracks();
196
196
  this.invokeStateChangedMethodsOnTracks();
197
197
  }
@@ -205,13 +205,13 @@
205
205
  this._time = 0;
206
206
  this._isPlaying = false;
207
207
  this._isPaused = false;
208
- this.evaluate();
208
+ this.internalEvaluate();
209
209
  if (pauseChanged) this.invokePauseChangedMethodsOnTracks();
210
210
  }
211
211
  this._isPlaying = false;
212
212
  this._isPaused = false;
213
213
  if (pauseChanged && !wasPlaying) this.invokePauseChangedMethodsOnTracks();
214
- if(wasPlaying) this.invokeStateChangedMethodsOnTracks();
214
+ if (wasPlaying) this.invokeStateChangedMethodsOnTracks();
215
215
  if (this._internalUpdateRoutine)
216
216
  this.stopCoroutine(this._internalUpdateRoutine);
217
217
  this._internalUpdateRoutine = null;
@@ -219,28 +219,7 @@
219
219
  }
220
220
 
221
221
  evaluate() {
222
- if (!this.isValid()) return;
223
- let t = this._time;
224
- switch (this.extrapolationMode) {
225
- case DirectorWrapMode.Hold:
226
- if (this._speed > 0)
227
- t = Math.min(t, this._duration);
228
- else if (this._speed < 0)
229
- t = Math.max(t, 0);
230
- this._time = t;
231
- break;
232
- case DirectorWrapMode.Loop:
233
- t %= this._duration;
234
- this._time = t;
235
- break;
236
- case DirectorWrapMode.None:
237
- if (t > this._duration) {
238
- this.stop();
239
- return;
240
- }
241
- break;
242
- }
243
- this.internalEvaluate(t);
222
+ this.internalEvaluate(true);
244
223
  }
245
224
 
246
225
  isValid() {
@@ -303,14 +282,46 @@
303
282
  while (this._isPlaying && this.activeAndEnabled) {
304
283
  if (!this._isPaused && this._isPlaying) {
305
284
  this._time += this.context.time.deltaTime * this.speed;
306
- this.evaluate();
285
+ this.internalEvaluate();
307
286
  }
308
287
  // for (let i = 0; i < 5; i++)
309
288
  yield;
310
289
  }
311
290
  }
312
291
 
313
- private internalEvaluate(time: number) {
292
+ /**
293
+ * PlayableDirector lifecycle should always call this instead of "evaluate"
294
+ * @param called_by_user If true the evaluation is called by the user (e.g. via evaluate())
295
+ */
296
+ private internalEvaluate(called_by_user: boolean = false) {
297
+ // when the timeline is called by a user via evaluate() we want to keep updating activation tracks
298
+ // because "isPlaying" might be false but the director is still active. See NE-3737
299
+
300
+ if (!this.isValid()) return;
301
+
302
+ let t = this._time;
303
+ switch (this.extrapolationMode) {
304
+ case DirectorWrapMode.Hold:
305
+ if (this._speed > 0)
306
+ t = Math.min(t, this._duration);
307
+ else if (this._speed < 0)
308
+ t = Math.max(t, 0);
309
+ this._time = t;
310
+ break;
311
+ case DirectorWrapMode.Loop:
312
+ t %= this._duration;
313
+ this._time = t;
314
+ break;
315
+ case DirectorWrapMode.None:
316
+ if (t > this._duration) {
317
+ this.stop();
318
+ return;
319
+ }
320
+ break;
321
+ }
322
+
323
+ const time = this._time;
324
+
314
325
  for (const track of this.playableAsset!.tracks) {
315
326
  if (track.muted) continue;
316
327
  switch (track.type) {
@@ -319,7 +330,7 @@
319
330
  // then we want to leave objects active state as they were
320
331
  // see NE-3241
321
332
  // TODO: support all "post-playback-state" settings an activation track has, this is just "Leave as is"
322
- if (!this._isPlaying) continue;
333
+ if (!called_by_user && !this._isPlaying) continue;
323
334
 
324
335
  for (let i = 0; i < track.outputs.length; i++) {
325
336
  const binding = track.outputs[i];
@@ -518,9 +529,9 @@
518
529
  }
519
530
  // Try to share the mixer with the animator
520
531
  if (binding instanceof Animator && binding.runtimeAnimatorController) {
521
- if(!binding.__internalDidAwakeAndStart) binding.initializeRuntimeAnimatorController();
532
+ if (!binding.__internalDidAwakeAndStart) binding.initializeRuntimeAnimatorController();
522
533
  // Call bind once to ensure the animator is setup and has a mixer
523
- if(!binding.runtimeAnimatorController.mixer) binding.runtimeAnimatorController.bind(binding);
534
+ if (!binding.runtimeAnimatorController.mixer) binding.runtimeAnimatorController.bind(binding);
524
535
  handler.mixer = binding.runtimeAnimatorController.mixer;
525
536
  }
526
537
  // If we can not get the mixer from the animator then create a new one
src/engine-components/webxr/WebXR.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { XRSessionMode } from "../../engine/engine_setup.js";
8
8
  import { getWorldPosition, getWorldQuaternion, setWorldPosition, setWorldQuaternion } from "../../engine/engine_three_utils.js";
9
9
  import { INeedleEngineComponent } from "../../engine/engine_types.js";
10
- import { getParam, isMozillaXR, setOrAddParamsToUrl } from "../../engine/engine_utils.js";
10
+ import { getParam, isMozillaXR, isQuest, setOrAddParamsToUrl } from "../../engine/engine_utils.js";
11
11
 
12
12
  import { Behaviour, GameObject } from "../Component.js";
13
13
  import { noVoip } from "../Voip.js";
@@ -17,6 +17,7 @@
17
17
  import { WebXRSync } from "./WebXRSync.js";
18
18
  import { XRFlag, XRState, XRStateFlag } from "../XRFlag.js";
19
19
  import { showBalloonWarning } from '../../engine/debug/index.js';
20
+ import { isDestroyed } from '../../engine/engine_gameobject.js';
20
21
 
21
22
  const debugWebXR = getParam("debugwebxr");
22
23
 
@@ -154,7 +155,7 @@
154
155
  }
155
156
 
156
157
  public get Rig(): Object3D {
157
- if (!this.rig) this.ensureRig();
158
+ this.ensureRig();
158
159
  return this.rig;
159
160
  }
160
161
 
@@ -214,9 +215,7 @@
214
215
  sync.webXR = this;
215
216
  }
216
217
  this.webAR = new WebAR(this);
217
- }
218
218
 
219
- start() {
220
219
  if (location.protocol == 'http:' && location.host.indexOf('localhost') < 0) {
221
220
  showBalloonWarning("WebXR only works on https");
222
221
  console.warn("WebXR only works on https. https://engine.needle.tools/docs/xr.html");
@@ -284,7 +283,7 @@
284
283
  private _currentHeadPose: XRViewerPose | null = null;
285
284
  public get HeadPose(): XRViewerPose | null { return this._currentHeadPose; }
286
285
 
287
- onBeforeRender(frame) {
286
+ onBeforeRender(frame:XRFrame | null | undefined) {
288
287
  if (!frame) return;
289
288
  // TODO: figure out why screen is black if we enable the code written here
290
289
  // const referenceSpace = renderer.xr.getReferenceSpace();
@@ -292,7 +291,9 @@
292
291
 
293
292
 
294
293
  if (session) {
295
- const pose = frame.getViewerPose(this.context.renderer.xr.getReferenceSpace());
294
+ const referenceSpace = this.context.renderer.xr.getReferenceSpace();
295
+ if(!referenceSpace) return;
296
+ const pose = frame.getViewerPose(referenceSpace);
296
297
  if (!pose) return;
297
298
  this._currentHeadPose = pose;
298
299
  const transform: XRRigidTransform = pose?.transform;
@@ -303,6 +304,11 @@
303
304
  if (WebXR._isInXr === false && session) {
304
305
  this.onEnterXR(session, frame);
305
306
  }
307
+ else if (this.IsInVR) {
308
+ if (this.context.mainCamera) {
309
+ this.ensureRig();
310
+ }
311
+ }
306
312
 
307
313
  for (const ctrl of this.controllers) {
308
314
  ctrl.onUpdate(session);
@@ -364,7 +370,7 @@
364
370
  }
365
371
 
366
372
  private ensureRig() {
367
- if (!this.rig) {
373
+ if (!this.rig || isDestroyed(this.rig)) {
368
374
  // currently just used for pose
369
375
  const xrRig = GameObject.findObjectOfType(XRRig, this.context);
370
376
  if (xrRig) {
@@ -381,6 +387,23 @@
381
387
  this.context.scene.add(this.rig);
382
388
  }
383
389
  }
390
+
391
+ // Make sure the webxr camera is parented to the xr rig
392
+ if (this.context.isInXR && this.context.mainCamera && this.context.mainCamera.parent !== this.rig) {
393
+ this.rig.add(this.context.mainCamera);
394
+
395
+ // Hack: make sure we have the correct position and rotation (e.g. where we are dealing with an implicitly created rig)
396
+ // This handles the case where we switch between multiple scenes
397
+ if (this.IsInVR) {
398
+ const other = GameObject.findObjectOfType(XRRig);
399
+ if (other && other?.gameObject !== this.rig) {
400
+ this.rig.position.copy(other.gameObject.position);
401
+ this.rig.quaternion.copy(other.gameObject.quaternion);
402
+ this.rig.rotateY(Math.PI);
403
+ this.rig.scale.copy(other.gameObject.scale);
404
+ }
405
+ }
406
+ }
384
407
  }
385
408
 
386
409
 
@@ -429,7 +452,6 @@
429
452
  }
430
453
  cam.layers.enableAll();
431
454
  }
432
- this.rig.add(this.context.mainCamera);
433
455
  if (this._requestedAR) {
434
456
  this.context.scene.add(this.rig);
435
457
  }
@@ -597,7 +619,7 @@
597
619
  this.didPlaceARSessionRoot = false;
598
620
  this.getAROverlayContainer();
599
621
 
600
- const deviceType = navigator.userAgent?.includes("OculusBrowser") ? ControllerType.PhysicalDevice : ControllerType.Touch;
622
+ const deviceType = isQuest() ? ControllerType.PhysicalDevice : ControllerType.Touch;
601
623
  const controllerCount = deviceType === ControllerType.Touch ? 4 : 2;
602
624
  for (let i = 0; i < controllerCount; i++) {
603
625
  WebXRController.Create(this.webxr, i, this.webxr.gameObject as GameObject, deviceType)
src/engine-components/webxr/WebXRPlaneTracking.ts CHANGED
@@ -56,12 +56,12 @@
56
56
 
57
57
  onEnable(): void {
58
58
  WebXR.addEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
59
- WebXR.addEventListener("modify-ar-options", this.onModifyAROptions);
59
+ WebXR.addEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
60
60
  }
61
61
 
62
62
  onDisable(): void {
63
63
  WebXR.removeEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
64
- WebXR.removeEventListener("modify-ar-options", this.onModifyAROptions);
64
+ WebXR.removeEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
65
65
  }
66
66
 
67
67
  private onModifyAROptions = (event: any) => {