阅读《计算机图形学编程(使用OpenGL和C++)》4 - 画立方体
绘制一个对象,它的顶点数据需要发送给顶点着色器。通常会把顶点数据在C++端放入一个缓冲区,并把这个缓冲区和着色器中声明的顶点属性相关联。其步骤如下:
只做一次的步骤,一般放在 init() 中。
1、创建一个缓冲区。
2、将顶点数据复制进缓冲区。
如果是动画场景的话,每帧都要做,一般在 display() 中。
1、启用包含了顶点数据的缓冲区。
2、将这个缓冲区和一个顶点属性相关联。
3、启用这个顶点属性。
4、使用 glDrawArrays() 绘制对象。
在 OpenGL 中,缓冲区被包含在顶点缓冲对象 (Vertex Buffer Object, VBO) 中,通常在程序开始的时候统一创建。
当 glDrawArrays() 执行时,缓冲区中的数据开始流动,从缓冲区的开头开始,按顺序流过顶点着色器。顶点着色器对每个顶点执行一次。3D空间中的顶点需要3个数值,所以着色器中的顶点属性常常会以vec3类型接收到这3个数值。然后,对缓冲区中的每组这3个数值,着色器会被调用。指定 GL_TRIANGLES时,光栅化是逐个三角形完成的。对 glDrawArrays() 的调用通常在其他调整这个模型的渲染设置的命令之前。
OpenGL中还有一种相关结构,叫作顶点数组对象 (Vertex Array Object, VAO),至少创建一个VAO。
下面两个 OpenGL 命令分别创建 VAO 和 VBO,并返回它们的整数型ID。这两个命令各自有两个参数,1、创建多少个ID。2、用来保存返回的 ID 的数组。
glGenVertexArrays(GLsizei n, GLuint* arrays);
glGenBuffers(GLsizei n, GLuint* buffers);
glBindVertexArray()命令将指定的 VAO 标记为“活跃”,这样生成的缓冲区就会和这个 VAO相关联。
glBindVertexArray(GLuint array)
每个缓冲区需要有在顶点着色器中声明的相应的顶点属性变量。顶点属性通常是着色器中首先被声明的变量。我们在顶点着色器中可以这样声明:
layout (location = 0) in vec3 position;
关键字 "in" 是输入 (input) 的意思,表示这个顶点属性将会从缓冲区中接收数值。
"vec3" 是着色器每次调用会抓到3个浮点类型数值,分别表示x、y、z,它们组成一个顶点数据。
变量名字 "position"。
"layout (location = 0)" 叫作 "layout修饰符",就是我们把顶点属性和特定缓冲区关联起来的方法。这个顶点属性的识别号是0。
现在我们绘制一个立方体。
要渲染一个场景以使它看起来是3D的,需要构建适当的变换矩阵。并将它们应用于模型的每个顶点。在顶点着色器中应用所需的矩阵运算是最有效的,并且习惯上会将这些矩阵从 C++/OpenGL 应用程序发送给着色器中的统一变量。
关键字 "uniform" 在着色器中声明统一变量。
uniform mat4 mv_matrix; uniform mat4 proj_matrix;
关键字 "mat4"表示这些是4×4矩阵。因为3D变换是4×4,因此mat4是GLSL着色器统一中常用的数据类型。
在片段着色器光栅化之前,由顶点定义的图元被转换为片段。光栅化过程会线性插值顶点属性值,以便显示的像素能无缝连接建模的曲面。
统一变量类似初始化过的常量,并且在每次顶点着色器调用(即从缓冲区发送的每个顶点)中保持不变。统一变量本身不是插值的;无论有多少顶点,它始终包含相同的值。
在顶点着色器中看到顶点属性被声明为 "in",表示它们从缓冲区接收值。顶点属性还可以改为声明为 "out",它们会将值发送到管线中的下一个阶段。OpenGL 有一个内置的vec4变量名字叫作 gl_Position 为顶点位置声明的一个 "out" 变量。变换后的顶点将自动输出到光栅着色器,最终将相应的像素位置发送到片段着色器。
可以构建3个矩阵并将它们发送到统一变量:模型矩阵、视图矩阵、透视矩阵。
模型矩阵在世界坐标空间中表示对象的位置和朝向。如果模型移动,需要不断重建该矩阵。
视图矩阵移动并旋转世界中的模型,以模拟相机在所需位置的效果。因为相机可以移动,所以它也需要每帧创建一次。根据所需的摄像机位置和朝向构建。将模型和视图矩阵结合成单个 "MV" 矩阵。
透视矩阵是一种变换,它根据所需的视锥提供3D效果。只需要创建一次,它需要使用屏幕窗口的宽度和高度(以及所需的视锥体参数),除非调整窗口大小,否则通常不变。
将 MV 和投影矩阵发送到相应的着色器统一变量。
main.cpp
#include <GL/glew.h> #include <GLFW/glfw3.h> #include <iostream> #include "glm/glm.hpp" #include "glm/gtc/matrix_transform.hpp" #include "glm/gtc/type_ptr.hpp" #include "Utils.h" using namespace std; #define numVAOs 1 #define numVBOs 2 float cameraX, cameraY, cameraZ; float cubeLocX, cubeLocY, cubeLocZ; GLuint renderingProgram; GLuint vao[numVAOs]; // OpenGL要求这些数值以数组的形式指定 GLuint vbo[numVBOs]; // 给display()用 GLuint mvLoc, projLoc; int width, height; float aspect; glm::mat4 pMat, vMat, mMat, mvMat; void setupVertices(void) { float vertexPositions[108] = { -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, -1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, -1.0f, }; glGenVertexArrays(numVAOs, vao); // 创建一个vao,并返回它的整数型ID存进数组vao中 glBindVertexArray(vao[0]); // 激活vao glGenBuffers(numVBOs, vbo);// 创建两个vbo,并返回它们的整数型ID存进数组vbo中 glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); // 激活vbo第0个缓冲区 glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW); // 将包含顶点数据的数组复制进活跃缓冲区(这里是第0个VBO) } void init(GLFWwindow* window) { renderingProgram = Utils::createShaderProgram("vertShader.glsl", "fragShader.glsl"); cameraX = 0.0f; cameraY = 0.0f; cameraZ = 8.0f; cubeLocX = 0.0f; cubeLocY = -2.0f; cubeLocZ = 0.0f; // 沿Y轴下移以展示透视 setupVertices(); } void display(GLFWwindow* window, double currentTime) { glClear(GL_DEPTH_BUFFER_BIT); glUseProgram(renderingProgram); // 获取MV矩阵和投影矩阵的统一变量的引用 mvLoc = glGetUniformLocation(renderingProgram, "mv_matrix"); projLoc = glGetUniformLocation(renderingProgram, "proj_matrix"); // 构建透视矩阵 glfwGetFramebufferSize(window, &width, &height); aspect = (float)width / (float)height; pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f); // 1.0472 radians = 60 degrees // 构建视图矩阵、模型矩阵和视图-模型矩阵 vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ)); mMat = glm::translate(glm::mat4(1.0f), glm::vec3(cubeLocX, cubeLocY, cubeLocZ)); mvMat = vMat * mMat; // 将透视矩阵和MV矩阵复制给相应的统一变量 glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat)); // GLM函数调用value_ptr()返回对矩阵数据的引用 glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat)); // 将VBO关联给顶点着色器中相应的顶点属性 glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); // 标记第0个缓冲区为“活跃” glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); // 将第0个属性关联到缓冲区 glEnableVertexAttribArray(0); // 启用第0个顶点属性 // 调整OpenGL设置,绘制模型 glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glDrawArrays(GL_TRIANGLES, 0, 36); // 执行该语句,第0个VBO中的数据将被传输给拥有位置0的layout修饰符的顶点属性中。这会将立方体的顶点数据发送到着色器。 } int main(void) { GLFWwindow* window; /* Initialize the library */ if (!glfwInit()) return -1; /* Create a windowed mode window and its OpenGL context */ window = glfwCreateWindow(400, 300, "Hello World", NULL, NULL); //没用到的参数分别用来允许全屏显示以及资源共享 if (!window) { glfwTerminate(); return -1; } /* Make the window's context current */ glfwMakeContextCurrent(window); GLenum err = glewInit(); if (err != GLEW_OK) { std::cout << "Error: " << glewGetErrorString(err) << std::endl; } glfwSwapInterval(1); // 交互缓冲区间隔设为1,即每帧更新一次,交换间隔表示交换缓冲区之前等待的帧数,通常称为Vsync垂直同步 init(window); /* Loop until the user closes the window */ while (!glfwWindowShouldClose(window)) { display(window, glfwGetTime()); // glfwGetTime()返回GLFW初始化之后经过的时间 /* Swap front and back buffers */ glfwSwapBuffers(window); // GLFW默认使用两个缓冲区 /* Poll for and process events */ glfwPollEvents(); // 处理窗口相关事件(如按键事件) } glfwDestroyWindow(window); glfwTerminate(); return 0; }
顶点着色器 vertShader.glsl
#version 460 layout (location = 0) in vec3 position; uniform mat4 mv_matrix; uniform mat4 proj_matrix; void main(void) { gl_Position = proj_matrix * mv_matrix *vec4(position, 1.0); }
顶点着色器中传入的顶点属性position的位置被指定为"0",因此display()函数可以简单地通过在glVertexAttribPointer()函数调用的第一个参数和glEnableVertexAttribArray()函数调用中使用0来引用此变量。
片段着色器 fragShader.glsl
#version 460 out vec4 color; uniform mat4 mv_matrix; uniform mat4 proj_matrix; void main(void) { color = vec4(1.0, 0.0, 0.0, 1.0); }
结果如下:
用三角形创建一个立方体,一个面由2个三角形组成,一共六个面,即12个三角形,36个顶点,每个顶点具有3个值(x, y, z),因此数组中有36×3=108个值。
这里建立了两个VBO,但只用了一个,将立方体顶点加载到第0个VBO缓冲区中。
init() 函数还给定了立方体和相机在世界中的位置。
要一个构建透视矩阵的工具函数,GLM已经包含了一个:
// 给定Y轴的指定视场角、屏幕纵横比以及所需的近、远剪裁平面的情况 glm::perspective(<field of view>, <aspect ratio>, <near plane>, <far plane>);
对 translate() 函数的 GLM 调用的形式,构建一个变换矩阵:从单位矩阵和以向量的形式指定变换值。许多 GLM 变换操作使用这种方法。
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
注意着色器,它们都包含相同的统一变量声明块。并不总是一定要这样做,但在特定渲染程序中的所有着色器中包含相同的统一变量声明块通常是一种好习惯。
让我们对着色器进行一些轻微的修改。
顶点着色器 vertShader.glsl
#version 460 layout (location = 0) in vec3 position; uniform mat4 mv_matrix; uniform mat4 proj_matrix; out vec4 varyingColor; void main(void) { gl_Position = proj_matrix * mv_matrix *vec4(position, 1.0); varyingColor = vec4(position, 1.0) * 0.5 + vec4(0.5, 0.5, 0.5, 0.5); }
我们根据每个顶点的位置为每个顶点指定一种颜色,并将该颜色放在输出的顶点属性varyingColor中。代码中将位置乘以0.5,然后加0.5,以将取值范围从 [-1...+1] 转换为 [0...1]。
片段着色器 fragShader.glsl
#version 460 in vec4 varyingColor; out vec4 color; uniform mat4 mv_matrix; uniform mat4 proj_matrix; void main(void) { color = varyingColor; }
接收传入的颜色(由光栅着色器插值)并用它设置输出像素的颜色。
结果可看到整个立方体从一个角到另一个角明显是被插值了。管线处理(顶点着色器、曲面细分着色器、几何着色器、光栅化、片段着色器、像素操作)
由于main()函数包含一个渲染循环,故我们可以基于时间变化的平移和旋转来构建模型矩阵为立方体设置动画。
在main.cpp 开头定义变量处定义
glm::mat4 tMat, rMat;
在display()函数中修改代码如下:
void display(GLFWwindow* window, double currentTime) { glClear(GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT); ...// 使用当前时间来计算x, y和z的不同变换 tMat = glm::translate(glm::mat4(1.0f), glm::vec3(sin(0.35f*currentTime)*2.0f, cos(0.52f*currentTime)*2.0f, sin(0.7f*currentTime)*2.0f)); // 用1.75来调整旋转速度 rMat = glm::rotate(glm::mat4(1.0f), 1.75f*(float)currentTime, glm::vec3(0.0f, 1.0f, 0.0f)); rMat = glm::rotate(rMat, 1.75f*(float)currentTime, glm::vec3(1.0f, 0.0f, 0.0f)); rMat = glm::rotate(rMat, 1.75f*(float)currentTime, glm::vec3(0.0f, 0.0f, 1.0f)); mMat = tMat * rMat; // 构建视图矩阵、模型矩阵和视图-模型矩阵 vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ)); //mMat = glm::translate(glm::mat4(1.0f), glm::vec3(cubeLocX, cubeLocY, cubeLocZ)); mvMat = vMat * mMat; ...
}
添加此动画说明了每次通过display()清除深度缓冲区以确保正确进行隐藏面消除的重要性。它还需要清除颜色缓冲区;否则,立方体会在移动时留下痕迹。
注意最后一行中的矩阵乘法——操作中tMat和rMat的顺序很重要。它计算两个变换的结合,平移放在左边,旋转放在右边。当顶点随后乘以此矩阵时,计算从右到左进行,这意味着首先完成旋转,然后才是平移。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了