Needle Engine

Changes between version 3.32.0-alpha and 3.32.1-alpha
Files changed (13) hide show
  1. src/engine-components/webxr/Avatar.ts +1 -1
  2. src/engine-components/ui/Canvas.ts +28 -11
  3. src/engine-components/Component.ts +3 -3
  4. src/engine/engine_addressables.ts +3 -3
  5. src/engine/engine_context.ts +12 -8
  6. src/engine/engine_gameobject.ts +14 -6
  7. src/engine/engine_input.ts +5 -5
  8. src/engine/engine_networking.ts +7 -4
  9. src/engine/engine_types.ts +10 -0
  10. src/engine/xr/NeedleXRController.ts +56 -13
  11. src/engine/xr/NeedleXRSession.ts +20 -11
  12. src/engine-components/SyncedTransform.ts +16 -0
  13. src/engine-components/webxr/WebXR.ts +10 -6
src/engine-components/webxr/Avatar.ts CHANGED
@@ -41,7 +41,7 @@
41
41
  marker.avatar = this.gameObject;
42
42
  marker.connectionId = playerstate.owner;
43
43
  }
44
- else console.error("No player state found for avatar", this);
44
+ else if(this.context.connection.isConnected) console.error("No player state found for avatar", this);
45
45
  }
46
46
 
47
47
  onLeaveXR(_args: NeedleXREventArgs): void {
src/engine-components/ui/Canvas.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  import { getParam } from "../../engine/engine_utils.js";
13
13
  import { LayoutGroup } from "./Layout.js";
14
14
  import { Mathf } from "../../engine/engine_math.js";
15
+ import { NeedleXREventArgs } from "../../engine/xr/index.js";
15
16
 
16
17
  export enum RenderMode {
17
18
  ScreenSpaceOverlay = 0,
@@ -200,19 +201,30 @@
200
201
  }
201
202
  }
202
203
 
204
+ onEnterXR(args: NeedleXREventArgs): void {
205
+ if (this.screenspace) {
206
+ if (args.xr.isVR || args.xr.isPassThrough) {
207
+ this.gameObject.visible = false;
208
+ }
209
+ }
210
+ }
211
+ onLeaveXR(args: NeedleXREventArgs): void {
212
+ if (this.screenspace) {
213
+ if (args.xr.isVR || args.xr.isPassThrough) {
214
+ this.gameObject.visible = true;
215
+ }
216
+ }
217
+ }
218
+
203
219
  onBeforeRenderRoutine = () => {
204
- if (this.context.isInVR) {
205
- this.onUpdateRenderMode();
206
- this.handleLayoutUpdates();
207
- // TODO TMUI @swingingtom - For VR this is so we don't have text clipping
208
- this.shadowComponent?.updateMatrixWorld(true);
209
- this.shadowComponent?.updateWorldMatrix(true, true);
210
- this.invokeBeforeRenderEvents();
211
- EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context, true);
220
+ this.previousParent = this.gameObject.parent;
221
+ if ((this.context.xr?.isVR || this.context.xr?.isPassThrough) && this.screenspace) {
222
+ // see https://linear.app/needle/issue/NE-4114
223
+ this.gameObject.visible = false;
224
+ this.gameObject.removeFromParent();
212
225
  return;
213
226
  }
214
227
 
215
- this.previousParent = this.gameObject.parent;
216
228
  // console.log(this.previousParent?.name + "/" + this.gameObject.name);
217
229
 
218
230
  if (this.renderOnTop || this.screenspace) {
@@ -231,7 +243,12 @@
231
243
  }
232
244
 
233
245
  onAfterRenderRoutine = () => {
234
- if(this.context.isInVR) return;
246
+ if ((this.context.xr?.isVR || this.context.xr?.isPassThrough) && this.screenspace) {
247
+ this.previousParent?.add(this.gameObject);
248
+ // this is currently causing an error during XR (https://linear.app/needle/issue/NE-4114)
249
+ // this.gameObject.visible = true;
250
+ return;
251
+ }
235
252
  if ((this.screenspace || this.renderOnTop) && this.previousParent && this.context.mainCamera) {
236
253
  if (this.screenspace) {
237
254
  const camObj = this.context.mainCamera;
@@ -276,7 +293,7 @@
276
293
  for (const ch of this._rectTransforms) {
277
294
  if (matrixWorldChanged) ch.markDirty();
278
295
  let layout = this._layoutGroups.get(ch.gameObject);
279
- if(ch.isDirty && !layout){
296
+ if (ch.isDirty && !layout) {
280
297
  layout = ch.gameObject.getComponentInParent(LayoutGroup) as LayoutGroup;
281
298
  }
282
299
  if (ch.isDirty || layout?.isDirty) {
src/engine-components/Component.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import { syncDestroy, syncInstantiate } from "../engine/engine_networking_instantiate.js";
7
7
  import type { ConstructorConcrete, SourceIdentifier, IComponent, IGameObject, Constructor, GuidsMap, Collision, ICollider } from "../engine/engine_types.js";
8
8
  import { addNewComponent, destroyComponentInstance, findObjectOfType, findObjectsOfType, getComponent, getComponentInChildren, getComponentInParent, getComponents, getComponentsInChildren, getComponentsInParent, getOrAddComponent, moveComponentInstance, removeComponent } from "../engine/engine_components.js";
9
- import { findByGuid, destroy, InstantiateOptions, instantiate, HideFlags, foreachComponent, markAsInstancedRendered, isActiveInHierarchy, isActiveSelf, isUsingInstancing, setActive, isDestroyed } from "../engine/engine_gameobject.js";
9
+ import { findByGuid, destroy, InstantiateOptions, instantiate, HideFlags, foreachComponent, markAsInstancedRendered, isActiveInHierarchy, isActiveSelf, isUsingInstancing, setActive, isDestroyed, IInstantiateOptions } from "../engine/engine_gameobject.js";
10
10
 
11
11
  import { Euler, Object3D, Quaternion, Scene, Vector3 } from "three";
12
12
  import { showBalloonWarning, isDevEnvironment } from "../engine/debug/index.js";
@@ -74,7 +74,7 @@
74
74
  * @param instance object to instantiate
75
75
  * @param opts options for the instantiation
76
76
  */
77
- public static instantiateSynced(instance: GameObject | Object3D | null, opts: InstantiateOptions): GameObject | null {
77
+ public static instantiateSynced(instance: GameObject | Object3D | null, opts: IInstantiateOptions): GameObject | null {
78
78
  if (!instance) return null;
79
79
  return syncInstantiate(instance as any, opts) as GameObject | null;
80
80
  }
@@ -83,7 +83,7 @@
83
83
  * @param instance object to instantiate
84
84
  * @param opts options for the instantiation (e.g. with what parent, position, etc.)
85
85
  */
86
- public static instantiate(instance: GameObject | Object3D | null, opts: InstantiateOptions | null = null): GameObject | null {
86
+ public static instantiate(instance: GameObject | Object3D | null, opts: IInstantiateOptions | null = null): GameObject | null {
87
87
  return instantiate(instance, opts) as GameObject | null;
88
88
  }
89
89
 
src/engine/engine_addressables.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { download } from "./engine_web_api.js";
8
8
  import { getLoader } from "./engine_gltf.js";
9
9
  import type { IComponent, IGameObject, SourceIdentifier } from "./engine_types.js";
10
- import { destroy, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
10
+ import { destroy, IInstantiateOptions, instantiate, InstantiateOptions, isDestroyed } from "./engine_gameobject.js";
11
11
 
12
12
  const debug = getParam("debugaddressables");
13
13
 
@@ -245,12 +245,12 @@
245
245
  }
246
246
 
247
247
  /** loads and returns a new instance of `asset` */
248
- async instantiate(parent?: Object3D | InstantiateOptions) {
248
+ async instantiate(parent?: Object3D | IInstantiateOptions) {
249
249
  return this.onInstantiate(parent, false);
250
250
  }
251
251
 
252
252
  /** loads and returns a new instance of `asset` - this call is networked so an instance will be created on all connected users */
253
- async instantiateSynced(parent?: Object3D | InstantiateOptions, saveOnServer: boolean = true) {
253
+ async instantiateSynced(parent?: Object3D | IInstantiateOptions, saveOnServer: boolean = true) {
254
254
  return this.onInstantiate(parent, true, saveOnServer);
255
255
  }
256
256
 
src/engine/engine_context.ts CHANGED
@@ -26,7 +26,7 @@
26
26
  import { LightDataRegistry, type ILightDataRegistry } from './engine_lightdata.js';
27
27
  import { PlayerViewManager } from './engine_playerview.js';
28
28
 
29
- import { type CoroutineData, type GLTF, type ICamera, type IComponent, type IContext, type ILight, type LoadedGLTF } from "./engine_types.js";
29
+ import { INeedleXRSession, type CoroutineData, type GLTF, type ICamera, type IComponent, type IContext, type ILight, type LoadedGLTF } from "./engine_types.js";
30
30
  import { destroy, foreachComponent } from './engine_gameobject.js';
31
31
  import { ContextEvent, ContextRegistry } from './engine_context_registry.js';
32
32
  import { delay, getParam } from './engine_utils.js';
@@ -247,10 +247,14 @@
247
247
  return this._domY;
248
248
  }
249
249
  get isInXR() { return this.renderer?.xr?.isPresenting || false; }
250
- // TODO: can we get the session mode from the xr session without relying on the initiator to set it?
251
- xrSessionMode: XRSessionMode | undefined = undefined;
250
+ /** shorthand for `NeedleXRSession.active`
251
+ * Automatically set by NeedleXRSession when a XR session is active */
252
+ xr: INeedleXRSession | null = null;
253
+ get xrSessionMode() { return this.xr?.mode; }
252
254
  get isInVR() { return this.xrSessionMode === "immersive-vr"; }
253
255
  get isInAR() { return this.xrSessionMode === "immersive-ar"; }
256
+ /** If a XR session is active and in pass through mode (immersive-ar on e.g. Quest) */
257
+ get isInPassThrough() { return this.xr ? this.xr.isPassThrough : false; }
254
258
  /** access the raw `XRSession` object (shorthand for `context.renderer.xr.getSession()`). For more control use `NeedleXRSession.active` */
255
259
  get xrSession() { return this.renderer?.xr?.getSession(); }
256
260
  /** @returns the latest XRFrame (if a XRSession is currently active)
@@ -438,7 +442,7 @@
438
442
 
439
443
  private _disposeCallbacks: Function[] = [];
440
444
 
441
-
445
+
442
446
  /** will request a renderer size update the next render call (will call updateSize the next update) */
443
447
  requestSizeUpdate() { this._sizeChanged = true; }
444
448
 
@@ -495,7 +499,7 @@
495
499
  async create(opts?: ContextCreateArgs) {
496
500
  try {
497
501
  this._isCreating = true;
498
- if(opts !== this._originalCreationArgs)
502
+ if (opts !== this._originalCreationArgs)
499
503
  this._originalCreationArgs = utils.deepClone(opts);
500
504
  window.addEventListener("unhandledrejection", this.onUnhandledRejection)
501
505
  const res = await this.internalOnCreate(opts);
@@ -725,7 +729,7 @@
725
729
  private async internalOnCreate(opts?: ContextCreateArgs) {
726
730
  const createId = ++this._createId;
727
731
 
728
- if(debug) console.log("Creating context", this.name, opts);
732
+ if (debug) console.log("Creating context", this.name, opts);
729
733
 
730
734
  this.clear();
731
735
  // stop the animation loop if its running during creation
@@ -920,7 +924,7 @@
920
924
  }
921
925
 
922
926
  args?.onLoadingStart?.call(this, i, file);
923
- if(debug) console.log("Context Load " + file);
927
+ if (debug) console.log("Context Load " + file);
924
928
  const res = await loader.loadSync(this, file, file, loadingHash, prog => {
925
929
  progressArg.name = file;
926
930
  progressArg.progress = prog;
@@ -996,7 +1000,7 @@
996
1000
  catch (err) {
997
1001
  this._renderlooperrors += 1;
998
1002
  if ((isDevEnvironment() || debug) && (err instanceof Error || err instanceof TypeError))
999
- showBalloonMessage("Caught unhandled exception during render-loop.<br/>Stopping renderloop...<br/>See console for details.", LogType.Error);
1003
+ showBalloonMessage(`Caught unhandled exception during render-loop - see console for details.`, LogType.Error);
1000
1004
  console.error(err);
1001
1005
  if (this._renderlooperrors > 10) {
1002
1006
  console.warn("Stopping render loop due to error")
src/engine/engine_gameobject.ts CHANGED
@@ -28,20 +28,28 @@
28
28
  HideAndDontSave = DontSave | NotEditable | HideInHierarchy, // 0x0000003D
29
29
  }
30
30
 
31
+ export type IInstantiateOptions = {
32
+ idProvider?: UIDProvider;
33
+ //** parent guid or object */
34
+ parent?: string | Object3D;
35
+ position?: Vector3;
36
+ /** for duplicatable parenting */
37
+ keepWorldPosition?: boolean;
38
+ rotation?: Quaternion;
39
+ scale?: Vector3;
40
+ /** if the instantiated object should be visible */
41
+ visible?: boolean;
42
+ context?: Context;
43
+ }
31
44
 
32
- export class InstantiateOptions {
45
+ export class InstantiateOptions implements IInstantiateOptions {
33
46
  idProvider?: UIDProvider | undefined;
34
-
35
- //** parent guid */
36
47
  parent?: string | undefined | Object3D;
37
- /** for duplicatable parenting */
38
48
  keepWorldPosition?: boolean
39
49
  position?: Vector3 | undefined;
40
50
  rotation?: Quaternion | undefined;
41
51
  scale?: Vector3 | undefined;
42
-
43
52
  visible?: boolean | undefined;
44
-
45
53
  context?: Context | undefined;
46
54
  }
47
55
 
src/engine/engine_input.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  ray?: Ray;
14
14
  /** The control object for this input. In the case of spatial devices the controller,
15
15
  * otherwise a generated object in screen space. The object may not be in the scene. */
16
- device: Object3D;
16
+ device: IGameObject;
17
17
  buttonName: ButtonName | "none";
18
18
  }
19
19
 
@@ -26,7 +26,7 @@
26
26
  /** A ray in worldspace for the event */
27
27
  readonly ray?: Ray;
28
28
  /** The device space (this object is not necessarily rendered in the scene but you can access or copy the matrix) */
29
- readonly space: Object3D;
29
+ readonly space: IGameObject;
30
30
 
31
31
  isClick: boolean = false;
32
32
  isDoubleClick: boolean = false;
@@ -335,7 +335,7 @@
335
335
  private _pointerEvent: Event[] = [];
336
336
  private _pointerUsed: boolean[] = [];
337
337
  /** This is added/updated for pointers. screenspace pointers set this to the camera near plane */
338
- private _pointerSpace: Object3D[] = [];
338
+ private _pointerSpace: IGameObject[] = [];
339
339
 
340
340
  getKeyDown(): string | null {
341
341
  for (const key in this.keysPressed) {
@@ -658,10 +658,10 @@
658
658
  private readonly tempNearPlaneVector = new Vector3();
659
659
  private readonly tempFarPlaneVector = new Vector3();
660
660
  private readonly tempLookMatrix = new Matrix4();
661
- private getAndUpdateSpatialObjectForScreenPosition(id: number, screenX: number, screenY: number): Object3D {
661
+ private getAndUpdateSpatialObjectForScreenPosition(id: number, screenX: number, screenY: number): IGameObject {
662
662
  let space = this._pointerSpace[id]
663
663
  if (!space) {
664
- space = new Object3D();
664
+ space = new Object3D() as unknown as IGameObject;
665
665
  this._pointerSpace[id] = space;
666
666
  }
667
667
  this._pointerSpace[id] = space;
src/engine/engine_networking.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  const defaultNetworkingBackendUrlProvider = "https://urls.needle.tools/default-networking-backend/index";
2
2
  let serverUrl: string | undefined = "wss://needle-tiny-starter.glitch.me/socket";
3
3
 
4
- import { Websocket, type WebsocketBuilder } from 'websocket-ts';
4
+ import { type Websocket } from 'websocket-ts';
5
5
  // import { Networking } from '../engine-components/Networking.js';
6
6
  import { Context } from './engine_setup.js';
7
7
  import * as utils from "./engine_utils.js";
@@ -14,6 +14,7 @@
14
14
 
15
15
  export const debugNet = utils.getParam("debugnet") ? true : false;
16
16
  export const debugOwner = debugNet || utils.getParam("debugowner") ? true : false;
17
+ const debugnetBin = utils.getParam("debugnetbin");
17
18
 
18
19
  export interface INetworkingWebsocketUrlProvider {
19
20
  getWebsocketUrl(): string | null;
@@ -389,7 +390,7 @@
389
390
 
390
391
  /** Send a binary message to the server (broadcasted to all connected users) */
391
392
  public sendBinary(bin: Uint8Array) {
392
- if (debugNet) console.log("<< bin", bin.length);
393
+ if (debugnetBin) console.log("<< send binary", this.context.time.frame, (bin.length / 1024) + " KB");
393
394
  this._ws?.send(bin);
394
395
  }
395
396
 
@@ -547,10 +548,11 @@
547
548
  console.error("⊠ Websocket error", i, ev);
548
549
  resolve(false);
549
550
  })
550
- .onMessage(this.onMessage.bind(this))
551
551
  .onRetry(() => { console.log("Retry connecting to networking websocket") })
552
552
  .build();
553
-
553
+ ws.addEventListener(pkg.WebsocketEvent.message, (socket, msg) => {
554
+ this.onMessage(socket, msg);
555
+ });
554
556
  });
555
557
  }
556
558
 
@@ -581,6 +583,7 @@
581
583
  }
582
584
 
583
585
  private async handleIncomingBinaryMessage(blob: Blob) {
586
+ if (debugnetBin) console.log("<< bin", this.context.time.frame);
584
587
  const buf = await blob.arrayBuffer();
585
588
  var data = new Uint8Array(buf);
586
589
  const bb = new flatbuffers.ByteBuffer(data);
src/engine/engine_types.ts CHANGED
@@ -97,6 +97,16 @@
97
97
  stopAllCoroutinesFrom(script: IComponent);
98
98
  }
99
99
 
100
+ export interface INeedleXRSession {
101
+ get running(): boolean;
102
+ readonly mode: XRSessionMode;
103
+ readonly session: XRSession;
104
+
105
+ get isVR();
106
+ get isAR();
107
+ get isPassThrough();
108
+ }
109
+
100
110
  export declare interface INeedleEngineComponent extends HTMLElement {
101
111
  getAROverlayContainer(): HTMLElement;
102
112
  onEnterAR(session: XRSession, overlayContainer: HTMLElement);
src/engine/xr/NeedleXRController.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { AxesHelper, Object3D, Quaternion, Ray, Vector3 } from "three";
1
+ import { AxesHelper, Int8BufferAttribute, Object3D, Quaternion, Ray, Vector3 } from "three";
2
2
  import { MotionController, fetchProfile } from "@webxr-input-profiles/motion-controllers";
3
- import type { ButtonName, Vec3, XRControllerButtonName } from "../engine_types.js";
3
+ import type { ButtonName, IGameObject, Vec3, XRControllerButtonName } from "../engine_types.js";
4
4
  import { Context } from "../engine_context.js";
5
5
  import { Gizmos } from "../engine_gizmos.js";
6
6
  import { InputEvents, NEPointerEventInit, PointerType, NEPointerEvent } from "../engine_input.js";
@@ -178,7 +178,7 @@
178
178
  * Children will be automatically detached and put into the scene when the controller disconnects
179
179
  */
180
180
  get object() { return this._object; }
181
- private readonly _object: Object3D;
181
+ private readonly _object: IGameObject;
182
182
 
183
183
  private readonly _debugAxesHelper = new AxesHelper(.03);
184
184
 
@@ -191,7 +191,7 @@
191
191
  this.xr = session;
192
192
  this.inputSource = device;
193
193
  this.index = index;
194
- this._object = new Object3D();
194
+ this._object = new Object3D() as unknown as IGameObject;
195
195
  if (debug)
196
196
  this._object.add(this._debugAxesHelper);
197
197
  this.xr.context.scene.add(this._object);
@@ -321,7 +321,7 @@
321
321
  * @param key the controller button name e.g. x-button
322
322
  * @returns the gamepad button if it exists on the controller - otherwise undefined
323
323
  */
324
- getButton(key: ButtonName | "primary-button" | "primary"): GamepadButton | undefined {
324
+ getButton(key: ButtonName | "primary-button" | "primary"): NeedleGamepadButton | undefined {
325
325
  if (!this._layout) return undefined;
326
326
 
327
327
  switch (key) {
@@ -331,21 +331,21 @@
331
331
  else return undefined;
332
332
  break;
333
333
  case "primary":
334
- return this.inputSource.gamepad?.buttons[0];
334
+ return this.toNeedleGamepadButton(0);
335
335
  }
336
336
 
337
337
 
338
- if (this._buttonMap.has(key))
339
- return this._buttonMap.get(key)!;
338
+ if (this._buttonMap.has(key)) {
339
+ return this.toNeedleGamepadButton(this._buttonMap.get(key)!);
340
+ }
340
341
  const componentModel = this._layout?.components[key];
341
342
  if (componentModel?.gamepadIndices) {
342
343
  switch (componentModel.type) {
343
344
  case "button":
344
345
  if (this.inputSource.gamepad) {
345
346
  const index = componentModel.gamepadIndices!.button!;
346
- const button = this.inputSource.gamepad?.buttons[index];
347
- this._buttonMap.set(key, button);
348
- return button;
347
+ this._buttonMap.set(key, index);
348
+ return this.toNeedleGamepadButton(index);
349
349
  }
350
350
  break;
351
351
  default:
@@ -357,6 +357,25 @@
357
357
  return undefined;
358
358
  }
359
359
 
360
+ private readonly _needleGamepadButtons = new Array<NeedleGamepadButton>();
361
+ /** combine the InputState information + the GamepadButton information (since GamepadButtons can not be extended) */
362
+ private toNeedleGamepadButton(index: number): NeedleGamepadButton {
363
+ const button = this.inputSource.gamepad?.buttons[index];
364
+ const state = this.states[index];
365
+ const needleButton = this._needleGamepadButtons[index] || new NeedleGamepadButton();
366
+ if (button) {
367
+ needleButton.pressed = button.pressed;
368
+ needleButton.value = button.value;
369
+ needleButton.touched = button.touched;
370
+ }
371
+ if (state) {
372
+ needleButton.isDown = state.isDown;
373
+ needleButton.isUp = state.isUp;
374
+ }
375
+ this._needleGamepadButtons[index] = needleButton;
376
+ return needleButton;
377
+ }
378
+
360
379
  /**
361
380
  * Get the values of a controller joystick
362
381
  * @link https://github.com/immersive-web/webxr-gamepads-module/blob/main/gamepads-module-explainer.md
@@ -394,7 +413,7 @@
394
413
  }
395
414
 
396
415
 
397
- private readonly _buttonMap = new Map<ButtonName, GamepadButton>();
416
+ private readonly _buttonMap = new Map<ButtonName, number>();
398
417
 
399
418
  // the motion controller contains the controller scheme, we use this to simplify button access
400
419
  private _motioncontroller?: MotionController;
@@ -506,11 +525,19 @@
506
525
  // is down
507
526
  if (button.pressed && !state.pressed) {
508
527
  inputEvent = InputEvents.PointerDown;
528
+ state.isDown = true;
529
+ state.isUp = false;
509
530
  }
510
531
  // is up
511
532
  else if (!button.pressed && state.pressed) {
512
533
  inputEvent = InputEvents.PointerUp;
534
+ state.isDown = false;
535
+ state.isUp = true;
513
536
  }
537
+ else {
538
+ state.isDown = false;
539
+ state.isUp = false;
540
+ }
514
541
 
515
542
  state.value = button.value;
516
543
  state.pressed = button.pressed;
@@ -553,6 +580,22 @@
553
580
  }
554
581
 
555
582
  class InputState {
583
+ /** if the button was pressed the last update */
584
+ isDown: boolean = false;
585
+ /** if the button was released the last update */
586
+ isUp: boolean = false;
587
+
556
588
  pressed: boolean = false;
557
589
  value: number = 0;
558
- }
590
+ };
591
+
592
+ /** Enhanced GamepadButton with `isDown` and `isUp` information */
593
+ class NeedleGamepadButton {
594
+ touched: boolean = false;
595
+ pressed: boolean = false;
596
+ value: number = 0;
597
+ /** was the button just pressed down the last update */
598
+ isDown: boolean = false;
599
+ /** was the button just released the last update */
600
+ isUp: boolean = false;
601
+ }
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import type { IXRRig } from "./XRRig.js";
8
8
  import { ImplictXRRig, flipForwardMatrix, flipForwardQuaternion } from "./internal.js";
9
9
  import { delay, getParam, isDesktop, isQuest } from "../engine_utils.js";
10
- import { ICamera, IComponent } from "../engine_types.js";
10
+ import { ICamera, IComponent, INeedleXRSession } from "../engine_types.js";
11
11
  import { NeedleXRSync } from "./NeedleXRSync.js";
12
12
  import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../debug/index.js";
13
13
  import { registerFrameEventCallback, unregisterFrameEventCallback } from "../engine_lifecycle_functions_internal.js";
@@ -131,7 +131,7 @@
131
131
  * - Listen to XRSession controller removed events with `NeedleXRSession.onControllerRemoved(...)`
132
132
  *
133
133
  */
134
- export class NeedleXRSession {
134
+ export class NeedleXRSession implements INeedleXRSession {
135
135
 
136
136
  private static _sync: NeedleXRSync | null = null;
137
137
  static getXRSync(context: Context) {
@@ -386,7 +386,7 @@
386
386
  }
387
387
 
388
388
  /** Returns true if the xr session is still active */
389
- get running() { return !this._ended && this.session; }
389
+ get running(): boolean { return !this._ended && this.session != null; }
390
390
 
391
391
  /**
392
392
  * @link https://developer.mozilla.org/en-US/docs/Web/API/XRSession
@@ -438,6 +438,9 @@
438
438
  if (this.controllers.some(c => c.inputSource.targetRayMode === "tracked-pointer"))
439
439
  return true;
440
440
  }
441
+ if (isDevEnvironment() && isDesktop() && this.mode === "immersive-ar") {
442
+ return true;
443
+ }
441
444
  return false;
442
445
  }
443
446
  get isAR() { return this.mode === 'immersive-ar'; }
@@ -658,9 +661,9 @@
658
661
  this.session = session;
659
662
  this.mode = mode;
660
663
  this.context = context;
661
- this.context.xrSessionMode = this.mode;
662
664
  this.context.renderer.xr.enabled = true;
663
665
  this.context.renderer.xr.setSession(this.session);
666
+ this.context.xr = this;
664
667
 
665
668
  this._xr_scripts = [...extra.scripts];
666
669
  this._xr_update_scripts = this._xr_scripts.filter(e => typeof e.onUpdateXR === "function");
@@ -767,8 +770,8 @@
767
770
  const index2 = this.context.post_render_callbacks.indexOf(this.onAfter);
768
771
  if (index2 >= 0) this.context.post_render_callbacks.splice(index2, 1);
769
772
 
773
+ this.context.xr = null;
770
774
  this.context.renderer.xr.enabled = false;
771
- this.context.xrSessionMode = undefined;
772
775
  this.context.mainCameraComponent?.applyClearFlags();
773
776
 
774
777
  // make sure we disconnect all controllers
@@ -834,6 +837,9 @@
834
837
  const frame = context.xrFrame;
835
838
  if (!frame) return;
836
839
 
840
+ // ensure that XR is always set to a running session
841
+ this.context.xr = this;
842
+
837
843
  // ensure that we always have the correct main camera reference
838
844
  // we need to store the main camera reference here because the main camera can be disabled and then it's removed from the context
839
845
  // but in XR we want to ensure we always have a active camera (or at least parent the camera to the active rig)
@@ -911,6 +917,7 @@
911
917
  continue;
912
918
  }
913
919
  if (!script.activeAndEnabled) {
920
+ this.context.new_scripts_xr.splice(i, 1);
914
921
  this.markInactive(script);
915
922
  continue;
916
923
  }
@@ -1017,10 +1024,10 @@
1017
1024
 
1018
1025
  /** mark a script as inactive and invokes callbacks */
1019
1026
  private markInactive(script: INeedleXRSessionEventReceiver) {
1020
- if (this._inactive_scripts.includes(script)) return;
1027
+ if (this._inactive_scripts.indexOf(script) >= 0) return;
1028
+ // inactive scripts should not receive any regular callbacks anymore
1029
+ this.removeScript(script, false);
1021
1030
  this._inactive_scripts.push(script);
1022
- // inactive scripts should not receive any regular callbacks anymore
1023
- this.removeScript(script);
1024
1031
  // inactive scripts receive callbacks as if the XR session has ended
1025
1032
  for (const ctrl of this.controllers) this.invokeCallback_ControllerRemoved(script, ctrl);
1026
1033
  this.invokeCallback_LeaveXR(script);
@@ -1041,14 +1048,16 @@
1041
1048
 
1042
1049
  private readonly _script_to_remove: INeedleXRSessionEventReceiver[] = [];
1043
1050
 
1044
- private removeScript(script: INeedleXRSessionEventReceiver) {
1051
+ private removeScript(script: INeedleXRSessionEventReceiver, removeCompletely: boolean = true) {
1045
1052
  if (debug) console.log("Remove XRScript", script);
1046
1053
  const index = this._xr_scripts.indexOf(script);
1047
1054
  if (index >= 0) this._xr_scripts.splice(index, 1);
1048
1055
  const index2 = this._xr_update_scripts.indexOf(script);
1049
1056
  if (index2 >= 0) this._xr_update_scripts.splice(index2, 1);
1050
- const index3 = this._inactive_scripts.indexOf(script);
1051
- if (index3 >= 0) this._inactive_scripts.splice(index3, 1);
1057
+ if (removeCompletely) {
1058
+ const index3 = this._inactive_scripts.indexOf(script);
1059
+ if (index3 >= 0) this._inactive_scripts.splice(index3, 1);
1060
+ }
1052
1061
  }
1053
1062
 
1054
1063
  private invokeCallback_EnterXR(script: INeedleXRSessionEventReceiver) {
src/engine-components/SyncedTransform.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  import { Transform } from '../engine-schemes/transform.js';
11
11
  import { registerBinaryType } from '../engine-schemes/schemes.js';
12
12
  import { setWorldEuler } from '../engine/engine_three_utils.js';
13
+ import { onUpdate } from '../engine/engine_lifecycle_api.js';
13
14
 
14
15
  const debug = utils.getParam("debugsync");
15
16
  export const SyncedTransformIdentifier = "STRS";
@@ -35,8 +36,19 @@
35
36
  }
36
37
 
37
38
 
39
+ let FAST_ACTIVE_SYNCTRANSFORMS = 0;
40
+ let FAST_INTERVAL = 0;
41
+ onUpdate((ctx) => {
42
+ const isRunningOnGlitch = ctx.connection.currentServerUrl?.includes("glitch");
43
+ const threshold = isRunningOnGlitch ? 10 : 40;
44
+ FAST_INTERVAL = Math.floor(FAST_ACTIVE_SYNCTRANSFORMS / threshold);
45
+ FAST_ACTIVE_SYNCTRANSFORMS = 0;
46
+ if(debug && FAST_INTERVAL > 0) console.log("Sync Transform Fast Interval", FAST_INTERVAL);
47
+ })
48
+
38
49
  export class SyncedTransform extends Behaviour {
39
50
 
51
+
40
52
  // public autoOwnership: boolean = true;
41
53
  public overridePhysics: boolean = true
42
54
  public interpolatePosition: boolean = true;
@@ -293,8 +305,12 @@
293
305
 
294
306
  const updateInterval = 10;
295
307
  const fastUpdate = this.rb || this.fastMode;
308
+
296
309
  if (this._needsUpdate && (updateInterval <= 0 || updateInterval > 0 && this.context.time.frameCount % updateInterval === 0 || fastUpdate)) {
297
310
 
311
+ FAST_ACTIVE_SYNCTRANSFORMS++;
312
+ if (fastUpdate && FAST_INTERVAL > 0 && this.context.time.frameCount % FAST_INTERVAL !== 0) return;
313
+
298
314
  if (debug)
299
315
  console.log("send update", this.context.connection.connectionId, this.guid, this.gameObject.name, this.gameObject.guid);
300
316
 
src/engine-components/webxr/WebXR.ts CHANGED
@@ -108,13 +108,17 @@
108
108
  }
109
109
 
110
110
  private async handleOfferSession() {
111
- const hasVRSupport = await NeedleXRSession.isVRSupported();
112
- if (hasVRSupport) {
113
- return NeedleXRSession.offerSession("immersive-vr", "default", this.context);
111
+ if (this.createVRButton) {
112
+ const hasVRSupport = await NeedleXRSession.isVRSupported();
113
+ if (hasVRSupport && this.createVRButton) {
114
+ return NeedleXRSession.offerSession("immersive-vr", "default", this.context);
115
+ }
114
116
  }
115
- const hasARSupport = await NeedleXRSession.isARSupported();
116
- if (hasARSupport) {
117
- return NeedleXRSession.offerSession("immersive-ar", "default", this.context);
117
+ if (this.createARButton) {
118
+ const hasARSupport = await NeedleXRSession.isARSupported();
119
+ if (hasARSupport && this.createARButton) {
120
+ return NeedleXRSession.offerSession("immersive-ar", "default", this.context);
121
+ }
118
122
  }
119
123
  return false;
120
124
  }