@needle-tools/engine 3.5.0-alpha → 3.5.2-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/needle-engine.js +10447 -10303
  3. package/dist/needle-engine.min.js +375 -372
  4. package/dist/needle-engine.umd.cjs +319 -316
  5. package/lib/engine/codegen/register_types.js +0 -2
  6. package/lib/engine/codegen/register_types.js.map +1 -1
  7. package/lib/engine/engine_context.d.ts +8 -3
  8. package/lib/engine/engine_context.js +36 -17
  9. package/lib/engine/engine_context.js.map +1 -1
  10. package/lib/engine/engine_element_loading.js +2 -2
  11. package/lib/engine/engine_element_loading.js.map +1 -1
  12. package/lib/engine/engine_license.d.ts +2 -0
  13. package/lib/engine/engine_license.js +25 -4
  14. package/lib/engine/engine_license.js.map +1 -1
  15. package/lib/engine-components/ReflectionProbe.js +16 -7
  16. package/lib/engine-components/ReflectionProbe.js.map +1 -1
  17. package/lib/engine-components/Renderer.js +3 -4
  18. package/lib/engine-components/Renderer.js.map +1 -1
  19. package/lib/engine-components/SceneSwitcher.d.ts +9 -0
  20. package/lib/engine-components/SceneSwitcher.js +128 -0
  21. package/lib/engine-components/SceneSwitcher.js.map +1 -1
  22. package/lib/engine-components/codegen/components.d.ts +0 -1
  23. package/lib/engine-components/codegen/components.js +0 -1
  24. package/lib/engine-components/codegen/components.js.map +1 -1
  25. package/lib/engine-components/export/usdz/ThreeUSDZExporter.d.ts +9 -5
  26. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js +25 -11
  27. package/lib/engine-components/export/usdz/ThreeUSDZExporter.js.map +1 -1
  28. package/lib/engine-components/export/usdz/USDZExporter.d.ts +1 -0
  29. package/lib/engine-components/export/usdz/USDZExporter.js +14 -5
  30. package/lib/engine-components/export/usdz/USDZExporter.js.map +1 -1
  31. package/lib/engine-components/export/usdz/extensions/behavior/Behaviour.d.ts +1 -5
  32. package/lib/engine-components/export/usdz/extensions/behavior/Behaviour.js +1 -10
  33. package/lib/engine-components/export/usdz/extensions/behavior/Behaviour.js.map +1 -1
  34. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js +4 -4
  35. package/lib/engine-components/export/usdz/extensions/behavior/BehaviourComponents.js.map +1 -1
  36. package/lib/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.js.map +1 -1
  37. package/lib/engine-components/ui/Canvas.js +29 -16
  38. package/lib/engine-components/ui/Canvas.js.map +1 -1
  39. package/lib/engine-components/ui/Layout.js +10 -5
  40. package/lib/engine-components/ui/Layout.js.map +1 -1
  41. package/lib/engine-components/ui/RectTransform.js +9 -6
  42. package/lib/engine-components/ui/RectTransform.js.map +1 -1
  43. package/lib/tsconfig.tsbuildinfo +1 -1
  44. package/package.json +1 -1
  45. package/plugins/vite/license.js +2 -2
  46. package/src/engine/codegen/register_types.js +2 -4
  47. package/src/engine/engine_context.ts +43 -19
  48. package/src/engine/engine_element_loading.ts +2 -2
  49. package/src/engine/engine_license.ts +25 -4
  50. package/src/engine-components/ReflectionProbe.ts +17 -7
  51. package/src/engine-components/Renderer.ts +5 -5
  52. package/src/engine-components/RendererLightmap.ts +1 -1
  53. package/src/engine-components/SceneSwitcher.ts +136 -1
  54. package/src/engine-components/codegen/components.ts +0 -1
  55. package/src/engine-components/export/usdz/ThreeUSDZExporter.ts +38 -12
  56. package/src/engine-components/export/usdz/USDZExporter.ts +14 -7
  57. package/src/engine-components/export/usdz/extensions/behavior/Behaviour.ts +5 -15
  58. package/src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +19 -19
  59. package/src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +2 -2
  60. package/src/engine-components/ui/Canvas.ts +29 -16
  61. package/src/engine-components/ui/Layout.ts +10 -5
  62. package/src/engine-components/ui/RectTransform.ts +10 -6
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/engine",
3
- "version": "3.5.0-alpha",
3
+ "version": "3.5.2-alpha",
4
4
  "description": "Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in",
5
5
  "main": "dist/needle-engine.umd.cjs",
6
6
  "type": "module",
@@ -13,8 +13,8 @@ export const needleLicense = (command, config, userSettings) => {
13
13
  if (isNeedleEngineFile || isViteChunkFile) {
14
14
  const needleConfig = await loadConfig();
15
15
  if (needleConfig) {
16
- if (needleConfig.hasProLicense === true) {
17
- src = src.replace("NEEDLE_ENGINE_COMMERCIAL_USE_LICENSE = false;", "NEEDLE_ENGINE_COMMERCIAL_USE_LICENSE = " + needleConfig.hasProLicense + ";");
16
+ if (typeof needleConfig.license === "string") {
17
+ src = src.replace("const NEEDLE_ENGINE_LICENSE_TYPE: string = \"\";", "const NEEDLE_ENGINE_LICENSE_TYPE: string = \"" + needleConfig.license + "\";");
18
18
  return { code: src, map: null }
19
19
  }
20
20
  }
@@ -1,5 +1,5 @@
1
1
  import { TypeStore } from "./../engine_typestore"
2
-
2
+
3
3
  // Import types
4
4
  import { __Ignore } from "../../engine-components/codegen/components";
5
5
  import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
@@ -184,7 +184,6 @@ import { TriggerModel } from "../../engine-components/export/usdz/extensions/beh
184
184
  import { UIRaycastUtils } from "../../engine-components/ui/RaycastUtils";
185
185
  import { UIRootComponent } from "../../engine-components/ui/BaseUIComponent";
186
186
  import { UsageMarker } from "../../engine-components/Interactable";
187
- import { USDZBehaviours } from "../../engine-components/export/usdz/extensions/behavior/Behaviour";
188
187
  import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter";
189
188
  import { USDZText } from "../../engine-components/export/usdz/extensions/USDZText";
190
189
  import { VariantAction } from "../../engine-components/export/usdz/extensions/behavior/Actions";
@@ -214,7 +213,7 @@ import { XRGrabModel } from "../../engine-components/webxr/WebXRGrabRendering";
214
213
  import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering";
215
214
  import { XRRig } from "../../engine-components/webxr/WebXRRig";
216
215
  import { XRState } from "../../engine-components/XRFlag";
217
-
216
+
218
217
  // Register types
219
218
  TypeStore.add("__Ignore", __Ignore);
220
219
  TypeStore.add("ActionBuilder", ActionBuilder);
@@ -399,7 +398,6 @@ TypeStore.add("TriggerModel", TriggerModel);
399
398
  TypeStore.add("UIRaycastUtils", UIRaycastUtils);
400
399
  TypeStore.add("UIRootComponent", UIRootComponent);
401
400
  TypeStore.add("UsageMarker", UsageMarker);
402
- TypeStore.add("USDZBehaviours", USDZBehaviours);
403
401
  TypeStore.add("USDZExporter", USDZExporter);
404
402
  TypeStore.add("USDZText", USDZText);
405
403
  TypeStore.add("VariantAction", VariantAction);
@@ -80,7 +80,7 @@ export enum XRSessionMode {
80
80
  ImmersiveAR = "immersive-ar",
81
81
  }
82
82
 
83
- export declare type OnBeforeRenderCallback = (renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void
83
+ export declare type OnRenderCallback = (renderer: WebGLRenderer, scene: Scene, camera: Camera, geometry: BufferGeometry, material: Material, group: Group) => void
84
84
 
85
85
 
86
86
  export function registerComponent(script: IComponent, context?: Context) {
@@ -440,33 +440,57 @@ export class Context implements IContext {
440
440
  }
441
441
  }
442
442
 
443
- private _onBeforeRenderListeners: { [key: string]: OnBeforeRenderCallback[] } = {};
443
+
444
+
445
+ private _onBeforeRenderListeners = new Map<string, OnRenderCallback[]>();
446
+ private _onAfterRenderListeners = new Map<string, OnRenderCallback[]>();
444
447
 
445
448
  /** use this to subscribe to onBeforeRender events on threejs objects */
446
- addBeforeRenderListener(target: Object3D, callback: OnBeforeRenderCallback) {
447
- if (!this._onBeforeRenderListeners[target.uuid]) {
448
- this._onBeforeRenderListeners[target.uuid] = [];
449
- const onBeforeRenderCallback = (renderer, scene, camera, geometry, material, group) => {
450
- const arr = this._onBeforeRenderListeners[target.uuid];
451
- if (!arr) return;
452
- for (let i = 0; i < arr.length; i++) {
453
- const fn = arr[i];
454
- fn(renderer, scene, camera, geometry, material, group);
455
- }
456
- }
457
- target.onBeforeRender = onBeforeRenderCallback as any;
449
+ addBeforeRenderListener(target: Object3D, callback: OnRenderCallback) {
450
+ if (!this._onBeforeRenderListeners.has(target.uuid)) {
451
+ this._onBeforeRenderListeners.set(target.uuid, []);
452
+ target.onBeforeRender = this._createRenderCallbackWrapper(target, this._onBeforeRenderListeners);
453
+ }
454
+ this._onBeforeRenderListeners.get(target.uuid)?.push(callback);
455
+ }
456
+ removeBeforeRenderListener(target: Object3D, callback: OnRenderCallback) {
457
+ if (this._onBeforeRenderListeners.has(target.uuid)) {
458
+ const arr = this._onBeforeRenderListeners.get(target.uuid)!;
459
+ const idx = arr.indexOf(callback);
460
+ if (idx >= 0) arr.splice(idx, 1);
458
461
  }
459
- this._onBeforeRenderListeners[target.uuid].push(callback);
460
462
  }
461
463
 
462
- removeBeforeRenderListener(target: Object3D, callback: OnBeforeRenderCallback) {
463
- if (this._onBeforeRenderListeners[target.uuid]) {
464
- const arr = this._onBeforeRenderListeners[target.uuid];
464
+ /** use this to subscribe to onAfterRender events on threejs objects */
465
+ addAfterRenderListener(target: Object3D, callback: OnRenderCallback) {
466
+ if (!this._onAfterRenderListeners.has(target.uuid)) {
467
+ this._onAfterRenderListeners.set(target.uuid, []);
468
+ target.onAfterRender = this._createRenderCallbackWrapper(target, this._onAfterRenderListeners);
469
+ }
470
+ this._onAfterRenderListeners.get(target.uuid)?.push(callback);
471
+ }
472
+ removeAfterRenderListener(target: Object3D, callback: OnRenderCallback) {
473
+ if (this._onAfterRenderListeners.has(target.uuid)) {
474
+ const arr = this._onAfterRenderListeners.get(target.uuid)!;
465
475
  const idx = arr.indexOf(callback);
466
476
  if (idx >= 0) arr.splice(idx, 1);
467
477
  }
468
478
  }
469
479
 
480
+
481
+ private _createRenderCallbackWrapper(target: Object3D, array: Map<string, OnRenderCallback[]>): OnRenderCallback {
482
+ return (renderer, scene, camera, geometry, material, group) => {
483
+ const arr = array.get(target.uuid);
484
+ if (!arr) return;
485
+ for (let i = 0; i < arr.length; i++) {
486
+ const fn = arr[i];
487
+ fn(renderer, scene, camera, geometry, material, group);
488
+ }
489
+ }
490
+ }
491
+
492
+
493
+
470
494
  private _requireDepthTexture: boolean = false;
471
495
  private _requireColorTexture: boolean = false;
472
496
  private _renderTarget?: WebGLRenderTarget;
@@ -584,7 +608,7 @@ export class Context implements IContext {
584
608
  else {
585
609
  ContextRegistry.dispatchCallback(ContextEvent.MissingCamera, this);
586
610
  if (!this.mainCamera && !this.isManagedExternally)
587
- console.error("MISSING camera", this);
611
+ console.warn("Missing camera in main scene", this);
588
612
  }
589
613
  }
590
614
 
@@ -3,7 +3,7 @@ import { Mathf } from "./engine_math";
3
3
  import { LoadingProgressArgs } from "./engine_setup";
4
4
  import { getParam } from "./engine_utils";
5
5
  import { logoSVG } from "./assets"
6
- import { hasProLicense } from "./engine_license";
6
+ import { hasCommercialLicense, hasProLicense } from "./engine_license";
7
7
 
8
8
  const debug = getParam("debugloadingbar");
9
9
  const debugRendering = getParam("debugloadingbarrendering");
@@ -301,7 +301,7 @@ export class EngineLoadingView implements ILoadingViewHandler {
301
301
  }
302
302
  }
303
303
 
304
- if (!hasLicense) {
304
+ if (!hasCommercialLicense()) {
305
305
  const nonCommercialContainer = document.createElement("div");
306
306
  nonCommercialContainer.style.paddingTop = ".6em";
307
307
  nonCommercialContainer.style.fontSize = ".8em";
@@ -7,10 +7,28 @@ const debug = getParam("debuglicense");
7
7
 
8
8
  // This is modified by a bundler (e.g. vite)
9
9
  // Do not edit manually
10
- const NEEDLE_ENGINE_COMMERCIAL_USE_LICENSE = false;
10
+ const NEEDLE_ENGINE_LICENSE_TYPE: string = "";
11
+ if (debug) console.log("License Type: " + NEEDLE_ENGINE_LICENSE_TYPE)
11
12
 
12
13
  export function hasProLicense() {
13
- return NEEDLE_ENGINE_COMMERCIAL_USE_LICENSE;
14
+ switch (NEEDLE_ENGINE_LICENSE_TYPE) {
15
+ case "pro":
16
+ case "enterprise":
17
+ return true;
18
+ };
19
+ return false;
20
+ }
21
+
22
+ export function hasIndieLicense() {
23
+ switch (NEEDLE_ENGINE_LICENSE_TYPE) {
24
+ case "indie":
25
+ return true;
26
+ }
27
+ return false;
28
+ }
29
+
30
+ export function hasCommercialLicense() {
31
+ return hasProLicense() || hasIndieLicense();
14
32
  }
15
33
 
16
34
 
@@ -52,7 +70,8 @@ function insertNonCommercialUseHint(ctx: IContext) {
52
70
  }
53
71
  }, 100);
54
72
 
55
- logNonCommercialUse();
73
+ if (!hasCommercialLicense())
74
+ logNonCommercialUse();
56
75
 
57
76
  let svg = `<img class="logo" src="${logoSVG}" style="width: 40px; height: 40px; margin-right: 2px; vertical-align: middle; margin-bottom: 2px;"/>`;
58
77
  const logoElement = document.createElement("div");
@@ -66,7 +85,9 @@ function insertNonCommercialUseHint(ctx: IContext) {
66
85
  // textElement.innerHTML = "Needle Engine<br/><span class=\"non-commercial\">Non Commercial</span>";
67
86
  licenseElement.appendChild(textElement);
68
87
 
69
- licenseElement.title = "Needle Engine — non commercial version";
88
+ licenseElement.title = "Needle Engine";
89
+ if (!hasCommercialLicense())
90
+ licenseElement.title += " non commercial version";
70
91
  licenseElement.addEventListener("click", () => {
71
92
  globalThis.open("https://needle.tools", "_blank");
72
93
  });
@@ -129,17 +129,28 @@ export class ReflectionProbe extends Behaviour {
129
129
  // otherwise some renderers outside of the probe will be affected or vice versa
130
130
  for (let i = 0; i < _rend.sharedMaterials.length; i++) {
131
131
  const material = _rend.sharedMaterials[i];
132
- if (!material) continue;
133
- if (material["envMap"] === undefined) continue;
134
- if (material["envMap"] === this.texture) {
132
+ if (!material) {
135
133
  continue;
136
134
  }
135
+ if (material["envMap"] === undefined) {
136
+ continue;
137
+ }
138
+
137
139
  let cached = rendererCache[i];
138
140
 
139
141
  // make sure we have the currently assigned material cached (and an up to date clone of that)
140
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
141
- if (!cached || cached.material !== material || cached.copy.version !== material.version) {
142
- if(debug) console.log("Cloning material", material.name, material.version);
143
+ let isCachedInstance = material === cached?.copy;
144
+ let hasChanged = !cached || cached.material.uuid !== material.uuid || cached.copy.version !== material.version;
145
+ if (!isCachedInstance && hasChanged) {
146
+ if (debug) {
147
+ let reason = "";
148
+ if (!cached) reason = "not cached";
149
+ else if (cached.material !== material) reason = "reference changed; cached instance?: " + isCachedInstance;
150
+ else if (cached.copy.version !== material.version) reason = "version changed";
151
+ console.warn("Cloning material", material.name, material.version, "Reason:", reason, "\n", material.uuid, "\n", cached?.copy.uuid, "\n", _rend.name);
152
+ }
153
+
143
154
  const clone = material.clone();
144
155
  clone.version = material.version;
145
156
 
@@ -158,8 +169,7 @@ export class ReflectionProbe extends Behaviour {
158
169
  clone[$reflectionProbeKey] = this;
159
170
  clone[$originalMaterial] = material;
160
171
 
161
- if (debug)
162
- console.log("Set reflection", _rend.name, _rend.guid);
172
+ if (debug) console.log("Set reflection", _rend.name, _rend.guid);
163
173
  }
164
174
 
165
175
  /** this is the material that we copied and that has the reflection probe */
@@ -605,15 +605,15 @@ export class Renderer extends Behaviour implements IRenderer {
605
605
  }
606
606
  }
607
607
 
608
- }
608
+
609
+ if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) {
610
+ this._reflectionProbe.onSet(this);
611
+ }
609
612
 
613
+ }
610
614
  onBeforeRenderThree(_renderer, _scene, _camera, _geometry, material, _group) {
611
615
 
612
616
  this.loadProgressiveTextures(material);
613
- // TODO: we might want to just set the material.envMap for one material during rendering instead of cloning the material
614
- if (this.reflectionProbeUsage !== ReflectionProbeUsage.Off && this._reflectionProbe) {
615
- this._reflectionProbe.onSet(this);
616
- }
617
617
 
618
618
  if (material.envMapIntensity !== undefined) {
619
619
  const factor = this.hasLightmap ? Math.PI : 1;
@@ -1,7 +1,7 @@
1
1
  import { Behaviour, GameObject } from "./Component";
2
2
  import * as THREE from "three";
3
3
  import { Texture } from "three";
4
- import { Context, OnBeforeRenderCallback } from "../engine/engine_setup";
4
+ import { Context, OnRenderCallback } from "../engine/engine_setup";
5
5
 
6
6
  // this component is automatically added by the Renderer if the object has lightmap uvs AND we have a lightmap
7
7
  // for multimaterial objects GLTF exports a "Group" with the renderer component
@@ -56,11 +56,27 @@ export class SceneSwitcher extends Behaviour {
56
56
  @serializable()
57
57
  useSceneLighting: boolean = true;
58
58
 
59
+ /** how many scenes after the currently active scene should be preloaded */
60
+ @serializable()
61
+ preloadNext: number = 1;
62
+
63
+ /** how many scenes before the currently active scene should be preloaded */
64
+ @serializable()
65
+ preloadPrevious: number = 1;
66
+
67
+ /** how many scenes can be loaded in parallel */
68
+ @serializable()
69
+ preloadConcurrent: number = 2;
70
+
71
+
72
+ get currentIndex(): number { return this._currentIndex; }
59
73
 
60
74
  private _currentIndex: number = -1;
61
75
  private _currentScene: AssetReference | undefined = undefined;
62
76
  private _engineElementOverserver: MutationObserver | undefined = undefined;
63
77
 
78
+ private _preloadScheduler?: PreLoadScheduler;
79
+
64
80
  async start() {
65
81
  if (this._currentIndex === -1 && !await this.tryLoadFromQueryParam()) {
66
82
  const value = this.context.domElement.getAttribute(ENGINE_ELEMENT_SCENE_ATTRIBUTE_NAME);
@@ -100,6 +116,13 @@ export class SceneSwitcher extends Behaviour {
100
116
  this._engineElementOverserver.observe(this.context.domElement, {
101
117
  attributes: true
102
118
  });
119
+
120
+ if (!this._preloadScheduler)
121
+ this._preloadScheduler = new PreLoadScheduler(this);
122
+ this._preloadScheduler.maxLoadAhead = this.preloadNext;
123
+ this._preloadScheduler.maxLoadBehind = this.preloadPrevious;
124
+ this._preloadScheduler.maxConcurrent = this.preloadConcurrent;
125
+ this._preloadScheduler.begin();
103
126
  }
104
127
 
105
128
  onDisable(): void {
@@ -107,6 +130,7 @@ export class SceneSwitcher extends Behaviour {
107
130
  this.context.input.removeEventListener(InputEvents.KeyDown, this.onKeyDown);
108
131
  this.context.input.removeEventListener(InputEvents.PointerMove, this.onPointerMove);
109
132
  this.context.input.removeEventListener(InputEvents.PointerUp, this.onPointerUp);
133
+ this._preloadScheduler?.stop();
110
134
  }
111
135
 
112
136
  private onPopState = async (_state: PopStateEvent) => {
@@ -189,7 +213,7 @@ export class SceneSwitcher extends Behaviour {
189
213
  select(index: number | string): Promise<boolean> {
190
214
  if (debug) console.log("select", index);
191
215
 
192
- if(typeof index === "object"){
216
+ if (typeof index === "object") {
193
217
  // If a user tries to reference a scene object in a UnityEvent and invoke select(obj)
194
218
  // Then the object will be serialized as a object { guid : ... } or with the index json pointer
195
219
  // This case is not supported right now. Object references in the editor must not be scene references
@@ -271,6 +295,15 @@ export class SceneSwitcher extends Behaviour {
271
295
  return false;
272
296
  }
273
297
 
298
+ preload(index: number) {
299
+ if (index >= 0 && index < this.scenes.length) {
300
+ const scene = this.scenes[index];
301
+ if(scene instanceof AssetReference)
302
+ return scene.preload();
303
+ }
304
+ return couldNotLoadScenePromise;
305
+ }
306
+
274
307
  private tryLoadFromQueryParam() {
275
308
  if (!this.queryParameterName?.length) return couldNotLoadScenePromise;
276
309
  // try restore the scene from the url
@@ -308,3 +341,105 @@ export class SceneSwitcher extends Behaviour {
308
341
  return couldNotLoadScenePromise;
309
342
  }
310
343
  }
344
+
345
+
346
+
347
+
348
+ class PreLoadScheduler {
349
+ maxLoadAhead: number;
350
+ maxLoadBehind: number;
351
+ maxConcurrent: number;
352
+
353
+ private _isRunning: boolean = false;
354
+ private _rooms: SceneSwitcher;
355
+ private _roomTasks: LoadTask[] = [];
356
+ private _maxConcurrentLoads: number = 1;
357
+
358
+ constructor(rooms: SceneSwitcher, ahead: number = 1, behind: number = 1, maxConcurrent: number = 2) {
359
+ this._rooms = rooms;
360
+ this.maxLoadAhead = ahead;
361
+ this.maxLoadBehind = behind;
362
+ this.maxConcurrent = maxConcurrent;
363
+ }
364
+
365
+ begin() {
366
+ if (this._isRunning) return;
367
+ if (debug) console.log("Preload begin")
368
+ this._isRunning = true;
369
+ let lastRoom: number = -1;
370
+ let searchDistance: number;
371
+ let searchCall: number;
372
+ const array = this._rooms.scenes;
373
+ let interval = setInterval(() => {
374
+ if (this.allLoaded()) {
375
+ if (debug)
376
+ console.log("All scenes loaded");
377
+ this.stop();
378
+ }
379
+ if (!this._isRunning) {
380
+ clearInterval(interval);
381
+ return;
382
+ }
383
+ if (this.canLoadNewScene() === false) return;
384
+ if (lastRoom !== this._rooms.currentIndex) {
385
+ lastRoom = this._rooms.currentIndex;
386
+ searchCall = 0;
387
+ searchDistance = 0;
388
+ }
389
+ const searchForward = searchCall % 2 === 0;
390
+ if (searchForward) searchDistance += 1;
391
+ searchCall += 1;
392
+ const maxSearchDistance = searchForward ? this.maxLoadAhead : this.maxLoadBehind;
393
+ if (searchDistance > maxSearchDistance) return;
394
+ let roomIndex = searchForward ? lastRoom + searchDistance : lastRoom - searchDistance;
395
+ if (roomIndex < 0) return;
396
+ // if (roomIndex < 0) roomIndex = array.length + roomIndex;
397
+ if (roomIndex < 0 || roomIndex >= array.length) return;
398
+ const scene = array[roomIndex];
399
+ new LoadTask(roomIndex, scene, this._roomTasks);
400
+ }, 200);
401
+ }
402
+
403
+ stop() {
404
+ this._isRunning = false;
405
+ }
406
+
407
+ canLoadNewScene(): boolean {
408
+ return this._roomTasks.length < this._maxConcurrentLoads;
409
+ }
410
+
411
+ allLoaded(): boolean {
412
+ for (const room of this._rooms.scenes) {
413
+ if (room.isLoaded() === false) return false;
414
+ }
415
+ return true;
416
+ }
417
+ }
418
+
419
+ class LoadTask {
420
+
421
+ index: number;
422
+ asset: AssetReference;
423
+ tasks: LoadTask[];
424
+
425
+ constructor(index: number, asset: AssetReference, tasks: LoadTask[]) {
426
+ this.index = index;
427
+ this.asset = asset;
428
+ this.tasks = tasks;
429
+ tasks.push(this);
430
+ this.awaitLoading();
431
+ }
432
+
433
+ private async awaitLoading() {
434
+ if (!this.asset.isLoaded()) {
435
+ if (debug)
436
+ console.log("Preload start: " + this.asset.uri, this.index);
437
+ await this.asset.preload();
438
+ if (debug)
439
+ console.log("Preload finished: " + this.asset.uri, this.index);
440
+ }
441
+
442
+ const i = this.tasks.indexOf(this);
443
+ if (i >= 0) this.tasks.splice(i, 1);
444
+ }
445
+ }
@@ -179,7 +179,6 @@ export { TriggerModel } from "../export/usdz/extensions/behavior/BehavioursBuild
179
179
  export { UIRaycastUtils } from "../ui/RaycastUtils";
180
180
  export { UIRootComponent } from "../ui/BaseUIComponent";
181
181
  export { UsageMarker } from "../Interactable";
182
- export { USDZBehaviours } from "../export/usdz/extensions/behavior/Behaviour";
183
182
  export { USDZExporter } from "../export/usdz/USDZExporter";
184
183
  export { USDZText } from "../export/usdz/extensions/USDZText";
185
184
  export { VariantAction } from "../export/usdz/extensions/behavior/Actions";
@@ -17,7 +17,6 @@ import {
17
17
  Camera,
18
18
  Color,
19
19
  MeshStandardMaterial,
20
- LinearEncoding,
21
20
  sRGBEncoding,
22
21
  MeshPhysicalMaterial,
23
22
  } from 'three';
@@ -49,7 +48,6 @@ class USDObject {
49
48
  parent: USDObject | null;
50
49
  children: Array<USDObject | null> = [];
51
50
  _eventListeners: {};
52
- mesh: any;
53
51
 
54
52
  static createEmptyParent( object ) {
55
53
 
@@ -93,7 +91,7 @@ class USDObject {
93
91
 
94
92
  clone() {
95
93
 
96
- const clone = new USDObject( MathUtils.generateUUID(), this.name, this.matrix, this.mesh, this.material );
94
+ const clone = new USDObject( MathUtils.generateUUID(), this.name, this.matrix, this.geometry, this.material );
97
95
  clone.isDynamic = this.isDynamic;
98
96
  return clone;
99
97
 
@@ -384,9 +382,20 @@ class USDZExporterContext {
384
382
 
385
383
  }
386
384
 
385
+ /**[documentation](https://developer.apple.com/documentation/arkit/usdz_schemas_for_ar/preliminary_anchoringapi/preliminary_anchoring_type) */
386
+ export type Anchoring = "plane" | "image" | "face" | "none"
387
+ /**[documentation](https://developer.apple.com/documentation/arkit/usdz_schemas_for_ar/preliminary_anchoringapi/preliminary_planeanchoring_alignment) */
388
+ export type Alignment = "horizontal" | "vertical" | "any";
389
+
387
390
  class USDZExporterOptions {
388
- ar: { anchoring: { type: string } } = { anchoring: { type: 'plane' } };
389
- planeAnchoring: { alignment: string } = { alignment: 'horizontal' };
391
+ ar: {
392
+ anchoring: { type: Anchoring },
393
+ planeAnchoring: { alignment: Alignment },
394
+ } = {
395
+ anchoring: { type: 'plane' },
396
+ planeAnchoring: { alignment: 'horizontal' }
397
+ };
398
+ quickLookCompatible: boolean = false;
390
399
  extensions: any[] = [];
391
400
  }
392
401
 
@@ -595,7 +604,7 @@ function parseDocument( context: USDZExporterContext ) {
595
604
 
596
605
  writer.appendLine( `token preliminary:anchoring:type = "${context.exporter.sceneAnchoringOptions.ar.anchoring.type}"` );
597
606
  if (context.exporter.sceneAnchoringOptions.ar.anchoring.type === 'plane')
598
- writer.appendLine( `token preliminary:planeAnchoring:alignment = "${context.exporter.sceneAnchoringOptions.planeAnchoring.alignment}"` );
607
+ writer.appendLine( `token preliminary:planeAnchoring:alignment = "${context.exporter.sceneAnchoringOptions.ar.planeAnchoring.alignment}"` );
599
608
  // bit hacky as we don't have a callback here yet. Relies on the fact that the image is named identical in the ImageTracking extension.
600
609
  if (context.exporter.sceneAnchoringOptions.ar.anchoring.type === 'image')
601
610
  writer.appendLine( `rel preliminary:imageAnchoring:referenceImage = </${context.document.name}/Scenes/Scene/AnchoringReferenceImage>` );
@@ -1077,7 +1086,7 @@ function buildMaterial( material: MeshStandardMaterial, textures ) {
1077
1086
  const pad = ' ';
1078
1087
  const inputs: Array<string> = [];
1079
1088
  const samplers: Array<string> = [];
1080
- const exportForQuickLook = true;
1089
+ const exportForQuickLook = false;
1081
1090
 
1082
1091
  function buildTexture( texture, mapType, color: Color | undefined = undefined, opacity: number | undefined = undefined ) {
1083
1092
 
@@ -1089,6 +1098,11 @@ function buildMaterial( material: MeshStandardMaterial, textures ) {
1089
1098
 
1090
1099
  const repeat = texture.repeat.clone();
1091
1100
  const offset = texture.offset.clone();
1101
+ const rotation = texture.rotation;
1102
+
1103
+ // rotation is around the wrong point. after rotation we need to shift offset again so that we're rotating around the right spot
1104
+ let xRotationOffset = Math.sin(rotation);
1105
+ let yRotationOffset = Math.cos(rotation);
1092
1106
 
1093
1107
  // texture coordinates start in the opposite corner, need to correct
1094
1108
  offset.y = 1 - offset.y - repeat.y;
@@ -1097,21 +1111,31 @@ function buildMaterial( material: MeshStandardMaterial, textures ) {
1097
1111
  // Apple Feedback: FB10036297 and FB11442287
1098
1112
  if ( exportForQuickLook ) {
1099
1113
 
1114
+ // This is NOT correct yet in QuickLook, but comes close for a range of models.
1115
+ // It becomes more incorrect the bigger the offset is
1116
+
1100
1117
  offset.x = offset.x / repeat.x;
1101
1118
  offset.y = offset.y / repeat.y;
1102
1119
 
1120
+ offset.x += xRotationOffset / repeat.x;
1121
+ offset.y += yRotationOffset - 1;
1122
+ }
1123
+
1124
+ else {
1125
+
1126
+ // results match glTF results exactly. verified correct in usdview.
1127
+ offset.x += xRotationOffset * repeat.x;
1128
+ offset.y += (1 - yRotationOffset) * repeat.y;
1129
+
1103
1130
  }
1104
1131
 
1105
1132
  textures[ id ] = texture;
1106
1133
  const uvReader = mapType == 'occlusion' ? 'uvReader_st2' : 'uvReader_st';
1107
1134
 
1108
- const needsTextureTransform = ( repeat.x != 1 || repeat.y != 1 || offset.x != 0 || offset.y != 0 );
1135
+ const needsTextureTransform = ( repeat.x != 1 || repeat.y != 1 || offset.x != 0 || offset.y != 0 || rotation != 0 );
1109
1136
  const textureTransformInput = `</Materials/Material_${material.id}/${uvReader}.outputs:result>`;
1110
1137
  const textureTransformOutput = `</Materials/Material_${material.id}/Transform2d_${mapType}.outputs:result>`;
1111
1138
 
1112
- const rawTextureExtra = `(
1113
- colorSpace = "Raw"
1114
- )`;
1115
1139
  const needsTextureScale = mapType !== 'normal' && (color && (color.r !== 1 || color.g !== 1 || color.b !== 1 || opacity !== 1)) || false;
1116
1140
  const needsNormalScaleAndBias = mapType === 'normal';
1117
1141
  const normalScaleValueString = (material.normalScale ? material.normalScale.x * 2 : 2).toFixed( PRECISION );
@@ -1127,13 +1151,15 @@ function buildMaterial( material: MeshStandardMaterial, textures ) {
1127
1151
  float2 inputs:in.connect = ${textureTransformInput}
1128
1152
  float2 inputs:scale = ${buildVector2( repeat )}
1129
1153
  float2 inputs:translation = ${buildVector2( offset )}
1154
+ float inputs:rotation = ${(rotation / Math.PI * 180).toFixed( PRECISION )}
1130
1155
  float2 outputs:result
1131
1156
  }
1132
1157
  ` : '' }
1133
1158
  def Shader "Texture_${texture.id}_${mapType}"
1134
1159
  {
1135
1160
  uniform token info:id = "UsdUVTexture"
1136
- asset inputs:file = @textures/Texture_${id}.${isRGBA ? 'png' : 'jpg'}@ ${mapType === 'normal' ? rawTextureExtra : ''}
1161
+ asset inputs:file = @textures/Texture_${id}.${isRGBA ? 'png' : 'jpg'}@
1162
+ token inputs:sourceColorSpace = "${ texture.colorSpace === 'srgb' ? 'sRGB' : 'raw' }"
1137
1163
  float2 inputs:st.connect = ${needsTextureTransform ? textureTransformOutput : textureTransformInput}
1138
1164
  ${needsTextureScale ? `
1139
1165
  float4 inputs:scale = (${color ? color.r + ', ' + color.g + ', ' + color.b : '1, 1, 1'}, ${opacity ? opacity : '1'})
@@ -13,6 +13,7 @@ import { showBalloonMessage, showBalloonWarning } from "../../../engine/debug/de
13
13
  import { Context } from "../../../engine/engine_setup";
14
14
  import { WebARSessionRoot } from "../../webxr/WebARSessionRoot";
15
15
  import { hasProLicense } from "../../../engine/engine_license";
16
+ import { BehaviorExtension } from "./extensions/behavior/Behaviour";
16
17
 
17
18
  const debug = getParam("debugusdz");
18
19
 
@@ -51,6 +52,9 @@ export class USDZExporter extends Behaviour {
51
52
  @serializable()
52
53
  planeAnchoringAlignment: "horizontal" | "vertical" | "any" = "horizontal";
53
54
 
55
+ @serializable()
56
+ interactive: boolean = true;
57
+
54
58
  extensions: IUSDExporterExtension[] = [];
55
59
 
56
60
  private link!: HTMLAnchorElement;
@@ -84,9 +88,11 @@ export class USDZExporter extends Behaviour {
84
88
  this.objectToExport = this.gameObject;
85
89
  if (!this.objectToExport?.children?.length && !(this.objectToExport as Mesh)?.isMesh)
86
90
  this.objectToExport = this.context.scene;
87
- }
88
-
89
91
 
92
+ if (this.interactive) {
93
+ this.extensions.push(new BehaviorExtension());
94
+ }
95
+ }
90
96
 
91
97
  onEnable() {
92
98
  const ios = isiOS()
@@ -157,12 +163,13 @@ export class USDZExporter extends Behaviour {
157
163
  ar: {
158
164
  anchoring: {
159
165
  type: this.anchoringType,
160
- }
161
- },
162
- planeAnchoring: {
163
- alignment: this.planeAnchoringAlignment,
166
+ },
167
+ planeAnchoring: {
168
+ alignment: this.planeAnchoringAlignment,
169
+ },
164
170
  },
165
- extensions: extensions
171
+ extensions: extensions,
172
+ quickLookCompatible: true,
166
173
  });
167
174
  const blob = new Blob([arraybuffer], { type: 'application/octet-stream' });
168
175