• Wind Simulation in 'God of War'(GDC2019 战神4风力场模拟)


    Wind Simulation in 'God of War'(GDC2019)

    战神4中的风力场模拟

    这次带来的分享的主题是,圣莫妮卡工作室他们在战神4中关于GPU模拟风力场。

    演讲者Rupert Renard 12年游戏行业开发经验,参与过战神4,塞尔达传说,质量效益3等大作。

    0.What Wind is Used For

    风在游戏中能带来什么?

    1

    通常情况线下,风可以是一个简单的正弦波,影响物体的摆动,但是为了创造一个更生动的世界,战神4写了一套完善的风力系统,可以作用的对象包括

    • 粒子
    • 头发
    • 树叶
    • 皮毛
    • 声音系统
    • 布料

    这里的头发,树叶,皮毛,被集成在另一个系统中,子系统中处理局部空间的扰动。

    风的强弱可以烘托环境氛围,营造气氛。

    1.CPU Origins

    2

    CPU上流体模拟的传统方法,03年GDC上有一篇经典文章 “Real-Time Fluid Dynamics for Games”,主要是在不影响流体表现的前提线下,通过简化流体表达方程提高计算速度。

    那篇论文提到了方法,计算过程主要是 通过密度(density)添加力(add force),结合流体本身的扩散(diffuse),达到流体的效果(move),解决 boundary issues(边界问题),会在bound box 外面再包一层,论文地址

    但是战神他们觉得现在都9012了,为啥不用一些更先进的其他方法去尝试呢

    2.Wind Tiers

    风的类型和级别

    3

    三种风的类型:

    • Static Wind
    • Dynamic Wind
    • Counter Wind

    静态风:静态风是一个全局的风,均匀地应用于场景中的所有物体。它可以随着时间的推移而改变,也可以随着玩家在世界各地的移动而改变。有时会用scrolling noise texture 来做静态风。

    动态风:动态风是他们的重点,作用范围是在玩家周围形成一个3D立体的空间,并随着玩家的移动而移动的。

    逆风:逆风是其实是一个机制,用来模拟在风中移动的物体,是否受到风的影响。
    如果一个物体的运动速度和方向与静态风或动态风大致相同,就会抵消风的作用,并给出物体不受风影响的表现。

    3.1

    SampleWind(object) := StaticWind + DynamicWind[object.position] - object.velocity

    公式也比较好理解。

    风的影响的采样公式 = 全局静态风一个vector3 + 动态风场中物体位置的风采样 - 物体的移动速度vector3

    3.Dynamic Wind Details

    动态风详解

    4

    用32x16x32 的三维纹理来存, 每立方米 一个纹理单位。 为了在GPU上快速方便的模拟风的计算,选择了标准的三维纹理volume,而没有使用层次化的volume。

    战神的动态风场在玩家周围也足够大,能包含斧头扔出去的距离。所以他们的动态风场xz是比y大一倍的。

    使用每帧5次的迭代,没有什么特别愿意,只是刚好找到了一个比较balance的值。

    风的产生设计了不同类型的“发动机”,用来给风场注入速度。

    战神里面的Advection 对流提供了,正向和反向的2种,他们强烈建议别图便宜只搞一种,后面会说原因。

    他们尝试过用压强来模拟风场,但是他们的美术不喜欢,而且压强有个弊端,就是不能是负的。但是压强他们也做了,把压强做为一个额外的使用参数

    4.Storage

    5

    每个属性都有单独的三维纹理,x的速度,y的速度,z的速度

    5.1

    关于三维纹理的切片方向也有讲究,他们选择的是xz轴的切片。(据说,这样做在计算的更高效,因为很多时候风的流向都是水平运动)

    5.Diffusion

    风的扩散。

    6

    随时间推移,某个cell会对周围的cell产生影响。可以理解成是使流体模拟达到平衡的一种机制。它被用来在相邻的cell之间传递能量。

    这来的扩散就用到了 double的buffer,2个buffer交替存数据。

    验证之后发现将速度属性分成三个单独的三维纹理,计算的时候尤其高效。

    是因为如果不分离的话,在计算风的迭代的时候,xyz三个方向全部计算完成之后,才能进行下次的迭代。但其实这三个方向的计算,发生在不同迭代轴上,是互不影响的

    看着2张图就能看出来。

    6.1

    先解释下VGPR。

    AMD GCN 计算单元中, 一个GCN计算单元(CU),包含四个SIMDs(单指令流多数据流),每一个包含一个包含32位的VGPRs(矢量通用寄存器)的64KB寄存器文件

    着色器最终处理每个线程的带宽更少,这是因为每个线程的数据更少,这意味着每个线程的VGPR更少,这意味着更好的占用潜力。
    对于希望异步运行的着色器,更少的VGPRs也是非常好的选择

    好处是,GPU在计算迭代的时候,更少的等待时间。

    对于希望异步运行的shader,更少的VGPRs也是非常好的选择

    6.Motors

    风力发动机

    7

    6种不同类型的Motors

    • Directional 平行风 (类似unity WindZone里的Directional)
    • Omni 全向风 (类似unity里的Spherial)
    • Vortex 旋涡,沿某个轴产生风
    • Moving 运动发动机,锥形,可以理解成发动机在运动,产生风场是锥形扩散的
    • Cylinder 圆柱的上下面可以大小不一样
    • Pressure 直接就是压强

    当时面临的一个挑战就是不同类型的Motor混合时候,互相作用

    // 平行风,out返回float3的velocity
    void ApplyMotorDirectional(in float3 cellPosWS, uniform MatorDirectional motorDirectional, in out float3 velocityWS)
    {   
        // 计算cell到motor的距离
        float distanceSq = lengthSq(cellPosWS - motorDirectional.posWS);
        // 距离的平方小于motor的作用范围,加上速度
        // force = direction * strength * deltaTime
        if(distanceSq < motorDirectional.force)
            velocityWS += motorDirectional.force;
    }
    
    // 全向风,作用朝四面八方,辐射出去,存在作用半径radius
    void ApplyMotorOmni(in float3 cellPosWS, uniform MotorOmni motorOmni, in out float3 velocityWS)
    {
        // force = strength * deltaTime
        float3 differenceWs = cellPosWS - motorOmni.posWS;
        float distanceSq = lengthSq(differenceWs);
        // 速度受到作用半径和距离的影响
        if(distanceSq < motorOmni.radiusSq)
            velocityWS += motorOmni.force * rsqrt(distanceSq) * differenceWs
    }
    
    // 螺旋风
    void ApplyMotorVortex(in float3 cellPosWS, uniform MotorVortex motorVortex, in out float3 velocityWS)
    {
        // force = strength * deltaTime
        float3 differenceWs = cellPosWS - motorVortex.posWS;
        float distanceSq = lengthSq(differenceWs);
        // 速度受到作用半径和螺旋风轴向叉乘的影响
        if(distanceSq < motorVortex.radiusSq)
            velocityWS += motorVortex.force * cross(motorVortex.axis, rsqrt(distanceSq) * differenceWs)
    }
    

    和unity里的WindZone做一个简单对比,
    unity中3D 在 Game Object > 3D Object > Wind Zone,提供了wind zone,但是类型较为单一,只提供了Direction和Spherical两种,差不多等价于战神里的 Directional 和 Omni。

    unity主要还是用压强来实现的,暴露的参数,Main(强度)近似战神Motor类型中的force。

    7.2

    7.Advection

    平流(或者叫水平对流)

    8

    是基于速度传递能量的过程,发生在纹理和纹理之间,可以用来传播速度属性。

    通过平流 来传播速度,模拟能量的流动。

    处理平流可以处理diffusion扩散一样,按轴进行分离,减少等待时间。

    但是会存在一个问题,在做迭代的时候, 正向和反向的会同时对数据读写,写入数据的时候发生数据争抢。多线程的时候可能同时有不同的线程在往texel纹理中写数据。

    8.Spin Compare & Exchange

    交换比较

    9

    多线程运作的时候,纹理写入,因为内存可见性的原因,不是原子运算,可能最后返回的结果有偏差。

    举个简单的例子开多线程 执行i++ 1000次,最后返回的结果不一定是1000,是一样的原因。

    简单解释下,多线程线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。

    这里用到的解决方法是多线程里面常见的CompareExchange,函数差不多是这样的
    CompareExchange(Double, Double, Double)

    void SpinCompareExchange(uniform RWTexture3D<float> rwTex, uniform unit3 rwTexSize, in unit3 coord, in float value)
    {
        if(all(coord < rwTexSize))
        {
            float curVal = 0;
            for(;;)
            {
                float oldVal, newVal = curVal + value;
                InterlockedCompareExchange(rwTex[coord], curVal, newVal, oldVal);
                if (curVal == oldVal)
                    break;
                curVal = oldVal
            }
        }
    }
    

    把目标操作数(第1参数所指向的内存中的数)与一个值(第3参数)比较,如果相等,则用另一个值(第2参数)与目标操作数(第1参数所指向的内存中的数)交换

    整个操作过程是锁定内存的,其它处理器不会同时访问内存,从而实现多处理器环境下的线程互斥。

    这个地方可以直接参考MSDN compareExchange函数的api

    9.Atomic Add

    10

    定长操作。上一页提交浮点型在比较的时候,硬件没法对浮点型进行原子运算。所以战神换了个思路,损失一定精度,转化成定长的浮点(或者说是定点),16.16,上下都保证一定的精度。

    也叫fixed-point number,定点数的计算效率是比浮点数更高的

    wiki上解释是:

    定点数类型的值其实就是个整数,需要额外做比例进位,进多少位需要根据具体的定点数类型决定。例如 1.23 使用 1/1000 比例的定点数表示时是 1230;1,230,000 使用 1000 比例的定点数表示也是 1230。与浮点数不同,相同类型的定点数中所有值的缩放系数都是一致的,在计算过程中也保持不变。

    缩放系数通常是 10 或 2的幂,前者方便人类读写,后者易于高效计算。不过有时也会使用其它比例,例如可以用 1/3600 的比例的定点数来表示以小时为单位的时间值,可以精确到秒。

    定点数的最大值,可以通过将其内部所使用的整数的最大值乘以缩放系数求得,最小值同理。

    浮点数的精度是有尾数位决定的,正常单精度float,双精度double的小数位如下:

    float:  
    1bit(符号位) 8bits(指数位) 23bits(尾数位)
    double:
    1bit(符号位) 11bits(指数位) 52bits(尾数位)
    

    他们把小数位按16位算精度,转换成int,在做原子加的操作。

    // float转int要乘的宏
    # define FXDPT_SIZE(1<<16)
    void AtomicAdd(uniform RWTexture3D<int> rwTex, uniform unit3 twTexSize, in unit3 coord, in float value)
    {
        if(all(coord < rwTexSize))
        {
            InterlockedAdd(rwTex[coord], (int)(value * FXDPT_SIZE));
        }
    }
    
    

    10.Scheduling

    调度算法以及耗时。

    11

    风力模拟,在GPU管线上是第一次做的事,模拟差不多耗时0.1ms。

    模拟的过程本身也是异步的,和渲染物体,渲染粒子,并行。

    11.1

    蓝色表示扩散,红色是Motor相关,橘色是正向对流初始化的一些过程,黄色开始计算正向对流,后面是反向平流,最后紫色导出,方便gpu和cpu访问。

    整个过程VGPR和SGPR都很低,适合并行运算。

    diffuse进行了5次,耗时43.2ms,平均一次8ms,这是因为使用了分离轴的技巧(3个轴互不影响分离计算)。这种方法,如果使用更大的Volume,性能的提高会更加明显。

    后面他们觉得forward平流和reverse评论在setup的阶段,可以用buffer来记录之前的数据,空间换时间,差不多又节约了10ms左右。

    11.2

    他们还做了一个实验,把每个轴的size扩大一倍,等于整体体积扩大了8倍。

    实验结果可以看出来,最后一列是耗时,可以看出差不多都是7,8倍的样子,说明耗时和数据量基本线性相关,只有export导出的部分耗时增多,演讲者说可能是纹理寻址的问题。

    11.Timing-Full Frame

    12

    这张图展示了战神 一帧绘制各部分的耗时,其实wind的耗时所占的比重很小,上面下那是的是异步并行的耗时,

    12.Debugging

    战神他们团队的debug工具真是完善,

    13

    • 可视化2D的切面
    • 可以用向量更改wind的属性
    • 粒子发射器
    • 锁定volume位置
    • volume中风力的采样和显示

    可视化3D volume风场和2D的风 切面是最直接有效的。可以很方便美术去布风,看效果,也方便程序去debug。图中绿色的是一个directional motor(定向风发动机)。

    奎爷的斧头也是一个Moving Motor,扔出去之后也会产生风影响周围的环境。

    13.3

    13.3

    采样方法也分了2种:一种是均匀时间间隔,在固定距离间隔的位置采样,然后绘制矢量图标,表示风力;另一种是直接用矢量图标表示风力,越密集表示风强度越大。

    13.Wind Customers

    14

    因为模拟的结果,不仅仅是GPU用,CPU布料和声音的系统也会用到,CPU和GPU通信又不叫耗,战神用了一个比较能接受的方法,把速度属性xyz,存成RGB16的 double buffer texture。保证流畅性,CPU上布料和声音系统读取的是上一帧GPU返回的结果。

    14.Beaufort scale

    蒲福風級,Beaufort风力等级。就是几级风力等级。

    15

    可以参考wiki Beaufort风力等级

    0到12的等级,0代表没有风,12代表飓风的力量。比如能听到树叶的嗦嗦的声音,差不多是风力2级,地面差不多2m/s。

    小树摇摆,差不多5级风,地面9m/s。 这等于把游戏中的风和现实世界关联上了,这样更具有真实性。

    15.Conclusion

    16

    • 风的模拟使游戏更加生动
    • 高性能和低消耗也是可以实现的
    • 正向平流和反向一定要同事使用
    • 好的debug工具可以事半功倍

    16.1 since 2003

    相比2003的方法,战神把流体风的模拟放到了GPU上,更好的发挥了硬件GPU并行计算的性能,有更高的质量。

    后面就是他安利大家去听他同事Sean的 Interactive Wind and Vegetation in God of War,讲植被和风交互,会详细介绍客户端表现,包括在Vertex Shader上风是怎么影响物体的,还有双高度场的草场交互。

    个人总结

    最后谈一下,自己的一点点小感想。

    战神他们的对风力场的创新,是相当于把原来一直CPU上做的一件事件,放到了GPU上,做到了并行高效的效果。

    多线程部分,并行的一些思想,原子操作等等。

    计算的时候用定点数,相比浮点数,提交运算效率

    最后他们的debug工具真是完善呀,开发效率高,调试效率高。实名羡慕...

    参考文献

    1.Wind Simulation in God of War

    2.Introduction to Fixed Point Number Representation(berkeley.cs61c)

    3.Real-Time Fluid Dynamics for Games

  • 相关阅读:
    机器学习-liuyubobobo(慕课网)
    python进阶 廖雪峰(慕课网)
    ajax 报0错误
    nav破解
    thinkphp5--关于多条件查询的分页处理问题
    JS/JQuery获取当前元素的上一个/下一个兄弟级元素等元素的方法
    linux 批量删除文件
    Linux下which命令使用详解(转)
    thinkphp5和nginx不得不说的故事
    Git基本操作和使用
  • 原文地址:https://www.cnblogs.com/SeekHit/p/11137846.html
Copyright © 2020-2023  润新知