赞
踩
书接上文,上回说道,烘焙顶点的方法存在着两个缺陷以及一个致命弱点:
这些都是由于烘焙顶点是一种针对“现象”而非“原因”的技术。驱动顶点运动的是骨骼的运动,因此信息密度最大的方法应该针对骨骼动画的信息而运作。
要把骨骼动画烘焙到纹理上以驱动顶点动画。要实现这个目标首先要分拆里面的命题:
此时,实现的思路就出来了:
首先要解决的是烘焙的信息(即矩阵)如何计算的问题。这里涉及到蒙皮顶点和骨骼的关系问题。
这个问题换一个描述方法,蒙皮上的顶点要如何随骨骼移动而移动?这个问题就明了一些,能够关注本专栏的大手子们必然知道,就像相机渲染需要经过一系列的矩阵运算一样,骨骼的移动必然导致骨骼的变换矩阵发生了改变,而如果我们预先把一系列矩阵运算的结果计算出来,只需要在顶点着色器中读出矩阵然后mul一下,就能达到结果。
这一系列矩阵运算到底是啥呢?
首先肯定是模型空间下的顶点转换到该骨骼节点空间下的顶点;这一步Unity已经提供了,就是Mesh.bindPoses。我们来看看文档里咋写的:
The bind pose is the inverse of the transformation matrix of the bone, when the bone is in the bind pose
BindPose是骨骼的转换矩阵的逆矩阵。
BindPose这个说法,可能来源于美术绑定骨骼的时候摆成了T-Pose,我觉得暂时没有更好的中文称呼。文档下方是这样计算BindPose的:模型的局部转世界乘以骨骼的世界转局部矩阵。
所以使用这个矩阵变换顶点,相当于先把模型空间下的顶点转到世界空间,然后再从世界坐标空间转到骨骼空间。
bones
然而,骨骼在每帧都会发生平移、旋转、缩放,而骨骼空间的坐标描述不了这个变化。因此我们进行第二步,引入骨骼节点的在世界坐标下的变化信息。
【1】中的实现,是从骨骼节点上溯至根节点,不停的左乘变换矩阵。我觉得有更好的解决方法,就是直接用Transform.localToWorldMatrix。反正烘焙的时候基本上都是在空场景里做的,直接一步到位。这时候,顶点就从骨骼空间转换到了世界空间。
最后,我们实际要用的,还是模型空间内的坐标,因此再将世界空间内的节点变换回骨骼空间。
所以,蒙皮顶点的坐标空间变换是这样的:模型空间-骨骼空间-世界空间-模型空间。
开始的时候,我纳闷一件事情,特喵的,bindPose里用了worldToLocalMatrix,我后面再左乘一个localToWorldMatrix,不就白忙活了吗? 后来我意识到一个误区,就是bindPose,是Mesh静态的数据,是在T-pose下完成绑定时的数据,这里面计算所使用的那个worldToLocalMatrix,并不会随着每帧的运动而变化。因此为了引入时效性信息,自然要从每帧里额外左乘一个localToWorldMatrix进来。
所以这一部分的代码如下:
Texture2D
虽然我们有了每帧每骨骼的变换信息,但是还有一点,就是一个顶点受哪些骨骼的影响及其程度如何,还没有实现。但是仔细一想就明白,这个索引跟权重是一个Mesh静态的数据。
既然是Mesh静态的,那就直接写到Mesh里,最常见的当然就是UV通道了。UV是一个Vector2的向量,因此一次只能存一对权重索引数据。如果你对精度提出了更高的要求,那可以用2个UV通道或者4个UV通道。
本文的写法是用2个UV通道,也就是支持一个顶点受两个骨骼的影响。
划掉,Tangent还是不能改的。TANGENT_SPACE_ROTATION宏要用到它。感谢 @头像是狐狸吗
private
剩下的就是在Shader中完成了。先从预存数据的uv中拿出该顶点对应的骨骼和权重;随后在纹理中读出矩阵;最后拿蒙皮顶点做变换即可。
float
这篇文章的主体,我已经在三天前就已经写好了,但是迟迟做不出来效果,为什么我会这么蠢呢?
总结了一下,有三个地方:
第一,主体部分沿用了上一篇文章中顶点纹理的代码。导致Texture2D的格式是RGB24,忽略了8位颜色分量不支持负数,需要手动压缩分量到01区间,这点忽略了,导致查了很久。
这一部分的问题,需要看Unity的文档,里头这么写的:
第二个,因为只支持两个骨骼,所以不能老老实实的按权重UV里得到的值来写,原本Unity支持的是一个顶点受4个骨骼影响,所以得出的uv值不能这么用:
mul
而是应该:
mul
第三个,搞这个东西,肯定要上移动端,移动端肯定还是用RGBA32这类的更加合适。UnityCG.cginc中提供了两个代码,可以把[0,1)内的浮点数映射到8位颜色分量上:
// Encoding/decoding [0..1) floats into 8 bit/channel RGBA. Note that 1.0 will not be encoded properly.
我选择在C#里Encode,Shader里Decode。因此烘焙相关的代码改成如下形式:
private
当然,这样不好,因为这意味着要构建一个矩阵,就要采样12次;支持两个骨骼,就要采样24次。如果要在移动端使用,可以选择用half映射而不是float映射,然后只支持一个骨骼。反正要用到这玩意儿的时候,基本也不太会在乎细节上的品质……
本文实现了基于预计算矩阵和GPU的蒙皮动画,跟上文提到的基于预计算顶点纹理的蒙皮动画而言,各有优劣。有句话说,“政治是妥协的艺术”,游戏开发也是这样的,同等情况下没有十全十美的技术。
上文是用空间换效率,本文则是用效率换空间;当然,它们的诞生主要是为了解决大规模角色的批次问题——毕竟,在GC面前,这些消耗都显得可以接受了。
【1】https://blog.csdn.net/lzhq1982/article/details/88121451
【2】踏雪寻梅:[Unity3d手游开发笔记]骨骼蒙皮动画
【3】Unity - Scripting API: TextureFormat
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。