【GDC 2018】Water Rendering in Far
这里分享的是Far Cry5在GDC 2018上分享的他们水体的渲染实现方案,照例对渲染方案的要点做一下总结:
- 整个实现方案分为引擎层(负责数据的管理,包括生成、查询等),渲染层(负责渲染)以及工具层(提供编辑能力)三部分
- 局部水体(河流等)由四叉树结构实现
- 通过flowmap实现水体的流动效果
- displacement的计算在GPU完成,但是需要回读到CPU用于游戏交互
- 支持面向PCG的水体编辑能力(样条曲线)
- 采用类似Projected Grid的屏幕空间曲面细分来实现mesh(四叉树只是用于存储数据),屏幕空间pass的输入是一个低模的mesh(大致的形状)与材质(指导曲面细分)
- 材质会通过一个compute shader写入到一个structure buffer,buffer尺寸跟屏幕空间像素数目一致
- 水体(低模mesh)会被切割成一个个的tile,近景跟远景tile可见性判断方式会有所不同(近景用occlude query,远景用的是HZB Query),经过剔除后,远景部分会通过一个DP完成绘制,近景则需要更多的DP
- 经过上一步的绘制之后,会得到一个mini G-Buffer,存储了水体的数据(带有多个后续计算需要用到的数据)、法线跟深度等三个RT
- 水体的交互效果是基于粒子实现的,将粒子的box decal投影到水面上生成一个交互buffer
- 波形数据采用了FBM实现了高低频混合,并通过噪声对法线做了扰动来规避重复
- 波形做了LOD优化,近景需要9个FBM采样,远处则只用3个
- 做完上述准备后,就要进入最后的屏幕空间pass了,为了避免屏幕pass的浪费,还会增加一个额外pass用于对屏幕空间划分后的tile的有效性进行判断,只绘制那些有水体的tile
- 为了避免projected grid的顶点闪烁问题,这里曲面细分得到的顶点密度接近像素密度,会导致硬件在执行层面的低效,后续顶点计算会考虑挪动到CS中
- 因为是屏幕空间的shading,因此还需要自行生成水面的法线、粗糙度等数据,以得到更为真实的反光效果
- foam是通过对贴图采样实现,没有采用雅克比矩阵的计算数值,而是通过一个噪声贴图调制得到
- 针对性能,这里尝试了min16float的方式优化了VGPR的消耗(HLSL&GLSL都支持),并对寄存器压力的原因与优化方式做了解释,最终GPU的单帧消耗为1.5ms
![](https://img.haomeiwen.com/i19200103/e8949ef13d203e54.png)
![](https://img.haomeiwen.com/i19200103/4f482edb4d2e38c8.png)
大纲:
- 前作中的方案简介
- Montana(蒙大拿)区域的水体效果总览(需求)
- 总体的方案目标
- 单帧绘制流程
- 性能优化
- 问题与方案汇总
![](https://img.haomeiwen.com/i19200103/72cf54326d4e9a0e.png)
Far cry 2,老式blinn-phong specular渲染
![](https://img.haomeiwen.com/i19200103/8418ad3ad392c176.png)
Far cry 3,场景PBR,水体还是非PBR的
![](https://img.haomeiwen.com/i19200103/a93305445e206cae.png)
Far Cry 4,加了法线对反射的扰动
![](https://img.haomeiwen.com/i19200103/bf20985c36f33773.png)
Far Cry Primal
![](https://img.haomeiwen.com/i19200103/63530b1c0e2aabe7.png)
Far Cry5的效果:PBR水、带了flowmap效果、反射、折射效果等
![](https://img.haomeiwen.com/i19200103/bfa674c1a75d7bb2.png)
![](https://img.haomeiwen.com/i19200103/530ad200088a3605.png)
![](https://img.haomeiwen.com/i19200103/d61ff9ea2abc62e7.png)
![](https://img.haomeiwen.com/i19200103/bf5fed6cc0b182dc.png)
![](https://img.haomeiwen.com/i19200103/375b2656c098511e.png)
![](https://img.haomeiwen.com/i19200103/cc532b11bd6c62ff.png)
作者对蒙大拿区域的水体效果用真实照片的形式做了汇总,这也是Far Cry5尝试达到的效果,和具备的能力(倾斜水体,支持flowmap、foam跟瀑布)
![](https://img.haomeiwen.com/i19200103/a7348ba8a3edaeca.png)
针对水体而言,总体来说有如下几方面的需求:
- 引擎层:提供数据生成、查询、贴图streaming能力
- 工具层:给美术同学提供制作、编辑能力
- 渲染层:负责完成数据的可视化展示
![](https://img.haomeiwen.com/i19200103/2d793ecb7cfa9703.png)
引擎层的数据做进一步的细拆:
- 要支持通过一个简单的接口实现快速查询,提到了两方面的数据:
- 用bitfield标注有效性的四叉树结构,用于支持动态水体
- 离线烘焙的水体高度图(湖泊、河流、瀑布等水体需要),分块存储,支持streaming
- 提供水体流动与物理数据
- flowmap要支持streaming,在GPU进行计算,但是要回读到CPU用于游戏逻辑
- 材质数据
- 需要提供提前烘焙好的材质map
![](https://img.haomeiwen.com/i19200103/6211db26c5f7f8fb.png)
这里展示了几个水体查询的接口,包括水体高度、流向信息的接口
![](https://img.haomeiwen.com/i19200103/aaa291a8bcf72011.png)
工具层的要求:
- 易用
- 易迭代
- 提供程序化能力
![](https://img.haomeiwen.com/i19200103/b62f904a81691801.png)
这个视频展示了程序化工具的能力:
- 支持闭合样条曲线创建湖泊
- 支持开放样条曲线(带宽度)创建河流
- 生成水体的时候,会顺带将底部的材质、配套的一些物件做有选择性的生成
![](https://img.haomeiwen.com/i19200103/bc019e3364a4221c.png)
渲染层的要求:
- 需要支持屏幕空间的曲面细分(屏幕空间的意义是避免全图细分带来的高消耗)
- 要支持逐个像素的材质混合
- 尽量使用async compute能力
- 支持带foam的flowmap
![](https://img.haomeiwen.com/i19200103/1295330a46eae916.png)
![](https://img.haomeiwen.com/i19200103/5bbd746707be7c09.png)
![](https://img.haomeiwen.com/i19200103/60832c7edef3a8e1.png)
![](https://img.haomeiwen.com/i19200103/25305d3998437108.png)
![](https://img.haomeiwen.com/i19200103/3c5ff89c77ca2503.png)
![](https://img.haomeiwen.com/i19200103/4a251090dc975c3c.png)
![](https://img.haomeiwen.com/i19200103/eb2bdcb7a5f92994.png)
![](https://img.haomeiwen.com/i19200103/9064b85fb8db4d54.png)
![](https://img.haomeiwen.com/i19200103/8b587d6d9b0d7233.png)
![](https://img.haomeiwen.com/i19200103/9316562048976a42.png)
这里展示了Far Cry 5的水体效果,包含了水上、水下、江河湖海等各个层面。
下面来看下如何实现屏幕空间的Tessellation。
![](https://img.haomeiwen.com/i19200103/c81c162c01e0aa81.png)
总的来说,美术同学提供的输入有两个:
- 一个低分辨率的mesh
- 一个材质,用于指导上述mesh该如何Tessellate
之后就会创建一个屏幕空间的Tessellation mesh,如图左边所示
![](https://img.haomeiwen.com/i19200103/e441e71e383be96b.png)
美术同学提供的材质,其实就包含了一系列的参数,比如振幅、粗糙度、速度、scale等,这些数据后面会被烘焙到一个structure buffer中(如图右边所示)
![](https://img.haomeiwen.com/i19200103/83eeb723851263ff.png)
不过如果我们用DX11来实现的话,会遇到一个问题,即我们不能将每个像素需要用的贴图存储到这个buffer中(?也不应该这样用吧)
这里的方式是将贴图存储到array中,之后将每个像素用的贴图转化为贴图的index
![](https://img.haomeiwen.com/i19200103/0581a66698da0182.png)
当角色在世界中漫游的时候,可以想象,在这个过程中,会需要将各个位置用到的材质加载进来,为了能够高效的处理各个材质,这里做了一个indirection操作。
即会通过一个compute shader将所有的材质数据取出来,存储到一个很大的structure buffer中(即上图中的Water Material Buffer),之后我们在渲染或计算的时候,就不用再额外采样这些材质。
这里也可以推断出,上述buffer的尺寸与材质的数目是一一对应的,而每个像素(屏幕空间?)则需要存储该像素所对应的材质在这个buffer中的index,之后运算的时候就可以只采样这个buffer即可(贴图咋处理?前面说过,只需要将贴图的index存在上述材质buffer中即可)
![](https://img.haomeiwen.com/i19200103/fc7cd8dd0f36e154.png)
接下来看看整体的渲染流程是怎样的,流程图如上所示,下面对每个阶段做分别介绍。
![](https://img.haomeiwen.com/i19200103/15dd925dcbe989a3.png)
首先要做的就是标注出水体在哪些像素上是可见的,这里需要针对近景跟远景做分别处理,之所以要分开,是因为近景mesh精度要求高,且覆盖面积大,采用不同的算法可以同时保证精度与性能。
![](https://img.haomeiwen.com/i19200103/3e8b7f230c6e692b.png)
近景采用conditional rendering方案(?),通过遮挡查询来判断哪些water mesh instance是可见的,为了降低消耗,这里直接采用mesh instance的AABB取代直接的mesh,之后存储下每个mesh instance的查询结果。
![](https://img.haomeiwen.com/i19200103/58e162fcb196f3ad.png)
远景的mesh是按照四叉树的结构组织的,有两种类型,分别是平面水体与带有高低起伏的水体(瀑布等)。
整个计算过程是发生在GPU(Compute Shader)上的,以sector(四叉树的一个节点)为单位进行,使用sector的AABB(有高低起伏的,要从高度图中获取数据)对遮挡buffer(类似于HZB)进行可见性判断计算,之后基于可见性构建远景mesh的draw indirect arguments buffer。
![](https://img.haomeiwen.com/i19200103/5ca0b0572ca4a59f.png)
![](https://img.haomeiwen.com/i19200103/4a9a7aa229a4a5eb.png)
通过遮挡剔除,我们可以尽可能的降低需要绘制的消耗:
- DP
- 面数等
远景的部分,会合并成一个DrawCall,而近景由于绘制精度要求高,难以合并(?),通常会需要用多个DrawCall来绘制
![](https://img.haomeiwen.com/i19200103/7353be31780620d3.png)
如果想要将所有的计算都放到屏幕空间,那我们就需要一个mini G-Buffer,这个G-buffer的分辨率要求低,但是FOV要求高(考虑displacement)
![](https://img.haomeiwen.com/i19200103/ecb3c221dfd7060f.png)
前面说过,美术同学给的输入是一个低分辨率的mesh,针对这些mesh,我们执行一次position pass,得到mini GBuffer。
G-buffer总共需要上图所示的三类数据(包含近景跟远景):
- Data:
- Mesh的法线数据:
- Water Mesh的深度数据:
![](https://img.haomeiwen.com/i19200103/bf2cd948b7bf00fe.png)
下面看看三张贴图里都存了啥。
- Data贴图存储了:
- 8bit的shader ID,指示了材质的一些参数,是否front face,是否支持光照,是否是有效像素等
- 8bit的材质structure buffer index,其实就是前面介绍的材质structure buffer的index
- 8bit的两个miplevel:algae & foam。用于后续采样两类贴图的时候作为输入(因为屏幕空间计算就没有办法通过DDX/DDY获取贴图的miplevel了,所以需要提前计算给到)。
![](https://img.haomeiwen.com/i19200103/c94d286a7f075676.png)
因为水体是可以交互的,所以这里还需要考虑交互的输入。
这里的实现方法相对简单,就是采用粒子实现,每个粒子都会生成一个投影的box decal,这个decal在绘制的时候,需要转换到mesh instance的object space,从而将数据正确应用到贴图上
![](https://img.haomeiwen.com/i19200103/1c9f6388be7eb6d3.png)
这里会剔除掉屏幕外的像素(旋转视角的话,缺失了此前的数据会不会导致效果异常?),并对displacement贴图(decal的)进行采样,之后应用水体效果的一些动画逻辑与数据(如splash等多帧效果需要)。
为了实现新增的粒子的displacement跟已有displacement的平滑衔接,需要对box decal的displacement做一个沿着边沿的fade。
最后,针对多个decal,这里采用了max alpha blend的算法来实现混合。
![](https://img.haomeiwen.com/i19200103/6c24f6039db0a450.png)
最后得到了包含所有splash的displacement结果
![](https://img.haomeiwen.com/i19200103/701b57b63cc4f2e6.png)
为了避免重复,这里会通过噪声来对法线等数据做扰动,同时通过FBM算法来混合高低频信息,得到更为真实的效果表现。
在目前的设计中,总共需要叠加9个octave,但是这样消耗就太高了,这里的优化方案是增加LOD策略,近处用9个,远处就只有3个(不降低到0个的原因是,这样会导致反射效果的极端异常)。
为了节省性能,displacement 贴图不用做clear,因为会有另一张贴图指示哪些区域是有效的,有效的数据会被覆写,所以不必要clear。
![](https://img.haomeiwen.com/i19200103/92647da168f6b59a.png)
在进入屏幕空间Tessellation之前,还需要做一次剔除,去掉那些不可见的部分。
![](https://img.haomeiwen.com/i19200103/1684f6a04ffcde28.png)
这里将整个屏幕划分成一个个的小tile,每个tile的尺寸为32(测试发现这个尺寸对所有硬件都是最佳的?)。
剔除需要给出两个结果:
- tile是否覆盖水体
- 每个tile中有效像素的数目(何用?)
![](https://img.haomeiwen.com/i19200103/f5a9484c69b96fed.png)
这里给出了计算tile中有效数目的详细说明,需要注意的是,如果是在主机或者DX12的平台上,可以通过WaveActiveBallot指令,将8x8(一个group)个原子指令减少到1个。
结果存入一个structure buffer中。
![](https://img.haomeiwen.com/i19200103/18a106ee8180496e.png)
之后会用另一个pass来获取上述structure buffer,并判断当前tile是否有数据(只要大于0的话,为啥要统计像素数?),有的话,就将这个tile放到待绘制列表里:
- 对indirect draw arguments buffer加一
- 将tile ID(dispatchThreadId.x)写入到第二个structure buffer中(waterTileDataOutput)
截图右上角显示,当前帧的水体有606个mesh instance,每个带有1536个索引(顶点),可以看出是经过高度细分的。
![](https://img.haomeiwen.com/i19200103/52a659f849bd6b7c.png)
经过上述处理之后,就知道屏幕空间哪些tile要绘制水体,要做Tessellation。
![](https://img.haomeiwen.com/i19200103/3eb093a90c389334.png)
接下来开始进行Tessellation逻辑,上图给出了mesh的顶点密度,针对每个quad会做曲面细分。
![](https://img.haomeiwen.com/i19200103/6ee71d4fac71f1c8.png)
这里通过indirect draw一次性完成所有tile的绘制与曲面细分。这里没有对近远景做额外处理,采用的是常量的Tessellation密度。
![](https://img.haomeiwen.com/i19200103/05a7461be15a123c.png)
Tessellation完后,我们就得到了一个较为密集的顶点(这也是为什么采用了类似projected grid的方案,却不会有明显跳变的原因,因为顶点密度接近像素,而逐像素的displacement是不会有跳变的),接下来看看针对每个顶点,要做哪些计算:
- 获取水体的深度,转换到世界空间的坐标
- 采样该点的displacement数据,包括水体自带的FBM displacement,以及交互粒子的displacement
- 通过Nan的方式将无效的顶点剔除掉(VS剔除的唯一方法)
- 将displacement应用后的坐标投影回屏幕空间,并将该点对应的uv写入到RT中(用于实现大FOV到小FOV RT的映射)
- 通过深度测试来判断可见性
![](https://img.haomeiwen.com/i19200103/e55f9e038bff75cb.png)
![](https://img.haomeiwen.com/i19200103/8d00f72fbbf52957.png)
![](https://img.haomeiwen.com/i19200103/bdccd592effbcca4.png)
![](https://img.haomeiwen.com/i19200103/609c44c9cb9f1fc4.png)
![](https://img.haomeiwen.com/i19200103/d8380dc09425ec4e.png)
![](https://img.haomeiwen.com/i19200103/a37eb1fe6fb4cef3.png)
![](https://img.haomeiwen.com/i19200103/a0bf23f9a9e2dd74.png)
![](https://img.haomeiwen.com/i19200103/0546b2245a1b8056.png)
![](https://img.haomeiwen.com/i19200103/aa625316b538dcf5.png)
大FOV是为了避免水体的displacement将水体收缩,导致边缘数据缺失
![](https://img.haomeiwen.com/i19200103/9fa705e1ffe4be3a.png)
经过大FOV到小FOV的映射采样之后,就能得到正确结果(虽然这个映射会带来一些扭曲,但是在相机移动的情况下,基本上可以忽略)
![](https://img.haomeiwen.com/i19200103/9b1ce91105d41354.png)
上面介绍了mini G-Buffer的延迟管线计算逻辑,接下来看看法线数据。
![](https://img.haomeiwen.com/i19200103/2a67836eeaad14b7.png)
前面说了,美术同学是不用提供法线贴图的,这里是通过算法生成一张屏幕空间的法线贴图:
- 通过深度贴图计算得到
- 会根据相机到水体的距离,来调整参与计算的四个位置的offset从而实现精度的自适应
- 为了避免法线的跳变,这里还会将高频的displacement normal跟mesh(美术同学会提供一个低精度的mesh)的normal做一次混合
![](https://img.haomeiwen.com/i19200103/a66bc381d978f694.png)
除了法线,为了得到正确的屏幕空间高光效果,还需要生成屏幕空间的光滑贴图:
- 计算出每个像素的高斯法线(跟前面的法线的区别在于?)
- 最后基于法线的variance(方差)计算得到光滑度(或者就直接取方差作为光滑度,方差大说明周边法线扰动大,也就意味着不够光滑)
![](https://img.haomeiwen.com/i19200103/cdfd1a54a8184d5b.png)
![](https://img.haomeiwen.com/i19200103/6fa4ac2ca927c198.png)
![](https://img.haomeiwen.com/i19200103/ff8eb82dea9f51ce.png)
基于光滑度以及前面的逐像素的材质数据,就可以在屏幕空间中得到普通绘制方式的高光效果
![](https://img.haomeiwen.com/i19200103/db1b9918c4ca6819.png)
最后来看看foam
![](https://img.haomeiwen.com/i19200103/a715f2414271b603.png)
大致的实现思路是用一张噪声贴图来对foam贴图进行调制(放弃如FFT中的物理计算了?)。
这里一个较为复杂的点,是需要注意与flowmap的结合(没讲具体如何跟foam结合)。
foam的显示位置是通过SDF控制的,而SDF则是提前烘焙的,会采集海岸线、露出水面的物件等信息。
foam主要有两种:
- 近岸海浪的foam
- displacement foam
最终的foam是两者的叠加混合。
![](https://img.haomeiwen.com/i19200103/ccb52425dee510c1.png)
flowmap的烘焙是离线完成的
![](https://img.haomeiwen.com/i19200103/73e6cffd7fc85063.png)
基于地形跟水平面,通过flood-fill算法生成(从水源点开始,寻找水的流动方向,将结果写入flowmap)。
![](https://img.haomeiwen.com/i19200103/d4a1cad4197f67f8.png)
如果直接上flood-fill不考虑地形的高低落差的话,会出现水流效果跟真实情况存在差距的问题(如上图所示),为了避免这种情况,这里会取用SDF的数据对flood-fill算法进行约束。
![](https://img.haomeiwen.com/i19200103/b3ef58fe0c61fe5b.png)
最终得到的flowmap是一个atlas,存储的是近景的flowmap数据,分辨率较高,会需要走Streaming(GPU通过VT的方式进行取用)
![](https://img.haomeiwen.com/i19200103/1c7db8b8012d9ff6.png)
远景的数据则是一张覆盖全图的低分辨率贴图。
![](https://img.haomeiwen.com/i19200103/34c94b61a4061c3e.png)
这是覆盖全图的heightmap数据,也是离线烘焙的,一个像素覆盖8m范围。
![](https://img.haomeiwen.com/i19200103/22baf54281379b6a.png)
最后看看这些数据如何组装
![](https://img.haomeiwen.com/i19200103/10013bd2b51c93a7.png)
前面我们提到了,这里我们可以拿到带水和不带水的depth map,同时还可以拿到各个像素的material id,基于这些数据,我们就可以对各个像素做shading了。
这里采用的是Forward+的光照shading计算逻辑:
- 会基于上述数据,计算间接光,包括ambient(通过GI获取),反射(通过环境贴图与SSR方式得到)等数据
- 还会计算直接光照
- 计算局部光照
- 计算exposure光(?)
材质还支持混合,如上图右边所示,可以支持两种材质之间的混合,这个主要用于解决两种不同的水体的混合区域的过渡效果,这里的blend只会针对一些关键参数,其他参数的blend不会对效果造成过大影响,因此就不用额外浪费算力了。
![](https://img.haomeiwen.com/i19200103/9099e4e01e4d62e0.png)
![](https://img.haomeiwen.com/i19200103/58bb96d762bc28c1.png)
![](https://img.haomeiwen.com/i19200103/02b19cfcdba89448.png)
为了得到较好的光照效果,这里还做了一些额外的处理:
- 没有给美术同学提供一种直接控制颜色的手段,而是将散射系数暴露给他们进行控制
- 如上图所示,共12个参数,对应12种类型的水体,美术同学只需要从table中选择所需要的水体类型(或者创建新的)即可
- 每个类型就带一个参数,RGB颜色与最后的浑浊度(turbidity),基于这些参数可以创建任意类型的水体效果(干净的、浑浊的、海洋的、河流的)
![](https://img.haomeiwen.com/i19200103/9aff14be65b5a30f.png)
最后还需要采样foam等贴图,接下来看看这些贴图采样后的效果
![](https://img.haomeiwen.com/i19200103/030d727af2b63092.png)
这是添加了折射之后的light transport效果
![](https://img.haomeiwen.com/i19200103/7883642101747cb0.png)
叠加SSLR(screen space local reflection)效果
![](https://img.haomeiwen.com/i19200103/92c08b6563c1f1d7.png)
环境贴图反射效果
![](https://img.haomeiwen.com/i19200103/ee390cb700e71fe4.png)
叠加所有效果后的表现,包括foam、反射、折射、splash粒子、涟漪等效果。不过要想得到这个效果,会需要做非常繁重的VGPR(vector general purpose register)计算,下面来看看怎么优化这部分的消耗。
![](https://img.haomeiwen.com/i19200103/db13a356bc7cfedf.png)
首先就是调整部分属性的精度,如上图所示,可以节省9个VGPR调用。那么min16float是什么格式呢?
![](https://img.haomeiwen.com/i19200103/161106208344f965.png)
这是HLSL的一种基本类型(跟half是不一样的):
- 使用这种类型,编译器就知道其数据精度是可以降低的
- 存储在buffer中的数据还是全精度的,只是在采样的时候,GPU会自动将之转换为16位的(那存储全精度的意义在哪里)
- 但是这里并不是一定会需要采用低精度的(所以这就是存储全精度的意义,那么什么情况下会用低精度的?由软件跟硬件stack决定,具体一点呢?开发者基于算法的可能性来评估,并由QA同学测试确认)
- 支持整型与浮点型
- GLSL也有类似的字段
![](https://img.haomeiwen.com/i19200103/b02e80470d201915.png)
哪些地方需要全精度的数据:
- 贴图采样的UV坐标,精度不足容易导致块状效果
- 法线向量,精度不足,光照结果会非常低质量
3.两个接近相等的数值的相减结果 - 除以接近于0的数
- 存在累计误差的地方
![](https://img.haomeiwen.com/i19200103/8186901fe88cf037.png)
寄存器占用数目过高也会对shader的执行效率产生较大的影响,这也是日常GPU瓶颈的一个很重要的原因。
一个常用的手段是提高GPU的Occupancy(为啥提高Occupancy能够降低寄存器的压力?),Occupancy的解释是:
当前SIMD组件待执行的WaveFronts的数目与SIMD可以分配的最大slot数目的比值
这个数值越大,意味着SIMD可以调度的空间越大,当某个wavefront执行阻塞时,就可以调整到另一个Wavefront上继续执行以提升SIMD的执行效率,减少整体执行的耗时。
Occupancy会有助于增加VGPR(寄存器) usage的discrete threshold,如果VGPR压力过大的话,通过这种方式可以得到更大的收益。
![](https://img.haomeiwen.com/i19200103/e752faa83ea37286.png)
寄存器压力有这么几个主要的原因:
- 过早的读取了内存数据,并持续占用
- 循环的unrolling
- 大量的中间变量
![](https://img.haomeiwen.com/i19200103/3f5e8a3850f6a237.png)
寄存器分配的时候也会可能分配了过大的寄存器:
- 数据的通道过多:
- 多个通道的数据需要放置在连续的寄存器中
- 通道的数据需要保序
- 贴图的维度过多:
- 贴图数据同样需要连续跟保序
为啥这样的设定就会导致寄存器浪费?是有一些数据本来是不需要的吗?
![](https://img.haomeiwen.com/i19200103/ace414d4cea4b627.png)
live寄存器数目是衡量分配overhead的重要指标,如何甄别哪些寄存器是live的?可以尝试AMD的Radeon GPU Analyzer工具,下面用一个案例来介绍。
![](https://img.haomeiwen.com/i19200103/3d2dd29f77a829d5.png)
在这个案例中,没有overhead,所有的运算都是在原地(寄存器)中完成的
![](https://img.haomeiwen.com/i19200103/1917517fa4655cdb.png)
但是,如果我们稍微修改下代码,如右下角所示,在这里就只需要5个VGPR,但是分配了6个(因为计算不能在原地进行,且V2后续就不再使用,但是又不能释放 ),导致了一个浪费。
![](https://img.haomeiwen.com/i19200103/cef8e09c700f253e.png)
另外,如果使用半精度的浮点数,也有助于减少寄存器分配的浪费,比如min16float4分配的寄存器会少很多。
![](https://img.haomeiwen.com/i19200103/49400c0d1daf17bd.png)
![](https://img.haomeiwen.com/i19200103/8098394351d444a5.png)
下面来看下水体方案实现过程中的一些问题,主要有上述图中所整理的几方面的内容:
- 两个depth buffer带来的大量的bug(什么时候该用哪个并不那么明确,加上链式的后续计算,stencil等,情况会变得非常复杂)
- 大量的小尺寸贴图,可以通过pack解决,同时使用pingpong机制实现复用降低内存消耗
- 屏幕空间曲面细分带来了一些问题
- 需要大量的VS计算,与现代GPU中PS运算单元更多的设计相冲突。修改了数据的精度,降低到16位;后面准备将这个计算挪到Compute shader中,来避免这个问题
- 边界情况导致的异常效果(见后面的图)
![](https://img.haomeiwen.com/i19200103/eef342dea65711fd.png)
如上图所示,当有两个相互连接的水体(河流+海洋)时,水体在远景处就会出现拉伸
![](https://img.haomeiwen.com/i19200103/a390a773ebf57073.png)
这里是修复后的效果。
![](https://img.haomeiwen.com/i19200103/40ce630f29e57ed2.png)
解决方案是,通过一个compute shader来检测两个水体相接的位置,之后调低这部分区域的displacement。
上图中右边的红色图是屏幕空间中的displacement贴图(FBM计算)
![](https://img.haomeiwen.com/i19200103/9ab091fab198adf5.png)
![](https://img.haomeiwen.com/i19200103/c5c0c8adce0c8d44.png)
对这个图进行边缘检测,并做模糊
![](https://img.haomeiwen.com/i19200103/70a14135db7b8dce.png)
Far Cry有个钓鱼小游戏,这个会有near-z跟far-z的问题。
如上图所示,钓线会频繁的改变颜色,在开启SSR的时候,会导致水体出现比较明显的闪烁效果。
![](https://img.haomeiwen.com/i19200103/2783e160bd1bfcb9.png)
解决方案是在SSR的ray tracing做完之后,再增加一个后处理pass。
![](https://img.haomeiwen.com/i19200103/1dde2215f45d3d9a.png)
![](https://img.haomeiwen.com/i19200103/9a1ac2cdf12b27d2.png)
![](https://img.haomeiwen.com/i19200103/ff86eb7cb9b0ab67.png)
这里提供了一些debug工具
![](https://img.haomeiwen.com/i19200103/64b4797b5305d266.png)
最终的性能数据,GPU总计耗时1.9ms,绿色是compute shader部分
![](https://img.haomeiwen.com/i19200103/ba00264e32f71f4a.png)
如果开启async compute,还能优化0.6ms
![](https://img.haomeiwen.com/i19200103/859302daa8aa7ab5.png)
这里给了个最终的视频。
![](https://img.haomeiwen.com/i19200103/0aa11035d538e2fa.png)
![](https://img.haomeiwen.com/i19200103/9ee27101a596b5a1.png)