Needle Engine

Changes between version 3.11.5-beta and 3.11.6-beta
Files changed (21) hide show
  1. src/engine-components/Animation.ts +1 -1
  2. src/engine-components/export/usdz/extensions/Animation.ts +80 -36
  3. src/engine-components/AnimationUtils.ts +4 -0
  4. src/engine-components/export/usdz/utils/animationutils.ts +93 -18
  5. src/engine-components/AnimatorController.ts +1 -1
  6. src/engine-components/export/usdz/extensions/behavior/Behaviour.ts +6 -3
  7. src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts +3 -1
  8. src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts +4 -0
  9. src/engine/debug/debug_overlay.ts +2 -2
  10. src/engine/engine_components.ts +4 -3
  11. src/engine/engine_element.ts +15 -0
  12. src/engine/engine_license.ts +1 -1
  13. src/engine/engine_networking_peer.ts +11 -3
  14. src/engine/engine_networking.ts +8 -6
  15. src/engine/extensions/NEEDLE_progressive.ts +6 -5
  16. src/engine/extensions/NEEDLE_render_objects.ts +0 -5
  17. src/engine-components/export/usdz/ThreeUSDZExporter.ts +6 -1
  18. src/engine-components/timeline/TimelineTracks.ts +5 -0
  19. src/engine-components/export/usdz/USDZExporter.ts +15 -6
  20. src/engine-components/webxr/WebARSessionRoot.ts +1 -1
  21. src/engine-components/XRFlag.ts +6 -6
src/engine-components/Animation.ts CHANGED
@@ -67,7 +67,7 @@
67
67
  }
68
68
 
69
69
  get animations(): AnimationClip[] {
70
- return this.gameObject.animations;
70
+ return this.gameObject.animations || [];
71
71
  }
72
72
  /**
73
73
  * @deprecated Currently unsupported
src/engine-components/export/usdz/extensions/Animation.ts CHANGED
@@ -4,9 +4,10 @@
4
4
  import { USDObject, buildMatrix } from "../ThreeUSDZExporter.js";
5
5
  import { IUSDExporterExtension } from "../Extension.js";
6
6
 
7
- import { Object3D, Matrix4, Vector3, Quaternion, Interpolant, AnimationClip, KeyframeTrack } from "three";
7
+ import { Object3D, Matrix4, Vector3, Quaternion, Interpolant, AnimationClip, KeyframeTrack, PropertyBinding } from "three";
8
8
 
9
9
  const debug = getParam("debugusdzanimation");
10
+ const debugSerialization = getParam("debugusdzanimationserialization");
10
11
 
11
12
  export interface UsdzAnimation {
12
13
  createAnimation(ext: AnimationExtension, model: USDObject, context);
@@ -40,6 +41,7 @@
40
41
  private ext: AnimationExtension;
41
42
  private root: Object3D;
42
43
  private target: Object3D;
44
+ private duration = 0;
43
45
 
44
46
  constructor(ext: AnimationExtension, root: Object3D, target: Object3D, clip: AnimationClip) {
45
47
  this.ext = ext;
@@ -52,6 +54,9 @@
52
54
  if (track.name.endsWith("position")) this.pos = track;
53
55
  if (track.name.endsWith("quaternion")) this.rot = track;
54
56
  if (track.name.endsWith("scale")) this.scale = track;
57
+
58
+ // this.duration = Math.max(this.duration, track.times[track.times.length - 1]);
59
+ this.duration = this.clip.duration;
55
60
  }
56
61
 
57
62
  getFrames(): number {
@@ -59,11 +64,12 @@
59
64
  }
60
65
 
61
66
  getDuration(): number {
62
- const times = this.pos?.times ?? this.rot?.times ?? this.scale?.times;
63
- if (!times) return 0;
64
- return times[times.length - 1];
67
+ return this.duration;
65
68
  }
66
69
 
70
+ static animationDurationPadding = 1;
71
+
72
+ /*
67
73
  getStartTime(arr: TransformData[]): number {
68
74
  let sum = 0;
69
75
  for (let i = 0; i < arr.length; i++) {
@@ -71,10 +77,37 @@
71
77
  if (entry === this) {
72
78
  return sum;
73
79
  }
74
- else sum += entry.getDuration();
80
+ // we need some padding, otherwise we can end up with animations
81
+ // written on the SAME time sample belonging to different clips
82
+ else sum += entry.getDuration() + TransformData.animationDurationPadding;
75
83
  }
76
84
  return sum;
77
85
  }
86
+ */
87
+
88
+ getStartTime(dict: AnimationDict): number {
89
+
90
+ // TODO this collection of clip slots should happen once and not for each node & track!
91
+
92
+ // collect all clips in dict
93
+ // walk through clips until we find ours and sum up durations
94
+ // to make sure each clip gets a unique slot
95
+ const clips : Array<AnimationClip> = [];
96
+ for (const [key, value] of dict) {
97
+ for (const entry of value) {
98
+ if (!clips.includes(entry.clip)) clips.push(entry.clip);
99
+ }
100
+ }
101
+
102
+ if (debug) console.log(clips, this.clip);
103
+
104
+ let sum = 0;
105
+ for (const clip of clips) {
106
+ if (clip === this.clip) break;
107
+ sum += clip.duration + TransformData.animationDurationPadding;
108
+ }
109
+ return sum;
110
+ }
78
111
  }
79
112
 
80
113
  declare type AnimationDict = Map<Object3D, Array<TransformData>>;
@@ -91,21 +124,27 @@
91
124
  if (!targets) return Infinity;
92
125
  let longestStartTime: number = -1;
93
126
  for (const target of targets) {
127
+ if (debug) console.log(" " + target.name)
94
128
  const data = this.dict.get(target);
95
129
  let startTimeInSeconds = 0;
96
130
  if (data?.length) {
131
+ let dataContainedClip = false;
97
132
  for (const entry of data) {
133
+ if (debug) console.log(" ", entry.clip.name, entry.getDuration());
98
134
  if (entry.clip === clip) {
135
+ dataContainedClip = true;
99
136
  break;
100
137
  }
101
- startTimeInSeconds += entry.getDuration();
138
+ startTimeInSeconds += entry.getDuration() + TransformData.animationDurationPadding;
102
139
  }
103
- longestStartTime = Math.max(longestStartTime, startTimeInSeconds);
140
+ if (dataContainedClip)
141
+ longestStartTime = Math.max(longestStartTime, startTimeInSeconds);
104
142
  }
105
143
  else {
106
144
  console.warn("No animation found on root", root, clip, data);
107
145
  }
108
146
  }
147
+ if (debug) console.log("getStartTime01 for " + clip.name + ": " + longestStartTime, root, clip, targets);
109
148
  return longestStartTime;
110
149
  }
111
150
 
@@ -114,9 +153,11 @@
114
153
  if (!this.rootTargetMap.has(root)) this.rootTargetMap.set(root, []);
115
154
  // this.rootTargetMap.get(root)?.push(clip);
116
155
 
156
+ if (clip.tracks) {
117
157
  for (const track of clip.tracks) {
118
- const trackName = track.name.split(".")[2];
119
- const animationTarget = root.getObjectByName(trackName); // object name
158
+ const parsedPath = PropertyBinding.parseTrackName(track.name);
159
+ const animationTarget = PropertyBinding.findNode(root, parsedPath.nodeName);
160
+
120
161
  if (!animationTarget) {
121
162
  console.warn("no object found for track", track.name, "using " + root.name + " instead");
122
163
  continue;
@@ -140,6 +181,7 @@
140
181
  const targets = this.rootTargetMap.get(root);
141
182
  if (!targets?.includes(animationTarget)) targets?.push(animationTarget);
142
183
  }
184
+ }
143
185
 
144
186
  // get the entry for this object.
145
187
  // This doesnt work if we have clips animating multiple objects
@@ -148,8 +190,7 @@
148
190
  }
149
191
 
150
192
  onAfterHierarchy(_context) {
151
- if (debug)
152
- console.log(this.dict);
193
+ if (debug) console.log("Animation clips per animation target node", this.dict);
153
194
  }
154
195
 
155
196
  private serializers: SerializeAnimation[] = [];
@@ -158,8 +199,7 @@
158
199
  for (const ser of this.serializers) {
159
200
  const parent = ser.model?.parent;
160
201
  const isEmptyParent = parent?.isDynamic === true;
161
- if (debug)
162
- console.log(isEmptyParent, ser.model?.parent);
202
+ if (debugSerialization) console.log(isEmptyParent, ser.model?.parent);
163
203
  if (isEmptyParent) {
164
204
  ser.registerCallback(parent);
165
205
  }
@@ -203,28 +243,21 @@
203
243
  }
204
244
  if (!this.callback)
205
245
  this.callback = this.onSerialize.bind(this);
206
- if (debug)
207
- console.log("REPARENT", model);
246
+ if (debugSerialization) console.log("REPARENT", model);
208
247
  this.model = model;
209
248
  this.model.addEventListener("serialize", this.callback);
210
249
  }
211
250
 
212
251
  onSerialize(writer, _context) {
213
252
  if (!this.model) return;
214
- if (debug)
215
- console.log("SERIALIZE", this.model.name, this.object.type);
216
- // do we have a track for this?
253
+
217
254
  const object = this.object;
255
+ // do we have animation data for this node? if not, return
218
256
  const arr = this.dict.get(object);
219
257
  if (!arr) return;
258
+
259
+ if (debugSerialization) console.log("SERIALIZE", this.model.name, this.object.type, arr);
220
260
 
221
- // console.log("found data for", object, "exporting animation now");
222
-
223
-
224
-
225
- // assumption: all tracks have the same time values
226
- // TODO collect all time values and then use the interpolator to access
227
-
228
261
  const composedTransform = new Matrix4();
229
262
  const translation = new Vector3();
230
263
  const rotation = new Quaternion();
@@ -233,6 +266,10 @@
233
266
  writer.appendLine("matrix4d xformOp:transform.timeSamples = {");
234
267
  writer.indent++;
235
268
 
269
+ // TransformData is a collection of clips (position, rotation and scale) for a particular node
270
+ // We need to make sure that the same underlying animation clip ends up
271
+ // at the same start time in the USD file, and that we're not getting overlaps to other clips.
272
+ // That means that the same clip (transformData.clip) should end up at the same start time for all nodes.
236
273
  for (const transformData of arr) {
237
274
  const posTimesArray = transformData.pos?.times;
238
275
  const rotTimesArray = transformData.rot?.times;
@@ -243,25 +280,31 @@
243
280
  if (posTimesArray) for (const t of posTimesArray) timesArray.push(t);
244
281
  if (rotTimesArray) for (const t of rotTimesArray) timesArray.push(t);
245
282
  if (scaleTimesArray) for (const t of scaleTimesArray) timesArray.push(t);
246
- // sort
283
+
284
+ // we also need to make sure we have start and end times for these tracks
285
+ // TODO seems we can't get track duration from the KeyframeTracks
286
+ if (!timesArray.includes(0)) timesArray.push(0);
287
+
288
+ // sort times so it's increasing
247
289
  timesArray.sort((a, b) => a - b);
290
+ // make sure time values are unique
248
291
  timesArray = [...new Set(timesArray)];
249
292
 
250
293
  if (!timesArray || timesArray.length === 0) {
251
- console.error("got an animated object but no time values??", object, transformData);
294
+ console.error("got an animated object but no time values?", object, transformData);
252
295
  continue;
253
296
  }
254
- const startTime = transformData.getStartTime(arr);
297
+ const startTime = transformData.getStartTime(this.dict);
255
298
 
256
- if (debug)
257
- writer.appendLine(transformData.clip.name + ": start=" + startTime.toFixed(3) + ", length=" + transformData.getDuration().toFixed(3) + ", frames=" + transformData.getFrames());
299
+ if (debug) {
300
+ const clipName = transformData.clip.name;
301
+ const duration = transformData.getDuration();
302
+ console.log("Write .timeSamples:", clipName, startTime, duration, arr);
303
+ writer.appendLine("# " + clipName + ": start=" + (startTime * transformData.frameRate).toFixed(3) + ", length=" + (duration * transformData.frameRate).toFixed(3) + ", frames=" + transformData.getFrames());
304
+ }
258
305
 
259
- // ignore until https://github.com/three-types/three-ts-types/pull/293 gets merged
260
- //@ts-ignore
261
306
  const positionInterpolant: Interpolant | undefined = transformData.pos?.createInterpolant();
262
- //@ts-ignore
263
307
  const rotationInterpolant: Interpolant | undefined = transformData.rot?.createInterpolant();
264
- //@ts-ignore
265
308
  const scaleInterpolant: Interpolant | undefined = transformData.scale?.createInterpolant();
266
309
 
267
310
  if (!positionInterpolant) translation.set(object.position.x, object.position.y, object.position.z);
@@ -286,8 +329,9 @@
286
329
 
287
330
  composedTransform.compose(translation, rotation, scale);
288
331
 
289
- let line = `${(startTime + time) * transformData.frameRate}: ${buildMatrix(composedTransform)},`;
290
- if (debug) line = "#" + index + "\t" + line;
332
+ const line = `${(startTime + time) * transformData.frameRate}: ${buildMatrix(composedTransform)},`;
333
+ if (debug)
334
+ writer.appendLine("# " + index);
291
335
  writer.appendLine(line);
292
336
  }
293
337
 
src/engine-components/AnimationUtils.ts CHANGED
@@ -74,6 +74,10 @@
74
74
  if (!animation.tracks || animation.tracks.length <= 0) continue;
75
75
  for (const t in animation.tracks) {
76
76
  const track = animation.tracks[t];
77
+ // TODO use PropertyBinding API directly, needs testing
78
+ // const parsedPath = PropertyBinding.parseTrackName(track.name);
79
+ // const obj = PropertyBinding.findNode(gltf.scene, parsedPath.nodeName);
80
+
77
81
  const objectName = track["__objectName"] ?? track.name.substring(0, track.name.indexOf("."));
78
82
  let obj = gltf.scene.getObjectByName(objectName);
79
83
  if (!obj) {
src/engine-components/export/usdz/utils/animationutils.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Animator } from "../../../Animator.js";
2
- import { Object3D, Color, AnimationClip, KeyframeTrack } from "three";
2
+ import { Animation } from "../../../Animation.js";
3
+ import { Object3D, AnimationClip, KeyframeTrack, PropertyBinding } from "three";
3
4
  import { AnimationExtension } from "../extensions/Animation.js";
4
5
  import { GameObject } from "../../../Component.js";
5
6
  import { getParam } from "../../../../engine/engine_utils.js";
@@ -9,39 +10,95 @@
9
10
  export function registerAnimatorsImplictly(root: Object3D, ext: AnimationExtension) {
10
11
 
11
12
  // collect animators and their clips
12
- const animationClips: { root: Object3D, clips: THREE.AnimationClip[] }[] = [];
13
+ const animationClips: { root: Object3D, clips: AnimationClip[] }[] = [];
13
14
  const animators = GameObject.getComponentsInChildren(root, Animator);
15
+ const animationComponents = GameObject.getComponentsInChildren(root, Animation);
14
16
 
15
17
  // insert rest pose clip
16
18
  let injectedRestPose = false;
19
+ const restPoseClip = new AnimationClip("rest", .01, []);
20
+ const nodesWithRestPoseTracks: Map<Object3D, string[]> = new Map();
17
21
 
18
- if (debug)
19
- console.log(animators);
22
+ const checkInjectRestPose = (clips: AnimationClip[], clip: AnimationClip) => {
23
+ // we need to inject a rest pose clip so that the animation position is correct
24
+ // e.g. when the animation starts in the air and animates down we dont want the object to move under the ground
25
+ // TODO rest pose needs to contain ALL animated objects of all clips
26
+ if (!injectedRestPose && clip.tracks.length > 0) {
27
+ injectedRestPose = true;
28
+ /*
29
+ const track = clip.tracks[0];
30
+ const trackBaseName = track.name.substring(0, track.name.lastIndexOf("."));
31
+ const currentPositionTrack = new KeyframeTrack(trackBaseName + ".position", [0, .01], [0, 0, 0, 0, 0, 0]);
32
+ const currentRotationTrack = new KeyframeTrack(trackBaseName + ".quaternion", [0, .01], [0, 0, 0, 1, 0, 0, 0, 1]);
33
+ restPoseClip.tracks = [currentPositionTrack, currentRotationTrack];
34
+ */
35
+ clips.push(restPoseClip);
36
+ }
20
37
 
38
+ if (clip.tracks.length > 0) {
39
+ for (const track of clip.tracks) {
40
+ const parsedPath = PropertyBinding.parseTrackName(track.name);
41
+ const targetNode = PropertyBinding.findNode(root, parsedPath.nodeName);
42
+ if (targetNode) {
43
+ // keep track of which nodes already have a rest pose track
44
+ if (!nodesWithRestPoseTracks.has(targetNode))
45
+ nodesWithRestPoseTracks.set(targetNode, []);
46
+ const propertyNames = nodesWithRestPoseTracks.get(targetNode)!;
47
+ // make a new KeyframeTrack with the same track name, but the current values of the object
48
+ switch (parsedPath.propertyName) {
49
+ case "position": {
50
+ if (propertyNames.includes("position")) continue;
51
+ const currentData = targetNode.position;
52
+ const newTrack = new KeyframeTrack(track.name, [0, .01], [
53
+ currentData.x, currentData.y, currentData.z,
54
+ currentData.x, currentData.y, currentData.z
55
+ ]);
56
+ propertyNames.push("position");
57
+ restPoseClip.tracks.push(newTrack);
58
+ break;
59
+ }
60
+ case "quaternion": {
61
+ if (propertyNames.includes("quaternion")) continue;
62
+ const currentData = targetNode.quaternion;
63
+ const newTrack = new KeyframeTrack(track.name, [0, .01], [
64
+ currentData.x, currentData.y, currentData.z, currentData.w,
65
+ currentData.x, currentData.y, currentData.z, currentData.w
66
+ ]);
67
+ propertyNames.push("quaternion");
68
+ restPoseClip.tracks.push(newTrack);
69
+ break;
70
+ }
71
+ case "scale": {
72
+ if (propertyNames.includes("scale")) continue;
73
+ const currentData = targetNode.scale;
74
+ const newTrack = new KeyframeTrack(track.name, [0, .01], [
75
+ currentData.x, currentData.y, currentData.z,
76
+ currentData.x, currentData.y, currentData.z
77
+ ]);
78
+ propertyNames.push("scale");
79
+ restPoseClip.tracks.push(newTrack);
80
+ break;
81
+ }
82
+ }
83
+ }
84
+ }
85
+ }
86
+ }
87
+
21
88
  for (const animator of animators) {
22
89
  if (!animator || !animator.runtimeAnimatorController) continue;
23
90
 
24
91
  if (debug)
25
92
  console.log(animator);
26
93
 
27
- const clips: THREE.AnimationClip[] = [];
94
+ const clips: AnimationClip[] = [];
28
95
 
29
-
30
96
  for (const action of animator.runtimeAnimatorController.enumerateActions()) {
31
97
  if (debug)
32
98
  console.log(action);
33
99
  const clip = action.getClip();
34
100
 
35
- // we need to inject a rest pose clip so that the animation position is correct
36
- // e.g. when the animation starts in the air and animates down we dont want the object to move under the ground
37
- if (!injectedRestPose && clip.tracks.length > 0) {
38
- injectedRestPose = true;
39
- const track = clip.tracks[0];
40
- const trackBaseName = track.name.substring(0, track.name.lastIndexOf("."));
41
- const currentPositionTrack = new KeyframeTrack(trackBaseName + ".position", [0, .01], [0, 0, 0, 0, 0, 0]);
42
- const currentRotationTrack = new KeyframeTrack(trackBaseName + ".quaternion", [0, .01], [0, 0, 0, 1, 0, 0, 0, 1]);
43
- clips.push(new AnimationClip("rest", .01, [currentPositionTrack, currentRotationTrack]));
44
- }
101
+ checkInjectRestPose(clips, clip);
45
102
 
46
103
  if (!clips.includes(clip))
47
104
  clips.push(clip);
@@ -50,9 +107,27 @@
50
107
  animationClips.push({ root: animator.gameObject, clips: clips });
51
108
  }
52
109
 
53
- if (debug)
54
- console.log(animationClips);
110
+ for (const animationComponent of animationComponents) {
111
+ if (debug)
112
+ console.log(animationComponent);
55
113
 
114
+ const clips: AnimationClip[] = [];
115
+
116
+ for (const clip of animationComponent.animations) {
117
+ checkInjectRestPose(clips, clip);
118
+
119
+ if (!clips.includes(clip))
120
+ clips.push(clip);
121
+ }
122
+
123
+ animationClips.push({ root: animationComponent.gameObject, clips: clips });
124
+ }
125
+
126
+ if (debug) {
127
+ console.log("Rest Pose Clip", restPoseClip);
128
+ console.log("USDZ Animation Clips", animationClips);
129
+ }
130
+
56
131
  for (const pair of animationClips) {
57
132
  for (const clip of pair.clips)
58
133
  ext.registerAnimation(pair.root, clip);
src/engine-components/AnimatorController.ts CHANGED
@@ -545,7 +545,7 @@
545
545
  }
546
546
 
547
547
  *enumerateActions() {
548
-
548
+ if (!this.model.layers) return;
549
549
  for (const layer of this.model.layers) {
550
550
  const sm = layer.stateMachine;
551
551
  for (let index = 0; index < sm.states.length; index++) {
src/engine-components/export/usdz/extensions/behavior/Behaviour.ts CHANGED
@@ -3,7 +3,10 @@
3
3
  import { IUSDExporterExtension } from "../../Extension.js";
4
4
  import { USDObject, USDWriter } from "../../ThreeUSDZExporter.js";
5
5
  import { BehaviorModel } from "./BehavioursBuilder.js";
6
+ import { getParam } from "../../../../../engine/engine_utils.js";
6
7
 
8
+ const debug = getParam("debugusdz");
9
+
7
10
  export interface UsdzBehaviour {
8
11
  createBehaviours?(ext: BehaviorExtension, model: USDObject, context: IContext): void;
9
12
  beforeCreateDocument?(ext: BehaviorExtension, context: IContext): void | Promise<void>;
@@ -83,13 +86,13 @@
83
86
  }
84
87
 
85
88
  async onAfterSerialize(context) {
86
- console.log("onAfterSerialize", this.behaviourComponentsCopy)
89
+ if (debug) console.log("onAfterSerialize behaviours", this.behaviourComponentsCopy)
90
+
87
91
  for (const beh of this.behaviourComponentsCopy) {
88
92
 
89
- console.log("behaviour", beh)
90
93
  if (typeof beh.afterSerialize === "function") {
91
94
 
92
- console.log("beh has afterSerialize", beh)
95
+ if (debug) console.log("behaviour has afterSerialize", beh)
93
96
 
94
97
  const isAsync = beh.afterSerialize.constructor.name === "AsyncFunction";
95
98
 
src/engine-components/export/usdz/extensions/behavior/BehaviourComponents.ts CHANGED
@@ -197,6 +197,7 @@
197
197
  async beforeCreateDocument(_ext: BehaviorExtension, _context) {
198
198
  this.targetModels = [];
199
199
  ChangeMaterialOnClick._materialTriggersPerId = {}
200
+ ChangeMaterialOnClick.variantSwitchIndex = 0;
200
201
 
201
202
  // Ensure that the progressive textures have been loaded for all variants and materials
202
203
  if (this.materialToSwitch) {
@@ -277,13 +278,14 @@
277
278
  );
278
279
  }
279
280
 
281
+ static variantSwitchIndex: number = 0;
280
282
  private createVariants() {
281
283
  if (!this.variantMaterial) return null;
282
284
 
283
285
  const variantModels: USDObject[] = [];
284
286
  for (const target of this.targetModels) {
285
287
  const variant = target.clone();
286
- variant.name += " variant_" + this.variantMaterial.name;
288
+ variant.name += " variant_" + this.variantMaterial.name + "_" + ChangeMaterialOnClick.variantSwitchIndex++;
287
289
  variant.name = variant.name.replace(/\s/g, "_");
288
290
  variant.material = this.variantMaterial;
289
291
  variant.geometry = target.geometry;
src/engine-components/export/usdz/extensions/behavior/BehavioursBuilder.ts CHANGED
@@ -2,7 +2,10 @@
2
2
  import { USDDocument, USDObject, USDWriter, makeNameSafeForUSD } from "../../ThreeUSDZExporter.js";
3
3
 
4
4
  import { BehaviorExtension } from "./Behaviour.js";
5
+ import { getParam } from "../../../../../engine/engine_utils.js";
5
6
 
7
+ const debug = getParam("debugusdz");
8
+
6
9
  // TODO: rename to usdz element
7
10
  export interface IBehaviorElement {
8
11
  id: string;
@@ -455,6 +458,7 @@
455
458
  const group = ActionBuilder.sequence(act, back);
456
459
  return group;
457
460
  }
461
+ if (debug) console.log("Start Animation Action", act);
458
462
  return act;
459
463
  }
460
464
 
src/engine/debug/debug_overlay.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  import { ContextRegistry } from "../engine_context_registry.js";
4
4
 
5
5
  const debug = getParam("debugdebug");
6
- let hide = true;
7
- if(getParam("noerrors")) hide = true;
6
+ let hide = false;
7
+ if (getParam("noerrors")) hide = true;
8
8
 
9
9
  const arContainerClassName = "ar";
10
10
  const globalErrorContainerKey = "needle_engine_global_error_container";
src/engine/engine_components.ts CHANGED
@@ -117,10 +117,10 @@
117
117
  let didWarnAboutComponentAccess: boolean = false;
118
118
 
119
119
  function onGetComponent<T>(obj: Object3D | null | undefined, componentType: Constructor<T>, arr?: T[]) {
120
- if (obj === null || obj === undefined) return;
120
+ if (obj === null || obj === undefined) return null;
121
121
  if (!obj.isObject3D) {
122
122
  console.error("Object is not object3D");
123
- return;
123
+ return null;
124
124
  }
125
125
  if (!(obj?.userData?.components)) return null;
126
126
  if (typeof componentType === "string") {
@@ -131,7 +131,7 @@
131
131
  }
132
132
  if (debug)
133
133
  console.log("FIND", componentType);
134
- if (componentType === undefined || componentType === null) return;
134
+ if (componentType === undefined || componentType === null) return null;
135
135
  for (let i = 0; i < obj.userData.components.length; i++) {
136
136
  const component = obj.userData.components[i];
137
137
  if (componentType === null || component.constructor.name === componentType["name"] || component.constructor.name === componentType) {
@@ -156,6 +156,7 @@
156
156
  }
157
157
  while (parent);
158
158
  }
159
+ if(!arr) return null;
159
160
  return arr;
160
161
  }
161
162
 
src/engine/engine_element.ts CHANGED
@@ -265,9 +265,12 @@
265
265
  const alias = this.getAttribute("alias");
266
266
  this.classList.add("loading");
267
267
 
268
+
269
+
268
270
  // Loading start events
269
271
  const allowOverridingDefaultLoading = hasCommercialLicense();
270
272
  // default loading can be overriden by calling preventDefault in the onload start event
273
+ this.ensureLoadStartIsRegistered();
271
274
  let useDefaultLoading = this.dispatchEvent(new CustomEvent("loadstart", {
272
275
  detail: {
273
276
  context: this._context,
@@ -440,12 +443,24 @@
440
443
  return false;
441
444
  }
442
445
 
446
+ private _previouslyRegisteredMap: Map<string, Function> = new Map();
447
+ private ensureLoadStartIsRegistered() {
448
+ const attributeValue = this.getAttribute("loadstart");
449
+ if (attributeValue)
450
+ this.registerEventFromAttribute("loadstart", attributeValue);
451
+ }
443
452
  private registerEventFromAttribute(eventName: string, code: string) {
453
+ const prev = this._previouslyRegisteredMap.get(eventName);
454
+ if (prev) {
455
+ this._previouslyRegisteredMap.delete(eventName);
456
+ this.removeEventListener(eventName, prev as any);
457
+ }
444
458
  if (typeof code === "string" && code.length > 0) {
445
459
  // indirect eval https://esbuild.github.io/content-types/#direct-eval
446
460
  const fn = (0, eval)(code);
447
461
  // const fn = new Function(newValue);
448
462
  if (typeof fn === "function") {
463
+ this._previouslyRegisteredMap.set(eventName, fn);
449
464
  this.addEventListener(eventName, evt => fn?.call(globalThis, this._context, evt));
450
465
  }
451
466
  }
src/engine/engine_license.ts CHANGED
@@ -138,7 +138,7 @@
138
138
  const licenseDelay = 200;
139
139
 
140
140
  async function onNonCommercialVersionDetected(ctx: IContext) {
141
- await runtimeLicenseCheckPromise;
141
+ await runtimeLicenseCheckPromise?.catch(() => { });
142
142
  if (hasCommercialLicense()) return;
143
143
  logNonCommercialUse();
144
144
  ctx.domElement.addEventListener("ready", () => {
src/engine/engine_networking_peer.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  import type Peer from "peerjs";
2
2
  import { type DataConnection } from "peerjs";
3
+ import { ConstructorConcrete } from "./engine_types.js";
3
4
 
5
+ async function importPeer(): Promise<ConstructorConcrete<Peer>> {
6
+ const pkg = await import("peerjs");
7
+ console.log(pkg);
8
+ if(pkg.default === undefined) return pkg as any;
9
+ return pkg.default;
10
+ }
11
+
4
12
  enum MessageType {
5
13
  ConnectionList = "connection-list"
6
14
  }
@@ -29,7 +37,7 @@
29
37
  }
30
38
 
31
39
  private async trySetupHost(id: string) {
32
- const { Peer } = await import("peerjs");
40
+ const Peer = await importPeer();
33
41
  const host = new Peer(id);
34
42
  host.on("error", err => {
35
43
  console.error(err);
@@ -42,7 +50,7 @@
42
50
  }
43
51
 
44
52
  private async trySetupClient(hostId: string) {
45
- const { Peer } = await import("peerjs");
53
+ const Peer = await importPeer();
46
54
  this._client = new Peer();
47
55
  this._client.on("error", err => {
48
56
  console.error("Client error", err);
@@ -92,7 +100,7 @@
92
100
  this._peer.on("close", () => {
93
101
  this.broadcast("BYE");
94
102
  });
95
- setInterval(()=>{
103
+ setInterval(() => {
96
104
  this.broadcast("HELLO");
97
105
  }, 2000);
98
106
  }
src/engine/engine_networking.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  const defaultNetworkingBackendUrlProvider = "https://urls.needle.tools/default-networking-backend/index";
2
- let serverUrl : string | undefined = "wss://needle-tiny-starter.glitch.me/socket";
2
+ let serverUrl: string | undefined = "wss://needle-tiny-starter.glitch.me/socket";
3
3
 
4
- import { type Websocket, type WebsocketBuilder } from 'websocket-ts';
4
+ import { Websocket, type WebsocketBuilder } from 'websocket-ts';
5
5
  // import { Networking } from '../engine-components/Networking.js';
6
6
  import { Context } from './engine_setup.js';
7
7
  import * as utils from "./engine_utils.js";
@@ -238,6 +238,7 @@
238
238
  this.context = context;
239
239
  }
240
240
 
241
+ /** Experimental: networking via peerjs */
241
242
  public get peer(): PeerNetworking {
242
243
  if (!this._peer) {
243
244
  this._peer = new PeerNetworking();
@@ -465,21 +466,22 @@
465
466
  didResolve = true;
466
467
  res(val);
467
468
  }
468
- if(serverUrl === undefined){
469
+ if (serverUrl === undefined) {
469
470
  console.log("Fetch default backend url: " + defaultNetworkingBackendUrlProvider);
470
471
  const failed = false;
471
472
  const defaultUrlResponse = await fetch(defaultNetworkingBackendUrlProvider);
472
473
  serverUrl = await defaultUrlResponse.text();
473
- if(failed) return;
474
+ if (failed) return;
474
475
  }
475
476
 
476
- if(serverUrl === undefined){
477
+ if (serverUrl === undefined) {
477
478
  resolve(false);
478
479
  return;
479
480
  }
480
481
 
481
482
  console.log("⊡ Connecting to networking backend on\n" + serverUrl)
482
- const { WebsocketBuilder } = await import('websocket-ts');
483
+ const pkg = await import('websocket-ts');
484
+ const WebsocketBuilder = pkg.default?.WebsocketBuilder ?? pkg.WebsocketBuilder;
483
485
  const ws = new WebsocketBuilder(serverUrl)
484
486
  .onOpen(() => {
485
487
  this._connectingToWebsocketPromise = null;
src/engine/extensions/NEEDLE_progressive.ts CHANGED
@@ -42,10 +42,10 @@
42
42
  return EXTENSION_NAME;
43
43
  }
44
44
 
45
- static assignTextureLOD(context: Context, source: SourceIdentifier | undefined, material: Material, level: number = 0) {
45
+ static assignTextureLOD(context: Context, source: SourceIdentifier | undefined, material: Material, level: number = 0) : Promise<any> {
46
46
  if (!material) return Promise.resolve(null);
47
47
 
48
- const promises: Promise<Texture | null>[] = [];
48
+ const promises: Array<Promise<Texture | null>> = [];
49
49
 
50
50
  for (const slot of Object.keys(material)) {
51
51
  const val = material[slot];
@@ -66,7 +66,7 @@
66
66
  }
67
67
  }
68
68
 
69
- return promises;
69
+ return Promise.all(promises);
70
70
  }
71
71
 
72
72
  private static assignTextureLODForSlot(context: Context, source: SourceIdentifier | undefined, material: Material, level: number, slot: string, val: any): Promise<Texture | null> {
@@ -75,9 +75,10 @@
75
75
  if (debug) console.log("-----------\n", "FIND", material.name, slot, val?.name, val?.userData, val, material);
76
76
 
77
77
  return NEEDLE_progressive.getOrLoadTexture(context, source, material, slot, val, level).then(t => {
78
+
78
79
  if (t?.isTexture === true) {
79
80
 
80
- if (debug) console.log("Assign LOD", material.name, slot, t.name, t["guid"], material, "Prev:", val, "Now:", t, "\n--------------");
81
+ if (debug) console.warn("Assign LOD", material.name, slot, t.name, t["guid"], material, "Prev:", val, "Now:", t, "\n--------------");
81
82
 
82
83
  material[slot] = t;
83
84
  t.needsUpdate = true;
@@ -191,7 +192,7 @@
191
192
  addDracoAndKTX2Loaders(loader, context);
192
193
 
193
194
 
194
- if (debug) console.log("Load " + uri, material.name, slot, ext.guid);
195
+ if (debug) console.warn("Start loading " + uri, material.name, slot, ext.guid);
195
196
  if (debug) {
196
197
  await delay(Math.random() * 1000);
197
198
  }
src/engine/extensions/NEEDLE_render_objects.ts CHANGED
@@ -109,7 +109,6 @@
109
109
  }
110
110
 
111
111
  afterRoot(_result: GLTF): Promise<void> | null {
112
- console.log("AFTER ROOOOOO")
113
112
  const extensions = this.parser.json.extensions;
114
113
  if (extensions) {
115
114
  const ext = extensions[EXTENSION_NAME];
@@ -138,10 +137,6 @@
138
137
 
139
138
  }
140
139
 
141
-
142
-
143
-
144
-
145
140
  enum StencilOp {
146
141
  Keep = 0,
147
142
  Zero = 1,
src/engine-components/export/usdz/ThreeUSDZExporter.ts CHANGED
@@ -19,7 +19,6 @@
19
19
  Camera,
20
20
  Color,
21
21
  MeshStandardMaterial,
22
- sRGBEncoding,
23
22
  MeshPhysicalMaterial,
24
23
  Object3D,
25
24
  MeshBasicMaterial,
@@ -1276,6 +1275,12 @@
1276
1275
 
1277
1276
  inputs.push( `${pad}float inputs:opacity = ${effectiveOpacity}` );
1278
1277
 
1278
+ if ( material.alphaTest > 0.0 ) {
1279
+
1280
+ inputs.push( `${pad}float inputs:opacityThreshold = ${material.alphaTest}` );
1281
+
1282
+ }
1283
+
1279
1284
  }
1280
1285
 
1281
1286
  if ( material instanceof MeshStandardMaterial ) {
src/engine-components/timeline/TimelineTracks.ts CHANGED
@@ -191,6 +191,11 @@
191
191
  const baseName = track.name.substring(0, indexOfProperty);
192
192
  const objName = baseName.substring(baseName.lastIndexOf(".") + 1);
193
193
  const targetObj = root.getObjectByName(objName);
194
+ // TODO can't animate unnamed objects which use GUID as name this way, need scene.getObjectByProperty('uuid', objectName);
195
+ // This should be right but needs testing:
196
+ // const parsedPath = PropertyBinding.parseTrackName(track.name);
197
+ // const targetObj = PropertyBinding.findNode(root, parsedPath.nodeName);
198
+
194
199
  if (targetObj) {
195
200
  if (!foundPositionTrack) {
196
201
  const trackName = baseName + ".position";
src/engine-components/export/usdz/USDZExporter.ts CHANGED
@@ -18,6 +18,7 @@
18
18
  import { TextExtension } from "./extensions/USDZText.js";
19
19
  import { USDZUIExtension } from "./extensions/USDZUI.js";
20
20
  import { Renderer } from "../../Renderer.js"
21
+ import { XRFlag, XRState, XRStateFlag } from "../../XRFlag.js";
21
22
 
22
23
  const debug = getParam("debugusdz");
23
24
 
@@ -72,8 +73,8 @@
72
73
 
73
74
  start() {
74
75
  if (debug) {
75
- console.log(this);
76
- console.log("Debug USDZ, press 't' to export")
76
+ console.log("USDZExporter", this);
77
+ console.log("Debug USDZ Mode. Press 'T' to export")
77
78
  window.addEventListener("keydown", (evt) => {
78
79
  switch (evt.key) {
79
80
  case "t":
@@ -154,7 +155,7 @@
154
155
 
155
156
  // see https://developer.apple.com/documentation/arkit/adding_an_apple_pay_button_or_a_custom_action_in_ar_quick_look
156
157
  const overlay = this.buildQuicklookOverlay();
157
- if (debug) console.log(overlay);
158
+ if (debug) console.log("QuickLook Overlay", overlay);
158
159
  const callToAction = overlay.callToAction ? encodeURIComponent(overlay.callToAction) : "";
159
160
  const checkoutTitle = overlay.checkoutTitle ? encodeURIComponent(overlay.checkoutTitle) : "";
160
161
  const checkoutSubtitle = overlay.checkoutSubtitle ? encodeURIComponent(overlay.checkoutSubtitle) : "";
@@ -194,6 +195,10 @@
194
195
  await Promise.all(progressiveLoading);
195
196
  if (debug) showBalloonMessage("Load textures: done");
196
197
 
198
+ // apply XRFlags
199
+ const currentXRState = XRState.Global.Mask;
200
+ XRState.Global.Set(XRStateFlag.AR);
201
+
197
202
  // make sure we apply the AR scale
198
203
  this.applyWebARSessionRoot();
199
204
 
@@ -246,7 +251,7 @@
246
251
 
247
252
  // see https://developer.apple.com/documentation/arkit/adding_an_apple_pay_button_or_a_custom_action_in_ar_quick_look
248
253
  const overlay = this.buildQuicklookOverlay();
249
- if (debug) console.log(overlay);
254
+ if (debug) console.log("QuickLook Overlay", overlay);
250
255
  const callToAction = overlay.callToAction ? encodeURIComponent(overlay.callToAction) : "";
251
256
  const checkoutTitle = overlay.checkoutTitle ? encodeURIComponent(overlay.checkoutTitle) : "";
252
257
  const checkoutSubtitle = overlay.checkoutSubtitle ? encodeURIComponent(overlay.checkoutSubtitle) : "";
@@ -264,6 +269,9 @@
264
269
 
265
270
  if (debug) console.log("USDZ generation done. Downloading as " + this.link.download);
266
271
 
272
+ // restore XR flags
273
+ XRState.Global.Set(currentXRState);
274
+
267
275
  // TODO detect QuickLook availability:
268
276
  // https://webkit.org/blog/8421/viewing-augmented-reality-assets-in-safari-for-ios/#:~:text=inside%20the%20anchor.-,Feature%20Detection,-To%20detect%20support
269
277
  }
@@ -388,7 +396,6 @@
388
396
  }
389
397
 
390
398
  private applyWebARSessionRoot() {
391
- if(debug) console.log("applyWebARSessionRoot")
392
399
  if (!this.objectToExport) return;
393
400
 
394
401
  // first check if the sessionroot is in the parent hierarchy
@@ -399,6 +406,8 @@
399
406
  // that#s the case when no objectToExport is explictly assigned and the whole scene is being exported
400
407
  if(!sessionRoot) sessionRoot = GameObject.getComponentInChildren(this.objectToExport, WebARSessionRoot);
401
408
 
409
+ if(debug) console.log("applyWebARSessionRoot", sessionRoot);
410
+
402
411
  if (!sessionRoot) {
403
412
  if(debug) console.warn("No WebARSessionRoot found in parent hierarchy", this.objectToExport);
404
413
  return;
@@ -407,7 +416,7 @@
407
416
  // either apply the scale to the object being exported or to the sessionRoot object itself
408
417
  const target = hasSessionRootInParentHierarchy ? this.objectToExport : sessionRoot.gameObject;
409
418
  const scale = 1 / sessionRoot!.arScale;
410
- if(debug) console.log("Scale", scale, target);
419
+ if (debug) console.log("AR Session Root scale", scale, target);
411
420
  target.matrix.makeScale(scale, scale, scale);
412
421
  if (sessionRoot.invertForward) {
413
422
  target.matrix.multiply(new Matrix4().makeRotationY(Math.PI));
src/engine-components/webxr/WebARSessionRoot.ts CHANGED
@@ -39,7 +39,7 @@
39
39
  }
40
40
  }
41
41
 
42
- private _arScale: number = 5;
42
+ private _arScale: number = 1;
43
43
  private _rig: Object3D | null = null;
44
44
  private _startPose: Matrix4 | null = null;
45
45
  private _placementPose: Matrix4 | null = null;
src/engine-components/XRFlag.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { Behaviour, GameObject } from "./Component.js";
2
- import * as utils from "../engine/engine_utils.js";
2
+ import { getParam } from "../engine/engine_utils.js";
3
+ import { serializable } from "../engine/engine_serialization_decorator.js";
3
4
 
4
5
 
5
- const debug = utils.getParam("debugflags");
6
+ const debug = getParam("debugflags");
6
7
 
7
8
  export enum XRStateFlag {
8
9
  Never = 0,
@@ -14,8 +15,6 @@
14
15
  All = 0xffffffff
15
16
  }
16
17
 
17
- // console.log(XRStateFlag);
18
-
19
18
  export class XRState {
20
19
 
21
20
  public static Global: XRState = new XRState();
@@ -70,6 +69,9 @@
70
69
  private static firstApply: boolean;
71
70
  private static buffer: XRState = new XRState();
72
71
 
72
+ @serializable()
73
+ public visibleIn!: number;
74
+
73
75
  awake() {
74
76
  XRFlag.registry.push(this);
75
77
  }
@@ -90,8 +92,6 @@
90
92
  XRFlag.registry.splice(i, 1);
91
93
  }
92
94
 
93
- public visibleIn!: number;
94
-
95
95
  public get isOn(): boolean { return this.gameObject.visible; }
96
96
 
97
97
  public UpdateVisible(state: XRState | XRStateFlag | null = null) {