Search icon CANCEL
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Conferences
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
OpenGL Development Cookbook

You're reading from   OpenGL Development Cookbook OpenGL brings an added dimension to your graphics by utilizing the remarkable power of modern GPUs. This straight-talking cookbook is perfect for intermediate C++ programmers who want to exploit the full potential of OpenGL.

Arrow left icon
Product type Paperback
Published in Jun 2013
Publisher Packt
ISBN-13 9781849695046
Length 326 pages
Edition 1st Edition
Tools
Arrow right icon
Author (1):
Arrow left icon
Muhammad Mobeen Movania Muhammad Mobeen Movania
Author Profile Icon Muhammad Mobeen Movania
Muhammad Mobeen Movania
Arrow right icon
View More author details
Toc

Table of Contents (10) Chapters Close

Preface 1. Introduction to Modern OpenGL FREE CHAPTER 2. 3D Viewing and Object Picking 3. Offscreen Rendering and Environment Mapping 4. Lights and Shadows 5. Mesh Model Formats and Particle Systems 6. GPU-based Alpha Blending and Global Illumination 7. GPU-based Volume Rendering Techniques 8. Skeletal and Physically-based Simulation on the GPU Index

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:

  1. 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);
    }
  2. 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;
        }
      }
    }
  3. 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);
    }
  4. 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();
  5. 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;
  6. 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);
  7. 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();
    }
  8. 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:

There's more…
You have been reading a chapter from
OpenGL Development Cookbook
Published in: Jun 2013
Publisher: Packt
ISBN-13: 9781849695046
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime