Needle Engine

Changes between version 3.3.0-alpha and 3.4.0-alpha
Files changed (27) hide show
  1. src/engine/codegen/register_types.js +52 -4
  2. plugins/vite/reload.js +13 -2
  3. src/engine-components/Animation.ts +4 -0
  4. src/engine-components/export/usdz/extensions/Animation.ts +37 -45
  5. src/engine-components/ui/BaseUIComponent.ts +7 -1
  6. src/engine-components/ui/Canvas.ts +80 -5
  7. src/engine-components/codegen/components.ts +25 -1
  8. src/engine/engine_gameobject.ts +3 -2
  9. src/engine/engine_three_utils.ts +2 -2
  10. src/engine-components/export/usdz/Extension.ts +4 -5
  11. src/engine-components/ui/Graphic.ts +2 -0
  12. src/engine-components/ui/Image.ts +3 -3
  13. src/engine-components/ui/Interfaces.ts +30 -6
  14. src/engine-components/ui/Layout.ts +303 -4
  15. src/engine-components/utils/LookAt.ts +60 -7
  16. src/engine-components/postprocessing/PostProcessingHandler.ts +1 -1
  17. src/engine-components/ui/RectTransform.ts +65 -40
  18. src/engine-components/export/usdz/types.ts +0 -39
  19. src/engine-components/export/usdz/USDZExporter.ts +39 -17
  20. src/engine-components/webxr/WebXRImageTracking.ts +100 -27
  21. src/engine-components/export/usdz/extensions/behavior/Actions.ts +99 -0
  22. src/engine-components/export/usdz/extensions/behavior/Behaviour.ts +181 -0
  23. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +503 -0
  24. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +459 -0
  25. src/engine-components/export/usdz/extensions/DocumentExtension.ts +10 -0
  26. src/engine-components/export/usdz/ThreeUSDZExporter.ts +1280 -0
  27. src/engine-components/export/usdz/extensions/USDZText.ts +142 -0
src/engine/codegen/register_types.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { TypeStore } from "./../engine_typestore"
2
-
2
+
3
3
  // Import types
4
4
  import { __Ignore } from "../../engine-components/codegen/components";
5
+ import { ActionBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
6
+ import { ActionCollection } from "../../engine-components/export/usdz/extensions/behavior/Actions";
7
+ import { ActionModel } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
5
8
  import { AlignmentConstraint } from "../../engine-components/AlignmentConstraint";
6
9
  import { Animation } from "../../engine-components/Animation";
7
10
  import { AnimationCurve } from "../../engine-components/AnimationCurve";
@@ -26,6 +29,8 @@
26
29
  import { AxesHelper } from "../../engine-components/AxesHelper";
27
30
  import { BaseUIComponent } from "../../engine-components/ui/BaseUIComponent";
28
31
  import { BasicIKConstraint } from "../../engine-components/BasicIKConstraint";
32
+ import { BehaviorExtension } from "../../engine-components/export/usdz/extensions/behavior/Behaviour";
33
+ import { BehaviorModel } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
29
34
  import { Behaviour } from "../../engine-components/Component";
30
35
  import { Bloom } from "../../engine-components/postprocessing/Effects/Bloom";
31
36
  import { BoxCollider } from "../../engine-components/Collider";
@@ -37,6 +42,8 @@
37
42
  import { Canvas } from "../../engine-components/ui/Canvas";
38
43
  import { CanvasGroup } from "../../engine-components/ui/CanvasGroup";
39
44
  import { CapsuleCollider } from "../../engine-components/Collider";
45
+ import { ChangeMaterialOnClick } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
46
+ import { ChangeTransformOnClick } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
40
47
  import { CharacterController } from "../../engine-components/CharacterController";
41
48
  import { CharacterControllerInput } from "../../engine-components/CharacterController";
42
49
  import { ChromaticAberration } from "../../engine-components/postprocessing/Effects/ChromaticAberration";
@@ -50,10 +57,12 @@
50
57
  import { DeleteBox } from "../../engine-components/DeleteBox";
51
58
  import { DepthOfField } from "../../engine-components/postprocessing/Effects/DepthOfField";
52
59
  import { DeviceFlag } from "../../engine-components/DeviceFlag";
60
+ import { DocumentExtension } from "../../engine-components/export/usdz/extensions/DocumentExtension";
53
61
  import { DragControls } from "../../engine-components/DragControls";
54
62
  import { DropListener } from "../../engine-components/DropListener";
55
63
  import { Duplicatable } from "../../engine-components/Duplicatable";
56
64
  import { EmissionModule } from "../../engine-components/ParticleSystemModules";
65
+ import { EmphasizeOnClick } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
57
66
  import { EventList } from "../../engine-components/EventList";
58
67
  import { EventListEvent } from "../../engine-components/EventList";
59
68
  import { EventSystem } from "../../engine-components/ui/EventSystem";
@@ -70,13 +79,14 @@
70
79
  import { GridHelper } from "../../engine-components/GridHelper";
71
80
  import { GridLayoutGroup } from "../../engine-components/ui/Layout";
72
81
  import { GroundProjectedEnv } from "../../engine-components/GroundProjection";
82
+ import { GroupActionModel } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
83
+ import { HideOnStart } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
73
84
  import { HingeJoint } from "../../engine-components/Joints";
74
85
  import { HorizontalLayoutGroup } from "../../engine-components/ui/Layout";
75
86
  import { Image } from "../../engine-components/ui/Image";
76
87
  import { InheritVelocityModule } from "../../engine-components/ParticleSystemModules";
77
88
  import { InputField } from "../../engine-components/ui/InputField";
78
89
  import { Interactable } from "../../engine-components/Interactable";
79
- import { LayoutGroup } from "../../engine-components/ui/Layout";
80
90
  import { Light } from "../../engine-components/Light";
81
91
  import { LimitVelocityOverLifetimeModule } from "../../engine-components/ParticleSystemModules";
82
92
  import { LODGroup } from "../../engine-components/LODGroup";
@@ -98,17 +108,21 @@
98
108
  import { OpenURL } from "../../engine-components/utils/OpenURL";
99
109
  import { OrbitControls } from "../../engine-components/OrbitControls";
100
110
  import { Outline } from "../../engine-components/ui/Outline";
111
+ import { Padding } from "../../engine-components/ui/Layout";
101
112
  import { ParticleBurst } from "../../engine-components/ParticleSystemModules";
102
113
  import { ParticleSubEmitter } from "../../engine-components/ParticleSystemSubEmitter";
103
114
  import { ParticleSystem } from "../../engine-components/ParticleSystem";
104
115
  import { ParticleSystemRenderer } from "../../engine-components/ParticleSystem";
105
116
  import { PixelationEffect } from "../../engine-components/postprocessing/Effects/Pixelation";
106
117
  import { PlayableDirector } from "../../engine-components/timeline/PlayableDirector";
118
+ import { PlayAnimationOnClick } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
107
119
  import { PlayerColor } from "../../engine-components/PlayerColor";
108
120
  import { PlayerState } from "../../engine-components-experimental/networking/PlayerSync";
109
121
  import { PlayerSync } from "../../engine-components-experimental/networking/PlayerSync";
110
122
  import { PointerEventData } from "../../engine-components/ui/PointerEvents";
111
123
  import { PostProcessingHandler } from "../../engine-components/postprocessing/PostProcessingHandler";
124
+ import { PreliminaryAction } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
125
+ import { PreliminaryTrigger } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
112
126
  import { PresentationMode } from "../../engine-components-experimental/Presentation";
113
127
  import { QuickLookOverlay } from "../../engine-components/export/usdz/USDZExporter";
114
128
  import { RawImage } from "../../engine-components/ui/Image";
@@ -127,6 +141,7 @@
127
141
  import { SceneSwitcher } from "../../engine-components/SceneSwitcher";
128
142
  import { ScreenCapture } from "../../engine-components/ScreenCapture";
129
143
  import { ScreenSpaceAmbientOcclusion } from "../../engine-components/postprocessing/Effects/ScreenspaceAmbientOcclusion";
144
+ import { SetActiveOnClick } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
130
145
  import { ShadowCatcher } from "../../engine-components/ShadowCatcher";
131
146
  import { ShapeModule } from "../../engine-components/ParticleSystemModules";
132
147
  import { SignalAsset } from "../../engine-components/timeline/SignalAsset";
@@ -151,24 +166,33 @@
151
166
  import { SyncedCamera } from "../../engine-components/SyncedCamera";
152
167
  import { SyncedRoom } from "../../engine-components/SyncedRoom";
153
168
  import { SyncedTransform } from "../../engine-components/SyncedTransform";
169
+ import { TapGestureTrigger } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
154
170
  import { TeleportTarget } from "../../engine-components/webxr/WebXRController";
155
171
  import { TestRunner } from "../../engine-components/TestRunner";
156
172
  import { TestSimulateUserData } from "../../engine-components/TestRunner";
157
173
  import { Text } from "../../engine-components/ui/Text";
174
+ import { TextBuilder } from "../../engine-components/export/usdz/extensions/USDZText";
175
+ import { TextExtension } from "../../engine-components/export/usdz/extensions/USDZText";
158
176
  import { TextureSheetAnimationModule } from "../../engine-components/ParticleSystemModules";
159
177
  import { TiltShiftEffect } from "../../engine-components/postprocessing/Effects/TiltShiftEffect";
160
178
  import { ToneMapping } from "../../engine-components/postprocessing/Effects/Tonemapping";
161
179
  import { TrailModule } from "../../engine-components/ParticleSystemModules";
162
180
  import { TransformData } from "../../engine-components/export/usdz/extensions/Animation";
163
181
  import { TransformGizmo } from "../../engine-components/TransformGizmo";
182
+ import { TriggerBuilder } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
183
+ import { TriggerModel } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
164
184
  import { UIRaycastUtils } from "../../engine-components/ui/RaycastUtils";
165
185
  import { UIRootComponent } from "../../engine-components/ui/BaseUIComponent";
166
186
  import { UsageMarker } from "../../engine-components/Interactable";
187
+ import { USDZBehaviours } from "../../engine-components/export/usdz/extensions/behavior/Behaviour";
167
188
  import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter";
189
+ import { USDZText } from "../../engine-components/export/usdz/extensions/USDZText";
190
+ import { VariantAction } from "../../engine-components/export/usdz/extensions/behavior/Actions";
168
191
  import { VelocityOverLifetimeModule } from "../../engine-components/ParticleSystemModules";
169
192
  import { VerticalLayoutGroup } from "../../engine-components/ui/Layout";
170
193
  import { VideoPlayer } from "../../engine-components/VideoPlayer";
171
194
  import { Vignette } from "../../engine-components/postprocessing/Effects/Vignette";
195
+ import { VisibilityAction } from "../../engine-components/export/usdz/extensions/behavior/BehaviourComponents";
172
196
  import { Voip } from "../../engine-components/Voip";
173
197
  import { Volume } from "../../engine-components/postprocessing/Volume";
174
198
  import { VolumeParameter } from "../../engine-components/postprocessing/VolumeParameter";
@@ -190,9 +214,12 @@
190
214
  import { XRGrabRendering } from "../../engine-components/webxr/WebXRGrabRendering";
191
215
  import { XRRig } from "../../engine-components/webxr/WebXRRig";
192
216
  import { XRState } from "../../engine-components/XRFlag";
193
-
217
+
194
218
  // Register types
195
219
  TypeStore.add("__Ignore", __Ignore);
220
+ TypeStore.add("ActionBuilder", ActionBuilder);
221
+ TypeStore.add("ActionCollection", ActionCollection);
222
+ TypeStore.add("ActionModel", ActionModel);
196
223
  TypeStore.add("AlignmentConstraint", AlignmentConstraint);
197
224
  TypeStore.add("Animation", Animation);
198
225
  TypeStore.add("AnimationCurve", AnimationCurve);
@@ -217,6 +244,8 @@
217
244
  TypeStore.add("AxesHelper", AxesHelper);
218
245
  TypeStore.add("BaseUIComponent", BaseUIComponent);
219
246
  TypeStore.add("BasicIKConstraint", BasicIKConstraint);
247
+ TypeStore.add("BehaviorExtension", BehaviorExtension);
248
+ TypeStore.add("BehaviorModel", BehaviorModel);
220
249
  TypeStore.add("Behaviour", Behaviour);
221
250
  TypeStore.add("Bloom", Bloom);
222
251
  TypeStore.add("BoxCollider", BoxCollider);
@@ -228,6 +257,8 @@
228
257
  TypeStore.add("Canvas", Canvas);
229
258
  TypeStore.add("CanvasGroup", CanvasGroup);
230
259
  TypeStore.add("CapsuleCollider", CapsuleCollider);
260
+ TypeStore.add("ChangeMaterialOnClick", ChangeMaterialOnClick);
261
+ TypeStore.add("ChangeTransformOnClick", ChangeTransformOnClick);
231
262
  TypeStore.add("CharacterController", CharacterController);
232
263
  TypeStore.add("CharacterControllerInput", CharacterControllerInput);
233
264
  TypeStore.add("ChromaticAberration", ChromaticAberration);
@@ -241,10 +272,12 @@
241
272
  TypeStore.add("DeleteBox", DeleteBox);
242
273
  TypeStore.add("DepthOfField", DepthOfField);
243
274
  TypeStore.add("DeviceFlag", DeviceFlag);
275
+ TypeStore.add("DocumentExtension", DocumentExtension);
244
276
  TypeStore.add("DragControls", DragControls);
245
277
  TypeStore.add("DropListener", DropListener);
246
278
  TypeStore.add("Duplicatable", Duplicatable);
247
279
  TypeStore.add("EmissionModule", EmissionModule);
280
+ TypeStore.add("EmphasizeOnClick", EmphasizeOnClick);
248
281
  TypeStore.add("EventList", EventList);
249
282
  TypeStore.add("EventListEvent", EventListEvent);
250
283
  TypeStore.add("EventSystem", EventSystem);
@@ -261,13 +294,14 @@
261
294
  TypeStore.add("GridHelper", GridHelper);
262
295
  TypeStore.add("GridLayoutGroup", GridLayoutGroup);
263
296
  TypeStore.add("GroundProjectedEnv", GroundProjectedEnv);
297
+ TypeStore.add("GroupActionModel", GroupActionModel);
298
+ TypeStore.add("HideOnStart", HideOnStart);
264
299
  TypeStore.add("HingeJoint", HingeJoint);
265
300
  TypeStore.add("HorizontalLayoutGroup", HorizontalLayoutGroup);
266
301
  TypeStore.add("Image", Image);
267
302
  TypeStore.add("InheritVelocityModule", InheritVelocityModule);
268
303
  TypeStore.add("InputField", InputField);
269
304
  TypeStore.add("Interactable", Interactable);
270
- TypeStore.add("LayoutGroup", LayoutGroup);
271
305
  TypeStore.add("Light", Light);
272
306
  TypeStore.add("LimitVelocityOverLifetimeModule", LimitVelocityOverLifetimeModule);
273
307
  TypeStore.add("LODGroup", LODGroup);
@@ -289,17 +323,21 @@
289
323
  TypeStore.add("OpenURL", OpenURL);
290
324
  TypeStore.add("OrbitControls", OrbitControls);
291
325
  TypeStore.add("Outline", Outline);
326
+ TypeStore.add("Padding", Padding);
292
327
  TypeStore.add("ParticleBurst", ParticleBurst);
293
328
  TypeStore.add("ParticleSubEmitter", ParticleSubEmitter);
294
329
  TypeStore.add("ParticleSystem", ParticleSystem);
295
330
  TypeStore.add("ParticleSystemRenderer", ParticleSystemRenderer);
296
331
  TypeStore.add("PixelationEffect", PixelationEffect);
297
332
  TypeStore.add("PlayableDirector", PlayableDirector);
333
+ TypeStore.add("PlayAnimationOnClick", PlayAnimationOnClick);
298
334
  TypeStore.add("PlayerColor", PlayerColor);
299
335
  TypeStore.add("PlayerState", PlayerState);
300
336
  TypeStore.add("PlayerSync", PlayerSync);
301
337
  TypeStore.add("PointerEventData", PointerEventData);
302
338
  TypeStore.add("PostProcessingHandler", PostProcessingHandler);
339
+ TypeStore.add("PreliminaryAction", PreliminaryAction);
340
+ TypeStore.add("PreliminaryTrigger", PreliminaryTrigger);
303
341
  TypeStore.add("PresentationMode", PresentationMode);
304
342
  TypeStore.add("QuickLookOverlay", QuickLookOverlay);
305
343
  TypeStore.add("RawImage", RawImage);
@@ -318,6 +356,7 @@
318
356
  TypeStore.add("SceneSwitcher", SceneSwitcher);
319
357
  TypeStore.add("ScreenCapture", ScreenCapture);
320
358
  TypeStore.add("ScreenSpaceAmbientOcclusion", ScreenSpaceAmbientOcclusion);
359
+ TypeStore.add("SetActiveOnClick", SetActiveOnClick);
321
360
  TypeStore.add("ShadowCatcher", ShadowCatcher);
322
361
  TypeStore.add("ShapeModule", ShapeModule);
323
362
  TypeStore.add("SignalAsset", SignalAsset);
@@ -342,24 +381,33 @@
342
381
  TypeStore.add("SyncedCamera", SyncedCamera);
343
382
  TypeStore.add("SyncedRoom", SyncedRoom);
344
383
  TypeStore.add("SyncedTransform", SyncedTransform);
384
+ TypeStore.add("TapGestureTrigger", TapGestureTrigger);
345
385
  TypeStore.add("TeleportTarget", TeleportTarget);
346
386
  TypeStore.add("TestRunner", TestRunner);
347
387
  TypeStore.add("TestSimulateUserData", TestSimulateUserData);
348
388
  TypeStore.add("Text", Text);
389
+ TypeStore.add("TextBuilder", TextBuilder);
390
+ TypeStore.add("TextExtension", TextExtension);
349
391
  TypeStore.add("TextureSheetAnimationModule", TextureSheetAnimationModule);
350
392
  TypeStore.add("TiltShiftEffect", TiltShiftEffect);
351
393
  TypeStore.add("ToneMapping", ToneMapping);
352
394
  TypeStore.add("TrailModule", TrailModule);
353
395
  TypeStore.add("TransformData", TransformData);
354
396
  TypeStore.add("TransformGizmo", TransformGizmo);
397
+ TypeStore.add("TriggerBuilder", TriggerBuilder);
398
+ TypeStore.add("TriggerModel", TriggerModel);
355
399
  TypeStore.add("UIRaycastUtils", UIRaycastUtils);
356
400
  TypeStore.add("UIRootComponent", UIRootComponent);
357
401
  TypeStore.add("UsageMarker", UsageMarker);
402
+ TypeStore.add("USDZBehaviours", USDZBehaviours);
358
403
  TypeStore.add("USDZExporter", USDZExporter);
404
+ TypeStore.add("USDZText", USDZText);
405
+ TypeStore.add("VariantAction", VariantAction);
359
406
  TypeStore.add("VelocityOverLifetimeModule", VelocityOverLifetimeModule);
360
407
  TypeStore.add("VerticalLayoutGroup", VerticalLayoutGroup);
361
408
  TypeStore.add("VideoPlayer", VideoPlayer);
362
409
  TypeStore.add("Vignette", Vignette);
410
+ TypeStore.add("VisibilityAction", VisibilityAction);
363
411
  TypeStore.add("Voip", Voip);
364
412
  TypeStore.add("Volume", Volume);
365
413
  TypeStore.add("VolumeParameter", VolumeParameter);
plugins/vite/reload.js CHANGED
@@ -9,6 +9,7 @@
9
9
  const __dirname = path.dirname(__filename);
10
10
 
11
11
  const filesUsingHotReload = new Set();
12
+ let assetsDirectory = "";
12
13
 
13
14
  export const needleReload = (command, config, userSettings) => {
14
15
  if (command === "build") return;
@@ -23,8 +24,9 @@
23
24
  if (res) config = res;
24
25
  }
25
26
 
27
+ const projectConfig = tryLoadProjectConfig();
28
+ assetsDirectory = path.resolve(projectConfig?.assetsDirectory || "assets");
26
29
 
27
- const projectConfig = tryLoadProjectConfig();
28
30
  const buildDirectory = projectConfig?.buildDirectory?.length ? process.cwd().replaceAll("\\", "/") + "/" + projectConfig?.buildDirectory : "";
29
31
  if (buildDirectory?.length) {
30
32
  setTimeout(() => console.log("Build directory: ", buildDirectory), 100);
@@ -153,9 +155,18 @@
153
155
  }
154
156
  }
155
157
 
156
- if (file.endsWith(".vue") || file.endsWith(".ts") || file.endsWith(".js") || file.endsWith(".jsx") || file.endsWith(".tsx"))
158
+ // these are known file types we export from integrations
159
+ const knownExportFileTypes = [ ".glb", ".gltf", ".bin", "exr", ".ktx2", ".mp3", ".ogg", ".mp4", ".webm" ];
160
+ if (!knownExportFileTypes.some((type) => file.endsWith(type)))
157
161
  return;
158
162
 
163
+ // we only care about exports into "assets"
164
+ if (!path.resolve(file).startsWith(assetsDirectory))
165
+ return;
166
+
167
+ if (file.endsWith(".svelte") || file.endsWith(".vue") || file.endsWith(".ts") || file.endsWith(".js") || file.endsWith(".jsx") || file.endsWith(".tsx"))
168
+ return;
169
+
159
170
  if (file.endsWith(lockFileName)) return;
160
171
  let fileSize = "";
161
172
  const isGlbOrGltfFile = file.endsWith(".glb") || file.endsWith(".bin");
src/engine-components/Animation.ts CHANGED
@@ -176,6 +176,10 @@
176
176
  return this.internalOnPlay(act, options);
177
177
  }
178
178
  }
179
+ if (!(clip instanceof AnimationClip)) {
180
+ console.warn("Clip is no AnimationClip", clip, "on object: " + this.name)
181
+ return;
182
+ }
179
183
  const act = this.mixer.clipAction(clip);
180
184
  this.actions.push(act);
181
185
  return this.internalOnPlay(act, options);
src/engine-components/export/usdz/extensions/Animation.ts CHANGED
@@ -1,17 +1,18 @@
1
1
  import { GameObject } from "../../../Component";
2
2
  import { getParam } from "../../../../engine/engine_utils";
3
- import { Object3D, Color, Matrix4, MeshStandardMaterial, Vector3, Quaternion, Interpolant } from "three";
4
- //@ts-ignore
5
- import { USDZObject, buildMatrix } from "three/examples/jsm/exporters/USDZExporter"
6
- import { IUSDZExporterExtension } from "../Extension";
7
3
 
4
+ import { USDObject, buildMatrix } from "../ThreeUSDZExporter";
5
+ import { IUSDExporterExtension } from "../Extension";
6
+
7
+ import { Object3D, Matrix4, Vector3, Quaternion, Interpolant, AnimationClip, KeyframeTrack } from "three";
8
+
8
9
  const debug = getParam("debugusdzanimation");
9
10
 
10
11
  export interface UsdzAnimation {
11
- createAnimation(ext: AnimationExtension, model: USDZObject, context);
12
+ createAnimation(ext: AnimationExtension, model: USDObject, context);
12
13
  }
13
14
 
14
- export type AnimationClipCollection = Array<{ root: Object3D, clips: Array<THREE.AnimationClip> }>;
15
+ export type AnimationClipCollection = Array<{ root: Object3D, clips: Array<AnimationClip> }>;
15
16
 
16
17
  export class RegisteredAnimationInfo {
17
18
 
@@ -20,9 +21,9 @@
20
21
 
21
22
  private ext: AnimationExtension;
22
23
  private root: Object3D;
23
- private clip: THREE.AnimationClip;
24
+ private clip: AnimationClip;
24
25
 
25
- constructor(ext: AnimationExtension, root: THREE.Object3D, clip: THREE.AnimationClip) {
26
+ constructor(ext: AnimationExtension, root: Object3D, clip: AnimationClip) {
26
27
  this.ext = ext;
27
28
  this.root = root;
28
29
  this.clip = clip;
@@ -30,17 +31,17 @@
30
31
  }
31
32
 
32
33
  export class TransformData {
33
- clip: THREE.AnimationClip;
34
- pos?: THREE.KeyframeTrack;
35
- rot?: THREE.KeyframeTrack;
36
- scale?: THREE.KeyframeTrack;
34
+ clip: AnimationClip;
35
+ pos?: KeyframeTrack;
36
+ rot?: KeyframeTrack;
37
+ scale?: KeyframeTrack;
37
38
  get frameRate(): number { return 60; }
38
39
 
39
40
  private ext: AnimationExtension;
40
41
  private root: Object3D;
41
42
  private target: Object3D;
42
43
 
43
- constructor(ext: AnimationExtension, root: Object3D, target: Object3D, clip: THREE.AnimationClip) {
44
+ constructor(ext: AnimationExtension, root: Object3D, target: Object3D, clip: AnimationClip) {
44
45
  this.ext = ext;
45
46
  this.root = root;
46
47
  this.target = target;
@@ -78,14 +79,14 @@
78
79
 
79
80
  declare type AnimationDict = Map<Object3D, Array<TransformData>>;
80
81
 
81
- export class AnimationExtension implements IUSDZExporterExtension {
82
+ export class AnimationExtension implements IUSDExporterExtension {
82
83
 
83
84
  get extensionName(): string { return "animation" }
84
85
  private dict: AnimationDict = new Map();
85
86
  // private rootTargetMap: Map<Object3D, Object3D[]> = new Map();
86
87
  private rootTargetMap: Map<Object3D, Object3D[]> = new Map();
87
88
 
88
- getStartTime01(root: Object3D, clip: THREE.AnimationClip) {
89
+ getStartTime01(root: Object3D, clip: AnimationClip) {
89
90
  const targets = this.rootTargetMap.get(root);
90
91
  if (!targets) return Infinity;
91
92
  let longestStartTime: number = -1;
@@ -108,7 +109,7 @@
108
109
  return longestStartTime;
109
110
  }
110
111
 
111
- registerAnimation(root: Object3D, clip: THREE.AnimationClip): RegisteredAnimationInfo | null {
112
+ registerAnimation(root: Object3D, clip: AnimationClip): RegisteredAnimationInfo | null {
112
113
  if (!clip || !root) return null;
113
114
  if (!this.rootTargetMap.has(root)) this.rootTargetMap.set(root, []);
114
115
  // this.rootTargetMap.get(root)?.push(clip);
@@ -165,7 +166,7 @@
165
166
  }
166
167
  }
167
168
 
168
- onExportObject(object, model: USDZObject, _context) {
169
+ onExportObject(object, model: USDObject, _context) {
169
170
 
170
171
  GameObject.foreachComponent(object, (comp) => {
171
172
  const c = comp as unknown as UsdzAnimation;
@@ -187,7 +188,7 @@
187
188
 
188
189
  object: Object3D;
189
190
  dict: AnimationDict;
190
- model: USDZObject;
191
+ model: USDObject | undefined = undefined;
191
192
 
192
193
  private callback?: Function;
193
194
 
@@ -196,7 +197,7 @@
196
197
  this.dict = dict;
197
198
  }
198
199
 
199
- registerCallback(model: USDZObject) {
200
+ registerCallback(model: USDObject) {
200
201
  if (this.model && this.callback) {
201
202
  this.model.removeEventListener("serialize", this.callback);
202
203
  }
@@ -209,6 +210,7 @@
209
210
  }
210
211
 
211
212
  onSerialize(writer, _context) {
213
+ if (!this.model) return;
212
214
  if (debug)
213
215
  console.log("SERIALIZE", this.model.name, this.object.type);
214
216
  // do we have a track for this?
@@ -228,22 +230,24 @@
228
230
  const rotation = new Quaternion();
229
231
  const scale = new Vector3(1, 1, 1);
230
232
 
231
- // TODO doesn't support individual time arrays right now
232
- // could use these in case we don't have time values that are identical
233
- /*
234
- const translationInterpolant = o.pos?.createInterpolant() as THREE.Interpolant;
235
- const rotationInterpolant = o.rot?.createInterpolant() as THREE.Interpolant;
236
- const scaleInterpolant = o.scale?.createInterpolant() as THREE.Interpolant;
237
- */
238
-
239
233
  writer.appendLine("matrix4d xformOp:transform.timeSamples = {");
240
234
  writer.indent++;
241
235
 
242
236
  for (const transformData of arr) {
243
- let timesArray = transformData.pos?.times;
244
- if (!timesArray || transformData.rot && transformData.rot.times?.length > timesArray?.length) timesArray = transformData.rot?.times;
245
- if (!timesArray || transformData.scale && transformData.scale.times?.length > timesArray?.length) timesArray = transformData.scale?.times;
246
- if (!timesArray) {
237
+ let posTimesArray = transformData.pos?.times;
238
+ let rotTimesArray = transformData.rot?.times;
239
+ let scaleTimesArray = transformData.scale?.times;
240
+
241
+ // timesArray is the sorted union of all time values
242
+ let timesArray: number[] = [];
243
+ if (posTimesArray) for (const t of posTimesArray) timesArray.push(t);
244
+ if (rotTimesArray) for (const t of rotTimesArray) timesArray.push(t);
245
+ if (scaleTimesArray) for (const t of scaleTimesArray) timesArray.push(t);
246
+ // sort
247
+ timesArray.sort((a, b) => a - b);
248
+ timesArray = [...new Set(timesArray)];
249
+
250
+ if (!timesArray || timesArray.length === 0) {
247
251
  console.error("got an animated object but no time values??", object, transformData);
248
252
  continue;
249
253
  }
@@ -276,8 +280,8 @@
276
280
  rotation.set(quat[0], quat[1], quat[2], quat[3]);
277
281
  }
278
282
  if (scaleInterpolant) {
279
- const scale = scaleInterpolant.evaluate(time);
280
- scale.set(scale[0], scale[1], scale[2]);
283
+ const scaleVal = scaleInterpolant.evaluate(time);
284
+ scale.set(scaleVal[0], scaleVal[1], scaleVal[2]);
281
285
  }
282
286
 
283
287
  composedTransform.compose(translation, rotation, scale);
@@ -290,17 +294,5 @@
290
294
  }
291
295
  writer.indent--;
292
296
  writer.appendLine("}");
293
-
294
- /*
295
- let transform3 = new Matrix4();
296
- transform3.compose(0.2,0,0);
297
- const transform = buildMatrix(model.matrix);
298
- const transform2 = buildMatrix(transform3.multiply(model.matrix));
299
-
300
- writer.appendLine(`matrix4d xformOp:transform.timeSamples = {
301
- 0: ${transform},
302
- 30: ${transform2}
303
- }`);
304
- */
305
297
  }
306
298
  }
src/engine-components/ui/BaseUIComponent.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  import { EventSystem } from "./EventSystem";
5
5
  import { showGizmos } from '../../engine/engine_default_parameters';
6
6
  import { AxesHelper, Object3D } from 'three';
7
- import { IGraphic } from './Interfaces';
7
+ import { ICanvas, IGraphic } from './Interfaces';
8
8
  import { ShadowCastingMode } from '../Renderer';
9
9
  export const includesDir = "./include";
10
10
 
@@ -50,6 +50,12 @@
50
50
  return this._root;
51
51
  }
52
52
 
53
+ protected get Canvas() {
54
+ const cv = this.Root as any as ICanvas;
55
+ if (cv?.isCanvas) return cv;
56
+ return null;
57
+ }
58
+
53
59
  // private _intermediate?: Object3D;
54
60
  protected _parentComponent?: BaseUIComponent | null = undefined;
55
61
 
src/engine-components/ui/Canvas.ts CHANGED
@@ -3,12 +3,14 @@
3
3
  import { FrameEvent } from "../../engine/engine_setup";
4
4
  import { BaseUIComponent, UIRootComponent } from "./BaseUIComponent";
5
5
  import { GameObject } from "../Component";
6
- import { Object3D } from "three";
6
+ import { Matrix4, Object3D } from "three";
7
7
  import { RectTransform } from "./RectTransform";
8
- import { ICanvas } from "./Interfaces";
8
+ import { ICanvas, ILayoutGroup, IRectTransform } from "./Interfaces";
9
9
  import { Camera } from "../Camera";
10
10
  import { EventSystem } from "./EventSystem";
11
11
  import * as ThreeMeshUI from 'three-mesh-ui'
12
+ import { getParam } from "../../engine/engine_utils";
13
+ import { LayoutGroup } from "./Layout";
12
14
 
13
15
  export enum RenderMode {
14
16
  ScreenSpaceOverlay = 0,
@@ -17,8 +19,14 @@
17
19
  Undefined = -1,
18
20
  }
19
21
 
22
+ const debugLayout = getParam("debuguilayout");
23
+
20
24
  export class Canvas extends UIRootComponent implements ICanvas {
21
25
 
26
+ get isCanvas() {
27
+ return true;
28
+ }
29
+
22
30
  get screenspace(): any {
23
31
  return this.renderMode !== RenderMode.WorldSpace;
24
32
  }
@@ -122,6 +130,10 @@
122
130
  super.awake();
123
131
  }
124
132
 
133
+ start() {
134
+ this.onUpdateRenderMode();
135
+ }
136
+
125
137
  onEnable() {
126
138
  super.onEnable();
127
139
  this._updateRenderSettingsRoutine = undefined;
@@ -149,7 +161,29 @@
149
161
  private _boundRenderSettingsChanged = this.onRenderSettingsChanged.bind(this);
150
162
 
151
163
  private previousParent: Object3D | null = null;
164
+ private _lastMatrixWorld: Matrix4 | null = null;
165
+ private _rectTransforms: IRectTransform[] = [];
152
166
 
167
+ registerTransform(rt: IRectTransform) {
168
+ this._rectTransforms.push(rt);
169
+ }
170
+ unregisterTransform(rt: IRectTransform) {
171
+ const index = this._rectTransforms.indexOf(rt);
172
+ if (index !== -1) {
173
+ this._rectTransforms.splice(index, 1);
174
+ }
175
+ }
176
+
177
+ private _layoutGroups: Map<Object3D, ILayoutGroup> = new Map();
178
+ registerLayoutGroup(group: ILayoutGroup) {
179
+ const obj = group.gameObject;
180
+ this._layoutGroups.set(obj, group)
181
+ }
182
+ unregisterLayoutGroup(group: ILayoutGroup) {
183
+ const obj = group.gameObject;
184
+ this._layoutGroups.delete(obj);
185
+ }
186
+
153
187
  onBeforeRenderRoutine = () => {
154
188
  if (this.renderOnTop) {
155
189
  // This is just a test but in reality it should be combined with all world canvases with render on top in one render pass
@@ -158,6 +192,7 @@
158
192
  }
159
193
  else {
160
194
  this.onUpdateRenderMode();
195
+ this.handleLayoutUpdates();
161
196
  // TODO: we might need to optimize this. This is here to make sure the TMUI text clipping matrices are correct. Ideally the text does use onBeforeRender and apply the clipping matrix there so we dont have to force update all the matrices here
162
197
  this.shadowComponent?.updateMatrixWorld(true);
163
198
  this.shadowComponent?.updateWorldMatrix(true, true);
@@ -171,13 +206,42 @@
171
206
  this.context.renderer.autoClear = false;
172
207
  this.context.renderer.clearDepth();
173
208
  this.onUpdateRenderMode(true);
209
+ this.handleLayoutUpdates();
174
210
  this.shadowComponent?.updateMatrixWorld(true);
211
+ // this.handleLayoutUpdates();
175
212
  EventSystem.ensureUpdateMeshUI(ThreeMeshUI, this.context);
176
213
  this.context.renderer.render(this.gameObject, this.context.mainCamera);
177
214
  this.context.renderer.autoClear = true;
178
215
  }
216
+ this._lastMatrixWorld?.copy(this.gameObject.matrixWorld);
179
217
  }
180
218
 
219
+ private handleLayoutUpdates() {
220
+ if (this._lastMatrixWorld === null) {
221
+ this._lastMatrixWorld = new Matrix4();
222
+ }
223
+ const matrixWorldChanged = !this._lastMatrixWorld.equals(this.gameObject.matrixWorld);
224
+ if (debugLayout && matrixWorldChanged) console.log("Canvas Layout changed", this.context.time.frameCount, this.name);
225
+
226
+ // TODO: optimize this, we should only need to update a subhierarchy of the parts where layout has changed
227
+ let didLog = false;
228
+ for (const ch of this._rectTransforms) {
229
+ if (matrixWorldChanged) ch.markDirty();
230
+ let layout = this._layoutGroups.get(ch.gameObject);
231
+ if(ch.isDirty && !layout){
232
+ layout = ch.gameObject.getComponentInParent(LayoutGroup) as LayoutGroup;
233
+ }
234
+ if (ch.isDirty || layout?.isDirty) {
235
+ if (debugLayout && !didLog) {
236
+ console.log("CANVAS UPDATE ### " + ch.name + " ##################################### " + this.context.time.frame);
237
+ // didLog = true;
238
+ }
239
+ layout?.updateLayout();
240
+ ch.updateTransform();
241
+ }
242
+ }
243
+ }
244
+
181
245
  applyRenderSettings() {
182
246
  this.onRenderSettingsChanged();
183
247
  }
@@ -205,6 +269,7 @@
205
269
  private _lastWidth: number = -1;
206
270
  private _lastHeight: number = -1;
207
271
 
272
+
208
273
  private onUpdateRenderMode(force: boolean = false) {
209
274
  if (!force) {
210
275
  if (this._renderMode === this._activeRenderMode && this._lastWidth === this.context.domWidth && this._lastHeight === this.context.domHeight) {
@@ -241,6 +306,13 @@
241
306
  canvas.quaternion.identity();
242
307
 
243
308
  const rect = this.gameObject.getComponent(RectTransform)!;
309
+ let hasChanged = false;
310
+ if (rect.sizeDelta.x !== this.context.domWidth) {
311
+ hasChanged = true;
312
+ }
313
+ if (rect.sizeDelta.y !== this.context.domHeight) {
314
+ hasChanged = true;
315
+ }
244
316
 
245
317
  const vFOV = camera.fieldOfView! * Math.PI / 180;
246
318
  const h = 2 * Math.tan(vFOV / 2) * Math.abs(plane);
@@ -248,11 +320,14 @@
248
320
  canvas.scale.y = h / this.context.domHeight;
249
321
  // Set scale.z, otherwise small offsets in screenspace mode have different visual results based on export scale and other settings
250
322
  canvas.scale.z = .01;
251
- rect.sizeDelta.x = this.context.domWidth;
252
- rect.sizeDelta.y = this.context.domHeight;
253
- rect?.markDirty();
254
323
 
324
+ if (hasChanged) {
325
+ rect.sizeDelta.x = this.context.domWidth;
326
+ rect.sizeDelta.y = this.context.domHeight;
327
+ rect?.markDirty();
328
+ }
255
329
 
330
+
256
331
  // this.context.scene.add(this.gameObject)
257
332
  // this.gameObject.scale.multiplyScalar(.01);
258
333
  // this.gameObject.position.set(0,0,0);
src/engine-components/codegen/components.ts CHANGED
@@ -1,5 +1,8 @@
1
1
  // Export types
2
2
  export class __Ignore {}
3
+ export { ActionBuilder } from "../export/usdz/extensions/behavior/BehavioursBuilder";
4
+ export { ActionCollection } from "../export/usdz/extensions/behavior/Actions";
5
+ export { ActionModel } from "../export/usdz/extensions/behavior/BehavioursBuilder";
3
6
  export { AlignmentConstraint } from "../AlignmentConstraint";
4
7
  export { Animation } from "../Animation";
5
8
  export { AnimationCurve } from "../AnimationCurve";
@@ -24,6 +27,8 @@
24
27
  export { AxesHelper } from "../AxesHelper";
25
28
  export { BaseUIComponent } from "../ui/BaseUIComponent";
26
29
  export { BasicIKConstraint } from "../BasicIKConstraint";
30
+ export { BehaviorExtension } from "../export/usdz/extensions/behavior/Behaviour";
31
+ export { BehaviorModel } from "../export/usdz/extensions/behavior/BehavioursBuilder";
27
32
  export { Behaviour } from "../Component";
28
33
  export { Bloom } from "../postprocessing/Effects/Bloom";
29
34
  export { BoxCollider } from "../Collider";
@@ -35,6 +40,8 @@
35
40
  export { Canvas } from "../ui/Canvas";
36
41
  export { CanvasGroup } from "../ui/CanvasGroup";
37
42
  export { CapsuleCollider } from "../Collider";
43
+ export { ChangeMaterialOnClick } from "../export/usdz/extensions/behavior/BehaviourComponents";
44
+ export { ChangeTransformOnClick } from "../export/usdz/extensions/behavior/BehaviourComponents";
38
45
  export { CharacterController } from "../CharacterController";
39
46
  export { CharacterControllerInput } from "../CharacterController";
40
47
  export { ChromaticAberration } from "../postprocessing/Effects/ChromaticAberration";
@@ -48,10 +55,12 @@
48
55
  export { DeleteBox } from "../DeleteBox";
49
56
  export { DepthOfField } from "../postprocessing/Effects/DepthOfField";
50
57
  export { DeviceFlag } from "../DeviceFlag";
58
+ export { DocumentExtension } from "../export/usdz/extensions/DocumentExtension";
51
59
  export { DragControls } from "../DragControls";
52
60
  export { DropListener } from "../DropListener";
53
61
  export { Duplicatable } from "../Duplicatable";
54
62
  export { EmissionModule } from "../ParticleSystemModules";
63
+ export { EmphasizeOnClick } from "../export/usdz/extensions/behavior/BehaviourComponents";
55
64
  export { EventList } from "../EventList";
56
65
  export { EventListEvent } from "../EventList";
57
66
  export { EventSystem } from "../ui/EventSystem";
@@ -68,13 +77,14 @@
68
77
  export { GridHelper } from "../GridHelper";
69
78
  export { GridLayoutGroup } from "../ui/Layout";
70
79
  export { GroundProjectedEnv } from "../GroundProjection";
80
+ export { GroupActionModel } from "../export/usdz/extensions/behavior/BehavioursBuilder";
81
+ export { HideOnStart } from "../export/usdz/extensions/behavior/BehaviourComponents";
71
82
  export { HingeJoint } from "../Joints";
72
83
  export { HorizontalLayoutGroup } from "../ui/Layout";
73
84
  export { Image } from "../ui/Image";
74
85
  export { InheritVelocityModule } from "../ParticleSystemModules";
75
86
  export { InputField } from "../ui/InputField";
76
87
  export { Interactable } from "../Interactable";
77
- export { LayoutGroup } from "../ui/Layout";
78
88
  export { Light } from "../Light";
79
89
  export { LimitVelocityOverLifetimeModule } from "../ParticleSystemModules";
80
90
  export { LODGroup } from "../LODGroup";
@@ -96,15 +106,19 @@
96
106
  export { OpenURL } from "../utils/OpenURL";
97
107
  export { OrbitControls } from "../OrbitControls";
98
108
  export { Outline } from "../ui/Outline";
109
+ export { Padding } from "../ui/Layout";
99
110
  export { ParticleBurst } from "../ParticleSystemModules";
100
111
  export { ParticleSubEmitter } from "../ParticleSystemSubEmitter";
101
112
  export { ParticleSystem } from "../ParticleSystem";
102
113
  export { ParticleSystemRenderer } from "../ParticleSystem";
103
114
  export { PixelationEffect } from "../postprocessing/Effects/Pixelation";
104
115
  export { PlayableDirector } from "../timeline/PlayableDirector";
116
+ export { PlayAnimationOnClick } from "../export/usdz/extensions/behavior/BehaviourComponents";
105
117
  export { PlayerColor } from "../PlayerColor";
106
118
  export { PointerEventData } from "../ui/PointerEvents";
107
119
  export { PostProcessingHandler } from "../postprocessing/PostProcessingHandler";
120
+ export { PreliminaryAction } from "../export/usdz/extensions/behavior/BehaviourComponents";
121
+ export { PreliminaryTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents";
108
122
  export { QuickLookOverlay } from "../export/usdz/USDZExporter";
109
123
  export { RawImage } from "../ui/Image";
110
124
  export { Raycaster } from "../ui/Raycaster";
@@ -122,6 +136,7 @@
122
136
  export { SceneSwitcher } from "../SceneSwitcher";
123
137
  export { ScreenCapture } from "../ScreenCapture";
124
138
  export { ScreenSpaceAmbientOcclusion } from "../postprocessing/Effects/ScreenspaceAmbientOcclusion";
139
+ export { SetActiveOnClick } from "../export/usdz/extensions/behavior/BehaviourComponents";
125
140
  export { ShadowCatcher } from "../ShadowCatcher";
126
141
  export { ShapeModule } from "../ParticleSystemModules";
127
142
  export { SignalAsset } from "../timeline/SignalAsset";
@@ -146,24 +161,33 @@
146
161
  export { SyncedCamera } from "../SyncedCamera";
147
162
  export { SyncedRoom } from "../SyncedRoom";
148
163
  export { SyncedTransform } from "../SyncedTransform";
164
+ export { TapGestureTrigger } from "../export/usdz/extensions/behavior/BehaviourComponents";
149
165
  export { TeleportTarget } from "../webxr/WebXRController";
150
166
  export { TestRunner } from "../TestRunner";
151
167
  export { TestSimulateUserData } from "../TestRunner";
152
168
  export { Text } from "../ui/Text";
169
+ export { TextBuilder } from "../export/usdz/extensions/USDZText";
170
+ export { TextExtension } from "../export/usdz/extensions/USDZText";
153
171
  export { TextureSheetAnimationModule } from "../ParticleSystemModules";
154
172
  export { TiltShiftEffect } from "../postprocessing/Effects/TiltShiftEffect";
155
173
  export { ToneMapping } from "../postprocessing/Effects/Tonemapping";
156
174
  export { TrailModule } from "../ParticleSystemModules";
157
175
  export { TransformData } from "../export/usdz/extensions/Animation";
158
176
  export { TransformGizmo } from "../TransformGizmo";
177
+ export { TriggerBuilder } from "../export/usdz/extensions/behavior/BehavioursBuilder";
178
+ export { TriggerModel } from "../export/usdz/extensions/behavior/BehavioursBuilder";
159
179
  export { UIRaycastUtils } from "../ui/RaycastUtils";
160
180
  export { UIRootComponent } from "../ui/BaseUIComponent";
161
181
  export { UsageMarker } from "../Interactable";
182
+ export { USDZBehaviours } from "../export/usdz/extensions/behavior/Behaviour";
162
183
  export { USDZExporter } from "../export/usdz/USDZExporter";
184
+ export { USDZText } from "../export/usdz/extensions/USDZText";
185
+ export { VariantAction } from "../export/usdz/extensions/behavior/Actions";
163
186
  export { VelocityOverLifetimeModule } from "../ParticleSystemModules";
164
187
  export { VerticalLayoutGroup } from "../ui/Layout";
165
188
  export { VideoPlayer } from "../VideoPlayer";
166
189
  export { Vignette } from "../postprocessing/Effects/Vignette";
190
+ export { VisibilityAction } from "../export/usdz/extensions/behavior/BehaviourComponents";
167
191
  export { Voip } from "../Voip";
168
192
  export { Volume } from "../postprocessing/Volume";
169
193
  export { VolumeParameter } from "../postprocessing/VolumeParameter";
src/engine/engine_gameobject.ts CHANGED
@@ -163,8 +163,9 @@
163
163
  return internalForEachComponent(instance, cb, recursive);
164
164
  }
165
165
 
166
- export function* foreachComponentEnumerator<T extends IComponent>(instance: Object3D, type?: Constructor<T>, includeChildren: boolean = false): Generator<T> {
166
+ export function* foreachComponentEnumerator<T extends IComponent>(instance: Object3D, type?: Constructor<T>, includeChildren: boolean = false, maxLevel: number = 999, _currentLevel: number = 0): Generator<T> {
167
167
  if (!instance?.userData.components) return;
168
+ if (_currentLevel > maxLevel) return;
168
169
  for (const comp of instance.userData.components) {
169
170
  if (type && comp?.isComponent === true && comp instanceof type) {
170
171
  yield comp;
@@ -175,7 +176,7 @@
175
176
  }
176
177
  if (includeChildren === true) {
177
178
  for (const ch of instance.children) {
178
- yield* foreachComponentEnumerator(ch, type, true);
179
+ yield* foreachComponentEnumerator(ch, type, true, maxLevel, _currentLevel + 1);
179
180
  }
180
181
  }
181
182
  }
src/engine/engine_three_utils.ts CHANGED
@@ -98,8 +98,8 @@
98
98
  const tempVec = _worldScale2;
99
99
  const obj2 = obj.parent;
100
100
  obj2.getWorldScale(tempVec);
101
- tempVec.divide(vec);
102
- obj.scale.copy(tempVec);
101
+ obj.scale.copy(vec);
102
+ obj.scale.divide(tempVec);
103
103
  }
104
104
 
105
105
  const _forward = new Vector3();
src/engine-components/export/usdz/Extension.ts CHANGED
@@ -1,12 +1,11 @@
1
- import { USDZObject } from "./types";
1
+ import { USDObject } from "./ThreeUSDZExporter";
2
2
 
3
+ export interface IUSDExporterExtension {
3
4
 
4
- export interface IUSDZExporterExtension {
5
-
6
5
  get extensionName(): string;
7
6
  onBeforeBuildDocument?(context);
8
7
  onAfterBuildDocument?(context);
9
- onExportObject?(object, model : USDZObject, context);
8
+ onExportObject?(object, model : USDObject, context);
10
9
  onAfterSerialize?(context);
11
- onAfterHierarchy?(context);
10
+ onAfterHierarchy?(context, writer : any);
12
11
  }
src/engine-components/ui/Graphic.ts CHANGED
@@ -9,6 +9,8 @@
9
9
  import { GameObject } from '../Component';
10
10
  import SimpleStateBehavior from "three-mesh-ui/examples/behaviors/states/SimpleStateBehavior"
11
11
  import { Outline } from './Outline';
12
+ import { BehaviorExtension, UsdzBehaviour } from '../../engine-components/export/usdz/extensions/behavior/Behaviour';
13
+ import { USDObject } from '../../engine-components/export/usdz/ThreeUSDZExporter';
12
14
 
13
15
  const _colorStateObject: { backgroundColor: Color, backgroundOpacity: number } = {
14
16
  backgroundColor: new Color(1, 1, 1),
src/engine-components/ui/Image.ts CHANGED
@@ -49,9 +49,9 @@
49
49
  if(this.sprite?.texture?.name === "Knob") {
50
50
  opts.borderRadius = 999;
51
51
  }
52
- opts.borderColor = new Color(.4, .4, .4);
53
- opts.borderOpacity = this.color.alpha;
54
- opts.borderWidth = .3;
52
+ // opts.borderColor = new Color(.4, .4, .4);
53
+ // opts.borderOpacity = this.color.alpha;
54
+ // opts.borderWidth = .3;
55
55
  }
56
56
 
57
57
  }
src/engine-components/ui/Interfaces.ts CHANGED
@@ -1,24 +1,48 @@
1
+ import { Behaviour } from "../Component";
1
2
  import { IComponent } from "../../engine/engine_types";
2
3
 
3
4
  export interface ICanvas {
4
- get screenspace() : boolean;
5
+ get isCanvas(): boolean;
6
+ get screenspace(): boolean;
7
+ registerTransform(rt: IRectTransform);
8
+ unregisterTransform(rt: IRectTransform);
5
9
  }
6
10
 
7
11
  export interface ICanvasGroup {
8
- get isCanvasGroup() : boolean;
12
+ get isCanvasGroup(): boolean;
9
13
  blocksRaycasts: boolean;
10
14
  interactable: boolean;
11
15
  }
12
16
 
13
17
  export interface IGraphic extends IComponent {
14
- get isGraphic() : boolean;
18
+ get isGraphic(): boolean;
15
19
  raycastTarget: boolean;
16
20
  }
17
21
 
18
22
  export interface IRectTransform extends IComponent {
23
+ get isDirty(): boolean;
24
+ markDirty();
25
+ updateTransform();
26
+ }
19
27
 
28
+ export interface IRectTransformChangedReceiver {
29
+ onParentRectTransformChanged(comp: IRectTransform): void;
20
30
  }
21
31
 
22
- export interface IRectTransformChangedReceiver {
23
- onParentRectTransformChanged(comp : IRectTransform) : void;
24
- }
32
+ export interface ILayoutGroup extends IComponent {
33
+ get isLayoutGroup(): boolean;
34
+ get isDirty(): boolean;
35
+ updateLayout();
36
+ }
37
+
38
+ // export abstract class LayoutGroup extends Behaviour implements IRectTransformChangedReceiver, ILayoutGroup {
39
+ // get isLayoutGroup(): boolean {
40
+ // return true;
41
+ // }
42
+ // updateLayout() {
43
+ // throw new Error("Method not implemented.");
44
+ // }
45
+ // onParentRectTransformChanged(comp: IRectTransform): void {
46
+ // throw new Error("Method not implemented.");
47
+ // }
48
+ // }
src/engine-components/ui/Layout.ts CHANGED
@@ -1,17 +1,316 @@
1
- import { Behaviour } from "../Component";
1
+ import { ILayoutGroup, IRectTransform, IRectTransformChangedReceiver } from "./Interfaces";
2
+ import { Behaviour, GameObject } from "../Component";
3
+ import { serializable } from "../../engine/engine_serialization";
4
+ import { Canvas } from "./Canvas";
5
+ import { RectTransform } from "./RectTransform";
6
+ import { getParam } from "../../engine/engine_utils";
2
7
 
3
- export class LayoutGroup extends Behaviour {
8
+ const debug = getParam("debuguilayout");
9
+
10
+ export class Padding {
11
+ @serializable()
12
+ left: number = 0;
13
+ @serializable()
14
+ right: number = 0;
15
+ @serializable()
16
+ top: number = 0;
17
+ @serializable()
18
+ bottom: number = 0;
19
+
20
+ get vertical() {
21
+ return this.top + this.bottom;
22
+ }
23
+ get horizontal() {
24
+ return this.left + this.right;
25
+ }
26
+ }
27
+
28
+ export enum TextAnchor {
29
+ UpperLeft = 0,
30
+ UpperCenter = 1,
31
+ UpperRight = 2,
32
+ MiddleLeft = 3,
33
+ MiddleCenter = 4,
34
+ MiddleRight = 5,
35
+ LowerLeft = 6,
36
+ LowerCenter = 7,
37
+ LowerRight = 8,
38
+ Custom = 9
39
+ }
40
+
41
+ enum Axis {
42
+ Horizontal = "x",
43
+ Vertical = "y"
44
+ }
45
+
46
+ export abstract class LayoutGroup extends Behaviour implements ILayoutGroup {
47
+
48
+ private _rectTransform: RectTransform | null = null;
49
+ private get rectTransform() {
50
+ return this._rectTransform;
51
+ }
52
+
53
+ onParentRectTransformChanged(_comp: IRectTransform): void {
54
+ this._needsUpdate = true;
55
+ }
56
+
57
+ private _needsUpdate: boolean = false;
58
+ get isDirty(): boolean {
59
+ return this._needsUpdate;
60
+ }
61
+
62
+ get isLayoutGroup(): boolean {
63
+ return true;
64
+ }
65
+
66
+ updateLayout() {
67
+ if (!this._rectTransform) return;
68
+ if (debug)
69
+ console.warn("Layout Update", this.context.time.frame, this.name);
70
+ this._needsUpdate = false;
71
+ this.onCalculateLayout(this._rectTransform);
72
+ }
73
+
74
+ // onBeforeRender(): void {
75
+ // this.updateLayout();
76
+ // }
77
+
78
+ @serializable()
79
+ childAlignment: TextAnchor = TextAnchor.UpperLeft;
80
+
81
+ @serializable()
4
82
  reverseArrangement: boolean = false;
83
+
84
+ @serializable()
85
+ spacing: number = 0;
86
+ @serializable(Padding)
87
+ padding!: Padding;
88
+
89
+ @serializable()
90
+ minWidth: number = 0;
91
+ @serializable()
92
+ minHeight: number = 0;
93
+
94
+ @serializable()
95
+ flexibleHeight: number = 0;
96
+ @serializable()
97
+ flexibleWidth: number = 0;
98
+
99
+ @serializable()
100
+ preferredHeight: number = 0;
101
+ @serializable()
102
+ preferredWidth: number = 0;
103
+
104
+ start() {
105
+ this._needsUpdate = true;
106
+ }
107
+
108
+ onEnable(): void {
109
+ if(debug) console.log(this.name, this);
110
+ this._rectTransform = this.gameObject.getComponent(RectTransform);
111
+ const canvas = this.gameObject.getComponentInParent(Canvas);
112
+ if (canvas) {
113
+ canvas.registerLayoutGroup(this);
114
+ }
115
+ this._needsUpdate = true;
116
+ }
117
+
118
+ onDisable(): void {
119
+ const canvas = this.gameObject.getComponentInParent(Canvas);
120
+ if (canvas) {
121
+ canvas.unregisterLayoutGroup(this);
122
+ }
123
+ }
124
+
125
+ protected abstract onCalculateLayout(rt: RectTransform);
126
+
127
+
128
+
129
+ // for animation:
130
+ private set m_Spacing(val) {
131
+ if (val === this.spacing) return;
132
+ this._needsUpdate = true;
133
+ this.spacing = val;
134
+ }
135
+ get m_Spacing() {
136
+ return this.spacing;
137
+ }
5
138
  }
6
139
 
7
- export class VerticalLayoutGroup extends LayoutGroup {
140
+ export abstract class HorizontalOrVerticalLayoutGroup extends LayoutGroup {
8
141
 
142
+ @serializable()
143
+ childControlHeight: boolean = true;
144
+ @serializable()
145
+ childControlWidth: boolean = true;
146
+ @serializable()
147
+ childForceExpandHeight: boolean = false;
148
+ @serializable()
149
+ childForceExpandWidth: boolean = false;
150
+ @serializable()
151
+ childScaleHeight: boolean = false;
152
+ @serializable()
153
+ childScaleWidth: boolean = false;
154
+
155
+ protected abstract get primaryAxis(): Axis;
156
+
157
+ protected onCalculateLayout(rect: RectTransform) {
158
+ const axis = this.primaryAxis;
159
+
160
+ const totalWidth = rect.width;
161
+ let actualWidth = totalWidth;
162
+ const totalHeight = rect.height;
163
+ let actualHeight = totalHeight;
164
+ actualWidth -= this.padding.horizontal;
165
+ actualHeight -= this.padding.vertical;
166
+
167
+ // console.log(rt.name, "width=" + totalWidth + ", height=" + totalHeight)
168
+
169
+ const paddingAxis = axis === Axis.Horizontal ? this.padding.horizontal : this.padding.vertical;
170
+ const isHorizontal = axis === Axis.Horizontal;
171
+ const isVertical = !isHorizontal;
172
+ const otherAxis = isHorizontal ? "y" : "x";
173
+ const controlSize = isHorizontal ? this.childControlWidth : this.childControlHeight;
174
+ const controlSizeOtherAxis = isHorizontal ? this.childControlHeight : this.childControlWidth;
175
+ const forceExpandSize = isHorizontal ? this.childForceExpandWidth : this.childForceExpandHeight;
176
+ const forceExpandSizeOtherAxis = isHorizontal ? this.childForceExpandHeight : this.childForceExpandWidth;
177
+ const actualExpandSize = isHorizontal ? actualHeight : actualWidth;
178
+ const totalSpace = isHorizontal ? totalWidth : totalHeight;
179
+ // 0 is left/top, 0.5 is middle, 1 is right/bottom
180
+ const alignmentOnAxis = 0.5 * (isHorizontal ? this.childAlignment % 3 : Math.floor(this.childAlignment / 3));
181
+
182
+ let start = 0;
183
+ if (isHorizontal) {
184
+ start += this.padding.left;
185
+ }
186
+ else
187
+ start += this.padding.top;
188
+
189
+
190
+ // Calculate total size of the elements
191
+ let totalChildSize = 0;
192
+ let actualRectTransformChildCount = 0;
193
+ for (let i = 0; i < this.gameObject.children.length; i++) {
194
+ const ch = this.gameObject.children[i];
195
+ const rt = GameObject.getComponent(ch, RectTransform);
196
+ if (rt?.activeAndEnabled) {
197
+ actualRectTransformChildCount += 1;
198
+ if (isHorizontal) {
199
+ totalChildSize += rt.width;
200
+ }
201
+ else {
202
+ totalChildSize += rt.height;
203
+ }
204
+ }
205
+ }
206
+
207
+ let sizePerChild = 0;
208
+ const totalSpacing = this.spacing * (actualRectTransformChildCount - 1)
209
+ if (forceExpandSize || controlSize) {
210
+ let size = 0;
211
+ if (isHorizontal) {
212
+ size = actualWidth -= totalSpacing;
213
+ }
214
+ else {
215
+ size = actualHeight -= totalSpacing;
216
+ }
217
+ if (actualRectTransformChildCount > 0)
218
+ sizePerChild = size / actualRectTransformChildCount;
219
+ }
220
+
221
+ let leftOffset = 0;
222
+ leftOffset += this.padding.left;
223
+ leftOffset -= this.padding.right;
224
+
225
+ if (alignmentOnAxis !== 0) {
226
+ start = totalSpace - totalChildSize;
227
+ start *= alignmentOnAxis;
228
+ start -= totalSpacing * alignmentOnAxis;
229
+ if (isHorizontal) {
230
+ start -= this.padding.right * alignmentOnAxis;
231
+ start += this.padding.left * (1 - alignmentOnAxis);
232
+ if (start < this.padding.left) {
233
+ start = this.padding.left;
234
+ }
235
+ }
236
+ else {
237
+ start -= this.padding.bottom * alignmentOnAxis;
238
+ start += this.padding.top * (1 - alignmentOnAxis);
239
+ if (start < this.padding.top) {
240
+ start = this.padding.top;
241
+ }
242
+ }
243
+ }
244
+
245
+ // Apply layout
246
+ let k = 0;
247
+ for (let i = 0; i < this.gameObject.children.length; i++) {
248
+ const ch = this.gameObject.children[i];
249
+ const rt = GameObject.getComponent(ch, RectTransform);
250
+ if (rt?.activeAndEnabled) {
251
+ rt.pivot?.set(.5, .5);
252
+ // Horizontal padding
253
+ const x = totalWidth * .5 + leftOffset * .5;
254
+ if (rt.anchoredPosition.x !== x)
255
+ rt.anchoredPosition.x = x;
256
+ const y = totalHeight * -.5
257
+ if (rt.anchoredPosition.y !== y)
258
+ rt.anchoredPosition.y = y;
259
+ // Set the size for the secondary axis (e.g. height for a horizontal layout group)
260
+ if (forceExpandSizeOtherAxis && controlSizeOtherAxis && rt.sizeDelta[otherAxis] !== actualExpandSize) {
261
+ rt.sizeDelta[otherAxis] = actualExpandSize;
262
+ }
263
+ // Set the size for the primary axis (e.g. width for a horizontal layout group)
264
+ if (forceExpandSize && controlSize && rt.sizeDelta[axis] !== sizePerChild) {
265
+ rt.sizeDelta[axis] = sizePerChild
266
+ }
267
+
268
+ const size = isHorizontal ? rt.width : rt.height;
269
+ let halfSize = size * .5;
270
+ start += halfSize;
271
+
272
+ if (forceExpandSize) {
273
+ let preferredStart = sizePerChild * (k + 1) - sizePerChild * .5;
274
+ if (preferredStart > start)
275
+ start = preferredStart;
276
+ }
277
+
278
+ let value = start;
279
+ if (axis === Axis.Vertical)
280
+ value = -value;
281
+ // Only set the position if it's not already the correct one to avoid triggering the rectTransform dirty event
282
+ if (rt.anchoredPosition[axis] !== value) {
283
+ rt.anchoredPosition[axis] = value
284
+ }
285
+
286
+ start += halfSize;
287
+ start += this.spacing;
288
+ k += 1;
289
+ }
290
+ }
291
+ }
292
+
293
+
9
294
  }
10
295
 
11
- export class HorizontalLayoutGroup extends LayoutGroup {
296
+ export class VerticalLayoutGroup extends HorizontalOrVerticalLayoutGroup {
12
297
 
298
+ protected get primaryAxis() {
299
+ return Axis.Vertical;
300
+ }
301
+
13
302
  }
14
303
 
304
+ export class HorizontalLayoutGroup extends HorizontalOrVerticalLayoutGroup {
305
+
306
+ protected get primaryAxis() {
307
+ return Axis.Horizontal;
308
+ }
309
+
310
+ }
311
+
15
312
  export class GridLayoutGroup extends LayoutGroup {
313
+ protected onCalculateLayout() {
314
+ }
16
315
 
17
316
  }
src/engine-components/utils/LookAt.ts CHANGED
@@ -1,21 +1,74 @@
1
1
  import { serializable } from "../../engine/engine_serialization";
2
2
  import { Behaviour } from "../Component";
3
- import { Object3D } from "three";
4
- import { getWorldPosition, lookAtInverse } from "../../engine/engine_three_utils";
3
+ import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
4
+ import { getWorldPosition, getWorldQuaternion, setWorldQuaternion } from "../../engine/engine_three_utils";
5
5
 
6
- export class LookAt extends Behaviour {
6
+ import { USDObject } from "../../engine-components/export/usdz/ThreeUSDZExporter";
7
+ import { UsdzBehaviour } from "../../engine-components/export/usdz/extensions/behavior/Behaviour";
8
+ import { ActionBuilder, BehaviorModel, TriggerBuilder, USDVec3 } from "../../engine-components/export/usdz/extensions/behavior/BehavioursBuilder";
7
9
 
10
+ export class LookAt extends Behaviour implements UsdzBehaviour {
11
+
8
12
  @serializable(Object3D)
9
13
  target?: Object3D;
10
14
 
11
15
  @serializable()
12
16
  invertForward: boolean = false;
13
17
 
18
+ @serializable()
19
+ keepUpDirection: boolean = true;
20
+
21
+ @serializable()
22
+ copyTargetRotation: boolean = false;
23
+
24
+ private static flipYQuat: Quaternion = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
25
+
14
26
  onBeforeRender(): void {
15
- if (!this.target) return;
16
- if (!this.invertForward)
17
- this.gameObject.lookAt(getWorldPosition(this.target!));
27
+ let target: Object3D | null | undefined = this.target;
28
+ if (!target) target = this.context.mainCamera;
29
+ if (!target) return;
30
+
31
+ const lookTarget = getWorldPosition(target);
32
+ const lookFrom = getWorldPosition(this.gameObject);
33
+
34
+ if (this.keepUpDirection)
35
+ lookTarget.y = lookFrom.y;
36
+
37
+ if (this.copyTargetRotation)
38
+ setWorldQuaternion(this.gameObject, getWorldQuaternion(target));
18
39
  else
19
- lookAtInverse(this.gameObject, getWorldPosition(this.target!));
40
+ this.gameObject.lookAt(lookTarget);
41
+
42
+ if (this.invertForward)
43
+ this.gameObject.quaternion.multiply(LookAt.flipYQuat);
20
44
  }
45
+
46
+ createBehaviours(ext, model: USDObject, _context) {
47
+ if (model.uuid === this.gameObject.uuid) {
48
+ let alignmentTarget = model;
49
+
50
+ // not entirely sure why we need to do this - looks like LookAt with up vector doesn't work properly in
51
+ // QuickLook, so we need to introduce an empty parent and rotate the model by 90° around Y
52
+ if (this.keepUpDirection) {
53
+ const parent = USDObject.createEmptyParent(model);
54
+ alignmentTarget = parent;
55
+
56
+ // rotate by 90° - counter-rotation on the parent makes sure
57
+ // that without Preliminary Behaviours it still looks right
58
+ parent.matrix.multiply(new Matrix4().makeRotationZ(Math.PI / 2));
59
+ model.matrix.multiply(new Matrix4().makeRotationZ(-Math.PI / 2));
60
+ }
61
+
62
+ const lookAt = new BehaviorModel("lookat " + this.name,
63
+ TriggerBuilder.sceneStartTrigger(),
64
+ ActionBuilder.lookAtCameraAction(
65
+ alignmentTarget,
66
+ undefined,
67
+ this.invertForward ? USDVec3.back : USDVec3.forward,
68
+ this.keepUpDirection ? USDVec3.up : USDVec3.zero
69
+ ),
70
+ );
71
+ ext.addBehavior(lookAt);
72
+ }
73
+ }
21
74
  }
src/engine-components/postprocessing/PostProcessingHandler.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { HalfFloatType, sRGBEncoding, WebGLRenderTarget } from "three";
1
+ import { HalfFloatType } from "three";
2
2
  import { Context } from "../../engine/engine_setup";
3
3
  import { getParam, isMobileDevice } from "../../engine/engine_utils";
4
4
  import { BloomEffect, BrightnessContrastEffect, ChromaticAberrationEffect, DepthDownsamplingPass, DepthOfFieldEffect, Effect, EffectComposer, EffectPass, HueSaturationEffect, NormalPass, Pass, PixelationEffect, RenderPass, SelectiveBloomEffect, SSAOEffect, VignetteEffect } from "postprocessing";
src/engine-components/ui/RectTransform.ts CHANGED
@@ -7,10 +7,11 @@
7
7
  import { getParam } from "../../engine/engine_utils";
8
8
  import { onChange } from "./Utils";
9
9
  import { foreachComponentEnumerator } from "../../engine/engine_gameobject";
10
- import { ICanvas, IRectTransform, IRectTransformChangedReceiver } from "./Interfaces";
10
+ import { ICanvas, IRectTransform, IRectTransformChangedReceiver, ILayoutGroup } from "./Interfaces";
11
11
  import { GameObject } from '../Component';
12
12
 
13
13
  const debug = getParam("debugui");
14
+ const debugLayout = getParam("debuguilayout");
14
15
 
15
16
  export class Size {
16
17
  width!: number;
@@ -30,7 +31,7 @@
30
31
 
31
32
  export class RectTransform extends BaseUIComponent implements IRectTransform, IRectTransformChangedReceiver {
32
33
 
33
- offset: number = 0.05;
34
+ offset: number = 0.1;
34
35
 
35
36
  // @serializable(Object3D)
36
37
  // root? : Object3D;
@@ -94,14 +95,13 @@
94
95
  return this.sizeDelta.y;
95
96
  }
96
97
 
97
- private lastMatrixWorld!: Matrix4;
98
+ // private lastMatrixWorld!: Matrix4;
98
99
  private lastMatrix!: Matrix4;
99
100
  private rectBlock!: Object3D;
100
101
  private _transformNeedsUpdate: boolean = false;
101
102
 
102
103
  awake() {
103
104
  super.awake();
104
- this.lastMatrixWorld = new Matrix4();
105
105
  this.lastMatrix = new Matrix4();
106
106
  this.rectBlock = new Object3D();;
107
107
  this.rectBlock.position.z = .1;
@@ -112,9 +112,10 @@
112
112
 
113
113
  // TODO: we need to replace this with the watch that e.g. Rigibody is using (or the one in utils?)
114
114
  // perhaps we can also just manually check the few properties in the update loops?
115
- onChange(this, "_anchoredPosition", () => { this._transformNeedsUpdate = true; });
116
- onChange(this, "sizeDelta", () => { this._transformNeedsUpdate = true; });
117
- onChange(this, "pivot", () => { this._transformNeedsUpdate = true; });
115
+ // TODO: check if value actually changed, this is called on assignment
116
+ onChange(this, "_anchoredPosition", () => { this.markDirty(); });
117
+ onChange(this, "sizeDelta", () => { this.markDirty(); });
118
+ onChange(this, "pivot", () => { this.markDirty(); });
118
119
 
119
120
  // When exported with an anchored position offset we remove it here
120
121
  // because it would otherwise be applied twice when the anchoring is animated
@@ -135,29 +136,68 @@
135
136
  super.onEnable();
136
137
  this.addShadowComponent(this.rectBlock);
137
138
  this._transformNeedsUpdate = true;
139
+ this.Canvas?.registerTransform(this);
138
140
  }
139
141
 
140
142
  onDisable() {
141
143
  super.onDisable();
142
144
  this.removeShadowComponent();
145
+ this.Canvas?.unregisterTransform(this);
143
146
  }
144
147
 
145
- onParentRectTransformChanged(_comp: IRectTransform) {
148
+ onParentRectTransformChanged(comp: IRectTransform) {
149
+ if (this._transformNeedsUpdate) return;
146
150
  // When the parent rect transform changes we have to to recalculate our transform
151
+ this.onApplyTransform(debugLayout ? `${comp.name} changed` : undefined);
152
+ }
153
+
154
+ get isDirty() {
155
+ if(!this._transformNeedsUpdate) this._transformNeedsUpdate = !this.lastMatrix.equals(this.gameObject.matrix);
156
+ return this._transformNeedsUpdate;
157
+ }
158
+
159
+ // private _copyMatrixAfterRender: boolean = false;
160
+
161
+ markDirty() {
162
+ if (this._transformNeedsUpdate) return;
163
+ if (debugLayout) console.warn("RectTransform markDirty()", this.name)
147
164
  this._transformNeedsUpdate = true;
148
- this.applyTransform();
165
+ // If mark dirty is called explictly we want to allow updating the transform again when updateTransform is called
166
+ // if we dont reset it here we get delayed layout updates
167
+ this._lastUpdateFrame = -1;
149
168
  }
150
169
 
170
+
171
+ /** Will update the transforms if it changed or is dirty */
172
+ updateTransform() {
173
+ // TODO: instead of checking matrix again it would perhaps be better to test if position, rotation or scale have changed individually?
174
+ const transformChanged = this._transformNeedsUpdate || !this.lastMatrix.equals(this.gameObject.matrix);// || !this.lastMatrixWorld.equals(this.gameObject.matrixWorld);
175
+ if (transformChanged && this.canUpdate()) {
176
+ this.onApplyTransform(this._transformNeedsUpdate ? "Marked dirty" : "Matrix changed");
177
+ }
178
+ }
179
+
151
180
  private _parentRectTransform?: RectTransform;
181
+ private _lastUpdateFrame: number = -1;
152
182
 
153
- private applyTransform() {
183
+ private canUpdate() {
184
+ return this._transformNeedsUpdate && this.activeAndEnabled && this._lastUpdateFrame !== this.context.time.frame;
185
+ }
186
+
187
+ private onApplyTransform(reason?: string) {
188
+ // TODO: need to improve the update logic, with this UI updates have some frame delay but dont happen exponentially per hierarchy
189
+ if (this.context.time.frameCount === this._lastUpdateFrame) return;
190
+ this._lastUpdateFrame = this.context.time.frameCount;
191
+
154
192
  const uiobject = this.shadowComponent;
155
193
  if (!uiobject) return;
156
- this._transformNeedsUpdate = false;
157
194
  this._parentRectTransform = GameObject.getComponentInParent(this.gameObject.parent!, RectTransform) as RectTransform;
158
195
 
159
- if (debug) console.log("RectTransform ApplyTransform", this.name, this.isRoot());
196
+ this._transformNeedsUpdate = false;
197
+ this.lastMatrix.copy(this.gameObject.matrix);
160
198
 
199
+ if (debugLayout) console.warn("RectTransform → ApplyTransform", this.name + " because " + reason);
200
+
161
201
  if (!this.isRoot()) {
162
202
  // Reset temp matrix
163
203
  uiobject.matrix.identity();
@@ -183,8 +223,7 @@
183
223
  tempMatrix.setPosition(tempVec.x, tempVec.y, tempVec.z);
184
224
  uiobject.matrix.premultiply(tempMatrix);
185
225
  // apply scale if necessary
186
- if (this.gameObject.scale.x || this.gameObject.scale.y || this.gameObject.scale.z)
187
- uiobject.matrix.scale(this.gameObject.scale);
226
+ uiobject.matrix.scale(this.gameObject.scale);
188
227
  }
189
228
  else {
190
229
  // We have to rotate the canvas when it's in worldspace
@@ -192,43 +231,29 @@
192
231
  if (!canvas.screenspace) uiobject.rotation.y = Math.PI;
193
232
  }
194
233
 
195
- this._copyMatrixAfterRender = true;
196
- this.lastMatrix.copy(this.gameObject.matrix);
197
-
198
234
  // iterate other components on this object that might need to know about the transform change
199
235
  // e.g. Graphic components should update their width and height
200
236
  const includeChildren = true;
201
- for (const comp of foreachComponentEnumerator(this.gameObject, BaseUIComponent, includeChildren)) {
237
+ for (const comp of foreachComponentEnumerator(this.gameObject, BaseUIComponent, includeChildren, 1)) {
202
238
  if (comp === this) continue;
239
+ if (!comp.activeAndEnabled) continue;
203
240
  const callback = comp as any as IRectTransformChangedReceiver;
204
- if (callback.onParentRectTransformChanged)
241
+ if (callback.onParentRectTransformChanged) {
242
+ // if (debugLayout) console.log(`RectTransform ${this.name} → call`, comp.name + "/" + comp.constructor.name)
205
243
  callback.onParentRectTransformChanged(this);
244
+ }
206
245
  }
207
- }
208
246
 
209
- private _copyMatrixAfterRender: boolean = false;
210
-
211
- markDirty() {
212
- this._transformNeedsUpdate = true;
247
+ // const layout = GameObject.getComponentInParent(this.gameObject, ILayoutGroup);
213
248
  }
214
249
 
250
+ // onAfterRender() {
251
+ // if (this._copyMatrixAfterRender) {
252
+ // // can we only have this event when the transform changed in this frame? Otherwise all RectTransforms will be iterated. Not sure what is better
253
+ // this.lastMatrixWorld.copy(this.gameObject.matrixWorld);
254
+ // }
255
+ // }
215
256
 
216
- onBeforeRender() {
217
- // TODO: instead of checking matrix again it would perhaps be better to test if position, rotation or scale have changed individually?
218
- const transformChanged = this.gameObject.matrixWorldNeedsUpdate || this._transformNeedsUpdate || !this.lastMatrixWorld.equals(this.gameObject.matrixWorld) || !this.lastMatrix.equals(this.gameObject.matrix);
219
- if (transformChanged)
220
- {
221
- this.applyTransform();
222
- }
223
- }
224
-
225
- onAfterRender() {
226
- if (this._copyMatrixAfterRender) {
227
- // can we only have this event when the transform changed in this frame? Otherwise all RectTransforms will be iterated. Not sure what is better
228
- this.lastMatrixWorld.copy(this.gameObject.matrixWorld);
229
- }
230
- }
231
-
232
257
  /** applies the position offset to the passed in vector */
233
258
  private applyAnchoring(pos: Vector3) {
234
259
  pos.x += this.anchoredPosition.x;
src/engine-components/export/usdz/types.ts DELETED
@@ -1,39 +0,0 @@
1
- import { Object3D, Matrix4, Material, BufferGeometry } from "three";
2
-
3
- // keep in sync with USDZExporter.js
4
-
5
- /** implementation is in three */
6
- export declare class USDZDocument {
7
- name: string;
8
- get isDocumentRoot(): boolean;
9
- add(obj: USDZObject);
10
- remove(obj: USDZObject);
11
- traverse(callback: (obj: USDZObject) => void);
12
- findById(uuid: string): USDZObject | undefined;
13
- get isDynamic(): boolean;
14
- }
15
-
16
-
17
-
18
- /** implementation is in three */
19
- export declare class USDZObject {
20
- static createEmptyParent(parent: USDZObject);
21
- uuid: string;
22
- name: string;
23
- matrix: Matrix4;
24
- material: Material;
25
- geometry: BufferGeometry;
26
- parent: USDZObject | USDZDocument | null;
27
- children: USDZObject[];
28
- _eventListeners: { [event: string]: Function[] };
29
- isDynamic: boolean;
30
-
31
- is(obj: Object3D): boolean;
32
- isEmpty(): boolean;
33
- clone();
34
- getPath();
35
- add(child: USDZObject);
36
- remove(child: USDZObject);
37
- addEventListener(evt: string, listener: Function);
38
- removeEventListener(evt: string, listener: Function);
39
- }
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -1,15 +1,15 @@
1
1
  import { delay, getParam, isiOS, isMobileDevice, isSafari } from "../../../engine/engine_utils";
2
- import { Object3D, Color, Mesh, Matrix4 } from "three";
3
- import { USDZExporter as ThreeUSDZExporter } from "three/examples/jsm/exporters/USDZExporter";
2
+ import { Object3D, Mesh, Matrix4 } from "three";
3
+ import { USDZExporter as ThreeUSDZExporter } from "./ThreeUSDZExporter";
4
4
  import { AnimationExtension } from "./extensions/Animation"
5
5
  import { ensureQuicklookLinkIsCreated } from "./utils/quicklook";
6
6
  import { getFormattedDate } from "./utils/timeutils";
7
7
  import { registerAnimatorsImplictly } from "./utils/animationutils";
8
- import { IUSDZExporterExtension } from "./Extension";
8
+ import { IUSDExporterExtension } from "./Extension";
9
9
  import { Behaviour, GameObject } from "../../Component";
10
10
  import { WebXR } from "../../webxr/WebXR"
11
11
  import { serializable } from "../../../engine/engine_serialization";
12
- import { isDevEnvironment, showBalloonMessage, showBalloonWarning } from "../../../engine/debug/debug";
12
+ import { showBalloonMessage, showBalloonWarning } from "../../../engine/debug/debug";
13
13
  import { Context } from "../../../engine/engine_setup";
14
14
  import { WebARSessionRoot } from "../../webxr/WebARSessionRoot";
15
15
  import { hasProLicense } from "../../../engine/engine_license";
@@ -32,7 +32,7 @@
32
32
  export class USDZExporter extends Behaviour {
33
33
 
34
34
  @serializable(Object3D)
35
- objectToExport?: THREE.Object3D;
35
+ objectToExport?: Object3D;
36
36
 
37
37
  @serializable()
38
38
  autoExportAnimations: boolean = false;
@@ -43,13 +43,20 @@
43
43
  @serializable(QuickLookOverlay)
44
44
  overlay?: QuickLookOverlay;
45
45
 
46
- extensions: IUSDZExporterExtension[] = [];
46
+ // Currently not exposed to integrations - not fully tested. Set from code (e.g. image tracking)
47
+ @serializable()
48
+ anchoringType: "plane" | "image" | "face" | "none" = "plane";
47
49
 
50
+ // Currently not exposed to integrations - not fully tested. Set from code (e.g. image tracking)
51
+ @serializable()
52
+ planeAnchoringAlignment: "horizontal" | "vertical" | "any" = "horizontal";
53
+
54
+ extensions: IUSDExporterExtension[] = [];
55
+
48
56
  private link!: HTMLAnchorElement;
49
57
  private webxr?: WebXR;
50
58
  private webARSessionRoot: WebARSessionRoot | undefined;
51
59
 
52
-
53
60
  start() {
54
61
  if (debug) {
55
62
  console.log(this);
@@ -72,13 +79,11 @@
72
79
  this.exportAsync();
73
80
  });
74
81
 
75
- if (!this.objectToExport) this.objectToExport = this.gameObject;
76
-
77
-
78
- if (isDevEnvironment() && (!this.objectToExport?.children?.length && !(this.objectToExport as Mesh)?.isMesh)) {
79
- showBalloonWarning("USDZ Exporter has nothing to export");
80
- console.warn("USDZExporter has no objects to export assigned:", this)
81
- }
82
+ // fall back to this object or to the scene if it's empty and doesn't have a mesh
83
+ if (!this.objectToExport)
84
+ this.objectToExport = this.gameObject;
85
+ if (!this.objectToExport?.children?.length && !(this.objectToExport as Mesh)?.isMesh)
86
+ this.objectToExport = this.context.scene;
82
87
  }
83
88
 
84
89
 
@@ -135,13 +140,30 @@
135
140
  this.dispatchEvent(new CustomEvent("before-export", { detail: eventArgs }))
136
141
 
137
142
  let name = this.exportFileName ?? this.objectToExport?.name ?? this.name;
138
- if (debug) name += "-" + getFormattedDate();
139
- else if (!hasProLicense()) name = name + " - Made with Needle";
143
+ if (!hasProLicense()) name += "-MadeWithNeedle";
144
+ name += "-" + getFormattedDate(); // seems iOS caches the file in some cases, this ensures we always have a fresh file
140
145
 
141
146
  //@ts-ignore
142
147
  exporter.debug = debug;
148
+
149
+ // sanitize anchoring types
150
+ if (this.anchoringType !== "plane" && this.anchoringType !== "none" && this.anchoringType !== "image" && this.anchoringType !== "face")
151
+ this.anchoringType = "plane";
152
+ if (this.planeAnchoringAlignment !== "horizontal" && this.planeAnchoringAlignment !== "vertical" && this.planeAnchoringAlignment !== "any")
153
+ this.planeAnchoringAlignment = "horizontal";
154
+
143
155
  //@ts-ignore
144
- const arraybuffer = await exporter.parse(this.objectToExport, extensions);
156
+ const arraybuffer = await exporter.parse(this.objectToExport, {
157
+ ar: {
158
+ anchoring: {
159
+ type: this.anchoringType,
160
+ }
161
+ },
162
+ planeAnchoring: {
163
+ alignment: this.planeAnchoringAlignment,
164
+ },
165
+ extensions: extensions
166
+ });
145
167
  const blob = new Blob([arraybuffer], { type: 'application/octet-stream' });
146
168
 
147
169
  this.dispatchEvent(new CustomEvent("after-export", { detail: eventArgs }))
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -1,10 +1,14 @@
1
- import { WebXR } from "./WebXR";
1
+ import { WebXR, WebXREvent } from "./WebXR";
2
2
  import { serializable } from "../../engine/engine_serialization";
3
3
  import { Behaviour, GameObject } from "../Component";
4
4
  import { Object3D, Quaternion, Vector3 } from "three";
5
5
  import { CircularBuffer, getParam } from "../../engine/engine_utils";
6
6
  import { AssetReference } from "../../engine/engine_addressables";
7
+ import { showBalloonWarning } from "../../engine/debug";
7
8
 
9
+ import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter";
10
+ import { USDZExporterContext, USDWriter, imageToCanvas } from "../../engine-components/export/usdz/ThreeUSDZExporter";
11
+
8
12
  // https://github.com/immersive-web/marker-tracking/blob/main/explainer.md
9
13
 
10
14
  const debug = getParam("debugimagetracking");
@@ -18,16 +22,6 @@
18
22
  readonly measuredSize: number;
19
23
  readonly state: "tracked" | "emulated";
20
24
 
21
- // private _matrix: Matrix4 | null = null;
22
- // private get matrix(): Matrix4 {
23
- // if (!this._matrix) {
24
- // // this._matrix = WebXRTrackedImage._matrixBuffer.get();
25
- // // const matrix = this._pose.transform.matrix;
26
- // // this._matrix.fromArray(matrix);
27
- // }
28
- // return this._matrix!;
29
- // }
30
-
31
25
  /** Copy the image position to a vector */
32
26
  getPosition(vec: Vector3) {
33
27
  this.ensureTransformData();
@@ -42,13 +36,23 @@
42
36
  return quat;
43
37
  }
44
38
 
45
- applyToObject(object: Object3D) {
39
+ private static y180 = new Quaternion().setFromAxisAngle(new Vector3(0, 1, 0), Math.PI);
40
+ applyToObject(object: Object3D, t01: number | undefined = undefined) {
46
41
  this.ensureTransformData();
47
- object.position.copy(this._position);
48
- object.quaternion.copy(this._rotation);
42
+ // check if position/_position or rotation/_rotation changed more than just a little bit
43
+ const haveChanged = object.position.distanceToSquared(this._position) > 0.05 || object.quaternion.angleTo(this._rotation) > 0.05;
44
+ if (t01 === undefined || t01 >= 1 || haveChanged) {
45
+ object.position.copy(this._position);
46
+ object.quaternion.copy(this._rotation);
47
+ }
48
+ else {
49
+ t01 = Math.max(0, Math.min(1, t01));
50
+ object.position.lerp(this._position, t01);
51
+ object.quaternion.slerp(this._rotation, t01);
52
+ }
53
+ object.quaternion.multiply(WebXRTrackedImage.y180);
49
54
  }
50
55
 
51
- // private static _matrixBuffer: CircularBuffer<Matrix4> = new CircularBuffer(() => new Matrix4(), 20);
52
56
  private static _positionBuffer: CircularBuffer<Vector3> = new CircularBuffer(() => new Vector3(), 20);
53
57
  private static _rotationBuffer: CircularBuffer<Quaternion> = new CircularBuffer(() => new Quaternion(), 20);
54
58
  private _position!: Vector3;
@@ -58,8 +62,14 @@
58
62
  this._position = WebXRTrackedImage._positionBuffer.get();
59
63
  this._rotation = WebXRTrackedImage._rotationBuffer.get();
60
64
  const t = this._pose.transform;
61
- this._position.set(-t.position.x, t.position.y, -t.position.z);
62
- this._rotation.set(-t.orientation.x, t.orientation.y, -t.orientation.z, t.orientation.w);
65
+
66
+ // when parented to the world, we need to flip data here
67
+ //this._position.set(-t.position.x, t.position.y, -t.position.z);
68
+ // this._rotation.set(-t.orientation.x, t.orientation.y, -t.orientation.z, t.orientation.w);
69
+
70
+ // for some reason when parented to the XRRig, we need the original data
71
+ this._position.set(t.position.x, t.position.y, t.position.z);
72
+ this._rotation.set(t.orientation.x, t.orientation.y, t.orientation.z, t.orientation.w);
63
73
  }
64
74
  }
65
75
 
@@ -99,6 +109,32 @@
99
109
  imageDoesNotMove: boolean = false;
100
110
  }
101
111
 
112
+ class ImageTrackingExtension {
113
+
114
+ get extensionName() { return "image-tracking"; }
115
+
116
+ private filename: string;
117
+ private widthInMeters: number;
118
+ private imageData: Uint8Array;
119
+
120
+ constructor(filename: string, imageData: Uint8Array, widthInMeters: number) {
121
+ this.filename = filename;
122
+ this.imageData = imageData;
123
+ this.widthInMeters = widthInMeters;
124
+ }
125
+
126
+ onAfterHierarchy(_context: USDZExporterContext, writer: USDWriter) {
127
+ writer.beginBlock(`def Preliminary_ReferenceImage "AnchoringReferenceImage"`);
128
+ writer.appendLine(`uniform asset image = @tracker/` + this.filename + `@`);
129
+ writer.appendLine(`uniform double physicalWidth = ` + (this.widthInMeters * 100).toFixed(8));
130
+ writer.closeBlock();
131
+ }
132
+
133
+ onAfterSerialize(context: USDZExporterContext) {
134
+ context.files['tracker/' + this.filename] = this.imageData;
135
+ }
136
+ }
137
+
102
138
  export class WebXRImageTracking extends Behaviour {
103
139
 
104
140
  @serializable(WebXRImageTrackingModel)
@@ -121,22 +157,43 @@
121
157
  imageElement.addEventListener("load", async () => {
122
158
  const img = await createImageBitmap(imageElement);
123
159
  WebXRImageTracking._imageElements.set(url, img);
160
+
161
+ // read back Uint8Array to use in USDZ -
162
+ // TODO better would be to do that once we actually need it
163
+ const canvas = await imageToCanvas( img );
164
+ if (canvas) {
165
+ const blob = await new Promise( resolve => canvas.toBlob( resolve, 'image/png', 1 ) ) as any;
166
+ const arrayBuffer = await blob.arrayBuffer();
167
+
168
+ const exporter = GameObject.findObjectOfType(USDZExporter);
169
+ if (exporter) {
170
+ exporter.extensions.push(
171
+ new ImageTrackingExtension("marker.png", new Uint8Array(arrayBuffer), this.trackedImages[0].widthInMeters)
172
+ );
173
+ exporter.anchoringType = "image";
174
+ }
175
+ }
124
176
  });
125
177
  }
126
178
  }
127
179
  }
128
180
  }
129
181
 
182
+ private xr: WebXR | null = null;
183
+
130
184
  onEnable(): void {
131
- WebXR.addEventListener("modify-ar-options", this.onModifyAROptions);
132
- WebXR.addEventListener("xrStarted", this.onXRStarted);
133
- this.addEventListener("image-tracking", this.onImageTrackingUpdate)
185
+ this.xr = GameObject.findObjectOfType(WebXR);
186
+ WebXR.addEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
187
+ WebXR.addEventListener(WebXREvent.XRStarted, this.onXRStarted);
188
+ WebXR.addEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
189
+ this.addEventListener("image-tracking", this.onImageTrackingUpdate);
134
190
  }
135
191
 
136
192
  onDisable(): void {
137
- WebXR.removeEventListener("modify-ar-options", this.onModifyAROptions);
138
- WebXR.removeEventListener("xrStarted", this.onXRStarted);
139
- this.removeEventListener("image-tracking", this.onImageTrackingUpdate)
193
+ WebXR.removeEventListener(WebXREvent.ModifyAROptions, this.onModifyAROptions);
194
+ WebXR.removeEventListener(WebXREvent.XRStarted, this.onXRStarted);
195
+ WebXR.removeEventListener(WebXREvent.XRUpdate, this.onXRUpdate);
196
+ this.removeEventListener("image-tracking", this.onImageTrackingUpdate);
140
197
  }
141
198
 
142
199
  private onModifyAROptions = (event: any) => {
@@ -197,8 +254,12 @@
197
254
 
198
255
  if (asset) {
199
256
  trackedData!.object = asset;
200
- if (asset !== this.gameObject)
201
- this.gameObject.add(asset);
257
+
258
+ // make sure to parent to the WebXR.rig
259
+ if (this.xr) {
260
+ this.xr.Rig.add(asset);
261
+ }
262
+
202
263
  image.applyToObject(asset);
203
264
  if (!asset.activeSelf)
204
265
  GameObject.setActive(asset, true);
@@ -215,6 +276,10 @@
215
276
 
216
277
  if (!trackedData.object) continue;
217
278
 
279
+ if (this.xr) {
280
+ this.xr.Rig.add(trackedData.object);
281
+ }
282
+
218
283
  image.applyToObject(trackedData.object);
219
284
  if (!trackedData.object.activeSelf)
220
285
  GameObject.setActive(trackedData.object, true);
@@ -229,9 +294,17 @@
229
294
  }
230
295
  };
231
296
 
232
- onBeforeRender(frame: XRFrame | null): void {
297
+ private onXRUpdate = (evt): void => {
298
+ const frame = evt.frame;
299
+ if (!frame) return;
300
+
301
+ if (frame.session && !("getImageTrackingResults" in frame)) {
302
+ showBalloonWarning("Image tracking is currently not supported on this device. On Chrome for Android, you can enable the <a href=\"chrome://flags/#webxr-incubations\">chrome://flags/#webxr-incubations</a> flag.");
303
+ return;
304
+ }
305
+
233
306
  //@ts-ignore
234
- if (frame?.session && typeof frame.getImageTrackingResults === "function") {
307
+ if (frame.session && typeof frame.getImageTrackingResults === "function") {
235
308
  //@ts-ignore
236
309
  const results = frame.getImageTrackingResults();
237
310
  if (results.length) {
src/engine-components/export/usdz/extensions/behavior/Actions.ts ADDED
@@ -0,0 +1,99 @@
1
+ import { Object3D, Matrix4, Material, BufferGeometry } from "three";
2
+ import { ActionBuilder, ActionModel } from "./BehavioursBuilder";
3
+ import { USDObject, USDDocument } from "../../ThreeUSDZExporter";
4
+
5
+ export abstract class DocumentAction {
6
+
7
+ get id(): string { return this.object.uuid; }
8
+
9
+ protected object: Object3D;
10
+ protected model?: USDObject;
11
+
12
+ constructor(obj: Object3D) {
13
+ this.object = obj;
14
+ }
15
+
16
+ apply(document: USDDocument) {
17
+ if (!this.model) {
18
+ this.model = document.findById(this.object.uuid);
19
+ if (!this.model) {
20
+ console.error("could not find model with id " + this.object.uuid);
21
+ return;
22
+ }
23
+ }
24
+ this.onApply(document);
25
+ }
26
+
27
+ protected abstract onApply(document: USDDocument);
28
+ }
29
+
30
+ export class VariantAction extends DocumentAction {
31
+ constructor(obj: Object3D, matrix?: Matrix4, material?: Material, geometry?: BufferGeometry) {
32
+ super(obj);
33
+ this.matrix = matrix;
34
+ this.material = material;
35
+ this.geometry = geometry;
36
+ }
37
+
38
+ private matrix: Matrix4 | undefined;
39
+ private material: Material | undefined;
40
+ private geometry: BufferGeometry | undefined;
41
+
42
+ protected onApply(_: USDDocument) {
43
+ const model = this.model;
44
+ if (!model) return;
45
+ if (!model.parent?.isDynamic) {
46
+ USDObject.createEmptyParent(model);
47
+ }
48
+ const clone = model.clone();
49
+ if (this.matrix) clone.matrix = this.matrix;
50
+ if (this.material) clone.material = this.material;
51
+ if (this.geometry) clone.geometry = this.geometry;
52
+ model.parent?.add(clone);
53
+ }
54
+
55
+ private _enableAction?: ActionModel;
56
+ private _disableAction?: ActionModel;
57
+
58
+ enable(): ActionModel {
59
+ if (this._enableAction) return this._enableAction;
60
+ this._enableAction = ActionBuilder.fadeAction(this.object, 0, true);;
61
+ return this._enableAction;
62
+ }
63
+
64
+ disable(): ActionModel {
65
+ if (this._disableAction) return this._disableAction;
66
+ this._disableAction = ActionBuilder.fadeAction(this.object, 0, false);;
67
+ return this._disableAction;
68
+ }
69
+ }
70
+
71
+ export class ActionCollection {
72
+
73
+ private actions: DocumentAction[];
74
+ private sortedActions?: { [key: string]: DocumentAction[] };
75
+
76
+ constructor(actions: DocumentAction[]) {
77
+ this.actions = [...actions]
78
+ }
79
+
80
+ // organize is called once when getting an action for the first time
81
+ // the sorted actions are baked then and adding new actions will not be added anymore
82
+ private organize() {
83
+ this.sortedActions = {};
84
+ for (const action of this.actions) {
85
+ const id = action.id;
86
+ if (!this.sortedActions[id]) {
87
+ this.sortedActions[id] = [];
88
+ }
89
+ this.sortedActions[id].push(action);
90
+ }
91
+ }
92
+
93
+ /** returns all document actions affecting the object passed in */
94
+ getActions(obj: Object3D): DocumentAction[] | null {
95
+ if (!this.sortedActions) this.organize();
96
+ return this.sortedActions![obj.uuid];
97
+ }
98
+
99
+ }
src/engine-components/export/usdz/extensions/behavior/Behaviour.ts ADDED
@@ -0,0 +1,181 @@
1
+ import { Behaviour, GameObject } from "../../../../Component";
2
+ import { USDZExporter } from "../../USDZExporter";
3
+ import { IUSDExporterExtension } from "../../Extension";
4
+ import { USDObject, USDWriter } from "../../ThreeUSDZExporter";
5
+ import { BehaviorModel } from "./BehavioursBuilder";
6
+ import { IContext } from "../../../../../engine/engine_types";
7
+
8
+ export interface UsdzBehaviour {
9
+ createBehaviours?(ext: BehaviorExtension, model: USDObject, context: IContext): void;
10
+ beforeCreateDocument?(ext: BehaviorExtension, context: IContext): void;
11
+ afterCreateDocument?(ext: BehaviorExtension, context: IContext): void;
12
+ }
13
+
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
+ export class BehaviorExtension implements IUSDExporterExtension {
24
+
25
+ get extensionName(): string {
26
+ return "Behaviour";
27
+ }
28
+
29
+ private behaviours: BehaviorModel[] = [];
30
+
31
+ addBehavior(beh: BehaviorModel) {
32
+ this.behaviours.push(beh);
33
+ }
34
+
35
+ behaviourComponents: Array<UsdzBehaviour> = [];
36
+
37
+
38
+ onBeforeBuildDocument(context) {
39
+ context.root.traverse(e => {
40
+ GameObject.foreachComponent(e, (comp) => {
41
+ const c = comp as unknown as UsdzBehaviour;
42
+ if (
43
+ typeof c.createBehaviours === "function" ||
44
+ typeof c.beforeCreateDocument === "function" ||
45
+ typeof c.afterCreateDocument === "function"
46
+ ) {
47
+ this.behaviourComponents.push(c);
48
+ c.beforeCreateDocument?.call(c, this, context);
49
+ }
50
+ }, false);
51
+ });
52
+ }
53
+
54
+ onExportObject(_object, model: USDObject, context) {
55
+
56
+ for (const beh of this.behaviourComponents) {
57
+ beh.createBehaviours?.call(beh, this, model, context);
58
+ }
59
+ }
60
+
61
+ onAfterBuildDocument(context) {
62
+ for (const beh of this.behaviourComponents) {
63
+ if (typeof beh.afterCreateDocument === "function")
64
+ beh.afterCreateDocument(this, context);
65
+ }
66
+ this.behaviourComponents.length = 0;
67
+ }
68
+
69
+ onAfterHierarchy(context, writer : USDWriter) {
70
+ if (this.behaviours?.length) {
71
+
72
+ // this.combineBehavioursWithSameTapActions();
73
+
74
+ writer.beginBlock('def Scope "Behaviors"');
75
+
76
+ for (const beh of this.behaviours)
77
+ beh.writeTo(this, context.document, writer);
78
+
79
+ writer.closeBlock();
80
+ }
81
+ }
82
+
83
+ // combine behaviours that have tap triggers on the same object
84
+ // private combineBehavioursWithSameTapActions() {
85
+ // // TODO: if behaviours have different settings (e.g. one is exclusive and another one is not) this wont work - we need more logic for that
86
+
87
+ // const combined: { [key: string]: { behaviorId: string, trigger: TriggerModel, actions: IBehaviorElement[] } } = {};
88
+
89
+ // for (let i = this.behaviours.length - 1; i >= 0; i--) {
90
+ // const beh = this.behaviours[i];
91
+ // const trigger = beh.trigger as TriggerModel;
92
+ // if (!Array.isArray(trigger) && TriggerBuilder.isTapTrigger(trigger)) {
93
+ // const targetObject = trigger.targetId;
94
+ // if (!targetObject) continue;
95
+ // if (!combined[targetObject]) {
96
+ // combined[targetObject] = { behaviorId: beh.id, trigger: trigger, actions: [] };
97
+ // }
98
+ // const action = beh.action;
99
+ // combined[targetObject].actions.push(action);
100
+ // this.behaviours.splice(i, 1);
101
+ // }
102
+ // }
103
+ // for (const key in combined) {
104
+ // const val = combined[key];
105
+ // console.log("Combine " + val.actions.length + " actions on " + val.trigger.id, val.actions);
106
+ // const beh = new BehaviorModel(val.behaviorId, val.trigger, ActionBuilder.sequence(...val.actions));
107
+ // this.behaviours.push(beh);
108
+ // }
109
+ // }
110
+ }
111
+
112
+
113
+
114
+
115
+ // const playAnimationOnTap = new BehaviorModel("b_" + model.name + "_playanim", TriggerBuilder.tapTrigger(model),
116
+ // ActionBuilder.parallel(
117
+ // ActionBuilder.lookAtCameraAction(model),
118
+ // ActionBuilder.sequence(
119
+ // //ActionBuilder.startAnimationAction(model, 0, 0, 1, false, true),
120
+ // ActionBuilder.emphasize(model, 1, MotionType.Float),
121
+ // ActionBuilder.waitAction(1),
122
+ // ActionBuilder.emphasize(model, 1, MotionType.Blink),
123
+ // ActionBuilder.waitAction(1),
124
+ // ActionBuilder.emphasize(model, 1, MotionType.Jiggle),
125
+ // ActionBuilder.waitAction(1),
126
+ // ActionBuilder.emphasize(model, 1, MotionType.Pulse),
127
+ // ActionBuilder.waitAction(1),
128
+ // ActionBuilder.emphasize(model, 1, MotionType.Spin),
129
+ // ActionBuilder.waitAction(1),
130
+ // ActionBuilder.emphasize(model, 1, MotionType.Bounce),
131
+ // ActionBuilder.waitAction(1),
132
+ // ActionBuilder.emphasize(model, 1, MotionType.Flip),
133
+ // ActionBuilder.waitAction(1),
134
+ // ).makeLooping()
135
+ // ).makeLooping()
136
+ // );
137
+ // this.behaviours.push(playAnimationOnTap);
138
+ // return;
139
+
140
+ // const identityMatrix = new Matrix4().identity();
141
+
142
+ // const emptyParent = new USDZObject(model.name + "_empty", model.matrix);
143
+ // const parent = model.parent;
144
+ // parent.add(emptyParent);
145
+ // model.matrix = identityMatrix;
146
+ // emptyParent.add(model);
147
+
148
+
149
+ // const geometry = new THREE.SphereGeometry(.6, 32, 16);
150
+ // const modelVariant = new USDZObject(model.name + "_variant", identityMatrix, geometry, new MeshStandardMaterial({ color: 0xff0000 }));
151
+ // emptyParent.add(modelVariant);
152
+
153
+ // const matrix2 = new Matrix4();
154
+ // matrix2.makeTranslation(.5, 0, 0);
155
+ // const modelVariant2 = new USDZObject(model.name + "_variant2", matrix2, geometry, new MeshStandardMaterial({ color: 0xffff00 }));
156
+ // emptyParent.add(modelVariant2);
157
+
158
+ // const hideVariantOnStart = new BehaviorModel("b_" + model.name + "_start", TriggerBuilder.sceneStartTrigger(), ActionBuilder.fadeAction(modelVariant, 0, false));
159
+ // this.behaviours.push(hideVariantOnStart);
160
+
161
+ // const showVariant = new BehaviorModel("b_" + model.name + "_show_variant", [TriggerBuilder.tapTrigger(model)], new GroupActionModel("group", [
162
+ // ActionBuilder.fadeAction(model, 0, false),
163
+ // ActionBuilder.fadeAction(modelVariant, 0, true),
164
+ // ]));
165
+ // this.behaviours.push(showVariant);
166
+
167
+ // const showOriginal = new BehaviorModel("b_" + model.name + "_show_original", [
168
+ // TriggerBuilder.tapTrigger(modelVariant),
169
+ // TriggerBuilder.tapTrigger(modelVariant2)
170
+ // ],
171
+ // new GroupActionModel("group", [
172
+ // ActionBuilder.fadeAction([modelVariant, modelVariant2], 0, false),
173
+ // //ActionBuilder.waitAction(1),
174
+ // ActionBuilder.fadeAction(model, 0, true),
175
+ // //ActionBuilder.waitAction(.2),
176
+ // ActionBuilder.startAnimationAction(model, 0, 1000, 1, false, true),
177
+ // //ActionBuilder.lookAtCameraAction(model, 2, Vec3.forward, Vec3.up),
178
+ // //ActionBuilder.waitAction(1),
179
+ // //ActionBuilder.fadeAction(modelVariant2, 0, true),
180
+ // ]).makeSequence());
181
+ // this.behaviours.push(showOriginal);
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts ADDED
@@ -0,0 +1,503 @@
1
+ import { Behaviour, GameObject } from "../../../../Component";
2
+ import { Animator } from "../../../../Animator";
3
+ import { Renderer } from "../../../../Renderer";
4
+ import { serializable } from "../../../../../engine/engine_serialization_decorator";
5
+ import { IPointerClickHandler } from "../../../../ui/PointerEvents";
6
+ import { RegisteredAnimationInfo, UsdzAnimation } from "../Animation";
7
+ import { getWorldPosition, getWorldQuaternion, getWorldScale, setWorldPosition, setWorldQuaternion, setWorldScale } from "../../../../../engine/engine_three_utils";
8
+
9
+ import { Object3D, Material, Vector3, Quaternion } from "three";
10
+ import { USDObject } from "../../ThreeUSDZExporter";
11
+
12
+ import { BehaviorExtension, UsdzBehaviour } from "./Behaviour";
13
+ import { ActionBuilder, ActionModel, BehaviorModel, MotionType, Space, TriggerBuilder } from "./BehavioursBuilder";
14
+
15
+ export class ChangeTransformOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
16
+
17
+ @serializable(Object3D)
18
+ object?: Object3D;
19
+
20
+ @serializable(Object3D)
21
+ target?: Object3D;
22
+
23
+ @serializable()
24
+ duration: number = 1;
25
+
26
+ @serializable()
27
+ relativeMotion: boolean = false;
28
+
29
+ private coroutine: Generator | null = null;
30
+
31
+ private targetPos = new Vector3();
32
+ private targetRot = new Quaternion();
33
+ private targetScale = new Vector3();
34
+
35
+ private *moveToTarget() {
36
+
37
+ if (!this.target || !this.object) return;
38
+
39
+ const thisPos = getWorldPosition(this.object).clone();
40
+ const targetPos = getWorldPosition(this.target).clone();
41
+
42
+ const thisRot = getWorldQuaternion(this.object).clone();
43
+ const targetRot = getWorldQuaternion(this.target).clone();
44
+
45
+ const thisScale = getWorldScale(this.object).clone();
46
+ const targetScale = getWorldScale(this.target).clone();
47
+
48
+ const dist = thisPos.distanceTo(targetPos);
49
+ const rotDist = thisRot.angleTo(targetRot);
50
+ const scaleDist = thisScale.distanceTo(targetScale);
51
+
52
+ if (dist < 0.01 && rotDist < 0.01 && scaleDist < 0.01) {
53
+ setWorldPosition(this.object, targetPos);
54
+ setWorldQuaternion(this.object, targetRot);
55
+ setWorldScale(this.object, targetScale);
56
+ this.coroutine = null;
57
+ return;
58
+ }
59
+
60
+ let t01 = 0;
61
+ let eased = 0;
62
+ while (t01 < 1) {
63
+
64
+ t01 += this.context.time.deltaTime / this.duration;
65
+ if (t01 > 1) t01 = 1;
66
+
67
+ // apply ease-in-out
68
+ // https://easings.net/
69
+ eased = t01 < 0.5 ? 4 * t01 * t01 * t01 : 1 - Math.pow(-2 * t01 + 2, 3) / 2;
70
+
71
+ this.targetPos.lerpVectors(thisPos, targetPos, eased);
72
+ this.targetRot.slerpQuaternions(thisRot, targetRot, eased);
73
+ this.targetScale.lerpVectors(thisScale, targetScale, eased);
74
+
75
+ setWorldPosition(this.object, this.targetPos);
76
+ setWorldQuaternion(this.object, this.targetRot);
77
+ setWorldScale(this.object, this.targetScale);
78
+
79
+ yield;
80
+ }
81
+
82
+ this.coroutine = null;
83
+ }
84
+
85
+ private *moveRelative() {
86
+
87
+ if (!this.target || !this.object) return;
88
+
89
+ const thisPos = this.object.position.clone();
90
+ const thisRot = this.object.quaternion.clone();
91
+ const thisScale = this.object.scale.clone();
92
+
93
+ const posOffset = this.target.position.clone();
94
+ const rotOffset = this.target.quaternion.clone();
95
+ const scaleOffset = this.target.scale.clone();
96
+
97
+ // convert into right space
98
+ posOffset.applyQuaternion(this.object.quaternion);
99
+
100
+ this.targetPos.copy(this.object.position).add(posOffset);
101
+ this.targetRot.copy(this.object.quaternion).multiply(rotOffset);
102
+ this.targetScale.copy(this.object.scale).multiply(scaleOffset);
103
+
104
+ let t01 = 0;
105
+ let eased = 0;
106
+ while (t01 < 1) {
107
+
108
+ t01 += this.context.time.deltaTime / this.duration;
109
+ if (t01 > 1) t01 = 1;
110
+
111
+ // apply ease-in-out
112
+ // https://easings.net/
113
+ eased = t01 < 0.5 ? 4 * t01 * t01 * t01 : 1 - Math.pow(-2 * t01 + 2, 3) / 2;
114
+
115
+ this.object.position.lerpVectors(thisPos, this.targetPos, eased);
116
+ this.object.quaternion.slerpQuaternions(thisRot, this.targetRot, eased);
117
+ this.object.scale.lerpVectors(thisScale, this.targetScale, eased);
118
+
119
+ yield;
120
+ }
121
+
122
+ this.coroutine = null;
123
+ }
124
+
125
+ onPointerClick() {
126
+ if (this.coroutine) this.stopCoroutine(this.coroutine);
127
+ if (!this.relativeMotion)
128
+ this.coroutine = this.startCoroutine(this.moveToTarget());
129
+ else
130
+ this.coroutine = this.startCoroutine(this.moveRelative());
131
+ }
132
+
133
+ beforeCreateDocument(ext) {
134
+ if (this.target && this.object && this.gameObject) {
135
+ const moveForward = new BehaviorModel("Move to " + this.target?.name,
136
+ TriggerBuilder.tapTrigger(this.gameObject),
137
+ ActionBuilder.transformAction(this.object, this.target, this.duration, this.relativeMotion ? Space.Relative : Space.Absolute),
138
+ );
139
+ ext.addBehavior(moveForward);
140
+ }
141
+ }
142
+ }
143
+
144
+ export class ChangeMaterialOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
145
+
146
+ @serializable(Material)
147
+ materialToSwitch?: Material;
148
+
149
+ @serializable(Material)
150
+ variantMaterial?: Material;
151
+
152
+ private _objectsWithThisMaterial: Renderer[] = [];
153
+
154
+ awake() {
155
+ if (this.variantMaterial && this.materialToSwitch) {
156
+ const renderer = GameObject.findObjectsOfType(Renderer);
157
+ for (const rend of renderer) {
158
+ if (rend.sharedMaterial === this.materialToSwitch) {
159
+ this._objectsWithThisMaterial.push(rend);
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ onPointerClick() {
166
+ if (!this.variantMaterial) return;
167
+ for (const rend of this._objectsWithThisMaterial) {
168
+ rend.sharedMaterial = this.variantMaterial;
169
+ }
170
+ }
171
+
172
+ private selfModel!: USDObject;
173
+ private targetModels: USDObject[] = [];
174
+
175
+ private static _materialTriggersPerId: { [key: string]: ChangeMaterialOnClick[] } = {}
176
+
177
+ createBehaviours(_ext: BehaviorExtension, model: USDObject, _context) {
178
+
179
+ const shouldExport = this._objectsWithThisMaterial.find(o => o.gameObject.uuid === model.uuid);
180
+ if (shouldExport) {
181
+ this.targetModels.push(model);
182
+ }
183
+ if (this.gameObject.uuid === model.uuid) {
184
+ this.selfModel = model;
185
+ if (this.materialToSwitch) {
186
+ if (!ChangeMaterialOnClick._materialTriggersPerId[this.materialToSwitch.uuid])
187
+ ChangeMaterialOnClick._materialTriggersPerId[this.materialToSwitch.uuid] = [];
188
+ ChangeMaterialOnClick._materialTriggersPerId[this.materialToSwitch.uuid].push(this);
189
+ }
190
+ }
191
+ }
192
+
193
+ afterCreateDocument(ext: BehaviorExtension, _context) {
194
+
195
+ if (!this.materialToSwitch) return;
196
+ const handlers = ChangeMaterialOnClick._materialTriggersPerId[this.materialToSwitch.uuid];
197
+ if (handlers) {
198
+ const variants: { [key: string]: USDObject[] } = {}
199
+ for (const handler of handlers) {
200
+ const createdVariants = handler.createVariants();
201
+ if (createdVariants && createdVariants.length > 0)
202
+ variants[handler.selfModel.uuid] = createdVariants;
203
+ }
204
+ const otherVariants: any[] = [];
205
+ for (const handler of handlers) {
206
+ for (const key in variants) {
207
+ if (key !== handler.selfModel.uuid) {
208
+ otherVariants.push(variants[key]);
209
+ }
210
+ }
211
+ handler.createAndAttachBehaviors(ext, variants[handler.selfModel.uuid], otherVariants);
212
+ }
213
+ }
214
+ delete ChangeMaterialOnClick._materialTriggersPerId[this.materialToSwitch.uuid];
215
+ }
216
+
217
+ private createAndAttachBehaviors(ext: BehaviorExtension, myVariants, otherVariants) {
218
+ const start: ActionModel[] = [];
219
+ const select: ActionModel[] = [];
220
+
221
+ // the order here matters
222
+ for (const target of this.targetModels) {
223
+ const hideOriginal = ActionBuilder.fadeAction(target, 0, false);
224
+ select.push(hideOriginal);
225
+ }
226
+ for (const v of otherVariants) {
227
+ select.push(ActionBuilder.fadeAction(v, 0, false));
228
+ }
229
+ for (const v of myVariants) {
230
+ start.push(ActionBuilder.fadeAction(v, 0, false));
231
+ select.push(ActionBuilder.fadeAction(v, 0, true));
232
+ }
233
+
234
+ ext.addBehavior(new BehaviorModel("Select " + this.selfModel.name,
235
+ TriggerBuilder.tapTrigger(this.selfModel),
236
+ ActionBuilder.parallel(...select))
237
+ );
238
+ ext.addBehavior(new BehaviorModel("Start hidden " + this.selfModel.name,
239
+ TriggerBuilder.sceneStartTrigger(),
240
+ ActionBuilder.parallel(...start))
241
+ );
242
+ }
243
+
244
+ private createVariants() {
245
+ if (!this.variantMaterial) return null;
246
+
247
+ const variantModels: USDObject[] = [];
248
+ for (const target of this.targetModels) {
249
+ const variant = target.clone();
250
+ variant.name += " variant_" + this.variantMaterial.name;
251
+ variant.name = variant.name.replace(/\s/g, "_");
252
+ variant.material = this.variantMaterial;
253
+ variant.geometry = target.geometry;
254
+ variant.matrix = target.matrix;
255
+
256
+ if (!target.parent || !target.parent.isEmpty()) {
257
+ USDObject.createEmptyParent(target);
258
+ }
259
+ if (target.parent) target.parent.add(variant);
260
+ variantModels.push(variant);
261
+ }
262
+ return variantModels;
263
+ }
264
+ }
265
+
266
+ export class SetActiveOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour {
267
+
268
+ @serializable(Object3D)
269
+ target?: Object3D;
270
+
271
+ // TODO bring back, doesn't work yet
272
+ private toggleOnClick: boolean = false;
273
+
274
+ @serializable()
275
+ targetState: boolean = true;
276
+
277
+ @serializable()
278
+ hideSelf: boolean = true;
279
+
280
+ onPointerClick() {
281
+ if (!this.toggleOnClick && this.hideSelf)
282
+ this.gameObject.visible = false;
283
+ if (this.target)
284
+ this.target.visible = this.toggleOnClick ? !this.target.visible : this.targetState;
285
+ }
286
+
287
+ private selfModel!: USDObject;
288
+ private otherModel?: USDObject;
289
+ private toggleModel?: USDObject;
290
+
291
+ createBehaviours(_, model, _context) {
292
+ if (model.uuid === this.gameObject.uuid) {
293
+ this.selfModel = model;
294
+ }
295
+ }
296
+
297
+ private stateBeforeCreatingDocument: boolean = false;
298
+
299
+ beforeCreateDocument() {
300
+ this.stateBeforeCreatingDocument = this.gameObject.visible;
301
+ this.gameObject.visible = true;
302
+ }
303
+
304
+ afterCreateDocument(ext, context) {
305
+ if (!this.target) return;
306
+
307
+ this.otherModel = context.document.findById(this.target.uuid);
308
+
309
+ if (this.selfModel && this.otherModel) {
310
+ let hideClickedObject = this.hideSelf;
311
+ let targetState = this.targetState;
312
+ if (this.toggleOnClick) {
313
+ hideClickedObject = true;
314
+ targetState = !this.target.visible;
315
+
316
+ // TODO check where we have to create the clone; here it doesn't show up
317
+ this.toggleModel = this.selfModel.clone();
318
+ this.toggleModel.name += "_toggle";
319
+ if (this.selfModel.parent)
320
+ this.selfModel.parent.add(this.toggleModel);
321
+ }
322
+
323
+ const sequence: ActionModel[] = [];
324
+ if (hideClickedObject)
325
+ sequence.push(ActionBuilder.fadeAction(this.selfModel, 0, false));
326
+ if (this.toggleModel)
327
+ sequence.push(ActionBuilder.fadeAction(this.toggleModel, 0, true));
328
+ sequence.push(ActionBuilder.fadeAction(this.otherModel, 0, this.targetState));
329
+
330
+ ext.addBehavior(new BehaviorModel("Toggle_" + this.selfModel.name + "_hideSelf",
331
+ TriggerBuilder.tapTrigger(this.selfModel),
332
+ ActionBuilder.sequence(...sequence)
333
+ ));
334
+
335
+ // TODO this is relatively similar to the material variant switching logic, can we reuse that?
336
+ if (this.toggleOnClick && this.toggleModel) {
337
+ const toggleSequence: ActionModel[] = [];
338
+ toggleSequence.push(ActionBuilder.fadeAction(this.toggleModel, 0, false));
339
+ toggleSequence.push(ActionBuilder.fadeAction(this.selfModel, 0, true));
340
+ toggleSequence.push(ActionBuilder.fadeAction(this.otherModel, 0, !this.targetState));
341
+
342
+ ext.addBehavior(new BehaviorModel("Toggle_" + this.selfModel.name + "_toggleSelf",
343
+ TriggerBuilder.tapTrigger(this.toggleModel),
344
+ ActionBuilder.sequence(...toggleSequence)
345
+ ));
346
+
347
+ ext.addBehavior(new BehaviorModel("HideOnStart_" + this.gameObject.name,
348
+ TriggerBuilder.sceneStartTrigger(),
349
+ ActionBuilder.fadeAction(this.toggleModel, 0, false)
350
+ ));
351
+ }
352
+ }
353
+
354
+ this.gameObject.visible = this.stateBeforeCreatingDocument;
355
+ }
356
+ }
357
+
358
+ export class HideOnStart extends Behaviour implements UsdzBehaviour {
359
+
360
+ start() {
361
+ this.gameObject.visible = false;
362
+ }
363
+
364
+ createBehaviours(ext, model, _context) {
365
+ if (model.uuid === this.gameObject.uuid)
366
+ ext.addBehavior(new BehaviorModel("HideOnStart_" + this.gameObject.name,
367
+ TriggerBuilder.sceneStartTrigger(),
368
+ ActionBuilder.fadeAction(model, 0, false)
369
+ ));
370
+ }
371
+
372
+ beforeCreateDocument() {
373
+ this.gameObject.visible = true;
374
+ }
375
+
376
+ afterCreateDocument() {
377
+ this.gameObject.visible = false;
378
+ }
379
+ }
380
+
381
+ export class EmphasizeOnClick extends Behaviour implements UsdzBehaviour {
382
+
383
+ @serializable()
384
+ target?: Object3D;
385
+
386
+ @serializable()
387
+ duration: number = 0.5;
388
+
389
+ @serializable()
390
+ motionType: MotionType = MotionType.bounce;
391
+
392
+ beforeCreateDocument() { }
393
+
394
+ createBehaviours(ext, model, _context) {
395
+ if (!this.target) return;
396
+
397
+ if (model.uuid === this.gameObject.uuid)
398
+ {
399
+ const emphasize = new BehaviorModel("emphasize " + this.name,
400
+ TriggerBuilder.tapTrigger(this.gameObject),
401
+ ActionBuilder.emphasize(this.target, this.duration, this.motionType, undefined, "basic"),
402
+ );
403
+ ext.addBehavior(emphasize);
404
+ }
405
+ }
406
+
407
+ afterCreateDocument(_ext, _context) { }
408
+ }
409
+
410
+ export class PlayAnimationOnClick extends Behaviour implements IPointerClickHandler, UsdzBehaviour, UsdzAnimation {
411
+
412
+ @serializable(Object3D)
413
+ target?: Object3D;
414
+
415
+ @serializable(Animator)
416
+ animator?: Animator;
417
+
418
+ @serializable()
419
+ stateName?: string;
420
+
421
+ onPointerClick() {
422
+ if (!this.target) return;
423
+ if (this.stateName)
424
+ this.animator?.play(this.stateName, 0, 0, .1);
425
+ }
426
+
427
+ private selfModel: any;
428
+ private registeredAnimationModel: any;
429
+ private registeredAnimation?: RegisteredAnimationInfo;
430
+
431
+ createBehaviours(_ext, model, _context) {
432
+ if (model.uuid === this.gameObject.uuid)
433
+ this.selfModel = model;
434
+ }
435
+
436
+ afterCreateDocument(ext, context) {
437
+ if (!this.registeredAnimation || !this.registeredAnimationModel) return;
438
+ const document = context.document;
439
+ document.traverse(model => {
440
+ if (model.uuid === this.target?.uuid && this.registeredAnimation) {
441
+ const playAnimationOnTap = new BehaviorModel("tap " + this.name + " for " + this.stateName + " on " + this.target?.name,
442
+ TriggerBuilder.tapTrigger(this.selfModel),
443
+ ActionBuilder.startAnimationAction(model, this.registeredAnimation.start, this.registeredAnimation.duration)
444
+ );
445
+ ext.addBehavior(playAnimationOnTap);
446
+ }
447
+ });
448
+ }
449
+
450
+ createAnimation(ext, model, _context) {
451
+ if (this.target && this.animator) {
452
+ const state = this.animator?.runtimeAnimatorController?.findState(this.stateName);
453
+ this.registeredAnimationModel = model;
454
+ this.registeredAnimation = ext.registerAnimation(this.target, state?.motion.clip);
455
+ }
456
+ }
457
+
458
+ }
459
+
460
+ export class PreliminaryAction extends Behaviour {
461
+ getType(): string | void { }
462
+ @serializable(Object3D)
463
+ target?: Object3D;
464
+
465
+
466
+ getDuration(): number | void { };
467
+ }
468
+
469
+ export class PreliminaryTrigger extends Behaviour {
470
+
471
+ @serializable(PreliminaryAction)
472
+ target?: PreliminaryAction;
473
+ }
474
+
475
+ export class VisibilityAction extends PreliminaryAction {
476
+
477
+ //@type int
478
+ @serializable()
479
+ type: VisibilityActionType = VisibilityActionType.Hide;
480
+
481
+ @serializable()
482
+ duration: number = 1;
483
+
484
+ getType() {
485
+ switch (this.type) {
486
+ case VisibilityActionType.Hide: return "hide";
487
+ case VisibilityActionType.Show: return "show";
488
+ }
489
+ }
490
+
491
+ getDuration() {
492
+ return this.duration;
493
+ }
494
+ }
495
+
496
+ export class TapGestureTrigger extends PreliminaryTrigger {
497
+
498
+ }
499
+
500
+ export enum VisibilityActionType {
501
+ Show = 0,
502
+ Hide = 1,
503
+ }
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts ADDED
@@ -0,0 +1,459 @@
1
+ import { Object3D } from "three";
2
+ import { USDDocument, USDObject, USDWriter, makeNameSafeForUSD } from "../../ThreeUSDZExporter";
3
+
4
+ import { BehaviorExtension } from "./Behaviour";
5
+
6
+ // TODO: rename to usdz element
7
+ export interface IBehaviorElement {
8
+ id: string;
9
+ writeTo(document: USDDocument, writer: USDWriter);
10
+ }
11
+
12
+ export class BehaviorModel {
13
+
14
+ static global_id: number = 0;
15
+ id: string;
16
+ trigger: IBehaviorElement | IBehaviorElement[];
17
+ action: IBehaviorElement;
18
+ exclusive: boolean = false;
19
+
20
+ makeExclusive(exclusive: boolean): BehaviorModel {
21
+ this.exclusive = exclusive;
22
+ return this;
23
+ }
24
+
25
+ constructor(id: string, trigger: IBehaviorElement | IBehaviorElement[], action: IBehaviorElement) {
26
+ this.id = "Behavior_" + makeNameSafeForUSD(id) + "_" + BehaviorModel.global_id++;
27
+ this.trigger = trigger;
28
+ this.action = action;
29
+ }
30
+
31
+ writeTo(_ext: BehaviorExtension, document: USDDocument, writer: USDWriter) {
32
+ if (!this.trigger || !this.action) return;
33
+ writer.beginBlock(`def Preliminary_Behavior "${this.id}"`);
34
+ writer.appendLine(`rel actions = <${this.action.id}>`);
35
+ writer.appendLine(`uniform bool exclusive = ${this.exclusive}`);
36
+ let triggerString = "";
37
+ if (Array.isArray(this.trigger)) {
38
+ triggerString = "[";
39
+ for (let i = 0; i < this.trigger.length; i++) {
40
+ const tr = this.trigger[i];
41
+ triggerString += "<" + tr.id + ">";
42
+ if (i + 1 < this.trigger.length) triggerString += ", ";
43
+ }
44
+ triggerString += "]";
45
+ }
46
+ else
47
+ triggerString = `<${this.trigger.id}>`;
48
+
49
+ writer.appendLine(`rel triggers = ${triggerString} `);
50
+ writer.appendLine();
51
+ if (Array.isArray(this.trigger)) {
52
+ for (const trigger of this.trigger) {
53
+ trigger.writeTo(document, writer);
54
+ writer.appendLine();
55
+ }
56
+ }
57
+ else
58
+ this.trigger.writeTo(document, writer);
59
+ writer.appendLine();
60
+ this.action.writeTo(document, writer);
61
+ writer.closeBlock();
62
+ }
63
+ }
64
+
65
+
66
+ type Target = USDObject | USDObject[] | Object3D | Object3D[];
67
+
68
+ /** called to resolve target objects to usdz paths */
69
+ function resolve(targetObject: Target, document: USDDocument): string {
70
+ let result: string = "";
71
+ if (Array.isArray(targetObject)) {
72
+ let str = "[ ";
73
+ for (let i = 0; i < targetObject.length; i++) {
74
+ let obj = targetObject[i];
75
+ if (typeof obj === "string")
76
+ str += obj;
77
+ else if ( typeof obj === "object") {
78
+ //@ts-ignore
79
+ if (obj.isObject3D) {
80
+ //@ts-ignore
81
+ obj = document.findById(obj.uuid);
82
+ }
83
+ const res = (obj as any).getPath?.call(obj) as string;
84
+ str += res;
85
+ }
86
+ if (i + 1 < targetObject.length) str += ", ";
87
+ }
88
+ str += " ]";
89
+ result = str;
90
+ }
91
+ else if (typeof targetObject === "object") {
92
+ //@ts-ignore
93
+ if (targetObject.isObject3D) {
94
+ //@ts-ignore
95
+ targetObject = document.findById(targetObject.uuid);
96
+ }
97
+ result = (targetObject as any).getPath?.call(targetObject) as string;
98
+ }
99
+
100
+ // in three there's now a new "Scenes" parent and "Scene" xform that's injected;
101
+ // we need to add this to our path here so that we have full paths
102
+ result = result.replace(document.name, document.name + "/Scenes/Scene");
103
+ return result;
104
+ }
105
+
106
+ export class TriggerModel implements IBehaviorElement {
107
+ static global_id: number = 0;
108
+
109
+ id: string;
110
+ targetId?: string | Target;
111
+ tokenId?: string;
112
+ type?: string;
113
+
114
+ constructor(targetId?: string | Target, id?: string) {
115
+ if (targetId) this.targetId = targetId;
116
+ if (id) this.id = id;
117
+ else this.id = "Trigger_" + TriggerModel.global_id++;
118
+ }
119
+
120
+ writeTo(document: USDDocument, writer: USDWriter) {
121
+ writer.beginBlock(`def Preliminary_Trigger "${this.id}"`);
122
+ if (this.targetId) {
123
+ if (typeof this.targetId !== "string") this.targetId = resolve(this.targetId, document);
124
+ writer.appendLine(`rel affectedObjects = ` + this.targetId);
125
+ }
126
+ if (this.tokenId)
127
+ writer.appendLine(`token info:id = "${this.tokenId}"`);
128
+ if (this.type)
129
+ writer.appendLine(`token type = "${this.type}"`);
130
+ writer.closeBlock();
131
+ }
132
+ }
133
+
134
+ export class TriggerBuilder {
135
+
136
+ static sceneStartTrigger(): TriggerModel {
137
+ const trigger = new TriggerModel();
138
+ trigger.targetId = undefined;
139
+ trigger.tokenId = "SceneTransition";
140
+ trigger.type = "enter";
141
+ return trigger;
142
+ }
143
+
144
+ static tapTrigger(targetObject: Target): TriggerModel {
145
+ const trigger = new TriggerModel(targetObject);
146
+ trigger.tokenId = "TapGesture";
147
+ return trigger;
148
+ }
149
+
150
+ static isTapTrigger(trigger?: TriggerModel) {
151
+ return trigger?.tokenId === "TapGesture";
152
+ }
153
+ }
154
+
155
+ export class GroupActionModel implements IBehaviorElement {
156
+
157
+ static global_id: number = 0;
158
+ static getId(): number {
159
+ return this.global_id++;
160
+ }
161
+
162
+ id: string;
163
+ actions: IBehaviorElement[];
164
+ loops: number = 0;
165
+ performCount: number = 1;
166
+ type: string = "serial";
167
+
168
+ constructor(id: string, actions: IBehaviorElement[]) {
169
+ this.id = id;
170
+ this.actions = actions;
171
+ }
172
+
173
+ addAction(el: IBehaviorElement): GroupActionModel {
174
+ this.actions.push(el);
175
+ return this;
176
+ }
177
+
178
+ makeParallel(): GroupActionModel {
179
+ this.type = "parallel";
180
+ return this;
181
+ }
182
+
183
+ makeSequence(): GroupActionModel {
184
+ this.type = "serial";
185
+ return this;
186
+ }
187
+
188
+ makeLooping() {
189
+ this.loops = 1;
190
+ return this;
191
+ }
192
+
193
+ makeRepeat(count: number) {
194
+ this.performCount = count;
195
+ return this;
196
+ }
197
+
198
+ writeTo(document: USDDocument, writer: USDWriter) {
199
+ writer.beginBlock(`def Preliminary_Action "${this.id}"`);
200
+ writer.beginArray("rel actions");
201
+ for (const act of this.actions) {
202
+ if (!act) continue;
203
+ writer.appendLine("<" + act.id + ">,");
204
+ }
205
+ writer.closeArray();
206
+ writer.appendLine();
207
+
208
+ writer.appendLine(`token info:id = "Group"`);
209
+ writer.appendLine(`bool loops = ${this.loops} `);
210
+ writer.appendLine(`int performCount = ${this.performCount} `);
211
+ writer.appendLine(`token type = "${this.type}"`);
212
+ writer.appendLine();
213
+
214
+ for (const act of this.actions) {
215
+ if (!act) continue;
216
+ act.writeTo(document, writer);
217
+ writer.appendLine();
218
+ }
219
+
220
+ writer.closeBlock();
221
+ }
222
+ }
223
+
224
+ export enum MotionType {
225
+ pop = 0,
226
+ blink = 1,
227
+ bounce = 2,
228
+ flip = 3,
229
+ float = 4,
230
+ jiggle = 5,
231
+ pulse = 6,
232
+ spin = 7,
233
+ };
234
+
235
+ export enum Space {
236
+ Relative = "relative",
237
+ Absolute = "absolute"
238
+ };
239
+
240
+ export class ActionModel implements IBehaviorElement {
241
+
242
+ private static global_id: number = 0;
243
+
244
+ id: string;
245
+ tokenId?: string;
246
+ affectedObjects?: string | Target;
247
+ easeType?: string;;
248
+ motionType?: string;
249
+ duration?: number;
250
+ moveDistance?: number;
251
+ style?: string;
252
+ type?: string;
253
+ front?: Vec3;
254
+ up?: Vec3;
255
+ start?: number;
256
+ animationSpeed?: number;
257
+ reversed?: boolean;
258
+ pingPong?: boolean;
259
+ xFormTarget?: Target | string;
260
+
261
+ clone(): ActionModel {
262
+ const copy = new ActionModel();
263
+ const id = copy.id;
264
+ Object.assign(copy, this);
265
+ copy.id = id;
266
+ return copy;
267
+ }
268
+
269
+ constructor(affectedObjects?: string | Target, id?: string) {
270
+ if (affectedObjects) this.affectedObjects = affectedObjects;
271
+ if (id) this.id = id;
272
+ /*else if (affectedObjects) {
273
+ this.id = "Action_" + sanitizeId(affectedObjects);
274
+ }
275
+ else */
276
+ else
277
+ this.id = "Action";
278
+ this.id += "_" + ActionModel.global_id++;
279
+ }
280
+
281
+ writeTo(document: USDDocument, writer: USDWriter) {
282
+ writer.beginBlock(`def Preliminary_Action "${this.id}"`);
283
+ if (this.affectedObjects) {
284
+ if (typeof this.affectedObjects !== "string") this.affectedObjects = resolve(this.affectedObjects, document);
285
+ writer.appendLine('rel affectedObjects = ' + this.affectedObjects);
286
+ }
287
+ if (typeof this.duration === "number")
288
+ writer.appendLine(`double duration = ${this.duration} `);
289
+ if (this.easeType)
290
+ writer.appendLine(`token easeType = "${this.easeType}"`);
291
+ if (this.tokenId)
292
+ writer.appendLine(`token info:id = "${this.tokenId}"`);
293
+ if (this.motionType)
294
+ writer.appendLine(`token motionType = "${this.motionType}"`);
295
+ if (typeof this.moveDistance === "number")
296
+ writer.appendLine(`double moveDistance = ${this.moveDistance} `);
297
+ if (this.style)
298
+ writer.appendLine(`token style = "${this.style}"`);
299
+ if (this.type)
300
+ writer.appendLine(`token type = "${this.type}"`);
301
+ if (this.front)
302
+ writer.appendLine(`vector3d front = (${this.front.x}, ${this.front.y}, ${this.front.z})`);
303
+ if (this.up)
304
+ writer.appendLine(`vector3d upVector = (${this.up.x}, ${this.up.y}, ${this.up.z})`);
305
+ if (typeof this.start === "number") {
306
+ writer.appendLine(`double start = ${this.start} `);
307
+ }
308
+ if (typeof this.animationSpeed === "number") {
309
+ writer.appendLine(`double animationSpeed = ${this.animationSpeed} `);
310
+ }
311
+ if (typeof this.reversed === "boolean") {
312
+ writer.appendLine(`bool reversed = ${this.reversed}`)
313
+ }
314
+ if (typeof this.pingPong === "boolean") {
315
+ writer.appendLine(`bool reverses = ${this.pingPong}`)
316
+ }
317
+ if (this.xFormTarget) {
318
+ if (typeof this.xFormTarget !== "string")
319
+ this.xFormTarget = resolve(this.xFormTarget, document);
320
+ writer.appendLine(`rel xformTarget = ${this.xFormTarget}`)
321
+ }
322
+ writer.closeBlock();
323
+ }
324
+ }
325
+
326
+ class Vec3 {
327
+ x: number = 0;
328
+ y: number = 0;
329
+ z: number = 0;
330
+
331
+ constructor(x: number, y: number, z: number) {
332
+ this.x = x;
333
+ this.y = y;
334
+ this.z = z;
335
+ }
336
+
337
+ static get up(): Vec3 {
338
+ return new Vec3(0, 1, 0);
339
+ }
340
+
341
+ static get right(): Vec3 {
342
+ return new Vec3(1, 0, 0);
343
+ }
344
+
345
+ static get forward(): Vec3 {
346
+ return new Vec3(0, 0, 1);
347
+ }
348
+
349
+ static get back(): Vec3 {
350
+ return new Vec3(0, 0, -1);
351
+ }
352
+
353
+ static get zero(): Vec3 {
354
+ return new Vec3(0, 0, 0);
355
+ }
356
+ }
357
+
358
+ export class ActionBuilder {
359
+
360
+ static sequence(...params: IBehaviorElement[]) {
361
+ const group = new GroupActionModel("group_" + GroupActionModel.getId(), params);
362
+ return group.makeSequence();
363
+ }
364
+
365
+ static parallel(...params: IBehaviorElement[]) {
366
+ const group = new GroupActionModel("group_" + GroupActionModel.getId(), params);
367
+ return group.makeParallel();
368
+ }
369
+
370
+ static fadeAction(targetObject: Target, duration: number, show: boolean): ActionModel {
371
+ const act = new ActionModel(targetObject);
372
+ act.tokenId = "Visibility";
373
+ act.type = show ? "show" : "hide";
374
+ act.duration = duration;
375
+
376
+ act.style = "basic";
377
+ act.motionType = "none";
378
+ act.moveDistance = 0;
379
+ act.easeType = "none";
380
+ return act;
381
+ }
382
+
383
+ /**
384
+ * creates an action that plays an animation
385
+ * @param start offset in seconds!
386
+ * @param duration in seconds! 0 means play to end
387
+ */
388
+ static startAnimationAction(targetObject: Target, start: number, duration: number = 0, animationSpeed: number = 1, reversed: boolean = false, pingPong: boolean = false): IBehaviorElement {
389
+ const act = new ActionModel(targetObject);
390
+ act.tokenId = "StartAnimation";
391
+ act.start = start;
392
+ // start is time in seconds, the documentation is not right here
393
+ act.duration = duration;
394
+ // duration of 0 is play to end
395
+ act.animationSpeed = animationSpeed;
396
+ act.reversed = reversed;
397
+ act.pingPong = pingPong;
398
+ if (reversed) {
399
+ act.start -= duration;
400
+ //console.warn("Reversed animation does currently not work. The resulting file will most likely not playback.", act.id, targetObject);
401
+ }
402
+ if (pingPong) {
403
+ act.pingPong = false;
404
+ const back = act.clone();
405
+ back.reversed = !reversed;
406
+ back.start = act.start;
407
+ if (back.reversed) {
408
+ back.start -= duration;
409
+ }
410
+ const group = ActionBuilder.sequence(act, back);
411
+ return group;
412
+ }
413
+ return act;
414
+ }
415
+
416
+ static waitAction(duration: number): ActionModel {
417
+ const act = new ActionModel();
418
+ act.tokenId = "Wait";
419
+ act.duration = duration;
420
+ return act;
421
+ }
422
+
423
+ static lookAtCameraAction(targets: Target, duration: number = 9999999999999, front?: Vec3, up?: Vec3): ActionModel {
424
+ const act = new ActionModel(targets);
425
+ act.tokenId = "LookAtCamera";
426
+ act.duration = duration;
427
+ act.front = front ?? Vec3.forward;
428
+ // 0,0,0 is a special case for "free look"
429
+ // 0,1,0 is for "y-locked look-at"
430
+ act.up = up ?? Vec3.up;
431
+ return act;
432
+ }
433
+
434
+ static emphasize(targets: Target, duration: number, motionType: MotionType = MotionType.bounce, moveDistance: number = 1, style: string = "basic") {
435
+ const act = new ActionModel(targets);
436
+ act.tokenId = "Emphasize";
437
+ act.duration = duration;
438
+ act.style = style ?? "basic";
439
+ act.motionType = MotionType[motionType];
440
+ act.moveDistance = moveDistance;
441
+ return act;
442
+ }
443
+
444
+ static transformAction(targets: Target, transformTarget: Target, duration: number, transformType: Space, easeType: string = "inout") {
445
+ const act = new ActionModel(targets);
446
+ act.tokenId = "Transform";
447
+ act.duration = duration;
448
+ act.type = transformType;
449
+ act.easeType = easeType;
450
+ if (Array.isArray(transformTarget)) {
451
+ console.error("Transform target must not be an array", transformTarget);
452
+ }
453
+ act.xFormTarget = transformTarget;
454
+ return act;
455
+ }
456
+
457
+ }
458
+
459
+ export { Vec3 as USDVec3 }
src/engine-components/export/usdz/extensions/DocumentExtension.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { IUSDExporterExtension } from "../Extension";
2
+
3
+ export class DocumentExtension implements IUSDExporterExtension {
4
+
5
+ get extensionName(): string {
6
+ return "DocumentExtension";
7
+ }
8
+
9
+ onAfterBuildDocument(_context: any) { }
10
+ }
src/engine-components/export/usdz/ThreeUSDZExporter.ts ADDED
@@ -0,0 +1,1280 @@
1
+ import {
2
+ PlaneGeometry,
3
+ Texture,
4
+ Uniform,
5
+ PerspectiveCamera,
6
+ Scene,
7
+ Mesh,
8
+ ShaderMaterial,
9
+ WebGLRenderer,
10
+ MathUtils,
11
+ Matrix4,
12
+ RepeatWrapping,
13
+ MirroredRepeatWrapping,
14
+ DoubleSide,
15
+ BufferGeometry,
16
+ Material,
17
+ Camera,
18
+ Color,
19
+ MeshStandardMaterial,
20
+ } from 'three';
21
+ import * as fflate from 'three/examples/jsm/libs/fflate.module.js';
22
+
23
+ function makeNameSafe( str ) {
24
+ str = str.replace( /[^a-zA-Z0-9_]/g, '' );
25
+
26
+ // if str does not start with a-zA-Z_ add _ to the beginning
27
+ if ( !str.match( /^[a-zA-Z_]/ ) )
28
+ str = '_' + str;
29
+
30
+ return str;
31
+ }
32
+
33
+ class USDObject {
34
+
35
+ static USDObject_export_id = 0;
36
+
37
+ uuid: string;
38
+ name: string;
39
+ matrix: Matrix4;
40
+ private _isDynamic: boolean;
41
+ get isDynamic() { return this._isDynamic; }
42
+ private set isDynamic( value ) { this._isDynamic = value; }
43
+ geometry: BufferGeometry | null;
44
+ material: Material | null;
45
+ camera: Camera | null;
46
+ parent: USDObject | null;
47
+ children: Array<USDObject | null> = [];
48
+ _eventListeners: {};
49
+ mesh: any;
50
+
51
+ static createEmptyParent( object ) {
52
+
53
+ const emptyParent = new USDObject( MathUtils.generateUUID(), object.name + '_empty_' + ( USDObject.USDObject_export_id ++ ), object.matrix );
54
+ const parent = object.parent;
55
+ parent.add( emptyParent );
56
+ emptyParent.add( object );
57
+ emptyParent.isDynamic = true;
58
+ object.matrix = new Matrix4().identity();
59
+ return emptyParent;
60
+
61
+ }
62
+
63
+ constructor( id, name, matrix, mesh: BufferGeometry | null = null, material: Material | null = null, camera: Camera | null = null ) {
64
+
65
+ this.uuid = id;
66
+ this.name = makeNameSafe( name );
67
+ this.matrix = matrix;
68
+ this.geometry = mesh;
69
+ this.material = material;
70
+ this.camera = camera;
71
+ this.parent = null;
72
+ this.children = [];
73
+ this._eventListeners = {};
74
+ this._isDynamic = false;
75
+
76
+ }
77
+
78
+ is( obj ) {
79
+
80
+ if ( ! obj ) return false;
81
+ return this.uuid === obj.uuid;
82
+
83
+ }
84
+
85
+ isEmpty() {
86
+
87
+ return ! this.geometry;
88
+
89
+ }
90
+
91
+ clone() {
92
+
93
+ const clone = new USDObject( MathUtils.generateUUID(), this.name, this.matrix, this.mesh, this.material );
94
+ clone.isDynamic = this.isDynamic;
95
+ return clone;
96
+
97
+ }
98
+
99
+ getPath() {
100
+
101
+ let current = this.parent;
102
+ let path = this.name;
103
+ while ( current ) {
104
+
105
+ path = current.name + '/' + path;
106
+ current = current.parent;
107
+
108
+ }
109
+
110
+ return '</' + path + '>';
111
+
112
+ }
113
+
114
+ add( child ) {
115
+
116
+ if ( child.parent ) {
117
+
118
+ child.parent.remove( child );
119
+
120
+ }
121
+
122
+ child.parent = this;
123
+ this.children.push( child );
124
+
125
+ }
126
+
127
+ remove( child ) {
128
+
129
+ const index = this.children.indexOf( child );
130
+ if ( index >= 0 ) {
131
+
132
+ if ( child.parent === this ) child.parent = null;
133
+ this.children.splice( index, 1 );
134
+
135
+ }
136
+
137
+ }
138
+
139
+ addEventListener( evt, listener ) {
140
+
141
+ if ( ! this._eventListeners[ evt ] ) this._eventListeners[ evt ] = [];
142
+ this._eventListeners[ evt ].push( listener );
143
+
144
+ }
145
+
146
+ removeEventListener( evt, listener ) {
147
+
148
+ if ( ! this._eventListeners[ evt ] ) return;
149
+ const index = this._eventListeners[ evt ].indexOf( listener );
150
+ if ( index >= 0 ) {
151
+
152
+ this._eventListeners[ evt ].splice( index, 1 );
153
+
154
+ }
155
+
156
+ }
157
+
158
+ onSerialize( writer, context ) {
159
+
160
+ const listeners = this._eventListeners[ 'serialize' ];
161
+ if ( listeners ) listeners.forEach( listener => listener( writer, context ) );
162
+
163
+ }
164
+
165
+ }
166
+
167
+
168
+ class USDDocument extends USDObject {
169
+
170
+ stageLength: number;
171
+
172
+ get isDocumentRoot() {
173
+
174
+ return true;
175
+
176
+ }
177
+ get isDynamic() {
178
+
179
+ return false;
180
+
181
+ }
182
+
183
+ constructor() {
184
+
185
+ super(undefined, 'StageRoot', new Matrix4(), null, null, null);
186
+ this.children = [];
187
+ this.stageLength = 200;
188
+
189
+ }
190
+
191
+ add( child: USDObject ) {
192
+
193
+ child.parent = this;
194
+ this.children.push( child );
195
+
196
+ }
197
+
198
+ remove( child: USDObject ) {
199
+
200
+ const index = this.children.indexOf( child );
201
+ if ( index >= 0 ) {
202
+
203
+ if ( child.parent === this ) child.parent = null;
204
+ this.children.splice( index, 1 );
205
+
206
+ }
207
+
208
+ }
209
+
210
+ traverse( callback, current: USDObject | null = null ) {
211
+
212
+ if ( current !== null ) callback( current );
213
+ else current = this;
214
+ if ( current.children ) {
215
+
216
+ for ( const child of current.children ) {
217
+
218
+ this.traverse( callback, child );
219
+
220
+ }
221
+
222
+ }
223
+
224
+ }
225
+
226
+ findById( uuid ) {
227
+
228
+ let found = false;
229
+ function search( current ) {
230
+
231
+ if ( found ) return;
232
+ if ( current.uuid === uuid ) {
233
+
234
+ found = true;
235
+ return current;
236
+
237
+ }
238
+
239
+ if ( current.children ) {
240
+
241
+ for ( const child of current.children ) {
242
+
243
+ const res = search( child );
244
+ if ( res ) return res;
245
+
246
+ }
247
+
248
+ }
249
+
250
+ }
251
+
252
+ return search( this );
253
+
254
+ }
255
+
256
+
257
+ buildHeader() {
258
+
259
+ return `#usda 1.0
260
+ (
261
+ customLayerData = {
262
+ string creator = "Three.js USDZExporter"
263
+ }
264
+ defaultPrim = "${makeNameSafe( this.name )}"
265
+ metersPerUnit = 1
266
+ upAxis = "Y"
267
+ startTimeCode = 0
268
+ endTimeCode = ${this.stageLength}
269
+ timeCodesPerSecond = 60
270
+ framesPerSecond = 60
271
+ )
272
+ `;
273
+
274
+ }
275
+
276
+ }
277
+
278
+ const newLine = '\n';
279
+
280
+ class USDWriter {
281
+ str: string;
282
+ indent: number;
283
+
284
+ constructor() {
285
+
286
+ this.str = '';
287
+ this.indent = 0;
288
+
289
+ }
290
+
291
+ clear() {
292
+
293
+ this.str = '';
294
+ this.indent = 0;
295
+
296
+ }
297
+
298
+ beginBlock( str ) {
299
+
300
+ str = this.applyIndent( str );
301
+ this.str += str;
302
+ this.str += newLine;
303
+ this.str += this.applyIndent( '{' );
304
+ this.str += newLine;
305
+ this.indent += 1;
306
+
307
+ }
308
+
309
+ closeBlock() {
310
+
311
+ this.indent -= 1;
312
+ this.str += this.applyIndent( '}' ) + newLine;
313
+
314
+ }
315
+
316
+ beginArray( str ) {
317
+
318
+ str = this.applyIndent( str + ' = [' );
319
+ this.str += str;
320
+ this.str += newLine;
321
+ this.indent += 1;
322
+
323
+ }
324
+
325
+ closeArray() {
326
+
327
+ this.indent -= 1;
328
+ this.str += this.applyIndent( ']' ) + newLine;
329
+
330
+ }
331
+
332
+ appendLine( str = '' ) {
333
+
334
+ str = this.applyIndent( str );
335
+ this.str += str;
336
+ this.str += newLine;
337
+
338
+ }
339
+
340
+ toString() {
341
+
342
+ return this.str;
343
+
344
+ }
345
+
346
+ applyIndent( str ) {
347
+
348
+ let indents = '';
349
+ for ( let i = 0; i < this.indent; i ++ ) indents += '\t';
350
+ return indents + str;
351
+
352
+ }
353
+
354
+ }
355
+
356
+ class USDZExporterContext {
357
+ root: any;
358
+ exporter: any;
359
+ extensions: any;
360
+ materials: {};
361
+ textures: {};
362
+ files: {};
363
+ document: USDDocument;
364
+ output: string;
365
+
366
+ constructor( root, exporter, extensions ) {
367
+
368
+ this.root = root;
369
+ this.exporter = exporter;
370
+
371
+ if ( extensions )
372
+ this.extensions = extensions;
373
+
374
+ this.materials = {};
375
+ this.textures = {};
376
+ this.files = {};
377
+ this.document = new USDDocument();
378
+ this.output = '';
379
+
380
+ }
381
+
382
+ }
383
+
384
+ class USDZExporterOptions {
385
+ ar: { anchoring: { type: string } } = { anchoring: { type: 'plane' } };
386
+ planeAnchoring: { alignment: string } = { alignment: 'horizontal' };
387
+ extensions: any[] = [];
388
+ }
389
+
390
+ class USDZExporter {
391
+ debug: boolean;
392
+ sceneAnchoringOptions: {} = {};
393
+ extensions: any;
394
+
395
+ constructor() {
396
+
397
+ this.debug = false;
398
+
399
+ }
400
+
401
+ async parse( scene, options: USDZExporterOptions = new USDZExporterOptions() ) {
402
+
403
+ options = Object.assign( {
404
+ ar: {
405
+ anchoring: { type: 'plane' },
406
+ planeAnchoring: { alignment: 'horizontal' }
407
+ },
408
+ extensions: []
409
+ }, options );
410
+
411
+ this.sceneAnchoringOptions = options;
412
+ // @ts-ignore
413
+ const context = new USDZExporterContext( scene, this, options.extensions );
414
+ this.extensions = context.extensions;
415
+
416
+ const files = context.files;
417
+ const modelFileName = 'model.usda';
418
+
419
+ // model file should be first in USDZ archive so we init it here
420
+ files[ modelFileName ] = null;
421
+
422
+ const materials = context.materials;
423
+ const textures = context.textures;
424
+
425
+ invokeAll( context, 'onBeforeBuildDocument' );
426
+
427
+ traverseVisible( scene, context.document, context );
428
+
429
+ invokeAll( context, 'onAfterBuildDocument' );
430
+
431
+ parseDocument( context );
432
+
433
+ invokeAll( context, 'onAfterSerialize' );
434
+
435
+ context.output += buildMaterials( materials, textures );
436
+
437
+ const header = context.document.buildHeader();
438
+ const final = header + '\n' + context.output;
439
+
440
+ // full output file
441
+ if ( this.debug )
442
+ console.log( final );
443
+
444
+ files[ modelFileName ] = fflate.strToU8( final );
445
+ context.output = '';
446
+
447
+ for ( const id in textures ) {
448
+
449
+ let texture = textures[ id ];
450
+ const isRGBA = texture.format === 1023;
451
+ if ( texture.isCompressedTexture ) {
452
+
453
+ texture = copyTexture( texture );
454
+
455
+ }
456
+
457
+ // TODO add readback options for textures that don't have texture.image
458
+ const canvas = await imageToCanvas( texture.image );
459
+
460
+ if ( canvas ) {
461
+
462
+ const blob = await new Promise( resolve => canvas.toBlob( resolve, isRGBA ? 'image/png' : 'image/jpeg', 1 ) ) as any;
463
+ files[ `textures/Texture_${id}.${isRGBA ? 'png' : 'jpg'}` ] = new Uint8Array( await blob.arrayBuffer() );
464
+
465
+ } else {
466
+
467
+ console.warn( 'Can`t export texture: ', texture );
468
+
469
+ }
470
+
471
+ }
472
+
473
+ // 64 byte alignment
474
+ // https://github.com/101arrowz/fflate/issues/39#issuecomment-777263109
475
+
476
+ let offset = 0;
477
+
478
+ for ( const filename in files ) {
479
+
480
+ const file = files[ filename ];
481
+ const headerSize = 34 + filename.length;
482
+
483
+ offset += headerSize;
484
+
485
+ const offsetMod64 = offset & 63;
486
+
487
+ if ( offsetMod64 !== 4 ) {
488
+
489
+ const padLength = 64 - offsetMod64;
490
+ const padding = new Uint8Array( padLength );
491
+
492
+ files[ filename ] = [ file, { extra: { 12345: padding } } ];
493
+
494
+ }
495
+
496
+ offset = file.length;
497
+
498
+ }
499
+
500
+ return fflate.zipSync( files, { level: 0 } );
501
+
502
+ }
503
+
504
+ }
505
+
506
+ function traverseVisible( object, parentModel, context ) {
507
+
508
+ if ( ! object.visible ) return;
509
+
510
+ let model: USDObject | undefined = undefined;
511
+ const geometry = object.geometry;
512
+ const material = object.material;
513
+
514
+
515
+ if ( object.isMesh && material && (material.isMeshStandardMaterial || material.isMeshBasicMaterial) && ! object.isSkinnedMesh ) {
516
+
517
+ const name = getObjectId( object );
518
+ model = new USDObject( object.uuid, name, object.matrix, geometry, material );
519
+
520
+ } else if ( object.isCamera ) {
521
+
522
+ const name = getObjectId( object );
523
+ model = new USDObject( object.uuid, name, object.matrix, undefined, undefined, object );
524
+
525
+ } else {
526
+
527
+ const name = getObjectId( object );
528
+ model = new USDObject( object.uuid, name, object.matrix );
529
+
530
+ }
531
+
532
+ if ( model ) {
533
+
534
+ if ( parentModel ) {
535
+
536
+ parentModel.add( model );
537
+
538
+ }
539
+
540
+ parentModel = model;
541
+
542
+ if ( context.extensions ) {
543
+
544
+ for ( const ext of context.extensions ) {
545
+
546
+ if ( ext.onExportObject ) ext.onExportObject.call( ext, object, model, context );
547
+
548
+ }
549
+
550
+ }
551
+
552
+ } else {
553
+
554
+ const name = getObjectId( object );
555
+ const empty = new USDObject( object.uuid, name, object.matrix );
556
+ if ( parentModel ) {
557
+
558
+ parentModel.add( empty );
559
+
560
+ }
561
+
562
+ parentModel = empty;
563
+
564
+ }
565
+
566
+ for ( const ch of object.children ) {
567
+
568
+ traverseVisible( ch, parentModel, context );
569
+
570
+ }
571
+
572
+ }
573
+
574
+ function parseDocument( context: USDZExporterContext ) {
575
+
576
+ for ( const child of context.document.children ) {
577
+
578
+ addResources( child, context );
579
+
580
+ }
581
+
582
+ const writer = new USDWriter();
583
+
584
+ writer.beginBlock( `def Xform "${context.document.name}"` );
585
+
586
+ writer.beginBlock( `def Scope "Scenes" (
587
+ kind = "sceneLibrary"
588
+ )` );
589
+
590
+ writer.beginBlock( `def Xform "Scene" (
591
+ apiSchemas = ["Preliminary_AnchoringAPI"]
592
+ customData = {
593
+ bool preliminary_collidesWithEnvironment = 0
594
+ string sceneName = "Scene"
595
+ }
596
+ sceneName = "Scene"
597
+ )` );
598
+
599
+ writer.appendLine( `token preliminary:anchoring:type = "${context.exporter.sceneAnchoringOptions.ar.anchoring.type}"` );
600
+ if (context.exporter.sceneAnchoringOptions.ar.anchoring.type === 'plane')
601
+ writer.appendLine( `token preliminary:planeAnchoring:alignment = "${context.exporter.sceneAnchoringOptions.ar.planeAnchoring.alignment}"` );
602
+ // 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.
603
+ if (context.exporter.sceneAnchoringOptions.ar.anchoring.type === 'image')
604
+ writer.appendLine( `rel preliminary:imageAnchoring:referenceImage = </${context.document.name}/Scenes/Scene/AnchoringReferenceImage>` );
605
+ writer.appendLine();
606
+
607
+ for ( const child of context.document.children ) {
608
+
609
+ buildXform( child, writer, context );
610
+
611
+ }
612
+
613
+ invokeAll( context, 'onAfterHierarchy', writer );
614
+
615
+ writer.closeBlock();
616
+ writer.closeBlock();
617
+ writer.closeBlock();
618
+
619
+ context.output += writer.toString();
620
+
621
+ }
622
+
623
+ function addResources( object, context: USDZExporterContext ) {
624
+
625
+ const geometry = object.geometry;
626
+ let material = object.material;
627
+
628
+ if ( geometry ) {
629
+
630
+ if ( material.isMeshStandardMaterial ) { // || material.isMeshBasicMaterial // TODO convert unlit to lit+emissive
631
+
632
+ const geometryFileName = 'geometries/Geometry_' + geometry.id + '.usd';
633
+
634
+ if ( ! ( geometryFileName in context.files ) ) {
635
+
636
+ const meshObject = buildMeshObject( geometry );
637
+ context.files[ geometryFileName ] = buildUSDFileAsString( meshObject, context );
638
+
639
+ }
640
+
641
+ if ( ! ( material.uuid in context.materials ) ) {
642
+
643
+ context.materials[ material.uuid ] = material;
644
+
645
+ }
646
+
647
+ } else {
648
+
649
+ console.warn( 'THREE.USDZExporter: Unsupported material type (USDZ only supports MeshStandardMaterial)', name );
650
+
651
+ }
652
+
653
+ }
654
+
655
+ for ( const ch of object.children ) {
656
+
657
+ addResources( ch, context );
658
+
659
+ }
660
+
661
+ }
662
+
663
+ function invokeAll( context: USDZExporterContext, name: string, writer: USDWriter | null = null ) {
664
+
665
+ if ( context.extensions ) {
666
+
667
+ for ( const ext of context.extensions ) {
668
+
669
+ if ( typeof ext[ name ] === 'function' )
670
+ ext[ name ]( context, writer );
671
+
672
+ }
673
+
674
+ }
675
+
676
+ }
677
+
678
+ function copyTexture( texture ) {
679
+
680
+ const geometry = new PlaneGeometry( 2, 2, 1, 1 );
681
+ const material = new ShaderMaterial( {
682
+ uniforms: { blitTexture: new Uniform( texture ) },
683
+ vertexShader: `
684
+ varying vec2 vUv;
685
+ void main(){
686
+ vUv = uv;
687
+ gl_Position = vec4(position.xy * 1.0,0.,.999999);
688
+ }`,
689
+ fragmentShader: `
690
+ uniform sampler2D blitTexture;
691
+ varying vec2 vUv;
692
+ void main(){
693
+ gl_FragColor = vec4(vUv.xy, 0, 1);
694
+ gl_FragColor = texture2D( blitTexture, vUv);
695
+ }`
696
+ } );
697
+
698
+ const mesh = new Mesh( geometry, material );
699
+ mesh.frustumCulled = false;
700
+ const cam = new PerspectiveCamera();
701
+ const scene = new Scene();
702
+ scene.add( mesh );
703
+ const renderer = new WebGLRenderer( { antialias: false } );
704
+ renderer.setSize( texture.image.width, texture.image.height );
705
+ renderer.clear();
706
+ renderer.render( scene, cam );
707
+
708
+ return new Texture( renderer.domElement );
709
+
710
+ }
711
+
712
+
713
+ function isImageBitmap( image ) {
714
+
715
+ return ( typeof HTMLImageElement !== 'undefined' && image instanceof HTMLImageElement ) ||
716
+ ( typeof HTMLCanvasElement !== 'undefined' && image instanceof HTMLCanvasElement ) ||
717
+ ( typeof OffscreenCanvas !== 'undefined' && image instanceof OffscreenCanvas ) ||
718
+ ( typeof ImageBitmap !== 'undefined' && image instanceof ImageBitmap );
719
+
720
+ }
721
+
722
+ async function imageToCanvas( image, color: string | undefined = undefined, flipY = false ) {
723
+
724
+ if ( isImageBitmap( image ) ) {
725
+
726
+ const scale = 1024 / Math.max( image.width, image.height );
727
+
728
+ const canvas = document.createElement( 'canvas' );
729
+ canvas.width = image.width * Math.min( 1, scale );
730
+ canvas.height = image.height * Math.min( 1, scale );
731
+
732
+ const context = canvas.getContext( '2d' );
733
+ if (!context) throw new Error('Could not get canvas 2D context');
734
+
735
+ if ( flipY === true ) {
736
+
737
+ context.translate( 0, canvas.height );
738
+ context.scale( 1, - 1 );
739
+
740
+ }
741
+
742
+ context.drawImage( image, 0, 0, canvas.width, canvas.height );
743
+
744
+ // TODO remove, not used anymore
745
+ if ( color !== undefined ) {
746
+
747
+ const hex = parseInt( color, 16 );
748
+
749
+ const r = ( hex >> 16 & 255 ) / 255;
750
+ const g = ( hex >> 8 & 255 ) / 255;
751
+ const b = ( hex & 255 ) / 255;
752
+
753
+ const imagedata = context.getImageData( 0, 0, canvas.width, canvas.height );
754
+ const data = imagedata.data;
755
+
756
+ for ( let i = 0; i < data.length; i += 4 ) {
757
+
758
+ data[ i + 0 ] = data[ i + 0 ] * r;
759
+ data[ i + 1 ] = data[ i + 1 ] * g;
760
+ data[ i + 2 ] = data[ i + 2 ] * b;
761
+
762
+ }
763
+
764
+ context.putImageData( imagedata, 0, 0 );
765
+
766
+ }
767
+
768
+ return canvas;
769
+
770
+ } else {
771
+
772
+ throw new Error( 'THREE.USDZExporter: No valid image data found. Unable to process texture.' );
773
+
774
+ }
775
+
776
+ }
777
+
778
+ //
779
+
780
+ const PRECISION = 7;
781
+
782
+ function buildHeader() {
783
+
784
+ return `#usda 1.0
785
+ (
786
+ customLayerData = {
787
+ string creator = "Three.js USDZExporter"
788
+ }
789
+ metersPerUnit = 1
790
+ upAxis = "Y"
791
+ )
792
+ `;
793
+
794
+ }
795
+
796
+ function buildUSDFileAsString( dataToInsert, _context: USDZExporterContext ) {
797
+
798
+ let output = buildHeader();
799
+ output += dataToInsert;
800
+ return fflate.strToU8( output );
801
+
802
+ }
803
+
804
+ function getObjectId( object ) {
805
+
806
+ return object.name.replace( /[-<>\(\)\[\]§$%&\/\\\=\?\,\;]/g, '' ) + '_' + object.id;
807
+
808
+ }
809
+
810
+ // Xform
811
+
812
+ export function buildXform( model, writer, context ) {
813
+
814
+ const matrix = model.matrix;
815
+ const geometry = model.geometry;
816
+ const material = model.material;
817
+ const camera = model.camera;
818
+ const name = model.name;
819
+ const transform = buildMatrix( matrix );
820
+
821
+ if ( matrix.determinant() < 0 ) {
822
+
823
+ console.warn( 'THREE.USDZExporter: USDZ does not support negative scales', name );
824
+
825
+ }
826
+
827
+ if ( geometry )
828
+ writer.beginBlock( `def Xform "${name}" (prepend references = @./geometries/Geometry_${geometry.id}.usd@</Geometry>)` );
829
+ else if ( camera )
830
+ writer.beginBlock( `def Camera "${name}"` );
831
+ else
832
+ writer.beginBlock( `def Xform "${name}"` );
833
+
834
+ if ( material )
835
+ writer.appendLine( `rel material:binding = </Materials/Material_${material.id}>` );
836
+ writer.appendLine( `matrix4d xformOp:transform = ${transform}` );
837
+ writer.appendLine( 'uniform token[] xformOpOrder = ["xformOp:transform"]' );
838
+
839
+ if ( camera ) {
840
+
841
+ if ( camera.isOrthographicCamera ) {
842
+
843
+ writer.appendLine( `float2 clippingRange = (${camera.near}, ${camera.far})` );
844
+ writer.appendLine( `float horizontalAperture = ${( ( Math.abs( camera.left ) + Math.abs( camera.right ) ) * 10 ).toPrecision( PRECISION )}` );
845
+ writer.appendLine( `float verticalAperture = ${( ( Math.abs( camera.top ) + Math.abs( camera.bottom ) ) * 10 ).toPrecision( PRECISION )}` );
846
+ writer.appendLine( 'token projection = "orthographic"' );
847
+
848
+ } else {
849
+
850
+ writer.appendLine( `float2 clippingRange = (${camera.near.toPrecision( PRECISION )}, ${camera.far.toPrecision( PRECISION )})` );
851
+ writer.appendLine( `float focalLength = ${camera.getFocalLength().toPrecision( PRECISION )}` );
852
+ writer.appendLine( `float focusDistance = ${camera.focus.toPrecision( PRECISION )}` );
853
+ writer.appendLine( `float horizontalAperture = ${camera.getFilmWidth().toPrecision( PRECISION )}` );
854
+ writer.appendLine( 'token projection = "perspective"' );
855
+ writer.appendLine( `float verticalAperture = ${camera.getFilmHeight().toPrecision( PRECISION )}` );
856
+
857
+ }
858
+
859
+ }
860
+
861
+ if ( model.onSerialize ) {
862
+
863
+ model.onSerialize( writer, context );
864
+
865
+ }
866
+
867
+ if ( model.children ) {
868
+
869
+ writer.appendLine();
870
+ for ( const ch of model.children ) {
871
+
872
+ buildXform( ch, writer, context );
873
+
874
+ }
875
+
876
+ }
877
+
878
+ writer.closeBlock();
879
+
880
+ }
881
+
882
+ function fn( num ) {
883
+
884
+ return num.toFixed( 10 );
885
+
886
+ }
887
+
888
+ function buildMatrix( matrix ) {
889
+
890
+ const array = matrix.elements;
891
+
892
+ return `( ${buildMatrixRow( array, 0 )}, ${buildMatrixRow( array, 4 )}, ${buildMatrixRow( array, 8 )}, ${buildMatrixRow( array, 12 )} )`;
893
+
894
+ }
895
+
896
+ function buildMatrixRow( array, offset ) {
897
+
898
+ return `(${fn( array[ offset + 0 ] )}, ${fn( array[ offset + 1 ] )}, ${fn( array[ offset + 2 ] )}, ${fn( array[ offset + 3 ] )})`;
899
+
900
+ }
901
+
902
+ // Mesh
903
+
904
+ function buildMeshObject( geometry ) {
905
+
906
+ const mesh = buildMesh( geometry );
907
+ return `
908
+ def "Geometry"
909
+ {
910
+ ${mesh}
911
+ }
912
+ `;
913
+
914
+ }
915
+
916
+ function buildMesh( geometry ) {
917
+
918
+ const name = 'Geometry';
919
+ const attributes = geometry.attributes;
920
+ const count = attributes.position.count;
921
+
922
+ return `
923
+ def Mesh "${name}"
924
+ {
925
+ int[] faceVertexCounts = [${buildMeshVertexCount( geometry )}]
926
+ int[] faceVertexIndices = [${buildMeshVertexIndices( geometry )}]
927
+ normal3f[] normals = [${buildVector3Array( attributes.normal, count )}] (
928
+ interpolation = "vertex"
929
+ )
930
+ point3f[] points = [${buildVector3Array( attributes.position, count )}]
931
+ ${attributes.uv ?
932
+ `float2[] primvars:st = [${buildVector2Array( attributes.uv, count )}] (
933
+ interpolation = "vertex"
934
+ )` : '' }
935
+ ${attributes.uv2 ?
936
+ `float2[] primvars:st2 = [${buildVector2Array( attributes.uv2, count )}] (
937
+ interpolation = "vertex"
938
+ )` : '' }
939
+ uniform token subdivisionScheme = "none"
940
+ }
941
+ `;
942
+
943
+ }
944
+
945
+ function buildMeshVertexCount( geometry ) {
946
+
947
+ const count = geometry.index !== null ? geometry.index.count : geometry.attributes.position.count;
948
+
949
+ return Array( count / 3 ).fill( 3 ).join( ', ' );
950
+
951
+ }
952
+
953
+ function buildMeshVertexIndices( geometry: BufferGeometry ) {
954
+
955
+ const index = geometry.index;
956
+ const array: Array<number> = [];
957
+
958
+ if ( index !== null ) {
959
+
960
+ for ( let i = 0; i < index.count; i ++ ) {
961
+
962
+ array.push( index.getX( i ) );
963
+
964
+ }
965
+
966
+ } else {
967
+
968
+ const length = geometry.attributes.position.count;
969
+
970
+ for ( let i = 0; i < length; i ++ ) {
971
+
972
+ array.push( i );
973
+
974
+ }
975
+
976
+ }
977
+
978
+ return array.join( ', ' );
979
+
980
+ }
981
+
982
+ function buildVector3Array( attribute, count ) {
983
+
984
+ if ( attribute === undefined ) {
985
+
986
+ console.warn( 'USDZExporter: Normals missing.' );
987
+ return Array( count ).fill( '(0, 0, 0)' ).join( ', ' );
988
+
989
+ }
990
+
991
+ const array: Array<string> = [];
992
+
993
+ for ( let i = 0; i < attribute.count; i ++ ) {
994
+
995
+ const x = attribute.getX( i );
996
+ const y = attribute.getY( i );
997
+ const z = attribute.getZ( i );
998
+
999
+ array.push( `(${x.toPrecision( PRECISION )}, ${y.toPrecision( PRECISION )}, ${z.toPrecision( PRECISION )})` );
1000
+
1001
+ }
1002
+
1003
+ return array.join( ', ' );
1004
+
1005
+ }
1006
+
1007
+ function buildVector2Array( attribute, count ) {
1008
+
1009
+ if ( attribute === undefined ) {
1010
+
1011
+ console.warn( 'USDZExporter: UVs missing.' );
1012
+ return Array( count ).fill( '(0, 0)' ).join( ', ' );
1013
+
1014
+ }
1015
+
1016
+ const array: Array<string> = [];
1017
+
1018
+ for ( let i = 0; i < attribute.count; i ++ ) {
1019
+
1020
+ const x = attribute.getX( i );
1021
+ const y = attribute.getY( i );
1022
+
1023
+ array.push( `(${x.toPrecision( PRECISION )}, ${1 - y.toPrecision( PRECISION )})` );
1024
+
1025
+ }
1026
+
1027
+ return array.join( ', ' );
1028
+
1029
+ }
1030
+
1031
+ // Materials
1032
+
1033
+ function buildMaterials( materials, textures ) {
1034
+
1035
+ const array: Array<string> = [];
1036
+
1037
+ for ( const uuid in materials ) {
1038
+
1039
+ const material = materials[ uuid ];
1040
+
1041
+ array.push( buildMaterial( material, textures ) );
1042
+
1043
+ }
1044
+
1045
+ return `def "Materials"
1046
+ {
1047
+ ${array.join( '' )}
1048
+ }
1049
+
1050
+ `;
1051
+
1052
+ }
1053
+
1054
+ function buildMaterial( material, textures ) {
1055
+
1056
+ // https://graphics.pixar.com/usd/docs/UsdPreviewSurface-Proposal.html
1057
+
1058
+ const pad = ' ';
1059
+ const inputs: Array<string> = [];
1060
+ const samplers: Array<string> = [];
1061
+ const exportForQuickLook = true;
1062
+
1063
+ function buildTexture( texture, mapType, color: Color | undefined = undefined, opacity: number | undefined = undefined ) {
1064
+
1065
+ const id = texture.id + ( color ? '_' + color.getHexString() : '' ) + ( opacity ? '_' + opacity : '' );
1066
+ const isRGBA = texture.format === 1023;
1067
+
1068
+ const wrapS = ( texture.wrapS == RepeatWrapping ) ? 'repeat' : ( texture.wrapS == MirroredRepeatWrapping ? 'mirror' : 'clamp' );
1069
+ const wrapT = ( texture.wrapT == RepeatWrapping ) ? 'repeat' : ( texture.wrapT == MirroredRepeatWrapping ? 'mirror' : 'clamp' );
1070
+
1071
+ const repeat = texture.repeat.clone();
1072
+ const offset = texture.offset.clone();
1073
+
1074
+ // texture coordinates start in the opposite corner, need to correct
1075
+ offset.y = 1 - offset.y - repeat.y;
1076
+
1077
+ // turns out QuickLook is buggy and interprets texture repeat inverted.
1078
+ // Apple Feedback: FB10036297 and FB11442287
1079
+ if ( exportForQuickLook ) {
1080
+
1081
+ offset.x = offset.x / repeat.x;
1082
+ offset.y = offset.y / repeat.y;
1083
+
1084
+ }
1085
+
1086
+ textures[ id ] = texture;
1087
+ const uvReader = mapType == 'occlusion' ? 'uvReader_st2' : 'uvReader_st';
1088
+
1089
+ const needsTextureTransform = ( repeat.x != 1 || repeat.y != 1 || offset.x != 0 || offset.y != 0 );
1090
+ const textureTransformInput = `</Materials/Material_${material.id}/${uvReader}.outputs:result>`;
1091
+ const textureTransformOutput = `</Materials/Material_${material.id}/Transform2d_${mapType}.outputs:result>`;
1092
+
1093
+ return `
1094
+ ${needsTextureTransform ? `def Shader "Transform2d_${mapType}" (
1095
+ sdrMetadata = {
1096
+ string role = "math"
1097
+ }
1098
+ )
1099
+ {
1100
+ uniform token info:id = "UsdTransform2d"
1101
+ float2 inputs:in.connect = ${textureTransformInput}
1102
+ float2 inputs:scale = ${buildVector2( repeat )}
1103
+ float2 inputs:translation = ${buildVector2( offset )}
1104
+ float2 outputs:result
1105
+ }
1106
+ ` : '' }
1107
+ def Shader "Texture_${texture.id}_${mapType}"
1108
+ {
1109
+ uniform token info:id = "UsdUVTexture"
1110
+ asset inputs:file = @textures/Texture_${id}.${isRGBA ? 'png' : 'jpg'}@
1111
+ float2 inputs:st.connect = ${needsTextureTransform ? textureTransformOutput : textureTransformInput}
1112
+ float4 inputs:scale = (${color ? color.r + ', ' + color.g + ', ' + color.b : '1, 1, 1'}, ${opacity ? opacity : '1'})
1113
+ token inputs:wrapS = "${wrapS}"
1114
+ token inputs:wrapT = "${wrapT}"
1115
+ float outputs:r
1116
+ float outputs:g
1117
+ float outputs:b
1118
+ float3 outputs:rgb
1119
+ ${material.transparent || material.alphaTest > 0.0 ? 'float outputs:a' : ''}
1120
+ }`;
1121
+
1122
+ }
1123
+
1124
+ const effectiveOpacity = ( material.transparent || material.alphaTest ) ? material.opacity : 1;
1125
+
1126
+ if ( material.side === DoubleSide ) {
1127
+
1128
+ console.warn( 'THREE.USDZExporter: USDZ does not support double sided materials', material );
1129
+
1130
+ }
1131
+
1132
+ if ( material.map !== null ) {
1133
+
1134
+ inputs.push( `${pad}color3f inputs:diffuseColor.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:rgb>` );
1135
+
1136
+ if ( material.transparent ) {
1137
+
1138
+ inputs.push( `${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>` );
1139
+
1140
+ } else if ( material.alphaTest > 0.0 ) {
1141
+
1142
+ inputs.push( `${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.map.id}_diffuse.outputs:a>` );
1143
+ inputs.push( `${pad}float inputs:opacityThreshold = ${material.alphaTest}` );
1144
+
1145
+ }
1146
+
1147
+ samplers.push( buildTexture( material.map, 'diffuse', material.color, effectiveOpacity ) );
1148
+
1149
+ } else {
1150
+
1151
+ inputs.push( `${pad}color3f inputs:diffuseColor = ${buildColor( material.color )}` );
1152
+
1153
+ }
1154
+
1155
+ if ( material.emissiveMap ) {
1156
+
1157
+ inputs.push( `${pad}color3f inputs:emissiveColor.connect = </Materials/Material_${material.id}/Texture_${material.emissiveMap.id}_emissive.outputs:rgb>` );
1158
+
1159
+ samplers.push( buildTexture( material.emissiveMap, 'emissive' ) );
1160
+
1161
+ } else if ( material.emissive?.getHex() > 0 ) {
1162
+
1163
+ inputs.push( `${pad}color3f inputs:emissiveColor = ${buildColor( material.emissive )}` );
1164
+
1165
+ } else {
1166
+
1167
+ inputs.push( `${pad}color3f inputs:emissiveColor = (0, 0, 0)` );
1168
+
1169
+ }
1170
+
1171
+ if ( material.normalMap ) {
1172
+
1173
+ inputs.push( `${pad}normal3f inputs:normal.connect = </Materials/Material_${material.id}/Texture_${material.normalMap.id}_normal.outputs:rgb>` );
1174
+
1175
+ samplers.push( buildTexture( material.normalMap, 'normal' ) );
1176
+
1177
+ }
1178
+
1179
+ if ( material.aoMap ) {
1180
+
1181
+ inputs.push( `${pad}float inputs:occlusion.connect = </Materials/Material_${material.id}/Texture_${material.aoMap.id}_occlusion.outputs:r>` );
1182
+
1183
+ samplers.push( buildTexture( material.aoMap, 'occlusion' ) );
1184
+
1185
+ }
1186
+
1187
+ if ( material.roughnessMap && material.roughness === 1 ) {
1188
+
1189
+ inputs.push( `${pad}float inputs:roughness.connect = </Materials/Material_${material.id}/Texture_${material.roughnessMap.id}_roughness.outputs:g>` );
1190
+
1191
+ samplers.push( buildTexture( material.roughnessMap, 'roughness' ) );
1192
+
1193
+ } else {
1194
+
1195
+ inputs.push( `${pad}float inputs:roughness = ${material.roughness}` );
1196
+
1197
+ }
1198
+
1199
+ if ( material.metalnessMap && material.metalness === 1 ) {
1200
+
1201
+ inputs.push( `${pad}float inputs:metallic.connect = </Materials/Material_${material.id}/Texture_${material.metalnessMap.id}_metallic.outputs:b>` );
1202
+
1203
+ samplers.push( buildTexture( material.metalnessMap, 'metallic' ) );
1204
+
1205
+ } else {
1206
+
1207
+ inputs.push( `${pad}float inputs:metallic = ${material.metalness}` );
1208
+
1209
+ }
1210
+
1211
+ if ( material.alphaMap ) {
1212
+
1213
+ inputs.push( `${pad}float inputs:opacity.connect = </Materials/Material_${material.id}/Texture_${material.alphaMap.id}_opacity.outputs:r>` );
1214
+ inputs.push( `${pad}float inputs:opacityThreshold = 0.0001` );
1215
+
1216
+ samplers.push( buildTexture( material.alphaMap, 'opacity' ) );
1217
+
1218
+ } else {
1219
+
1220
+ inputs.push( `${pad}float inputs:opacity = ${effectiveOpacity}` );
1221
+
1222
+ }
1223
+
1224
+ if ( material.isMeshPhysicalMaterial ) {
1225
+
1226
+ inputs.push( `${pad}float inputs:clearcoat = ${material.clearcoat}` );
1227
+ inputs.push( `${pad}float inputs:clearcoatRoughness = ${material.clearcoatRoughness}` );
1228
+ inputs.push( `${pad}float inputs:ior = ${material.ior}` );
1229
+
1230
+ }
1231
+
1232
+ return `
1233
+ def Material "Material_${material.id}"
1234
+ {
1235
+ def Shader "PreviewSurface"
1236
+ {
1237
+ uniform token info:id = "UsdPreviewSurface"
1238
+ ${inputs.join( '\n' )}
1239
+ int inputs:useSpecularWorkflow = 0
1240
+ token outputs:surface
1241
+ }
1242
+
1243
+ token outputs:surface.connect = </Materials/Material_${material.id}/PreviewSurface.outputs:surface>
1244
+
1245
+ def Shader "uvReader_st"
1246
+ {
1247
+ uniform token info:id = "UsdPrimvarReader_float2"
1248
+ token inputs:varname = "st"
1249
+ float2 inputs:fallback = (0.0, 0.0)
1250
+ float2 outputs:result
1251
+ }
1252
+
1253
+ def Shader "uvReader_st2"
1254
+ {
1255
+ uniform token info:id = "UsdPrimvarReader_float2"
1256
+ token inputs:varname = "st2"
1257
+ float2 inputs:fallback = (0.0, 0.0)
1258
+ float2 outputs:result
1259
+ }
1260
+
1261
+ ${samplers.join( '\n' )}
1262
+
1263
+ }
1264
+ `;
1265
+
1266
+ }
1267
+
1268
+ function buildColor( color ) {
1269
+
1270
+ return `(${color.r}, ${color.g}, ${color.b})`;
1271
+
1272
+ }
1273
+
1274
+ function buildVector2( vector ) {
1275
+
1276
+ return `(${ vector.x }, ${ vector.y })`;
1277
+
1278
+ }
1279
+
1280
+ export { USDZExporter, USDZExporterContext, USDWriter, USDObject, buildMatrix, USDDocument, makeNameSafe as makeNameSafeForUSD, imageToCanvas };
src/engine-components/export/usdz/extensions/USDZText.ts ADDED
@@ -0,0 +1,142 @@
1
+ import { IBehaviorElement } from "../extensions/behavior/BehavioursBuilder";
2
+ import { USDDocument, USDWriter } from "../ThreeUSDZExporter";
3
+
4
+
5
+ export enum TextWrapMode {
6
+ singleLine = "singleLine",
7
+ hardBreaks = "hardBreaks",
8
+ flowing = "flowing",
9
+ }
10
+
11
+ export enum HorizontalAlignment {
12
+ left = "left",
13
+ center = "center",
14
+ right = "right",
15
+ justified = "justified"
16
+ }
17
+
18
+ export enum VerticalAlignment {
19
+ top = "top",
20
+ middle = "middle",
21
+ lowerMiddle = "lowerMiddle",
22
+ baseline = "baseline",
23
+ bottom = "bottom"
24
+ }
25
+
26
+ export class USDZText implements IBehaviorElement {
27
+
28
+ static global_id: number = 0;
29
+ static getId(): number {
30
+ return this.global_id++;
31
+ }
32
+
33
+ id: string;
34
+ content: string = "";
35
+ font?: string[] = [];
36
+ pointSize: number = 144;
37
+ width?: number;
38
+ height?: number;
39
+ depth?: number;
40
+ wrapMode?: TextWrapMode;
41
+ horizontalAlignment?: HorizontalAlignment;
42
+ verticalAlignment?: VerticalAlignment;
43
+
44
+ setDepth(depth: number): USDZText {
45
+ this.depth = depth;
46
+ return this;
47
+ }
48
+
49
+ setPointSize(pointSize: number): USDZText {
50
+ this.pointSize = pointSize;
51
+ return this;
52
+ }
53
+
54
+ setHorizontalAlignment(align: HorizontalAlignment) {
55
+ this.horizontalAlignment = align;
56
+ return this;
57
+ }
58
+
59
+ setVerticalAlignment(align: VerticalAlignment) {
60
+ this.verticalAlignment = align;
61
+ return this;
62
+ }
63
+
64
+ constructor(id: string) {
65
+ this.id = id;
66
+ }
67
+
68
+ writeTo(_document: USDDocument | undefined, writer: USDWriter) {
69
+
70
+
71
+ writer.beginBlock(`def Preliminary_Text "${this.id}"`);
72
+
73
+ if (this.content)
74
+ writer.appendLine(`string content = "${this.content}"`);
75
+
76
+ if (!this.font || this.font.length <= 0) {
77
+ this.font ||= [];
78
+ this.font?.push("sans-serif");
79
+ }
80
+ const str = this.font.map(s => `"${s}"`).join(", ");
81
+ writer.appendLine(`string[] font = [ ${str} ]`);
82
+
83
+ writer.appendLine(`double pointSize = ${this.pointSize}`);
84
+ if (typeof this.width === "number")
85
+ writer.appendLine(`double width = ${this.width}`);
86
+ if (typeof this.height === "number")
87
+ writer.appendLine(`double height = ${this.height}`);
88
+ if (typeof this.depth === "number")
89
+ writer.appendLine(`double depth = ${this.depth}`);
90
+ if (this.wrapMode)
91
+ writer.appendLine(`token wrapMode = "${this.wrapMode}"`);
92
+ if (this.horizontalAlignment)
93
+ writer.appendLine(`token horizontalAlignment = "${this.horizontalAlignment}"`);
94
+ if (this.verticalAlignment)
95
+ writer.appendLine(`token verticalAlignment = "${this.verticalAlignment}"`);
96
+
97
+ writer.closeBlock();
98
+
99
+ }
100
+
101
+ }
102
+
103
+
104
+ export class TextBuilder {
105
+ static singleLine(str: string, pointSize?: number, depth?: number): USDZText {
106
+
107
+ const text = new USDZText("text_" + USDZText.getId());
108
+ text.content = str;
109
+ if (pointSize)
110
+ text.pointSize = pointSize;
111
+ if (depth)
112
+ text.depth = depth;
113
+ return text;
114
+ }
115
+
116
+ static multiLine(str: string, width: number, height: number, horizontal: HorizontalAlignment, vertical: VerticalAlignment, wrapMode?: TextWrapMode) {
117
+ const text = new USDZText("text_" + USDZText.getId());
118
+ text.content = str;
119
+ text.width = width;
120
+ text.height = height;
121
+ text.horizontalAlignment = horizontal;
122
+ text.verticalAlignment = vertical;
123
+ if (wrapMode)
124
+ text.wrapMode = wrapMode;
125
+ return text;
126
+ }
127
+ }
128
+
129
+
130
+ export class TextExtension {
131
+ onExportObject(_object, model, _context) {
132
+ model.addEventListener("serialize", (writer, _context) => {
133
+ const text = TextBuilder.multiLine("Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
134
+ 1, 1, HorizontalAlignment.justified, VerticalAlignment.top);
135
+ text.pointSize = 300;
136
+ text.depth = .01;
137
+ text.writeTo(undefined, writer);
138
+ });
139
+ }
140
+ }
141
+
142
+