着色
着色
我们在前面已经把图像三角形化了,明白了三角形的绘制规则,这样我们可以把一个物体画出来了,如下图:
我们看到这个图是感受不出来是一个3d的模型,就是看起来很不真实,我们可以认出这个是个牛的模型,但是不知道这个是什么牛。
我们现实世界中看一个物体认为他是三维物体,是因为光照的关系,让物体表面有了明暗的差别,让我们知道是什么牛是因为物体表面的质感,也就是材质。
如果我们要在电脑上实现明暗的差别,简单的说就是为我们的渲染世界加一个灯,然后我们根据灯的一些属性(位置,方向,强度,颜色等)接合我们模型的颜色和位置,就可以产生明暗差别的效果(高光,漫反射,环境光)。如下图:

计算明暗差别这个过程一般叫做着色,换句话说,着色是根据材质属性(如漫反射属性等)、光源信息(如光源方向、辐照度等),使用一个等式去计算沿某个观察方向的出射度的过程。我们也把这个等式称为光照模型。
标准光照模型
标准光照模型也就是Binn-Phong着色模型,主要由四部分组成:
- 漫反射(Diffuse):当光线从光源照射到模型表面时,该表面会向每个方向散射多少辐射量
- 高光(Specular):描述当光线从光源照射到模型表面时,该表面会在完全镜面反射方向散射多少辐射量
- 环境光(Ambient):描述其他所有的间接光照
- 自发光(Emissive):当给定一个方向时,一个表面本身回想该方向发射多少辐射量。需要注意的是,如果没有使用全局光照技术,这些自发光的表面并不会真的照亮周围的物体,而是它本身看起来更亮了而已。这个一般物体是没有的。
对于每个着色点,我们可以简化成如下图所示:

其中有:
:表面法线方向 :入射光方向 :观察方向
注意:所有向量都是单位向量,所以确保向量进行过 normalize 操作。
另外,着色过程中是不会产生阴影的,阴影是通过其他技术手段来生成的。
漫反射
漫反射光照是用于对那些被物体表面随机散射到各个方向的辐射度进行建模。

在漫反射中,视角的位置不重要,因为反射是完全随机的,因此可以认为在任何反射方向上的分布都是一样的。所以物体表面的颜色都是一样在任何方向上。
但是我们看到上面的牛的图片中并不是每个地方的颜色都一样,这里就会说到兰伯特定律(Lambert's law):反射光线的强度与表面法线和光源方向之间夹角的余弦值成正比。因此入射光线的角度就很重要了。
可以这样理解这个过程,平时看一个物体的颜色,会受到物体本身表面的颜色和照射到该物体上的光的影响。比如:一个白色盒子在绿色灯光上,这个盒子我们看到就会是绿色,当然这个盒子上的绿色会根据我们灯光的强度和方向来改变成浅绿色或者深绿色。
光的方向
我们知道一个红色的物体为什么是红色?是因为这个物体吸收了我们可以从光打到表面上,表面吸收了多少能量来理解这个:

这里我们可以看上图:
- 左:光线与平面法线平行,平面接受了所有光线的能量(6根光线)
- 中:光线与平面法线夹角为 60度,平面只接受了一半光线的能量(3根光线)
- 右:平面能接受的能量与该平面的法线和入射光的角度有关。其关系是:平面接受的能量的百分比 = cos(n,l) = n·l
光的强度
我们明白了光照射到平面的方向的关系了,光的强度我们可以用烤火来理解,我们离火堆很近温度就高一些,离火堆远一点,温度就小一点,那我们就可以以火堆为圆心,火堆到我们的距离为半径,模拟成下图:

兰伯特光照模型
接合上面两个因素我们就可以得到一个公式:

这其中:
:着色点的漫反射分量 :漫反射系数,可调节。一般使用该着色点的RGB颜色。 :光照强度 :如果光照方向与表面发现反方向,是不会接收到光照的,反方向点乘为负数,所以限制为0。
Kd数值对漫反射数值的影响:

高光反射
我们平时看一些金属或者光滑的表面,我们会发现一些亮白色的块,那个就是高光。我们也可以容易的发现,当我们的眼睛移动时,物体表面的亮白色块也会移动。所以观察方向会影响到高光,相对漫反射中的条件就会多一个视角方向。这个亮块就是我们光源的反射造成的,它的颜色通常是光源的颜色。
Phong
我们要模拟高光现象,用到了Phong光照模型,我们先看高光反射示意图:

图中所示:
:光源 :法线 :光源放射 :观察
我们可以认为σ角越小时,就会看到高光。这个好比我们直接眼睛看光源,我们正对看光源,就会被闪瞎。我们就可以得到简单的公式:c = l(v·r)。
这个公式有两个问题:
- 点乘的结果会是负数
- 这样造成的亮块会比真实的亮块宽一些
为了解决这个问题我们可以把公式写成 :c = lmax(0,v·r)^p。

上图可以看出为什么我们使用指数可以把亮块变窄,也就是数值就变窄。

r 向量我们可以使用上图解释列出公式:r = −l + 2(l · n)n
然后写出最终公式:
Blinn
上述就是Phong光照模型,Blinn提出了一个简单的修改方法来得到类似的效果。它的基本思想是,避免计算反射方向 r。为此,Blinn模型引入了一个新的矢量 h,他是通过 l 和 v 的取平均后再归一化得到,也就是光源方向与视角方向的中间。根据我们漫反射公式,我们就可以写出最终公式:

公式中:
:高光反射 :高光反射系数 :用来调节高光的可是范围
Ks与p系数变化对高光的影响:

在硬件实现时,如果摄像机和光源距离模型足够远的话,Blinn模型会快于Phong模型,这是因为,此时可以认为 v 和 l 都是定值,因此 h 将是一个常量。但是,当 v 或者 l 不是定值时,Phong模型可能反而更快一些。需要注意的是,这两种光照模型都是经验模型,也就是说,我们不应该认为Blinn模型是对“正确的”Phong模型的近似。实际上,在一些情况下,Blinn模型更符合实验结果。
环境光
虽然标准光照模型的重点在于描述直接光照,但在现实的世界中,物体也可以被间接光照所照亮。间接光照指的是,光线通常会在多个物体之间反射,最后进入摄像机,也就是说,在管线进入摄像机之前,经过了不止一次的物体反射。例如,在红地毯上放置一个浅灰色的沙发,那么沙发底部也会有红色,这些红色是由红地毯反射了一部分光线,再反弹到沙发上的。
在标准光照模型中,我们使用了一种被称为环境光的部分来近似模拟间接光照。环境光的计算非常简单,它通常是个全局变量,即场景中的所有物体都使用这个环境光。下面给出计算环境光的部分:

这其中:
:着色点的环境光分量 :材质的环境系数 :场景的环境光强度
值得注意这个公式也是近似的算法。
Blinn Phong 展示
当我们计算出漫反射、高光、环境光之后,把它们加起来,就能得到 Blinn Phong 着色模型的结果了:

Blinn Phong的着色公式为:
逐像素还是逐顶点
上面,我们给出了基本光照模型使用的数学公式,那么我们在哪里计算这些光照模型呢?通常来讲,我们有两个选择:在片元着色器中计算,也被称为逐像素光照;在顶点着色器中计算,也被称为逐顶点光照。
平面着色
平面着色是指整个三角面中共用一个方向的法线,因此表现为整个三角面的颜色都是一样的。如下图:

Phong着色
在逐像素光照中,我们会以每个像素为基础,得到它的法线(可以是对顶点法线插值得到的,也可以是从法线纹理中采样得到的),然后进行光照模型的计算。这种在面片之间对顶点法线进行插值的技术被称为Phong着色,也被称为Phong插值或法线插值着色技术。这不同于我们之前讲到的Phong光照模型。
Phong着色如下图:

高洛德着色
与之相对的是逐顶点光照,也被称为高洛德着色。在逐顶点光照中,我们在每个顶点上计算光照,然后会在渲染图元内部进行线性插值,最后输出成像素颜色。**由于顶点数目往往小于像素数目,因此逐顶点光照的计算量往往要小于逐像素光照。**高洛德着色如下图:

但是,由于逐顶点光照依赖于线性插值来得到像素光照,因此,当光照模型中有非线性的计算(例如计算高光反射)时,逐顶点光照会在渲染图元内部对顶点颜色进行插值,这回导致渲染图元内部的颜色总是暗于顶点处的最高颜色,这在某些情况下会产生明显的棱角。
对比

如图所示,着色频率对结果的影响,取决于模型的复杂度。模型的复杂度越低,着色结果区别越大,但随着模型复杂度的提高,平面着色也可以达到冯氏着色的效果。因此我们不能迷信冯氏着色一定比平面着色好。
简单实现
Vector3f phong_fragment_shader(const fragment_shader_payload &payload)
{
Vector3f ka = Vector3f(0.005, 0.005, 0.005);
Vector3f kd = payload.color;
Vector3f ks = Vector3f(0.7937, 0.7937, 0.7937);
auto l1 = light{{20, 20, 20}, {500, 500, 500}};
auto l2 = light{{-20, 20, 0}, {500, 500, 500}};
std::vector<light> lights = {l1, l2};
Vector3f amb_light_intensity{10, 10, 10};
Vector3f eye_pos{0, 0, 10};
float p = 150;
Vector3f color = payload.color;
Vector3f point = payload.view_pos;
Vector3f normal = payload.normal;
Vector3f result_color = {0, 0, 0};
for (auto &light : lights)
{
// For each light source in the code, calculate what the *ambient*, *diffuse*, and *specular*
// components are. Then, accumulate that result on the *result_color* object.
Vector3f l = (light.position - point).normalized();
Vector3f I_r2 = (light.intensity / (light.position - point).squaredNorm()).normalized();
Vector3f v = (eye_pos - point).normalized();
Vector3f h = (v + l).normalized();
Vector3f n = normal.normalized();
Vector3f ambient = ka.cwiseProduct(amb_light_intensity);
Vector3f diffuse = kd.cwiseProduct(I_r2) * std::max(0.f, n.dot(l));
Vector3f specular = ks.cwiseProduct(I_r2) * pow(std::max(0.f, n.dot(h)), p);
result_color += ambient + specular + diffuse;
}
return result_color * 255.f;
}