按照冯氏光照模型的说法,任何光线都被分割成环境光照、漫反射光照和镜面光照三部分,这三部分分别与物体产生作用叠加后得到我们看到的颜色。但是实际上在现实中有许多种不同的光源(Light Source),比如太阳、蜡烛、手电筒、灯泡等等,仔细观察可以发现这些光源发射光线的方式是不同的,概括起来有三种:

- 方向光(Directional Light):又称定向光、平行光,这种光源发射出来的光线是平行光线,每一条光线的方向是相同的,太阳光可以看成是方向光;
- 点光源(Point Light):一个点光源朝四周发射出放射状的光线,灯泡、蜡烛等就是这种光源;
- 聚光灯(Spot Light):光源发射出一个圆锥体光,圆锥体外部没有光线,内部光线强度分布表现为靠近圆锥边缘的光强会逐渐变小,手电筒、舞台投光灯等就是这种光源
当然这三种光源不能概括现实中所有的光源,还有更加复杂的,比如灯管就不包括在上述三种灯源中(可以用一排点光源模拟)。话虽如此,这三种光源还是能代表大多数真实光源的,如果用过unity,应该就知道它也提供了这三种光源类型,并且只有这三种才支持实时渲染。
所以我们也用OpenGL去模拟这三种光源的光照效果,并且实现片段着色器的编写。
方向光
方向光(平行光)的特点就是光的照射方向不变,并且光照强度不随着空间变化而衰减。这是因为我们默认把太阳光当成最重要的平行光,而太阳距离地球很远,在地球上的一点点距离变化导致的衰减可以忽略不计,但若是人工制造的平行光(比如用凸透镜+点光源制造的平行光)就必须要考虑衰减了。为了方便起见,渲染时就不考虑衰减了。
方向光需要考虑的参数包括:
- 光照方向 d i r e c t i o n direction direction ;
- 三个光照分量 a m b i e n t ambient ambient, d i f f u s e diffuse diffuse, s p e c u l a r specular specular ;
有了这几个参数,按照冯氏光照模型的方法计算三个光照分量的值,然后累加即可。
点光源
点光源的特点是光线呈放射状,并且光照强度随着距离的增大而衰减(Attenuation)。这里的衰减是两方面的:一方面点光源是球面波,按照能量守恒,光强会按
1
R
2
\frac{1}{R^2}
R21衰减,原理就是反比于球体的表面积;另一方面光与介质发生作用而导致能量形式转变,光能变成了热能等等,所以会衰减。假设待渲染的某个片段和光源的距离为
d
d
d ,那么该片段处的光照衰减值近似为:
L
u
m
i
n
o
s
i
t
y
=
1.0
Q
u
a
d
r
a
t
i
c
∗
d
2
+
L
i
n
e
a
r
∗
d
+
C
o
n
s
t
a
n
t
Luminosity=\frac{1.0}{Quadratic*d^2+Linear*d+Constant}
Luminosity=Quadratic∗d2+Linear∗d+Constant1.0
式中的三个参数
Q
u
a
d
r
a
t
i
c
,
L
i
n
e
a
r
,
C
o
n
s
t
a
n
t
{Quadratic,Linear,Constant}
Quadratic,Linear,Constant 的取值可以参考这里,一般来说,常数项可以保持为
1.0
1.0
1.0不变,而一次项和二次项的系数随着覆盖距离的增大而减小,因为越小的系数在越大的距离才会衰减到一定小的值。
点光源需要考虑的参数包括:
- 光源位置 p o s i t i o n position position;
- 三个衰减系数 Q u a d r a t i c , L i n e a r , C o n s t a n t {Quadratic,Linear,Constant} Quadratic,Linear,Constant
- 三个光照分量 a m b i e n t ambient ambient, d i f f u s e diffuse diffuse, s p e c u l a r specular specular ;
因此我们计算完三个光照分量的值之后,还需要乘上 L u m i n o s i t y Luminosity Luminosity ,才得到最终的结果。
聚光灯
聚光灯的特点除了包括点光源的所有特点之外,还把光照限制在了一个圆锥体内部,因此聚光灯不可避免的有一个光照方向。在圆锥体的内部的同一圆切面中,光照强度的分布也不是均匀的,体现为偏离中心比较多的光照强度会变小。因此我们给聚光灯定义内圆锥面和外圆锥面,在同一个圆形切面中,内圆锥面内部的光强相等,内圆锥面与外圆锥面之间的光强线性递减到 0 。

定义内圆锥半角(圆锥半角就是母线和轴的夹角)为
α
\alpha
α,外圆锥半角为
β
\beta
β,被照射位置和光源的连线与轴的夹角为
θ
\theta
θ ,那么新的一个光强系数满足:
I
n
t
e
n
s
i
t
y
=
c
o
s
θ
−
c
o
s
β
c
o
s
α
−
c
o
s
β
Intensity=\frac{cos\theta-cos\beta}{cos\alpha-cos\beta}
Intensity=cosα−cosβcosθ−cosβ
把
I
n
t
e
n
s
i
t
y
Intensity
Intensity **约束(Clamp)**在
[
0
,
1
]
[0,1]
[0,1]之间,就得到了想要的结果。
聚光灯需要考虑的参数包括:
- 光照方向 d i r e c t i o n direction direction ;
- 光源位置 p o s i t i o n position position;
- 内外圆锥半角的余弦值 i n n e r C o s , o u t e r C o s innerCos,outerCos innerCos,outerCos;
- 三个衰减系数 Q u a d r a t i c , L i n e a r , C o n s t a n t {Quadratic,Linear,Constant} Quadratic,Linear,Constant
- 三个光照分量 a m b i e n t ambient ambient, d i f f u s e diffuse diffuse, s p e c u l a r specular specular ;
在计算完三个光照分量的值之后,乘上 L u m i n o s i t y ∗ I n t e n s i t y Luminosity*Intensity Luminosity∗Intensity,就得到最终的结果。
片段着色器代码
在OpenGL中编写片段着色器的GLSL代码,把每种光源需要的参数封装成结构体,每种光源计算得到RGB的过程封装成函数,然后可以任意定义任意个数量的光源,然后累加起来光照强度得到最终的颜色输出。具体的代码已经注释得很详细:
#version 430 core
// 从前面传下来的数据
in vec3 fragNormal; // 片段法向量(非单位向量)
in vec3 FragPos; // 片段位置
in vec2 TexCoords; // 纹理采样坐标
// 片段着色器的颜色输出
out vec4 FragColor;
// 定义物体材质,因为这里用了图片纹理做光照贴图,因此就可以用采样器直接获得对应坐标的RGB
// 也可以直接定义纯色的RGB向量
struct Material
{
sampler2D diffuse; // 漫反射纹理
sampler2D specular; // 镜面反射纹理
float shininess; // 反光度:越高,反光能力越强,散射的越少,高光点越小
};
uniform Material material; // 物体材质
uniform vec3 viewPos; // 观察者位置
// 各个光源类型的定义,以及对应求解反射光的函数定义
// 函数参数的normal、viewDir必须是单位向量
//----------------- 方向光 ------------------
struct DirLight
{
vec3 direction; // 光照方向
// 三个光照分量
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
vec3 calDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
vec3 lightDir = normalize(-light.direction);
// 环境光照
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
// 漫反射光照
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
// 镜面光照
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); // 材质中【反光度】决定的系数
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
// 合并
return (ambient + diffuse + specular);
}
//-------------------------------------------
//----------------- 点光源 ------------------
struct PointLight
{
vec3 position; // 光源位置
// 三个衰减系数
float constant;
float linear;
float quadratic;
// 三个光照分量
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
vec3 calPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// 环境光照
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
// 漫反射光照
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
// 镜面光照
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); // 材质中【反光度】决定的系数
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
// 合并,考虑光强衰减
float distance = length(light.position - fragPos);
float luminosity = 1.0 / (light.constant + light.linear * distance + light.quadratic * distance * distance); // 光强衰减系数
return (ambient + diffuse + specular) * luminosity;
}
//-------------------------------------------
//----------------- 聚光灯 ------------------
struct SpotLight
{
vec3 direction; // 光照方向
vec3 position; // 光源位置
float innerCos; // 内圆锥半角余弦值
float outerCos; // 外圆锥半角余弦值
// 三个衰减系数
float constant;
float linear;
float quadratic;
// 三个光照分量
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
vec3 calSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
vec3 lightDir = normalize(light.position - fragPos);
// 环境光照
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
// 漫反射光照
float diff = max(dot(normal, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
// 镜面光照
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); // 材质中【反光度】决定的系数
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
// 考虑光强衰减
float distance = length(light.position - fragPos);
float luminosity = 1.0 / (light.constant + light.linear * distance + light.quadratic * distance * distance); // 光强衰减系数
// 从这里开始加入聚光灯的判断
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.innerCos - light.outerCos;
float intensity = clamp((theta - light.outerCos) / epsilon, 0.0, 1.0); // 聚光灯决定的光强系数
return (ambient + diffuse + specular) * luminosity * intensity;
}
//-------------------------------------------
// !!!!! 前面部分定义了一些到目前为止必需的东西,而后面可以根据自己的需要进行编写 !!!!!
// 比如后面,我定义了一个方向光、一个聚光灯和四个点光源
uniform DirLight dirLight;
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];
uniform SpotLight spotLight;
uniform int isSpotLight; // 用这个控制聚光灯的亮灭
void main()
{
vec3 norm = normalize(fragNormal); // 法向量的单位向量
vec3 viewDir = normalize(viewPos - FragPos); // 观察方向的单位向量
vec3 result = vec3(0.0f, 0.0f, 0.0f);
// 后面直接调用函数就可以很方便地计算出每种光照的影响
result += calDirLight(dirLight, norm, viewDir);
for(int i = 0; i < NR_POINT_LIGHTS; i++)
result += calPointLight(pointLights[i], norm, FragPos, viewDir);
if(isSpotLight == 1)
result += calSpotLight(spotLight, norm, FragPos, viewDir);
FragColor = vec4(result, 1.0f);
}
Shader的参数比较多,一定小心注意要把所有用到的uniform都赋好值。