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

模型是游戏3D场景中的基础单位,它们变化繁多,这些3D模型除了骨骼动画、顶点动画外,很多时候为了画面效果我们还需要:切割,简化,变形,捏脸,飘动等等。那么它们是怎么实现的呢?我们今天就来讲讲这蕴含其中的技术与原理。

===

首先我们需要了解下3D模型的基础知识,基础知识很重要,花里胡哨的技巧在基础知识面前都是万变不离其宗的。

在模型的世界里,众多的顶点勾画出了一个完整的3D模型,顶点之间的连线组成了三角形或多边形,大部分情况下我们的模型还是以三角形为主,任意多边形网格也都能转换成三角形网格。三角形以其简单性而吸引人,相对于一般多边形网格许多操作对三角网格会更容易些,除了细分着色器和几何着色器这两个不常用的着色器可以处理多边形外,许多重要阶段的顶点处理方式都是以三角形为单位进行处理的。

这么多的顶点是怎么表达三角形网格的呢。通常使用索引三角形网格的表达方式,索引三角形网格它有两个列表,一个是顶点列表,里面存储了网格所有的顶点,另一个是索引列表,里面存储了所有成形三角形的索引,列表从头部到尾部依次排开,每三个索引指向三个顶点,这三个顶点代表一个三角形。除了顶点其实我们还需要其他信息,包括纹理映射坐标uv、表面法向量和切向量,顶点颜色值等等附加数据,这些数据都需要自己建立一个与顶点列表同样大小的列表来存储以便在顶点传入时提取相对应的数据传入到GPU处理。

三个索引指向了三个顶点组成了一个三角形,所以索引的顺序也很重要,我们必须考虑面的“正向”和“反向”从而决定我们是否要渲染它们。因此我们用顺时针方向列出顶点,以确保我们能顺利计算出面的朝向。

使用索引的方式表示三角形也并不代表图形卡中一定需要传入索引,10年前大多数图形卡都不直接支持索引而是通过传入三个一组的顶点来代表三角形,当时一个网格中有很多三角形且他们都有各自的领边共享的话,传入图形卡的数据会有很大的顶点冗余,一个顶点可能被当做多个三角形的顶点传入很多次。但现在不同了,我们所用的设备几乎都支持索引的方式渲染网格,我们只需要传入模型的顶点和构成三角面的索引就可以渲染出整个模型网格。

无论怎么样CPU与GPU之间的数据传输速率还是有限的,因此为了节省数据传输的消耗,通常图形卡都会有三种方式去做优化,第一种就是缓存命中,就像CPU的高速缓存那样,图形卡也做了缓存操作,当传入的顶点数据命中,即已经在缓存中的数据,则不必再传入数据,可以直接从显存中取,如果没有命中则需要传入顶点数据并临时保存于缓存中。其次是三角带方式,以共享边的方式把所有三角形排开每个顶点加上共享边则可以成为一个三角形,这种方式省去了索引表和也节省了顶点传入数据量,但很多复杂的多边形网格需要被拆分成共享边形式的数据并且需要在传入引擎做些预计算,灵活度相对比较低。另外一种三角扇,灵活度则更低,它以一个顶点为中心点,其他相邻的两个顶点与中心点的连线形成三角形,在复杂的网格上也需要拆分数据并且也同样需要对网格做预计算,这种方式与三角带的方式所需要传入的顶点数据差不多却更为不灵活,实际项目中很少使用。

因此顶点索引是主流的三角形表达方式,其他方式都是为了优化索引这个数组而设计出来的,应用的范围相对小一点。

顶点索引在程序中的表现为,把所有顶点放进一个数组里,再用另一个整数数组作为索引来表达三角形的组成(整数代表顶点数组里的index下标)。在索引数组里,每3个索引组成一个三角形,当4个顶点的数组表达了一个矩形网格时,这个矩形网格相当于两个三角形组成的面片,索引的数组大小就是6个,其中前3个索引表达第一个三角形,后3个索引表达了另一个三角形。

我们来看看这个矩形网格数据:(0,0,0), (0,1,0), (1,1,0), (1,0,0) 这个4个顶点构成了正方形。索引数组中的数据为,0,1,2,2,3,0 ,其中前三个0,1,2构成一个三角形,后三个2,3,0构成另一个三角形,每三个索引单元描述三角形的三个顶点。更复杂的网格数据与上述格式一样,在顶点数组中存储了所有顶点坐标的数据,在索引数组中每3个索引指向3个顶点构成了一个三角形网格,所有三角形网格描绘了整个网格上具体的面片。

我们再来完整的叙述一遍网格数据从制作到渲染的过程。首先通常3D模型由3D美术同学制作出来并导出成 Unity3D 能够识别的格式即 FBX 文件,里面已经包含了顶点和索引数据,我们在程序中将 FBX 实例化成 Unity3D 的 GameObject 后他们身上附带的 MeshFilter 组件存储了网格的顶点数据和索引数据(我们也可以通过自己创建顶点数组和索引数据,以手动的方式输入顶点数据与索引数据,就如我们上面描述矩形网格那样)。有了 MeshFilter 帮助我们存储顶点和索引数据,就可以通过 MeshRender 或 SkinMeshRender 来渲染模型,这些顶点数据通常都会和材质球结合在渲染时一起送入图形卡,其中与我们想象的不一样的是在送入时并不会有索引数据的送入,而是由三个顶点一组组成的三角形顶点送入图形卡。接着又图形卡处理我们送入的数据后渲染到帧缓存并输出到屏幕。

除了顶点和索引描述了模型的轮廓,我们还需要其他数据来渲染模型,包括贴图,uv,颜色,法线等。下面我们就来讲一讲这些常见的数据是如何作用在模型渲染上的。

那么贴图是怎么渲染上去的呢?

如果把3D空间中的三角形当做一个2D的面片来看待就会好理解一些,一个2D的三角形面片为了把图片贴上去,就需要在图片上也指定三个点,当贴图上的三个点形成的三角形与顶点三角形比例一致时,这部分三角形贴图贴到对应的顶点上后就会有顶点与图片的拟合,如果贴图上的三角形与顶点上的三角形比例不对称也可以贴,只是我们看起来会感觉拉伸或扭曲。这三个在贴图上的点坐标就叫uv,它们由2个浮点数组成,这两个浮点数的范围是0到1,0表示贴图的左上角起始位置,1表示贴图的最大偏移位置也就是右下角,一听uv二个字母我们就应该要知道是说的是图片上的坐标。

用这种三角形贴图的方式贴到3D模型的每个三角形上,就可以如期的绘制出有’皮‘的3D模型了。这样一来我们在绘制3D模型时,除了顶点和索引数组外就又多了另一个数组叫uv数组,这个uv数组是用于存储uv坐标而存在,由于已经有了索引来表达三角形的三个顶点,所以uv数组就不再需要索引来表达了,只需要按照顶点的索引形成的三角形来定制uv的顺序就可以了。

我们还是用简单的矩形网格数据举例来说明:

[(0,0,0), (0,1,0), (1,1,0), (1,0,0)] 这个4个顶点构成了正方形。

[0,1,2,2,3,0] 组成了顶点索引,它表达了2个三角形的形成。

[(0,0), (0,1), (1,1), (1,1), (1,0), (0,0)] 组成了uv数组,表达了两个三角形上贴图的绘制范围。

如果有一个带着材质球的贴图传入图形卡,这面这些数据将会在画面上显示这个正方形的图片。我们可不能小看这些基础知识,所有花里胡哨的技巧都是建立在基础知识之上的,这些基础知识在平时的实践中有很大很大的用途。下面就来介绍些我们在具体实践中的一些技巧,看看这些基础知识是如何灵活的运用在这些技巧上的。

顶点在拆分模型时的表现

模型切割也即模型的分裂,在游戏中是比较常见的手法。我们来说说如何用直线切割的方式切割模型,就如’切水果游戏‘那样,横向或纵向的直线切割。

我们知道在Unity3D中一个3D模型是由一个渲染实例构成,也就是说一个Render组件(MeshRender或者SkinnedMeshRender,这里统一称为Render组件)只能渲染一个模型。那么要把一个Render渲染的模型切割成2个,就相当于把这个渲染组件,变成两个Render渲染组件,从而渲染两个不同的模型。

有了这个大的方向就会容易多,我们可以把原来的Render渲染组件中的顶点数组、顶点索引数组,uv数组,都提取出来,并将它们分成两部分,一部分是切割后的左半部分,另一部分是切割后的右半部分。再把这两部分,分别放入新建的两个新的Render渲染组件实例中去,就得到了切割后的模型。在切割后对这两个切割后的模型加入碰撞体和物理运动组件(或者说重力引擎Rigidbody),可以让画面表现的更真实,像是一个有重量的球体被切割后倒地分成了两半那样。

这其中核心的关键是如何拆分成两部分,我们要知道一个顶点在左半边还是右半边,以及切割中在哪里生成新的切割顶点,如何将这新的切割顶点与原来的顶点集合缝合。

我们首先面临的是怎么区分点在左半边还是右半边,其实我们不用区分是左边还是右边,只要区分顶点是否是在平面的同一侧即可,方法可以通过矢量的点积值判断是否在平面同一侧,点与平面的法线的点积(Dot)值可以来判断是否为相同一侧。一般来说我们在切割时应该会知道切割平面,比如像切水果游戏那样在屏幕上划一下代表切割平面,滑动时我们可以知道滑动起点和终点从而知道切割平面的法线,以及从摄像机碰面出发的平面方向,因此可以利用这两个数据得到点积需要的数据。

public float PointDotClipplane(Vector3 point)
{
	return Vector3.Dot((point - touchEnd), planeNormal);
}

上述函数中用指尖的结束的点位、平面法线、模型顶点这三个数据来计算点积,得出的结果如果大于0则为一侧,小于0则为零一侧。当然这三个数据在使用前都必须转换到同一个坐标系中才正确。

其次我们需要计算三角形三条线是否与切割平面相交,从而来判定是否有新交点。我们前面计算过点在切割平面左边还是右边的结果,如果一条线段的两个顶点在点积时的结果方向不一致就说明线段与切割平面有相交,这样我们就能很快判定出哪些线段需要计算交点。如果线段有相交则需要计算交点,看起来复杂其实不需要害怕,平面与线段的交点其实也在线段上,因此我们只要计算出一个交点在从起点向终点推进的比例就能得出交点的结果,即t为比例时最终结果x坐标为 x = (begin.x - end.x) * t + begin.x,另外的y和z也是同理。所以关键点到了比例值t怎么求,它有几种方法,其中一种是起点到平面的垂直距离与起点到终点的垂直距离的比例就是t,另外还可以在xyz轴方向任意轴上计算起点到切割线的距离,以及终点到切割线的距离,他们相加的值为整条线与切割线的垂直距离,起点到切割线的距离除以整条线的距离得到的值就是t。

切割后中空部分需要缝合,由于缝合面上所有点是在同一平面,所以缝合时只需要缝合新生成出来的交点部分就可以了,因为其他部分还是保留原来的样子。

缝合的算法有好几种,主要的目的是将多新生成出来的点,有规则的组成新的三角形进而形成一整个切割面。其中一种相对简单的算法步骤为,选一个点用这个点与其他所有需要缝合的点形成的线段算出夹角值,用夹角的大小进行排序,排序后的结果也就是顺时针或者逆时针的点位,然后按顺序先将靠最前面的前三个点形成一个三角形,后面的点位与它之前的两个点形成新的三角形,也就是第4个点与前两个点也就是第三个点和第二个点形成三角形,第5个点与第4个和第3个点形成新的三角形,类似于我们前面介绍的扇形三角形,这样依次类推缝合切割面。这种不筛选顶点的缝合算法在凸多边形的情况下没什么问题,但在复杂的凹多边形情况下就会出现问题,因此我们仍然要寻找出所有corner点,即角上的顶点,用角上的顶点来做为起点缝合三角形会表现的更加合适。详细请参考资料(github.com/hugoscurti/mesh-cutter)

网格变形

扭曲模型的操作相当于将3D模型变形,例如将模型凹陷进去,或拉长凸出来,甚至对部分区域放大或者缩小等。

有了上面介绍过的3D模型的基础知识,这里在编写模型的变形时就显得更加容易。模型网格由三角形组成的,三角形是由点组成的,要变形就得移动顶点的位置,不只一个顶点的位置,而是一片顶点的位置。

扭曲和变形在实际项目中也有很多应用,爆炸后的地面凹陷,拉某个球时球有个先被拉伸再恢复的过程,以及用3D模型来表现制陶工艺,也有些角色扮演类游戏中,操控的摇杆就是用可拉伸的泡泡糖网格表现的,这些都是对模型内某个范围或者某些顶点的进行位移后表现出来的。

顶点的点位的移动相对比较简单,就是取出顶点数组,修改坐标,再放回去而已。难点一是准确找出需要修改的顶点,二是不同的点修改的值不同,找出修改顶点偏移量。

我们这里拿爆炸凹陷,球体拉伸反弹,制陶工艺这三个技巧来简单分析下。

爆炸凹陷

首先找出爆照范围的这块地面网格,并取出这块地面网格上所有顶点数据,对所有顶点求出在爆炸范围球体内的顶点,这些顶点就是需要修改的顶点。

其次来做顶点凹陷,凹陷算法的目的是把顶点位置修改到爆炸球体的球表面上,这个算法相当于,如何把一个点对应到一个球体的面上。

由于球面坐标可以用经纬度定位,经转化后公式为

x = cos(a)cos(b)

y = cos(a)sin(b)

z = sin(a)

其中a,b为经纬度,这样我们就能从原始顶点到球中心点计算出方向矢量,从而计算出经纬度,最后得到该顶点修改后的球面位置,计算出来后修改点位置再装入渲染的实例中,凹陷变形就完成了,顶点索引和uv都不需要任何变化。

球体拉伸与反弹恢复

在拉伸球体时,计算所有顶点与要拉的那个点位的距离,距离越大顶点需要的偏移量越小,这个比例肯定不能是个正比关系,一定是一个衰减的曲线,假设距离是d,拉伸的距离为f,结果为res,那么最简单的衰减公式为 res = f/(d * d),用这个公式对每个顶点进行计算,得到一个需要移动的数据res,这个数据是根据距离大小而衰减的,与拉伸点离得越近移动量越大,反之越小。修改完成所有顶点的坐标后,再推入渲染实例中去,就得到了球体拉伸的效果。

球体的拉伸后的放开恢复时,这里我们假定球体的屁股是被固定的。恢复和拉伸有点相似,拉的最远的反弹的快,也就是与原有的位置距离最大的点反弹的速度最大,也就是说,我们需要记录原来没被拉伸时的顶点的坐标位置,他们与原来的位置相减就是反弹速度的基础变量。

反弹力度肯定也不是正比关系,肯定也是类似衰减公式的增强公式,或者说是弧线比例,最简单的公式就做个平方,res = d * d,或者为了更平滑点,找个更好的曲线公式res = (d / max) * (d - 2) * k。

反弹恢复的力度,在不断得计算过程中,会由于顶点与原有的点位的距离缩小而减小,后又由于反弹过度而不断放大,有一个来回反弹的过程,最后恢复到平静不再移动的状态,用这个公式能就很好的体现出来,因为它与原顶点距离有关,力度在不断得衰减,最后形成稳定态。

最后再说说制陶工艺的模拟

一个罐体模型在转盘上不停的转,当用手(鼠标或者触摸屏)去触摸它的时,在触摸的点会形成凹陷,或者拉伸,人们通过在这样不断的凹陷和拉伸过程中制作出了一个完整的陶瓷的模样,这就是制陶工艺的过程。

在一个叫做《釉彩》的手机App中有具体的表现,里面你可以用一个很丑的泥罐,通过来回、上下、左右的手指滑动制作出一个你喜欢的陶瓷品,制作出来的陶瓷品可以让别人定做,也可以通过里面的超市直接购买。

它的原理非常简单,就是在当你触碰时,根据你的手指滑动的方向,把范围内的顶点向手指滑动方向偏移,并且有一个衰减范围,离手指最近的点越近拉伸的距离越大,离得越远拉伸的距离越小。顶点选取的范围的判定可以认为是在一个矩形中的范围,比如认定的手指滑动的矩形范围,从而构建出一个相应的立方体范围,进而选出在立方体内的顶点,再进行衰减式的位移,最终构建出,可上下,左右,对陶瓷罐的拉伸变形操作。

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

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

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

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

    Copyright attention

    Please don't reprint without authorize.

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

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