跳到主要内容

光照

光照是3D图形中最重要的主题之一,它决定了场景的真实感和视觉效果。本章将介绍OpenGL中的光照模型和实现方法。

光照基础

现实世界中的光照非常复杂,OpenGL使用简化的模型来模拟光照效果。最常用的模型是冯氏光照模型(Phong Lighting Model)。

冯氏光照模型

冯氏光照模型由三个分量组成:

  • 环境光(Ambient):模拟间接光照,使物体不会完全黑暗
  • 漫反射(Diffuse):模拟光线直接照射到物体表面的效果
  • 镜面反射(Specular):模拟光滑表面的高光效果
最终颜色 = 环境光 + 漫反射 + 镜面反射

环境光

环境光是最简单的光照分量,它假设场景中有一些基础光照,使物体不会完全黑暗。

// 片段着色器
void main() {
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;

vec3 result = ambient * objectColor;
FragColor = vec4(result, 1.0);
}

漫反射

漫反射模拟光线照射到粗糙表面的效果。光照强度取决于光线方向与表面法线之间的夹角。

法线向量

法线是垂直于表面的向量,用于计算光照:

// 顶点数据包含法线
float vertices[] = {
// 位置 // 法线
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 0.0f, 0.0f, 1.0f,
// ...
};

漫反射计算

漫反射强度 = max(dot(法线, 光线方向), 0)

// 顶点着色器
#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 projection;

void main() {
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
gl_Position = projection * view * vec4(FragPos, 1.0);
}
// 片段着色器
#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;

uniform vec3 lightPos;
uniform vec3 lightColor;
uniform vec3 objectColor;

void main() {
// 环境光
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;

// 漫反射
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);
}

法线矩阵

当模型发生非均匀缩放时,法线会变形。需要使用法线矩阵来正确变换法线:

Normal = mat3(transpose(inverse(model))) * aNormal;

镜面反射

镜面反射模拟光滑表面的高光效果,取决于观察者的位置。

// 片段着色器
#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;

uniform vec3 lightPos;
uniform vec3 viewPos;
uniform vec3 lightColor;
uniform vec3 objectColor;

void main() {
// 环境光
float ambientStrength = 0.1;
vec3 ambient = ambientStrength * lightColor;

// 漫反射
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;

// 镜面反射
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);
}

高光指数

高光指数(shininess)控制高光的大小和锐利程度:

  • 较小的值(如2):大而模糊的高光
  • 较大的值(如256):小而锐利的高光

光源类型

点光源

点光源从一点向所有方向发射光线,光线强度随距离衰减:

struct PointLight {
vec3 position;

float constant;
float linear;
float quadratic;

vec3 ambient;
vec3 diffuse;
vec3 specular;
};

// 衰减计算
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;

衰减参数参考:

距离范围constantlinearquadratic
71.00.71.8
131.00.350.44
201.00.220.20
501.00.090.032
1001.00.0450.0075

方向光

方向光模拟远处的光源(如太阳),所有光线平行:

struct DirLight {
vec3 direction;

vec3 ambient;
vec3 diffuse;
vec3 specular;
};

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 * material.diffuse;
vec3 diffuse = light.diffuse * diff * material.diffuse;
vec3 specular = light.specular * spec * material.specular;

return ambient + diffuse + specular;
}

聚光灯

聚光灯从一点向特定方向发射锥形光线:

struct SpotLight {
vec3 position;
vec3 direction;
float cutOff;
float outerCutOff;

float constant;
float linear;
float quadratic;

vec3 ambient;
vec3 diffuse;
vec3 specular;
};

vec3 CalcSpotLight(SpotLight light, vec3 normal, vec3 fragPos, vec3 viewDir) {
vec3 lightDir = normalize(light.position - fragPos);

// 检查是否在聚光灯范围内
float theta = dot(lightDir, normalize(-light.direction));
float epsilon = light.cutOff - light.outerCutOff;
float intensity = clamp((theta - light.outerCutOff) / epsilon, 0.0, 1.0);

// 衰减
float distance = length(light.position - fragPos);
float attenuation = 1.0 / (light.constant + light.linear * distance +
light.quadratic * distance * distance);

// 漫反射
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 * material.diffuse;
vec3 diffuse = light.diffuse * diff * material.diffuse * intensity;
vec3 specular = light.specular * spec * material.specular * intensity;

return (ambient + diffuse + specular) * attenuation;
}

材质

材质定义了物体如何响应光照:

struct Material {
vec3 ambient;
vec3 diffuse;
vec3 specular;
float shininess;
};

uniform Material material;

使用纹理作为材质

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

多光源

组合多种光源:

#version 330 core
out vec4 FragColor;

in vec3 FragPos;
in vec3 Normal;
in vec2 TexCoords;

struct DirLight {
vec3 direction;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};

struct PointLight {
vec3 position;
float constant;
float linear;
float quadratic;
vec3 ambient;
vec3 diffuse;
vec3 specular;
};

struct Material {
sampler2D diffuse;
sampler2D specular;
float shininess;
};

uniform DirLight dirLight;
uniform PointLight pointLights[4];
uniform Material material;
uniform vec3 viewPos;

// 函数声明
vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);
vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);

void main() {
vec3 norm = normalize(Normal);
vec3 viewDir = normalize(viewPos - FragPos);

// 方向光
vec3 result = CalcDirLight(dirLight, norm, viewDir);

// 点光源
for(int i = 0; i < 4; i++)
result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);

FragColor = vec4(result, 1.0);
}

光照贴图

使用纹理来定义材质属性:

// 加载漫反射贴图
unsigned int diffuseMap = loadTexture("container2.png");

// 加载镜面反射贴图
unsigned int specularMap = loadTexture("container2_specular.png");

// 设置uniform
glUniform1i(glGetUniformLocation(shader.ID, "material.diffuse"), 0);
glUniform1i(glGetUniformLocation(shader.ID, "material.specular"), 1);

// 绑定纹理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, diffuseMap);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, specularMap);

完整示例

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include "shader.h"
#include "camera.h"

// 顶点数据
float vertices[] = {
// 位置 // 法线 // 纹理坐标
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f, 0.0f, 0.0f,
// ... 其他面
};

int main() {
// 初始化...

Camera camera(glm::vec3(0.0f, 0.0f, 3.0f));

Shader lightingShader("lighting.vs", "lighting.fs");
Shader lampShader("lamp.vs", "lamp.fs");

// 创建VAO、VBO...

// 光源位置
glm::vec3 lightPos(1.2f, 1.0f, 2.0f);

while (!glfwWindowShouldClose(window)) {
processInput(window);

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// 绘制物体
lightingShader.use();
lightingShader.setVec3("light.position", lightPos);
lightingShader.setVec3("viewPos", camera.Position);

// 光照属性
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);

// 材质属性
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);

// 变换矩阵
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom),
800.0f / 600.0f, 0.1f, 100.0f);
glm::mat4 view = camera.GetViewMatrix();
glm::mat4 model = glm::mat4(1.0f);

lightingShader.setMat4("projection", projection);
lightingShader.setMat4("view", view);
lightingShader.setMat4("model", model);

glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);

// 绘制光源
lampShader.use();
lampShader.setMat4("projection", projection);
lampShader.setMat4("view", view);
model = glm::mat4(1.0f);
model = glm::translate(model, lightPos);
model = glm::scale(model, glm::vec3(0.2f));
lampShader.setMat4("model", model);

glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);

glfwSwapBuffers(window);
glfwPollEvents();
}

glfwTerminate();
return 0;
}

小结

光照是3D图形的核心:

  • 冯氏光照模型包含环境光、漫反射和镜面反射
  • 法线用于计算光照方向
  • 不同类型的光源有不同的特性
  • 材质定义物体如何响应光照

下一章我们将学习 模型加载,加载复杂的3D模型。

光照调试

如果光照效果不正确,检查以下几点:

  1. 法线是否正确设置和变换
  2. 光源位置是否正确
  3. 观察者位置是否正确传递给着色器
  4. 向量是否归一化