@@ -52,14 +52,7 @@
|
|
52
52
|
hash: filehash.digest('hex'),
|
53
53
|
size: stats.size
|
54
54
|
});
|
55
|
-
|
56
|
-
const fullpath = `${ directory } / ${ entry.name }`;
|
57
|
-
const stats = fs.statSync(fullpath);
|
58
|
-
info.totalsize += stats.size;
|
59
|
-
}
|
60
|
-
catch {
|
61
|
-
//ignore
|
62
|
-
}
|
55
|
+
info.totalsize += stats.size;
|
63
56
|
}
|
64
57
|
}
|
65
58
|
}
|
@@ -24,13 +24,13 @@
|
|
24
24
|
}
|
25
25
|
return this._start;
|
26
26
|
}
|
27
|
-
get duration(): number { return this.clip.
|
27
|
+
get duration(): number { return this.clip?.duration ?? TransformData.restPoseClipDuration; }
|
28
28
|
|
29
29
|
private ext: AnimationExtension;
|
30
30
|
private root: Object3D;
|
31
|
-
private clip: AnimationClip;
|
31
|
+
private clip: AnimationClip | null;
|
32
32
|
|
33
|
-
constructor(ext: AnimationExtension, root: Object3D, clip: AnimationClip) {
|
33
|
+
constructor(ext: AnimationExtension, root: Object3D, clip: AnimationClip | null) {
|
34
34
|
this.ext = ext;
|
35
35
|
this.root = root;
|
36
36
|
this.clip = clip;
|
@@ -38,7 +38,7 @@
|
|
38
38
|
}
|
39
39
|
|
40
40
|
export class TransformData {
|
41
|
-
clip
|
41
|
+
clip: AnimationClip | null;
|
42
42
|
pos?: KeyframeTrack;
|
43
43
|
rot?: KeyframeTrack;
|
44
44
|
scale?: KeyframeTrack;
|
@@ -55,7 +55,7 @@
|
|
55
55
|
static animationDurationPadding = 1 / 60;
|
56
56
|
static restPoseClipDuration = 1 / 60;
|
57
57
|
|
58
|
-
constructor(root: Object3D | null, target: Object3D, clip: AnimationClip |
|
58
|
+
constructor(root: Object3D | null, target: Object3D, clip: AnimationClip | null) {
|
59
59
|
this.root = root;
|
60
60
|
this.target = target;
|
61
61
|
this.clip = clip;
|
@@ -177,10 +177,18 @@
|
|
177
177
|
|
178
178
|
get extensionName(): string { return "animation" }
|
179
179
|
|
180
|
+
/** For each animated object, contains time/pos/rot/scale samples in the format that USD needs,
|
181
|
+
* ready to be written to the .usda file.
|
182
|
+
*/
|
180
183
|
private dict: AnimationDict = new Map();
|
181
|
-
|
184
|
+
/** Map of all roots (Animation/Animator or scene) and all targets that they animate.
|
185
|
+
* We need that info so that we can ensure that each target has the same number of TransformData entries
|
186
|
+
* so that switching between animations doesn't result in data "leaking" to another clip.
|
187
|
+
*/
|
188
|
+
private rootTargetMap = new Map<Object3D, Array<Object3D>>();
|
182
189
|
private rootAndClipToRegisteredAnimationMap = new Map<string, RegisteredAnimationInfo>();
|
183
|
-
|
190
|
+
/** Clips registered for each root */
|
191
|
+
private rootToRegisteredClip = new Map<Object3D, Array<AnimationClip>>();
|
184
192
|
|
185
193
|
private serializers: SerializeAnimation[] = [];
|
186
194
|
|
@@ -214,14 +222,16 @@
|
|
214
222
|
|
215
223
|
getClipCount(root: Object3D): number {
|
216
224
|
// don't count the rest pose
|
217
|
-
let currentCount = this.
|
225
|
+
let currentCount = this.rootToRegisteredClip.get(root)?.length ?? 0;
|
218
226
|
if (this.injectRestPoses) currentCount = currentCount ? currentCount - 1 : 0;
|
219
227
|
return currentCount ?? 0;
|
220
228
|
}
|
221
229
|
|
222
230
|
// TODO why do we have this here and on TransformData? Can RegisteredAnimationInfo not cache this value?
|
223
231
|
// TODO we probably want to assert here that this is the same value on all nodes
|
224
|
-
getStartTime01(root: Object3D, clip: AnimationClip) {
|
232
|
+
getStartTime01(root: Object3D, clip: AnimationClip | null) {
|
233
|
+
// This is a rest pose clip, it always starts at 0
|
234
|
+
if (!clip) return 0;
|
225
235
|
|
226
236
|
const targets = this.rootTargetMap.get(root);
|
227
237
|
if (!targets) return 0;
|
@@ -241,14 +251,38 @@
|
|
241
251
|
}
|
242
252
|
|
243
253
|
// The same clip could be registered for different roots. All of them need written animation data then.
|
244
|
-
// The same root could have multiple clips registered to it.
|
245
|
-
|
246
|
-
|
254
|
+
// The same root could have multiple clips registered to it. If it does, the clips need to write
|
255
|
+
// independent time data, so that playing back an animation on that root doesn't result in data "leaking"/"overlapping".
|
256
|
+
// The structure we need is:
|
257
|
+
// - MyRoot
|
258
|
+
// Animator
|
259
|
+
// - Clip1: CubeScale (only animates MyCube), duration: 3s
|
260
|
+
// - Clip2: SphereRotation (only animates MySphere), duration: 2s
|
261
|
+
// - MyCube
|
262
|
+
// - MySphere
|
263
|
+
// Results in:
|
264
|
+
// - MyRoot
|
265
|
+
// - MyCube
|
266
|
+
// - # rest clip (0..0.1)
|
267
|
+
// - # CubeScale (0.2..3.2)
|
268
|
+
// - # rest clip for SphereRotation (3.3..5.3)
|
269
|
+
// - MySphere
|
270
|
+
// - # rest clip (0..0.1)
|
271
|
+
// - # rest clip for CubeScale (0.2..3.2)
|
272
|
+
// - # SphereRotation (3.3..5.3)
|
273
|
+
|
274
|
+
/** Register an AnimationClip for a specific root object.
|
275
|
+
* @param root The root object that the animation clip is targeting.
|
276
|
+
* @param clip The animation clip to register. If null, a rest pose is registered.
|
277
|
+
* @returns The registered animation info, which contains the start time and duration of the clip.
|
278
|
+
*/
|
279
|
+
registerAnimation(root: Object3D, clip: AnimationClip | null): RegisteredAnimationInfo | null {
|
280
|
+
if(!root) return null;
|
247
281
|
if (!this.rootTargetMap.has(root)) this.rootTargetMap.set(root, []);
|
248
282
|
|
249
283
|
// if we registered that exact pair already, just return the info
|
250
284
|
// no proper tuples in JavaScript, but we can use the uuids for a unique key here
|
251
|
-
const hash = root.uuid + clip
|
285
|
+
const hash = root.uuid + (clip?.uuid ?? "-rest");
|
252
286
|
if (this.rootAndClipToRegisteredAnimationMap.has(hash)) {
|
253
287
|
return this.rootAndClipToRegisteredAnimationMap.get(hash)!;
|
254
288
|
}
|
@@ -261,11 +295,11 @@
|
|
261
295
|
// in animation 0 and some data overrides each other incorrectly.
|
262
296
|
// When we don't inject a rest clip, we start at 0.
|
263
297
|
const startIndex = this.injectRestPoses ? 1 : 0;
|
264
|
-
const currentCount = this.
|
298
|
+
const currentCount = (this.rootToRegisteredClip.get(root)?.length ?? 0) + startIndex;
|
265
299
|
|
266
300
|
const targets = this.rootTargetMap.get(root);
|
267
301
|
const unregisteredNodesForThisClip = new Set(targets);
|
268
|
-
if (clip.tracks) {
|
302
|
+
if (clip && clip.tracks) {
|
269
303
|
for (const track of clip.tracks) {
|
270
304
|
const parsedPath = PropertyBinding.parseTrackName(track.name);
|
271
305
|
const animationTarget = PropertyBinding.findNode(root, parsedPath.nodeName);
|
@@ -298,19 +332,13 @@
|
|
298
332
|
// TODO most likely doesn't work for overlapping clips (clips where a root is a child of another root)
|
299
333
|
// TODO in that case we may need to pad globally, not per root
|
300
334
|
|
301
|
-
//
|
335
|
+
// Inject a rest pose if we don't have it already
|
302
336
|
if (this.injectRestPoses && !transformDataForTarget[0]) {
|
303
|
-
|
304
|
-
transformDataForTarget[0] = model;
|
337
|
+
transformDataForTarget[0] = new TransformData(null, animationTarget, null);
|
305
338
|
}
|
306
339
|
// These all need to be at the same index, otherwise our padding went wrong
|
307
340
|
let model = transformDataForTarget[currentCount];
|
308
341
|
|
309
|
-
// validation (for debugging)
|
310
|
-
// let modelFoundByClip = transformDataForTarget.find(x => x.clip === clip);
|
311
|
-
// let modelFoundByClipIndex = modelFoundByClip ? transformDataForTarget.indexOf(modelFoundByClip) : -1;
|
312
|
-
// console.assert(model === modelFoundByClip, "We should find the same model by index and by clip; " + currentCount + " !== " + modelFoundByClipIndex);
|
313
|
-
|
314
342
|
if (!model) {
|
315
343
|
model = new TransformData(root, animationTarget, clip);
|
316
344
|
transformDataForTarget[currentCount] = model;
|
@@ -322,14 +350,17 @@
|
|
322
350
|
}
|
323
351
|
}
|
324
352
|
|
353
|
+
if (debug) console.log("Unregistered nodes for this clip", unregisteredNodesForThisClip, "clip", clip, "at slot", currentCount, "for root", root, "targets", targets)
|
354
|
+
|
325
355
|
// add padding for nodes that are not animated by this clip
|
326
356
|
for (const target of unregisteredNodesForThisClip) {
|
327
357
|
const transformDataForTarget = this.dict.get(target);
|
328
358
|
if (!transformDataForTarget) continue;
|
329
359
|
|
330
|
-
//
|
360
|
+
// Inject rest pose if these nodes don't have it yet for some reason – this is likely a bug
|
331
361
|
if (this.injectRestPoses && !transformDataForTarget[0]) {
|
332
|
-
|
362
|
+
console.warn("Adding rest pose for ", target, clip, "at slot", currentCount, "This is likely a bug, should have been added earlier.");
|
363
|
+
const model = new TransformData(null, target, null);
|
333
364
|
transformDataForTarget[0] = model;
|
334
365
|
}
|
335
366
|
|
@@ -345,7 +376,13 @@
|
|
345
376
|
// This doesnt work if we have clips animating multiple objects
|
346
377
|
const info = new RegisteredAnimationInfo(this, root, clip);
|
347
378
|
this.rootAndClipToRegisteredAnimationMap.set(hash, info);
|
348
|
-
|
379
|
+
|
380
|
+
if (debug) console.log({root, clip, info});
|
381
|
+
if (clip) {
|
382
|
+
const registered = this.rootToRegisteredClip.get(root);
|
383
|
+
if (!registered) this.rootToRegisteredClip.set(root, [clip]);
|
384
|
+
else registered.push(clip);
|
385
|
+
}
|
349
386
|
return info;
|
350
387
|
}
|
351
388
|
|
@@ -354,6 +391,49 @@
|
|
354
391
|
}
|
355
392
|
|
356
393
|
onAfterBuildDocument(_context: any) {
|
394
|
+
|
395
|
+
// Validation: go through all roots, check if their data and lengths are consistent.
|
396
|
+
// There are some cases that we can patch up here, especially if non-overlapping animations have resulted in "holes"
|
397
|
+
// in TransformData where only now we know what the matching animation clip actually is.
|
398
|
+
if (debug) console.log("Animation data", { dict: this.dict, rootTargetMap: this.rootTargetMap, rootToRegisteredClip: this.rootToRegisteredClip});
|
399
|
+
|
400
|
+
for (const root of this.rootTargetMap.keys()) {
|
401
|
+
const targets = this.rootTargetMap.get(root);
|
402
|
+
|
403
|
+
if (!targets) continue;
|
404
|
+
|
405
|
+
// The TransformData[] arrays here should have the same length, and the same durations
|
406
|
+
let arrayLength: number | undefined = undefined;
|
407
|
+
const durations: Array<number | undefined> = [];
|
408
|
+
for (const target of targets) {
|
409
|
+
const datas = this.dict.get(target);
|
410
|
+
if (!datas) {
|
411
|
+
console.error("No data found for target on USDZ export – please report a bug!", target);
|
412
|
+
continue;
|
413
|
+
}
|
414
|
+
|
415
|
+
if (arrayLength === undefined) arrayLength = datas?.length;
|
416
|
+
if (arrayLength !== datas?.length) console.error("Different array lengths for targets – please report a bug!", datas);
|
417
|
+
|
418
|
+
for (let i = 0; i < datas.length; i++) {
|
419
|
+
let data = datas[i];
|
420
|
+
if (!data) {
|
421
|
+
// If we don't have TransformData for this object yet, we're emitting a rest pose with the duration
|
422
|
+
// of the matching clip that was registered for this root.
|
423
|
+
const index = i - (this.injectRestPoses ? 1 : 0);
|
424
|
+
datas[i] = new TransformData(null, target, this.rootToRegisteredClip.get(root)![index]);
|
425
|
+
data = datas[i];
|
426
|
+
}
|
427
|
+
|
428
|
+
if (!durations[i]) durations[i] = data.getDuration();
|
429
|
+
else if (durations[i] !== data.getDuration()) {
|
430
|
+
console.error("Different durations for targets – please report a bug!", datas);
|
431
|
+
continue;
|
432
|
+
}
|
433
|
+
}
|
434
|
+
}
|
435
|
+
}
|
436
|
+
|
357
437
|
for (const ser of this.serializers) {
|
358
438
|
const parent = ser.model?.parent;
|
359
439
|
const isEmptyParent = parent?.isDynamic === true;
|
@@ -567,7 +647,7 @@
|
|
567
647
|
// if we don't have animation data, create an empty one –
|
568
648
|
// it will automatically map to the rest pose, albeit inefficiently
|
569
649
|
else
|
570
|
-
emptyTransformData = new TransformData(null, bone,
|
650
|
+
emptyTransformData = new TransformData(null, bone, null);
|
571
651
|
|
572
652
|
for (let i = 0; i < count; i++) {
|
573
653
|
const transformData = boneEntryInData ? boneEntryInData[i] : emptyTransformData!;
|
@@ -751,7 +831,7 @@
|
|
751
831
|
if (arr0) {
|
752
832
|
for (let i = 0; i < arr0.length; i++) {
|
753
833
|
if (arr0[i] !== undefined) continue;
|
754
|
-
arr0[i] = new TransformData(null, this.object,
|
834
|
+
arr0[i] = new TransformData(null, this.object, null);
|
755
835
|
}
|
756
836
|
}
|
757
837
|
|
@@ -802,6 +882,11 @@
|
|
802
882
|
currentStartTime += arr[i].getDuration() + TransformData.animationDurationPadding;
|
803
883
|
}
|
804
884
|
|
885
|
+
const formatter = Intl.NumberFormat("en", {
|
886
|
+
maximumFractionDigits: 2,
|
887
|
+
minimumFractionDigits: 0,
|
888
|
+
});
|
889
|
+
|
805
890
|
for (let i = 0; i < arr.length; i++) {
|
806
891
|
const transformData = arr[i];
|
807
892
|
if (!transformData) continue;
|
@@ -814,17 +899,19 @@
|
|
814
899
|
continue;
|
815
900
|
}
|
816
901
|
|
817
|
-
if (debug)
|
902
|
+
// if (debug) // writing out the clip name and duration is useful even when not debugging
|
903
|
+
{
|
818
904
|
const clipName = transformData.clip?.name ?? "rest";
|
819
905
|
const duration = transformData.getDuration();
|
820
|
-
console.log("Write .timeSamples:", clipName, startTime, duration, arr);
|
821
|
-
writer.appendLine("# " + clipName + ": start=" + (startTime * transformData.frameRate)
|
906
|
+
if (debug) console.log("Write .timeSamples:", clipName, startTime, duration, arr);
|
907
|
+
writer.appendLine("# " + clipName + ": start=" + formatter.format(startTime * transformData.frameRate) + ", length=" + formatter.format(duration * transformData.frameRate) + ", frames=" + transformData.getFrames());
|
822
908
|
}
|
823
909
|
|
824
910
|
for (const { time, translation, rotation, scale } of transformData.getValues(timesArray)) {
|
825
911
|
composedTransform.compose(translation, rotation, scale);
|
826
912
|
|
827
|
-
const
|
913
|
+
const timeStr = formatter.format((startTime + time) * transformData.frameRate);
|
914
|
+
const line = `${timeStr}: ${buildMatrix(composedTransform)},`;
|
828
915
|
writer.appendLine(line);
|
829
916
|
}
|
830
917
|
}
|
@@ -26,6 +26,7 @@
|
|
26
26
|
const animationClips: { root: Object3D, clips: AnimationClip[] }[] = [];
|
27
27
|
const animators = GameObject.getComponentsInChildren(root, Animator);
|
28
28
|
const animationComponents = GameObject.getComponentsInChildren(root, Animation);
|
29
|
+
const animatorsWithPlayAtStart = new Array<Animator | Animation>();
|
29
30
|
|
30
31
|
const constructedObjects = new Array<Object3D>();
|
31
32
|
|
@@ -39,6 +40,13 @@
|
|
39
40
|
if (!activeState) continue;
|
40
41
|
if (!activeState.motion || !activeState.motion.clip) continue;
|
41
42
|
if (activeState.motion.clip.tracks?.length < 1) continue;
|
43
|
+
// We could skip if the current state has already ended playing
|
44
|
+
// but there's an edge case where we'd do this exactly in a transition,
|
45
|
+
// and also it's a bit weird when someone has watched the animation in the browser already
|
46
|
+
// and then it doesn't play in AR, so this is disabled for now.
|
47
|
+
// if (animator.getCurrentStateInfo()?.normalizedTime == 1) continue;
|
48
|
+
|
49
|
+
if (animatorsWithPlayAtStart.includes(animator)) continue;
|
42
50
|
|
43
51
|
// Create a PlayAnimationOnClick component that will play the animation on start
|
44
52
|
// This is a bit hacky right now (we're using the internal "start" trigger)
|
@@ -50,6 +58,8 @@
|
|
50
58
|
const go = new Object3D();
|
51
59
|
GameObject.addComponent(go, newComponent);
|
52
60
|
constructedObjects.push(go);
|
61
|
+
|
62
|
+
animatorsWithPlayAtStart.push(animator);
|
53
63
|
|
54
64
|
// the behaviour can be anywhere in the hierarchy
|
55
65
|
root.add(go);
|
@@ -85,6 +95,7 @@
|
|
85
95
|
for (const animationComponent of animationComponents) {
|
86
96
|
if (!animationComponent || !animationComponent.clip ||!animationComponent.enabled) continue;
|
87
97
|
if (!animationComponent.playAutomatically) continue;
|
98
|
+
if (animatorsWithPlayAtStart.includes(animationComponent)) continue;
|
88
99
|
|
89
100
|
// Create a PlayAnimationOnClick component that will play the animation on start
|
90
101
|
// This is a bit hacky right now (we're using the internal "start" trigger)
|
@@ -97,6 +108,8 @@
|
|
97
108
|
GameObject.addComponent(go, newComponent);
|
98
109
|
constructedObjects.push(go);
|
99
110
|
|
111
|
+
animatorsWithPlayAtStart.push(animationComponent);
|
112
|
+
|
100
113
|
// the behaviour can be anywhere in the hierarchy
|
101
114
|
root.add(go);
|
102
115
|
}
|
@@ -130,6 +143,8 @@
|
|
130
143
|
export function registerAudioSourcesImplictly(root: Object3D, _ext: AudioExtension): Array<Object3D> {
|
131
144
|
const audioSources = GameObject.getComponentsInChildren(root, AudioSource);
|
132
145
|
const playAudioOnClicks = GameObject.getComponentsInChildren(root, PlayAudioOnClick);
|
146
|
+
const audioWithPlayAtStart = new Array<AudioSource>();
|
147
|
+
|
133
148
|
|
134
149
|
const constructedObjects = new Array<Object3D>();
|
135
150
|
|
@@ -144,6 +159,7 @@
|
|
144
159
|
for (const audioSource of audioSources) {
|
145
160
|
if (!audioSource || !audioSource.clip) continue;
|
146
161
|
if (audioSource.volume <= 0) continue;
|
162
|
+
if (audioWithPlayAtStart.includes(audioSource)) continue;
|
147
163
|
|
148
164
|
const newComponent = new PlayAudioOnClick();
|
149
165
|
newComponent.target = audioSource;
|
@@ -153,6 +169,8 @@
|
|
153
169
|
console.log("implicit PlayAudioOnStart", go, newComponent);
|
154
170
|
constructedObjects.push(go);
|
155
171
|
|
172
|
+
audioWithPlayAtStart.push(audioSource);
|
173
|
+
|
156
174
|
root.add(go);
|
157
175
|
}
|
158
176
|
|
@@ -12,7 +12,7 @@
|
|
12
12
|
import { Behaviour, GameObject } from "../../../../Component.js";
|
13
13
|
import type { IPointerClickHandler, PointerEventData } from "../../../../ui/PointerEvents.js";
|
14
14
|
import { ObjectRaycaster,Raycaster } from "../../../../ui/Raycaster.js";
|
15
|
-
import { USDDocument, USDObject, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
15
|
+
import { makeNameSafeForUSD,USDDocument, USDObject, USDZExporterContext } from "../../ThreeUSDZExporter.js";
|
16
16
|
import { AnimationExtension, RegisteredAnimationInfo, type UsdzAnimation } from "../Animation.js";
|
17
17
|
import { AudioExtension } from "./AudioExtension.js";
|
18
18
|
import type { BehaviorExtension, UsdzBehaviour } from "./Behaviour.js";
|
@@ -297,6 +297,10 @@
|
|
297
297
|
);
|
298
298
|
}
|
299
299
|
|
300
|
+
private static getMaterialName(material: Material) {
|
301
|
+
return makeNameSafeForUSD(material.name || 'Material') + "_" + material.id;
|
302
|
+
}
|
303
|
+
|
300
304
|
static variantSwitchIndex: number = 0;
|
301
305
|
private createVariants() {
|
302
306
|
if (!this.variantMaterial) return null;
|
@@ -304,8 +308,8 @@
|
|
304
308
|
const variantModels: USDObject[] = [];
|
305
309
|
for (const target of this.targetModels) {
|
306
310
|
const variant = target.clone();
|
307
|
-
variant.name += "
|
308
|
-
variant.
|
311
|
+
variant.name += "_Variant_" + ChangeMaterialOnClick.variantSwitchIndex++ + "_" + ChangeMaterialOnClick.getMaterialName(this.variantMaterial);
|
312
|
+
variant.displayName = variant.displayName + ": Variant with material " + this.variantMaterial.name;
|
309
313
|
variant.material = this.variantMaterial;
|
310
314
|
variant.geometry = target.geometry;
|
311
315
|
variant.matrix = target.matrix;
|
@@ -811,14 +815,14 @@
|
|
811
815
|
if (model.uuid === this.target?.uuid) {
|
812
816
|
const sequence = ActionBuilder.sequence();
|
813
817
|
|
814
|
-
if (this.animationSequence
|
818
|
+
if (this.animationSequence && this.animationSequence.length > 0)
|
815
819
|
{
|
816
820
|
for (const anim of this.animationSequence) {
|
817
821
|
sequence.addAction(getOrCacheAction(model, anim));
|
818
822
|
}
|
819
823
|
}
|
820
824
|
|
821
|
-
if (this.animationLoopAfterSequence
|
825
|
+
if (this.animationLoopAfterSequence && this.animationLoopAfterSequence.length > 0) {
|
822
826
|
// only make a new action group if there's already stuff in the existing one
|
823
827
|
const loopSequence = sequence.actions.length == 0 ? sequence : ActionBuilder.sequence();
|
824
828
|
for (const anim of this.animationLoopAfterSequence) {
|
@@ -829,7 +833,7 @@
|
|
829
833
|
sequence.addAction(loopSequence);
|
830
834
|
}
|
831
835
|
|
832
|
-
const playAnimationOnTap = new BehaviorModel("
|
836
|
+
const playAnimationOnTap = new BehaviorModel("_tap_" + this.name + "_toPlayClip_" + this.stateName + "_on_" + this.target?.name,
|
833
837
|
this.trigger == "tap" ? TriggerBuilder.tapTrigger(this.selfModel) : TriggerBuilder.sceneStartTrigger(),
|
834
838
|
sequence
|
835
839
|
);
|
@@ -847,7 +851,7 @@
|
|
847
851
|
|
848
852
|
this.stateAnimationModel = model;
|
849
853
|
|
850
|
-
if (this.animation
|
854
|
+
if (this.animation) {
|
851
855
|
this.animationSequence = new Array<RegisteredAnimationInfo>();
|
852
856
|
this.animationLoopAfterSequence = new Array<RegisteredAnimationInfo>();
|
853
857
|
const anim = ext.registerAnimation(this.target, this.animation.clip);
|
@@ -920,6 +924,15 @@
|
|
920
924
|
}
|
921
925
|
}
|
922
926
|
|
927
|
+
// Special case: someone's trying to play an empty clip without any motion data, no loops or anything.
|
928
|
+
// In that case, we simply go to the rest clip – this is a common case for "idle" states.
|
929
|
+
if (statesUntilLoop.length === 1 && (!statesUntilLoop[0].motion?.clip || statesUntilLoop[0].motion?.clip.tracks?.length === 0)) {
|
930
|
+
this.animationSequence = new Array<RegisteredAnimationInfo>();
|
931
|
+
const anim = ext.registerAnimation(this.target, null);
|
932
|
+
if (anim) this.animationSequence.push(anim);
|
933
|
+
return;
|
934
|
+
}
|
935
|
+
|
923
936
|
// filter out any states that don't have motion data
|
924
937
|
statesUntilLoop = statesUntilLoop.filter(s => s.motion?.clip && s.motion?.clip.tracks?.length > 0);
|
925
938
|
statesLooping = statesLooping.filter(s => s.motion?.clip && s.motion?.clip.tracks?.length > 0);
|
@@ -932,11 +945,7 @@
|
|
932
945
|
|
933
946
|
const addStateToSequence = (state: State, sequence: Array<RegisteredAnimationInfo>) => {
|
934
947
|
if (!this.target) return;
|
935
|
-
|
936
|
-
console.warn("No clip found for state " + state.name + " on " + this.animator?.name + ", can't export animation data");
|
937
|
-
return;
|
938
|
-
}
|
939
|
-
const anim = ext.registerAnimation(this.target, state.motion.clip);
|
948
|
+
const anim = ext.registerAnimation(this.target, state.motion.clip ?? null);
|
940
949
|
if (anim) sequence.push(anim);
|
941
950
|
else console.warn("Couldn't register animation for state " + state.name + " on " + this.animator?.name);
|
942
951
|
};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { type Context,FrameEvent } from "./engine_context.js";
|
1
|
+
import { type Context, FrameEvent } from "./engine_context.js";
|
2
2
|
import type { ContextEvent } from "./engine_context_registry.js";
|
3
3
|
import { safeInvoke } from "./engine_generic_utils.js";
|
4
4
|
|
@@ -45,10 +45,14 @@
|
|
45
45
|
}
|
46
46
|
}
|
47
47
|
|
48
|
-
|
48
|
+
const bufferArray = new Array<LifecycleMethod>();
|
49
49
|
function invoke(ctx: Context, methods: Array<LifecycleMethod>) {
|
50
|
-
|
51
|
-
|
50
|
+
bufferArray.length = 0;
|
51
|
+
for (let i = 0; i < methods.length; i++) {
|
52
|
+
bufferArray.push(methods[i]);
|
53
|
+
}
|
54
|
+
for (let i = 0; i < bufferArray.length; i++) {
|
55
|
+
const method = bufferArray[i];
|
52
56
|
safeInvoke(method, ctx);
|
53
57
|
}
|
54
58
|
}
|
@@ -75,6 +75,10 @@
|
|
75
75
|
readonly speed: number;
|
76
76
|
/** The current action playing. It can be used to modify the action */
|
77
77
|
readonly action: AnimationAction | null;
|
78
|
+
/**
|
79
|
+
* If the state has any transitions
|
80
|
+
*/
|
81
|
+
readonly hasTransitions: boolean;
|
78
82
|
|
79
83
|
constructor(state: State, normalizedTime: number, length: number, speed: number) {
|
80
84
|
this.name = state.name;
|
@@ -83,6 +87,7 @@
|
|
83
87
|
this.length = length;
|
84
88
|
this.speed = speed;
|
85
89
|
this.action = state.motion.action || null;
|
90
|
+
this.hasTransitions = state.transitions?.length > 0 || false;
|
86
91
|
}
|
87
92
|
}
|
88
93
|
|
@@ -1152,10 +1152,11 @@
|
|
1152
1152
|
return makeNameSafe(bone.name || 'bone_' + bone.uuid);
|
1153
1153
|
}
|
1154
1154
|
|
1155
|
-
function getGeometryName(geometry: BufferGeometry,
|
1156
|
-
//
|
1157
|
-
//
|
1158
|
-
|
1155
|
+
function getGeometryName(geometry: BufferGeometry, _fallbackName: string) {
|
1156
|
+
// Using object names here breaks instancing...
|
1157
|
+
// So we removed fallbackName again. Downside: geometries don't have nice names...
|
1158
|
+
// A better workaround would be that we actually name them on glTF import (so the geometry has a name, not the object)
|
1159
|
+
return makeNameSafe(geometry.name || 'Geometry') + "_" + geometry.id;
|
1159
1160
|
}
|
1160
1161
|
|
1161
1162
|
function getMaterialName(material: Material) {
|
@@ -222,9 +222,14 @@
|
|
222
222
|
return null;
|
223
223
|
}
|
224
224
|
|
225
|
+
if (!this.objectToExport) {
|
226
|
+
console.warn("No object to export", this);
|
227
|
+
return null;
|
228
|
+
}
|
229
|
+
|
225
230
|
const blob = await this.export(this.objectToExport);
|
226
231
|
if (!blob) {
|
227
|
-
console.
|
232
|
+
console.error("USDZ generation failed. Please report a bug", this);
|
228
233
|
return null;
|
229
234
|
}
|
230
235
|
|
@@ -255,6 +260,7 @@
|
|
255
260
|
if (taskForThisObject) {
|
256
261
|
return taskForThisObject;
|
257
262
|
}
|
263
|
+
|
258
264
|
// start the export
|
259
265
|
const task = this.internalExport(objectToExport);
|
260
266
|
// store the task
|
@@ -263,8 +269,9 @@
|
|
263
269
|
return task.then((blob) => {
|
264
270
|
this._currentExportTasks.delete(objectToExport);
|
265
271
|
return blob;
|
266
|
-
}).catch((
|
272
|
+
}).catch((e) => {
|
267
273
|
this._currentExportTasks.delete(objectToExport);
|
274
|
+
console.error("Error during USDZ export – please report a bug!", e);
|
268
275
|
return null;
|
269
276
|
});
|
270
277
|
}
|