Zhytou

Zhytou

May the force be with me.

01 Aug 2024

GAMES101

Transformation

MVP

  • 模型变换(Model Transformation):模型变换指的是对3D场景中的单个对象或模型进行变换的过程。这种变换包括对对象的顶点进行缩放、旋转和平移,将对象定位和定向到世界坐标系中。
  • 视图/相机变换(View/Camera Transformation):视图或相机变换涉及将整个场景从世界坐标系转换到相机或视图坐标系的过程。这种变换定义了虚拟相机在场景中的位置和方向。它包括设置相机位置、定义目标点或观察方向以及指定上方向等操作。
  • 投影变换(Projection Transformation):投影变换是将3D场景的坐标转换为屏幕上的2D坐标的过程。这种变换模拟了3D场景如何投影到2D平面上,考虑到透视和深度提示。常见的投影类型包括透视投影和正交投影。
  • 视口变换(Viewport Transformation):将处于标准平面映射到屏幕分辨率范围之内,即[-1,1]^2[0,width]*[0,height], 其中width和height指屏幕分辨率大小。

其中,模型和视图变换通常都放在顶点着色器阶段,而投影变换则由图形流水线自动实现(裁剪/透视剔除)。

齐次坐标

为什么三维的坐标会使用四维矩阵进行平移或旋转的计算呢?即:为什么原本n维的向量要用一个n+1维向量来表示?

Why do we use 4x4 matrices to transform things in 3D?

其实,这种表达方式被称为齐次坐标(Homogeneous Coordinates),即原本n维的向量用一个n+1维向量来表示。其核心目的是为了更优雅的表示平移的距离,比如:

平移

旋转矩阵推导

2d旋转

3d旋转

Triangle

判断点在三角形内部

设三角形顶点为P1、P2和P3,待检测点为Q,则Q在三角形P1P2P3内部的条件为,下面三个叉乘符号相同:

  • P0P1×P0Q
  • P1P2×P1Q
  • P2P0×P2Q

Rasterization

光栅化(Rasterization)就是把几何图元转化成片元(像素)的过程。其中,光栅(Raster)一词来自德语,表示栅格。因此,光栅化的通俗理解就是在栅格显示设备上绘制图像。

基本图元

一般来说,光栅化的输入都是三角形图元。而采用三角形作为基本图元的原因包括:

  • 三角形是最基本的多边形。
  • 三角形具有平面性。
  • 三角形可以明确定义内部和外部,且可以通过向量叉积来判断。
  • 任意多边形可以拆分成多个三角形。

采样绘制

采样绘制就是将三角形图元分割为屏幕上的像素,确定哪些像素需要在稍后上色。具体来说,就是根据像素中心是否位于图元内部这一点来判断的,如下图所示。

采样绘制

Aliasing

走样原理

站在信号与系统的角度来看采样,在时域上是原信号和周期冲激信号的一个乘积。那么根据傅里叶变化,在频域上就是原信号和周期冲激信号的卷积,如下图所示。

傅里叶变换

那么当原信号频谱过宽或采样用的冲激信号频率太低,就容易发生信号重叠,由此造成走样。

走样原理

抗锯齿/抗走样原理

根据走样原理,由此可得出抗锯齿原理其实就是尽可能避免信号高频部分发生重叠。一般来说,常见的抗锯齿方法有两种,分别是:

  • 提高采样频率:直接使用更高的采样频率对原信号进行采样,从而将频谱副本推得更远,避免重叠。
  • 模糊+采样:先使用一个平滑滤波器去滤除部分高频信号,再进行采样,进而避免潜在的高频重叠。

SSAA

超采样抗锯齿(Supersampling Anti-Aliasing,SSAA)正是上述第一种思路(提高采样频率)的直接体现。它以高于目标分辨率的分辨率渲染场景,然后通过下采样将多个子像素的颜色平均,得到最终像素颜色。这种方法效果最好,但由于每个子采样点都需要完整执行片元着色器,性能开销极大。

SSAA

MSAA

多重采样抗锯齿(Multisample Anti-aliasing, MSAA)是对SSAA的改进。它保留了SSAA在子像素层面进行覆盖率和深度/模板测试的思想,但避免了SSAA中最耗时的部分:对每个子采样点执行片元着色器。

MSAA的核心优化在于:它假设每个像素内的颜色变化主要来自几何边缘,而非材质内部。因此,每个像素只执行一次片元着色器,而多个子采样点仅用于更精确地计算几何覆盖率(即像素中每个采样点是否被三角形覆盖)。最终颜色根据覆盖率对片元着色器的结果进行混合。深度和模板测试同样在子采样点级别进行,以保证几何边缘的精确性。以4xMSAA为例,其运行流程如下:

  • 首先,GPU会将屏幕上的任意一个普通像素,在内部划分出4个互相错开的子采样点(四角/四等分位置)。
  • 接着,GPU对每一个子采样点独立执行覆盖率检测与深度、模板测试:判断该采样点是否被当前三角形图元覆盖,以及是否被前方几何体遮挡。
  • 若像素内至少存在一个有效子采样点(被图元覆盖且通过深度/模板测试),则GPU会以像素中心为输入,执行一次片元着色器。所有子采样点均共享该颜色,这也是MSAA相比SSAA大幅节省性能的核心,不会为4个子采样分别重复着色。
  • 然后,为不同状态的子采样分配对应颜色:被当前图元覆盖的子采样,赋值为片元着色输出的颜色;未被覆盖、或是被其他物体遮挡的子采样,保留原有背景 / 后方物体的颜色。
  • 最后,进入解析阶段:硬件将同一个像素内 4 个子采样的颜色做加权平均,输出最终像素颜色。

然而,MSAA的一个主要问题是与延迟渲染管线不兼容。在延迟渲染中,场景的几何信息(位置、法线、颜色等)会被写入G-Buffer,但光照计算发生在后期。MSAA需要对每个子采样点维护并最终解析这些几何属性,这极大地增加了G-Buffer的复杂度和带宽开销,使得传统MSAA难以高效地集成到延迟渲染框架中。

FXAA

SSAA、MSAA依靠提升采样频率实现抗锯齿,属于GPU光栅化阶段的硬件原生功能。这类硬件抗锯齿方案在现代延迟渲染逐渐成为主流后,因巨大性能开销而难。为此,以模糊为核心的后处理抗锯齿方案应运而生。

其中,快速近似抗锯齿(Fast Approximate Anti-Aliasing, FXAA)便是一种代表性方案。它完全脱离几何光栅化流程,在整帧画面渲染完成后,通过全屏着色器实现:

  • 边缘检测:将全屏 RGB 颜色转化为人类视觉更敏感的亮度(Luma)信息,通过计算像素间的亮度差值与对比度,快速识别出画面中的锯齿边缘。
  • 低通滤波:确定锯齿边缘的长度和方向后,利用平滑滤波器对该区域进行局部模糊混合(Blur Blending),以此在视觉上弱化阶梯状走样。

FXAA兼容性极强,可适配前向渲染、延迟渲染等所有管线,运行成本极低。但它也存在明显缺陷:算法会对边缘统一做模糊,容易造成画面整体发虚,丢失细线、纹理、文字等精细细节。

TAA

时域抗锯齿(Temporal Anti-Aliasing, TAA)是另一种重要的后处理抗锯齿方案。它利用前一帧和当前帧之间的像素信息进行抗锯齿处理,通过时间上的积累和混合来减少锯齿,并提高图像的质量和稳定性。在具体运行流程中,TAA 通过全屏着色器主要执行以下核心步骤:

  • 时域抖动(Jittering):顶点着色器在每帧计算投影矩阵时,控制相机在亚像素级别(通常小于 1 像素)进行微弱的随机晃动。连续数帧的累积,使得原本因单帧像素网格限制而漏掉的几何细节能交替被不同的像素捕捉到。
  • 时域回溯(Reprojection):由于场景中的相机和物体都在运动,Shader 需要利用管线输出的速度缓冲(Velocity Buffer / Motion Vectors),精确计算出当前像素在上一帧的屏幕绝对坐标,从而跨越时间去采样上一帧的颜色缓存(History Buffer)。
  • 历史混合(Blending):将当前帧捕获的动态像素与历史帧的像素进行高比例的线性加权混合(例如 10%当前 + 90%历史),实现像素级在时间轴上的平滑过度。
  • 邻域剪裁(Neighborhood Clamping):这是消除残影的关键技术。Shader 会检查历史颜色是否超出了当前像素周围3×3邻域的颜色区间(Bounding Box)。若超出,则判定该处的历史信息因遮挡改变而失效,强制将其裁剪丢弃,以此消除动态鬼影与残影(Ghosting)。

Visibility/Occlusion

可见性与遮挡判断是渲染管线中用于减少无效渲染、保证正确遮挡关系的核心环节。该过程由粗到细、从CPU到GPU分层进行,主要包括剔除和深度消隐等算法。

Culling

剔除(Culling)是指在渲染过程中,根据模型几何关系或朝向判断某些图元或物体不可见,从而直接将其丢弃,避免进入后续流程造成不必要的性能开销。Culling根据执行阶段和原理不同,可分为:

  • 视锥体剔除:发生在应用阶段,一般由CPU运行。属于对象级/集群级的粗粒度空间判定。CPU会检查整个网格体的包围盒(AABB/OBB)是否在相机视锥体六个平面内部,若完全不在,则直接将整个模型丢弃,在源头上免除向GPU发送无效DrawCall。
  • 背面剔除:发生在光栅化阶段,属于GPU固定管线电路。GPU通过检查三角形的法线方向,剔除背面朝向观察者的三角形,从而减小开销。

在OpenGL中,可以先使用函数glEnable(GL_CULL_FACE)开启面剔除功能。接着,使用函数glCullFace函数来实现。它需要传入一个参数,用于指定剔除面类,其可能的选项包括:

  • GL_BACK:只剔除背面。
  • GL_FRONT:只剔除正面。
  • GL_FRONT_AND_BACK:剔除背面和正面。

Clipping

裁剪(Clipping)与剔除不同,它不进行丢弃决策,而是进行几何修正。当图元(如三角形)部分位于视锥体外、部分位于视锥体内时,裁剪单元会计算图元与视锥体边界的交点,生成新的顶点,并将原图元分割成若干个完全位于视锥体内的小图元。

剔除和裁剪的区别

这确保了任何跨越边界的三角形都能被正确渲染,避免了因顶点位于视平面背后而导致的数学错误。简单来说,剔除是“全丢或全留”,而裁剪是“切掉外边,留下里边”。

Hidden Surface Removal Algorithms

消隐算法(Hidden Surface Removal Algorithms)用于解决多个物体在同一像素上重叠时的遮挡关系问题,即判断哪个片元最终应该显示在屏幕上。

画家算法:

受画家作画启发,按照片元距离观察者的远近,从最远处开始依次向帧缓冲中写入颜色。后绘制的近处片元会覆盖先前绘制的远处片元,从而自然形成正确的遮挡关系。

不过,由于它依赖图元级的宏观排序,面对空间穿插(两面交叉横切)以及循环遮挡(A挡B,B挡C,C挡A的闭环状态)的拓扑结构时,算法在数学上直接失效,渲染出的画面会发生严重的物理遮挡穿帮。

Z-Buffer算法

Z-Buffer(也称深度缓冲)是当前 GPU 最主流的消隐方案。它额外维护一张与帧缓冲尺寸相同的深度缓冲,为每个像素记录当前已渲染片元的最小深度值(即离观察者最近的距离)。当新的片元到达时,GPU 比较其深度值与缓冲中已有深度值:若新片元深度更小(更近),则覆盖颜色并更新深度缓冲;否则丢弃该片元。这一过程与物体的绘制顺序无关,且硬件原生支持,效率极高。

扫描线算法

扫描线算法是一种经典的图像空间消隐算法,在早期软渲染时代具有重要历史地位。与传统逐像素处理不同,该算法以屏幕上的水平扫描线为基本处理单元,自上而下逐行推进,一次性解算整条扫描线上所有多边形之间的相交与遮挡关系。

在数据结构层面,算法维护了边表(Edge Table, ET)和活动边表(Active Edge Table, AET)。当扫描线横切过多个三角形的边界时,算法将扫描线划分为若干个连续的X轴区间(Spans)。利用图形的时空连续性,在一个区间内遮挡关系保持恒定,因此算法只需在区间起点处比较一次Z值,即可确定整个区间应显示哪个三角形的颜色,随后按区间进行批量写操作。

尽管传统的硬件扫描线消隐已被淘汰,但其核心思想——沿扫描线方向利用几何连续性批量处理像素——在近年来得到了复兴。在虚幻引擎5的Nanite虚拟几何体系中,软光栅化器在Compute Shader中执行类似扫描线的区间消隐逻辑。通过将屏幕划分为2×2像素的像素块(Quad),Nanite在每个块内进行深度连续性测试,一次性消隐并合并微多边形三角形(像素级三角形),极大地降低了海量小三角形带来的过度绘制(Overdraw)问题。这可以看作扫描线思想在现代GPU Compute Shader中的一次成功演化。

Graphics Pipeline

OpenGL Pipeline Overview

  • 应用阶段:输入一堆三维空间中的点。
  • 几何阶段:依次进行MVP变换,并进行裁剪和屏幕映射,得到三角形顶点对应屏幕像素位置。
  • 光栅化:通过顶点插值,得到被三角形覆盖的所有像素位置。
  • 片元处理:进行着色计算,并依次进行透明度、模板和深度测试,最终得到显示在屏幕上的图像。

渲染管线

Coordinate Transformations

几何阶段中的顶点处理应该是渲染管线中较为复杂的部分,下面详细总结其流程:

  • Model Space:用于渲染的模型文件内的坐标,也即模型空间坐标。

  • World Space:依次对模型进行平移、旋转和缩放变换,得到世界空间坐标。其中,除坐标外,顶点的其他属性同样需要受该模型变换矩阵矩阵影响,比如:

oFragPos = (uModelMatrix * vec4(iVertPos, 1.0)).xyz;
oFragNormal = (uNormalMatrix * vec4(iVertNormal, 0.0)).xyz;
oFragTangent = (uModelMatrix * vec4(iVertTangent, 0.0)).xyz;
oFragUV = iVertUV;
  • View/Camera Space:将世界空间整体做平移、旋转变换,把坐标系原点对齐到相机位置,Z 轴朝向相机视线反方向,最终得到相机坐标系下的坐标。

  • Clip Space:将数据投影至距离相机n处的平面,得到裁剪空间下的坐标。其坐标转换公式如下所示。其中,P为投影矩阵,far和near分别为远和近裁剪面的距离,ratio为宽高比,$\theta$为视角。此时,除去w,便可得到归一化坐标(Normalized Device Coordinates)。随后,裁剪掉x、y或z任意一个大于1或小于-1的坐标。

$$ P(x,y,z,1)^{T}=(\frac{1}{ratio \cdot tan\frac{\theta}{2}}x, \frac{1}{tan\frac{\theta}{2}}y, \frac{z-near}{far-near}, -z)^{T} $$

  • Screen Space:该阶段由 视口变换(Viewport Transform) + 深度范围变换 共同完成,将 ([-1,1]) 区间的 NDC 坐标,线性映射为帧缓冲区上的像素坐标,也就是屏幕空间坐标。

以OpenGL为例,

Test and bledning

在完成片元着色计算后,紧接着会进行各种测试和混合操作,最终得到呈现在屏幕上的图片。这些测试按从前到后的顺序为:

  • 模板测试

  • 透明度测试:它其实和半透明渲染没什么关系,只是用到了alpha做判断,当alpha值小于某个阈值时丢弃该片元。

  • 深度测试:它用于比较片元的深度值与深度缓冲区中对应位置的深度值,并决定是否绘制该片元。深度测试可以确保物体的遮挡关系正确,避免后面的物体遮挡前面的物体。

Early-Z测试

一般来说,深度测试都发生在片元着色之后。为了减少进入片元着色的像素数量,一种提前进行深度测试的Early-Z技术被提出了。它位于光栅化阶段之后,像素处理阶段之前。

不过,Early-Z会带来透明测试的冲突,例如某个片元A虽然遮挡了另一个片元B,但A却是透明的,GPU应当渲染的是片元B,这就产生了矛盾。

混合

颜色混合

Shading

In computer graphics, shading is the process of applying a material to an object.

着色(Shading)是计算机图形学中用于模拟光照效果的过程,通过对物体表面的光照属性进行计算,为物体赋予逼真的外观。

Shading Frequency

根据着色的位置的频率,可将其划分为三类典型的着色方法:

  • 面着色(Flat Shading):一个平面计算一次,计算结果应用给该平面的每个点;
  • 顶点着色(Vertex Shading/Grouraud Shading):每个顶点都计算一次,然后面内点做插值;
  • 像素着色(Pixel Shading/Phong Shading):算出每个顶点法线,对法线进行插值得到面内每个像素的法线,再做着色计算。

Shaing_Frequency

上图为三种着色方式的渲染效果对比:

  • 对于面着色而言,物体表面呈现出极其强烈的低多边形块状感(Low-Poly / Faceted Look)。由于相邻三角面之间完全没有过渡,几何边缘的阶跃非常刺眼,几乎无法表现光滑的球体或生物皮肤。
  • 而在高洛德着色渲染图中,多面体的棱角感被大大削弱,表面整体变得相对平滑。然而,细看会发现它的内部光照过渡略显生硬(呈现出一种不自然的“倒三角线性淡化”痕迹),且高光亮点非常暗淡或直接丢失。
  • 在这三者中冯氏着色呈现出最好的效果,球体表面呈现出完美、丝滑的圆润质感。最关键的是,它精确地还原了一个凝聚、明亮且具有强烈方向感的镜面高光点(Specular Highlight),让物体的材质瞬间有了“反光和硬度”的真实物理质感。

在基于GPU的现代渲染管线中,着色逻辑的实现位置由着色频率决定:若采用高洛德着色,光照计算需要放在顶点着色器中完成。而目前主流游戏与商用 3A 项目,均统一使用冯氏着色,将法线贴图采样、光照运算、BRDF等核心逻辑放在片元着色器内执行。顶点着色器如今仅负责输出顶点世界坐标、原始法线等基础数据,交由光栅化阶段完成插值即可。

Illumination Model

光照模型(Illumination Model),有时也被称为着色模型,是用于计算任意给定位置的光线强度的数学模型。

图形学中的光照模型在历史演进中主要分为两大阵营:经验/半经验光照模型与基于物理的光照模型。前者(如 Lambert, Phong, Blinn-Phong)侧重于利用简化的数学公式在极低能耗下凑出“看起来正确”的视觉视觉经验;后者(如 Cook-Torrance)则严格遵循热力学能量守恒与微表面光学理论。

下面,本章将依次聚焦介绍经验模型中最核心的Lambert漫反射模型以及Phong反射模型,而关于Cook-Torrance BRDF的介绍则统一归纳于后续的PBR章节中。

Lambert’s Cosine Law

Lambert模型用于描述粗糙材料的表面。它是一种理想模型,只考虑漫反射,即完全漫反射。这些反射光,在不同角度的辐射强度会依余弦公式变化,角度越大强度越弱。

Lambert余弦定理

Phong/Blinn-Phong Model

在Phong/Blinn-Phong光照模型中,它将任意位置发出的光线分成漫反射(Diffuse/Lambertian)、镜面(Specular)和环境(Ambient)三个分量。

假设光源强度为I,带计算位置和光源距离为r,则该位置入射光线强度$L_{in}=I/r^2$。此时,出射光线强度为三个分量之和,即$L_{out}=L_d+L_s+L_a$。

Diffuse/Lambertian

其中,漫反射强度由入射光线强度$L_{in}$、材料漫反射系数$K_d$、法向量方向$\vec{n}$和反射光线方向$\vec{out}$一起决定,即$L_d=k_d*L_{in}max(0, \vec{n}\vec{out})$。

Specular

类似的,高光强度(或镜面反射强度)也由入射光线强度$L_{in}$、材料镜面反射系数$K_s$、材料的高光反光度shininess、法向量方向$\vec{n}$和反射光线方向$\vec{out}$一起决定。其中,高光反光度描述的是材料对镜面反光散射的能力。一个物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。在下面的图片里,你会看到不同反光度的视觉效果影响:

高光反光度

尽管Phong模型和Blinn-Phong模型计算这一分量所需参数类似,但计算方法却不同,这也是二者的区别。其中,前者的计算方式如下$L_s=k_sL_{in}pow(max(0, \vec{in}\vec{out}), shininess)$。而后者则是引入了一个名为$\vec{h}$的半程向量来改进前者计算中出现的高光不连续问题,其示意图如下。具体来说,$\vec{h}=\frac{\vec{in}+\vec{out}}{||\vec{in}+\vec{out}||}$,而高光强度$L_s=k_sL_{in}pow(max(0, \vec{n}\vec{h}), shininess)$。

半程向量

Ambient

至于环境光的计算方式就非常简单了。它表示为光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色。

Render

Opuque Froward Rendering

Deferred Rendering

Transparent Rendering

Texture

Texture Mapping

纹理贴图(Texture Mapping)是计算机图形学中将二维图像映射到三维物体表面的技术。通过定义纹理坐标(UV 坐标,范围通常为 [0,1]),将图像的每个像素对应到物体表面的顶点,最终在渲染时填充物体表面的颜色。

纹理环绕

  • GL_REPEAT:重复纹理(默认),坐标超出部分循环平铺。
  • GL_MIRRORED_REPEAT:镜像重复,超出部分镜像翻转后平铺。
  • GL_CLAMP_TO_EDGE:边缘拉伸,超出部分拉伸到边缘。

纹理过滤

当纹理像素(Texel)与屏幕像素(Pixel)大小不匹配时,通过插值算法优化显示效果,分为放大过滤(纹理太小)和缩小过滤(纹理太大)。

Mipmap

多级渐远纹理(Mipmap)是一种在计算机图形学中用于优化纹理渲染的技术,由一系列不同分辨率的纹理图像组成,这些图像是原始纹理的逐级降采样版本。具体来说,它的功能包括:

  • 提高渲染性能:在渲染场景时,远处的物体通常不需要使用高分辨率的纹理,因为它们在屏幕上只占据很少的像素。使用 Mipmap 可以根据物体与相机的距离选择合适分辨率的纹理进行渲染,减少纹理过滤时所需的计算量,从而提高渲染速度。
  • 减少纹理闪烁:当物体远离相机时,如果直接使用高分辨率纹理,由于纹理采样的不连续性,会导致纹理出现闪烁现象。Mipmap 通过提供不同分辨率的纹理,使得纹理在不同距离下的过渡更加平滑,减少了闪烁问题。

Mipmap 由多个层级(Level)组成,每个层级的纹理分辨率是上一层级的一半。层级 0 是原始纹理,层级 1 的纹理分辨率是层级 0 的一半,层级 2 的纹理分辨率是层级 1 的一半,依此类推。

在实际开发中,有时并不需要直接指定使用哪个 Mipmap 层级,而是通过纹理过滤器(Texture Filter)来自动选择合适的 Mipmap 层级。具体而言,由于GPU一次最少会渲染四个相邻的片元,它可以同时获得这四个片元的uv坐标,从而根据uv坐标的变换情况来选择合适的Mipmap层级。比如,邻近的片元uv坐标中u的差值为0.05,而此时层级为1的纹理分辨率为1024×1024,那么这两个片元选取的纹理像素点位置将相差0.05*1024=51.2,这显然跨度过大,很可能会造成锯齿效应,因此需要选择层级更高的纹理。

Light Mapping

光照贴图(Light Mapping)是一种典型的时空复用、预计算(Baking)抗静态光照走样的技术。它的核心思想是:将场景中静态光源对静态几何体的复杂光照(包括直接光、间接阴影、全局光照 GI)在离线阶段提前烘焙到一张 2D 纹理上,运行时直接采样并与基础色相乘。

Skybox Mapping

天空盒贴图(Skybox Mapping)是一种用于高效模拟无限远无限大无边界环境背景的渲染技术。其核心物理思想是:假想场景被一个无限大的立方体(或球体)包裹,通过阻断深度写入,使得无论相机如何物理平移,该背景在视差上永远处于无穷远处。

为了确保天空盒永远不遮挡场景中的任何真实几何体,在顶点着色器中,通常将变换后的裁剪空间坐标W分量直接赋给Z分量(即 gl_Position.z = gl_Position.w;)。经过透视除法后,Z/W = 1.0,强行将其压入不透明物体 Z-Buffer 的最远端(后裁剪面)。

Normal Mapping

法线贴图(Normal Mapping)是一种在像素级伪造表面细节的经验着色技术。它的核心物理思想是:保持低模(Low-Poly)的稀疏几何顶点不变,通过高频改变每个片元的法线方向,诱导光照方程(如 PBR)计算出具有凹凸感的高光与阴影变化。

法线贴图的解码/编码

由于图形纹理规范中,像素分量范围固定为[0,1],但三维方向向量的合法范围是[-1,1],因此法线贴图在采样时必须进行值域还原,将其映射至[-1,1]的正确范围中。

// Decode normal from GBuffer/Texture storage format[0.0, 1.0] to shader format[-1, 1]
vec3 N_decode(vec3 N) {
    return N * 2.0 - 1.0;
}

类似的,也可以定义一个编码函数,将[-1,1]的法线映射至[0,1]的范围中。

// Encode normal from shader format[-1, 1] to GBuffer/Texture storage format[0.0, 1.0]
vec3 N_encode(vec3 N) {
    return (N + 1.0) / 2.0;
}

切线空间

纹理映射本质上是一个从三维空间 $(x, y, z)$ 到二维空间 $(u, v)$ 的函数映射。为了让法线贴图能够跟随模型的任意旋转和骨骼动画而不失效,贴图中存储的并非世界空间下的绝对法线,而是相对于表面局部的切线空间法线。

对于三维空间中的任意参数化曲面 $P(u, v)$,其切线空间由三个基向量构成:

  • 切向量(Tangent):P对u的偏导数 $\frac{\partial P}{\partial u}$ ,它表示在 UV 贴图上沿 U 轴(水平向右)移动极小单位时,三维世界中对应物理点 P 的移动方向与速率。
  • 副切向量(Bitangent):P对v的偏导数 $\frac{\partial P}{\partial v}$ ,它表示在 UV 贴图上沿 V 轴(垂直向下)移动极小单位时,三维世界中对应物理点 P 的移动方向与速率。
  • 面法线(Normal):原始表面的几何法线,通常由 T 和 B 的叉乘得出。

在实际的离散网格中,切线空间通常由三角形图元的顶点信息确定。已知三角形三个顶点的空间坐标为 $(P_1, P_2, P_3)$,对应的纹理 UV 坐标为 $((u_1,v_1),(u_2,v_2),(u_3,v_3))$。此时,我们可以通过三维空间中的边向量 $E$ 与二维纹理坐标的差值 $\Delta u, \Delta v$,建立起如下线性组合关系:

$$ \begin{cases} E_1 = \Delta u_1 \cdot T + \Delta v_1 \cdot B \ E_2 = \Delta u_2 \cdot T + \Delta v_2 \cdot B \end{cases} $$

$$ \begin{bmatrix} E_{1x} & E_{1y} & E_{1z} \ E_{2x} & E_{2y} & E_{2z} \end{bmatrix} = \begin{bmatrix} \Delta u_1 & \Delta v_1 \ \Delta u_2 & \Delta v_2 \end{bmatrix} \begin{bmatrix} T_x & T_y & T_z \ B_x & B_y & B_z \end{bmatrix} $$

进一步,可以计算出切线空间的基向量T和B,即

$$ T = \frac{1}{\Delta u_1 \Delta v_2 - \Delta v_1 \Delta u_2} (\Delta v_2 \cdot E_1 - \Delta v_1 \cdot E_2) $$

$$ B = \frac{1}{\Delta u_1 \Delta v_2 - \Delta v_1 \Delta u_2} (-\Delta u_2 \cdot E_1 + \Delta u_1 \cdot E_2) $$

此外,在实际渲染时,法线贴图中采样出来的法线TN,是切线空间中的法线。因此,其采样出来切线空间法线还需要进行转换,才能得到世界空间法线,进而参与后续光照计算。具体而言,该转换方法如下:

// Transform tangent-space normal from normal map to world space
vec3 N_toWorld(vec3 N, vec3 T, vec3 TN) {
    vec3 B = normalize(cross(N, T));

    mat3 TBN = mat3(T, B, N);
    return normalize(TBN * TN);
}

Displacement Mapping

位移/置换贴图(Displacement Mapping)是一种真正改变模型几何拓扑结构的微观细节渲染技术。不同于法线贴图在像素着色器里的“视觉欺骗”,位移贴图通过一张标量高度图(Height Map),在管线的前端真正物理性地抬升或压低顶点。

PBR

PBR 白皮书

基于物理的渲染(Physicallly Based Rendering, PBR)是计算机图形学中一种主流的着色方法。相比于,传统的经验模型,如Phong式光照模型,PBR方法基于物理学原理和光学特性,更加真实和准确。具体来说,PBR理论包括:

  • 基于物理的材质(Material);
  • 基于物理的光照(Lighting);
  • 基于物理适配的摄像机(Camera)。

而该理论的三大基础则分别是辐射度量学、微平面模型和反射方程。其中,前两者的出现基本上只是为了证明:PBR里的数学公式是符合物理的;而后者才是PBR中的核心。换句话说说,反射方程的推导、简化和应用构成了PBR理论。

最后,基于物理的渲染相比传统方法有以下优势:

  • 直观参数(最重要的一点):PBR使用的参数(如粗糙度、金属度等)与实际物理属性紧密相关,这使得艺术家和开发者能够更直观地理解和调整材质参数以获得预期的视觉效果。
  • 一致的材质表现:PBR模型提供了一种标准化的方法来表示材质属性,这意味着同样的材质设置在不同的光照条件下能保持一致的外观。这种一致性对于跨平台开发和维护大型项目尤其重要。
  • 逼真的视觉效果:由于PBR基于真实的物理光照模型,它能够更好地模拟光与材质的交互,提供更逼真的阴影、高光和反射效果。这种逼真效果能够显著提升视觉细节和沉浸感。

Radiometry

辐射度量学(Radiometry)是究各种电磁辐射强弱的学科。它定义了一系列物理变量,用来从不同角度描述电磁辐射强度,包括:

  • 辐射能(Radiant Energy)指电磁波中电场能量和磁场能量的总和,也叫电磁波的能量,常用符号Q表示。
  • 辐射通量/功率(Radiant Flux/Power)指单位时间内产生的、反射的、接收的能量,常用符号$\Phi$表示。
  • 辐射强度(Radiant Intensity)指单位立体角上,产生的、反射的、接收的辐射通量,常用符号I表示。
  • 辐照度(Irradiance)指单位投影面积上接收到的辐射通量,常用符号E表示。
  • 辐射率(Radiance)指单位投影面积收到/发出的单位立体角上的辐射通量,常用符号L表示。

其中,和计算机图形学相关的两个物理量其实是辐照度(Irradiance)和辐射率(Radiance)。它们的关系如下:

Irradiance和Radiance的关系

Optics and Microfacet Model

微平面模型

微表面模型是计算机图形学中用于模拟真实表面的微观结构的一种技术。它将现实世界中任意材料视作多个微小凹凸结构组成,并使用统计学原理去描述微观结构的几何形状、法线分布和表面粗糙度等参数从而计算光线的反射和折射。

为了更好的理解微平面模型,下面将介绍一些光学基础知识。

折射和折射率

而光在传播到两种不同介质交界处时,原始光波和新的光波的相速度(Phase Velocity)的比率定义了介质的光学性质,就是折射率(Index Of Refraction,IOR),由字母n表示。

除了代表光的相速度的实部n之外,还用希腊字母κ(kappa)表示介质将光能转为为其他形式能量的吸收性。n和κ通常都随波长而变化,两者组合成复数n +iκ,称为复折射率(complex index of refraction)。

也就是说,折射率IOR是一个复数(complex number),其分为实部和虚部两部分:

  • 折射率的实部(real part)度量了物质如何影响光速,即相对于光在真空中传播速度减慢的度量。
  • 折射率的虚部(imaginary part)确定了光在传播时是否被吸收,转换成其他形式的能量,通常是热能。非吸收性介质虚部为零。

光与平面交互

漫反射

在了解了折射和折射率之后,我们可以进一步总结得出光与平面交互方法,包括:

  • 反射(Reflection)。光线在两种介质交界处的直接反射即镜面反射(Specular)。金属的镜面反射颜色为三通道的彩色,而非金属的镜面反射颜色为单通道的单色。
  • 折射(Refraction)。 从表面折射入介质的光,会发生吸收和散射,而介质的整体外观由其散射和吸收特性的组合决定,其中:
    • 散射(Scattering)。 折射率的快速变化引起散射,光的方向会改变(分裂成多个方向),但是光的总量或光谱分布不会改变。散射最终被视作的类型与观察尺度有关:
      • 次表面散射(Subsurface Scattering)。观察像素小于散射距离,散射被视作次表面散射。
      • 漫反射(Diffuse)。观察像素大于散射距离,散射被视作漫反射。
      • 透射(Transmission)。入射光经过折射穿过物体后的出射现象。透射为次表面散射的特例。
    • 吸收(Absorption)。 具有复折射率的物质区域会引起吸收,具体原理是光波频率与该材质原子中的电子振动的频率相匹配。复折射率(complex number)的虚部(imaginary part)确定了光在传播时是否被吸收(转换成其他形式的能量)。发生吸收的介质的光量会随传播的距离而减小(如果吸收优先发生于某些波长,则可能也会改变光的颜色),而光的方向不会因为吸收而改变。任何颜色色调通常都是由吸收的波长相关性引起的。

因此,从宏观上看,光在介质表面扣除吸收部分,即剩下:(镜面)反射、漫反射和透射三部分。

菲涅尔效应和方程

BRDF and BxDF

BRDF(Bidirectional Reflectance Distribution Function),译作双向反射分布函数,是一个用来描述物体表面如何反射光线的方程。简单来说,它就是微表面模型的数学表达。

具体的,BRDF表示了当给定一条入射光的时候,某一条特定的出射光线的性质是怎么样的。它的精确定义是出射光辐射率(Radiance)的微分和入射光辐照度(Irradiance)的微分之比,即$f(l,v)=\frac{dL_o(v)}{dE(l)}$。其中,v和l分别为出射和入射光线方向。

其实,采用ωi和ωo作为输入参数的Blinn-Phong光照模型也可以被当作是一个BRDF。不过由于Blinn-Phong模型并没有遵循能量守恒定律,因此它不被认为是基于物理的渲染。至于,目前广泛用于基于物理渲染和实时渲染的BRDF模型则是一种被称为Cook-Torrance的BRDF模型。我们将在后两节详细介绍它。

除了BRDF之外,图形学中还有一些其他的双向分布函数,比如BRDF、BTDF、BSDF、BSSRDF等,这些一起被称作BxDF。

BxDF

在上述这些BxDF中,BRDF最为简单,也最为常用。因为游戏和电影中的大多数物体都是不透明的,用BRDF就完全足够。而BSDF、BTDF、BSSRDF往往更多用于半透明材质和次表面散射材质。

Rendering Equation

渲染方程是一个描述光能在场景中流转的方程,它基于能量守恒定律,在理论上给出了一个完美的光能求解结果。在一个特定的位置和方向,出射光Lo是发射光Le与反射光之和。而反射光本身则是各个方向的入射光Li之和乘以表面反射率及入射角。

渲染方程

其中,积分中的$f_r(x, \vec{w'}, \vec{w})$正是BRDF,即出射光辐射率的微分和入射光辐照度的微分之比。

Cook-Torrance BRDF

learnopengl中关于Cook-Torrance BRDF的介绍

Cook-Torrance BRDF是一种目前最流行一种BRDF反射模型,它包含漫反射和镜面反射两个部分:$f_r=k_df_{lambert}+k_sf_{cook-torrance}$。其中,$f_{lambert}=\frac\rho\pi$,而$f_{cook-torrance}=\frac{DFG}{4(\vec{w_o}\vec{n})(\vec{w_i}\vec{n})}$。

此外,上述表达式其实只考虑直接光,即它是一个局部光照模型。若在有限光源的情况下,计算Cook-Torrance BRDF的间接光/环境光。其方法和传统模型类似,可通过一个名为环境光遮蔽贴图(Ambient Occlusion, AO)的方式获取每个位置的环境光强度,比如:

// albedo就是片元位置的baseColor
vec3 ambient = vec3(0.03) * albedo * ao;

至于,使用IBL技术表示的全局光照则需要使用更复杂的手段解决,比如预计算PRT等。

Diffuse Part

Cook-Torrance BRDF中,漫反射系数$f_{lambert}=\frac\rho\pi$的推导基于一个前提:入射光是均匀且遍布整个半球方向。那么出射光则可表示为:

$$ \begin{align} L_o(w_o) &= \int_{H^2}{f_rL_i(w_i)cos\theta}dw_i\ &= f_rL_i\int_{H^2}{cos\theta}dw_i\ &= f_rL_i\pi\ f_r &= \frac\rho\pi \end{align} $$

其中,$\rho$表示的是发生反射位置的颜色。

Specular Part

Cook-Torrance BRDF的镜面反射部分包含法线分布函数(Normal Distribution Function)、菲涅尔方程(Fresnel Equation)、几何函数(Geometry Function)以及用于矫正的分母 。

其中,分母作为微观几何的局部空间和整个宏观表面的局部空间之间变换的微平面量的校正,有两点需要注意:

  • 对于分母中的点积,仅仅避免负值是不够的 - 也必须避免零值。通常通过在常规的clamp或绝对值操作之后添加非常小的正值来完成。
  • Microfacet Cook-Torrance BRDF是实践中使用最广泛的模型,实际上也是人们可以想到的最简单的微平面模型。它仅对几何光学系统中的单层微表面上的单个散射进行建模,没有考虑多次散射,分层材质,以及衍射。Microfacet模型,实际上还有很长的路要走。

菲涅尔项

其中,菲涅尔方程描述的是被反射的光线对比光线被折射的部分所占的比率,这个比率会随着观察角度变化而变化。比如,当我们站在湖边低头看脚下的湖水,会发现水是透明的,反射不会特别强烈;而如果我们看远处的湖面时,会发现水并不透明,而且反射非常强烈。

目前,菲涅尔项一般都采用Schlick近似。因为计算成本低廉,而且精度足。

法线分布函数

法线分布函数用于描述给定法线$\vec{n}$、半程向量$\vec{h}$和粗糙度$\alpha$时,法线方向和半程向量方向一致的比例。它满足一些列基本性质,包括:

  • 微平面法线密度始终为非负值;
  • 微表面的总面积始终不小于宏观表面总面积;
  • 任何方向上微观表面投影面积始终与宏观表面投影面积相同;
  • 若观察方向为法线方向,则其积分可以归一化。

一个最常见的法线分布函数其实就是Blinn-Phong中关于镜面反射引入的shiness参数,其具体表达如下:$NDF(\vec{n}, \vec{h}, shiness)=pow(max(0, \vec{n}*\vec{h}), shininess)$。当粗糙度越小(或反光度越大),镜面反射光线越集中,物体表面越有光泽;反之,镜面反射越散射,物体表面越缺乏光泽。

法线分布函数

除此之外,还有比较经典的CGX法线分布函数$NDF(\vec{n}, \vec{h}, \alpha)=\frac{\alpha^2}{\pi((\vec{n}*\vec{h})^2(\alpha^2-1)+1)^2}$。将它在GLSL实现可得:

float D_GGX_TR(vec3 N, vec3 H, float a)
{
  float a2     = a*a;
  float NdotH  = max(dot(N, H), 0.0);
  float NdotH2 = NdotH*NdotH;

  float nom    = a2;
  float denom  = (NdotH2 * (a2 - 1.0) + 1.0);
  denom        = PI * denom * denom;

  return nom / denom;
}

注意:NDF其实和传统的Normal Mapping有一定相似。只不过,法线贴图只为每个像素提供。而法线分布函数使用法线贴图和粗糙度贴图一起,提供了亚像素精度的细节。且传统法线贴图没有经过归一化处理,不满足能量守恒,容易出现失真。

几何函数

几何函数用于描述给定法线$\vec{n}$、视线$\vec{v}$和粗糙度$\alpha$时,反射光线被遮蔽的比率,它是实现PBR能量守恒的核心。比如:$G(\vec{n}, \vec{v}, k)=\frac{\vec{n}\vec{v}}{(\vec{n}\vec{v})(1-k)+k}$。

其中,k是针对粗糙度的重映射(Remapping),取决于我们要用的是针对直接光照还是针对IBL光照的几何函数。

$$ k_{direct}=\frac{(\alpha+1)^2}{8}\ k_{ibl}=\frac{\alpha^2}{2} $$

Disney Principled BRDF

迪士尼动画工作室在SIGGRAPH 2012上著名的talk《Physically-based shading at Disney》中提出了迪士尼原则的BRDF(Disney Principled BRDF),奠定了后续游戏行业和电影行业PBR的方向和标准。它的核心贡献其实是避免了过去PBR中复杂晦涩物理参数,而引入了一系列直观的参数,包括:用于表示表示材料吸收光的金属度(metallic)和用于表示材料镜面反射集中度的粗糙度(roughness)等。

漫反射/镜面反射的比例

在Cook-Torrance BRDF中,$k_d$和$k_s$参数分别表示了漫反射和镜面反射的比例。将它们用菲涅尔项和金属度(能够反映折射部分,也就是被金属吸收的部分)表示如下:

$$ k_d=(1-F)*(1-metallic)\ k_s=F $$

换句话说,Cook-Torrance BRDF可改写为

$$ \begin{align} f_r =& k_df_{lambert}+k_sf_{cook-torrance}\ =& (1-F)(1-metallic)\frac\rho\pi+F\frac{DG}{4(\vec{w_o}\vec{n})(\vec{w_i}*\vec{n})}
\end{align} $$

PBR常见材料/纹理

PBR纹理列表

对美工来说,PBR一般右两种材质工作流:金属/粗糙度工作流(Metal/Roughness)和镜面反射/光泽度工作流(Specular/Glossiness)。它们的目的都是进行PBR渲染,获取尽可能真实的渲染效果,即一体两面。

其中,金属度/粗错度工作流需要三张贴图来表示PBR需要的参数:

  • albedo/baseColor:每个Texel表示对应位置的表面颜色,且不包含任何光影信息。
  • metallic:每个Texel表示对应位置金属成分的占比,1表示100%的纯金属,0表示100%的非金属。
  • roughness:每个Texel表示对应位置粗糙程度。越粗糙,该位置镜面反射光线越分散,光泽程度越低。

类似的,镜面反射/光泽度工作流也需要三张贴图表示PBR所需参数:

  • diffuse/albedo:每个Texel表示对应位置的表面颜色。
  • specular:每个Texel表示对应位置的$F_0$,即入射角为0°时的菲涅尔系数。
  • glossiness:每个Texel表示对应位置粗糙程度。

Linear Space and Tone Mapping

learnopengl中关于线性空间和HDR的介绍

线性空间

在渲染中,颜色值的计算总是发生在线性空间中的。因为,线性空间中的颜色计算才是正确的。然而人眼对亮度的感知是非线性的,所以为了更好的利用表示颜色带宽,所以引入了Gama矫正。因此,在渲染时,也需要将输出颜色给进行Gama矫正。

色调调整

一般来说,当存储在帧缓冲(Framebuffer)中时,亮度和颜色的值是默认被限制在0.0到1.0之间的。换句话说,显示器只能显示值为0.0到1.0间的颜色。然而,光照方程中却没有这个限制。当片段颜色超过1.0时,被称作HDR(High Dynamic Range, 高动态范围)。与之相对的,处于0.0到1.0的颜色则被称为标准动态范围(LDR,Low Dynamic Range)。

在实时渲染中,使用HDR记录颜色信息是很重要的,因为它允许片段保留更多的细节,同时还让我们能够根据光源的真实强度指定它的强度。不过,由于显示器只能显示LDR范围的颜色,将计算得到的HDR结果转换为LDR颜色就是一个必要过程,即色调映射。

色调映射(Tone Mapping)是一个损失很小的转换浮点颜色值至我们所需的LDR[0.0, 1.0]范围内的过程。最简单的色调映射算法是Reinhard色调映射,它涉及到分散整个HDR颜色值到LDR颜色值上,所有的值都有对应。比如:

void main()
{             
  const float gamma = 2.2;
  vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;

  // Reinhard色调映射
  vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
  // Gamma校正
  mapped = pow(mapped, vec3(1.0 / gamma));

  color = vec4(mapped, 1.0);
}   

PBR Implementation

learnopengl中关于使用opengl实现一个PBR着色器

PBR GI——IBL

learnopengl中漫反射IBL learnopengl中镜面反射IBL

Ray Tracing

光线追踪 vs 路径追踪

一般来说光线追踪是指一种解决渲染问题的思路,而路径追踪是光线追踪这种思路下的具体的算法。除了路径追踪外,还包括:

  • 光线投射(Ray Casting/Whitted-style Ray Tracing):同样向场景发射光线,但只做一次交点计算,不考虑光线的反射和折射。接着,通过交点和阴影连线,计算光照效果。
  • 光子映射(Photon Mapping):首先从光源发射大量光子并追踪其在场景中的传播(记录位置、能量等信息),接着从摄像机发射光线,通过查询光子数据计算场景光照,同时模拟直接光照、间接光照及焦散等复杂光影效果。

Collision detection

记光线起点为 $\mathbf{O}$,方向为单位向量 $\mathbf{D}$,则射线上任意点可表示为:
$$ \mathbf{R}(t) = \mathbf{O} + t\mathbf{D} \quad (t \geq 0) $$

三角形

给定三角形的三个顶点 $\mathbf{V_0}, \mathbf{V_1}, \mathbf{V_2}$,那么三角形确定的平面法向量 $\mathbf{N}$ 可通过叉乘计算:

$$ \mathbf{N} = (\mathbf{V_1} - \mathbf{V_0}) \times (\mathbf{V_2} - \mathbf{V_0}) $$

进一步,可确定此平面方程为:

$$ (\mathbf{P} - \mathbf{V_0}) \cdot \mathbf{N} = 0 $$

将射线方程代入平面方程:

$$ (\mathbf{O} + t\mathbf{D} - \mathbf{V_0}) \cdot \mathbf{N} = 0 $$

解得:

$$ t = \frac{(\mathbf{V_0} - \mathbf{O}) \cdot \mathbf{N}}{\mathbf{D} \cdot \mathbf{N}} $$

  • 若分母 $\mathbf{D} \cdot \mathbf{N} \approx 0$,射线与平面平行(无交点);
  • 若 $t < 0$,交点在射线反方向。

此时,只需判断该交点是否在三角形内部,即可确认射线与三角形是否发生碰撞。

具体来说,可以使用重心法确认,即将交点表示为三个顶点的线性组合:

记 $$ \mathbf{e_1} = \mathbf{V_1} - \mathbf{V_0}, \quad \mathbf{e_2} = \mathbf{V_2} - \mathbf{V_0}, \quad \mathbf{OP} = \mathbf{P} - \mathbf{V_0} $$

那么$\mathbf{OP}$可表示为 $\mathbf{e_1}$ 和 $\mathbf{e_2}$ 的线性组合:
$$ \mathbf{OP} = u\mathbf{e_1} + v\mathbf{e_2} $$

求出$u$和$v$,若 $u \geq 0, v \geq 0, u + v \leq 1$,则交点在三角形内部。

AABB

Scene Management

Octree

八叉树(Octree)是三维空间划分的数据结构之一,它用于加速空间查询。简单来说,八叉树的空间划分方式是,把一个立方体分割为八个小立法体,然后递归地分割小立方体。

octree

BVH:

包围体积层次结构(Bounding Volume Hierarchy, BVH)是一种用于加速射线追踪的数据结构。BVH的基本思想是,将场景中的物体划分为一个个包围盒,然后递归地将这些包围盒划分为更小的包围盒,直到达到某个终止条件。

SAH:

表面积启发算法(Surface Area Heuristics, SAH)通过改变包围盒的划分位置从而最小化光线遍历代价的一种优化方法。其具体实现往往是定义一个代价函数,然后通过枚举所有可能的划分位置,找到最小代价的划分位置。其中,代价函数一般包括两部分:遍历代价和划分代价。最常见的代价函数为,扫描线穿过包围盒的代价为1,包围盒划分代价为0.125。

References