# 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)光照。

basic_lighting_phong

环境光照:即使在黑暗的情况下,世界上也有月亮或远处的光亮,物体不会永远都是黑的。

漫反射光照:模拟光源对物体的方向性影响。物体的某一部分越是正对着光源,它就越亮。

镜面光照:模拟有光泽的物体上出现的亮点。

# 环境光照

光的颜色乘以一个很小的常量环境因子,再乘以物体颜色

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 为反光度

image-20251220015519059

环境光负责基础亮度,漫反射负责物体被光线直接照射的主体亮度,镜面反射负责物体表面的高光亮点

环境光越强,物体基础亮度越高。

漫反射越强,物体整体反射亮度越高。

镜面强度越强,物体高光越亮、越明显。

# 材质

镜面强度影响高光的亮度,反光度影响高光的大小或集中程度。

#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)本质上就是把纹理采样和冯氏光照的漫反射分量结合。

# 镜面光贴图

镜面高光的强度可以通过图像每个像素的亮度来获取。

image-20251220033029058

现在可以看到强子的材质和真实的钢制边框箱子非常类似了。

放射光贴图:它存储了每个片段的发光值,这样物体能够忽略光照条件进行发光。

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

image-20251220034207214

# 投光物

投光物:将光投射到物体的光源

# 平行光

光源处于很远的地方,每条光纤近似平行。称为定向光。例如太阳。

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)来表示的,切光角指定了聚光的半径(译注:是圆锥的半径不是距光源距离那个半径).

light_casters_spotlight_angles

手电筒:位于观察者位置的聚光,瞄准玩家视角正前方。

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执行,不要选择当前文件!!!

image-20251220010911341

Last Updated: 1/30/2026, 8:32:15 AM