@aura3d/assets 1.3.3 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/GLTFLoader.js +1 -0
- package/dist/GLTFLoader.js.map +1 -1
- package/dist/GLTFRenderResources.d.ts.map +1 -1
- package/dist/GLTFRenderResources.js +1 -0
- package/dist/GLTFRenderResources.js.map +1 -1
- package/package.json +38 -5
- package/src/AdvancedAssetCorpus.ts +0 -454
- package/src/AssetBundleCacheFixtures.ts +0 -290
- package/src/AssetCache.ts +0 -76
- package/src/AssetCompatibility.ts +0 -224
- package/src/AssetCorpus.ts +0 -329
- package/src/AssetDependencyGraph.ts +0 -83
- package/src/AssetHandle.ts +0 -74
- package/src/AssetImportPreflight.ts +0 -179
- package/src/AssetInspection.ts +0 -706
- package/src/AssetLoader.ts +0 -36
- package/src/AssetManager.ts +0 -211
- package/src/AssetRegistry.ts +0 -44
- package/src/AssetRenderDefaults.ts +0 -36
- package/src/AudioLoader.ts +0 -41
- package/src/BlenderExportValidation.ts +0 -227
- package/src/CarConceptMaterialStability.ts +0 -1405
- package/src/EXRLoader.ts +0 -1
- package/src/ExternalParityGLTFCorpus.ts +0 -65
- package/src/GLTFAnimationRuntime.ts +0 -1416
- package/src/GLTFAutoFit.ts +0 -133
- package/src/GLTFCompressionDecoders.ts +0 -190
- package/src/GLTFExtensionSupport.ts +0 -138
- package/src/GLTFLoader.ts +0 -4059
- package/src/GLTFRenderResources.ts +0 -2269
- package/src/HDRLoader.ts +0 -1
- package/src/ImageLoader.ts +0 -52
- package/src/ImportPipeline.ts +0 -148
- package/src/KTX2BasisTextureTranscoder.ts +0 -225
- package/src/LoadContext.ts +0 -60
- package/src/MaterialLoader.ts +0 -166
- package/src/MeshOptimization.ts +0 -93
- package/src/OBJLoader.ts +0 -551
- package/src/SceneAnalysisFixtures.ts +0 -438
- package/src/SceneLoader.ts +0 -62
- package/src/ShaderLoader.ts +0 -30
- package/src/TextureLoader.ts +0 -30
- package/src/TexturePipeline.ts +0 -96
- package/src/TextureStreaming.ts +0 -20
- package/src/WorkerAssetJobs.ts +0 -56
- package/src/advanced-gallery/index.ts +0 -18
- package/src/asset-corpus/GLTFSceneLoader.ts +0 -9
- package/src/asset-corpus/ProductionAssetCorpus.ts +0 -230
- package/src/asset-corpus/ProductionAssetPipeline.ts +0 -5
- package/src/asset-corpus/ProductionGLTFRenderPipeline.ts +0 -182
- package/src/asset-corpus/ProductionHDRTextureLoader.ts +0 -2
- package/src/asset-corpus/ProductionKTX2TextureLoader.ts +0 -2
- package/src/asset-corpus/ProductionTextureLoader.ts +0 -2
- package/src/asset-corpus/index.ts +0 -29
- package/src/browser-index.ts +0 -229
- package/src/createRenderableScene.ts +0 -128
- package/src/index.ts +0 -325
- package/src/loadRenderableAsset.ts +0 -67
- package/src/loaders/CubeTextureLoader.ts +0 -8
- package/src/loaders/EXRLoader.ts +0 -10
- package/src/loaders/HDRLoader.ts +0 -7
- package/src/loaders/KTX2Loader.ts +0 -7
- package/src/loaders/LoaderDiagnostics.ts +0 -36
- package/src/loaders/MTLLoader.ts +0 -20
- package/src/loaders/OBJLoader.ts +0 -24
- package/src/loaders/TextureLoader.ts +0 -14
- package/src/loaders/ThreeCompatGLTFLoader.ts +0 -19
- package/src/loaders/index.ts +0 -14
- package/src/threejs-compatibility/ThreeCompatAssetProvenance.ts +0 -51
- package/src/threejs-compatibility/ThreeCompatAssetRegistry.ts +0 -61
- package/src/threejs-example-parity/index.ts +0 -262
- package/tests/assets.test.ts +0 -393
- package/tsconfig.json +0 -7
|
@@ -1,1416 +0,0 @@
|
|
|
1
|
-
import { AnimationAction, AnimationClip, AnimationMixer, normalizeQuat, slerpQuat, solveTwoBoneIk, type AnimationEvent, type AnimationMixerOptions, type AnimationValue, type LoopMode, type TrackValueType, type TwoBoneIkResult } from "@aura3d/animation";
|
|
2
|
-
import { invertMat4, multiplyMat4, Renderable, Scene, transformPoint, type Mat4, type SceneNode, type Vec3 } from "@aura3d/scene";
|
|
3
|
-
import type { GLTFAsset, GLTFMeshAsset, GLTFSkinAsset } from "./GLTFLoader";
|
|
4
|
-
|
|
5
|
-
export interface GLTFSceneAnimationRuntimeOptions {
|
|
6
|
-
readonly scene: Scene;
|
|
7
|
-
readonly clips: readonly AnimationClip[];
|
|
8
|
-
readonly asset?: Pick<GLTFAsset, "meshes" | "skins">;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Synonym groups for fuzzy clip-name matching. Real catalog assets label the same
|
|
13
|
-
* motion wildly differently ("Loops" vs "Idle", "Take 001" vs "Run", "sprint" vs
|
|
14
|
-
* "run"). Each inner array is a set of interchangeable canonical tokens.
|
|
15
|
-
*/
|
|
16
|
-
const GLTF_CLIP_SYNONYM_GROUPS: readonly (readonly string[])[] = [
|
|
17
|
-
["idle", "static", "rest", "loops", "loop", "tpose", "t-pose", "bind"],
|
|
18
|
-
["walk", "walking", "stroll"],
|
|
19
|
-
["run", "running", "sprint", "jog"],
|
|
20
|
-
["wave", "emote-yes", "emoteyes", "greet", "hello"],
|
|
21
|
-
["jump", "jumping", "hop"],
|
|
22
|
-
["attack", "punch", "hit", "strike"],
|
|
23
|
-
["death", "die", "dead"]
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
function normalizeClipToken(name: string): string {
|
|
27
|
-
return name.toLowerCase().replace(/[\s_.\-]+/g, "");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Resolve a requested clip name to the best available clip name.
|
|
32
|
-
*
|
|
33
|
-
* Matching order:
|
|
34
|
-
* 1. Exact case-insensitive (ignoring whitespace/underscores/dots/dashes).
|
|
35
|
-
* 2. Synonym-group match: the requested name and an available name belong to
|
|
36
|
-
* the same synonym group (e.g. "run" resolves to "Sprint").
|
|
37
|
-
* 3. Substring match in either direction (e.g. "walk" -> "WalkCycle").
|
|
38
|
-
* 4. Fallback to the first available clip.
|
|
39
|
-
*
|
|
40
|
-
* Returns `undefined` only when there are no available clips.
|
|
41
|
-
*/
|
|
42
|
-
export function resolveGLTFClipName(
|
|
43
|
-
requested: string,
|
|
44
|
-
available: readonly string[]
|
|
45
|
-
): string | undefined {
|
|
46
|
-
if (available.length === 0) return undefined;
|
|
47
|
-
const requestedToken = normalizeClipToken(requested);
|
|
48
|
-
|
|
49
|
-
for (const name of available) {
|
|
50
|
-
if (normalizeClipToken(name) === requestedToken) return name;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const requestedGroup = GLTF_CLIP_SYNONYM_GROUPS.find((group) => group.includes(requestedToken));
|
|
54
|
-
if (requestedGroup) {
|
|
55
|
-
for (const name of available) {
|
|
56
|
-
if (requestedGroup.includes(normalizeClipToken(name))) return name;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
for (const name of available) {
|
|
61
|
-
const token = normalizeClipToken(name);
|
|
62
|
-
if (requestedToken.length > 0 && (token.includes(requestedToken) || requestedToken.includes(token))) {
|
|
63
|
-
return name;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return available[0];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface GLTFSceneAnimationClipBoneMask {
|
|
71
|
-
/** Only apply tracks whose node name matches one of these (substring match), if present. */
|
|
72
|
-
readonly include?: readonly string[];
|
|
73
|
-
/** Never apply tracks whose node name matches one of these (substring match). */
|
|
74
|
-
readonly exclude?: readonly string[];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export interface GLTFSceneAnimationClipSample {
|
|
78
|
-
readonly clipName: string;
|
|
79
|
-
readonly time: number;
|
|
80
|
-
readonly weight?: number;
|
|
81
|
-
readonly additive?: boolean;
|
|
82
|
-
/**
|
|
83
|
-
* Optional per-clip bone mask for layered playback (e.g. an upper-body attack over a full-body
|
|
84
|
-
* locomotion base). When set, only matching node tracks from this clip are blended in. Default
|
|
85
|
-
* (undefined) applies the whole clip, preserving existing behavior.
|
|
86
|
-
*/
|
|
87
|
-
readonly mask?: GLTFSceneAnimationClipBoneMask;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function clipMaskAllowsNode(mask: GLTFSceneAnimationClipBoneMask | undefined, nodeName: string): boolean {
|
|
91
|
-
if (!mask) return true;
|
|
92
|
-
if (mask.exclude && mask.exclude.some((entry) => nodeName.includes(entry))) return false;
|
|
93
|
-
if (mask.include && mask.include.length > 0) return mask.include.some((entry) => nodeName.includes(entry));
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* A single bone's local transform inside a {@link GLTFScenePose}. Components are plain tuples in
|
|
99
|
-
* glTF/scene convention (translation/scale as `[x,y,z]`, rotation as a quaternion `[x,y,z,w]`). For
|
|
100
|
-
* convenience the bridge also accepts the object form (`{x,y,z}` / `{x,y,z,w}`) emitted directly by
|
|
101
|
-
* `@aura3d/animation`'s `retargetHumanoidPose`, so a retargeted `AnimationPose` can be handed in
|
|
102
|
-
* without re-shaping.
|
|
103
|
-
*/
|
|
104
|
-
export interface GLTFScenePoseBoneTransform {
|
|
105
|
-
readonly position?: readonly [number, number, number] | { readonly x: number; readonly y: number; readonly z: number };
|
|
106
|
-
readonly rotation?: readonly [number, number, number, number] | { readonly x: number; readonly y: number; readonly z: number; readonly w: number };
|
|
107
|
-
readonly scale?: readonly [number, number, number] | { readonly x: number; readonly y: number; readonly z: number };
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* An externally-computed pose keyed directly by GLB **node names** (not semantic humanoid slots).
|
|
112
|
-
* This is the render-time bridge target for a retargeted `AnimationPose`: the keys of `bones` are
|
|
113
|
-
* the target rig's node names — exactly what `retargetHumanoidPose(...).bones` produces for a
|
|
114
|
-
* humanoid map (`binding.target.name`). `morphTargets` maps a node name to a single morph weight.
|
|
115
|
-
*/
|
|
116
|
-
export interface GLTFScenePose {
|
|
117
|
-
readonly bones: Record<string, GLTFScenePoseBoneTransform>;
|
|
118
|
-
readonly morphTargets?: Record<string, number>;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export interface GLTFSceneAnimationApplyResult {
|
|
122
|
-
readonly clipName: string;
|
|
123
|
-
readonly time: number;
|
|
124
|
-
readonly blendedClipCount?: number;
|
|
125
|
-
readonly tracksApplied: number;
|
|
126
|
-
readonly transformTracksApplied: number;
|
|
127
|
-
readonly morphWeightTracksApplied: number;
|
|
128
|
-
readonly skinningPalettesUpdated: number;
|
|
129
|
-
readonly missingTargets: readonly string[];
|
|
130
|
-
readonly unsupportedTracks: readonly string[];
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export interface GLTFSceneAnimationRuntimeSnapshot {
|
|
134
|
-
readonly clipCount: number;
|
|
135
|
-
readonly nodeTargetCount: number;
|
|
136
|
-
readonly morphTargetNodeCount: number;
|
|
137
|
-
readonly skinningBindingCount: number;
|
|
138
|
-
readonly clips: readonly string[];
|
|
139
|
-
readonly lastApply?: GLTFSceneAnimationApplyResult;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface GLTFSceneAnimationClipBindingDiagnostics {
|
|
143
|
-
readonly clipName: string;
|
|
144
|
-
readonly trackCount: number;
|
|
145
|
-
readonly supportedTrackCount: number;
|
|
146
|
-
readonly boundTrackCount: number;
|
|
147
|
-
readonly transformTrackCount: number;
|
|
148
|
-
readonly morphWeightTrackCount: number;
|
|
149
|
-
readonly missingTargetCount: number;
|
|
150
|
-
readonly unsupportedTrackCount: number;
|
|
151
|
-
readonly skinningBindingCount: number;
|
|
152
|
-
readonly boundNodeNames: readonly string[];
|
|
153
|
-
readonly missingTargets: readonly string[];
|
|
154
|
-
readonly unsupportedTracks: readonly string[];
|
|
155
|
-
readonly animatesSkeleton: boolean;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export interface GLTFSceneAnimationMixerOptions extends GLTFSceneAnimationRuntimeOptions {
|
|
159
|
-
readonly autoPlay?: string | false;
|
|
160
|
-
readonly mixer?: AnimationMixerOptions;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export interface GLTFSceneAnimationMixerUpdateResult {
|
|
164
|
-
readonly events: readonly AnimationEvent[];
|
|
165
|
-
readonly applyResult: GLTFSceneAnimationApplyResult;
|
|
166
|
-
readonly activeActions: readonly GLTFSceneAnimationActionSnapshot[];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export interface GLTFSceneAnimationMixerSnapshot extends GLTFSceneAnimationRuntimeSnapshot {
|
|
170
|
-
readonly mixerActionCount: number;
|
|
171
|
-
readonly pendingValueCount: number;
|
|
172
|
-
readonly elapsedTime: number;
|
|
173
|
-
readonly timeScale: number;
|
|
174
|
-
readonly activeClipNames: readonly string[];
|
|
175
|
-
readonly actions: readonly GLTFSceneAnimationActionSnapshot[];
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
export interface GLTFSceneAnimationPlayOptions {
|
|
179
|
-
readonly weight?: number;
|
|
180
|
-
readonly timeScale?: number;
|
|
181
|
-
readonly loopMode?: LoopMode;
|
|
182
|
-
readonly reset?: boolean;
|
|
183
|
-
readonly fadeDuration?: number;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export interface GLTFSceneAnimationActionSnapshot {
|
|
187
|
-
readonly clipName: string;
|
|
188
|
-
readonly duration: number;
|
|
189
|
-
readonly time: number;
|
|
190
|
-
readonly weight: number;
|
|
191
|
-
readonly timeScale: number;
|
|
192
|
-
readonly playing: boolean;
|
|
193
|
-
readonly paused: boolean;
|
|
194
|
-
readonly loopMode: LoopMode;
|
|
195
|
-
readonly active: boolean;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export interface GLTFImportedSkeletonIKOptions {
|
|
199
|
-
readonly skinName?: string;
|
|
200
|
-
readonly jointNames?: readonly [string, string, string];
|
|
201
|
-
readonly target: readonly [number, number, number];
|
|
202
|
-
readonly pole?: readonly [number, number, number];
|
|
203
|
-
readonly weight?: number;
|
|
204
|
-
readonly allowStretch?: boolean;
|
|
205
|
-
readonly apply?: boolean;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export interface GLTFImportedSkeletonIKResult {
|
|
209
|
-
readonly skinName: string;
|
|
210
|
-
readonly jointNames: readonly [string, string, string];
|
|
211
|
-
readonly solution: TwoBoneIkResult;
|
|
212
|
-
readonly applied: boolean;
|
|
213
|
-
readonly skinningPalettesUpdated: number;
|
|
214
|
-
readonly missingTargets: readonly string[];
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
export interface GLTFImportedSkeletonIKControllerOptions extends Omit<GLTFImportedSkeletonIKOptions, "target"> {
|
|
218
|
-
readonly target: readonly [number, number, number];
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export interface GLTFImportedSkeletonIKControllerSnapshot {
|
|
222
|
-
readonly target: readonly [number, number, number];
|
|
223
|
-
readonly pole?: readonly [number, number, number];
|
|
224
|
-
readonly weight?: number;
|
|
225
|
-
readonly allowStretch?: boolean;
|
|
226
|
-
readonly apply: boolean;
|
|
227
|
-
readonly skinName?: string;
|
|
228
|
-
readonly jointNames?: readonly [string, string, string];
|
|
229
|
-
readonly lastResult?: GLTFImportedSkeletonIKResult;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export interface GLTFSceneAnimationCloneSample {
|
|
233
|
-
readonly cloneId: string;
|
|
234
|
-
readonly clipName: string;
|
|
235
|
-
readonly time: number;
|
|
236
|
-
readonly weight?: number;
|
|
237
|
-
readonly additive?: boolean;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export interface GLTFSceneAnimationCloneSampleResult {
|
|
241
|
-
readonly cloneId: string;
|
|
242
|
-
readonly clipName: string;
|
|
243
|
-
readonly time: number;
|
|
244
|
-
readonly applyResult: GLTFSceneAnimationApplyResult;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export interface GLTFSceneAnimationCloneSamplerSnapshot {
|
|
248
|
-
readonly cloneCount: number;
|
|
249
|
-
readonly lastSampleCount: number;
|
|
250
|
-
readonly lastSkinningPalettesUpdated: number;
|
|
251
|
-
readonly lastResults: readonly GLTFSceneAnimationCloneSampleResult[];
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
export interface GLTFSceneMorphTargetControllerOptions {
|
|
255
|
-
readonly target: string;
|
|
256
|
-
readonly labels?: readonly string[];
|
|
257
|
-
readonly initialWeights?: readonly number[];
|
|
258
|
-
readonly clamp?: boolean;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
export interface GLTFSceneMorphTargetControllerSnapshot {
|
|
262
|
-
readonly target: string;
|
|
263
|
-
readonly labels: readonly string[];
|
|
264
|
-
readonly weights: readonly number[];
|
|
265
|
-
readonly lastApply?: GLTFSceneAnimationApplyResult;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
export class GLTFSceneMorphTargetController {
|
|
269
|
-
private readonly labels: readonly string[];
|
|
270
|
-
private readonly labelToIndex = new Map<string, number>();
|
|
271
|
-
private readonly clampWeights: boolean;
|
|
272
|
-
private weights: number[];
|
|
273
|
-
private lastApply: GLTFSceneAnimationApplyResult | undefined;
|
|
274
|
-
|
|
275
|
-
constructor(
|
|
276
|
-
private readonly runtime: GLTFSceneAnimationRuntime,
|
|
277
|
-
private readonly options: GLTFSceneMorphTargetControllerOptions
|
|
278
|
-
) {
|
|
279
|
-
if (options.target.trim().length === 0) {
|
|
280
|
-
throw new Error("glTF morph target controller target cannot be empty.");
|
|
281
|
-
}
|
|
282
|
-
this.labels = [...(options.labels ?? [])];
|
|
283
|
-
this.labels.forEach((label, index) => {
|
|
284
|
-
if (label.trim().length === 0) {
|
|
285
|
-
throw new Error("glTF morph target controller labels cannot be empty.");
|
|
286
|
-
}
|
|
287
|
-
this.labelToIndex.set(label, index);
|
|
288
|
-
});
|
|
289
|
-
this.clampWeights = options.clamp ?? true;
|
|
290
|
-
this.weights = [...(options.initialWeights ?? new Array(Math.max(1, this.labels.length)).fill(0))].map((weight) => this.normalizeWeight(weight));
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
setWeight(indexOrLabel: number | string, weight: number): this {
|
|
294
|
-
const index = this.resolveIndex(indexOrLabel);
|
|
295
|
-
this.ensureWeightIndex(index);
|
|
296
|
-
this.weights[index] = this.normalizeWeight(weight);
|
|
297
|
-
return this;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
setWeights(weights: readonly number[]): this {
|
|
301
|
-
if (weights.length === 0) {
|
|
302
|
-
throw new Error("glTF morph target controller requires at least one morph weight.");
|
|
303
|
-
}
|
|
304
|
-
this.weights = weights.map((weight) => this.normalizeWeight(weight));
|
|
305
|
-
return this;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
getWeights(): readonly number[] {
|
|
309
|
-
return [...this.weights];
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
apply(time: number, label = `morph:${this.options.target}`): GLTFSceneAnimationApplyResult {
|
|
313
|
-
this.lastApply = this.runtime.applyAnimationValues(label, time, new Map([[this.options.target, [...this.weights]]]));
|
|
314
|
-
return this.lastApply;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
snapshot(): GLTFSceneMorphTargetControllerSnapshot {
|
|
318
|
-
return {
|
|
319
|
-
target: this.options.target,
|
|
320
|
-
labels: [...this.labels],
|
|
321
|
-
weights: [...this.weights],
|
|
322
|
-
...(this.lastApply === undefined ? {} : { lastApply: this.lastApply })
|
|
323
|
-
};
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
private resolveIndex(indexOrLabel: number | string): number {
|
|
327
|
-
if (typeof indexOrLabel === "number") {
|
|
328
|
-
if (!Number.isInteger(indexOrLabel) || indexOrLabel < 0) {
|
|
329
|
-
throw new Error("glTF morph target controller index must be a non-negative integer.");
|
|
330
|
-
}
|
|
331
|
-
return indexOrLabel;
|
|
332
|
-
}
|
|
333
|
-
const index = this.labelToIndex.get(indexOrLabel);
|
|
334
|
-
if (index === undefined) {
|
|
335
|
-
throw new Error(`glTF morph target label "${indexOrLabel}" was not found.`);
|
|
336
|
-
}
|
|
337
|
-
return index;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
private ensureWeightIndex(index: number): void {
|
|
341
|
-
while (this.weights.length <= index) {
|
|
342
|
-
this.weights.push(0);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
private normalizeWeight(weight: number): number {
|
|
347
|
-
if (!Number.isFinite(weight)) {
|
|
348
|
-
throw new Error("glTF morph target controller weights must be finite.");
|
|
349
|
-
}
|
|
350
|
-
if (!this.clampWeights) return weight;
|
|
351
|
-
return Math.min(1, Math.max(0, weight));
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export class GLTFSceneAnimationCloneSampler {
|
|
356
|
-
private lastResults: GLTFSceneAnimationCloneSampleResult[] = [];
|
|
357
|
-
|
|
358
|
-
constructor(private readonly runtime: GLTFSceneAnimationRuntime) {}
|
|
359
|
-
|
|
360
|
-
sampleClones(
|
|
361
|
-
samples: readonly GLTFSceneAnimationCloneSample[],
|
|
362
|
-
onSample?: (result: GLTFSceneAnimationCloneSampleResult) => void
|
|
363
|
-
): readonly GLTFSceneAnimationCloneSampleResult[] {
|
|
364
|
-
const results: GLTFSceneAnimationCloneSampleResult[] = [];
|
|
365
|
-
for (const sample of samples) {
|
|
366
|
-
const applyResult = sample.weight === undefined && sample.additive !== true
|
|
367
|
-
? this.runtime.applyClipByName(sample.clipName, sample.time)
|
|
368
|
-
: this.runtime.applyClips([{
|
|
369
|
-
clipName: sample.clipName,
|
|
370
|
-
time: sample.time,
|
|
371
|
-
...(sample.weight === undefined ? {} : { weight: sample.weight }),
|
|
372
|
-
...(sample.additive === undefined ? {} : { additive: sample.additive })
|
|
373
|
-
}]);
|
|
374
|
-
const result: GLTFSceneAnimationCloneSampleResult = {
|
|
375
|
-
cloneId: sample.cloneId,
|
|
376
|
-
clipName: sample.clipName,
|
|
377
|
-
time: applyResult.time,
|
|
378
|
-
applyResult
|
|
379
|
-
};
|
|
380
|
-
results.push(result);
|
|
381
|
-
onSample?.(result);
|
|
382
|
-
}
|
|
383
|
-
this.lastResults = results;
|
|
384
|
-
return results;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
snapshot(): GLTFSceneAnimationCloneSamplerSnapshot {
|
|
388
|
-
return {
|
|
389
|
-
cloneCount: new Set(this.lastResults.map((result) => result.cloneId)).size,
|
|
390
|
-
lastSampleCount: this.lastResults.length,
|
|
391
|
-
lastSkinningPalettesUpdated: this.lastResults.reduce((sum, result) => sum + result.applyResult.skinningPalettesUpdated, 0),
|
|
392
|
-
lastResults: this.lastResults.map((result) => ({ ...result }))
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
export class GLTFImportedSkeletonIKController {
|
|
398
|
-
private target: readonly [number, number, number];
|
|
399
|
-
private pole: readonly [number, number, number] | undefined;
|
|
400
|
-
private weight: number | undefined;
|
|
401
|
-
private allowStretch: boolean | undefined;
|
|
402
|
-
private apply: boolean;
|
|
403
|
-
private lastResult: GLTFImportedSkeletonIKResult | undefined;
|
|
404
|
-
|
|
405
|
-
constructor(
|
|
406
|
-
private readonly runtime: GLTFSceneAnimationRuntime,
|
|
407
|
-
private readonly options: Omit<GLTFImportedSkeletonIKControllerOptions, "target" | "pole" | "weight" | "allowStretch" | "apply">
|
|
408
|
-
& Pick<GLTFImportedSkeletonIKControllerOptions, "skinName" | "jointNames">,
|
|
409
|
-
initial: Pick<GLTFImportedSkeletonIKControllerOptions, "target" | "pole" | "weight" | "allowStretch" | "apply">
|
|
410
|
-
) {
|
|
411
|
-
this.target = initial.target;
|
|
412
|
-
this.pole = initial.pole;
|
|
413
|
-
this.weight = initial.weight;
|
|
414
|
-
this.allowStretch = initial.allowStretch;
|
|
415
|
-
this.apply = initial.apply ?? true;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
setTarget(target: readonly [number, number, number]): this {
|
|
419
|
-
this.target = target;
|
|
420
|
-
return this;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
setPole(pole: readonly [number, number, number] | undefined): this {
|
|
424
|
-
this.pole = pole;
|
|
425
|
-
return this;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
setWeight(weight: number | undefined): this {
|
|
429
|
-
if (weight !== undefined && (!Number.isFinite(weight) || weight < 0 || weight > 1)) {
|
|
430
|
-
throw new Error("glTF imported skeleton IK controller weight must be between 0 and 1.");
|
|
431
|
-
}
|
|
432
|
-
this.weight = weight;
|
|
433
|
-
return this;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
setAllowStretch(allowStretch: boolean | undefined): this {
|
|
437
|
-
this.allowStretch = allowStretch;
|
|
438
|
-
return this;
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
setApply(apply: boolean): this {
|
|
442
|
-
this.apply = apply;
|
|
443
|
-
return this;
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
solve(patch: Partial<Pick<GLTFImportedSkeletonIKControllerOptions, "target" | "pole" | "weight" | "allowStretch" | "apply">> = {}): GLTFImportedSkeletonIKResult {
|
|
447
|
-
if (patch.target !== undefined) this.setTarget(patch.target);
|
|
448
|
-
if ("pole" in patch) this.setPole(patch.pole);
|
|
449
|
-
if ("weight" in patch) this.setWeight(patch.weight);
|
|
450
|
-
if ("allowStretch" in patch) this.setAllowStretch(patch.allowStretch);
|
|
451
|
-
if (patch.apply !== undefined) this.setApply(patch.apply);
|
|
452
|
-
this.lastResult = this.runtime.solveImportedSkeletonTwoBoneIK({
|
|
453
|
-
...this.options,
|
|
454
|
-
target: this.target,
|
|
455
|
-
...(this.pole === undefined ? {} : { pole: this.pole }),
|
|
456
|
-
...(this.weight === undefined ? {} : { weight: this.weight }),
|
|
457
|
-
...(this.allowStretch === undefined ? {} : { allowStretch: this.allowStretch }),
|
|
458
|
-
apply: this.apply
|
|
459
|
-
});
|
|
460
|
-
return this.lastResult;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
snapshot(): GLTFImportedSkeletonIKControllerSnapshot {
|
|
464
|
-
return {
|
|
465
|
-
target: this.target,
|
|
466
|
-
...(this.pole === undefined ? {} : { pole: this.pole }),
|
|
467
|
-
...(this.weight === undefined ? {} : { weight: this.weight }),
|
|
468
|
-
...(this.allowStretch === undefined ? {} : { allowStretch: this.allowStretch }),
|
|
469
|
-
apply: this.apply,
|
|
470
|
-
...(this.options.skinName === undefined ? {} : { skinName: this.options.skinName }),
|
|
471
|
-
...(this.options.jointNames === undefined ? {} : { jointNames: this.options.jointNames }),
|
|
472
|
-
...(this.lastResult === undefined ? {} : { lastResult: this.lastResult })
|
|
473
|
-
};
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
interface RuntimeSkinningBinding {
|
|
478
|
-
readonly node: SceneNode;
|
|
479
|
-
readonly renderable: Renderable;
|
|
480
|
-
readonly mesh: GLTFMeshAsset;
|
|
481
|
-
readonly skin: GLTFSkinAsset;
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
type WeightedAccumulator = { value: AnimationValue; weight: number; type: TrackValueType };
|
|
485
|
-
type TargetAccumulator = { type: TrackValueType; base?: WeightedAccumulator; additive?: AnimationValue };
|
|
486
|
-
|
|
487
|
-
export class GLTFSceneAnimationRuntime {
|
|
488
|
-
private readonly clipsByName = new Map<string, AnimationClip>();
|
|
489
|
-
private readonly nodesByName = new Map<string, SceneNode[]>();
|
|
490
|
-
private readonly morphRenderablesByNodeName = new Map<string, Renderable[]>();
|
|
491
|
-
private readonly skinningBindings: RuntimeSkinningBinding[] = [];
|
|
492
|
-
private lastApply?: GLTFSceneAnimationApplyResult;
|
|
493
|
-
|
|
494
|
-
constructor(private readonly options: GLTFSceneAnimationRuntimeOptions) {
|
|
495
|
-
for (const clip of options.clips) {
|
|
496
|
-
this.clipsByName.set(clip.name, clip);
|
|
497
|
-
}
|
|
498
|
-
this.reindexScene();
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
applyClipByName(name: string, time: number): GLTFSceneAnimationApplyResult {
|
|
502
|
-
const clip = this.clipsByName.get(name);
|
|
503
|
-
if (!clip) {
|
|
504
|
-
throw new Error(`glTF animation clip "${name}" was not found.`);
|
|
505
|
-
}
|
|
506
|
-
return this.applyClip(clip, time);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/** Names of every clip registered on this runtime, in declaration order. */
|
|
510
|
-
clipNames(): readonly string[] {
|
|
511
|
-
return [...this.clipsByName.keys()];
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
/**
|
|
515
|
-
* Resolve a requested clip name to the best available registered clip name
|
|
516
|
-
* using fuzzy matching (exact -> synonym group -> substring -> first clip).
|
|
517
|
-
* Returns `undefined` when the runtime has no clips.
|
|
518
|
-
*/
|
|
519
|
-
resolveClipName(name: string): string | undefined {
|
|
520
|
-
return resolveGLTFClipName(name, this.clipNames());
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* Apply a clip selected by fuzzy name resolution. Unlike {@link applyClipByName}
|
|
525
|
-
* this tolerates differing source clip names (e.g. requesting "idle" when the
|
|
526
|
-
* asset only ships "Loops"). Throws only when no clips exist at all.
|
|
527
|
-
*/
|
|
528
|
-
applyClipByNameFuzzy(name: string, time: number): GLTFSceneAnimationApplyResult {
|
|
529
|
-
const resolved = this.resolveClipName(name);
|
|
530
|
-
if (resolved === undefined) {
|
|
531
|
-
throw new Error("glTF animation runtime has no clips to resolve.");
|
|
532
|
-
}
|
|
533
|
-
return this.applyClipByName(resolved, time);
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
applyClip(clip: AnimationClip, time: number): GLTFSceneAnimationApplyResult {
|
|
537
|
-
if (!Number.isFinite(time) || time < 0) {
|
|
538
|
-
throw new Error("glTF animation runtime time must be finite and non-negative.");
|
|
539
|
-
}
|
|
540
|
-
const wrappedTime = clip.duration > 0 && time > clip.duration ? time % clip.duration : Math.min(time, clip.duration);
|
|
541
|
-
this.lastApply = this.applySampledTargets(clip.name, wrappedTime, sampleClipTracks(clip, wrappedTime, 1, false));
|
|
542
|
-
return this.lastApply;
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
applyClips(samples: readonly GLTFSceneAnimationClipSample[]): GLTFSceneAnimationApplyResult {
|
|
546
|
-
if (samples.length === 0) {
|
|
547
|
-
throw new Error("glTF animation runtime blend requires at least one clip sample.");
|
|
548
|
-
}
|
|
549
|
-
const accumulators = new Map<string, TargetAccumulator>();
|
|
550
|
-
const unsupportedTracks: string[] = [];
|
|
551
|
-
let maxTime = 0;
|
|
552
|
-
const names: string[] = [];
|
|
553
|
-
|
|
554
|
-
for (const sample of samples) {
|
|
555
|
-
const clip = this.clipsByName.get(sample.clipName);
|
|
556
|
-
if (!clip) {
|
|
557
|
-
throw new Error(`glTF animation clip "${sample.clipName}" was not found.`);
|
|
558
|
-
}
|
|
559
|
-
if (!Number.isFinite(sample.time) || sample.time < 0) {
|
|
560
|
-
throw new Error("glTF animation runtime blend time must be finite and non-negative.");
|
|
561
|
-
}
|
|
562
|
-
const weight = sample.weight ?? 1;
|
|
563
|
-
if (!Number.isFinite(weight) || weight < 0) {
|
|
564
|
-
throw new Error("glTF animation runtime blend weight must be finite and non-negative.");
|
|
565
|
-
}
|
|
566
|
-
if (weight === 0) continue;
|
|
567
|
-
const wrappedTime = clip.duration > 0 && sample.time > clip.duration ? sample.time % clip.duration : Math.min(sample.time, clip.duration);
|
|
568
|
-
maxTime = Math.max(maxTime, wrappedTime);
|
|
569
|
-
names.push(`${clip.name}@${Number(wrappedTime.toFixed(4))}x${Number(weight.toFixed(4))}${sample.additive ? "+add" : ""}`);
|
|
570
|
-
for (const track of clip.tracks) {
|
|
571
|
-
const target = parseAnimationTarget(track.target);
|
|
572
|
-
if (!target) {
|
|
573
|
-
unsupportedTracks.push(track.target);
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
if (!clipMaskAllowsNode(sample.mask, target.nodeName)) continue;
|
|
577
|
-
blendInto(accumulators, track.target, track.valueType, track.sample(wrappedTime), weight, sample.additive === true);
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const sampledTargets = new Map<string, AnimationValue>();
|
|
582
|
-
for (const [target, accumulator] of accumulators) {
|
|
583
|
-
sampledTargets.set(target, finalizeTargetBlend(accumulator));
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
this.lastApply = this.applySampledTargets(
|
|
587
|
-
`blend:${names.join(",")}`,
|
|
588
|
-
maxTime,
|
|
589
|
-
{ sampledTargets, unsupportedTracks },
|
|
590
|
-
samples.length
|
|
591
|
-
);
|
|
592
|
-
return this.lastApply;
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
applyAnimationValues(
|
|
596
|
-
label: string,
|
|
597
|
-
time: number,
|
|
598
|
-
values: ReadonlyMap<string, AnimationValue>,
|
|
599
|
-
unsupportedTracks: readonly string[] = []
|
|
600
|
-
): GLTFSceneAnimationApplyResult {
|
|
601
|
-
if (label.trim().length === 0) {
|
|
602
|
-
throw new Error("glTF animation runtime value application label cannot be empty.");
|
|
603
|
-
}
|
|
604
|
-
if (!Number.isFinite(time) || time < 0) {
|
|
605
|
-
throw new Error("glTF animation runtime value application time must be finite and non-negative.");
|
|
606
|
-
}
|
|
607
|
-
this.lastApply = this.applySampledTargets(label, time, {
|
|
608
|
-
sampledTargets: values,
|
|
609
|
-
unsupportedTracks
|
|
610
|
-
});
|
|
611
|
-
return this.lastApply;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* Render-time bridge: drive the loaded GLB from an externally-computed pose whose keys are GLB
|
|
616
|
-
* **node names** (e.g. the output of `@aura3d/animation`'s `retargetHumanoidPose`, whose
|
|
617
|
-
* `bones` keys are the target rig node names). Each bone transform's `position`/`rotation`/`scale`
|
|
618
|
-
* is written onto the matching scene node's local transform, and each `morphTargets` entry sets
|
|
619
|
-
* that node's morph weight. This reuses the same {@link applyAnimationValues} / track-binding path
|
|
620
|
-
* as embedded clips (targets are emitted as `"<node>.translation|rotation|scale|weights"`), so
|
|
621
|
-
* skinning palettes and missing-target reporting behave identically. Additive to `applyClip*` —
|
|
622
|
-
* it does not touch the mixer or clip registry.
|
|
623
|
-
*/
|
|
624
|
-
applyPose(pose: GLTFScenePose, label = "retargeted-pose", time = 0): GLTFSceneAnimationApplyResult {
|
|
625
|
-
if (!pose || typeof pose !== "object" || typeof pose.bones !== "object" || pose.bones === null) {
|
|
626
|
-
throw new Error("glTF animation runtime applyPose requires a pose with a bones record.");
|
|
627
|
-
}
|
|
628
|
-
const values = new Map<string, AnimationValue>();
|
|
629
|
-
for (const [nodeName, transform] of Object.entries(pose.bones)) {
|
|
630
|
-
if (!transform) continue;
|
|
631
|
-
if (transform.position !== undefined) {
|
|
632
|
-
values.set(`${nodeName}.translation`, toVec3Tuple(transform.position, `${nodeName}.translation`));
|
|
633
|
-
}
|
|
634
|
-
if (transform.rotation !== undefined) {
|
|
635
|
-
values.set(`${nodeName}.rotation`, toQuatTuple(transform.rotation, `${nodeName}.rotation`));
|
|
636
|
-
}
|
|
637
|
-
if (transform.scale !== undefined) {
|
|
638
|
-
values.set(`${nodeName}.scale`, toVec3Tuple(transform.scale, `${nodeName}.scale`));
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
if (pose.morphTargets) {
|
|
642
|
-
for (const [nodeName, weight] of Object.entries(pose.morphTargets)) {
|
|
643
|
-
if (!Number.isFinite(weight)) {
|
|
644
|
-
throw new Error(`glTF animation runtime applyPose morph weight for "${nodeName}" must be finite.`);
|
|
645
|
-
}
|
|
646
|
-
values.set(`${nodeName}.weights`, [weight]);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
return this.applyAnimationValues(label, time, values);
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
solveImportedSkeletonTwoBoneIK(options: GLTFImportedSkeletonIKOptions): GLTFImportedSkeletonIKResult {
|
|
653
|
-
if (!this.options.asset || this.options.asset.skins.length === 0) {
|
|
654
|
-
throw new Error("glTF imported skeleton IK requires an asset with at least one skin.");
|
|
655
|
-
}
|
|
656
|
-
const skin = resolveIKSkin(this.options.asset.skins, options.skinName);
|
|
657
|
-
const jointNames = options.jointNames ?? firstTwoBoneJointChain(skin);
|
|
658
|
-
const [rootName, midName, endName] = jointNames;
|
|
659
|
-
this.options.scene.updateWorldTransforms();
|
|
660
|
-
const rootNode = this.nodesByName.get(rootName)?.[0];
|
|
661
|
-
const midNode = this.nodesByName.get(midName)?.[0];
|
|
662
|
-
const endNode = this.nodesByName.get(endName)?.[0];
|
|
663
|
-
const missingTargets = [
|
|
664
|
-
...(!rootNode ? [`${skin.name}.${rootName}`] : []),
|
|
665
|
-
...(!midNode ? [`${skin.name}.${midName}`] : []),
|
|
666
|
-
...(!endNode ? [`${skin.name}.${endName}`] : [])
|
|
667
|
-
];
|
|
668
|
-
if (!rootNode || !midNode || !endNode) {
|
|
669
|
-
return {
|
|
670
|
-
skinName: skin.name,
|
|
671
|
-
jointNames,
|
|
672
|
-
solution: solveTwoBoneIk({
|
|
673
|
-
root: [0, 0, 0],
|
|
674
|
-
mid: [0, 1, 0],
|
|
675
|
-
end: [0, 2, 0],
|
|
676
|
-
target: [0, 2, 0.01],
|
|
677
|
-
allowStretch: true
|
|
678
|
-
}),
|
|
679
|
-
applied: false,
|
|
680
|
-
skinningPalettesUpdated: 0,
|
|
681
|
-
missingTargets
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const solution = solveTwoBoneIk({
|
|
686
|
-
root: worldPosition(rootNode),
|
|
687
|
-
mid: worldPosition(midNode),
|
|
688
|
-
end: worldPosition(endNode),
|
|
689
|
-
target: options.target,
|
|
690
|
-
...(options.pole ? { pole: options.pole } : {}),
|
|
691
|
-
...(options.weight !== undefined ? { weight: options.weight } : {}),
|
|
692
|
-
...(options.allowStretch !== undefined ? { allowStretch: options.allowStretch } : {})
|
|
693
|
-
});
|
|
694
|
-
if (options.apply === false) {
|
|
695
|
-
return {
|
|
696
|
-
skinName: skin.name,
|
|
697
|
-
jointNames,
|
|
698
|
-
solution,
|
|
699
|
-
applied: false,
|
|
700
|
-
skinningPalettesUpdated: 0,
|
|
701
|
-
missingTargets: []
|
|
702
|
-
};
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
setWorldPosition(midNode, solution.mid);
|
|
706
|
-
this.options.scene.updateWorldTransforms();
|
|
707
|
-
setWorldPosition(endNode, solution.end);
|
|
708
|
-
this.options.scene.updateWorldTransforms();
|
|
709
|
-
const skinning = this.refreshSkinningPalettes();
|
|
710
|
-
return {
|
|
711
|
-
skinName: skin.name,
|
|
712
|
-
jointNames,
|
|
713
|
-
solution,
|
|
714
|
-
applied: true,
|
|
715
|
-
skinningPalettesUpdated: skinning.updated,
|
|
716
|
-
missingTargets: skinning.missingTargets
|
|
717
|
-
};
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
createTwoBoneIKController(options: GLTFImportedSkeletonIKControllerOptions): GLTFImportedSkeletonIKController {
|
|
721
|
-
return new GLTFImportedSkeletonIKController(this, {
|
|
722
|
-
...(options.skinName === undefined ? {} : { skinName: options.skinName }),
|
|
723
|
-
...(options.jointNames === undefined ? {} : { jointNames: options.jointNames })
|
|
724
|
-
}, {
|
|
725
|
-
target: options.target,
|
|
726
|
-
...(options.pole === undefined ? {} : { pole: options.pole }),
|
|
727
|
-
...(options.weight === undefined ? {} : { weight: options.weight }),
|
|
728
|
-
...(options.allowStretch === undefined ? {} : { allowStretch: options.allowStretch }),
|
|
729
|
-
...(options.apply === undefined ? {} : { apply: options.apply })
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
createCloneSampler(): GLTFSceneAnimationCloneSampler {
|
|
734
|
-
return new GLTFSceneAnimationCloneSampler(this);
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
createMorphTargetController(options: GLTFSceneMorphTargetControllerOptions): GLTFSceneMorphTargetController {
|
|
738
|
-
return new GLTFSceneMorphTargetController(this, options);
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
snapshot(): GLTFSceneAnimationRuntimeSnapshot {
|
|
742
|
-
return {
|
|
743
|
-
clipCount: this.clipsByName.size,
|
|
744
|
-
nodeTargetCount: this.nodesByName.size,
|
|
745
|
-
morphTargetNodeCount: this.morphRenderablesByNodeName.size,
|
|
746
|
-
skinningBindingCount: this.skinningBindings.length,
|
|
747
|
-
clips: [...this.clipsByName.keys()],
|
|
748
|
-
...(this.lastApply ? { lastApply: this.lastApply } : {})
|
|
749
|
-
};
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
inspectClipBindings(name?: string): readonly GLTFSceneAnimationClipBindingDiagnostics[] {
|
|
753
|
-
if (name !== undefined) {
|
|
754
|
-
const clip = this.clipsByName.get(name);
|
|
755
|
-
if (!clip) {
|
|
756
|
-
throw new Error(`glTF animation clip "${name}" was not found.`);
|
|
757
|
-
}
|
|
758
|
-
return [this.inspectClipBinding(clip)];
|
|
759
|
-
}
|
|
760
|
-
return [...this.clipsByName.values()].map((clip) => this.inspectClipBinding(clip));
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
reindexScene(): void {
|
|
764
|
-
this.nodesByName.clear();
|
|
765
|
-
this.morphRenderablesByNodeName.clear();
|
|
766
|
-
this.skinningBindings.length = 0;
|
|
767
|
-
this.options.scene.traverse((node) => {
|
|
768
|
-
const nodes = this.nodesByName.get(node.name) ?? [];
|
|
769
|
-
nodes.push(node);
|
|
770
|
-
this.nodesByName.set(node.name, nodes);
|
|
771
|
-
});
|
|
772
|
-
for (const { node, renderable } of this.options.scene.collectRenderables()) {
|
|
773
|
-
if (renderable.morphWeights.length === 0) continue;
|
|
774
|
-
for (const nodeName of new Set([node.name, node.parent?.name].filter((name): name is string => typeof name === "string" && name.length > 0))) {
|
|
775
|
-
const renderables = this.morphRenderablesByNodeName.get(nodeName) ?? [];
|
|
776
|
-
renderables.push(renderable);
|
|
777
|
-
this.morphRenderablesByNodeName.set(nodeName, renderables);
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
if (this.options.asset) {
|
|
781
|
-
const meshesByName = new Map(this.options.asset.meshes.map((mesh) => [mesh.name, mesh]));
|
|
782
|
-
for (const { node, renderable } of this.options.scene.collectRenderables()) {
|
|
783
|
-
if (!renderable.skinning) continue;
|
|
784
|
-
const mesh = meshesByName.get(renderable.geometry);
|
|
785
|
-
const skin = mesh?.skinIndex === undefined ? undefined : this.options.asset.skins[mesh.skinIndex];
|
|
786
|
-
if (!mesh || !skin || skin.joints.length > 96) continue;
|
|
787
|
-
this.skinningBindings.push({ node, renderable, mesh, skin });
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
private refreshSkinningPalettes(): { readonly updated: number; readonly missingTargets: readonly string[] } {
|
|
793
|
-
if (this.skinningBindings.length === 0) {
|
|
794
|
-
return { updated: 0, missingTargets: [] };
|
|
795
|
-
}
|
|
796
|
-
let updated = 0;
|
|
797
|
-
const missingTargets: string[] = [];
|
|
798
|
-
for (const binding of this.skinningBindings) {
|
|
799
|
-
const matrices = new Float32Array(binding.skin.joints.length * 16);
|
|
800
|
-
const inverseMeshWorld = invertMat4(binding.node.transform.worldMatrix);
|
|
801
|
-
let complete = true;
|
|
802
|
-
for (let index = 0; index < binding.skin.jointNames.length; index += 1) {
|
|
803
|
-
const jointName = binding.skin.jointNames[index]!;
|
|
804
|
-
const jointNode = this.nodesByName.get(jointName)?.[0];
|
|
805
|
-
const inverseBind = binding.skin.inverseBindMatrices[index];
|
|
806
|
-
if (!jointNode || !inverseBind) {
|
|
807
|
-
missingTargets.push(`${binding.skin.name}.${jointName}`);
|
|
808
|
-
complete = false;
|
|
809
|
-
break;
|
|
810
|
-
}
|
|
811
|
-
const jointMatrix = multiplyMat4(multiplyMat4(inverseMeshWorld, jointNode.transform.worldMatrix), inverseBind as Mat4);
|
|
812
|
-
matrices.set(jointMatrix, index * 16);
|
|
813
|
-
}
|
|
814
|
-
if (!complete) continue;
|
|
815
|
-
binding.renderable.skinning = {
|
|
816
|
-
jointCount: binding.skin.joints.length,
|
|
817
|
-
matrices
|
|
818
|
-
};
|
|
819
|
-
updated += 1;
|
|
820
|
-
}
|
|
821
|
-
return { updated, missingTargets };
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
private inspectClipBinding(clip: AnimationClip): GLTFSceneAnimationClipBindingDiagnostics {
|
|
825
|
-
let supportedTrackCount = 0;
|
|
826
|
-
let boundTrackCount = 0;
|
|
827
|
-
let transformTrackCount = 0;
|
|
828
|
-
let morphWeightTrackCount = 0;
|
|
829
|
-
const boundNodeNames = new Set<string>();
|
|
830
|
-
const missingTargets = new Set<string>();
|
|
831
|
-
const unsupportedTracks = new Set<string>();
|
|
832
|
-
|
|
833
|
-
for (const track of clip.tracks) {
|
|
834
|
-
const target = parseAnimationTarget(track.target);
|
|
835
|
-
if (!target) {
|
|
836
|
-
unsupportedTracks.add(track.target);
|
|
837
|
-
continue;
|
|
838
|
-
}
|
|
839
|
-
supportedTrackCount += 1;
|
|
840
|
-
if (target.path === "weights") {
|
|
841
|
-
morphWeightTrackCount += 1;
|
|
842
|
-
const renderables = this.morphRenderablesByNodeName.get(target.nodeName) ?? [];
|
|
843
|
-
if (renderables.length === 0) {
|
|
844
|
-
missingTargets.add(track.target);
|
|
845
|
-
continue;
|
|
846
|
-
}
|
|
847
|
-
boundTrackCount += 1;
|
|
848
|
-
boundNodeNames.add(target.nodeName);
|
|
849
|
-
continue;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
transformTrackCount += 1;
|
|
853
|
-
const nodes = this.nodesByName.get(target.nodeName) ?? [];
|
|
854
|
-
if (nodes.length === 0) {
|
|
855
|
-
missingTargets.add(track.target);
|
|
856
|
-
continue;
|
|
857
|
-
}
|
|
858
|
-
boundTrackCount += 1;
|
|
859
|
-
boundNodeNames.add(target.nodeName);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
return {
|
|
863
|
-
clipName: clip.name,
|
|
864
|
-
trackCount: clip.tracks.length,
|
|
865
|
-
supportedTrackCount,
|
|
866
|
-
boundTrackCount,
|
|
867
|
-
transformTrackCount,
|
|
868
|
-
morphWeightTrackCount,
|
|
869
|
-
missingTargetCount: missingTargets.size,
|
|
870
|
-
unsupportedTrackCount: unsupportedTracks.size,
|
|
871
|
-
skinningBindingCount: this.skinningBindings.length,
|
|
872
|
-
boundNodeNames: [...boundNodeNames].sort(),
|
|
873
|
-
missingTargets: [...missingTargets].sort(),
|
|
874
|
-
unsupportedTracks: [...unsupportedTracks].sort(),
|
|
875
|
-
animatesSkeleton: this.skinningBindings.length > 0 && boundTrackCount > 0 && transformTrackCount > 0
|
|
876
|
-
};
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
private applySampledTargets(
|
|
880
|
-
clipName: string,
|
|
881
|
-
time: number,
|
|
882
|
-
sampled: { readonly sampledTargets: ReadonlyMap<string, AnimationValue>; readonly unsupportedTracks: readonly string[] },
|
|
883
|
-
blendedClipCount?: number
|
|
884
|
-
): GLTFSceneAnimationApplyResult {
|
|
885
|
-
let transformTracksApplied = 0;
|
|
886
|
-
let morphWeightTracksApplied = 0;
|
|
887
|
-
const missingTargets: string[] = [];
|
|
888
|
-
|
|
889
|
-
for (const [trackTarget, value] of sampled.sampledTargets) {
|
|
890
|
-
const target = parseAnimationTarget(trackTarget);
|
|
891
|
-
if (!target) {
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
if (target.path === "weights") {
|
|
895
|
-
const renderables = this.morphRenderablesByNodeName.get(target.nodeName) ?? [];
|
|
896
|
-
if (renderables.length === 0) {
|
|
897
|
-
missingTargets.push(trackTarget);
|
|
898
|
-
continue;
|
|
899
|
-
}
|
|
900
|
-
const weights = asNumberArray(value);
|
|
901
|
-
for (const renderable of renderables) {
|
|
902
|
-
renderable.morphWeights = weights;
|
|
903
|
-
}
|
|
904
|
-
morphWeightTracksApplied += 1;
|
|
905
|
-
continue;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
const nodes = this.nodesByName.get(target.nodeName) ?? [];
|
|
909
|
-
if (nodes.length === 0) {
|
|
910
|
-
missingTargets.push(trackTarget);
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
|
-
for (const node of nodes) {
|
|
914
|
-
applyTransformValue(node, target.path, value, trackTarget);
|
|
915
|
-
}
|
|
916
|
-
transformTracksApplied += 1;
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
this.options.scene.updateWorldTransforms();
|
|
920
|
-
const skinning = this.refreshSkinningPalettes();
|
|
921
|
-
return {
|
|
922
|
-
clipName,
|
|
923
|
-
time,
|
|
924
|
-
...(blendedClipCount === undefined ? {} : { blendedClipCount }),
|
|
925
|
-
tracksApplied: transformTracksApplied + morphWeightTracksApplied,
|
|
926
|
-
transformTracksApplied,
|
|
927
|
-
morphWeightTracksApplied,
|
|
928
|
-
skinningPalettesUpdated: skinning.updated,
|
|
929
|
-
missingTargets: [...missingTargets, ...skinning.missingTargets],
|
|
930
|
-
unsupportedTracks: sampled.unsupportedTracks
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
export function createGLTFSceneAnimationRuntime(options: GLTFSceneAnimationRuntimeOptions): GLTFSceneAnimationRuntime {
|
|
936
|
-
return new GLTFSceneAnimationRuntime(options);
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
export class GLTFSceneAnimationMixerBinding {
|
|
940
|
-
readonly runtime: GLTFSceneAnimationRuntime;
|
|
941
|
-
readonly mixer: AnimationMixer;
|
|
942
|
-
readonly actions: ReadonlyMap<string, AnimationAction>;
|
|
943
|
-
private readonly pendingValues = new Map<string, AnimationValue>();
|
|
944
|
-
private elapsedTime = 0;
|
|
945
|
-
private lastApply?: GLTFSceneAnimationApplyResult;
|
|
946
|
-
|
|
947
|
-
constructor(options: GLTFSceneAnimationMixerOptions) {
|
|
948
|
-
this.runtime = new GLTFSceneAnimationRuntime(options);
|
|
949
|
-
this.mixer = new AnimationMixer({
|
|
950
|
-
setAnimationValue: (target, value) => {
|
|
951
|
-
this.pendingValues.set(target, cloneAnimationValue(value));
|
|
952
|
-
}
|
|
953
|
-
}, options.mixer ?? {});
|
|
954
|
-
const actions = new Map<string, AnimationAction>();
|
|
955
|
-
for (const clip of options.clips) {
|
|
956
|
-
const action = new AnimationAction(clip).setWeight(0);
|
|
957
|
-
this.mixer.addAction(action);
|
|
958
|
-
actions.set(clip.name, action);
|
|
959
|
-
}
|
|
960
|
-
const autoPlay = options.autoPlay === false ? undefined : options.autoPlay ?? options.clips[0]?.name;
|
|
961
|
-
if (autoPlay) {
|
|
962
|
-
const action = actions.get(autoPlay);
|
|
963
|
-
if (!action) {
|
|
964
|
-
throw new Error(`glTF animation mixer autoPlay clip "${autoPlay}" was not found.`);
|
|
965
|
-
}
|
|
966
|
-
action.setWeight(1).play();
|
|
967
|
-
}
|
|
968
|
-
this.actions = actions;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
listClips(): readonly string[] {
|
|
972
|
-
return [...this.actions.keys()];
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
play(name: string, weight = 1): AnimationAction {
|
|
976
|
-
return this.playClip(name, { weight });
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
playClip(name: string, options: GLTFSceneAnimationPlayOptions = {}): AnimationAction {
|
|
980
|
-
const action = this.requireAction(name);
|
|
981
|
-
this.configureAction(action, options);
|
|
982
|
-
action.play();
|
|
983
|
-
return action;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
playExclusive(name: string, options: GLTFSceneAnimationPlayOptions = {}): AnimationAction {
|
|
987
|
-
const action = this.requireAction(name);
|
|
988
|
-
const fadeDuration = options.fadeDuration ?? 0;
|
|
989
|
-
for (const [clipName, candidate] of this.actions) {
|
|
990
|
-
if (clipName === name) continue;
|
|
991
|
-
if (fadeDuration > 0) {
|
|
992
|
-
candidate.fadeTo(0, fadeDuration);
|
|
993
|
-
} else {
|
|
994
|
-
candidate.setWeight(0);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
this.configureAction(action, { ...options, weight: options.weight ?? 1 });
|
|
998
|
-
action.play();
|
|
999
|
-
return action;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
pause(name?: string): void {
|
|
1003
|
-
for (const action of this.resolveActions(name)) {
|
|
1004
|
-
action.pause();
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
resume(name?: string): void {
|
|
1009
|
-
for (const action of this.resolveActions(name)) {
|
|
1010
|
-
if (action.weight > 0) action.play();
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
|
|
1014
|
-
stop(name?: string): void {
|
|
1015
|
-
for (const action of this.resolveActions(name)) {
|
|
1016
|
-
action.stop().setWeight(0);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
seek(name: string, time: number): AnimationAction {
|
|
1021
|
-
const action = this.requireAction(name);
|
|
1022
|
-
action.seek(time);
|
|
1023
|
-
this.elapsedTime = action.time;
|
|
1024
|
-
return action;
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
setTimeScale(timeScale: number): void {
|
|
1028
|
-
if (!Number.isFinite(timeScale) || timeScale < 0) {
|
|
1029
|
-
throw new Error("glTF animation mixer timeScale must be finite and non-negative.");
|
|
1030
|
-
}
|
|
1031
|
-
this.mixer.timeScale = timeScale;
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
setActionTimeScale(name: string, timeScale: number): AnimationAction {
|
|
1035
|
-
return this.requireAction(name).setTimeScale(timeScale);
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
setActionWeight(name: string, weight: number): AnimationAction {
|
|
1039
|
-
return this.requireAction(name).setWeight(weight);
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
setActionLoop(name: string, loopMode: LoopMode): AnimationAction {
|
|
1043
|
-
return this.requireAction(name).setLoop(loopMode);
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
crossFade(fromName: string, toName: string, duration: number): void {
|
|
1047
|
-
const from = this.requireAction(fromName);
|
|
1048
|
-
const to = this.requireAction(toName);
|
|
1049
|
-
if (!to.playing) {
|
|
1050
|
-
to.setWeight(0).play();
|
|
1051
|
-
}
|
|
1052
|
-
this.mixer.crossFade(from, to, duration);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
applyClipSamples(samples: readonly GLTFSceneAnimationClipSample[]): GLTFSceneAnimationMixerUpdateResult {
|
|
1056
|
-
this.pendingValues.clear();
|
|
1057
|
-
this.lastApply = this.runtime.applyClips(samples);
|
|
1058
|
-
this.elapsedTime = Math.max(this.elapsedTime, this.lastApply.time);
|
|
1059
|
-
return { events: [], applyResult: this.lastApply, activeActions: this.actionSnapshots().filter((action) => action.active) };
|
|
1060
|
-
}
|
|
1061
|
-
|
|
1062
|
-
update(deltaSeconds: number): GLTFSceneAnimationMixerUpdateResult {
|
|
1063
|
-
if (!Number.isFinite(deltaSeconds) || deltaSeconds < 0) {
|
|
1064
|
-
throw new Error("glTF animation mixer delta must be finite and non-negative.");
|
|
1065
|
-
}
|
|
1066
|
-
this.pendingValues.clear();
|
|
1067
|
-
const events = this.mixer.update(deltaSeconds);
|
|
1068
|
-
this.elapsedTime += deltaSeconds;
|
|
1069
|
-
this.lastApply = this.runtime.applyAnimationValues(
|
|
1070
|
-
"gltf-animation-mixer",
|
|
1071
|
-
this.elapsedTime,
|
|
1072
|
-
this.pendingValues
|
|
1073
|
-
);
|
|
1074
|
-
return { events, applyResult: this.lastApply, activeActions: this.actionSnapshots().filter((action) => action.active) };
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
getAction(name: string): AnimationAction | undefined {
|
|
1078
|
-
return this.actions.get(name);
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
snapshot(): GLTFSceneAnimationMixerSnapshot {
|
|
1082
|
-
return {
|
|
1083
|
-
...this.runtime.snapshot(),
|
|
1084
|
-
mixerActionCount: this.actions.size,
|
|
1085
|
-
pendingValueCount: this.pendingValues.size,
|
|
1086
|
-
elapsedTime: this.elapsedTime,
|
|
1087
|
-
timeScale: this.mixer.timeScale,
|
|
1088
|
-
activeClipNames: this.actionSnapshots().filter((action) => action.active).map((action) => action.clipName),
|
|
1089
|
-
actions: this.actionSnapshots(),
|
|
1090
|
-
...(this.lastApply ? { lastApply: this.lastApply } : {})
|
|
1091
|
-
};
|
|
1092
|
-
}
|
|
1093
|
-
|
|
1094
|
-
private configureAction(action: AnimationAction, options: GLTFSceneAnimationPlayOptions): void {
|
|
1095
|
-
if (options.reset === true) {
|
|
1096
|
-
action.reset();
|
|
1097
|
-
}
|
|
1098
|
-
if (options.weight !== undefined) {
|
|
1099
|
-
if (options.fadeDuration !== undefined && options.fadeDuration > 0) {
|
|
1100
|
-
action.fadeTo(options.weight, options.fadeDuration);
|
|
1101
|
-
} else {
|
|
1102
|
-
action.setWeight(options.weight);
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
if (options.timeScale !== undefined) {
|
|
1106
|
-
action.setTimeScale(options.timeScale);
|
|
1107
|
-
}
|
|
1108
|
-
if (options.loopMode !== undefined) {
|
|
1109
|
-
action.setLoop(options.loopMode);
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
private resolveActions(name: string | undefined): readonly AnimationAction[] {
|
|
1114
|
-
return name === undefined ? [...this.actions.values()] : [this.requireAction(name)];
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
private actionSnapshots(): readonly GLTFSceneAnimationActionSnapshot[] {
|
|
1118
|
-
return [...this.actions.values()].map((action) => {
|
|
1119
|
-
const snapshot = action.snapshot();
|
|
1120
|
-
return {
|
|
1121
|
-
...snapshot,
|
|
1122
|
-
active: snapshot.playing && !snapshot.paused && snapshot.weight > 0
|
|
1123
|
-
};
|
|
1124
|
-
});
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
private requireAction(name: string): AnimationAction {
|
|
1128
|
-
const action = this.actions.get(name);
|
|
1129
|
-
if (!action) {
|
|
1130
|
-
throw new Error(`glTF animation action "${name}" was not found.`);
|
|
1131
|
-
}
|
|
1132
|
-
return action;
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
export function createGLTFSceneAnimationMixer(options: GLTFSceneAnimationMixerOptions): GLTFSceneAnimationMixerBinding {
|
|
1137
|
-
return new GLTFSceneAnimationMixerBinding(options);
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1140
|
-
function parseAnimationTarget(target: string): { readonly nodeName: string; readonly path: "translation" | "rotation" | "scale" | "weights" } | undefined {
|
|
1141
|
-
const separator = target.lastIndexOf(".");
|
|
1142
|
-
if (separator <= 0 || separator === target.length - 1) {
|
|
1143
|
-
return undefined;
|
|
1144
|
-
}
|
|
1145
|
-
const path = target.slice(separator + 1);
|
|
1146
|
-
if (path !== "translation" && path !== "rotation" && path !== "scale" && path !== "weights") {
|
|
1147
|
-
return undefined;
|
|
1148
|
-
}
|
|
1149
|
-
return {
|
|
1150
|
-
nodeName: target.slice(0, separator),
|
|
1151
|
-
path
|
|
1152
|
-
};
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
function resolveIKSkin(skins: readonly GLTFSkinAsset[], skinName: string | undefined): GLTFSkinAsset {
|
|
1156
|
-
if (skinName === undefined) {
|
|
1157
|
-
const skin = skins.find((candidate) => candidate.jointNames.length >= 3);
|
|
1158
|
-
if (!skin) {
|
|
1159
|
-
throw new Error("glTF imported skeleton IK requires a skin with at least three joints.");
|
|
1160
|
-
}
|
|
1161
|
-
return skin;
|
|
1162
|
-
}
|
|
1163
|
-
const skin = skins.find((candidate) => candidate.name === skinName);
|
|
1164
|
-
if (!skin) {
|
|
1165
|
-
throw new Error(`glTF imported skeleton IK skin "${skinName}" was not found.`);
|
|
1166
|
-
}
|
|
1167
|
-
if (skin.jointNames.length < 3) {
|
|
1168
|
-
throw new Error(`glTF imported skeleton IK skin "${skinName}" does not contain a two-bone chain.`);
|
|
1169
|
-
}
|
|
1170
|
-
return skin;
|
|
1171
|
-
}
|
|
1172
|
-
|
|
1173
|
-
function firstTwoBoneJointChain(skin: GLTFSkinAsset): readonly [string, string, string] {
|
|
1174
|
-
const root = skin.jointNames[0];
|
|
1175
|
-
const mid = skin.jointNames[1];
|
|
1176
|
-
const end = skin.jointNames[2];
|
|
1177
|
-
if (!root || !mid || !end) {
|
|
1178
|
-
throw new Error(`glTF imported skeleton IK skin "${skin.name}" does not contain a two-bone chain.`);
|
|
1179
|
-
}
|
|
1180
|
-
return [root, mid, end];
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
function worldPosition(node: SceneNode): Vec3 {
|
|
1184
|
-
node.updateWorldTransform();
|
|
1185
|
-
return [node.transform.worldMatrix[12], node.transform.worldMatrix[13], node.transform.worldMatrix[14]];
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
function setWorldPosition(node: SceneNode, position: readonly [number, number, number]): void {
|
|
1189
|
-
const local = node.parent
|
|
1190
|
-
? transformPoint(invertMat4(node.parent.transform.worldMatrix), [position[0], position[1], position[2]])
|
|
1191
|
-
: [position[0], position[1], position[2]] as Vec3;
|
|
1192
|
-
node.transform.setPosition(local[0], local[1], local[2]);
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
function applyTransformValue(node: SceneNode, path: "translation" | "rotation" | "scale", value: AnimationValue, target: string): void {
|
|
1196
|
-
if (path === "translation") {
|
|
1197
|
-
const vector = asVec3(value, target);
|
|
1198
|
-
node.transform.setPosition(vector[0], vector[1], vector[2]);
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1201
|
-
if (path === "rotation") {
|
|
1202
|
-
const quat = asQuat(value, target);
|
|
1203
|
-
node.transform.setRotation(quat[0], quat[1], quat[2], quat[3]);
|
|
1204
|
-
return;
|
|
1205
|
-
}
|
|
1206
|
-
const vector = asVec3(value, target);
|
|
1207
|
-
node.transform.setScale(vector[0], vector[1], vector[2]);
|
|
1208
|
-
}
|
|
1209
|
-
|
|
1210
|
-
function asVec3(value: AnimationValue, target: string): [number, number, number] {
|
|
1211
|
-
if (!Array.isArray(value) || value.length !== 3 || value.some((component) => typeof component !== "number" || !Number.isFinite(component))) {
|
|
1212
|
-
throw new Error(`glTF animation track "${target}" sampled an invalid vec3.`);
|
|
1213
|
-
}
|
|
1214
|
-
return [value[0], value[1], value[2]];
|
|
1215
|
-
}
|
|
1216
|
-
|
|
1217
|
-
function asQuat(value: AnimationValue, target: string): [number, number, number, number] {
|
|
1218
|
-
if (!Array.isArray(value) || value.length !== 4 || value.some((component) => typeof component !== "number" || !Number.isFinite(component))) {
|
|
1219
|
-
throw new Error(`glTF animation track "${target}" sampled an invalid quaternion.`);
|
|
1220
|
-
}
|
|
1221
|
-
return [value[0], value[1], value[2], value[3]];
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
function toVec3Tuple(
|
|
1225
|
-
value: readonly [number, number, number] | { readonly x: number; readonly y: number; readonly z: number },
|
|
1226
|
-
target: string
|
|
1227
|
-
): [number, number, number] {
|
|
1228
|
-
const tuple = Array.isArray(value)
|
|
1229
|
-
? value
|
|
1230
|
-
: [(value as { x: number }).x, (value as { y: number }).y, (value as { z: number }).z];
|
|
1231
|
-
if (tuple.length !== 3 || tuple.some((component) => typeof component !== "number" || !Number.isFinite(component))) {
|
|
1232
|
-
throw new Error(`glTF pose "${target}" requires a finite vec3.`);
|
|
1233
|
-
}
|
|
1234
|
-
return [tuple[0]!, tuple[1]!, tuple[2]!];
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
function toQuatTuple(
|
|
1238
|
-
value: readonly [number, number, number, number] | { readonly x: number; readonly y: number; readonly z: number; readonly w: number },
|
|
1239
|
-
target: string
|
|
1240
|
-
): [number, number, number, number] {
|
|
1241
|
-
const tuple = Array.isArray(value)
|
|
1242
|
-
? value
|
|
1243
|
-
: [(value as { x: number }).x, (value as { y: number }).y, (value as { z: number }).z, (value as { w: number }).w];
|
|
1244
|
-
if (tuple.length !== 4 || tuple.some((component) => typeof component !== "number" || !Number.isFinite(component))) {
|
|
1245
|
-
throw new Error(`glTF pose "${target}" requires a finite quaternion.`);
|
|
1246
|
-
}
|
|
1247
|
-
return [tuple[0]!, tuple[1]!, tuple[2]!, tuple[3]!];
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
function asNumberArray(value: AnimationValue): number[] {
|
|
1251
|
-
if (!Array.isArray(value) || value.some((component) => typeof component !== "number" || !Number.isFinite(component))) {
|
|
1252
|
-
throw new Error("glTF morph-weight animation sampled an invalid number array.");
|
|
1253
|
-
}
|
|
1254
|
-
return [...value];
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
function sampleClipTracks(
|
|
1258
|
-
clip: AnimationClip,
|
|
1259
|
-
time: number,
|
|
1260
|
-
weight: number,
|
|
1261
|
-
additive: boolean
|
|
1262
|
-
): { readonly sampledTargets: ReadonlyMap<string, AnimationValue>; readonly unsupportedTracks: readonly string[] } {
|
|
1263
|
-
const accumulators = new Map<string, TargetAccumulator>();
|
|
1264
|
-
const unsupportedTracks: string[] = [];
|
|
1265
|
-
for (const track of clip.tracks) {
|
|
1266
|
-
if (!parseAnimationTarget(track.target)) {
|
|
1267
|
-
unsupportedTracks.push(track.target);
|
|
1268
|
-
continue;
|
|
1269
|
-
}
|
|
1270
|
-
blendInto(accumulators, track.target, track.valueType, track.sample(time), weight, additive);
|
|
1271
|
-
}
|
|
1272
|
-
const sampledTargets = new Map<string, AnimationValue>();
|
|
1273
|
-
for (const [target, accumulator] of accumulators) {
|
|
1274
|
-
sampledTargets.set(target, finalizeTargetBlend(accumulator));
|
|
1275
|
-
}
|
|
1276
|
-
return { sampledTargets, unsupportedTracks };
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
function blendInto(
|
|
1280
|
-
accumulators: Map<string, TargetAccumulator>,
|
|
1281
|
-
target: string,
|
|
1282
|
-
type: TrackValueType,
|
|
1283
|
-
value: AnimationValue,
|
|
1284
|
-
weight: number,
|
|
1285
|
-
additive: boolean
|
|
1286
|
-
): void {
|
|
1287
|
-
if (weight <= 0) return;
|
|
1288
|
-
const current = accumulators.get(target);
|
|
1289
|
-
if (!current) {
|
|
1290
|
-
const accumulator: TargetAccumulator = { type };
|
|
1291
|
-
if (additive) {
|
|
1292
|
-
accumulator.additive = additiveContribution(type, value, weight);
|
|
1293
|
-
} else {
|
|
1294
|
-
accumulator.base = { value: cloneAnimationValue(value), weight, type };
|
|
1295
|
-
}
|
|
1296
|
-
accumulators.set(target, accumulator);
|
|
1297
|
-
return;
|
|
1298
|
-
}
|
|
1299
|
-
if (current.type !== type) {
|
|
1300
|
-
throw new Error(`Cannot blend ${type} glTF animation track into existing ${current.type} target ${target}.`);
|
|
1301
|
-
}
|
|
1302
|
-
if (additive) {
|
|
1303
|
-
current.additive = current.additive === undefined
|
|
1304
|
-
? additiveContribution(type, value, weight)
|
|
1305
|
-
: combineAdditive(type, current.additive, value, weight);
|
|
1306
|
-
return;
|
|
1307
|
-
}
|
|
1308
|
-
if (!current.base) {
|
|
1309
|
-
current.base = { value: cloneAnimationValue(value), weight, type };
|
|
1310
|
-
return;
|
|
1311
|
-
}
|
|
1312
|
-
blendBase(current.base, type, value, weight);
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
function blendBase(current: WeightedAccumulator, type: TrackValueType, value: AnimationValue, weight: number): void {
|
|
1316
|
-
const total = current.weight + weight;
|
|
1317
|
-
const t = weight / total;
|
|
1318
|
-
if (type === "scalar") {
|
|
1319
|
-
current.value = (current.value as number) + ((value as number) - (current.value as number)) * t;
|
|
1320
|
-
} else if (type === "vector3") {
|
|
1321
|
-
const a = current.value as [number, number, number];
|
|
1322
|
-
const b = value as [number, number, number];
|
|
1323
|
-
current.value = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
1324
|
-
} else if (type === "quaternion") {
|
|
1325
|
-
current.value = slerpQuat(current.value as [number, number, number, number], value as [number, number, number, number], t);
|
|
1326
|
-
} else if (type === "number-array") {
|
|
1327
|
-
const a = current.value as readonly number[];
|
|
1328
|
-
const b = value as readonly number[];
|
|
1329
|
-
if (a.length !== b.length) {
|
|
1330
|
-
throw new Error("Cannot blend glTF morph animation values with different lengths.");
|
|
1331
|
-
}
|
|
1332
|
-
current.value = a.map((component, index) => component + (b[index]! - component) * t);
|
|
1333
|
-
} else {
|
|
1334
|
-
current.value = cloneAnimationValue(value);
|
|
1335
|
-
}
|
|
1336
|
-
current.weight = total;
|
|
1337
|
-
}
|
|
1338
|
-
|
|
1339
|
-
function finalizeTargetBlend(accumulator: TargetAccumulator): AnimationValue {
|
|
1340
|
-
const base = accumulator.base
|
|
1341
|
-
? finalizeBaseBlend(accumulator.base)
|
|
1342
|
-
: accumulator.additive !== undefined
|
|
1343
|
-
? additiveNeutral(accumulator.type, accumulator.additive)
|
|
1344
|
-
: undefined;
|
|
1345
|
-
if (base === undefined) {
|
|
1346
|
-
throw new Error("glTF animation target accumulator has no sampled value.");
|
|
1347
|
-
}
|
|
1348
|
-
return accumulator.additive === undefined ? base : applyAdditive(accumulator.type, base, accumulator.additive);
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
function finalizeBaseBlend(accumulator: WeightedAccumulator): AnimationValue {
|
|
1352
|
-
return accumulator.type === "quaternion"
|
|
1353
|
-
? normalizeQuat(accumulator.value as [number, number, number, number])
|
|
1354
|
-
: cloneAnimationValue(accumulator.value);
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
function additiveContribution(type: TrackValueType, value: AnimationValue, weight: number): AnimationValue {
|
|
1358
|
-
if (type === "scalar") return (value as number) * weight;
|
|
1359
|
-
if (type === "vector3") {
|
|
1360
|
-
const vector = value as [number, number, number];
|
|
1361
|
-
return [vector[0] * weight, vector[1] * weight, vector[2] * weight];
|
|
1362
|
-
}
|
|
1363
|
-
if (type === "number-array") {
|
|
1364
|
-
return (value as readonly number[]).map((component) => component * weight);
|
|
1365
|
-
}
|
|
1366
|
-
if (type === "quaternion") {
|
|
1367
|
-
return slerpQuat([0, 0, 0, 1], normalizeQuat(value as [number, number, number, number]), weight);
|
|
1368
|
-
}
|
|
1369
|
-
throw new Error("Additive glTF animation layers require scalar, vector3, quaternion, or number-array tracks.");
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
function combineAdditive(type: TrackValueType, current: AnimationValue, value: AnimationValue, weight: number): AnimationValue {
|
|
1373
|
-
return applyAdditive(type, current, additiveContribution(type, value, weight));
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
function applyAdditive(type: TrackValueType, base: AnimationValue, delta: AnimationValue): AnimationValue {
|
|
1377
|
-
if (type === "scalar") return (base as number) + (delta as number);
|
|
1378
|
-
if (type === "vector3") {
|
|
1379
|
-
const a = base as [number, number, number];
|
|
1380
|
-
const b = delta as [number, number, number];
|
|
1381
|
-
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
1382
|
-
}
|
|
1383
|
-
if (type === "number-array") {
|
|
1384
|
-
const a = base as readonly number[];
|
|
1385
|
-
const b = delta as readonly number[];
|
|
1386
|
-
if (a.length !== b.length) {
|
|
1387
|
-
throw new Error("Cannot add glTF morph animation values with different lengths.");
|
|
1388
|
-
}
|
|
1389
|
-
return a.map((component, index) => component + b[index]!);
|
|
1390
|
-
}
|
|
1391
|
-
if (type === "quaternion") {
|
|
1392
|
-
return normalizeQuat(multiplyQuat(base as [number, number, number, number], delta as [number, number, number, number]));
|
|
1393
|
-
}
|
|
1394
|
-
throw new Error("Additive glTF animation layers require scalar, vector3, quaternion, or number-array tracks.");
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
function additiveNeutral(type: TrackValueType, value: AnimationValue): AnimationValue {
|
|
1398
|
-
if (type === "scalar") return 0;
|
|
1399
|
-
if (type === "vector3") return [0, 0, 0];
|
|
1400
|
-
if (type === "number-array") return (value as readonly number[]).map(() => 0);
|
|
1401
|
-
if (type === "quaternion") return [0, 0, 0, 1];
|
|
1402
|
-
throw new Error("Additive glTF animation layers require scalar, vector3, quaternion, or number-array tracks.");
|
|
1403
|
-
}
|
|
1404
|
-
|
|
1405
|
-
function cloneAnimationValue(value: AnimationValue): AnimationValue {
|
|
1406
|
-
return Array.isArray(value) ? [...value] : value;
|
|
1407
|
-
}
|
|
1408
|
-
|
|
1409
|
-
function multiplyQuat(a: [number, number, number, number], b: [number, number, number, number]): [number, number, number, number] {
|
|
1410
|
-
return [
|
|
1411
|
-
a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
|
|
1412
|
-
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
|
|
1413
|
-
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
|
|
1414
|
-
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
|
|
1415
|
-
];
|
|
1416
|
-
}
|