@nice2dev/game-engine 1.1.9 → 1.2.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/CHANGELOG.md +139 -0
- package/dist/cjs/ai/StateMachine.js +54 -0
- package/dist/cjs/editor/AnimationRetargeting.js +13 -0
- package/dist/cjs/editor/TimelineEditor.js +12 -0
- package/dist/cjs/index.js +8 -0
- package/dist/cjs/native-engine/AnimationSystem.js +840 -0
- package/dist/esm/ai/StateMachine.js +53 -1
- package/dist/esm/editor/AnimationRetargeting.js +13 -1
- package/dist/esm/editor/TimelineEditor.js +12 -1
- package/dist/esm/index.js +4 -3
- package/dist/esm/native-engine/AnimationSystem.js +835 -0
- package/dist/types/index.d.ts +4 -0
- package/package.json +3 -3
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AnimationSystem.ts
|
|
3
|
+
* @description Animation System for Nice2Dev Game Engine
|
|
4
|
+
* PRO-1.11.4: Animation System
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Skeletal animation
|
|
8
|
+
* - Blend trees
|
|
9
|
+
* - Animation state machine
|
|
10
|
+
* - Inverse Kinematics (IK)
|
|
11
|
+
* - Procedural animation
|
|
12
|
+
* - Morph targets
|
|
13
|
+
* - Animation events
|
|
14
|
+
*/
|
|
15
|
+
// ============================================================
|
|
16
|
+
// Animator Controller
|
|
17
|
+
// ============================================================
|
|
18
|
+
class AnimatorController {
|
|
19
|
+
constructor(boneDescriptors) {
|
|
20
|
+
this.clips = new Map();
|
|
21
|
+
this.layers = [];
|
|
22
|
+
this.stateMachine = null;
|
|
23
|
+
this.parameters = new Map();
|
|
24
|
+
this.ikChains = new Map();
|
|
25
|
+
this.morphTargets = null;
|
|
26
|
+
this.eventCallbacks = new Map();
|
|
27
|
+
this.skeleton = new SkeletonImpl(boneDescriptors);
|
|
28
|
+
}
|
|
29
|
+
// ─── Clips ─────────────────────────────────────────────
|
|
30
|
+
addClip(descriptor) {
|
|
31
|
+
const clip = new AnimationClipImpl(descriptor);
|
|
32
|
+
this.clips.set(descriptor.name, clip);
|
|
33
|
+
return clip;
|
|
34
|
+
}
|
|
35
|
+
getClip(name) {
|
|
36
|
+
return this.clips.get(name);
|
|
37
|
+
}
|
|
38
|
+
removeClip(name) {
|
|
39
|
+
this.clips.delete(name);
|
|
40
|
+
}
|
|
41
|
+
// ─── Layers ────────────────────────────────────────────
|
|
42
|
+
addLayer(descriptor) {
|
|
43
|
+
const layer = new AnimationLayerImpl(descriptor, this.skeleton, this.clips, this.parameters);
|
|
44
|
+
this.layers.push(layer);
|
|
45
|
+
return layer;
|
|
46
|
+
}
|
|
47
|
+
getLayer(nameOrIndex) {
|
|
48
|
+
if (typeof nameOrIndex === 'number') {
|
|
49
|
+
return this.layers[nameOrIndex];
|
|
50
|
+
}
|
|
51
|
+
return this.layers.find((l) => l.name === nameOrIndex);
|
|
52
|
+
}
|
|
53
|
+
getLayerCount() {
|
|
54
|
+
return this.layers.length;
|
|
55
|
+
}
|
|
56
|
+
// ─── State Machine ─────────────────────────────────────
|
|
57
|
+
createStateMachine(descriptor) {
|
|
58
|
+
this.stateMachine = new AnimationStateMachine(descriptor, this.clips, this.parameters);
|
|
59
|
+
// Initialize parameters
|
|
60
|
+
if (descriptor.parameters) {
|
|
61
|
+
for (const param of descriptor.parameters) {
|
|
62
|
+
this.parameters.set(param.name, param.defaultValue ?? (param.type === 'bool' || param.type === 'trigger' ? false : 0));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return this.stateMachine;
|
|
66
|
+
}
|
|
67
|
+
getStateMachine() {
|
|
68
|
+
return this.stateMachine;
|
|
69
|
+
}
|
|
70
|
+
// ─── Parameters ────────────────────────────────────────
|
|
71
|
+
setFloat(name, value) {
|
|
72
|
+
this.parameters.set(name, value);
|
|
73
|
+
}
|
|
74
|
+
getFloat(name) {
|
|
75
|
+
return this.parameters.get(name) ?? 0;
|
|
76
|
+
}
|
|
77
|
+
setInt(name, value) {
|
|
78
|
+
this.parameters.set(name, Math.floor(value));
|
|
79
|
+
}
|
|
80
|
+
getInt(name) {
|
|
81
|
+
return Math.floor(this.parameters.get(name)) ?? 0;
|
|
82
|
+
}
|
|
83
|
+
setBool(name, value) {
|
|
84
|
+
this.parameters.set(name, value);
|
|
85
|
+
}
|
|
86
|
+
getBool(name) {
|
|
87
|
+
return this.parameters.get(name) ?? false;
|
|
88
|
+
}
|
|
89
|
+
setTrigger(name) {
|
|
90
|
+
this.parameters.set(name, true);
|
|
91
|
+
}
|
|
92
|
+
resetTrigger(name) {
|
|
93
|
+
this.parameters.set(name, false);
|
|
94
|
+
}
|
|
95
|
+
// ─── IK ────────────────────────────────────────────────
|
|
96
|
+
addIKChain(name, descriptor) {
|
|
97
|
+
const chain = new IKChainImpl(descriptor);
|
|
98
|
+
this.ikChains.set(name, chain);
|
|
99
|
+
return chain;
|
|
100
|
+
}
|
|
101
|
+
getIKChain(name) {
|
|
102
|
+
return this.ikChains.get(name);
|
|
103
|
+
}
|
|
104
|
+
removeIKChain(name) {
|
|
105
|
+
this.ikChains.delete(name);
|
|
106
|
+
}
|
|
107
|
+
// ─── Morph Targets ─────────────────────────────────────
|
|
108
|
+
createMorphTargetManager() {
|
|
109
|
+
this.morphTargets = new MorphTargetManagerImpl();
|
|
110
|
+
return this.morphTargets;
|
|
111
|
+
}
|
|
112
|
+
getMorphTargetManager() {
|
|
113
|
+
return this.morphTargets;
|
|
114
|
+
}
|
|
115
|
+
// ─── Events ────────────────────────────────────────────
|
|
116
|
+
onEvent(eventName, callback) {
|
|
117
|
+
if (!this.eventCallbacks.has(eventName)) {
|
|
118
|
+
this.eventCallbacks.set(eventName, []);
|
|
119
|
+
}
|
|
120
|
+
this.eventCallbacks.get(eventName).push(callback);
|
|
121
|
+
}
|
|
122
|
+
offEvent(eventName, callback) {
|
|
123
|
+
const callbacks = this.eventCallbacks.get(eventName);
|
|
124
|
+
if (callbacks) {
|
|
125
|
+
const idx = callbacks.indexOf(callback);
|
|
126
|
+
if (idx >= 0) {
|
|
127
|
+
callbacks.splice(idx, 1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
dispatchEvent(eventName, data) {
|
|
132
|
+
const callbacks = this.eventCallbacks.get(eventName);
|
|
133
|
+
if (callbacks) {
|
|
134
|
+
for (const cb of callbacks) {
|
|
135
|
+
cb(data);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// ─── Update ────────────────────────────────────────────
|
|
140
|
+
update(deltaTime) {
|
|
141
|
+
// Update state machine
|
|
142
|
+
if (this.stateMachine) {
|
|
143
|
+
this.stateMachine.update(deltaTime);
|
|
144
|
+
// Check for animation events
|
|
145
|
+
const events = this.stateMachine.getTriggeredEvents();
|
|
146
|
+
for (const event of events) {
|
|
147
|
+
this.dispatchEvent(event.name, event.data);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// Update layers
|
|
151
|
+
for (const layer of this.layers) {
|
|
152
|
+
layer.evaluate(deltaTime);
|
|
153
|
+
}
|
|
154
|
+
// Apply state machine to skeleton
|
|
155
|
+
if (this.stateMachine) {
|
|
156
|
+
this.stateMachine.sample(this.skeleton);
|
|
157
|
+
}
|
|
158
|
+
// Apply layers to skeleton
|
|
159
|
+
for (const _layer of this.layers) {
|
|
160
|
+
// Layer application happens in evaluate
|
|
161
|
+
}
|
|
162
|
+
// Compute world transforms
|
|
163
|
+
this.skeleton.computeWorldTransforms();
|
|
164
|
+
// Apply IK
|
|
165
|
+
for (const chain of this.ikChains.values()) {
|
|
166
|
+
if (chain.isEnabled()) {
|
|
167
|
+
chain.solve(this.skeleton);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Reset triggers
|
|
171
|
+
for (const [name, value] of this.parameters.entries()) {
|
|
172
|
+
if (value === true && this.isTrigger(name)) {
|
|
173
|
+
this.parameters.set(name, false);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
isTrigger(_name) {
|
|
178
|
+
// Check if parameter is a trigger type
|
|
179
|
+
return false; // Would check against parameter definitions
|
|
180
|
+
}
|
|
181
|
+
// ─── Output ────────────────────────────────────────────
|
|
182
|
+
getSkeleton() {
|
|
183
|
+
return this.skeleton;
|
|
184
|
+
}
|
|
185
|
+
getJointMatrices() {
|
|
186
|
+
return this.skeleton.getJointMatrices();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// ============================================================
|
|
190
|
+
// Animation State Machine
|
|
191
|
+
// ============================================================
|
|
192
|
+
class AnimationStateMachine {
|
|
193
|
+
constructor(descriptor, clips, parameters) {
|
|
194
|
+
this.states = new Map();
|
|
195
|
+
this.transitions = [];
|
|
196
|
+
this.currentState = null;
|
|
197
|
+
this.activeTransition = null;
|
|
198
|
+
this.previousTime = 0;
|
|
199
|
+
this.triggeredEvents = [];
|
|
200
|
+
this.parameters = parameters;
|
|
201
|
+
// Create states
|
|
202
|
+
for (const stateDesc of descriptor.states) {
|
|
203
|
+
const clip = stateDesc.clip ? (clips.get(stateDesc.clip) ?? null) : null;
|
|
204
|
+
const state = new AnimationStateImpl(stateDesc, clip);
|
|
205
|
+
this.states.set(stateDesc.name, state);
|
|
206
|
+
}
|
|
207
|
+
// Create transitions
|
|
208
|
+
for (const transDesc of descriptor.transitions) {
|
|
209
|
+
const from = this.states.get(transDesc.from);
|
|
210
|
+
const to = this.states.get(transDesc.to);
|
|
211
|
+
if (from && to) {
|
|
212
|
+
this.transitions.push(new AnimationTransitionImpl(from, to, transDesc));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
// Set default state
|
|
216
|
+
if (descriptor.defaultState) {
|
|
217
|
+
this.currentState = this.states.get(descriptor.defaultState) ?? null;
|
|
218
|
+
this.currentState?.enter();
|
|
219
|
+
}
|
|
220
|
+
else if (this.states.size > 0) {
|
|
221
|
+
this.currentState = this.states.values().next().value ?? null;
|
|
222
|
+
this.currentState?.enter();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
getCurrentState() {
|
|
226
|
+
return this.currentState;
|
|
227
|
+
}
|
|
228
|
+
isInTransition() {
|
|
229
|
+
return this.activeTransition !== null;
|
|
230
|
+
}
|
|
231
|
+
getTransitionProgress() {
|
|
232
|
+
return this.activeTransition?.progress ?? 0;
|
|
233
|
+
}
|
|
234
|
+
update(deltaTime) {
|
|
235
|
+
this.triggeredEvents = [];
|
|
236
|
+
// Update active transition
|
|
237
|
+
if (this.activeTransition) {
|
|
238
|
+
this.activeTransition.update(deltaTime);
|
|
239
|
+
if (this.activeTransition.isComplete()) {
|
|
240
|
+
this.currentState = this.activeTransition.to;
|
|
241
|
+
this.activeTransition = null;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// Check for transitions
|
|
246
|
+
if (this.currentState) {
|
|
247
|
+
for (const transition of this.transitions) {
|
|
248
|
+
if (transition.from === this.currentState && transition.check(this.parameters)) {
|
|
249
|
+
// Check exit time
|
|
250
|
+
if (transition.hasExitTime) {
|
|
251
|
+
if (this.currentState.normalizedTime >= transition.exitTime) {
|
|
252
|
+
this.startTransition(transition);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
this.startTransition(transition);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// Update current state
|
|
265
|
+
const prevTime = this.currentState?.time ?? 0;
|
|
266
|
+
this.currentState?.update(deltaTime);
|
|
267
|
+
const currTime = this.currentState?.time ?? 0;
|
|
268
|
+
// Check for events
|
|
269
|
+
if (this.currentState?.clip) {
|
|
270
|
+
const event = this.currentState.clip.getEvent(currTime, prevTime);
|
|
271
|
+
if (event) {
|
|
272
|
+
this.triggeredEvents.push(event);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
startTransition(transition) {
|
|
277
|
+
this.currentState?.exit();
|
|
278
|
+
transition.to.enter();
|
|
279
|
+
transition.to.time = transition.offset * transition.to.clip.duration;
|
|
280
|
+
transition.progress = 0;
|
|
281
|
+
this.activeTransition = transition;
|
|
282
|
+
}
|
|
283
|
+
sample(skeleton) {
|
|
284
|
+
if (this.activeTransition) {
|
|
285
|
+
// Blend between states
|
|
286
|
+
1 - this.activeTransition.progress;
|
|
287
|
+
const toWeight = this.activeTransition.progress;
|
|
288
|
+
// Sample from state
|
|
289
|
+
this.activeTransition.from.sample(skeleton);
|
|
290
|
+
// Would need to blend, simplified here
|
|
291
|
+
if (toWeight > 0.5) {
|
|
292
|
+
this.activeTransition.to.sample(skeleton);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
else if (this.currentState) {
|
|
296
|
+
this.currentState.sample(skeleton);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
getTriggeredEvents() {
|
|
300
|
+
return this.triggeredEvents;
|
|
301
|
+
}
|
|
302
|
+
forceState(stateName) {
|
|
303
|
+
const state = this.states.get(stateName);
|
|
304
|
+
if (state) {
|
|
305
|
+
this.currentState?.exit();
|
|
306
|
+
this.currentState = state;
|
|
307
|
+
this.currentState.enter();
|
|
308
|
+
this.activeTransition = null;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// ============================================================
|
|
313
|
+
// Internal Implementations
|
|
314
|
+
// ============================================================
|
|
315
|
+
class SkeletonImpl {
|
|
316
|
+
constructor(descriptors) {
|
|
317
|
+
this.boneMap = new Map();
|
|
318
|
+
this.bones = descriptors.map((desc, index) => ({
|
|
319
|
+
name: desc.name,
|
|
320
|
+
index,
|
|
321
|
+
parentIndex: desc.parentIndex,
|
|
322
|
+
children: [],
|
|
323
|
+
localPosition: { ...desc.position },
|
|
324
|
+
localRotation: { ...desc.rotation },
|
|
325
|
+
localScale: { ...desc.scale },
|
|
326
|
+
worldPosition: { ...desc.position },
|
|
327
|
+
worldRotation: { ...desc.rotation },
|
|
328
|
+
worldScale: { ...desc.scale },
|
|
329
|
+
inverseBindMatrix: new Float32Array(16),
|
|
330
|
+
}));
|
|
331
|
+
// Build bone map and children
|
|
332
|
+
for (let i = 0; i < this.bones.length; i++) {
|
|
333
|
+
const bone = this.bones[i];
|
|
334
|
+
this.boneMap.set(bone.name, i);
|
|
335
|
+
if (bone.parentIndex >= 0) {
|
|
336
|
+
this.bones[bone.parentIndex].children.push(i);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
this.jointMatrices = new Float32Array(this.bones.length * 16);
|
|
340
|
+
this.computeWorldTransforms();
|
|
341
|
+
}
|
|
342
|
+
getBone(nameOrIndex) {
|
|
343
|
+
if (typeof nameOrIndex === 'number') {
|
|
344
|
+
return this.bones[nameOrIndex];
|
|
345
|
+
}
|
|
346
|
+
const index = this.boneMap.get(nameOrIndex);
|
|
347
|
+
return index !== undefined ? this.bones[index] : undefined;
|
|
348
|
+
}
|
|
349
|
+
getBoneIndex(name) {
|
|
350
|
+
return this.boneMap.get(name) ?? -1;
|
|
351
|
+
}
|
|
352
|
+
getBoneCount() {
|
|
353
|
+
return this.bones.length;
|
|
354
|
+
}
|
|
355
|
+
setLocalPose(boneIndex, position, rotation, scale) {
|
|
356
|
+
const bone = this.bones[boneIndex];
|
|
357
|
+
if (bone) {
|
|
358
|
+
bone.localPosition = { ...position };
|
|
359
|
+
bone.localRotation = { ...rotation };
|
|
360
|
+
bone.localScale = { ...scale };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
getLocalPose(boneIndex) {
|
|
364
|
+
const bone = this.bones[boneIndex];
|
|
365
|
+
return {
|
|
366
|
+
position: { ...bone.localPosition },
|
|
367
|
+
rotation: { ...bone.localRotation },
|
|
368
|
+
scale: { ...bone.localScale },
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
computeWorldTransforms() {
|
|
372
|
+
for (const bone of this.bones) {
|
|
373
|
+
if (bone.parentIndex < 0) {
|
|
374
|
+
// Root bone
|
|
375
|
+
bone.worldPosition = { ...bone.localPosition };
|
|
376
|
+
bone.worldRotation = { ...bone.localRotation };
|
|
377
|
+
bone.worldScale = { ...bone.localScale };
|
|
378
|
+
}
|
|
379
|
+
else {
|
|
380
|
+
const parent = this.bones[bone.parentIndex];
|
|
381
|
+
// Transform local by parent world
|
|
382
|
+
bone.worldPosition = this.transformPoint(bone.localPosition, parent);
|
|
383
|
+
bone.worldRotation = this.multiplyQuaternions(parent.worldRotation, bone.localRotation);
|
|
384
|
+
bone.worldScale = {
|
|
385
|
+
x: parent.worldScale.x * bone.localScale.x,
|
|
386
|
+
y: parent.worldScale.y * bone.localScale.y,
|
|
387
|
+
z: parent.worldScale.z * bone.localScale.z,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
// Update joint matrix
|
|
391
|
+
this.updateJointMatrix(bone);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
transformPoint(point, parent) {
|
|
395
|
+
// Simplified - would use proper quaternion rotation
|
|
396
|
+
return {
|
|
397
|
+
x: parent.worldPosition.x + point.x * parent.worldScale.x,
|
|
398
|
+
y: parent.worldPosition.y + point.y * parent.worldScale.y,
|
|
399
|
+
z: parent.worldPosition.z + point.z * parent.worldScale.z,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
multiplyQuaternions(a, b) {
|
|
403
|
+
return {
|
|
404
|
+
x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
|
|
405
|
+
y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
|
|
406
|
+
z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
|
|
407
|
+
w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
updateJointMatrix(bone) {
|
|
411
|
+
const offset = bone.index * 16;
|
|
412
|
+
// Simplified - would compose TRS matrix
|
|
413
|
+
// Final = WorldMatrix * InverseBindMatrix
|
|
414
|
+
this.jointMatrices[offset] = 1;
|
|
415
|
+
this.jointMatrices[offset + 5] = 1;
|
|
416
|
+
this.jointMatrices[offset + 10] = 1;
|
|
417
|
+
this.jointMatrices[offset + 15] = 1;
|
|
418
|
+
this.jointMatrices[offset + 12] = bone.worldPosition.x;
|
|
419
|
+
this.jointMatrices[offset + 13] = bone.worldPosition.y;
|
|
420
|
+
this.jointMatrices[offset + 14] = bone.worldPosition.z;
|
|
421
|
+
}
|
|
422
|
+
getJointMatrices() {
|
|
423
|
+
return this.jointMatrices;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
class AnimationClipImpl {
|
|
427
|
+
constructor(descriptor) {
|
|
428
|
+
this.name = descriptor.name;
|
|
429
|
+
this.duration = descriptor.duration;
|
|
430
|
+
this.tracks = descriptor.tracks;
|
|
431
|
+
this.events = descriptor.events ?? [];
|
|
432
|
+
}
|
|
433
|
+
sample(time, skeleton) {
|
|
434
|
+
for (const track of this.tracks) {
|
|
435
|
+
const boneIndex = skeleton.getBoneIndex(track.targetBone);
|
|
436
|
+
if (boneIndex < 0) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const value = this.sampleTrack(track, time);
|
|
440
|
+
const pose = skeleton.getLocalPose(boneIndex);
|
|
441
|
+
switch (track.property) {
|
|
442
|
+
case 'position':
|
|
443
|
+
skeleton.setLocalPose(boneIndex, value, pose.rotation, pose.scale);
|
|
444
|
+
break;
|
|
445
|
+
case 'rotation':
|
|
446
|
+
skeleton.setLocalPose(boneIndex, pose.position, value, pose.scale);
|
|
447
|
+
break;
|
|
448
|
+
case 'scale':
|
|
449
|
+
skeleton.setLocalPose(boneIndex, pose.position, pose.rotation, value);
|
|
450
|
+
break;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
sampleTrack(track, time) {
|
|
455
|
+
const keyframes = track.keyframes;
|
|
456
|
+
if (keyframes.length === 0) {
|
|
457
|
+
return track.property === 'rotation' ? { x: 0, y: 0, z: 0, w: 1 } : { x: 0, y: 0, z: 0 };
|
|
458
|
+
}
|
|
459
|
+
if (keyframes.length === 1 || time <= keyframes[0].time) {
|
|
460
|
+
return keyframes[0].value;
|
|
461
|
+
}
|
|
462
|
+
if (time >= keyframes[keyframes.length - 1].time) {
|
|
463
|
+
return keyframes[keyframes.length - 1].value;
|
|
464
|
+
}
|
|
465
|
+
// Find surrounding keyframes
|
|
466
|
+
let i = 0;
|
|
467
|
+
while (i < keyframes.length - 1 && keyframes[i + 1].time < time) {
|
|
468
|
+
i++;
|
|
469
|
+
}
|
|
470
|
+
const k0 = keyframes[i];
|
|
471
|
+
const k1 = keyframes[i + 1];
|
|
472
|
+
const t = (time - k0.time) / (k1.time - k0.time);
|
|
473
|
+
// Interpolate
|
|
474
|
+
if (track.property === 'rotation') {
|
|
475
|
+
return this.slerpQuaternion(k0.value, k1.value, t);
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
return this.lerpVec3(k0.value, k1.value, t);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
lerpVec3(a, b, t) {
|
|
482
|
+
return {
|
|
483
|
+
x: a.x + (b.x - a.x) * t,
|
|
484
|
+
y: a.y + (b.y - a.y) * t,
|
|
485
|
+
z: a.z + (b.z - a.z) * t,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
slerpQuaternion(a, b, t) {
|
|
489
|
+
// Simplified slerp
|
|
490
|
+
let dot = a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w;
|
|
491
|
+
if (dot < 0) {
|
|
492
|
+
b = { x: -b.x, y: -b.y, z: -b.z, w: -b.w };
|
|
493
|
+
dot = -dot;
|
|
494
|
+
}
|
|
495
|
+
if (dot > 0.9995) {
|
|
496
|
+
// Linear interpolation for very close quaternions
|
|
497
|
+
return this.normalizeQuaternion({
|
|
498
|
+
x: a.x + (b.x - a.x) * t,
|
|
499
|
+
y: a.y + (b.y - a.y) * t,
|
|
500
|
+
z: a.z + (b.z - a.z) * t,
|
|
501
|
+
w: a.w + (b.w - a.w) * t,
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
const theta0 = Math.acos(dot);
|
|
505
|
+
const theta = theta0 * t;
|
|
506
|
+
const sinTheta = Math.sin(theta);
|
|
507
|
+
const sinTheta0 = Math.sin(theta0);
|
|
508
|
+
const s0 = Math.cos(theta) - (dot * sinTheta) / sinTheta0;
|
|
509
|
+
const s1 = sinTheta / sinTheta0;
|
|
510
|
+
return {
|
|
511
|
+
x: a.x * s0 + b.x * s1,
|
|
512
|
+
y: a.y * s0 + b.y * s1,
|
|
513
|
+
z: a.z * s0 + b.z * s1,
|
|
514
|
+
w: a.w * s0 + b.w * s1,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
normalizeQuaternion(q) {
|
|
518
|
+
const len = Math.sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w);
|
|
519
|
+
return { x: q.x / len, y: q.y / len, z: q.z / len, w: q.w / len };
|
|
520
|
+
}
|
|
521
|
+
getEvent(time, previousTime) {
|
|
522
|
+
for (const event of this.events) {
|
|
523
|
+
if (event.time > previousTime && event.time <= time) {
|
|
524
|
+
return event;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
class AnimationLayerImpl {
|
|
531
|
+
constructor(descriptor, _skeleton, _clips, _parameters) {
|
|
532
|
+
this._mask = null;
|
|
533
|
+
this._currentState = null;
|
|
534
|
+
this.name = descriptor.name;
|
|
535
|
+
this.blendMode = descriptor.blendMode ?? 'override';
|
|
536
|
+
this._weight = descriptor.weight ?? 1;
|
|
537
|
+
if (descriptor.mask) {
|
|
538
|
+
this._mask = new AvatarMaskImpl(descriptor.mask);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
getWeight() {
|
|
542
|
+
return this._weight;
|
|
543
|
+
}
|
|
544
|
+
setWeight(weight) {
|
|
545
|
+
this._weight = Math.max(0, Math.min(1, weight));
|
|
546
|
+
}
|
|
547
|
+
getBlendMode() {
|
|
548
|
+
return this.blendMode;
|
|
549
|
+
}
|
|
550
|
+
setBlendMode(mode) {
|
|
551
|
+
this.blendMode = mode;
|
|
552
|
+
}
|
|
553
|
+
getMask() {
|
|
554
|
+
return this._mask;
|
|
555
|
+
}
|
|
556
|
+
setMask(mask) {
|
|
557
|
+
this._mask = mask;
|
|
558
|
+
}
|
|
559
|
+
getCurrentState() {
|
|
560
|
+
return this._currentState;
|
|
561
|
+
}
|
|
562
|
+
setCurrentState(state) {
|
|
563
|
+
this._currentState = state;
|
|
564
|
+
}
|
|
565
|
+
evaluate(_deltaTime) {
|
|
566
|
+
// Update and blend animations for this layer
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
class AvatarMaskImpl {
|
|
570
|
+
constructor(bones) {
|
|
571
|
+
this.weights = new Map();
|
|
572
|
+
for (const bone of bones) {
|
|
573
|
+
this.weights.set(bone, 1);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
getBoneWeight(boneName) {
|
|
577
|
+
return this.weights.get(boneName) ?? 0;
|
|
578
|
+
}
|
|
579
|
+
setBoneWeight(boneName, weight) {
|
|
580
|
+
this.weights.set(boneName, weight);
|
|
581
|
+
}
|
|
582
|
+
setAllWeights(weight) {
|
|
583
|
+
for (const bone of this.weights.keys()) {
|
|
584
|
+
this.weights.set(bone, weight);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
class AnimationStateImpl {
|
|
589
|
+
constructor(descriptor, clip) {
|
|
590
|
+
this.blendTree = null;
|
|
591
|
+
this.normalizedTime = 0;
|
|
592
|
+
this.time = 0;
|
|
593
|
+
this.name = descriptor.name;
|
|
594
|
+
this.type = descriptor.type;
|
|
595
|
+
this.clip = clip;
|
|
596
|
+
this.speed = descriptor.speed ?? 1;
|
|
597
|
+
this.wrapMode = descriptor.wrapMode ?? 'loop';
|
|
598
|
+
}
|
|
599
|
+
enter() {
|
|
600
|
+
this.time = 0;
|
|
601
|
+
this.normalizedTime = 0;
|
|
602
|
+
}
|
|
603
|
+
exit() {
|
|
604
|
+
// Cleanup
|
|
605
|
+
}
|
|
606
|
+
update(deltaTime) {
|
|
607
|
+
this.time += deltaTime * this.speed;
|
|
608
|
+
if (this.clip) {
|
|
609
|
+
const duration = this.clip.duration;
|
|
610
|
+
switch (this.wrapMode) {
|
|
611
|
+
case 'once':
|
|
612
|
+
if (this.time > duration) {
|
|
613
|
+
this.time = duration;
|
|
614
|
+
}
|
|
615
|
+
break;
|
|
616
|
+
case 'loop':
|
|
617
|
+
while (this.time > duration) {
|
|
618
|
+
this.time -= duration;
|
|
619
|
+
}
|
|
620
|
+
break;
|
|
621
|
+
case 'pingpong':
|
|
622
|
+
// Would implement ping-pong
|
|
623
|
+
break;
|
|
624
|
+
case 'clamp':
|
|
625
|
+
this.time = Math.max(0, Math.min(this.time, duration));
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
this.normalizedTime = duration > 0 ? this.time / duration : 0;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
sample(skeleton) {
|
|
632
|
+
if (this.clip) {
|
|
633
|
+
this.clip.sample(this.time, skeleton);
|
|
634
|
+
}
|
|
635
|
+
else if (this.blendTree) {
|
|
636
|
+
this.blendTree.sample(skeleton);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
class AnimationTransitionImpl {
|
|
641
|
+
constructor(from, to, descriptor) {
|
|
642
|
+
this.progress = 0;
|
|
643
|
+
this.from = from;
|
|
644
|
+
this.to = to;
|
|
645
|
+
this.duration = descriptor.duration ?? 0.25;
|
|
646
|
+
this.offset = descriptor.offset ?? 0;
|
|
647
|
+
this.conditions = descriptor.conditions ?? [];
|
|
648
|
+
this.hasExitTime = descriptor.hasExitTime ?? false;
|
|
649
|
+
this.exitTime = descriptor.exitTime ?? 1;
|
|
650
|
+
}
|
|
651
|
+
check(parameters) {
|
|
652
|
+
if (this.conditions.length === 0) {
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
for (const condition of this.conditions) {
|
|
656
|
+
const value = parameters.get(condition.parameter);
|
|
657
|
+
if (value === undefined) {
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
660
|
+
let result = false;
|
|
661
|
+
switch (condition.mode) {
|
|
662
|
+
case 'equals':
|
|
663
|
+
result = value === condition.value;
|
|
664
|
+
break;
|
|
665
|
+
case 'not-equals':
|
|
666
|
+
result = value !== condition.value;
|
|
667
|
+
break;
|
|
668
|
+
case 'greater':
|
|
669
|
+
result = value > condition.value;
|
|
670
|
+
break;
|
|
671
|
+
case 'less':
|
|
672
|
+
result = value < condition.value;
|
|
673
|
+
break;
|
|
674
|
+
case 'if':
|
|
675
|
+
result = value === true;
|
|
676
|
+
break;
|
|
677
|
+
case 'if-not':
|
|
678
|
+
result = value === false;
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
if (!result) {
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return true;
|
|
686
|
+
}
|
|
687
|
+
update(deltaTime) {
|
|
688
|
+
if (this.duration > 0) {
|
|
689
|
+
this.progress += deltaTime / this.duration;
|
|
690
|
+
this.progress = Math.min(1, this.progress);
|
|
691
|
+
}
|
|
692
|
+
else {
|
|
693
|
+
this.progress = 1;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
isComplete() {
|
|
697
|
+
return this.progress >= 1;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
class IKChainImpl {
|
|
701
|
+
constructor(descriptor) {
|
|
702
|
+
this._target = { x: 0, y: 0, z: 0 };
|
|
703
|
+
this._poleTarget = null;
|
|
704
|
+
this._enabled = true;
|
|
705
|
+
this.type = descriptor.type;
|
|
706
|
+
this.rootBone = descriptor.rootBone;
|
|
707
|
+
this.tipBone = descriptor.tipBone;
|
|
708
|
+
this._weight = descriptor.weight ?? 1;
|
|
709
|
+
this._iterations = descriptor.iterations ?? 10;
|
|
710
|
+
this._tolerance = descriptor.tolerance ?? 0.001;
|
|
711
|
+
this._poleTarget = descriptor.poleTarget ?? null;
|
|
712
|
+
}
|
|
713
|
+
getTarget() {
|
|
714
|
+
return { ...this._target };
|
|
715
|
+
}
|
|
716
|
+
setTarget(target) {
|
|
717
|
+
this._target = { ...target };
|
|
718
|
+
}
|
|
719
|
+
getPoleTarget() {
|
|
720
|
+
return this._poleTarget ? { ...this._poleTarget } : null;
|
|
721
|
+
}
|
|
722
|
+
setPoleTarget(target) {
|
|
723
|
+
this._poleTarget = target ? { ...target } : null;
|
|
724
|
+
}
|
|
725
|
+
getWeight() {
|
|
726
|
+
return this._weight;
|
|
727
|
+
}
|
|
728
|
+
setWeight(weight) {
|
|
729
|
+
this._weight = Math.max(0, Math.min(1, weight));
|
|
730
|
+
}
|
|
731
|
+
isEnabled() {
|
|
732
|
+
return this._enabled;
|
|
733
|
+
}
|
|
734
|
+
setEnabled(enabled) {
|
|
735
|
+
this._enabled = enabled;
|
|
736
|
+
}
|
|
737
|
+
solve(skeleton) {
|
|
738
|
+
if (!this._enabled || this._weight <= 0) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
switch (this.type) {
|
|
742
|
+
case 'two-bone':
|
|
743
|
+
this.solveTwoBone(skeleton);
|
|
744
|
+
break;
|
|
745
|
+
case 'fabrik':
|
|
746
|
+
this.solveFABRIK(skeleton);
|
|
747
|
+
break;
|
|
748
|
+
case 'ccd':
|
|
749
|
+
this.solveCCD(skeleton);
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
solveTwoBone(skeleton) {
|
|
754
|
+
// Two-bone analytical IK (common for arms/legs)
|
|
755
|
+
const root = skeleton.getBone(this.rootBone);
|
|
756
|
+
const tip = skeleton.getBone(this.tipBone);
|
|
757
|
+
if (!root || !tip) {
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
// Would implement two-bone IK solver
|
|
761
|
+
}
|
|
762
|
+
solveFABRIK(_skeleton) {
|
|
763
|
+
// Forward And Backward Reaching Inverse Kinematics
|
|
764
|
+
for (let i = 0; i < this._iterations; i++) {
|
|
765
|
+
// Forward pass
|
|
766
|
+
// Backward pass
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
solveCCD(_skeleton) {
|
|
770
|
+
// Cyclic Coordinate Descent
|
|
771
|
+
for (let i = 0; i < this._iterations; i++) {
|
|
772
|
+
// Iterate through chain from tip to root
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
class MorphTargetManagerImpl {
|
|
777
|
+
constructor() {
|
|
778
|
+
this.targets = new Map();
|
|
779
|
+
}
|
|
780
|
+
getMorphTarget(name) {
|
|
781
|
+
return this.targets.get(name);
|
|
782
|
+
}
|
|
783
|
+
getMorphTargets() {
|
|
784
|
+
return Array.from(this.targets.values());
|
|
785
|
+
}
|
|
786
|
+
addMorphTarget(descriptor) {
|
|
787
|
+
const target = new MorphTargetImpl(descriptor);
|
|
788
|
+
this.targets.set(descriptor.name, target);
|
|
789
|
+
return target;
|
|
790
|
+
}
|
|
791
|
+
removeMorphTarget(name) {
|
|
792
|
+
this.targets.delete(name);
|
|
793
|
+
}
|
|
794
|
+
getWeight(name) {
|
|
795
|
+
return this.targets.get(name)?.weight ?? 0;
|
|
796
|
+
}
|
|
797
|
+
setWeight(name, weight) {
|
|
798
|
+
const target = this.targets.get(name);
|
|
799
|
+
if (target) {
|
|
800
|
+
target.weight = Math.max(0, Math.min(1, weight));
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
getBlendedVertices(baseVertices) {
|
|
804
|
+
const result = new Float32Array(baseVertices);
|
|
805
|
+
for (const target of this.targets.values()) {
|
|
806
|
+
if (target.weight <= 0) {
|
|
807
|
+
continue;
|
|
808
|
+
}
|
|
809
|
+
for (let i = 0; i < result.length; i++) {
|
|
810
|
+
result[i] += target.vertices[i] * target.weight;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return result;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
class MorphTargetImpl {
|
|
817
|
+
constructor(descriptor) {
|
|
818
|
+
this.weight = 0;
|
|
819
|
+
this.name = descriptor.name;
|
|
820
|
+
this.vertices = descriptor.vertices;
|
|
821
|
+
this.normals = descriptor.normals ?? null;
|
|
822
|
+
this.tangents = descriptor.tangents ?? null;
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
// ============================================================
|
|
826
|
+
// Factory Functions
|
|
827
|
+
// ============================================================
|
|
828
|
+
function createAnimatorController(bones) {
|
|
829
|
+
return new AnimatorController(bones);
|
|
830
|
+
}
|
|
831
|
+
function createAnimationClip(descriptor) {
|
|
832
|
+
return new AnimationClipImpl(descriptor);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export { AnimationStateMachine, AnimatorController, createAnimationClip, createAnimatorController };
|