目录
九、颜色
创建一个光照场景
?十、基础光照-冯氏光照模型
环境光照
漫反射光照
法线矩阵
镜面光照
十一、材质
十二、光照贴图
漫反射贴图
镜面光贴图
?十三、投光物
平行光-太阳
点光源
实现衰减
聚光
手电筒
平滑/软化边缘
十四、多光源
定向光
点光源
词汇表
九、颜色
定义物体的颜色为物体从一个光源反射各个颜色分量的大小。
创建一个光照场景
首先我们需要一个物体来作为被投光(Cast the light)的对象,光源。
为灯创建一个新的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);
定义一个片段着色器
#version 330 core
out vec4 FragColor;
uniform vec3 objectColor;
uniform vec3 lightColor;
void main()
{
FragColor = vec4(lightColor * objectColor, 1.0);//将光源的颜色和物体(反射的)颜色相乘
}
// 在此之前不要忘记首先 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.0); // 将向量的四个分量全部设置为1.0
}
声明一个全局vec3 变量来表示光源在场景的世界空间坐标中的位置
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));
?十、基础光照-冯氏光照模型
冯氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照
?环境光照(Ambient Lighting):即使在黑暗的情况下,世界上通常也仍然有一些光亮(月亮、远处的光)漫反射光照(Diffuse Lighting):模拟光源对物体的方向性影响(Directional Impact)。它是冯氏光照模型中视觉上最显著的分量。物体的某一部分越是正对着光源,它就会越亮。镜面光照(Specular Lighting):模拟有光泽物体上面出现的亮点。
环境光照
光能够在其它的表面上反射,对一个物体产生间接的影响。考虑到这种情况的算法叫做全局照明(Global Illumination)算法。
我们使用一个很小的常量(光照)颜色,添加到物体片段的最终颜色中
void main()
{//用光的颜色乘以一个很小的常量环境因子,再乘以物体的颜色
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}
漫反射光照
?图左上方有一个光源,它所发出的光线落在物体的一个片段上。
计算漫反射光照需要什么?
layout (location = 1) in vec3 aNormal;
将法向量由顶点着色器传递到片段着色器
out vec3 Normal;
void main()
{
Normal = aNormal;
}
在片段着色器中定义相应的输入变量
in vec3 Normal;
- 定向的光线:作为光源的位置与片段的位置之间向量差的方向向量。
由于光源的位置是一个静态变量,在片段着色器中把它声明为uniform
uniform vec3 lightPos;
//在渲染循环中(渲染循环的外面也可以,因为它不会改变)更新uniform
lightingShader.setVec3("lightPos", lightPos);
片段的位置:在世界空间中进行所有的光照计算,因此我们需要一个在世界空间中的顶点位置。
out vec3 FragPos; ?
? ? ? FragPos = vec3(model * vec4(aPos, 1.0));
//光的方向向量是光源位置向量与片段位置向量之间的向量差。
vec3 norm = normalize(Normal);//把法线和最终的方向向量都进行标准化
vec3 lightDir = normalize(lightPos - FragPos);
//对norm和lightDir向量进行点乘,计算光源对当前片段实际的漫发射影响
float diff = max(dot(norm, lightDir), 0.0);
//如果两个向量之间的角度大于90度,点乘的结果就会变成负数
//结果值再乘以光的颜色,得到漫反射分量
vec3 diffuse = diff * lightColor;
环境光分量和漫反射分量,我们把它们相加,然后把结果乘以物体的颜色
vec3 result = (ambient + diffuse) * objectColor;
FragColor = vec4(result, 1.0);
法线矩阵
法向量只是一个方向向量,不能表达空间中的特定位置。同时,法向量没有齐次坐标(顶点位置中的w分量)。这意味着,位移不应该影响到法向量。
其次,如果模型矩阵执行了不等比缩放,顶点的改变会导致法向量不再垂直于表面了。
?法线矩阵(Normal Matrix),它使用了一些线性代数的操作来移除对法向量错误缩放的影响。
在顶点着色器中,我们可以使用inverse和transpose函数自己生成这个法线矩阵,这两个函数对所有类型矩阵都有效。(注意我们还要把被处理过的矩阵强制转换为3×3矩阵,来保证它失去了位移属性以及能够乘以vec3 的法向量。)
Normal = mat3(transpose(inverse(model))) * aNormal;
镜面光照
镜面光照也是依据光的方向向量和物体的法向量来决定的,但是它也依赖于观察方向,例如玩家是从什么方向看着这个片段的。镜面光照是基于光的反射特性。
?观察向量是镜面光照附加的一个变量,我们可以使用观察者世界空间位置和片段的位置来计算它。
uniform vec3 viewPos;//观察者的世界空间坐标,简单地使用摄像机对象的位置坐标代替
lightingShader.setVec3("viewPos", camera.Position);
//镜面强度
float specularStrength = 0.5;
//计算视线方向向量,和对应的沿着法线轴的反射向量
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
//reflect函数要求第一个向量是从光源指向片段位置的向量,所以取反
//计算镜面分量
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);//32是高光的反光度(Shininess)
vec3 specular = specularStrength * spec * lightColor;
加到环境光分量和漫反射分量里,再用结果乘以物体的颜色
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
十一、材质
在OpenGL中模拟多种类型的物体,我们必须为每个物体分别定义一个材质(Material)属性。
材质颜色(Material Color):环境光照(Ambient Lighting)、漫反射光照(Diffuse Lighting)和镜面光照(Specular Lighting)。
创建一个结构体(Struct)来储存物体的材质属性,声明一个uniform变量。
struct Material {
vec3 ambient;//通常这是和物体颜色相同的颜色
vec3 diffuse;//在漫反射光照下物体的颜色
vec3 specular;//镜面光照对物体的颜色影响
float shininess;//影响镜面高光的散射/半径
};
uniform Material 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;//镜面光分量通常会保持为vec3(1.0),以最大强度发光
};
uniform Light light;
十二、光照贴图
引入漫反射和镜面光贴图(Map)
漫反射贴图
使用一张覆盖物体的图像,让我们能够逐片段索引其独立的颜色值。它通常叫做一个漫反射贴图(Diffuse Map)。
在着色器中使用漫反射贴图的方法和纹理教程中是完全一样的。但这次我们会将纹理储存为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));
//将要用的纹理单元赋值到material.diffuse这个uniform采样器
lightingShader.setInt("material.diffuse", 0);
...
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);
顶点数据现在包含了顶点位置、法向量和立方体顶点处的纹理坐标。
layout (location = 2) in vec2 aTexCoords;
...
out vec2 TexCoords;
void main()
{
...
TexCoords = aTexCoords;
}
镜面光贴图
专门用于镜面高光的纹理贴图,生成一个黑白的(如果你想得话也可以是彩色的)纹理,来定义物体每部分的镜面光强度。镜面高光的强度可以通过图像每个像素的亮度来获取。
? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ? ? ? ? ? ?镜面光贴图? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?漫反射贴图
黑色代表颜色向量vec3(0.0) ,灰色代表颜色向量vec3(0.5) 。在片段着色器中,我们接下来会取样对应的颜色值并将它乘以光源的镜面强度。一个像素越「白」,乘积就会越大,物体的镜面光分量就会越亮。
lightingShader.setInt("material.specular", 1);
...
glActiveTexture(GL_TEXTURE1);//绑定到合适的纹理单元
glBindTexture(GL_TEXTURE_2D, specularMap);
//更新片段着色器的材质属性
struct Material {
sampler2D diffuse;
sampler2D specular;//new
float shininess;
};
vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
?十三、投光物
将光投射(Cast)到物体的光源叫做投光物(Light Caster)。
平行光-太阳
当一个光源处于很远的地方时,来自光源的每条光线就会近似于互相平行。当我们使用一个假设光源处于无限远处的模型时,它就被称为定向光,因为它的所有光线都有着相同的方向,它与光源的位置是没有关系的。
?定义一个光线方向向量而不是位置向量来模拟一个定向光。直接使用光的direction向量
struct Light {
// vec3 position; // 使用定向光就不再需要了
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
...
void main()
{
vec3 lightDir = normalize(-light.direction);
//vec3 lightDir = normalize(lightPos - FragPos);不用这个
...
}
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
点光源
点光源是处于世界中某一个位置的光源,它会朝着所有方向发光,但光线会随着距离逐渐衰减。
随着光线传播距离的增长逐渐削减光的强度通常叫做衰减(Attenuation)。?在现实世界中,灯在近处通常会非常亮,但随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。
d代表了片段距光源的距离。
实现衰减
?为了实现衰减,在片段着色器中我们还需要三个额外的值:也就是公式中的常数项、一次项和二次项。它们最好储存在之前定义的Light结构体中。
struct Light {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constant;
float linear;
float quadratic;
};
//根据公式计算衰减值
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;
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 :聚光所指向的方向。- :聚光半径的切光角。落在这个角度之外的物体都不会被这个聚光所照亮。
- :LightDir向量和SpotDir向量之间的夹角
手电筒
手电筒就是普通的聚光,但它的位置和方向会随着玩家的位置和朝向不断更新。
struct Light {
vec3 position;
vec3 direction;
float cutOff;
...
};
lightingShader.setFloat("light.cutOff", glm::cos(glm::radians(12.5f)));
//用角度值计算了一个余弦值
//在片段着色器中,我们会计算LightDir和SpotDir向量的点积,这个点积返回的将是一个余弦值而不是角度值
float theta = dot(lightDir, normalize(-light.direction));
if(theta > light.cutOff)
{
// 执行光照计算
}
else // 否则,使用环境光,让场景在聚光之外时不至于完全黑暗
color = vec4(light.ambient * vec3(texture(material.diffuse, TexCoords)), 1.0);
平滑/软化边缘
为了创建一种看起来边缘平滑的聚光,我们需要模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。为了创建一个外圆锥,我们只需要再定义一个余弦值来代表聚光方向向量和外圆锥向量(等于它的半径)的夹角。然后,如果一个片段处于内外圆锥之间,将会给它计算出一个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);
//clamp函数,它把第一个参数约束(Clamp)在了0.0到1.0之间
...
// 将不对环境光做出影响,让它总是能有一点光
diffuse *= intensity;
specular *= intensity;
十四、多光源
为了在场景中使用多个光源,我们希望将光照计算封装到GLSL函数中,我们对每个光照类型都创建一个不同的函数:定向光、点光源和聚光。
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);
}
定向光
struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
uniform DirLight dirLight;
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);
}
设置点光源的uniform值,点光源的uniform现在是一个PointLight的数组了
lightingShader.setFloat("pointLights[0].constant", 1.0f);
词汇表
- 冯氏光照模型(Phong Lighting Model):一个通过计算环境光,漫反射,和镜面光分量的值来估计真实光照的模型。
- 法线矩阵(Normal Matrix):一个3x3矩阵,或者说是没有平移的模型(或者模型-观察)矩阵。
- 衰减(Attenuation):光随着距离减少强度的过程。
- 聚光(Spotlight):一个被定义为在某一个方向上的锥形的光源。
|