【写在最前】由于上过GAMES101和202,第五课就没写blog。但有一部分(后半部分)还是值得回看课件
1、地形
几何
原则
游戏中大面积的地形不可能全部精密计算加载,不可能一直都以极高频率去采样地形高度,所以需要进行采样的优化处理,其中主要的两个原则是:
- 到相机的距离和FOV
即离相机过远的地形和FOV以外的地形不参与或粗略进行计算。(例如FPS中倍镜的实现即是通过缩小FOV来实现细节的放大以及视野范围的缩小) - Error Bound
与GT(预计算结果)相比,误差应该控制在一定阈值内,随着离相机远近而放大放小这个阈值,视觉上来看采样导致的高度误差应不超过一个像素
切分
- 最基础的切分方法就对等腰三角形的斜边进行切分。(二叉树)
- 如果某一条边的两侧采样密度差很多,可能会导致出现高低差裂缝(稀疏那侧在这条边上没有采样点,密集那侧有,则很可能密集采样的那个点的高度和那条线不是同一高度),这时候就必须对稀疏的一侧也进行进一步采样直到缝隙消失。这个行为就是T-Junctions
四叉树(Quad)
最常用的方法仍是四叉树。
优点:
- 易构建
- 因为和其他几何材质数据形式更贴合,所以易于管理各类几何空间数据,包括物体的剔除和数据流处理。(纹理等)
缺点:
- mesh的切分对比三角mesh相对不灵活
- 叶子结点的方格必须是连续的
四叉树也会遇到T-Junctions问题。解决方案是吸附:让采样密度高的一侧多出来的采样点调整高度,吸附到采样密度低的那条边上
TIN(Triangulated Irregular Network)
不规则的三角形。
优点:
- 便于实时渲染
- 在特定地形下需要的三角形更少(比如大面积平原、沙漠)
缺点:
- 需要特定预处理
- 重复使用性低
GPU Drived Tessellator(Hardware Tessellation)
这个步骤处于顶点着色和几何着色之间,分为
- 外壳着色器(Hull Shader)
- 镶嵌器(Tessellator)
- 域着色器(Domain Shader)
具体来说:
hull shader的输入是特殊的patch片元,它是由几个控制点定义的细分曲面,贝塞尔片元或其它曲线元素。hull shader有两个功能,首先配置tessellator及告诉tessellator生成多少个三角形。然后,它在控制点上执行处理(transform等)。hull shader能够修改输入的patch, 根据需要添加或删除控制点。最后,hull shader输出控制点集及tessellator数据到domain shader。
tessellator在管线中是固定功能管线,它的功能是添加新顶点并传送到domain shader。hull shader 发送细分表面类型(如三角形,四边形或等值线)到tessellator。等值线用于头发渲染。hull shader发送的其它重要的数据是细分因子(OpenGL中为细分 levels),有两种类型:内(inner )和外边(outer edge), 内因子决定三角形或四边形内部进行多少次细分,外边因子决定外边切分为多少次。
domain以重心坐标为中心为每个点生成坐标,并在patch的评估方程中使用这些坐标来生成位置(position),法线(normal),纹理坐标(texture coordinates)和其他所需的顶点信息。
引用于https://zhuanlan.zhihu.com/p/391322740
后续又发展出Mesh Shader(DX12以上 win10+),用来替代这个三个步骤。
为什么这种基于GPU的方法很重要呢?主要是其可以实时调整所有顶点的位置和数量。因而实现实时可变地形。这也是许多比如黑神话悟空预告片中雪地场景实现的重要技术。
Non-HeightField
上述所讲都是基于高度的地形,还有其他类别,比如单纯基于模型。这时候也有很多trick,比如在山体模型上挖个洞做隧道(通过取一个隧道中心点,将其位置设为none,从而几何会退化,去掉所有包含这个点的三角形,最终形成一个洞,后续再利用隧洞模型掩盖锯齿状的边缘)
Marching Cube
通过每一个体素(小立方体)的各个顶点在面的上下位置来判断切面是如何与这个体素相交。
材质
真实世界中材质数量很多,所以需要处理大量的材质混合情况
两种材质混合(texture splatting)。
传统情况下渲染地表,使用的都是texture splatting技术,也就是说,地表纹理使用多种不同的纹理混合而来。然后使用一张叫做splat map的权重图来记录这些纹理的权重。
考虑最简单的两种材质的混合问题:
- 如果只是简单的加权两种材质的rgb的话,会导致平滑但不自然。
- 一个有效的方法是利用地形的高度,在两个材质相交的每个点,谁高显示谁的rgb
- 简单融合的话信息是纯离散的,所以在相机快速移动或在远处低角度看的时候就会有闪烁或者很sharp。因而又引入一个高度bias来优化,当两种材质高度差大于bias时直接用高的材质,低于bias时按照高度差融合,如下图:
Texture Array
现实世界远不止两种材质,所以在多材质相叠的时候,就引入了texture array。简单来讲就是将每个材质用一个index表示,在实际处理时根据用到的index和其对应的权重来进行混合计算。
【注意】texture array的处理方式不同于3D texture,后者利用八个顶点上下两个面进行七次插值计算出一个最终颜色信息。
视差贴图
当前处理凹凸效果最基础的方法是Bump mapping。
升级版就是Parallax mapping,其特点在于假设地表下的材质和有高度凹凸的,所以当相机看到某个点时,实际上由于上面有凹凸会导致有一个offset,比如实际上看到的点应该高于那个平面上的点,从而让视角效果更好。其缺点一是成本较高,需要走几步更新一下,缺点二是仍未解决只是视觉效果而未改变几何的问题。
更好的解决方法是Displacement mapping。根据前文所述的实时可变地形,切实的改变地形高低。
Virtual Texture
核心思想是只将需要用的部分放入内存,其他都留在硬盘中。
这里简单阐述下方案:绘制两边,第一遍把需要用的的贴图的tile以及自身的UV等等信息输出到rendertarget上,CPU根据RT的数据将资源送到GPU,第二遍绘制才是真正的渲染
这个概念取自于Virtual Memory,与虚拟内存类似的是,一个很大的Texture将不会全部加载到内存中,而是根据实际需求,将需要的部分加载。与虚拟内存不同的是,它不会阻塞执行,可以使用更高的mipmap来暂时显示,它对基于block的压缩贴图有很好的支持。 基本思路是,会将纹理的mipmap chain分割为相同大小的tile或page,这里的纹理叫虚纹理,然后通过某种映射,映射到一张内存中存在的纹理,这里的纹理是物理纹理,在游戏视野发生变化的时候,一部分物理纹理会被替换出去,一部分物理纹理会被加载。
可以参考https://zhuanlan.zhihu.com/p/138484024和https://www.zhihu.com/question/453803452
VT的另一个好处是可以在加载新的tile时预先bake好材质的融合再放入物理材质中,这样后续调用就更快了。
现在VT基本上一统天下了。
与其相关的两个硬件知识:
- DirectStorage 让GPU来完成各种分析解压等任务,而CPU只需要传递被压缩的数据就行,从而提高计算速度
- DMA(索尼)直接把数据从硬盘传到GPU
其它问题
浮点精度问题
因为浮点数精度是有限的,所以当表示大距离的时候就会出现问题,比如当相机和某个点离世界坐标原点越来越远(看月球)时就会出现其精度不够,比如最小精度可能都大于1了,然后就会出现“抖动”,因为物体的坐标精度不够了,已经不准了
解决方案为Camera-Relative Rendering。在任何其他几何变换影响物体之前,通过重设世界空间相机位置来转换物体。其实就是把camera设为世界坐标原点,这样就可以用相对坐标来代替员阿里的绝对坐标了(当然还需要更新所有其他矩阵)
植被道路贴花等
比较琐碎和笼统,没啥好记的。
2、大气
首先和光线追踪类似,大气渲染也有类似的渲染公式,在实际处理中也有类似 Blinn-Phong的拟合模型。关键参数是当前点到天顶的角度和到太阳的角度
但这种模型换了情况就不适用了马上,比如天气变了,日夜变化,甚至多了PM2.5。
大气散射理论
光和介质的接触:
- Absorption 吸收
- Out-scattering 散射
- Emission 自发光
- In-scattering 其他介质的光散射过来
四种方式累积起来形成RTE公式
体渲染公式:
其实就是RTE公式的导数
大气中的真实物理
大气中真实存在两种介质:气体分子和气溶胶分子,前者往往小于太阳光中各种光的波长,而后者一般接近这些波长。
因此散射也分为了两种:
散射
Rayleigh Scattering(瑞利散射)
当空气中介质尺寸远小于波长的时候(气体分子):
- 空气中的光是四面八方均匀散射,不太具有方向性
- 对于越短的波长(蓝紫光),它散射的越远,对越长的波长(红光),散射的越近
详细来说,其散射公式如下:
其中θ是光散射出去的方向和入射角之间的角度,n是空气折射率。而密度比。
ρ
\rho
ρ 这个数字在海平面上等于1,随
ρ
(
z
)
=
exp
−
h
/
H
\rho(z) = \exp{-h/H}\,
ρ(z)=exp−h/H 呈指数递减
几何部分决定了不同方向光的散射情况不同(但整体差别不太大,而且旋转不变);波长部分表明波长越短,散射越强;密度部分表明了海拔对散射的影响。
这也解释了天空在白天是蓝色在傍晚是偏红的原因:
Mie scattering(米氏散射)
当空气中介质尺寸接近或大于波长时(气溶胶):
- 有一定方向性
- 对波长不敏感
详细来说,其散射公式如下:
对比瑞利散射,多了几何参数g的影响。
其实际生活影响就是雾和太阳光环:
吸收
空气中主要是臭氧(
O
3
O_3
O3)和甲烷(
C
H
4
CH_4
CH4)能够吸收波长的光,比如臭氧吸收红橙黄,甲烷吸收红光。比如海王星上大量甲烷导致其显现蓝色。
实际计算过程中往往假设这些气体是均匀分布在整个大气中的(虽然实际上并不是,比如臭氧集中在大气上层)
实时渲染大气
预计算LUT
实际计算时,Ray Marching是最常用的方法。其原理主要是依照路径逐步计算累积,如下图:
其中T就是所谓通透度(密度)
和普通物体的实时渲染类似,预计算也在该领域得到运用,LUT就是一个将许多复杂计算进行预计算保存下来的表格。在通透度方面,一个公式得以提出:
我们注意到,两个点之间的通透度关系借此只由两个参数决定(
h
h
h 和
θ
\theta
θ )由此,我们可以得到一个关于传播通透度的LUT表。
提出这个公式之后,原来的单次散射公式就可以表示为:
这里最重要的是一种参数化思想(和上面一样),利用三个参数
η
\eta
η(太阳顶角)、
θ
\theta
θ (观察顶角)和
ϕ
\phi
ϕ (太阳到观察视角之间水平角度)再加上当前高度
h
h
h 表示站在所有点在所有太阳角度下看任何方向的情况。(这三个角都是方位角并进一步通过cos计算得到相应结果(
μ
s
\mu_s
μs 、
μ
\mu
μ 以及
v
v
v)。我们将这个4D表示存到多个3Dtexture array(其实3D texture的插值更合理)里面方便查询。
因此,我们获得了通透度LUT表和单次散射LUT表,利用这两张表,就可以计算出二次、三次以及更多次散射的LUT表
缺点:
- 预计算成本很高。首先是多次散射的迭代计算成本高,其次是在移动端没法用。(即使在PC端可能也有很多毫秒甚至一秒,但可能分到很多帧去完成)
- 没法处理动态环境调整。比如从晴天变成下雨的雾天,此时需要均匀过度,每一帧都要插值计算,所以没法处理。包括艺术家调节参数也不方便
- 虽然获得预计算表,但单单是去逐像素进行高维的插值查询也是成本很高的(所以经常需要为了效率去下采样)
简化版
因为上述缺点,一种更简化的多次散射方案被提出,其特点在于认为任何散射都是各向同性的,向所有方向均匀散射,所以对一个介质来说,其所有邻居介质收到来自它的散射能量都是相同的,因而一次散射带来的结果只是整体削减(吸收)了固定百分比。
在这种设定下,多次散射也就单纯变成了一次散射的指数变化(等比数列),而一个介质收到的不同次数的散射总量就是其求和(等比数列求和)。
这样一来,由于极大加快了速度,所以这种方法可以每一帧都进行计算。从而导致原方法LUT参数中的高度
h
h
h 以及太阳顶角
η
\eta
η 都不需要参与预计算(因为都实时更新了),所以新方法的LUT表中只需要计算出观察的天顶角
θ
\theta
θ 和一个水平方向环绕360度的夹角
ϕ
\phi
ϕ (对应原来太阳到观察视角水平方向的夹角)即可。化四维为二维。
再加入相机距离可以进一步获得3D的LUT表来实现ray marching
云的渲染
Volumetric Cloud Modeling
云渲染从最早学术界的Mesh方法到早起游戏的单纯叠贴图,逐渐发展到了用于3A行业的Volumetric Cloud Modeling。其本身有各种优点:
- 真实的云的形状
- 可以实现大尺寸的云
- 支持动态天气
- 支持动态的体积光照和阴影
同时也具有一个明显的缺点:
- 效率低下,成本昂贵
其具体依赖于一个称为Weather Texture的东西,而它由两个部分组成:
- 天空中云的随机分布
- 云的厚度(从0到1表示)
利用这两个部分就可以实现比如云的移动(对1进行平移)以及厚度变化等
但是单单这样做会导致云的形状变成柱状,所以引入了Noise,比如Perlin Noise(棉絮状噪声)和Worley Noise(细胞结构状)等,先利用低频把云的规则边缘模糊化,再加上一些高频来优化细节。
实际如何进行渲染呢?Ray Marching。具体来说分四步:
- 对每个屏幕像素做射线
- 在该射线未接触到任何云之前大步前进
- 碰到云之后密集计算
- 利用前述方法计算,但进行简化(因为云比较密,所以可以做这个假设)