LearnOpenGL-光照章节学习笔记
颜色
我们在现实生活中看到某一物体的颜色并不是这个物体真正拥有的颜色,而是它所反射的(Reflected)颜色。换句话说,那些不能被物体所吸收(Absorb)的颜色(被拒绝的颜色)就是我们能够感知到的物体的颜色。
当把光源的颜色与物体的颜色值相乘,所得到的就是这个物体所反射的颜色(也就是我们所感知到的颜色)。
vec3 lightColor = vec3(1.0,1.0,1.0); //光是白色
vec3 toyColor = vec3(1.0,0.5,0.31);
vec3 res = light * toyColor;
创建一个光照场景
物体的片元着色器新建两个uniform变量:lightColor、objectColor
#version 330 core
out vec4 FragColor;
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
FragColor = vec4(lightColor * objectColor, 1.0);
}
我们还要创建一个光源立方体,所以要为这个物体创建专门的VAO
unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
// 只需要绑定VBO不用再次设置VBO的数据,因为箱子的VBO数据中已经包含了正确的立方体顶点数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 设置灯立方体的顶点属性(对我们的灯来说仅仅只有位置数据)
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
然后设置颜色uniform变量
// 在此之前不要忘记首先 use 对应的着色器程序(来设定uniform)
lightingShader.use();
lightingShader.setVec3("objectColor", 1.0f, 0.5f, 0.31f);
lightingShader.setVec3("lightColor", 1.0f, 1.0f, 1.0f);
当我们修改顶点或者片段着色器后,灯的位置或颜色也会随之改变,这并不是我们想要的效果。我们不希望灯的颜色在接下来的教程中因光照计算的结果而受到影响,而是希望它能够与其它的计算分离。我们希望灯一直保持明亮,不受其它颜色变化的影响(这样它才更像是一个真实的光源)。所以为其准备一个专门的片元着色器
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f);
}
然后根据需求分别计算物体和光源的mvp矩阵
基础光照
Phong光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。
关于光照模型的原理可以看 Games101学习笔记 Shading
- 环境光:其他物体的光,在Phong模型中设置为常量
- 漫反射:假设光线到物体是均匀反射出去的,模拟光源对物体方向性影响
- 镜面光照(高光反射):模拟有光泽物体上面出现的亮点。完全反射
一、环境光照
考虑物体接受来自其他物体的反射光,叫做全局光照,我们一般把环境光照设置为常量
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
二、漫反射光照
计算漫反射光照需要用到法线向量,可以直接在顶点数据中定义,顶点着色器也要声明aNormal以及输出变量Normal
计算漫反射光照:diffuse = lightColor * objectColor * max(dot(normal, lightDir), 0)。为了得到光源方向,我们需要得到光源位置(在片段着色器中设置为uniform变量 lightPos)和顶点位置(在顶点着色器中作为输出 FragPos变量)
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
uniform mat4 model;
uniform mat4 view;
uniform mat4 proj;
void main()
{
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = aNormal;
gl_Position = proj * view * vec4(FragPos, 1.0);
}
#version 330 core
out vec4 FragColor;
in vec3 Normal;
in vec3 FragPos;
uniform vec3 lightPos;
uniform vec3 lightColor;
uniform vec3 objectColor;
void main()
{
// ambient
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
}
目前片段着色器里的计算都是在世界空间坐标中进行的。所以,我们应该把法向量也转换为世界空间坐标,我们需要一个法线矩阵来进行转换,法线矩阵被定义为「model矩阵左上角3x3部分的逆矩阵的转置矩阵」,可以在顶点着色器中进行
Normal = mat3(transpose(inverse(model))) * aNormal;
矩阵求逆是一项对于着色器开销很大的运算,因为它必须在场景中的每一个顶点上进行,所以应该尽可能地避免在着色器中进行求逆运算。最好先在CPU上计算出法线矩阵,再通过uniform把它传递给着色器(就像模型矩阵一样)。
三、镜面反射
通过根据法向量翻折入射光的方向来计算反射向量。然后我们计算反射向量与观察方向的角度差,它们之间夹角越小,镜面光的作用就越大。
计算镜面反射:specular = lightColor * specularColor * max(dot(halfDir, normal)) ^ gloss
halfDir半程向量为光线方向与观察方向的角平分线方向向量,normalize(lightDir, viewDir)即可
viewDir即为摄像机位置-顶点位置
//specular
float specularStrength = 0.5;
vec3 viewDir = normalize(viewPos - FragPos);
vec3 halfDir = normalize(viewDir + lightDir);
vec3 specular = lightColor * specularStrength * pow(max(dot(norm, halfDir), 0.0), 32);
材质
如果我们想要在OpenGL中模拟多种类型的物体,我们必须针对每种表面定义不同的材质(Material)属性。我们可以分别为三个光照分量定义一个材质颜色(Material Color):环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)和镜面光照(Specular Lighting)。通过为每个分量指定一个颜色,我们就能够对表面的颜色输出有细粒度的控制了。
在片段着色器中,我们创建一个结构体(Struct)来储存物体的材质属性
#version 330 core
struct Material {
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess; //反光度(Shininess)分量,影响镜面高光的散射/半径。
};
uniform Material material;
如果要调整每个部分的光照影响度
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform Light light;
光照贴图
一、漫反射贴图
它是一个表现了物体所有的漫反射颜色的纹理图像。在着色器中使用漫反射贴图的方法和“纹理”教程是完全一样的。但这次会将纹理储存为Material结构体中的一个sampler2D。我们将之前定义的vec3漫反射颜色向量替换为漫反射贴图
struct Material {
sampler2D diffuse;
vec3 specular;
float shininess;
};
...
in vec2 TexCoords;
更新两个VAO的顶点属性指针来匹配新的顶点数据,并加载箱子图像为一个纹理。在绘制箱子之前,我们希望将要用的纹理单元赋值到material.diffuse这个uniform采样器,并绑定箱子的纹理到这个纹理单元
lightingShader.setInt("material.diffuse", 0);
...
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);
二、镜面光贴图
对于木头来说,我们不想让它镜面反射,可以将镜面反射材质设为vec3(0.0),但是对于金属边框,我们可以使用专门用于镜面光的贴图。
镜面高光的强度可以通过每个像素的亮度(每个像素都可以用一个颜色向量表示)来获取,在片元着色器中,使用采样对应的颜色值乘上镜面强度
struct Material {
sampler2D diffuse;
sampler2D specular;
float shininess;
};
.....
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
FragColor = vec4(ambient + diffuse + specular, 1.0);
在主程序中,新建一个纹理,在渲染之前先把它绑定到合适的纹理单元上
lightingShader.setInt("material.specular", 1);
...
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);
三、放射光贴图
是储存了每个片段的发光值(Emission Value)的贴图
直接用采样得到的像素值来附在纹理上就好,不受光照影响
struct Material {
sampler2D diffuse;
sampler2D specular;
sampler2D emissive;
float shininess;
};
......
//自发光
vec3 emissive = vec3(texture(material.emissive, TexCoords)).rgb;
vec3 result = (ambient + diffuse + specular + emissive) * objectColor;
FragColor = vec4(result, 1.0);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, emissiveMap);
投光物
将光投射(Cast)到物体的光源叫做投光物(Light Caster)。
一、平行光
假设光源处于无限远处的模型时,它就被称为定向光,光源的每条光线就会近似于互相平行,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。
直接使用光的direction向量而不是通过position来计算lightDir向量。
struct Light {
// vec3 position; // 使用定向光就不再需要了
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
...
void main()
{
vec3 lightDir = normalize(-light.direction);
...
}
二、点光源
光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减。
衰减
根据片元距光源的距离计算了衰减值
我们需要定义三个可配置的量:常数项
K
c
K_{c}
Kc、一次项
K
l
K_{l}
Kl、二次项
K
q
K_{q}
Kq
- 常数项通常保持1.0,用途是保证分母不会小于1,否则会发生距离增大而光线增强的效果
- 一次项以线性减少强度
- 二次项会与 d 2 d^{2} d2 相乘,二次递减
实现
需要将light.direction,改为light.position,还需要增加三个量:常数项 K c K_{c} Kc、一次项 K l K_{l} Kl、二次项 K q K_{q} Kq
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
假设光源照亮范围为50
lightingShader.setFloat("light.constant", 1.0f);
lightingShader.setFloat("light.linear", 0.09f);
lightingShader.setFloat("light.quadratic", 0.032f);
三、聚光灯
聚光是位于环境中某个位置的光源,它只朝一个特定方向而不是所有方向照射光线。OpenGL中聚光是用 一个世界空间位置、一个方向和一个切光角(Cutoff Angle) 来表示的,切光角指定了聚光的半径。
- LightDir:从片段指向光源的向量。
- SpotDir:聚光所指向的方向。
- ϕ:指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。
- θ:LightDir向量和SpotDir向量之间的夹角。在聚光内部的话θ值应该比ϕ值小。
struct Light {
vec3 position;
vec3 direction;
float cutOff;
...
};
在主程序中,直接将 cosΦ 传入。
ourShader.setVec3("light.position", camera.Position);
ourShader.setVec3("light.direction", camera.Front);
ourShader.setFloat("light.cutOff", cos(radians(12.5f)));
在片元着色器中,我们要对θ和Φ进行比较,如果 θ < Φ,那么就可以进行正常光照计算,反之就返回环境光照。要实现两个角度的比较,可以通过比较余弦值的大小(就不用耗性能的求反三角函数来得到角度)
float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff)
{
// 执行光照计算
}
else // 否则,使用环境光,让场景在聚光之外时不至于完全黑暗
color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
平滑边缘
为了让聚光灯的边缘平滑,模拟聚光灯时需要一个外圆锥(再创建一个)和内圆锥(上面的圆锥)
为了创建一个外圆锥,我们只需要再定义一个余弦值γ来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个0.0到1.0之间的强度值。如果片段在内圆锥之内,它的强度就是1.0,如果在外圆锥之外强度值就是0.0。
- ϵ=ϕ−γ,内(ϕ)和外圆锥(γ)之间的余弦值差
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);
.....
// 将不对环境光做出影响,让它总是能有一点光
diffuse *= intensity;
specular *= intensity;
- clamp函数把第一个参数约束(Clamp)在了0.0到1.0之间。这保证强度值不会在[0, 1]区间之外
- 不需要再判断θ 和 Φ,因为已经有了强度值的限制
多光源
要进行多个光源计算,如果都放在main函数里那么会非常冗杂,可以为每种灯光设置相应的函数,然后汇总到main函数里
out vec4 FragColor;
void main()
{
// 定义一个输出颜色值
vec3 output;
// 将定向光的贡献加到输出中
output += someFunctionToCalculateDirectionalLight();
// 对所有的点光源也做相同的事情
for(int i = 0; i < nr_of_point_lights; i++)
output += someFunctionToCalculatePointLight();
// 也加上其它的光源(比如聚光)
output += someFunctionToCalculateSpotLight();
FragColor = vec4(output, 1.0);
}
一、平行光(定向光)
可以在片元着色器中专门声明一个定向光的结构体
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform DirLight dirLight;
然后将结构体传入以下函数
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
vec3 lightDir = light.direction;
vec3 halfDir = normalize(lightDir + viewDir);
vec3 diffuse = light.diffuse * vec3(texture(material.diffuse, TexCoords)) * max(dot(lightDir, normal), 0.0);
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
vec3 specular = light.specular * vec3(texture(material.specular, TexCoords)) * pow(max(dot(halfDir, normal), 0.0), material.shininess);
return (diffuse + ambient + specular);
}
二、点光源
点光源位置、衰减…定义一个点光源结构体(需要四个点光源,在GLSL中使用了预处理指令来定义了我们场景中点光源的数量)
struct PointLight {
vec3 position;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
#define NR_POINT_LIGHTS 4
uniform PointLight pointLights[NR_POINT_LIGHTS];
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
float distance = length(light.position - fragPos);
vec3 lightDir = normalize(lightPos - fragPos);
vec3 halfDir = normalize(lightDir + viewDir);
float atten = 1.0 / (light.constant + light.linear * distance + light.quadratic * distance * distance);
vec3 diffuse = light.diffuse * vec3(texture(material.diffuse, TexCoords)) * max(dot(normal, lightDir)) * atten;
vec3 specular = light.specular * vec3(texture(material.specular, TexCoords)) * pow(max(dot(normal, halfDir), 0.0), material.shininess) * atten;
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords)) * atten;
return(diffuse + specular + ambient);
}