Dynamically subdividing a plane using the geometry shader with instanced rendering
In order to avoid pushing the same data multiple times, we can exploit the instanced rendering functions. We will now see how we can omit the multiple glDrawElements
calls in the previous recipe with a single glDrawElementsInstanced
call.
Getting ready
Before doing this, we assume that the reader knows how to use the geometry shader in the OpenGL 3.3 core profile. The code for this recipe is in the Chapter1\SubdivisionGeometryShader_Instanced
directory.
How to do it…
Converting the previous recipe to use instanced rendering requires the following steps:
- Change the vertex shader to handle the instance modeling matrix and output world space positions (
shaders/shader.vert
).#version 330 core layout(location=0) in vec3 vVertex; uniform mat4 M[4]; void main() { gl_Position = M[gl_InstanceID]*vec4(vVertex, 1); }
- Change the geometry shader to replace the
MVP
matrix with thePV
matrix (shaders/shader.geom
).#version 330 core layout (triangles) in; layout (triangle_strip, max_vertices=256) out; uniform int sub_divisions; uniform mat4 PV; void main() { vec4 v0 = gl_in[0].gl_Position; vec4 v1 = gl_in[1].gl_Position; vec4 v2 = gl_in[2].gl_Position; float dx = abs(v0.x-v2.x)/sub_divisions; float dz = abs(v0.z-v1.z)/sub_divisions; float x=v0.x; float z=v0.z; for(int j=0;j<sub_divisions*sub_divisions;j++) { gl_Position = PV * vec4(x,0,z,1); EmitVertex(); gl_Position = PV * vec4(x,0,z+dz,1); EmitVertex(); gl_Position = PV * vec4(x+dx,0,z,1); EmitVertex(); gl_Position = PV * vec4(x+dx,0,z+dz,1); EmitVertex(); EndPrimitive(); x+=dx; if((j+1) %sub_divisions == 0) { x=v0.x; z+=dz; } } }
- Initialize the per-instance model matrices (
M
).void OnInit() { //set the instance modeling matrix M[0] = glm::translate(glm::mat4(1), glm::vec3(-5,0,-5)); M[1] = glm::translate(M[0], glm::vec3(10,0,0)); M[2] = glm::translate(M[1], glm::vec3(0,0,10)); M[3] = glm::translate(M[2], glm::vec3(-10,0,0)); .. shader.Use(); shader.AddAttribute("vVertex"); shader.AddUniform("PV"); shader.AddUniform("M"); shader.AddUniform("sub_divisions"); glUniform1i(shader("sub_divisions"), sub_divisions); glUniformMatrix4fv(shader("M"), 4, GL_FALSE, glm::value_ptr(M[0])); shader.UnUse();
- Render instances using the
glDrawElementInstanced
call.void OnRender() { glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glm::mat4 T =glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 0.0f, dist)); glm::mat4 Rx=glm::rotate(T,rX,glm::vec3(1.0f, 0.0f, 0.0f)); glm::mat4 V =glm::rotate(Rx,rY,glm::vec3(0.0f, 1.0f,0.0f)); glm::mat4 PV = P*V; shader.Use(); glUniformMatrix4fv(shader("PV"),1,GL_FALSE,glm::value_ptr(PV)); glUniform1i(shader("sub_divisions"), sub_divisions); glDrawElementsInstanced(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0, 4); shader.UnUse(); glutSwapBuffers(); }
How it works…
First, we need to store the model matrix for each instance separately. Since we have four instances, we store a uniform array of four elements (M[4]
). Second, we multiply the per-vertex position (vVertex
) with the model matrix for the current instance (M[gl_InstanceID]
).
Tip
Note that the gl_InstanceID
built-in attribute will be filled with the index of each instance automatically at the time of the glDrawElementsInstanced
call. Also note that this built-in attribute is only accessible in the vertex shader.
The MVP
matrix is omitted from the geometry shader since now the input vertex positions are in world space. So we only need to multiply them with the combined view projection (PV
) matrix. On the application side, the MV
matrix is removed. Instead, we store the model matrix array for all four instances (glm::mat4 M[4]
). The values of these matrices are initialized in the OnInit()
function as follows:
M[0] = glm::translate(glm::mat4(1), glm::vec3(-5,0,-5)); M[1] = glm::translate(M[0], glm::vec3(10,0,0)); M[2] = glm::translate(M[1], glm::vec3(0,0,10)); M[3] = glm::translate(M[2], glm::vec3(-10,0,0));
The rendering function, OnRender()
, creates the combined view projection matrix (PV
) and then calls glDrawElementsInsntanced
. The first four parameters are similar to the glDrawElements
function. The final parameter is the total number of instances desired. Instanced rendering is an efficient mechanism for rendering identical geometry whereby the GL_ARRAY_BUFFER
and GL_ELEMENT_ARRAY_BUFFER
bindings are shared between instances allowing the GPU to do efficient resource access and sharing.
void OnRender() { glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); glm::mat4 T = glm::translate(glm::mat4(1.0f),glm::vec3(0.0f, 0.0f, dist)); glm::mat4 Rx = glm::rotate(T, rX, glm::vec3(1.0f, 0.0f, 0.0f)); glm::mat4 V = glm::rotate(Rx, rY, glm::vec3(0.0f, 1.0f, 0.0f)); glm::mat4 PV = P*V; shader.Use(); glUniformMatrix4fv(shader("PV"),1,GL_FALSE,glm::value_ptr(PV)); glUniform1i(shader("sub_divisions"), sub_divisions); glDrawElementsInstanced(GL_TRIANGLES,6,GL_UNSIGNED_SHORT,0, 4); shader.UnUse(); glutSwapBuffers(); }
There is always a limit on the maximum number of matrices one can output from the vertex shader and this has some performance implications as well. Some performance improvements can be obtained by replacing the matrix storage with translation and scaling vectors, and an orientation quaternion which can then be converted on the fly into a matrix in the shader.
See also
The official OpenGL wiki can be found at http://www.opengl.org/wiki/Built-in_Variable_%28GLSL%29.
An instance rendering tutorial from OGLDev can be found at http://ogldev.atspace.co.uk/www/tutorial33/tutorial33.html.