Jim's GameDev Blog

可交互的植物

2016-6-9

要让场景中的植物模拟出随风摆动的动画可以为场景增加很多生机,这些动态元素使得场景不再生硬。要做到这个效果,方法就是在着色器中修改顶点位置。根据修改顶点位置的算法不同,实现效果好坏也有差异。一般情况下简单的代码就能够得到比较好的效果了。而我们这里讨论的是在此基础上更进一步,如何让植物和周围的环境产生交互(不仅仅是模拟风中摆动)。比如当角色走过草丛是,被角色衣服刮到的草会产生摆动,角色在草丛中跳跃释放技能时,技能的震波会影响到周围的植物。类似下面的效果。

img

img

上面两个演示效果中的植物是模型,而不是面片,实际情况中到底使用模型还是面片都需要慎重考虑。下面会介绍我所想到的四种实现方式,这几种方式各有优缺点,第一种方式我有实现过,后面三种都是思考阶段,并没有实现过。

方法1

上面演示图中的效果其实就是使用这种方式实现的。思路是,在应用程序中计算单个植物所受的外力,将这个外力使用 uniform 值传递给着色器,着色器使用 uniform 外力值对顶点进行偏移。就是这么简单,但是有几个问题需要想清楚。

问题1:DrawCall Batching

普通的随风摆动的植物是可以在一个 DrawCall 中绘制出来的(静态合并),因为使用的是同一个材质球。但是当我们调用设置 uniform 值,将外力传递给着色器时,就会出现一个问题,是设置 material 的值,还是设置 sharedMaterial 的值。如果是 sharedMaterial,那么意味值所有的植物收到了相同的外力,这显然不对,因为有的植物受外力了,而其他植物还是维持随风摆动的效果。如果设置 material 的 uniform 值,那就意味着原本的一个 DrawCall 将会被打破(Unity 会替我们自动 new 出新的 Material 对象),如果有十个植物受外力,将会新产生十个 DrawCall。

如果使用 uniform 设置外力的方式的话,DrawCall 这个问题似乎无法避免,但是可以相对地减少副作用。首先,我们可以使用 Unity 提供给我们的另一个接口,叫做 MaterialPropertyBlock,使用这个接口可以避免 Unity 自动 new 出新的 Material 对象的消耗,并且文档也说了这比直接调用 Materal 的 SetUniform 接口更高效。其次,当植物受外力时 DrawCall 注定被打破,那么当外力逐渐消失植物恢复原始状态时,能否让 DrawCall 也恢复到原始状态呢(一个 DrawCall)。当使用了 MaterialPropertyBlock 这个接口是就变得非常容易了,只需要调用它的 clear 方法即可。如果没有使用 MaterialPropertyBlock 接口,那就只能自己缓存和恢复 Material了。

问题2:固定点和移动点

并不是植物模型的每一个顶点会因为受外力而偏移的,一般来说远离地面的顶点会偏移得比较厉害一点,而接近地面的顶点有很少的偏移或根本不偏移。否则就会看到整颗植物都在移动,这样的效果显然是不正确的。所以这里我们用顶点中的 color 分量来存储一些信息,表示顶点的固定程度,固定程度越大(越接近0),越不会偏移。

问题3:应用程序中的计算量

对于着色器而言,由此带来的计算量几乎是忽略不计的。但是如何减少应用程序中的计算量还是比较关键的,因为每一帧都要不断更新所有植物的外力,所以一定要小心处理。比如你可以使用四叉树的或者栅格的方式,将场景中的植物规划好,这样就能最大限度的减少计算量,免去不必要的计算。

优点

就如上面的两张演示图所示,最终的表面效果还是比较好的。而且每颗植物都是真实的模型,而并非面片(Billboard),所以效果更真实。

缺点

正如上面 Drawcall Batching 部分所说的一样,当植物受外力时,DrawCall 被打破是不可避免的,所以当有大量植物都处于受外力的时候,DrawCall 会升高,当外力消失时 DrawCall 再回到正常值。

方法2:

一般为了减少三角形面数和渲染的压力,像小型的花草都会制作成面片,如果是十字面片是四个三角形,单面片是两个三角形。这时,我们可以将这种类型的植物看成是 CPU 计算的粒子(粒子其实也是面片)。直接在 CPU 阶段计算并修改顶点信息中的位置值,而计算操作甚至可以放到另以及线程中进行,这样主线程的消耗其实就是填充 VBO 了。这里有一点要注意了,如果有一百颗草,不建议将一百颗草合并到一个 DrawCall 中,因为只要其中一颗草的顶点修改了,就需要刷新整个 VBO,这样的数据量是很大的,所以比如将一百颗草分成十组,如果其中几组中一部分草的顶点修改了,只要刷新对应几组的 VBO 即可。

优点

效果可以接受,DrawCall 稳定,不会出现方法1中的情况。

缺点

由于是用面片来表现的,所以效果没有方法1来的好。只适用于面片模型。动态 VBO 也会有一定的消耗。

方法3

这种方法大概是这样的,有一张纹理,纹理中的每一个像素点都表示了当前位置外力的方向(RGB)和大小(A),在着色器中将顶点坐标映射到纹理坐标上,取出像素点数据(即为外力方向和大小),将外力作用于顶点偏移。同样有几个问题需要说明。

问题1:动态纹理

由于纹理中的像素需要动态更新,这个操作的效率直接关系到占用多少的 CPU 资源。首先我们可以降低纹理的尺寸,一般大片的植物使用 256x256 的纹理就足够了,小片的话 128x128 都是可以的,甚至更低,因为顶点植物模型的顶点密度本身就不高,我们也不需要做到像素点密度和顶点密度的对应。其次可以将计算颜色数组的操作放到子线程去做,只有提交纹理到 GPU 由主线程处理。

问题2:顶点阶段采样纹理

因为需要在顶点阶段采样纹理,所以你至少需要 OpenglES 3.0 或其他更高级的绘图 API 的支持。我有查阅过一些资料,说是顶点阶段采样纹理的效率不是很高,至于影响有多大,还有待验证。

问题3:表现效果的可控性

这种方式的可控性并不如想象中来的高。举例来说,一颗植物的顶点会被映射到多个像素点上,而这几个像素点所代表的力的方向有可能是完全相反的,这是植物模型就会被撕扯开,造成较差的表现效果。

优点

DrawCall 稳定。外力形式变化多样。

缺点

动态纹理有消耗。需要高级的底层 API 支持。表现效果有可能会失控。

方法4

应用程序通过 uniform 值,将外力传递给着色器,模型需要在顶点数据上记录一个索引值,比如记录在 tangent 或者 color 分量中,以便使用这个索引值从一堆 uniform 数组中取出外力值。

优点

DrawCall 稳定。不管是复杂模型还是面片都行得通。表现效果也可以得到最大化的控制。

缺点

着色器中的常量寄存器数量是有限的,需要合理规划下。同一时刻设置大量的 uniform 值也是存在一定开销的,并且这个开销还比较可观,需要注意。


以上四种就是我目前想到的能够实现交互式植物的方法,每个方法都有其优点和弊端,实际使用的时候还需要进一步评测。