目录
- 前言
- 层次细节
- PBR渲染方程
- IBL环境光照
- 集群延迟光照
- 实时阴影
- 环境光遮蔽
- 体积特效
- 屏幕空间反射
- 视差
- 植物渲染
- 反向动力学骨骼
- 后记
前言
在摸了一个寒假并且经历了【早上睡大觉,中午打原神,晚上英雄联盟,深夜看嘉然电棍炫狗大司马】的魔怔生活之后,终于决定来给自己的博客除下草,遂更新。
玩了一个月的原神,我注意到了一些图形与渲染上的不错的细节,于是打算分享一蛤。本文将从 猜测 的角度出发,谈谈《原神》中主要图形特效的渲染技术与优化方法。
本篇博客是杂谈类文章,仅代表个人猜想与看法,不保证正确,毕竟自己确实太菜,思路对不上米社的也大佬很正常 怎么我一开篇就搁这摆烂了
不说批话了,开始!
层次细节
首先注意到《原神》是一个开放世界的游戏,这意味着场景非常大,那么绘制的三角形数目也非常多!如何解决巨量的 draw call 是一个首要的难题!
场景非常大,这意味着一次性绘制所有物体变得不可能。别说绘制,就是把这些三维模型全部放进内存,显存中都很困难,毕竟游戏本体高达 30+ GB,而 NV 的老黄再怎么疯狂也不会造出一块 32 G 显存的显卡(谁知道呢,也许过两年就有了
事实上,大多数的游戏都采样 LOD 技术来解决这个问题。LOD 技术全名为 Level Of Detail,即层次细节。LOD 的核心思想就是 “好钢用在刀刃上”,即 为近处的物体分配较高细节程度的模型,而远处的物体则分配较低细节程度的模型,以达到减少资源消耗的目的。事实上不止模型,模型对于的纹理等信息也会做相应的 LOD 处理。
下图演示了不同细节程度的模型,较高细节程度的模型具有更好的品质,但是三角面片数目增加了,渲染的开销也增加了:
在现实世界中也是如此。如下的照片中,远处的楼宇表面的瓷砖变得无法分辨,而近处的瓷砖则非常清晰:
这告诉我们 无需为远处的楼宇绘制瓷砖,这也侧面证明了 LOD 技术能在不失真实感的情况下节省绘制的开销。
来看《原神》中的 LOD 技术,在游戏中 LOD 分为三个层次,即远,中,近。三个层次的模型及其纹理具有显著的区别。注意拱门模型从远到近变化:
可以看到随着距离的逼近,拱门的三角形面片数目逐渐增多,并且纹理逐渐清晰。
除了模型的 LOD,纹理贴图也具有 LOD,通过访问低分辨率的纹理以降低开销,下面是游戏中纹理的 LOD 效果,注意从远到近地上的白色纹理逐渐清晰:
如果你的眼睛足够锐利,可以察觉到 2 条纹理等级分割线。在不同距离切换使用不同分辨率的纹理。虽然 gif 被压缩了并不明显
纹理的 LOD 技术可以由显卡驱动负责,根据指定的 LOD 等级,以 2 的次方的大小来生成逐步缩小的纹理,这也是为何远处的物体纹理都非常 “马赛克” 的原因:
一张正方形砖墙生成的 LOD 纹理大概如下,纹理查询算法会按照我们的要求,去选取一张最合适的分辨率:
此外,在多光源光照的处理上,也使用了 LOD 技术。因为光照计算非常宝贵,没有必要为远处的非常小的场景计算宝贵的光照。此外,图形程序员还为 LOD 光源在最大距离处添加了一个线性的淡入淡出的效果,防止画面突然闪烁。注意远处的灯逐渐亮起:
层次细节技术能够给予场景更大的体量,同时保持低消耗。于是美工需要准备好几套不同层次细节程度的模型,在绘制之前,我们根据摄像机到模型的距离,来判断应该选取何种程度的模型进行绘制即可。
PBR渲染方程
PBR 又名 physically based render,即基于物理的渲染。是迪士尼提出的一种能够在实时渲染上面逼近真实图像的渲染模型。PBR 不仅能够以较小的代价获取很高质量的材质渲染感,而且一定程度上规范了模型素材的材质格式。下图是典型的 PBR 渲染效果图:
注:
图片引自博客 《PBR 渲染之你上你也行!》
好吧这只是个人翻译,原文叫:physically based rendering and you can too
在现代计算机游戏上,PBR 已经成为了一种标配的渲染方式,或者说一种 “流程”,在原神中也不例外的使用了 PBR 作为渲染,并且拥有一套自己的风格:
PBR 包括一套非常复杂的公式和其背后的物理原理,比如漫反射,镜面反射,菲涅尔效应等。下面细说。
在传统的 phong 光照模型中,简单通过光源和法向量的夹角,来获取一个点的颜色。对于所有的物体都有同样的公式,这意味着无法区别不同的材质。事实上传统的 phong 光照模型渲染出来的东西像塑料一样:
而 PBR 渲染则通过指定一系列的参数,来从物理上逼近不同的材质,从金属到木头,都可以通过美术设计者指定的参数与纹理进行规格化的渲染。PBR 的规范化流程表明,在美工制作完成 PBR 的素材之后,在游戏中也会有相同视觉效果。下图展示了 PBR 强大的模拟能力:
注:
图片引自 迪士尼的一篇 talk,是讲 BRDF 的,十分深奥
这种级别的 talk 不是我这纯种啥卵能够玩明白的,于是只偷了张图过来凑合一下。。。
来看一组《原神》中的截图:通过 PBR 渲染方程,可以模拟不同的材质表面。从左到右从上到下分别是石头,砖块,木头,瓦片,漆木,大理石。这足以秀一下 PBR 的肌肉:
在 PBR 中,通过一些规范化的纹理贴图,来描述不同的材质的参数,进而根据 PBR 的公式得出不同的结果,这些贴图包括:
- 法向贴图
- 金属度贴图
- 反照率贴图
- 粗糙度贴图
- 环境立方体贴图
- 环境光遮蔽贴图
- 其他…
这些贴图提供的信息用以辅助 PBR 公式并且得出正确的结果。PBR 的难点就在于公式非常繁杂,下面给出 PBR 的基本公式:
L = ∫ ( k d C o π + k s D G F 4 ( N ⋅ ω 0 ) ( N ⋅ ω i ) ) ( N ⋅ ω i ) C l d ω i L=\int \left( k_d \frac{C_o}{\pi}+ k_s \frac{D \ G \ F}{4(N \cdot \omega_0)(N \cdot \omega_i)} \right) (N \cdot \omega_i) \ C_l \ d\omega_i L=∫(kdπCo+ks4(N⋅ω0)(N⋅ωi)D G F)(N⋅ωi) Cl dωi
和大多数渲染方程类似,PBR 默认使用路径追踪式的定义,即 ω 0 \omega_0 ω0 为入射光线(射入人眼), ω i \omega_i ωi 为出射光线(射向光源),或者说来自光源的光线的反方向:
此外, N N N 是法向量, k d k_d kd 和 k s k_s ks 分别表示了漫反射和镜面反射的占比系数, C o C_o Co 是材质贴图原始颜色, C l C_l Cl 则是灯光颜色。再来看剩下的 DFG 三兄弟:
其中 D D D 表示微平面分布函数,也就是镜面反射的波峰函数。在 phong 光照模型中,通常用 ( cos θ ) α (\cos \theta) ^ \alpha (cosθ)α 来近似,而 PBR 使用多种复杂的近似函数比如 GGX 等,这里不细说了。
而 F F F 表示菲涅尔效应,通常使用 Schlick Fresnel 来近似,就是那个著名的 ( 1 − cos θ ) 5 (1-\cos\theta)^5 (1−cosθ)5
再来看 G G G,表示几何遮蔽。唔… 说实话我没搞懂这一项到底是离线烘培的模型环境光遮蔽,还是高光的次级波瓣近似,先挖个坑(逃
注意到下图中,大理石材质的反光效果就是基于 PBR 技术渲染的。通过一定的反射系数,可以反射来自光源的高光:
除了来自光源的高光,PBR 还允许镜面反射的光线源来自周围的环境立方图。比如下图的地面反射就采样于环境立方图。没有芭芭拉的倒影表明我们并未开启 SSR,这是单纯靠渲染方程的反射:
静态的环境立方体贴图在游戏开发的时候就离线渲染好了,我们计算相机到像素的光线的 反射光线 的方向,然后再根据这个方向,去环境立方图里面采样:
此外,如果你足够细心,就可以在游戏中发现这样子的立方体贴图分界线。开发者为了适应不同的环境,根据当前像素的不同位置,选取不同的立方图进行采样。好吧,其实分界挺明显的,晃动镜头就可以看出区别:
此外,留意地面上的因为瓷砖不平整产生的扭曲,这要归功于我们的法线贴图。
IBL环境光照
除了反射贴图,对于环境光(Ambient)也有对应的贴图。在普通的 phong 光照模型中,ambient 项是一个常数。而 PBR 允许我们利用一张环境立方图来描述场景的环境光,这项技术也叫做 IBL,Image Based Lighting,即基于图像的光照。
通常在离线的状态下预先烘培一张环境光贴图即可,并且环境光贴图通常是由镜面反射的立方图贴图通过模糊而得到的,比如:
注:
图片引自:Using Image Based Lighting (IBL)
塞拉斯的大招转的太快辣
和镜面反射的环境立方体贴图类似,只是我们把采样作为 Ambient 项进行计算。下图是一个典型的 IBL 的效果。在没有光源的情况下,左边的地面呈现红色,而右边的地面呈现原本的颜色:
再补一张图,可以清晰的看出来 IBL 的效果还是非常真实的,并且具有很高的正确性。左侧的阁楼灯火通明,所以提供的环境光应当偏红,而右侧的楼梯面朝大海,环境光偏蓝,如图:
米哈游在 2020 的 Unity 大会上的 talk 也提到了 Ambient 探针,球体的演示能够更加生动:
注:
原文地址:/read/cv8444599/
此外,该 talk 还提到了在室内外使用不同的环境光立方图贴图,并且需要美工人为地给模型的顶点标记上室内室外,同时在室内室外切换的时候做一个线性插值。
集群延迟光照
在游戏中拥有众多的光源,而多光源的渲染一直是拖慢实时渲染的主要因素。如图,游戏场景中拥有数十个光源:
正常的思路是遍历所有的光源并且计算光照,这意味着场景的光源增多一倍,渲染的代价也要翻倍。这是几乎不能接受的!
事实上,在 Unity 2020 的 talk 中,开发者也谈到了《原神》采用的是 Clustered deferred lighting 的策略去处理大量的灯光。
Clustered deferred lighting 即 集群延迟着色,是 “集群” 与 “延迟着色” 两种思路的结合。抛开集群不谈,我们先来看看 “延迟渲染” 这一伟大的思想:
在前向渲染(也就是比较远古的时候)大家都直接 draw call,然后运行片段着色器计算光照。这样做的效率非常低下,我们来算一笔账:
假设有 n n n 盏灯, 100 , 000 100,000 100,000 个三角形,平均每个三角形能够产生 300 300 300 个像素,那么最终需要运行
t 1 = n ∗ 100000 ∗ 300 = 30 , 000 , 000 t_1 = n * 100000 * 300 = 30,000,000 t1=n∗100000∗300=30,000,000
次片段着色器,这意味着我们执行了 t 1 t_1 t1 次光照算法。这显然是没有必要的,因为片元着色器运行在深度测试之前,这意味着我们 对被遮挡的像素也计算了光照 ,而这些计算显然是没有必要的!
于是提出了延迟渲染的概念,延迟渲染的核心思想就是在深度测试之后再计算光照,这样只对我们看得见的物体计算光照,确保好钢用在刀刃上。
回想光照的计算需要那些信息:片元法线,片元位置,片元材质,光源位置… 延迟渲染预先将这些信息存储在一些缓冲中,这些缓冲叫做 g-buffer。
下图是 CryEngine 在 2013 年的《孤岛危机3》(又名显卡危机3)中首次使用的延迟渲染 g-buffer,正是这些信息支持了光照的计算:
注:
这里没有世界坐标纹理,因为世界坐标可以通过其他途径获得:
这里世界坐标可以通过 ndc 坐标 + 投影矩阵的逆矩阵解算出来
而 ndc 坐标可以通过深度图(Depth,z axis)和像素坐标(x y axis)进行重建
相信大家都懂,我就不细????了(逃
在真正计算光照的时候,我们从 g-buffer 中取出信息,并且实际地执行光照计算。这里贴一张我以前的博客的图(忽略那个 shadowmap 吧 Orz):
屏幕有多少像素,后处理阶段的片段着色器就跑多少次。那么计算光照的复杂度就只和 屏幕分辨率 ,光源数目有关,我们只需计算:
t 2 = n ∗ 1920 ∗ 1080 = 2 , 073 , 600 t_2=n*1920*1080=2,073,600 t2=n∗1920∗1080=2,073,600
这么多次光照即可。因为深度测试帮我们剔除了很多看不见的像素,所以绘制的开销得以降低。
事实表明延迟渲染是非常棒的优化策略,可以降低一个数量级的开销。如图展示了拥有 1800 个光源的渲染场景,而这在前向渲染下几乎是不可能的:
在了解了延迟渲染之后,再来谈谈 “集群” 的思路。emmm 如果了的并不是很解,可以康康我之前的博客:
OpenGL学习(十一):延迟渲染管线
或者是参考 Learn OpenGL 的相关章节 以获取更加权威的资讯
使用延迟着色法渲染光源仍然有可以优化的空间,因为 每个像素仅受部分灯光的影响,无需对所有的灯光都计算光照。
首先从直觉上来讲,灯光只会影响一定范围内的像素。对于 距离过远 的像素则不受灯光的影响,这样更加符合常理,此外,在背面 被遮挡 的像素也不需要计算光照:
因为过远或者背光的像素不需要计算光照,于是每个像素都拥有一个独属于他的灯光集合。下图中,区域 1 受两盏灯的影响,而区域 2 只受一盏灯,他们有不同的灯光集合:
对于一个拥有 100 个光源的大场景,假设一个点平均受 10 个光源的影响,那么比起遍历所有的光源,仅遍历影响该点的光源,减少了 90 次的计算!
还有一个小问题:计算有效灯光集合的复杂度。如果对每一个像素,都遍历所有灯光并且筛选出有效的灯光,这和我们暴力计算没有复杂度区别,坏了。
事实上 互相邻近像素都受同一组灯光的影响,于是引入集群的思想。集群灯光的处理分为三个主要步骤:
- 先根据场景信息(比如位置,法线)将场景分为不同的 集群
- 然后对每个集群,计算影响该 集群 的灯光
- 最后计算光照时,先找到 像素 所在的 集群 ,然后遍历影响该集群的所有灯,同时计算光照
如图展示了不同集群寻找灯光的大概过程。首先找到该像素所在的集群,然后根据集群,索引影响该像素的灯光:
一种经典的划分集群的方式是根据空间位置进行划分。将相机的视锥体分为若干个子区域。可以按照 xy 方向先划分出 tile,再在深度方向上继续细分:
当然也有其他划分集群的算法,比如加入法线,深度信息等等,这里就不详细讨论了。但是思路大体是这样子。对于无效光源越频繁出现的场景,集群的思路能够让渲染的效率成倍提升!
实时阴影
聊完光照,再来看看计算机游戏中必不可少的特效之一:实时阴影。游戏中的实时阴影分为很多个部分:
- 太阳阴影(正交投影的 shadow mapping)
- 光源阴影
- 室内点光源(点光源的 omnidirectional shadow mapping)
- 云阴影
经典算法 shadow maaping 通过从光源方向渲染一张深度纹理,以获取离光源最近的物体的距离 closestDepth
。然后再判断目标片元到光源的距离 currentDepth
是否大于最近物体,如果是,那么目标片元处于阴影中,图解如下:
我之前的博客写过一次:OpenGL学习(九)阴影映射,这里直接偷了
因为 shadow mapping 的原理比较简单,同时开销较小,所以一般的游戏都采用这种方法绘制实时阴影,《原神》也不例外。
首先来看直接阴影。直接阴影是最常见的阴影,由太阳发出的平行光投射而成:
平行光的 shadow mapping 代码并不难写,但是要在一个宏大的场景下使用 shadow mapping,就需要渲染一张分辨率非常高的阴影贴图,否则渲染出来的阴影就会有很多锯齿,并且精度非常低:
这里偷了 Learn OpenGL 的图,我太懒了。。。
而《原神》的开放世界需要有很远的能见度,即使在最低的阴影质量下,在超远视距的情况下也可以渲染出阴影,并且优化还不错,如下图,这是一个非常夸张的距离:
而米忽悠也在上文提到的那个 Unity 2020 的 talk 中表明了优化方法。米忽悠使用 Cascaded shadow map,也就是级联阴影映射技术来对阴影进行优化。级联阴影映射和 LOD 技术异曲同工,即 近处的景物使用高分辨率的阴影贴图,远处则使用低分辨率 。将透视投影的视锥体分为几个片段,然后用不同分辨率来渲染阴影贴图:
注:
上图不完全正确
因为不同距离的阴影贴图能看到的景物不同,而不是像上图的右侧一样渲染出不同分辨率的相同物体
而在米哈游的那个 talk 中提到,相比于传统的 4 级划分,《原神》的阴影渲染采用了非常变态的 8 级阴影贴图的划分,以保证远景和近景具有足够的品质。
此外,采用延迟更新的 “shadow cache” 策略,前 4 级 cache 每一帧保证更新一次,而后 4 级 cache 则轮流更新(因为是远景区别不大),这样每一帧只需要更新 5 张贴图。
再来看软阴影。因为阴影贴图会产生锯齿,而且现实中的阴影一般不会非常棱角分明。相反,现实中的阴影会随着和投影物体的距离的增大,而逐渐模糊,如图是照片中的两个小片段:
而在游戏中使用了随机分布的点,对阴影贴图进行了多次采样,从而判断一个点在阴影中的程度,以达到模糊的效果,而非传统的高斯模糊。因为前者具有更好的品质。
talk 中提到使用的是泊松分布,并且进行了 11 次采样。此外,通过对随机序列进行旋转,进一步提升软阴影的品质,同时避免重复的图样(pattern)出现。
而米社有说使用了基于距离的软阴影,emmm 在实际游戏中我貌似没有明显地察觉到。下图理论上应该是腿部的阴影较为尖锐,而头部的阴影较为模糊才对,而我并没有看出来:
好吧。。。可能这个特效是 ps 版的特供?毕竟抛开软阴影这一套重量级的流程,再执行一次模糊处理确实有点捉襟见肘。
正是因为这一套组合拳开销太大,无需对每一个像素都做,我们 只需要对阴影边缘的像素做一次软阴影 即可。于是米忽悠搞了张 mask 的图以标记阴影的属性,是阴影,半影,还是不在阴影中。红色的区域表明是阴影的边缘,我们应该计算软阴影:
通过对原始阴影贴图的 “田” 字型的 4x4=16 个像素进行采样来判断当前点的阴影程度,然后输出到 mask 贴图中。因此这个 mask 的分辨率是原图的 1/4,此外,16 个像素的开销还是大,于是只在 16 里面取部分像素的结果输出到 mask 贴图,虽然不正确,但是经过模糊处理,仍然能够得到不错的结果。
具体怎么取?米忽悠没说,我也不知道,那你去找物管啊
再来看光源阴影。在游戏中的一些光源也拥有实时阴影,和平行光最大的区别就是阴影会随着人物的移动而发生形变。因为来自太阳的平行光,在程序上会人为 地将太阳光源和摄像机绑定着一起移动,而场景的局部光源则不会:
这种光源的实时阴影在场景中大量存在,不得不考虑性能问题。在实际渲染中是通过两种方式获得阴影贴图:
- 对于静态的物体,离线烘培一张阴影贴图
- 对于动态的物体(比如玩家)则实时从光源方向渲染阴影贴图
这么做的好处就是减少了不必要的 draw call,因为绘制阴影贴图也需要调用 draw call 的。
下面的图揭示了有趣的现象:楼梯和栏杆属于静态物体,使用预先绘制的阴影贴图。而玩家人物则属于动态物体,拥有实时阴影贴图。两位路人则没这好待遇,于是没有影子:
此外,对于太阳光造成的阴影,可以和人造光源的阴影同时存在。如下图:
关于动态阴影,原理也不是很复杂。根据片元的位置,选择能够影响它的光源,然后循环计算 shadow mapping 即可。值得注意的是,这一步可以使用上文提到的 集群光照计算的策略。
从截图可以看到,随着距离的靠近,光照和阴影被计算,并且是 同时 出现的。这说明光照和实时阴影可能在同一个 pass 中被计算:
以上的两种阴影都是平行光的阴影,再来看另一种 “点光源” 的实时阴影
平行光阴影的特点是一旦物体在光源背后,就无法产生投影。而点光源阴影则没有这种限制,在光源的任意方向上都能够产生投影:
在游戏中的室内场景就能见到这种投影方式。可以看到在光源的任意方向,都可以产生投影,即使投影处的高度已经超过灯:
事实上通过 omnidirectional shadow mapping 技术就可以实现。听起来很 gds,其实很简单,就是在点光源的 上下左右前后 6 个方向上面各做一次 shadow mapping ,然后得到 6 张阴影贴图,在计算的时候根据像素位置,去对应的阴影贴图中取数据即可。
最后是云阴影,实际上游戏中并没有实时的云阴影,那些阴影都是利用像素的世界坐标的 xz 轴,从一张 2D 的噪声图中采样然后直接贴上去的:
在有云阴影的位置,天空上的云并不与之对应。事实上游戏中很少有玩家能够接触到的体积云,除了巨神峰和某个秘境之外
环境光遮蔽
环境光遮蔽(Ambient Occlusion)用以补充光照不足且褶皱较多的场景的细节度。在开始介绍《原神》中的 AO 技术之前,先来看看现实中的 AO 效果。
环境光遮蔽就是环境光造成的阴影。在两个物体相邻但是不贴在一起的时候,一个物体往往会挡住射向另一个物体的环境光,并且 投射出微弱的阴影 。
注意下图:牙刷,沐浴露,书架分别在后面的 墙上 投射出遮蔽的阴影,并且阴影的强度随着他们距离墙的距离的减少而增加:
为了拍摄这幅图片我的牙刷刷墙了 3 次
环境光遮蔽能够显著地提升场景的表现能力,尤其是在弱光照的情况下,因为环境光遮蔽模拟了物体之间的层次情况。下面来对比一下有无环境光遮蔽的场景:
可以明显的看到,在没有太阳直射的时候,环境光遮蔽帮我们体现了场景的层次。如果单看左边,我们还以为城墙到底是一张贴图,而右边的带有环境光遮蔽的场景告诉我们,中间的城墙是凹下去的。
因为没有太阳直射,那么渲染方程中的 Ambient 项几乎是一个常数(尽管有 IBL 来帮忙但是还是不明显)于是所有物体都体现出同样的亮度,而引入环境光遮蔽之后,物体的颜色和位置有了关联,场景的保真度也就提升了。
再来看游戏中的环境光遮蔽。游戏中的 AO 分为 3 种不同的实现:
- HBAO,经典通用算法
- 体素 AO,用于静态物体
- 胶囊 AO,用于动态物体,比如玩家
其中各种方法取长补短,共同组成了最终场景的 AO 效果。我们从最基础的 AO 开始说起。在这之前记住:几乎任何 AO 的核心都是判断一个像素 受到遮挡的程度
在 2007 年的孤岛危机 1 中首次提出 SSAO 的概念。SSAO 又名屏幕空间环境光屏蔽(Screen Space Ambient Occlusion),通过屏幕空间的深度信息来判断遮挡程度。在 SSAO 中怎么判断呢?其实就是在点的周围采样一圈:
通过深度图(比对采样深度和测试深度)来判断采样的位置是否遮挡目标像素,这和 ray marching 算法的求交类似。如果采样总是被遮挡(对应上图红色箭头)那么反应了该点的遮蔽程度较高。
再来看 HBAO,HBAO 全名 Horizon Based Ambient Occlusion,不同于 SSAO 的一个方向一次采样,HBAO 使用 ray marching 策略进行一个方向多次采样,并且通过深度图得到一个 “高度场” 再根据高度场的信息得到视野角(horizon angle)通过视野角来描述该点被遮挡的程度:
此外还有一些 trick,比如随机旋转方向,模糊,时间滤波(类似 TAA)等,这里游戏中应该是实现了随机+模糊,算是一套常规的实现,对于任意大小的场景都能有较好的效果,注意墙上书本的投影:
因为是屏幕空间的算法,不在屏幕空间内的物体就无法产生遮挡,此外对于采样的距离我们也不好把握。距离大了,容易造成误判,距离小了又不能体现远端物体的遮挡,于是引入另一种方法:volume AO
volume AO 利用预先烘培的三维信息来计算遮挡。相比于上文提到的实时 AO,volume AO 适用于静态物体。在离线环境下记录物体的遮挡信息(最简单的比如存放 01 以表示该位置有无物体)并且存放在 volume texture(一种存储 3D 信息的纹理)中。在绘制 AO 的时候通过对 3D 纹理的查询可以获取屏幕空间之外的信息。
volume AO 是对基础 AO 的一种补充。比如下图中,volume AO 就可以绘制一些常规算法难以绘制的 ”死角“ 。遮挡信息也很灵活,既可以是预先绘制好的 AO map,也可以单纯存储物体的 type 或者 id …
唔。。。在米忽悠自己的 Unity 2020 talk 中提到了 volume AO 的效果:
但是实际游戏中,我把特效拉满也无法复现这个效果,可能是 ps 独占?或者我的 A 卡不支持?如图:
我倒是在教堂里面发现了奇怪的疑似体积 AO 的东西。注意下图中凳子下方的很淡的阴影,即使特效开极低也存在:
好吧不管力。。。快进到下一个
遮挡信息预先存储,volume AO 对于动态物体(比如玩家)就无能为力了,于是还有一种专门为了绘制动态人物的方法。
利用 capsule AO 来为动态的人物绘制环境光遮蔽。capsule AO 人如其名,用类似 “胶囊” 的包围盒包住人物的四肢:
在计算 AO 的时候遍历这些胶囊,判断是否对当前像素造成遮挡即可。事实上人物向墙面的投影就是用这种方式计算的:
注意 椅子并没有产生环境光遮蔽 ,而人物模型却产生了。这是因为常规的 AO 算法都有一定的角度的限制,在比较极端的角度无法有效的工作。这也侧面说明了《原神》中的 AO 是多种技术的混合。
体积特效
体积特效分为体积光和体积雾,两者都是使用 ray marching 算法实现的。关于 ray march 可以看看我之前的博客:体积云渲染实战
这里简单提一下,就是发射光线,记录信息,没了!下面是体积云的 ray march 示意图(是的,我懒得重新画了)而 “相交” 判断的不同,能够造成不同效果的绘制。
比如体积云是根据 3D 的密度函数来判断采样是否在云层中。体积光是通过将坐标转换到光源视角,同时做一次深度的判断来检验采样点是否暴露在光源之下(有点像 shadow map)
先来看体积光。体积光分为两种绘制方式,分别是贴图和 ray march。前者单纯是一张静态的贴图,而后者可以根据光被遮挡的情况正确的呈现,因为后者严格按照视觉规律进行模拟计算:
再来看一张有意思的图:因为 ray march 的步长不够,走到一半循环就结束了,于是在距离山体较近的地方,体积光缺了一截:
步长限制了渲染范围,这也是 ray march 的通病了,有很多 trick 可以改进,这里就不展开细说了。
再把目光放到体积雾上边。体积雾的效果乍一看好像和基于距离的线性(或者指数)雾没啥区别:
但事实上线性雾的颜色仅来源于两个信息:距离,视线方向。虽然我们可以通过视线方向和太阳位置的偏差来绘制夕阳下的雾,并且大多数情况下都是好用的。图为 关闭 体积雾时的效果:
但是人类的贪欲永远没有极限。如果想为每一盏灯也绘制照亮雾的效果,比如下图的照片是有一次深夜坐地铁的时候拍的,高楼上的工地亮着的大灯能够照亮旁边的雾:
猜猜这是科苑站的那个出口?
A. B, B. D, C. A D. C
那么传统的线性雾就没办法做到。因为灯光是从相机到目标片元路途上的信息,只有通过 ray march 老老实实地一步一步去记录,才能正确地绘制。
如果 ray march 的过程中遇到的位置属于光源范围,那么就积累亮度,这就是基本的思路。下图是《原神》中关闭了 bloom 的情况下,开启体积雾而产生的效果:
可以看到光源很好的点亮了雾。而光源的信息和前文的 volume AO 一样,存储于 3D 的体素纹理中。我估计美工在设置的时候,光源的体积应该略大于实际模型,这样才能造成 “扩散” 的感觉。
还有很重要的一点:如何优化性能?采样次数是最主要的指标。减少采样次数,图像会变得非常 “破碎” 且 “层次分明”,使用随机步长以减少图像的 分层感。而随机的步长则会带来噪声,如图:
使用时间滤波的思路处理噪声,比如 TAA 抗锯齿就是这么弄的,然后可以得到能够接受的结果:
注:
上图引自游戏《INSIDE》在 GDC 上的 talk 的 ppt
原视频可以看油管:传送门,14 分 40 秒左右
因为《原神》肯定不会把没 de noise 的 debug 版放出来给玩家,于是只能偷别的游戏的图了。。。
他两游戏的体积特效的优化方式都是类似的,随机步长+TAA
屏幕空间反射
PBR 中虽然有反射方程,但是图像来源却是预先绘制的立方体贴图,或者说 reflection probe(反射探针)那么反射的位置当且仅当 渲染相机和绘制立方体贴图时的预处理相机位于相同位置 时,才能得到正确的反射。
否则无法做到完全正确的镜面的反射。比如下图反射的物体和实际的物体对不上,注意下图中的三根柱子的位置和反射的位置其实是错位的:
此外,不在环境立方图里面的物体也不会被反射,比如玩家,或者是路人 NPC,如图:
于是便需要屏幕空间反射来补全缺失的 营养 这一块。可以看到屏幕空间反射的效果十分正确:
屏幕空间反射又名 SSR,能够从物理上模拟光的步进,并且通过和类似 ray march 的方式,一步一步步进,最终找到镜面反射的像素的来源:
注:
此图来自于我之前的博客:从零开始编写minecraft光影包(9)高级水面绘制
没错,又偷了!我摆烂了
而屏幕空间反射的缺点就是只能反射屏幕空间的东西,换句话说,你看得到的东西才会被反射,如图,视线范围内的柱子才拥有反射:
此外,《原神》中的屏幕空间反射也遵循菲涅尔定律。菲涅尔定律告诉我们:平视镜面的时候反射光居多,而俯视镜面的时候反射光偏少:
在游戏中的反射也存在相同的近似,注意一排灯光的亮度,随着视角角度的减小而衰减:
此外,在反射的边缘,SSR 会和 PBR 的反射做一个插值以求尽量平滑过渡。好吧,也不平滑。。。
视差
视差贴图通过高度纹理,描述物体表面的高度,并且能够模拟具有巨量微结构的物体。要知道这在传统绘制中需要倍增十几倍的 mesh 才能做到。一张典型的视差贴图效果如下:
上图仅使用了一个四边形,就模拟出数十个四边形才能实现的凹凸效果。这是一种视觉上的欺骗。
视差贴图通过摄像机的角度,和平面的高度纹理,计算出一个 偏移的纹理坐标 ,以欺骗我们的眼睛。以二维为例。假设视线射向平面,那么对于 A 点,应该去 A 点的纹理处采样:
而高度图告诉我们,平面上 A 点的高度是 h,那么视线实际射到的是 B 点,于是我们要采样 B 点对应的纹理值,这样你的眼睛会认为你看到了 B 点:
通过射向 A 点的射线,和 A 点的高度,计算出纹理坐标的偏移量,就是上图中 A 到 B 实际需要偏移的纹理坐标。然后利用偏移的纹理坐标实际采样纹理,即可模拟凹凸表面。下面是《原神》中的视差效果:
注:
有多种方法可以计算视差的纹理偏移量,可以参考:Learn OpenGL
植物渲染
唔。。。植物的渲染一直是比较蛋疼的,要想在性能和美观上取得平衡非常不易。下面是《原神》中大量植物的场景:
要想模拟枝繁叶茂,就需要很多的 mesh,比如下图是一个非常逼真的松树,包含了 55336 个三角形,非常恐怖:
而实时游戏,光这一棵树都承受不起了,别说一群了!
事实上现代游戏中,植物总是作为透明物体被渲染。通常使用一个或者多个三角面片就可以表示,然后贴个带 alpha 通道的透明贴图,在 alpha 通道显示透明度为 1 的地方都没有颜色,于是可以直接看到后面的场景,就好像真的有这么多叶子一样:
事实上从极端的侧面角度看过去,任然能够看到植物渲染的时候,原本的三角面片的结构就会暴露无遗:
此外,对于远端的树木或者植被,因为 LOD 需要,干脆直接不渲染 3D 的模型了,改用一个四方形 + 贴图的形式。如图:
关于植物渲染,除了节省开销以外,如何更加真实的模拟自然植物的物理现象也很重要。还有一个小细节:当玩家走过草的时候,草会被压弯:
这个特效也不难实现,在顶点着色器中,将草的顶部顶点,根据玩家和草顶点的位置向量,做一个偏移即可:
注:这里是错切变化
因为没有装 ps 所以没法绘制正确的错切图,意思到了就行(摆烂x3
至于顶点着色器怎么判断是草的顶部顶点?估计是美工刷了个顶点属性上去,然后顶点着色器特判一下即可。或者通过当前顶点和地形的高度差来判断,方法有很多。。。
反向动力学骨骼
你可能注意到一个细节:当人物站在高度不一致的平面上时,左右脚会自动调整 y 的位置,并且带动大腿小腿同时调整:
仅调整脚部的位置不难,难点是如何通过脚部 y 的变化,来调整其父骨骼,爷骨骼的变化。这就是反向动力学骨骼。下图为 mikumikudance 里面的反向动力学骨骼的效果:
可以看到在仅调整脚部 y 坐标的情况下,整条腿都发生了变化。只有两条骨骼的情况下很容易调整,通过简单的余弦定理即可,因为骨骼的长度总是固定不变的,通过三角形的三个边长,确定对应的角度,不是什么困难的事情:
当然,任何一个游戏的模型都不可能这么简单,除了 y 轴,还有 xz 轴的骨骼,他们拥有更加复杂的变化,并且有时候会出现非常冗长的 IK 链,这就需要一些算法去通过子骨骼的变化来解算父骨骼的变化。
不仅脚部拥有这个特性,人物的手部也有。在爬山爬墙的时候,玩家的手总是能够 “自适应” 凹凸不平的表面,这也是反向动力学骨骼解算的结果:
当然,这些算法并不是任何时候都能起作用,比如,额:
至此,本文的最后一个讨论正式结束。但是现代计算机游戏的种种技术远不是一篇文章能够简单谈完的,这是无数的厂商,程序员,软硬件工程师,科学家日夜奋斗的结晶。
希望大家能够通过这篇文章,对现代计算机图形技术有一个管中窥豹的认识,能够大概的知晓屏幕上面的芭芭拉是通过一系列精妙的算法得到的,而不是魔理沙的月球魔法。(这句我在我之前的博客也用过,经典偷过来
后记
唔。。。现在是 3 月 12 日的凌晨了,事实上我在 2 月下旬就起草了文章,结果中间一直摸,直到今天这个大坑今天算是填完了?
时间飞逝,而我的魔怔程度却丝毫不减。今年年初立了个 flag 我的 光线追踪的那篇博客 会是今年最长,看来这一下子就回收了。。。
其实文章的完成并没有想象中的顺利。第一次写这种杂谈类型的文章,从一开始的不知如何下手,到磕磕绊绊地码公式,查资料,看 talk,包括但不限于顶着极低的帧率,顶着 100% 的 CPU 和 GPU 在敲 markdown,还要提防好机油们来我世界偷菜… 好在这些我都扛过来了
令我意外的是,拥有国内*技术力的《原神》开发组竟然没有使用一些比较新的技术,比如光追。不过鉴于技术总监的原话:“时间紧,只能选择成熟稳定的技术,没有时间试错!”,并且《原神》需要跨平台,优化与稳定才是大头,所以也算是意料之外情理之中
话说回来,在完成这篇文章之后,我也算是对实下游戏图形渲染方向有了大概的了解,俗话说罗马不是一天建成的,但至少在这个潮湿的 3 月我迈出了小小的一步。
从上高中起我就想当图程,当时是玩 mc 光影入的坑,特别膜 SE,也就是 SEUS 光影作者。和大多数同学不同,我写的第一个程序不是 hello world,而是 *= 2;
,并且在 mc 里面将一切都变得 “绿色” 起来。
尽管一路上见证了互联网的兴起与内卷,人工智能的猛烈,并且身边的人都和我江:图程毕业即失业。在这之前我也非常迷茫,自己是否选择了计算机中的 “生化环材” ?带着疑问,我开始登山,并且敲下本篇文章的第一个字符。
说巧不巧,在敲上面那句话的时候,刚好响起了我最喜欢的一首车万同人曲《Can’t look away》
V 姐带着法国 + 日本口音的英语响起:“追逐梦想是非常短暂的,过不了多久我便无法控制,我已经做出了改变,现在我无法离开”
再回头看看,我想,我有了自己的答案。
十年前 vulkan 横空出世之时大喊我是你爹,众人高呼 OpenGL 死刑立即执行。在蹉跎之间,与 vulkan 和 d3d 两座大山夹击之下,GL 不仅没寄,而且 +1s,此外 ue 依然火热并且最近要出 ⑤,cocos2D 活跃在小游戏,而 unity 有式微之趋,要知道当年 u3d 可是仌,而 coco 半截身子已经埋在土里了… 所以未来是啥样子捏?狗都不知道!
emm 写完后记百感交集却又哽咽,在这里在立个 flag 吧,无论在那个 branch 上,我都希望以后的我对今天的我说:“这把不选卢仙,啥b!”