大家好,我是阿赵。
在几年前,我曾经写过一篇介绍GPUSkinning的文章,这么多年之后,还是看到不停有朋友在翻看这篇旧文章。今天上去GitHub看了一下,GPUSkinning这个开源的插件已经很久没有更新过了,还是停留在2017年的0.2.3版本。GPUSkinning的魅力在于可以在消耗比较低的情况下同屏显示很多个蒙皮动画的角色。
看了一下之前写的文章,当时的我,水平也比较有限,所以只是简单的介绍了一下这个插件的用法。这么多年过去了,我感觉可以更深入的讨论一下这个插件的用法,还有它的实现原理。
一、使用说明和原理介绍
1、下载和安装
由于最近上GitHub似乎有些困难,所以我这里再提供多一个能下载的地址:
https://kgithub.com/chengkehan/GPUSkinning/
把插件下载到本地后解压缩,里面是一个Unity项目,也可以把GPUSkinning文件夹复制到自己的项目里面
2、采样动画数据
接下来要把我们基于Animator的动画转换成GPUSkinning插件使用的动画格式。
1.添加GPUSkinningSampler脚本
把我们需要的带动画的预设文件拖到场景里面,然后添加一个GPUSkinningSampler脚本上去
这里要注意一点,这个Animator组件里面的AnimatorController不能是AnimatorOverrideController,一定要是非Override的AnimatorController。不然会有报错提示。
2.必须设置的选项
这里有几个东西需要设置,首先是这个动画角色的名称,然后是一个顶点受多少根骨骼影响,使用怎样的shader,设置根骨骼节点,选择是否生成新的Shader,最后指定动画片段。
需要注意的东西有:
(1)顶点受多少根骨骼影响这个是可以后改的,不管选择多少根骨骼,工具都会导出uv2和uv3数据,其中uv2的数据是该顶点对应第1、2根骨骼的蒙皮权重,uv3是顶点对应第3、4根骨骼的蒙皮权重。区别只是在于自动选择或者生成的shader不同而已,shader可以后面在材质球直接选择或者修改的。
(2)是否选择生成新shader,决定于你的模型是否有特殊的效果。如果有,比如原来的模型是使用了卡通渲染,或者pbr渲染之类的,在插件默认提供的shader里面找不到相同效果的shader可以选择时,就可以选择生成新shader,那么就会生成一个新的shader文件,里面的顶点程序是写好了蒙皮方法的,比如skin2或者skin3或者skin4之类,我们就可以修改这个shader,达到想要的效果
(3)先设置动画片段的数量,然后动画片段需要手动拖进去,后面有两个选项,RootMotion是根节点是否移动,Individual Difference是该动画如果同时出现在多个个体身上时,是否需要有差异。之前的文章里面我说过GPUSkinning的其中一个缺点是在场景里面同一个角色播放同一个动作时会完全一样,这里的Individual Difference是为了解决这个问题,如果某个动作选择勾上这个选项,那么如果这个动作循环播放时,将会随机取一个数字,来错开动画播放的帧。
3.LOD选项
在展开LOD选项后,可以配置LOD等级,这是一个可选项,如果本身模型有做多个不同等级的LOD模型,可以在这里设置
设置一个size,然后指定不同LOD等级使用的网格模型和距离。
4.预览和编辑
点击Preview/Edit按钮,可以打开预览和编辑的内容
这里可以选择动画,预览动画,并且增加动画事件
可以设置Bounds
最下面还有一个骨骼列表,前面可以勾选。这个勾选的作用,是可以保留某个骨骼节点。因为在通过GPUSkinning播放动画时,原有的骨骼和网格模型都不存在了,只有一个空节点。如果我们想做一些跟随功能,比如角色手上拿着的武器可以替换,那么我们就可以把手的骨骼勾上,这样播放的时候,会看到手的骨骼的节点,那么就可以把武器挂上去了。
5.采样动画
在LOD下面,有个Step1:Play Scene的按钮。这其实就是提示我们如果要开始采样动画,就先点这个按钮运行场景。
运行场景之后,这里会出现Step2,意思就是第二步,正式开始采样了。点击一下这个按钮
插件就开始采样动画,并生成需要的文件了。实际上的过程,是逐个动画播放一次,然后每一帧获取骨骼的bindposes矩阵,这个矩阵是每根骨骼的位移旋转缩放,然后记录下来。
最后生成了这些文件
接下来说一下这些文件的作用
(1)anim属性文件
这个文件是导出的动画文件的总文件,里面包含了这个动画角色的大部分信息,比如名字、骨骼列表、动画列表、Bounds范围、根骨骼index、保存每帧动画的texture的宽高、Lod等级设置等。
(2)材质球文件
每个角色,都会生成一个材质球,如果在生成时勾选了新Shader,这里还会生成一个对应的Shader
(3)网格模型文件
包含了本身的角色的网格模型,还有lod等级里面的网格模型
仔细的看看信息,它包含了uv2和uv3的信息,上面介绍过,就是这个模型的顶点蒙皮权重
(4)还有一个包含Texture的byte文件。
这个文件其实是一个贴图文件来的,不过是什么格式并不重要,因为里面的颜色值rgba,其实是每一帧每根骨骼的bindpose矩阵,3个顶点颜色代表了一个矩阵。
之前在采样的时候说过,实际上是每个动画播放一次,然后每帧记录bindposes,这里就是把这些数据记录在rgba颜色里面。在需要播放动画的时候,从这些颜色值里面还原骨骼矩阵,然后结合着模型的uv2和uv3带有的蒙皮信息,就可以算出顶点在某一帧动画里面的位置了。
3、运行动画
当生成完了需要的文件之后,就可以进行播放了
先建一个空的GameObject在场景里,然后添加一个GPUSkinningPlayerMono脚本到GameObject上。
把刚才生成的文件对应的拖入到GPUSkinningPlayerMono脚本里面
当文件都拖入之后,就能看到场景里面出现了会动的模型了。
这里可以选择需要播放的动画
然后在运行的时候,可以通过脚本播放指定的动画:
player = GetComponent<GPUSkinningPlayerMono>().Player;
player.Play("Idle");
观察一下GameObject节点,会发现刚才勾选了暴露的手部骨骼,在下面生成出来了。
现在可以创建很多个这样的角色了同时运行。
二、原理的归纳
刚才在介绍用法的时候,也介绍了一部分原理,这里归纳一下:
1、数据准备阶段
1.通过uv2和uv3记录网格模型的顶点蒙皮信息
2.采样的时候,每个动画播放一遍,然后每一帧记录骨骼的位移旋转缩放
3.把动画里面每根骨骼的位移旋转缩放矩阵,通过rgba颜色,一个颜色记录四个数据,然后一个4x4矩阵原则上是需要4个像素的颜色记录的,但由于最下面一行数据是固定的0,0,01,所以只记录前面3行就够了,所以这个工具实际上只使用了3个像素颜色来记录一个骨骼的矩阵。这些像素色,记录在一张Texture2D上,然后通过Texture2D.GetRawTextureData方法获得byte[]并保存。
2、播放阶段
1.通过一个控制器统一去加载和分配角色的动画资源,比如同一个角色,他使用的材质球其实是一样的,这样可以合并DrawCall
2.通过Texture2D.LoadRawTextureData方法还原Texture2D数据,并还原每一帧每根骨骼的矩阵
3.在需要播放动画的时候,通过uv2和uv3去得蒙皮信息,然后通过矩阵和权重计算出顶点的位置,达到了播放动画的目的。
4.通过Renderer.SetPropertyBlock给Render设置不同的参数,就能得到差异化的播放动画了。
三、扩展思考
类似GPUSkinning的功能,阿赵我其实自己也实现过,我实现过2次:
一次是在以前做页游的时代,使用AS3编写过在GPU进行蒙皮骨骼动画播放的功能,骨骼动画的原理在之前的文章里面也介绍过,有兴趣的朋友可以去翻一下旧文章。
另外一次是在Unity里面实现的,和GPUSkinning有点类似,也是通过贴图来记录动画关键帧信息,不过却有点不一样。
我自己的实现方式具体是这样的:
我在贴图里面记录的不是骨骼的信息,我也不记录顶点的蒙皮信息,我只关心顶点位置。
1、记录顶点的uv2
我把所有的顶点编了一个顺序,每个顶点的序号记录在uv2的u坐标里面,然后uv2的v坐标固定是0
Vector2[] uv2 = new Vector2[vertCount];
for(int i = 0;i<vertCount;i++)
{Vector2 tempUV = new Vector2((float)i /(float) (vertCount-1), 0);uv2[i] = tempUV;
}newMesh.uv2 = uv2;
这里的意思是,只要根据uv2采样贴图,就能得到顶点对应某一帧的像素颜色,然后移动v坐标,就可以实现不同帧数的采样。
2、记录关键帧的时候,同样把所有动画播放一次,然后每一帧记录。不过记录的不是骨骼的bineposes,而是每个顶点在当前帧的实际位置。
这里会存在2个问题
1.由于颜色的值只能是0-255,或者说是0-1范围,但顶点的坐标是没有范围的,而且可以是负数的。为了解决这个问题,所以需要先计算出所有顶点可能出现的最大和最小范围,记录两个Bounds,然后颜色值实际记录的,是当前顶点坐标在最大最小值之间的比例,所以肯定是0-1的。
2.如果模型顶点很多,图片的宽度会变得特别的大,比如一个一万顶点的模型,动画总共有100帧,按照上面说的生成方式,图片的大小将会是10000x100,这样明显是不合理的,所以一般来说会设置一个最大宽度,比如2048,那么上面这个模型,将会生成一张2048*500的贴图。
3、播放动画的时候,就非常简单了,只需要指定当前需要播放第几帧,然后根据uv2的v坐标来上下移动贴图的采样范围,就能得到某一帧的颜色点,然后计算出顶点的实际坐标了。
4、由于只需要移动uv2的v坐标,所以控制方式可以很多样,单纯在shader里面用_Time来自动播放也行,传入Index指定播放某一帧也可以。
对比一下GPUSkinning,我这种方法有优点,也有缺点
优点:
我这种计算方式,实际上是比GPUSkinning的计算量要小的,因为所有参数包括贴图都是直接放入材质球里面,基本上不需要C#的额外干涉就能播放,也不需要计算权重,直接设置顶点坐标就行。而且控制的手段也比较的灵活。
缺点:
我这种计算方式,缺点也很明显。由于记录关键帧动画是根据顶点来记录的,所以如果顶点很多的模型,比如上万个顶点的模型,也是有的,需要记录的数据就会非常多。而GPUSkinning记录的是骨骼,一个角色模型骨骼再多也就几十到上百根骨骼而已,记录的数据就明显少很多。
说了这么多,最后说句老实话,如果要我写一套像GPUSkinning一样完整的解决方案,阿赵我还是很难写出来的,因为要配套写很多相关的编辑器工具,对于我来说,工具量还是非常大的。