Needle Engine

Changes between version 3.47.8 and 3.48.0
Files changed (28) hide show
  1. plugins/vite/poster.js +17 -11
  2. src/engine-components/export/usdz/extensions/behavior/Actions.ts +1 -1
  3. src/engine-components/export/usdz/extensions/Animation.ts +18 -14
  4. src/engine-components/export/usdz/extensions/behavior/Behaviour.ts +58 -1
  5. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +4 -4
  6. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +8 -3
  7. src/engine/engine_components.ts +2 -2
  8. src/engine/engine_input.ts +6 -1
  9. src/engine/engine_loaders.ts +2 -14
  10. src/engine/engine_physics_rapier.ts +8 -6
  11. src/engine/engine_three_utils.ts +8 -1
  12. src/engine/engine_types.ts +5 -2
  13. src/engine-components/ui/EventSystem.ts +1 -1
  14. src/engine-components/utils/LookAt.ts +2 -2
  15. src/engine/webcomponents/needle menu/needle-menu-spatial.ts +19 -3
  16. src/engine/webcomponents/needle menu/needle-menu.ts +8 -0
  17. src/engine/xr/NeedleXRController.ts +70 -19
  18. src/engine/xr/NeedleXRSession.ts +2 -1
  19. src/engine-components/postprocessing/PostProcessingEffect.ts +2 -0
  20. src/engine-components/postprocessing/PostProcessingHandler.ts +1 -1
  21. src/engine-components/RigidBody.ts +4 -0
  22. src/engine-components/SceneSwitcher.ts +8 -3
  23. src/engine-components/export/usdz/ThreeUSDZExporter.ts +318 -42
  24. src/engine-components/export/usdz/USDZExporter.ts +2 -0
  25. src/engine-components/export/usdz/extensions/USDZText.ts +3 -2
  26. src/engine-components/export/usdz/extensions/USDZUI.ts +2 -2
  27. src/engine-components/postprocessing/Volume.ts +11 -0
  28. src/engine-components/webxr/WebXRImageTracking.ts +2 -3
plugins/vite/poster.js CHANGED
@@ -34,18 +34,24 @@
34
34
  return;
35
35
  }
36
36
  }
37
- const targetPath = "./" + getPosterPath();
38
- console.log("Received poster, saving to " + targetPath);
39
- // remove data:image/png;base64, from the beginning of the string
40
- if (targetPath.endsWith(".webp"))
41
- data.data = data.data.replace(/^data:image\/webp;base64,/, "");
42
- else
43
- data.data = data.data.replace(/^data:image\/png;base64,/, "");
44
- const dir = path.dirname(targetPath);
45
- if (!fs.existsSync(dir)) {
46
- fs.mkdirSync(dir, { recursive: true })
37
+ try {
38
+ const targetPath = "./" + getPosterPath();
39
+ console.debug(`Received poster, saving to ${targetPath}`);
40
+ // remove data:image/png;base64, from the beginning of the string
41
+ if (targetPath.endsWith(".webp"))
42
+ data.data = data.data.replace(/^data:image\/webp;base64,/, "");
43
+ else
44
+ data.data = data.data.replace(/^data:image\/png;base64,/, "");
45
+ const dir = path.dirname(targetPath);
46
+ if (!fs.existsSync(dir)) {
47
+ fs.mkdirSync(dir, { recursive: true })
48
+ }
49
+ fs.writeFileSync(targetPath, Buffer.from(data.data, "base64"));
50
+ console.debug("Saved poster to file");
47
51
  }
48
- fs.writeFileSync(targetPath, Buffer.from(data.data, "base64"));
52
+ catch (err) {
53
+ console.error("Failed to save poster", err.message);
54
+ }
49
55
  });
50
56
  },
51
57
  transformIndexHtml: {
src/engine-components/export/usdz/extensions/behavior/Actions.ts CHANGED
@@ -47,7 +47,7 @@
47
47
  USDObject.createEmptyParent(model);
48
48
  }
49
49
  const clone = model.clone();
50
- if (this.matrix) clone.matrix = this.matrix;
50
+ if (this.matrix) clone.setMatrix(this.matrix);
51
51
  if (this.material) clone.material = this.material;
52
52
  if (this.geometry) clone.geometry = this.geometry;
53
53
  model.parent?.add(clone);
src/engine-components/export/usdz/extensions/Animation.ts CHANGED
@@ -812,11 +812,11 @@
812
812
  writer.appendLine( `uniform token[] joints = [${bonesArray}]` );
813
813
  writer.appendLine( `uniform token purpose = "guide"` );
814
814
  writer.appendLine( `uniform matrix4d[] restTransforms = [${rest.map( m => buildMatrix( m ) ).join( ', ' )}]` );
815
+
815
816
  // In glTF, transformations on the Skeleton are ignored (NODE_SKINNED_MESH_LOCAL_TRANSFORMS validator warning)
816
- // So here we also just write an identity transform.
817
- writer.appendLine( `matrix4d xformOp:transform = ${buildMatrix( new Matrix4() )}` );
818
- // writer.appendLine( `matrix4d xformOp:transform = ${buildMatrix( matrix )}` );
819
- writer.appendLine( `uniform token[] xformOpOrder = ["xformOp:transform"]` );
817
+ // So here we don't write anything to get an identity transform.
818
+ // writer.appendLine( `matrix4d xformOp:transform = ${buildMatrix( new Matrix4() )}` );
819
+ // writer.appendLine( `uniform token[] xformOpOrder = ["xformOp:transform"]` );
820
820
 
821
821
  const timeSampleObjects = createAllTimeSampleObjects ( boneAndInverse.map( x => x.bone ) );
822
822
 
@@ -902,6 +902,7 @@
902
902
  this.skinnedMeshExport(writer, _context);
903
903
 
904
904
  const object = this.object;
905
+ const model = this.model;
905
906
 
906
907
  // do we have animation data for this node? if not, return
907
908
  const arr = this.dict.get(object);
@@ -951,9 +952,6 @@
951
952
  useGrouping: false,
952
953
  });
953
954
 
954
-
955
- const xFormOrder: Array<"xformOp:translate" | "xformOp:orient" | "xformOp:scale"> = [];
956
-
957
955
  function writeAnimationTimesamples(arr: TransformData[], type: "position" | "rotation" | "scale") {
958
956
 
959
957
  const hasAnimationData = arr.some(x => x && {
@@ -967,15 +965,15 @@
967
965
 
968
966
  switch (type) {
969
967
  case "position":
970
- xFormOrder.push("xformOp:translate");
968
+ model.needsTranslate = true;
971
969
  writer.beginBlock(`double3 xformOp:translate.timeSamples = {`, '');
972
970
  break;
973
971
  case "rotation":
974
- xFormOrder.push("xformOp:orient");
972
+ model.needsOrient = true;
975
973
  writer.beginBlock(`quatf xformOp:orient.timeSamples = {`, '');
976
974
  break;
977
975
  case "scale":
978
- xFormOrder.push("xformOp:scale");
976
+ model.needsScale = true;
979
977
  writer.beginBlock(`double3 xformOp:scale.timeSamples = {`, '');
980
978
  break;
981
979
  }
@@ -1027,10 +1025,16 @@
1027
1025
  writeAnimationTimesamples(arr, "rotation");
1028
1026
  writeAnimationTimesamples(arr, "scale");
1029
1027
 
1030
- if (xFormOrder.length > 0) {
1031
- const xformUnique = [...new Set(xFormOrder)];
1032
- writer.appendLine(`uniform token[] xformOpOrder = [${xformUnique.map(x => `"${x}"`).join(', ')}]`);
1033
- }
1028
+ // We must be careful here that we don't overwrite the xformOpOrder that the object already had.
1029
+ // it _might_ not even have any (if the position/quaternion/scale are all indentity values)
1030
+ // so in some cases we end up writing the same xformOpOrder multiple times here...
1031
+ // So right now, we just always write the xformOpOrder when traversing the hierarchy, in case something is animated.
1032
+
1033
+ // if (xFormOrder.length > 0) {
1034
+ // const xformUnique = [...new Set(xFormOrder)];
1035
+ // writer.appendLine(`uniform token[] xformOpOrder = [${xformUnique.map(x => `"${x}"`).join(', ')}]`);
1036
+ // writer.appendLine('uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:orient", "xformOp:scale"]');
1037
+ // }
1034
1038
 
1035
1039
  // for (let i = 0; i < arr.length; i++) {
1036
1040
  // const transformData = arr[i];
src/engine-components/export/usdz/extensions/behavior/Behaviour.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import type { IUSDExporterExtension } from "../../Extension.js";
4
4
  import type { USDObject, USDWriter, USDZExporterContext } from "../../ThreeUSDZExporter.js";
5
5
  import { AudioExtension } from "./AudioExtension.js";
6
- import type { BehaviorModel } from "./BehavioursBuilder.js";
6
+ import { ActionModel, type BehaviorModel, GroupActionModel, IBehaviorElement, type Target, TriggerModel } from "./BehavioursBuilder.js";
7
7
 
8
8
  const debug = getParam("debugusdzbehaviours");
9
9
 
@@ -44,6 +44,11 @@
44
44
  private behaviourComponentsCopy: Array<UsdzBehaviour> = [];
45
45
  private audioClips: Array<{clipUrl: string, filesKey: string}> = [];
46
46
  private audioClipsCopy: Array<{clipUrl: string, filesKey: string}> = [];
47
+ private targetUuids: Set<string> = new Set();
48
+
49
+ getAllTargetUuids() {
50
+ return this.targetUuids;
51
+ }
47
52
 
48
53
  onBeforeBuildDocument(context: USDZExporterContext) {
49
54
  if (!context.root) return Promise.resolve();
@@ -88,6 +93,58 @@
88
93
  this.behaviourComponents.length = 0;
89
94
  this.audioClipsCopy = this.audioClips.slice();
90
95
  this.audioClips.length = 0;
96
+
97
+ // We want to know all trigger sources and action targets.
98
+ // These can be nested in Group Actions.
99
+
100
+ const triggerSources = new Set<Target>();
101
+ const actionTargets = new Set<Target>();
102
+ const targetUuids = new Set<string>();
103
+
104
+ function collect (actionModel: IBehaviorElement) {
105
+ if (actionModel instanceof GroupActionModel) {
106
+ for (const action of actionModel.actions) {
107
+ collect(action);
108
+ }
109
+ }
110
+ else if (actionModel instanceof ActionModel) {
111
+ const affected = actionModel.affectedObjects;
112
+ if (affected) {
113
+ if (typeof affected === "object")
114
+ actionTargets.add(affected as Target);
115
+ else if (typeof affected === "string")
116
+ actionTargets.add({uuid: affected} as any as Target);
117
+ }
118
+
119
+ const xform = actionModel.xFormTarget;
120
+ if (xform) {
121
+ if (typeof xform === "object")
122
+ actionTargets.add(xform as Target);
123
+ else if (typeof xform === "string")
124
+ actionTargets.add({uuid: xform} as any as Target);
125
+ }
126
+ }
127
+ }
128
+
129
+ // collect all targets of all triggers and actions
130
+ for (const beh of this.behaviours) {
131
+ if (beh.trigger instanceof TriggerModel && typeof beh.trigger.targetId === "object" )
132
+ triggerSources.add(beh.trigger.targetId as Target);
133
+ collect(beh.action);
134
+ }
135
+
136
+ for (const source of new Set([...triggerSources, ...actionTargets])) {
137
+ // shouldn't happen but strictly speaking a trigger source could be set to an array
138
+ if (Array.isArray(source)) {
139
+ for (const s of source)
140
+ targetUuids.add(s.uuid);
141
+ }
142
+ else
143
+ targetUuids.add(source.uuid);
144
+ }
145
+
146
+ if (debug) console.log("All Behavior trigger sources and action targets", triggerSources, actionTargets, targetUuids);
147
+ this.targetUuids = new Set(targetUuids);
91
148
  }
92
149
 
93
150
  onAfterHierarchy(context: USDZExporterContext, writer: USDWriter) {
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Material, Mesh, Object3D, Quaternion, Vector3 } from "three";
1
+ import { Material, Matrix4, Mesh, Object3D, Quaternion, Vector3 } from "three";
2
2
 
3
3
  import { isDevEnvironment, showBalloonWarning } from "../../../../../engine/debug/index.js";
4
4
  import { serializable } from "../../../../../engine/engine_serialization_decorator.js";
@@ -334,7 +334,7 @@
334
334
  variant.displayName = variant.displayName + ": Variant with material " + this.variantMaterial.name;
335
335
  variant.material = this.variantMaterial;
336
336
  variant.geometry = target.geometry;
337
- variant.matrix = target.matrix;
337
+ variant.transform = target.transform;
338
338
 
339
339
  if (!target.parent || !target.parent.isEmpty()) {
340
340
  USDObject.createEmptyParent(target);
@@ -471,7 +471,7 @@
471
471
  // We create clones exactly once for this gameObject, so that all SetActiveOnClick on the same object use the same trigger.
472
472
  if (!this.gameObject[SetActiveOnClick.toggleClone]) {
473
473
  const clone = this.selfModelClone.clone();
474
- clone.matrix.identity();
474
+ clone.setMatrix(new Matrix4());
475
475
  clone.name += "_toggle" + (SetActiveOnClick.clonedToggleIndex++);
476
476
  originalModel.add(clone);
477
477
  this.gameObject[SetActiveOnClick.toggleClone] = clone;
@@ -482,7 +482,7 @@
482
482
 
483
483
  if (!this.gameObject[SetActiveOnClick.reverseToggleClone]) {
484
484
  const clone = this.selfModelClone.clone();
485
- clone.matrix.identity();
485
+ clone.setMatrix(new Matrix4());
486
486
  clone.name += "_toggleReverse" + (SetActiveOnClick.clonedToggleIndex++);
487
487
  originalModel.add(clone);
488
488
  this.gameObject[SetActiveOnClick.reverseToggleClone] = clone;
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts CHANGED
@@ -91,7 +91,7 @@
91
91
  }
92
92
 
93
93
 
94
- type Target = USDObject | USDObject[] | Object3D | Object3D[];
94
+ export type Target = USDObject | USDObject[] | Object3D | Object3D[];
95
95
 
96
96
  const addedStrings = new Set<string>();
97
97
  /** called to resolve target objects to usdz paths */
@@ -134,11 +134,16 @@
134
134
  addedStrings.clear();
135
135
  }
136
136
  else if (typeof targetObject === "object") {
137
+ const sourceObject = targetObject;
137
138
  //@ts-ignore
138
- if (targetObject.isObject3D) {
139
+ if (sourceObject.isObject3D) {
139
140
  //@ts-ignore
140
- targetObject = document.findById(targetObject.uuid);
141
+ targetObject = document.findById(sourceObject.uuid);
141
142
  }
143
+ if (!targetObject) {
144
+ console.error("Invalid target object in behavior, the target object is likely missing from USDZ export. Is the object exported?", sourceObject);
145
+ throw new Error(`Invalid target object in behavior, the target object is likely missing from USDZ export. Please report a bug. uuid: ${sourceObject.uuid}.`);
146
+ }
142
147
  result = (targetObject as any).getPath?.call(targetObject) as string;
143
148
  }
144
149
 
src/engine/engine_components.ts CHANGED
@@ -349,8 +349,8 @@
349
349
  * const myComponents = findObjectsOfType(MyComponent);
350
350
  * ```
351
351
  */
352
- export function findObjectsOfType<T extends IComponent>(type: Constructor<T>, array: T[], contextOrScene: undefined | Object3D | { scene: Scene } = undefined): T[] {
353
- if (!type) return array;
352
+ export function findObjectsOfType<T extends IComponent>(type: Constructor<T>, array?: T[], contextOrScene: undefined | Object3D | { scene: Scene } = undefined): T[] {
353
+ if (!type) return array ?? [];
354
354
  if (!array) array = [];
355
355
  array.length = 0;
356
356
 
src/engine/engine_input.ts CHANGED
@@ -134,7 +134,7 @@
134
134
  this._used = true;
135
135
  }
136
136
 
137
- /** Unique identifier for this input: a combination of the deviceIndex + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 101) */
137
+ /** Unique identifier for this input: a combination of the deviceIndex + button to uniquely identify the exact input (e.g. LeftController:Button0 = 0, RightController:Button1 = 11) */
138
138
  override get pointerId(): number { return this._pointerid; }
139
139
  private readonly _pointerid;
140
140
 
@@ -419,6 +419,11 @@
419
419
  return count;
420
420
  }
421
421
 
422
+ /**
423
+ * Gets the position of the given pointer index in pixel
424
+ * @param i The pointer index
425
+ * @returns The position of the pointer in pixel
426
+ */
422
427
  getPointerPosition(i: number): Vector2 | null {
423
428
  if (i >= this._pointerPositions.length) return null;
424
429
  return this._pointerPositions[i];
src/engine/engine_loaders.ts CHANGED
@@ -5,23 +5,11 @@
5
5
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
6
6
  import { KTX2Loader } from 'three/examples/jsm/loaders/KTX2Loader.js';
7
7
 
8
- import { isDevEnvironment } from './debug/index.js';
9
8
  import { Context } from "./engine_setup.js"
10
9
  import { getParam } from "./engine_utils.js";
11
10
 
12
11
  const debug = getParam("debugdecoders");
13
12
 
14
- // NOTE: keep in sync with gltf-progressive
15
- let DEFAULT_DRACO_DECODER_LOCATION = 'https://www.gstatic.com/draco/versioned/decoders/1.5.7/';
16
- let DEFAULT_KTX2_TRANSCODER_LOCATION = 'https://www.gstatic.com/basis-universal/versioned/2021-04-15-ba1c3e4/';
17
- fetch(DEFAULT_DRACO_DECODER_LOCATION + "draco_decoder.js", { method: "head" })
18
- .catch(_ => {
19
- if (isDevEnvironment()) console.warn("Failed to load draco decoder from \"" + DEFAULT_DRACO_DECODER_LOCATION + "\".\nFalling back to local version at \"./include/draco\"");
20
- DEFAULT_DRACO_DECODER_LOCATION = "./include/draco/";
21
- DEFAULT_KTX2_TRANSCODER_LOCATION = "./include/ktx2/";
22
- });
23
-
24
-
25
13
  let meshoptDecoder: typeof MeshoptDecoder;
26
14
 
27
15
  let loaders: null | { dracoLoader: DRACOLoader, ktx2Loader: KTX2Loader, meshoptDecoder: typeof MeshoptDecoder } = null;
@@ -36,10 +24,10 @@
36
24
 
37
25
  export function setDracoDecoderPath(path: string | undefined) {
38
26
  if (path !== undefined && typeof path === "string") {
27
+ setDracoDecoderLocation(path);
39
28
  const loaders = ensureLoaders();
40
29
  if (debug) console.log("Setting draco decoder path to", path);
41
30
  loaders.dracoLoader.setDecoderPath(path);
42
- setDracoDecoderLocation(path);
43
31
  }
44
32
  }
45
33
 
@@ -53,10 +41,10 @@
53
41
 
54
42
  export function setKtx2TranscoderPath(path: string) {
55
43
  if (path !== undefined && typeof path === "string") {
44
+ setKTX2TranscoderLocation(path);
56
45
  const loaders = ensureLoaders();
57
46
  if (debug) console.log("Setting ktx2 transcoder path to", path);
58
47
  loaders.ktx2Loader.setTranscoderPath(path);
59
- setKTX2TranscoderLocation(path);
60
48
  }
61
49
  }
62
50
 
src/engine/engine_physics_rapier.ts CHANGED
@@ -167,13 +167,13 @@
167
167
  this.validate();
168
168
  const body = this.internal_getRigidbody(rigidbody);
169
169
  if (body) body.addForce(force, wakeup)
170
- else console.warn("Rigidbody doesn't exist: can not apply force");
170
+ else console.warn("Rigidbody doesn't exist: can not apply force (does your object with the Rigidbody have a collider?)");
171
171
  }
172
172
  addImpulse(rigidbody: IRigidbody, force: Vec3, wakeup: boolean) {
173
173
  this.validate();
174
174
  const body = this.internal_getRigidbody(rigidbody);
175
175
  if (body) body.applyImpulse(force, wakeup);
176
- else console.warn("Rigidbody doesn't exist: can not apply impulse");
176
+ else console.warn("Rigidbody doesn't exist: can not apply impulse (does your object with the Rigidbody have a collider?)");
177
177
  }
178
178
  getLinearVelocity(comp: IRigidbody | ICollider): Vec3 | null {
179
179
  this.validate();
@@ -182,6 +182,7 @@
182
182
  const vel = body.linvel();
183
183
  return vel;
184
184
  }
185
+ // else console.warn("Rigidbody doesn't exist: can not get linear velocity (does your object with the Rigidbody have a collider?)");
185
186
  return null;
186
187
  }
187
188
  getAngularVelocity(rb: IRigidbody): Vec3 | null {
@@ -191,6 +192,7 @@
191
192
  const vel = body.angvel();
192
193
  return vel;
193
194
  }
195
+ // else console.warn("Rigidbody doesn't exist: can not get angular velocity (does your object with the Rigidbody have a collider?)");
194
196
  return null;
195
197
  }
196
198
  resetForces(rb: IRigidbody, wakeup: boolean) {
@@ -207,14 +209,14 @@
207
209
  this.validate();
208
210
  const body = this.internal_getRigidbody(rb);
209
211
  if (body) body.applyImpulse(vec, wakeup);
210
- else console.warn("Rigidbody doesn't exist: can not apply impulse");
212
+ else console.warn("Rigidbody doesn't exist: can not apply impulse (does your object with the Rigidbody have a collider?)");
211
213
  }
212
214
 
213
215
  wakeup(rb: IRigidbody) {
214
216
  this.validate();
215
217
  const body = this.internal_getRigidbody(rb);
216
218
  if (body) body.wakeUp();
217
- else console.warn("Rigidbody doesn't exist: can not wake up");
219
+ else console.warn("Rigidbody doesn't exist: can not wake up (does your object with the Rigidbody have a collider?)");
218
220
  }
219
221
  isSleeping(rb: IRigidbody) {
220
222
  this.validate();
@@ -225,13 +227,13 @@
225
227
  this.validate();
226
228
  const body = this.internal_getRigidbody(rb);
227
229
  if (body) body.setAngvel(vec, wakeup);
228
- else console.warn("Rigidbody doesn't exist: can not set angular velocity");
230
+ else console.warn("Rigidbody doesn't exist: can not set angular velocity (does your object with the Rigidbody have a collider?)");
229
231
  }
230
232
  setLinearVelocity(rb: IRigidbody, vec: Vec3, wakeup: boolean) {
231
233
  this.validate();
232
234
  const body = this.internal_getRigidbody(rb);
233
235
  if (body) body.setLinvel(vec, wakeup);
234
- else console.warn("Rigidbody doesn't exist: can not set linear velocity");
236
+ else console.warn("Rigidbody doesn't exist: can not set linear velocity (does your object with the Rigidbody have a collider?)");
235
237
  }
236
238
 
237
239
  private context?: IContext;
src/engine/engine_three_utils.ts CHANGED
@@ -4,6 +4,7 @@
4
4
 
5
5
  import { useForAutoFit } from "./engine_camera.js";
6
6
  import { Mathf } from "./engine_math.js"
7
+ import { Vec3 } from "./engine_types.js";
7
8
  import { CircularBuffer } from "./engine_utils.js";
8
9
 
9
10
  /**
@@ -91,10 +92,11 @@
91
92
  export function getTempVector(): Vector3;
92
93
  export function getTempVector(vec3: Vector3): Vector3;
93
94
  export function getTempVector(vec3: [number, number, number]): Vector3;
95
+ export function getTempVector(vec3: Vec3): Vector3;
94
96
  export function getTempVector(dom: DOMPointReadOnly): Vector3;
95
97
  export function getTempVector(x: number): Vector3;
96
98
  export function getTempVector(x: number, y: number, z: number): Vector3;
97
- export function getTempVector(vecOrX?: Vector3 | [number, number, number] | DOMPointReadOnly | number, y?: number, z?: number): Vector3 {
99
+ export function getTempVector(vecOrX?: Vec3 | Vector3 | [number, number, number] | DOMPointReadOnly | number, y?: number, z?: number): Vector3 {
98
100
  const vec = _tempVecs.get();
99
101
  vec.set(0, 0, 0); // initialize with default values
100
102
  if (vecOrX instanceof Vector3) vec.copy(vecOrX);
@@ -106,6 +108,11 @@
106
108
  vec.y = y !== undefined ? y : vec.x;
107
109
  vec.z = z !== undefined ? z : vec.x;
108
110
  }
111
+ else if (typeof vecOrX === "object") {
112
+ vec.x = vecOrX.x;
113
+ vec.y = vecOrX.y;
114
+ vec.z = vecOrX.z;
115
+ }
109
116
  }
110
117
  return vec;
111
118
  }
src/engine/engine_types.ts CHANGED
@@ -523,10 +523,13 @@
523
523
  * https://w3c.github.io/gamepad/#remapping
524
524
  */
525
525
  export type GamepadButtonName = "a-button" | "b-button" | "x-button" | "y-button";
526
+
527
+ /** Needle-defined names for stylus (MX Ink) */
528
+ export type StylusButtonName = "stylus-touch" | "stylus-tip";
529
+
526
530
  /** Button names as used in the xr profile */
531
+ export type XRControllerButtonName = "thumbrest" | "xr-standard-trigger" | "xr-standard-squeeze" | "xr-standard-thumbstick" | "xr-standard-touchpad" | "menu" | GamepadButtonName | StylusButtonName;
527
532
 
528
- export type XRControllerButtonName = "thumbrest" | "xr-standard-trigger" | "xr-standard-squeeze" | "xr-standard-thumbstick" | "xr-standard-touchpad" | "menu" | GamepadButtonName;
529
-
530
533
  export type XRGestureName = "pinch";
531
534
 
532
535
  /** All known (types) button names for various devices and cases combined. You should use the device specific names if you e.g. know you only deal with a mouse use MouseButtonName */
src/engine-components/ui/EventSystem.ts CHANGED
@@ -93,7 +93,7 @@
93
93
  if (this.raycaster.length <= 0) {
94
94
  const res = GameObject.findObjectOfType(Raycaster, this.context);
95
95
  if (!res) {
96
- const rc = GameObject.addNewComponent(this.context.scene, ObjectRaycaster);
96
+ const rc = GameObject.addComponent(this.context.scene, ObjectRaycaster);
97
97
  this.raycaster.push(rc);
98
98
  if (isDevEnvironment() || debug)
99
99
  console.warn("Added an ObjectRaycaster to the scene because no raycaster was found.");
src/engine-components/utils/LookAt.ts CHANGED
@@ -70,8 +70,8 @@
70
70
  // rotate by 90° - counter-rotation on the parent makes sure
71
71
  // that without Preliminary Behaviours it still looks right
72
72
  const flip = this.invertForward ? -1 : 1;
73
- parent.matrix.multiply(new Matrix4().makeRotationZ(Math.PI / 2 * flip));
74
- model.matrix.multiply(new Matrix4().makeRotationZ(-Math.PI / 2 * flip));
73
+ parent.setMatrix(parent.getMatrix().multiply(new Matrix4().makeRotationZ(Math.PI / 2 * flip)));
74
+ model.setMatrix(model.getMatrix().multiply(new Matrix4().makeRotationZ(-Math.PI / 2 * flip)));
75
75
  }
76
76
 
77
77
  const lookAt = new BehaviorModel("lookat " + this.name,
src/engine/webcomponents/needle menu/needle-menu-spatial.ts CHANGED
@@ -62,6 +62,18 @@
62
62
  this.menu?.removeFromParent();
63
63
  }
64
64
 
65
+ private userRequestedMenu = false;
66
+ /** Bring up the spatial menu. This is typically invoked from a button click.
67
+ * The menu will show at a lower height to be easily accessible.
68
+ * @returns true if the menu was shown, false if it can't be shown because the menu has been disabled.
69
+ */
70
+ setDisplay(display: boolean) {
71
+ if (!this.enabled) return false;
72
+
73
+ this.userRequestedMenu = display;
74
+ return true;
75
+ }
76
+
65
77
  onDestroy() {
66
78
  const index = this._context.pre_render_callbacks.indexOf(this.preRender);
67
79
  if (index > -1) {
@@ -156,7 +168,7 @@
156
168
 
157
169
  const showMenuThreshold = fwd.y > .6;
158
170
  const hideMenuThreshold = fwd.y > .4;
159
- const newVisibleState = menu.visible ? hideMenuThreshold : showMenuThreshold;
171
+ const newVisibleState = (menu.visible ? hideMenuThreshold : showMenuThreshold) || this.userRequestedMenu;
160
172
  const becomesVisible = !menu.visible && newVisibleState;
161
173
  menu.visible = newVisibleState || (isDesktop() && debug as boolean);
162
174
 
@@ -167,7 +179,7 @@
167
179
 
168
180
  if (becomesVisible || testBecomesVisible) {
169
181
  menu.position.copy(this._menuTarget.position);
170
- menu.position.y += 1;
182
+ menu.position.y += 0.25;
171
183
  this._menuTarget.position.copy(menu.position);
172
184
  this.positionFilter.reset(menu.position);
173
185
  menu.quaternion.copy(this._menuTarget.quaternion);
@@ -178,7 +190,7 @@
178
190
  this.ensureRenderOnTop(this.menu as any as Object3D);
179
191
  this._menuTarget.position.copy(menuTargetPosition);
180
192
  this._context.scene.add(this._menuTarget);
181
- lookAtObject(this._menuTarget, this._context.mainCamera!, false, true);
193
+ lookAtObject(this._menuTarget, this._context.mainCamera!, true, true);
182
194
  this._menuTarget.removeFromParent();
183
195
  }
184
196
  this.positionFilter.filter(this._menuTarget.position, menu.position, this._context.time.time);
@@ -212,6 +224,10 @@
212
224
  private familyName = "Needle Spatial Menu";
213
225
  private menu?: ThreeMeshUI.Block;
214
226
 
227
+ get isVisible() {
228
+ return this.menu?.visible;
229
+ }
230
+
215
231
  private getMenu() {
216
232
  if (this.menu) {
217
233
  return this.menu;
src/engine/webcomponents/needle menu/needle-menu.ts CHANGED
@@ -178,6 +178,14 @@
178
178
  this._spatialMenu.setEnabled(enabled);
179
179
  }
180
180
 
181
+ setSpatialMenuVisible(display: boolean) {
182
+ this._spatialMenu.setDisplay(display);
183
+ }
184
+
185
+ get spatialMenuIsVisible() {
186
+ return this._spatialMenu.isVisible;
187
+ }
188
+
181
189
  /**
182
190
  * Call to add or remove a button to the menu to show a QR code for the current page
183
191
  * If enabled=true then a button will be added to the menu that will show a QR code for the current page when clicked.
src/engine/xr/NeedleXRController.ts CHANGED
@@ -134,6 +134,9 @@
134
134
  /** is left side. shorthand for `side === 'left'` */
135
135
  get isLeft() { return this.side === 'left'; }
136
136
 
137
+ /** is XR stylus, e.g. Logitech MX Ink */
138
+ get isStylus() { return this._isMxInk; }
139
+
137
140
  /** The XRTransientInputHitTestSource can be used to perform hit tests with the controller ray against the real world.
138
141
  * see https://developer.mozilla.org/en-US/docs/Web/API/XRSession/requestHitTestSourceForTransientInput for more information
139
142
  * Requires the hit-test feature to be enabled in the XRSession
@@ -158,6 +161,7 @@
158
161
  private _hasSelectEvent = false;
159
162
  get hasSelectEvent() { return this._hasSelectEvent; }
160
163
  private _isMxInk = false;
164
+ private _isMetaQuestTouchController = false;
161
165
 
162
166
  /** Perform a hit test against the XR planes or meshes. shorthand for `xr.getHitTest(controller)`
163
167
  * @returns the hit test result (with position and rotation in worldspace) or null if no hit was found
@@ -736,8 +740,15 @@
736
740
  private initialize() {
737
741
  // WORKAROUND for hand controllers that don't have a select event
738
742
  this._hasSelectEvent = this.profiles.includes("generic-hand-select") || this.profiles.some(p => p.startsWith("generic-trigger"));
739
- // WORKAROUND for Logitech MX Ink not having a proper profile on QuestOS v68
740
- this._isMxInk = this.profiles.includes("meta-quest-touch-plus") && this.inputSource.gamepad?.buttons.length === 7;
743
+
744
+ // Used to determine special layout for Quest controllers, e.g. last button is menu button
745
+ this._isMetaQuestTouchController = this.profiles.includes("meta-quest-touch-plus") || this.profiles.includes("oculus-touch-v3");
746
+
747
+ this._isMxInk =
748
+ // Proper profile starting with v69 and browser 35.1
749
+ this.profiles.includes("logitech-mx-ink") ||
750
+ // Workaround for Logitech MX Ink not having a proper profile on QuestOS v68
751
+ this.profiles.includes("meta-quest-touch-plus") && this.inputSource.gamepad?.buttons.length === 7;
741
752
 
742
753
  if (!this._layout) {
743
754
  // Ignore transient-pointer since we likely don't want to spawn a controller visual just for a temporary pointer.
@@ -893,36 +904,76 @@
893
904
  const state = this.states[index] || new InputState();
894
905
  let eventName: InputEventNames | null = null;
895
906
 
896
- // is down
897
- if (button.pressed && !state.pressed) {
898
- eventName = "pointerdown";
899
- state.isDown = true;
900
- state.isUp = false;
907
+ // Special handling for MX Ink stylus on Quest OS v69+.
908
+ // We're never getting a "pressed" state here, so we determine pressed state based on the value.
909
+ if (this._isMxInk && (index === 4 || index === 5)) {
910
+ if (button.value > 0 && !state.pressed) {
911
+ eventName = "pointerdown";
912
+ state.isDown = true;
913
+ state.isUp = false;
914
+ }
915
+ else if (button.value === 0 && state.pressed) {
916
+ eventName = "pointerup";
917
+ state.isDown = false;
918
+ state.isUp = true;
919
+ }
920
+ else if (state.pressed) {
921
+ eventName = "pointermove";
922
+ state.isDown = false;
923
+ state.isUp = false;
924
+ }
925
+ state.pressed = button.value > 0;
926
+ state.value = button.value;
901
927
  }
902
- // is up
903
- else if (!button.pressed && state.pressed) {
904
- eventName = "pointerup"
905
- state.isDown = false;
906
- state.isUp = true;
907
- }
928
+ // Regular controller handling.
908
929
  else {
909
- state.isDown = false;
910
- state.isUp = false;
930
+ // is down
931
+ if (button.pressed && !state.pressed) {
932
+ eventName = "pointerdown";
933
+ state.isDown = true;
934
+ state.isUp = false;
935
+ }
936
+ // is up
937
+ else if (!button.pressed && state.pressed) {
938
+ eventName = "pointerup"
939
+ state.isDown = false;
940
+ state.isUp = true;
941
+ }
942
+ else {
943
+ state.isDown = false;
944
+ state.isUp = false;
945
+ }
946
+ state.pressed = button.pressed;
947
+ state.value = button.value;
911
948
  }
912
-
913
- state.value = button.value;
914
- state.pressed = button.pressed;
915
949
  this.states[index] = state;
916
950
 
917
951
  // the selection event is handled in the "selectstart" callback
918
952
  const emitEvent = index !== this._selectButtonIndex && index !== this._squeezeButtonIndex;
919
953
 
920
954
  if (eventName != null && emitEvent) {
921
- const name = this._layout?.gamepad[index];
955
+ let name = this._layout?.gamepad[index];
956
+ if (this._isMxInk && index === 4) name = "stylus-touch";
957
+ if (this._isMxInk && index === 5) name = "stylus-tip";
922
958
  if (debug || debugCustomGesture) console.log("Emitting pointer event", eventName, index, name, button.value, this.gamepad, this._layout);
923
959
  this.emitPointerEvent(eventName, index, name ?? "none", false, null, button.value);
924
960
  }
925
961
  }
962
+
963
+ // For Quest controllers, the last button is the menu button
964
+ if (this._isMetaQuestTouchController) {
965
+ const menuButtonIndex = this.gamepad.buttons.length - 1;
966
+ const menuButtonState = this.states[menuButtonIndex];
967
+ if (menuButtonState) {
968
+ if (menuButtonState.isDown) {
969
+ const menu = this.context.menu;
970
+ if (menu.spatialMenuIsVisible)
971
+ menu.setSpatialMenuVisible(false);
972
+ else
973
+ this.context.menu.setSpatialMenuVisible(true);
974
+ }
975
+ }
976
+ }
926
977
  }
927
978
 
928
979
  // update hand gesture states
src/engine/xr/NeedleXRSession.ts CHANGED
@@ -373,7 +373,8 @@
373
373
  // handle iOS platform where "immersive-ar" is not supported
374
374
  // TODO: should we add a separate mode (e.g. "AR")? https://linear.app/needle/issue/NE-5303
375
375
  if (mode === "immersive-ar" && isiOS()) {
376
- if (InternalUSDZRegistry.exportAndOpen()) {
376
+ const arSupported = await this.isARSupported();
377
+ if (!arSupported && InternalUSDZRegistry.exportAndOpen()) {
377
378
  return null;
378
379
  }
379
380
  }
src/engine-components/postprocessing/PostProcessingEffect.ts CHANGED
@@ -78,6 +78,7 @@
78
78
  private _manager: IPostProcessingManager | null = null;
79
79
 
80
80
  onEnable(): void {
81
+ super.onEnable();
81
82
  this.onEffectEnabled();
82
83
  // Dont override the serialized value by enabling (we could also just disable this component / map enabled to active)
83
84
  if (this.__internalDidAwakeAndStart)
@@ -85,6 +86,7 @@
85
86
  }
86
87
 
87
88
  onDisable(): void {
89
+ super.onDisable();
88
90
  this._manager?.removeEffect(this);
89
91
  this.active = false;
90
92
  }
src/engine-components/postprocessing/PostProcessingHandler.ts CHANGED
@@ -91,7 +91,7 @@
91
91
 
92
92
  context[activeKey] = this;
93
93
 
94
- if (debug) console.log("refreshing volume profile", components);
94
+ if (debug) console.log("Apply Postprocessing Effects", components);
95
95
 
96
96
  this._lastVolumeComponents = [...components];
97
97
 
src/engine-components/RigidBody.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Matrix4, Object3D, Quaternion, Vector3, Vector3Like } from "three";
2
2
 
3
+ import { isDevEnvironment } from "../engine/debug/index.js";
3
4
  import { CollisionDetectionMode, RigidbodyConstraints } from "../engine/engine_physics.types.js";
4
5
  import { serializable } from "../engine/engine_serialization_decorator.js";
5
6
  import { Context, FrameEvent } from "../engine/engine_setup.js";
@@ -313,6 +314,9 @@
313
314
  }
314
315
  this._watch.start(true, true);
315
316
  this.startCoroutine(this.beforePhysics(), FrameEvent.LateUpdate);
317
+ if (isDevEnvironment() && !this.context.physics.engine?.getBody(this)) {
318
+ console.warn(`Rigidbody was not created: Does your object (${this.name}) have a collider?`);
319
+ }
316
320
  }
317
321
 
318
322
  onDisable() {
src/engine-components/SceneSwitcher.ts CHANGED
@@ -65,12 +65,13 @@
65
65
  **/
66
66
  export interface ISceneEventListener {
67
67
  /** Called when the scene is loaded and added */
68
- sceneOpened?(sceneSwitcher: SceneSwitcher): Promise<void>
68
+ sceneOpened(sceneSwitcher: SceneSwitcher): Promise<void>
69
69
  /** Called before the scene is being removed (due to another scene being loaded) */
70
- sceneClosing?(): Promise<void>
70
+ sceneClosing(): Promise<void>
71
71
  }
72
72
 
73
73
 
74
+
74
75
  /** The SceneSwitcher can be used to dynamically load and unload extra content
75
76
  * Available scenes are defined in the `scenes` array.
76
77
  * Loaded scenes will be added to the SceneSwitcher's GameObject as a child and removed when another scene is loaded by the same SceneSwitcher.
@@ -783,7 +784,11 @@
783
784
  }
784
785
 
785
786
  private tryGetSceneEventListener(obj: Object3D, level: number = 0): ISceneEventListener | null {
786
- const sceneListener = GameObject.foreachComponent(obj, c => (c as any as ISceneEventListener).sceneClosing ? c : undefined) as ISceneEventListener | undefined;
787
+ const sceneListener = GameObject.foreachComponent(obj, c => {
788
+ const i = c as any as ISceneEventListener;
789
+ if (i.sceneClosing! || i.sceneOpened!) return i;
790
+ else return undefined;
791
+ });
787
792
  // if we didnt find any component with the listener on the root object
788
793
  // we also check the first level of its children because a scene might be a group
789
794
  if (level === 0 && !sceneListener && obj.children.length) {
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -7,6 +7,7 @@
7
7
  BufferGeometry,
8
8
  Color,
9
9
  DoubleSide,
10
+ InterleavedBufferAttribute,
10
11
  LinearFilter,
11
12
  Material,
12
13
  MathUtils,
@@ -19,6 +20,7 @@
19
20
  OrthographicCamera,
20
21
  PerspectiveCamera,
21
22
  PlaneGeometry,
23
+ Quaternion,
22
24
  RGBAFormat,
23
25
  Scene,
24
26
  ShaderMaterial,
@@ -27,6 +29,7 @@
27
29
  Texture,
28
30
  Uniform,
29
31
  UnsignedByteType,
32
+ Vector3,
30
33
  Vector4,
31
34
  WebGLRenderer,
32
35
  WebGLRenderTarget} from 'three';
@@ -89,6 +92,16 @@
89
92
  return structuralNodes;
90
93
  }
91
94
 
95
+ declare type USDObjectTransform = {
96
+ position: Vector3 | null;
97
+ quaternion: Quaternion | null;
98
+ scale: Vector3 | null;
99
+ }
100
+
101
+ const PositionIdentity = new Vector3();
102
+ const QuaternionIdentity = new Quaternion();
103
+ const ScaleIdentity = new Vector3(1,1,1);
104
+
92
105
  class USDObject {
93
106
 
94
107
  static USDObject_export_id = 0;
@@ -101,7 +114,30 @@
101
114
  extraSchemas: string[] = [];
102
115
  displayName?: string;
103
116
  visibility?: "inherited" | "invisible"; // defaults to "inherited" in USD
104
- matrix: Matrix4;
117
+ getMatrix(): Matrix4 {
118
+ if (!this.transform) return new Matrix4();
119
+ const { position, quaternion, scale } = this.transform;
120
+ const matrix = new Matrix4();
121
+ matrix.compose(position || PositionIdentity, quaternion || QuaternionIdentity, scale || ScaleIdentity);
122
+ return matrix;
123
+ }
124
+ setMatrix( value ) {
125
+ if (!value || !(value instanceof Matrix4)) {
126
+ this.transform = null;
127
+ return;
128
+ }
129
+ const position = new Vector3();
130
+ const quaternion = new Quaternion();
131
+ const scale = new Vector3();
132
+ value.decompose(position, quaternion, scale);
133
+ this.transform = { position, quaternion, scale };
134
+ }
135
+ /** @deprecated Use `transform`, or `getMatrix()` if you really need the matrix */
136
+ get matrix() { return this.getMatrix(); }
137
+ /** @deprecated Use `transform`, or `setMatrix()` if you really need the matrix */
138
+ set matrix( value ) { this.setMatrix( value ); }
139
+
140
+ transform: USDObjectTransform | null = null;
105
141
  private _isDynamic: boolean;
106
142
  get isDynamic() { return this._isDynamic; }
107
143
  private set isDynamic( value ) { this._isDynamic = value; }
@@ -114,31 +150,42 @@
114
150
  animations: AnimationClip[] | null;
115
151
  _eventListeners: {};
116
152
 
153
+ // these are for tracking which xformops are needed
154
+ needsTranslate: boolean = false;
155
+ needsOrient: boolean = false;
156
+ needsScale: boolean = false;
157
+
117
158
  static createEmptyParent( object: USDObject ) {
118
159
 
119
- const emptyParent = new USDObject( MathUtils.generateUUID(), object.name + '_empty_' + ( USDObject.USDObject_export_id ++ ), object.matrix );
160
+ const emptyParent = new USDObject( MathUtils.generateUUID(), object.name + '_empty_' + ( USDObject.USDObject_export_id ++ ), object.transform );
120
161
  const parent = object.parent;
121
162
  if (parent) parent.add( emptyParent );
122
163
  emptyParent.add( object );
123
164
  emptyParent.isDynamic = true;
124
- object.matrix = new Matrix4().identity();
165
+ object.transform = null;
125
166
  return emptyParent;
126
167
 
127
168
  }
128
169
 
129
170
  static createEmpty() {
130
171
 
131
- const empty = new USDObject( MathUtils.generateUUID(), 'Empty_' + ( USDObject.USDObject_export_id ++ ), new Matrix4() );
172
+ const empty = new USDObject( MathUtils.generateUUID(), 'Empty_' + ( USDObject.USDObject_export_id ++ ) );
132
173
  empty.isDynamic = true;
133
174
  return empty;
134
175
  }
135
176
 
136
- constructor( id, name, matrix, mesh: BufferGeometry | null = null, material: MeshStandardMaterial | MeshBasicMaterial | Material | null = null, camera: PerspectiveCamera | OrthographicCamera | null = null, skinnedMesh: SkinnedMesh | null = null, animations: AnimationClip[] | null = null ) {
177
+ constructor( id, name, transform: USDObjectTransform | null = null, mesh: BufferGeometry | null = null, material: MeshStandardMaterial | MeshBasicMaterial | Material | null = null, camera: PerspectiveCamera | OrthographicCamera | null = null, skinnedMesh: SkinnedMesh | null = null, animations: AnimationClip[] | null = null ) {
137
178
 
138
179
  this.uuid = id;
139
180
  this.name = makeNameSafe( name );
140
181
  this.displayName = name;
141
- this.matrix = matrix.clone();
182
+
183
+ if (!transform) this.transform = null;
184
+ else this.transform = {
185
+ position: transform.position?.clone() || null,
186
+ quaternion: transform.quaternion?.clone() || null,
187
+ scale: transform.scale?.clone() || null
188
+ };
142
189
  this.geometry = mesh;
143
190
  this.material = material;
144
191
  this.camera = camera;
@@ -166,7 +213,7 @@
166
213
 
167
214
  clone() {
168
215
 
169
- const clone = new USDObject( MathUtils.generateUUID(), this.name, this.matrix, this.geometry, this.material );
216
+ const clone = new USDObject( MathUtils.generateUUID(), this.name, this.transform, this.geometry, this.material );
170
217
  clone.isDynamic = this.isDynamic;
171
218
  return clone;
172
219
 
@@ -276,7 +323,7 @@
276
323
 
277
324
  constructor() {
278
325
 
279
- super(undefined, 'StageRoot', new Matrix4(), null, null, null);
326
+ super(undefined, 'StageRoot', null, null, null, null);
280
327
  this.children = [];
281
328
  this.stageLength = 200;
282
329
 
@@ -320,9 +367,9 @@
320
367
  findById( uuid: string ) {
321
368
 
322
369
  let found = false;
323
- function search( current: USDObject ) {
370
+ function search( current: USDObject ): USDObject | undefined {
324
371
 
325
- if ( found ) return;
372
+ if ( found ) return undefined;
326
373
  if ( current.uuid === uuid ) {
327
374
 
328
375
  found = true;
@@ -341,7 +388,7 @@
341
388
  }
342
389
 
343
390
  }
344
-
391
+ return undefined;
345
392
  }
346
393
 
347
394
  return search( this );
@@ -531,6 +578,7 @@
531
578
 
532
579
  class USDZExporter {
533
580
  debug: boolean;
581
+ pruneUnusedNodes: boolean;
534
582
  sceneAnchoringOptions: USDZExporterOptions = new USDZExporterOptions();
535
583
  extensions: Array<IUSDExporterExtension> = [];
536
584
  keepObject?: (object: Object3D) => boolean;
@@ -538,6 +586,7 @@
538
586
  constructor() {
539
587
 
540
588
  this.debug = false;
589
+ this.pruneUnusedNodes = true;
541
590
 
542
591
  }
543
592
 
@@ -563,8 +612,11 @@
563
612
  Progress.report('export-usdz', "Done onBeforeBuildDocument");
564
613
 
565
614
  Progress.report('export-usdz', "Reparent bones to common ancestor");
566
- // HACK let's find all skeletons and reparent them to their skelroot / armature / uppermost bone parent
615
+
616
+ // Find all skeletons and reparent them to their skelroot / armature / uppermost bone parent.
617
+ // This may not be correct in all cases.
567
618
  const reparentings: Array<{ object: Object3D, originalParent: Object3D | null, newParent: Object3D }> = [];
619
+ const allReparentingObjects = new Set<string>();
568
620
  scene?.traverse(object => {
569
621
  if (!options.exportInvisible && !object.visible) return;
570
622
 
@@ -573,14 +625,19 @@
573
625
 
574
626
  const commonAncestor = findCommonAncestor(bones);
575
627
  if (commonAncestor) {
576
- reparentings.push( { object, originalParent: object.parent, newParent: commonAncestor } );
628
+ const newReparenting = { object, originalParent: object.parent, newParent: commonAncestor };
629
+ reparentings.push( newReparenting );
630
+
631
+ // keep track of which nodes are important for skeletal export consistency
632
+ allReparentingObjects.add(newReparenting.object.uuid);
633
+ if (newReparenting.newParent) allReparentingObjects.add(newReparenting.newParent.uuid);
634
+ if (newReparenting.originalParent) allReparentingObjects.add(newReparenting.originalParent.uuid);
577
635
  }
578
636
  }
579
637
  });
580
638
 
581
639
  for ( const reparenting of reparentings ) {
582
640
  const { object, originalParent, newParent } = reparenting;
583
- // if (this.debug) console.log("REPARENTING", object, "from", originalParent, "to", newParent);
584
641
  newParent.add( object );
585
642
  }
586
643
 
@@ -595,6 +652,28 @@
595
652
  Progress.report('export-usdz', "Invoking onAfterBuildDocument");
596
653
  await invokeAll( context, 'onAfterBuildDocument' );
597
654
 
655
+ // At this point, we know all animated objects, all skinned mesh objects, and all objects targeted by behaviors.
656
+ // We can prune all empty nodes (no geometry or material) depth-first.
657
+ // This avoids unnecessary export of e.g. animated bones as nodes when they have no children
658
+ // (for example, a sword attached to a hand still needs that entire hierarchy exported)
659
+ const behaviorExt = context.extensions.find( ext => ext.extensionName === 'Behaviour' ) as BehaviorExtension | undefined;
660
+ const allBehaviorTargets = behaviorExt?.getAllTargetUuids() ?? new Set<string>();
661
+
662
+ // Prune pass. Depth-first removal of nodes that don't affect the outcome of the scene.
663
+ if (this.pruneUnusedNodes) {
664
+ const options = {
665
+ allBehaviorTargets,
666
+ debug: false,
667
+ boneReparentings: allReparentingObjects
668
+ };
669
+ if (this.debug) logUsdHierarchy(context.document, "Hierarchy BEFORE pruning", options);
670
+ prune( context.document, options );
671
+ if (this.debug) logUsdHierarchy(context.document, "Hierarchy AFTER pruning");
672
+ }
673
+ else if (this.debug) {
674
+ console.log("Pruning of empty nodes is disabled. This may result in a larger USDZ file.");
675
+ }
676
+
598
677
  Progress.report('export-usdz', { message: "Parsing document", autoStep: 10 });
599
678
  await parseDocument( context, () => {
600
679
  // injected after stageRoot.
@@ -724,6 +803,14 @@
724
803
  let model: USDObject | undefined = undefined;
725
804
  let geometry: BufferGeometry | undefined = undefined;
726
805
  let material: Material | Material[] | undefined = undefined;
806
+
807
+ const transform: USDObjectTransform = { position: object.position, quaternion: object.quaternion, scale: object.scale };
808
+ if (object.position.x === 0 && object.position.y === 0 && object.position.z === 0)
809
+ transform.position = null;
810
+ if (object.quaternion.x === 0 && object.quaternion.y === 0 && object.quaternion.z === 0 && object.quaternion.w === 1)
811
+ transform.quaternion = null;
812
+ if (object.scale.x === 1 && object.scale.y === 1 && object.scale.z === 1)
813
+ transform.scale = null;
727
814
 
728
815
  if (object instanceof Mesh || object instanceof SkinnedMesh) {
729
816
  geometry = object.geometry;
@@ -745,17 +832,17 @@
745
832
 
746
833
  const name = getObjectId( object );
747
834
  const skinnedMeshObject = object instanceof SkinnedMesh ? object : null;
748
- model = new USDObject( object.uuid, name, object.matrix, geometry, material, undefined, skinnedMeshObject, object.animations );
835
+ model = new USDObject( object.uuid, name, transform, geometry, material, undefined, skinnedMeshObject, object.animations );
749
836
 
750
837
  } else if ( object instanceof PerspectiveCamera || object instanceof OrthographicCamera ) {
751
838
 
752
839
  const name = getObjectId( object );
753
- model = new USDObject( object.uuid, name, object.matrix, undefined, undefined, object );
840
+ model = new USDObject( object.uuid, name, transform, undefined, undefined, object );
754
841
 
755
842
  } else {
756
843
 
757
844
  const name = getObjectId( object );
758
- model = new USDObject( object.uuid, name, object.matrix, undefined, undefined, undefined, undefined, object.animations );
845
+ model = new USDObject( object.uuid, name, transform, undefined, undefined, undefined, undefined, object.animations );
759
846
 
760
847
  }
761
848
 
@@ -785,7 +872,7 @@
785
872
  } else {
786
873
 
787
874
  const name = getObjectId( object );
788
- const empty = new USDObject( object.uuid, name, object.matrix );
875
+ const empty = new USDObject( object.uuid, name, { position: object.position, quaternion: object.quaternion, scale: object.scale } );
789
876
  if ( parentModel ) {
790
877
 
791
878
  parentModel.add( empty );
@@ -804,6 +891,106 @@
804
891
 
805
892
  }
806
893
 
894
+ function logUsdHierarchy( object: USDObject, prefix: string, ...extraLogObjects: any[] ) {
895
+
896
+ const item = {};
897
+ let itemCount = 0;
898
+
899
+ function collectItem( object: USDObject, current) {
900
+ itemCount++;
901
+ let name = object.displayName || object.name;
902
+ name += " (" + object.uuid + ")";
903
+ const hasAny = object.geometry || object.material || object.camera || object.skinnedMesh;
904
+ if (hasAny) {
905
+ name += " (" + (object.geometry ? "geo, " : "") + (object.material ? "mat, " : "") + (object.camera ? "cam, " : "") + (object.skinnedMesh ? "skin, " : "") + ")";
906
+ }
907
+ current[name] = {};
908
+ const props = { object };
909
+ if (object.material) props['mat'] = true;
910
+ if (object.geometry) props['geo'] = true;
911
+ if (object.camera) props['cam'] = true;
912
+ if (object.skinnedMesh) props['skin'] = true;
913
+
914
+ current[name]._self = props;
915
+ for ( const child of object.children ) {
916
+ if (child) {
917
+ collectItem(child, current[name]);
918
+ }
919
+ }
920
+ }
921
+
922
+ collectItem(object, item);
923
+
924
+ console.log(prefix + " (" + itemCount + " nodes)", item, ...extraLogObjects);
925
+ }
926
+
927
+ function prune ( object: USDObject, options : {
928
+ allBehaviorTargets: Set<string>,
929
+ debug: boolean,
930
+ boneReparentings: Set<string>
931
+ } ) {
932
+
933
+ let allChildsWerePruned = true;
934
+
935
+ const prunedChilds = new Array<USDObject>();
936
+ const keptChilds = new Array<USDObject>();
937
+
938
+ if (object.children.length === 0) {
939
+ allChildsWerePruned = true;
940
+ }
941
+ else {
942
+ const childs = [...object.children];
943
+ for ( const child of childs ) {
944
+ if (child) {
945
+ const childWasPruned = prune(child, options);
946
+ if (options.debug) {
947
+ if (childWasPruned) prunedChilds.push(child);
948
+ else keptChilds.push(child);
949
+ }
950
+ allChildsWerePruned = allChildsWerePruned && childWasPruned;
951
+ }
952
+ }
953
+ }
954
+
955
+ // check if this object is referenced by any behavior
956
+ const isBehaviorSourceOrTarget = options.allBehaviorTargets.has(object.uuid);
957
+
958
+ // check if this object has any material or geometry
959
+ const isVisible = object.geometry || object.material || object.camera || object.skinnedMesh || false;
960
+
961
+ // check if this object is part of any reparenting
962
+ const isBoneReparenting = options.boneReparentings.has(object.uuid);
963
+
964
+ const canBePruned = allChildsWerePruned && !isBehaviorSourceOrTarget && !isVisible && !isBoneReparenting;
965
+
966
+ if (canBePruned) {
967
+ if (options.debug) console.log("Pruned object:", (object.displayName || object.name) + " (" + object.uuid + ")", {
968
+ isVisible,
969
+ isBehaviorSourceOrTarget,
970
+ allChildsWerePruned,
971
+ isBoneReparenting,
972
+ object,
973
+ prunedChilds,
974
+ keptChilds
975
+ });
976
+ object.parent?.remove(object);
977
+ }
978
+ else {
979
+ if (options.debug) console.log("Kept object:", (object.displayName || object.name) + " (" + object.uuid + ")", {
980
+ isVisible,
981
+ isBehaviorSourceOrTarget,
982
+ allChildsWerePruned,
983
+ isBoneReparenting,
984
+ object,
985
+ prunedChilds,
986
+ keptChilds
987
+ });
988
+ }
989
+
990
+ // if it has no children and is not a behavior source or target, and is not visible, prune it
991
+ return canBePruned;
992
+ }
993
+
807
994
  async function parseDocument( context: USDZExporterContext, afterStageRoot: () => string ) {
808
995
 
809
996
  Progress.start("export-usdz-resources", "export-usdz");
@@ -1224,7 +1411,8 @@
1224
1411
 
1225
1412
  Progress.report("export-usdz-xforms", { message: "buildXform " + model.displayName || model.name, autoStep: true });
1226
1413
 
1227
- const matrix = model.matrix;
1414
+ // const matrix = model.matrix;
1415
+ const transform = model.transform;
1228
1416
  const geometry = model.geometry;
1229
1417
  const material = model.material;
1230
1418
  const camera = model.camera;
@@ -1236,13 +1424,15 @@
1236
1424
  }
1237
1425
  }
1238
1426
 
1239
- const transform = buildMatrix( matrix );
1427
+ // const transform = buildMatrix( matrix );
1240
1428
 
1429
+ /*
1241
1430
  if ( matrix.determinant() < 0 ) {
1242
1431
 
1243
1432
  console.warn( 'NeedleUSDZExporter: USDZ does not support negative scales', name );
1244
1433
 
1245
1434
  }
1435
+ */
1246
1436
 
1247
1437
  const isSkinnedMesh = geometry && geometry.isBufferGeometry && geometry.attributes.skinIndex !== undefined && geometry.attributes.skinIndex.count > 0;
1248
1438
  const objType = isSkinnedMesh ? 'SkelRoot' : 'Xform';
@@ -1264,16 +1454,20 @@
1264
1454
  writer.beginBlock( `def Camera "${name}"`, "(", false );
1265
1455
  else if ( model.type !== undefined)
1266
1456
  writer.beginBlock( `def ${model.type} "${name}"` );
1267
- else
1457
+ else // if (model.type === undefined)
1268
1458
  writer.beginBlock( `def Xform "${name}"`, "(", false);
1269
1459
 
1270
- if (model.displayName)
1271
- writer.appendLine(`displayName = "${model.displayName}"`);
1272
1460
  if (model.type === undefined) {
1273
1461
  if (model.extraSchemas?.length)
1274
1462
  _apiSchemas.push(...model.extraSchemas);
1275
1463
  if (_apiSchemas.length)
1276
1464
  writer.appendLine(`prepend apiSchemas = [${_apiSchemas.map(s => `"${s}"`).join(', ')}]`);
1465
+ }
1466
+
1467
+ if (model.displayName)
1468
+ writer.appendLine(`displayName = "${model.displayName}"`);
1469
+
1470
+ if ( camera || model.type === undefined) {
1277
1471
  writer.closeBlock( ")" );
1278
1472
  writer.beginBlock();
1279
1473
  }
@@ -1292,16 +1486,30 @@
1292
1486
  writer.closeBlock();
1293
1487
  }
1294
1488
  }
1489
+ let haveWrittenAnyXformOps = false;
1295
1490
  if ( isSkinnedMesh ) {
1296
1491
  writer.appendLine( `rel skel:skeleton = <Rig>` );
1297
1492
  writer.appendLine( `rel skel:animationSource = <Rig/_anim>`);
1298
- writer.appendLine( `matrix4d xformOp:transform = ${buildMatrix(new Matrix4())}` ); // always identity / in world space
1493
+ haveWrittenAnyXformOps = false;
1494
+ // writer.appendLine( `matrix4d xformOp:transform = ${buildMatrix(new Matrix4())}` ); // always identity / in world space
1299
1495
  }
1300
1496
  else if (model.type === undefined) {
1301
- writer.appendLine( `matrix4d xformOp:transform = ${transform}` );
1497
+ if (transform) {
1498
+ haveWrittenAnyXformOps = haveWrittenAnyXformOps || (transform.position !== null || transform.quaternion !== null || transform.scale !== null);
1499
+ if (transform.position) {
1500
+ model.needsTranslate = true;
1501
+ writer.appendLine( `double3 xformOp:translate = (${fn(transform.position.x)}, ${fn(transform.position.y)}, ${fn(transform.position.z)})` );
1502
+ }
1503
+ if (transform.quaternion) {
1504
+ model.needsOrient = true;
1505
+ writer.appendLine( `quatf xformOp:orient = (${fn(transform.quaternion.w)}, ${fn(transform.quaternion.x)}, ${fn(transform.quaternion.y)}, ${fn(transform.quaternion.z)})` );
1506
+ }
1507
+ if (transform.scale) {
1508
+ model.needsScale = true;
1509
+ writer.appendLine( `double3 xformOp:scale = (${fn(transform.scale.x)}, ${fn(transform.scale.y)}, ${fn(transform.scale.z)})` );
1510
+ }
1511
+ }
1302
1512
  }
1303
- if (model.type === undefined)
1304
- writer.appendLine( 'uniform token[] xformOpOrder = ["xformOp:transform"]' );
1305
1513
 
1306
1514
  if (model.visibility !== undefined)
1307
1515
  writer.appendLine(`token visibility = "${model.visibility}"`);
@@ -1333,6 +1541,20 @@
1333
1541
 
1334
1542
  }
1335
1543
 
1544
+ // after serialization, we know which xformops to actually define here:
1545
+ if (model.type === undefined) {
1546
+ // TODO only write the necessary ones – this isn't trivial though because we need to know
1547
+ // if some of them are animated, and then we need to include those.
1548
+ // Best approach would likely be to write xformOpOrder _after_ onSerialize
1549
+ // and keep track of what was written in onSerialize (e.g. model.needsTranslate = true)
1550
+ const ops = new Array<string>();
1551
+ if (model.needsTranslate) ops.push('"xformOp:translate"');
1552
+ if (model.needsOrient) ops.push('"xformOp:orient"');
1553
+ if (model.needsScale) ops.push('"xformOp:scale"');
1554
+ if (ops.length)
1555
+ writer.appendLine( `uniform token[] xformOpOrder = [${ops.join(', ')}]` );
1556
+ }
1557
+
1336
1558
  if ( model.children ) {
1337
1559
 
1338
1560
  writer.appendLine();
@@ -1380,7 +1602,7 @@
1380
1602
 
1381
1603
  }
1382
1604
 
1383
- function buildMesh( geometry, bones: Bone[] = [], quickLookCompatible: boolean = true) {
1605
+ function buildMesh( geometry: BufferGeometry, bones: Bone[] = [], quickLookCompatible: boolean = true) {
1384
1606
 
1385
1607
  const name = 'Geometry';
1386
1608
  const attributes = geometry.attributes;
@@ -1394,7 +1616,7 @@
1394
1616
  const sortedBones: Array<{bone: Object3D, index: number}> = [];
1395
1617
  const indexMapping: number[] = [];
1396
1618
  let sortedSkinIndex = new Array<number>();
1397
- let sortedSkinIndexAttribute: BufferAttribute | null = attributes.skinIndex;
1619
+ let sortedSkinIndexAttribute: BufferAttribute | InterleavedBufferAttribute | null = attributes.skinIndex;
1398
1620
  let bonesArray = "";
1399
1621
  if (hasBones) {
1400
1622
  const uuidsFound:string[] = [];
@@ -1468,13 +1690,12 @@
1468
1690
  )` : '' }
1469
1691
  point3f[] points = [${buildVector3Array( attributes.position, count )}]
1470
1692
  ${attributes.uv ?
1471
- `texCoord2f[] primvars:st = [${buildVector2Array( attributes.uv, count )}] (
1693
+ `texCoord2f[] primvars:st = [${buildVector2Array( attributes.uv, count, true )}] (
1472
1694
  interpolation = "vertex"
1473
1695
  )` : '' }
1474
- ${attributes.uv2 ?
1475
- `texCoord2f[] primvars:st2 = [${buildVector2Array( attributes.uv2, count )}] (
1476
- interpolation = "vertex"
1477
- )` : '' }
1696
+ ${attributes.uv1 ? buildCustomAttributeAccessor('st1', attributes.uv1) : '' }
1697
+ ${attributes.uv2 ? buildCustomAttributeAccessor('st2', attributes.uv2) : '' }
1698
+ ${attributes.uv3 ? buildCustomAttributeAccessor('st3', attributes.uv3) : '' }
1478
1699
  ${isSkinnedMesh ?
1479
1700
  `matrix4d primvars:skel:geomBindTransform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) (
1480
1701
  elementSize = 1
@@ -1490,6 +1711,10 @@
1490
1711
  elementSize = 4
1491
1712
  interpolation = "vertex"
1492
1713
  )` : '' }
1714
+ ${attributes.color ?
1715
+ `color3f[] primvars:displayColor = [${buildVector3Array( attributes.color, count )}] (
1716
+ interpolation = "vertex"
1717
+ )` : '' }
1493
1718
  uniform token subdivisionScheme = "none"
1494
1719
  }
1495
1720
  }
@@ -1510,7 +1735,7 @@
1510
1735
  `;
1511
1736
  }
1512
1737
 
1513
- function buildMeshVertexCount( geometry ) {
1738
+ function buildMeshVertexCount( geometry: BufferGeometry ) {
1514
1739
 
1515
1740
  const count = geometry.index !== null ? geometry.index.count : geometry.attributes.position.count;
1516
1741
 
@@ -1553,6 +1778,29 @@
1553
1778
 
1554
1779
  }
1555
1780
 
1781
+ /** Returns a string with the correct attribute declaration for the given attribute. Could have 2,3,4 components. */
1782
+ function buildCustomAttributeAccessor( primvarName: string, attribute: BufferAttribute | InterleavedBufferAttribute ) {
1783
+ const count = attribute.itemSize;
1784
+ switch (count) {
1785
+ case 2:
1786
+ // TODO: Check if we want to flip Y here as well. We do that for texcoords, but the data in UV1..N could be intended for other purposes.
1787
+ return `texCoord2f[] primvars:${primvarName} = [${buildVector2Array( attribute, count, true )}] (
1788
+ interpolation = "vertex"
1789
+ )`;
1790
+ case 3:
1791
+ return `texCoord3f[] primvars:${primvarName} = [${buildVector3Array( attribute, count )}] (
1792
+ interpolation = "vertex"
1793
+ )`;
1794
+ case 4:
1795
+ return `double4[] primvars:${primvarName} = [${buildVector4Array2( attribute, count )}] (
1796
+ interpolation = "vertex"
1797
+ )`;
1798
+ default:
1799
+ console.warn('USDZExporter: Attribute with ' + count + ' components are currently not supported. Results may be undefined for ' + primvarName + '.');
1800
+ return '';
1801
+ }
1802
+ }
1803
+
1556
1804
  function buildVector3Array( attribute, count ) {
1557
1805
 
1558
1806
  if ( attribute === undefined ) {
@@ -1578,6 +1826,33 @@
1578
1826
 
1579
1827
  }
1580
1828
 
1829
+
1830
+ function buildVector4Array2( attribute, count ) {
1831
+
1832
+ if ( attribute === undefined ) {
1833
+
1834
+ console.warn( 'USDZExporter: Attribute is missing. Results may be undefined.' );
1835
+ return Array( count ).fill( '(0, 0, 0, 0)' ).join( ', ' );
1836
+
1837
+ }
1838
+
1839
+ const array: Array<string> = [];
1840
+
1841
+ for ( let i = 0; i < attribute.count; i ++ ) {
1842
+
1843
+ const x = attribute.getX( i );
1844
+ const y = attribute.getY( i );
1845
+ const z = attribute.getZ( i ) || 0;
1846
+ const w = attribute.getW( i ) || 0;
1847
+
1848
+ array.push( `(${x.toPrecision( PRECISION )}, ${y.toPrecision( PRECISION )}, ${z.toPrecision( PRECISION )}, ${w.toPrecision( PRECISION )})` );
1849
+
1850
+ }
1851
+
1852
+ return array.join( ', ' );
1853
+
1854
+ }
1855
+
1581
1856
  function buildVector4Array( attribute, ints = false ) {
1582
1857
  const array: Array<string> = [];
1583
1858
 
@@ -1599,8 +1874,8 @@
1599
1874
 
1600
1875
  }
1601
1876
 
1602
- function buildVector2Array( attribute, count ) {
1603
-
1877
+ function buildVector2Array( attribute: BufferAttribute | InterleavedBufferAttribute, count: number, flipY: boolean = false ) {
1878
+
1604
1879
  if ( attribute === undefined ) {
1605
1880
 
1606
1881
  console.warn( 'USDZExporter: UVs missing.' );
@@ -1613,10 +1888,11 @@
1613
1888
  for ( let i = 0; i < attribute.count; i ++ ) {
1614
1889
 
1615
1890
  const x = attribute.getX( i );
1616
- const y = attribute.getY( i );
1891
+ let y = attribute.getY( i );
1892
+ if (flipY)
1893
+ y = 1 - y;
1894
+ array.push( `(${x.toPrecision( PRECISION )}, ${y.toPrecision( PRECISION )})` );
1617
1895
 
1618
- array.push( `(${x.toPrecision( PRECISION )}, ${1 - y.toPrecision( PRECISION )})` );
1619
-
1620
1896
  }
1621
1897
 
1622
1898
  return array.join( ', ' );
@@ -1993,10 +2269,10 @@
1993
2269
  }
1994
2270
  ` : ''}
1995
2271
  ${usedUVChannels.has(1) ? `
1996
- def Shader "uvReader_st2"
2272
+ def Shader "uvReader_st1"
1997
2273
  {
1998
2274
  uniform token info:id = "UsdPrimvarReader_float2"
1999
- token inputs:varname = "st2"
2275
+ token inputs:varname = "st1"
2000
2276
  float2 inputs:fallback = (0.0, 0.0)
2001
2277
  float2 outputs:result
2002
2278
  }
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -26,6 +26,7 @@
26
26
  import { ensureQuicklookLinkIsCreated } from "./utils/quicklook.js";
27
27
 
28
28
  const debug = getParam("debugusdz");
29
+ const debugUsdzPruning = getParam("debugusdzpruning");
29
30
 
30
31
  /**
31
32
  * Custom branding for the QuickLook overlay, used by {@link USDZExporter}.
@@ -384,6 +385,7 @@
384
385
 
385
386
  //@ts-ignore
386
387
  exporter.debug = debug;
388
+ exporter.pruneUnusedNodes = !debugUsdzPruning;
387
389
  exporter.keepObject = (object) => {
388
390
  // This explicitly removes geometry and material data from disabled renderers.
389
391
  // Note that this is different to the object itself being active –
src/engine-components/export/usdz/extensions/USDZText.ts CHANGED
@@ -161,9 +161,10 @@
161
161
  height = rt.height;
162
162
  }
163
163
 
164
- newModel.matrix = rotateYAxisMatrix.clone();
164
+ const mat = rotateYAxisMatrix.clone();
165
165
  if (rt) // Not ideal but works for now:
166
- newModel.matrix.premultiply(invertX);
166
+ mat.premultiply(invertX);
167
+ newModel.setMatrix(mat);
167
168
 
168
169
  const color = text.color.clone();
169
170
  newModel.material = new MeshStandardMaterial({ color: color, emissive: color });
src/engine-components/export/usdz/extensions/USDZUI.ts CHANGED
@@ -99,7 +99,7 @@
99
99
 
100
100
  if (shadowComponent) {
101
101
  const mat = shadowComponent.matrix;
102
- shadowRootModel.matrix.copy(mat);
102
+ shadowRootModel.setMatrix(mat);
103
103
 
104
104
  const usdObjectMap = new Map<Object3D, USDObject>();
105
105
  const opacityMap = new Map<Object3D, number>();
@@ -111,7 +111,7 @@
111
111
  if (child === shadowComponent) return;
112
112
 
113
113
  const childModel = USDObject.createEmpty();
114
- childModel.matrix.copy(child.matrix);
114
+ childModel.setMatrix(child.matrix);
115
115
 
116
116
  const childParent = child.parent;
117
117
  const isText = !!childParent && typeof childParent["textContent"] === "string" && childParent["textContent"].length > 0;
src/engine-components/postprocessing/Volume.ts CHANGED
@@ -82,6 +82,15 @@
82
82
  this._isDirty = true;
83
83
  return effect;
84
84
  }
85
+ else if (effect instanceof PostProcessingEffect) {
86
+ // if the effect is part of the shared profile remove it from there
87
+ const si = this.sharedProfile?.components?.indexOf(effect);
88
+ if (si !== undefined && si !== -1) {
89
+ this._isDirty = true;
90
+ this.sharedProfile?.components?.splice(si, 1);
91
+ }
92
+ }
93
+
85
94
  return effect;
86
95
  }
87
96
 
@@ -191,6 +200,8 @@
191
200
  this._activeEffects.push(effect);
192
201
  }
193
202
 
203
+ if(debug) console.log("Apply PostProcessing", this._activeEffects);
204
+
194
205
  if (this._activeEffects.length > 0) {
195
206
  if (!this._postprocessing)
196
207
  this._postprocessing = new PostProcessingHandler(this.context);
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -206,9 +206,8 @@
206
206
  const mat = relativeMatrix
207
207
  .clone()
208
208
  .invert()
209
- model.matrix = mat
210
- // apply session root scale again after undoing the world transformation
211
- .scale(new Vector3(scale, scale, scale));
209
+ // apply session root scale again after undoing the world transformation
210
+ model.setMatrix(mat.scale(new Vector3(scale, scale, scale)));
212
211
 
213
212
  // Unfortunately looks like Apple's docs are incomplete:
214
213
  // https://developer.apple.com/documentation/realitykit/preliminary_anchoringapi#Nest-and-Layer-Anchorable-Prims