Needle Engine

Changes between version 3.5.1-alpha and 3.5.2-alpha
Files changed (11) hide show
  1. plugins/vite/license.js +2 -2
  2. src/engine/codegen/register_types.js +2 -4
  3. src/engine-components/export/usdz/extensions/behavior/Behaviour.ts +5 -15
  4. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +19 -19
  5. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +2 -2
  6. src/engine-components/codegen/components.ts +0 -1
  7. src/engine/engine_element_loading.ts +2 -2
  8. src/engine/engine_license.ts +25 -4
  9. src/engine-components/SceneSwitcher.ts +136 -1
  10. src/engine-components/export/usdz/ThreeUSDZExporter.ts +36 -7
  11. src/engine-components/export/usdz/USDZExporter.ts +14 -7
plugins/vite/license.js CHANGED
@@ -13,8 +13,8 @@
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
  }
src/engine/codegen/register_types.js CHANGED
@@ -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 @@
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 @@
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 @@
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);
src/engine-components/export/usdz/extensions/behavior/Behaviour.ts CHANGED
@@ -1,9 +1,8 @@
1
- import { Behaviour, GameObject } from "../../../../Component";
2
- import { USDZExporter } from "../../USDZExporter";
1
+ import { GameObject } from "../../../../Component";
2
+ import { IContext } from "../../../../../engine/engine_types";
3
3
  import { IUSDExporterExtension } from "../../Extension";
4
4
  import { USDObject, USDWriter } from "../../ThreeUSDZExporter";
5
5
  import { BehaviorModel } from "./BehavioursBuilder";
6
- import { IContext } from "../../../../../engine/engine_types";
7
6
 
8
7
  export interface UsdzBehaviour {
9
8
  createBehaviours?(ext: BehaviorExtension, model: USDObject, context: IContext): void;
@@ -11,15 +10,6 @@
11
10
  afterCreateDocument?(ext: BehaviorExtension, context: IContext): void;
12
11
  }
13
12
 
14
- export class USDZBehaviours extends Behaviour {
15
- start() {
16
- const exporter = GameObject.findObjectOfType(USDZExporter);
17
- if (exporter) {
18
- exporter.extensions.push(new BehaviorExtension());
19
- }
20
- }
21
- }
22
-
23
13
  export class BehaviorExtension implements IUSDExporterExtension {
24
14
 
25
15
  get extensionName(): string {
@@ -40,8 +30,8 @@
40
30
  GameObject.foreachComponent(e, (comp) => {
41
31
  const c = comp as unknown as UsdzBehaviour;
42
32
  if (
43
- typeof c.createBehaviours === "function" ||
44
- typeof c.beforeCreateDocument === "function" ||
33
+ typeof c.createBehaviours === "function" ||
34
+ typeof c.beforeCreateDocument === "function" ||
45
35
  typeof c.afterCreateDocument === "function"
46
36
  ) {
47
37
  this.behaviourComponents.push(c);
@@ -66,7 +56,7 @@
66
56
  this.behaviourComponents.length = 0;
67
57
  }
68
58
 
69
- onAfterHierarchy(context, writer : USDWriter) {
59
+ onAfterHierarchy(context, writer: USDWriter) {
70
60
  if (this.behaviours?.length) {
71
61
 
72
62
  // this.combineBehavioursWithSameTapActions();
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../../../../../engine/engine_three_utils";
8
8
 
9
9
  import { Object3D, Material, Vector3, Quaternion, AnimationAction } from "three";
10
- import { USDObject } from "../../ThreeUSDZExporter";
10
+ import { USDDocument, USDObject } from "../../ThreeUSDZExporter";
11
11
 
12
12
  import { BehaviorExtension, UsdzBehaviour } from "./Behaviour";
13
13
  import { ActionBuilder, ActionModel, BehaviorModel, IBehaviorElement, MotionType, Space, TriggerBuilder } from "./BehavioursBuilder";
@@ -44,7 +44,7 @@
44
44
 
45
45
  const thisScale = getWorldScale(this.object).clone();
46
46
  const targetScale = getWorldScale(this.target).clone();
47
-
47
+
48
48
  const dist = thisPos.distanceTo(targetPos);
49
49
  const rotDist = thisRot.angleTo(targetRot);
50
50
  const scaleDist = thisScale.distanceTo(targetScale);
@@ -63,10 +63,10 @@
63
63
 
64
64
  t01 += this.context.time.deltaTime / this.duration;
65
65
  if (t01 > 1) t01 = 1;
66
-
66
+
67
67
  // apply ease-in-out
68
68
  // https://easings.net/
69
- eased = t01 < 0.5 ? 4 * t01 * t01 * t01 : 1 - Math.pow(-2 * t01 + 2, 3) / 2;
69
+ eased = t01 < 0.5 ? 4 * t01 * t01 * t01 : 1 - Math.pow(-2 * t01 + 2, 3) / 2;
70
70
 
71
71
  this.targetPos.lerpVectors(thisPos, targetPos, eased);
72
72
  this.targetRot.slerpQuaternions(thisRot, targetRot, eased);
@@ -83,7 +83,7 @@
83
83
  }
84
84
 
85
85
  private *moveRelative() {
86
-
86
+
87
87
  if (!this.target || !this.object) return;
88
88
 
89
89
  const thisPos = this.object.position.clone();
@@ -107,10 +107,10 @@
107
107
 
108
108
  t01 += this.context.time.deltaTime / this.duration;
109
109
  if (t01 > 1) t01 = 1;
110
-
110
+
111
111
  // apply ease-in-out
112
112
  // https://easings.net/
113
- eased = t01 < 0.5 ? 4 * t01 * t01 * t01 : 1 - Math.pow(-2 * t01 + 2, 3) / 2;
113
+ eased = t01 < 0.5 ? 4 * t01 * t01 * t01 : 1 - Math.pow(-2 * t01 + 2, 3) / 2;
114
114
 
115
115
  this.object.position.lerpVectors(thisPos, this.targetPos, eased);
116
116
  this.object.quaternion.slerpQuaternions(thisRot, this.targetRot, eased);
@@ -120,7 +120,7 @@
120
120
  }
121
121
 
122
122
  this.coroutine = null;
123
- }
123
+ }
124
124
 
125
125
  onPointerClick() {
126
126
  if (this.coroutine) this.stopCoroutine(this.coroutine);
@@ -313,11 +313,12 @@
313
313
  hideClickedObject = true;
314
314
  targetState = !this.target.visible;
315
315
 
316
- // TODO check where we have to create the clone; here it doesn't show up
316
+ if (!this.selfModel.parent || this.selfModel.parent.isEmpty())
317
+ USDDocument.createEmptyParent(this.selfModel);
318
+
317
319
  this.toggleModel = this.selfModel.clone();
318
320
  this.toggleModel.name += "_toggle";
319
- if (this.selfModel.parent)
320
- this.selfModel.parent.add(this.toggleModel);
321
+ this.selfModel.parent!.add(this.toggleModel);
321
322
  }
322
323
 
323
324
  const sequence: ActionModel[] = [];
@@ -344,8 +345,8 @@
344
345
  ActionBuilder.sequence(...toggleSequence)
345
346
  ));
346
347
 
347
- ext.addBehavior(new BehaviorModel("HideOnStart_" + this.gameObject.name,
348
- TriggerBuilder.sceneStartTrigger(),
348
+ ext.addBehavior(new BehaviorModel("HideOnStart_" + this.gameObject.name,
349
+ TriggerBuilder.sceneStartTrigger(),
349
350
  ActionBuilder.fadeAction(this.toggleModel, 0, false)
350
351
  ));
351
352
  }
@@ -363,8 +364,8 @@
363
364
 
364
365
  createBehaviours(ext, model, _context) {
365
366
  if (model.uuid === this.gameObject.uuid)
366
- ext.addBehavior(new BehaviorModel("HideOnStart_" + this.gameObject.name,
367
- TriggerBuilder.sceneStartTrigger(),
367
+ ext.addBehavior(new BehaviorModel("HideOnStart_" + this.gameObject.name,
368
+ TriggerBuilder.sceneStartTrigger(),
368
369
  ActionBuilder.fadeAction(model, 0, false)
369
370
  ));
370
371
  }
@@ -394,8 +395,7 @@
394
395
  createBehaviours(ext, model, _context) {
395
396
  if (!this.target) return;
396
397
 
397
- if (model.uuid === this.gameObject.uuid)
398
- {
398
+ if (model.uuid === this.gameObject.uuid) {
399
399
  const emphasize = new BehaviorModel("emphasize " + this.name,
400
400
  TriggerBuilder.tapTrigger(this.gameObject),
401
401
  ActionBuilder.emphasize(this.target, this.duration, this.motionType, undefined, "basic"),
@@ -417,7 +417,7 @@
417
417
 
418
418
  @serializable()
419
419
  stateName?: string;
420
-
420
+
421
421
  @serializable()
422
422
  stateNameAfterPlaying?: string;
423
423
 
@@ -486,7 +486,7 @@
486
486
 
487
487
  createAnimation(ext, model, _context) {
488
488
  if (this.target && this.animator) {
489
-
489
+
490
490
  const state = this.animator?.runtimeAnimatorController?.findState(this.stateName);
491
491
  this.stateAnimationModel = model;
492
492
  this.stateAnimation = ext.registerAnimation(this.target, state?.motion.clip);
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts CHANGED
@@ -74,7 +74,7 @@
74
74
  let obj = targetObject[i];
75
75
  if (typeof obj === "string")
76
76
  str += obj;
77
- else if ( typeof obj === "object") {
77
+ else if (typeof obj === "object") {
78
78
  //@ts-ignore
79
79
  if (obj.isObject3D) {
80
80
  //@ts-ignore
@@ -436,7 +436,7 @@
436
436
  act.tokenId = "Emphasize";
437
437
  act.duration = duration;
438
438
  act.style = style ?? "basic";
439
- act.motionType = MotionType[motionType];
439
+ act.motionType = MotionType[motionType];
440
440
  act.moveDistance = moveDistance;
441
441
  return act;
442
442
  }
src/engine-components/codegen/components.ts CHANGED
@@ -179,7 +179,6 @@
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";
src/engine/engine_element_loading.ts CHANGED
@@ -3,7 +3,7 @@
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 @@
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";
src/engine/engine_license.ts CHANGED
@@ -7,13 +7,31 @@
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;
14
20
  }
15
21
 
22
+ export function hasIndieLicense() {
23
+ switch (NEEDLE_ENGINE_LICENSE_TYPE) {
24
+ case "indie":
25
+ return true;
26
+ }
27
+ return false;
28
+ }
16
29
 
30
+ export function hasCommercialLicense() {
31
+ return hasProLicense() || hasIndieLicense();
32
+ }
33
+
34
+
17
35
  ContextRegistry.registerCallback(ContextEvent.ContextRegistered, evt => {
18
36
  showLicenseInfo(evt.context);
19
37
  });
@@ -52,7 +70,8 @@
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 @@
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
  });
src/engine-components/SceneSwitcher.ts CHANGED
@@ -56,11 +56,27 @@
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;
59
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; }
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 @@
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 @@
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 @@
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 @@
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 @@
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
+ }
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -48,7 +48,6 @@
48
48
  parent: USDObject | null;
49
49
  children: Array<USDObject | null> = [];
50
50
  _eventListeners: {};
51
- mesh: any;
52
51
 
53
52
  static createEmptyParent( object ) {
54
53
 
@@ -92,7 +91,7 @@
92
91
 
93
92
  clone() {
94
93
 
95
- 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 );
96
95
  clone.isDynamic = this.isDynamic;
97
96
  return clone;
98
97
 
@@ -383,9 +382,20 @@
383
382
 
384
383
  }
385
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
+
386
390
  class USDZExporterOptions {
387
- ar: { anchoring: { type: string } } = { anchoring: { type: 'plane' } };
388
- 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;
389
399
  extensions: any[] = [];
390
400
  }
391
401
 
@@ -594,7 +604,7 @@
594
604
 
595
605
  writer.appendLine( `token preliminary:anchoring:type = "${context.exporter.sceneAnchoringOptions.ar.anchoring.type}"` );
596
606
  if (context.exporter.sceneAnchoringOptions.ar.anchoring.type === 'plane')
597
- writer.appendLine( `token preliminary:planeAnchoring:alignment = "${context.exporter.sceneAnchoringOptions.planeAnchoring.alignment}"` );
607
+ writer.appendLine( `token preliminary:planeAnchoring:alignment = "${context.exporter.sceneAnchoringOptions.ar.planeAnchoring.alignment}"` );
598
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.
599
609
  if (context.exporter.sceneAnchoringOptions.ar.anchoring.type === 'image')
600
610
  writer.appendLine( `rel preliminary:imageAnchoring:referenceImage = </${context.document.name}/Scenes/Scene/AnchoringReferenceImage>` );
@@ -1076,7 +1086,7 @@
1076
1086
  const pad = ' ';
1077
1087
  const inputs: Array<string> = [];
1078
1088
  const samplers: Array<string> = [];
1079
- const exportForQuickLook = true;
1089
+ const exportForQuickLook = false;
1080
1090
 
1081
1091
  function buildTexture( texture, mapType, color: Color | undefined = undefined, opacity: number | undefined = undefined ) {
1082
1092
 
@@ -1088,7 +1098,12 @@
1088
1098
 
1089
1099
  const repeat = texture.repeat.clone();
1090
1100
  const offset = texture.offset.clone();
1101
+ const rotation = texture.rotation;
1091
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);
1106
+
1092
1107
  // texture coordinates start in the opposite corner, need to correct
1093
1108
  offset.y = 1 - offset.y - repeat.y;
1094
1109
 
@@ -1096,15 +1111,28 @@
1096
1111
  // Apple Feedback: FB10036297 and FB11442287
1097
1112
  if ( exportForQuickLook ) {
1098
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
+
1099
1117
  offset.x = offset.x / repeat.x;
1100
1118
  offset.y = offset.y / repeat.y;
1101
1119
 
1120
+ offset.x += xRotationOffset / repeat.x;
1121
+ offset.y += yRotationOffset - 1;
1102
1122
  }
1103
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
+
1130
+ }
1131
+
1104
1132
  textures[ id ] = texture;
1105
1133
  const uvReader = mapType == 'occlusion' ? 'uvReader_st2' : 'uvReader_st';
1106
1134
 
1107
- 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 );
1108
1136
  const textureTransformInput = `</Materials/Material_${material.id}/${uvReader}.outputs:result>`;
1109
1137
  const textureTransformOutput = `</Materials/Material_${material.id}/Transform2d_${mapType}.outputs:result>`;
1110
1138
 
@@ -1123,6 +1151,7 @@
1123
1151
  float2 inputs:in.connect = ${textureTransformInput}
1124
1152
  float2 inputs:scale = ${buildVector2( repeat )}
1125
1153
  float2 inputs:translation = ${buildVector2( offset )}
1154
+ float inputs:rotation = ${(rotation / Math.PI * 180).toFixed( PRECISION )}
1126
1155
  float2 outputs:result
1127
1156
  }
1128
1157
  ` : '' }
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -13,6 +13,7 @@
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 @@
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,10 +88,12 @@
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;
91
+
92
+ if (this.interactive) {
93
+ this.extensions.push(new BehaviorExtension());
94
+ }
87
95
  }
88
96
 
89
-
90
-
91
97
  onEnable() {
92
98
  const ios = isiOS()
93
99
  const safari = isSafari();
@@ -157,12 +163,13 @@
157
163
  ar: {
158
164
  anchoring: {
159
165
  type: this.anchoringType,
160
- }
166
+ },
167
+ planeAnchoring: {
168
+ alignment: this.planeAnchoringAlignment,
169
+ },
161
170
  },
162
- planeAnchoring: {
163
- alignment: this.planeAnchoringAlignment,
164
- },
165
- extensions: extensions
171
+ extensions: extensions,
172
+ quickLookCompatible: true,
166
173
  });
167
174
  const blob = new Blob([arraybuffer], { type: 'application/octet-stream' });