Dynamically subdividing a plane using the geometry shader
After the vertex shader, the next programmable stage in the OpenGL v3.3 graphics pipeline is the geometry shader. This shader contains inputs from the vertex shader stage. We can either feed these unmodified to the next shader stage or we can add/omit/modify vertices and primitives as desired. One thing that the vertex shaders lack is the availability of the other vertices of the primitive. Geometry shaders have information of all on the vertices of a single primitive.
The advantage with geometry shaders is that we can add/remove primitives on the fly. Moreover it is easier to get all vertices of a single primitive, unlike in the vertex shader, which has information on a single vertex only. The main drawback of geometry shaders is the limit on the number of new vertices we can generate, which is dependent on the hardware. Another disadvantage is the limited availability of the surrounding primitives.
In this recipe, we will dynamically subdivide a planar mesh using the geometry shader.
Getting ready
This recipe assumes that the reader knows how to render a simple triangle using vertex and fragment shaders using the OpenGL v3.3 core profile. We render four planar meshes in this recipe which are placed next to each other to create a bigger planar mesh. Each of these meshes is subdivided using the same geometry shader. The code for this recipe is located in the Chapter1\SubdivisionGeometryShader
directory.
How to do it…
We can implement the geometry shader using the following steps:
- Define a vertex shader (
shaders/shader.vert
) which outputs object space vertex positions directly.#version 330 core layout(location=0) in vec3 vVertex; void main() { gl_Position = vec4(vVertex, 1); }
- Define a geometry shader (
shaders/shader.geom
) which performs the subdivision of the quad. The shader is explained in the next section.#version 330 core layout (triangles) in; layout (triangle_strip, max_vertices=256) out; uniform int sub_divisions; uniform mat4 MVP; 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 = MVP * vec4(x,0,z,1); EmitVertex(); gl_Position = MVP * vec4(x,0,z+dz,1); EmitVertex(); gl_Position = MVP * vec4(x+dx,0,z,1); EmitVertex(); gl_Position = MVP * vec4(x+dx,0,z+dz,1); EmitVertex(); EndPrimitive(); x+=dx; if((j+1) %sub_divisions == 0) { x=v0.x; z+=dz; } } }
- Define a fragment shader (
shaders/shader.frag
) that simply outputs a constant color.#version 330 core layout(location=0) out vec4 vFragColor; void main() { vFragColor = vec4(1,1,1,1); }
- Load the shaders using the GLSLShader class in the
OnInit()
function.shader.LoadFromFile(GL_VERTEX_SHADER, "shaders/shader.vert"); shader.LoadFromFile(GL_GEOMETRY_SHADER,"shaders/shader.geom"); shader.LoadFromFile(GL_FRAGMENT_SHADER,"shaders/shader.frag"); shader.CreateAndLinkProgram(); shader.Use(); shader.AddAttribute("vVertex"); shader.AddUniform("MVP"); shader.AddUniform("sub_divisions"); glUniform1i(shader("sub_divisions"), sub_divisions); shader.UnUse();
- Create the geometry and topology.
vertices[0] = glm::vec3(-5,0,-5); vertices[1] = glm::vec3(-5,0,5); vertices[2] = glm::vec3(5,0,5); vertices[3] = glm::vec3(5,0,-5); GLushort* id=&indices[0]; *id++ = 0; *id++ = 1; *id++ = 2; *id++ = 0; *id++ = 2; *id++ = 3;
- Store the geometry and topology in the buffer object(s). Also enable the line display mode.
glGenVertexArrays(1, &vaoID); glGenBuffers(1, &vboVerticesID); glGenBuffers(1, &vboIndicesID); glBindVertexArray(vaoID); glBindBuffer (GL_ARRAY_BUFFER, vboVerticesID); glBufferData (GL_ARRAY_BUFFER, sizeof(vertices), &vertices[0], GL_STATIC_DRAW); glEnableVertexAttribArray(shader["vVertex"]); glVertexAttribPointer(shader["vVertex"], 3, GL_FLOAT, GL_FALSE,0,0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices[0], GL_STATIC_DRAW); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
- Set up the rendering code to bind the
GLSLShader
shader, pass the uniforms and then draw the geometry.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 MV=glm::rotate(Rx,rY, glm::vec3(0.0f,1.0f,0.0f)); MV=glm::translate(MV, glm::vec3(-5,0,-5)); shader.Use(); glUniform1i(shader("sub_divisions"), sub_divisions); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(P*MV)); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); MV=glm::translate(MV, glm::vec3(10,0,0)); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(P*MV)); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); MV=glm::translate(MV, glm::vec3(0,0,10)); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(P*MV)); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); MV=glm::translate(MV, glm::vec3(-10,0,0)); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(P*MV)); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); shader.UnUse(); glutSwapBuffers(); }
- Delete the shader and other OpenGL objects.
void OnShutdown() { shader.DeleteShaderProgram(); glDeleteBuffers(1, &vboVerticesID); glDeleteBuffers(1, &vboIndicesID); glDeleteVertexArrays(1, &vaoID); cout<<"Shutdown successfull"<<endl; }
How it works…
Let's dissect the geometry shader.
#version 330 core layout (triangles) in; layout (triangle_strip, max_vertices=256) out;
The first line signifies the GLSL version of the shader. The next two lines are important as they tell the shader processor about the input and output primitives of our geometry shader. In this case, the input will be triangles
and the output will be a triangle_strip
.
In addition, we also need to give the maximum number of output vertices from this geometry shader. This is a hardware specific number. For the hardware used in this development, the max_vertices
value is found to be 256
. This information can be obtained by querying the GL_MAX_GEOMETRY_OUTPUT_VERTICES
field and it is dependent on the primitive type used and the number of attributes stored per-vertex.
uniform int sub_divisions; uniform mat4 MVP;
Next, we declare two uniforms, the total number of subdivisions desired (sub_divisions
) and the combined modelview projection matrix (MVP
).
void main() { vec4 v0 = gl_in[0].gl_Position; vec4 v1 = gl_in[1].gl_Position; vec4 v2 = gl_in[2].gl_Position;
The bulk of the work takes place in the main entry point function. For each triangle pushed from the application, the geometry shader is run once. Thus, for each triangle, the positions of its vertices are obtained from the gl_Position
attribute which is stored in the built-in gl_in
array. All other attributes are input as an array in the geometry shader. We store the input positions in local variable v0
, v1
, and v2
.
Next, we calculate the size of the smallest quad for the given subdivision based on the size of the given base triangle and the total number of subdivisions required.
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 = MVP * vec4(x, 0, z,1); EmitVertex(); gl_Position = MVP * vec4(x, 0,z+dz,1); EmitVertex(); gl_Position = MVP * vec4(x+dx,0, z,1); EmitVertex(); gl_Position = MVP * vec4(x+dx,0,z+dz,1); EmitVertex(); EndPrimitive(); x+=dx; if((j+1) % sub_divisions == 0) { x=v0.x; z+=dz; } } }
We start from the first vertex. We store the x
and z
values of this vertex in local variables. Next, we iterate N*N
times, where N
is the total number of subdivisions required. For example, if we need to subdivide the mesh three times on both axes, the loop will run nine times, which is the total number of quads. After calculating the positions of the four vertices, they are emitted by calling EmitVertex()
. This function emits the current values of output variables to the current output primitive on the primitive stream. Next, the EndPrimitive()
call is issued to signify that we have emitted the four vertices of triangle_strip
.
After these calculations, the local variable x
is incremented by dx
amount. If we are at an iteration that is a multiple of sub_divisions
, we reset variable x
to the x
value of the first vertex while incrementing the local variable z
.
The fragment shader outputs a constant color (white: vec4(1,1,1,1)
).
There's more…
The application code is similar to the last recipes. We have an additional shader (shaders/shader.geom
), which is our geometry shader that is loaded from file.
shader.LoadFromFile(GL_VERTEX_SHADER, "shaders/shader.vert"); shader.LoadFromFile(GL_GEOMETRY_SHADER,"shaders/shader.geom"); shader.LoadFromFile(GL_FRAGMENT_SHADER,"shaders/shader.frag"); shader.CreateAndLinkProgram(); shader.Use(); shader.AddAttribute("vVertex"); shader.AddUniform("MVP"); shader.AddUniform("sub_divisions"); glUniform1i(shader("sub_divisions"), sub_divisions); shader.UnUse();
The notable additions are highlighted, which include the new geometry shader and an additional uniform for the total subdivisions desired (sub_divisions
). We initialize this uniform at initialization. The buffer object handling is similar to the simple triangle recipe. The other difference is in the rendering function where there are some additional modeling transformations (translations) after the viewing transformation.
The OnRender()
function starts by clearing the color and depth buffers. It then calculates the viewing transformation as in the previous recipe.
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 MV=glm::rotate(Rx,rY, glm::vec3(0.0f,1.0f,0.0f));
MV=glm::translate(MV, glm::vec3(-5,0,-5));
Since our planer mesh geometry is positioned at origin going from -5 to 5 on the X and Z axes, we have to place them in the appropriate place by translating them, otherwise they would overlay each other.
Next, we first bind the shader program. Then we pass the shader uniforms which include the sub_divisions
uniform and the combined modelview projection matrix (MVP
) uniform. Then we pass the attributes by issuing a call to the glDrawElements
function. We then add the relative translation for each instance to get a new modelview matrix for the next draw call. This is repeated three times to get all four planar meshes placed properly in the world space.
In this recipe, we handle keyboard input to allow the user to change the subdivision level dynamically. We first attach our keyboard event handler (OnKey
) to glutKeyboardFunc
. The keyboard event handler is defined as follows:
void OnKey(unsigned char key, int x, int y) { switch(key) { case ',': sub_divisions--; break; case '.': sub_divisions++; break; } sub_divisions = max(1,min(8, sub_divisions)); glutPostRedisplay(); }
We can change the subdivision levels by pressing the , and . keys. We then check to make sure that the subdivisions are within the allowed limit. Finally, we request the freeglut function, glutPostRedisplay()
, to repaint the window to show the new mesh. Compiling and running the demo code displays four planar meshes. Pressing the , key decreases the subdivision level and the . key increases the subdivision level. The output from the subdivision geometry shader showing multiple subdivision levels is displayed in the following screenshot:
See also
You can view the Geometry shader tutorial part 1 and 2 at Geeks3D:
http://www.geeks3d.com/20111111/simple-introduction-to-geometry-shaders-glsl-opengl-tutorial-part1/
http://www.geeks3d.com/20111117/simple-introduction-to-geometry-shader-in-glsl-part-2/