骨骼动画
骨骼动画是一种常见但类型特殊的动画,我们提供了 预烘焙骨骼动画 和 实时计算骨骼动画 两套系统,针对不同方向的需求,分别优化。
这两套系统的唯一开关就是 SkeletalAnimation
上的 useBakedAnimation
开关,启用时会使用预烘焙骨骼动画系统,禁用后会使用实时计算骨骼动画系统,运行时也可以无缝切换。
骨骼动画的组件接口请参考 MeshRenderer API。
预烘焙骨骼动画系统
这个系统的压倒性目的是性能,部分表现力的牺牲也被认为是可以接受的。
我们针对性地做了很多底层优化,目前的运行时流程大致如下:
- 所有动画数据都会按照指定帧率提前预采样、烘焙到全局复用的骨骼动画贴图合集上;
- 根据运行平台是否支持浮点纹理,会对应使用 RGBA32F 或 RGBA8 格式的 fallback;(这层用户不必关心,不对最终表现有影响,只是低端平台最后的保底策略)
- 每个骨骼动画组件(SkeletalAnimation)负责维护当前的播放进度,以 UBO(一个 vec4)的形式存储;
- 各蒙皮模型组件(SkinnedMeshRenderer)持有预烘焙蒙皮模型类(BakedSkinningModel),根据同样提前烘焙好的包围盒信息计算 culling,更新 UBO,在 GPU 上从贴图合集内取到当前数据完成蒙皮。
实时计算骨骼动画系统
这个系统的压倒性目的是表现力,确保所有细节的正确显示,和完整的程序控制能力。
目前的运行时流程大致如下:
- 所有动画数据根据当前全局时间动态插值计算;
- 动画数据会输出到场景的骨骼节点树中;
- 用户和其他任何系统都可以通过操纵这个骨骼节点树对蒙皮效果产生影响;
- 各蒙皮模型组件(SkinnedMeshRenderer)持有普通蒙皮模型类(SkinningModel),每帧提取骨骼节点树信息计算 culling,将当前帧完整骨骼变换信息上传 UBO,在 GPU 内完成蒙皮。
这为以下所有功能提供了最基础的支撑:
- blendshape 支持
- 任意数量动画片段的混合和 masking
- IK、二级物理影响
- 纯程序控制关节位置
两套系统的选择与最佳实践
目前所有模型资源在导入后,prefab 中全部默认使用预烘焙系统,以达到最佳性能。我们建议只在明显感到预烘焙系统的表现力无法达标的情况下,再使用实时计算系统。
虽然两套系统可以在运行时无缝切换,但尽量不要高频执行,因为每次切换都涉及底层渲染数据的重建。
蒙皮算法
我们内置提供两种常见标准蒙皮算法,它们性能相近,只对最终表现有影响:
- LBS(线性混合蒙皮):骨骼信息以 3x4 矩阵形式存储,直接对矩阵线性插值实现蒙皮,有体积损失等典型已知问题。
- DQS(双四元数蒙皮):骨骼信息以双四元数形式插值,对不含有缩放变换的骨骼动画效果更精确自然,但出于性能考虑,对所有缩放动画有近似简化处理。
引擎默认使用 LBS,可以通过修改引擎 skeletal-animation-utils.ts 的 updateJointData
函数引用与 cc-skinning.chunk 中的头文件引用来切换蒙皮算法。
推荐对蒙皮动画质量有较高追求的项目可以尝试启用 DQS,但因 GLSL 400 之前都没有 fma
指令,如 cross
等操作在某些 GPU 上无法绕过浮点抵消问题,误差较大,可能引入部分可见瑕疵。
挂点系统
如果需要将某些外部节点挂到指定的骨骼关节上,需要使用骨骼动画组件的 挂点(Socket)系统:
- 在要对接的骨骼动画组件下新建一个子节点(直属 parent 应为动画组件所在节点);
- 在骨骼动画组件的 sockets 属性中添加一个数组元素,path 从下拉列表中选择要挂接的那根骨骼的路径(注意骨骼动画组件的 defaultClip 一定要有值,下拉列表的选项依赖这个属性),target 指定为刚刚创建的子节点;
- 这个子节点就成为目标挂点了,可以把任何外部节点放到这个子节点下,都会跟随指定骨骼变换了。
FBX 或 glTF 资源内的挂点模型会自动对接挂点系统,无需任何手动操作。
关于动态 Instancing
基于预烘焙系统的框架设计,蒙皮模型的 instancing 也成为了触手可及的功能,但要保证正确性还需要收集一些比较底层的信息。
这里的根本问题是,同一个 drawcall 内的各个模型使用的骨骼贴图必须是同一张,如果不是同一张,显示效果会完全错乱。所以如何将动画数据分配到每张骨骼贴图上,就成为了一个需要用户自定义的信息,对应在编辑器项目设置的 骨骼贴图布局面板 进行配置。
注意:
- 只有预烘焙系统下支持 instancing,我们虽然没有严格禁止实时计算框架下启用 instancing(只有编辑器内的警告),但动画效果一定会有问题,取决于模型实际的材质分配情况,最好的情况是不同 instance 间显示完全一致的动画,最坏情况下会导致模型完全错乱。
- 对于材质中已经开启 instancing 的模型,平面阴影系统也会自动同步使用 instancing 绘制。特别地,蒙皮模型的阴影合批对骨骼贴图布局的要求更高一些,因为阴影的管线状态是统一的,所有开启阴影的蒙皮模型的动画 都需要保证在同一张贴图上(相比与绘制模型本身时,只需要相同 drawcall 内的 instance 之间保持骨骼贴图一致)。
批量蒙皮模型组件
目前底层上传 GPU 的骨骼纹理已做到全局自动合批复用,上层数据目前可以通过使用 批量蒙皮模型组件(BatchedSkinnedMeshRenderer)将同一个骨骼动画组件控制的所有子蒙皮模型合并:
合批版 effect 书写相对复杂一点,但基本可以基于子材质使用的普通 effect,加入一些相对直接的预处理和接口改动即可,编辑器内的内置资源里 (util/batched-unlit) 提供了一个合批版 builtin-unlit,可以参考。
注意:只有预烘焙系统下使用批量蒙皮模型组件可以保证正确性,实时计算框架下虽然也能使用,但合并后的骨骼数量超过 30 个(Uniform 数组最大数量限制)时会有渲染问题。