深度
世界中有许多物体,我们应该根据远近来确定那些部分应该被画到屏幕上。
画家算法

画家算法先绘制远处的模型的三角形面,再绘制近处的三角形面,近处的会覆盖掉远处的。三角形的深度为三个顶点的平均深度(视图空间的Z值)。然后从远到近进行排序,依次着色。
但是画家算法在处理循环遮挡情况下存在问题,而且排序开销较大,不主要使用。
深度缓冲 Z-Buffer

对于每一个像素都记录它的最小深度(使用重心坐标进行深度插值),同时需要帧缓冲FrameBuffer用于记录该像素的经过着色后的颜色。最后FrameBuffer就是这一帧的实际画面。

着色是向物体应用材质的过程。
Blinn-Phong光照模型
物体表面发出的光包括:
- 镜面高光(specular highlights)
- 漫反射(diffuse reflection)
- 环境光照(ambient lighting)

- 视线方向 – v
- 表面法线 – n
- 光照方向 – l
- 表面参数,颜色、粗糙度等等
着色首先不考虑阴影。
漫反射

光照到物体表面,光线会向四周均匀地散射出去。

表面接受到光线的强度与光线与表面的夹角有关。接受到的能量与该夹角的余弦值成正比。

假如传播过程中光照能量不损耗,那么各个球面上的能量相等。所以,一个点光照的强弱与它距离光源距离的平方成反比。

综上,可得漫反射光照的计算公式:
其中,是漫反射系数,在0到1之间,表示亮度与颜色,为光照强度,为与光源的距离,为表面法线,为入射方向。

随着K值变大,着色效果如下图所示。
高光
光几乎镜面反射。当观察向量和反射向量足够接近时,就会出现高光现象。我们可以利用半程向量来判断观察向量与反射向量的夹角。

半程向量:
高光:
其中,是高光影响因素。高光计算中不考虑入射角度与接收能量的关系,即不需要乘。半程向量夹角处有p次方是为了缩小高光区域。实际上,只有3度左右的范围能看到高光。一般,p大约在100到200之间。

环境光照
真实的环境光照非常复杂。Blinn-Phong光照模型假设所有地方的环境光是相同的。

环境光被简化为常数。

最后,将三者相加即可得到最终光照效果。
着色频率

三个球分别对应:对平面着色、对顶点着色(使用差值计算内部颜色)、对像素(使用差值计算内部法线)着色
平面着色 Shade each triangle (flat shading)
- 三角形是一个面,有一条法线
- 对光滑平面来说效果并不好
高洛德着色 Shade each vertex (Gouraud shading)
- 通过顶点差值计算像素的颜色
- 每个顶点有一条法线
古罗着色 Shade each pixel (Phong shading)
为每个像素应用着色模型
通过顶点差值计算像素的法线
顶点法线

最精确的求顶点法线的方式是通过几何体来求。也可以简化为从周围相邻的面的法线取平均值。
图形管线

图形管线是图形渲染的全流程。主要包含顶点着色(MVP变换)->光栅化->片元着色等阶段。其中,顶点着色器与片元着色器部分是可编程的。
纹理映射
任何一个三维物体的表面其实是二维的,纹理就是三维物体表面再开后的二维面。模型上的每个三角形顶点都会被分配给纹理上的一个坐标(u,v)。u、v始终在0到1之间。
至于怎么映射、以及纹理如何设计,我们不关心。
重心坐标
通过重心坐标可以做线性插值,其中可以是顶点的位置、材质坐标、颜色、法向量等等属性。
注意到,三维空间内三角形的重心坐标在投影后,不一定是二维投影的重心坐标。所以应该在三维空间做插值,再投影到二维上去;而不是先投影到二维,再做插值。
详细见:https://xuanlin.life/computer-graphics-rasterization.html
应用纹理
把纹理应用到物体过程为:
- 对于每个采样点(像素中心),可以通过重心坐标插值得到纹理对应坐标。原来我们只知道顶点对应的坐标,现在通过插值得到所有采样点的了。
- 获取材质坐标的颜色,并设置该采样点的漫反射系数Kd即可。
纹理放大
因为虽然UV是连续的,但是UV对应的纹理贴图是离散的。当纹理图片分辨率过低,不同UV可能对应的实际纹理像素是相同的,那么就会出现多个像素点仍然对应一个(u,v)坐标,并将该(u,v)坐标下颜色信息复制给该像素点,这样形成的图片边界过渡就会非常不自然,形成一个个的小格子。
对于纹理放大,有三种做法:

最近像素
直接选取距离uv坐标最近的像素。
双线性插值

双线性插值就是选取周围4个像素做插值计算出该uv的颜色。
事实上,在纹理被放大和缩小时都可以使用nearest、bilinear或bicubic的做法。这称为纹理滤波(texture filtering)。
纹理放大
屏幕上一个像素所覆盖的区域,映射回纹理空间后,纹理分辨率过高,可能覆盖了一大片纹理区域,包含了成百上千个纹素,就会出现一个像素点对应多个(u,v)坐标。但屏幕像素渲染时,我们却只在这个像素的中心取一个点,通过插值得到一个(u,v)坐标,然后去纹理上采一个点的颜色。这就相当于用一个点的信息来概括一整片区域,完全忽略了区域内的变化,这必然导致走样(比如远处地面的摩尔纹)。
所以,我们需要一个获取一个范围内纹理的像素的平均信息,这就是Mipmap。

Mipmap的额外空间占用仅比原来多1/3。
计算mipmap等级

在光栅化时,三角形内部的每一个屏幕像素 (x, y) ,都会被插值出一个独特的 (u, v)。这本身就定义了两个连续函数:u = f(x, y)与v = g(x, y)。
当这个小方块映射到纹理上时,它的形状可以用纹理坐标(u,v)对屏幕坐标(x,y)的偏导数来近似描述。
- du/dx、dv/dx:在屏幕x方向上移动一个像素,纹理坐标u和v的变化量。
- du/dy、dv/dy:在屏幕y方向上移动一个像素,纹理坐标u和v的变化量。
简单做法:光栅化时,是把像素编成一个2×2的小组(quad)来并行处理的。 在这个2×2的组里,四个像素的屏幕坐标和纹理坐标都是已知的。于是:
有了这两个向量,我们就能估算像素在纹理空间覆盖区域的“大小”。为了安全和简单,通常取这两个向量长度的最大值作为特征长度 L。
表示屏幕上的1个像素,大约对应了纹理上 L×L 个纹素的范围。
Mipmap的每一层都是上一层长宽各缩小一半,即面积变为1/4。第0层是原始分辨率,第1层是原来的1/2,第D层是原来的。
三线性插值
计算出连续值 D 后,直接取整会造成Mipmap层之间的接缝。因此实践中都会用三线性插值:
- 取 D 的整数部分 D_floor 和 D_ceil = D_floor + 1。
- 分别在层级 D_floor 和 D_ceil 上做双线性插值,得到两个颜色值。
- 再根据 D 的小数部分,把这两个颜色值做线性混合。
这样就能得到平滑过渡的结果。
各向异性过滤
上述前提的假设是屏幕像素在纹理上的“脚印”是一个正正方形。但现实中,由于透视投影和斜视角,脚印常常被拉得很长。比如一堵向远方延伸的墙,屏幕上一个像素对应在纹理上的区域可能是一个扁长的矩形。
当我们强行用最大边长 L把它当正方形处理时:
- 选的Mipmap层级 D = log2(L) 会过高(因为L是长边,远比短边大)。
- 结果就是,一个原本应该很清晰、沿着墙面延伸的纹理,在短边方向上被过度模糊了。

首先要获取屏幕x、y方向的纹理坐标变化率向量,组成一个雅可比矩阵:
对矩阵求特征值,其中最大值为λ_major,最小值为λ_minor。它们的平方根就正比于两个轴的长度。将它们开方得L_major与L_minor。
各项异性比例N= L_major / L_minor。个值决定了我们需要沿长轴做多少次采样来覆盖整个椭圆。硬件通常限制在一个上限值(如x16),也就是我们常说的“x4各向异性过滤”、“x16各向异性过滤”。
Mipmap等级:。注意,这里用的是短轴,从而保证了在这个变化最平缓、信息最密集的方向上,我们选到了足够高分辨率的纹理层,不会过度模糊。
以像素映射到纹理空间的那个中心点 (u0, v0) 为基准,沿着计算出的长轴方向,向两边均匀撒下 N 个采样点,而短轴方向只采样一个点。其总长度
待完成~