图形学:解析绘制三角形实例

管理员
## 三角形实例源码 ```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. 双缓冲机制:需要持续交换前后缓冲区 ### 核心结论: 没有渲染循环 = 静态图片 有渲染循环 = 动态交互程序 即使三角形现在不动,通过渲染循环可以轻松地让它旋转、移动或改变颜色。 ```
所属分类:
评论 0

发表评论 取消回复

Shift+Enter 换行  ·  Enter 发送
还没有评论,来发表第一条吧