【从零开始学 OpenGL:现代图形渲染实战】第02篇-渲染管线与第一个三角形
2026/4/6 18:23:45 网站建设 项目流程
第2篇渲染管线与第一个三角形前置知识第1篇环境搭建与第一个窗口 — 你需要已经配置好 GLFW GLAD 的开发环境并能创建一个空白窗口。本篇目标理解 OpenGL可编程渲染管线的完整流程掌握VBO、VAO、EBO三大缓冲对象的概念与用法学会编写、编译、链接最简着色器Shader在屏幕上渲染一个彩色三角形使用EBO索引绘制画一个矩形一、OpenGL 可编程渲染管线OpenGL 的渲染管线是一条将 3D 顶点数据最终转化为屏幕上 2D 像素的流水线。下图展示了它的主要阶段1.1 顶点着色器Vertex Shader顶点着色器是管线中第一个可编程阶段。它对每个输入顶点执行一次主要职责是将顶点坐标从模型空间变换到裁剪空间通过 MVP 矩阵将颜色、法线等顶点属性传递给后续阶段最简顶点着色器示例#version 330 core layout (location 0) in vec3 aPos; layout (location 1) in vec3 aColor; out vec3 vertexColor; void main() { gl_Position vec4(aPos, 1.0); vertexColor aColor; }gl_Position是内置变量表示该顶点在裁剪空间中的位置。layout (location 0)指定了顶点属性的索引与后面glVertexAttribPointer对应。1.2 图元装配Primitive Assembly顶点着色器处理完所有顶点后图元装配阶段将顶点按照指定模式GL_TRIANGLES、GL_LINES等组装成几何图元三角形、线段、点。通俗地说图元装配就是将分散的顶点按规则连接成三角形或其他基本形状的过程。你可以理解为把散落的点用线连起来组成一个个小三角形。1.3 光栅化Rasterization光栅化将图元映射到屏幕像素上生成片段Fragment。每个片段对应屏幕上一个潜在的像素位置并携带经过插值的属性颜色、纹理坐标等。通俗地说光栅化就是将数学描述的几何图形转换为屏幕上的像素点阵的过程类比一下就像把矢量图转换为位图——把用公式描述的三角形变成由一个个像素点填充的三角形。关键概念如果三角形三个顶点有不同颜色光栅化阶段会自动进行线性插值使得三角形内部呈现平滑的颜色渐变。1.4 片段着色器Fragment Shader片段着色器是管线中第二个可编程阶段。它对每个片段执行一次决定最终像素的颜色。#version 330 core in vec3 vertexColor; out vec4 FragColor; void main() { FragColor vec4(vertexColor, 1.0); }1.5 测试与混合Tests Blending管线最后阶段包含一系列测试和操作阶段作用深度测试比较片段深度值丢弃被遮挡的片段模板测试根据模板缓冲区的值决定是否丢弃片段混合将片段颜色与帧缓冲中已有颜色混合实现半透明效果通过所有测试的片段颜色被写入帧缓冲区最终显示到屏幕上。二、顶点数据与缓冲对象2.1 标准化设备坐标NDC在没有任何变换矩阵的情况下OpenGL 使用标准化设备坐标x、y、z 三个轴的范围都是[-1.0, 1.0]。只有落在这个范围内的坐标才会最终显示到屏幕上。2.2 VBOVertex Buffer ObjectVBO 是一块 GPU 上的内存区域用来存储顶点数据。使用 VBO 可以一次性将大量顶点数据发送到 GPU避免逐顶点传输的性能开销。floatvertices[]{// 位置 // 颜色-0.5f,-0.5f,0.0f,1.0f,0.0f,0.0f,// 左下 - 红0.5f,-0.5f,0.0f,0.0f,1.0f,0.0f,// 右下 - 绿0.0f,0.5f,0.0f,0.0f,0.0f,1.0f// 顶部 - 蓝};unsignedintVBO;glGenBuffers(1,VBO);glBindBuffer(GL_ARRAY_BUFFER,VBO);glBufferData(GL_ARRAY_BUFFER,sizeof(vertices),vertices,GL_STATIC_DRAW);glBufferData的最后一个参数指定数据使用模式模式含义GL_STATIC_DRAW数据几乎不变最常用GL_DYNAMIC_DRAW数据会频繁改变GL_STREAM_DRAW数据每次绘制都改变2.3 VAOVertex Array ObjectVAO 记录了如何解读 VBO 中的顶点数据。它保存了glVertexAttribPointer的调用状态这样在绑定 VAO 后就能自动恢复所有顶点属性配置。自动恢复是什么意思VAO 就像一个配置快照。在初始化阶段你绑定一个 VAO然后配置好所有顶点属性glVertexAttribPointer、glEnableVertexAttribArray、绑定 EBO 等这些配置都会被 VAO 记住。之后在渲染循环中你只需要glBindVertexArray(VAO)一行代码之前的全部配置就自动生效了不需要重新调用一遍。这在绘制多个不同物体时尤其有用——每个物体一个 VAO切换绘制时只需切换 VAO 即可。unsignedintVAO;glGenVertexArrays(1,VAO);glBindVertexArray(VAO);// 在这之后配置 VBO 和顶点属性……2.4 顶点属性与交错布局我们的顶点数据采用交错存储每个顶点的位置和颜色紧挨着排列。// 位置属性 (location 0)glVertexAttribPointer(0,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)0);glEnableVertexAttribArray(0);// 颜色属性 (location 1)glVertexAttribPointer(1,3,GL_FLOAT,GL_FALSE,6*sizeof(float),(void*)(3*sizeof(float)));glEnableVertexAttribArray(1);glEnableVertexAttribArray的作用glVertexAttribPointer只是告诉 OpenGL 某个属性的数据怎么读格式、步长、偏移等但该属性通道默认是关闭的。必须调用glEnableVertexAttribArray将其打开数据才能真正流向着色器。如果漏掉这一步着色器中对应的in变量会收到默认值(0, 0, 0, 1)——这也是三角形显示纯黑的常见原因之一。glVertexAttribPointer各参数含义参数说明第1个属性索引对应着色器layout (location x)第2个属性分量数vec3 → 3第3个数据类型第4个是否归一化第5个步长stride相邻两个同属性值之间的字节距离第6个偏移offset该属性在一个顶点中的起始字节位置2.5 EBOElement Buffer Object绘制矩形需要两个三角形6个顶点但矩形只有4个顶点其中有2个顶点被共用了。EBO 通过索引数组引用 VBO 中的顶点避免重复存储。floatrectVertices[]{0.5f,0.5f,0.0f,// 右上0.5f,-0.5f,0.0f,// 右下-0.5f,-0.5f,0.0f,// 左下-0.5f,0.5f,0.0f// 左上};unsignedintindices[]{0,1,3,// 第一个三角形1,2,3// 第二个三角形};unsignedintEBO;glGenBuffers(1,EBO);glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,EBO);glBufferData(GL_ELEMENT_ARRAY_BUFFER,sizeof(indices),indices,GL_STATIC_DRAW);使用glDrawElements代替glDrawArrays进行索引绘制glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);三、着色器的编译与链接着色器程序需要经历编写 → 编译 → 链接三个步骤3.1 编译着色器unsignedintvertexShaderglCreateShader(GL_VERTEX_SHADER);glShaderSource(vertexShader,1,vertexShaderSource,NULL);glCompileShader(vertexShader);// 检查编译错误intsuccess;charinfoLog[512];glGetShaderiv(vertexShader,GL_COMPILE_STATUS,success);if(!success){glGetShaderInfoLog(vertexShader,512,NULL,infoLog);std::cerr顶点着色器编译失败:\ninfoLogstd::endl;}3.2 链接着色器程序unsignedintshaderProgramglCreateProgram();glAttachShader(shaderProgram,vertexShader);glAttachShader(shaderProgram,fragmentShader);glLinkProgram(shaderProgram);// 检查链接错误glGetProgramiv(shaderProgram,GL_LINK_STATUS,success);if(!success){glGetProgramInfoLog(shaderProgram,512,NULL,infoLog);std::cerr着色器程序链接失败:\ninfoLogstd::endl;}// 链接成功后删除着色器对象glDeleteShader(vertexShader);glDeleteShader(fragmentShader);3.3 使用着色器程序glUseProgram(shaderProgram);四、核心 API 速查表缓冲对象API说明glGenBuffers(n, id)生成 n 个缓冲对象ID 存入 idglBindBuffer(target, id)将缓冲对象绑定到目标GL_ARRAY_BUFFER/GL_ELEMENT_ARRAY_BUFFERglBufferData(target, size, data, usage)创建缓冲存储并上传数据顶点数组对象API说明glGenVertexArrays(n, id)生成 n 个 VAOglBindVertexArray(id)绑定 VAO后续顶点属性配置都记录在此 VAO 中glVertexAttribPointer(index, size, type, normalized, stride, offset)定义顶点属性的读取方式glEnableVertexAttribArray(index)启用指定索引的顶点属性着色器API说明glCreateShader(type)创建着色器对象GL_VERTEX_SHADER/GL_FRAGMENT_SHADERglShaderSource(shader, count, source, lengths)设置着色器源码glCompileShader(shader)编译着色器glCreateProgram()创建着色器程序glAttachShader(program, shader)将着色器附加到程序glLinkProgram(program)链接着色器程序glUseProgram(program)激活着色器程序绘制API说明glDrawArrays(mode, first, count)按顺序绘制顶点glDrawElements(mode, count, type, offset)使用索引绘制五、代码实战完整源码见 src/main.cpp。下面是关键流程总结5.1 整体流程初始化 GLFW / GLAD ↓ 编写着色器源码字符串 ↓ 编译顶点着色器 片段着色器 ↓ 链接为着色器程序 ↓ 准备顶点数据 → 创建 VAO / VBO/ EBO ↓ 渲染循环 清屏 → 激活着色器 → 绑定 VAO → 绘制 → 交换缓冲 ↓ 清理资源 → 退出5.2 彩色三角形三角形的三个顶点分别为红、绿、蓝色光栅化阶段的插值会产生平滑的彩色渐变效果。核心顶点数据floattriangleVertices[]{// 位置 // 颜色-0.5f,-0.5f,0.0f,1.0f,0.0f,0.0f,// 左下 - 红0.5f,-0.5f,0.0f,0.0f,1.0f,0.0f,// 右下 - 绿0.0f,0.5f,0.0f,0.0f,0.0f,1.0f// 顶部 - 蓝};绘制调用glBindVertexArray(triangleVAO);glDrawArrays(GL_TRIANGLES,0,3);5.3 EBO 矩形矩形由4个顶点 6个索引组成。按下空格键可以在三角形和矩形之间切换显示。unsignedintrectIndices[]{0,1,3,1,2,3};绘制调用glBindVertexArray(rectVAO);glDrawElements(GL_TRIANGLES,6,GL_UNSIGNED_INT,0);5.4 构建与运行cdsrcmkdirbuildcdbuild cmake..make./OpenGL_Triangle六、常见问题Q1窗口一片黑什么都看不到排查清单确认glClearColor和glClear被正确调用检查着色器编译/链接是否有错误日志输出确认顶点坐标在 NDC 范围 [-1, 1] 内确认glUseProgram在glDrawArrays之前调用确认 VAO 在绑定 VBO 和配置属性之前绑定Q2三角形是纯白/纯黑的没有颜色片段着色器可能没有正确接收vertexColor。确认顶点着色器的out变量名和片段着色器的in变量名一致。确认第二个顶点属性颜色已经通过glEnableVertexAttribArray(1)启用。Q3glVertexAttribPointer的 stride 和 offset 怎么算stride 一个顶点占用的总字节数。我们的顶点有位置(3 float) 颜色(3 float) 6 float 6 * sizeof(float) 24 字节。offset 该属性在顶点内部的起始偏移。位置是第一个属性偏移为 0颜色紧随其后偏移为3 * sizeof(float) 12 字节。Q4为什么要在链接后glDeleteShader着色器对象在链接到程序后就不再需要了。删除它们可以释放 GPU 上的编译中间产物是良好实践。程序对象仍然持有编译后的代码。Q5GL_STATIC_DRAW和GL_DYNAMIC_DRAW有什么区别它们是给驱动的提示帮助驱动决定把数据放在哪种内存区域。STATIC适合不变的几何体DYNAMIC适合频繁更新的数据如粒子系统。用错不会导致错误但可能影响性能。七、练习练习 1倒三角形修改顶点数据将三角形翻转为倒三角形尖朝下。思考一下你需要改哪些坐标练习 2两个三角形使用两个 VAO 和两个 VBO在屏幕上并排绘制两个不同颜色的三角形。提示一个三角形偏左一个偏右。练习 3线框模式调用glPolygonMode(GL_FRONT_AND_BACK, GL_LINE)将矩形以线框模式绘制。尝试切换回GL_FILL模式观察区别。如何在运行时通过按键切换两种模式参考资料LearnOpenGL - Hello TriangleOpenGL Reference PagesKhronos OpenGL Wiki - Rendering Pipeline

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询