Post

图形学八股

游戏客户端面试可能会涉及的计算机图形学八股

图形学八股

数字图像处理

伽马矫正

  • 来源1:显示设备与输入电压呈非线性关系,一般来说是满足$l=u^{2.2}$这样的幂次关系
  • 来源2:人眼对暗部颜色比较敏感,因此我们在存储图像时,应该尽可能多的存储暗色,即使用尽可能大的范围来存储暗色,尽可能小的数据范围来存储亮色,由此在存储颜色时会通过$l=u^{0.45}$进行存储

主要目的是补偿显示设备与人眼之间的差异,,即会对输入的信号做幂次运算,具体为,一般来说显示设备与输入电压并非线性关系,而原始图像却是线性的,为了抵消显示设备的非线性响应,由此引入伽马矫正 \(v_{out}=v_{in}^\gamma\) 因此要想保证其输出的值与原始视觉效果一致,要对存储的图像进行编码,即通过如下的方式 \(v_out=v_{in}^{\frac{1}{\gamma}}\)

参考显示与图像领域中的Gamma值来源与Gamma校正

信号处理

低差异序列(low discrepancy sequence)

参考低差异序列

图形学渲染管线

渲染管线的整个过程如下

  • 顶点数据的输入:主要包括顶点坐标、纹理坐标、顶点法线、顶点颜色信息,图元的组成则由索引信息来构成,比如对于三角形图元,就由3个索引指向顶点数组中的三个顶点构成一个三角形

  • 顶点着色器:用于对顶点坐标进行坐标变换,对输入的顶点坐标进行MVP变换

  • 曲面细分(可选阶段):利用镶嵌化处理技术对三角形面进行细分,借助此技术可以实现LOD,使得离摄像机近的物体能够有更丰富的细节

  • 几何着色器(可选阶段):将输入的点、线扩展成多边形(三角形等)

  • 图元组装:将输入的顶点组转成指定的图元,此阶段会进行裁剪(clipping)和背面剔除(culling),以减少进入光栅化阶段的图元数量,加速渲染过程。

  • 透视除法和视口变换:用于进行屏幕映射,将顶点映射到屏幕空间。透视除法就是将齐次坐标中的$\omega$转化为1的过程,该过程将经过MVP变换到裁剪空间的顶点转换到NDC(Normalized Device Coordinates),即$[-1,1]^3$;视口变换则将原本处于裁剪空间的坐标转化到屏幕坐标,视口变换(Viewport Transformation)矩阵如下 $$ \left[ \begin{array}{ll} &\frac{width}{2} & 0 & 0 & \frac{width}{2}
    &0 & \frac{height}{2} & 0 & \frac{height}{2}
    &0 &0 &1 & 0
    &0 &0 &0 &1

    \end{array} \right] $$

  • 光栅化:将连续的物体通过离散化为屏幕的像素,主要包括三角形设置和三角形遍历,从而确定图元所覆盖的片段,并利用插值(重心坐标插值)得到所覆盖片段的属性,然后送到片段着色器进行颜色计算,片段和像素是两个概念,最终显示在屏幕上的是像素,而片段只是像素的候选,只有通过测试的片段才会被保留下来,最终显示在屏幕上

  • 片段着色器(fragment shader):计算每个片段的颜色,片段可以被理解为候选像素,即其并非最终显示到屏幕上的颜色,而是需要进行后续测试的候选像素,通过测试阶段的所有测试后即可最终显示到屏幕上

  • 测试(test)、混合(blend)、像素着色:裁剪测试、Alpha测试、模板测试、深度测试,没有经过测试的片段会在此阶段被舍弃;通过测试的片段进入混合阶段,Alpha混合可以根据片段的alpha(不透明度),alpha=0表示完全透明,alpha=1表示完全不透明。通过测试的片段会被写入到像素中,呈现在屏幕上。

在RTR4中,其将渲染管线分为了应用阶段、几何处理阶段、光栅化阶段、逐像素处理阶段

  • 应用阶段:该阶段在软件层面上执行一系列工作,如空间加速算法、视锥剔除(以对象为粒度进行剔除,将不在视锥中的物体剔除,视锥由FoV,Aspect Ratio确定)、碰撞检测、动画物理模拟
  • 几何处理阶段:顶点着色器,裁剪,屏幕映射
  • 光栅化阶段:三角形设置和三角形遍历
  • 逐像素处理阶段:像素着色和测试混合(test and blend)

各种测试

深度测试

在光栅化之后,与像素着色一起进行,在写入像素值时,会记录像素的深度(z-buffer),当某个片段的深度值小于z-buffer中记录的深度值时,该片段就会被舍弃,即对于映射到同一屏幕坐标的片段,只有离相机更近的片段会保留,其他的片段则被舍弃。若通过了深度测试,则会更新深度缓冲中的深度值。传统的深度测试流程大致如下

  1. 顶点着色器:处理顶点变换
  2. 光栅化:将图元转化为离散的片段(像素候选)
  3. 片段着色器:计算片段的颜色
  4. 深度测试:比较片段深度值与深度缓冲的大小,决定保留或舍弃片段
  5. 写入帧缓冲:通过测试的片段被写入帧缓冲,显示在屏幕上

对于那些要在深度测试被舍弃的片段,其使用片段着色器计算着色结果就会显得有些冗余,特别是当片段着色器的计算量较大时。因此,early-z就被提出来用于优化这一点,其在光栅化之后,片段着色器之前进行深度测试,从而减少进入片段着色器的片段,提高渲染效率。但是early-z需要保证在片段着色器阶段不会改变片段的深度值,否则early-z的筛选结果就是不正确的,就不能使用early-z。

对于透明物体的渲染,启用early-z也会出现问题,比如物体A的深度值更大,但是物体 A是透明物体,启用early-z之后,位于early-z之后的不透明物体B的片段就被舍弃,就不会渲染在屏幕上,导致错误,也就是Early-z会影响Alpha Test导致透明物体的渲染出现问题。通过PrePass+Early-z结合的技术可以解决该问题

  • 首先通过PreDepthPass写入深度,并在此阶段通过Alpha Test把Opacity Mask的深度剔除掉,使得位于透明物体后的不透明物体可以显示出来
  • 然后按照正常的渲染流程,并结合early-z技术将片段筛选出来,并通过片段着色器计算片段的颜色,参考PreZ And EarlyZ In UE4

Alpha测试

根据片段的不透明度(Alpha值)来决定是否保留片段

模板测试

设定一个模板,只有在模板中的片段才会被保留,比如使用模板测试进行描边渲染

裁剪测试

通过自定义裁剪窗口,只有位于裁剪窗口内的片段才会被保留

测试的顺序

裁剪测试$\rightarrow$Alpha测试$\rightarrow$模板测试$\rightarrow$深度测试(顺序并不绝对)

坐标变换(transformation)

坐标系变换矩阵推导

坐标系变换矩阵即把某个点从一个坐标系变换到另一个坐标系,该变换矩阵的形式如下(对于三维空间而言),其中$u,v,w$为坐标系的基向量,$e$为坐标原点的位置,都是在原始坐标系中的表示,从矩阵的形式上可以看出,就是由4个列向量构成的,为什么是这样的形式,下面给出详细的推导过程 \(\begin{bmatrix} x_u & y_u & z_u & -x_e\\ x_v & y_v & z_v & -y_e\\ x_w & y_w & z_w & -z_e\\ 0 & 0 & 0 &1 \end{bmatrix}\)

坐标系由坐标原点和一组正交基确定,那么坐标系变换也只要从这两个方面入手即可。

本质上就是分为两步,考虑原始坐标系$x$和目标坐标系$x^\prime$,要从原始坐标系$x$变换到目标坐标系$x^\prime$,需要经过如下两个步骤

  1. 旋转正交基,使各原始坐标系和目标坐标系的对应正交基平行且同向
  2. 平移坐标原点,使原始坐标系的坐标原点与目标坐标系的坐标原点重合

如果得到了上述两个变换的变换矩阵,已知原始坐标系中的点$P$,只要将上述变换矩阵的逆变换矩阵作用到点$P$,就可以得到点$P$在目标坐标系中的表示了,为什么是逆变换矩阵,下面给出简单的证明,假设$P_{x}$为点$P$在原始坐标系中的表示,$P_{x^\prime}$为点$P$在目标坐标系中的表示,$M_R$为从原始坐标系向目标坐标系变换的旋转变换矩阵,$M_T$为从原始坐标系向目标坐标系变换的平移变换矩阵,那么可以得到如下的等式 \(P_x=M_TM_RP_{x^{\prime}}\) 那么要从原始坐标表示$P_x$获得在目标坐标系中的表示$P_{x^\prime}$,只需要在等式两边成上变换矩阵的逆变换矩阵即可,如下 \((M_TM_R)^{-1}P_x=M_R^{-1}M_T^{-1}P_x=P_{x^\prime}\) 首先考虑向量在不同坐标系下的表示,假设坐标系$x$的坐标原点为$o$,正交坐标基为${\vec{i},\vec{j},\vec{k} }$,坐标系$x^\prime$的坐标原点为$o^\prime$,正交坐标基为${\vec{u},\vec{v},\vec{w}}$,假设已知向量$\vec{p}$在原始坐标系$x$下的表示为$(a_x,b_x,c_x)$,我们要求的就是向量$\vec{p}$在目标坐标系$x^\prime$下的表示$(a_{x^\prime},b_{x^\prime},c_{x^\prime})$,通过坐标基的形式表示向量$\vec{p}$如下 \(\vec{p}=\vec{o}+ a_x\vec{i}+b_x\vec{j}+c_x\vec{k}=\vec{o^\prime}+a_{x^\prime}\vec{u}+b_{x^\prime}\vec{v}+c_{x^\prime}\vec{w}\) 上述表示考虑了向量的平移,最前面的坐标原点向量即考虑坐标的表示是相对于坐标原点进行表示的,但是我们知道对于自由向量(free vector)而言,平移变换并不会改变向量,因此可以忽略平移的项,表示如下 \(\vec{p}=a_x\vec{i}+b_x\vec{j}+c_x\vec{k}=a_{x^\prime}\vec{u}+b_{x^\prime}\vec{v}+c_{x^\prime}\vec{w}\) 可以发现,通过在等式两边点乘坐标系$x^\prime$的对应坐标基,就可以得到向量$\vec{p}$在坐标系$x^\prime$的该坐标基上的分量,如下 \(a_{x^\prime}=(a_x\vec{i})\cdot \vec{u}+(b_x\vec{j})\cdot\vec{u}+(c_x\vec{k})\cdot\vec{u}\\ b_{x^\prime}=(a_x\vec{i})\cdot \vec{v}+(b_x\vec{j})\cdot\vec{v}+(c_x\vec{k})\cdot\vec{v}\\ c_{x^\prime}=(a_x\vec{i})\cdot \vec{w}+(b_x\vec{j})\cdot\vec{w}+(c_x\vec{k})\cdot\vec{w}\) 转换成矩阵的形式就是 \(\begin{pmatrix} a_{x^\prime}\\ b_{x^\prime}\\ c_{x^\prime} \end{pmatrix}=\begin{pmatrix}\vec{u}\ \vec{v} \ \vec{w}\end{pmatrix}^{-1}\begin{pmatrix}a_x\\ b_x\\ c_x\end{pmatrix}=\begin{pmatrix}a_u & b_u & c_u\\ a_v & b_v & c_v\\ a_w & b_w & c_w\end{pmatrix}\begin{pmatrix}a_x\\ b_x\\ c_x\end{pmatrix}\) 从几何的角度去理解就是,想从向量从一个坐标系中的表示得到在另一个坐标系中的表示,只需要求出该向量在目标坐标系中各正交坐标基上的投影即可,而投影则可以通过向量的内积得到,因此只需要分别将对应的坐标基与向量作内积就可以得到在新的坐标系上的表示。参考Transformation Matrix Between Two Cartesian Systems

上面的讨论都是关于向量的,对于点来说,平移是会影响其相对位置的,因此只需要将点再乘上从原始坐标系到目标坐标系平移变换的逆变换即可,平移变换的逆变换很容易得到,就是向反方向平移对应的距离即可,因此就可以得到在齐次坐标系下的坐标系变换矩阵 \(\begin{bmatrix} x_u & y_u & z_u & -x_e\\ x_v & y_v & z_v & -y_e\\ x_w & y_w & z_w & -z_e\\ 0 & 0 & 0 &1 \end{bmatrix}\)

MVP变换

对于齐次坐标下的变换矩阵,其统一了线性变换和平移变换,需要注意的是变换矩阵表示的先做线性变换,然后再做平移变换

模型变换矩阵Model

模型变换矩阵将物体从模型的局部坐标系转换到世界坐标系(或者全局坐标系),可以把这一步理解为把模型放到我们要渲染的世界中

视图(摄像)变换矩阵View

观察变换将世界坐标系变换到观察坐标系,摄像机的定义主要包括其观察方向(gaze direction)$\vec{g}$,摄像机位置$\vec{p}$,view-up vector(用于指定摄像机的上方)$\vec{t}$,通过这几个参数就可以建立观察坐标系,以摄像机位置作为坐标系原点,坐标系的正交坐标基如下 \(\begin{align} &\vec{w}=\frac{\vec{g}}{||\vec{g}||}\\ &\vec{u}=\frac{\vec{t}\times\vec{w}}{||\vec{t}\times\vec{w}||}\\ &\vec{v}= \vec{w}\times\vec{u} \end{align}\) 如何记忆上述坐标基的计算过程,首先观察方向一定是作为z轴,这与图形学中的z轴一致,也就是把$\vec{g}$向量的单位向量作为坐标系的z轴

然后通过up-view和$\vec{w}$的叉乘得到坐标系的x轴,这可以类比右手系是如何通过其他两个坐标基得到x轴的,也是通过y轴的基向量与z轴的基向量叉乘得到的

最后通过将$\vec{w}$和$\vec{u}$叉乘得到y轴的基向量,这也可以类比右手系,即z轴的基向量与x轴的基向量叉乘得到y轴的基向量

得到了观察坐标系的基向量,就可以很容易得到坐标系变换矩阵了,参考坐标系变换矩阵推导

视图变换矩阵的另一种视角(参考games101lecture4)

当然可以从另一种角度来考虑观察变换矩阵,把摄像机和场景中的物体同时移动,使得相机在固定的位置(称为标准位置),此标准位置规定,摄像机位置位于(0,0,0),其gazeDir朝向z轴负方向,朝上方向为y轴,以上所说的轴和点坐标都是指世界坐标系。

由于是把场景中的摄像机和物体同时移动,因此其相对位置不会发生改变,那么就也不会影响相机的拍摄结果

那么此变换就可以分为以下两步

  1. 将摄像机平移至原点,$T_{translate}$
  2. 旋转摄像机坐标轴与世界坐标轴对齐,$T_{rotate}$
  3. 因此视图变换矩阵可以表示为$M_{view}=T_{rotate}T_{translate}$

对于平移变换矩阵,很容易就可以求出 \(M_{translate}=\begin{bmatrix} 1 & 0 & 0 & -x_e\\ 0 & 1 & 0 & -y_e\\ 0 & 0 & 1 & -z_e\\ 0 & 0 & 0& 1 \end{bmatrix}\) 对于旋转变换矩阵,如果直接求,即把摄像机坐标轴旋转到与世界坐标系的坐标轴对齐,不太好求,为此我们可以求其逆变换矩阵,即把世界坐标系的坐标轴旋转到与摄像机坐标系的坐标轴对齐,如对于世界坐标的x轴(1,0,0,0),要想把它变换到摄像机坐标系中的$\vec{u}$,可以很容易得到旋转变换矩阵的第一列即$[x_u,y_u,z_u]$,以此类推,就可以得到该旋转变换矩阵

而对于原来我们要求的变换,就是要求上述求出的旋转变换矩阵的逆变换,而由于旋转变换矩阵为正交矩阵,因此可以直接通过求上述矩阵的转置得到

$AA^T=E$,则称矩阵$A$为正交矩阵,即正交矩阵的逆矩阵等于其转置矩阵

投影变换矩阵Projection

正交投影Orthographic Projection

正交投影的投影空间为一个长方体(cuboid),定义该长方体所覆盖范围使用如下6个参数

l,r:左右边界

t,b:上下边界

n,f:近平面、远平面,注意由于摄像机朝向z轴的负方向,因此$n>f$

投影的过程就是把处于这样的长方体中的物体变换到$[-1,1]^3$的NDC中,那么就需要进行以下的两个变换

  1. 把cuboid移动到坐标原点,使其中心与坐标原点重合,对于给定的cuboid边界信息,可以很容易确定cuboid的中心坐标$(\frac{l+r}{2}, \frac{t+b}{2}, \frac{n+f}{2})$,那么平移变换矩阵如下 \(M_{translate}=\begin{bmatrix}1 & 0 & 0 & \text{-}\frac{l + r}{2}\\ 0 & 1 & 0 & \text{-}\frac{t+b}{2}\\ 0 & 0 & 1 & \text{-}\frac{n+f}{2}\end{bmatrix}\)

  2. 将坐标放缩到$[-1,1]^3$,对于x坐标需要缩放$\frac{2}{r-l}$,对于y坐标需要缩放$\frac{2}{t-b}$,对于z坐标需要缩放$\frac{2}{n-f}$,因此缩放变换矩阵为 \(M_{scale}=\begin{bmatrix}\frac{2}{r-l} & 0 & 0 & 0\\ 0 & \frac{2}{t-b} & 0 & 0\\ 0 & 0 & \frac{2}{n-f} & 0\\ 0 & 0 & 0 & 1\end{bmatrix}\)

  3. 综上所述,正交投影变换的矩阵就是以上两个变换矩阵共同作用的结果表示为 \(M_{orth}=M_{scale}M_{translate}=\begin{bmatrix} \frac{2}{r-l} & 0 & 0 & \text{-}\frac{l+r}{r-l}\\ 0 & \frac{2}{t-b} & 0 & \text{-}\frac{t+b}{t-b}\\ 0 & 0 & \frac{2}{n-f} & \text{-}\frac{n+f}{n-f}\\ 0 & 0 & 0 & 1 \end{bmatrix}\)

透视投影Perspective Projection

对于透视投影,其与正交投影的区别如下,主要区别在于其对于场景中的物体会有放缩变换,即会产生近大远小的感觉,更符合真实世界,因此更常用

对于透视投影,可以把它分为两步

  1. 首先挤压frustum(或者说视锥),使得其远平面和近平面的大小一样
  2. 然后该投影变换就变成了正交投影,直接使用正交投影变换矩阵即可

第二步我们已经知道其变换矩阵了,现在的问题就是要求第一步的变换矩阵,即将视锥挤压称一个长方体,对于视锥内的某点$(x,y,z)$,假设其变换后的点坐标为$(x^\prime, y^\prime, z^\prime)$,首先考虑y坐标的变换,如下图,可以根据相似三角形的关系得到如下的关系,即$y^\prime=\frac{n}{z}y$,同理可以得到x坐标之间的关系$x^\prime=\frac{n}{z}x$

根据上述x,y变换前后的关系,即如下所示,由于在齐次坐标表示下,给每个分量乘上同一个数不会影响坐标,因此可以写出如下的等价变换 \(\begin{pmatrix} x\\ y\\ z\\ 1 \end{pmatrix}\Rightarrow\begin{pmatrix}\frac{n}{z}x\\\frac{n}{z}y\\?\\1\end{pmatrix}\Leftrightarrow\begin{pmatrix}nx\\ny\\?\\z\end{pmatrix}\) 通过上述变换前后的关系,我们可以写出部分的变换矩阵,如下 \(M_{pers\rightarrow orth}=\begin{bmatrix}n & 0 & 0 & 0\\0 & n & 0 & 0\\? & ? & ? & ?\\ 0 & 0 & 1 & 0\end{bmatrix}\) 要想求变换矩阵的第三行,就是要建立变换前后z坐标的关系,首先对于该挤压变换,我们可以发现,对于近平面上的点,其坐标不会发生改变,即可以得到如下的变换关系 \(\begin{pmatrix}x\\y\\n\\1\end{pmatrix}\Rightarrow\begin{pmatrix}x\\y\\n\\1\end{pmatrix}\Leftrightarrow\begin{pmatrix}nx\\ny\\n^2\\n\end{pmatrix}\) 根据上述前后变换的关系,可以知道其必然与x和y坐标无关,因此可以假设矩阵的第三行为[0 0 A B],可以得到如下的线性方程 \(An+B=n^2\) 需要解出未知数A和B还需要一个线性方程,为此我们可以从远平面上得到,对于远平面来说,其z坐标不会发生变化,因此可以得到如下的变换关系 \(\begin{pmatrix}x\\y\\f\\1\end{pmatrix}\Rightarrow\begin{pmatrix}\frac{n}{f}x\\\frac{n}{f}y\\f\\1\end{pmatrix}\Leftrightarrow\begin{pmatrix}nx\\ny\\f^2\\f\end{pmatrix}\) 因此可以得到如下的线性方程 \(Af+B=f^2\) 根据以上两个线性方程,可以得到未知数A和B的值,A=n+f,B=-nf,因此该变换矩阵就可以求解出来,如下 \(M_{pers\rightarrow orth}=\begin{bmatrix}n & 0 & 0 & 0\\0 & n & 0 & 0\\0 & 0 & n + f & \text{-}nf\\ 0 & 0 & 1 & 0\end{bmatrix}\)

法向量的变换

  • 在对顶点坐标信息进行变换时,通常会对其进行MVP变换,将其变换到裁剪空间中,法向量也是顶点所包含的信息,其用来计算光照(比如计算Bling-Phong关照模型中的漫反射项时用到的Lambert定律),一般来说光照相关的计算都是在世界坐标系中进行的,因此需要将模型的局部坐标内的法向量通过模型变换矩阵(Model Matrix)变换到世界坐标系中,但是直接将模型变换矩阵乘上法向量,其结果是不太对的,比如对于只有平移变换的模型矩阵,法向量不应该被作用,因为平移变换并不会改变法向量;只有旋转变换的模型矩阵可以直接作用到法向量,因为当模型发生了旋转,法向量也会随之旋转;只有缩放变换的模型矩阵,也不能直接作用到法向量,因为法向量理应是单位向量,不应该有长度,这会对光照的计算带来额外的系数(这里是否有点问题,因为在使用法向量前都会对法向量进行归一化处理),因此如果直接把缩放变换矩阵作用到法向量上,还应该把法向量变为单位向量

  • 法向量的变换矩阵的求解方法是把模型变换矩阵的逆矩阵的转置作用到法向量上,求解的过程如下。

    • 对于使用平面来说,可以通过点法式进行表示,具体而言就是平面上任意点与法向量与平面的交点构成的向量与法向量垂直,假设任意点构成的向量在齐次空间坐标系中的表示为$\vec{v}=(x,y,z,w)^T$,法向量为$\vec{n}=(x_n,y_n,z_n,w_n)^T$,那么就可以得到平面方程$\vec{n}\cdot\vec{v}=0$,其通过矩阵表示就是 \(\left(x_n, y_n, z_n, w_n\right) \begin{pmatrix} x \\ y \\ z \\ w \end{pmatrix}=0\)

    • 那么在世界坐标系中的平面上的向量按理说也应该要满足这样的等式,那么经过模型变换后的平面上的向量很容易得到,假设模型变换矩阵为$M$,那么世界坐标系中的向量就是 \(\vec{v}^{\prime}=M\begin{pmatrix} x\\ y\\ z\\ w \end{pmatrix}\)

    • 要使平面的方程依然成立,只需要在法向量后乘上$M^{-1}$,即模型变换的逆矩阵即可,这样就可以保证等式依然成立,那么平面的方程就变为如下的形式 \((x_n,y_n,z_n,w_n)M^{-1}M\begin{pmatrix}x\\ y\\ z\\ w\end{pmatrix}=0\)

    • 也就是说世界坐标系中的法向量为$(x_n,y_n,z_n,w_n)M^{-1}$,即$\vec{n}^TM^{-1}$,把其转化为矩阵右乘的形式就是$(M^{-1})^T\vec{n}$,也就是如下 \(\vec{n}^{\prime}=(M^{-1})^T\begin{pmatrix}x_n\\ y_n\\ z_n\\ w_n\end{pmatrix}\)

    • 因此在vertex shader中,通常将输入的顶点法向量信息作上述的变换,如下

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      
      #version 330 core
      layout (location = 0) in vec3 aPos;
      layout (location = 1) in vec3 aNormal;
      layout (location = 2) in vec2 aTexCoords;
      // 利用shadowmap来渲染场景中的阴影
      out VS_OUT {
          vec3 FragPos;
          vec3 Normal;
          vec2 TexCoords;
          vec4 FragPosLightSpace; // 像素在光的视角下的坐标,用于与shadowmap记录的深度值做比较
      } vs_out;
          
      uniform mat4 projection;
      uniform mat4 view;
      uniform mat4 model;
      uniform mat4 lightSpaceMatrix;
          
      void main() 
      {
          vs_out.FragPos = vec3(model * vec4(aPos, 1.0));
          vs_out.Normal = transpose(inverse(mat3(model))) * aNormal;  //  针对法向量的变换
          vs_out.TexCoords = aTexCoords;
          vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);
          gl_Position = projection * view * vec4(vs_out.FragPos, 1.0);
      }
      
    • 参考OpenGL Normal Vector Transformation

      纹理映射

      法线贴图

虽然模型的顶点信息包含了顶点的法线,但是其是顶点层面的法线信息,而法线贴图是像素层面的法线信息,能够给模型更多的细节信息,比如凹凸不平的表面等。 使用顶点法线信息进行渲染,其法线信息是逐表面的法线信息,而表面内部的细节信息就无法很好的表示出来,比如表面内存在凹凸不平的情况,使用之前的方法会任务该表面的法线是一致的,因此渲染出来的效果就是一个很平坦的面,因此需要使用一种逐fragment的法线信息来补充这些细节信息,因此就出现了法线贴图(normal mapping)技术,其就好像在告诉渲染器在原先的平面是由若干法线各不相同的微表面构成的。

tangent space(切线空间)

  • 法线贴图被定义在切线空间,通过TBN矩阵可以将其从切线空间转化到世界空间,本质上就是进行坐标系的变换,因此我们希望找到切线空间的坐标基在世界坐标的表示,通过坐标基就可以得到坐标系之间的变换矩阵。想要求此坐标基,为此我们需要通过坐标系之间的关系来求该坐标基

  • 要定义一个坐标系,则需要3个正交基(up, forward, right),已知的是up vector即平面的法向量

  • 对于其他两个正交基$\vec{T},\vec{B}$,使用该表面上的一个三角形进行计算,已知三角形三个顶点的世界坐标,计算三角形两边的向量(世界坐标下的表示),使用向量两端的纹理坐标的差值作为在所求向量基上的分量,假设三角形的两边的向量为$\vec{E_1}$和$\vec{E_2}$,在两条边上的纹理坐标的差值分别为$(\Delta U_1, \Delta V_1)$和$(\Delta U_2, \Delta V_2)$,则可以得到如下的方程 \(\vec{E_1} = \Delta U_1 \vec{T} + \Delta V_1 \vec{B}\\ \vec{E_2} = \Delta U_2 \vec{T} + \Delta V_2 \vec{B}\) 使用向量分量的形式展开上述方程,得 \((E_{1x},E_{1y},E_{1z})=\Delta U_1 (T_x, T_y, T_z) + \Delta V_1 (B_x, B_y, B_z)\\ (E_{2x}, E_{2y}, E_{2z}) = \Delta U_2 (T_x, T_y, T_z) + \Delta V_2 (B_x, B_y, B_z)\) 上述方程可以写成矩阵运算的形式 \(\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}\) 等式两边同时乘上纹理坐标的差分矩阵的逆矩阵就可以计算出向量$\vec{T}$和$\vec{B}$,可以得到如下的计算公式,已知的是三角形两条边的向量在世界坐标中的表示,以及向量两端的纹理坐标的差值 \(\begin{bmatrix} T_x & T_y & T_z \\ B_x & B_y & B_z\end{bmatrix}=\frac{1}{\Delta U_1\Delta V_2-\Delta U_2 \Delta V_1}\begin{bmatrix}\Delta V_2 & -\Delta U_2 \\ -\Delta V_1 & \Delta U_1\end{bmatrix}\begin{bmatrix}E_{1x} & E_{1y} & E_{1z}\\ E_{2x} & E_{2y} & E_{2z} \end{bmatrix}\)

    逆矩阵的计算:假设有方阵A,其行列式A$\neq$0,则其存在逆矩阵,称其为非奇异矩阵;否则其存在逆矩阵,称其为奇异矩阵. 方阵A的逆矩阵可以通过$\mathrm{A}^{-1}=\frac{\mathrm{A}^*}{\mathrm{A}}$计算,其中$\mathrm{A}^*$为方阵A的伴随矩阵.

    伴随矩阵的计算:对于方阵A的每个元素$\mathrm{A}{i,j}$,计算其代数余子式$\mathrm{M}{i,j}$,然后将$\mathrm{M}_{i,j}(-1)^{(i+j)}$作为伴随矩阵$\mathrm{A}^*$对应位置的元素

    代数余子式:对于代数余子式$\mathrm{M}_{i,j}$,就是将原矩阵的$i$行和$j$列删除后,由剩下的元素构成的行列式的值

    使用法线贴图

  • 法线的范围是[-1,1],而颜色空间的范围是[0,1],因此在将法线存储到法线贴图时,会进行一次变换,即从[-1,1]$\rightarrow$[0,1],即$colorSpaceVal = normalSpaceVal * 0.5 + 0.5$

  • 因此在读取法线贴图中的法线信息后,需要进行上述变换的逆变换,将其变换为法线的数值范围,即$normalSpaceVal = 2 * colorSpaceVal - 1$

  • 需要注意的是对于opengl,其需要对法线贴图的y(或者green)值进行反转,即将原本g值,变为1-g,其原因是opengl处理纹理时对于y轴与图像坐标是相反的,其认为底部的y值为0,而图像坐标中顶部的y为0,如下为opengl的纹理坐标。在进行纹理映射时需要将归一化的纹理坐标转化为图像坐标,然后通过图像坐标得到映射后的颜色

  • 得到TBN矩阵之后,该矩阵将切线空间变换到世界空间,有两种使用方式

    1. 将法线贴图中的法线信息转化,通过TBN矩阵将法线从切线空间(tangent space)变换到世界空间,这里需要将TBN从vertex shader传给fragment shader,然后逐fragment进行法线信息的变换,其计算的阶段在fragment shader
    2. 通过TBN的逆矩阵,可以将世界空间的信息变换到切线空间,为此我们可以将参与光照计算的信息从世界通过TBN矩阵的逆矩阵变换到切线空间,(值得一提的是由于TBN是一个正交矩阵,因此可以通过transpose求得其逆矩阵,而无需调用计算开销更大的inverse来计算其逆变换矩阵)然后传给fragment shader,这里的计算是在vertex shader中进行的,其计算的次数远少于在fragment shader的计算次数,因此该方法的计算开销更小,往往一个vertex shader会对应多个fragment shader
  • 正交化修正,在大型Mesh中,有顶点被大量的面共享,会导致切线空间的向量被平均化,以达到平滑过渡的效果,由此导致切线空间的基向量不再正交,非正交的TBN矩阵在光照计算时会出现问题,因此需要通过某种方式将其变为正交基,使用施密特正交化可以实现这一点

    施密特正交化(Gram-Schmidt process):假设初始的基向量为${a_1,a_2,\cdots, a_n}$,经过施密特正交化后的正交基为${b_1,b_2,\cdots,b_n}$,通过如下的方式可以对原始的基向量进行正交化 \(b_1=a_1\\ b_2=a_2-\frac{b_1\cdot a_2}{b_1\cdot b_1}b_1\\ b_3=a_3-\frac{b_1\cdot a_3}{b_1\cdot b_1}b_1-\frac{b_2\cdot a_3}{b_2\cdot b_2}b_2\\ \cdots\)

    对于TBN矩阵的三个向量基而言,通过上述施密特正交化,实现如下,下述正交中,并没有分母的$b_1\cdot b_1$,这是由于向量N已经是单位向量,分母可以省略

1
2
3
4
5
6
7
8
vec3 T = normalize(vec3(model * vec4(aTangent, 0.0)));
vec3 N = normalize(vec3(model * vec4(aNormal, 0.0)));
// re-orthogonalize T with respect to N
T = normalize(T - dot(T, N) * N);
// then retrieve perpendicular vector B with the cross product of T and N
vec3 B = cross(N, T);

mat3 TBN = mat3(T, B, N) 

视差贴图(Parallax Mapping)

技术背景

对于一般的位移贴图(displacement mapping),其通过贴图上存储的信息来改变顶点的位置信息,贴图上存储了每个像素的高度信息,这样的贴图被称为height map,但是为了传递这些额外的顶点的高度信息,需要在平面内添加更多的vertex,这样会带来额外的计算开销,为此就有了视差贴图技术,其通过某种trick使得fragment的纹理坐标随着其在height map中的高度以及观察视角产生位移,其技术细节大致如下所述

  • 对于fragment A,想要根据其在height map中的高度以及观察的方向对其纹理坐标进行偏移,即我们希望得到的是在B点的纹理坐标得到的渲染结果,为此通过frag-to-view方向向量并乘上A点的高度得到向量P,然后由P对应的纹理坐标大致近似B点对应的纹理坐标

  • 为了让向量P在x,y分量上的位移与纹理坐标的坐标轴一致,因此将viewDir向量变换到tangent space进行计算,这样就可以保证其在平面上的两个分量的方向与纹理坐标一致,就可以直接将其偏移量应用到纹理坐标上

Steep Parallax Mapping

  • 该方法就是对上述实现的改进,上述实现本质上就是在viewdir方向选取了一个采样点,而steep parallax mapping就是选取采样点,具体步骤如下
  • 首先在高度方向上进行分层,按层朝viewDir方向遍历,我们要找的目标点就是在层高和高度贴图的高度相等的位置,我们从首层(深度为0的层)不断向viewDir方向遍历,初始时层高小于真实高度(即对应height map上的值),随着不断遍历层高的真实的高度值的差会越来越小,直到我们遇到层高大于真实高度,我们就认为近似找到了该位置,如下图所示
  • 理论上来说,选取的采样点越多(即设置的层数越多)得到的结果就越精细,但是也会增加额外的计算开销。此外,可以通过设置一种自适应的层数的选取方式来优化算法的性能,因为当viewDir越接近垂直,其所需要的采样点就越少,所以可以通过此点来自适应的选取层数

    Parallax Occlusion Mapping

  • 对上述算法的进一步优化,对于上述满足条件的位置,我们选取其前一次的纹理对应的深度,作为插值的依据,然后将两个纹理坐标进行线性插值,得到最终的纹理坐标
  • 插值的权重由viewDir向量到两个点的距离决定

PBR

辐射度量学基本量定义

  • Radiant Energy:用$Q$表示,穿过某个平面的光能,单位是焦耳

  • Radiant Flux(Power):用$\Phi$表示,含义为单位时间内穿过一个截面的光能

  • Intensity:用$I$表示,单位立体角上的Radiant Flux,与距离无关

  • Irrandiance:用$E$表示,单位面积上的Radiant Flux,与距离呈平方衰减关系(考虑一个点光源向周围散射,其Radiant Flux不变,假设为$\Phi$,其散射到的面积为一个球面为$4\pi r^2$,因此其Irrandiance为$\frac{\Phi}{4\pi r^2}$

  • Radiance:用$L$表示,单位立体角、单位面积上的Radiant Flux,与距离无关

光学基础

光的散射

光在两种不同介质之间会发生折射和反射,可以根据反射平面是否为光学平面分情况讨论

  • 光学平面

    • 在光学平面上的反射,满足入射角(入射光线与平面发现的夹角)等于反射角的性质,如下图所示,假设平面的法向量为$\vec{n}$,入射光线反方向的方向向量为$\vec{i}$,出射光线的方向向量$\vec{r}$容易通过向量的运算求出,具体如下

      • 假设入射角为$\theta$,通过向量分解可以将反射光的方向向量$\vec{r}$分解为平面法向量方向$\vec{v}{\bot}$和平面方向$\vec{v}{\parallel}$两个分量的向量和,即$\vec{r}=\vec{v}\bot+\vec{v}\parallel$

      • 对于$\vec{r}$在法向量方向上的分量$\vec{v}{\bot}$,可以通过$\vec{i}$在法向量上的投影得到,即$\vec{v}{\bot}=(\vec{i}\cdot\vec{n})\vec{n}$

      • 对于$\vec{r}$在平面方向上的分量,通过向量的减法,使用上述$\vec{v}\bot$减去$\vec{i}$得到,即$\vec{v}\parallel=\vec{v}_\bot-\vec{i}=(\vec{i}\cdot\vec{n})\vec{n}-\vec{i}$

      • 因此最终$\vec{r}=2(\vec{i}\cdot\vec{n})\vec{n}-\vec{i}$

    • 对于光的折射,通过Snell’s Law求出折射角

      \(\frac{\sin\theta_i}{\sin\theta_t}=\frac{v_i}{v_t}=\frac{\lambda_i}{\lambda_t}=\frac{\eta_t}{\eta_i}\) 其中$\theta_i$表示入射角,$\theta_t$表示折射角(即折射光线相对于法线的角度),$v$表示在介质中的光速,$\lambda$表示介质中的波长,$n$表示介质中的折射率,注意折射率的比值是相反的

      Snell’s Law的另一种形式表示为$\sin\theta_i\eta_i=\sin\theta_t\eta_t$

      全反射现象:没有发生折射,当入射介质的折射率$\eta_i$比折射介质的折射率$\eta_t$大,就有可能发生,推导过程,通过求折射角的余弦,$\cos\theta_t=\sqrt{1-\left(\frac{\eta_i}{\eta_t}\right) ^2\sin^2\theta_i}$,当根号内部的值小于0时,就没有意义,也就不会发生折射

    • 反射和折射的比率通过菲涅尔方程得出,由于该方程过于复杂,在图形学中通过Schlick's Approximation来计算

      $R(\theta)=R_0+(1-R_0)(1-\cos\theta)^5$

      $R_0=\left(\frac{n_1-n_2}{n_1+n_2}\right)^2$

  • 非光学平面(凹凸不平的平面)

    • 可以看成是大量微小光学平面的集合,其朝向各异(即微小光学平面的法向量各异)

次表面散射(SSS(SubSurface Scattering))

  • 金属这种物体,当光线折射近表面后,会立即被金属中的自由电子吸收,不再可见

  • 对于非金属,其往往不是由单一成分组成,可以认为介质中存在着许多折射率不同的微粒,光线在介质内遇到这些微粒后会发生反射和折射,不断在介质内部传播,散射到不同方向,其中一部分再次穿过物体表面被观察到,这种现象就是次表面反射

单位立体角推导

单位立体角是平面角在三维空间的推广,对于平面的角,其定义是单位圆上的弧长,推广到三维空间,单位立体角就是单位球对应的球面上的面积,在图形学中其微元一般表示为$\mathrm{d}\omega$,具体推导过程如下

如上图所示,在球坐标系表示下,球面上面积的微元可以表示为$r^2\sin\phi \mathrm{d}\theta \mathrm{d}\phi$,其中$\theta$为向量投影到平面上与x轴的夹角,$\phi$为与z轴的夹角,在图形学的公式中,以上两个角的表示往往相反,由于这是一般情况下球上的面积微元,因此要转化到单位球上的面积微元只要把r变为1即可,因此$\mathrm{d}w=\sin\phi\mathrm{d}\phi\mathrm{d}\theta$

BRDF

用于描述入射光和反射光的关系,具体来说其表示出射radiance和入射irradiance的比值

渲染方程

渲染方程的含义就是从某个着色点到摄像机看到的颜色等于其自发光项+反射光项

参考资料

基于物理着色:BRDF

阴影技术

光照贴图(LightMap)

  • 实现原理:简单来说就是把光照提前计算成一张贴图,在渲染时直接从该贴图中采样得到光照,将计算开销在离线阶段完成,减小实时渲染的计算开销
  • 缺陷:只支持静态光源,仅适用于不移动的物体和光源

    ShadowMap

  • 原理:阴影的产生根源是missing of light,因此我们需要知道的是光源在场景中最远照射到的深度,对于更深的位置就处于阴影中,shadow map的原理就是从光源的角度渲染一张深度贴图,在从camera进行场景渲染,对于每一个点将其在光照视角下的深度与深度贴图中记录的深度进行比较,来判断其是否被遮挡,即是一个两趟(two pass)的算法
  • 缺陷:多光源下需要生成多张shadow map

    PCF(Percentage Closer Filter)

  • 原理:在ShadowMap的基础上进行了改进,shadowmap只能生成硬阴影,即非0(不可见)即(可见),而现实中的阴影往往是有过渡段的,称为半影,因为现实世界中的光源往往是面光源,没有那么理想的点光源,PCF对于某个着色点的可见性判断在其周围某个区域的深度贴图内进行采样并比较,比较的结果为0或1,然后将比较的结果进行加权平均,得到该着色点的可见性

    PCSS(Percentage Closer Soft Shadow)

  • 原理:在PCF的基础上进行了动态改变Filter尺寸的改进,filter的尺寸决定了阴影的软硬程度,filter尺寸越小,则阴影就越硬,通过观察可以发现,当blocker与shading point的距离越近,产生的阴影就越硬,因此对于某个着色点,其filter的大小可以根据其与blocker的距离进行动态确定

    CSM(Cascade ShadowMap)

  • 原理:使用shadow map在渲染大型场景时,由于其分辨率固定,因此常常会出现问题,低分辨率带来的锯齿问题和高分辨率带来的存储开销,CSM通过LOD(level of detail)的思想,根据对象到观察者的距离提供不同分辨率的shadow map来解决上述问题。具体而言,将视锥体按照深度的远近分成子块,给每个子块渲染shadowmap,距离camera越近,其分辨率越高,如下图所示。

FAQ

裁剪空间和NDC的区别和联系

在顶点着色器阶段,通过MVP变换后的顶点坐标位于齐次坐标系,尤其是对于投影变换,其$\omega$不一定为1,即不是归一化后的坐标,将该空间称为裁剪空间

将坐标经过透视除法,即$(x,y,z,\omega)\rightarrow(x/\omega,y/\omega,z/\omega)$后,就可以将位于裁剪空间中的坐标变换到NDC,其位于$[-1,1]^3$

裁剪和剔除

裁剪:把部分处于NDC的三角形图元进行裁剪,形成新的顶点和三角形

剔除:把完全不处于NDC的三角形丢弃,让其不进入后续的光栅化阶段,背面剔除,按照三角形的顶点索引顺序,计算三角形面的朝向,把面向背面(即法线方向延z轴负方向)的面剔除

This post is licensed under CC BY 4.0 by the author.

Trending Tags