aada 0.1.1

纯 Rust 的 2D 骨骼动画引擎,兼容 Spine 4.x 导出格式(JSON + atlas)。求解到分层 Mesh 与各 Mesh 渲染设置为止,之后交给渲染器。
Documentation
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
//! 运行时网格数据与蒙皮变形。
//!
//! [`MeshData`] 保存顶点在影响骨骼空间的局部坐标(按 influence 逐顶点展开)、骨骼绑定与权重。
//! [`MeshData::deform`] 把局部顶点按骨骼世界变换加权变换到模型空间;`deform_with_deform`
//! 额外叠加动画 deform(加权 = 按 influence 的 delta,非加权 = 绝对位置)。

use serde::{Deserialize, Serialize};

use crate::{
    custom_index::NameMap,
    data::runtime::{skeleton::BoneIdx, slot::SlotIdx},
    new_index,
};
#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct MeshMetadata {
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub slot: Option<String>,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub skin: Option<String>,
}
new_index!(MeshIdx);

#[derive(Debug, Clone)]
pub struct MeshData {
    pub meta: MeshMetadata,

    /// 纹理名称
    /// 一般对应于实际纹理中的一个子区域,会有另外的一个文件进行存储
    pub texture_name: String,
    /// 子纹理 UV 坐标(用于 UV Offset 动画的基础坐标)
    /// 格式: [u0, v0, u1, v1, ...]
    /// 运行时可通过 UV Offset 转换为最终 UV
    pub uvs: Vec<f32>,
    /// 顶点数量
    pub vertex_count: usize,
    /// 三角形索引(uint16 足够用于 2D)
    pub triangles: Vec<u16>,

    // === 骨骼绑定信息 ===
    /// 此 mesh 用到的所有骨骼实际索引
    /// 例: [arm, hand, leg, ...] 对应于 BoneMap 中的索引
    /// 如果为空,使用 Slot 的主骨骼
    pub bones: Option<Vec<BoneIdx>>,

    /// 每个顶点的影响骨骼数量
    /// 长度 = vertex_count,如果不存在或长度不合适自动追加填充为全1值(只受到1骨骼影响)
    pub influence_counts: Vec<u8>,

    /// 顶点在各个骨骼空间的位置
    /// 存储顺序:按顶点索引 i 从 0 到 N-1 遍历,
    /// 对每个顶点 i,若其受 `influence_counts[i] = n` 个骨骼影响,
    /// 则紧接着写入 n 个 (x, y) 坐标,共 2*n 个 f32。
    /// 这些坐标的骨骼顺序必须与 `influence_bone_indices` 中对应段完全一致。
    ///
    /// 示例:
    /// - 顶点0:受骨骼\[1, 2\]影响 → 写入 [x0_in_bone1, y0_in_bone1, x0_in_bone2, y0_in_bone2]
    /// - 顶点1:受骨骼\[0\]影响   → 写入 [x1_in_bone0, y1_in_bone0]
    ///
    /// 存储数据为:
    ///
    /// [x0_in_bone1, y0_in_bone1, x0_in_bone2, y0_in_bone2, x1_in_bone0, y1_in_bone0, ...]
    ///
    /// 总长度 = 2 * Σ(influence_counts)
    pub vertices_locals: Vec<f32>,

    /// 影响骨骼索引(指向 `bones` 的索引)
    ///
    /// 存储顺序:按顶点索引 i 从 0 到 N-1 遍历,
    /// 对每个顶点 i,若其受 `influence_counts[i] = n` 个骨骼影响,
    /// 则紧接着写入 n 个骨骼索引(u8),指向 `bones` 数组。
    ///
    /// 这些索引的顺序必须与 `vertices_locals` 中对应顶点的局部坐标顺序严格一致。
    ///
    /// 示例:
    /// - 顶点0:受骨骼\["arm", "hand"\]影响 → bones = ["arm", "hand", ...],索引为 [0, 1]
    /// - 顶点1:受骨骼\["leg"\]影响       → 索引为 \[2\]
    ///   存储数据为:
    ///   [0, 1, 2, ...]
    ///
    /// 总长度 = Σ(influence_counts)
    pub influence_bone_indices: Vec<u8>,

    /// 对应权重(每个骨骼影响的插值系数)
    ///
    /// 存储顺序:与 `influence_bone_indices` 完全一一对应,
    /// 即第 k 个权重作用于第 k 个骨骼索引所指的骨骼。
    ///
    /// 对任意顶点 i,其对应的 n 个权重之和应为 1.0(工具链必须保证),
    /// 以确保蒙皮变换结果正确。
    ///
    /// 示例(接上例):
    /// - 顶点0 权重 [0.6, 0.4] → 表示 60% 受 "arm" 影响,40% 受 "hand" 影响
    /// - 顶点1 权重 [1.0]
    ///   存储数据为:
    ///   [0.6, 0.4, 1.0, ...]
    ///
    /// 总长度 = Σ(influence_counts)
    pub influence_weights: Vec<f32>,

    /// 裁剪附件标记:Some(end_slot) 表示该网格是裁剪多边形(无 UV/三角形),
    /// 裁剪范围覆盖本 slot 之后、end_slot 之前(不含 end_slot)的所有网格
    pub clip_end_slot: Option<SlotIdx>,
}
#[derive(Debug, Clone)]
pub struct MeshSet {
    pub data_map: NameMap<MeshIdx, MeshData>,
}
impl Default for MeshSet {
    fn default() -> Self {
        Self::new()
    }
}

impl MeshSet {
    pub fn new() -> Self {
        Self {
            data_map: NameMap::new(),
        }
    }
    pub fn gen_name_skin_slot_name(&self, skin: &str, slot: &str, name: &str) -> String {
        format!("{}_{}_{}", skin, slot, name)
    }
    /// 按全名插入网格(名称重复时返回已有索引)
    pub fn insert_mesh(&mut self, full_name: String, mesh_data: MeshData) -> MeshIdx {
        self.data_map.insert(full_name, mesh_data)
    }
    /// 按全名查网格索引
    pub fn get_index(&self, name: &str) -> Option<MeshIdx> {
        self.data_map.get_index(name)
    }
}

impl MeshData {
    /// 蒙皮变形:根据 Pose 的世界变换,把每个顶点的骨骼空间坐标变换到模型空间
    ///
    /// - 对每个顶点,把其在各影响骨骼空间中的位置用骨骼的世界变换变换,再按权重加权求和
    /// - `bones` 为 None 时所有顶点使用 `slot_bone`(Slot 的主骨骼)
    /// - 返回模型空间顶点位置(与 [Self::uvs] 一一对应),UV 与三角形索引不变
    pub fn deform(
        &self,
        pose: &mut crate::data::runtime::skeleton::Pose,
        skeleton: &crate::data::runtime::skeleton::Skeleton,
        slot_bone: BoneIdx,
    ) -> Vec<glam::Vec2> {
        deform_vertices(
            self.vertex_count,
            &self.bones,
            &self.influence_counts,
            &self.influence_bone_indices,
            &self.influence_weights,
            &self.vertices_locals,
            None,
            pose,
            skeleton,
            slot_bone,
        )
    }
    /// 带动画 deform 的变形:`deform` 为采样后的顶点数据(加权 = delta、非加权 = 绝对位置)
    pub fn deform_with_deform(
        &self,
        deform: &[f32],
        pose: &mut crate::data::runtime::skeleton::Pose,
        skeleton: &crate::data::runtime::skeleton::Skeleton,
        slot_bone: BoneIdx,
    ) -> Vec<glam::Vec2> {
        deform_vertices(
            self.vertex_count,
            &self.bones,
            &self.influence_counts,
            &self.influence_bone_indices,
            &self.influence_weights,
            &self.vertices_locals,
            Some(deform),
            pose,
            skeleton,
            slot_bone,
        )
    }
}

/// 蒙皮变形的公共算法:把骨骼空间顶点按权重加权变换到模型空间
///
/// 与 [MeshData::deform] 语义一致,clip 多边形等无 UV/三角形数据也用同一套变形
///
/// `deform`:动画 deform 顶点数据(每顶点 2 个分量)。加权网格为 delta(叠加到 base 顶点),
/// 非加权网格为绝对位置(直接使用);None 表示无 deform
/// 参数对应网格数据的自然形态,保持与官方移植代码对照
#[allow(clippy::too_many_arguments)]
pub(crate) fn deform_vertices(
    vertex_count: usize,
    bones: &Option<Vec<BoneIdx>>,
    influence_counts: &[u8],
    influence_bone_indices: &[u8],
    influence_weights: &[f32],
    vertices_locals: &[f32],
    deform: Option<&[f32]>,
    pose: &mut crate::data::runtime::skeleton::Pose,
    skeleton: &crate::data::runtime::skeleton::Skeleton,
    slot_bone: BoneIdx,
) -> Vec<glam::Vec2> {
    let weighted = bones.is_some();
    // 预取本网格用到的骨骼世界变换(一次 check_dirty,顶点循环内直接索引,避免每 influence 一次 cache 查找)
    let world_affines: Vec<glam::Affine2> = match bones {
        Some(bones) => bones
            .iter()
            .map(|&b| pose.get_world_affine(skeleton, b))
            .collect(),
        None => vec![pose.get_world_affine(skeleton, slot_bone)],
    };
    let mut positions = Vec::with_capacity(vertex_count);
    let mut offset = 0usize;
    for v in 0..vertex_count {
        let count = influence_counts[v] as usize;
        let mut p = glam::Vec2::ZERO;
        // deform 语义(官方 spine):加权网格 = 每个 influence 的骨骼局部 delta(deform 数组按 influence 展开,
        // 每 influence 2 个 float);非加权网格 = 绝对局部位置(每顶点 2 个 float,直接替换 base)
        for _ in 0..count {
            let local_bone = influence_bone_indices[offset] as usize;
            let weight = influence_weights[offset];
            let (bx, by) = if weighted {
                match deform {
                    Some(d) if d.len() >= 2 * (offset + 1) => (
                        vertices_locals[2 * offset] + d[2 * offset],
                        vertices_locals[2 * offset + 1] + d[2 * offset + 1],
                    ),
                    _ => (
                        vertices_locals[2 * offset],
                        vertices_locals[2 * offset + 1],
                    ),
                }
            } else {
                match deform {
                    Some(d) if d.len() >= 2 * (v + 1) => (d[2 * v], d[2 * v + 1]),
                    _ => (vertices_locals[2 * v], vertices_locals[2 * v + 1]),
                }
            };
            let local = glam::Vec2::new(bx, by);
            let world = world_affines[local_bone];
            p += world.transform_point2(local) * weight;
            offset += 1;
        }
        positions.push(p);
    }
    positions
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        cacl::affine::build_affine,
        custom_index::AsIndex,
        data::runtime::skeleton::{BoneDataStatic, Skeleton},
        utils::default,
    };

    fn build_skeleton() -> Skeleton {
        let mut skeleton = Skeleton::new();
        let root = skeleton.insert_bone("root", default()).unwrap();
        // 一根在 (50,0) 处带 45 度旋转的子骨骼
        let arm = BoneDataStatic {
            translate_x: 50.0,
            rotation_deg: 90.0,
            parent_index: Some(root),
            ..Default::default()
        };
        skeleton.insert_bone("arm", arm).unwrap();
        skeleton.build_children_map();
        skeleton.build_roots();
        skeleton
    }

    fn build_mesh() -> MeshData {
        // 四边形,4 个顶点:两个受 root(权重 0.3)+ arm(权重 0.7),两个只受 root
        MeshData {
            meta: Default::default(),
            texture_name: "test".to_string(),
            uvs: vec![0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0],
            vertex_count: 4,
            triangles: vec![0, 1, 2, 0, 2, 3],
            bones: Some(vec![unsafe { BoneIdx::from_index(0) }, unsafe {
                BoneIdx::from_index(1)
            }]),
            influence_counts: vec![2, 2, 1, 1],
            // 顶点0: root空间(0,0) 权重0.3, arm空间(0,0) 权重0.7
            // 顶点1: root空间(10,0) 权重0.3, arm空间(10,0) 权重0.7
            // 顶点2: root空间(10,10) 权重1
            // 顶点3: root空间(0,10) 权重1
            vertices_locals: vec![
                0.0, 0.0, 0.0, 0.0, //
                10.0, 0.0, 10.0, 0.0, //
                10.0, 10.0, //
                0.0, 10.0,
            ],
            influence_bone_indices: vec![0, 1, 0, 1, 0, 0],
            influence_weights: vec![0.3, 0.7, 0.3, 0.7, 1.0, 1.0],
            clip_end_slot: None,
        }
    }

    #[test]
    fn deform_single_bone_identity() {
        let skeleton = build_skeleton();
        let mut pose = skeleton.default_pose();
        let mesh = build_mesh();
        // setup pose 下 root 无变换,单骨骼顶点应保持原位置
        let positions = mesh.deform(&mut pose, &skeleton, unsafe { BoneIdx::from_index(0) });
        assert!((positions[2].x - 10.0).abs() < 1e-5);
        assert!((positions[2].y - 10.0).abs() < 1e-5);
        assert!((positions[3].x - 0.0).abs() < 1e-5);
        assert!((positions[3].y - 10.0).abs() < 1e-5);
    }

    #[test]
    fn deform_translated_bone() {
        let skeleton = build_skeleton();
        let mut pose = skeleton.default_pose();
        let mesh = build_mesh();
        // 平移 root:全部顶点跟随
        pose.bone_data[unsafe { BoneIdx::from_index(0) }].translate_x = 100.0;
        let positions = mesh.deform(&mut pose, &skeleton, unsafe { BoneIdx::from_index(0) });
        for p in &positions {
            assert!(p.x >= 99.9, "x={} should follow root translation", p.x);
        }
        // 顶点2 (root空间 10,10) -> 世界 (110,10)
        assert!((positions[2].x - 110.0).abs() < 1e-4);
        assert!((positions[2].y - 10.0).abs() < 1e-4);
    }

    #[test]
    fn deform_weighted_mix() {
        let skeleton = build_skeleton();
        let mut pose = skeleton.default_pose();
        let mesh = build_mesh();
        // arm 在 (50,0) 旋转90度:arm 局部 (10,0) -> 世界 (50,10)
        // 顶点1: 0.3*root(10,0) + 0.7*arm(50,10) = (3,0)+(35,7) = (38,7)
        let positions = mesh.deform(&mut pose, &skeleton, unsafe { BoneIdx::from_index(0) });
        let v1 = positions[1];
        assert!((v1.x - 38.0).abs() < 1e-4, "x={}", v1.x);
        assert!((v1.y - 7.0).abs() < 1e-4, "y={}", v1.y);
        // 顶点0: root(0,0)+arm(50,0)=0.3*0+0.7*(50,0)=(35,0)
        let v0 = positions[0];
        assert!((v0.x - 35.0).abs() < 1e-4);
        assert!((v0.y - 0.0).abs() < 1e-4);
    }

    #[test]
    fn deform_without_bones_uses_slot_bone() {
        let skeleton = build_skeleton();
        let mut pose = skeleton.default_pose();
        let mut mesh = build_mesh();
        mesh.bones = None;
        mesh.influence_counts = vec![1, 1, 1, 1];
        mesh.influence_bone_indices = vec![0, 0, 0, 0];
        mesh.vertices_locals = vec![
            0.0, 0.0, //
            10.0, 0.0, //
            10.0, 10.0, //
            0.0, 10.0,
        ];
        mesh.influence_weights = vec![1.0, 1.0, 1.0, 1.0];
        let positions = mesh.deform(&mut pose, &skeleton, unsafe { BoneIdx::from_index(1) });
        // arm 局部坐标 -> 世界
        let world = build_affine(
            [50.0, 0.0],
            90.0,
            [1.0, 1.0],
            [0.0, 0.0],
            crate::data::runtime::skeleton::ShearMode::KeepLength,
        );
        for (i, p) in positions.iter().enumerate() {
            let local =
                glam::Vec2::new(mesh.vertices_locals[2 * i], mesh.vertices_locals[2 * i + 1]);
            assert!((p.x - world.transform_point2(local).x).abs() < 1e-4);
            assert!((p.y - world.transform_point2(local).y).abs() < 1e-4);
        }
    }

    /// 加权网格 deform:delta 按 influence 叠加到骨骼局部 base(每 influence 2 分量),再乘骨骼
    #[test]
    fn deform_weighted_with_delta() {
        let skeleton = build_skeleton();
        let mut pose = skeleton.default_pose();
        let mesh = build_mesh();
        // influence 布局(offset: 0,1=顶点0 的 root/arm;2,3=顶点1 的 root/arm;4,5=顶点2/3 的 root):
        // delta: 顶点1 的两个 influence 各 + (5,0)
        let deform = vec![
            0.0, 0.0, // 顶点0/root
            0.0, 0.0, // 顶点0/arm
            5.0, 0.0, // 顶点1/root
            5.0, 0.0, // 顶点1/arm
            0.0, 0.0, // 顶点2/root
            0.0, 0.0, // 顶点3/root
        ];
        let positions = mesh.deform_with_deform(&deform, &mut pose, &skeleton, unsafe {
            BoneIdx::from_index(0)
        });
        // 顶点1: root(10+5,0)·0.3 + arm(10+5,0->世界(50,15))·0.7 = (4.5,0)+(35,10.5) = (39.5,10.5)
        let v1 = positions[1];
        assert!((v1.x - 39.5).abs() < 1e-4, "x={}", v1.x);
        assert!((v1.y - 10.5).abs() < 1e-4, "y={}", v1.y);
        // 顶点2(无 delta):不变
        assert!((positions[2].x - 10.0).abs() < 1e-4);
    }

    /// 非加权网格 deform:数据为绝对位置(直接替换 base)
    #[test]
    fn deform_unweighted_absolute_positions() {
        let skeleton = build_skeleton();
        let mut pose = skeleton.default_pose();
        let mut mesh = build_mesh();
        mesh.bones = None;
        mesh.influence_counts = vec![1, 1, 1, 1];
        mesh.influence_bone_indices = vec![0, 0, 0, 0];
        mesh.vertices_locals = vec![
            0.0, 0.0, //
            10.0, 0.0, //
            10.0, 10.0, //
            0.0, 10.0,
        ];
        mesh.influence_weights = vec![1.0, 1.0, 1.0, 1.0];
        let deform = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
        let positions = mesh.deform_with_deform(&deform, &mut pose, &skeleton, unsafe {
            BoneIdx::from_index(0)
        });
        // 绝对位置直接输出(root 无变换)
        assert!((positions[0].x - 1.0).abs() < 1e-4);
        assert!((positions[0].y - 2.0).abs() < 1e-4);
        assert!((positions[1].x - 3.0).abs() < 1e-4);
        assert!((positions[1].y - 4.0).abs() < 1e-4);
    }
}