【USparkle专栏】假如你深怀绝技,爱“搞点研究”,乐于共享也博采众长,咱们等待你的参加,让才智的火花磕碰交错,让知识的传递生生不息!


一、开发需求

这是工程开发的细节,不是理论篇,不了解RVT理论概念的,请先搜索。

RVT的理论遍及度比较高,FARCRY5和Unreal Engine里都有许多的共享,这种计划工作量和难度主要是在工程细节上。也便是一个资深技能完好按计划写完落地功用大概要1个月。所以我很想造一个既有差不多功用收益,又简单许多的完成计划,差不多开发3天可落地项目。

为什么要对地势做RVT?由于地势的采样除了颤动混合,其他都需求十分屡次的采样Albedo和Normal然后混合。这现已成为多数项目在GPU方面的最大开支,所以有必要缓存他。其次,地势的Mesh结构单一性、原料一致性、贴图共用性都导致仅对地势完成RVT是比较便利的。

以下是我参与的项目4年多来地势烘托的技能迭代进程,原本计划仅1次采样的颤动混合作为终极计划,实际上产品和美术关于噪点难以承受。这样就反而简单了,有必要开发RVT的想法就在我脑中种下了。

《生死狙击2》地势烘托技能进化进程

实在项目落地弥补
一般视角:提升2ms,70fps->82fps。截图为最大收好处空气背包与跳伞时:

大地势的一种简化RVT

1050 中画质 线上游戏地势(优化前)

大地势的一种简化RVT

1050 中画质 线上游戏地势(优化后)

二、功用比照

采用i5-9600KF AMD、RX590、Unity 5.6测验,一共16层地表。

静帧比照
Unity自带地势:231 FPS
本计划地势:546 FPS

移动时比照
看视频里帧数

先放作用图看下收益幅度,或许才有耐心能看完细节。后边与GPUDriven地势结合,功用会更好,由于这现已是地势地块CPU瓶颈了。

大地势的一种简化RVT

视频

三、主要思路

原文的思路有许多种,也很杂乱。首要要多烘托一份数据,然后Feedback,推迟一帧取得,然后各种VT的不同尺度,掩盖不同PageTable的数量,对应物理贴图图集里的不同巨细,这些不同巨细的掩盖或对应联络有些是在1个Mipmap里做,有些是放到不同的Mipmap上完成,以及物理贴图图集里,不同尺度可用空间的请求、收回、占用保护等。还有异步加载时处于为准备好的区域如何寻觅替代的更低精度的Mipmap地址等等。看的我十分晕,所以这一刻忘记这一切概念。重新想一个最最简单直观的。先用四叉树区分地块,这一点之前做四叉树静态阴影和四叉树GPUDriven的地势都用到过。所以比较简单了。

依据相机间隔如下图这样区分国际空间,并给每个空间分配一个独立的编号,叫他物理地址索引。咱们只要让每一块都用一张相同巨细的贴图去显现,那么天然便是比较合理的运用显存了,也等于是近处用了mipmap0,远处用了mipmap1,2,3….了,用这种思想来完成就直观且便利许多,由于一切贴图尺度相同可以用一个Texture2DArray来存放,而编号便是这个数组的Index。当某一块的尺度需求改变时,才重新加载改变后对应的图。

大地势的一种简化RVT

用四叉树把国际空间按xz平面投影(依据相机间隔)区分方法

四、四叉树的完成

四叉树是这个计划的主要功用所以会写得比较多。四叉树尽管重复运用,但常常长的不同,这是由于有时候需求用来遍历,有时候需求用来查找,有时候是为了内容附近而压缩数据,有时候是做LOD区分。所以这儿详细讲下这次四叉树的完成细节。

由于上图每个要显现的节点都是叶节点,不是枝节点。所以常见有2种方法遍历。

  1. 每帧从根节点开端遍历,递归查询自己的4个子节点,做LOD是否发生改变的判别
  2. 把叶节点,记录到一个队列里,每帧只对这些叶节点做LOD是否发生改变的判别

为什么要判别LOD是否发生改变呢?由于假如LOD没变,那么原来显现图不需求替换,就不需求做任何处理。假如发现远离了相机并且LOD需求更大,那么说明不需求这么高清了,他可以测验兼并,用他父节点来加载一张掩盖更大面积的图来显现(图是相同尺度的所以掩盖更大面积等于更低精度),反之相机靠近了,LOD就需求小,他就需求细分出4个节点,每个节点都去加载对应的图,这样他就精度翻倍了。

我把这2种都完成了一遍,发现第2种的代码逻辑更直观,第1种需求做一个状态保护,所以这儿讲第2种的方法。

四叉树数据结构

大地势的一种简化RVT

四叉树数据结构

static变量:

currentAllLeaves:当时帧一切叶节点

nextAllLeaves:下一帧一切叶节点

physicEmptyIndexQueue:可用的物理地址队列

onLoadData:某节点需求加载贴图资源时回调,由于这种加载一般不做在树结构内

splitCount:当时帧现已细分的次数

eventFrameSplitCountMax:每帧可细分的最大次数,与splitCount一同,防止在同一帧加载太多导致卡顿,实际是一种简单又高功用的分帧机制。分帧加载机制,我用分帧细分四叉树替代,极大简化了保护。 否则异步的加载,相邻部分加载完成替换索引会呈现脏数据等问题。

成员变量:

x,z,size:四叉树最最根底的数据,记录这个格子坐标和尺度

children,parent:描绘四叉树树结构联络的引证,类似Transform

isLeaf:判别是否是叶节点

parentMerged:当时帧Parent是否被兼并过了,由于遍历某节点的4个子节点顺序是不可控的,防止呈现一个子节点判别应该兼并,但其他子节点却判别为细分呈现对立。

physicTexIndex:当时节点的物理贴图(Texture2DArray)索引,用他来烘托自己掩盖的区域

创立根节点
一个树一般手动创立根节点,然后通过规则让他自己内部去细分或兼并。也常在这儿做些初始化或静态数据创立。这儿主要是创立一个Node节点size取得一个物理地址,并放入叶节点。由于这个时候,根节点便是叶节点,他还没子节点。这儿没设置xz,是由于不管实在场景如何,四叉树内部都是从(0,0)点开端往x ,z 方向去核算的。外部的实际情况可依据Offset调整,不在内部考虑外界的特殊性。

大地势的一种简化RVT

创立根节点函数

每帧更新一切叶节点状态
遍历一切当时叶节点,查看LOD是否发生改变,假如没改变就放入下一帧叶节点队列。假如改变,依据变大仍是变小来做兼并仍是细分的处理,最终是常见的交流2个列表,下一帧的数据作为下一帧的“当时”数据重复遍历。不必简单赋值而用交流,是由于不想每帧New一个空队列产生GC。

大地势的一种简化RVT

每帧主循环

关于每个节点,首要判别他父节点是否需求兼并,假如自己和其他3兄弟节点都没子节点且 父节点核算后LOD发现应该兼并,那么才履行兼并,并且设置每个子节点的parentMerged为true,假如不能兼并再判别自己是保持到下一帧仍是细分,细分也有许多束缚,这些细节的考量是我花的主要时间。

大地势的一种简化RVT

节点LOD核算判别是保持仍是兼并仍是细分

兼并与细分
兼并函数比较简单,把自己放入叶节点,把4个子目标标记兼并后,收回子目标物理索引,分配自己一个地址索引然后加载这个索引对应的资源。

大地势的一种简化RVT

节点兼并

大地势的一种简化RVT

节点细分

不管兼并仍是细分,每帧都只履行一次,这样很好地完成了分帧处理,假如要更好的作用还需求设置权重决议处理的顺序,比方近的优先,或LOD改变大的优先。还有一个小技巧便是先收回索引资源,然后再分配,这样削减一点点资源不足的情况。

每帧只对一个叶节点处理一次 完成天然的分帧作用

实时移动相机的四叉树分帧细分与兼并作用

创立贴图内容
分配了索引之后,咱们可以依据节点所在的方位和Size,去加载这块混合后的贴图。并拷贝到Texture2DArray对应的Index里。这儿说的加载不是真的加载,假如是SVT那便是硬盘加载。咱们做RVT,这儿其实是实时创立。为了流程描绘一致特意说成加载。这种实时创立有2种方法,第一种是放个相机去拍,这种简单也能对格子贴花、路面等自动支撑,可是功用欠好。由于烘托流程要走一遍。相机要对地势Mesh各种处理,这些都是咱们不需求的。所以我这儿采用功用更高的Blit方法,缺陷是做路面与贴花时需求再开发功用支撑。

大地势的一种简化RVT

实时生成地块内容

原本直接用地势Shader改改就行,可是他是每4张一个Pass,需求Blit好屡次,关键是还要对这些成果做混合,像素拷贝太多了,所以这儿改了下,用Texture2DArray存放地势地表纹路比方16张。一次性采样完。由于要输出Albedo和Normal2份数据,所以这儿给了开关,假如要一次取得可测验MRT,但我这儿不想再展开。使用Builtin自带地势Shader的Firstpass,一次性采样完一切图层。

大地势的一种简化RVT

依据地块方位不同、尺度不同,做偏移和缩放

大地势的一种简化RVT

制造节点对应贴图的Shader

索引贴图
四叉树节点上对应的贴图创立好了,可是烘托的时候,一个ShadingPoint怎么知道自己要采样第几张图的呢?依据自己的国际坐标或地势UV来查询四叉树?这肯定是欠好的杂乱又过度采样。所以都是给他制造一张索引图,和他UV一一对应,他依据地势UV就能访问到对应的纹素,比方Frag函数里一个ShadingPoint,他在地势上UV是(0.5,0.5),那么他去索引图的(0.5,0.5)采样就可以取得节点数据,包含了索引、尺度和起始的xz4个值。然后就可以核算出在Texture2DArray里的UV坐标和Index。

可是这个索引假如是均匀的,其单位是多少呢?比方这儿咱们四叉树最小一格Size是1(国际坐标先当1米用)。那么这个索引图便是1个纹素对应1米。四叉树节点加载好贴图放入Texture2DArray后 ,就要填充索引图对应纹素的内容,好让采样的Shader能查询正确。假如这个节点Size是1当然就填1×1纹素,假如是4×4的Size就掩盖了4×4米,当然索引图需求填充4×4纹素同样的值。这样看起来有点傻,第1,浪费空间;第2,写入数据也变多;第3,采样时候采样了不是同一方位的同一个值成果正确但缓存命中变差。所以有同事建议依据FARCRY5那套把2×2的写到Mipmap1的一个像素,把4×4写到Mipmap2的一个像素。看上去很夸姣实际上不行,由于我这计划省掉了Feedback这一整个进程,所以CPU依据间隔核算出的Mipmaps与烘托时Fragment里核算的会不一致,这样导致他去索引贴图某Mipmap取值时,内容是错误的,由于写入的是另一个Mipmap,而只要获取到正确内容后 才知道CPU核算这处的LOD是多少,也才知道他写入了哪个Mipmap。这儿运用ComputeShader填充索引贴图内容。就一句代码:

Result[id.xy  uint2(offsetX, offsetZ)] = value;

大地势的一种简化RVT

制造数据放入数组并填充索引贴图内容

大地势的一种简化RVT

斜面噪点

噪点比照

按这样完成出来,会发现斜面有噪点,这是必然的。由于咱们是依据间隔指定的LOD,也就相当于贴图的Mipmap。而实际烘托是依据ddx和ddy来核算Mipmap的。也便是说法线与视角方向挨近垂直的面,他们即使离的近,也不能用mipmap0的,由于2个屏幕像素在贴图空间跨了好几个纹素。所以需求给他准备多份Mipmap数据,可是最明晰的那个便是咱们现在给的,所以严格来说,假如咱们要传给他mipmap4,但他最终一级到mipmap8,咱们需求把4、5、6、7、8都传给他。但实际上不会这么极端。所以我通过实践发现给4个满足用了。所以咱们这样批改代码和Shader,咱们需求在Shader内手动核算Mipmap并与CPU核算的Mipmap(也便是节点的Size巨细做差值,由于用一个同尺度贴图烘托一个更大Size规模,等于现已做了Mipmap改变)这个算法我自己想的。

大地势的一种简化RVT

传4个Mipmap用于不同视点的不同需求

大地势的一种简化RVT

批改Mipmap问题的Shader采样

批改Mipmap核算后不会特别锐化

这样看起来作用就好多了,可是ddx和ddy必定要用均匀的地势UV来做,假如用数组内UV会有接缝,这是由于相邻节点联接处textureArray内UV一个是0一个是1,ddx/ddy就会误认为是不接连的改变。

MRT晋级
为了做贴花和道理烘托准备,现已晋级到MRT烘托方法,从0.2(2次0.1)ms优化到0.14ms。其中ComputeShader开支较大是测验问题,重复加载最远处图来测验,实际上远处图改变频率很低。

大地势的一种简化RVT

原来2次DrawQuad

大地势的一种简化RVT

MRT1次DrawQuad

Git库地址:
GitHub – jackie2009/unityRVTTerrain: a runtime virtural texture terrain for unity 5.6


这是侑虎科技第1551篇文章,感谢作者jackie 偶然不帅供稿。欢迎转发共享,未经作者授权请勿转载。假如您有任何独特的见地或者发现也欢迎联络咱们,一同探讨。(QQ群:465082844)

作者主页:www.zhihu.com/people/jack…

再次感谢jackie 偶然不帅的共享,假如您有任何独特的见地或者发现也欢迎联络咱们,一同探讨。(QQ群:465082844)