一、绘制实例
当需要大量绘制物体时,代码看起来如下:
// *************************************************// C++ 代码// *************************************************// 在屏幕上不同位置绘制1000个矩形,调用了1000次glDrawArrayfor(unsigned int i = 0; i < 1000; i++){bindVAO(); // 绑定VAObindTexture(); // 绑定纹理setUniforms(); // 设置uniformglDrawArray(GL_TRIANGLES, 0, 6);}// *************************************************// 顶点着色器代码// *************************************************#version 330 corelayout(location = 0) in vec3 aPos;uniform mat4 modelMatrix;void main(){gl_Position = modelMatrix * vec4(aPos, 1.0);}
如果像这样绘制模型的大量实例(Instance),你很快就会因为绘制调用过多而达到性能瓶颈。与绘制顶点本身相比,使用glDrawArrays或glDrawElements函数告诉GPU去绘制你的顶点数据会消耗更多的性能,因为OpenGL在绘制顶点数据之前需要做很多准备工作(比如告诉GPU该从哪个缓冲读取数据,从哪寻找顶点属性,而且这些都是在相对缓慢的CPU到GPU总线(CPU to GPU Bus)上进行的)。所以即便渲染顶点非常快,命令GPU去渲染却未必。
如果我们能够将数据一次性发送给GPU,然后使用一个绘制函数让OpenGL利用这些数据绘制多个物体,就会更方便了。这就是实例化(Instancing)。
使用方法如下,同样是绘制1000个矩形:
// *************************************// C++ 代码// *************************************bindVAO(); // 绑定VAObindTexture(); // 绑定纹理setUniform(); // 设置uniformglDrawArraysInstanced(GL_TRIANGLES, 0, 6,1000);// 除了最后一个参数,其他参数都和glDrawArray一样// 最后一个参数表示绘制的实例个数,在这里6个顶点组成的两个三角形构成的矩形是一个实例,// 在顶点着色器中由内建变量gl_InstanceID来表示当前迭代的顶点所属的实例ID// 一个矩形有6个顶点,在这6个顶点的顶点着色器迭代中,都是同一个实例gl_InstanceID// *************************************// 顶点着色器代码// *************************************#version 330 corelayout(location = 0) in vec3 aPos;uniform mat4 modelMatrix[1000];// 绘制数量可能会导致modelMatrix超出系统支持的大小上限// 改进方法:// 总结为:实例化数组:将数组数据定义为顶点属性,仅在顶点着色器渲染一个或多个新实例时才会更新。// 具体为:将uniform数据改为顶点属性数据,且设置每绘制一个实例才会往前推进一个单位,默认都是每// 个顶点迭代推荐一个单位,也即是每个顶点对应一个数据一个矩形6个顶点在一个实例中,使用相// 同的数据,只会绘制完这6个顶点才会步进到下一个单位。void main(){gl_Position = modelMatrix[gl_InstanceID] * vec4(aPos, 1.0);}
glDrawArraysInstanced
void glDrawArraysInstanced(GLenum mode,GLint first,GLsizei count,GLsizei instanceCount);// 绘制instanceCount个实例,每个实例由first起始的count个顶点按mode模式绘制而成。// mode、first、count参数同glDrawArrays函数参数的意义一样。// instanceCount: 绘制的实例数量// 和下面代码效果相同if( mode or count is invalid )generate appropriate errorelse{for( int i = 0; i < instanceCount; i++ ) {instanceID = i; // 对应顶点着色器中的gl_InstanceID的值glDrawArrays( mode, first, count );}instanceID = 0;}
- GL_INVALID_ENUM is generated if mode is not one of the accepted values.
- GL_INVALID_OPERATION is generated if a geometry shader is active and mode is incompatible with the input primitive type of the geometry shader in the currently installed program object.
- GL_INVALID_VALUE is generated if count or primcount are negative.
- GL_INVALID_OPERATION is generated if a non-zero buffer object name is bound to an enabled array and the buffer object’s data store is currently mapped.
glDrawElementsInstanced
```cpp
void glDrawElementsInstanced(GLenum mode, GLsizei count, GLenum type, const void *indices, GLsizei instanceCount);
// 上面glDrawArraysInstanced的elements版本。
// mode、count、type、indices参数同glDrawElements参数的意义一样。 // instanceCount: 绘制的实例数量
// 和下面代码效果相同 if( mode, count, or type is invalid ) generate appropriate error else { for( int i = 0; i < instanceCount; i++ ) { instanceID = i; // 对应顶点着色器中的gl_InstanceID的值 glDrawElements( mode, count, type, indices ); } instanceID = 0; }
- **GL_INVALID_ENUM **is generated if mode is not one of GL_POINTS, GL_LINE_STRIP, GL_LINE_LOOP, GL_LINES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN, or GL_TRIANGLES.- **GL_INVALID_VALUE **is generated if count or primcount are negative.- **GL_INVALID_OPERATION **is generated if a geometry shader is active and mode is incompatible with the input primitive type of the geometry shader in the currently installed program object.- **GL_INVALID_OPERATION **is generated if a non-zero buffer object name is bound to an enabled array and the buffer object's data store is currently mapped.<a name="X8Fnu"></a>## gl_InstanceID顶点着色器内建变量,表示当前迭代的顶点所属的实例ID。<a name="TGQpa"></a># 二、实例化数组按照上面代码中的注释改进出的代码如下:```cpp// *************************************// 顶点着色器代码// *************************************#version 330 corelayout(location = 0) in vec3 aPos;layout(location = 10) in mat4 aModel; // 矩阵数据作为顶点属性数据输入// 顶点属性允许的数据类型最大大小等于一个vec4,显然不能直接设置矩阵类型属性// 解决方法是把矩阵看成连续的4个vec4类型顶点属性来设置,所以上面的矩阵类型顶点属性可以看做以下类型:// layout(location = 10) in vec4 part1;// layout(location = 11) in vec4 part2;// layout(location = 12) in vec4 part3;// layout(location = 13) in vec4 part4;void main(){gl_Position = aModel * vec4(aPos, 1.0);}// *************************************// C++ 代码// *************************************GLuint VAO; // VAOGLuint VBO_model; // 保存模型矩阵数据的VBOGLuint VBO_pos; // 保存顶点位置数据的VBOconst GLuint QUAD_NUM = 1000; // 绘制的矩形数量glm::mat4 models[QUAD_NUM]; // 每个矩形对应一个模型变换矩阵,用于变换到不同位置// 将矩阵数据载入VBO,并设置为顶点属性数据,总结一句话就是为了达到下面这句话的效果// layout(location = 10) in mat4 aModel;loadInstanceVertexAttrib(VAO, VBO_model, models, QUAD_NUM, 10);// 函数实现如下void loadInstanceVertexAttrib(const GLuint &VAO, // VAOGLuint &VBO, // 保存矩阵数据的VBOglm::mat4 *models, // 要实例化的矩阵数组const GLuint &modelNum, // 矩阵数组长度const GLuint &startLocation) // 矩阵顶点属性位置值(第一个vec4的位置值){const GLsizei vec4Size = sizeof( glm::vec4 ); // vec4大小const GLsizei stride = 4 * vec4Size; // 步长,即为一个矩阵长度const GLint componentNum = 4; // 分量数量,每个vec4有4个GLfloat分量const GLuint location = 10;glGenBuffers(1, &VBO);glBindBuffers(GL_ARRAY_BUFFER, VBO);glBufferData(GL_ARRAY_BUFFER, modelNum * vec4Size, models, GL_STATIC_DRAW);glBindVertexArray(VAO); // VAO在前面设置aPos顶点属性数据时,已经创建glEnableVertexAttribArray(startLocation + 0);glEnableVertexAttribArray(startLocation + 1);glEnableVertexAttribArray(startLocation + 2);glEnableVertexAttribArray(startLocation + 3);glVertexAttribPointer(startLocation + 0, componentNum, GL_FLOAT, stride, (GLvoid*)(0 * vec4Size));glVertexAttribPointer(startLocation + 1, componentNum, GL_FLOAT, stride, (GLvoid*)(1 * vec4Size));glVertexAttribPointer(startLocation + 2, componentNum, GL_FLOAT, stride, (GLvoid*)(2 * vec4Size));glVertexAttribPointer(startLocation + 3, componentNum, GL_FLOAT, stride, (GLvoid*)(3 * vec4Size));// 每个实例对应一个矩阵数据,函数解释见下面。glVertexAttribDivisor(startLocation + 0, 1);glVertexAttribDivisor(startLocation + 1, 1);glVertexAttribDivisor(startLocation + 2, 1);glVertexAttribDivisor(startLocation + 3, 1);glBindVertexArray(0);}
glVertexAttribDivisor
void glVertexAttribDivisor(GLuint index,GLuint divisor);// 设置通用顶点属性在实例化渲染时的前进速率(更新速率)// index: 通用顶点属性索引, 取值范围[0, GL_MAX_VERTEX_ATTRIBS)// divisor: 决定前进速率(advance rate)。// divisor = 0时,表示每个顶点迭代更新一次数据,就是每个顶点对应一个数据,默认情况。// divisor >= 1时,表示每divisor个实例渲染迭代更新一次数据,就是每divisor个实例对应一个数据。
GL_INVALID_VALUE is generated if index is greater than or equal to the value of GL_MAX_VERTEX_ATTRIBS.
三、实践项目
绘制一个数以千计陨石组成的行星带。
https://github.com/JackieLong/OpenGL/tree/main/project_instancing_test
