@@ -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
|
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
119
|
-
const animationTarget = root.
|
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 (
|
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 (
|
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
|
-
|
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
|
-
|
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
|
294
|
+
console.error("got an animated object but no time values?", object, transformData);
|
252
295
|
continue;
|
253
296
|
}
|
254
|
-
const startTime = transformData.getStartTime(
|
297
|
+
const startTime = transformData.getStartTime(this.dict);
|
255
298
|
|
256
|
-
if (debug)
|
257
|
-
|
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
|
-
|
290
|
-
if (debug)
|
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
|
|
@@ -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) {
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { Animator } from "../../../Animator.js";
|
2
|
-
import {
|
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:
|
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
|
-
|
19
|
-
|
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:
|
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
|
-
|
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
|
-
|
54
|
-
|
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);
|
@@ -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++) {
|
@@ -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("
|
95
|
+
if (debug) console.log("behaviour has afterSerialize", beh)
|
93
96
|
|
94
97
|
const isAsync = beh.afterSerialize.constructor.name === "AsyncFunction";
|
95
98
|
|
@@ -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;
|
@@ -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
|
|
@@ -3,8 +3,8 @@
|
|
3
3
|
import { ContextRegistry } from "../engine_context_registry.js";
|
4
4
|
|
5
5
|
const debug = getParam("debugdebug");
|
6
|
-
let hide =
|
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";
|
@@ -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
|
|
@@ -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
|
}
|
@@ -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", () => {
|
@@ -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
|
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
|
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
|
}
|
@@ -1,7 +1,7 @@
|
|
1
1
|
const defaultNetworkingBackendUrlProvider = "https://urls.needle.tools/default-networking-backend/index";
|
2
|
-
let serverUrl
|
2
|
+
let serverUrl: string | undefined = "wss://needle-tiny-starter.glitch.me/socket";
|
3
3
|
|
4
|
-
import {
|
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
|
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;
|
@@ -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.
|
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.
|
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
|
}
|
@@ -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,
|
@@ -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 ) {
|
@@ -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";
|
@@ -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
|
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("
|
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));
|
@@ -39,7 +39,7 @@
|
|
39
39
|
}
|
40
40
|
}
|
41
41
|
|
42
|
-
private _arScale: number =
|
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;
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import { Behaviour, GameObject } from "./Component.js";
|
2
|
-
import
|
2
|
+
import { getParam } from "../engine/engine_utils.js";
|
3
|
+
import { serializable } from "../engine/engine_serialization_decorator.js";
|
3
4
|
|
4
5
|
|
5
|
-
const debug =
|
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) {
|