Skip to content

Commit

Permalink
chapter 3
Browse files Browse the repository at this point in the history
  • Loading branch information
ElvisQin committed Jan 7, 2024
1 parent 23ff171 commit 58ecb18
Show file tree
Hide file tree
Showing 49 changed files with 26,959 additions and 9 deletions.
2 changes: 1 addition & 1 deletion website/docs/10-precomputed-radiance-transfer/index.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
# 11 基于预计算的全局光照技术
# 10 基于预计算的全局光照技术

2 changes: 1 addition & 1 deletion website/docs/11-voxel-based-global-illumination/index.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# 12 基于体素的全局光照技术
# 11 基于体素的全局光照技术
2 changes: 1 addition & 1 deletion website/docs/12-distance-field-gi/index.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# 13 基于距离场的全局光照技术
# 12 基于距离场的全局光照技术
40 changes: 40 additions & 0 deletions website/docs/3-shading/1-shading-basis/1-rasterization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
title: 3.1.1 光栅化技术
---

将需要迭代计算的渲染方程改为能够使用一些材质参数一次性计算的着色方程(第3.1节式(2))之后,利用图形硬件提供的图形处理器接口(如OpenGL,DirectX等),场景的渲染过程就变得十分简单,整个渲染过程可以简述如下(如图(1)所示):

<Figure num="1" id="f:shade-pipeline" caption="利用图形处理器接口提供的渲染管线渲染场景的过程,首先将顶点光栅化为片元,然后根据相关材质参数在片元着色器中计算像素的颜色,帧缓存中的深度缓存被用来剔除被遮挡的片元">
<img src="/img/figures/shade/pipeline.svg" width="600"/>
</Figure>

1. 放置一个虚拟摄像机在3D场景中的某个位置,并设置一个视锥体来表示3D世界中的可视区域,这片区域被映射到屏幕上一块2D的具有一定分辨率的窗口。
2. 场景中每个物体所包含的顶点数据以及绘制该物体所需要的所有环境贴图,阴影等被提交到OpenGL开始绘制该物体,这些顶点所构成的图元被视锥体裁剪(物体在被绘制之前通常也会在CPU端执行一个基于物体包围盒的3D空间的裁剪。),处于视锥体外部的图元将被丢弃,与视锥体相交的图元将被裁剪,剩下的图元被光栅化到与窗口分辨率对应的像素位置,每个像素块称为一个片元。
3. 片元着色器对每个片元使用第3.1节式(2)进行着色计算,它遍历场景中的每个光源分别计算其对该片元辐射照度的贡献,并将最终结果与帧缓存上对应像素位置上的深度值进行比较,如果通过深度测试则将该颜色值与帧缓存上的颜色值进行混合。
4. 当所有物体被遍历完后帧缓存上的颜色缓存被送到显示设备进行显示,或者其结果被读回到宿主程序。

这个传统的渲染过程非常简单,它基本上就是直接利用图形处理器接口提供的经典渲染管线,因此它具有所有图形处理器接口提供的便利或优点:

- 结合图形接口深度测试和颜色混合的机制,传统的渲染管线可以很容易地实现对半透明物体的绘制。
- MSAA被集成到渲染管线,它可以对每个片元的深度进行多次采样,而使用一次着色计算以实现反锯齿。
- 每个物体可以根据其图形特征使用独立的着色器或着色器组合。

当场景的结构比较简单(拥有较少的物体数量以及光源数量)时,上述的渲染方法非常简单且高效,然而随着场景结构复杂度的增加,该方法会变得非常低效。

在传统的渲染管线中,所有很影响性能的因素几乎都跟一个称为过度绘制(overdraw)的概念有关。一般来说,要求得一个摄像机所能看到的场景中的每一个像素点,必须沿从摄像机穿过每一个2D屏幕上像素点的方向上,与整个场景数据做一次相交计算,该方向与场景所有交点的最近点即为屏幕上该点的可视点,然而这样的相交计算非常复杂,所以光栅化技术的核心要点正是简化了这个相交计算:它对整个场景只遍历一次,并记录下屏幕上每个像素点方向上的表面点的深度值,然后让后续的同样落于该像素点的表面点的深度值与该值进行一个简单的深度比较,并保留深度值更小的表面点,这样当整个场景被遍历一遍之后,帧缓存上留下的就是整个场景中所有可视的表面点,这个过程非常高效。然而它的缺陷就是,对于每个表面点,我们必须计算出完整颜色值,即是对其执行第3.1节式(2)的计算,这样就导致了过度绘制,因为大量的被遮挡的表面点将被深度测试丢弃,从而导致计算资源的浪费,尤其当场景中有大量光源的时候,第3.1节式(2)中需要分别计算每个光源的贡献,这种资源浪费随着光源数量的增加而增加,例如本书后面讲述的即时辐射度方法(参见第(9)章)可能有上万个虚拟的点光源(VPL),它也随着场景复杂度的增加而增加,因为更多的表面点可能被遮挡。这种渲染性能和场景复杂度的耦合导致的结果是灾难性的,它使得应用程序可能具有极不稳定的帧率。

<Figure num="2" id="f:shade-light-culling" caption="当以物体的几何尺寸进行光源剔除时: (a)大物体小光源的场景会使得每个片元可能计算大量无效的光源影响,而(b)小物体大光源的场景会使得大量小物体重复进行光源剔除计算">
<img src="/img/figures/shade/light-culling.svg" width="650"/>
</Figure>

其次,即使是针对有效片元(那些最终没有被深度测试丢弃的片元)的着色计算,由于场景中可能分布成千上万的光源,因此为了提高渲染性能,光源剔除(light culling)十分关键,它要求我们在绘制一个物体之前,应该排除那些对其完全没有影响的光源。这里考虑场景中像直线光源这样对整个场景都有影响的光源还是少数(例如一个场景可能只有一个太阳光源),大部分光源都是点或者其他面积光源,这些光源的辐射强度都受一个距离递减函数的影响,如图(2)(b)所示,因此光源剔除基本上可以排除大量的无效光源(光照影响为0),从而大大提高渲染性能。光源剔除的目标是使每个片元计算的光源的数量达到最少,有时候我们也称其为光源分配(light assignment),这是着色管线基础架构的重要内容。

在目前的这种方法中,进行光照剔除的唯一方法是在绘制之前对物体与光源执行包围盒比较,然后只将那些影响该物体的光源信息上传至GPU以进行最节省的片元着色计算。因此为了最大限度降低每个物体受其影响的光源数量,我们应该减少一次绘制的顶点的数量,或者说尽可能每次绘制更少数量的物体,然而这却和每次尽可能绘制更多顶点以降低GPU状态切换的高昂性能代价相矛盾,这个矛盾使得我们很难权衡每次绘制应该选择的批绘制的大小。

当我们以物体的尺寸为依据来选择批绘制块的大小时,如果场景中包含少量大物体和大量小光源时,虽然每个光源可能仅影响大物体的一部分,但是我们不得不对该次绘制使用全部光源,因为以物体的几何尺寸为依据无法剔除这些光源,如图(2)(a)所示;同样,当场景中包含大量小物体以及少量大范围的光源时,每个小物体都要单独分别对每个光源进行剔除计算,尽管我们知道多个小物体同时都处于该光源的影响范围之内,如图(2)(b)所示。更糟糕的是,场景中可能同时存在这两种情况,因此不管我们怎样权衡,都不可能使每片元计算光源数量达到最低。所以本章后面讲述的着色架构的重要改进就是不以几何物体尺寸,而是以屏幕空间的2D分块(tile)为依据进行光源剔除。

最后,由于传统的渲染管线的着色计算必须在一个片元着色器中完成,因此这些不同的光源组合将导致非常复杂的着色器管理。这些着色器的组合数量是物体类型数量和每种类型不同光源数量(\#lights/type)的一个排列组合。如果我们只使用一个巨大的着色器,在着色器内部使用根据条件进行各种分支切换,这种臃肿包含众多分支的着色器称为大型着色器(uber shader),GPU中的大量分支计算将导致非常低的性能,参见第(2)章的内容。

同时,也因为所有着色方法被混在一起,所以所有的输入数据不得不随时在内存中处于可用状态,这包括所有的光照图,反射图等等。

由于以上传统渲染管线的各种问题,现代实时渲染方法通常采用改进的方法进行物体表面执行着色计算,这其中最著名的是本章将要讨论的针对过度绘制改进的延迟着色技术,以及在延迟着色技术基础上,针对光源剔除改进的基于2D分块和3D分簇的延迟渲染技术。
29 changes: 29 additions & 0 deletions website/docs/3-shading/1-shading-basis/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
title: 3.1 着色技术基础
---

在第1章讲述渲染方程的时候,我们说明了渲染方程是一个费雷德霍姆第二类方程,为了方便,我们重写一个简化的渲染方程如下:

$$
L_o(p,\mathbf{v})=L_e(p,\mathbf{v})+{\rm \int}_\Omega f(\mathbf{l},\mathbf{v})\otimes L_i(p,\mathbf{l})\cos{\theta_i}{\rm d}\omega_i
$$
<Eq num="1" id="eq:shade-rendering-equation"/>

这里$L_o(p,\mathbf{v})$表示点$p$处沿$\mathbf{v}$方向的辐射亮度,$L_e(p,\mathbf{v})$表示点$p$处沿$\mathbf{v}$方向自发光的辐射亮度,$L_i(p,\mathbf{l})$表示沿$\mathbf{l}$方向射向$p$点的辐射亮度,$f(\mathbf{l},\mathbf{v})$表示$p$点处的BRDF函数。

对于方程(1),其完整的解通常需要使用一些迭代的方法来计算,本书后面会讨论多种解这个方程的方法。但是很显然,这样一个方程并不是对GPU友好的,我们不能直接将它放入着色器中求解,着色器中要处理的公式,它必须是能够直接根据材质参数计算出最终结果的,也就是说着色器中不能包含未知的参数。

因此,正如将在后面的一些章节中看到的那样,实时渲染方法中通常将渲染方程分解为多个部分,并使每个部分能够以各种方式形成着色器中的一个参数,最终着色方程可以直接根据这些参数(它们通常都是某种程度上的近似值)计算出一个像素点的最终颜色。这些参数可能是一个包含间接光照的球谐函数,一个包含远距离环境反射的环境贴图,或者是一个光源的阴影贴图等等。它们可能以预处理的方式提前在预处理阶段计算出来,也可能实时地使用光栅化技术来计算某个量,不管怎样,这些参数使得最终在着色器中我们可以使用一个公式计算出最终的颜色值。

有了这些材质参数,着色方程可以被写成一个只包含最基本的几个变量的形式,渲染方程的各个部分可以根据这些最基本的变量以及材质参数计算出来(例如通过观察方向和物体表面的法线方向就可以确定入射光的方向从而对环境贴图进行采样),这个包含基本变量的着色方程如下:

$$
L_{o}(\mathbf{v})=\sum^{n}_{k=1}f_{\rm shade}(E_{L_k},\mathbf{l}_k,\mathbf{v},\mathbf{n},\mathbf{c}_{\rm diff},\mathbf{c}_{\rm spec},m)
$$
<Eq num="2" id="eq:shade-shading-equation"/>

这里的$\sum$加数形式表示辐射亮度$L_o(\mathbf{v})$是所有光源的累积贡献,$E_{L_k}$表示第$k$个光源的辐射照度,$\mathbf{l}_k$表示入射光方向矢量,$\mathbf{v}$表示观察方向矢量,$\mathbf{n}$表示表面法线矢量,$\mathbf{c}_{\rm diff}$和$\mathbf{c}_{\rm spec}$分别表示表面的漫反射折射率和高光反射折射率,$m$表示某个高光模型(例如Blinn-Phong模型)中的高光扩散系数(specular spread factor)或者粗糙度。

这里有两个变量是随着光源的变化而变化的:即入射光方向矢量$\mathbf{l}_k$和光源辐射照度$E_{L_k}$,对于辐射照度$E_{L_k}$,它一般可以通过光源参数中的辐射强度$I$和距离递减函数求得,即:$E=I\cos{\theta}f_{\rm dist}(r)$,参见第1.3节的内容。需要注意的是,公式(2)仅考虑点光源和直线光线,它们可以很直接地计算出辐射照度$E_{L_k}$的值,对于其他光源如环境贴图,在渲染中通常使用单独的渲染通道来处理,此时,利用式(2)中的变量仍然能够满足计算辐射照度的条件(例如环境贴图需要的入射光方向)。

有了如式(2)这样直接的着色方程表述,对一个物体表面进行着色也就是在着色器中执行该式计算的过程。最简单的方式就是在OpenGL渲染管线中对场景中的所有几何体的顶点进行光栅化,然后在片元着色器中执行式(2)的计算,不过不幸的是,这种方法虽然简单却有很大的缺陷导致其性能很低,因此工程师们针对渲染管线建立一些不同的基础架构,如延迟着色,它们用来改善直接光栅化技术的性能问题,这些架构方法正是本章要讨论的内容,要深入理解这些架构我们首先需要了解光栅化技术存在的一些问题。
65 changes: 65 additions & 0 deletions website/docs/3-shading/2-deferred-shading/1-deferred-lighting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
title: 3.2.1 延迟光照计算
---
虽然延迟着色将着色计算与深度测试分离开来,使得着色计算仅针对屏幕空间中的像素点进行,从而大大节省计算资源,但我们仍然面临光源循环导致的帧缓存数据重复读取的计算资源浪费,于是我们想要进一步从着色计算中抽离出仅与光源相关的部分,这样就可以节省出光源循环中对帧缓存数据的重复读取。

渲染方程通常拆分成漫反射和高光反射,所以第3.1节式(2)可以写成如下的形式:

$$
L_o(\mathbf{v})=\sum^{n}_{k=1}\bigg(\mathbf{c}_{\rm diff}\otimes f_{\rm diff}(E_{L_k},\mathbf{l}_k,\mathbf{n})+\mathbf{c}_{\rm spec}\otimes f_{\rm spec}(E_{L_k},\mathbf{l}_k,\mathbf{n},\mathbf{v},m)\bigg)
$$
<Eq num="1"/>

考虑到物体表面的反射率是由材质提供的常数,所以上式可以进一步写成:

$$
L_o(\mathbf{v})=\mathbf{c}_{\rm diff}\otimes\sum^{n}_{k=1}f_{\rm diff}(E_{L_k},\mathbf{l}_k,\mathbf{n})+\mathbf{c}_{\rm spec}\otimes \sum^{n}_{k=1}f_{\rm spec}(E_{L_k},\mathbf{l}_k,\mathbf{n},\mathbf{v},m)
$$
<Eq num="2" id="eq:shade-deferred-lighting"/>

分析上式,我们可以将漫反射率和高光反射率从着色方程中抽离出来,使得与光源相关的计算部分只剩下法线和高光扩展系数,而光源入射方向是可以从法线方向计算出来的。因此,按照这样的拆分,我们可以只需要在G-buffer中存储法线和高光扩展系数,法线是一个RGB颜色值,高光扩散系数通常是一个很小的整数,因此整个G-buffer只需要一个32位的颜色缓存(即是光照计算的延迟着色器中只需要读写一个32位而不是128位甚至更多的数据)即可,这种情况下甚至不需要硬件支持多目标渲染(MRT)。

不过此时我们需要两个累积缓存来分别保存漫反射和高光反射部分“辐射照度”的值,它们分别是:

$$
\begin{aligned}
g_{\rm diff}&=\sum^{n}_{k=1}f_{\rm diff}(E_{L_k},\mathbf{l}_k,\mathbf{n})\\
g_{\rm spec}&=\sum^{n}_{k=1}f_{\rm spec}(E_{L_k},\mathbf{l}_k,\mathbf{n},\mathbf{v},m)
\end{aligned}
$$
<Eq num="3"/>

当光照计算阶段完成之后,我们再对场景使用渲染管线执行第二次渲染,但是此时的着色计算仅需要从上述两个累积存储中取出辐射照度的数据,然后依照下式进行计算即是最终的着色颜色值:

$$
L_o{\mathbf{v}}=\mathbf{c}_{\rm diff}\otimes g_{\rm diff}+\mathbf{c}_{\rm spec}\otimes g_{\rm spec}
$$
<Eq num="4"/>

如果硬件支持MRT,我们同样可以直接将法线以外的材质数据保存在G-buffer中,所以第二次几何通道不需要重新渲染整个场景,而仅对屏幕空间执行一次渲染即可。延迟光照计算的渲染过程如图(1)所示。

<Figure num="1" id="f:shade-deferred-lighting" caption="延迟光照计算中的各个阶段,首先光照计算需要的表面属性被写入到一个法线缓存中,然后光照计算读取法线缓存中的数据将计算出的辐射“辐射照度”信息写入到光照累积缓存中,最后在对场景执行一次光栅化,并直接读取光照累积缓存中的数据对像素点进行最终着色计算">
<img src="/img/figures/shade/deferred-lighting.svg" width="100%"/>
</Figure>

由上述内容可知,延迟光照计算的过程可以简述如下:

1. 渲染场景中所有不透明的几何体,并将法线矢量$\mathbf{n}$和高光扩展系数$m$写入到帧缓存中,由于法线矢量占据三个分量,而高光扩展系数是一个很小的整数,所以它们可以被合进一个“n/m缓存”,这只需要一个4分量的颜色缓存即可,所以可以不需要MRT的支持。此过程称为第一个几何通道。
2. 和延迟着色一样,该阶段逐个渲染每个光源包围盒所在的几何体,并对该光源覆盖的每个屏幕像素点进行光照计算;和延迟着色不一样的是,它只计算“辐射照度”而不是辐射亮度,并且最终输出的漫反射和高光反射的辐射照度被写入到两个累积缓存中。此阶段为延迟光照计算阶段。
3. 重新渲染所有不透明几何体,但是此时并不需要对表面进行光照计算,它仅仅需要从前一阶段的两个颜色累积缓存中读取值执行式(2)的计算即可。并将最终的着色结果写入到帧缓存中。此阶段为第二次几何通道。
4. 绘制所有半透明的物体。

延迟光照计算方法被大量使用于游戏引擎中,其中对其进行优化的地方主要集中于延迟光照计算阶段输出的两个辐射照度量的存储表述。每个颜色值分别包含RGB三个通道,所以需要两个各自具有3通道的颜色缓存对象来存储,在Crytek的CryEngine3[a:AbitmoreDeferred-CryEngine3]引擎中,他们使用一个4通道的颜色缓存(A16R16G16B16f或A8R8G8B8)来同时记录漫反射和高光反射的辐射照度,其中前3个通道表示漫反射值diffuse,第4个通道表示高光的强度strength,所以高光的颜色值可以由diffuse*strength计算得出,它的效果如图(2)所示。

<Figure num="2" id="f:shade-deferred-lighting-crytek" caption="Crytek在CryEngine3中使用一个4通道的颜色缓存来同时记录漫反射和高光反射(图片来自Crytek)">
<img src="/img/figures/shade/tea0.png" alt="原始6通道" width="49%"/>
<img src="/img/figures/shade/tea1.png" alt="4通道" width="49%"/>
</Figure>

Crytek并没有说明他们使用何种方法来编码高光反射的值,但是从图(2)可以看出,其中茶壶同时被一个红色光源和一个绿色光源照亮,图(2)(a)的原始6通道的颜色缓存能够准确反应反射物体的颜色,它的边沿上呈现红绿两种高光,而4通道的方案失去了反射环境的颜色,所以它仅仅保留了高光的亮度(luminance),而丢弃了高光的色度(chrominance)。因为人眼对于高光的亮度感应较色度更为明显,所以一般情况下没有太大问题。Pavlos Mavridis等[a:TheCompactYCoCgFrameBuffer]提出了一种帧缓存的压缩方法可以用一个4分量的颜色缓存更精确地存储两个颜色值,它的效果如图(3)(c)所示。

<Figure num="3" id="f:shade-deferred-lighting-1" caption="使用YCoCg压缩帧缓存用一个4通道的颜色缓存可以准确存储高光的色度和亮度,(b)中的方案直接丢弃色度而仅保留亮度,所以在高光部分看不出光源的颜色">
<img src="/img/figures/shade/deferred-lighting-1.jpg" alt="原始6通道" width="33%"/>
<img src="/img/figures/shade/deferred-lighting-2.jpg" alt="4通道" width="33%"/>
<img src="/img/figures/shade/deferred-lighting-3.jpg" alt="压缩帧缓存" width="33%"/>
</Figure>
Loading

0 comments on commit 58ecb18

Please sign in to comment.