图形学:解析绘制三角形实例
## 三角形实例源码
```cpp
// C++ OpenGL 示例
#include
#include
#include
void framebuffer_size_callback(GLFWwindow* window, int width, int height);
void processInput(GLFWwindow* window);
// 着色器源码
const char* vertexShaderSource =
"#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main() {\n"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
"}\n";
const char* fragmentShaderSource =
"#version 330 core\n"
"out vec4 FragColor;\n"
"void main() {\n"
" FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);\n"
"}\n";
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(800, 600, "LearnOpenGL", NULL, NULL);
if (window == NULL) {
std::cout << "Failed to create GLFW window" << std::endl;
glfwTerminate();
return -1;
}
glfwMakeContextCurrent(window);
glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);
// 初始化GLEW
if (glewInit() != GLEW_OK) {
std::cout << "Failed to initialize GLEW" << std::endl;
return -1;
}
// 编译顶点着色器
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// 编译片段着色器
unsigned int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);
// 创建着色器程序
unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
// 删除着色器对象
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);
// 设置顶点数据
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
unsigned int VBO, VAO; // 声明两个ID变量
glGenVertexArrays(1, &VAO); // 生成1个VAO,获得唯一ID
glGenBuffers(1, &VBO); // 生成1个VBO,获得唯一ID
// 绑定VAO
glBindVertexArray(VAO);
// 绑定VBO并设置数据
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 设置顶点属性指针
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 解绑
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindVertexArray(0);
// 渲染循环
while (!glfwWindowShouldClose(window)) {
// 输入处理
processInput(window);
// 清屏
glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// 绘制三角形
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
// 交换缓冲区
glfwSwapBuffers(window);
glfwPollEvents();
}
// 清理资源
glDeleteVertexArrays(1, &VAO);
glDeleteBuffers(1, &VBO);
glDeleteProgram(shaderProgram);
glfwTerminate();
return 0;
}
void framebuffer_size_callback(GLFWwindow* window, int width, int height) {
glViewport(0, 0, width, height);
}
void processInput(GLFWwindow* window) {
if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
glfwSetWindowShouldClose(window, true);
}
```
## OpenGL渲染管线完整流程图
```bash
## 一、整体流程概览
CPU端准备数据 → 上传到GPU → 顶点着色器处理 → 图元装配 → 光栅化 → 片段着色器处理 → 输出到屏幕
## 二、初始化阶段(main函数开头)
### 1. 创建窗口和OpenGL上下文
glfwInit(); // 初始化GLFW库
glfwCreateWindow(800, 600, ...); // 创建窗口
glfwMakeContextCurrent(window); // 创建OpenGL上下文(状态机)
说明:OpenGL上下文相当于一个巨大的状态机,记录所有设置
### 2. 编译着色器程序
vertexShader = glCreateShader(GL_VERTEX_SHADER); // 创建顶点着色器对象
glCompileShader(vertexShader); // 编译GPU代码
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glCompileShader(fragmentShader);
shaderProgram = glCreateProgram(); // 创建程序对象
glAttachShader(shaderProgram, vertexShader); // 附加着色器
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram); // 链接成可执行程序
说明:
- 将GLSL源码编译成GPU能执行的二进制代码
- 链接后得到一个程序ID(shaderProgram)
## 三、数据准备阶段
### 3. 创建VAO和VBO
glGenVertexArrays(1, &VAO); // 创建VAO对象
glGenBuffers(1, &VBO); // 创建VBO对象
glBindVertexArray(VAO); // 激活VAO(记录后续所有配置)
glBindBuffer(GL_ARRAY_BUFFER, VBO); // 激活VBO
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 将CPU内存的顶点数据拷贝到GPU显存
### 内存布局示意图
CPU内存(RAM) GPU显存(VRAM)
┌─────────────┐ ┌─────────────┐
│ vertices[0] │ -0.5, -0.5 →│ VBO对象 │
│ vertices[1] │ 0.5, -0.5 │ [-0.5,-0.5] │
│ vertices[2] │ 0.0, 0.5 │ [ 0.5,-0.5] │
└─────────────┘ │ [ 0.0, 0.5] │
└─────────────┘
↑
VAO指向VBO
### 4. 配置顶点属性
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3*sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
说明:
- 告诉GPU:VBO中每3个float为一个顶点位置
- 属性索引0对应顶点着色器的 layout (location = 0) in vec3 aPos;
## 四、渲染循环阶段
### 5. 每帧的绘制流程
while (!glfwWindowShouldClose(window)) {
// 步骤1:清屏
glClearColor(0.2f, 0.3f, 0.3f, 1.0f); // 设置清除颜色
glClear(GL_COLOR_BUFFER_BIT); // 用该颜色清除颜色缓冲区
// 步骤2:激活程序
glUseProgram(shaderProgram); // GPU开始使用这个着色器程序
// 步骤3:绑定VAO
glBindVertexArray(VAO); // GPU知道数据在哪、如何读取
// 步骤4:绘制!
glDrawArrays(GL_TRIANGLES, 0, 3);
// 参数:画三角形,从第0个顶点开始,画3个顶点
// 步骤5:交换缓冲区(双缓冲技术)
glfwSwapBuffers(window); // 显示绘制的画面
}
## 五、GPU内部的详细处理流程
当调用 glDrawArrays(GL_TRIANGLES, 0, 3) 时,GPU依次执行:
### 第1步:顶点着色器(Vertex Shader)处理
GLSL代码:
layout (location = 0) in vec3 aPos; // 从VBO读取顶点位置
void main() {
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
GPU对3个顶点并行执行:
| 顶点 | 输入 aPos | 输出 gl_Position |
|------|-----------|------------------|
| 0 | (-0.5, -0.5, 0.0) | (-0.5, -0.5, 0.0, 1.0) |
| 1 | (0.5, -0.5, 0.0) | (0.5, -0.5, 0.0, 1.0) |
| 2 | (0.0, 0.5, 0.0) | (0.0, 0.5, 0.0, 1.0) |
### 第2步:图元装配(Primitive Assembly)
- 将3个顶点组装成1个三角形
- 执行透视除法:NDC = (x/w, y/w, z/w)
- 视口变换:映射到屏幕坐标(例如800x600窗口)
### 第3步:光栅化(Rasterization)
- 确定三角形覆盖了哪些像素(片段)
- 为每个像素生成一个片段(包含位置、深度等信息)
示例:假设三角形覆盖了约3000个像素
三角形区域 → 生成3000个片段
每个片段有:屏幕坐标(x,y)、深度值(z)
### 第4步:片段着色器(Fragment Shader)处理
GLSL代码:
out vec4 FragColor;
void main() {
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
}
说明:每个片段并行执行,输出固定颜色:橙色(1.0, 0.5, 0.2, 1.0)
### 第5步:逐片段操作
- 深度测试:检查片段是否被遮挡
- 颜色混合(如果开启)
- 写入颜色缓冲区(帧缓冲区)
## 六、关键机制解释
### 双缓冲(Double Buffering)
后台缓冲区(绘制中)→ glfwSwapBuffers() → 前台缓冲区(显示)
作用:避免屏幕闪烁和撕裂,用户只看到完整的帧
### OpenGL状态机
glBindVertexArray(VAO); // 设置当前VAO为"激活状态"
glDrawArrays(...); // 使用当前激活的VAO绘制
glBindVertexArray(0); // 恢复状态
### 并行处理
- 顶点着色器:GPU可能有数千个核心,同时处理所有顶点
- 片段着色器:同时处理数百万个像素
- 这就是GPU比CPU快的原因
## 七、完整数据流
CPU端:
vertices[] → glBufferData() → GPU显存(VBO)
↓
VAO配置: 位置属性(索引0) → 指向VBO
↓
渲染循环:
glUseProgram(shaderProgram) → 激活着色器
glBindVertexArray(VAO) → 告诉GPU使用这个VAO
glDrawArrays() → 开始渲染
↓
GPU端:
VBO数据 → 顶点着色器 → 图元装配 → 光栅化 → 片段着色器 → 帧缓冲区
(计算位置) (组装三角形) (生成像素) (计算颜色) (存储结果)
↓
glfwSwapBuffers() → 显示到屏幕
```
## 渲染循环(Render Loop)深度解析
```bash
## 一、为什么需要渲染循环?
### 如果没有渲染循环(静态绘制)
// 只绘制一次
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
// 程序结束
存在的问题:
- 窗口移动、大小改变时,画面不会更新
- 无法响应键盘/鼠标输入
- 动画、旋转、移动都无法实现
- 被其他窗口遮挡后恢复,画面丢失
### 有了渲染循环(持续绘制)
while (!glfwWindowShouldClose(window)) {
processInput(); // 处理输入
updateLogic(); // 更新游戏逻辑
render(); // 重新绘制
glfwSwapBuffers(); // 显示新画面
}
## 二、计算机图形学的原理
### 1. 动画本质是快速刷新
- 电影:24帧/秒(每帧约41.7ms)
- 游戏:60帧/秒(每帧约16.7ms)
- 屏幕本身一直在刷新(通常60Hz或144Hz)
渲染循环确保每帧都重新绘制:
帧1 → 帧2 → 帧3 → 帧4 → ...
↓ ↓ ↓ ↓
显示 显示 显示 显示
### 2. 双缓冲机制需要持续交换
glfwSwapBuffers(window); // 交换前后缓冲区
- 后台缓冲区:正在绘制
- 前台缓冲区:正在显示
- 每帧都需要交换,否则画面静止
## 三、实际应用场景对比
### 场景1:静态编辑器(如Photoshop)
while (!quit) {
if (有用户操作) {
重新绘制选中区域();
}
}
特点:只在需要时重绘(节约GPU资源)
### 场景2:动态游戏(如三角形程序)
while (!quit) {
handleInput(); // 每帧都检查按键
updatePhysics(); // 每帧更新位置
render(); // 每帧重新绘制
}
特点:每帧都重绘,因为:
- 物体可能在移动
- 摄像机可能在旋转
- 动画在播放
- 粒子系统在更新
## 四、没有渲染循环会怎样?
### 实验代码
int main() {
// ... 初始化代码 ...
// 只绘制一次
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);
glfwSwapBuffers(window);
// 没有渲染循环
sleep(5); // 等待5秒
glfwTerminate();
return 0;
}
### 实验结果
- 三角形显示5秒后程序退出
- 按ESC无效(无法响应输入)
- 窗口拖动后可能黑屏
- 无法做任何交互
## 五、渲染循环的典型结构
while (!glfwWindowShouldClose(window)) {
// 1. 处理输入(非阻塞)
processInput(window);
// 2. 更新逻辑(位置、动画、物理等)
updateGameState(deltaTime);
// 3. 渲染
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawAllObjects();
// 4. 交换缓冲区
glfwSwapBuffers(window);
// 5. 处理窗口事件
glfwPollEvents();
// 6. 帧率控制(可选)
sleep(16ms); // 限制60帧
}
## 六、为什么叫"循环"?
英文名称:Render Loop 或 Game Loop
流程图:
┌─────────────────────────────────────┐
│ │
│ 初始化 → [渲染循环] → 清理资源 │
│ ↓ ↑ │
│ 绘制一帧 │ │
│ ↓ │ │
│ 显示画面 │ │
│ ↓ │ │
│ 检查退出条件 ─┘ │
│ │
└─────────────────────────────────────┘
## 七、真实世界的类比
### 电影院放映机
- 胶片每秒转动24格
- 每格都要重新投射光线
- 循环过程:投射 → 换下一格 → 投射 → ...
### 翻书动画
- 每页都要重新绘制
- 快速翻页形成动画
- 循环过程:翻页 → 看画面 → 翻下一页 → ...
## 八、性能考虑
### 为什么不是死循环?
// 错误示例
while (true) { // 死循环
render();
}
问题:
- CPU占用100%(浪费电)
- 没有事件处理,窗口会"无响应"
### 正确做法:事件驱动 + 渲染循环
while (!shouldClose) {
glfwPollEvents(); // 处理事件,没有事件就立即返回
render(); // 持续渲染
// CPU占用仍然较高,但可以响应事件
}
### 节能优化版本
while (!shouldClose) {
if (有事件需要处理 || 有动画在播放) {
render();
} else {
glfwWaitEvents(); // 无事件时休眠,节能
}
}
## 九、程序执行流程对比
### 有渲染循环(正常运行)
int main() {
// 初始化代码...
while (!glfwWindowShouldClose(window)) { // 条件永远为真,无限循环
// 不断执行
}
// 清理代码...
return 0; // 循环结束后才执行这里
}
结果:程序卡在循环里,窗口一直显示
### 没有渲染循环(闪退)
int main() {
// 初始化代码...
glfwCreateWindow(...);
glewInit();
// ... 创建VAO/VBO/着色器等
// while (!glfwWindowShouldClose(window)) { // 被注释了
// 清理资源
glDeleteVertexArrays(...);
glDeleteBuffers(...);
glfwTerminate();
return 0; // 程序立即结束!
}
结果:程序执行完所有代码后立即退出,窗口一闪而过
### 为什么会"闪一下"
程序启动 → 创建窗口 → 初始化OpenGL → 绘制一帧? → 清理资源 → 销毁窗口 → 程序退出
↓ ↓
正常执行 窗口显示时间极短(几毫秒)
注意:没有渲染循环时,连一帧都没有绘制!因为绘制命令在while循环里
## 十、glfwWindowShouldClose 的工作原理
int glfwWindowShouldClose(GLFWwindow* window) {
return window->shouldClose; // 默认是 false (0)
}
- 初始返回 0(false)
- 用户点击关闭按钮 → 设置为 1(true)
- 按 ESC 键调用 glfwSetWindowShouldClose(window, true) → 设置为 1
while (!glfwWindowShouldClose(window)) 等价于:
while (window->shouldClose == false) { // 无限循环直到用户关闭窗口
// ...
}
## 十一、窗口的生命周期
创建窗口 渲染循环运行 关闭窗口
↓ ↓ ↓
[创建] ────────→ [活跃状态] ──────→ [销毁]
↑
│
用户看到窗口
没有渲染循环:
[创建] ──→ [立即销毁] (用户几乎看不到)
## 十二、总结
### 渲染循环存在的原因:
1. 动画需求:每秒60次的刷新才能产生流畅动画
2. 交互需求:实时响应键盘/鼠标输入
3. 动态场景:物体位置、颜色、大小随时间变化
4. 窗口系统:窗口移动、调整大小需要重新绘制
5. 双缓冲机制:需要持续交换前后缓冲区
### 核心结论:
没有渲染循环 = 静态图片
有渲染循环 = 动态交互程序
即使三角形现在不动,通过渲染循环可以轻松地让它旋转、移动或改变颜色。
```