阅读《计算机图形学编程(使用OpenGL和C++)》7 - 同一个场景渲染不同的对象
同一个场景渲染不同的对象,一种简单的方法是为每个模型使用单独的缓冲区。每个模型都需要自己的模型矩阵,这样我们就需要为我们渲染的每个模型生成一个新的模型-视图矩阵。还需要为每个模型单独调用glDrawArrays()。因此,我们需要修改init()和display()函数。
让我们继续添加一个简单的金字塔,这样我们的场景就包括一个立方体和一个金字塔。
顶点和片段着色器代码被省略了,他们和之前4节最后展示的代码一样。
main.cpp
void setupVertices(void) { // 立方体 float cubePositions[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, }; // 金字塔有18个顶点,由6个三角形组成(侧面4个,底面2个) float pyramidOsitions[54] = { -1.0f, -1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.0f, // 前面 1.0f, -1.0f, 1.0f, 1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 0.0f, // 右面 1.0f, -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 0.0f, 1.0f, 0.0f, // 后面 -1.0f, -1.0f, -1.0f, -1.0f, -1.0f, 1.0f, 0.0f, 1.0f, 0.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(1, 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(cubePositions), cubePositions, GL_STATIC_DRAW); // 将包含顶点数据的数组复制进活跃缓冲区(这里是第0个VBO) glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); // 激活vbo第0个缓冲区 glBufferData(GL_ARRAY_BUFFER, sizeof(pyramidOsitions), pyramidOsitions, GL_STATIC_DRAW); // 将包含顶点数据的数组复制进活跃缓冲区(这里是第1个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轴下移以展示透视 pyrLocX = 2.0f; pyrLocY = 2.0f; pyrLocZ = 0.0f; setupVertices(); } void display(GLFWwindow* window, double currentTime) { glClear(GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_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)); // 绘制立方体(使用0号缓冲区) mMat = glm::translate(glm::mat4(1.0f), glm::vec3(cubeLocX, cubeLocY, cubeLocZ)); mvMat = vMat * mMat; glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat)); // 着色器需要视图矩阵的统一变量 glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat)); glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); // 标记第0个缓冲区为“活跃” glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); // 将第0个属性关联到缓冲区 glEnableVertexAttribArray(0); // 启用第0个顶点属性 glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glDrawArrays(GL_TRIANGLES, 0, 36); // 执行该语句,第0个VBO中的数据将被传输给拥有位置0的layout修饰符的顶点属性中。这会将立方体的顶点数据发送到着色器。 // 绘制金字塔(使用1号缓冲区) mMat = glm::translate(glm::mat4(1.0f), glm::vec3(pyrLocX, pyrLocY, pyrLocZ)); mvMat = vMat * mMat; glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvMat)); // 着色器需要视图矩阵的统一变量 glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat)); glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); // 标记第1个缓冲区为“活跃” glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); // 将第0个属性关联到缓冲区 glEnableVertexAttribArray(0); // 启用第0个顶点属性 glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LEQUAL); glDrawArrays(GL_TRIANGLES, 0, 18); // 执行该语句,第1个VBO中的数据将被传输给拥有位置0的layout修饰符的顶点属性中。这会将金字塔的顶点数据发送到着色器。 }
结果如下:
矩阵堆栈
矩阵堆栈是一堆变换矩阵。它使得变换可以构建在其他变换之上(或者从其他变换中被移除)。
可以使用C++标准模板库(STL)的stack类。
“*=”运算符在mat4中被重载,因此它可以用于连接矩阵。
我们不再通过创建mat4的实例来构建变换,而是使用push()命令在堆栈顶部创建新的矩阵。然后再根据需要将期望的变换应用于堆栈顶部的新创建的矩阵。
模仿太阳系运行的动画效果。
将视图矩阵放入堆栈中,位于视图矩阵正上方的矩阵将是太阳的MV矩阵。在它之上的矩阵将是地球的MV矩阵,由太阳的MV矩阵的副本和应用于其之上的地球模型矩阵变换组成。也就是说,地球的MV矩阵是通过将行星的变换结合到太阳的变换中而建立的。同样,月球的MV矩阵位于行星的MV矩阵之上,并通过将月球的模型矩阵变换应用于紧邻其下方的行星的MV矩阵来构建。
在渲染月球之后,可以通过从堆栈中“弹出”第一个月球的矩阵(将堆栈的顶部恢复到行星的模型-视图矩阵),然后重复第二个月球的过程,来渲染第二个“月球”。
基本方法如下。
(1)我们声明我们的堆栈,给它起名为“mvStack”。
(2)当相对于父对象创建新对象时,调用“mvStack.push(mvStack.top())”。
(3)应用新对象所需的变换,也就是将所需的变换乘以它。
(4)完成对象或子对象的绘制后,调用“mvStack.pop()”从矩阵堆栈顶部移除其模型-视图矩阵。
main.cpp
void display(GLFWwindow* window, double currentTime) { glClear(GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_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 glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(pMat)); // 将视图矩阵推入堆栈 vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ)); mvStack.push(vMat); // 金字塔 == 太阳 mvStack.push(mvStack.top()); mvStack.top() *= glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, 0.0f)); // 太阳位置 mvStack.push(mvStack.top()); mvStack.top() *= glm::rotate(glm::mat4(1.0f), (float)currentTime, glm::vec3(1.0f, 0.0f, 0.0f)); // 太阳旋转 glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvStack.top())); glBindBuffer(GL_ARRAY_BUFFER, vbo[1]); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(0); glEnable(GL_DEPTH_TEST); glEnable(GL_LEQUAL); glDrawArrays(GL_TRIANGLES, 0, 18); // 绘制太阳 mvStack.pop(); // 从堆栈中移除太阳的轴旋转 // 立方体 == 行星 mvStack.push(mvStack.top()); mvStack.top() *= glm::translate(glm::mat4(1.0f), glm::vec3(sin((float)currentTime)*4.0, 0.0f, cos((float)currentTime)*4.0)); mvStack.push(mvStack.top()); mvStack.top() *= glm::rotate(glm::mat4(1.0f), (float)currentTime, glm::vec3(0.0, 1.0, 0.0)); // 行星旋转 glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvStack.top())); glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(0); glDrawArrays(GL_TRIANGLES, 0, 36); // 绘制行星 mvStack.pop(); // 从堆栈中移除行星的轴旋转 // 小立方体 == 月球 mvStack.push(mvStack.top()); mvStack.top() *= glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, sin((float)currentTime)*2.0, cos((float)currentTime)*2.0)); mvStack.top() *= glm::rotate(glm::mat4(1.0f), (float)currentTime, glm::vec3(0.0f, 0.0f, 1.0f)); // 月球旋转 mvStack.top() *= glm::scale(glm::mat4(1.0f), glm::vec3(0.25f, 0.25, 0.25)); // 让月球小一些 glUniformMatrix4fv(mvLoc, 1, GL_FALSE, glm::value_ptr(mvStack.top())); glBindBuffer(GL_ARRAY_BUFFER, vbo[0]); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0); glEnableVertexAttribArray(0); glDrawArrays(GL_TRIANGLES, 0, 36); // 绘制行星 // 从堆栈中移除月球缩放、旋转、位置矩阵,行星位置矩阵,太阳位置矩阵,和视图矩阵 mvStack.pop(); mvStack.pop(); mvStack.pop(); mvStack.pop(); }
着色器代码不变
效果如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律