Appearance
换装管线闭环设计
虚拟形象换装不是「预设套装切换」,而是任意部件的独立建模、独立加载、运行时拼接——这条管线要从美术制作规范一路闭环到运行时的骨骼合并,任何一环松掉,用户看到的就是穿模或者换装卡死。
首次换装≈1–2s
缓存命中<100ms
蒙皮限制4 骨骼/顶点
背景
B 站虚拟形象允许用户自由搭配衣服、裙子、翅膀、头发等部件。和传统游戏的预设套装不同,这里是任意组合:每个部件独立建模、独立压缩、独立下载,运行时拼到同一个角色身上。这意味着管线必须同时回答三个问题:美术按什么规范做,资源按什么格式发,运行时怎么把不同部件的骨骼拼成一套。

问题
| 问题 | 表现 | 根因方向 |
|---|---|---|
| 部件各自带骨骼 | 裙子、翅膀自带的骨骼和身体骨架对不上 | 缺少统一骨架规范 |
| 组合冲突 | 裙子和裤子同时穿必穿模 | 缺少槽位与互斥规则 |
| 移动端蒙皮超限 | 低端机 GPU 蒙皮寄存器有限 | 每顶点骨骼影响数无约束 |
| 换装卡顿 | 下载、解压、合并全在主链路 | 无缓存与异步化 |
| 连续换装泄漏 | 换十几次后内存持续上涨 | 旧 Mesh/纹理未释放 |
方案
mermaid
flowchart LR
A["美术制作<br>统一骨架/蒙皮规范"] --> B["资源打包<br>glb + ktx2 + 部件配置"]
B --> C["配置系统<br>槽位/互斥/依赖"]
C --> D["运行时加载<br>Draco 解压/KTX2 上传"]
D --> E["换装引擎<br>骨骼合并 + 重绑定"]
E --> F["SkinnedMesh 更新"]1. 资源制作规范(美术侧闭环)
- 所有部件使用同一套骨架,统一命名、统一层级,禁止部件模型里加独立骨骼;
- 蒙皮约束:每根骨骼最多影响 4 个顶点(移动端限制),蒙皮权重总和 = 1.0。
规范的意义是把运行时合并的成本前置到制作期消化——部件不夹带私货骨骼,合并时只需要做引用对齐,不需要做拓扑缝合。
2. 打包配置(工程化侧闭环)
每个部件一份 JSON 配置:
json
{
"id": "w_skirt_001",
"type": "skirt",
"slot": "bottom",
"model": "w_skirt_001.glb",
"textures": ["tex_default.ktx2"],
"bones": ["Hips", "Spine"],
"collision": "none"
}配置系统负责三件事:槽位(body / top / bottom / hair / wing),互斥(skirt 和 pants 同属 bottom 槽位,互斥),依赖检查(翅膀依赖身体模型的背骨节点,身体没加载完翅膀排队)。

3. 运行时换装管线
用户选择部件 → 检查槽位互斥 → 请求下载模型 → Draco 解压 → KTX2 上传 GPU
→ 合并骨骼 → 更新 SkinnedMesh → 换装完成关键实现:骨骼合并
整条管线最难的一环:不同部件的骨骼是独立的,必须合并到同一个 Skeleton 才能共用一套动画。
身体骨骼 (60 bones) + 裙子骨骼 (5 bones,引用身体的 Hips, Spine)
→ 合并为 65 bones 的统一 Skeleton
→ 裙子 Mesh 重新绑定到合并后的 Skeleton

合并后部件 Mesh 的蒙皮索引要重映射到新 Skeleton 的骨骼下标,这一步做错了模型会「炸」成放射状碎片——调试期最常见的画面。配合制作期「禁止独立骨骼」的规范,合并实际退化成「查找引用骨骼在新 Skeleton 中的下标并替换」,稳定且快速。
指标
| 场景 | 耗时/表现 |
|---|---|
| 首次换装 | 下载 + 解压 + 骨骼合并 ≈ 1–2s |
| 再次换同一部件 | 缓存命中,< 100ms |
| 连续换装 | 无内存泄漏(修复旧 Mesh/纹理释放后) |
| 蒙皮规格 | 每顶点 ≤ 4 骨骼影响,权重和 = 1.0 |
复盘
- 换装系统的稳定性七分靠制作规范,三分靠运行时代码——运行时能做的兜底很有限,规范必须在美术侧就卡住。
- 槽位 + 互斥 + 依赖检查要放在配置层表达,不要散在换装代码的 if 里,否则新增部件类型时必漏。
- 骨骼合并尽量让它「退化成下标重映射」;如果需要真的缝骨骼树,说明上游规范已经破了。
- 缓存和释放要成对设计:缓存让二次换装进 100ms,释放漏了连续换装就变成内存事故。