LearnOpenGL学习笔记(六) - 材质、光照贴图与投光物

本文最后更新于 2024年8月6日 下午

有趣。

材质

在通常的着色器编写中,并不是直接使用 objectColor 计算表面颜色的,而是使用材质(Material)。材质定义了物体表面的反射特性,包含环境光、漫反射率和镜面反射率等属性。

与 C 语言类似,OpenGL 也可以定义结构体来组织数据。以下示例展示了如何在 GLSL 中定义一个 Material 结构体,并将其用作 uniform 变量:

1
2
3
4
5
6
7
8
#version 330 core
struct Material {
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};
uniform Material material;

环境光

ambient 材质向量定义了在环境光照下,这个表面反射的颜色。环境光通常是模拟全局光照的,它在所有方向上均匀地影响物体的每个部分。ambient 通常设置为表面的基础颜色,以确保即使在阴影中物体也能被轻微看到。

漫反射

diffuse 材质向量定义了表面的漫反射颜色。漫反射模拟了光在粗糙表面上的扩散反射,使光在多个方向上散射。漫反射颜色设置为期望的物体颜色,因为它直接影响物体在被光照射时的可见颜色。

镜面反射

specular 材质向量设置了表面的镜面反射颜色。它决定了光在表面上的镜面高光颜色。这种反射通常用于模拟光滑表面的光泽度或闪亮效果。高光的颜色可以是白色的(表示强光反射),也可以是其他颜色,具体取决于表面材料的特性。

镜面反射度

shininess 参数影响镜面高光的散射程度或半径。较高的 shininess 值会使高光更加集中和尖锐,模拟光滑或抛光的表面;较低的 shininess 值会使高光更加扩散和柔和,模拟粗糙的表面。

材质属性的设置需要丰富的实践。

光源

通常来说,物体对于环境光、漫反射光和高光的反射力度是不同的。材质描述了物体在反射这三类光时的颜色属性,而反射力度是另一种截然不同的属性。

1
2
3
4
5
6
7
struct Light {
vec3 position;
vec3 ambient; //环境光影响系数
vec3 diffuse; //漫反射率
vec3 specular; //镜面反射率
};
uniform Light light;

一般,环境光的反射力度较小,在0.1f左右。漫反射可以在0.5f-0.7f左右,而高光一般都为1.0f。

我该如何理解漫反射、镜面反射率?

与漫反射颜色不同,漫反射率是指物体对漫反射颜色中R、G、B分量的反射程度。

假设漫反射率为(0.2f, 0.3f, 0.4f),那么,漫反射颜色中,有20%的红色能被漫反射到观察者视角中,30%的绿色以及40%的红色同理。

镜面反射率也是如此。镜面反射率通常被设置为(1.0f, 1.0f, 1.0f),因为镜面反射一般直接反映出光源的颜色。

光照贴图

之前,我们对材质三个光照分类的控制,是使用传入uniform来实现的。但实际上,我们经常会遇到一个物体的不同部分是不同材质的情况。为了处理这种情况,我们引入光照贴图(Map)的概念,对材质的不同区域设置不同的光照分量强度。

贴图,类似于纹理,也是一种覆盖物体的图像。它允许着色器逐片段索引其中的颜色值。

和纹理一样,在着色器内使用sampler2D类型定义采样器,并使用texture(sampler2D tex, vec2 uv)函数采样。

漫反射贴图

漫反射贴图(Diffuse Map)可以看作是传统光照模型(如Phong、Blinn-Phong)中的Base-Color。它表现了物体本身的颜色。

1
2
3
4
5
6
7
8
9
10
struct Material {
sampler2D diffuse;
vec3 specular;
float shininess;
};
...
in vec2 TexCoords;
...
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 ambient = vec3(texture(material.diffuse,TexCoords))*lightColor*light.ambient;

这里移除了结构体内的vec3 ambient属性,因为几乎在所有情况下,环境光颜色都等于漫反射颜色,所以环境光用漫反射贴图进行采样。

高光贴图

高光贴图用于控制高光分量。

vec3 specular = specularRatio * lightColor * vec3(texture(material.specular, TexCoords)) * light.specular

投光物

将光投射到物体的光源叫做投光物(Light Caster)。

平行光

平行光,又称定向光(Directional Light),投射的所有光线都来自于同一方向,与光源的位置无关。

太阳光被视为一种平行光。

对于定向光,其结构体中只需包含一个方向向量和三个光照分量即可。

1
2
3
4
5
6
struct Light {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#version 330 core

struct Material {
sampler2D diffuse;
sampler2D specular;
float shininess;
};

struct Light {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
vec3 color;
float strength;
};

uniform Light light;
uniform Material material;
uniform vec3 viewPos;
uniform float time;

in vec3 norm;
in vec3 fragPos;
in vec2 TexCoords;
out vec4 FragColor;

void main() {
// 计算光源方向
vec3 lightDir = normalize(-light.direction);

// 计算环境光
vec3 ambient = vec3(texture(material.diffuse, TexCoords)) * light.ambient;

// 计算漫反射光
vec3 normal = normalize(norm);
float diffuseStrength = max(dot(normal, lightDir), 0.0);
vec3 diffuse = vec3(texture(material.diffuse, TexCoords)) * diffuseStrength * light.diffuse;

// 计算高光
vec3 viewDir = normalize(viewPos - fragPos);
vec3 reflectDir = reflect(-lightDir, normal);
float specularStrength = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = specularStrength * vec3(texture(material.specular, TexCoords)) * light.specular;

// 合并所有光照效果
vec3 result = ambient + diffuse + specular;

// 输出最终颜色
FragColor = vec4(result*light.color*light.strength, 1.0f);
}

点光源

与平行光不同,点光源(Point Light)的光线会随距离衰减(Attenuation)。

在现实世界中,灯在近处通常会非常亮,但随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。为了模拟这一过程,我们使用下列公式:

\begin{equation} F_{att} = \frac{1.0}{K_c + K_l * d + K_q * d^2} \end{equation}

其中,FattF_{att}是衰减率,用于乘以光照强度。

KcK_{c}是常数项,通常为1.0,用于保证分母大于1,使得衰减率始终随距离增大而减小。 KlK_{l}是一次项系数,以线性方式减少强度。 KqK_{q}是二次项系数,当距离较大时,二次项的影响会更加显著。。

经过该公式计算的衰减率乘以光强,最终得到的亮度如下:

img

三个K值的具体值设置需要实践经验。一次项系数越小,光源覆盖的距离越大,二次项系数的变化趋势与一次项系数相同,但它比一次项系数小更多。

距离 常数项 一次项 二次项
7 1.0 0.7 1.8
13 1.0 0.35 0.44
20 1.0 0.22 0.20
32 1.0 0.14 0.07
50 1.0 0.09 0.032
65 1.0 0.07 0.017
100 1.0 0.045 0.0075
160 1.0 0.027 0.0028
200 1.0 0.022 0.0019
325 1.0 0.014 0.0007
600 1.0 0.007 0.0002
3250 1.0 0.0014 0.000007

具体实现衰减同样需要修改着色器中的Light结构体,并把计算得到的衰减值乘以三个光照分量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct Light {
//光源位置
vec3 position;
//材质光照分量
vec3 ambient;
vec3 diffuse;
vec3 specular;
//灯光自身属性
vec3 color;
float strength;
//点光源衰减参数
float constant;
float linear;
float quadratic;
};

void main(){
float distance = length(fragPos-light.position);
float attenuation = 1.0/(light.constant+distance*light.linear+distance*distance*light.quadratic);
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
// 合并所有光照效果
vec3 result = ambient + diffuse + specular;
// 输出最终颜色
FragColor = vec4(result*light.color*light.strength, 1.0f);
}

聚光

聚光(Spotlight)是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。

聚光可以用一个世界坐标、一个方向和一个切光角(Cufoff Angle)确定。切光角指定了光锥体的半径。

image-20240717122930242

如图所示,θ代表图元和聚光方向的夹角,ϕ代表切光角。

具体计算过程为:首先判断θ的cos值,若大于cosϕ,则说明片段位于光锥体内,执行光照计算(与点光源相同)。若小于,则直接输出环境光色。

使用smoothstep(float t1, float t2, float x)函数来生成平滑边缘。

当x小于t1时,函数返回0;x大于t2时,函数返回1;x位于[t1, t2]时,进行平滑插值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
struct Light {
vec3 direction;
vec3 position;
float cutoff;
float outer;
vec3 ambient;
vec3 diffuse;
vec3 specular;
vec3 color;
float strength;
};
...
void main() {
//计算聚光区域
vec3 light_frag_dir = normalize(fragPos-light.position);
float theta = dot(light_frag_dir,normalize(light.direction));
float spotRange = smoothstep(light.outer,light.cutoff,theta);
vec3 lightDir = -light.direction;
// 计算环境光
vec3 ambient = vec3(texture(material.diffuse, TexCoords)) * light.ambient;
// 计算漫反射光
vec3 normal = normalize(norm);
float diffuseStrength = max(dot(normal, lightDir), 0.0);
vec3 diffuse = vec3(texture(material.diffuse, TexCoords)) * diffuseStrength * light.diffuse;
// 计算高光
vec3 viewDir = normalize(viewPos - fragPos);
vec3 reflectDir = reflect(-lightDir, normal);
float specularStrength = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = specularStrength * vec3(texture(material.specular, TexCoords)) * light.specular;
// 合并所有光照效果
vec3 result = ambient + diffuse*spotRange + specular*spotRange;
// 输出最终颜色
FragColor = vec4(result*light.color*light.strength, 1.0f);
}

多光源

GLSL中的函数和C函数很相似,它有一个函数名、一个返回值类型,如果函数不是在main函数之前声明的,我们还必须在代码文件顶部声明一个原型。

为了实现多光源效果,我们需要将每个光源对各光照分量的贡献进行累加。

对于数组类型的uniform,使用“pointLights[0].position”来访问。

多个光源对片段的影响就是简单的相加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
uniform DirectionalLight dirLight;
uniform SpotLight spotLight;
uniform PointLight pointLights[NR_POINT_LIGHTS];

vec3 CalcDirLight(DirectionalLight light, vec3 normal, vec3 viewDir){
vec3 lightDir = normalize(-light.direction);
vec3 ambient = light.ambient * texture(material.diffuse,TexCoords).rgb;
vec3 diffuse = max(dot(lightDir,normal),0.0f)*light.diffuse*texture(material.diffuse,TexCoords).rgb;
vec3 reflectDir = reflect(-lightDir,normal);
vec3 specular = pow(max(dot(viewDir,reflectDir),0.0f),material.shininess)*light.specular*texture(material.specular,TexCoords).rgb;
return (ambient+diffuse+specular)*light.color*light.strength;
}

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 viewDir, vec3 fragPos){
float distance = length(light.position - fragPos);
float attenuation = 1/(light.constant+light.linear*distance+light.quadratic*distance*distance);
vec3 lightDir = normalize(light.position - fragPos);
vec3 ambient = light.ambient * texture(material.diffuse,TexCoords).rgb;
vec3 diffuse = max(dot(lightDir,normal),0.0f)*light.diffuse*texture(material.diffuse,TexCoords).rgb;
vec3 reflectDir = normalize(reflect(-lightDir,normal));
vec3 specular = pow(max(dot(viewDir,reflectDir),0.0f),material.shininess)*light.specular*texture(material.specular,TexCoords).rgb;
return (ambient+diffuse+specular)*attenuation*light.color*light.strength;
}

vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir){
float distance = length(light.position - fragPos);
float attenuation = 1/(light.constant+light.linear*distance+light.quadratic*distance*distance);

vec3 lightFragVec = normalize(fragPos-light.position);
float theta = dot(lightFragVec,normalize(light.direction));
float spotRange = smoothstep(light.outer,light.cutoff,theta);
vec3 lightDir = -light.direction;
vec3 ambient = light.ambient * texture(material.diffuse,TexCoords).rgb;
vec3 diffuse = max(dot(lightDir,normal),0.0f)*light.diffuse*texture(material.diffuse,TexCoords).rgb;
vec3 reflectDir = normalize(reflect(-lightDir,normal));
vec3 specular = pow(max(dot(viewDir,reflectDir),0.0f),material.shininess)*light.specular*texture(material.specular,TexCoords).rgb;
return ((ambient+diffuse)*spotRange+specular)*attenuation*light.color*light.strength;
}

void main(){
vec3 normal = normalize(norm);
vec3 viewDir = normalize(viewPos-fragPos);
vec3 result = vec3(0.0);
for(int i = 0; i < NR_POINT_LIGHTS; i++){
result += CalcPointLight(pointLights[i], normal,viewDir,fragPos);
}
result += CalcDirLight(dirLight,normal,viewDir);
if(spotLightSwitch){
result += CalcSpotLight(spotLight, normal, fragPos,viewDir);
}
FragColor = vec4(result,1.0f);
}

LearnOpenGL学习笔记(六) - 材质、光照贴图与投光物
http://example.com/2024/07/17/LearnOpenGL学习笔记(六)-材质、光照贴图与投光物/
作者
Yoi
发布于
2024年7月17日
许可协议