本文最后更新于 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}
其中,Fatt是衰减率,用于乘以光照强度。
Kc是常数项,通常为1.0,用于保证分母大于1,使得衰减率始终随距离增大而减小。
Kl是一次项系数,以线性方式减少强度。
Kq是二次项系数,当距离较大时,二次项的影响会更加显著。。
经过该公式计算的衰减率乘以光强,最终得到的亮度如下:

三个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)确定。切光角指定了光锥体的半径。
如图所示,θ代表图元和聚光方向的夹角,ϕ代表切光角。
具体计算过程为:首先判断θ的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); }
|