阅读《计算机图形学编程(使用OpenGL和C++)》6 -同一个场景渲染相同的对象
渲染同一个场景,多个相同对象的情况,比如说24个立方体,可以利用循环变量来计算立方体的旋转和平移参数,以便每次绘制立方体时,都会构建不同的模型矩阵。
void display(GLFWwindow* window, double currentTime) { ... // 构建透视矩阵 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)); for (int i=0; i<24; i++) { tf = currentTime + i; // tf =="time factor(时间因子)" float类型 // 使用当前时间来计算x, y和z的不同变换 tMat = glm::translate(glm::mat4(1.0f), glm::vec3(sin(0.35f*tf)*8.0f, cos(0.52f*tf)*8.0f, sin(0.7f*tf)*8.0f)); // 用1.75来调整旋转速度 rMat = glm::rotate(glm::mat4(1.0f), 1.75f*tf, glm::vec3(0.0f, 1.0f, 0.0f)); rMat = glm::rotate(rMat, 1.75f*tf, glm::vec3(1.0f, 0.0f, 0.0f)); rMat = glm::rotate(rMat, 1.75f*tf, glm::vec3(0.0f, 0.0f, 1.0f)); mMat = tMat * rMat; 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修饰符的顶点属性中。这会将立方体的顶点数据发送到着色器。 } }
实例化提供只用一个调用就告诉显卡渲染一个对象的多个副本。将程序中的 glDrawArrays() 调用改为 glDrawArraysInstanced() ,这样就可以要求OpenGL 绘制多个副本。
顶点着色器可以访问内置变量gl_InstanceID,这是一个整数,指向当前正在处理对象的第几个实例。
为了使用实例化,需要将构建不同模型矩阵的计算[上方在display()中的循环内实现]移动到顶点着色器中。由于GLSL不提供平移或旋转函数,并且我们无法从着色器内部调用GLM,我们需要在着色器内自己编写工具函数。我们还需要将“时间因子”通过统一变量传递给顶点着色器。我们还需要将模型和视图矩阵传递到单独的统一变量中,因为对每个立方体的模型矩阵都需要进行旋转计算。
顶点着色器 vertShader.glsl
#version 460 layout (location = 0) in vec3 position; uniform mat4 m_matrix; // 这些是分开的模型和视图矩阵 uniform mat4 v_matrix; uniform mat4 proj_matrix; uniform float tf; // 用于动画和放置立方体的时间因子 out vec4 varyingColor; mat4 buildRotateX(float rad); // 矩阵变换工具函数的声明 mat4 buildRotateY(float rad); // GLSL要求函数先声明后调用 mat4 buildRotateZ(float rad); mat4 buildTranslate(float x, float y, float z); void main(void) { float i = gl_InstanceID + tf; // 取值基于时间因子,但是对每个立方体示例也都是不同的 float a = sin(203.0 * i/8000.0) * 403.0; // 这些是用来平移的x、y、z float b = sin(301.0 * i/4001.0) * 401.0; float c = sin(400.0 * i/6003.0) * 405.0; // 构建旋转和平移矩阵,将会应用于当前立方体的模型矩阵 mat4 localRotX = buildRotateX(1000*i); mat4 localRotY = buildRotateY(1000*i); mat4 localRotZ = buildRotateZ(1000*i); mat4 localTrans = buildTranslate(a, b, c); // 构建模型矩阵,然后是模型-视图矩阵 mat4 newM_matrix = m_matrix * localTrans * localRotX * localRotY * localRotZ; mat4 mv_matrix = v_matrix * newM_matrix; 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); } // 构建平移矩阵的工具函数 mat4 buildTranslate(float x, float y, float z){ mat4 trans = mat4(1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, x, y, z, 1.0); return trans; } //构建并返回绕X轴的旋转矩阵 mat4 buildRotateX(float rad){ mat4 xrot = mat4(1.0, 0.0, 0.0, 0.0, 0.0, cos(rad), -sin(rad), 0.0, 0.0, sin(rad), cos(rad), 0.0, 0.0, 0.0, 0.0, 1.0); return xrot; } //构建并返回绕Y轴的旋转矩阵 mat4 buildRotateY(float rad){ mat4 yrot = mat4(cos(rad), 0.0, sin(rad), 0.0, 0.0, 1.0, 0.0, 0.0, -sin(rad), 0.0, cos(rad), 0.0, 0.0, 0.0, 0.0, 1.0); return yrot; } //构建并返回绕Z轴的旋转矩阵 mat4 buildRotateZ(float rad){ mat4 zrot = mat4(cos(rad), -sin(rad), 0.0, 0.0, sin(rad), cos(rad), 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0); return zrot; }
在C++/OpenGL应用程序(display函数中)
void display(GLFWwindow* window, double currentTime) { glClear(GL_DEPTH_BUFFER_BIT); glClear(GL_COLOR_BUFFER_BIT); glUseProgram(renderingProgram); // 获取MV矩阵和投影矩阵的统一变量的引用 tfLoc = glGetUniformLocation(renderingProgram, "tf"); mLoc = glGetUniformLocation(renderingProgram, "m_matrix"); vLoc = glGetUniformLocation(renderingProgram, "v_matrix"); projLoc = glGetUniformLocation(renderingProgram, "proj_matrix"); // 构建视图矩阵、模型矩阵 vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ)); mMat = glm::translate(glm::mat4(1.0f), glm::vec3(cubeLocX, cubeLocY, cubeLocZ)); // 构建透视矩阵 glfwGetFramebufferSize(window, &width, &height); aspect = (float)width / (float)height; pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f); // 1.0472 radians = 60 degrees // 为了获得时间因子信息 timeFactor = ((float)currentTime); glUniform1f(tfLoc, (float)timeFactor); // 将透视矩阵和MV矩阵复制给相应的统一变量 glUniformMatrix4fv(mLoc, 1, GL_FALSE, glm::value_ptr(mMat)); // GLM函数调用value_ptr()返回对矩阵数据的引用 glUniformMatrix4fv(vLoc, 1, GL_FALSE, glm::value_ptr(vMat)); // 着色器需要视图矩阵的统一变量 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); glDrawArraysInstanced(GL_TRIANGLES, 0, 36, 100000); }
实例化让我们可以极大地扩展对象的副本数量;在这个例子中,即使对于很普通的GPU,实现100000个立方体的动画仍然是可行的。对代码的更改主要是一些常量的修改,是为了将大量立方体进一步分散开
如:
顶点着色器
float a = sin(203.0 * i/8000.0) * 403.0; float b = cos(301.0 * i/4001.0) * 401.0; float c = sin(400.0 * i/6003.0) * 405.0;
C++/OpenGL应用程序
cameraZ = 420.0f; // 将摄像机沿着Z轴再移远一些,以看到更多的立方体 ... glDrawArraysInstanced(GL_TRIANGLES, 0, 36, 100000);