Needle Engine

Changes between version 3.37.15-alpha.1 and 3.37.16-alpha
Files changed (10) hide show
  1. src/engine-components/export/usdz/extensions/Animation.ts +34 -5
  2. src/engine/engine_gizmos.ts +45 -2
  3. src/engine/engine_lods.ts +20 -31
  4. src/engine/engine_physics.ts +3 -20
  5. src/engine-components/export/usdz/Extension.ts +5 -5
  6. src/engine-components/export/usdz/extensions/behavior/PhysicsExtension.ts +2 -2
  7. src/engine-components/Renderer.ts +8 -11
  8. src/engine-components/export/usdz/ThreeUSDZExporter.ts +28 -18
  9. src/engine-components/export/usdz/USDZExporter.ts +14 -11
  10. src/engine-components/webxr/WebXRImageTracking.ts +55 -5
src/engine-components/export/usdz/extensions/Animation.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { AnimationClip, Bone,Interpolant, KeyframeTrack, Matrix4, Object3D, PropertyBinding, Quaternion, Vector3 } from "three";
2
2
 
3
+ import { isDevEnvironment, showBalloonWarning } from "../../../../engine/debug/debug.js";
3
4
  import { getParam } from "../../../../engine/engine_utils.js";
4
5
  import { Animator } from "../../../Animator.js";
5
6
  import { GameObject } from "../../../Component.js";
@@ -76,15 +77,37 @@
76
77
  if (animator) this.useRootMotion = animator.applyRootMotion;
77
78
  }
78
79
 
79
- addTrack(track) {
80
+ addTrack(track: KeyframeTrack) {
80
81
  if (!this.clip) {
81
82
  console.error("This is a rest clip but you're trying to add tracks to it – this is likely a bug");
82
83
  return;
83
84
  }
84
85
 
85
86
  if (track.name.endsWith("position")) this.pos = track;
86
- if (track.name.endsWith("quaternion")) this.rot = track;
87
- if (track.name.endsWith("scale")) this.scale = track;
87
+ else if (track.name.endsWith("quaternion")) this.rot = track;
88
+ else if (track.name.endsWith("scale")) this.scale = track;
89
+ else {
90
+ if (track.name.endsWith("activeSelf")) {
91
+ /*
92
+ // Construct a scale track, then apply to the existing scale track.
93
+ // Not supported right now, because it would also require properly tracking that these objects need to be enabled
94
+ // at animation start because they're animating the enabled state... otherwise they just stay disabled from scene start
95
+ const newValues = [...track.values].map((v) => v ? [1,1,1] : [0,0,0]).flat();
96
+ const scaleTrack = new KeyframeTrack(track.name.replace(".activeSelf", ".scale"), track.times, newValues, InterpolateDiscrete);
97
+ if (!this.scale)
98
+ {
99
+ this.scale = scaleTrack;
100
+ console.log("Mock scale track", this.scale);
101
+ }
102
+ */
103
+ console.warn("[USDZ] Animation of enabled/disabled state is not supported for USDZ export and will NOT be exported: " + track.name + " on " + (this.root?.name ?? this.target.name) + ". Animate scale 0/1 instead.");
104
+ }
105
+ else {
106
+ console.warn("[USDZ] Animation track type not supported for USDZ export and will NOT be exported: " + track.name + " on " + (this.root?.name ?? this.target.name) + ". Only .position, .rotation, .scale are supported.");
107
+ }
108
+
109
+ if (isDevEnvironment()) showBalloonWarning("[USDZ] Some animations can't be exported. See console for details.");
110
+ }
88
111
  }
89
112
 
90
113
  getFrames(): number {
@@ -303,6 +326,10 @@
303
326
  const targets = this.rootTargetMap.get(root);
304
327
  const unregisteredNodesForThisClip = new Set(targets);
305
328
  if (clip && clip.tracks) {
329
+ // We could sort so that supported tracks come first, this allows us to support some additional tracks by
330
+ // modifying what has already been written for the supported ones (e.g. enabled -> modify scale).
331
+ // Only needed if we're actually emulating some unsupported track types in addTrack(...).
332
+ // const sortedTracks = clip.tracks.filter(x => !!x).sort((a, _b) => a.name.endsWith("position") || a.name.endsWith("quaternion") || a.name.endsWith("scale") ? -1 : 1);
306
333
  for (const track of clip.tracks) {
307
334
  const parsedPath = PropertyBinding.parseTrackName(track.name);
308
335
  const animationTarget = PropertyBinding.findNode(root, parsedPath.nodeName);
@@ -473,7 +500,7 @@
473
500
  dict: AnimationDict;
474
501
  model: USDObject | undefined = undefined;
475
502
 
476
- private callback?: Function;
503
+ private callback?: (writer: USDWriter, context: USDZExporterContext) => void;
477
504
 
478
505
  constructor(object: Object3D, dict: AnimationDict) {
479
506
  this.object = object;
@@ -488,7 +515,9 @@
488
515
  this.callback = this.onSerialize.bind(this);
489
516
  if (debugSerialization) console.log("REPARENT", model);
490
517
  this.model = model;
491
- this.model.addEventListener("serialize", this.callback);
518
+
519
+ if (this.callback)
520
+ this.model.addEventListener("serialize", this.callback);
492
521
  }
493
522
 
494
523
  skinnedMeshExport(writer: USDWriter, _context: USDZExporterContext) {
src/engine/engine_gizmos.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AxesHelper, Box3, BoxGeometry, BufferAttribute, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Mesh, Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
1
+ import { AxesHelper, Box3, BoxGeometry, BufferAttribute, BufferGeometry, Color, type ColorRepresentation, CylinderGeometry, EdgesGeometry, Line, LineBasicMaterial, LineSegments, Matrix4, Mesh, MeshBasicMaterial,Object3D, Quaternion, SphereGeometry, Vector3 } from 'three';
2
2
  import ThreeMeshUI, { Inline, Text } from "three-mesh-ui"
3
3
  import { type Options } from 'three-mesh-ui/build/types/core/elements/MeshUIBaseElement.js';
4
4
 
@@ -57,7 +57,7 @@
57
57
  element.position.z = position.z;
58
58
  return element as LabelHandle;
59
59
  }
60
-
60
+
61
61
  static DrawRay(origin: Vec3, dir: Vec3, color: ColorRepresentation = defaultColor, duration: number = 0, depthTest: boolean = true) {
62
62
  if (!Gizmos.enabled) return;
63
63
  const obj = Internal.getLine(duration);
@@ -163,6 +163,38 @@
163
163
  obj.material["wireframe"] = wireframe;
164
164
  this.DrawLine(pt0, pt1, color, duration, depthTest);
165
165
  }
166
+
167
+ /**
168
+ * Render a wireframe mesh in the scene. The mesh will be removed after the given duration (if duration is 0 it will be rendered for one frame).
169
+ * If a mesh object is provided then the mesh's matrixWorld and geometry will be used. Otherwise, the provided matrix and geometry will be used.
170
+ * @param options the options for the wire mesh
171
+ * @param options.duration the duration in seconds the mesh will be rendered. If 0 it will be rendered for one frame
172
+ * @param options.color the color of the wire mesh
173
+ * @param options.depthTest if true the wire mesh will be rendered with depth test
174
+ * @param options.mesh the mesh object to render (if it is provided the matrix and geometry will be used)
175
+ * @param options.matrix the matrix of the mesh to render
176
+ * @param options.geometry the geometry of the mesh to render
177
+ * @example
178
+ * ```typescript
179
+ * Gizmos.DrawWireMesh({ duration: 1, color: 0xff0000, mesh: myMesh });
180
+ * ```
181
+ */
182
+ static DrawWireMesh(options: { duration?: number, color?: ColorRepresentation, depthTest?: boolean } & ({ mesh: Mesh } | { matrix: Matrix4, geometry: BufferGeometry })) {
183
+ const mesh = Internal.getMesh(options.duration ?? 0);
184
+ if ("mesh" in options) {
185
+ mesh.geometry = options.mesh.geometry;
186
+ mesh.matrix.copy(options.mesh.matrixWorld);
187
+ }
188
+ else {
189
+ mesh.geometry = options.geometry;
190
+ mesh.matrix.copy(options.matrix);
191
+ }
192
+ mesh.matrixAutoUpdate = false;
193
+ mesh.matrixWorldAutoUpdate = false;
194
+ mesh.material["color"].set(options.color ?? defaultColor);
195
+ mesh.material["depthTest"] = options.depthTest ?? true;
196
+ mesh.material["wireframe"] = true;
197
+ }
166
198
  }
167
199
 
168
200
  const box: BoxGeometry = new BoxGeometry(1, 1, 1);
@@ -291,10 +323,21 @@
291
323
  return arrowHead;
292
324
  }
293
325
 
326
+ static getMesh(duration: number): Mesh {
327
+ let mesh = this.mesh.pop();
328
+ if (!mesh) {
329
+ mesh = new Mesh();
330
+ mesh.material = new MeshBasicMaterial();
331
+ }
332
+ this.registerTimedObject(Context.Current, mesh, duration, this.mesh);
333
+ return mesh;
334
+ }
335
+
294
336
  private static linesCache: Array<Line> = [];
295
337
  private static spheresCache: Mesh[] = [];
296
338
  private static boxesCache: Mesh[] = [];
297
339
  private static arrowHeadsCache: Mesh[] = [];
340
+ private static mesh: Mesh[] = [];
298
341
  private static textLabelCache: Array<Text> = [];
299
342
 
300
343
  private static registerTimedObject(context: Context, object: Object3D, duration: number, cache: Array<Object3D>) {
src/engine/engine_lods.ts CHANGED
@@ -1,10 +1,9 @@
1
- import { LODsManager as _LODsManager, NEEDLE_progressive,NEEDLE_progressive_mesh_model, NEEDLE_progressive_plugin } from "@needle-tools/gltf-progressive";
1
+ import { LODsManager as _LODsManager, NEEDLE_progressive, NEEDLE_progressive_mesh_model, NEEDLE_progressive_plugin } from "@needle-tools/gltf-progressive";
2
2
  import { Box3, BufferGeometry, Camera, Mesh, PerspectiveCamera, Scene, Sphere, Vector3, WebGLRenderer } from "three";
3
3
 
4
4
  import { findResourceUsers } from "./engine_assetdatabase.js";
5
5
  import type { Context } from "./engine_context.js";
6
6
  import { Gizmos } from "./engine_gizmos.js";
7
- import { getRaycastMesh, setRaycastMesh } from "./engine_physics.js";
8
7
  import { getTempVector } from "./engine_three_utils.js";
9
8
  import { IGameObject } from "./engine_types.js";
10
9
  import { getParam } from "./engine_utils.js";
@@ -21,6 +20,20 @@
21
20
  readonly context: Context;
22
21
  private _lodsManager?: _LODsManager;
23
22
 
23
+ /**
24
+ * The target triangle density is the desired max amount of triangles on screen when the mesh is filling the screen.
25
+ * @default 200_000
26
+ */
27
+ get targetTriangleDensity() {
28
+ return this._lodsManager?.targetTriangleDensity ?? -1;
29
+ }
30
+ set targetTriangleDensity(value: number) {
31
+ if (!this._lodsManager) {
32
+ return;
33
+ }
34
+ this._lodsManager.targetTriangleDensity = value;
35
+ }
36
+
24
37
  constructor(context: Context) {
25
38
  this.context = context;
26
39
  }
@@ -28,41 +41,17 @@
28
41
  /** @internal */
29
42
  setRenderer(renderer: WebGLRenderer) {
30
43
  this._lodsManager?.disable();
31
- this._lodsManager = new _LODsManager(renderer);
32
- this._lodsManager.plugins.push(this);
44
+ _LODsManager.removePlugin(this);
45
+ _LODsManager.addPlugin(this);
46
+ _LODsManager.debugDrawLine = Gizmos.DrawLine;
47
+ this._lodsManager = _LODsManager.get(renderer);
33
48
  this._lodsManager.enable();
34
- _LODsManager.debugDrawLine = Gizmos.DrawLine;
35
49
  }
36
50
 
37
- /** @internal */
38
- onBeforeGetLODMesh(mesh: Mesh): void {
39
- if (!getRaycastMesh(mesh)) {
40
- setRaycastMesh(mesh, mesh.geometry as BufferGeometry);
41
- }
42
- }
43
51
 
44
52
  /** @internal */
45
- onRegisteredNewMesh(mesh: Mesh, _ext: NEEDLE_progressive_mesh_model): void {
46
- const geometry = mesh.geometry as BufferGeometry;
47
- if (geometry) {
48
- geometry["needle:raycast-mesh"] = true;
49
- if (!getRaycastMesh(mesh)) {
50
- if (debug) console.log("Set raycast mesh", mesh.name, mesh.uuid, geometry);
51
- setRaycastMesh(mesh, geometry);
52
- findResourceUsers(geometry, true).forEach(user => {
53
- if (user instanceof Mesh) {
54
- setRaycastMesh(user, geometry);
55
- }
56
- });
57
- }
58
- }
59
- }
60
-
61
-
62
-
63
- /** @internal */
64
53
  onAfterUpdatedLOD(_renderer: WebGLRenderer, _scene: Scene, camera: Camera, mesh: Mesh, level: number): void {
65
- if(debug) this.onRenderDebug(camera, mesh, level);
54
+ if (debug) this.onRenderDebug(camera, mesh, level);
66
55
  }
67
56
 
68
57
  private onRenderDebug(camera: Camera, mesh: Mesh, level: number) {
src/engine/engine_physics.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { ArrayCamera, AxesHelper, Box3, BufferGeometry,Camera, type Intersection, Layers, Line, Mesh, Object3D, PerspectiveCamera, Ray, Raycaster, Sphere, Vector2, Vector3 } from 'three'
1
+ import { getRaycastMesh } from '@needle-tools/gltf-progressive';
2
+ import { ArrayCamera, AxesHelper, Box3, BufferGeometry, Camera, type Intersection, Layers, Line, Mesh, Object3D, PerspectiveCamera, Ray, Raycaster, Sphere, Vector2, Vector3 } from 'three'
2
3
 
3
4
  import { Gizmos } from './engine_gizmos.js';
4
5
  import { Context } from './engine_setup.js';
@@ -10,25 +11,6 @@
10
11
  const debugPhysics = getParam("debugphysics");
11
12
  const layerMaskHelper: Layers = new Layers();
12
13
 
13
- export function getRaycastMesh(obj: Object3D) {
14
- if (obj.userData?.["needle:raycast-mesh"] instanceof BufferGeometry) {
15
- return obj.userData["needle:raycast-mesh"];
16
- }
17
- if (debugPhysics) {
18
- if (!obj["needle:warned about missing raycast mesh"]) {
19
- obj["needle:warned about missing raycast mesh"] = true;
20
- console.warn("No raycast mesh found for object: " + obj.name);
21
- }
22
- }
23
- return null;
24
- }
25
- export function setRaycastMesh(obj: Object3D, geom: BufferGeometry) {
26
- if (obj.type === "Mesh" || obj.type === "SkinnedMesh") {
27
- if (!obj.userData) obj.userData = {};
28
- obj.userData["needle:raycast-mesh"] = geom;
29
- if(debugPhysics) delete obj["needle:warned about missing raycast mesh"];
30
- }
31
- }
32
14
 
33
15
  export declare type RaycastTestObjectReturnType = void | boolean | "continue in children";
34
16
  export declare type RaycastTestObjectCallback = (obj: Object3D) => RaycastTestObjectReturnType;
@@ -328,6 +310,7 @@
328
310
  const raycastMesh = getRaycastMesh(obj);
329
311
  if (raycastMesh) mesh.geometry = raycastMesh;
330
312
  raycaster.intersectObject(obj, false, results);
313
+ if (debugPhysics) Gizmos.DrawWireMesh({ mesh: obj as Mesh, depthTest: false, duration: .1 })
331
314
  mesh.geometry = geometry;
332
315
  }
333
316
  }
src/engine-components/export/usdz/Extension.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Object3D } from "three";
2
2
 
3
- import { USDObject, USDZExporterContext } from "./ThreeUSDZExporter.js";
3
+ import { USDObject, USDWriter, USDZExporterContext } from "./ThreeUSDZExporter.js";
4
4
 
5
5
  /**
6
6
  * Interface for USDZ Exporter Extensions used by {@link USDZExporter}
@@ -14,12 +14,12 @@
14
14
  /**
15
15
  * Called before the document is built
16
16
  */
17
- onBeforeBuildDocument?(context);
17
+ onBeforeBuildDocument?(context: USDZExporterContext);
18
18
  /**
19
19
  * Called after the document is built
20
20
  */
21
- onAfterBuildDocument?(context);
21
+ onAfterBuildDocument?(context: USDZExporterContext);
22
22
  onExportObject?(object: Object3D, model: USDObject, context: USDZExporterContext);
23
- onAfterSerialize?(context);
24
- onAfterHierarchy?(context, writer: any);
23
+ onAfterSerialize?(context: USDZExporterContext);
24
+ onAfterHierarchy?(context: USDZExporterContext, writer: USDWriter);
25
25
  }
src/engine-components/export/usdz/extensions/behavior/PhysicsExtension.ts CHANGED
@@ -111,11 +111,11 @@
111
111
  if (box) {
112
112
  writer.appendLine(`token shapeType = "Box"`);
113
113
  writer.appendLine(`float3 extent = (${box.max.x - box.min.x}, ${box.max.y - box.min.y}, ${box.max.z - box.min.z})`);
114
- console.log("[USDZ]: Only Box, Sphere, and Capsule colliders are supported in visionOS/iOS. MeshCollider will be exported as Box", colliderSource);
114
+ console.log("[USDZ] Only Box, Sphere, and Capsule colliders are supported in visionOS/iOS. MeshCollider will be exported as Box", colliderSource);
115
115
  }
116
116
  }
117
117
  else {
118
- console.warn("[USDZ]: Only Box, Sphere, and Capsule colliders are supported in visionOS/iOS. Ignoring collider:", colliderSource)
118
+ console.warn("[USDZ] Only Box, Sphere, and Capsule colliders are supported in visionOS/iOS. Ignoring collider:", colliderSource)
119
119
  }
120
120
 
121
121
  writer.beginBlock(`def RealityKitStruct "pose"`, "{", true );
src/engine-components/Renderer.ts CHANGED
@@ -1,19 +1,16 @@
1
- import { AxesHelper, Box3, BufferGeometry, Color, InstancedMesh, Material, Matrix4, Mesh, MeshBasicMaterial, Object3D, PerspectiveCamera, RawShaderMaterial, SkinnedMesh, Sphere, Texture, Vector2, Vector3, Vector4 } from "three";
1
+ import { getRaycastMesh } from "@needle-tools/gltf-progressive";
2
+ import { AxesHelper, Material, Mesh, Object3D, SkinnedMesh, Texture, Vector4 } from "three";
2
3
 
3
4
  import { showBalloonWarning } from "../engine/debug/index.js";
4
5
  import { getComponent, getOrAddComponent } from "../engine/engine_components.js";
5
- import { ContextEvent, NeedleEngine } from "../engine/engine_context_registry.js";
6
- import { Gizmos, LabelHandle } from "../engine/engine_gizmos.js";
7
- import { $instancingAutoUpdateBounds, $instancingRenderer, InstancingUtil, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
8
- import { onStart } from "../engine/engine_lifecycle_api.js";
6
+ import { Gizmos } from "../engine/engine_gizmos.js";
7
+ import { InstancingUtil, NEED_UPDATE_INSTANCE_KEY } from "../engine/engine_instancing.js";
9
8
  import { isLocalNetwork } from "../engine/engine_networking_utils.js";
10
- import { getRaycastMesh } from "../engine/engine_physics.js";
11
9
  import { serializable } from "../engine/engine_serialization_decorator.js";
12
- import { Context, FrameEvent } from "../engine/engine_setup.js";
13
- import { getTempVector, getWorldDirection, getWorldPosition, getWorldScale } from "../engine/engine_three_utils.js";
14
- import type { IGameObject, IRenderer, ISharedMaterials } from "../engine/engine_types.js";
15
- import { getParam, isMobileDevice } from "../engine/engine_utils.js";
16
- import { NEEDLE_progressive, ProgressiveMaterialTextureLoadingResult } from "../engine/extensions/NEEDLE_progressive.js";
10
+ import { FrameEvent } from "../engine/engine_setup.js";
11
+ import { getTempVector } from "../engine/engine_three_utils.js";
12
+ import type { IRenderer, ISharedMaterials } from "../engine/engine_types.js";
13
+ import { getParam } from "../engine/engine_utils.js";
17
14
  import { NEEDLE_render_objects } from "../engine/extensions/NEEDLE_render_objects.js";
18
15
  import { setCustomVisibility } from "../engine/js-extensions/Layers.js";
19
16
  import { Behaviour, GameObject } from "./Component.js";
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -95,7 +95,10 @@
95
95
 
96
96
  uuid: string;
97
97
  name: string;
98
- type?: string; // by default, Xform is used
98
+ /** If no type is provided, type is chosen automatically (Xform or Mesh) */
99
+ type?: string;
100
+ /** MaterialBindingAPI and SkelBindingAPI are handled automatically, extra schemas can be added here */
101
+ extraSchemas: string[] = [];
99
102
  displayName?: string;
100
103
  visibility?: "inherited" | "invisible"; // defaults to "inherited" in USD
101
104
  matrix: Matrix4;
@@ -227,14 +230,14 @@
227
230
 
228
231
  }
229
232
 
230
- addEventListener( evt, listener ) {
233
+ addEventListener( evt, listener: ( writer: USDWriter, context: USDZExporterContext ) => void ) {
231
234
 
232
235
  if ( ! this._eventListeners[ evt ] ) this._eventListeners[ evt ] = [];
233
236
  this._eventListeners[ evt ].push( listener );
234
237
 
235
238
  }
236
239
 
237
- removeEventListener( evt, listener ) {
240
+ removeEventListener( evt, listener: ( writer: USDWriter, context: USDZExporterContext ) => void ) {
238
241
 
239
242
  if ( ! this._eventListeners[ evt ] ) return;
240
243
  const index = this._eventListeners[ evt ].indexOf( listener );
@@ -812,6 +815,7 @@
812
815
  Progress.end("export-usdz-resources");
813
816
 
814
817
  const writer = new USDWriter();
818
+ const arAnchoringOptions = context.exporter.sceneAnchoringOptions.ar;
815
819
 
816
820
  writer.beginBlock( `def Xform "${context.document.name}"` );
817
821
 
@@ -819,20 +823,21 @@
819
823
  kind = "sceneLibrary"
820
824
  )` );
821
825
 
822
- writer.beginBlock( `def Xform "Scene" (
823
- apiSchemas = ["Preliminary_AnchoringAPI"]
824
- customData = {
825
- bool preliminary_collidesWithEnvironment = 0
826
- string sceneName = "Scene"
827
- }
828
- sceneName = "Scene"
829
- )` );
826
+ writer.beginBlock( `def Xform "Scene"`, '(', false);
827
+ writer.appendLine( `apiSchemas = ["Preliminary_AnchoringAPI"]` );
828
+ writer.appendLine( `customData = {`);
829
+ writer.appendLine( ` bool preliminary_collidesWithEnvironment = 0` );
830
+ writer.appendLine( ` string sceneName = "Scene"`);
831
+ writer.appendLine( `}` );
832
+ writer.appendLine( `sceneName = "Scene"` );
833
+ writer.closeBlock( ')' );
834
+ writer.beginBlock();
830
835
 
831
- writer.appendLine( `token preliminary:anchoring:type = "${context.exporter.sceneAnchoringOptions.ar.anchoring.type}"` );
832
- if (context.exporter.sceneAnchoringOptions.ar.anchoring.type === 'plane')
833
- writer.appendLine( `token preliminary:planeAnchoring:alignment = "${context.exporter.sceneAnchoringOptions.ar.planeAnchoring.alignment}"` );
836
+ writer.appendLine( `token preliminary:anchoring:type = "${arAnchoringOptions.anchoring.type}"` );
837
+ if (arAnchoringOptions.anchoring.type === 'plane')
838
+ writer.appendLine( `token preliminary:planeAnchoring:alignment = "${arAnchoringOptions.planeAnchoring.alignment}"` );
834
839
  // 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.
835
- if (context.exporter.sceneAnchoringOptions.ar.anchoring.type === 'image')
840
+ if (arAnchoringOptions.anchoring.type === 'image')
836
841
  writer.appendLine( `rel preliminary:imageAnchoring:referenceImage = </${context.document.name}/Scenes/Scene/AnchoringReferenceImage>` );
837
842
  writer.appendLine();
838
843
 
@@ -1228,8 +1233,7 @@
1228
1233
 
1229
1234
  const isSkinnedMesh = geometry && geometry.isBufferGeometry && geometry.attributes.skinIndex !== undefined && geometry.attributes.skinIndex.count > 0;
1230
1235
  const objType = isSkinnedMesh ? 'SkelRoot' : 'Xform';
1231
- const apiSchemas = isSkinnedMesh ? '"MaterialBindingAPI", "SkelBindingAPI"' : '"MaterialBindingAPI"';
1232
-
1236
+ const _apiSchemas = new Array<string>();
1233
1237
  writer.appendLine();
1234
1238
  if ( geometry ) {
1235
1239
  writer.beginBlock( `def ${objType} "${name}"`, "(", false );
@@ -1239,7 +1243,9 @@
1239
1243
  writer.appendLine(`prepend references = @./geometries/${getGeometryName(geometry, name)}.usda@</Geometry_doubleSided>`);
1240
1244
  else
1241
1245
  writer.appendLine(`prepend references = @./geometries/${getGeometryName(geometry, name)}.usda@</Geometry>`);
1242
- writer.appendLine(`prepend apiSchemas = [${apiSchemas}]`);
1246
+ _apiSchemas.push("MaterialBindingAPI");
1247
+ if (isSkinnedMesh)
1248
+ _apiSchemas.push("SkelBindingAPI");
1243
1249
  }
1244
1250
  else if ( camera )
1245
1251
  writer.beginBlock( `def Camera "${name}"`, "(", false );
@@ -1251,6 +1257,10 @@
1251
1257
  if (model.displayName)
1252
1258
  writer.appendLine(`displayName = "${model.displayName}"`);
1253
1259
  if (model.type === undefined) {
1260
+ if (model.extraSchemas?.length)
1261
+ _apiSchemas.push(...model.extraSchemas);
1262
+ if (_apiSchemas.length)
1263
+ writer.appendLine(`prepend apiSchemas = [${_apiSchemas.map(s => `"${s}"`).join(', ')}]`);
1254
1264
  writer.closeBlock( ")" );
1255
1265
  writer.beginBlock();
1256
1266
  }
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -578,21 +578,17 @@
578
578
  private _rootRotationBeforeExport: Quaternion = new Quaternion();
579
579
  private _rootScaleBeforeExport: Vector3 = new Vector3();
580
580
 
581
- private applyWebARSessionRoot() {
582
- if (!this.objectToExport) return;
581
+ getARScaleAndTarget(): { scale: number, _invertForward: boolean, target: Object3D } {
582
+ if (!this.objectToExport) return { scale: 1, _invertForward: false, target: this.gameObject };
583
583
 
584
- // first check if the sessionroot is in the parent hierarchy
585
- // if that's the case we apply the scale to the object being exported
586
584
  let sessionRoot = GameObject.getComponentInParent(this.objectToExport, WebARSessionRoot);
587
585
  const hasSessionRootInParentHierarchy = sessionRoot !== null && sessionRoot !== undefined;
588
- // if it's not in the parent hierarchy BUT in the child hierarchy we apply it to the sessionRoot object itself
589
- // that's the case when no objectToExport is explictly assigned and the whole scene is being exported
590
586
  if(!sessionRoot) sessionRoot = GameObject.getComponentInChildren(this.objectToExport, WebARSessionRoot);
591
587
 
592
588
  if (debug) console.log("applyWebARSessionRoot", sessionRoot, sessionRoot?.arScale);
593
589
 
594
590
  let arScale = 1;
595
- let invertForward = false;
591
+ let _invertForward = false;
596
592
  const target = hasSessionRootInParentHierarchy || !sessionRoot ? this.objectToExport : sessionRoot.gameObject;
597
593
 
598
594
  if (!sessionRoot) {
@@ -601,12 +597,19 @@
601
597
  }
602
598
  else {
603
599
  arScale = sessionRoot.arScale;
604
- invertForward = sessionRoot.invertForward;
600
+ _invertForward = sessionRoot.invertForward;
605
601
  }
606
-
607
- // either apply the scale to the object being exported or to the sessionRoot object itself
602
+
608
603
  const scale = 1 / arScale;
609
604
 
605
+ return { scale, _invertForward, target };
606
+ }
607
+
608
+ private applyWebARSessionRoot() {
609
+ if (!this.objectToExport) return;
610
+
611
+ const { scale, _invertForward, target } = this.getARScaleAndTarget();
612
+
610
613
  this._rootSessionRootWasAppliedTo = target;
611
614
  this._rootPositionBeforeExport.copy(target.position);
612
615
  this._rootRotationBeforeExport.copy(target.quaternion);
@@ -614,7 +617,7 @@
614
617
 
615
618
  target.scale.multiplyScalar(scale);
616
619
  // legacy, should likely be deleted
617
- if (invertForward) {
620
+ if (_invertForward) {
618
621
  target.matrix.multiply(USDZExporter.invertForwardMatrix);
619
622
  }
620
623
  // udate childs as well
src/engine-components/webxr/WebXRImageTracking.ts CHANGED
@@ -1,11 +1,13 @@
1
1
  import { Matrix4, Object3D, Quaternion, Vector3 } from "three";
2
+ import { Object3DEventMap } from "three";
2
3
 
3
- import { showBalloonWarning } from "../../engine/debug/index.js";
4
+ import { isDevEnvironment, showBalloonWarning } from "../../engine/debug/index.js";
4
5
  import { AssetReference } from "../../engine/engine_addressables.js";
5
6
  import { serializable } from "../../engine/engine_serialization.js";
6
7
  import { CircularBuffer, getParam } from "../../engine/engine_utils.js";
7
8
  import { type NeedleXREventArgs, NeedleXRSession } from "../../engine/xr/api.js";
8
- import { imageToCanvas, USDWriter, USDZExporterContext } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
9
+ import { IUSDExporterExtension } from "../../engine-components/export/usdz/Extension.js";
10
+ import { imageToCanvas, USDObject, USDWriter, USDZExporterContext } from "../../engine-components/export/usdz/ThreeUSDZExporter.js";
9
11
  import { USDZExporter } from "../../engine-components/export/usdz/USDZExporter.js";
10
12
  import { Behaviour, GameObject } from "../Component.js";
11
13
  import { Renderer } from "../Renderer.js";
@@ -143,7 +145,7 @@
143
145
  hideWhenTrackingIsLost: boolean = true;
144
146
  }
145
147
 
146
- class ImageTrackingExtension {
148
+ class ImageTrackingExtension implements IUSDExporterExtension {
147
149
 
148
150
  get extensionName() { return "image-tracking"; }
149
151
 
@@ -159,14 +161,62 @@
159
161
 
160
162
  onAfterHierarchy(_context: USDZExporterContext, writer: USDWriter) {
161
163
  writer.beginBlock(`def Preliminary_ReferenceImage "AnchoringReferenceImage"`);
162
- writer.appendLine(`uniform asset image = @tracker/` + this.filename + `@`);
164
+ writer.appendLine(`uniform asset image = @image_tracking/` + this.filename + `@`);
163
165
  writer.appendLine(`uniform double physicalWidth = ` + (this.widthInMeters * 100).toFixed(8));
164
166
  writer.closeBlock();
165
167
  }
166
168
 
169
+ onBeforeBuildDocument(_context: USDZExporterContext) {
170
+ const imageTracking = GameObject.findObjectOfType(WebXRImageTracking);
171
+ if (!imageTracking || !imageTracking.trackedImages) return;
172
+
173
+ // Warn if more than one tracked image is used for USDZ; that's not supported at the moment.
174
+ if (imageTracking.trackedImages.length > 1)
175
+ {
176
+ if (isDevEnvironment()) showBalloonWarning("USDZ: Only one tracked image is supported.");
177
+ console.warn("USDZ: Only one tracked image is supported.");
178
+ }
179
+ }
180
+
167
181
  onAfterSerialize(context: USDZExporterContext) {
168
- context.files['tracker/' + this.filename] = this.imageData;
182
+ context.files['image_tracking/' + this.filename] = this.imageData;
169
183
  }
184
+
185
+ onExportObject(object: Object3D<Object3DEventMap>, model: USDObject, _context: USDZExporterContext) {
186
+ const imageTracking = GameObject.findObjectOfType(WebXRImageTracking);
187
+ if (!imageTracking || !imageTracking.trackedImages) return;
188
+
189
+ for (const trackedImage of imageTracking.trackedImages) {
190
+ if (trackedImage.object?.asset === object) {
191
+ const exporter = GameObject.findObjectOfType(USDZExporter);
192
+ if (!exporter) continue;
193
+
194
+ const { scale } = exporter.getARScaleAndTarget();
195
+
196
+ // We have to reset the image tracking object's position and rotation, because QuickLook applies them.
197
+ // On Android WebXR they're replaced by the tracked data.
198
+ model.matrix = object.matrixWorld.clone()
199
+ .invert()
200
+ .multiply(new Matrix4().makeRotationY(Math.PI))
201
+ // apply session root scale again after undoing the world transformation
202
+ // TODO check if we actually do that in WebXR image tracking
203
+ .scale(new Vector3(scale, scale, scale));
204
+
205
+ // Unfortunately looks like Apple's docs are incomplete:
206
+ // https://developer.apple.com/documentation/realitykit/preliminary_anchoringapi#Nest-and-Layer-Anchorable-Prims
207
+ // In practice, it seems that nesting is not allowed – no image tracking will be applied to nested objects.
208
+ // Thus, we can't have separate transforms for "regularly placing content" and "placing content with an image marker".
209
+ // model.extraSchemas.push("Preliminary_AnchoringAPI");
210
+ model.addEventListener("serialize", (_writer: USDWriter, _context: USDZExporterContext) => {
211
+ // writer.appendLine( `token preliminary:anchoring:type = "image"` );
212
+ // writer.appendLine( `rel preliminary:imageAnchoring:referenceImage = </${context.document.name}/Scenes/Scene/AnchoringReferenceImage>` );
213
+ });
214
+
215
+ // We can only apply this to the first tracked image, more are not supported by QuickLook.
216
+ break;
217
+ }
218
+ }
219
+ }
170
220
  }
171
221
 
172
222
  export class WebXRImageTracking extends Behaviour {