Blinn-Phong Reflection Model
上一节课以及学习了漫反射项,这节课我们接着继续学习剩下的项
Specular Term
上一节我们学过漫反射的方向是均匀的向法线方向上的半球进行均匀反射,而对于镜面反射,它的反射方向集中在与光照方向与法线对称的方向上的一个区域上(即类似图中黄色的区域),而对于绝对光滑的表面来说它的反射方向就是镜面反射方向(即光照方向与法线对称的方向)。而当高光项是一个与我们的观察反向与反射区域的夹角相关的那么一个项,如果观察方向与镜面反射方向接近的时候我就能看到高光,而其他的时候就看不到高光。
但是 Blinn-Phong Model 有一个非常精妙的发现,观察方向如光照方向加起来再归一化得到的一个新的向量
这个向量叫做半程向量,这个向量的作用就是:如果我们的观察方向与镜面方向反向非常接近的时候,那么这个半程向量与法线方向就会非常接近,我们可以通过半程向量与法线方向的夹角来计算观察与反射反向的夹角。
这样做的好处就是可以简化计算,计算反射方向需要通过 $\mathbf{r} = 2(\mathbf{n} \cdot \mathbf{l})\mathbf{n} - \mathbf{l}$ 而 $\mathbf{h}$ 只需要简单的向量加法和归一化
所以高光项的公式为,当我们的半程向量与法线方向越接近的时候,那么高光就越亮
其中
- $L_s$ 为高光项
- $k_s$ 为高光系数
事实上正确的公式应该为
$$
L_s = k_s\frac{I}{r^2} \max(0, \mathbf{n}\cdot \mathbf{l}) \max(0, \mathbf{h} \cdot \mathbf{n})^p
$$
而 Bilnn-Phong 模型是一个经验模型,他把高光项中的Lambert’s Cosine Law简化掉了
如果我们不使用半程向量直接计算反射反向与观察反向的夹角即:
$$
L_s = k_s\frac{I}{r^2} \max(0, \mathbf{r} \cdot \mathbf{v})^p
$$
那么这个就是 Phong 模型中的高光项公式,Bilnn-Phong 模型是 Phong 模型的改进
我们仔细观察公式可以看到公式最后还有一个指数 $p$ ,这个指数 $p$ 是用于调整接近程度的, 对于普通的余弦函数的问题它的变化过于平缓,这会导致我们在很多区域都能看到高光,但是我们希望在一个小的区域才有高光,所以我们可以通过这么一个指数 $p$ 来调整余下,让只有非常接近的角度才有高光
他们的变化如下
可以看到 $p$ 越大高光区域越小,$k_s$越大高光越亮
Ambient Term
在 Blinn-Phong 模型中环境光项 Ambient Term 非常简单,这是由于他们使用了非常大胆的假设,假设了我们的环境光是一个常数,但实际上环境光是一个非常复杂的项,它接受了来自环境中各种其他物体反射的光。 环境光项的公式为
$$ L_a = k_a I_a $$其中
- $L_a$ 为环境光项
- $k_a$ 为环境光系数
它和光照方向,观察方向,反射方向,法线反向都没关系,类似于物体的颜色,从哪个角度看都一样
最后我们讲漫反射项,镜面反射项,环境光项,全部混合在一起就得到了 Blinn-Phong 光照模型了
$$ L = L_a + L_d + L_s = k_a I_a + k_d \frac{I}{r^2} \max(0, \mathbf{n} \cdot \mathbf{l}) + k_s \frac{I}{r^2} \max(0, \mathbf{h} \cdot \mathbf{n})^p $$
我们对模型的所有的像素点进行着色就能得到我们想要的场景了
Shading Ferquencies
我们可以看到上面三个球,他们的几何模型都是一样的,但是使用了不同的着色方式,
第一个是逐面进行着色,第二个是逐顶点进行着色,第三个是逐像素进行着色,可以看到使用不同的着色方式的到的效果大不一样,逐像素的着色效果远远好于前面两个。而对表面进行着色是最简单的因为我们在Bilnn-Phong模型中需要使用到法线,而面大多都是利用法线定义的自带法线,而对顶点进行着色需要我们手动规定这个顶点的法线方向,而对像素进行着色则需要利用三角形顶点的法线对内部的像素进行插值得到内部每个像素的法线。
我们再来看这么一幅图,如果一个模型的面数很足够多,那么对它使用简单的逐面进行着色,得到的效果也不错,而对于使用逐像素着色无论这个几何体多么复杂效果看起来都差不多,所以这是一个需要权衡的问题。
对于一个面来说它的法线非常简单
对于一个顶点来说我们怎么获得它的法线呢?
我们通常使用对一个顶点周围链接的平面的法线进行平均来计算
当然这个算法可能不太准确,人们通常需要通过对应面的面积进行加权平均
而对于一个像素来说我们怎么获得它的法线呢?
我们需要通过对三角形的三个顶点的法线进行重心坐标插值而获得内部像素的法线,在上一节的作业解析中我们详细介绍过重心坐标插值了
Graphics Pipeline
实时渲染管线 Graphics Pipeline 从一个场景到一个图像的过程中间到底经历了什么,其实就是通过了一个流水线操作(渲染管线),而现代渲染管线通常由下面这几个部分组成
Application inpute vertex data->Input Assembler->Vertex Processing->Tessellation Processing->Geometry Processing->Clipping & Screen Mapping ->Rasterization->Fragment Processing->Per-Sample Operations->Output
而简化后的管线是这样的
Application inpute vertex data->Vertex Processing->Rasterization->Fragment Processing->Per-Sample Operations->Output
其中我们以及学会了其中简化后的部分我们已经通过过程学过了
- Application inpute vertex data实际上就是我们在应用层组织好的顶点数据
- Vertex Processing就是顶点处理,进行MVP变换最后到屏幕空间
- Rasterization光栅化阶段就是将屏幕空间转化为像素
- Fragment Processing就是我们对像素进行着色的过程
- Per-Sample Operations就是我们进行深度测试等操作的阶段
而这么一个流水线操作在我们的显卡中是有专门的对应的硬件进行处理的,渲染管线中的某些部分是固定的只可以配置,有些部分是可以直接编程的。而现代的GPU通常来说都是可编程渲染管线的,而可编程的模块我们叫做着色器Shader,运行在上面的代码我们一般也叫做Shader,对于处理Vertex Processing我们叫做顶点着色器,处理Fragment Processing的模块我们叫做片段着色器(或者叫像素着色器)管线更详细的可以去看LearnOpenGL的的介绍,这里不做太多的介绍了
显卡的介绍这里就不做太多介绍了
Texture Mapping
纹理映射 Texture Mapping,我们的物体想要变得更好看还需要使用纹理,也就是游戏中常说的贴图,通过贴图我们可以给一个几何形体的不同位置定义不同的颜色,而这是怎么做到的呢?这就需要使用到纹理映射了。简单来说我们就是通过一种方式将几何体上的点映射到平面图形上的一个点,让几何形体上的点与纹理上的像素建立一个一一对应的关系。

我的偶像闫令琪老师说可以将一张地球仪上的图平铺到一个平面上,但是根据高斯绝妙定理来说这是绝对不可能能做到的,老师应该是为了教学好理解而这么比喻的吧
我们知道模型都是由非常多的三角形组成的,所以我们只需要解决三角形的纹理映射纹理就行了。那么我们怎么映射呢?其实很简单我们只需要定义某个三角形三个顶点应该对应纹理空间(图片的二维直角坐标系,图片的左下角位于原点)下的哪些纹理坐标就行了,而对于内部的纹理坐标我们可以进行重心坐标插值得到,而对于三角形顶点的纹理坐标应该是对应纹理的哪里?这是美术师的工作。
而下面这幅图就是纹理空间的样子,通常来说对于一张纹理它的范围都是$[0,1]\times [0,1]$的,如果纹理不是一张正方形,那么他会被压缩到这个范围中
当然你可以将不同的三角形映射到同一个纹理上
