Needle Engine

Changes between version 3.11.0-beta and 3.11.1-beta
Files changed (60) hide show
  1. src/engine-components/export/usdz/extensions/Animation.ts +3 -3
  2. src/engine-components/AnimationUtils.ts +8 -4
  3. src/engine-components/avatar/Avatar_MustacheShake.ts +1 -1
  4. src/engine-components/ui/BaseUIComponent.ts +1 -1
  5. src/engine-components/BasicIKConstraint.ts +12 -12
  6. src/engine-components/Camera.ts +1 -1
  7. src/engine-components/ui/Canvas.ts +1 -1
  8. src/engine-components/CharacterController.ts +1 -1
  9. src/engine/debug/debug_overlay.ts +1 -1
  10. src/engine-components/Duplicatable.ts +1 -1
  11. src/engine/engine_addressables.ts +1 -1
  12. src/engine/engine_context.ts +8 -4
  13. src/engine/engine_element_loading.ts +9 -3
  14. src/engine/engine_element.ts +38 -14
  15. src/engine/engine_gameobject.ts +1 -2
  16. src/engine/engine_hot_reload.ts +1 -1
  17. src/engine/engine_input_utils.ts +1 -1
  18. src/engine/engine_input.ts +3 -3
  19. src/engine/engine_license.ts +4 -4
  20. src/engine/engine_networking_auto.ts +1 -1
  21. src/engine/engine_networking_peer.ts +1 -1
  22. src/engine/engine_networking.ts +1 -1
  23. src/engine/engine_patcher.ts +1 -1
  24. src/engine/engine_physics_rapier.ts +2 -2
  25. src/engine/engine_scenelighting.ts +1 -1
  26. src/engine/engine_serialization_builtin_serializer.ts +3 -4
  27. src/engine/engine_serialization_core.ts +3 -3
  28. src/engine/engine_utils_screenshot.ts +35 -1
  29. src/engine/engine_utils.ts +1 -1
  30. src/engine/engine_web_api.ts +2 -2
  31. src/engine-components/EventList.ts +1 -1
  32. src/engine-components/js-extensions/ExtensionUtils.ts +1 -1
  33. src/engine-components/ui/Graphic.ts +1 -1
  34. src/engine-components/ui/Layout.ts +2 -2
  35. src/engine-components/LODGroup.ts +1 -1
  36. src/engine/extensions/NEEDLE_components.ts +1 -1
  37. src/engine/extensions/NEEDLE_progressive.ts +2 -2
  38. src/needle-engine.ts +50 -50
  39. src/engine-components/OffsetConstraint.ts +1 -1
  40. src/engine-components/ParticleSystem.ts +1 -1
  41. src/engine-components/ParticleSystemModules.ts +2 -2
  42. src/engine-components-experimental/networking/PlayerSync.ts +1 -1
  43. src/engine-components/ReflectionProbe.ts +2 -2
  44. src/engine/codegen/register_types.ts +2 -2
  45. src/engine-components/Renderer.ts +1 -1
  46. src/engine-components/SceneSwitcher.ts +4 -4
  47. src/engine-components/ScreenCapture.ts +1 -1
  48. src/engine-components/timeline/SignalAsset.ts +1 -1
  49. src/engine-components/Skybox.ts +2 -2
  50. src/engine-components/SpriteRenderer.ts +1 -1
  51. src/engine-components/SyncedCamera.ts +207 -207
  52. src/engine-components/SyncedTransform.ts +336 -336
  53. src/engine-components/ui/Text.ts +4 -4
  54. src/engine-components/export/usdz/ThreeUSDZExporter.ts +3 -3
  55. src/engine-components/timeline/TimelineTracks.ts +1 -1
  56. src/engine-components/export/usdz/extensions/USDZUI.ts +1 -1
  57. src/engine-components/webxr/WebARCameraBackground.ts +1 -1
  58. src/engine-components/webxr/WebXRAvatar.ts +1 -1
  59. src/engine-components/webxr/WebXRController.ts +1 -1
  60. src/engine-components/webxr/WebXRSync.ts +463 -463
src/engine-components/export/usdz/extensions/Animation.ts CHANGED
@@ -234,9 +234,9 @@
234
234
  writer.indent++;
235
235
 
236
236
  for (const transformData of arr) {
237
- let posTimesArray = transformData.pos?.times;
238
- let rotTimesArray = transformData.rot?.times;
239
- let scaleTimesArray = transformData.scale?.times;
237
+ const posTimesArray = transformData.pos?.times;
238
+ const rotTimesArray = transformData.rot?.times;
239
+ const scaleTimesArray = transformData.scale?.times;
240
240
 
241
241
  // timesArray is the sorted union of all time values
242
242
  let timesArray: number[] = [];
src/engine-components/AnimationUtils.ts CHANGED
@@ -73,10 +73,15 @@
73
73
  for (const t in animation.tracks) {
74
74
  const track = animation.tracks[t];
75
75
  const objectName = track["__objectName"] ?? track.name.substring(0, track.name.indexOf("."));
76
- const obj = gltf.scene.getObjectByName(objectName);
76
+ let obj = gltf.scene.getObjectByName(objectName);
77
77
  if (!obj) {
78
- // console.warn("could not find " + objectName, animation, gltf.scene);
79
- continue;
78
+ // this finds unnamed objects that still have tracks targeting them
79
+ obj = gltf.scene.getObjectByProperty('uuid', objectName);
80
+
81
+ if (!obj) {
82
+ // console.warn("could not find " + objectName, animation, gltf.scene);
83
+ continue;
84
+ }
80
85
  }
81
86
  let animationComponent = findAnimationGameObjectInParent(obj);
82
87
  if (!animationComponent) {
@@ -84,7 +89,6 @@
84
89
  }
85
90
  const animations = animationComponent.animations = animationComponent.animations || [];
86
91
  animation["name_animator"] = animationComponent.name;
87
- // console.log(objectName, obj, animator.name, animations.length);
88
92
  if (animations.indexOf(animation) < 0) {
89
93
  animations.push(animation);
90
94
  }
src/engine-components/avatar/Avatar_MustacheShake.ts CHANGED
@@ -23,7 +23,7 @@
23
23
  if(!this._startPosition) {
24
24
  this._startPosition = this.gameObject.position.clone();
25
25
  }
26
- let t = freq / 100;
26
+ const t = freq / 100;
27
27
  this.gameObject.position.y = this._startPosition.y + t * 0.07;
28
28
  }
29
29
  }
src/engine-components/ui/BaseUIComponent.ts CHANGED
@@ -108,7 +108,7 @@
108
108
  this.gameObject.add(container);
109
109
  }
110
110
  else {
111
- let targetShadowComponent = this._parentComponent.shadowComponent;
111
+ const targetShadowComponent = this._parentComponent.shadowComponent;
112
112
  if (targetShadowComponent) {
113
113
  // console.log("ADD", this.name, "to", this._parentComponent.name, targetShadowComponent);
114
114
  targetShadowComponent?.add(container);
src/engine-components/BasicIKConstraint.ts CHANGED
@@ -19,35 +19,35 @@
19
19
  // console.log(this);
20
20
 
21
21
  // find center
22
- let toPos = utils.getWorldPosition(this.to).clone();
23
- let fromPos = utils.getWorldPosition(this.from).clone();
24
- let dist = toPos.distanceTo(fromPos);
22
+ const toPos = utils.getWorldPosition(this.to).clone();
23
+ const fromPos = utils.getWorldPosition(this.from).clone();
24
+ const dist = toPos.distanceTo(fromPos);
25
25
 
26
- let dir0 = toPos.clone();
26
+ const dir0 = toPos.clone();
27
27
  dir0.sub(fromPos);
28
- let center = fromPos.clone();
28
+ const center = fromPos.clone();
29
29
  center.add(toPos);
30
30
  center.multiplyScalar(0.5);
31
31
 
32
32
  // find direction we should offset in
33
- let hintDir = utils.getWorldPosition(this.hint).clone();
33
+ const hintDir = utils.getWorldPosition(this.hint).clone();
34
34
  hintDir.sub(center);
35
35
 
36
- let offsetDir = new Vector3();
36
+ const offsetDir = new Vector3();
37
37
  offsetDir.crossVectors(hintDir, dir0);
38
38
  offsetDir.crossVectors(dir0, offsetDir);
39
39
  offsetDir.normalize();
40
40
 
41
- let halfDist = dist * 0.5;
42
- let stretchDistance = Math.max(this.desiredDistance, halfDist);
43
- let offsetLength = Math.sqrt(stretchDistance * stretchDistance - halfDist * halfDist);
41
+ const halfDist = dist * 0.5;
42
+ const stretchDistance = Math.max(this.desiredDistance, halfDist);
43
+ const offsetLength = Math.sqrt(stretchDistance * stretchDistance - halfDist * halfDist);
44
44
 
45
- let resultPos = offsetDir.clone();
45
+ const resultPos = offsetDir.clone();
46
46
  resultPos.multiplyScalar(offsetLength);
47
47
  resultPos.add(center);
48
48
  utils.setWorldPosition(this.gameObject, resultPos);
49
49
 
50
- let lookPos = center.clone();
50
+ const lookPos = center.clone();
51
51
  lookPos.sub(offsetDir);
52
52
  this.gameObject.lookAt(lookPos);
53
53
  }
src/engine-components/Camera.ts CHANGED
@@ -176,7 +176,7 @@
176
176
  private static _origin: THREE.Vector3 = new Vector3();
177
177
  private static _direction: THREE.Vector3 = new Vector3();
178
178
  public screenPointToRay(x: number, y: number, ray?: Ray): Ray {
179
- let cam = this.cam;
179
+ const cam = this.cam;
180
180
  const origin = Camera._origin;
181
181
  origin.set(x, y, -1);
182
182
  this.context.input.convertScreenspaceToRaycastSpace(origin);
src/engine-components/ui/Canvas.ts CHANGED
@@ -253,7 +253,7 @@
253
253
  if (debugLayout && matrixWorldChanged) console.log("Canvas Layout changed", this.context.time.frameCount, this.name);
254
254
 
255
255
  // TODO: optimize this, we should only need to update a subhierarchy of the parts where layout has changed
256
- let didLog = false;
256
+ const didLog = false;
257
257
  for (const ch of this._rectTransforms) {
258
258
  if (matrixWorldChanged) ch.markDirty();
259
259
  let layout = this._layoutGroups.get(ch.gameObject);
src/engine-components/CharacterController.ts CHANGED
@@ -34,7 +34,7 @@
34
34
  }
35
35
 
36
36
  onEnable() {
37
- let rb = this.rigidbody;
37
+ const rb = this.rigidbody;
38
38
  let collider = this.gameObject.getComponent(CapsuleCollider);
39
39
  if (!collider)
40
40
  collider = this.gameObject.addNewComponent(CapsuleCollider) as CapsuleCollider;
src/engine/debug/debug_overlay.ts CHANGED
@@ -42,7 +42,7 @@
42
42
  };
43
43
  window.addEventListener("error", (event) => {
44
44
  if (!event) return;
45
- let message = event.error;
45
+ const message = event.error;
46
46
  if (message === undefined) {
47
47
  if (isLocalNetwork())
48
48
  console.warn("Received unknown error", event, event.target);
src/engine-components/Duplicatable.ts CHANGED
@@ -135,7 +135,7 @@
135
135
  if (!current) return false;
136
136
  if (current === search) return true;
137
137
  if (current.children) {
138
- for (let child of current.children) {
138
+ for (const child of current.children) {
139
139
  if (this.isInChildren(child, search)) {
140
140
  return true;
141
141
  }
src/engine/engine_addressables.ts CHANGED
@@ -44,7 +44,7 @@
44
44
  }
45
45
 
46
46
  dispose() {
47
- for (let key in this._assetReferences) {
47
+ for (const key in this._assetReferences) {
48
48
  const ref = this._assetReferences[key];
49
49
  ref?.unload();
50
50
  }
src/engine/engine_context.ts CHANGED
@@ -402,7 +402,7 @@
402
402
  camera.updateProjectionMatrix();
403
403
  }
404
404
 
405
- async onCreate(buildScene?: (context: Context, loadingOptions?: LoadingOptions) => Promise<void>, opts?: LoadingOptions) {
405
+ async onCreate(buildScene?: (context: Context, loadingOptions?: LoadingOptions) => Promise<GLTF[] | null>, opts?: LoadingOptions) {
406
406
  try {
407
407
  this._isCreating = true;
408
408
  const res = await this.internalOnCreate(buildScene, opts);
@@ -610,7 +610,7 @@
610
610
  }
611
611
 
612
612
 
613
- private async internalOnCreate(buildScene?: (context: Context, opts?: LoadingOptions) => Promise<GLTF[] | void>, opts?: LoadingOptions) {
613
+ private async internalOnCreate(buildScene?: (context: Context, opts?: LoadingOptions) => Promise<GLTF[] | null>, opts?: LoadingOptions) {
614
614
 
615
615
  this.clear();
616
616
  // stop the animation loop if its running during creation
@@ -624,11 +624,15 @@
624
624
 
625
625
  // load and create scene
626
626
  let prepare_succeeded = true;
627
- let loadedFiles: GLTF[] | undefined | void = undefined;
627
+ let loadedFiles: GLTF[] | undefined | null = undefined;
628
628
  try {
629
629
  Context.Current = this;
630
- if (buildScene)
630
+ if (buildScene) {
631
631
  loadedFiles = await buildScene(this, opts);
632
+ // if the files are null the loading was cancelled
633
+ // this happens when the src attribute changes during loading and we want to load other files
634
+ if(loadedFiles === null) return false;
635
+ }
632
636
  }
633
637
  catch (err) {
634
638
  console.error(err);
src/engine/engine_element_loading.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  const debugRendering = getParam("debugloadingrendering");
10
10
  const debugLicense = getParam("debuglicense");
11
11
 
12
- declare type LoadingStyleOption = "dark" | "light";
12
+ declare type LoadingStyleOption = "dark" | "light" | "auto";
13
13
 
14
14
  export class LoadingElementOptions {
15
15
  className?: string;
@@ -177,8 +177,14 @@
177
177
  if (debug && !existing) console.log("Creating loading element");
178
178
  this._loadingElement = existing || document.createElement("div");
179
179
 
180
- const loadingStyle: LoadingStyleOption = this._element.getAttribute("loading-style") as LoadingStyleOption;
181
-
180
+ let loadingStyle: LoadingStyleOption = this._element.getAttribute("loading-style") as LoadingStyleOption;
181
+ if (loadingStyle === "auto") {
182
+ if (window.matchMedia('(prefers-color-scheme: dark)').matches)
183
+ loadingStyle = "dark";
184
+ else
185
+ loadingStyle = "light";
186
+ }
187
+
182
188
  const hasLicense = hasProLicense();
183
189
  if (!existing) {
184
190
  this._loadingElement.style.position = "absolute";
src/engine/engine_element.ts CHANGED
@@ -87,6 +87,8 @@
87
87
  private _loadingProgress01: number = 0;
88
88
  private _loadingView?: ILoadingViewHandler;
89
89
  private _previousSrc: string | null | string[] = null;
90
+ /** set to true after <needle-engine> did load completely at least once. Set to false when <needle-engine> is removed from the document */
91
+ private _didFullyLoad: boolean = false;
90
92
 
91
93
  constructor() {
92
94
  super();
@@ -101,9 +103,15 @@
101
103
  :host {
102
104
  position: relative;
103
105
  display: block;
104
- width: 600px;
105
- height: 300px;
106
+ width: max(600px, 100vw);
107
+ height: max(300px, 100vh);
106
108
  }
109
+ @media (max-width: 600px) {
110
+ :host {
111
+ width: 100vw;
112
+ height: 100vh;
113
+ }
114
+ }
107
115
  :host canvas {
108
116
  position: absolute;
109
117
  user-select: none;
@@ -171,6 +179,7 @@
171
179
  }
172
180
 
173
181
  disconnectedCallback() {
182
+ this._didFullyLoad = false;
174
183
  const keepAlive = this.getAttribute("keep-alive");
175
184
  if (debug) console.warn("<needle-engine> disconnected, keep-alive:", keepAlive);
176
185
  if (keepAlive === undefined || keepAlive !== "true") {
@@ -261,25 +270,38 @@
261
270
  },
262
271
  cancelable: true
263
272
  }));
273
+ // for local development we allow overriding the loading screen - but we notify the user that it won't work in a deployed environment
264
274
  if (useDefaultLoading === false && !allowOverridingDefaultLoading) {
265
275
  if (!isDevEnvironment())
266
276
  useDefaultLoading = true;
267
277
  console.warn("Needle Engine: You need a commercial license to override the default loading view. Visit https://needle.tools/pricing");
268
278
  if (isDevEnvironment()) showBalloonWarning("You need a <a target=\"_blank\" href=\"https://needle.tools/pricing\">commercial license</a> to override the default loading view. This will not work in production.");
269
279
  }
280
+ // create the loading view idf necessary
270
281
  if (!this._loadingView && useDefaultLoading)
271
282
  this._loadingView = new EngineLoadingView(this);
272
- if (useDefaultLoading)
273
- this._loadingView?.onLoadingBegin("begin load");
274
- if (debug)
275
- console.warn("--------------", loadId, "Needle Engine: Begin loading", alias ?? "", filesToLoad);
283
+ if (useDefaultLoading) {
284
+ // Only show the loading screen immedialty if we haven't loaded anything before
285
+ if (this._didFullyLoad !== true)
286
+ this._loadingView?.onLoadingBegin("begin load");
287
+ else {
288
+ // If we have loaded a glb previously and are loading a new glb due to e.g. src change
289
+ // we don't want to show the loading screen immediately to avoid blinking if the glb to be loaded is tiny
290
+ // so we wait a bit and only show the loading screen if the loading takes longer than a short moment
291
+ setTimeout(() => {
292
+ // If the loading progress is already above ~ 70% we also don't need to show the loading screen anymore
293
+ if (this._loadingView && this._loadingProgress01 < 0.3 && this._loadId === loadId)
294
+ this._loadingView.onLoadingBegin("begin load");
295
+ }, 300)
296
+ }
297
+ }
298
+ if (debug) console.warn("--------------", loadId, "Needle Engine: Begin loading", alias ?? "", filesToLoad);
276
299
  this.onBeforeBeginLoading();
277
300
 
278
- if (debug) console.log(filesToLoad);
301
+ type LoadFunctionResult = (ctx: Context) => Promise<LoadedGLTF[] | null>;
302
+ let loadFunction: null | LoadFunctionResult = null;
303
+ const loadedFiles: Array<LoadedGLTF> = [];
279
304
 
280
- let loadFunction: any = null;
281
- let loadedFiles: Array<LoadedGLTF> = [];
282
-
283
305
  if (filesToLoad?.length > 0) {
284
306
  loadFunction = async (ctx: Context) => {
285
307
  let hash = 0;
@@ -312,6 +334,7 @@
312
334
  }
313
335
  }
314
336
  const res = await loader.loadSync(ctx, url, url, hash, prog => {
337
+ if(this._loadId !== loadId) return;
315
338
  // Calc progress
316
339
  progress.progress = prog;
317
340
  this._loadingProgress01 = calculateProgress01(progress);
@@ -332,13 +355,13 @@
332
355
  // if the loading ID changed but the scene has already been loaded we want to dispose it
333
356
  if (this._loadId !== loadId) {
334
357
  destroy(obj, true, true)
335
- return loadedFiles;
358
+ return null;
336
359
  }
337
360
  GameObject.add(obj, ctx.scene, ctx);
338
361
  }
339
362
  }
340
363
  }
341
- if (this._loadId !== loadId) return loadedFiles;
364
+ if (this._loadId !== loadId) return null;
342
365
  return loadedFiles;
343
366
  };
344
367
  }
@@ -347,7 +370,7 @@
347
370
  if (currentHash !== null && currentHash !== undefined)
348
371
  this._context.hash = currentHash;
349
372
  this._context.alias = alias;
350
- await this._context.onCreate(loadFunction);
373
+ await this._context.onCreate(loadFunction as any);
351
374
  if (this._loadId !== loadId) return;
352
375
  if (debug)
353
376
  console.warn("--------------", loadId, "Needle Engine: finished loading", alias ?? "", filesToLoad);
@@ -355,6 +378,7 @@
355
378
  if (useDefaultLoading) {
356
379
  this._loadingView?.onLoadingUpdate(1, "creating scene");
357
380
  }
381
+ this._didFullyLoad = true;
358
382
  this.classList.remove("loading");
359
383
  this.classList.add("loading-finished");
360
384
  this.dispatchEvent(new CustomEvent("loadfinished", {
@@ -554,7 +578,7 @@
554
578
  const parts = str.split("/");
555
579
  let name = parts[parts.length - 1];
556
580
  // Remove params
557
- let index = name.indexOf("?")
581
+ const index = name.indexOf("?")
558
582
  if (index > 0)
559
583
  name = name.substring(0, index);
560
584
  return decodeURIComponent(name);
src/engine/engine_gameobject.ts CHANGED
@@ -317,8 +317,7 @@
317
317
  instance.userData = {};
318
318
  const children = instance.children;
319
319
  instance.children = [];
320
- let clone: Object3D | GameObject;
321
- clone = instance.clone(false);
320
+ const clone: Object3D | GameObject = instance.clone(false);
322
321
  apply(clone);
323
322
  // if(instance[$originalGuid])
324
323
  // clone[$originalGuid] = instance[$originalGuid];
src/engine/engine_hot_reload.ts CHANGED
@@ -102,7 +102,7 @@
102
102
  const instancesOfType = instances.get(newType.name);
103
103
 
104
104
  let hotReloadMessage = "[Needle Engine] Updating type: " + key;
105
- let typesCount = instancesOfType?.length ?? -1;
105
+ const typesCount = instancesOfType?.length ?? -1;
106
106
  if (typesCount > 0) hotReloadMessage += " x" + typesCount;
107
107
  else hotReloadMessage += " - no instances";
108
108
  console.log(hotReloadMessage);
src/engine/engine_input_utils.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  document.removeEventListener('touchstart', fn);
10
10
  res();
11
11
  };
12
- let fn = callback;
12
+ const fn = callback;
13
13
  document.addEventListener('pointerdown', fn);
14
14
  document.addEventListener('click', fn);
15
15
  document.addEventListener('dragstart', fn);
src/engine/engine_input.ts CHANGED
@@ -487,20 +487,20 @@
487
487
  private onMouseDown = (evt: MouseEvent) => {
488
488
  if (evt.defaultPrevented) return;
489
489
  if (this.canReceiveInput(evt) === false) return;
490
- let i = evt.button;
490
+ const i = evt.button;
491
491
  this.onDown({ button: i, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, source: evt });
492
492
  }
493
493
 
494
494
  private onMouseMove = (evt: MouseEvent) => {
495
495
  if (evt.defaultPrevented) return;
496
- let i = evt.button;
496
+ const i = evt.button;
497
497
  const args: PointerEventArgs = { button: i, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, source: evt, movementX: evt.movementX, movementY: evt.movementY };
498
498
  this.onMove(args);
499
499
  }
500
500
 
501
501
  private onMouseUp = (evt: MouseEvent) => {
502
502
  if (evt.defaultPrevented) return;
503
- let i = evt.button;
503
+ const i = evt.button;
504
504
  if (!this.isNewEvent(evt.timeStamp, i, this._pointerUpTimestamp)) return;
505
505
  this.onUp({ button: i, clientX: evt.clientX, clientY: evt.clientY, pointerType: PointerType.Mouse, source: evt });
506
506
  }
src/engine/engine_license.ts CHANGED
@@ -38,7 +38,7 @@
38
38
  sendUsageMessageToAnalyticsBackend();
39
39
  });
40
40
 
41
- export let runtimeLicenseCheckPromise: Promise<void>;
41
+ export let runtimeLicenseCheckPromise: Promise<void> | undefined = undefined;
42
42
  async function checkLicense() {
43
43
  // Only perform the runtime license check once
44
44
  if (runtimeLicenseCheckPromise) return runtimeLicenseCheckPromise;
@@ -107,7 +107,7 @@
107
107
  }
108
108
  }, 100);
109
109
 
110
- let svg = `<img class="logo" src="${logoSVG}" style="width: 40px; height: 40px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;"/>`;
110
+ const svg = `<img class="logo" src="${logoSVG}" style="width: 40px; height: 40px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;"/>`;
111
111
  const logoElement = document.createElement("div");
112
112
  logoElement.innerHTML = svg;
113
113
  logoElement.classList.add("logo");
@@ -158,7 +158,7 @@
158
158
  border: 2px solid rgba(160,160,160,.5);
159
159
  `;
160
160
  // url must contain https for firefox to make it clickable
161
- let licenseText = "Needle Engine — No license active, commercial use is not allowed. Visit https://needle.tools/pricing for more information and licensing options.";
161
+ const licenseText = "Needle Engine — No license active, commercial use is not allowed. Visit https://needle.tools/pricing for more information and licensing options.";
162
162
  console.log("%c " + licenseText, style);
163
163
  }
164
164
 
@@ -295,7 +295,7 @@
295
295
  try {
296
296
  const analyticsBackendUrlForward = "https://urls.needle.tools/analytics-endpoint-v2";
297
297
  const res = await fetch(analyticsBackendUrlForward);
298
- let analyticsUrl = await res.text();
298
+ const analyticsUrl = await res.text();
299
299
  if (analyticsUrl) {
300
300
  if (debug) console.log("Analytics backend url", analyticsUrl);
301
301
 
src/engine/engine_networking_auto.ts CHANGED
@@ -346,7 +346,7 @@
346
346
 
347
347
  if (setter) {
348
348
  descriptor.set = function (value: T) {
349
- let valueChanged = false;
349
+ const valueChanged = false;
350
350
 
351
351
  const syncer = getSyncer(this);
352
352
 
src/engine/engine_networking_peer.ts CHANGED
@@ -28,7 +28,7 @@
28
28
  }
29
29
 
30
30
  private trySetupHost(id: string) {
31
- let host = new Peer(id);
31
+ const host = new Peer(id);
32
32
  host.on("error", err => {
33
33
  console.error(err);
34
34
  this._host = undefined;
src/engine/engine_networking.ts CHANGED
@@ -467,7 +467,7 @@
467
467
  }
468
468
  if(serverUrl === undefined){
469
469
  console.log("Fetch default backend url: " + defaultNetworkingBackendUrlProvider);
470
- let failed = false;
470
+ const failed = false;
471
471
  const defaultUrlResponse = await fetch(defaultNetworkingBackendUrlProvider);
472
472
  serverUrl = await defaultUrlResponse.text();
473
473
  if(failed) return;
src/engine/engine_patcher.ts CHANGED
@@ -154,7 +154,7 @@
154
154
  }
155
155
 
156
156
  function getPatches(prototype, fieldName: string) {
157
- let patchesMap = patches().get(prototype);
157
+ const patchesMap = patches().get(prototype);
158
158
  if (!patchesMap) {
159
159
  return null;
160
160
  }
src/engine/engine_physics_rapier.ts CHANGED
@@ -947,8 +947,8 @@
947
947
  this.calculateJointRelativeMatrices(body1.gameObject, body2.gameObject, this._tempMatrix);
948
948
  this._tempMatrix.decompose(this._tempPosition, this._tempQuaternion, this._tempScale);
949
949
 
950
- let params = RAPIER.JointData.revolute(anchor, this._tempPosition, axis);
951
- let joint = this.world.createImpulseJoint(params, b1, b2, true);
950
+ const params = RAPIER.JointData.revolute(anchor, this._tempPosition, axis);
951
+ const joint = this.world.createImpulseJoint(params, b1, b2, true);
952
952
  if (debugPhysics)
953
953
  console.log("ADD HINGE JOINT", joint)
954
954
  }
src/engine/engine_scenelighting.ts CHANGED
@@ -233,7 +233,7 @@
233
233
  // fallback
234
234
  if (this._waitPromise) return this._waitPromise;
235
235
  this._waitPromise = new Promise((res, _rej) => {
236
- let interval = setInterval(async () => {
236
+ const interval = setInterval(async () => {
237
237
  const ex = this.internalGetReflection(sourceId);
238
238
  if (ex) {
239
239
  clearInterval(interval);
src/engine/engine_serialization_builtin_serializer.ts CHANGED
@@ -112,7 +112,7 @@
112
112
  // independent of order of loading
113
113
  let res: GameObject | Behaviour | undefined | null = undefined;
114
114
  // first try to search in the current gltf scene (if any)
115
- let gltfScene = context.gltf?.scene;
115
+ const gltfScene = context.gltf?.scene;
116
116
  if (gltfScene) {
117
117
  res = GameObject.findByGuid(data.guid, gltfScene);
118
118
  }
@@ -273,9 +273,8 @@
273
273
  printWarningMethodNotFound();
274
274
  }
275
275
  }
276
- let fn: CallInfo | undefined;
276
+
277
277
  let args = call.argument;
278
-
279
278
  if (args !== undefined) {
280
279
  if (typeof args === "object") {
281
280
  // Try to deserialize the call argument to either a object or a component reference
@@ -287,7 +286,7 @@
287
286
 
288
287
  // This is the final method we pass to the call info (or undefined if the method couldnt be resolved)
289
288
  const eventMethod = hasMethod ? this.createEventMethod(target, call.method, args) : undefined;
290
- fn = new CallInfo(eventMethod, call.enabled);
289
+ const fn = new CallInfo(eventMethod, call.enabled);
291
290
 
292
291
 
293
292
  if (!fn.method)
src/engine/engine_serialization_core.ts CHANGED
@@ -54,7 +54,7 @@
54
54
  console.log("FOUND " + name, type.name, type.constructor.name, res, this.typeMap);
55
55
  return res;
56
56
  }
57
- let parent = Object.getPrototypeOf(type);
57
+ const parent = Object.getPrototypeOf(type);
58
58
  const hasPrototypeOrConstructor = parent.prototype || parent.constructor;
59
59
  // console.log(name, type, parent);
60
60
  if (!hasPrototypeOrConstructor) {
@@ -221,7 +221,7 @@
221
221
  if (types === undefined) return null;
222
222
  const res = {};
223
223
  for (const key in types) {
224
- let val = obj[key];
224
+ const val = obj[key];
225
225
 
226
226
  // if the object bein serialized is some type of object check if we have special handling registered for it
227
227
  if (val !== undefined && val !== null && typeof val === "object") {
@@ -609,7 +609,7 @@
609
609
  if (source === undefined || source === null) return;
610
610
  if (target === undefined || target === null) return;
611
611
 
612
- let onlyDeclared = false;
612
+ const onlyDeclared = false;
613
613
  // if (onlyDeclared === true && target.constructor) {
614
614
  // if (target.constructor[ALL_PROPERTIES_MARKER] === true)
615
615
  // onlyDeclared = false;
src/engine/engine_utils_screenshot.ts CHANGED
@@ -1,10 +1,22 @@
1
+ import { ContextRegistry } from "./engine_context_registry.js";
1
2
  import { Context } from "./engine_setup.js";
2
3
  import { PerspectiveCamera, Camera } from "three";
3
4
 
4
5
  declare type ImageMimeType = "image/webp" | "image/png";
5
6
 
6
- export function screenshot(context: Context, width: number, height: number, mimeType: ImageMimeType = "image/webp", camera?: Camera | null) {
7
+ /**
8
+ * Take a screenshot from the current scene
9
+ */
10
+ export function screenshot(context?: Context, width?: number, height?: number, mimeType: ImageMimeType = "image/webp", camera?: Camera | null): string | null {
7
11
 
12
+ if (!context) {
13
+ context = ContextRegistry.Current as Context;
14
+ if (!context) {
15
+ console.error("Can not save screenshot: No needle-engine context found or provided.");
16
+ return null;
17
+ }
18
+ }
19
+
8
20
  if (!camera) {
9
21
  camera = context.mainCamera;
10
22
  if (!camera) {
@@ -15,6 +27,9 @@
15
27
  const prevWidth = context.renderer.domElement.width;
16
28
  const prevHeight = context.renderer.domElement.height;
17
29
 
30
+ if (!width) width = prevWidth;
31
+ if (!height) height = prevHeight;
32
+
18
33
  try {
19
34
  const canvas = context.renderer.domElement;
20
35
 
@@ -39,4 +54,23 @@
39
54
  }
40
55
 
41
56
  return null;
57
+ }
58
+
59
+ let saveImageElement: HTMLAnchorElement | null = null;
60
+
61
+ /** Download a image (must be a data url) */
62
+ export function saveImage(dataUrl: string | null, filename: string) {
63
+ if (!dataUrl) {
64
+ return;
65
+ }
66
+ if (!dataUrl.startsWith("data:image")) {
67
+ console.error("Can not save image: Data url is not an image", dataUrl);
68
+ return;
69
+ }
70
+ if (!saveImageElement) {
71
+ saveImageElement = document.createElement("a");
72
+ }
73
+ saveImageElement.href = dataUrl;
74
+ saveImageElement.download = filename;
75
+ saveImageElement.click();
42
76
  }
src/engine/engine_utils.ts CHANGED
@@ -252,7 +252,7 @@
252
252
  // Take the source uri as the base path
253
253
  const basePath = source.substring(0, pathIndex + 1);
254
254
  // Append the relative uri
255
- let newUri = basePath + uri;
255
+ const newUri = basePath + uri;
256
256
  // newUri = new URL(newUri, globalThis.location.href).href;
257
257
  if (debugGetPath) console.log("source:", source, "- changed uri \nfrom", uri, "\n→ ", newUri, "\n" + basePath);
258
258
  return newUri;
src/engine/engine_web_api.ts CHANGED
@@ -97,7 +97,7 @@
97
97
  if (!reader) return null;
98
98
 
99
99
  let received: number = 0;
100
- let chunks: Uint8Array[] = [];
100
+ const chunks: Uint8Array[] = [];
101
101
  while (true) {
102
102
  const { done, value } = await reader.read();
103
103
  if (value) {
@@ -112,7 +112,7 @@
112
112
  }
113
113
  const final = new Uint8Array(received);
114
114
  let position = 0;
115
- for (let chunk of chunks) {
115
+ for (const chunk of chunks) {
116
116
  final.set(chunk, position);
117
117
  position += chunk.length;
118
118
  }
src/engine-components/EventList.ts CHANGED
@@ -43,7 +43,7 @@
43
43
  if (this.key !== undefined) {
44
44
  let temp = "";
45
45
  let foundFirstLetter = false;
46
- for (let c of this.key) {
46
+ for (const c of this.key) {
47
47
  if (foundFirstLetter && isUpperCase(c))
48
48
  temp += "-";
49
49
  foundFirstLetter = true;
src/engine-components/js-extensions/ExtensionUtils.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  console.warn("No prototype found", obj, obj.prototype, obj.constructor);
12
12
  return;
13
13
  }
14
- let handler = handlers.get(prototype);
14
+ const handler = handlers.get(prototype);
15
15
  if (handler) {
16
16
  // console.log("OK", prototype);
17
17
  handler.apply(obj);
src/engine-components/ui/Graphic.ts CHANGED
@@ -150,7 +150,7 @@
150
150
  if (this._currentlyCreatingPanel) return;
151
151
  this._currentlyCreatingPanel = true;
152
152
 
153
- let offset = .015;
153
+ const offset = .015;
154
154
  // if (this.Root) offset = .02 * (1 / this.Root.gameObject.scale.z);
155
155
  const opts = {
156
156
  backgroundColor: this.color,
src/engine-components/ui/Layout.ts CHANGED
@@ -269,13 +269,13 @@
269
269
  }
270
270
 
271
271
  const size = isHorizontal ? rt.width : rt.height;
272
- let halfSize = size * .5;
272
+ const halfSize = size * .5;
273
273
  start += halfSize;
274
274
 
275
275
  // TODO: this isnt correct yet!
276
276
  if (forceExpandSize) {
277
277
  // this is the center of the cell
278
- let preferredStart = sizePerChild * k - sizePerChild * .5;
278
+ const preferredStart = sizePerChild * k - sizePerChild * .5;
279
279
  if (preferredStart > start) {
280
280
  start = preferredStart - sizePerChild * .5 + size + this.padding.left;
281
281
  start -= halfSize;
src/engine-components/LODGroup.ts CHANGED
@@ -67,7 +67,7 @@
67
67
 
68
68
  if (this.lodModels && Array.isArray(this.lodModels)) {
69
69
  let maxDistance = 0;
70
- let renderers: Renderer[] = [];
70
+ const renderers: Renderer[] = [];
71
71
  for (const model of this.lodModels) {
72
72
  maxDistance = Math.max(model.distance, maxDistance);
73
73
  const lod = new LOD(model);
src/engine/extensions/NEEDLE_components.ts CHANGED
@@ -104,7 +104,7 @@
104
104
  }
105
105
 
106
106
  writeNode(node: Object3D, nodeDef) {
107
- let nodeIndex = this.writer.json.nodes.length;
107
+ const nodeIndex = this.writer.json.nodes.length;
108
108
  console.log(node.name, nodeIndex, node.uuid);
109
109
  const context = new ExportData(node, nodeIndex, nodeDef);
110
110
  this.exportContext[nodeIndex] = context;
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -43,7 +43,7 @@
43
43
 
44
44
  const promises: Promise<Texture | null>[] = [];
45
45
 
46
- for (let slot of Object.keys(material)) {
46
+ for (const slot of Object.keys(material)) {
47
47
  const val = material[slot];
48
48
  if (val?.isTexture) {
49
49
  const task = this.assignTextureLODForSlot(context, source, material, level, slot, val);
@@ -53,7 +53,7 @@
53
53
 
54
54
  if (material instanceof RawShaderMaterial) {
55
55
  // iterate uniforms
56
- for (let slot of Object.keys(material.uniforms)) {
56
+ for (const slot of Object.keys(material.uniforms)) {
57
57
  const val = material.uniforms[slot].value;
58
58
  if (val?.isTexture) {
59
59
  const task = this.assignTextureLODForSlot(context, source, material, level, slot, val);
src/needle-engine.ts CHANGED
@@ -1,50 +1,50 @@
1
- import { makeErrorsVisibleForDevelopment } from "./engine/debug/debug_overlay.js";
2
- makeErrorsVisibleForDevelopment();
3
-
4
- import "./engine/engine_element.js";
5
- import "./engine/engine_setup.js";
6
- export * from "./engine/api.js";
7
- export * from "./engine-components/api.js";
8
- export * from "./engine-components-experimental/api.js";
9
- export * from "./engine-schemes/api.js";
10
-
11
- // make accessible for external javascript
12
- import { Context } from "./engine/engine_setup.js";
13
- const Needle = { Context: Context };
14
- if (globalThis["Needle"] !== undefined) {
15
- console.warn("Needle Engine is already imported");
16
- }
17
- globalThis["Needle"] = Needle;
18
- function registerGlobal(obj: object) {
19
- for (const key in obj) {
20
- Needle[key] = obj[key];
21
- }
22
- }
23
- import * as Component from "./engine-components/Component.js";
24
- registerGlobal(Component);
25
-
26
- import * as Components from "./engine-components/codegen/components.js";
27
- registerGlobal(Components);
28
-
29
-
30
- import { GameObject } from "./engine-components/Component.js";
31
- for (const method of Object.getOwnPropertyNames(GameObject)) {
32
- switch (method) {
33
- case "prototype":
34
- case "constructor":
35
- case "length":
36
- case "name":
37
- continue;
38
- default:
39
- Needle[method] = GameObject[method];
40
- break;
41
- }
42
- }
43
-
44
- // make three accessible
45
- import * as THREE from "three";
46
- if (!globalThis["THREE"]) {
47
- globalThis["THREE"] = THREE;
48
- }
49
- else console.warn("Threejs is already imported");
50
-
1
+ import { makeErrorsVisibleForDevelopment } from "./engine/debug/debug_overlay.js";
2
+ makeErrorsVisibleForDevelopment();
3
+
4
+ import "./engine/engine_element.js";
5
+ import "./engine/engine_setup.js";
6
+ export * from "./engine/api.js";
7
+ export * from "./engine-components/api.js";
8
+ export * from "./engine-components-experimental/api.js";
9
+ export * from "./engine-schemes/api.js";
10
+
11
+ // make accessible for external javascript
12
+ import { Context } from "./engine/engine_setup.js";
13
+ const Needle = { Context: Context };
14
+ if (globalThis["Needle"] !== undefined) {
15
+ console.warn("Needle Engine is already imported");
16
+ }
17
+ globalThis["Needle"] = Needle;
18
+ function registerGlobal(obj: object) {
19
+ for (const key in obj) {
20
+ Needle[key] = obj[key];
21
+ }
22
+ }
23
+ import * as Component from "./engine-components/Component.js";
24
+ registerGlobal(Component);
25
+
26
+ import * as Components from "./engine-components/codegen/components.js";
27
+ registerGlobal(Components);
28
+
29
+
30
+ import { GameObject } from "./engine-components/Component.js";
31
+ for (const method of Object.getOwnPropertyNames(GameObject)) {
32
+ switch (method) {
33
+ case "prototype":
34
+ case "constructor":
35
+ case "length":
36
+ case "name":
37
+ continue;
38
+ default:
39
+ Needle[method] = GameObject[method];
40
+ break;
41
+ }
42
+ }
43
+
44
+ // make three accessible
45
+ import * as THREE from "three";
46
+ if (!globalThis["THREE"]) {
47
+ globalThis["THREE"] = THREE;
48
+ }
49
+ else console.warn("Threejs is already imported");
50
+
src/engine-components/OffsetConstraint.ts CHANGED
@@ -52,7 +52,7 @@
52
52
  const quat = new Quaternion().setFromEuler(euler);
53
53
  if(this.affectRotation) utils.setWorldQuaternion(this.gameObject, rot.multiply(quat));
54
54
 
55
- let lookDirection = new Vector3();
55
+ const lookDirection = new Vector3();
56
56
  this.from.getWorldDirection(lookDirection).multiplyScalar(50);
57
57
  if (this.levelLookDirection) lookDirection.y = 0;
58
58
  if (this.alignLookDirection) this.gameObject.lookAt(lookDirection);
src/engine-components/ParticleSystem.ts CHANGED
@@ -451,7 +451,7 @@
451
451
  //////////////////////
452
452
  // calculate speed
453
453
  const baseVelocity = particle[$startVelocity];
454
- let gravityFactor = particle[$gravityFactor];
454
+ const gravityFactor = particle[$gravityFactor];
455
455
  if (gravityFactor !== 0) {
456
456
  const factor = gravityFactor * particle[$gravitySpeed];
457
457
  temp3.copy(this._gravityDirection).multiplyScalar(factor);
src/engine-components/ParticleSystemModules.ts CHANGED
@@ -973,7 +973,7 @@
973
973
  worldSpace: boolean = false;
974
974
 
975
975
  getWidth(size: number, _life01: number, pos01: number, t : number) {
976
- let res = this.widthOverTrail.evaluate(pos01, t);
976
+ const res = this.widthOverTrail.evaluate(pos01, t);
977
977
  size *= res;
978
978
  return size;
979
979
  }
@@ -1346,7 +1346,7 @@
1346
1346
  const speed = baseVelocity.length();
1347
1347
  if (speed > max) {
1348
1348
  this._temp.copy(baseVelocity).normalize().multiplyScalar(max);
1349
- let t = this.dampen * .5;
1349
+ const t = this.dampen * .5;
1350
1350
  // t *= scale;
1351
1351
  baseVelocity.x = Mathf.lerp(baseVelocity.x, this._temp.x, t);
1352
1352
  baseVelocity.y = Mathf.lerp(baseVelocity.y, this._temp.y, t);
src/engine-components-experimental/networking/PlayerSync.ts CHANGED
@@ -35,7 +35,7 @@
35
35
 
36
36
  const instance = await this.asset?.instantiateSynced({ parent: this.gameObject }, true);
37
37
  if (instance) {
38
- let pl = GameObject.getComponent(instance, PlayerState);
38
+ const pl = GameObject.getComponent(instance, PlayerState);
39
39
  if (pl) {
40
40
  pl.owner = this.context.connection.connectionId!;
41
41
  this.onPlayerSpawned?.invoke(instance);
src/engine-components/ReflectionProbe.ts CHANGED
@@ -140,8 +140,8 @@
140
140
 
141
141
  // make sure we have the currently assigned material cached (and an up to date clone of that)
142
142
  // TODO: this is causing problems with progressive textures sometimes (depending on the order) when the version changes and we re-create materials over and over. We might want to just set the material envmap instead of making a clone
143
- let isCachedInstance = material === cached?.copy;
144
- let hasChanged = !cached || cached.material.uuid !== material.uuid || cached.copy.version !== material.version;
143
+ const isCachedInstance = material === cached?.copy;
144
+ const hasChanged = !cached || cached.material.uuid !== material.uuid || cached.copy.version !== material.version;
145
145
  if (!isCachedInstance && hasChanged) {
146
146
  if (debug) {
147
147
  let reason = "";
src/engine/codegen/register_types.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { TypeStore } from "./../engine_typestore.js"
2
-
2
+
3
3
  // Import types
4
4
  import { __Ignore } from "../../engine-components/codegen/components.js";
5
5
  import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js";
@@ -217,7 +217,7 @@
217
217
  import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering.js";
218
218
  import { XRRig } from "../../engine-components/webxr/WebXRRig.js";
219
219
  import { XRState } from "../../engine-components/XRFlag.js";
220
-
220
+
221
221
  // Register types
222
222
  TypeStore.add("__Ignore", __Ignore);
223
223
  TypeStore.add("ActionBuilder", ActionBuilder);
src/engine-components/Renderer.ts CHANGED
@@ -853,7 +853,7 @@
853
853
 
854
854
  private autoUpdateInstanceMatrix(obj: Object3D) {
855
855
  const original = obj.matrixWorld["multiplyMatrices"].bind(obj.matrixWorld);
856
- let previousMatrix: THREE.Matrix4 = obj.matrixWorld.clone();
856
+ const previousMatrix: THREE.Matrix4 = obj.matrixWorld.clone();
857
857
  const matrixChangeWrapper = (a: Matrix4, b: Matrix4) => {
858
858
  const newMatrixWorld = original(a, b);
859
859
  if (obj[NEED_UPDATE_INSTANCE_KEY] || previousMatrix.equals(newMatrixWorld) === false) {
src/engine-components/SceneSwitcher.ts CHANGED
@@ -147,7 +147,7 @@
147
147
 
148
148
  private onPopState = async (_state: PopStateEvent) => {
149
149
  if (!this.useHistory) return;
150
- let wasUsingHistory = this.useHistory;
150
+ const wasUsingHistory = this.useHistory;
151
151
  try {
152
152
  this.useHistory = false;
153
153
  let didResolve = false;
@@ -366,7 +366,7 @@
366
366
 
367
367
  function sceneUriToName(uri: string): string {
368
368
  const name = uri.split("/").pop();
369
- let value = name?.split(".").shift();
369
+ const value = name?.split(".").shift();
370
370
  if (value?.length) return value;
371
371
  return uri;
372
372
  }
@@ -398,7 +398,7 @@
398
398
  let searchDistance: number;
399
399
  let searchCall: number;
400
400
  const array = this._switcher.scenes;
401
- let interval = setInterval(() => {
401
+ const interval = setInterval(() => {
402
402
  if (this.allLoaded()) {
403
403
  if (debug)
404
404
  console.log("All scenes loaded");
@@ -419,7 +419,7 @@
419
419
  searchCall += 1;
420
420
  const maxSearchDistance = searchForward ? this.maxLoadAhead : this.maxLoadBehind;
421
421
  if (searchDistance > maxSearchDistance) return;
422
- let roomIndex = searchForward ? lastRoom + searchDistance : lastRoom - searchDistance;
422
+ const roomIndex = searchForward ? lastRoom + searchDistance : lastRoom - searchDistance;
423
423
  if (roomIndex < 0) return;
424
424
  // if (roomIndex < 0) roomIndex = array.length + roomIndex;
425
425
  if (roomIndex < 0 || roomIndex >= array.length) return;
src/engine-components/ScreenCapture.ts CHANGED
@@ -556,7 +556,7 @@
556
556
  call.on("stream", () => {
557
557
  // workaround for https://github.com/peers/peerjs/issues/636
558
558
  let intervalCounter = 0;
559
- let closeInterval = setInterval(() => {
559
+ const closeInterval = setInterval(() => {
560
560
  const isFirstInterval = intervalCounter === 0;
561
561
  if (!handle.isOpen && isFirstInterval) {
562
562
  intervalCounter += 1;
src/engine-components/timeline/SignalAsset.ts CHANGED
@@ -64,7 +64,7 @@
64
64
 
65
65
  invoke(sig: SignalAsset | string) {
66
66
  if (!this.events || !Array.isArray(this.events)) return;
67
- let id = typeof sig === "object" ? sig.guid : sig;
67
+ const id = typeof sig === "object" ? sig.guid : sig;
68
68
  for (const evt of this.events) {
69
69
  if (evt.signal.guid === id) {
70
70
  try {
src/engine-components/Skybox.ts CHANGED
@@ -40,7 +40,7 @@
40
40
  const context = args.context;
41
41
  const skyboxImage = context.domElement.getAttribute("skybox-image");
42
42
  const environmentImage = context.domElement.getAttribute("environment-image");
43
- let promises = new Array<Promise<any>>();
43
+ const promises = new Array<Promise<any>>();
44
44
 
45
45
  if (skyboxImage) {
46
46
  if (debug) console.log("Creating remote skybox to load " + skyboxImage);
@@ -174,7 +174,7 @@
174
174
  const cached = tryGetPreviouslyLoadedTexture(url);
175
175
  if (cached) {
176
176
  const res = await cached;
177
- if (res.source?.data?.length > 0) return res;
177
+ if (res.source?.data?.length > 0 || res.source?.data?.data?.length) return res;
178
178
  }
179
179
  const isEXR = url.endsWith(".exr");
180
180
  const isHdr = url.endsWith(".hdr");
src/engine-components/SpriteRenderer.ts CHANGED
@@ -106,7 +106,7 @@
106
106
  if (index < 0 || index >= this.spriteSheet.sprites.length)
107
107
  return;
108
108
  const slice = this.spriteSheet.sprites[index];
109
- let tex = slice?.texture;
109
+ const tex = slice?.texture;
110
110
  if (!tex) return;
111
111
  tex.colorSpace = THREE.SRGBColorSpace;
112
112
  if (tex.minFilter == NearestFilter && tex.magFilter == NearestFilter)
src/engine-components/SyncedCamera.ts CHANGED
@@ -1,208 +1,208 @@
1
- import { NetworkConnection } from "../engine/engine_networking.js";
2
- import { Behaviour, GameObject } from "./Component.js";
3
- import { Camera } from "./Camera.js";
4
- import * as utils from "../engine/engine_three_utils.js"
5
- import { WebXR } from "./webxr/WebXR.js";
6
- import { Builder } from "flatbuffers";
7
- import { SyncedCameraModel } from "../engine-schemes/synced-camera-model.js";
8
- import { Vec3 } from "../engine-schemes/vec3.js";
9
- import { registerBinaryType } from "../engine-schemes/schemes.js";
10
- import { InstancingUtil } from "../engine/engine_instancing.js";
11
- import { serializable } from "../engine/engine_serialization_decorator.js";
12
- import { Object3D } from "three";
13
- import { AvatarMarker } from "./webxr/WebXRAvatar.js";
14
- import { AssetReference } from "../engine/engine_addressables.js";
15
- import { ViewDevice } from "../engine/engine_playerview.js";
16
- import { InstantiateOptions } from "../engine/engine_gameobject.js";
17
-
18
- const SyncedCameraModelIdentifier = "SCAM";
19
- registerBinaryType(SyncedCameraModelIdentifier, SyncedCameraModel.getRootAsSyncedCameraModel);
20
- const builder = new Builder();
21
-
22
- // enum CameraSyncEvent {
23
- // Update = "sync-update-camera",
24
- // }
25
-
26
- class CameraModel {
27
- userId: string;
28
- guid: string;
29
- // dontSave: boolean = true;
30
- // pos: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
31
- // rot: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
32
-
33
- constructor(connectionId: string, guid: string) {
34
- this.guid = guid;
35
- this.userId = connectionId;
36
- }
37
-
38
- send(cam: THREE.Camera | null | undefined, con: NetworkConnection) {
39
- if (cam) {
40
- builder.clear();
41
- const guid = builder.createString(this.guid);
42
- const userId = builder.createString(this.userId);
43
- SyncedCameraModel.startSyncedCameraModel(builder);
44
- SyncedCameraModel.addGuid(builder, guid);
45
- SyncedCameraModel.addUserId(builder, userId);
46
- const p = utils.getWorldPosition(cam);
47
- const r = utils.getWorldRotation(cam);
48
- SyncedCameraModel.addPos(builder, Vec3.createVec3(builder, p.x, p.y, p.z));
49
- SyncedCameraModel.addRot(builder, Vec3.createVec3(builder, r.x, r.y, r.z));
50
- const offset = SyncedCameraModel.endSyncedCameraModel(builder);
51
- builder.finish(offset, SyncedCameraModelIdentifier);
52
- con.sendBinary(builder.asUint8Array());
53
- }
54
- }
55
- }
56
-
57
- declare type UserCamInfo = {
58
- obj: THREE.Object3D,
59
- lastUpdate: number;
60
- userId: string;
61
- };
62
-
63
- export class SyncedCamera extends Behaviour {
64
-
65
- static instances: UserCamInfo[] = [];
66
-
67
- getCameraObject(userId: string): THREE.Object3D | null {
68
- const guid = this.userToCamMap[userId];
69
- if (!guid) return null;
70
- return this.remoteCams[guid].obj;
71
- }
72
-
73
- @serializable([Object3D, AssetReference])
74
- public cameraPrefab: THREE.Object3D | null | AssetReference = null;
75
-
76
- private _lastWorldPosition!: THREE.Vector3;
77
- private _lastWorldQuaternion!: THREE.Quaternion;
78
- private _model: CameraModel | null = null;
79
- private _needsUpdate: boolean = true;
80
- private _lastUpdateTime: number = 0;
81
-
82
- private remoteCams: { [id: string]: UserCamInfo } = {};
83
- private userToCamMap: { [id: string]: string } = {};
84
- private _camTimeoutInSeconds = 10;
85
- private _receiveCallback: Function | null = null;
86
-
87
- async awake() {
88
- this._lastWorldPosition = this.worldPosition.clone();
89
- this._lastWorldQuaternion = this.worldQuaternion.clone();
90
-
91
- if (this.cameraPrefab) {
92
-
93
- if ("uri" in this.cameraPrefab) {
94
- this.cameraPrefab = await this.cameraPrefab.instantiate(this.gameObject);
95
- }
96
-
97
- if (this.cameraPrefab && "isObject3D" in this.cameraPrefab) {
98
- this.cameraPrefab.visible = false;
99
- }
100
- }
101
-
102
- }
103
-
104
- onEnable(): void {
105
- this._receiveCallback = this.context.connection.beginListenBinary(SyncedCameraModelIdentifier, this.onReceivedRemoteCameraInfoBin.bind(this));
106
- }
107
-
108
- onDisable(): void {
109
- this.context.connection.stopListenBinary(SyncedCameraModelIdentifier, this._receiveCallback);
110
- }
111
-
112
- update(): void {
113
-
114
- for (const guid in this.remoteCams) {
115
- const cam = this.remoteCams[guid];
116
- const timeDiff = this.context.time.realtimeSinceStartup - cam.lastUpdate;
117
- if (!cam || (timeDiff) > this._camTimeoutInSeconds) {
118
- console.log("Remote cam timeout", cam, timeDiff);
119
- if (cam?.obj) {
120
- GameObject.destroy(cam.obj);
121
- }
122
- delete this.remoteCams[guid];
123
- if (cam)
124
- delete this.userToCamMap[cam.userId];
125
-
126
- SyncedCamera.instances.push(cam);
127
- this.context.players.removePlayerView(cam.userId, ViewDevice.Browser);
128
- continue;
129
- }
130
- }
131
-
132
- if (WebXR.IsInWebXR) return;
133
-
134
- const cam = this.context.mainCamera
135
- if (cam === null) {
136
- this.enabled = false;
137
- return;
138
- }
139
-
140
- if (!this.context.connection.isConnected || this.context.connection.connectionId === null) return;
141
-
142
- if (this._model === null) {
143
- this._model = new CameraModel(this.context.connection.connectionId, this.context.connection.connectionId + "_camera");
144
- }
145
-
146
- const wp = utils.getWorldPosition(cam);
147
- const wq = utils.getWorldQuaternion(cam);
148
- if (wp.distanceTo(this._lastWorldPosition) > 0.001 || wq.angleTo(this._lastWorldQuaternion) > 0.01) {
149
- this._needsUpdate = true;
150
- }
151
- this._lastWorldPosition.copy(wp);
152
- this._lastWorldQuaternion.copy(wq);
153
-
154
- if (!this._needsUpdate || this.context.time.frameCount % 2 !== 0) {
155
- if (this.context.time.realtimeSinceStartup - this._lastUpdateTime > this._camTimeoutInSeconds * .5) {
156
- // send update anyways to avoid timeout
157
- }
158
- else return;
159
- }
160
-
161
- this._lastUpdateTime = this.context.time.realtimeSinceStartup;
162
- this._needsUpdate = false;
163
- this._model.send(cam, this.context.connection);
164
- if (!this.context.isInXR)
165
- this.context.players.setPlayerView(this.context.connection.connectionId, cam, ViewDevice.Browser);
166
- }
167
-
168
- private onReceivedRemoteCameraInfoBin(model: SyncedCameraModel) {
169
- const guid = model.guid();
170
- if (!guid) return;
171
- const userId = model.userId();
172
- if (!userId) return;
173
- if (!this.context.connection.userIsInRoom(userId)) return;
174
- if (!this.cameraPrefab) return;
175
- let rc = this.remoteCams[guid];
176
- if (!rc) {
177
- if ("isObject3D" in this.cameraPrefab) {
178
- const opt = new InstantiateOptions();
179
- opt.context = this.context;
180
- const instance = GameObject.instantiate(this.cameraPrefab, opt) as GameObject;
181
- rc = this.remoteCams[guid] = { obj: instance, lastUpdate: this.context.time.realtimeSinceStartup, userId: userId };
182
- rc.obj.visible = true;
183
- this.gameObject.add(instance);
184
- this.userToCamMap[userId] = guid;
185
- SyncedCamera.instances.push(rc);
186
-
187
- const marker = GameObject.getOrAddComponent(instance, AvatarMarker);
188
- marker.connectionId = userId;
189
- marker.avatar = instance;
190
-
191
- }
192
- else {
193
- return;
194
- }
195
- // console.log(this.remoteCams);
196
- }
197
- const obj = rc.obj;
198
- this.context.players.setPlayerView(userId, obj, ViewDevice.Browser);
199
- rc.lastUpdate = this.context.time.realtimeSinceStartup;
200
- InstancingUtil.markDirty(obj);
201
- const pos = model.pos();
202
- if (pos)
203
- utils.setWorldPositionXYZ(obj, pos.x(), pos.y(), pos.z());
204
- const rot = model.rot();
205
- if (rot)
206
- utils.setWorldRotationXYZ(obj, rot.x(), rot.y(), rot.z());
207
- }
1
+ import { NetworkConnection } from "../engine/engine_networking.js";
2
+ import { Behaviour, GameObject } from "./Component.js";
3
+ import { Camera } from "./Camera.js";
4
+ import * as utils from "../engine/engine_three_utils.js"
5
+ import { WebXR } from "./webxr/WebXR.js";
6
+ import { Builder } from "flatbuffers";
7
+ import { SyncedCameraModel } from "../engine-schemes/synced-camera-model.js";
8
+ import { Vec3 } from "../engine-schemes/vec3.js";
9
+ import { registerBinaryType } from "../engine-schemes/schemes.js";
10
+ import { InstancingUtil } from "../engine/engine_instancing.js";
11
+ import { serializable } from "../engine/engine_serialization_decorator.js";
12
+ import { Object3D } from "three";
13
+ import { AvatarMarker } from "./webxr/WebXRAvatar.js";
14
+ import { AssetReference } from "../engine/engine_addressables.js";
15
+ import { ViewDevice } from "../engine/engine_playerview.js";
16
+ import { InstantiateOptions } from "../engine/engine_gameobject.js";
17
+
18
+ const SyncedCameraModelIdentifier = "SCAM";
19
+ registerBinaryType(SyncedCameraModelIdentifier, SyncedCameraModel.getRootAsSyncedCameraModel);
20
+ const builder = new Builder();
21
+
22
+ // enum CameraSyncEvent {
23
+ // Update = "sync-update-camera",
24
+ // }
25
+
26
+ class CameraModel {
27
+ userId: string;
28
+ guid: string;
29
+ // dontSave: boolean = true;
30
+ // pos: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
31
+ // rot: { x: number, y: number, z: number } = { x: 0, y: 0, z: 0 };
32
+
33
+ constructor(connectionId: string, guid: string) {
34
+ this.guid = guid;
35
+ this.userId = connectionId;
36
+ }
37
+
38
+ send(cam: THREE.Camera | null | undefined, con: NetworkConnection) {
39
+ if (cam) {
40
+ builder.clear();
41
+ const guid = builder.createString(this.guid);
42
+ const userId = builder.createString(this.userId);
43
+ SyncedCameraModel.startSyncedCameraModel(builder);
44
+ SyncedCameraModel.addGuid(builder, guid);
45
+ SyncedCameraModel.addUserId(builder, userId);
46
+ const p = utils.getWorldPosition(cam);
47
+ const r = utils.getWorldRotation(cam);
48
+ SyncedCameraModel.addPos(builder, Vec3.createVec3(builder, p.x, p.y, p.z));
49
+ SyncedCameraModel.addRot(builder, Vec3.createVec3(builder, r.x, r.y, r.z));
50
+ const offset = SyncedCameraModel.endSyncedCameraModel(builder);
51
+ builder.finish(offset, SyncedCameraModelIdentifier);
52
+ con.sendBinary(builder.asUint8Array());
53
+ }
54
+ }
55
+ }
56
+
57
+ declare type UserCamInfo = {
58
+ obj: THREE.Object3D,
59
+ lastUpdate: number;
60
+ userId: string;
61
+ };
62
+
63
+ export class SyncedCamera extends Behaviour {
64
+
65
+ static instances: UserCamInfo[] = [];
66
+
67
+ getCameraObject(userId: string): THREE.Object3D | null {
68
+ const guid = this.userToCamMap[userId];
69
+ if (!guid) return null;
70
+ return this.remoteCams[guid].obj;
71
+ }
72
+
73
+ @serializable([Object3D, AssetReference])
74
+ public cameraPrefab: THREE.Object3D | null | AssetReference = null;
75
+
76
+ private _lastWorldPosition!: THREE.Vector3;
77
+ private _lastWorldQuaternion!: THREE.Quaternion;
78
+ private _model: CameraModel | null = null;
79
+ private _needsUpdate: boolean = true;
80
+ private _lastUpdateTime: number = 0;
81
+
82
+ private remoteCams: { [id: string]: UserCamInfo } = {};
83
+ private userToCamMap: { [id: string]: string } = {};
84
+ private _camTimeoutInSeconds = 10;
85
+ private _receiveCallback: Function | null = null;
86
+
87
+ async awake() {
88
+ this._lastWorldPosition = this.worldPosition.clone();
89
+ this._lastWorldQuaternion = this.worldQuaternion.clone();
90
+
91
+ if (this.cameraPrefab) {
92
+
93
+ if ("uri" in this.cameraPrefab) {
94
+ this.cameraPrefab = await this.cameraPrefab.instantiate(this.gameObject);
95
+ }
96
+
97
+ if (this.cameraPrefab && "isObject3D" in this.cameraPrefab) {
98
+ this.cameraPrefab.visible = false;
99
+ }
100
+ }
101
+
102
+ }
103
+
104
+ onEnable(): void {
105
+ this._receiveCallback = this.context.connection.beginListenBinary(SyncedCameraModelIdentifier, this.onReceivedRemoteCameraInfoBin.bind(this));
106
+ }
107
+
108
+ onDisable(): void {
109
+ this.context.connection.stopListenBinary(SyncedCameraModelIdentifier, this._receiveCallback);
110
+ }
111
+
112
+ update(): void {
113
+
114
+ for (const guid in this.remoteCams) {
115
+ const cam = this.remoteCams[guid];
116
+ const timeDiff = this.context.time.realtimeSinceStartup - cam.lastUpdate;
117
+ if (!cam || (timeDiff) > this._camTimeoutInSeconds) {
118
+ console.log("Remote cam timeout", cam, timeDiff);
119
+ if (cam?.obj) {
120
+ GameObject.destroy(cam.obj);
121
+ }
122
+ delete this.remoteCams[guid];
123
+ if (cam)
124
+ delete this.userToCamMap[cam.userId];
125
+
126
+ SyncedCamera.instances.push(cam);
127
+ this.context.players.removePlayerView(cam.userId, ViewDevice.Browser);
128
+ continue;
129
+ }
130
+ }
131
+
132
+ if (WebXR.IsInWebXR) return;
133
+
134
+ const cam = this.context.mainCamera
135
+ if (cam === null) {
136
+ this.enabled = false;
137
+ return;
138
+ }
139
+
140
+ if (!this.context.connection.isConnected || this.context.connection.connectionId === null) return;
141
+
142
+ if (this._model === null) {
143
+ this._model = new CameraModel(this.context.connection.connectionId, this.context.connection.connectionId + "_camera");
144
+ }
145
+
146
+ const wp = utils.getWorldPosition(cam);
147
+ const wq = utils.getWorldQuaternion(cam);
148
+ if (wp.distanceTo(this._lastWorldPosition) > 0.001 || wq.angleTo(this._lastWorldQuaternion) > 0.01) {
149
+ this._needsUpdate = true;
150
+ }
151
+ this._lastWorldPosition.copy(wp);
152
+ this._lastWorldQuaternion.copy(wq);
153
+
154
+ if (!this._needsUpdate || this.context.time.frameCount % 2 !== 0) {
155
+ if (this.context.time.realtimeSinceStartup - this._lastUpdateTime > this._camTimeoutInSeconds * .5) {
156
+ // send update anyways to avoid timeout
157
+ }
158
+ else return;
159
+ }
160
+
161
+ this._lastUpdateTime = this.context.time.realtimeSinceStartup;
162
+ this._needsUpdate = false;
163
+ this._model.send(cam, this.context.connection);
164
+ if (!this.context.isInXR)
165
+ this.context.players.setPlayerView(this.context.connection.connectionId, cam, ViewDevice.Browser);
166
+ }
167
+
168
+ private onReceivedRemoteCameraInfoBin(model: SyncedCameraModel) {
169
+ const guid = model.guid();
170
+ if (!guid) return;
171
+ const userId = model.userId();
172
+ if (!userId) return;
173
+ if (!this.context.connection.userIsInRoom(userId)) return;
174
+ if (!this.cameraPrefab) return;
175
+ let rc = this.remoteCams[guid];
176
+ if (!rc) {
177
+ if ("isObject3D" in this.cameraPrefab) {
178
+ const opt = new InstantiateOptions();
179
+ opt.context = this.context;
180
+ const instance = GameObject.instantiate(this.cameraPrefab, opt) as GameObject;
181
+ rc = this.remoteCams[guid] = { obj: instance, lastUpdate: this.context.time.realtimeSinceStartup, userId: userId };
182
+ rc.obj.visible = true;
183
+ this.gameObject.add(instance);
184
+ this.userToCamMap[userId] = guid;
185
+ SyncedCamera.instances.push(rc);
186
+
187
+ const marker = GameObject.getOrAddComponent(instance, AvatarMarker);
188
+ marker.connectionId = userId;
189
+ marker.avatar = instance;
190
+
191
+ }
192
+ else {
193
+ return;
194
+ }
195
+ // console.log(this.remoteCams);
196
+ }
197
+ const obj = rc.obj;
198
+ this.context.players.setPlayerView(userId, obj, ViewDevice.Browser);
199
+ rc.lastUpdate = this.context.time.realtimeSinceStartup;
200
+ InstancingUtil.markDirty(obj);
201
+ const pos = model.pos();
202
+ if (pos)
203
+ utils.setWorldPositionXYZ(obj, pos.x(), pos.y(), pos.z());
204
+ const rot = model.rot();
205
+ if (rot)
206
+ utils.setWorldRotationXYZ(obj, rot.x(), rot.y(), rot.z());
207
+ }
208
208
  }
src/engine-components/SyncedTransform.ts CHANGED
@@ -1,337 +1,337 @@
1
- import * as THREE from 'three'
2
- import { OwnershipModel, RoomEvents } from "../engine/engine_networking.js"
3
- import { Behaviour, GameObject } from "./Component.js";
4
- import { Rigidbody } from "./RigidBody.js";
5
- import * as utils from "../engine/engine_utils.js"
6
- import { sendDestroyed } from '../engine/engine_networking_instantiate.js';
7
- import { InstancingUtil } from "../engine/engine_instancing.js";
8
- import { SyncedTransformModel } from '../engine-schemes/synced-transform-model.js';
9
- import * as flatbuffers from "flatbuffers";
10
- import { Transform } from '../engine-schemes/transform.js';
11
- import { registerBinaryType } from '../engine-schemes/schemes.js';
12
- import { setWorldEuler } from '../engine/engine_three_utils.js';
13
-
14
- const debug = utils.getParam("debugsync");
15
- export const SyncedTransformIdentifier = "STRS";
16
- registerBinaryType(SyncedTransformIdentifier, SyncedTransformModel.getRootAsSyncedTransformModel);
17
-
18
- const builder = new flatbuffers.Builder();
19
-
20
- export function createTransformModel(guid: string, b: Behaviour, fast: boolean = true): Uint8Array {
21
- builder.clear();
22
- const guidObj = builder.createString(guid);
23
- SyncedTransformModel.startSyncedTransformModel(builder);
24
- SyncedTransformModel.addGuid(builder, guidObj);
25
- SyncedTransformModel.addFast(builder, fast);
26
- const p = b.worldPosition;
27
- const r = b.worldEuler;
28
- const s = b.gameObject.scale; // todo: world scale
29
- // console.log(p, r, s);
30
- SyncedTransformModel.addTransform(builder, Transform.createTransform(builder, p.x, p.y, p.z, r.x, r.y, r.z, s.x, s.y, s.z));
31
- const res = SyncedTransformModel.endSyncedTransformModel(builder);
32
- // SyncedTransformModel.finishSyncedTransformModelBuffer(builder, res);
33
- builder.finish(res, SyncedTransformIdentifier);
34
- return builder.asUint8Array();
35
- }
36
-
37
-
38
- export class SyncedTransform extends Behaviour {
39
-
40
- // public autoOwnership: boolean = true;
41
- public overridePhysics: boolean = true
42
- public interpolatePosition: boolean = true;
43
- public interpolateRotation: boolean = true;
44
- public fastMode: boolean = false;
45
- public syncDestroy: boolean = false;
46
-
47
- // private _state!: SyncedTransformModel;
48
- private _model: OwnershipModel | null = null;
49
- private _needsUpdate: boolean = true;
50
- private rb: Rigidbody | null = null;
51
- private _wasKinematic: boolean | undefined = false;
52
- private _receivedDataBefore: boolean = false;
53
-
54
- private _targetPosition!: THREE.Vector3;
55
- private _targetRotation!: THREE.Quaternion;
56
-
57
- private _receivedFastUpdate: boolean = false;
58
- private _shouldRequestOwnership: boolean = false;
59
-
60
- public requestOwnership() {
61
- if (debug)
62
- console.log("Request ownership");
63
- if (!this._model) {
64
- this._shouldRequestOwnership = true;
65
- this._needsUpdate = true;
66
- }
67
- else
68
- this._model.requestOwnership();
69
- }
70
-
71
- public hasOwnership(): boolean | undefined {
72
- return this._model?.hasOwnership ?? undefined;
73
- }
74
-
75
- public isOwned(): boolean | undefined {
76
- return this._model?.isOwned;
77
- }
78
-
79
- private joinedRoomCallback: any = null;
80
- private receivedDataCallback: any = null;
81
-
82
- awake() {
83
- if (debug)
84
- console.log("new instance", this.guid, this);
85
- this._receivedDataBefore = false;
86
- this._targetPosition = new THREE.Vector3();
87
- this._targetRotation = new THREE.Quaternion();
88
-
89
- // sync instantiate issue was because they shared the same last pos vector!
90
- this.lastWorldPos = new THREE.Vector3();
91
- this.lastWorldRotation = new THREE.Quaternion();
92
-
93
- this.rb = GameObject.getComponentInChildren(this.gameObject, Rigidbody);
94
- if (this.rb) {
95
- this._wasKinematic = this.rb.isKinematic;
96
- }
97
-
98
- this.receivedUpdate = true;
99
- // this._state = new TransformModel(this.guid, this);
100
- this._model = new OwnershipModel(this.context.connection, this.guid);
101
- if (this.context.connection.isConnected) {
102
- this.tryGetLastState();
103
- }
104
-
105
- this.joinedRoomCallback = this.tryGetLastState.bind(this);
106
- this.context.connection.beginListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
107
- this.receivedDataCallback = this.onReceivedData.bind(this);
108
- this.context.connection.beginListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
109
- }
110
-
111
- onDestroy(): void {
112
- // TODO: can we add a new component for this?! do we really need this?!
113
- if (this.syncDestroy)
114
- sendDestroyed(this.guid, this.context.connection);
115
- this._model = null;
116
- this.context.connection.stopListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
117
- this.context.connection.stopListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
118
- }
119
-
120
- private tryGetLastState() {
121
- const model = this.context.connection.tryGetState(this.guid) as unknown as SyncedTransformModel;
122
- if (model) this.onReceivedData(model);
123
- }
124
-
125
- private tempEuler: THREE.Euler = new THREE.Euler();
126
-
127
- private onReceivedData(data: SyncedTransformModel) {
128
- if (this.destroyed) return;
129
- if (typeof data.guid === "function" && data.guid() === this.guid) {
130
- if (debug)
131
- console.log("new data", this.context.connection.connectionId, this.context.time.frameCount, this.guid, data);
132
- this.receivedUpdate = true;
133
- this._receivedFastUpdate = data.fast();
134
- const transform = data.transform();
135
- if (transform) {
136
- InstancingUtil.markDirty(this.gameObject, true);
137
- const position = transform.position();
138
- if (position) {
139
- if (this.interpolatePosition)
140
- this._targetPosition?.set(position.x(), position.y(), position.z());
141
- if (!this.interpolatePosition || !this._receivedDataBefore)
142
- this.setWorldPosition(position.x(), position.y(), position.z());
143
- }
144
-
145
- const rotation = transform.rotation();
146
- if (rotation) {
147
- this.tempEuler.set(rotation.x(), rotation.y(), rotation.z());
148
- if (this.interpolateRotation) {
149
- this._targetRotation.setFromEuler(this.tempEuler);
150
- }
151
- if (!this.interpolateRotation || !this._receivedDataBefore)
152
- setWorldEuler(this.gameObject, this.tempEuler);
153
- }
154
- }
155
- this._receivedDataBefore = true;
156
-
157
- // if (this.rb && !this._model?.hasOwnership) {
158
- // this.rb.setBodyFromGameObject(data.velocity)
159
- // }
160
- }
161
- }
162
-
163
- onEnable(): void {
164
- this.lastWorldPos.copy(this.worldPosition);
165
- this.lastWorldRotation.copy(this.worldQuaternion);
166
- this._needsUpdate = true;
167
- // console.log("ENABLE", this.guid, this.gameObject.guid, this.lastWorldPos);
168
- if (this._model) {
169
- this._model.updateIsOwned();
170
- }
171
- }
172
-
173
- onDisable(): void {
174
- if (this._model)
175
- this._model.freeOwnership();
176
- }
177
-
178
-
179
- private receivedUpdate = false;
180
- private lastWorldPos!: THREE.Vector3;
181
- private lastWorldRotation!: THREE.Quaternion;
182
-
183
- onBeforeRender() {
184
- if (!this.activeAndEnabled || !this.context.connection.isConnected) return;
185
- // console.log("BEFORE RENDER", this.destroyed, this.guid, this._model?.isOwned, this.name, this.gameObject);
186
-
187
- if (!this.context.connection.isInRoom || !this._model) {
188
- if (debug)
189
- console.log("no model or room", this.name, this.guid, this.context.connection.isInRoom);
190
- return;
191
- }
192
-
193
- if (this._shouldRequestOwnership) {
194
- this._shouldRequestOwnership = false;
195
- this._model.requestOwnership();
196
- }
197
-
198
- let wp = this.worldPosition;
199
- let wr = this.worldQuaternion;
200
- if (this._model.isOwned && !this.receivedUpdate) {
201
- const worlddiff = wp.distanceTo(this.lastWorldPos);
202
- const worldRot = wr.angleTo(this.lastWorldRotation);
203
- const threshold = this._model.hasOwnership || this.fastMode ? .0001 : .001;
204
- if (worlddiff > threshold || worldRot > threshold) {
205
- // console.log(worlddiff, worldRot);
206
- if (!this._model.hasOwnership) {
207
-
208
- if (debug)
209
- console.log(this.guid, "reset because not owned but", this.gameObject.name, this.lastWorldPos);
210
-
211
- this.worldPosition = this.lastWorldPos;
212
- wp.copy(this.lastWorldPos);
213
-
214
- this.worldQuaternion = this.lastWorldRotation;
215
- wr.copy(this.lastWorldRotation);
216
-
217
- InstancingUtil.markDirty(this.gameObject, true);
218
- this._needsUpdate = false;
219
- }
220
- else {
221
- this._needsUpdate = true;
222
- }
223
- }
224
- }
225
- // else if (this._model.isOwned === false) {
226
- // if (!this._didRequestOwnershipOnce && this.autoOwnership) {
227
- // this._didRequestOwnershipOnce = true;
228
- // this._model.requestOwnershipIfNotOwned();
229
- // }
230
- // }
231
-
232
-
233
- if (this._model && !this._model.hasOwnership && this._model.isOwned) {
234
- if (this._receivedDataBefore) {
235
- const factor = this._receivedFastUpdate || this.fastMode ? .5 : .3;
236
- const t = factor;//Mathf.clamp01(this.context.time.deltaTime * factor);
237
- let requireMarkDirty = false;
238
- if (this.interpolatePosition && this._targetPosition) {
239
- const pos = this.worldPosition;
240
- pos.lerp(this._targetPosition, t);
241
- this.worldPosition = pos;
242
- requireMarkDirty = true;
243
- }
244
- if (this.interpolateRotation && this._targetRotation) {
245
- const rot = this.worldQuaternion;
246
- rot.slerp(this._targetRotation, t);
247
- this.worldQuaternion = rot;
248
- requireMarkDirty = true;
249
- }
250
- if (requireMarkDirty)
251
- InstancingUtil.markDirty(this.gameObject, true);
252
- }
253
- }
254
-
255
-
256
- this.receivedUpdate = false;
257
- this.lastWorldPos.copy(wp);
258
- this.lastWorldRotation.copy(wr);
259
-
260
-
261
- // if (this._model.isOwned === false && this.autoOwnership) {
262
- // this.requestOwnership();
263
- // }
264
-
265
- if (!this._model) return;
266
- // only run if we are the owner
267
- if (!this._model || this._model.hasOwnership === undefined || !this._model.hasOwnership) {
268
- if (this.rb) {
269
- this.rb.isKinematic = this._model.isOwned ?? false;
270
- this.rb.setVelocity(0, 0, 0);
271
- }
272
- return;
273
- }
274
-
275
- // local user is owner:
276
-
277
- if (this.rb) {
278
- if (this._wasKinematic !== undefined) {
279
- if (debug)
280
- console.log("reset kinematic", this.rb.name, this._wasKinematic);
281
- this.rb.isKinematic = this._wasKinematic;
282
- }
283
-
284
- // hacky reset if too far off
285
- if (this.gameObject.position.distanceTo(new THREE.Vector3(0, 0, 0)) > 1000) {
286
- if (debug)
287
- console.log("RESET", this.name)
288
- this.gameObject.position.set(0, 1, 0);
289
- this.rb.setVelocity(0, 0, 0);
290
- }
291
- }
292
-
293
- const updateInterval = 10;
294
- const fastUpdate = this.rb || this.fastMode;
295
- if (this._needsUpdate && (updateInterval <= 0 || updateInterval > 0 && this.context.time.frameCount % updateInterval === 0 || fastUpdate)) {
296
-
297
- if (debug)
298
- console.log("send update", this.context.connection.connectionId, this.guid, this.gameObject.name, this.gameObject.guid);
299
-
300
- if (this.overridePhysics && this.rb) {
301
- // this.rb.setBodyFromGameObject();
302
- }
303
-
304
- this._needsUpdate = false;
305
- const st = createTransformModel(this.guid, this, fastUpdate ? true : false);
306
- // this._state.update(this, this.rb);
307
- // this._state.fast = fastUpdate ? true : false;
308
- this.context.connection.sendBinary(st);
309
- }
310
- }
311
-
312
-
313
- // private lastPosition: THREE.Vector3 = new THREE.Vector3();
314
-
315
- // private async setPosition(pt: THREE.Vector3) {
316
-
317
- // if (this._model.isConnected && !this._model?.hasOwnership) {
318
- // await this._model?.requestOwnershipAsync();
319
- // }
320
-
321
- // if (pt.distanceTo(this.lastPosition) < .001) {
322
- // return;
323
- // }
324
- // this.lastPosition.copy(pt);
325
-
326
- // if(this.gameObject.parent) this.gameObject.parent.worldToLocal(pt);
327
- // // this.gameObject.position.copy(pt);
328
- // this.gameObject.position.set(pt.x, pt.y, pt.z);
329
- // this._target.set(pt.x, pt.y, pt.z);
330
- // this._needsUpdate = true;
331
- // if (this.rb) {
332
- // this.gameObject.position.set(pt.x, pt.y + .5, pt.z);
333
- // this.rb.setVelocity(0, 0, 0);
334
- // this.rb.setBodyFromGameObject();
335
- // }
336
- // }
1
+ import * as THREE from 'three'
2
+ import { OwnershipModel, RoomEvents } from "../engine/engine_networking.js"
3
+ import { Behaviour, GameObject } from "./Component.js";
4
+ import { Rigidbody } from "./RigidBody.js";
5
+ import * as utils from "../engine/engine_utils.js"
6
+ import { sendDestroyed } from '../engine/engine_networking_instantiate.js';
7
+ import { InstancingUtil } from "../engine/engine_instancing.js";
8
+ import { SyncedTransformModel } from '../engine-schemes/synced-transform-model.js';
9
+ import * as flatbuffers from "flatbuffers";
10
+ import { Transform } from '../engine-schemes/transform.js';
11
+ import { registerBinaryType } from '../engine-schemes/schemes.js';
12
+ import { setWorldEuler } from '../engine/engine_three_utils.js';
13
+
14
+ const debug = utils.getParam("debugsync");
15
+ export const SyncedTransformIdentifier = "STRS";
16
+ registerBinaryType(SyncedTransformIdentifier, SyncedTransformModel.getRootAsSyncedTransformModel);
17
+
18
+ const builder = new flatbuffers.Builder();
19
+
20
+ export function createTransformModel(guid: string, b: Behaviour, fast: boolean = true): Uint8Array {
21
+ builder.clear();
22
+ const guidObj = builder.createString(guid);
23
+ SyncedTransformModel.startSyncedTransformModel(builder);
24
+ SyncedTransformModel.addGuid(builder, guidObj);
25
+ SyncedTransformModel.addFast(builder, fast);
26
+ const p = b.worldPosition;
27
+ const r = b.worldEuler;
28
+ const s = b.gameObject.scale; // todo: world scale
29
+ // console.log(p, r, s);
30
+ SyncedTransformModel.addTransform(builder, Transform.createTransform(builder, p.x, p.y, p.z, r.x, r.y, r.z, s.x, s.y, s.z));
31
+ const res = SyncedTransformModel.endSyncedTransformModel(builder);
32
+ // SyncedTransformModel.finishSyncedTransformModelBuffer(builder, res);
33
+ builder.finish(res, SyncedTransformIdentifier);
34
+ return builder.asUint8Array();
35
+ }
36
+
37
+
38
+ export class SyncedTransform extends Behaviour {
39
+
40
+ // public autoOwnership: boolean = true;
41
+ public overridePhysics: boolean = true
42
+ public interpolatePosition: boolean = true;
43
+ public interpolateRotation: boolean = true;
44
+ public fastMode: boolean = false;
45
+ public syncDestroy: boolean = false;
46
+
47
+ // private _state!: SyncedTransformModel;
48
+ private _model: OwnershipModel | null = null;
49
+ private _needsUpdate: boolean = true;
50
+ private rb: Rigidbody | null = null;
51
+ private _wasKinematic: boolean | undefined = false;
52
+ private _receivedDataBefore: boolean = false;
53
+
54
+ private _targetPosition!: THREE.Vector3;
55
+ private _targetRotation!: THREE.Quaternion;
56
+
57
+ private _receivedFastUpdate: boolean = false;
58
+ private _shouldRequestOwnership: boolean = false;
59
+
60
+ public requestOwnership() {
61
+ if (debug)
62
+ console.log("Request ownership");
63
+ if (!this._model) {
64
+ this._shouldRequestOwnership = true;
65
+ this._needsUpdate = true;
66
+ }
67
+ else
68
+ this._model.requestOwnership();
69
+ }
70
+
71
+ public hasOwnership(): boolean | undefined {
72
+ return this._model?.hasOwnership ?? undefined;
73
+ }
74
+
75
+ public isOwned(): boolean | undefined {
76
+ return this._model?.isOwned;
77
+ }
78
+
79
+ private joinedRoomCallback: any = null;
80
+ private receivedDataCallback: any = null;
81
+
82
+ awake() {
83
+ if (debug)
84
+ console.log("new instance", this.guid, this);
85
+ this._receivedDataBefore = false;
86
+ this._targetPosition = new THREE.Vector3();
87
+ this._targetRotation = new THREE.Quaternion();
88
+
89
+ // sync instantiate issue was because they shared the same last pos vector!
90
+ this.lastWorldPos = new THREE.Vector3();
91
+ this.lastWorldRotation = new THREE.Quaternion();
92
+
93
+ this.rb = GameObject.getComponentInChildren(this.gameObject, Rigidbody);
94
+ if (this.rb) {
95
+ this._wasKinematic = this.rb.isKinematic;
96
+ }
97
+
98
+ this.receivedUpdate = true;
99
+ // this._state = new TransformModel(this.guid, this);
100
+ this._model = new OwnershipModel(this.context.connection, this.guid);
101
+ if (this.context.connection.isConnected) {
102
+ this.tryGetLastState();
103
+ }
104
+
105
+ this.joinedRoomCallback = this.tryGetLastState.bind(this);
106
+ this.context.connection.beginListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
107
+ this.receivedDataCallback = this.onReceivedData.bind(this);
108
+ this.context.connection.beginListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
109
+ }
110
+
111
+ onDestroy(): void {
112
+ // TODO: can we add a new component for this?! do we really need this?!
113
+ if (this.syncDestroy)
114
+ sendDestroyed(this.guid, this.context.connection);
115
+ this._model = null;
116
+ this.context.connection.stopListen(RoomEvents.JoinedRoom, this.joinedRoomCallback);
117
+ this.context.connection.stopListenBinary(SyncedTransformIdentifier, this.receivedDataCallback);
118
+ }
119
+
120
+ private tryGetLastState() {
121
+ const model = this.context.connection.tryGetState(this.guid) as unknown as SyncedTransformModel;
122
+ if (model) this.onReceivedData(model);
123
+ }
124
+
125
+ private tempEuler: THREE.Euler = new THREE.Euler();
126
+
127
+ private onReceivedData(data: SyncedTransformModel) {
128
+ if (this.destroyed) return;
129
+ if (typeof data.guid === "function" && data.guid() === this.guid) {
130
+ if (debug)
131
+ console.log("new data", this.context.connection.connectionId, this.context.time.frameCount, this.guid, data);
132
+ this.receivedUpdate = true;
133
+ this._receivedFastUpdate = data.fast();
134
+ const transform = data.transform();
135
+ if (transform) {
136
+ InstancingUtil.markDirty(this.gameObject, true);
137
+ const position = transform.position();
138
+ if (position) {
139
+ if (this.interpolatePosition)
140
+ this._targetPosition?.set(position.x(), position.y(), position.z());
141
+ if (!this.interpolatePosition || !this._receivedDataBefore)
142
+ this.setWorldPosition(position.x(), position.y(), position.z());
143
+ }
144
+
145
+ const rotation = transform.rotation();
146
+ if (rotation) {
147
+ this.tempEuler.set(rotation.x(), rotation.y(), rotation.z());
148
+ if (this.interpolateRotation) {
149
+ this._targetRotation.setFromEuler(this.tempEuler);
150
+ }
151
+ if (!this.interpolateRotation || !this._receivedDataBefore)
152
+ setWorldEuler(this.gameObject, this.tempEuler);
153
+ }
154
+ }
155
+ this._receivedDataBefore = true;
156
+
157
+ // if (this.rb && !this._model?.hasOwnership) {
158
+ // this.rb.setBodyFromGameObject(data.velocity)
159
+ // }
160
+ }
161
+ }
162
+
163
+ onEnable(): void {
164
+ this.lastWorldPos.copy(this.worldPosition);
165
+ this.lastWorldRotation.copy(this.worldQuaternion);
166
+ this._needsUpdate = true;
167
+ // console.log("ENABLE", this.guid, this.gameObject.guid, this.lastWorldPos);
168
+ if (this._model) {
169
+ this._model.updateIsOwned();
170
+ }
171
+ }
172
+
173
+ onDisable(): void {
174
+ if (this._model)
175
+ this._model.freeOwnership();
176
+ }
177
+
178
+
179
+ private receivedUpdate = false;
180
+ private lastWorldPos!: THREE.Vector3;
181
+ private lastWorldRotation!: THREE.Quaternion;
182
+
183
+ onBeforeRender() {
184
+ if (!this.activeAndEnabled || !this.context.connection.isConnected) return;
185
+ // console.log("BEFORE RENDER", this.destroyed, this.guid, this._model?.isOwned, this.name, this.gameObject);
186
+
187
+ if (!this.context.connection.isInRoom || !this._model) {
188
+ if (debug)
189
+ console.log("no model or room", this.name, this.guid, this.context.connection.isInRoom);
190
+ return;
191
+ }
192
+
193
+ if (this._shouldRequestOwnership) {
194
+ this._shouldRequestOwnership = false;
195
+ this._model.requestOwnership();
196
+ }
197
+
198
+ const wp = this.worldPosition;
199
+ const wr = this.worldQuaternion;
200
+ if (this._model.isOwned && !this.receivedUpdate) {
201
+ const worlddiff = wp.distanceTo(this.lastWorldPos);
202
+ const worldRot = wr.angleTo(this.lastWorldRotation);
203
+ const threshold = this._model.hasOwnership || this.fastMode ? .0001 : .001;
204
+ if (worlddiff > threshold || worldRot > threshold) {
205
+ // console.log(worlddiff, worldRot);
206
+ if (!this._model.hasOwnership) {
207
+
208
+ if (debug)
209
+ console.log(this.guid, "reset because not owned but", this.gameObject.name, this.lastWorldPos);
210
+
211
+ this.worldPosition = this.lastWorldPos;
212
+ wp.copy(this.lastWorldPos);
213
+
214
+ this.worldQuaternion = this.lastWorldRotation;
215
+ wr.copy(this.lastWorldRotation);
216
+
217
+ InstancingUtil.markDirty(this.gameObject, true);
218
+ this._needsUpdate = false;
219
+ }
220
+ else {
221
+ this._needsUpdate = true;
222
+ }
223
+ }
224
+ }
225
+ // else if (this._model.isOwned === false) {
226
+ // if (!this._didRequestOwnershipOnce && this.autoOwnership) {
227
+ // this._didRequestOwnershipOnce = true;
228
+ // this._model.requestOwnershipIfNotOwned();
229
+ // }
230
+ // }
231
+
232
+
233
+ if (this._model && !this._model.hasOwnership && this._model.isOwned) {
234
+ if (this._receivedDataBefore) {
235
+ const factor = this._receivedFastUpdate || this.fastMode ? .5 : .3;
236
+ const t = factor;//Mathf.clamp01(this.context.time.deltaTime * factor);
237
+ let requireMarkDirty = false;
238
+ if (this.interpolatePosition && this._targetPosition) {
239
+ const pos = this.worldPosition;
240
+ pos.lerp(this._targetPosition, t);
241
+ this.worldPosition = pos;
242
+ requireMarkDirty = true;
243
+ }
244
+ if (this.interpolateRotation && this._targetRotation) {
245
+ const rot = this.worldQuaternion;
246
+ rot.slerp(this._targetRotation, t);
247
+ this.worldQuaternion = rot;
248
+ requireMarkDirty = true;
249
+ }
250
+ if (requireMarkDirty)
251
+ InstancingUtil.markDirty(this.gameObject, true);
252
+ }
253
+ }
254
+
255
+
256
+ this.receivedUpdate = false;
257
+ this.lastWorldPos.copy(wp);
258
+ this.lastWorldRotation.copy(wr);
259
+
260
+
261
+ // if (this._model.isOwned === false && this.autoOwnership) {
262
+ // this.requestOwnership();
263
+ // }
264
+
265
+ if (!this._model) return;
266
+ // only run if we are the owner
267
+ if (!this._model || this._model.hasOwnership === undefined || !this._model.hasOwnership) {
268
+ if (this.rb) {
269
+ this.rb.isKinematic = this._model.isOwned ?? false;
270
+ this.rb.setVelocity(0, 0, 0);
271
+ }
272
+ return;
273
+ }
274
+
275
+ // local user is owner:
276
+
277
+ if (this.rb) {
278
+ if (this._wasKinematic !== undefined) {
279
+ if (debug)
280
+ console.log("reset kinematic", this.rb.name, this._wasKinematic);
281
+ this.rb.isKinematic = this._wasKinematic;
282
+ }
283
+
284
+ // hacky reset if too far off
285
+ if (this.gameObject.position.distanceTo(new THREE.Vector3(0, 0, 0)) > 1000) {
286
+ if (debug)
287
+ console.log("RESET", this.name)
288
+ this.gameObject.position.set(0, 1, 0);
289
+ this.rb.setVelocity(0, 0, 0);
290
+ }
291
+ }
292
+
293
+ const updateInterval = 10;
294
+ const fastUpdate = this.rb || this.fastMode;
295
+ if (this._needsUpdate && (updateInterval <= 0 || updateInterval > 0 && this.context.time.frameCount % updateInterval === 0 || fastUpdate)) {
296
+
297
+ if (debug)
298
+ console.log("send update", this.context.connection.connectionId, this.guid, this.gameObject.name, this.gameObject.guid);
299
+
300
+ if (this.overridePhysics && this.rb) {
301
+ // this.rb.setBodyFromGameObject();
302
+ }
303
+
304
+ this._needsUpdate = false;
305
+ const st = createTransformModel(this.guid, this, fastUpdate ? true : false);
306
+ // this._state.update(this, this.rb);
307
+ // this._state.fast = fastUpdate ? true : false;
308
+ this.context.connection.sendBinary(st);
309
+ }
310
+ }
311
+
312
+
313
+ // private lastPosition: THREE.Vector3 = new THREE.Vector3();
314
+
315
+ // private async setPosition(pt: THREE.Vector3) {
316
+
317
+ // if (this._model.isConnected && !this._model?.hasOwnership) {
318
+ // await this._model?.requestOwnershipAsync();
319
+ // }
320
+
321
+ // if (pt.distanceTo(this.lastPosition) < .001) {
322
+ // return;
323
+ // }
324
+ // this.lastPosition.copy(pt);
325
+
326
+ // if(this.gameObject.parent) this.gameObject.parent.worldToLocal(pt);
327
+ // // this.gameObject.position.copy(pt);
328
+ // this.gameObject.position.set(pt.x, pt.y, pt.z);
329
+ // this._target.set(pt.x, pt.y, pt.z);
330
+ // this._needsUpdate = true;
331
+ // if (this.rb) {
332
+ // this.gameObject.position.set(pt.x, pt.y + .5, pt.z);
333
+ // this.rb.setVelocity(0, 0, 0);
334
+ // this.rb.setBodyFromGameObject();
335
+ // }
336
+ // }
337
337
  }
src/engine-components/ui/Text.ts CHANGED
@@ -182,7 +182,7 @@
182
182
 
183
183
 
184
184
  private getTextOpts(): object {
185
- let fontSize = this.fontSize;
185
+ const fontSize = this.fontSize;
186
186
  // if (this.canvas) {
187
187
  // fontSize /= this.canvas?.scaleFactor;
188
188
  // }
@@ -438,8 +438,8 @@
438
438
  // - Arial instead of assets/arial
439
439
  // - Arial should stay Arial instead of arial
440
440
  if (!this.font) return;
441
- let fontName = this.font;
442
- let familyName = this.getFamilyNameWithCorrectSuffix(fontName, fontStyle);
441
+ const fontName = this.font;
442
+ const familyName = this.getFamilyNameWithCorrectSuffix(fontName, fontStyle);
443
443
  if (debug) console.log("Selected font family:" + familyName);
444
444
 
445
445
  // ensure a font family is register under this name
@@ -514,7 +514,7 @@
514
514
  if (pathSeparatorIndex >= 0) {
515
515
  fontBaseName = fontBaseName.substring(pathSeparatorIndex + 1);
516
516
  }
517
- let isUpperCase = fontBaseName[0] === fontBaseName[0].toUpperCase();
517
+ const isUpperCase = fontBaseName[0] === fontBaseName[0].toUpperCase();
518
518
  const fontNameWithoutSuffix = familyName.substring(0, styleSeparator);
519
519
  if (debug) console.log("Select font: ", familyName, FontStyle[style], fontBaseName, isUpperCase, fontNameWithoutSuffix);
520
520
 
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -654,7 +654,7 @@
654
654
  function addResources( object, context: USDZExporterContext ) {
655
655
 
656
656
  const geometry = object.geometry;
657
- let material = object.material;
657
+ const material = object.material;
658
658
 
659
659
  if ( geometry ) {
660
660
 
@@ -1146,8 +1146,8 @@
1146
1146
  const rotation = texture.rotation;
1147
1147
 
1148
1148
  // rotation is around the wrong point. after rotation we need to shift offset again so that we're rotating around the right spot
1149
- let xRotationOffset = Math.sin(rotation);
1150
- let yRotationOffset = Math.cos(rotation);
1149
+ const xRotationOffset = Math.sin(rotation);
1150
+ const yRotationOffset = Math.cos(rotation);
1151
1151
 
1152
1152
  // texture coordinates start in the opposite corner, need to correct
1153
1153
  offset.y = 1 - offset.y - repeat.y;
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -61,7 +61,7 @@
61
61
  const model = models[index];
62
62
  if (isActive || time >= model.start && time <= model.end) {
63
63
  let weight = 1;
64
- let isBlendingWithNext = false;
64
+ const isBlendingWithNext = false;
65
65
 
66
66
  // this blending with next clips is already baked into easeIn/easeOut
67
67
  // if (allowBlendWithNext && index + 1 < models.length) {
src/engine-components/export/usdz/extensions/USDZUI.ts CHANGED
@@ -111,7 +111,7 @@
111
111
  private flipWindingOrder(geometry) {
112
112
  const index = geometry.index.array
113
113
  for (let i = 0, il = index.length / 3; i < il; i++) {
114
- let x = index[i * 3]
114
+ const x = index[i * 3]
115
115
  index[i * 3] = index[i * 3 + 2]
116
116
  index[i * 3 + 2] = x
117
117
  }
src/engine-components/webxr/WebARCameraBackground.ts CHANGED
@@ -159,7 +159,7 @@
159
159
  }
160
160
  }
161
161
  // TODO tint could be an uniform
162
- let backgroundFragment: string = /* glsl */`
162
+ const backgroundFragment: string = /* glsl */`
163
163
  uniform sampler2D t2D;
164
164
 
165
165
  varying vec2 vUv;
src/engine-components/webxr/WebXRAvatar.ts CHANGED
@@ -165,7 +165,7 @@
165
165
  if (this.head) {
166
166
 
167
167
  const device = this.webxr.IsInAR ? ViewDevice.Handheld : ViewDevice.Headset;
168
- let viewObj = this.head;
168
+ const viewObj = this.head;
169
169
  // if (this.isLocalAvatar) {
170
170
  // if (this.context.mainCamera && this.context.isInXR) {
171
171
  // viewObj = this.context.renderer.xr.getCamera(this.context.mainCamera);
src/engine-components/webxr/WebXRController.ts CHANGED
@@ -538,7 +538,7 @@
538
538
  this.didChangeScale = true;
539
539
  const rig = this.webXR.Rig;
540
540
  if (rig) {
541
- let args = this.switchScale(rig, doTeleport, isInMiniatureMode, newRigScale);
541
+ const args = this.switchScale(rig, doTeleport, isInMiniatureMode, newRigScale);
542
542
  doTeleport = args.doTeleport;
543
543
  isInMiniatureMode = args.isInMiniatureMode;
544
544
  newRigScale = args.newRigScale;
src/engine-components/webxr/WebXRSync.ts CHANGED
@@ -1,463 +1,463 @@
1
- import { Behaviour, GameObject } from "../Component.js";
2
- import { RoomEvents, OwnershipModel, NetworkConnection } from "../../engine/engine_networking.js";
3
- import { WebXR, WebXREvent } from "./WebXR.js";
4
- import { Group, Quaternion, Vector3, Vector4, WebXRManager } from "three";
5
- import { getParam } from "../../engine/engine_utils.js";
6
- import { Voip } from "../Voip.js";
7
- import { Builder, Long } from "flatbuffers";
8
- import { VrUserStateBuffer } from "../../engine-schemes/vr-user-state-buffer.js";
9
- import { Vec3 } from "../../engine-schemes/vec3.js";
10
- import { registerBinaryType } from "../../engine-schemes/schemes.js";
11
- import { Vec4 } from "../../engine-schemes/vec4.js";
12
- import { WebXRAvatar } from "./WebXRAvatar.js";
13
-
14
- // for debug GUI
15
- // import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
16
- // import { HTMLMesh } from 'three/examples/jsm/interactive/HTMLMesh.js';
17
- // import { InteractiveGroup } from 'three/examples/jsm/interactive/InteractiveGroup.js';
18
- // import { renderer, sceneData } from "../engine/engine_setup.js";
19
-
20
- const debugLogs = getParam("debugxr");
21
- const debugAvatar = getParam("debugavatar");
22
- // const debugAvatarVoip = getParam("debugavatarvoip");
23
-
24
- enum WebXRSyncEvent {
25
- WebXR_UserJoined = "webxr-user-joined",
26
- WebXR_UserLeft = "webxr-user-left",
27
- VRSessionStart = "vr-session-started",
28
- VRSessionEnd = "vr-session-ended",
29
- VRSessionUpdate = "vr-session-update",
30
- }
31
-
32
- enum XRMode {
33
- VR = "vr",
34
- AR = "ar",
35
- }
36
-
37
- const VRUserStateBufferIdentifier = "VRUS";
38
- registerBinaryType(VRUserStateBufferIdentifier, VrUserStateBuffer.getRootAsVrUserStateBuffer);
39
-
40
- function getTimeStampNow() {
41
- return new Date().getTime(); // avoid sending millis in flatbuffer
42
- }
43
-
44
- function flatbuffers_long_from_number(num: number): Long {
45
- let low = num & 0xffffffff
46
- let high = (num / Math.pow(2, 32)) & 0xfffff
47
- return Long.create(low, high);
48
- }
49
-
50
- export class VRUserState {
51
- public guid: string;
52
- public time!: number;
53
- public avatarId!: string;
54
- public position: Vector3 = new Vector3();
55
- public rotation: Vector4 = new Vector4();
56
- public scale: number = 1;
57
-
58
- public posLeftHand = new Vector3();
59
- public posRightHand = new Vector3();
60
-
61
- public rotLeftHand = new Quaternion();
62
- public rotRightHand = new Quaternion();
63
-
64
- public constructor(guid: string) {
65
- this.guid = guid;
66
- }
67
-
68
- private static invertRotation: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
69
-
70
- public update(rig: Group, pos: DOMPointReadOnly, rot: DOMPointReadOnly, webXR: WebXR, avatarId: string) {
71
- this.time = getTimeStampNow();
72
- this.avatarId = avatarId;
73
- this.position.set(pos.x, pos.y, pos.z);
74
- if (rig)
75
- this.position.applyMatrix4(rig.matrixWorld);
76
-
77
- let q0 = VRUserState.quat0;
78
- const q1 = VRUserState.quat1;
79
- q0.set(rot.x, rot.y, rot.z, rot.w);
80
- q0 = q0.multiplyQuaternions(q0, VRUserState.invertRotation);
81
-
82
- if (rig) {
83
- rig.getWorldQuaternion(q1);
84
- q0.multiplyQuaternions(q1, q0);
85
- }
86
-
87
- this.rotation.set(q0.x, q0.y, q0.z, q0.w);
88
- this.scale = rig.scale.x;
89
-
90
- // for controllers, it seems we need grip pose
91
- const ctrl0 = webXR.LeftController?.controllerGrip;
92
- if (ctrl0) {
93
- ctrl0.getWorldPosition(this.posLeftHand);
94
- ctrl0.getWorldQuaternion(this.rotLeftHand);
95
- }
96
- const ctrl1 = webXR.RightController?.controllerGrip;
97
- if (ctrl1) {
98
- ctrl1.getWorldPosition(this.posRightHand);
99
- ctrl1.getWorldQuaternion(this.rotRightHand);
100
- }
101
-
102
- // if this is a hand, we need to get the root bone of that / use that for position/rotation
103
- if (webXR.LeftController?.hand?.visible) {
104
- const wrist = webXR.LeftController.wrist;
105
- if (wrist) {
106
- wrist.getWorldPosition(this.posLeftHand);
107
- wrist.getWorldQuaternion(this.rotLeftHand);
108
- }
109
- }
110
-
111
- if (webXR.RightController?.hand?.visible) {
112
- const wrist = webXR.RightController.wrist;
113
- if (wrist) {
114
- wrist.getWorldPosition(this.posRightHand);
115
- wrist.getWorldQuaternion(this.rotRightHand);
116
- }
117
- }
118
- }
119
-
120
- private static quat0: Quaternion = new Quaternion();
121
- private static quat1: Quaternion = new Quaternion();
122
-
123
- public sendAsBuffer(builder: Builder, net: NetworkConnection) {
124
- builder.clear();
125
- const guid = builder.createString(this.guid);
126
- const id = builder.createString(this.avatarId);
127
- VrUserStateBuffer.startVrUserStateBuffer(builder);
128
- VrUserStateBuffer.addGuid(builder, guid);
129
- VrUserStateBuffer.addTime(builder, flatbuffers_long_from_number(this.time));
130
- VrUserStateBuffer.addAvatarId(builder, id);
131
- VrUserStateBuffer.addPosition(builder, Vec3.createVec3(builder, this.position.x, this.position.y, this.position.z));
132
- VrUserStateBuffer.addRotation(builder, Vec4.createVec4(builder, this.rotation.x, this.rotation.y, this.rotation.z, this.rotation.w));
133
- VrUserStateBuffer.addScale(builder, this.scale);
134
- VrUserStateBuffer.addPosLeftHand(builder, Vec3.createVec3(builder, this.posLeftHand.x, this.posLeftHand.y, this.posLeftHand.z));
135
- VrUserStateBuffer.addPosRightHand(builder, Vec3.createVec3(builder, this.posRightHand.x, this.posRightHand.y, this.posRightHand.z));
136
- VrUserStateBuffer.addRotLeftHand(builder, Vec4.createVec4(builder, this.rotLeftHand.x, this.rotLeftHand.y, this.rotLeftHand.z, this.rotLeftHand.w));
137
- VrUserStateBuffer.addRotRightHand(builder, Vec4.createVec4(builder, this.rotRightHand.x, this.rotRightHand.y, this.rotRightHand.z, this.rotRightHand.w));
138
- const res = VrUserStateBuffer.endVrUserStateBuffer(builder);
139
- builder.finish(res, VRUserStateBufferIdentifier);
140
- const arr = builder.asUint8Array();
141
- net.sendBinary(arr);
142
- }
143
-
144
- public setFromBuffer(guid: string, state: VrUserStateBuffer) {
145
- if (!guid) return;
146
- this.guid = guid;
147
- this.time = state.time().toFloat64();
148
- const id = state.avatarId();
149
- if (id)
150
- this.avatarId = id;
151
- const pos = state.position();
152
- if (pos)
153
- this.position.set(pos.x(), pos.y(), pos.z());
154
- // TODO: maybe just send one float more instead of converting back and forth
155
- const rot = state.rotation();
156
- if (rot)
157
- this.rotation.set(rot.x(), rot.y(), rot.z(), rot.w());
158
- const posLeftHand = state.posLeftHand();
159
- if (posLeftHand)
160
- this.posLeftHand.set(posLeftHand.x(), posLeftHand.y(), posLeftHand.z());
161
- const posRightHand = state.posRightHand();
162
- if (posRightHand)
163
- this.posRightHand.set(posRightHand.x(), posRightHand.y(), posRightHand.z());
164
- const rotLeftHand = state.rotLeftHand();
165
- if (rotLeftHand)
166
- this.rotLeftHand.set(rotLeftHand.x(), rotLeftHand.y(), rotLeftHand.z(), rotLeftHand.w());
167
- const rotRightHand = state.rotRightHand();
168
- if (rotRightHand)
169
- this.rotRightHand.set(rotRightHand.x(), rotRightHand.y(), rotRightHand.z(), rotRightHand.w());
170
- this.scale = state.scale();
171
- }
172
- }
173
-
174
- export class WebXRSync extends Behaviour {
175
-
176
- webXR: WebXR | null = null;
177
-
178
- // private allowCustomAvatars: boolean | null = true;
179
-
180
- private debugAvatarUser: WebXRAvatar | null = null;
181
- private voip: Voip | null = null;
182
-
183
- async awake() {
184
-
185
- if(!this.webXR) this.webXR = GameObject.getComponent(this.gameObject, WebXR);
186
- if(!this.webXR) this.webXR = GameObject.findObjectOfType(WebXR, this.context);
187
-
188
- if(!this.webXR)
189
- {
190
- console.log("Missing webxr component");
191
- this.webXR = GameObject.findObjectOfType(WebXR, this.context);
192
- if(!this.webXR) {
193
- console.error("Could not find webxr component");
194
- return;
195
- }
196
- }
197
-
198
- if (!this.voip) this.voip = GameObject.findObjectOfType(Voip, this.context);
199
-
200
- if (debugAvatar) {
201
- const debugGuid = "debug-avatar-" + debugAvatar;
202
- const newUser = new WebXRAvatar(this.context, debugGuid, this.webXR);
203
- // newUser.isLocalAvatar = true;
204
- this.debugAvatarUser = newUser;
205
- if (typeof debugAvatar === "string" && debugAvatar.length > 0) {
206
- if (await newUser.setAvatarOverride(debugAvatar)) {
207
- const debugState = new VRUserState(debugGuid);
208
- debugState.position.y += 1;
209
- const off = .5;
210
- debugState.posLeftHand.y += off;
211
- debugState.posLeftHand.x += off;
212
- debugState.posRightHand.y += off;
213
- debugState.posRightHand.x -= off;
214
- newUser.tryUpdate(debugState, 0);
215
- }
216
- else {
217
- newUser.destroy();
218
- }
219
- }
220
- }
221
- }
222
-
223
- onEnable() {
224
- // const debugUser = new WebXRAvatar(this.context, "sorry-no-guid", this.webXR!);
225
-
226
- if (!this.webXR) {
227
- this.webXR = GameObject.getComponent(this.gameObject, WebXR);
228
- if (!this.webXR) {
229
- console.warn("Missing webxr component on " + this.gameObject.name);
230
- return;
231
- }
232
- }
233
-
234
- this.eventSub_WebXRStartEvent = this.onXRSessionStart.bind(this);
235
- WebXR.addEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
236
- this.eventSub_WebXRUpdateEvent = this.onXRSessionUpdate.bind(this);
237
- WebXR.addEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
238
- this.eventSub_WebXREndEvent = this.onXRSessionEnded.bind(this);
239
- WebXR.addEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
240
-
241
- this.eventSub_ConnectionEvent = this.onConnected.bind(this);
242
- this.context.connection.beginListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
243
- this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserJoined, _evt => {
244
- console.log("webxr user joined evt");
245
- });
246
- this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserLeft, evt => {
247
- const hasId = evt.id !== null && evt.id !== undefined;
248
- if (!hasId) return;
249
- console.log("webxr user left evt");
250
- if (hasId) {
251
- const avatar = this.avatars[evt.id];
252
- avatar?.destroy();
253
- this.avatars[evt.id] = undefined;
254
- }
255
- });
256
- this.context.connection.beginListenBinary(VRUserStateBufferIdentifier, (state: VrUserStateBuffer) => {
257
- // console.log("BUFFER", state);
258
- const guid = state.guid();
259
- if (!guid) return;
260
- const time = state.time().toFloat64();
261
- const temp = this.tempState;
262
- temp.setFromBuffer(guid, state);
263
- // console.log(temp);
264
- const user = this.onTryGetAvatar(guid, time);
265
- user?.tryUpdate(temp, time);
266
- });
267
- this.context.connection.beginListen(WebXRSyncEvent.VRSessionUpdate, (state: VRUserState) => {
268
- const guid = state.guid;
269
- const time = state.time;
270
- const user = this.onTryGetAvatar(guid, time);
271
- user?.tryUpdate(state, time);
272
- });
273
- }
274
-
275
- private tempState: VRUserState = new VRUserState("");
276
-
277
- private onTryGetAvatar(guid: string, time: number) {
278
- if (guid === this.context.connection.connectionId) return null; // ignore self in case we receive that also!
279
- const timeDiff = new Date().getTime() - time;
280
- if (timeDiff > 5000) {
281
- if (debugLogs)
282
- console.log("old data", timeDiff, guid)
283
- return null;
284
- }
285
- let user = this.avatars[guid];
286
- if (user === undefined) {
287
- try {
288
- console.log("create new avatar");
289
- const newUser = new WebXRAvatar(this.context, guid, this.webXR!);
290
- user = newUser;
291
- this.avatars[guid] = newUser;
292
- } catch (err) {
293
- this.avatars[guid] = null;
294
- console.error(err);
295
- }
296
- }
297
- return user;
298
- }
299
-
300
- onDisable() {
301
- if (this.eventSub_ConnectionEvent)
302
- this.context.connection.stopListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
303
- WebXR.removeEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
304
- WebXR.removeEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
305
- WebXR.removeEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
306
- }
307
-
308
- update(): void {
309
-
310
- const now = getTimeStampNow();
311
-
312
- if (this.debugAvatarUser) {
313
- this.debugAvatarUser.lastUpdate = now;
314
- }
315
-
316
- this.detectPotentiallyDisconnectedAvatarsAndRemove();
317
-
318
- for (const key in this.avatars) {
319
- const avatar = this.avatars[key];
320
- if (!avatar) continue;
321
- avatar.update();
322
- }
323
- }
324
-
325
-
326
- private _removeAvatarsList: string[] = [];
327
- private detectPotentiallyDisconnectedAvatarsAndRemove() {
328
- const utcnow = getTimeStampNow();
329
- for (const key in this.avatars) {
330
- const avatar = this.avatars[key];
331
- if (!avatar) {
332
- this._removeAvatarsList.push(key);
333
- continue;
334
- }
335
- if (utcnow - avatar.lastUpdate > 10_000) {
336
- console.log("avatar timed out (didnt receive any updates in a while) - destroying it now");
337
- avatar.destroy();
338
- this.avatars[key] = undefined;
339
- }
340
- }
341
- for (const rem of this._removeAvatarsList) {
342
- delete this.avatars[rem];
343
- }
344
- this._removeAvatarsList.length = 0;
345
- }
346
-
347
- private buildLocalAvatar() {
348
- if (this.localAvatar) return;
349
- const connectionId = this.context.connection?.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
350
- this.localAvatar = new WebXRAvatar(this.context, connectionId, this.webXR!);
351
- this.localAvatar.isLocalAvatar = true;
352
- this.localAvatar.setAvatarOverride(this.getAvatarId());
353
- this.avatars[this.localAvatar.guid] = this.localAvatar;
354
- }
355
-
356
-
357
- private eventSub_ConnectionEvent: Function | null = null;
358
- private eventSub_WebXRStartEvent: Function | null = null;
359
- private eventSub_WebXREndEvent: Function | null = null;
360
- private eventSub_WebXRUpdateEvent: Function | null = null;
361
- private avatars: { [key: string]: WebXRAvatar | undefined | null } = {}
362
- private localAvatar: WebXRAvatar | null = null;
363
- private k_LocalAvatarNoNetworkingGuid = "local";
364
-
365
- private onConnected() {
366
- // this event gets fired when we have joined a room and are ready to update
367
- if (debugLogs)
368
- console.log("Hey you are connected as " + this.context.connection.connectionId);
369
-
370
- if (this.localAvatar?.guid === this.k_LocalAvatarNoNetworkingGuid) {
371
- if (this.localAvatar) {
372
- this.localAvatar?.destroy();
373
- this.avatars[this.localAvatar.guid] = undefined;
374
- }
375
- this.localAvatar = null;
376
- this.xrState = null;
377
- this.ownership?.freeOwnership();
378
- this.ownership = null;
379
- }
380
- }
381
-
382
- private onXRSessionStart(_evt: { session: XRSession }) {
383
- console.log("XR session started");
384
- this.context.connection.send(WebXRSyncEvent.WebXR_UserJoined, { id: this.context.connection.connectionId, mode: XRMode.VR });
385
-
386
- if (this.localAvatar) {
387
- this.localAvatar?.destroy();
388
- this.avatars[this.localAvatar.guid] = undefined;
389
- this.localAvatar = null;
390
- }
391
- this.xrState = null;
392
- this.ownership?.freeOwnership();
393
- this.ownership = null;
394
-
395
- if (this.avatars) {
396
- for (const key in this.avatars) {
397
- this.avatars[key]?.updateFlags();
398
- }
399
- }
400
- }
401
-
402
- private onXRSessionEnded(_evt: { session: XRSession }) {
403
- console.log("XR session ended");
404
- this.context.connection.send(WebXRSyncEvent.WebXR_UserLeft, { id: this.context.connection.connectionId, mode: XRMode.VR });
405
- if(this.localAvatar){
406
- this.localAvatar?.destroy();
407
- this.avatars[this.localAvatar.guid] = undefined;
408
- this.localAvatar = null;
409
- }
410
- }
411
-
412
- private ownership: OwnershipModel | null = null;
413
- private xrState: VRUserState | null = null;
414
- private builder: Builder = new Builder(1024);
415
-
416
- private onXRSessionUpdate(evt: { rig: Group, frame: XRFrame, xr: WebXRManager, input: XRInputSource[] }) {
417
-
418
- this.xrState ??= new VRUserState(this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
419
- this.ownership ??= new OwnershipModel(this.context.connection, this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
420
- this.ownership.guid = this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
421
- this.buildLocalAvatar();
422
-
423
-
424
- const { frame, xr, rig } = evt;
425
- const pose = frame.getViewerPose(xr.getReferenceSpace()!);
426
- if (!pose) return; // e.g. if user is not wearing headset
427
- const transform: XRRigidTransform = pose?.transform;
428
- const pos = transform.position;
429
- const rot = transform.orientation;
430
- this.xrState.update(rig, pos, rot, this.webXR!, this.getAvatarId());
431
-
432
- if (this.localAvatar) {
433
- if (this.context.connection.connectionId) {
434
- this.localAvatar.guid = this.context.connection.connectionId;
435
- }
436
- this.localAvatar.tryUpdate(this.xrState, 0);
437
- }
438
-
439
- if (this.ownership && !this.ownership.hasOwnership && this.context.connection.isConnected) {
440
- if (this.context.time.frameCount % 120 === 0)
441
- this.ownership.requestOwnership();
442
- if (!this.ownership.hasOwnership) {
443
- // console.log("NO OWNERSHIP", this.ownership.guid);
444
- return;
445
- }
446
- }
447
-
448
- if (!this.context.connection.isConnected || !this.context.connection.connectionId) {
449
- return;
450
- }
451
-
452
- this.xrState.sendAsBuffer(this.builder, this.context.connection);
453
-
454
- // this.context.connection.send(WebXRSyncEvent.VRSessionUpdate, this.xrState);
455
-
456
- }
457
-
458
- private getAvatarId() {
459
- const urlAvatar = getParam("avatar") as string;
460
- const avatarId = urlAvatar ?? null;
461
- return avatarId;
462
- }
463
- }
1
+ import { Behaviour, GameObject } from "../Component.js";
2
+ import { RoomEvents, OwnershipModel, NetworkConnection } from "../../engine/engine_networking.js";
3
+ import { WebXR, WebXREvent } from "./WebXR.js";
4
+ import { Group, Quaternion, Vector3, Vector4, WebXRManager } from "three";
5
+ import { getParam } from "../../engine/engine_utils.js";
6
+ import { Voip } from "../Voip.js";
7
+ import { Builder, Long } from "flatbuffers";
8
+ import { VrUserStateBuffer } from "../../engine-schemes/vr-user-state-buffer.js";
9
+ import { Vec3 } from "../../engine-schemes/vec3.js";
10
+ import { registerBinaryType } from "../../engine-schemes/schemes.js";
11
+ import { Vec4 } from "../../engine-schemes/vec4.js";
12
+ import { WebXRAvatar } from "./WebXRAvatar.js";
13
+
14
+ // for debug GUI
15
+ // import { GUI } from "three/examples/jsm/libs/lil-gui.module.min.js";
16
+ // import { HTMLMesh } from 'three/examples/jsm/interactive/HTMLMesh.js';
17
+ // import { InteractiveGroup } from 'three/examples/jsm/interactive/InteractiveGroup.js';
18
+ // import { renderer, sceneData } from "../engine/engine_setup.js";
19
+
20
+ const debugLogs = getParam("debugxr");
21
+ const debugAvatar = getParam("debugavatar");
22
+ // const debugAvatarVoip = getParam("debugavatarvoip");
23
+
24
+ enum WebXRSyncEvent {
25
+ WebXR_UserJoined = "webxr-user-joined",
26
+ WebXR_UserLeft = "webxr-user-left",
27
+ VRSessionStart = "vr-session-started",
28
+ VRSessionEnd = "vr-session-ended",
29
+ VRSessionUpdate = "vr-session-update",
30
+ }
31
+
32
+ enum XRMode {
33
+ VR = "vr",
34
+ AR = "ar",
35
+ }
36
+
37
+ const VRUserStateBufferIdentifier = "VRUS";
38
+ registerBinaryType(VRUserStateBufferIdentifier, VrUserStateBuffer.getRootAsVrUserStateBuffer);
39
+
40
+ function getTimeStampNow() {
41
+ return new Date().getTime(); // avoid sending millis in flatbuffer
42
+ }
43
+
44
+ function flatbuffers_long_from_number(num: number): Long {
45
+ const low = num & 0xffffffff
46
+ const high = (num / Math.pow(2, 32)) & 0xfffff
47
+ return Long.create(low, high);
48
+ }
49
+
50
+ export class VRUserState {
51
+ public guid: string;
52
+ public time!: number;
53
+ public avatarId!: string;
54
+ public position: Vector3 = new Vector3();
55
+ public rotation: Vector4 = new Vector4();
56
+ public scale: number = 1;
57
+
58
+ public posLeftHand = new Vector3();
59
+ public posRightHand = new Vector3();
60
+
61
+ public rotLeftHand = new Quaternion();
62
+ public rotRightHand = new Quaternion();
63
+
64
+ public constructor(guid: string) {
65
+ this.guid = guid;
66
+ }
67
+
68
+ private static invertRotation: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
69
+
70
+ public update(rig: Group, pos: DOMPointReadOnly, rot: DOMPointReadOnly, webXR: WebXR, avatarId: string) {
71
+ this.time = getTimeStampNow();
72
+ this.avatarId = avatarId;
73
+ this.position.set(pos.x, pos.y, pos.z);
74
+ if (rig)
75
+ this.position.applyMatrix4(rig.matrixWorld);
76
+
77
+ let q0 = VRUserState.quat0;
78
+ const q1 = VRUserState.quat1;
79
+ q0.set(rot.x, rot.y, rot.z, rot.w);
80
+ q0 = q0.multiplyQuaternions(q0, VRUserState.invertRotation);
81
+
82
+ if (rig) {
83
+ rig.getWorldQuaternion(q1);
84
+ q0.multiplyQuaternions(q1, q0);
85
+ }
86
+
87
+ this.rotation.set(q0.x, q0.y, q0.z, q0.w);
88
+ this.scale = rig.scale.x;
89
+
90
+ // for controllers, it seems we need grip pose
91
+ const ctrl0 = webXR.LeftController?.controllerGrip;
92
+ if (ctrl0) {
93
+ ctrl0.getWorldPosition(this.posLeftHand);
94
+ ctrl0.getWorldQuaternion(this.rotLeftHand);
95
+ }
96
+ const ctrl1 = webXR.RightController?.controllerGrip;
97
+ if (ctrl1) {
98
+ ctrl1.getWorldPosition(this.posRightHand);
99
+ ctrl1.getWorldQuaternion(this.rotRightHand);
100
+ }
101
+
102
+ // if this is a hand, we need to get the root bone of that / use that for position/rotation
103
+ if (webXR.LeftController?.hand?.visible) {
104
+ const wrist = webXR.LeftController.wrist;
105
+ if (wrist) {
106
+ wrist.getWorldPosition(this.posLeftHand);
107
+ wrist.getWorldQuaternion(this.rotLeftHand);
108
+ }
109
+ }
110
+
111
+ if (webXR.RightController?.hand?.visible) {
112
+ const wrist = webXR.RightController.wrist;
113
+ if (wrist) {
114
+ wrist.getWorldPosition(this.posRightHand);
115
+ wrist.getWorldQuaternion(this.rotRightHand);
116
+ }
117
+ }
118
+ }
119
+
120
+ private static quat0: Quaternion = new Quaternion();
121
+ private static quat1: Quaternion = new Quaternion();
122
+
123
+ public sendAsBuffer(builder: Builder, net: NetworkConnection) {
124
+ builder.clear();
125
+ const guid = builder.createString(this.guid);
126
+ const id = builder.createString(this.avatarId);
127
+ VrUserStateBuffer.startVrUserStateBuffer(builder);
128
+ VrUserStateBuffer.addGuid(builder, guid);
129
+ VrUserStateBuffer.addTime(builder, flatbuffers_long_from_number(this.time));
130
+ VrUserStateBuffer.addAvatarId(builder, id);
131
+ VrUserStateBuffer.addPosition(builder, Vec3.createVec3(builder, this.position.x, this.position.y, this.position.z));
132
+ VrUserStateBuffer.addRotation(builder, Vec4.createVec4(builder, this.rotation.x, this.rotation.y, this.rotation.z, this.rotation.w));
133
+ VrUserStateBuffer.addScale(builder, this.scale);
134
+ VrUserStateBuffer.addPosLeftHand(builder, Vec3.createVec3(builder, this.posLeftHand.x, this.posLeftHand.y, this.posLeftHand.z));
135
+ VrUserStateBuffer.addPosRightHand(builder, Vec3.createVec3(builder, this.posRightHand.x, this.posRightHand.y, this.posRightHand.z));
136
+ VrUserStateBuffer.addRotLeftHand(builder, Vec4.createVec4(builder, this.rotLeftHand.x, this.rotLeftHand.y, this.rotLeftHand.z, this.rotLeftHand.w));
137
+ VrUserStateBuffer.addRotRightHand(builder, Vec4.createVec4(builder, this.rotRightHand.x, this.rotRightHand.y, this.rotRightHand.z, this.rotRightHand.w));
138
+ const res = VrUserStateBuffer.endVrUserStateBuffer(builder);
139
+ builder.finish(res, VRUserStateBufferIdentifier);
140
+ const arr = builder.asUint8Array();
141
+ net.sendBinary(arr);
142
+ }
143
+
144
+ public setFromBuffer(guid: string, state: VrUserStateBuffer) {
145
+ if (!guid) return;
146
+ this.guid = guid;
147
+ this.time = state.time().toFloat64();
148
+ const id = state.avatarId();
149
+ if (id)
150
+ this.avatarId = id;
151
+ const pos = state.position();
152
+ if (pos)
153
+ this.position.set(pos.x(), pos.y(), pos.z());
154
+ // TODO: maybe just send one float more instead of converting back and forth
155
+ const rot = state.rotation();
156
+ if (rot)
157
+ this.rotation.set(rot.x(), rot.y(), rot.z(), rot.w());
158
+ const posLeftHand = state.posLeftHand();
159
+ if (posLeftHand)
160
+ this.posLeftHand.set(posLeftHand.x(), posLeftHand.y(), posLeftHand.z());
161
+ const posRightHand = state.posRightHand();
162
+ if (posRightHand)
163
+ this.posRightHand.set(posRightHand.x(), posRightHand.y(), posRightHand.z());
164
+ const rotLeftHand = state.rotLeftHand();
165
+ if (rotLeftHand)
166
+ this.rotLeftHand.set(rotLeftHand.x(), rotLeftHand.y(), rotLeftHand.z(), rotLeftHand.w());
167
+ const rotRightHand = state.rotRightHand();
168
+ if (rotRightHand)
169
+ this.rotRightHand.set(rotRightHand.x(), rotRightHand.y(), rotRightHand.z(), rotRightHand.w());
170
+ this.scale = state.scale();
171
+ }
172
+ }
173
+
174
+ export class WebXRSync extends Behaviour {
175
+
176
+ webXR: WebXR | null = null;
177
+
178
+ // private allowCustomAvatars: boolean | null = true;
179
+
180
+ private debugAvatarUser: WebXRAvatar | null = null;
181
+ private voip: Voip | null = null;
182
+
183
+ async awake() {
184
+
185
+ if(!this.webXR) this.webXR = GameObject.getComponent(this.gameObject, WebXR);
186
+ if(!this.webXR) this.webXR = GameObject.findObjectOfType(WebXR, this.context);
187
+
188
+ if(!this.webXR)
189
+ {
190
+ console.log("Missing webxr component");
191
+ this.webXR = GameObject.findObjectOfType(WebXR, this.context);
192
+ if(!this.webXR) {
193
+ console.error("Could not find webxr component");
194
+ return;
195
+ }
196
+ }
197
+
198
+ if (!this.voip) this.voip = GameObject.findObjectOfType(Voip, this.context);
199
+
200
+ if (debugAvatar) {
201
+ const debugGuid = "debug-avatar-" + debugAvatar;
202
+ const newUser = new WebXRAvatar(this.context, debugGuid, this.webXR);
203
+ // newUser.isLocalAvatar = true;
204
+ this.debugAvatarUser = newUser;
205
+ if (typeof debugAvatar === "string" && debugAvatar.length > 0) {
206
+ if (await newUser.setAvatarOverride(debugAvatar)) {
207
+ const debugState = new VRUserState(debugGuid);
208
+ debugState.position.y += 1;
209
+ const off = .5;
210
+ debugState.posLeftHand.y += off;
211
+ debugState.posLeftHand.x += off;
212
+ debugState.posRightHand.y += off;
213
+ debugState.posRightHand.x -= off;
214
+ newUser.tryUpdate(debugState, 0);
215
+ }
216
+ else {
217
+ newUser.destroy();
218
+ }
219
+ }
220
+ }
221
+ }
222
+
223
+ onEnable() {
224
+ // const debugUser = new WebXRAvatar(this.context, "sorry-no-guid", this.webXR!);
225
+
226
+ if (!this.webXR) {
227
+ this.webXR = GameObject.getComponent(this.gameObject, WebXR);
228
+ if (!this.webXR) {
229
+ console.warn("Missing webxr component on " + this.gameObject.name);
230
+ return;
231
+ }
232
+ }
233
+
234
+ this.eventSub_WebXRStartEvent = this.onXRSessionStart.bind(this);
235
+ WebXR.addEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
236
+ this.eventSub_WebXRUpdateEvent = this.onXRSessionUpdate.bind(this);
237
+ WebXR.addEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
238
+ this.eventSub_WebXREndEvent = this.onXRSessionEnded.bind(this);
239
+ WebXR.addEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
240
+
241
+ this.eventSub_ConnectionEvent = this.onConnected.bind(this);
242
+ this.context.connection.beginListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
243
+ this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserJoined, _evt => {
244
+ console.log("webxr user joined evt");
245
+ });
246
+ this.context.connection.beginListen(WebXRSyncEvent.WebXR_UserLeft, evt => {
247
+ const hasId = evt.id !== null && evt.id !== undefined;
248
+ if (!hasId) return;
249
+ console.log("webxr user left evt");
250
+ if (hasId) {
251
+ const avatar = this.avatars[evt.id];
252
+ avatar?.destroy();
253
+ this.avatars[evt.id] = undefined;
254
+ }
255
+ });
256
+ this.context.connection.beginListenBinary(VRUserStateBufferIdentifier, (state: VrUserStateBuffer) => {
257
+ // console.log("BUFFER", state);
258
+ const guid = state.guid();
259
+ if (!guid) return;
260
+ const time = state.time().toFloat64();
261
+ const temp = this.tempState;
262
+ temp.setFromBuffer(guid, state);
263
+ // console.log(temp);
264
+ const user = this.onTryGetAvatar(guid, time);
265
+ user?.tryUpdate(temp, time);
266
+ });
267
+ this.context.connection.beginListen(WebXRSyncEvent.VRSessionUpdate, (state: VRUserState) => {
268
+ const guid = state.guid;
269
+ const time = state.time;
270
+ const user = this.onTryGetAvatar(guid, time);
271
+ user?.tryUpdate(state, time);
272
+ });
273
+ }
274
+
275
+ private tempState: VRUserState = new VRUserState("");
276
+
277
+ private onTryGetAvatar(guid: string, time: number) {
278
+ if (guid === this.context.connection.connectionId) return null; // ignore self in case we receive that also!
279
+ const timeDiff = new Date().getTime() - time;
280
+ if (timeDiff > 5000) {
281
+ if (debugLogs)
282
+ console.log("old data", timeDiff, guid)
283
+ return null;
284
+ }
285
+ let user = this.avatars[guid];
286
+ if (user === undefined) {
287
+ try {
288
+ console.log("create new avatar");
289
+ const newUser = new WebXRAvatar(this.context, guid, this.webXR!);
290
+ user = newUser;
291
+ this.avatars[guid] = newUser;
292
+ } catch (err) {
293
+ this.avatars[guid] = null;
294
+ console.error(err);
295
+ }
296
+ }
297
+ return user;
298
+ }
299
+
300
+ onDisable() {
301
+ if (this.eventSub_ConnectionEvent)
302
+ this.context.connection.stopListen(RoomEvents.JoinedRoom, this.eventSub_ConnectionEvent);
303
+ WebXR.removeEventListener(WebXREvent.XRStarted, this.eventSub_WebXRStartEvent);
304
+ WebXR.removeEventListener(WebXREvent.XRUpdate, this.eventSub_WebXRUpdateEvent);
305
+ WebXR.removeEventListener(WebXREvent.XRStopped, this.eventSub_WebXREndEvent);
306
+ }
307
+
308
+ update(): void {
309
+
310
+ const now = getTimeStampNow();
311
+
312
+ if (this.debugAvatarUser) {
313
+ this.debugAvatarUser.lastUpdate = now;
314
+ }
315
+
316
+ this.detectPotentiallyDisconnectedAvatarsAndRemove();
317
+
318
+ for (const key in this.avatars) {
319
+ const avatar = this.avatars[key];
320
+ if (!avatar) continue;
321
+ avatar.update();
322
+ }
323
+ }
324
+
325
+
326
+ private _removeAvatarsList: string[] = [];
327
+ private detectPotentiallyDisconnectedAvatarsAndRemove() {
328
+ const utcnow = getTimeStampNow();
329
+ for (const key in this.avatars) {
330
+ const avatar = this.avatars[key];
331
+ if (!avatar) {
332
+ this._removeAvatarsList.push(key);
333
+ continue;
334
+ }
335
+ if (utcnow - avatar.lastUpdate > 10_000) {
336
+ console.log("avatar timed out (didnt receive any updates in a while) - destroying it now");
337
+ avatar.destroy();
338
+ this.avatars[key] = undefined;
339
+ }
340
+ }
341
+ for (const rem of this._removeAvatarsList) {
342
+ delete this.avatars[rem];
343
+ }
344
+ this._removeAvatarsList.length = 0;
345
+ }
346
+
347
+ private buildLocalAvatar() {
348
+ if (this.localAvatar) return;
349
+ const connectionId = this.context.connection?.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
350
+ this.localAvatar = new WebXRAvatar(this.context, connectionId, this.webXR!);
351
+ this.localAvatar.isLocalAvatar = true;
352
+ this.localAvatar.setAvatarOverride(this.getAvatarId());
353
+ this.avatars[this.localAvatar.guid] = this.localAvatar;
354
+ }
355
+
356
+
357
+ private eventSub_ConnectionEvent: Function | null = null;
358
+ private eventSub_WebXRStartEvent: Function | null = null;
359
+ private eventSub_WebXREndEvent: Function | null = null;
360
+ private eventSub_WebXRUpdateEvent: Function | null = null;
361
+ private avatars: { [key: string]: WebXRAvatar | undefined | null } = {}
362
+ private localAvatar: WebXRAvatar | null = null;
363
+ private k_LocalAvatarNoNetworkingGuid = "local";
364
+
365
+ private onConnected() {
366
+ // this event gets fired when we have joined a room and are ready to update
367
+ if (debugLogs)
368
+ console.log("Hey you are connected as " + this.context.connection.connectionId);
369
+
370
+ if (this.localAvatar?.guid === this.k_LocalAvatarNoNetworkingGuid) {
371
+ if (this.localAvatar) {
372
+ this.localAvatar?.destroy();
373
+ this.avatars[this.localAvatar.guid] = undefined;
374
+ }
375
+ this.localAvatar = null;
376
+ this.xrState = null;
377
+ this.ownership?.freeOwnership();
378
+ this.ownership = null;
379
+ }
380
+ }
381
+
382
+ private onXRSessionStart(_evt: { session: XRSession }) {
383
+ console.log("XR session started");
384
+ this.context.connection.send(WebXRSyncEvent.WebXR_UserJoined, { id: this.context.connection.connectionId, mode: XRMode.VR });
385
+
386
+ if (this.localAvatar) {
387
+ this.localAvatar?.destroy();
388
+ this.avatars[this.localAvatar.guid] = undefined;
389
+ this.localAvatar = null;
390
+ }
391
+ this.xrState = null;
392
+ this.ownership?.freeOwnership();
393
+ this.ownership = null;
394
+
395
+ if (this.avatars) {
396
+ for (const key in this.avatars) {
397
+ this.avatars[key]?.updateFlags();
398
+ }
399
+ }
400
+ }
401
+
402
+ private onXRSessionEnded(_evt: { session: XRSession }) {
403
+ console.log("XR session ended");
404
+ this.context.connection.send(WebXRSyncEvent.WebXR_UserLeft, { id: this.context.connection.connectionId, mode: XRMode.VR });
405
+ if(this.localAvatar){
406
+ this.localAvatar?.destroy();
407
+ this.avatars[this.localAvatar.guid] = undefined;
408
+ this.localAvatar = null;
409
+ }
410
+ }
411
+
412
+ private ownership: OwnershipModel | null = null;
413
+ private xrState: VRUserState | null = null;
414
+ private builder: Builder = new Builder(1024);
415
+
416
+ private onXRSessionUpdate(evt: { rig: Group, frame: XRFrame, xr: WebXRManager, input: XRInputSource[] }) {
417
+
418
+ this.xrState ??= new VRUserState(this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
419
+ this.ownership ??= new OwnershipModel(this.context.connection, this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid);
420
+ this.ownership.guid = this.context.connection.connectionId ?? this.k_LocalAvatarNoNetworkingGuid;
421
+ this.buildLocalAvatar();
422
+
423
+
424
+ const { frame, xr, rig } = evt;
425
+ const pose = frame.getViewerPose(xr.getReferenceSpace()!);
426
+ if (!pose) return; // e.g. if user is not wearing headset
427
+ const transform: XRRigidTransform = pose?.transform;
428
+ const pos = transform.position;
429
+ const rot = transform.orientation;
430
+ this.xrState.update(rig, pos, rot, this.webXR!, this.getAvatarId());
431
+
432
+ if (this.localAvatar) {
433
+ if (this.context.connection.connectionId) {
434
+ this.localAvatar.guid = this.context.connection.connectionId;
435
+ }
436
+ this.localAvatar.tryUpdate(this.xrState, 0);
437
+ }
438
+
439
+ if (this.ownership && !this.ownership.hasOwnership && this.context.connection.isConnected) {
440
+ if (this.context.time.frameCount % 120 === 0)
441
+ this.ownership.requestOwnership();
442
+ if (!this.ownership.hasOwnership) {
443
+ // console.log("NO OWNERSHIP", this.ownership.guid);
444
+ return;
445
+ }
446
+ }
447
+
448
+ if (!this.context.connection.isConnected || !this.context.connection.connectionId) {
449
+ return;
450
+ }
451
+
452
+ this.xrState.sendAsBuffer(this.builder, this.context.connection);
453
+
454
+ // this.context.connection.send(WebXRSyncEvent.VRSessionUpdate, this.xrState);
455
+
456
+ }
457
+
458
+ private getAvatarId() {
459
+ const urlAvatar = getParam("avatar") as string;
460
+ const avatarId = urlAvatar ?? null;
461
+ return avatarId;
462
+ }
463
+ }