《Unity3D高级编程之进阶主程》第五章,3D模型与动画(四) - 3D模型的变与换2

上一篇介绍了一些3D模型的渲染基础知识,并且简单分析了用这些基础知识在3D模型的切割、拉伸、扭曲当中的运用技巧。

技术原理是重要的核心点,如果我们理解了技术原理,就能将技术表象解释清楚,在实际项目中才能将这种技术运用自如。
现实中也并没有这么容易,理解和运用自如之间,还是有一段很大的差距。在实际项目过程中,理解只是理解,只是能解释这种表象是如何发生的,运用自如,则是熟知了原理的优缺点,不再对这种技术有任何的偏见,能做什么不能做什么,做什么有优势,有什么有劣势,有了更加深入的理解,当没有了这么多的偏见,运用起来才能得心应手。
理论上说,我们每个人都应该把所有精力先放在理解原理上,然后再去做技术实现不就轻松了吗。理论上是这样的,但人类头脑的进化毕竟还没到那么高级的程度,还不能轻松的把抽象的事物短时间内在脑中有条不紊的整理清楚,这还没把人的情绪影响算进去,人性毕竟是弱点多于优点的,虽然表面上能处理一些看上去相对复杂的事物,但从学习知识的角度看,人类的大脑还是不够高级,说实话应该说是有点低级。
因此对于这么低级的大脑,我们只能通过反复的学习然后实践,再学习再实践,很多遍后才能对原理的理解推进那么一点点,我们也只能依靠长时间的、漫长时间的磨练,从有兴趣到放弃,再重新拿起后又放下,反反复复但始终不离弃,才能最终到达运用自如的境界。其实人之间没有聪明与不聪明之分,有的人善于思考,善于举一反三是因为前面很长一段时间他都一直在练习这种多思考和举一反三的方式,当我们看到他时他的思维方式已经成型,这些并不是先天就有,而是通过后天努力得来。天才这东西,不管它是否真的存在,我们都应该忽略它,转而更加专注于自己的努力上。

===

这篇我们来讲一讲模型简化和蒙皮动画。

简化模型

经常在项目中看到有模型有几万个面的情况,模型面数多的好处是能表现的更加精细画面更细腻,坏处却是加重了渲染压力,渲染的面数越多压力越重,帧率就会越低。所以一般都会对场景中的单个3D模型进行限制,或者对整体场景面数进行限制,前面我们在美术资源规范中讲述了规范的方法这里不做重述。

画面质量和性能需要权衡,通常都是要求模型降低面数而画面质量不变,LOD(Level Of Detail)经常可以在场景模型的质量与性能平衡中发挥巨大的作用,它的原理是随着镜头的靠近模型物体精细度会逐级更换更细腻的模型。

简化模型是LOD比较常用的方法,我们可以手动用3D模型软件简化每个模型,也可以用程序的方式简化模型。在实际的项目中,手动简化更加平滑但费时间,很多时候时间成本过大而选用程序工具去简化,因为用程序简化更加快速,缺点是不平滑。我们可以根据项目的需求,规模,画质等要求来权衡是否使用程序工具去简化模型,或者更加灵活点,一部分不太需要细致的模型使用程序工具简化,而另一部分比较需要细节化的模型使用手动简化,这样即照顾到了画质,又照顾到了工期。

这里我们来具体讲述下简化模型的算法。虽然市面上简化模型的插件和工具很多,但如果我们对原理有了更深入了解,在实际的项目中运用这些工具会更加自如且得心应手。

模型网格是由点,线,面组成。面由点和线组成,减面相当于减点和线,单纯的减去点和线容易引起模型变化不受控制,收缩线上的两顶点成为一个顶点则更加靠谱些。[Garland et al. 1997]提出了一种基于二次项误差作为度量代价的边收缩算法,其计算速度快并且简化质量较高。

该方法是去选择一条合适的边进行收缩时,定义一个边的收缩都是有代价的,每个顶点也有自己的代价。为了计算代价,对于网格中的每个顶点v,我们先定义一个4×4的对称误差矩阵Q,那么顶点v=[x y z 1]的代价为其二次项形式Δ(v)=vQ。

这样同时也定义了边收缩的代价公式,假设对于一条收缩边(v1,v2),其收缩后v1,v2顶点收缩为v3,我们定义顶点v3的误差矩阵Q3为Q3=Q1 + Q2,也就说是v1,v2的这条边的收缩为v3后代价为Δ(v3) = v3(Q1 + Q2),以此类推每条边都有一个代价。

有了上面的代价公式,下面的网格简化算法就容易理解多了:

	1,对所有的初始顶点都计算它们各自的Q矩阵.

	2,选择所有有效的边(这里取的是两点有连线的边,也可以将两点有连线且距离小于某个阈值的边归为有效边)

	3,对每一条有效边(v1,v2),计算最优收缩目标v3.误差(Q1+Q2)是收缩这条边的代价(cost)

	4,将所有的边按照cost的权值都放在队列中从小到大进行排序。

	5,每次移除队列顶部的代价(cost)最小的边,也就是收缩最小代价的边,删除v1,v2,并用v3替换。

	6,重复1-5步骤,直到顶点数少于某个设定的值,或者所有cost代价大于某个值,则停止收缩算法。

似乎有点难理解,其实整个算法并不复杂,关键这里有两个核心问题需要解决,一个是每个顶点的初始Q矩阵如何计算,另一个是v1,v2收缩为v3时的坐标位置该怎么计算。在原始网格模型中,每个顶点可以认为是其周围三角片所在平面的交集,也就是这些平面的交点就是顶点位置,因此我们定义顶点的误差为顶点到这些平面的距离平方和。

由此定义我们可以计算出每个顶点的初始误差矩阵Q:Δ(v)为顶点误差值 = vQ = 0,这里的初始顶点的误差值为0,是因为它最初与相交平面的距离平方和为0,即没有误差,也就是说,Q为v的逆矩阵,于是初始顶点的误差矩阵Q就是v的逆矩阵。

至于v1,v2收缩为v3时如何选择最优的坐标,简单的方法就是取v1,v2,和中点(v1+v2)/2的三个中收缩代价最小的一个为最优选择,另一种策略则是数值计算顶点v3位置使得Δ(v3)最小,由于Δ的表达式是一个二次项形式,因此令一阶导数为0。

按照这个算法步骤,不停的收缩最小代价的边,直到顶点数量小于某个值时停止,最终将得到一个简化的模型。

蒙皮骨骼动画原理

场景中有了3D模型又会有3D模型动画,那么3D模型和3D模型动画之间到底多了哪些数据,这些数据又是怎么起作用的呢,我们来分析下。为了能更直观的了解模型与模型动画的不同,我们以 Unity3D 的 MeshRenderer 和 SkinnedMeshRenderer 这两个组件作为切入点来讲解。

在Unity3D中,MeshRenderer 与 SkinnedMeshRenderer 这两个组件分别用于渲染 3D模型 和 3D模型动画,他们两个的模型数据都存储在 MeshFilter 中,因此他们都依赖于 MeshFilter 组件。其中 MeshRenderer 只负责渲染模型,我们也可以称它为普通网格渲染组件,它从MeshFilter中提取网格数据顶点数据,而蒙皮网格(SkinnedMeshRenderer)虽然也渲染模型,也从MeshFilter中提取模型网格顶点数据,但蒙皮网格被引擎程序员编写出来主要是为了渲染动画服务的,所以蒙皮网格除了3D模型数据外还有骨骼数据以及顶点权重数据。

我们前面说过3D模型渲染的数据传递过程,这里 MeshRender 也同样遵循这种规则,即先从 MeshFilter 中取得网格顶点数据、uv数据、颜色数据、法线数据等,结合自己身上的材质球都发送给GPU,其中包含了许多OpenGL的状态设置,在指令最后是一个Drawcall调用告诉GPU按照传送的这些数据渲染(见渲染管线与图形学章节中详解讲解内容)。

蒙皮网格(SkinnedMeshRenderer)在渲染时也遵循了和 MeshRender 一样的渲染步骤,如果蒙皮网格上没存储任何骨骼数据,那么它和普通网格MeshRender的作用没有任何区别,渲染的都是没有动画的3D模型。

有很多人并没有理解骨骼动画的原理,所以在实际项目中对3D模型骨骼动画的运用有很多误区,这里我们有必要阐述一下骨骼动画的原理,以及在Unity3D的SkinnedMeshRenderer上骨骼动画是如何组装和组成的。通过对骨骼动画的原理解剖和对 SkinnedMeshRenderer 的解剖,我们能彻底的明白骨骼动画的计算和渲染其实并不复杂,揭开这层薄薄的面纱后是一片平坦的开阔地。

我们知道3D模型要做动作,首先是模型网格上的点、线、面要动起来,只有点、线、面动起来了,每帧渲染的时候才能在帧渲染时渲染不同的网格形状,从而才有看起来会动的画面。那么怎么让点,线,面动起来呢?对我们来说有两种方法,一种是用一个算法来改变顶点位置,我们通常叫它为顶点动画,另一种是用骨骼的方式去影响网格顶点,我们叫它为骨骼动画。这两种动画方式都是通过在每一帧里偏移模型网格上的各个顶点,让模型变形从而形成动画的效果的。当每一帧模型网格的形状不一样,播放时就形成了动画,两种方法虽然方式不同,但遵循都是同一个原理。

起初3D模型动画只有刚性层级式动画(rigid hierarchical animation),它将整个模型拆分成多个部位,然后按照层级节点的方式安装上去,如下示意图。

	[根结点]
	--[躯干节点]
	----[右上臂]
	------[右上前臂]
	--------[右手]
	----[左上臂]
	------[左前臂]
	--------[左手]
	----[头]
	--[右大腿]
	----[右小腿]
	------[右脚]
	--[左大腿]
	----[左小腿]
	------[左脚]

这样模型以层级的方式布置在节点上,这样当父节点移动、旋转、缩放时子节点也随之而动。这种方式的动画问题很多,其中比较严重的是关节连接位置常产生‘裂缝’,因为它们并不是一个模型衔接而成而是由多个模型拼凑起来的,我们可以想象在节点旋转时模型与模型之间的衔接重合部分产生的问题,无论怎么摆放部件的位置在节点做动画时都会有这样或那样的‘裂缝’出现,因此当时的动画比较不自然。

除了刚性层级式动画(rigid hierarchical animation),最初也是用过改变每个顶点的动画,意思为把每个顶点都记录下来每帧都告诉引擎怎么改变顶点位置,这种方式太过暴力也很少使用,但另一个变种叫变形目标动画(morph target animation)常使用在脸部动画技术,它将动画做成几个固定的极端姿势的模型,然后在两个模型之间的每个顶点做线性插值,脸部的动作大约需要50组肌肉驱动,这种复杂细微程度的动画用2个网格顶点之间的线性插值表现再适合不过了。更多时候会使用程序员们编写的顶点走向算法去改变每个顶点的位置,这与今天我们用着色器(Shader)在顶点函数中去改变顶点位置成为动画效果类似,这种方式的动画效果在很多游戏中都有在使用,当然它也已经成为优化骨骼动画性能开销的必要手段,比如草、树在风中的左右摆动,丝带或国旗在空中自然飘动等。

蒙皮骨骼动画简单易用,它的出现让3D模型的动画效果就变得越来越丰富多彩。骨骼动画数据主要由一些骨骼点和权重数据组成,游戏角色中通常骨骼动画的骨骼数量都不会超过100个,这个数量与动画制作速度有一定关系但更多的是跟性能有关。通过对这些骨骼点操作,在上3DMAX,Maya这样的好用的动画编辑工具,我们能够创造出许许多多丰富多彩的动画效果。骨骼数据是怎么起作用的,下面我们来分析下。

首先,骨骼动画由骨骼点组成,骨骼点我们可以认为是带有相对空间坐标点的数据实体,每个模型骨骼动画中可以有许多个骨骼点但根节点只有一个,我们在现代手机游戏中每个人物的骨骼动画的数量一般都会在30个左右,PC单机游戏中会更多点到达75个左右。骨骼数量越多,表现出来的动画就会越细腻越有动感,但同时也消耗掉更多的运算量。

其次,骨骼点是树形结构,一个骨骼可以有很多个子骨骼,子节点存在于父节点的相对空间下,每个子骨骼都与父节点拥有相同的功能,由于子节点在父节点的空间下,因此当父节点移动、旋转、缩放时子节点也随着父节点的一起移动、旋转、缩放,他们的相对位置、相对角度、相对比例不变。这与Unity3D中的 GameObject 的节点相似,父子节点有着相对位置的关系,因此骨骼点在Unity3D中的存在形式是以 Transform 形式存在的,这样我们可以直观的从带有骨骼的模型中看到骨骼点的父子挂载结构。我们在Unity3D的 SkinnedMeshRenderer 组件中就有 bones 这个变量用于存储所有骨骼点,骨骼点的存储形式在 SkinnedMeshRenderer 中就是 Transform 数组形式存在,这可以从 bones 这个变量就是 Transform[] 数组类型上得知。

另外,一个骨骼点可以影响周围一定范围内的顶点,单一一个顶点也可以受到多个骨骼的影响。其实除了骨骼数据,模型中每个顶点都有对它顶点本身影响的最多4个骨骼的权重值,在Unity3D中对这4个骨骼权重数据做了存储,它们存放在 BoneWeight 这个Struct结构中,每个 SkinMeshRender 类都有一个 boneWeights 数组变量来记录所有顶点的骨骼权重值,对于那些没有骨骼动画的网格就没有这些数据。

每个顶点都需要有一个BoneWeight 结构实例以确保每个顶点都知道被哪些骨骼点影响,在 BoneWeight 中变量 boneIndex0,boneIndex1,boneIndex2,boneIndex3分别代表被影响的骨骼点的索引值,而weight0,weight1,weight2,weight3则是分别代表被0、1、2、3索引的骨骼点所影响的权重值,权重最大为1,最小为0,所有权重分量之和为1。

[缺Unity3D的Quality Setting图]

如上图所示,在Unity3D中的 Quality setting 图形质量设置中,我们可以看到关于Blend Weights 参数,就是关于一个顶点能被多少骨骼影响的参数选项。其中选项中有,1 Bone,2 Bones,4 Bones,表达的意思为一个顶点能被1个骨骼影响,或者被2个骨骼影响,或者被4个骨骼影响,能被影响的骨骼数越多,CPU消耗在骨骼计算蒙皮的时间越长,消耗量越大。

未完,篇幅有限,下篇继续

· 书籍著作, Unity3D, 前端技术

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    《Unity3D高级编程之进阶主程》第五章,3D模型与动画(四) - 3D模型的变与换2

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号,文章同步推送,致力于分享一个资深程序员在北上广深拼搏中对世界的理解

    QQ交流群: 777859752 (高级程序书友会)