基于Phong模型的基础光照
在本节中,我们将利用 Phong 光照模型来完成一个简单的光照场景的渲染。
一、Phong 光照模型
Phong光照模型是20世纪70年代被提出的一种渲染逼真图像的方法,模型的提出者是越南出生的计算机图形学研究员Bui Tuong Phong(1942-1975),他提出的近似方法实现了较逼真的图像。
Phong 模型是一种基于经验的光照模型,因此渲染出的图像一般不够真实,但是该模型计算消耗的性能较低,非常适合在一些对真实感要求不太高的场景中使用。
Phong 模型的计算主要由三个分量组成: 环境光项(Ambient)、漫反射项(Diffuse)以及高光反射项(Specular)。三个分量对应的光照结果如下图所示:
- 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
- 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
- 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
二、添加环境光照
环境光产生的原因一般是某种光源照亮了某个物体,这个物体将光线反射到我们观察的物体上面,使我们能够看到间接的光照,考虑到这种情况,计算就会变得非常复杂,这也是==全局光照(Global Illumination)==算法中研究的内容,Phong 模型允许我们对该部分的照明进行简化,也就是我们所说的环境光照,原理是将一个很小的颜色分量加到物体的每个片元着色计算当中,来模拟间接光照的影响。
在箱子的片元着色器中,添加一个环境光颜色:
void main()
{
float ambient_strength = 0.1;
vec3 ambient = ambient_strength * light_color;
vec3 color = ambient * object_color;
frag_color = vec4(color, 1.0);
}
这里添加了一个环境光强度,将环境光设置为光源亮度的十分之一,然后我们仅使用环境光照亮物体,得到的结果是这样的:
三、添加漫反射光照
漫反射表示粗糙物体表面向四面八方反射入射光的现象,这样不论你从哪个位置观察同一个点的光强都是一样的,也就是漫反射光跟观察者的方位无关。但是漫反射和光源的位置有关,对于某个 shading point ,其和光源连线与该 shading point 对应的物体表面的法线之间的夹角越小,漫反射光照越强,如下图:
这也很好理解, θ \theta θ 越小,物体表面接收到的光的竖直分量越多(水平分量不照亮物体)。因此,Phong 模型中对于漫反射部分的计算是这样的:
C d i f f u s e = k d C l i g h t m a x ( 0 , n ⃗ ⋅ l ⃗ ) C_{diffuse} = k_d C_{light}max(0,\vec n \cdot \vec l ) Cdiffuse=kdClightmax(0,n⋅l)
各个分量表示的含义:
- C d i f f u s e C_{diffuse} Cdiffuse 最终计算得到的漫反射颜色;
- k d k_d kd 物体的漫反射系数(可以简单理解为物体的颜色)
- C l i g h t C_{light} Clight 光源的颜色
- n n n 物体表面的法线方向(需要归一化)
- l l l 入射光的方向(便于计算一般指向光源)
因此,计算漫反射光照还需要的两个分量就是法线方向和入射光方向。
首先需要更新顶点数据,加入法线方向信息:新的顶点数据数组
然后重新指定定点布局,并在顶点着色器中进行布局指定:
Buffer_Layout layout2 = {
{Shader_Data_Type::Float3, "a_Position"},
{Shader_Data_Type::Float3, "a_normal"}
};
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec3 a_normal;
在 Phong 模型中,我们所有的光照计算都是在片元着色器中进行的,因此需要将顶点数据中的 normal 传到片元着色器中:
out vec3 v_normal;
void main()
{
v_normal = a_normal;
gl_Position = u_mvp * vec4(a_position, 1.0);
}
in vec3 v_normal;
最后就是根据之前的公式对漫反射光照进行计算,现在还缺少的变量是入射光的方向,这个也很好计算,只需要用光源的位置减去渲染目标点的位置即可,注意要使用同一个空间的坐标向量进行相减,这里我们均使用世界空间的坐标进行计算。
这里会用到 model 矩阵,所以我们将 model 矩阵作为 uniform 变量传入:
#version 330 core
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec3 a_normal;
out vec3 v_normal;
out vec3 v_world_pos;
uniform mat4 u_model;
uniform mat4 u_mvp;
void main()
{
gl_Position = u_mvp * vec4(a_position, 1.0);
v_normal = a_normal;
v_world_pos = vec3(model * vec4(a_position, 1.0));
}
片元着色器中的计算如下:
#version 330 core
out vec4 frag_color;
in vec3 v_normal;
in vec3 v_world_pos;
uniform vec3 light_color;
uniform vec3 object_color;
uniform vec3 light_pos;
void main()
{
float ambient_strength = 0.1;
vec3 ambient = ambient_strength * light_color;
vec3 light_dir = normalize(light_pos - v_world_pos);
vec3 diffuse_color = light_color * object_color * max(0.0, dot(v_normal, light_dir));
vec3 color = ambient * object_color + diffuse_color;
frag_color = vec4(color, 1.0);
}
如果一切都没问题,运行结果是这样的:
要注意的点是,我们是在世界空间中计算的光照,但是我们并没有把法向量 normal 也转化到世界空间中,那么该如何转化呢?首先要明确一点的是,法线的变换和顶点是不同的,不能像顶点一样乘上变换矩阵转化到别的空间,因为可能会出现下面的情况:
法线向量只能保证方向的一致性,而不能保证位置的一致性,所以,所有线向量必须以面的形式进行变换,具体就是要乘变换矩阵的逆转置矩阵,推导如下:
如果我们用一个法向量n = [a, b, c, d] 来描述一个平面,对于平面上的任意点p = [x, y, z, 1] 都遵循如下等式:
ntp = ax + by + cz + d = 0
如果将平面上的点进行可逆变换,乘以个可逆矩阵 R 来得到变换后的点 p1, 假设该平面做相同的变换之后对应的法向量为 n1,法向量 n 可以通过乘变换矩阵 Q 来得到 n1,这里 Q 是未知的:
p1 = R p
n1 = Q n
然后根据等式 n1tp1 = 0, 可以解得矩阵 Q
(Q n)t (R p) = 0
ntQtR p = 0
同时又有ntp = 0,所以QtR = E (单位矩阵),因此 Qt = R-1 , Q = (R-1)t 也就是逆转置矩阵。
所以为了将法线也进行变换,我们需要计算原先变换矩阵的逆转置矩阵,注意矩阵求逆是一项对于着色器开销很大的运算,因为它必须在场景中的每一个顶点上进行,所以应该尽可能地避免在着色器中进行求逆运算。最好先在CPU上计算出法线矩阵,再通过uniform把它传递给着色器(就像模型矩阵一样)。
计算逆转置矩阵并且在顶点着色器中进行声明:
glm::mat4 normal_matrix = glm::transpose(glm::inverse(model));
mvp = camera.get_view_projection_matrix() * model;
box_shader.bind();
box_shader.set_float3("light_color", glm::vec3(1.0f, 1.0f, 1.0f));
box_shader.set_float3("object_color", glm::vec3(1.0f, 0.5f, 0.31f));
box_shader.set_mat4("u_mvp", mvp);
box_shader.set_mat4("u_model", model);
box_shader.set_mat4("u_normal_matrix", normal_matrix);
v_normal = mat3(u_normal_matrix) * a_normal;
四、添加高光反射光照
和漫反射光照一样,镜面光照也决定于光的方向向量和物体的法向量,同时也决定于观察方向,比如眼睛是从什么方向看向这个着色点的。镜面光照决定于表面的反射特性。如果把物体表面设想为一面镜子,那么镜面光照最强的地方就是反射光所在的方向。
反射向量与观察方向的夹角越小,镜面光的作用就越大。
Phong 模型中,镜面高光的计算是这样的:
C s p e c u l a r = k s C l i g h t p o w ( m a x ( 0 , r ⃗ ⋅ v ⃗ ) , ρ ) C_{specular} = k_s C_{light}pow(max(0,\vec r \cdot \vec v), \rho) Cspecular=ksClightpow(max(0,r⋅v),ρ)
其中 ρ \rho ρ 是高光反射的反光度(Shininess),物体的反光度越高,反射光的能力越强,散射得越少,高光点就会越小。
因此式中需要求解的向量有反射向量 r 和视线方向 v,反射向量 r 可以通过reflect
函数来实现,v则需呀获取视点方向,也就是相机的位置,因此需要声明视点方向的全局变量:
// shader
uniform vec3 view_pos;
//cpp
box_shader.set_float3("view_pos", camera.get_position());
然后就可以在片元着色器中进行高光计算了:
vec3 view_dir = normalize(view_pos - v_world_pos);
vec3 reflect_dir = reflect(-light_dir, normal);
float specular_strength = 0.5;
vec3 specular_color = specular_strength * light_color * pow(max(dot(view_dir, reflect_dir), 0.0), 35.0);
vec3 diffuse_color = light_color * max(0.0, dot(normal, light_dir));
vec3 color = (ambient + diffuse_color + specular_color) * object_color;
通过控制反光度,可以得到下面的效果:
将光源的位置实时变化,可以得到下面的结果: