Appearance
捏脸系统设计
捏脸的本质是把「自由建模」这个需要专业 3D 知识的操作,降维成十几个滑块——用户拖的是「脸宽」「眼角角度」,系统背后算的是一组 BlendShape 权重的组合插值。
面部参数15 个
单参数映射1–3 个 BS
移动端调整<2ms
背景
虚拟形象要让普通用户捏出「像自己」或者「想要」的脸。专业 3D 软件的自由建模路线直接排除——用户没有顶点级操作的能力,移动端也没有那个交互空间。可行路线只有参数化:美术预先做好一组 BlendShape(BS,形态键),每个 BS 表达一个面部形变方向,系统把用户语义的参数(脸宽、眼距)映射成若干 BS 权重的组合。

问题
| 问题 | 表现 | 设计约束 |
|---|---|---|
| 参数语义 ≠ BS 语义 | 「脸宽」不是一个 BS 能表达的 | 一个参数要映射多个 BS |
| 组合冲突 | 极端参数组合下脸型崩坏 | 权重范围要钳制在合理区间 |
| 移动端算力 | 拖动滑块要实时预览 | 每帧 BS 计算必须毫秒级 |
| 结果不可复现 | 用户捏完换设备没了 | 参数组合要可序列化 |
方案
mermaid
flowchart LR
A["用户拖滑块<br>脸宽 = 0.7"] --> B["参数 → BS 映射表"]
B --> C["Face_Width_X<br>Lerp(-50, 50)"]
B --> D["Cheek_Width<br>Lerp(-20, 30) × 0.6"]
C --> E["BS 权重合成"]
D --> E
E --> F["GPU 顶点位移<br>实时预览"]设计要点是把「美术语义」和「用户语义」分离:BS 是美术做的形变基元,参数是用户能理解的概念,中间的映射表是策划/美术可调的数据,不是写死的代码。
关键实现
面部参数表
| 分类 | 参数 | BS 控制 |
|---|---|---|
| 脸型 | 脸宽、脸长、下巴尖度 | Face_Width, Face_Length, Jaw_Shape |
| 眼睛 | 眼大、眼距、眼角角度 | Eye_Size, Eye_Space, Eye_Angle |
| 鼻子 | 鼻宽、鼻高、鼻尖 | Nose_Width, Nose_Height, Nose_Tip |
| 嘴巴 | 嘴宽、嘴厚、嘴角 | Mouth_Width, Lip_Thick, Mouth_Corner |
| 眉毛 | 眉高、眉距、眉角 | Brow_Height, Brow_Space, Brow_Angle |
5 类 15 个参数,每个参数映射到 1–3 个 BS 的权重范围。
参数 → BS 映射
csharp
// 脸宽调整:同时影响 Face_Width_X 和 Cheek_Width
void ApplyFaceWidth(float value) {
SetBlendShape("Face_Width_X", Mathf.Lerp(-50f, 50f, value));
SetBlendShape("Cheek_Width", Mathf.Lerp(-20f, 30f, value * 0.6f));
}注意第二个 BS 带了 0.6 的衰减系数——主形变和协同形变不是等权重的,协同项过强会让脸显得「充气」。每个映射的范围和系数都是美术在编辑器里逐个调出来的。
预设系统
- 随机生成:在合理范围内随机参数组合,给用户一个「摇骰子」入口;
- 模板预设:女性 / 男性 / 可爱 / 帅气等方向的整组参数预设;
- 用户保存:当前参数组合序列化成 JSON 存档,换设备可恢复。
预设模板的价值被低估了:大部分用户不会从零捏,而是在一个顺眼的模板上微调,模板质量直接决定留存。
性能
每个 BS 调整是 O(1) 的顶点位移计算(权重 × 预存的顶点偏移),15 个 BS 同时调整在移动端不超 2ms,滑块拖动全程实时预览无压力;PC 端可支持到 50+ BS,给更精细的形变留了余量。
指标
| 指标 | 数值 |
|---|---|
| 用户可调参数 | 15 个(5 类) |
| 单参数 BS 映射 | 1–3 个,带范围与衰减系数 |
| 移动端 15 BS 同调 | < 2ms |
| PC 端 BS 上限 | 50+ |
复盘
- 参数化系统的核心是映射表数据化——写死在代码里的映射,美术每调一次都要发版,必死。
- 协同 BS 的衰减系数比主 BS 的范围更影响「自然感」,调参时间主要花在这。
- 性能不是捏脸的瓶颈(O(1) 位移很便宜),真正的瓶颈是美术做 BS 的产能,参数表设计要按产能倒推。
- 存档只存参数不存结果——存最终 Mesh 既大又会和 BS 版本脱节,参数 + BS 版本号才是可维护的存档格式。