【Siggraph 2020】 large voxel landscapes on mobile

在月光下坐了一会儿,心里很平和。生命待我不薄,我也知道自己的努力。没有特别幸运,也从未被亏待,前面的路更美或许更难,但我已做好准备,给出一切,来成为我自己。
—— 《如花在野》

这里来分享下Roblox在Siggraph 2020年上关于他们引擎或游戏中基于体素的地形绘制方案的相关细节的介绍,原始pdf跟video链接在文末有给出。

先来看下地形系统希望达成的目标:

  • 不能通过预计算(烘焙)的方式来完成计算:因为场景变化频率很高,预处理没有意义
  • 全3D地形,支持洞穴、悬挂物、桥梁等
  • 缩放性良好,能够支持到最大10+平方公里的地形
  • 编辑工具强大而便捷?
  • 支持丰富的材质效果(不只是视觉材质,还有物理材质)

地形是基于voxel创建的,而voxel是稀疏的,具有LOD层级的,每个voxel都对应于一个材质与occupancy(暂时不明白这个代表的是什么,位置+scale?),对于地形的其他属性的编辑与控制则是基于材质实现的(这里指的是什么?)

早期开发的时候,实验了较多类型的数据,最终基于简单考虑选择了occupancy方案:

  • 基于材质的方案
  • 基于SDF的方案
  • 基于Hermite插值(这类插值在给定几个数据采样点上要求插值函数的结果与采样点完全吻合,还要求插值函数在采样点处的导数与被插值函数的倒数相同)的方案
  • 基于occupancy的方案:从上图描述,看起来是指某个voxel被填充的比例

voxel的数据是以grid的方式进行存储的,这里由于voxel数据是稀疏的,因此grid数据也是稀疏存储的,而存储的最小单位不是voxel而是chunk。
每个chunk存储了一个mip pyramid,即一个包含了不同LOD(mip)的voxel cube,最高层的LOD(指的是1^3?)可以不用存储(为什么?是因为用不上,还是其他原因比如可以直接根据下一级数据直接计算得到?)
chunk的streaming逻辑是跟内存压力息息相关,即当内存压力大的时候,可以适当提高LOD层级,取较为粗糙的一级。

再来看下voxel mip的存储细节:

  • empty mip(即没有任何数据填充的mip?)与full mip(使用同一种数据填充的mip?),只需要消耗一个byte表示材质即可(看起来材质种类不超过256)
  • 对于每一行所使用的的材质相同的mip(默认都是填满的,不需要occupancy数据),只需要用一个压缩数据结构来表示,即每一行只需要一个材质byte,每一列应该也有类似逻辑
  • 对于同一行中存在不同材质的情况,就无法用压缩逻辑存储,这时候会使用2个bytes来存储每个voxel的数据,即每个voxel需要存储对应的material与occupancy
  • 在voxel数据更新之后,就会触发上面数据的Repack(重新计算与存储)

在拿到voxel数据之后,摆在面前的一个问题是,我们绘制上要怎么得到平滑效果,voxel平滑绘制最出名的方案是marching cube,但是评估后发现存在着一些问题:

  • marching cube输出的是一个non-uniform的网格拓扑结构(问题在哪里?)
  • 需要绑定breaking rules(具体是啥?)
  • 结果不直观,会对编辑(voxel数据)造成约束。

这里选择的是一个叫做dual method的方案,这个方案来自于Dual Contouring以及Naive Surface Net方案,实现过程放在CPU上,当然,经过了大量的优化。

大概介绍一下方案的实现逻辑:

  • 这里在每个(与等值面相交的)cell中都需要找到一个对应的顶点(这里没说顶点怎么计算的)
  • 相邻cell之间的连接是通过上图所示的quad(四边形)来实现的

看起来似乎是将occupancy不为零的voxel用一个个的cube累积起来了,不过中间的面片省掉,只保留了外部的面片?

再来看下这个方案的顶点数据的一些细节:

  • 在shading的时候,每个顶点只支持一种材质,而这种材质取用的则是基于顶点所处的voxel grid中grid points中的主材质
  • 顶点位置则是直接取等值面与voxel edge相交点的平均值(重心)
  • 顶点法线则是将面法线做平均得到
  • 除此之外,这里还做了一些额外的工作

上面展示的是到目前为止得到的一些效果。

这里roblox还对顶点做了一些变形处理。

这个形变是基于材质实现的,提供了如下的一些操作类型:

  • Shift,添加了伪随机偏移
  • Cubify,朝着box lerp
  • Quantize:量子化,阶跃函数
  • Barrel:沿着Y方向进行Cubify
  • 以及一些其他的操作,尤其是一些专为水体添加的数学运算

此外,材质还会定义出voxel在渲染时的边沿是软边还是硬边。

这里展示了不同材质对voxel网格化效果的影响。

这里来介绍下贴图映射的实现方案,这里的问题是,如果为每个顶点设定一种材质,那么我们最终输出的效果是怎么样的,每个像素渲染的时候如何采样?

由于像素是落在三角形面片上的,我们可以确定的是,需要对三个顶点的材质进行采样,之后基于质心方式进行插值(类似于光栅化时候的算法,可以参考Ray Tracing: Rendering a Triangle)

如上图所示,对于一个三角形ABC而言,其中的任意一个点P都可以表示成:

其中,0 <= u, v, w <= 1。这三个参数可以通过如下公式计算得到:

可以看到,其实就是顶点对应的三角形的面积占比,所以这也是为什么baricentric coordinate也被成为area coordinate的原因。

回到这里的问题,这里具体没有说怎么对三个材质效果进行blend,个人想法是,由于面片在光栅化的时候,会自动对顶点上的属性进行插值,因此这里我们可以通过一个取巧的办法来得到uvw的数值,即在三角形的三个顶点上添加一个属性baryParam,这是一个vector3,对于ABC三个顶点,我们分别赋值(1, 0, 0),(0, 1, 0),(0, 0, 1),这样经历硬件插值之后,我们在像素上就得到了(w, u, v)了,之后再用这个参数对采样后的贴图进行blend即可。

如果只是一个三角形还比较简单,但是这里是一个三角形list,因此在处理的时候需要对每个三角形的顶点进行遍历。

另外由于顶点来自于voxel的模拟,这里想要实现对3D地形材质的模拟,需要考虑三个方向(顶面、侧面、正面)材质叠合的效果,最简单最常用的肯定是triplanar mapping算法了,不过这里的问题在于贴图采样数会非常多。

triplanar算法会贡献3倍采样,像素需要对三个顶点的材质进行采样,再加上PBR的三张贴图,那就是27个采样,代价太高了。

这里基于Siggraph 2011的一篇文章做了近似处理,这篇文章介绍的是如何用若干张 小尺寸的贴图覆盖在不同的顶点上,之后在面片中间通过一个算法进行加权混合来得到高精分辨率贴图的效果。

基于这个算法,对于每个像素,只需要对每个顶点的贴图做一次采样,那么总计三套贴图的采样消耗,低端机就是三次,高端机(PBR)就是9次,相当于是Triplanar方案的三分之一。

这里的顶点是voxel转换到mesh后的顶点,这些顶点数据是运行时生成的,这里就需要知道,每个顶点对应的小贴图要如何选取?

从上图来看,这里说的是设计了总计18个投影平面,之后为每个顶点选定一个(具体算法不明,可以猜测一下,基于距离?),并将这个平面对应的索引写入顶点属性,这里没有提到贴图,但是猜测一下,每个投影平面会对应于一张贴图,只是这个贴图是怎么得到的,这里就没有提,在必要的时候可以截帧看看,或者找下这篇文章对应的视频做一下补充。

上面两张图,演示了一下效果,虽然并没有太get到里面的意思。

在将像素投影到顶点对应的贴图的UV平面之后,为了避免纹理重复,这里做了如下计算:

  1. 基于顶点的随机数种子,做一次平移(shift)与旋转(rotate)
  2. 基于每个材质的一些计算逻辑,对贴图采样的UV做一些处理

经过这个处理之后,如果三个顶点使用同一套材质,那么我们就能得到无重复的纹理效果。

可能真的要截帧看下具体这里是怎么计算的了,下面给出截帧代码与简单分析:

Shader hash 9629d120-ad00d2ac-9075c084-8169dd82

vs_5_0
      dcl_globalFlags refactoringAllowed
      dcl_constantbuffer cb0[23] (CB0), immediateIndexed
      dcl_constantbuffer cb1[1] (CB1), immediateIndexed
      dcl_constantbuffer cb2[4] (CB2), dynamicIndexed
      dcl_input v0.xyzw
      dcl_input v1.xyzw
      dcl_input v2.xyzw
      dcl_input v3.xyzw
      dcl_output_siv o0.xyzw, position
      dcl_output o1.xyzw
      dcl_output VertUV.xyzw
      dcl_output o3.xyzw
      dcl_output o4.xyzw
      dcl_output o5.xyz
      dcl_output o6.xyzw
      dcl_output o7.xyz
      dcl_output o8.xyz
      dcl_output o9.xyz
      dcl_temps 7
   0: ilt r0.x, v0.w, l(0)
   1: movc r0.x, r0.x, l(-0.0020), l(0x80000000)
   2: mov [precise(w)]  r1.w, l(1.0000)
   3: itof [precise(yzw)]  r0.yzw, v0.xxyz
   4: mul [precise(yzw)]  r0.yzw, r0.yyzw, CB1.wwww
   5: add [precise(xyz)]  r1.xyz, r0.yzwy, CB1.xyzx
   6: dp4 [precise(y)]  r0.y, CB0.ViewProjection[0][2].xyzw, r1.xyzw
   7: add o0.z, r0.x, r0.y
   8: dp4 [precise(x)]  o0.x, CB0.ViewProjection[0][0].xyzw, r1.xyzw
   9: dp4 [precise(y)]  o0.y, CB0.ViewProjection[0][1].xyzw, r1.xyzw
  10: dp4 [precise(w)]  o0.w, CB0.ViewProjection[0][3].xyzw, r1.xyzw
  11: imax [precise(x)]  r0.x, -v0.w, v0.w
  12: itof [precise(y)]  r0.y, r0.x
  13: ilt r0.x, l(3), r0.x
  14: movc o1.w, r0.x, l(0), l(1.0000)
  15: mul [precise(x)]  r0.x, r0.y, l(0.2500)
  16: frc [precise(x)]  r0.x, r0.x
  17: eq [precise(xyz)]  r0.xyz, r0.xxxx, l(0.2500, 0.5000, 0.7500, 0.0000)
  18: and [precise(xyz)]  o1.xyz, r0.xyzx, l(1.0000, 1.0000, 1.0000, 0.0000)
  19: utof r0.z, v2.w
  20: mul r0.x, r0.z, l(2.6651)
  21: round_ni r0.w, r0.x
  22: mov r0.x, v3.y
  23: dp3 r2.w, r1.xyzx, cb2[r0.x + 0].xyzx
  24: iadd r3.xyz, v3.xyzx, l(18, 18, 18, 0)
  25: dp3 r2.z, r1.xyzx, cb2[r3.y + 0].xyzx
  // r4.xyz = v2.xyz + 36
  // 36 is the offset for UV Transform Param in Constant Buffer
  // x & y for different vertex
  26: iadd r4.xyz, v2.xyzx, l(36, 36, 36, 0)
  27: mul r0.xy, r2.zwzz, cb2[r4.y + 0].xxxx
  28: mad r1.w, r0.z, l(0.0078), l(-1.0000)
  29: mul r2.z, r1.w, cb2[r4.y + 0].z
  30: mov r2.w, -r2.z
  31: mul r2.xy, r0.xyxx, r2.zwzz
  // r1.w = 1 - r2.z * r2.z;
  // looks like sin function or circle coordinates
  32: mad r1.w, -r2.z, r2.z, l(1.0000)
  // r1.w = sqrt(r1.w);
  33: sqrt r1.w, r1.w
  // r0.xy = ro.yx * r1.w + r2.xy
  34: mad r0.xy, r0.yxyy, r1.wwww, r2.xyxx
  // VertUV.zw = cb2[r4.y].y * r0.z + r0.x;
  35: mad VertUV.zw, cb2[r4.y + 0].yyyy, r0.zzzw, r0.xxxy
  36: mov r0.x, v3.x
  37: dp3 r0.y, r1.xyzx, cb2[r0.x + 0].xyzx
  38: dp3 r0.x, r1.xyzx, cb2[r3.x + 0].xyzx
  39: dp3 r2.x, r1.xyzx, cb2[r3.z + 0].xyzx
  40: mul r0.xy, r0.xyxx, cb2[r4.x + 0].xxxx
  41: utof r3.xyzw, v1.wxyz
  42: mad r5.xyzw, r3.yzwx, l(0.0079, 0.0079, 0.0079, 0.0078), l(-1.0000, -1.0000, -1.0000, -1.0000)
  43: mul r6.x, r5.w, cb2[r4.x + 0].z
  44: mov r6.y, -r6.x
  45: mul r0.zw, r0.xxxy, r6.xxxy
  46: mad r1.w, -r6.x, r6.x, l(1.0000)
  47: sqrt r1.w, r1.w
  48: mad r0.xy, r0.yxyy, r1.wwww, r0.zwzz
  49: mul r0.z, r3.x, l(2.6651)
  50: round_ni r3.y, r0.z
  51: mad VertUV.xy, cb2[r4.x + 0].yyyy, r3.xyxx, r0.xyxx
  52: mov o3.yw, l(0, 0, 0, 0)
  53: mov o3.x, cb2[r4.x + 0].w
  54: mov o3.z, cb2[r4.y + 0].w
  55: mov r0.x, v3.z
  56: dp3 r2.y, r1.xyzx, cb2[r0.x + 0].xyzx
  57: mul r0.xy, r2.xyxx, cb2[r4.z + 0].xxxx
  58: utof [precise(yzw)]  r2.xyzw, v3.wxyz
  59: mad r0.z, r2.x, l(0.0078), l(-1.0000)
  60: mul r3.x, r0.z, cb2[r4.z + 0].z
  61: mov r3.y, -r3.x
  62: mul r0.zw, r0.xxxy, r3.xxxy
  63: mad r1.w, -r3.x, r3.x, l(1.0000)
  64: sqrt r1.w, r1.w
  65: mad r0.xy, r0.yxyy, r1.wwww, r0.zwzz
  66: mul r0.z, r2.x, l(2.6651)
  67: round_ni r3.y, r0.z
  68: mov r3.x, r2.x
  69: lt [precise(xyz)]  r2.xyz, l(7.5000, 7.5000, 7.5000, 0.0000), r2.yzwy
  70: and [precise(xyz)]  o8.xyz, r2.xyzx, l(1.0000, 1.0000, 1.0000, 0.0000)
  71: mad o4.xy, cb2[r4.z + 0].yyyy, r3.xyxx, r0.xyxx
  72: mov o4.z, cb2[r4.z + 0].w
  73: mov o4.w, l(0)
  74: mad r0.xyz, r5.yxzy, l(6.0000, 6.0000, 6.0000, 0.0000), r1.yxzy
  75: mov o7.xyz, r5.xyzx
  76: mad o5.xyz, r0.xyzx, CB0.LightConfig0.xyzx, CB0.LightConfig1.xyzx
  77: add r0.xyz, -r1.xyzx, CB0.CameraPosition[0].xyzx
  78: mov [precise(xyz)]  o6.xyz, r1.xyzx
  79: dp3 r0.x, r0.xyzx, r0.xyzx
  80: sqrt o6.w, r0.x
  81: utof o9.xyz, v2.xyzx
  82: ret

Pixel Shader

Shader hash 2a2ef6fd-9e749ee5-4eaaf0b6-f70d7886

ps_5_0
      dcl_globalFlags refactoringAllowed
      dcl_constantbuffer cb0[53] (CB0), immediateIndexed
      dcl_constantbuffer cb8[15] (CB8), dynamicIndexed
      dcl_constantbuffer cb4[4] (CB4), dynamicIndexed
      dcl_sampler AlbedoMapSampler (s0), mode_default
      dcl_sampler ShadowAtlasSampler (s1), mode_default
      dcl_sampler PrefilteredEnvBlendTargetSampler (s2), mode_default
      dcl_sampler SpecularMapSampler (s3), mode_default
      dcl_sampler LightMapSampler (s6), mode_default
      dcl_sampler LightGridSkylightSampler (s7), mode_default
      dcl_sampler PrecomputedBRDFSampler (s11), mode_default
      dcl_sampler PrefilteredEnvIndoorSampler (s14), mode_default
      dcl_sampler PrefilteredEnvSampler (s15), mode_default
      dcl_resource_texture2darray (float,float,float,float) AlbedoMapTexture (t0)
      dcl_resource_texture2d (float,float,float,float) ShadowAtlasTexture (t1)
      dcl_resource_texturecube (float,float,float,float) PrefilteredEnvBlendTargetTexture (t2)
      dcl_resource_texture2darray (float,float,float,float) SpecularMapTexture (t3)
      dcl_resource_texture3d (float,float,float,float) LightMapTexture (t6)
      dcl_resource_texture3d (float,float,float,float) LightGridSkylightTexture (t7)
      dcl_resource_texture2d (float,float,float,float) PrecomputedBRDFTexture (t11)
      dcl_resource_texturecube (float,float,float,float) PrefilteredEnvIndoorTexture (t14)
      dcl_resource_texturecube (float,float,float,float) PrefilteredEnvTexture (t15)
      dcl_input_ps linear v1.xyzw
      dcl_input_ps linear v2.xyzw
      dcl_input_ps linear v3.xz
      dcl_input_ps linear v4.xyz
      dcl_input_ps linear v5.xyz
      dcl_input_ps linear v6.xyzw
      dcl_input_ps linear v7.xyz
      dcl_input_ps linear v9.xyz
      dcl_output o0.xyzw
      dcl_temps 14
   0: mov r0.xy, v2.xyxx
   1: mov r0.z, v3.x
   2: sample_indexable(texture2darray)(float,float,float,float) r1.xyz, r0.xyzx, SpecularMapTexture.xyzw, SpecularMapSampler
   3: mov r2.xy, v2.zwzz
   4: mov r2.z, v3.z
   5: sample_indexable(texture2darray)(float,float,float,float) r3.xyz, r2.xyzx, SpecularMapTexture.xyzw, SpecularMapSampler
   6: sample_indexable(texture2darray)(float,float,float,float) r4.xyz, v4.xyzx, SpecularMapTexture.xyzw, SpecularMapSampler
   7: mul r3.xyz, r3.xyzx, v1.yyyy
   8: mad r1.xyz, r1.xyzx, v1.xxxx, r3.xyzx
   9: mad r1.xyz, r4.xyzx, v1.zzzz, r1.xyzx
  10: sample_indexable(texture2darray)(float,float,float,float) r0.xyzw, r0.xyzx, AlbedoMapTexture.xyzw, AlbedoMapSampler
  11: sample_indexable(texture2darray)(float,float,float,float) r2.xyzw, r2.xyzx, AlbedoMapTexture.xyzw, AlbedoMapSampler
  12: sample_indexable(texture2darray)(float,float,float,float) r3.xyzw, v4.xyzx, AlbedoMapTexture.xyzw, AlbedoMapSampler
  // v9来自顶点属性v2
  // r4.xyz = v9.xyz + 0.5;
  13: add r4.xyz, v9.xyzx, l(0.5000, 0.5000, 0.5000, 0.0000)
  14: ftoi r4.xyz, r4.xyzx
  // r5.xyz = cb4[r4.x].xyz -1;
  15: add r5.xyz, l(-1.0000, -1.0000, -1.0000, 0.0000), cb4[r4.x + 0].xyzx
  // r5.xyz = r0.w * r5.xyz + 1;
  16: mad r5.xyz, r0.wwww, r5.xyzx, l(1.0000, 1.0000, 1.0000, 0.0000)
  // 基于透明度进行加权,透明度可能就是贴图的weightmap,加权需要基于一个constbuffer的数值,这个数值因顶点(面片)不同而有所不同
  // cb取值不知道怎么计算的,一共四个vector,其中前面三个是相同的,但是相加不等于1,平方和也不等于1,可能是用来给美术同学控制效果使用的
  // cb4取值小于1(0.43529,0.49412, 0.24313), 那么r0.w越大,采样占比越小
  // r0.xyz = r0.xyz * (r0.w * (cb4[r4.x].xyz -1) + 1);
  17: mul r0.xyz, r0.xyzx, r5.xyzx
  18: add r4.xyw, l(-1.0000, -1.0000, 0.0000, -1.0000), cb4[r4.y + 0].xyxz
  19: mad r4.xyw, r2.wwww, r4.xyxw, l(1.0000, 1.0000, 0.0000, 1.0000)
  20: mul r2.xyz, r2.xyzx, r4.xywx
  21: add r4.xyz, l(-1.0000, -1.0000, -1.0000, 0.0000), cb4[r4.z + 0].xyzx
  22: mad r4.xyz, r3.wwww, r4.xyzx, l(1.0000, 1.0000, 1.0000, 0.0000)
  23: mul r3.xyz, r3.xyzx, r4.xyzx
  24: mul r2.xyz, r2.xyzx, v1.yyyy
  // v1 looks like the vertex weight
  // blendVal = r0.xyz * v1.x + r2.xyz * v1.y + r3.xyz * v1.z;
  25: mad r0.xyz, r0.xyzx, v1.xxxx, r2.xyzx
  26: mad r0.xyz, r3.xyzx, v1.zzzz, r0.xyzx
  // r2.xyz = r0.xyz * r0.xyz; 
  // why square? make it dark?
  27: mul r2.xyz, r0.xyzx, r0.xyzx
  28: add r3.xyz, -v6.xyzx, CB0.CameraPosition[0].xyzx
  29: mad_sat r0.w, -v6.w, CB0.RefractionBias_FadeDistance_GlowFactor_SpecMul.y, l(1.0000)
  30: dp3 r1.w, v7.xyzx, v7.xyzx
  31: rsq r1.w, r1.w
  32: mul r4.xyz, r1.wwww, v7.xyzx
  33: dp3 r1.w, r4.xyzx, -CB0.Lamp0Dir.xyzx
  34: dp3 r2.w, r3.xyzx, r3.xyzx
  35: rsq r2.w, r2.w
  36: mul r5.xyz, r2.wwww, r3.xyzx
  37: mad r6.x, r1.y, l(0.9110), l(0.0890)
  // r0.xyz = r0.xyz * r0.xyz - 0.04;
  38: mad r0.xyz, r0.xyzx, r0.xyzx, l(-0.0400, -0.0400, -0.0400, 0.0000)
  // r0.xyz = r1.x * r0.xyz + 0.04;
  39: mad r0.xyz, r1.xxxx, r0.xyzx, l(0.0400, 0.0400, 0.0400, 0.0000)
  40: dp2 r1.y, r1.zzzz, r0.wwww
  41: mul r1.z, r0.w, CB0.SkyGradientBottom_EnvSpec.w
  42: dp3 r3.w, r4.xyzx, r5.xyzx
  43: max r6.y, r3.w, l(0.0001)
  44: dp3 r3.w, -r5.xyzx, r4.xyzx
  45: add r3.w, r3.w, r3.w
  46: mad r5.xyz, r4.xyzx, -r3.wwww, -r5.xyzx
  47: mad r7.xyz, -CB0.Lamp0Dir.xyzx, l(0.0010, 0.0010, 0.0010, 0.0000), v6.xyzx
  48: add r8.xyz, v5.xyzx, -CB0.LightConfig2.xyzx
  49: ge r8.xyz, abs(r8.xyzx), CB0.LightConfig3.xyzx
  50: and r8.xyz, r8.xyzx, l(1.0000, 1.0000, 1.0000, 0.0000)
  51: dp3 r3.w, r8.xyzx, l(1.0000, 1.0000, 1.0000, 0.0000)
  52: min r3.w, r3.w, l(1.0000)
  53: mad r8.xyz, -v5.yzxy, r3.wwww, v5.yzxy
  54: sample_indexable(texture3d)(float,float,float,float) r9.xyzw, r8.xyzx, LightMapTexture.xyzw, LightMapSampler
  55: sample_indexable(texture3d)(float,float,float,float) r6.zw, r8.xyzx, LightGridSkylightTexture.zwxy, LightGridSkylightSampler
  56: mad r8.xyzw, r3.wwww, -r9.xyzw, r9.xyzw
  57: add r9.xy, -r6.zwzz, l(1.0000, 1.0000, 0.0000, 0.0000)
  58: mad r6.zw, r3.wwww, r9.xxxy, r6.zzzw
  59: mul r3.w, r8.w, l(120.0000)
  60: mul r8.xyz, r3.wwww, r8.xyzx
  61: add r9.xyz, r7.xyzx, -CB0.CascadeSphere0.xyzx
  62: dp3 r3.w, r9.xyzx, r9.xyzx
  63: add r9.xyz, r7.xyzx, -CB0.CascadeSphere1.xyzx
  64: dp3 r4.w, r9.xyzx, r9.xyzx
  65: add r9.xyz, r7.xyzx, -CB0.CascadeSphere2.xyzx
  66: dp3 r5.w, r9.xyzx, r9.xyzx
  67: lt r3.w, r3.w, CB0.CascadeSphere0.w
  68: lt r4.w, r4.w, CB0.CascadeSphere1.w
  69: lt r5.w, r5.w, CB0.CascadeSphere2.w
  70: add r9.xyz, r7.xyzx, -CB0.CameraPosition[0].xyzx
  71: dp3 r8.w, r9.xyzx, r9.xyzx
  72: sqrt r8.w, r8.w
  73: mul r9.x, CB0.hybridLerpSlope.x, CB0.hybridLerpDist.x
  74: mad_sat r8.w, r8.w, CB0.hybridLerpSlope.x, -r9.x
  75: mul r9.x, r1.w, CB0.globalShadow.x
  76: lt r9.x, l(0), r9.x
  77: movc r5.w, r5.w, l(8), l(12)
  78: movc r4.w, r4.w, l(4), r5.w
  79: movc r3.w, r3.w, l(0), r4.w
  80: mov r7.w, l(1.0000)
  81: dp4 r10.x, cb8[r3.w + 0].xyzw, r7.xyzw
  82: dp4 r10.y, cb8[r3.w + 1].xyzw, r7.xyzw
  83: dp4 r3.w, cb8[r3.w + 2].xyzw, r7.xyzw
  84: sample_l(texture2d)(float,float,float,float) r7.xyzw, r10.xyxx, ShadowAtlasTexture.xyzw, ShadowAtlasSampler, l(0)
  85: mad r3.w, r3.w, l(2.0000), l(-1.0000)
  86: mul r4.w, r3.w, CB0.evsmPosExp.x
  87: mul r4.w, r4.w, l(1.4427)
  88: exp r10.x, r4.w
  89: mul r3.w, r3.w, -CB0.evsmNegExp.x
  90: mul r3.w, r3.w, l(1.4427)
  91: exp r3.w, r3.w
  92: mov r10.y, -r3.w
  93: mul r9.yz, cb0[51].zzwz, CB0.shadowBias.xxxx
  94: mul r9.yz, r10.xxyx, r9.yyzy
  95: mul r9.yz, r9.yyzy, r9.yyzy
  96: mad r7.yw, -r7.xxxz, r7.xxxz, r7.yyyw
  97: max r7.yw, r9.yyyz, r7.yyyw
  98: add r9.yz, -r7.xxzx, r10.xxyx
  99: mad r9.yz, r9.yyzy, r9.yyzy, r7.yywy
 100: div r7.yw, r7.yyyw, r9.yyyz
 101: add r7.yw, r7.yyyw, l(0.0000, -0.2000, 0.0000, -0.2000)
 102: mul_sat r7.yw, r7.yyyw, l(0.0000, 1.2500, 0.0000, 1.2500)
 103: ge r7.xz, r7.xxzx, r10.xxyx
 104: movc r7.xy, r7.xzxx, l(1.0000, 1.0000, 0.0000, 0.0000), r7.ywyy
 105: min r3.w, r7.y, r7.x
 106: add r4.w, -r3.w, r6.w
 107: mad r3.w, r8.w, r4.w, r3.w
 108: movc r3.w, r9.x, r3.w, r6.w
 109: mul r1.w, r1.w, CB0.SkyAmbient.w
 110: mul r1.w, r3.w, r1.w
 111: lt r3.w, l(0), r1.w
 112: mad r7.xyz, r3.xyzx, r2.wwww, -CB0.Lamp0Dir.xyzx
 113: dp3 r2.w, r7.xyzx, r7.xyzx
 114: rsq r2.w, r2.w
 115: mul r7.xyz, r2.wwww, r7.xyzx
 116: mov_sat r1.w, r1.w
 117: mul r2.w, r6.x, r6.x
 118: dp3 r4.w, r4.xyzx, r7.xyzx
 119: max r4.w, r4.w, l(0.0010)
 120: dp3 r5.w, -CB0.Lamp0Dir.xyzx, r7.xyzx
 121: add r6.w, -r5.w, l(1.0000)
 122: mul r7.x, r6.w, r6.w
 123: mul r7.x, r7.x, r7.x
 124: mul r7.y, r6.w, r7.x
 125: mad r6.w, -r7.x, r6.w, l(1.0000)
 126: mad r7.xyz, r0.xyzx, r6.wwww, r7.yyyy
 127: mul r2.w, r2.w, r2.w
 128: mad r6.w, r2.w, r2.w, r2.w
 129: mad r2.w, r4.w, r2.w, -r4.w
 130: mad r2.w, r2.w, r4.w, l(1.0000)
 131: mul r2.w, r2.w, r2.w
 132: mad r5.w, r5.w, l(3.0000), l(0.5000)
 133: mul r2.w, r2.w, r5.w
 134: mad r4.w, r4.w, l(0.7500), l(0.2500)
 135: mul r2.w, r2.w, r4.w
 136: max r2.w, r2.w, l(0.0001)
 137: div r2.w, r6.w, r2.w
 138: mul r2.w, r1.w, r2.w
 139: mul r9.xyz, r7.xyzx, r2.wwww
 140: mul r9.xyz, r9.xyzx, CB0.Lamp0Color.xyzx
 141: add r1.x, -r1.x, l(1.0000)
 142: mul r2.w, r1.x, r1.z
 143: mad r7.xyz, -r7.xyzx, r2.wwww, r1.xxxx
 144: mul r7.xyz, r7.xyzx, CB0.Lamp0Color.xyzx
 145: mad r7.xyz, r7.xyzx, r1.wwww, r8.xyzx
 146: mul r1.w, CB0.SkyAmbient.w, CB0.SkyAmbient.w
 147: mul r9.xyz, r9.xyzx, r1.wwww
 148: movc r7.xyz, r3.wwww, r7.xyzx, r8.xyzx
 149: and r8.xyz, r3.wwww, r9.xyzx
 150: mul r0.w, r0.w, v1.w
 151: mul r1.w, r6.x, l(5.0000)
 152: mul_sat r3.w, r5.y, l(1.5882)
 153: add r9.xyz, CB0.SkyGradientTop_EnvDiffuse.xyzx, -CB0.SkyGradientBottom_EnvSpec.xyzx
 154: mad r9.xyz, r3.wwww, r9.xyzx, CB0.SkyGradientBottom_EnvSpec.xyzx
 155: sample_l(texturecube)(float,float,float,float) r10.xyz, r5.xyzx, PrefilteredEnvTexture.xyzw, PrefilteredEnvSampler, r1.w
 156: sample_l(texturecube)(float,float,float,float) r11.xyz, r5.xyzx, PrefilteredEnvIndoorTexture.xyzw, PrefilteredEnvIndoorSampler, r1.w
 157: eq r3.w, CB0.AmbientColorNoIBL_CubeBlend.w, l(0)
 158: if_z r3.w
 159:   sample_l(texturecube)(float,float,float,float) r5.xyz, r5.xyzx, PrefilteredEnvBlendTargetTexture.xyzw, PrefilteredEnvBlendTargetSampler, r1.w
 160:   add r5.xyz, -r11.xyzx, r5.xyzx
 161:   mad r11.xyz, CB0.AmbientColorNoIBL_CubeBlend.wwww, r5.xyzx, r11.xyzx
 162: endif
 163: sample_indexable(texture2d)(float,float,float,float) r5.xy, r6.xyxx, PrecomputedBRDFTexture.xyzw, PrecomputedBRDFSampler
 // r0.xyz = r0.xyz * r5.x + r5.y;
 164: mad r0.xyz, r0.xyzx, r5.xxxx, r5.yyyy
 // r1.w = r5.y + r5.x;
 165: add r1.w, r5.y, r5.x
 // r0.xyz = r0.xyz / r1.w;
 // -> r0.xyz = r0.xyz / (r5.x + r5.y);
 166: div r0.xyz, r0.xyzx, r1.wwww
 167: mad r5.xyz, -r0.xyzx, r2.wwww, r1.xxxx
 168: mul r6.xyw, r4.xyxz, r4.xyxz
 169: lt r12.xyz, r4.xyzx, l(0, 0, 0, 0)
 170: and r6.xyw, r6.xyxw, r12.xyxz
 171: mad r4.xyz, r4.xyzx, r4.xyzx, -r6.xywx
 172: mul r12.xyz, r4.yyyy, CB0.AmbientCube[8].xyzx
 173: mad r12.xyz, r4.xxxx, CB0.AmbientCube[6].xyzx, r12.xyzx
 174: mad r12.xyz, r4.zzzz, CB0.AmbientCube[10].xyzx, r12.xyzx
 175: mad r12.xyz, r6.xxxx, CB0.AmbientCube[7].xyzx, r12.xyzx
 176: mad r12.xyz, r6.yyyy, CB0.AmbientCube[9].xyzx, r12.xyzx
 177: mad r12.xyz, r6.wwww, CB0.AmbientCube[11].xyzx, r12.xyzx
 178: mul r13.xyz, r4.yyyy, CB0.AmbientCube[2].xyzx
 179: mad r4.xyw, r4.xxxx, CB0.AmbientCube[0].xyxz, r13.xyxz
 180: mad r4.xyz, r4.zzzz, CB0.AmbientCube[4].xyzx, r4.xywx
 181: mad r4.xyz, r6.xxxx, CB0.AmbientCube[1].xyzx, r4.xyzx
 182: mad r4.xyz, r6.yyyy, CB0.AmbientCube[3].xyzx, r4.xyzx
 183: mad r4.xyz, r6.wwww, CB0.AmbientCube[5].xyzx, r4.xyzx
 184: mad r4.xyz, r4.xyzx, r6.zzzz, r12.xyzx
 // r6.xyx = r10.xyx * r9.xyx - r11.xyx;
 185: mad r6.xyw, r10.xyxz, r9.xyxz, -r11.xyxz
 // r6.xyx = r6.z * r6.xyx + r11.xyx;
 186: mad r6.xyw, r6.zzzz, r6.xyxw, r11.xyxz
 // r0.xyz = r0.xyz * r6.xyx;
 187: mul r0.xyz, r0.xyzx, r6.xywx
 // r0.xyz = r1.z * r0.xyz;
 188: mul r0.xyz, r1.zzzz, r0.xyzx
 189: mad r1.xzw, r5.xxyz, r4.xxyz, r7.xxyz
 // r0.xyz = r8.xyz * r0.w + r0.xyz;
 190: mad r0.xyz, r8.xyzx, r0.wwww, r0.xyzx
 191: add r0.w, -CB0.SkyAmbient.w, l(2.0000)
 192: mul r0.w, r0.w, r6.z
 // r4.xyz = B0.SkyAmbientNoIBL.xyz * r0.w + CB0.AmbientColorNoIBL_CubeBlend.xyz;
 // AmbientColor No IBL?
 193: mad r4.xyz, CB0.SkyAmbientNoIBL.xyzx, r0.wwww, CB0.AmbientColorNoIBL_CubeBlend.xyzx
 // r1.xzw = r1.x + r4.xxy;
 194: add r1.xzw, r1.xxx, r4.xxy
 // r1.xyz = r1.y + r1.xxw;
 195: add r1.xyz, r1.yyyy, r1.xxwx
 // r2 is color from texture
 // r0.xyz = r1.xyz * r2.xyz + r0.xyz;
 196: mad r0.xyz, r1.xyzx, r2.xyzx, r0.xyzx
 197: mad r0.w, CB0.FogParams.z, v6.w, CB0.FogParams.x
 198: exp r0.w, r0.w
 199: add_sat r0.w, r0.w, -CB0.FogParams.w
 200: mov r1.xyz, -r3.xyzx
 201: max r1.w, r0.w, CB0.FogParams.y
 202: mul r1.w, r1.w, l(5.0000)
 203: sample_l(texturecube)(float,float,float,float) r1.xyz, r1.xyzx, PrefilteredEnvTexture.xyzw, PrefilteredEnvSampler, r1.w
 204: ne r1.w, l(0, 0, 0, 0), CB0.FogParams.w
 205: movc r1.xyz, r1.wwww, CB0.FogColor_GlobalForceFieldTime.xyzx, r1.xyzx
 206: add r0.xyz, r0.xyzx, -r1.xyzx
 207: mad r0.xyz, r0.wwww, r0.xyzx, r1.xyzx
 208: mul_sat r0.xyz, r0.xyzx, CB0.Exposure_DoFDistance.yyyy
 209: sqrt o0.xyz, r0.xyzx
 210: mov o0.w, l(1.0000)
 211: ret

经分析,这里的做法跟文中借鉴的Siggraph 2011做法存在较大出入,这里的做法是:
对于VS而言:

  1. 每个顶点采样贴图的UV会跟世界坐标挂钩(即UV坐标是基于世界坐标经由某种hash公式计算得来)
  2. 每个顶点的UV坐标还需要基于顶点属性做一个旋转与平移,这样方能实现相同贴图下的不同混合结果,从而得到没有tiling的结果
  3. 需要基于法线进行某种运算,避免地形高度差过大导致的异常

对于PS而言:

  1. 在PS中分别使用不同的UV坐标对三张贴图(隶属于三个不同顶点)进行采样,得到V0,V1,V2
  2. 采样得到的结果是带alpha的,但是这里的alpha存储的应该是这个贴图对应的weight,好处是不需要运行时计算,不好的地方是我们就不知道这里的weight是怎么计算的
  3. 基于美术同学提供的参数,对采样结果进行调制,这一步之所以存在就是为了支持ConstVal对颜色的调制,否则可以直接在离线算好:
// Color.w -> Weight Calculated by Color?
Color.xyz = Color.xyz * (Color.w * (ConstVal.xyz -1) + 1);
  1. 将各个贴图采样结果按照顶点属性进行混合,这里的顶点属性推测存储的就是顶点的权重了(这个权重硬件光栅化得到的,在三角面的三个顶点上,应该分别对应于XYZ中的某个分量为1,其他为0)。
TexColor = V1 * VertWeight.x + V2 * VertWeight.y + V3 * VertWeight.z;

看起来效果还挺好的,有机会可以实践一下看看。

出于性能考虑,这里对材质采样算法做了LOD处理,低端设备或者较远距离的地形上,采用低级shader,只取权重最高的材质,效果上会有些损失,存在接缝问题。

这里介绍了多材质之间的混合算法,传统的基于线性的(加权)混合方法,会导致材质之间的对比度存在损失,另外混合效果也不够自然。

这里用了两种策略来优化:

  1. 基于高度的混合算法,可以适当的减少混合效果不自然的问题(因为材质跟地形网格做了关联?)
  2. 基于histogram-preserving的混合算法,可以减少对比度的损失。

下面两图中展示了对应的优化效果,后者看起来确实要自然一点。

这里来看下顶点属性的layout,每个顶点占用了20个字节,位置数据用int16表示(整数),还需要一个id指示当前顶点的索引(1~3,看起来用不了16位?),法线精度比坐标要低,用了uint8,此外,需要三个uint8用于存储材质信息(layer index,是指有三层,每一层指定一个材质索引吗?),还需要三个uint8存储normal segment,这个暂不清楚干啥用的,从名字上推测,大概率跟材质相关,三个uint8存储随机种子。

为了降低drawcall,材质是基于同一个母材质的材质实例,贴图被放置在texture array中,并保证每个chunk(不清楚这个对应什么单位)只需要一个drawcall触发,这样一来,在chunk中修改地形材质,并不会影响到性能。

在drawcall提交上,这里也做了约束,为了降低提交的消耗,这里只需要在每个drawcall之前修改uniform buffer(参数)与VB/IB,看来各个chunk使用的材质是完全共用的。

为了降低渲染压力,这里肯定是需要一个LOD策略的,整体地形(体素)是基于八叉树来绘制的,走近了就基于八叉树细分节点,走远了就合并,叶子节点包含了16^3个体素,大约有500~1000个面片。

前面将体素面片化的算法是在具有相同LOD的体素上执行的,对于不同LOD来说,面片衔接上可能会有问题,一种可行的做法是在存在衔接问题的地方添加新的面片来处理,不过这种做法相对复杂且低效。

这里采取的做法是添加额外的悬空面片,只需要一个额外的面片就足够了(这点估计存在理解偏差)。

具体做法是,额外添加了一层体素,使用这个体素来生成较好的缝合面片,为了消除深度竞争导致的问题,这里对缝合面片添加了朝着远处的深度偏移,从而使得缝合面片只有在衔接存在缺口的时候才会出现。

最终测试下来发现这个方案简单且有效。

基于上述方案,每个chunk最多需要增加对三个方向的缝合面片的绘制,不过由于LOD不考虑Y(高度)方向,因此Y方向上的缝合面片可以省略。

这里对IB的布局做了设计,按照[X][base][Z][Y]的方式排布索引数据,从而可以基于同一套数据,利用渲染API可以自行设定IB的起始位置与长度的能力,使用一个drawcall达成不同的渲染效果。

前面所说的对缝合面片的深度偏移可以直接放在顶点shader中完成。

由于水体是半透的,因此在处理上有一些特殊事项需要考虑。

roblox在同一个meshing pass中完成了多种不同物理态的数据的面片化,包括土地跟空气、土地跟水以及水跟空气等衔接的处理。

水体在材质上需要做一些约束,在形变上要注意不能存在凸起,否则就不物理了。

最后,水体的渲染是单独的(半透,材质也不同),跟陆地chunk分开的。

水体的渲染,是基于一张贴图经过tiling + scrolling来实现动画效果,另外此处也应用了前面说的detiling方案以降低不自然感,波浪的效果是通过对几何数据的调整来实现的,还添加了水底的迷雾效果,此外,在PC上还添加了反射跟折射以得到更真实的渲染表现,在后续还会考虑进一步提升海岸线的渲染表现。

目前的植被是基于clutter(贴片)实现的,后续可能会考虑提供更高质量的几何形状的植被。

在方案选型的时候,对card-based方案以及geometric grass方案做过调研,得到的结论是geometric grass在tiler上具有更好的性能。

  • card-based方案是什么?根据名字推测,可能是传统的用几个方向的剪影交叉重叠在一起实现的草方案:
  • geometric grass是什么?大致推测,可能就是roblox在下面图中展示的,基于一个个的贴片实现对草丛模拟的方案。
  • tiler是指什么?

geometry-based grass方案每个叶片只用3~5个顶点,草的位置则只需要基于一个稳定的随机算法来生成,在渲染上做了一些创新:

  • wrap diffuse
  • 基于半透(透明程度)的处理
  • 为diffuse/specular提供了基于高度的gradient

参考

[1]. large voxel landscapes on mobile - slides
[2]. large voxel landscapes on mobile - video

你可能感兴趣的:(【Siggraph 2020】 large voxel landscapes on mobile)