美文网首页水体渲染
【GDC 2018】Water Rendering in Far

【GDC 2018】Water Rendering in Far

作者: 离原春草 | 来源:发表于2024-08-18 21:27 被阅读0次

    这里分享的是Far Cry5在GDC 2018上分享的他们水体的渲染实现方案,照例对渲染方案的要点做一下总结:

    1. 整个实现方案分为引擎层(负责数据的管理,包括生成、查询等),渲染层(负责渲染)以及工具层(提供编辑能力)三部分
    2. 局部水体(河流等)由四叉树结构实现
    3. 通过flowmap实现水体的流动效果
    4. displacement的计算在GPU完成,但是需要回读到CPU用于游戏交互
    5. 支持面向PCG的水体编辑能力(样条曲线)
    6. 采用类似Projected Grid的屏幕空间曲面细分来实现mesh(四叉树只是用于存储数据),屏幕空间pass的输入是一个低模的mesh(大致的形状)与材质(指导曲面细分)
    7. 材质会通过一个compute shader写入到一个structure buffer,buffer尺寸跟屏幕空间像素数目一致
    8. 水体(低模mesh)会被切割成一个个的tile,近景跟远景tile可见性判断方式会有所不同(近景用occlude query,远景用的是HZB Query),经过剔除后,远景部分会通过一个DP完成绘制,近景则需要更多的DP
    9. 经过上一步的绘制之后,会得到一个mini G-Buffer,存储了水体的数据(带有多个后续计算需要用到的数据)、法线跟深度等三个RT
    10. 水体的交互效果是基于粒子实现的,将粒子的box decal投影到水面上生成一个交互buffer
    11. 波形数据采用了FBM实现了高低频混合,并通过噪声对法线做了扰动来规避重复
    12. 波形做了LOD优化,近景需要9个FBM采样,远处则只用3个
    13. 做完上述准备后,就要进入最后的屏幕空间pass了,为了避免屏幕pass的浪费,还会增加一个额外pass用于对屏幕空间划分后的tile的有效性进行判断,只绘制那些有水体的tile
    14. 为了避免projected grid的顶点闪烁问题,这里曲面细分得到的顶点密度接近像素密度,会导致硬件在执行层面的低效,后续顶点计算会考虑挪动到CS中
    15. 因为是屏幕空间的shading,因此还需要自行生成水面的法线、粗糙度等数据,以得到更为真实的反光效果
    16. foam是通过对贴图采样实现,没有采用雅克比矩阵的计算数值,而是通过一个噪声贴图调制得到
    17. 针对性能,这里尝试了min16float的方式优化了VGPR的消耗(HLSL&GLSL都支持),并对寄存器压力的原因与优化方式做了解释,最终GPU的单帧消耗为1.5ms
    Page 001 Page 002

    大纲:

    1. 前作中的方案简介
    2. Montana(蒙大拿)区域的水体效果总览(需求)
    3. 总体的方案目标
    4. 单帧绘制流程
    5. 性能优化
    6. 问题与方案汇总
    Page 003

    Far cry 2,老式blinn-phong specular渲染

    Page 004

    Far cry 3,场景PBR,水体还是非PBR的

    Page 005

    Far Cry 4,加了法线对反射的扰动

    Page 006

    Far Cry Primal

    Page 007

    Far Cry5的效果:PBR水、带了flowmap效果、反射、折射效果等

    Page 008 Page 009 Page 010 Page 011 Page 012 Page 013

    作者对蒙大拿区域的水体效果用真实照片的形式做了汇总,这也是Far Cry5尝试达到的效果,和具备的能力(倾斜水体,支持flowmap、foam跟瀑布)

    Page 014

    针对水体而言,总体来说有如下几方面的需求:

    1. 引擎层:提供数据生成、查询、贴图streaming能力
    2. 工具层:给美术同学提供制作、编辑能力
    3. 渲染层:负责完成数据的可视化展示
    Page 015

    引擎层的数据做进一步的细拆:

    1. 要支持通过一个简单的接口实现快速查询,提到了两方面的数据:
    • 用bitfield标注有效性的四叉树结构,用于支持动态水体
    • 离线烘焙的水体高度图(湖泊、河流、瀑布等水体需要),分块存储,支持streaming
    1. 提供水体流动与物理数据
    • flowmap要支持streaming,在GPU进行计算,但是要回读到CPU用于游戏逻辑
    1. 材质数据
    • 需要提供提前烘焙好的材质map
    Page 016

    这里展示了几个水体查询的接口,包括水体高度、流向信息的接口

    Page 017

    工具层的要求:

    1. 易用
    2. 易迭代
    3. 提供程序化能力
    Page 018

    这个视频展示了程序化工具的能力:

    1. 支持闭合样条曲线创建湖泊
    2. 支持开放样条曲线(带宽度)创建河流
    3. 生成水体的时候,会顺带将底部的材质、配套的一些物件做有选择性的生成
    Page 019

    渲染层的要求:

    1. 需要支持屏幕空间的曲面细分(屏幕空间的意义是避免全图细分带来的高消耗)
    2. 要支持逐个像素的材质混合
    3. 尽量使用async compute能力
    4. 支持带foam的flowmap
    Page 020 Page 021 Page 022 Page 023 Page 024 Page 025 Page 026 Page 027 Page 028 Page 029

    这里展示了Far Cry 5的水体效果,包含了水上、水下、江河湖海等各个层面。

    下面来看下如何实现屏幕空间的Tessellation。

    Page 030

    总的来说,美术同学提供的输入有两个:

    1. 一个低分辨率的mesh
    2. 一个材质,用于指导上述mesh该如何Tessellate

    之后就会创建一个屏幕空间的Tessellation mesh,如图左边所示

    Page 031

    美术同学提供的材质,其实就包含了一系列的参数,比如振幅、粗糙度、速度、scale等,这些数据后面会被烘焙到一个structure buffer中(如图右边所示)

    Page 032

    不过如果我们用DX11来实现的话,会遇到一个问题,即我们不能将每个像素需要用的贴图存储到这个buffer中(?也不应该这样用吧)

    这里的方式是将贴图存储到array中,之后将每个像素用的贴图转化为贴图的index

    Page 033

    当角色在世界中漫游的时候,可以想象,在这个过程中,会需要将各个位置用到的材质加载进来,为了能够高效的处理各个材质,这里做了一个indirection操作。

    即会通过一个compute shader将所有的材质数据取出来,存储到一个很大的structure buffer中(即上图中的Water Material Buffer),之后我们在渲染或计算的时候,就不用再额外采样这些材质。

    这里也可以推断出,上述buffer的尺寸与材质的数目是一一对应的,而每个像素(屏幕空间?)则需要存储该像素所对应的材质在这个buffer中的index,之后运算的时候就可以只采样这个buffer即可(贴图咋处理?前面说过,只需要将贴图的index存在上述材质buffer中即可)

    Page 034

    接下来看看整体的渲染流程是怎样的,流程图如上所示,下面对每个阶段做分别介绍。

    Page 035

    首先要做的就是标注出水体在哪些像素上是可见的,这里需要针对近景跟远景做分别处理,之所以要分开,是因为近景mesh精度要求高,且覆盖面积大,采用不同的算法可以同时保证精度与性能。

    Page 036

    近景采用conditional rendering方案(?),通过遮挡查询来判断哪些water mesh instance是可见的,为了降低消耗,这里直接采用mesh instance的AABB取代直接的mesh,之后存储下每个mesh instance的查询结果。

    Page 037

    远景的mesh是按照四叉树的结构组织的,有两种类型,分别是平面水体与带有高低起伏的水体(瀑布等)。

    整个计算过程是发生在GPU(Compute Shader)上的,以sector(四叉树的一个节点)为单位进行,使用sector的AABB(有高低起伏的,要从高度图中获取数据)对遮挡buffer(类似于HZB)进行可见性判断计算,之后基于可见性构建远景mesh的draw indirect arguments buffer。

    Page 038 Page 039

    通过遮挡剔除,我们可以尽可能的降低需要绘制的消耗:

    1. DP
    2. 面数等

    远景的部分,会合并成一个DrawCall,而近景由于绘制精度要求高,难以合并(?),通常会需要用多个DrawCall来绘制

    Page 040

    如果想要将所有的计算都放到屏幕空间,那我们就需要一个mini G-Buffer,这个G-buffer的分辨率要求低,但是FOV要求高(考虑displacement)

    Page 041

    前面说过,美术同学给的输入是一个低分辨率的mesh,针对这些mesh,我们执行一次position pass,得到mini GBuffer。

    G-buffer总共需要上图所示的三类数据(包含近景跟远景):

    1. Data:
    2. Mesh的法线数据:
    3. Water Mesh的深度数据:
    Page 043

    下面看看三张贴图里都存了啥。

    1. Data贴图存储了:
    • 8bit的shader ID,指示了材质的一些参数,是否front face,是否支持光照,是否是有效像素等
    • 8bit的材质structure buffer index,其实就是前面介绍的材质structure buffer的index
    • 8bit的两个miplevel:algae & foam。用于后续采样两类贴图的时候作为输入(因为屏幕空间计算就没有办法通过DDX/DDY获取贴图的miplevel了,所以需要提前计算给到)。
    Page 044

    因为水体是可以交互的,所以这里还需要考虑交互的输入。

    这里的实现方法相对简单,就是采用粒子实现,每个粒子都会生成一个投影的box decal,这个decal在绘制的时候,需要转换到mesh instance的object space,从而将数据正确应用到贴图上

    Page 045

    这里会剔除掉屏幕外的像素(旋转视角的话,缺失了此前的数据会不会导致效果异常?),并对displacement贴图(decal的)进行采样,之后应用水体效果的一些动画逻辑与数据(如splash等多帧效果需要)。

    为了实现新增的粒子的displacement跟已有displacement的平滑衔接,需要对box decal的displacement做一个沿着边沿的fade。

    最后,针对多个decal,这里采用了max alpha blend的算法来实现混合。

    Page 046

    最后得到了包含所有splash的displacement结果

    Page 047

    为了避免重复,这里会通过噪声来对法线等数据做扰动,同时通过FBM算法来混合高低频信息,得到更为真实的效果表现。

    在目前的设计中,总共需要叠加9个octave,但是这样消耗就太高了,这里的优化方案是增加LOD策略,近处用9个,远处就只有3个(不降低到0个的原因是,这样会导致反射效果的极端异常)。

    为了节省性能,displacement 贴图不用做clear,因为会有另一张贴图指示哪些区域是有效的,有效的数据会被覆写,所以不必要clear。

    Page 048

    在进入屏幕空间Tessellation之前,还需要做一次剔除,去掉那些不可见的部分。

    Page 049

    这里将整个屏幕划分成一个个的小tile,每个tile的尺寸为32(测试发现这个尺寸对所有硬件都是最佳的?)。

    剔除需要给出两个结果:

    1. tile是否覆盖水体
    2. 每个tile中有效像素的数目(何用?)
    Page 050

    这里给出了计算tile中有效数目的详细说明,需要注意的是,如果是在主机或者DX12的平台上,可以通过WaveActiveBallot指令,将8x8(一个group)个原子指令减少到1个。

    结果存入一个structure buffer中。

    Page 052

    之后会用另一个pass来获取上述structure buffer,并判断当前tile是否有数据(只要大于0的话,为啥要统计像素数?),有的话,就将这个tile放到待绘制列表里:

    1. 对indirect draw arguments buffer加一
    2. 将tile ID(dispatchThreadId.x)写入到第二个structure buffer中(waterTileDataOutput)

    截图右上角显示,当前帧的水体有606个mesh instance,每个带有1536个索引(顶点),可以看出是经过高度细分的。

    Page 053

    经过上述处理之后,就知道屏幕空间哪些tile要绘制水体,要做Tessellation。

    Page 054

    接下来开始进行Tessellation逻辑,上图给出了mesh的顶点密度,针对每个quad会做曲面细分。

    Page 055

    这里通过indirect draw一次性完成所有tile的绘制与曲面细分。这里没有对近远景做额外处理,采用的是常量的Tessellation密度。

    Page 056

    Tessellation完后,我们就得到了一个较为密集的顶点(这也是为什么采用了类似projected grid的方案,却不会有明显跳变的原因,因为顶点密度接近像素,而逐像素的displacement是不会有跳变的),接下来看看针对每个顶点,要做哪些计算:

    1. 获取水体的深度,转换到世界空间的坐标
    2. 采样该点的displacement数据,包括水体自带的FBM displacement,以及交互粒子的displacement
    3. 通过Nan的方式将无效的顶点剔除掉(VS剔除的唯一方法)
    4. 将displacement应用后的坐标投影回屏幕空间,并将该点对应的uv写入到RT中(用于实现大FOV到小FOV RT的映射)
    5. 通过深度测试来判断可见性
    Page 057 Page 058 Page 059 Page 060 Page 061 Page 062 Page 063 Page 064 Page 065

    大FOV是为了避免水体的displacement将水体收缩,导致边缘数据缺失

    Page 066

    经过大FOV到小FOV的映射采样之后,就能得到正确结果(虽然这个映射会带来一些扭曲,但是在相机移动的情况下,基本上可以忽略)

    Page 067

    上面介绍了mini G-Buffer的延迟管线计算逻辑,接下来看看法线数据。

    Page 068

    前面说了,美术同学是不用提供法线贴图的,这里是通过算法生成一张屏幕空间的法线贴图:

    1. 通过深度贴图计算得到
    2. 会根据相机到水体的距离,来调整参与计算的四个位置的offset从而实现精度的自适应
    3. 为了避免法线的跳变,这里还会将高频的displacement normal跟mesh(美术同学会提供一个低精度的mesh)的normal做一次混合
    Page 069

    除了法线,为了得到正确的屏幕空间高光效果,还需要生成屏幕空间的光滑贴图:

    1. 计算出每个像素的高斯法线(跟前面的法线的区别在于?)
    2. 最后基于法线的variance(方差)计算得到光滑度(或者就直接取方差作为光滑度,方差大说明周边法线扰动大,也就意味着不够光滑)
    Page 070 Page 071 Page 072

    基于光滑度以及前面的逐像素的材质数据,就可以在屏幕空间中得到普通绘制方式的高光效果

    Page 073

    最后来看看foam

    Page 074

    大致的实现思路是用一张噪声贴图来对foam贴图进行调制(放弃如FFT中的物理计算了?)。

    这里一个较为复杂的点,是需要注意与flowmap的结合(没讲具体如何跟foam结合)。

    foam的显示位置是通过SDF控制的,而SDF则是提前烘焙的,会采集海岸线、露出水面的物件等信息。

    foam主要有两种:

    1. 近岸海浪的foam
    2. displacement foam

    最终的foam是两者的叠加混合。

    Page 075

    flowmap的烘焙是离线完成的

    Page 076

    基于地形跟水平面,通过flood-fill算法生成(从水源点开始,寻找水的流动方向,将结果写入flowmap)。

    Page 077

    如果直接上flood-fill不考虑地形的高低落差的话,会出现水流效果跟真实情况存在差距的问题(如上图所示),为了避免这种情况,这里会取用SDF的数据对flood-fill算法进行约束。

    Page 078

    最终得到的flowmap是一个atlas,存储的是近景的flowmap数据,分辨率较高,会需要走Streaming(GPU通过VT的方式进行取用)

    Page 079

    远景的数据则是一张覆盖全图的低分辨率贴图。

    Page 080

    这是覆盖全图的heightmap数据,也是离线烘焙的,一个像素覆盖8m范围。

    Page 081

    最后看看这些数据如何组装

    Page 082

    前面我们提到了,这里我们可以拿到带水和不带水的depth map,同时还可以拿到各个像素的material id,基于这些数据,我们就可以对各个像素做shading了。

    这里采用的是Forward+的光照shading计算逻辑:

    1. 会基于上述数据,计算间接光,包括ambient(通过GI获取),反射(通过环境贴图与SSR方式得到)等数据
    2. 还会计算直接光照
    3. 计算局部光照
    4. 计算exposure光(?)

    材质还支持混合,如上图右边所示,可以支持两种材质之间的混合,这个主要用于解决两种不同的水体的混合区域的过渡效果,这里的blend只会针对一些关键参数,其他参数的blend不会对效果造成过大影响,因此就不用额外浪费算力了。

    Page 083 Page 084 Page 085

    为了得到较好的光照效果,这里还做了一些额外的处理:

    1. 没有给美术同学提供一种直接控制颜色的手段,而是将散射系数暴露给他们进行控制
    2. 如上图所示,共12个参数,对应12种类型的水体,美术同学只需要从table中选择所需要的水体类型(或者创建新的)即可
    3. 每个类型就带一个参数,RGB颜色与最后的浑浊度(turbidity),基于这些参数可以创建任意类型的水体效果(干净的、浑浊的、海洋的、河流的)
    Page 086

    最后还需要采样foam等贴图,接下来看看这些贴图采样后的效果

    Page 087

    这是添加了折射之后的light transport效果

    Page 088

    叠加SSLR(screen space local reflection)效果

    Page 089

    环境贴图反射效果

    Page 090

    叠加所有效果后的表现,包括foam、反射、折射、splash粒子、涟漪等效果。不过要想得到这个效果,会需要做非常繁重的VGPR(vector general purpose register)计算,下面来看看怎么优化这部分的消耗。

    Page 091

    首先就是调整部分属性的精度,如上图所示,可以节省9个VGPR调用。那么min16float是什么格式呢?

    Page 092

    这是HLSL的一种基本类型(跟half是不一样的):

    1. 使用这种类型,编译器就知道其数据精度是可以降低的
    2. 存储在buffer中的数据还是全精度的,只是在采样的时候,GPU会自动将之转换为16位的(那存储全精度的意义在哪里)
    3. 但是这里并不是一定会需要采用低精度的(所以这就是存储全精度的意义,那么什么情况下会用低精度的?由软件跟硬件stack决定,具体一点呢?开发者基于算法的可能性来评估,并由QA同学测试确认)
    4. 支持整型与浮点型
    5. GLSL也有类似的字段
    Page 093

    哪些地方需要全精度的数据:

    1. 贴图采样的UV坐标,精度不足容易导致块状效果
    2. 法线向量,精度不足,光照结果会非常低质量
      3.两个接近相等的数值的相减结果
    3. 除以接近于0的数
    4. 存在累计误差的地方
    Page 094

    寄存器占用数目过高也会对shader的执行效率产生较大的影响,这也是日常GPU瓶颈的一个很重要的原因。

    一个常用的手段是提高GPU的Occupancy(为啥提高Occupancy能够降低寄存器的压力?),Occupancy的解释是:

    当前SIMD组件待执行的WaveFronts的数目与SIMD可以分配的最大slot数目的比值

    这个数值越大,意味着SIMD可以调度的空间越大,当某个wavefront执行阻塞时,就可以调整到另一个Wavefront上继续执行以提升SIMD的执行效率,减少整体执行的耗时。

    Occupancy会有助于增加VGPR(寄存器) usage的discrete threshold,如果VGPR压力过大的话,通过这种方式可以得到更大的收益。

    Page 095

    寄存器压力有这么几个主要的原因:

    1. 过早的读取了内存数据,并持续占用
    2. 循环的unrolling
    3. 大量的中间变量
    Page 096

    寄存器分配的时候也会可能分配了过大的寄存器:

    1. 数据的通道过多:
    • 多个通道的数据需要放置在连续的寄存器中
    • 通道的数据需要保序
    1. 贴图的维度过多:
    • 贴图数据同样需要连续跟保序

    为啥这样的设定就会导致寄存器浪费?是有一些数据本来是不需要的吗?

    Page 097

    live寄存器数目是衡量分配overhead的重要指标,如何甄别哪些寄存器是live的?可以尝试AMD的Radeon GPU Analyzer工具,下面用一个案例来介绍。

    Page 098

    在这个案例中,没有overhead,所有的运算都是在原地(寄存器)中完成的

    Page 099

    但是,如果我们稍微修改下代码,如右下角所示,在这里就只需要5个VGPR,但是分配了6个(因为计算不能在原地进行,且V2后续就不再使用,但是又不能释放 ),导致了一个浪费。

    Page 100

    另外,如果使用半精度的浮点数,也有助于减少寄存器分配的浪费,比如min16float4分配的寄存器会少很多。

    Page 101 Page 102

    下面来看下水体方案实现过程中的一些问题,主要有上述图中所整理的几方面的内容:

    1. 两个depth buffer带来的大量的bug(什么时候该用哪个并不那么明确,加上链式的后续计算,stencil等,情况会变得非常复杂)
    2. 大量的小尺寸贴图,可以通过pack解决,同时使用pingpong机制实现复用降低内存消耗
    3. 屏幕空间曲面细分带来了一些问题
    • 需要大量的VS计算,与现代GPU中PS运算单元更多的设计相冲突。修改了数据的精度,降低到16位;后面准备将这个计算挪到Compute shader中,来避免这个问题
    • 边界情况导致的异常效果(见后面的图)
    Page 103

    如上图所示,当有两个相互连接的水体(河流+海洋)时,水体在远景处就会出现拉伸

    Page 104

    这里是修复后的效果。

    Page 105

    解决方案是,通过一个compute shader来检测两个水体相接的位置,之后调低这部分区域的displacement。

    上图中右边的红色图是屏幕空间中的displacement贴图(FBM计算)

    Page 106 Page 107

    对这个图进行边缘检测,并做模糊

    Page 108

    Far Cry有个钓鱼小游戏,这个会有near-z跟far-z的问题。

    如上图所示,钓线会频繁的改变颜色,在开启SSR的时候,会导致水体出现比较明显的闪烁效果。

    Page 109

    解决方案是在SSR的ray tracing做完之后,再增加一个后处理pass。

    Page 110 Page 111 Page 112

    这里提供了一些debug工具

    Page 113

    最终的性能数据,GPU总计耗时1.9ms,绿色是compute shader部分

    Page 114

    如果开启async compute,还能优化0.6ms

    Page 115

    这里给了个最终的视频。

    Page 116 Page 117

    相关文章

      网友评论

        本文标题:【GDC 2018】Water Rendering in Far

        本文链接:https://www.haomeiwen.com/subject/xeojkjtx.html