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);