anim-state.js
4.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// anim-state.js —— 每角色的多轨道动画状态机。移植自原型 evaluator.js 的 AnimationState。
//
// 核心算法(「走路 + 说话 + 指人」并行的根):
// · 每角色维护 base / lower_body / arm_l / arm_r / head 五条轨道,各自有当前 clip
// 与播放进度;
// · 轨道切换做交叉淡入(mix)——在过渡窗口内把旧 clip 与新 clip 按权重混合;
// · oneshot 播完自动回落 fallback;
// · sampleBones() 逐轨道采样,再用「后写覆盖」叠成每根 bone 的局部增量——
// 每条轨道**只写它拥有的 bone 子集**(TRACK_BONES),所以三条轨道互不串扰。
//
// 依赖注入:clip 库(getClip / TRACKS / TRACK_BONES / TRACK_DEFAULTS)从构造参数传入,
// 默认取 @drama/rig。这样 core 既不在包内顶层 import 死数据、又能在 Node 喂假 clip 库
// 做确定性单测。
import { sampleClip, blendOffset } from './sampling.js'
import { PROP_DEFAULT } from './matrix.js'
/** @typedef {import('./types.js').ClipLib} ClipLib */
export class AnimationState {
/**
* @param {import('./types.js').ClipLib} clipLib 注入的 clip 库(@drama/rig 或测试假库)。
*/
constructor(clipLib) {
/** @type {import('./types.js').ClipLib} */
this.lib = clipLib
/** @type {Record<string, {clipId:string,elapsed:number,prevClipId:(string|null),prevElapsed:number,mixDuration:number,mixElapsed:number,fallbackId:(string|null)}>} */
this.tracks = {}
for (const name of clipLib.TRACKS) {
this.tracks[name] = {
clipId: clipLib.TRACK_DEFAULTS[name],
elapsed: 0,
// 交叉淡入:从 prevClipId 过渡到 clipId
prevClipId: null,
prevElapsed: 0,
mixDuration: 0,
mixElapsed: 0,
// oneshot 播完后回落的目标
fallbackId: null,
}
}
}
/**
* 切换某轨道动作。
* @param {string} trackName
* @param {string} clipId
* @param {{mix?:number,fallback?:(string|null)}=} opts mix=交叉淡入时长(ms),fallback=oneshot 回落目标。
*/
setAnimation(trackName, clipId, { mix = 160, fallback = null } = {}) {
const tr = this.tracks[trackName]
if (!tr || !this.lib.getClip(clipId)) return
if (tr.clipId === clipId && !tr.prevClipId) return // 已在播且无过渡,不打断
tr.prevClipId = tr.clipId
tr.prevElapsed = tr.elapsed
tr.mixDuration = mix
tr.mixElapsed = 0
tr.clipId = clipId
tr.elapsed = 0
tr.fallbackId = fallback
}
/** 复位到各轨道默认 clip(打断 / 退场时用)。 */
reset() {
for (const name of this.lib.TRACKS) {
this.setAnimation(name, this.lib.TRACK_DEFAULTS[name], { mix: 120 })
}
}
/**
* 推进时间,处理 mix 结束与 oneshot 回落。
* @param {number} dtMs
*/
update(dtMs) {
for (const name of this.lib.TRACKS) {
const tr = this.tracks[name]
tr.elapsed += dtMs
if (tr.prevClipId) {
tr.prevElapsed += dtMs
tr.mixElapsed += dtMs
if (tr.mixElapsed >= tr.mixDuration) {
tr.prevClipId = null // 过渡结束
}
}
// oneshot 播完 → 回落
const clip = this.lib.getClip(tr.clipId)
if (clip && clip.kind === 'oneshot' && tr.elapsed >= clip.duration && tr.fallbackId) {
const fb = tr.fallbackId
tr.fallbackId = null
this.setAnimation(name, fb, { mix: 160 })
}
}
}
/**
* 采样所有轨道 → 每根 bone 的最终局部增量(后写覆盖叠加结果)。
* @returns {Record<string, import('./types.js').BoneOffset>}
*/
sampleBones() {
/** @type {Record<string, import('./types.js').BoneOffset>} */
const offsets = {}
for (const name of this.lib.TRACKS) {
const tr = this.tracks[name]
const owned = this.lib.TRACK_BONES[name]
const clip = this.lib.getClip(tr.clipId)
if (!clip) continue
let sampled = sampleClip(clip, tr.elapsed)
// 交叉淡入:与上一个 clip 混合
if (tr.prevClipId) {
const prevClip = this.lib.getClip(tr.prevClipId)
if (prevClip) {
const prevSampled = sampleClip(prevClip, tr.prevElapsed)
const w = tr.mixDuration > 0 ? Math.min(tr.mixElapsed / tr.mixDuration, 1) : 1
/** @type {Record<string, import('./types.js').BoneOffset>} */
const merged = {}
const bones = new Set([...Object.keys(sampled), ...Object.keys(prevSampled)])
for (const b of bones) {
const cur = sampled[b] || { ...PROP_DEFAULT }
const prv = prevSampled[b] || { ...PROP_DEFAULT }
merged[b] = blendOffset(prv, cur, w)
}
sampled = merged
}
}
// 后写覆盖:只写该轨道拥有的 bone
for (const boneName of owned) {
if (sampled[boneName]) offsets[boneName] = sampled[boneName]
}
}
return offsets
}
}