OpenGL 光照系统实现:从基础光照到 Phong 与 Blinn-Phong 模型
构建光照测试场景
为验证光照效果,首先搭建一个无纹理的纯色立方体场景,并添加一个小型立方体作为光源可视化标记。主程序中移除了 UV 坐标,仅保留顶点位置数据。
#include <iostream>
#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 cubeVertices[] = {
// 36个顶点,每3个float表示一个点
-0.5f, -0.5f, -0.5f,
0.5f, -0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
0.5f, 0.5f, -0.5f,
-0.5f, 0.5f, -0.5f,
-0.5f, -0.5f, -0.5f,
// ...(其余面省略,结构相同)
};
const unsigned int SCR_WIDTH = 800, SCR_HEIGHT = 600;
float deltaTime = 0.0f, lastFrame = 0.0f;
Camera mainCamera(glm::vec3(0.0f, 0.0f, 3.0f));
// 输入处理、回调函数等略
int main() {
// GLFW 初始化与窗口创建
glfwInit();
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "Lighting Demo", nullptr, nullptr);
if (!window) { /* 错误处理 */ }
glfwMakeContextCurrent(window);
if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { /* 错误处理 */ }
// 设置相机与输入回调
mainCamera = Camera(glm::vec3(0.0f, 0.0f, 3.0f));
glfwSetFramebufferSizeCallback(window, [](GLFWwindow*, int w, int h) {
glViewport(0, 0, w, h);
});
glfwSetCursorPosCallback(window, [](GLFWwindow*, double x, double y) {
mainCamera.ProcessMouseMovement(x, y);
});
glfwSetScrollCallback(window, [](GLFWwindow*, double, double yoffset) {
mainCamera.ProcessMouseScroll(yoffset);
});
// 创建VAO/VBO
unsigned int cubeVAO, VBO;
glGenVertexArrays(1, &cubeVAO);
glGenBuffers(1, &VBO);
glBindVertexArray(cubeVAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(cubeVertices), cubeVertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 光源VAO复用相同顶点数据
unsigned int lightVAO;
glGenVertexArrays(1, &lightVAO);
glBindVertexArray(lightVAO);
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
Shader objectShader("default.vs", "phong.fs");
Shader lightShader("default.vs", "light.fs");
glEnable(GL_DEPTH_TEST);
glm::vec3 lightPosition(1.2f, 1.0f, 2.0f);
glm::vec3 lightColor(1.0f, 1.0f, 1.0f);
while (!glfwWindowShouldClose(window)) {
float currentFrame = glfwGetTime();
deltaTime = currentFrame - lastFrame;
lastFrame = currentFrame;
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 处理输入
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
mainCamera.ProcessKeyboard(window, deltaTime);
glm::mat4 view = mainCamera.GetViewMatrix();
glm::mat4 projection = glm::perspective(glm::radians(mainCamera.Zoom),
(float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
// 渲染主物体
objectShader.use();
glm::mat4 model = glm::rotate(glm::mat4(1.0f),
glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
objectShader.setMat4("model", model);
objectShader.setMat4("view", view);
objectShader.setMat4("projection", projection);
objectShader.setVec3("objectColor", glm::vec3(1.0f, 0.5f, 0.31f));
objectShader.setVec3("lightColor", lightColor);
objectShader.setVec3("lightPos", lightPosition);
objectShader.setVec3("viewPos", mainCamera.Position);
glBindVertexArray(cubeVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
// 渲染光源小立方体
lightShader.use();
model = glm::mat4(1.0f);
model = glm::translate(model, lightPosition);
model = glm::scale(model, glm::vec3(0.2f));
lightShader.setMat4("model", model);
lightShader.setMat4("view", view);
lightShader.setMat4("projection", projection);
lightShader.setVec3("lightColor", lightColor);
glBindVertexArray(lightVAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glfwSwapBuffers(window);
glfwPollEvents();
}
glDeleteVertexArrays(1, &cubeVAO);
glDeleteVertexArrays(1, &lightVAO);
glDeleteBuffers(1, &VBO);
glfwTerminate();
return 0;
}
着色器代码如下:
顶点着色器(default.vs)
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
光源片段着色器(light.fs)
#version 330 core
out vec4 FragColor;
uniform vec3 lightColor;
void main() {
FragColor = vec4(lightColor, 1.0);
}
Phong 光照模型详解
Phong 模型由三部分组成:
- 环境光(Ambient):模拟间接光照,使用常量颜色避免物体完全变黑。
- 漫反射(Diffuse):基于 Lambert 定律,依赖表面法向量与光源方向的夹角。
- 镜面反射(Specular):模拟高光,依赖观察方向与反射光方向的对齐程度。
环境光照实现
在片段着色器中引入环境光系数:
vec3 ambient = 0.1 * lightColor;
FragColor = vec4(objectColor * ambient, 1.0);
漫反射光照
需为顶点添加法向量数据:
// 每个顶点:位置(3) + 法向量(3)
float verticesWithNormals[] = {
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 0.0f, -1.0f,
// ...
};
顶点着色器计算世界空间下的位置和变换后的法向量:
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out vec3 FragPos;
out vec3 Normal;
uniform mat4 model;
void main() {
gl_Position = projection * view * model * vec4(aPos, 1.0);
FragPos = vec3(model * vec4(aPos, 1.0));
Normal = mat3(transpose(inverse(model))) * aNormal;
}
片段着色器计算漫反射:
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
镜面反射(Phong)
引入观察位置,计算反射方向:
vec3 viewDir = normalize(viewPos - FragPos);
vec3 reflectDir = reflect(-lightDir, norm);
float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32);
vec3 specular = spec * 0.5 * lightColor;
Blinn-Phong 优化
使用半程向量(halfway vector)替代反射向量,提升性能与稳定性:
vec3 halfwayDir = normalize(lightDir + viewDir);
float spec = pow(max(dot(norm, halfwayDir), 0.0), 32);
vec3 specular = spec * 0.5 * lightColor;
最终颜色为三者叠加:
vec3 result = (ambient + diffuse + specular) * objectColor;
FragColor = vec4(result, 1.0);
