evaluator.js
3.74 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
// evaluator.js —— createEvaluator:世界矩阵链式求值。移植自原型 evaluator.js + humanoid-v1.js 的 indexRig。
//
// 职责:吃一个 RigDefinition(来自 @drama/rig),把骨骼按「父先于子」拓扑排序,
// 然后给定一个 AnimationState,逐根 bone 算出世界矩阵(角色局部空间,root 在原点):
// boneWorld = parentWorld × boneLocal, boneLocal = fromTRS(setup+增量)。
// 不碰像素、不碰 slot 贴图——那是 @drama/render-pixi 的事,core 只产出 Map<boneName, Mat2D>。
import { fromTRS, mul, decompose, degToRad, PROP_DEFAULT, matrix } from './matrix.js'
import { AnimationState } from './anim-state.js'
/**
* 把 rig 的 bones 按「父先于子」拓扑排序(保证世界变换可一次遍历算完),
* 并建 name→bone 索引。移植自原型 indexRig。环会在 visit 中被 visited 截断(rig 保证无环)。
* @param {import('./types.js').RigDefinition} rig
* @returns {{boneByName:Map<string,import('./types.js').RigBone>, orderedBones:import('./types.js').RigBone[]}}
*/
export function indexRig(rig) {
/** @type {Map<string, import('./types.js').RigBone>} */
const boneByName = new Map()
for (const b of rig.bones) boneByName.set(b.name, b)
/** @type {import('./types.js').RigBone[]} */
const ordered = []
const visited = new Set()
/** @param {import('./types.js').RigBone|undefined} b */
const visit = (b) => {
if (!b || visited.has(b.name)) return
if (b.parent) visit(boneByName.get(b.parent))
visited.add(b.name)
ordered.push(b)
}
for (const b of rig.bones) visit(b)
return { boneByName, orderedBones: ordered }
}
/**
* 世界变换求值。
* @param {{orderedBones:import('./types.js').RigBone[]}} rigIndex indexRig 产物
* @param {AnimationState} animState
* @param {(Record<string,{drot?:number,dx?:number,dy?:number}>|null)=} overrides
* 可选覆盖,叠加到动画采样之上(姿势编辑 / 微调用)。
* @returns {Map<string, import('./types.js').Mat2D>} 角色局部空间,root 在原点
*/
export function evaluateWorld(rigIndex, animState, overrides = null) {
const offsets = animState.sampleBones()
/** @type {Map<string, import('./types.js').Mat2D>} */
const world = new Map()
for (const bone of rigIndex.orderedBones) {
const off = offsets[bone.name] || PROP_DEFAULT
const ov = overrides && overrides[bone.name]
const rot = degToRad(bone.rotation + off.drot + (ov?.drot || 0))
const x = bone.pos.x + off.dx + (ov?.dx || 0)
const y = bone.pos.y + off.dy + (ov?.dy || 0)
const scale = 1 * off.dscale
const local = fromTRS(x, y, rot, scale)
const parentW = bone.parent ? world.get(bone.parent) : null
world.set(bone.name, parentW ? mul(parentW, local) : local)
}
return world
}
/**
* 求值器工厂。
* @param {import('./types.js').RigDefinition} rig
* @param {import('./types.js').ClipLib=} clipLib clip 库注入(默认应由装配层注入 @drama/rig)。
* @returns {{
* state: () => AnimationState,
* evaluateWorld: (animState:AnimationState, overrides?:(Record<string,Object>|null)) => Map<string,import('./types.js').Mat2D>,
* matrix: typeof matrix,
* indexRig: () => {orderedBones:import('./types.js').RigBone[]}
* }}
*/
export function createEvaluator(rig, clipLib) {
const rigIndex = indexRig(rig)
return {
/** 每角色一个 AnimationState。 */
state() {
if (!clipLib) {
throw new Error('createEvaluator: clipLib 未注入;state() 需要 clip 库(@drama/rig)')
}
return new AnimationState(clipLib)
},
evaluateWorld(animState, overrides = null) {
return evaluateWorld(rigIndex, animState, overrides)
},
matrix,
indexRig() {
return rigIndex
},
}
}
export { decompose }