# LearnOpenGL-02光照
# 颜色
反射颜色:光源颜色与物体颜色相乘。
glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
glm::vec3 toyColor(1.0f, 0.5f, 0.31f);
glm::vec3 result = lightColor * toyColor; // = (1.0f, 0.5f, 0.31f);
# 基础光照
风氏光照模型(Phong Lighting Model)。风氏光照模型的主要结构由3个分量组成:环境(Ambient)、漫反射(Diffuse)和镜面(Specular)光照。

环境光照:即使在黑暗的情况下,世界上也有月亮或远处的光亮,物体不会永远都是黑的。
漫反射光照:模拟光源对物体的方向性影响。物体的某一部分越是正对着光源,它就越亮。
镜面光照:模拟有光泽的物体上出现的亮点。
# 环境光照
光的颜色乘以一个很小的常量环境因子,再乘以物体颜色
void main()
{
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;
vec3 result = ambient * objectColor;
FragColor = vec4(result, 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);
}
法线矩阵:模型矩阵左上角3x3部分的逆矩阵的转置矩阵
当应用一个不等比例缩放时,法向量就不会垂直于对应的表面,光照会呗破坏。
# 镜面光照
反射向量与观察方向的夹角越小,镜面光的作用越大。
观察向量:可以使用观察者的世界空间位置和片段位置来计算。
float specularStrength = 0.5; # 镜面强度
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32); # 32 为反光度

环境光负责基础亮度,漫反射负责物体被光线直接照射的主体亮度,镜面反射负责物体表面的高光亮点。
环境光越强,物体基础亮度越高。
漫反射越强,物体整体反射亮度越高。
镜面强度越强,物体高光越亮、越明显。
# 材质
镜面强度影响高光的亮度,反光度影响高光的大小或集中程度。
#version 330 core
struct Material {
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};
uniform Material material;
环境光ambient、漫反射diffuse、镜面光specular、shininess反光度。
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;
一个光源对它的ambient、diffuse和specular光照分量有着不同的强度.
物体亮度乘以光源每种分量的强度
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);
我还在想物体本身的颜色,物体的颜色不都是反光吗?
# 光照贴图
现实世界中的物体通常并不只包含有一种材质,而是由多种材质所组成。想想一辆汽车:它的外壳非常有光泽,车窗会部分反射周围的环境,轮胎不会那么有光泽,所以它没有镜面高光,轮毂非常闪亮(如果你洗车了的话)。
# 漫反射贴图
对物体的每个片段单独设置漫反射颜色。
struct Material {
sampler2D diffuse;
vec3 specular;
float shininess;
};
...
in vec2 TexCoords;
漫反射贴图(Diffuse Map)本质上就是把纹理采样和冯氏光照的漫反射分量结合。
# 镜面光贴图
镜面高光的强度可以通过图像每个像素的亮度来获取。
现在可以看到强子的材质和真实的钢制边框箱子非常类似了。
放射光贴图:它存储了每个片段的发光值,这样物体能够忽略光照条件进行发光。
struct Material {
sampler2D diffuse;
sampler2D specular;
sampler2D emission;
float shininess;
};
void main()
{
// ambient
vec3 ambient = light.ambient * texture(material.diffuse, TexCoords).rgb;
// diffuse
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(light.position - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = light.diffuse * diff * texture(material.diffuse, TexCoords).rgb;
// specular
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
vec3 specular = light.specular * spec * texture(material.specular, TexCoords).rgb;
// emission
vec3 emission = texture(material.emission, TexCoords).rgb;
vec3 result = ambient + diffuse + specular + emission;
FragColor = vec4(result, 1.0);

# 投光物
投光物:将光投射到物体的光源
# 平行光
光源处于很远的地方,每条光纤近似平行。称为定向光。例如太阳。
struct Light {
// vec3 position; // 使用定向光就不再需要了
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};
...
void main()
{
vec3 lightDir = normalize(-light.direction);
...
}
光线方向要取反
lightingShader.setVec3("light.direction", -0.2f, -1.0f, -0.3f);
# 点光源
点光源会朝着所有方向发光,但是光线会随距离逐渐衰减。例如灯泡和火把。
衰减:在现实世界中,灯在近处通常会非常亮,但随着距离的增加光源的亮度一开始会下降非常快,但在远处时剩余的光强度就会下降的非常缓慢了。
$$ \begin{equation} F_{att} = \frac{1.0}{K_c + K_l * d + K_q * d^2} \end{equation} $$
d代表了片段距光源的距离,三个常数项:
- 常数项通常保持为1.0,它的主要作用是保证分母永远不会比1小。
- 一次项会与距离值相乘,以线性的方式减少强度。
- 二次项会与距离的平方相乘,让光源以二次递减的方式减少强度。
# 聚光
位于某个环境中某个位置的光源,它只朝特定方向照射光线。 例如路灯或手电筒。
OpenGL中聚光是用一个世界空间位置、一个方向和一个切光角(Cutoff Angle)来表示的,切光角指定了聚光的半径(译注:是圆锥的半径不是距光源距离那个半径).

手电筒:位于观察者位置的聚光,瞄准玩家视角正前方。
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);
# 平滑软化
聚光有一圈硬边。
模拟聚光有一个内圆锥(Inner Cone)和一个外圆锥(Outer Cone)。我们可以将内圆锥设置为上一部分中的那个圆锥,但我们也需要一个外圆锥,来让光从内圆锥逐渐减暗,直到外圆锥的边界。
如果片源位于内外圆锥之内,计算一个0-1的强度值。 $$ \begin{equation} I = \frac{\theta - \gamma}{\epsilon} \end{equation}, \epsilon = \phi - \gamma $$ θ是片源受照射方向与光线方向夹角,ϕ是内切角,γ是外切角,ϵ是内外余弦差值。
# 多光源
将光线计算封装在着色器中
# 定向光
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];
创建PointLight数组,4个点光源
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);
}
# 词汇表
- 颜色向量(Color Vector):一个通过红绿蓝(RGB)分量的组合描绘大部分真实颜色的向量。一个物体的颜色实际上是该物体所不能吸收的反射颜色分量。
- 风氏光照模型(Phong Lighting Model):一个通过计算环境光,漫反射,和镜面光分量的值来近似真实光照的模型。
- 环境光照(Ambient Lighting):通过给每个没有被光照的物体很小的亮度,使其不是完全黑暗的,从而对全局光照进行近似。
- 漫反射着色(Diffuse Shading):一个顶点/片段与光线方向越接近,光照会越强。使用了法向量来计算角度。
- 法向量(Normal Vector):一个垂直于平面的单位向量。
- 法线矩阵(Normal Matrix):一个3x3矩阵,或者说是没有平移的模型(或者模型-观察)矩阵。它也被以某种方式修改(逆转置),从而在应用非统一缩放时,保持法向量朝向正确的方向。否则法向量会在使用非统一缩放时被扭曲。
- 镜面光照(Specular Lighting):当观察者视线靠近光源在表面的反射线时会显示的镜面高光。镜面光照是由观察者的方向,光源的方向和设定高光分散量的反光度值三个量共同决定的。
- 风氏着色(Phong Shading):风氏光照模型应用在片段着色器。
- Gouraud着色(Gouraud shading):风氏光照模型应用在顶点着色器上。在使用很少数量的顶点时会产生明显的瑕疵。会得到效率提升但是损失了视觉质量。
- GLSL结构体(GLSL struct):一个类似于C的结构体,用作着色器变量的容器。大部分时间用来管理输入/输出/uniform。
- 材质(Material):一个物体反射的环境光,漫反射,镜面光颜色。这些东西设定了物体所拥有的颜色。
- 光照属性(Light(properties)):一个光的环境光,漫反射,镜面光的强度。可以使用任何颜色值,对每一个风氏分量(Phong Component)定义光源发出的颜色/强度。
- 漫反射贴图(Diffuse Map):一个设定了每个片段中漫反射颜色的纹理图片。
- 镜面光贴图(Specular Map):一个设定了每一个片段的镜面光强度/颜色的纹理贴图。仅在物体的特定区域显示镜面高光。
- 定向光(Directional Light):只有方向的光源。它被建模为无限距离,这使得它的所有光线看起来都是平行的,因此它的方向矢量在整个场景中保持不变。
- 点光源(Point Light):一个在场景中有位置的,光线逐渐衰减的光源。
- 衰减(Attenuation):光随着距离减少强度减小的过程,通常使用在点光源和聚光下。
- 聚光(Spotlight):一个被定义为在某一个方向上的锥形的光源。
- 手电筒(Flashlight):一个摆放在观察者视角的聚光。
- GLSL Uniform数组(GLSL Uniform Array):一个uniform值数组。它的工作原理和C语言数组大致一样,只是不能动态分配内存。
# 问题总结
# 相对路径问题
在调试第二章的源码是,总是运行失败。错误信息是文件读取失败。
打印当前工作目录后,发现是第一章的目录,发现是项目目录加载出错。
但是在构建目录bin目录下的exe 程序是可以正常运行的
| 对比维度 | VS 中直接 “运行当前文档”(cpp 文件) | 手动运行 bin 目录下的 exe 文件 |
|---|---|---|
| 工作目录(最关键) | 使用VS 调试配置中设定的工作目录(大概率是第一章的目录、项目根目录或 CMake 构建目录,并非第二章的目录) | 使用你执行 exe 时的终端当前目录(比如你进入bin/chapter02/lesson01后运行 exe,工作目录就是这个目录) |
| 着色器文件查找路径 | 从错误的工作目录找着色器(比如去第一章目录找第二章的着色器),自然找不到 | 从正确的工作目录找着色器(exe 同目录),能正常找到 |
| 运行的是 “谁” | VS 先编译生成 exe,再通过调试器(debugger) 启动 exe(受调试配置控制) | 直接运行编译好的 exe(不受 VS 配置控制,完全由系统环境决定) |
VS 的调试配置具有 “首次生成即固化、后续复用” 的特性,且不会自动为不同章节的文件生成独立配置
解决方案:项目整体生成后,每个有主函数的cpp,都会生成exe。选择对应的exe执行,不要选择当前文件!!!
