【SIGGRAPH 2019】Interactive Wind and Vegetation in 'God Of War'

本文为《战神4》中的风场与植被实现技术的学习文档,原文请通过链接下载查看。

这里给了一个《战神4》风场实现效果视频,请移步原文进行查阅。本文的演讲者Sean Feeley是索尼旗下圣塔莫妮卡工作室的一名资深TA,下面先来介绍下《战神4》中的风力实现方案,这里给出风力相关内容大体框架。

在引出《战神4》风力系统的目标之前,先来看下当前风力实现方案的现状:

  1. 在风力系统中,会给风指定一个全局唯一的方向与速度,方向与速度后面会随着此点对应的参数如贴图采样值而变化
  2. 给场景指定一张噪声贴图或者一个能产生类似数值的函数(procedural function),这个数据将沿着风力的方向滚动,这个噪声数据将用于对风场的局部数据(速度,方向)进行调整
  3. 计算出场景中各点的噪声数据(如采样上一步中的噪声贴图或者使用噪声生成函数计算出对应数值),并用这个数值来对此点的顶点数据进行移动

到此为止,一个最基本的风力系统就相当于完成了,而这也是当前大部分游戏项目的常见做法,而这个做法也是GPU Gem 3中介绍的Crysis所使用的实现技术的一个简化版本。总的来说,这个技术基本上能够满足游戏的所有需求,不过《大镖客2》对风力方案做了更进一步的改造与优化,受其影响,Sean Feeley也想尝试些新的东西,接下来会以这个方案作为基石,尝试通过对概念进行扩充来对这个方案进行多次整合与升级。

这里给出了一个使用前面介绍的基础风力系统方案实施的效果视频,总体来说效果表现良好,缺点在于可交互性(interactivity)(人,斧子对于草丛以及树叶的影响不够真实)以及动态效果(dynamism)(这一点作何理解?全场景中所有地方的风力看起来过于相似?)还有所不足。

那么,在这个现状下,还需要做点什么,为什么要这么做,升级后的风力系统对于游戏有什么提升,在《战神4》的主角荷尔蒙充足的前提下,如何将之与环境交互结合起来?

带着这些问题,这里列举了《战神4》对新的风力系统的三个目标或者说期许。

  1. 玩家应该能够感受到角色行为对于环境风力的影响(作为战神,理应对环境产生影响)
  2. 玩家应该能够明显感知到风力的方向
  3. 玩家可以感知到风力方向与速度在空间中的分布规律

这里给出的是应该能够,而非必须,因为不能让路过的风过于喧嚣,以至于喧宾夺主。如果做不到这三个目标,改造风力系统的成本可能就是不值得的。

在资源制作与工作流开发的过程中,除了上面提到的三个目标,还需要增添一些小目标:

  1. 最基本的,需要改善此前那种“只要有风,就给草增加起伏摆动”的效果。这里需要改善的不仅仅是功能,还需要为那些不需要全动态系统的情景提供更为真实可信的表现效果(没太懂这句话啥意思?不只是增加功能,还需要改善当前功能的表现?)
  2. 对于一个单独的与其他模型不搭界的模型,不管用什么样的风速,当风力经过这个模型的时候,其上面的表现都应该要是正常的
  3. 对于一组模型而言,当风从这组模型中间吹过时,各个模型之间的表现要是正常的,且相邻模型之间的风力表现应该要是连续的,且看上去令人信服的。

前面三个目标加上这里的三个小目标,对于最终想要得到的效果而言,限制条件就比较多了,因此这里降低一下实现方案的复杂度,这里追求的不是完全真实(PBR)的风力表现,而是看上去可信的风力表现。

总的来说,《战神4》工作组的实现方案完成了上述的目标,下面先来看下之前的测试场景中的一个对比效果。

视频展示,移步原文。

风力系统首先要解决的就是风力数据应该怎样存储下来,下面用一个launchpad(快速启动板)来描述《战神4》是如何实现上述目标的。这里用一个箭头来表示风力数据(箭头指向表示风力方向,箭头填充面积表示风力大小),由于每个地方的箭头都是相同的,因此所有位置的风力方向与速度都是相同的。

如果希望在场景中各个地方得到不同的风力,可以通过噪声来对风力强度也就是风力速度进行调整,如上图所示,各个箭头的填充面积都各不相同。如果用多层风力系统来进行模拟的话,效果会更好,当然,消耗也会更高。

当然,通过噪声还可以调整风力的方向,也就是如上所说的风力箭头。

但是,如果希望用风力来表示角色的行为或者一些局部的表现效果(比如爆炸之类的),还需要更为精细的控制。《战神4》通过使用一个volume space来实现对局部数据的存储与更新。

这里给出的是角色在游戏场景中攻击之后对风场的影响所对应的volume space在水平方向上的切片,其中在XYZ方向上的强度通过RGB来表示。

理论上来说,可以使用任意类型(2D或者3D都可以)的流体模拟结果来对每帧的volume space进行更新。对于《战神4》而言,使用的是3D的对流(advection)/扩散(diffusion)模拟结果来对volume space进行更新的。将角色的行为转化为风力向量,渲染成RGB,之后对结果进行模糊处理,以避免低分辨率导致的风力突变,再之后通过向下(即向靠近动力源的volume space切片)采样的方式来实现风力的传播与前进的模拟。

接下来由Rupert Renard来介绍《战神4》中的风力模拟实现。对于风力模拟中的流体仿真细节感兴趣的,可以通过图片中的链接进行深入研究。

《战神4》中的风力模拟的volume space覆盖范围很广:底盘是32 m x 32 m,高度为16 m。整个3D贴图使用的是1 m的物理分辨率,即使用一个voxel(体素)表示1 的体积。这里的voxel可能不一定是cube,不过经过测试,对voxel进行压扁或者拉高处理对于效果与性能并没有什么帮助。

考虑到相机后方的区域是不可见的,为了提高volume space的利用率,会将volume space覆盖范围的中心点设置在相机前方的某个点上面。由于依然还需要对玩家角色背后的区域进行风力模拟计算,因此这里只将中心点向前推移25%(这个数值跟场景与项目有密切的关系,可以根据自己的项目的相机与角色之间的距离进行调整,基本原则是需要保证相机在volume space的覆盖范围内),这种处理在纵轴上也会进行,因此就屏幕空间中可见的volume space对应的区域而言,其覆盖范围接近24 m x 24 m x 12 m。《战神4》中调整这套数值的目标是兼顾贴图分辨率覆盖范围以及斧子投掷距离,在保证贴图分辨率覆盖范围的前提下(避免覆盖范围过大,导致贴图物理分辨率降低,数据存在较大锯齿),覆盖范围越大越好。

经过前面的技术方案介绍,目前这里已经有了一套风力更新与计算的控制管理系统,接下来要解决的问题就是风的动力源的产生,也就是在实际应用中怎么样将这套管理系统应用起来。

《战神4》中将产生风力的几何图元称之为风力源(wind motor),风力源有着不同的类型,对应于不同的行为表现。

从形状上来看,风力源可以分成球形,圆柱形以及圆锥形三种,实际上圆锥可以堪称是将其中一个底面的圆的半径设置为0的圆柱。

在实际使用中,可以对这些形状的缩放尺寸,位置,强度进行动态修改(animation),最终生成的风力数据会被绘制到此前介绍的风力模拟volume中。

那么最终的风力数据是怎么生成的呢,这里一共提供了三种不同的风力分布模式:

  1. 方向风源(Directional Motor),风力方向在各个位置都相同,这个方向就是风源主轴方向(primary axis direction)(可与方向光类比)
  2. 径向风源(Omni Motor),在任意点的风力方向为从球心向此点引的射线方向(可类比点光)
  3. 涡流风源(Vortex Motor),任意点处的风力方向与主轴到此点最短连线相垂直,整体看来,像是围绕着主轴旋转

这里还有一种特殊的风力源:尾流源(wake motor)。这种风力源产生的风力方向跟其移动的方向一致,风力强度则与之移动的速度成正比。

这里是一个简单的尾流源的风力输出结果,不过如果我们希望将这个结果中风力分布的扇形区域扩大一点的话,就要将这个风力方向加上另外一个方向,即从球心到当前点的方向,如下图所示。

这个相加是等权的。

最终我们得到了如图所示的松果状风力数据。

另外,《战神4》这边在实现中会将前两帧的移动向量数据(motion input)进行平均后再使用,这种做法会使得一些弯曲比较明显的路径上的风力向量分布更为外向(目的是啥呢?避免风力堆积,不符合物理规律?)。这种做法的产生是一个意外,Rupert 在按照正确的做法计算的时候,失误的使用了当前帧位置与上一帧位置的距离来输出结果,后来发现问题所在后,修复了这个bug,后来发现有bug的版本表现更好,因此就保留了这种做法,不过这种做法是否有必要取决于项目组,不必要强制跟随。

下面来看下游戏中的一些结果展示。

尾流源的使用得到了很多不错的结果,使得这种风力源成为用得最多的一种风力源,比如将之与武器攻击所产生的效果相关联,就可以不需要添加任何逻辑就能够得到武器攻击对环境的影响效果。

在实际使用中,工作室专门只做了一个风力源动画表现library,可以在使用的时候跳过计算,直接取用,这里介绍的large burst例子就是library中的一个效果。

这里给出了测试场景中使用尾流源导致的风力扇形分布的更直观展示。在游戏中,本来预测应该会有大量的方向源使用,但是实际上方向源的使用并没有预想的那么频繁,比如某个boss战此前设计的需要大量的方向源,但是因为boss战所处的场景是寂静的岩石区域,风力表现基本等于没有,最后不了了之。

这个效果非常赞,可以看成是尾流源的教科书式的应用案例。

你可以明显感觉到,当玩家调整了其输出攻击的力度后,斧子飞出去攻击所造成的的风力强度也做了相应提升。

这里给出了一套攻击连招的风力表现效果,从视频中可以看到大量的尾流源是如何根据动画表现效果来对其他的物件(primitive shapes,这里指的是普通物件,还是其他的风力源,由于视频打不开,这里推测指的是普通物件)进行精细调制施加影响的。

理论上来说,可以为整个场景提供一个方向源以实现一个全局的风立场,这个方向源的覆盖范围甚至可以超出volume space的尺寸。

不过这个是《战神4》早期的制作想法,在ES 2016演示的时候用过,不过后来发现存在问题。

这种做法会导致一个不均匀的输出结果(uneven results),当相机顺着风查看场景时,风力会显得特别的强烈,而如果逆着风看过去,又会觉得风力很弱,虽然调参数调到吐血,也没能搞定这个问题。

这里尝试通过调试视图(Debug View)来分析原因,从这张图上看到,风力速度值存在一个梯度分布,风前进的方向上,越靠前风力越强,这就难怪会导致前面所看到的现象了。

那么为什么会有这种梯度表现呢,理论上来说不应该全局统一吗?这是因为在volume space中风力的确是往前吹了,但是由于边缘处没有持续输入风力,导致这些位置后继乏力,从而导致了这样的表现。

到目前为止,介绍的内容都是如何使用仿真效果来取代前面介绍页面中提到的基本风力系统。

而针对前面说的方向源表现问题,这里实际上是可以通过在基本风力系统定义一个环境风(ambient wind baseline,可以对比环境光进行理解)来解决。这个环境风的引入,可以去掉此前设计的大范围的风源,且可以使风力在超出volume space覆盖范围之外还依然存在。

此外,在这个基础上还可以添加上一层噪声控制机制,从而产生局部强风以及局部无风环境。

这里是添加了基本风力系统环境风之后的表现效果:

  1. 通过环境风模拟大范围的,极远处吹过来的风
  2. 通过动态调节的volume space控制的风源来处理近处的可交互风力效果

讲完了风力源的相关内容之后,下面要介绍的是风力接收物体的表现效果。

风力响应对象大概可以分成如图所示的四类,下面分别进行介绍。

配音对风力的响应是通过经典的WWISE属性来实现的:使用风速来对这个属性进行调节即可。

布料对风场的响应,《战神4》在这个上面的尝试自称并没有得到很好的表现:

  1. 《战神4》中的布料实现并没有得到很好的效果,不过后面会给出工作室做过的一些尝试
  2. 每个布料物件只会在transform center对风力系统进行一次采样
  3. 之后将上一步采样得到的风力应用到mesh上的每一个需要与风作用的顶点
  4. 根据风力方向与布料顶点法线的点乘来进行缩放处理,这个做法的目的是实现一套随着布料旋转翻折而变化的动画表现
  5. 跟基本风力系统的做法一样,这里还需要对噪声数据进行滚动更新

那么,为什么这套方案最终没有被使用呢?

对于新一代的《战神》,莫妮卡工作室做了很大程度的翻修,考虑到性能消耗与时间成本,在翻修的过程中做了很多折中,其中布料系统就是诸多不考虑进行系统升级元素中的一项,不过在加上风力系统之后,这套布料系统就暴露出很多的问题。为了得到令人满意的效果,就需要对风力在布料上的作用方案以及布料系统进行全盘考虑,而这个做法对于整个游戏来说,优先级比较低,因为在游戏中,布料还依然能够产生不错的动画效果,其中部分是非交互式的提前制作完成的仿真效果。此外,还可以通过直接跳过布料系统直接使用mesh receivers来实现交互效果。

总的来说,当前的布料效果暂时没达到非升级不可的地步。

这里来介绍下粒子系统与风力的交互效果实现。

每个粒子都会根据其所在的位置对风力的速度与方向进行采样,美术同学可以通过参数控制粒子对这些数据的响应程度(距离或者轻微)。从概念上来说,粒子受风力的影响表现跟受拖拽时的表现是一样的,不同的是,这次的拖拽时按照移动的流体的方式来进行的。因此一个静态的或者移动较慢的粒子在受到风力影响后会持续加速直到其速度与流体速度一致,而一个本身移动速度较快的粒子则会被牵扯降速到最终与流体保持一致。

在每一帧都会计算每个粒子要想匹配上风力的相关数据所需要的冲击力,并将其经过粒子自身的风力影响因子缩放之后应用到粒子上面。

粒子所遭受的风力是环境风与动态风源作用的叠加。

这里给出这套方案的测试效果,是早期的版本。在这个视频中给出了很多与风交互的响应效果,不过实际上,这些效果都是提前收工调制过的,所以这个视频看起来才这么好看。(这个测试场景中没有添加环境风)

这套方案虽然最终被采用了,不过斧子攻击时候的风源与设置与场景中的风源与设置达成了一个平衡,这个平衡虽然对于粒子效果来说算是可用的,但是其效果就不如前面一个测试场景中那么好看了。

.这个问题对于特效组来说并不算什么,他们正忙着激情四射的尽他们所能将所有东西移出系统(getting out of the system,啥意思?)

不过如果不考虑风力作用能够得到较好的效果的话,那么忽略风力的作用可能是一个不错的想法。从TA的角度来看,目前的方案过于简化以至于丢失了太多关键的控制效果。

如果想得到更好的效果,可以从如下两个方面考虑:

  1. 如果对环境风跟动态风源进行分开控制,可以忽略场景中环境风对粒子的影响,只考虑动态风源的作用。看起来像是一个馊主意,这也是为啥《战神4》没有这么做的原因之一,不过,实际上特效同学在制作特效的时候,是会将环境风力考虑进去的,因此不需要在运行时再重复添加一遍影响。而且,在部分特效制作完成的时候,场景的环境风场特性可能还没有确定下来,因此制作的特效表现可能不一定能跟环境匹配起来。最后,如果环境风过于强烈的话,可能会在特效还没来得及播放就被吹出屏幕了,对于特效的表现来说不太友好。
  2. 在风力添加到场景之前,已经对粒子系统添加了相关的噪声处理逻辑。这里的一个想法是,可以将这个影响单独抽取出来。此外,在不同的扰动设置(如在有风环境跟无缝环境)之间进行混合有助于修正在强风环境下制作的粒子翻滚的效果。

下面来介绍风力对mesh的影响,在这里要对一些概念进行重新定义。

mesh在场景中受风力影响的地方很多:角色,道具,伪布料等。在mesh与风力交互的系统中,总共包含五个关键的元素组件,其中两个是为动态风力源新增的。

这5个组件之间时如何关联的呢?

美术同学会对Vertex Data跟Model Parameters进行编辑与修正:

  1. 顶点数据(Vertex Data)会定义出mesh中的哪些部分可以受到风力影响,哪些不需要(需要工具支持)。
  2. 模型参数(Model Parameters)会规范模型受到风力影响之后的具体行为表现(最好为之建立一个library,方便使用的时候直接选择,而不需要从头调节)。

这两个数据在VS(Vertex Shader)中会被用到,且后面要介绍的Tech部分也会用到。对于静态风力系统而言,只需要这两个数据就足够了,但是对于动态风源(volume space控制的风源,下同),还有所不足。

对于动态风源而言,这里还为每个模型实例添加一个状态参数(state),这个参数后面会在VS中用到;此外,还需要使用Compute Shader(CS)对所有的state参数进行更新,这个更新过程包括对滚动的噪声的处理以及对原始的粗糙风力数据的平滑处理,这个更新过程可能会需要用到模型参数数据。下面来对这些组件进行细分介绍。

对于顶点数据,这里会使用四个浮点数来存储两个数据:

  1. 风力遮罩(wind mask),使用一个浮点数表示,用于表示风力对mesh运动的阻碍与衰减,这个数据通常表示当前顶点到根顶点(root vertex,当前顶点所在的branch上那个不动的顶点)的归一化距离,范围为[0, 1]
  2. 相对位置(relative location,相对于根顶点的距离),使用三个浮点数表示(x,y,z),这三个浮点数将用RGB(即每个元素的范围为[0, 1])的方式来表达。这里之所以存储相对距离而非绝对坐标,是在mesh数据发生移动时还依然能够保证数据的有效性(当然,如果发生缩放或者旋转的话,数据就需要进行更新了),此外相对数据有助于保证浮点数的精度。

下面来介绍下物件的一些相关参数。

这些参数可以分成三大类:

  1. 树叶(Leaf)参数,用于控制模型的形状与位置
  2. 树干(Settle)参数,用于对边缘部分在多帧之间的动画效果进行直接控制
  3. 树(Tree)参数,对于开启了这些控制逻辑的模型,这里会有第二套层级结构,这套层级结构会以物件的root为中心,因此后续的动画效果不再需要模型顶点数据,骨骼数据的支持(没太明白这个层级结构是干啥的)。

这里列举了众多的控制参数,功能是很强劲,但是如果调整不当的话,也容易产生异常效果,为了方便使用,必须要提供一套具有较好表现的默认参数。一个新的模型只需要在这套默认参数的基础上调整两到三个参数就能够得到很好的表现。不过在实际工作中发现,美术同学要想找到合适的需要调整的参数依然很困难。这里的做法是建立一套预先调制好的参数库,美术同学只需要根据模型特征对号入座选择对应的参数体系即可。

下面来看下这些参数在VS中的表现如何。

Movement scale参数表示的是顶点能够从原始的静态位置上所偏离的最大距离,这个数值越大,顶点受风力影响的时候飘动的幅度就越大,这个参数会受wind mask的调制影响。

顶点的偏移受到风力向量与噪声向量的同权控制(平均结果作为最终的控制向量),风力向量的控制会对顶点产生俯仰倾斜(lean)作用,而噪声向量则会对顶点产生摆动作用。(可以理解为一个控制上下浮动,一个控制左右摆动)

Density参数控制着噪声特征的强度。

这个参数通常只用两个值,一个用于表示低速运动,一个用于表示高速运动。当风速增加(最高可以达到20~30m/s)的时候,会对这些参数设置进行混合处理。对于大部分模型而言,这两个数值都是相同的,不过部分模型确实需要对于不同的风速给出不同的表现效果。

Bend参数用于控制模型受风力影响而倾斜时候弯曲的程度(或者说拉直straight的程度),一个较高的bend参数会将受风力影响的运动局限在模型尖端部分(tips)。这个参数将与此前所提到的wind mask搭配来使用,构成一个指数关系:(wind mask) ^ Bend,这个数值实际上是灰度wind mask上的一个gamma ramp(gamma曲线,参考显示器的gamma矫正曲线)

比如说,数值为1 的时候给的是一个straight piece,而数值为2的时候,给出的则是一个抛物线弯曲效果。

风力对mesh的交互式通过顶点动画实现的,也就是通过对顶点位置进行变换实现,这里不会像其他游戏一样对mesh进行旋转变换,因为旋转的实现会需要在shader中使用三角函数,这个操作消耗比较高。

不过,如果不对mesh进行旋转处理的话,可能会引入stretching问题(拉伸),因此这里在计算的时候,需要维持物件的长度不变(保长移动)。

这里的做法是,在进行位置变换之前,先检查当前顶点到root处的长度,并在保证当前顶点到root的方向不变的前提下,将之进行缩放到原始长度。

这样做可以避免物件与地表穿插,同时也能够展现一种在强力作用下的带阻尼的旋转(resisted rotation)效果。

不过实际上,这里并不一定要进行保长移动,可以根据一个参数来对拉伸效果进行线性插值,这个参数就叫做拉伸参数,下面给出了不同的拉伸参数下的效果表现。可以看到,拉伸参数越大,物件的飘动看起来更为柔软妖娆。

在部分物体上,添加一定程度的拉伸,受风力影响的效果表现要优于完全保长下的效果。这里给拉伸参数设定的默认值是0.2。

刚度参数Stiffness.

我们还可以通过对顶点uv坐标朝着root进行缩放的方式来实现一种僵硬(stiff)的表现效果。这个效果跟前面调节噪声密度后的表现很像,不同的是这里的动作效果会以root作为中心(即作为动作的起点,说实话我从效果上并没有看出明显区别来)。

刚度参数越大,物体上某个branch上各个顶点的偏移差异(方差)就越小;而branch之间的偏移差异却越大。

这里提供了两个用于调节物体弹性属性(spring property)的摆动参数(swary)。

计算过程在compute shader中只执行一次,之后将结果传递给vertex shader。前面说过,风力向量(wind vector)会对顶点进行倾斜(lean)处理,这个其实是假的。起初是打算从这个风力向量入手对顶点进行倾斜处理的,不过后面实际上是通过一个摆动向量(sway vector)来对顶点进行倾斜处理的,这个摆动向量(是摆动向量还是顶点数据?)会受风力向量的驱使(push around by wind vector)并被一个弹性力(阻尼作用)拉回到中心位置(center)

弹性力的强度就对应于摆动弹性参数(sway spring parameter)。

另一个摆动参数用于控制弹性力的阻尼效果(dampening of spring),这个会影响模型进入稳定状态的时间。

再回到Vertex Shader组件的介绍。如果在这里开启Tree(树木或者树干)模式,这里会额外再增加一层移动效果。

树木会绕着物件的root进行旋转,这里需要仿照此前为树叶移动过程中拉伸问题的修复处理方法,对树木的拉伸效果添加一层相同的处理逻辑。

为树木启用一个单独的bend参数,这个参数的表现效果跟之前树叶上的bend参数表现效果一样:对于归一化的距离进行指数处理,同样是一个伽马曲线。

笔直的树干在这个参数的作用下能够看到明显的区别,1.0对应于线性弯曲,而3.0则对应于三次方程弯曲(cubic bend)。

跟之前树叶的移动缩放比例一样,这里需要为树木增加一个移动缩放参数,这个数值表示的是树干在最大风力速度作用下倾斜时,尖端顶点能够偏移的最大距离。

树叶滞后效果非常有意思,如果不添加这个效果的话,在左边的这个图上可以看到,当树干进入到稳定状态时,树叶将笔直的干脆的毫不拖泥带水的进入到稳定态,非常的干涩。在这里,可以通过使用摆动冲量(sway momentum)来免费为树叶增加一个延迟的二级动画效果,而这个摆动冲量可以直接使用摆动位置来代替,这个数值之前在计算弹性力阻尼的时候就已经计算好了。

添加冲量之所以能够有效解决这个问题,是因为这个数值是位置的导数,而且因为sin函数的倒数是一条形状完全相同相位上有滞后的曲线,因此可以通过向着这个数值过渡(lerp)来添加一些时延效果。

而只有当风速开始减弱的时候才进行这项处理,因为只有这个时候,branch才是动画效果的主导者(leading the motion, 在风力加强的过程中,树叶跟树干都在努力飘动,所以没有必要,当风力恒定的时候,树干在恒定,树叶可能还在飘着,也没有必要,只有当风力减弱时,树干尝试回归到原始位置,树叶的飘动也开始减弱,这个时候添加这样一个滞后效果才显得必要,尤其是树干回归到原始位置前夕的时刻)。

自然界中,风力是一种应用到树叶上的作用力,树叶此时就起到一个船帆的作用,对树干产生牵引效果。不过当树干进入到稳定位置,此时作用在树叶上的则是树干的张力,同时,在拉扯的过程中,树叶还受到空气的阻力作用。因此只有当树干是动画效果的引导者的时候,才会产生树叶的滞后效应。

最后需要提到的一点是树干上的弹力:这里没有为树干另外再增加一个弹力参数。在树干模式下,树干的动画会使用此前导致树叶倾斜的摆动参数来对树干进行倾斜处理,倾斜的方向与风力方向保持一致。

下面来看下噪声函数的相关内容。这里使用的噪声,其基本数据来源于一张3D贴图,这张贴图会在游戏启动的时候计算完成,3D贴图的数据是一个个的随机3D向量,向量长度不超过1(所有向量都被限定在一个sphere中)。

3D噪声贴图采样时的过滤方式是三边线性过滤,因此,基本的噪声函数是一个C0(数值连续)的平滑函数,各个项目可以根据项目的需求更改噪声函数,后面介绍的技术应该都是适用的。

在基本噪声函数的基础上,使用分形噪声(fractal noise)来对之进行扩展,即对多个使用不同缩放比例的基本噪声函数进行叠加(如上图所示,最左边的即为基本噪声函数的输出结果)。

所谓的分形噪声,就是对一张噪声贴图按照不同的缩放比例进行采样(这些使用不同缩放比例采样得到的结果称为一个octave),用加权混合后的结果作为输出。

最常用的分形方法,就是对原始贴图的噪声按照2的倍率进行缩放,如512->256->128->64...,每级octave都是上级octave的一半;同时,由于噪声数据减半了,其对应的权重也相应减半。

上图中的黄色线条给出不同octave的权重的示意结果。

将多级octave加权混合后,得到的噪声结果包含了多级缩放比例的噪声数据,信号非常丰富。

噪声信号的缩放比较简单,只需要对贴图uv坐标进行缩放即可。不过这里有一个问题,由于这里噪声的缩放比例取决于风力速度,因此各个位置以及各个时刻的噪声采样缩放比例就是不一样的,使用uv缩放的方法来对噪声贴图进行采样,就可能会导致问题。

这里展示的采样结果中,左边的视频就是使用上面提到的uv缩放的方式得到的结果,可以看到,当各点缩放比例不同时,就可能导致采样后的噪声产生大小缩放的动态效果,这个并不符合风力波动的预期。

《战神4》这边给出了一个叫做logarithmic binning的解决方案,将采样点限定到POT(power of two),之后通过混合与过度(blend & fade)来得到最终所需要的效果,如右图所示。

除了logarithmic binning的方法,这里还可以使用另外一个解决方案(原始的解决方案是固定各个octave的权重,修改octave的缩放,之后加权输出),即固定各个octave的缩放比例,之后修改不同位置处的octave的加权权重(将上面的视频暂停,左边的结果就是这个方案的效果,可以看到,基本上能跟右边匹配上)。

下面给出Logarithmic Binning方案的实施细节。

这里给的是多个POT octave的噪声结果。假设美术同学设定的噪声采样值为1.3,那么距离这个采样值最近的POT octave级别为1.0,即这个octave将作为噪声的主要输出数据来源。

这里不使用指数权重(此前使用的是以2为底的指数权重,这里没有提不使用这个权重的具体原因,可能是考虑到消耗问题),而是使用三角形状的权重,这个三角形状的中心点在输入的采样值(即1.3)上,如上图所示。

这里一共选取4个octave做为采样的范围,之后将采样结果按照三角形权重进行混合,就能得到最终的效果。

回到前面提到的缩放比例会随着风速而变化的问题,按照这种采样方式,随着缩放比例的变化,采样所选取的octave会平滑的切入切出,最终输出的噪声数据不会出现此前的动画效果。

使用这个方法还有另外一个好处,那就是相邻的模型如果使用相近(而非完全相同)的缩放比例,那么在这些模型上的噪声效果将具有相似的滚动效果(没有一个直观的认识)。

上面介绍的是为单个模型上的风力交互效果所做的优化工作,下面来介绍多个物体群的风力交互效果。这一块的内容将集中在噪声滚动的compute shader算法。为了突出下面要介绍的算法的效果,这里使用的展示demo将使用一些非常狂野的配置。

在这个视频中,风力以一个稳定的方向按照一个稳定的速度吹着。

每个物体都对应于一个世界坐标——即物体中心所在的位置——以及一个uv坐标——即物体中心在噪声贴图上的对应坐标。

为了制作噪声在物件上滚动的效果,这里需要对uv坐标随着时间进行偏移处理,偏移方向与风的方向相反,如上面给的视频中,圆圈中的噪声看起来就是向着左下方移动的,跟风力向着右上方移动的方向刚好相反。每个需要与风交互的物体都需要记录其对应的贴图偏移,之后compute shader会在每帧根据风力的方向与速度对受到风力影响的所有物体的偏移值进行更新。

这个方法对于coherent wind(所有地方的风速与风向都相同)来说能够得到比较好的表现,对于一些比较复杂的风力效果,在短期内,也还能保持一个不错的表现,而随着时间的增加,效果开始变得随机,到最后完全不知道风在往哪个方向吹,在这个时候,如果将风更改回coherent wind,效果并没有随之恢复,为啥呢?

这里以两个在世界坐标上相邻的物体为例,这两个物体在噪声贴图上的uv坐标也是相近的。这两个物体重叠的区域,将拥有相同的噪声特征,而即使噪声贴图向前滚动,这两个物体重叠区域的特征也依然是相同的。

不过,如果两个物体的噪声滚动方向存在区别,那么随着时间的推移,这两者在噪声贴图上的uv坐标就开始渐行渐远,不过在世界坐标上,他们依然是相邻的,这就导致一种分裂,使得噪声贴图在世界坐标上开始不连续,这就是为什么在短期内,风力效果表现还不错,但是随着时间推移开始群魔乱舞的原因。

对于方向相同,但是速度不同的物体,也有同样的问题,这里给出的解决方案是周期性的对物体的uv坐标进行重置。

这种重置机制可以通过flow map来实现。flow map被广泛用在表面材质以及天穹(skydome)上。这里展示的这个远景流动效果是莫妮卡工作室的一个环境美术(environment artist)制作的。

这里的基本思想就是,按照一定的速率对贴图采样的uv坐标进行回放(rewind,即移动到一个位置之后,将之切回到此前经过的某个位置再次重复移动)来解决这个问题,不过这样做可能会导致效果的跳变(即从后面的某个位置突然切回到前面的位置,由于不连续就会导致跳变),这个跳变可以通过两次rewind来解决(即如上图所示,使用一前一后两个采样点,在前者进行rewind的时候,后者还在正常移动,两者叠加可以消除rewind导致的跳变),在两个采样点之间进行淡入淡出过渡可以通过对完全淡出的采样点进行rewind来避免rewind导致的跳变。

这里将这里给出的这种重置措施命名为flow flip event(倒带事件)

这里按照前面的rewind策略来实施噪声贴图采样(每个物体使用两个采样点,通过淡入淡出加上合适时机的rewind来避免效果紊乱),可以看到之前的紊乱问题已经不见了,不过引入了另外一个问题,那就是风力吹动的效果开始出现循环重复,当然这个问题也可以解决。

这里使用的白噪声贴图在各个地方的光谱属性(spectral properties)是相同的,因此这里不一定非要将uv坐标rewind到之前的起始位置,不过考虑到这里依然需要保证相邻物件依然具有相似的表现,因此这里尝试给出一套全局统一的随机rewind策略,这套策略每帧计算一次,将结果共享给所有物件使用。

可以看到,通过这个策略,原来的重复效果消失了,这种策略相当于使用3D数据得到了4D的噪声贴图,实际上新增的随机偏移值就相当于第四维坐标,通过这个第四维坐标来对原始的3D坐标进行偏移,并在偏移前后的坐标采到的贴图数据中进行混合与淡入淡出,得到最终的结果。

下面看下飓风输入下的效果表现,在这个速度下,使用0.5秒一次的倒带率(即每0.5秒进行一次rewind)可以得到不错的表现,不过如果降低风速而维持倒带率不变,这里草地的动画效果是由于噪声混合导致而非噪声滚动。

在这个速度下,使用单个噪声采样点加上噪声滚动得到的效果看起来比两个采样点+rewind策略得到的效果要慢一些。因此这里可以通过增加rewind前的时间长度来解决这个问题,这里将时间长度调整到5秒中,可以看到效果变好了。

如果再次提高风速呢?好的。。。前面的问题再次出现了,看来调节时间的方式不适用于风速变化的场景,需要找一个更稳定一点的解决办法。

这里先回想一下,flow map到底想要解决什么问题?这里给定一个时间按照一个速率来进行rewind,不过这里想想,时间参数真的是正确的吗,我们是如何知道我们应该调整这个数值的呢?

实际上,我们这里是在发现移动过于缓慢或者当动画产生紊乱的时候才需要进行调节。因此这个才是我们真正需要调整的东西:即需要定义一个最大紊乱程度参数。

这个参数可以定义成自从上一次rewind之后,在贴图坐标上移动的距离。不过这里还有一个问题,如果由于速度不同而导致相邻物体跟当前物体发生分歧,那么相邻物体rewind的周期跟当前物体rewind的周期也会不一样的,看起来有点像是汽车前面挡风玻璃上的两个雨刷从同步变成不同步。

这里需要一个方法来讲相近的速度打包(bundle)到一起,之后通过规划保证这些速度对应的rewind发生在同一帧,如果我们能够知道如何对所有缩放数据进行分组的话(group numbers together across all scales),或许就能够做到。

这里说回之前提到过的logarithmic binning算法。这里只考虑风的速度是POT(power of two)情况下的倒带率。

对每个风速,这里规定其行为表现与他们对应的最近的POT速度的行为表现一致。速度越快,rewind频率越高,可以看到处于同一个桶(bin)里的速度(比如0.8跟1.3),其rewind速率是相同的。

logarithmic binning算法的优势在哪个地方呢?那就是相邻桶里的速度通常是按照两倍或者1/2的速率进行rewind的。也就是说,通过两次rewind,最终能够同步上,这个特性对于速度相近但是却并不在一个桶里(比如0.7跟0.8)的物体而言非常重要。

如果风速是可变的,且大多数时候是恒定的,那么所有的物体都会呆在他们最开始分配的哪个速度桶内。而当某个物体的风速发生变化,这个物体就会开始切换到另一个POT桶里,之后就会发现这个物体与新桶内的物体并不同步。

为了解决这个问题,这里会调整新成员的淡入淡出速率,从而保证在下次rewind的时候,这个物体能够跟此前桶内的物体同步上。从视频中可以看出,落后的物体通过这种方式可以快速赶上,而超前的物体则会放慢速度。

需要注意的是, 因为风速是逐渐变化的,所以这个调整跳变的,因此物体通常是会从一个桶里过渡到相邻的桶里,这就意味着淡入淡出速率的的变化很少会高于一阶(2倍或者1/2)的。

这里给出的是这种方案的一个例外情况,如果物体进入到一个正准备rewind的新桶里,且这个物体的速率远远落后于新桶里的速率的话。

这种情况可以通过如下方式来定义:新桶已经走完rewind的75%的路程,而新加入的成员还没有走过rewind的25%的路程。

对于这种情况,这里的做法是放弃本次rewind同步,瞄准下次rewind同步,这种做法有助于避免突然的速率变化(在效果上对应于风力的突变,看起来很怪异)。

到现在为止,噪声特征的表现就稳定了:不论是coherent wind还是Divergent wind,不论是哪种风速;而且累计的噪声紊乱最终会优雅的恢复回来。

这里是早期开发版本中飓风下的rewind效果的表现,每次rewind的时候,物体由于跳变看起来像是从地上蹦起来似的。

可以看到:

  1. 距离圆心越远,rewind速率越高,反之越低。
  2. 相邻桶内的成员会按照一半或者一倍的速率进行rewind
  3. 同一个桶内的物件具有相同的phase,因此可以在同一帧内完成rewind。

下面要介绍的是一些杂项内容,比如风力系统开发的整个周期中的一些trick方法。

为了给美术同学解释这套系统的一些概念,这里可以考虑跟光照相关的概念进行类比来给出一个直观的介绍。

Wind Volume对应于Shadow Cascade,如果风源不在box中,就跟物体不在shadow cascade中一样,对于场景来说,就不会有任何贡献。风源就像是点光跟聚光灯等光源。环境风对应于方向光或者环境光,wind receiver对应于光照系统中的材质,用于表明物体是如何跟风力产生交互作用的。

这里还制作了一套用于对顶点数据进行编辑的工作流(工具),这套工作流对于90%的模型都能够提供很好的帮助。先选择需要编辑的模型,之后对于想要标记为锚点的顶点打上tag。这里的顶点色工具会根据当前顶点到所连接的最近的root的归一化距离来计算wind mask(用于标记风阻的数据),这里会对每个模型上互联的mesh piece进行单独的归一化处理。

如果需要对一些物体进行组合处理的话,美术同学将那些本身无连接关系的模型合并起来进行计算。而有时候,甚至根本不需要手工选择root vertices,而是通过算法推测给出。隐式的root顶点可以通过uv坐标在xyzuv轴上的数值来推导,比如采用局部最大值或者最小值规则计算出来,或者通过举例某个指定的表面(如地面)或者点坐标最近的顶点。

此外,还可以将手工指定的显式root与自动计算的隐式root结合起来使用:如果自动推测算法命中率高达90%,那么美术同学可手工标记出那些可能需要修正的顶点,之后再次运行算法进行矫正即可。

在开发过程中,会经常需要解答一个问题:到底什么样的风速适合当前编辑的模型呢?这里的一个不成熟的解答是:所有的风速,需要保证在所有的风速下模型的表现都能够撑住。

为了统一一下观念,这里给出Rupert所发现的Beaufort Scale。这个是水手在19世纪早期开发出来的用于描述12级风速作用下大海跟陆地区域的表现,这个对于上面的问题将是一个非常好的参照。

12级这个粒度可能太细了,这里将之缩减为5级,并用一个不算特别直观的名称来给出风的强度:

  1. 无风-Still(一点风都没有)
  2. 细风-calm(比较轻的风)
  3. 微风-Breezy(已经能够感受到明显的风)
  4. 强风-Strong(风已经非常剧烈了)
  5. 暴风-Violent(帽子已经飞了)

这里在工具上增加了相关的风力调节按钮,所有人都可以通过调节按钮来测试模型表现。有了这么直观的风速分级,前面提出的问题就算得到了解决。

这里是五级风对应的风速,不过数字实际上不太重要,因为最终都是对应于贴图滚动速度,而非真正的风速的表现(前面说过这里没有做成PBR的)

这里提一条建议,在调试模型在不同风速下表现的参数时,对着屏幕吹一口气,想象风吹动的方向,有助于找到感觉。虽然这种做法看起来比较蠢,不过据演讲者说确实非常有效。

游戏场景中存在一些尺寸非常大的树木,在风吹的时候的响应幅度可能比较小,表现不太好看。这里发现,如果将风力单独作用在树木的分支(branch)上,效果会更漂亮。也就是说,每条分支都会对风进行独立采样,这种做法还有其他的作用,比如当斧子砸到分支上,如果没有前面的分开独立采样的话,那么就会导致整棵树的晃动,这是不符合逻辑的。

这里是每个分支在不同设置下的表现效果,图中上面的那个分支,不开启树干模式,顶点数据改动只考虑从分支root到树叶尖端的梯度。

下面的两个分支会打开树木模式。上图中,左边的模型跟上面的分支使用相同的梯度,而右边的模型使用的是一个从分支核心位置(branch core)逐渐远离的(radiates away)的梯度设置(这是《战神4》最终采用的版本)。

在使用前面介绍过的一层额外的树木动画数据来对branch进行动画模拟的时候,具体做法如下:

1.branch的顶点数据生长趋势最好能够跟branch的uv数据在基色贴图中的生长趋势保持一致(用于计算梯度吗?),毕竟,如果在对应位置没有顶点数据,就不能将之标记为一个pivot。

2.之后用这些顶点来创建一个从branch core辐射出来的梯度,这个梯度比线性梯度要弱一点(rubbery)

3.不过有时候,会在保留左边的pivot的同时,对两者的mask进行混合,效果如下图中间方案:

这里给出上面三种梯度作用下的表现效果,可以看到,通过更改顶点色生成的方式,可以得到不同的但是同样赞的效果。

左边的模型是branch core的辐射梯度的表现,右边是线性梯度的表现,中间是两者混合后的表现。对于这个模型而言,《战神4》采用的是中间版本,不过实际上左边的这个版本也非常不错。

某种方法看起来可能比较蠢,不过如果真的有效,那么就不算蠢,这里再来看一个例子。

上面这张图中悬挂的横幅以及绳子,实际上是用一棵从上向下生长的树,你敢信?

《战神4》本来打算专门为此类物体增加一个悬挂移动的设置,不过由于弹性设置(springs)的效果看起来就已经足够了,所以这个功能后面就并没有继续制作了。

下面来介绍下头发与风力的交互作用。

如果每根头发上的顶点的pivot,都使用其距离头皮上的最近的点作为root,那么是能够得到正确表现的,这种方法跟接下来要介绍的方法不是一个方法,这里的pivot不是头发mesh上的顶点。

《战神4》中将顶点到头皮的距离作为顶点的wind mask(风阻),之后对整个头发mesh的wind mask进行归一化(即除以最大值吧?)。在这个视频中,左边的模型使用的就是这种技术,而右边的模型则是将头发的原点作为pivot得到的表现效果,中间的模型则是使用此二者的混合结果作为输出的表现效果,在这个角色上,《战神4》选取的是左边的方案。

下面再看下皮毛跟风力的交互实现,其实现方案比较tricky。

皮毛通常是使用一个个的四边形或者三角形来进行模拟的,这也就意味着此前的弯曲参数(bend parameter)应用到这个上面之后不会有任何效果,因为不管对0或者1应用任何指数,其结果都维持不变(0^x = 0, 1^x = 1)。(四边形,三角形,为什么就变成0,1了?bend parameter的使用方式:(wind mask)^bend,而在三角形或者四边形的情况下,各个顶点的wind mask要么等于0(即此顶点在高度上与pivot相等的时候),要么等于1(即此顶点达到最大高度的时候))

皮毛元素通常是与皮肤紧紧挨着的,也就意味着,即使轻微的位移都有可能导致皮毛与皮肤的穿插,而此时并没有一种弯曲效果(bend)能够应对这个问题。

这里给出的解决方法是,依然使用之前的pivot方法,不过,这里的wind mask不再是从顶点到面片的底部的距离,而是跟头发一样,用顶点到皮肤的距离作为wind mask的距离,之后对所有的皮肤面片的wind mask进行归一化(而非如前面的对单个面片进行归一化),这样就能规避前面无法弯曲的问题了。

在归一化之前,会计算毛发上的最大距离,之后用这个数值乘上0.9作为模型受风力影响时的参数(即用作归一化时候的被除数?为什么要这么做?可以提高风力影响的强度,避免一些短浅的毛发在风吹过时没有反应),不过归一化的时候,要注意clamp到1,避免超出1.0的wind mask数值出现。

胡须受风力影响的表现跟皮毛很像,不过相对于皮毛而言,组成胡须的面片(card)会包含多个span(不知道这个span是啥意思),至少这里给出的这个模型是这样的,而这种结构的胡须在风力作用时能够得到比较好的效果,因为在胡须模型上,bend parameter是有效的,且最终测试使用的指数数值比较大,Krato的胡子使用的指数是3.0,而皮肤上的毛发有多个span,其指数为3.3。这种较大的指数可以使得胡须内部保持紧密,而胡须末端则能够随风摆动。

为了让胡子能够对脸颊的轮廓起到一个塑形作用,通常会给胡子一个足够大的移动量。虽然在这种设定下,胡子有可能在风力作用下刺进皮肤内部,比如刺到人的嘴巴里,但实际上并没有发现这种现象。

《战神4》中的部分带胡须的模型的胡须动画效果是按照卡片的形式制作的(shell under the cards),不过这里实际上希望胡须的表现是紧密的紧贴皮肤的,而非像卡片一样到处移动。

后面会介绍《战神4》中的一些植被相关的工作。

对于植被的LOD以及阴影渲染,这里使用的一种impostor-grade解决方案,这种解决方案跟风力的交互时表现还算不错。

这种技术就是常说的公告板云(billboard clouds),当听到这个概念的时候,脑海里浮现的应该是上面这张图吧?

就是那种毛茸茸的,永远朝向相机的贴图四边形。。。然而,并不是的~

实际上,这张图才是billboard clouds的含义(实际上,这里的cloud并不是指的真实的云,而是指数目多的意思,比如点云,就是很多的点组成的一个集合,看起来像是堆积成云而已,但实际上跟云没有任何关系)。为了避免引起误会,这里将之用另外一个名称来描述:面片簇(card clusters)。

这种技术相对于公告板那种world-axis aligned面片,有着更为复杂的定义与实现。在《战神4》的实现方案中,主要参考了这两篇文献。

这个方案是在二十世纪九十年代中期提出来的,是通过解答下面两个问题来给出的:

  1. 如果这里使用更多的面片会有什么样的表现?
  2. 如果这里使用的正好就是我们所需要的面片,效果会怎么样?

对于那些立体几何模型,比如带叶片的植被,使用公告板制作后的效果还是非常令人满意的,这里展示的模型中,有一个是原始面片,其他模型都是以此面片作为基础通过叠加得到的。

在这个技术的实施过程中,有一个质量与消耗的平衡点。

对于一个输入模型,如果我们想按照前面的叠加规则来制作面片簇3D模型,那么我们如何才能找到最佳的原始卡片呢?

很简单,只需要在当前的经验体系下,对所有的卡片平面(card planes)进行检测,之后给每个卡片平面打分,得分高的就是这里要找的最佳模型。

一个潜在的卡片(potential card)可以看成是模型的一个厚厚的切片,这个切片的过程会将模型上跟卡片的平面接近平行的面片以一定的误差范围投影到这个卡片上,而其余的面片则投影到其他卡片上。

卡片的得分规则给出如下:这个卡片捕获的模型越多,得分越高,投影到卡片上的面片的形变越小得分越高。

从数学上来看,这个得分可以量化为模型面片投影到卡片平面上的面积之和。也就是世界坐标系下的面积乘上两者法线的点积。

这个寻找的过程可以用一个贪心算法来完成。

不过这个计算过程会有较大的计算量,能不能对这个算法进行加速呢?所有卡片空间是一个六维的plenoptic函数。这里可以将之缩减到两维:即卡片朝向以及卡片到物体中心的距离。其他的平面都是多余的。

之后对候选的卡片平面进行等距分割,分割的距离与误差范围成正比,如果能够忍受较大误差,也可以使用较少的分割。

对于方向而言,这里使用的是geodesic sphere的法线。由于反方向(negative direction)是多余的(因为是double-sided rendering的,所以反面可以直接复用正面的card),因此这里只需要一个半球就够了。对于这样的法线候选而言,很容易对候选数目进行扩展或者收缩。同时geodesic法线分布也避免了在极点(pole)处的过度采样。此外,使用一个斐波那契旋转(Fibonacci Spiral)线也能给出一个不错的结果。

如果卡片平面的朝向不发生变化的话,那么植株上的每个面片投影到这个卡片平面的面积也就不会变化,因此对每个朝向上的多个平行平面就可以批次(orientation batch)提前计算出每个面片的面积(即一次计算,多次复用)。当然,这里会忽略掉那些距离植株比较远的卡片,这里参与计算的都是与模型的bounding box相交的卡片。

我们渲染impostor的时候,都会使用正交相机来进行绘制。每个卡片在制作的时候,都应该调节相机位置,朝向以及覆盖范围,以实现与卡片内容的匹配。这里还需要注意裁剪平面的影响,远近裁剪平面之间的距离对于误差是有影响的(因为会导致深度贴图精度发生变化,从而导致最终渲染到各个卡片上的结果发生变化)。

最终输出的贴图是通过Python图像库以及Maya的UV auto-layout功能生成的。

最后,贪心算法给出的结果有时候并不是一定正确的,比如植株上某个靠近卡片的面片可能会给出一个比较高的分数(即投影面积足够大),但是实际上这个面片并不一定适合投影到这个卡片上。

在确定了最终所使用的的卡片之后,再回过头来检测某个面片对于另外一个相近的卡片的得分是否更高就会变得很容易(即使用贪心算法作为初始pass进行一遍预分配,之后通过一个额外的pass对其他候选card进行比对检查以对结果进行调整来得到更为精确的分配结果)。

如果找到了一个更好的卡片,这里会在渲染贴图之前,切换过去。这样做有助于避免使用前面的卡片时的裂缝问题(gap)。而且,使用新的更好的卡片进行绘制对原始模型的视觉密度的保留程度要高于将面片绘制到多个卡片上(这句话没太懂)。

*** ( which is, unfortunately, what we were doing when I captured all of these examples. What we shipped was better. Sorry for no proof here ).

在这个测试场景中,这里给出的是面片簇(card cluster)版本的树木效果,当角色走近的时候,会自动过渡到原始3D模型。

对于阴影的渲染,这里使用的一直是card cluster,因为即使不切换,反正也不会有人一一核对像素是否正确,不会有任何问题。

card cluster相对于impostors的一个优点是,可以实现对风效的模拟,同时还可以保留视差效果。这些效果是通过从基础模型传递到card cluster模型的一个顶点属性来得到的。虽然不是精确的一一映射,不过从表现上来看,能够提供一个不错的风力交互表现,尤其是在阴影部分。

如果对于card cluster继续深入下去的话,就会在边缘上发现一些偏差(matte edge inaccuracy)(边缘部分比较黑),这里需要对贴图进行flooding处理。这个过程通常包括以下几步:

  1. 通过对图像进行膨胀(dilating)以及模糊处理
  2. 在上面叠加上原始图像
  3. 重复上面两步直到满足需要

最终的结果看起来好像还不错,不过玩家不关心这个,重要的是,这个过程计算太慢了,尤其是对于大尺寸的图像而言。

这里输出的结果在边缘上与原始贴图的边缘颜色存在一些偏差,除了一些非自然的场景之外,可能不是很明显,不过这里是否可以做得更好呢?

这里给出一种叫做mip flooding的优化方法,这种方法执行效率更高,不过要求输入贴图的分辨率是POT的。

对于每个贴图,使用的是没有做任何预计算乘法的颜色数据:

  1. 将之下采样到其一半的分辨率,根据各个像素的alpha来对采样点进行加权输出
  2. 重复上述过程,直到最终输出的贴图分辨率的高度或者宽度等于一个像素为止

这里建议使用浮点数格式贴图。经过上面的步骤之后,得到一个mip chain,接下来对这个mip chain按照分辨率从小到大的顺序,使用最近邻算法进行上采样:

  1. 将高分辨率贴图的采样结果覆盖在低分辨率贴图的采样结果的上面(不考虑alpha的吗,那么不如直接采样最高贴图分辨率贴图算了?应该是需要考虑alpha blend的)
  2. 重复上述过程,直到最高分辨率的mip贴图也被处理完为止

由于对数时间复杂度,贴图处理的速度很快。而由于输出贴图上的大面积常量颜色(即大片的同颜色像素组成的色块),其最终输出到硬盘上的压缩率也会非常高。

如果在上采样过程中使用双线性采样算法的话,这个算法还可以用于快速的平滑的填充贴图中的hole(这些hole是哪里来的?)。

像素grid的对齐方式对于最终的输出结果有很大影响,so video could be tricky(没看懂这句话啥意思)

这个算法通过程序化的方式实现了植被的减面,其优点是植被渲染的面数降低,填充率同时也可能会有较大幅度的减少,阴影渲染可以得到很大程度的优化;其缺点在于近景可能会有质量损失,同时由于新增了Atlas用于承载Card Cluster的颜色数据,因此还会有内存与包体的增长,且地图中同时存在的植被种类越多,内存增长幅度越明显,具体是否划算还需要各个项目自行评估。

下面来介绍一下地面植被与角色之间的交互:在角色或者其他物件与植被碰撞之后,植被的动画效果是怎么样的。

这里的预期是,当角色踩到草丛之后,附近的植被将被推开。

这个效果的实现,通常是借助一张从上方垂直投射下来的渲染贴图来实现的。每个碰撞物体可以在这张贴图中渲染出一个代表其自身的数据形状,有点像是一个每个像素都表示一个方向向量的sprite,将之渲染到上述贴图的时候采用的是additive blend模式,这样才能使得多个形状的交互数据能够无序的很好的混合起来。

每个植被模型在渲染的时候,就会从这张渲染好的贴图中读取数据,根据读取出来的结果对顶点进行变换实现对应的动画效果。

Andrew Maximov在《神海4》中实现了这套效果。

另外一个方案是将碰撞物体的形状渲染到一个高度图中(而非一个方向图),为了能够按照无序的方式进行混合,在这种方案中会使用一个minimum blend模式来保留最大的扰动(minimum指的是最深的,也就是高度最高的,力度最大的),这种方案作用下,植被顶点被推动的强度将跟贴图的深度成正比,之后再加上前面描述过的branch 拉伸bug修复方式(保长拉伸)就可以得到一个旋转效果了。

此外,如果还想使用方向数据,也可以使用高度图中的梯度来作为对方向的模拟,同样能够实现很好的效果。

在这个视频中,可以看到,角色双脚周边的草被分开了,在这个视角下效果还算令人满意,不过游戏中的视角跟这个不同。在游戏视角下,角色的双脚基本上是看不到的,所以植被与角色的交互也同样是难以被观察到,这就尴尬了。

为了增强交互效果,就需要找到一种方式能够让玩家感知到角色走过的轨迹。

《战神4》在移动轨迹上的第一次尝试,使用两个通道数据记录XZ方向上的偏移,使用一个额外的通道用于记录效果的age。

之后使用丝带粒子系统(ribbon particle system)来创建轨迹geometry,这个geometry其实就是对小圆球的移动时的边缘轨迹进行扩展与记录,并根据数据生命周期对其进行淡出处理,如上图所示,蓝色通道表示的是geometry上的数据的age。

这个技术的实施效果还不错,在2017年的E3展览中用的就是这个技术,不过后面发现这个技术还存在一些问题:

  1. 只能使用球形碰撞体
  2. 多个碰撞体同时影响会导致render texture的深度竞争,从而导致角色两只脚中间的草丛会出现反复的pop。
  3. 此外,由于这项技术使用的是从上往下的投影,因此不论作用力的高度如何,都会对植被产生影响(比如天空上的移动,也会对草丛产生影响。。。)。

这里给出第二种尝试,那就是使用角色移动时的脚步来将角色的轨迹sprite以一种离散的方式添加到render texture上,当然,还是保留上一种方法中的淡出效果,而为了避免前面说到的高空触控影响,这里只处理与草丛产生实际接触的行为动作。

虽然这种方法从效果上来看是可行的,不过可惜对于特效制作团队的编辑负担过于沉重,所以不得不继续寻求新的方案。

这里展示了这种方案的效果。这种方案需要对材质以及animation添加额外的tag,而这个工作量会添加比较高的负担,因此先看下是否还有更好的方案。

这里给出的第三种方案是,采用近似的形状,比如布娃娃,来将模型底部高度渲染到两个通道中。其中一个通道保持不变(held every frame),另一个通道则随着时间向上淡出。

根据淡出的通道的数值(实际上是根据淡出跟非淡出通道之间的差值判断),可以判断出当前位置距离上一次碰撞过了多长时间。根据这个时间来决定是否要维持当前植被的弯曲效果。如果判定出植被依然处于弯曲状态,那么再根据非淡出通道的高度图计算出此时物体的斜率,以计算弯曲的方向与幅度(不考虑时间作用的吗?会需要根据age来调节弯曲效果的,这里没有提到而已)。

这里给出另一个demo视频,注意观察角色的双脚。这里使用的render texture是double buffer的,因此可以为顶点的动画构造出精准的移动向量。

在对淡出通道进行更新的时候,会采用read-modify-write流程+minimum blend方法进行更新,而只有当当前计算的高度数据比非淡出通道之前预存的高度数据更低的时候,才会对非淡出通道进行更新。

这里有一个新的问题,不是所有平台都支持read-modify-write的,且这个操作消耗比较高。对于这种情况,可以考虑使用double buffering来从上一帧数据进行read,之后将结果写入到当前帧的buffer中。不过这种做法就会导致在某一帧中非淡出通道与淡出通道之间的高度差出现负数的可能(negate height difference,比如非淡出通道没有更新上一帧buffer结果,但是上一帧buffer的淡出通道更新了),不过这个问题可以在植被的顶点shader中通过增加一个clamp到0的操作来解决。

最后,这个方案对于静态物体或者在较高位置处移动的碰撞体都有很好的表现。一个站立不动的角色的动画可能会导致植被从settleing animation经过一个较大的pop后进入trampled pos。这个问题可以通过将玩家站立位置(通常也是render texture的中心位置)的淡出速率缩小到0来解决,这个做法会是的角色脚下植被的settle animation速度降低到0。

这里是使用这种方案的效果展示

这里给出一个debug biew的演示视频。左边的是dual heightfield texture,右边是从world view的侧视图。

先绘制一个球,而在world view中则添加一个高度数据,接下来可以看到将球移走之后,右边的高度图的一个通道数值开始上浮,而另一个通道数值则维持不变。

下面将球加回去,并开始对其进行移动

高度图两个通道之间的差值除以淡出速率,给出上次交互后经过的时长。这里用一个渐进动画来表示植被的整个弯曲过程——其实是一个余弦乘上一个衰减——当植被进入settle状态后,差值变成了负数

这里使用碰撞后经过的时间来从这条动画曲线上获取一个数值,当球继续向前移动时,你可以看到非淡出通道的数据是如何被覆盖的:即当前新计算的高度数值大于淡出通道此时的高度数值时,进行覆盖。这就意味着一些浅一点的碰撞会需要等到植被从上一次碰撞的影响中恢复过来后才能施加影响,而深一点的碰撞却可以立即施加影响。

接下来看下实际场景中的表现。

对于一些小型角色而言,可以直接使用mesh sphere作为碰撞几何形状。如果使用的是简化的形状而非sphere,所以其效果更为精确。

对于大型的物体以及主要角色,可以通过将形状与joint绑定来实现更为精确的表述,从而可以得到更为正确的碰撞效果。

这里给出另一个示例视频,其中的船使用的是更为精确的碰撞形状。

这里来回顾下之前的内容,下面给出相关的参考文献。

你可能感兴趣的:(【SIGGRAPH 2019】Interactive Wind and Vegetation in 'God Of War')