光照章节学习
颜色
与自然光一样,物体的颜色是因为反射了这种颜色的光,我们也赋予物体一个颜色,使用乘法,使物体的颜色得以呈现
glm::vec3 lightColor(0.0f, 1.0f, 0.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (0.0f, 0.5f, 0.0f);
- 我们可以定义物体的颜色为物体从一个光源反射各个颜色分量的大小
比如(0,1,0)就是只反射绿色的光
创建一个光照场景
1.顶点着色器创建箱子
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
2.绑定VBO,设置灯的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);
3.创建片段着色器
#version 330 core
out vec4 FragColor;
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
//光和物体颜色相乘就是实际的颜色
FragColor = vec4(lightColor * objectColor, 1.0);
}
创建灯的着色器
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0); // 将向量的四个分量全部设置为1.0
}
4.使用
// 在此之前不要忘记首先 use 对应的着色器程序(来设定uniform)
lightingShader.use();
lightingShader.setVec3(“objectColor”, 1.0f, 0.5f, 0.31f);
lightingShader.setVec3(“lightColor”, 1.0f, 1.0f, 1.0f);
5.更改灯的位置
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);
然后我们把灯位移到这里,然后将它缩小一点,让它不那么明显:
model = glm::mat4();
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f));
6.绘制灯立方体
lampShader.use();
// 设置模型、视图和投影矩阵uniform
…
// 绘制灯立方体对象
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
基础光照
冯氏光照模型
- 环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光),所以物体几乎永远不会是完全黑暗的。为了模拟这个,我们会使用一个环境光照常量,它永远会给物体一些颜色。
- 漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。
- 镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。镜面光照的颜色相比于物体的颜色会更倾向于光的颜色。
环境光照
自然光可以反弹(全局光照),但算法太耗,所以弄个简单的
把环境光照添加到场景里非常简单。我们用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色,然后将最终结果作为片段的颜色:
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor; FragColor = vec4(result, 1.0);}
漫反射光照
根据法向量计算光源夹角,夹角越小,光对颜色影响越大
法向量
1.我们能够使用一个小技巧,使用叉乘对立方体所有的顶点计算法向量,但是由于3D立方体不是一个复杂的形状,所以我们可以简单地把法线数据手工添加到顶点数据中
2.更新光照的顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
…
3.我们只想使用每个顶点的前三个float,并且忽略后三个float,所以我们只需要把步长参数改成float大小的6倍就行了。
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
4.传递给片段着色器
out vec3 Normal;
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
Normal = aNormal;
}
5.片段着色器中拿两个向量计算角度
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos)
6.
对norm和lightDir向量进行点乘,计算光源对当前片段实际的漫发射影响。结果值再乘以光的颜色,得到漫反射分量。两个向量之间的角度越大,漫反射分量就会越小:
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;;
最后一件事
片段着色器都是在世界坐标下计算的,我们法向量也要变换到世界空间坐标下,对于法向量,位移没有意义,所以只需要旋转和缩放
,所以直接抛弃模型矩阵中的w分量,或者设为0,可以保证不位移
其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。因此,我们不能用这样的模型矩阵来变换法向量。下面的图展示了应用了不等比缩放的模型矩阵对法向量的影响:
修复这个行为的诀窍是使用一个为法向量专门定制的模型矩阵。这个矩阵称之为法线矩阵(Normal Matrix)
在顶点着色器中,我们可以使用inverse和transpose函数自己生成这个法线矩阵
Normal = mat3(transpose(inverse(model))) * aNormal;
矩阵求逆是一项对于着色器开销很大的运算,因为它必须在场景中的每一个顶点上进行,所以应该尽可能地避免在着色器中进行求逆运算。以学习为目的的话这样做还好,但是对于一个高效的应用来说,你最好先在CPU上计算出法线矩阵,再通过uniform把它传递给着色器(就像模型矩阵一样)。
镜面光照
通过根据法向量翻折入射光的方向来计算反射向量。然后我们计算反射向量与观察方向的角度差,它们之间夹角越小,镜面光的作用就越大
所以多一个观察向量
//传入观察向量
uniform vec3 viewPos;
lightingShader.setVec3(“viewPos”, camera.Position);
//设置镜面反射强度
float specularStrength = 0.5;
//计算反射
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
//求反射和观察的夹角
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = specularStrength * spec * lightColor;
//剩下的最后一件事情是把它加到环境光分量和漫反射分量里,再用结果乘以物体的颜色:
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
材质
在上一节中,我们定义了一个物体和光的颜色,并结合环境光与镜面强度分量,来决定物体的视觉输出。当描述一个表面时,我们可以分别为三个光照分量定义一个材质颜色(Material Color):环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)和镜面光照(Specular Lighting)
#version 330 core
struct Material {
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};
uniform Material material;
ambient材质向量定义了在环境光照下这个表面反射的是什么颜色,通常与表面的颜色相同。diffuse材质向量定义了在漫反射光照下表面的颜色。漫反射颜色(和环境光照一样)也被设置为我们期望的物体颜色。specular材质向量设置的是表面上镜面高光的颜色(或者甚至可能反映一个特定表面的颜色)。最后,shininess影响镜面高光的散射/半径。
我们在片段着色器中创建了一个材质结构体的uniform,所以下面我们希望修改一下光照的计算来遵从新的材质属性。由于所有材质变量都储存在一个结构体中,我们可以从uniform变量material中访问它们:
void main()
{
// 环境光
vec3 ambient = lightColor * material.ambient;
// 漫反射 vec3 norm = normalize(Normal); vec3 lightDir = normalize(lightPos - FragPos); float diff = max(dot(norm, lightDir), 0.0); vec3 diffuse = lightColor * (diff * material.diffuse); // 镜面光 vec3 viewDir = normalize(viewPos - FragPos); vec3 reflectDir = reflect(-lightDir, norm); float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess); vec3 specular = lightColor * (spec * material.specular); vec3 result = ambient + diffuse + specular; FragColor = vec4(result, 1.0);}
设置材质结构体的uniform
lightingShader.setVec3(“material.ambient”, 1.0f, 0.5f, 0.31f);
lightingShader.setVec3(“material.diffuse”, 1.0f, 0.5f, 0.31f);
lightingShader.setVec3(“material.specular”, 0.5f, 0.5f, 0.5f);
lightingShader.setFloat(“material.shininess”, 32.0f);
我们可以用同样的方式影响光源的漫反射和镜面光强度。这和我们在上一节中所做的极为相似,你可以认为我们已经创建了一些光照属性来影响各个光照分量。我们希望为光照属性创建类似材质结构体的东西:
struct Light {
vec3 position;
vec3 ambient; vec3 diffuse; vec3 specular;};
uniform Light light;
vec3 ambient = light.ambient * material.ambient;
vec3 diffuse = light.diffuse * (diff * material.diffuse);
vec3 specular = light.specular * (spec * material.specular);
lightingShader.setVec3(“light.ambient”, 0.2f, 0.2f, 0.2f);
lightingShader.setVec3(“light.diffuse”, 0.5f, 0.5f, 0.5f); // 将光照调暗了一些以搭配场景
lightingShader.setVec3(“light.specular”, 1.0f, 1.0f, 1.0f);
光照贴图
漫反射贴图
在着色器中使用漫反射贴图的方法和纹理教程中是完全一样的。但这次我们会将纹理储存为Material结构体中的一个sampler2D。我们将之前定义的vec3漫反射颜色向量替换为漫反射贴图
struct Material {
sampler2D diffuse;
vec3 specular;
float shininess;
};
…
in vec2 TexCoords;
//通过纹理采样器拿到颜色值
vec3 diffuse = light.diffuse * diff * vec3(texture(material.diffuse, TexCoords));
//不要忘记将环境光的材质颜色设置为漫反射材质颜色同样的值。
vec3 ambient = light.ambient * vec3(texture(material.diffuse, TexCoords));
镜面光贴图
一张黑白贴图,根据黑白获取反射颜色.
采样镜面光贴图
lightingShader.setInt(“material.specular”, 1);
…
glActiveTexture(GL_TEXTURE1);# 使用不同的纹理单元,1单元
glBindTexture(GL_TEXTURE_2D, specularMap);
接下来更新片段着色器的材质属性,让其接受一个sampler2D而不是vec3作为镜面光分量:
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);
投光物
平行光
无限远的平行光
struct Light {
// vec3 position; // 使用定向光就不再需要了
vec3 direction;
vec3 ambient; vec3 diffuse; vec3 specular;};
…
void main()
{
vec3 lightDir = normalize(-light.direction);
…
}
点光源
衰减
Fatt=1.0/Kc+Kl∗d+Kq∗d2
在这里d代表了片段距光源的距离。接下来为了计算衰减值,我们定义3个(可配置的)项:常数项Kc、一次项Kl和二次项Kq。
常数项通常保持为1.0,它的主要作用是保证分母永远不会比1小,否则的话在某些距离上它反而会增加强度,这肯定不是我们想要的效果。
一次项会与距离值相乘,以线性的方式减少强度。
二次项会与距离的平方相乘,让光源以二次递减的方式减少强度。二次项在距离比较小的时候影响会比一次项小很多,但当距离值比较大的时候它就会比一次项更大了。
距离 常数项 一次项 二次项
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
实现衰减
为了实现衰减,在片段着色器中我们还需要三个额外的值:也就是公式中的常数项、一次项和二次项。储存在光的结构体中
struct Light {
vec3 position;
vec3 ambient; vec3 diffuse; vec3 specular; float constant; float linear; float quadratic;};
我们仍需要公式中距光源的距离,还记得我们是怎么计算一个向量的长度的吗?我们可以通过获取片段和光源之间的向量差,并获取结果向量的长度作为距离项。我们可以使用GLSL内建的length函数来完成这一点:
float distance = length(light.position - FragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
接下来,我们将包含这个衰减值到光照计算中,将它分别乘以环境光、漫反射和镜面光颜色。
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
聚光
LightDir:从片段指向光源的向量。
SpotDir:聚光所指向的方向。
Phiϕ:指定了聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。
Thetaθ:LightDir向量和SpotDir向量之间的夹角。在聚光内部的话θ值应该比ϕ值小。
手电筒
需要的值有聚光的位置向量(来计算光的方向向量)、聚光的方向向量和一个切光角
struct Light {
vec3 position;
vec3 direction;
float cutOff;
…
};
接下来我们将合适的值传到着色器中
lightingShader.setVec3(“light.position”, camera.Position);
lightingShader.setVec3(“light.direction”, camera.Front);
lightingShader.setFloat(“light.cutOff”, glm::cos(glm::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);
平滑/软化边缘
内圆,外圆,通过余弦值的差做插值
根据插值计算从内圆到外圆的聚光衰减…
I=θ−γϵ
这里ϵ(Epsilon)是内(ϕ)和外圆锥(γ)之间的余弦值差(ϵ=ϕ−γ)。最终的I值就是在当前片段聚光的强度。
很难去表现这个公式是怎么工作的,所以我们用一些实例值来看看:
θ θ(角度) ϕ(内光切) ϕ(角度) γ(外光切) γ(角度) ϵ I
0.87 30 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.87 - 0.82 / 0.09 = 0.56
0.9 26 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.9 - 0.82 / 0.09 = 0.89
0.97 14 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.97 - 0.82 / 0.09 = 1.67
0.83 34 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.83 - 0.82 / 0.09 = 0.11
0.64 50 0.91 25 0.82 35 0.91 - 0.82 = 0.09 0.64 - 0.82 / 0.09 = -2.0
0.966 15 0.9978 12.5 0.953 17.5 0.9978 - 0.953 = 0.0448 0.966 - 0.953 / 0.0448 = 0.29
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;
…
多光源
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;
接下来我们可以将dirLight传入一个有着以下原型的函数。
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
和C/C++一样,如果我们想调用一个函数(这里是在main函数中调用),这个函数需要在调用者的行数之前被定义过。
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
vec3 lightDir = normalize(-light.direction);
// 漫反射着色
float diff = max(dot(normal, lightDir), 0.0);
// 镜面光着色
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.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));
return (ambient + diffuse + specular);
}
点光源
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)
{
vec3 lightDir = normalize(light.position - fragPos);
// 漫反射着色
float diff = max(dot(normal, lightDir), 0.0);
// 镜面光着色
vec3 reflectDir = reflect(-lightDir, normal);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
// 衰减
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * (distance * distance));
// 合并结果
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));
ambient *= attenuation;
diffuse *= attenuation;
specular *= attenuation;
return (ambient + diffuse + specular);
}
合并结果
void main()
{
// 属性
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);
// 第一阶段:定向光照 vec3 result = CalcDirLight(dirLight, norm, viewDir); // 第二阶段:点光源 for(int i = 0; i < NR_POINT_LIGHTS; i++) result += CalcPointLight(pointLights[i], norm, FragPos, viewDir); // 第三阶段:聚光 //result += CalcSpotLight(spotLight, norm, FragPos, viewDir); FragColor = vec4(result, 1.0);}
