@driftengine/animation 3.61.0
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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +74 -0
- package/dist/blend.d.ts +39 -0
- package/dist/blend.js +158 -0
- package/dist/blendTree.d.ts +115 -0
- package/dist/blendTree.js +171 -0
- package/dist/clip.d.ts +50 -0
- package/dist/clip.js +171 -0
- package/dist/ik.d.ts +29 -0
- package/dist/ik.js +329 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +12 -0
- package/dist/pose.d.ts +39 -0
- package/dist/pose.js +49 -0
- package/dist/retarget.d.ts +40 -0
- package/dist/retarget.js +72 -0
- package/dist/rigid.d.ts +56 -0
- package/dist/rigid.js +92 -0
- package/dist/rootMotion.d.ts +74 -0
- package/dist/rootMotion.js +182 -0
- package/dist/skeleton.d.ts +81 -0
- package/dist/skeleton.js +187 -0
- package/dist/spring.d.ts +108 -0
- package/dist/spring.js +386 -0
- package/dist/stateMachine.d.ts +75 -0
- package/dist/stateMachine.js +142 -0
- package/package.json +61 -0
- package/src/blend.ts +191 -0
- package/src/blendTree.ts +253 -0
- package/src/clip.ts +223 -0
- package/src/ik.ts +401 -0
- package/src/index.ts +38 -0
- package/src/pose.ts +60 -0
- package/src/retarget.ts +105 -0
- package/src/rigid.ts +99 -0
- package/src/rootMotion.ts +253 -0
- package/src/skeleton.ts +231 -0
- package/src/spring.ts +520 -0
- package/src/stateMachine.ts +181 -0
package/src/skeleton.ts
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { MAX_JOINTS } from '@driftengine/core';
|
|
2
|
+
import type { Joint } from '@driftengine/drft';
|
|
3
|
+
import { mat4, quat, vec3 } from 'gl-matrix';
|
|
4
|
+
|
|
5
|
+
import type { Pose } from './pose.ts';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A joint hierarchy and the skinning palette it resolves a pose into.
|
|
9
|
+
*
|
|
10
|
+
* **A joint is not a `SceneNode`, and that is deliberate.** Core has a transform hierarchy with a
|
|
11
|
+
* parent, dirty tracking and a derived world matrix, and reusing it here is the obvious move. A
|
|
12
|
+
* rig is sixty to ninety joints and a scene holds several characters, so a class instance per node
|
|
13
|
+
* holding its own matrices is the allocation the performance rules forbid — and the palette a
|
|
14
|
+
* shader reads has to be contiguous anyway, which an object graph is not. A *character* is still a
|
|
15
|
+
* `SceneNode`; its skeleton hangs off one, and `rigid.ts` is the seam between them.
|
|
16
|
+
*
|
|
17
|
+
* What it costs is that a joint cannot be reparented or addressed the way a scene node can. What
|
|
18
|
+
* would make it wrong is a consumer needing to attach arbitrary scene content to a joint — a sword
|
|
19
|
+
* in a hand — which is answered by reading the joint's world matrix out rather than by making the
|
|
20
|
+
* joint a node.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/* `Joint` is the format package's, for the reason `clip.ts` gives about the `ANIM` chunk. */
|
|
24
|
+
export type { Joint } from '@driftengine/drft';
|
|
25
|
+
|
|
26
|
+
export class Skeleton {
|
|
27
|
+
readonly joints: readonly Joint[];
|
|
28
|
+
readonly jointCount: number;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sixteen floats a joint, column-major, claimed once and written in place.
|
|
32
|
+
*
|
|
33
|
+
* This is a *skinning* palette rather than a set of world matrices: each entry is the joint's
|
|
34
|
+
* world transform times its inverse bind, so it takes a vertex from model space to where the
|
|
35
|
+
* joint has moved it. At the bind pose every entry is identity, whatever the bind pose is, which
|
|
36
|
+
* is the property `skeleton.test.ts` pins — a reversed multiplication order does not shift a
|
|
37
|
+
* mesh slightly, it explodes it.
|
|
38
|
+
*/
|
|
39
|
+
readonly palette: Float32Array;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Each joint's world matrix, sixteen floats a joint, resolved on the way to the palette.
|
|
43
|
+
*
|
|
44
|
+
* **Public because a palette entry is not a world transform.** An entry is the world matrix
|
|
45
|
+
* times the joint's inverse bind, which is what a shader needs and is useless to anything asking
|
|
46
|
+
* *where a joint is* — attaching a sword to a hand, or an IK solver reading a chain. Those want
|
|
47
|
+
* this. Valid after `applyPose` and meaningless before it.
|
|
48
|
+
*/
|
|
49
|
+
readonly world: Float32Array;
|
|
50
|
+
|
|
51
|
+
private readonly inverseBind: Float32Array;
|
|
52
|
+
|
|
53
|
+
/*
|
|
54
|
+
* One 16-float view per joint into each of the three arrays, built once at construction.
|
|
55
|
+
*
|
|
56
|
+
* `gl-matrix` writes through whatever it is handed, so a view lets `applyPose` multiply straight
|
|
57
|
+
* into the palette with no copy at all. The views exist because `subarray` **is an allocation** —
|
|
58
|
+
* a new typed-array object every call — and `applyPose` runs per character per frame, which is
|
|
59
|
+
* exactly where §4's rule about typed-array views bites. Built here, they cost one object per
|
|
60
|
+
* joint for the skeleton's life and nothing per frame.
|
|
61
|
+
*
|
|
62
|
+
* What it costs is three arrays of small objects held for the skeleton's life. What would make it
|
|
63
|
+
* wrong is a joint count large enough for that to matter, which the 512-joint cap rules out.
|
|
64
|
+
*/
|
|
65
|
+
private readonly worldViews: Float32Array[] = [];
|
|
66
|
+
private readonly paletteViews: Float32Array[] = [];
|
|
67
|
+
private readonly bindViews: Float32Array[] = [];
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param joints Sorted **parents-first**. Refused otherwise.
|
|
71
|
+
* @param inverseBind Sixteen floats a joint, column-major, in the same order as `joints`.
|
|
72
|
+
*/
|
|
73
|
+
constructor(joints: readonly Joint[], inverseBind: Float32Array) {
|
|
74
|
+
/*
|
|
75
|
+
* Parents before children, checked once here rather than sorted here.
|
|
76
|
+
*
|
|
77
|
+
* `applyPose` walks the joints in index order and reads each joint's parent matrix as it goes,
|
|
78
|
+
* which is only correct on a sorted hierarchy — and a rig that is nearly sorted produces a
|
|
79
|
+
* skeleton that is wrong in one limb, which reads as a bad animation rather than as a data
|
|
80
|
+
* error. Sorting is the importer's job because it is the layer that can also remap every index
|
|
81
|
+
* that names a joint; sorting here would leave a mesh's joint attribute pointing at the old
|
|
82
|
+
* order. See `gltfSkin.ts`.
|
|
83
|
+
*/
|
|
84
|
+
for (let j = 0; j < joints.length; j++) {
|
|
85
|
+
const parent = (joints[j] as Joint).parent;
|
|
86
|
+
if (parent >= j) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`Skeleton: joint ${j} ("${(joints[j] as Joint).name}") names parent ${parent}, which is ` +
|
|
89
|
+
`not before it. Joints must be sorted parents-first, because the palette is resolved ` +
|
|
90
|
+
`in index order and a parent read before it is written is a limb in the wrong place.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
if (parent < -1) {
|
|
94
|
+
throw new Error(`Skeleton: joint ${j} names parent ${parent}; -1 means "no parent"`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/*
|
|
99
|
+
* The cap is a fact about the palette *texture* — 2048 is WebGL2's guaranteed width and four
|
|
100
|
+
* texels carry a matrix — so it is defined in core beside the texture and imported here rather
|
|
101
|
+
* than restated. Two numbers for one decision is the shape that drifts, and this one would
|
|
102
|
+
* drift silently: a rig between the two would build here and read past the end of a texture row
|
|
103
|
+
* on the GPU, which draws limbs from whatever memory follows rather than failing.
|
|
104
|
+
*/
|
|
105
|
+
if (joints.length > MAX_JOINTS) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Skeleton: ${joints.length} joints exceeds the ${MAX_JOINTS} a palette texture holds. ` +
|
|
108
|
+
`That is WebGL2's guaranteed texture width rather than this machine's limit.`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (inverseBind.length !== joints.length * 16) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Skeleton: the inverse bind array has ${inverseBind.length} floats for ${joints.length} ` +
|
|
115
|
+
`joints; expected ${joints.length * 16} (16 per joint, column-major).`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.joints = joints;
|
|
120
|
+
this.jointCount = joints.length;
|
|
121
|
+
this.inverseBind = inverseBind;
|
|
122
|
+
this.palette = new Float32Array(joints.length * 16);
|
|
123
|
+
this.world = new Float32Array(joints.length * 16);
|
|
124
|
+
|
|
125
|
+
for (let j = 0; j < joints.length; j++) {
|
|
126
|
+
const at = j * 16;
|
|
127
|
+
this.worldViews.push(this.world.subarray(at, at + 16));
|
|
128
|
+
this.paletteViews.push(this.palette.subarray(at, at + 16));
|
|
129
|
+
this.bindViews.push(inverseBind.subarray(at, at + 16));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Resolve a pose into the palette. Allocates nothing.
|
|
135
|
+
*
|
|
136
|
+
* One pass in index order: compose the joint's local matrix from its TRS, multiply by its
|
|
137
|
+
* parent's already-written world matrix, then by its inverse bind into the palette. The
|
|
138
|
+
* parents-first ordering the constructor checks is what makes the single pass correct.
|
|
139
|
+
*/
|
|
140
|
+
/**
|
|
141
|
+
* Resolve the hierarchy from local matrices rather than from TRS.
|
|
142
|
+
*
|
|
143
|
+
* **For a rig whose poses are authored as matrices.** `applyPose` is the right door for
|
|
144
|
+
* anything that interpolates — a clip, a blend tree, a state machine — because a quaternion is
|
|
145
|
+
* what you can blend and Euler angles are not. But a rig computed fresh every frame from
|
|
146
|
+
* gameplay state has no interpolation to do, and forcing it through TRS means decomposing
|
|
147
|
+
* matrices it just composed: a square root and a branch per joint, and a chance to get the
|
|
148
|
+
* rotation order wrong that no test of the *engine* can catch.
|
|
149
|
+
*
|
|
150
|
+
* The two writers agree, and `skeleton.test.ts` pins that against `applyPose` rather than
|
|
151
|
+
* asserting it — a caller choosing between them on convenience must not be choosing between two
|
|
152
|
+
* answers.
|
|
153
|
+
*
|
|
154
|
+
* **What it costs** is a second way in, which is a real cost: a reader now has to know both
|
|
155
|
+
* exist. **What would make it wrong** is a caller reaching for this to avoid learning
|
|
156
|
+
* quaternions and then wanting to blend — at which point they need `applyPose` and have built
|
|
157
|
+
* their rig in the one representation that cannot get there.
|
|
158
|
+
*
|
|
159
|
+
* @param locals Sixteen floats a joint, column-major, in joint order. Each is the joint's
|
|
160
|
+
* transform **relative to its parent**, not its world transform.
|
|
161
|
+
*/
|
|
162
|
+
applyLocalMatrices(locals: Float32Array): void {
|
|
163
|
+
if (locals.length !== this.jointCount * 16) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`Skeleton: applyLocalMatrices wants sixteen floats a joint — ` +
|
|
166
|
+
`${this.jointCount * 16} for ${this.jointCount} joints, got ${locals.length}. ` +
|
|
167
|
+
`A short array leaves the tail joints holding whatever the last frame wrote, which ` +
|
|
168
|
+
`animates as one limb frozen rather than as an error.`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
for (let j = 0; j < this.jointCount; j++) {
|
|
172
|
+
const at = j * 16;
|
|
173
|
+
/* Copied element by element rather than with `subarray`, because a subarray is a new
|
|
174
|
+
typed-array object every call — the same allocation the views above exist to avoid,
|
|
175
|
+
and this runs per character per frame beside `applyPose`. */
|
|
176
|
+
for (let i = 0; i < 16; i++) SCRATCH_LOCAL[i] = locals[at + i] as number;
|
|
177
|
+
|
|
178
|
+
const parent = (this.joints[j] as Joint).parent;
|
|
179
|
+
const world = this.worldViews[j] as Float32Array;
|
|
180
|
+
if (parent < 0) {
|
|
181
|
+
world.set(SCRATCH_LOCAL);
|
|
182
|
+
} else {
|
|
183
|
+
mat4.multiply(world, this.worldViews[parent] as Float32Array, SCRATCH_LOCAL);
|
|
184
|
+
}
|
|
185
|
+
mat4.multiply(this.paletteViews[j] as Float32Array, world, this.bindViews[j] as Float32Array);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
applyPose(pose: Pose): void {
|
|
190
|
+
for (let j = 0; j < this.jointCount; j++) {
|
|
191
|
+
SCRATCH_T[0] = pose.translation[j * 3] as number;
|
|
192
|
+
SCRATCH_T[1] = pose.translation[j * 3 + 1] as number;
|
|
193
|
+
SCRATCH_T[2] = pose.translation[j * 3 + 2] as number;
|
|
194
|
+
SCRATCH_R[0] = pose.rotation[j * 4] as number;
|
|
195
|
+
SCRATCH_R[1] = pose.rotation[j * 4 + 1] as number;
|
|
196
|
+
SCRATCH_R[2] = pose.rotation[j * 4 + 2] as number;
|
|
197
|
+
SCRATCH_R[3] = pose.rotation[j * 4 + 3] as number;
|
|
198
|
+
SCRATCH_S[0] = pose.scale[j * 3] as number;
|
|
199
|
+
SCRATCH_S[1] = pose.scale[j * 3 + 1] as number;
|
|
200
|
+
SCRATCH_S[2] = pose.scale[j * 3 + 2] as number;
|
|
201
|
+
|
|
202
|
+
mat4.fromRotationTranslationScale(SCRATCH_LOCAL, SCRATCH_R, SCRATCH_T, SCRATCH_S);
|
|
203
|
+
|
|
204
|
+
const parent = (this.joints[j] as Joint).parent;
|
|
205
|
+
const world = this.worldViews[j] as Float32Array;
|
|
206
|
+
if (parent < 0) {
|
|
207
|
+
world.set(SCRATCH_LOCAL);
|
|
208
|
+
} else {
|
|
209
|
+
mat4.multiply(world, this.worldViews[parent] as Float32Array, SCRATCH_LOCAL);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
mat4.multiply(this.paletteViews[j] as Float32Array, world, this.bindViews[j] as Float32Array);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/*
|
|
218
|
+
* Module-scope scratch, claimed once for the process rather than per skeleton or per call.
|
|
219
|
+
*
|
|
220
|
+
* `applyPose` runs per character per frame, so anything allocated inside it is a garbage collector
|
|
221
|
+
* in the frame loop. Module scope rather than instance fields because these hold nothing between
|
|
222
|
+
* calls and one set serves every skeleton — the engine is single-threaded on this path, and a
|
|
223
|
+
* worker gets its own module instance.
|
|
224
|
+
*
|
|
225
|
+
* What would make it wrong is `applyPose` ever being re-entered, which would need it to call back
|
|
226
|
+
* into caller code; it calls only `gl-matrix`.
|
|
227
|
+
*/
|
|
228
|
+
const SCRATCH_T = vec3.create();
|
|
229
|
+
const SCRATCH_S = vec3.create();
|
|
230
|
+
const SCRATCH_R = quat.create();
|
|
231
|
+
const SCRATCH_LOCAL = mat4.create();
|