渲染同一个场景,多个相同对象的情况,比如说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);