In this chapter, we will cover:
- Setting up the OpenGL v3.3 core profile on Visual Studio 2010 using the GLEW and freeglut libraries
- Designing a GLSL shader class
- Rendering a simple colored triangle using shaders
- Doing a ripple mesh deformer using the vertex shader
- Dynamically subdividing a plane using the geometry shader
- Dynamically subdividing a plane using the geometry shader with instanced rendering
- Drawing a 2D image in a window using the fragment shader and SOIL image loading library
The OpenGL API has seen various changes since its creation in 1992. With every new version, new features were added and additional functionality was exposed on supporting hardware through extensions. Until OpenGL v2.0 (which was introduced in 2004), the functionality in the graphics pipeline was fixed, that is, there were fixed set of operations hardwired in the graphics hardware and it was impossible to modify the graphics pipeline. With OpenGL v2.0, the shader objects were introduced for the first time. That enabled programmers to modify the graphics pipeline through special programs called shaders, which were written in a special language called OpenGL shading language (GLSL).
After OpenGL v2.0, the next major version was v3.0. This version introduced two profiles for working with OpenGL; the core profile and the compatibility profile. The core profile basically contains all of the non-deprecated functionality whereas the compatibility profile retains deprecated functionality for backwards compatibility. As of 2012, the latest version of OpenGL available is OpenGL v4.3. Beyond OpenGL v3.0, the changes introduced in the application code are not as drastic as compared to those required for moving from OpenGL v2.0 to OpenGL v3.0 and above.
We will start with a very basic example in which we will set up the modern OpenGL v3.3 core profile. This example will simply create a blank window and clear the window with red color.
OpenGL or any other graphics API for that matter requires a window to display graphics in. This is carried out through platform specific codes. Previously, the GLUT library was invented to provide windowing functionality in a platform independent manner. However, this library was not maintained with each new OpenGL release. Fortunately, another independent project, freeglut, followed in the GLUT footsteps by providing similar (and in some cases better) windowing support in a platform independent way. In addition, it also helps with the creation of the OpenGL core/compatibility profile contexts. The latest version of freeglut may be downloaded from http://freeglut.sourceforge.net. The version used in the source code accompanying this book is v2.8.0. After downloading the freeglut library, you will have to compile it to generate the libs/dlls.
The extension mechanism provided by OpenGL still exists. To aid with getting the appropriate function pointers, the GLEW library is used. The latest version can be downloaded from http://glew.sourceforge.net. The version of GLEW used in the source code accompanying this book is v1.9.0. If the source release is downloaded, you will have to build GLEW first to generate the libs and dlls on your platform. You may also download the pre-built binaries.
Prior to OpenGL v3.0, the OpenGL API provided support for matrices by providing specific matrix stacks such as the modelview, projection, and texture matrix stacks. In addition, transformation functions such as translate, rotate, and scale, as well as projection functions were also provided. Moreover, immediate mode rendering was supported, allowing application programmers to directly push the vertex information to the hardware.
In OpenGL v3.0 and above, all of these functionalities are removed from the core profile, whereas for backward compatibility they are retained in the compatibility profile. If we use the core profile (which is the recommended approach), it is our responsibility to implement all of these functionalities including all matrix handling and transformations. Fortunately, a library called glm
exists that provides math related classes such as vectors and matrices. It also provides additional convenience functions and classes. For all of the demos in this book, we will use the glm
library. Since this is a headers only library, there are no linker libraries for glm
. The latest version of glm
can be downloaded from http://glm.g-truc.net. The version used for the source code in this book is v0.9.4.0.
There are several image formats available. It is not a trivial task to write an image loader for such a large number of image formats. Fortunately, there are several image loading libraries that make image loading a trivial task. In addition, they provide support for both loading as well as saving of images into various formats. One such library is the
SOIL
image loading library. The latest version of SOIL
can be downloaded from http://www.lonesock.net/soil.html.
Once we have downloaded the SOIL
library, we extract the file to a location on the hard disk. Next, we set up the include and library paths in the Visual Studio environment. The include path on my development machine is D:\Libraries\soil\Simple OpenGL Image Library\src
whereas, the library path is set to D:\Libraries\soil\Simple OpenGL Image Library\lib\VC10_Debug
. Of course, the path for your system will be different than mine but these are the folders that the directories should point to.
These steps will help us to set up our development environment. For all of the recipes in this book, Visual Studio 2010 Professional version is used. Readers may also use the free express edition or any other version of Visual Studio (for example, Ultimate/Enterprise). Since there are a myriad of development environments, to make it easier for users on other platforms, we have provided premake script files as well.
The code for this recipe is in the Chapter1/GettingStarted
directory.
Tip
You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.
Let us setup the development environment using the following steps:
- After downloading the required libraries, we set up the Visual Studio 2010 environment settings.
- We first create a new Win32 Console Application project as shown in the preceding screenshot. We set up an empty Win32 project as shown in the following screenshot:
- Next, we set up the include and library paths for the project by going into the Project menu and selecting project Properties. This opens a new dialog box. In the left pane, click on the Configuration Properties option and then on VC++ Directories.
- In the right pane, in the Include Directories field, add the GLEW and freeglut subfolder paths.
- Similarly, in the Library Directories, add the path to the lib subfolder of GLEW and freeglut libraries as shown in the following screenshot:
- Next, we add a new
.cpp
file to the project and name itmain.cpp
. This is the main source file of our project. You may also browse throughChapter1/ GettingStarted/GettingStarted/main.cpp
which does all this setup already. - Let us skim through the
Chapter1/ GettingStarted/GettingStarted/main.cpp
file piece by piece.#include <GL/glew.h> #include <GL/freeglut.h> #include <iostream>
These lines are the include files that we will add to all of our projects. The first is the GLEW header, the second is the freeglut header, and the final include is the standard input/output header.
- In Visual Studio, we can add the required linker libraries in two ways. The first way is through the Visual Studio environment (by going to the Properties menu item in the Project menu). This opens the project's property pages. In the configuration properties tree, we collapse the Linker subtree and click on the Input item. The first field in the right pane is
Additional Dependencies
. We can add the linker library in this field as shown in the following screenshot: - The second way is to add the
glew32.lib
file to the linker settings programmatically. This can be achieved by adding the followingpragma
:#pragma comment(lib, "glew32.lib")
- The next line is the using directive to enable access to the functions in the std namespace. This is not mandatory but we include this here so that we do not have to prefix
std::
to any standard library function from the iostream header file.using namespace std;
- The next lines define the width and height constants which will be the screen resolution for the window. After these declarations, there are five function definitions . The
OnInit()
function is used for initializing any OpenGL state or object,OnShutdown()
is used to delete an OpenGL object,OnResize()
is used to handle the resize event,OnRender()
helps to handle the paint event, andmain()
is the entry point of the application. We start with the definition of themain()
function.const int WIDTH = 1280; const int HEIGHT = 960; int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA); glutInitContextVersion (3, 3); glutInitContextFlags (GLUT_CORE_PROFILE | GLUT_DEBUG); glutInitContextProfile(GLUT_FORWARD_COMPATIBLE); glutInitWindowSize(WIDTH, HEIGHT);
- The first line
glutInit
initializes the GLUT environment. We pass the command line arguments to this function from our entry point. Next, we set up the display mode for our application. In this case, we request the GLUT framework to provide support for a depth buffer, double buffering (that is a front and a back buffer for smooth, flicker-free rendering), and the format of the frame buffer to be RGBA (that is with red, green, blue, and alpha channels). Next, we set the required OpenGL context version we desire by using theglutInitContextVersion
. The first parameter is the major version of OpenGL and the second parameter is the minor version of OpenGL. For example, if we want to create an OpenGL v4.3 context, we will callglutInitContextVersion (4, 3)
. Next, the context flags are specified:glutInitContextFlags (GLUT_CORE_PROFILE | GLUT_DEBUG); glutInitContextProfile(GLUT_FORWARD_COMPATIBLE);
- For any version of OpenGL including OpenGL v3.3 and above, there are two profiles available: the core profile (which is a pure shader based profile without support for OpenGL fixed functionality) and the compatibility profile (which supports the OpenGL fixed functionality). All of the matrix stack functionality
glMatrixMode(*)
,glTranslate*
,glRotate*
,glScale*
, and so on, and immediate mode calls such asglVertex*
,glTexCoord*
, andglNormal*
of legacy OpenGL, are retained in the compatibility profile. However, they are removed from the core profile. In our case, we will request a forward compatible core profile which means that we will not have any fixed function OpenGL functionality available. - Next, we set the screen size and create the window:
glutInitWindowSize(WIDTH, HEIGHT); glutCreateWindow("Getting started with OpenGL 3.3");
- Next, we initialize the GLEW library. It is important to initialize the GLEW library after the OpenGL context has been created. If the function returns
GLEW_OK
the function succeeds, otherwise the GLEW initialization fails.glewExperimental = GL_TRUE; GLenum err = glewInit(); if (GLEW_OK != err){ cerr<<"Error: "<<glewGetErrorString(err)<<endl; } else { if (GLEW_VERSION_3_3) { cout<<"Driver supports OpenGL 3.3\nDetails:"<<endl; } } cout<<"\tUsing glew "<<glewGetString(GLEW_VERSION)<<endl; cout<<"\tVendor: "<<glGetString (GL_VENDOR)<<endl; cout<<"\tRenderer: "<<glGetString (GL_RENDERER)<<endl; cout<<"\tVersion: "<<glGetString (GL_VERSION)<<endl; cout<<"\tGLSL: "<<glGetString(GL_SHADING_LANGUAGE_VERSION)<<endl;
The
glewExperimental
global switch allows the GLEW library to report an extension if it is supported by the hardware but is unsupported by the experimental or pre-release drivers. After the function is initialized, the GLEW diagnostic information such as the GLEW version, the graphics vendor, the OpenGL renderer, and the shader language version are printed to the standard output. - Finally, we call our initialization function
OnInit()
and then attach our uninitialization functionOnShutdown()
as theglutCloseFunc
method—the close callback function which will be called when the window is about to close. Next, we attach our display and reshape function to their corresponding callbacks. The main function is terminated with a call to theglutMainLoop()
function which starts the application's main loop.OnInit(); glutCloseFunc(OnShutdown); glutDisplayFunc(OnRender); glutReshapeFunc(OnResize); glutMainLoop(); return 0; }
The remaining functions are defined as follows:
For this simple example, we set the clear color to red (R:1, G:0, B:0, A:0). The first three are the red, green, and blue channels and the last is the alpha channel which is used in alpha blending. The only other function defined in this simple example is the OnRender()
function, which is our display callback function that is called on the paint event. This function first clears the color and depth buffers to the clear color and clear depth values respectively.
Tip
Similar to the color buffer, there is another buffer called the depth buffer. Its clear value can be set using the glClearDepth
function. It is used for hardware based hidden surface removal. It simply stores the depth of the nearest fragment encountered so far. The incoming fragment's depth value overwrites the depth buffer value based on the depth clear function specified for the depth test using the glDepthFunc
function. By default the depth value gets overwritten if the current fragment's depth is lower than the existing depth in the depth buffer.
The glutSwapBuffers
function is then called to set the current back buffer as the current front buffer that is shown on screen. This call is required in a double buffered OpenGL application. Running the code gives us the output shown in the following screenshot.
setup the development environment using the following steps:
- After downloading the required libraries, we set up the Visual Studio 2010 environment settings.
- We first create a new Win32 Console Application project as shown in the preceding screenshot. We set up an empty Win32 project as shown in the following screenshot:
- Next, we set up the include and library paths for the project by going into the Project menu and selecting project Properties. This opens a new dialog box. In the left pane, click on the Configuration Properties option and then on VC++ Directories.
- In the right pane, in the Include Directories field, add the GLEW and freeglut subfolder paths.
- Similarly, in the Library Directories, add the path to the lib subfolder of GLEW and freeglut libraries as shown in the following screenshot:
- Next, we add a new
.cpp
file to the project and name itmain.cpp
. This is the main source file of our project. You may also browse throughChapter1/ GettingStarted/GettingStarted/main.cpp
which does all this setup already. - Let us skim through the
Chapter1/ GettingStarted/GettingStarted/main.cpp
file piece by piece.#include <GL/glew.h> #include <GL/freeglut.h> #include <iostream>
These lines are the include files that we will add to all of our projects. The first is the GLEW header, the second is the freeglut header, and the final include is the standard input/output header.
- In Visual Studio, we can add the required linker libraries in two ways. The first way is through the Visual Studio environment (by going to the Properties menu item in the Project menu). This opens the project's property pages. In the configuration properties tree, we collapse the Linker subtree and click on the Input item. The first field in the right pane is
Additional Dependencies
. We can add the linker library in this field as shown in the following screenshot: - The second way is to add the
glew32.lib
file to the linker settings programmatically. This can be achieved by adding the followingpragma
:#pragma comment(lib, "glew32.lib")
- The next line is the using directive to enable access to the functions in the std namespace. This is not mandatory but we include this here so that we do not have to prefix
std::
to any standard library function from the iostream header file.using namespace std;
- The next lines define the width and height constants which will be the screen resolution for the window. After these declarations, there are five function definitions . The
OnInit()
function is used for initializing any OpenGL state or object,OnShutdown()
is used to delete an OpenGL object,OnResize()
is used to handle the resize event,OnRender()
helps to handle the paint event, andmain()
is the entry point of the application. We start with the definition of themain()
function.const int WIDTH = 1280; const int HEIGHT = 960; int main(int argc, char** argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DEPTH | GLUT_DOUBLE | GLUT_RGBA); glutInitContextVersion (3, 3); glutInitContextFlags (GLUT_CORE_PROFILE | GLUT_DEBUG); glutInitContextProfile(GLUT_FORWARD_COMPATIBLE); glutInitWindowSize(WIDTH, HEIGHT);
- The first line
glutInit
initializes the GLUT environment. We pass the command line arguments to this function from our entry point. Next, we set up the display mode for our application. In this case, we request the GLUT framework to provide support for a depth buffer, double buffering (that is a front and a back buffer for smooth, flicker-free rendering), and the format of the frame buffer to be RGBA (that is with red, green, blue, and alpha channels). Next, we set the required OpenGL context version we desire by using theglutInitContextVersion
. The first parameter is the major version of OpenGL and the second parameter is the minor version of OpenGL. For example, if we want to create an OpenGL v4.3 context, we will callglutInitContextVersion (4, 3)
. Next, the context flags are specified:glutInitContextFlags (GLUT_CORE_PROFILE | GLUT_DEBUG); glutInitContextProfile(GLUT_FORWARD_COMPATIBLE);
- For any version of OpenGL including OpenGL v3.3 and above, there are two profiles available: the core profile (which is a pure shader based profile without support for OpenGL fixed functionality) and the compatibility profile (which supports the OpenGL fixed functionality). All of the matrix stack functionality
glMatrixMode(*)
,glTranslate*
,glRotate*
,glScale*
, and so on, and immediate mode calls such asglVertex*
,glTexCoord*
, andglNormal*
of legacy OpenGL, are retained in the compatibility profile. However, they are removed from the core profile. In our case, we will request a forward compatible core profile which means that we will not have any fixed function OpenGL functionality available. - Next, we set the screen size and create the window:
glutInitWindowSize(WIDTH, HEIGHT); glutCreateWindow("Getting started with OpenGL 3.3");
- Next, we initialize the GLEW library. It is important to initialize the GLEW library after the OpenGL context has been created. If the function returns
GLEW_OK
the function succeeds, otherwise the GLEW initialization fails.glewExperimental = GL_TRUE; GLenum err = glewInit(); if (GLEW_OK != err){ cerr<<"Error: "<<glewGetErrorString(err)<<endl; } else { if (GLEW_VERSION_3_3) { cout<<"Driver supports OpenGL 3.3\nDetails:"<<endl; } } cout<<"\tUsing glew "<<glewGetString(GLEW_VERSION)<<endl; cout<<"\tVendor: "<<glGetString (GL_VENDOR)<<endl; cout<<"\tRenderer: "<<glGetString (GL_RENDERER)<<endl; cout<<"\tVersion: "<<glGetString (GL_VERSION)<<endl; cout<<"\tGLSL: "<<glGetString(GL_SHADING_LANGUAGE_VERSION)<<endl;
The
glewExperimental
global switch allows the GLEW library to report an extension if it is supported by the hardware but is unsupported by the experimental or pre-release drivers. After the function is initialized, the GLEW diagnostic information such as the GLEW version, the graphics vendor, the OpenGL renderer, and the shader language version are printed to the standard output. - Finally, we call our initialization function
OnInit()
and then attach our uninitialization functionOnShutdown()
as theglutCloseFunc
method—the close callback function which will be called when the window is about to close. Next, we attach our display and reshape function to their corresponding callbacks. The main function is terminated with a call to theglutMainLoop()
function which starts the application's main loop.OnInit(); glutCloseFunc(OnShutdown); glutDisplayFunc(OnRender); glutReshapeFunc(OnResize); glutMainLoop(); return 0; }
The remaining functions are defined as follows:
For this simple example, we set the clear color to red (R:1, G:0, B:0, A:0). The first three are the red, green, and blue channels and the last is the alpha channel which is used in alpha blending. The only other function defined in this simple example is the OnRender()
function, which is our display callback function that is called on the paint event. This function first clears the color and depth buffers to the clear color and clear depth values respectively.
Tip
Similar to the color buffer, there is another buffer called the depth buffer. Its clear value can be set using the glClearDepth
function. It is used for hardware based hidden surface removal. It simply stores the depth of the nearest fragment encountered so far. The incoming fragment's depth value overwrites the depth buffer value based on the depth clear function specified for the depth test using the glDepthFunc
function. By default the depth value gets overwritten if the current fragment's depth is lower than the existing depth in the depth buffer.
The glutSwapBuffers
function is then called to set the current back buffer as the current front buffer that is shown on screen. This call is required in a double buffered OpenGL application. Running the code gives us the output shown in the following screenshot.
OnRender()
function,
Tip
Similar to the color buffer, there is another buffer called the depth buffer. Its clear value can be set using the glClearDepth
function. It is used for hardware based hidden surface removal. It simply stores the depth of the nearest fragment encountered so far. The incoming fragment's depth value overwrites the depth buffer value based on the depth clear function specified for the depth test using the glDepthFunc
function. By default the depth value gets overwritten if the current fragment's depth is lower than the existing depth in the depth buffer.
The glutSwapBuffers
function is then called to set the current back buffer as the current front buffer that is shown on screen. This call is required in a double buffered OpenGL application. Running the code gives us the output shown in the following screenshot.
We will now have a look at how to set up shaders. Shaders are special programs that are run on the GPU. There are different shaders for controlling different stages of the programmable graphics pipeline. In the modern GPU, these include the vertex shader (which is responsible for calculating the clip-space position of a vertex), the tessellation control shader (which is responsible for determining the amount of tessellation of a given patch), the tessellation evaluation shader (which computes the interpolated positions and other attributes on the tessellation result), the geometry shader (which processes primitives and can add additional primitives and vertices if needed), and the fragment shader (which converts a rasterized fragment into a colored pixel and a depth). The modern GPU pipeline highlighting the different shader stages is shown in the following figure.
The GLSLShader
class is defined in the GLSLShader.[h/cpp]
files. We first declare the constructor and destructor which initialize the member variables. The next three functions, LoadFromString
, LoadFromFile
, and CreateAndLinkProgram
handle the shader compilation, linking, and program creation. The next two functions, Use
and UnUse
functions bind and unbind the program. Two std::map
datastructures are used. They store the attribute's/uniform's name as the key and its location as the value. This is done to remove the redundant call to get the attribute's/uniform's location each frame or when the location is required to access the attribute/uniform. The next two functions, AddAttribute
and AddUniform
add the locations of the attribute and uniforms into their respective std::map
(_attributeList
and _uniformLocationList
).
In a typical shader application, the usage of the GLSLShader
object is as follows:
- Create the
GLSLShader
object either on stack (for example,GLSLShader
shader;) or on the heap (for example,GLSLShader* shader=new GLSLShader();
) - Call
LoadFromFile
on theGLSLShader
object reference - Call
CreateAndLinkProgram
on theGLSLShader
object reference - Call
Use
on theGLSLShader
object reference to bind the shader object - Call
AddAttribute
/AddUniform
to store locations of all of the shader's attributes and uniforms respectively - Call
UnUse
on theGLSLShader
object reference to unbind the shader object
Execution of the above four functions creates a shader object. After the shader object is created, a shader program object is created using the following set of functions in the following sequence:
In the GLSLShader
class, the first four steps are handled in the LoadFromString
function and the later four steps are handled by the CreateAndLinkProgram
member function. After the shader program object has been created, we can set the program for execution on the GPU. This process is called shader binding. This is carried out by the glUseProgram
function which is called through the Use
/UnUse
functions in the GLSLShader
class.
For accessing any attribute/uniform location, we provide an indexer in the GLSLShader
class. In cases where there is an error in the compilation or linking stage, the shader log is printed to the console. Say for example, our GLSLshader
object is called shader
and our shader
contains a uniform called MVP
. We can first add it to the map of GLSLShader
by calling shader.AddUniform("MVP")
. This function adds the uniform's location to the map. Then when we want to access the uniform, we directly call shader("MVP")
and it returns the location of our uniform.
GLSLShader
class is
In a typical shader application, the usage of the GLSLShader
object is as follows:
- Create the
GLSLShader
object either on stack (for example,GLSLShader
shader;) or on the heap (for example,GLSLShader* shader=new GLSLShader();
) - Call
LoadFromFile
on theGLSLShader
object reference - Call
CreateAndLinkProgram
on theGLSLShader
object reference - Call
Use
on theGLSLShader
object reference to bind the shader object - Call
AddAttribute
/AddUniform
to store locations of all of the shader's attributes and uniforms respectively - Call
UnUse
on theGLSLShader
object reference to unbind the shader object
Execution of the above four functions creates a shader object. After the shader object is created, a shader program object is created using the following set of functions in the following sequence:
In the GLSLShader
class, the first four steps are handled in the LoadFromString
function and the later four steps are handled by the CreateAndLinkProgram
member function. After the shader program object has been created, we can set the program for execution on the GPU. This process is called shader binding. This is carried out by the glUseProgram
function which is called through the Use
/UnUse
functions in the GLSLShader
class.
For accessing any attribute/uniform location, we provide an indexer in the GLSLShader
class. In cases where there is an error in the compilation or linking stage, the shader log is printed to the console. Say for example, our GLSLshader
object is called shader
and our shader
contains a uniform called MVP
. We can first add it to the map of GLSLShader
by calling shader.AddUniform("MVP")
. This function adds the uniform's location to the map. Then when we want to access the uniform, we directly call shader("MVP")
and it returns the location of our uniform.
usage of the GLSLShader
object is as follows:
- Create the
GLSLShader
object either on stack (for example,GLSLShader
shader;) or on the heap (for example,GLSLShader* shader=new GLSLShader();
) - Call
LoadFromFile
on theGLSLShader
object reference - Call
CreateAndLinkProgram
on theGLSLShader
object reference - Call
Use
on theGLSLShader
object reference to bind the shader object - Call
AddAttribute
/AddUniform
to store locations of all of the shader's attributes and uniforms respectively - Call
UnUse
on theGLSLShader
object reference to unbind the shader object
Execution of the above four functions creates a shader object. After the shader object is created, a shader program object is created using the following set of functions in the following sequence:
In the GLSLShader
class, the first four steps are handled in the LoadFromString
function and the later four steps are handled by the CreateAndLinkProgram
member function. After the shader program object has been created, we can set the program for execution on the GPU. This process is called shader binding. This is carried out by the glUseProgram
function which is called through the Use
/UnUse
functions in the GLSLShader
class.
For accessing any attribute/uniform location, we provide an indexer in the GLSLShader
class. In cases where there is an error in the compilation or linking stage, the shader log is printed to the console. Say for example, our GLSLshader
object is called shader
and our shader
contains a uniform called MVP
. We can first add it to the map of GLSLShader
by calling shader.AddUniform("MVP")
. This function adds the uniform's location to the map. Then when we want to access the uniform, we directly call shader("MVP")
and it returns the location of our uniform.
In the GLSLShader
class, the first four steps are handled in the LoadFromString
function and the later four steps are handled by the CreateAndLinkProgram
member function. After the shader program object has been created, we can set the program for execution on the GPU. This process is called shader binding. This is carried out by the glUseProgram
function which is called through the Use
/UnUse
functions in the GLSLShader
class.
For accessing any attribute/uniform location, we provide an indexer in the GLSLShader
class. In cases where there is an error in the compilation or linking stage, the shader log is printed to the console. Say for example, our GLSLshader
object is called shader
and our shader
contains a uniform called MVP
. We can first add it to the map of GLSLShader
by calling shader.AddUniform("MVP")
. This function adds the uniform's location to the map. Then when we want to access the uniform, we directly call shader("MVP")
and it returns the location of our uniform.
GLSLShader
class, the first four steps are handled in the LoadFromString
function and the later four steps are handled by the CreateAndLinkProgram
member function. After the shader program object has been created, we can set the program for execution on the GPU. This process is called shader binding
For accessing any attribute/uniform location, we provide an indexer in the GLSLShader
class. In cases where there is an error in the compilation or linking stage, the shader log is printed to the console. Say for example, our GLSLshader
object is called shader
and our shader
contains a uniform called MVP
. We can first add it to the map of GLSLShader
by calling shader.AddUniform("MVP")
. This function adds the uniform's location to the map. Then when we want to access the uniform, we directly call shader("MVP")
and it returns the location of our uniform.
We will now put the GLSLShader
class to use by implementing an application to render a simple colored triangle on screen.
Let us start this recipe using the following steps:
- Define a vertex shader (
shaders/shader.vert
) to transform the object space vertex position to clip space.#version 330 core layout(location = 0) in vec3 vVertex; layout(location = 1) in vec3 vColor; smooth out vec4 vSmoothColor; uniform mat4 MVP; void main() { vSmoothColor = vec4(vColor,1); gl_Position = MVP*vec4(vVertex,1); }
- Define a fragment shader (
shaders/shader.frag
) to output a smoothly interpolated color from the vertex shader to the frame buffer.#version 330 core smooth in vec4 vSmoothColor; layout(location=0) out vec4 vFragColor; void main() { vFragColor = vSmoothColor; }
- Load the two shaders using the
GLSLShader
class in theOnInit()
function.shader.LoadFromFile(GL_VERTEX_SHADER, "shaders/shader.vert"); shader.LoadFromFile(GL_FRAGMENT_SHADER,"shaders/shader.frag"); shader.CreateAndLinkProgram(); shader.Use(); shader.AddAttribute("vVertex"); shader.AddAttribute("vColor"); shader.AddUniform("MVP"); shader.UnUse();
- Create the geometry and topology. We will store the attributes together in an interleaved vertex format, that is, we will store the vertex attributes in a struct containing two attributes, position and color.
vertices[0].color=glm::vec3(1,0,0); vertices[1].color=glm::vec3(0,1,0); vertices[2].color=glm::vec3(0,0,1); vertices[0].position=glm::vec3(-1,-1,0); vertices[1].position=glm::vec3(0,1,0); vertices[2].position=glm::vec3(1,-1,0); indices[0] = 0; indices[1] = 1; indices[2] = 2;
- Store the geometry and topology in the buffer object(s). The stride parameter controls the number of bytes to jump to reach the next element of the same attribute. For the interleaved format, it is typically the size of our vertex struct in bytes, that is,
sizeof(Vertex)
.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,stride,0); glEnableVertexAttribArray(shader["vColor"]); glVertexAttribPointer(shader["vColor"], 3, GL_FLOAT, GL_FALSE,stride, (const GLvoid*)offsetof(Vertex, color)); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices[0], GL_STATIC_DRAW);
- Set up the resize handler to set up the viewport and projection matrix.
void OnResize(int w, int h) { glViewport (0, 0, (GLsizei) w, (GLsizei) h); P = glm::ortho(-1,1,-1,1); }
- 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); shader.Use(); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(P*MV)); glDrawElements(GL_TRIANGLES, 3, 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); }
For this simple example, we will only use a vertex shader (shaders/shader.vert
) and a fragment shader (shaders/shader.frag
). The first line in the shader signifies the GLSL version of the shader. Starting from OpenGL v3.0, the version specifiers correspond to the OpenGL version used. So for OpenGL v3.3, the GLSL version is 330. In addition, since we are interested in the core profile, we add another keyword following the version number to signify that we have a core profile shader.
The vertex shader simply outputs the input per-vertex color to the output (vSmoothColor
). Such attributes that are interpolated across shader stages are called varying attributes. It also calculates the clip space position by multiplying the per-vertex position (vVertex
) with the combined modelview projection (MVP) matrix.
The fragment shader writes the input color (vSmoothColor
) to the frame buffer output (vFragColor
).
In the simple triangle demo application code, we store the GLSLShader
object reference in the global scope so that we can access it in any function we desire. We modify the OnInit()
function by adding the following lines:
The first two lines create the GLSL shader of the given type by reading the contents of the file with the given filename. In all of the recipes in this book, the vertex shader files are stored with a .vert
extension, the geometry shader files with a .geom
extension, and the fragment shader files with a .frag
extension. Next, the GLSLShader::CreateAndLinkProgram
function is called to create the shader program from the shader object. Next, the program is bound and then the locations of attributes and uniforms are stored.
In OpenGL v3.3 and above, we typically store the geometry information in buffer objects, which is a linear array of memory managed by the GPU. In order to facilitate the handling of buffer object(s) during rendering, we use a vertex array object (VAO). This object stores references to buffer objects that are bound after the VAO is bound. The advantage we get from using a VAO is that after the VAO is bound, we do not have to bind the buffer object(s).
In this demo, we declare three variables in global scope; vaoID
for VAO handling, and vboVerticesID
and vboIndicesID
for buffer object handling. The VAO object is created by calling the glGenVertexArrays
function. The buffer objects are generated using the glGenBuffers
function. The first parameter for both of these functions is the total number of objects required, and the second parameter is the reference to where the object handle is stored. These functions are called in the OnInit()
function.
In the next few calls, we enable the vertex attributes. This function needs the location of the attribute, which we obtain by the GLSLShader::operator[]
, passing it the name of the attribute whose location we require. We then call glVertexAttributePointer
to tell the GPU how many elements there are and what is their type, whether the attribute is normalized, the stride (which means the total number of bytes to skip to reach the next element; for our case since the attributes are stored in a Vertex
struct, the next element's stride is the size of our Vertex
struct), and finally, the pointer to the attribute in the given array. The last parameter requires explanation in case we have interleaved attributes (as we have). The offsetof
operator returns the offset in bytes, to the attribute in the given struct. Hence, the GPU knows how many bytes it needs to skip in order to access the next attribute of the given type. For the vVertex
attribute, the last parameter is 0
since the next element is accessed immediately after the stride. For the second attribute vColor
, it needs to hop 12 bytes before the next vColor
attribute is obtained from the given vertices array.
To complement the object generation in the OnInit()
function, we must provide the object deletion code. This is handled in the OnShutdown()
function. We first delete the shader program by calling the GLSLShader::DeleteShaderProgram
function. Next, we delete the two buffer objects (vboVerticesID
and vboIndicesID
) and finally we delete the vertex array object (vaoID
).
The rendering code of the simple triangle demo is as follows:
The rendering code first clears the color and depth buffer and binds the shader program by calling the GLSLShader::Use()
function. It then passes the combined modelview and projection matrix to the GPU by invoking the glUniformMatrix4fv
function. The first parameter is the location of the uniform which we obtain from the GLSLShader::operator()
function, by passing it the name of the uniform whose location we need. The second parameter is the total number of matrices we wish to pass. The third parameter is a Boolean signifying if the matrix needs to be transposed, and the final parameter is the float pointer to the matrix object. Here we use the glm::value_ptr
function to get the float pointer from the matrix object. Note that the OpenGL matrices are concatenated right to left since it follows a right handed coordinate system in a column major layout. Hence we keep the projection matrix on the left and the modelview matrix on the right. For this simple example, the modelview matrix (MV
) is set as the identity matrix.
After this function, the glDrawElements
call is made. Since we have left our VAO object (vaoID
) bound, we pass 0
to the final parameter of this function. This tells the GPU to use the references of the GL_ELEMENT_ARRAY_BUFFER
and GL_ARRAY_BUFFER
binding points of the bound VAO. Thus we do not need to explicitly bind the vboVerticesID
and vboIndicesID
buffer objects again. After this call, we unbind the shader program by calling the GLSLShader::UnUse()
function. Finally, we call the glutSwapBuffer
function to show the back buffer on screen. After compiling and running, we get the output as shown in the following figure:
Chapter1/SimpleTriangle
directory.
Let us start this recipe using the following steps:
- Define a vertex shader (
shaders/shader.vert
) to transform the object space vertex position to clip space.#version 330 core layout(location = 0) in vec3 vVertex; layout(location = 1) in vec3 vColor; smooth out vec4 vSmoothColor; uniform mat4 MVP; void main() { vSmoothColor = vec4(vColor,1); gl_Position = MVP*vec4(vVertex,1); }
- Define a fragment shader (
shaders/shader.frag
) to output a smoothly interpolated color from the vertex shader to the frame buffer.#version 330 core smooth in vec4 vSmoothColor; layout(location=0) out vec4 vFragColor; void main() { vFragColor = vSmoothColor; }
- Load the two shaders using the
GLSLShader
class in theOnInit()
function.shader.LoadFromFile(GL_VERTEX_SHADER, "shaders/shader.vert"); shader.LoadFromFile(GL_FRAGMENT_SHADER,"shaders/shader.frag"); shader.CreateAndLinkProgram(); shader.Use(); shader.AddAttribute("vVertex"); shader.AddAttribute("vColor"); shader.AddUniform("MVP"); shader.UnUse();
- Create the geometry and topology. We will store the attributes together in an interleaved vertex format, that is, we will store the vertex attributes in a struct containing two attributes, position and color.
vertices[0].color=glm::vec3(1,0,0); vertices[1].color=glm::vec3(0,1,0); vertices[2].color=glm::vec3(0,0,1); vertices[0].position=glm::vec3(-1,-1,0); vertices[1].position=glm::vec3(0,1,0); vertices[2].position=glm::vec3(1,-1,0); indices[0] = 0; indices[1] = 1; indices[2] = 2;
- Store the geometry and topology in the buffer object(s). The stride parameter controls the number of bytes to jump to reach the next element of the same attribute. For the interleaved format, it is typically the size of our vertex struct in bytes, that is,
sizeof(Vertex)
.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,stride,0); glEnableVertexAttribArray(shader["vColor"]); glVertexAttribPointer(shader["vColor"], 3, GL_FLOAT, GL_FALSE,stride, (const GLvoid*)offsetof(Vertex, color)); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices[0], GL_STATIC_DRAW);
- Set up the resize handler to set up the viewport and projection matrix.
void OnResize(int w, int h) { glViewport (0, 0, (GLsizei) w, (GLsizei) h); P = glm::ortho(-1,1,-1,1); }
- 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); shader.Use(); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(P*MV)); glDrawElements(GL_TRIANGLES, 3, 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); }
For this simple example, we will only use a vertex shader (shaders/shader.vert
) and a fragment shader (shaders/shader.frag
). The first line in the shader signifies the GLSL version of the shader. Starting from OpenGL v3.0, the version specifiers correspond to the OpenGL version used. So for OpenGL v3.3, the GLSL version is 330. In addition, since we are interested in the core profile, we add another keyword following the version number to signify that we have a core profile shader.
The vertex shader simply outputs the input per-vertex color to the output (vSmoothColor
). Such attributes that are interpolated across shader stages are called varying attributes. It also calculates the clip space position by multiplying the per-vertex position (vVertex
) with the combined modelview projection (MVP) matrix.
The fragment shader writes the input color (vSmoothColor
) to the frame buffer output (vFragColor
).
In the simple triangle demo application code, we store the GLSLShader
object reference in the global scope so that we can access it in any function we desire. We modify the OnInit()
function by adding the following lines:
The first two lines create the GLSL shader of the given type by reading the contents of the file with the given filename. In all of the recipes in this book, the vertex shader files are stored with a .vert
extension, the geometry shader files with a .geom
extension, and the fragment shader files with a .frag
extension. Next, the GLSLShader::CreateAndLinkProgram
function is called to create the shader program from the shader object. Next, the program is bound and then the locations of attributes and uniforms are stored.
In OpenGL v3.3 and above, we typically store the geometry information in buffer objects, which is a linear array of memory managed by the GPU. In order to facilitate the handling of buffer object(s) during rendering, we use a vertex array object (VAO). This object stores references to buffer objects that are bound after the VAO is bound. The advantage we get from using a VAO is that after the VAO is bound, we do not have to bind the buffer object(s).
In this demo, we declare three variables in global scope; vaoID
for VAO handling, and vboVerticesID
and vboIndicesID
for buffer object handling. The VAO object is created by calling the glGenVertexArrays
function. The buffer objects are generated using the glGenBuffers
function. The first parameter for both of these functions is the total number of objects required, and the second parameter is the reference to where the object handle is stored. These functions are called in the OnInit()
function.
In the next few calls, we enable the vertex attributes. This function needs the location of the attribute, which we obtain by the GLSLShader::operator[]
, passing it the name of the attribute whose location we require. We then call glVertexAttributePointer
to tell the GPU how many elements there are and what is their type, whether the attribute is normalized, the stride (which means the total number of bytes to skip to reach the next element; for our case since the attributes are stored in a Vertex
struct, the next element's stride is the size of our Vertex
struct), and finally, the pointer to the attribute in the given array. The last parameter requires explanation in case we have interleaved attributes (as we have). The offsetof
operator returns the offset in bytes, to the attribute in the given struct. Hence, the GPU knows how many bytes it needs to skip in order to access the next attribute of the given type. For the vVertex
attribute, the last parameter is 0
since the next element is accessed immediately after the stride. For the second attribute vColor
, it needs to hop 12 bytes before the next vColor
attribute is obtained from the given vertices array.
To complement the object generation in the OnInit()
function, we must provide the object deletion code. This is handled in the OnShutdown()
function. We first delete the shader program by calling the GLSLShader::DeleteShaderProgram
function. Next, we delete the two buffer objects (vboVerticesID
and vboIndicesID
) and finally we delete the vertex array object (vaoID
).
The rendering code of the simple triangle demo is as follows:
The rendering code first clears the color and depth buffer and binds the shader program by calling the GLSLShader::Use()
function. It then passes the combined modelview and projection matrix to the GPU by invoking the glUniformMatrix4fv
function. The first parameter is the location of the uniform which we obtain from the GLSLShader::operator()
function, by passing it the name of the uniform whose location we need. The second parameter is the total number of matrices we wish to pass. The third parameter is a Boolean signifying if the matrix needs to be transposed, and the final parameter is the float pointer to the matrix object. Here we use the glm::value_ptr
function to get the float pointer from the matrix object. Note that the OpenGL matrices are concatenated right to left since it follows a right handed coordinate system in a column major layout. Hence we keep the projection matrix on the left and the modelview matrix on the right. For this simple example, the modelview matrix (MV
) is set as the identity matrix.
After this function, the glDrawElements
call is made. Since we have left our VAO object (vaoID
) bound, we pass 0
to the final parameter of this function. This tells the GPU to use the references of the GL_ELEMENT_ARRAY_BUFFER
and GL_ARRAY_BUFFER
binding points of the bound VAO. Thus we do not need to explicitly bind the vboVerticesID
and vboIndicesID
buffer objects again. After this call, we unbind the shader program by calling the GLSLShader::UnUse()
function. Finally, we call the glutSwapBuffer
function to show the back buffer on screen. After compiling and running, we get the output as shown in the following figure:
this recipe using the following steps:
- Define a vertex shader (
shaders/shader.vert
) to transform the object space vertex position to clip space.#version 330 core layout(location = 0) in vec3 vVertex; layout(location = 1) in vec3 vColor; smooth out vec4 vSmoothColor; uniform mat4 MVP; void main() { vSmoothColor = vec4(vColor,1); gl_Position = MVP*vec4(vVertex,1); }
- Define a fragment shader (
shaders/shader.frag
) to output a smoothly interpolated color from the vertex shader to the frame buffer.#version 330 core smooth in vec4 vSmoothColor; layout(location=0) out vec4 vFragColor; void main() { vFragColor = vSmoothColor; }
- Load the two shaders using the
GLSLShader
class in theOnInit()
function.shader.LoadFromFile(GL_VERTEX_SHADER, "shaders/shader.vert"); shader.LoadFromFile(GL_FRAGMENT_SHADER,"shaders/shader.frag"); shader.CreateAndLinkProgram(); shader.Use(); shader.AddAttribute("vVertex"); shader.AddAttribute("vColor"); shader.AddUniform("MVP"); shader.UnUse();
- Create the geometry and topology. We will store the attributes together in an interleaved vertex format, that is, we will store the vertex attributes in a struct containing two attributes, position and color.
vertices[0].color=glm::vec3(1,0,0); vertices[1].color=glm::vec3(0,1,0); vertices[2].color=glm::vec3(0,0,1); vertices[0].position=glm::vec3(-1,-1,0); vertices[1].position=glm::vec3(0,1,0); vertices[2].position=glm::vec3(1,-1,0); indices[0] = 0; indices[1] = 1; indices[2] = 2;
- Store the geometry and topology in the buffer object(s). The stride parameter controls the number of bytes to jump to reach the next element of the same attribute. For the interleaved format, it is typically the size of our vertex struct in bytes, that is,
sizeof(Vertex)
.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,stride,0); glEnableVertexAttribArray(shader["vColor"]); glVertexAttribPointer(shader["vColor"], 3, GL_FLOAT, GL_FALSE,stride, (const GLvoid*)offsetof(Vertex, color)); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices[0], GL_STATIC_DRAW);
- Set up the resize handler to set up the viewport and projection matrix.
void OnResize(int w, int h) { glViewport (0, 0, (GLsizei) w, (GLsizei) h); P = glm::ortho(-1,1,-1,1); }
- 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); shader.Use(); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(P*MV)); glDrawElements(GL_TRIANGLES, 3, 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); }
For this simple example, we will only use a vertex shader (shaders/shader.vert
) and a fragment shader (shaders/shader.frag
). The first line in the shader signifies the GLSL version of the shader. Starting from OpenGL v3.0, the version specifiers correspond to the OpenGL version used. So for OpenGL v3.3, the GLSL version is 330. In addition, since we are interested in the core profile, we add another keyword following the version number to signify that we have a core profile shader.
The vertex shader simply outputs the input per-vertex color to the output (vSmoothColor
). Such attributes that are interpolated across shader stages are called varying attributes. It also calculates the clip space position by multiplying the per-vertex position (vVertex
) with the combined modelview projection (MVP) matrix.
The fragment shader writes the input color (vSmoothColor
) to the frame buffer output (vFragColor
).
In the simple triangle demo application code, we store the GLSLShader
object reference in the global scope so that we can access it in any function we desire. We modify the OnInit()
function by adding the following lines:
The first two lines create the GLSL shader of the given type by reading the contents of the file with the given filename. In all of the recipes in this book, the vertex shader files are stored with a .vert
extension, the geometry shader files with a .geom
extension, and the fragment shader files with a .frag
extension. Next, the GLSLShader::CreateAndLinkProgram
function is called to create the shader program from the shader object. Next, the program is bound and then the locations of attributes and uniforms are stored.
In OpenGL v3.3 and above, we typically store the geometry information in buffer objects, which is a linear array of memory managed by the GPU. In order to facilitate the handling of buffer object(s) during rendering, we use a vertex array object (VAO). This object stores references to buffer objects that are bound after the VAO is bound. The advantage we get from using a VAO is that after the VAO is bound, we do not have to bind the buffer object(s).
In this demo, we declare three variables in global scope; vaoID
for VAO handling, and vboVerticesID
and vboIndicesID
for buffer object handling. The VAO object is created by calling the glGenVertexArrays
function. The buffer objects are generated using the glGenBuffers
function. The first parameter for both of these functions is the total number of objects required, and the second parameter is the reference to where the object handle is stored. These functions are called in the OnInit()
function.
In the next few calls, we enable the vertex attributes. This function needs the location of the attribute, which we obtain by the GLSLShader::operator[]
, passing it the name of the attribute whose location we require. We then call glVertexAttributePointer
to tell the GPU how many elements there are and what is their type, whether the attribute is normalized, the stride (which means the total number of bytes to skip to reach the next element; for our case since the attributes are stored in a Vertex
struct, the next element's stride is the size of our Vertex
struct), and finally, the pointer to the attribute in the given array. The last parameter requires explanation in case we have interleaved attributes (as we have). The offsetof
operator returns the offset in bytes, to the attribute in the given struct. Hence, the GPU knows how many bytes it needs to skip in order to access the next attribute of the given type. For the vVertex
attribute, the last parameter is 0
since the next element is accessed immediately after the stride. For the second attribute vColor
, it needs to hop 12 bytes before the next vColor
attribute is obtained from the given vertices array.
To complement the object generation in the OnInit()
function, we must provide the object deletion code. This is handled in the OnShutdown()
function. We first delete the shader program by calling the GLSLShader::DeleteShaderProgram
function. Next, we delete the two buffer objects (vboVerticesID
and vboIndicesID
) and finally we delete the vertex array object (vaoID
).
The rendering code of the simple triangle demo is as follows:
The rendering code first clears the color and depth buffer and binds the shader program by calling the GLSLShader::Use()
function. It then passes the combined modelview and projection matrix to the GPU by invoking the glUniformMatrix4fv
function. The first parameter is the location of the uniform which we obtain from the GLSLShader::operator()
function, by passing it the name of the uniform whose location we need. The second parameter is the total number of matrices we wish to pass. The third parameter is a Boolean signifying if the matrix needs to be transposed, and the final parameter is the float pointer to the matrix object. Here we use the glm::value_ptr
function to get the float pointer from the matrix object. Note that the OpenGL matrices are concatenated right to left since it follows a right handed coordinate system in a column major layout. Hence we keep the projection matrix on the left and the modelview matrix on the right. For this simple example, the modelview matrix (MV
) is set as the identity matrix.
After this function, the glDrawElements
call is made. Since we have left our VAO object (vaoID
) bound, we pass 0
to the final parameter of this function. This tells the GPU to use the references of the GL_ELEMENT_ARRAY_BUFFER
and GL_ARRAY_BUFFER
binding points of the bound VAO. Thus we do not need to explicitly bind the vboVerticesID
and vboIndicesID
buffer objects again. After this call, we unbind the shader program by calling the GLSLShader::UnUse()
function. Finally, we call the glutSwapBuffer
function to show the back buffer on screen. After compiling and running, we get the output as shown in the following figure:
simple example, we will only use a vertex shader (shaders/shader.vert
) and a fragment shader (shaders/shader.frag
). The first line in the shader signifies the GLSL version of the shader. Starting from OpenGL v3.0, the version specifiers correspond to the OpenGL version used. So for OpenGL v3.3, the GLSL version is 330. In addition, since we are interested in the core profile, we add another keyword following the version number to signify that we have a core profile shader.
The vertex shader simply outputs the input per-vertex color to the output (vSmoothColor
). Such attributes that are interpolated across shader stages are called varying attributes. It also calculates the clip space position by multiplying the per-vertex position (vVertex
) with the combined modelview projection (MVP) matrix.
The fragment shader writes the input color (vSmoothColor
) to the frame buffer output (vFragColor
).
In the simple triangle demo application code, we store the GLSLShader
object reference in the global scope so that we can access it in any function we desire. We modify the OnInit()
function by adding the following lines:
The first two lines create the GLSL shader of the given type by reading the contents of the file with the given filename. In all of the recipes in this book, the vertex shader files are stored with a .vert
extension, the geometry shader files with a .geom
extension, and the fragment shader files with a .frag
extension. Next, the GLSLShader::CreateAndLinkProgram
function is called to create the shader program from the shader object. Next, the program is bound and then the locations of attributes and uniforms are stored.
In OpenGL v3.3 and above, we typically store the geometry information in buffer objects, which is a linear array of memory managed by the GPU. In order to facilitate the handling of buffer object(s) during rendering, we use a vertex array object (VAO). This object stores references to buffer objects that are bound after the VAO is bound. The advantage we get from using a VAO is that after the VAO is bound, we do not have to bind the buffer object(s).
In this demo, we declare three variables in global scope; vaoID
for VAO handling, and vboVerticesID
and vboIndicesID
for buffer object handling. The VAO object is created by calling the glGenVertexArrays
function. The buffer objects are generated using the glGenBuffers
function. The first parameter for both of these functions is the total number of objects required, and the second parameter is the reference to where the object handle is stored. These functions are called in the OnInit()
function.
In the next few calls, we enable the vertex attributes. This function needs the location of the attribute, which we obtain by the GLSLShader::operator[]
, passing it the name of the attribute whose location we require. We then call glVertexAttributePointer
to tell the GPU how many elements there are and what is their type, whether the attribute is normalized, the stride (which means the total number of bytes to skip to reach the next element; for our case since the attributes are stored in a Vertex
struct, the next element's stride is the size of our Vertex
struct), and finally, the pointer to the attribute in the given array. The last parameter requires explanation in case we have interleaved attributes (as we have). The offsetof
operator returns the offset in bytes, to the attribute in the given struct. Hence, the GPU knows how many bytes it needs to skip in order to access the next attribute of the given type. For the vVertex
attribute, the last parameter is 0
since the next element is accessed immediately after the stride. For the second attribute vColor
, it needs to hop 12 bytes before the next vColor
attribute is obtained from the given vertices array.
To complement the object generation in the OnInit()
function, we must provide the object deletion code. This is handled in the OnShutdown()
function. We first delete the shader program by calling the GLSLShader::DeleteShaderProgram
function. Next, we delete the two buffer objects (vboVerticesID
and vboIndicesID
) and finally we delete the vertex array object (vaoID
).
The rendering code of the simple triangle demo is as follows:
The rendering code first clears the color and depth buffer and binds the shader program by calling the GLSLShader::Use()
function. It then passes the combined modelview and projection matrix to the GPU by invoking the glUniformMatrix4fv
function. The first parameter is the location of the uniform which we obtain from the GLSLShader::operator()
function, by passing it the name of the uniform whose location we need. The second parameter is the total number of matrices we wish to pass. The third parameter is a Boolean signifying if the matrix needs to be transposed, and the final parameter is the float pointer to the matrix object. Here we use the glm::value_ptr
function to get the float pointer from the matrix object. Note that the OpenGL matrices are concatenated right to left since it follows a right handed coordinate system in a column major layout. Hence we keep the projection matrix on the left and the modelview matrix on the right. For this simple example, the modelview matrix (MV
) is set as the identity matrix.
After this function, the glDrawElements
call is made. Since we have left our VAO object (vaoID
) bound, we pass 0
to the final parameter of this function. This tells the GPU to use the references of the GL_ELEMENT_ARRAY_BUFFER
and GL_ARRAY_BUFFER
binding points of the bound VAO. Thus we do not need to explicitly bind the vboVerticesID
and vboIndicesID
buffer objects again. After this call, we unbind the shader program by calling the GLSLShader::UnUse()
function. Finally, we call the glutSwapBuffer
function to show the back buffer on screen. After compiling and running, we get the output as shown in the following figure:
demo application code, we store the GLSLShader
object reference in the global scope so that we can access it in any function we desire. We modify the OnInit()
function by adding the following lines:
The first two lines create the GLSL shader of the given type by reading the contents of the file with the given filename. In all of the recipes in this book, the vertex shader files are stored with a .vert
extension, the geometry shader files with a .geom
extension, and the fragment shader files with a .frag
extension. Next, the GLSLShader::CreateAndLinkProgram
function is called to create the shader program from the shader object. Next, the program is bound and then the locations of attributes and uniforms are stored.
In OpenGL v3.3 and above, we typically store the geometry information in buffer objects, which is a linear array of memory managed by the GPU. In order to facilitate the handling of buffer object(s) during rendering, we use a vertex array object (VAO). This object stores references to buffer objects that are bound after the VAO is bound. The advantage we get from using a VAO is that after the VAO is bound, we do not have to bind the buffer object(s).
In this demo, we declare three variables in global scope; vaoID
for VAO handling, and vboVerticesID
and vboIndicesID
for buffer object handling. The VAO object is created by calling the glGenVertexArrays
function. The buffer objects are generated using the glGenBuffers
function. The first parameter for both of these functions is the total number of objects required, and the second parameter is the reference to where the object handle is stored. These functions are called in the OnInit()
function.
In the next few calls, we enable the vertex attributes. This function needs the location of the attribute, which we obtain by the GLSLShader::operator[]
, passing it the name of the attribute whose location we require. We then call glVertexAttributePointer
to tell the GPU how many elements there are and what is their type, whether the attribute is normalized, the stride (which means the total number of bytes to skip to reach the next element; for our case since the attributes are stored in a Vertex
struct, the next element's stride is the size of our Vertex
struct), and finally, the pointer to the attribute in the given array. The last parameter requires explanation in case we have interleaved attributes (as we have). The offsetof
operator returns the offset in bytes, to the attribute in the given struct. Hence, the GPU knows how many bytes it needs to skip in order to access the next attribute of the given type. For the vVertex
attribute, the last parameter is 0
since the next element is accessed immediately after the stride. For the second attribute vColor
, it needs to hop 12 bytes before the next vColor
attribute is obtained from the given vertices array.
To complement the object generation in the OnInit()
function, we must provide the object deletion code. This is handled in the OnShutdown()
function. We first delete the shader program by calling the GLSLShader::DeleteShaderProgram
function. Next, we delete the two buffer objects (vboVerticesID
and vboIndicesID
) and finally we delete the vertex array object (vaoID
).
The rendering code of the simple triangle demo is as follows:
The rendering code first clears the color and depth buffer and binds the shader program by calling the GLSLShader::Use()
function. It then passes the combined modelview and projection matrix to the GPU by invoking the glUniformMatrix4fv
function. The first parameter is the location of the uniform which we obtain from the GLSLShader::operator()
function, by passing it the name of the uniform whose location we need. The second parameter is the total number of matrices we wish to pass. The third parameter is a Boolean signifying if the matrix needs to be transposed, and the final parameter is the float pointer to the matrix object. Here we use the glm::value_ptr
function to get the float pointer from the matrix object. Note that the OpenGL matrices are concatenated right to left since it follows a right handed coordinate system in a column major layout. Hence we keep the projection matrix on the left and the modelview matrix on the right. For this simple example, the modelview matrix (MV
) is set as the identity matrix.
After this function, the glDrawElements
call is made. Since we have left our VAO object (vaoID
) bound, we pass 0
to the final parameter of this function. This tells the GPU to use the references of the GL_ELEMENT_ARRAY_BUFFER
and GL_ARRAY_BUFFER
binding points of the bound VAO. Thus we do not need to explicitly bind the vboVerticesID
and vboIndicesID
buffer objects again. After this call, we unbind the shader program by calling the GLSLShader::UnUse()
function. Finally, we call the glutSwapBuffer
function to show the back buffer on screen. After compiling and running, we get the output as shown in the following figure:
In this recipe, we will deform a planar mesh using the vertex shader. We know that the vertex shader is responsible for outputting the clip space position of the given object space vertex. In between this conversion, we can apply the modeling transformation to transform the given object space vertex to world space position.
We can implement a ripple shader using the following steps:
- Define the vertex shader that deforms the object space vertex position.
#version 330 core layout(location=0) in vec3 vVertex; uniform mat4 MVP; uniform float time; const float amplitude = 0.125; const float frequency = 4; const float PI = 3.14159; void main() { float distance = length(vVertex); float y = amplitude*sin(-PI*distance*frequency+time); gl_Position = MVP*vec4(vVertex.x, y, vVertex.z,1); }
- Define a fragment shader 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 two shaders using the
GLSLShader
class in theOnInit()
function.shader.LoadFromFile(GL_VERTEX_SHADER, "shaders/shader.vert"); shader.LoadFromFile(GL_FRAGMENT_SHADER, "shaders/shader.frag"); shader.CreateAndLinkProgram(); shader.Use(); shader.AddAttribute("vVertex"); shader.AddUniform("MVP"); shader.AddUniform("time"); shader.UnUse();
- Create the geometry and topology.
int count = 0; int i=0, j=0; for( j=0;j<=NUM_Z;j++) { for( i=0;i<=NUM_X;i++) { vertices[count++] = glm::vec3( ((float(i)/(NUM_X-1)) *2-1)* HALF_SIZE_X, 0, ((float(j)/(NUM_Z-1))*2-1)*HALF_SIZE_Z); } } GLushort* id=&indices[0]; for (i = 0; i < NUM_Z; i++) { for (j = 0; j < NUM_X; j++) { int i0 = i * (NUM_X+1) + j; int i1 = i0 + 1; int i2 = i0 + (NUM_X+1); int i3 = i2 + 1; if ((j+i)%2) { *id++ = i0; *id++ = i2; *id++ = i1; *id++ = i1; *id++ = i2; *id++ = i3; } else { *id++ = i0; *id++ = i2; *id++ = i3; *id++ = i0; *id++ = i3; *id++ = i1; } } }
- Store the geometry and topology in the buffer object(s).
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);
- Set up the perspective projection matrix in the resize handler.
P = glm::perspective(45.0f, (GLfloat)w/h, 1.f, 1000.f);
- Set up the rendering code to bind the
GLSLShader
shader, pass the uniforms and then draw the geometry.void OnRender() { time = glutGet(GLUT_ELAPSED_TIME)/1000.0f * SPEED; 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)); glm::mat4 MVP= P*MV; shader.Use(); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(MVP)); glUniform1f(shader("time"), time); glDrawElements(GL_TRIANGLES,TOTAL_INDICES,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); }
In this recipe, the only attribute passed in is the per-vertex position (vVertex
). There are two uniforms: the combined modelview projection matrix (MVP
) and the current time (time
). We will use the time
uniform to allow progression of the deformer so we can observe the ripple movement. After these declarations are three constants, namely amplitude
(which controls how much the ripple moves up and down from the zero base line), frequency
(which controls the total number of waves), and PI
(a constant used in the wave formula). Note that we could have replaced the constants with uniforms and had them modified from the application code.
In our formula, we first find the distance (d
) of the vertex from the origin by using the Euclidean distance formula. This is given to us by the length
built-in GLSL function. Next, we input the distance into the sin
function multiplying the distance by the frequency (f
) and (π
). In our vertex shader, we replace the phase (φ
) with time.
Similar to the previous recipe, we declare the GLSLShader
object in the global scope to allow maximum visibility. Next, we initialize the GLSLShader
object in the OnInit()
function.
The only difference in this recipe is the addition of an additional uniform (time
).
This sort of topology can be generated using the following code:
In order to alternate the triangle directions and maintain their winding order, we take two different combinations, one for an even iteration and second for an odd iteration. This can be achieved using the following code:
After filling the vertices and indices arrays, we push this data to the GPU memory. We first create a vertex array object (vaoID
) and two buffer objects, the GL_ARRAY_BUFFER
binding for vertices and the GL_ELEMENT_ARRAY_BUFFER
binding for the indices array. These calls are exactly the same as in the previous recipe. The only difference is that now we only have a single per-vertex attribute, that is, the vertex position (vVertex
). The OnShutdown()
function is also unchanged as in the previous recipe.
Note that the matrix multiplication in glm
follows from right to left. So the order in which we generate the transformations will be applied in the reverse order. In our case the combined modelview matrix will be calculated as MV = (T*(Rx*Ry))
. The translation amount, dist
, and the rotation values, rX
and rY
, are calculated in the mouse input functions based on the user's input.
The OnMouseMove
function is defined as follows:
The OnMouseMove
function has only two parameters, the x
and y
screen location where the mouse currently is. The mouse move event is raised whenever the mouse enters and moves in the application window. Based on the state set in the OnMouseDown
function, we calculate the zoom amount (dist
) if the middle mouse button is pressed. Otherwise, we calculate the two rotation amounts (rX
and rY
). Next, we update the oldX
and oldY
positions for the next event. Finally we request the freeglut framework to repaint our application window by calling glutPostRedisplay()
function. This call sends the repaint event which re-renders our scene.
Chapter1\RippleDeformer
directory.
We can implement a ripple shader using the following steps:
- Define the vertex shader that deforms the object space vertex position.
#version 330 core layout(location=0) in vec3 vVertex; uniform mat4 MVP; uniform float time; const float amplitude = 0.125; const float frequency = 4; const float PI = 3.14159; void main() { float distance = length(vVertex); float y = amplitude*sin(-PI*distance*frequency+time); gl_Position = MVP*vec4(vVertex.x, y, vVertex.z,1); }
- Define a fragment shader 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 two shaders using the
GLSLShader
class in theOnInit()
function.shader.LoadFromFile(GL_VERTEX_SHADER, "shaders/shader.vert"); shader.LoadFromFile(GL_FRAGMENT_SHADER, "shaders/shader.frag"); shader.CreateAndLinkProgram(); shader.Use(); shader.AddAttribute("vVertex"); shader.AddUniform("MVP"); shader.AddUniform("time"); shader.UnUse();
- Create the geometry and topology.
int count = 0; int i=0, j=0; for( j=0;j<=NUM_Z;j++) { for( i=0;i<=NUM_X;i++) { vertices[count++] = glm::vec3( ((float(i)/(NUM_X-1)) *2-1)* HALF_SIZE_X, 0, ((float(j)/(NUM_Z-1))*2-1)*HALF_SIZE_Z); } } GLushort* id=&indices[0]; for (i = 0; i < NUM_Z; i++) { for (j = 0; j < NUM_X; j++) { int i0 = i * (NUM_X+1) + j; int i1 = i0 + 1; int i2 = i0 + (NUM_X+1); int i3 = i2 + 1; if ((j+i)%2) { *id++ = i0; *id++ = i2; *id++ = i1; *id++ = i1; *id++ = i2; *id++ = i3; } else { *id++ = i0; *id++ = i2; *id++ = i3; *id++ = i0; *id++ = i3; *id++ = i1; } } }
- Store the geometry and topology in the buffer object(s).
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);
- Set up the perspective projection matrix in the resize handler.
P = glm::perspective(45.0f, (GLfloat)w/h, 1.f, 1000.f);
- Set up the rendering code to bind the
GLSLShader
shader, pass the uniforms and then draw the geometry.void OnRender() { time = glutGet(GLUT_ELAPSED_TIME)/1000.0f * SPEED; 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)); glm::mat4 MVP= P*MV; shader.Use(); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(MVP)); glUniform1f(shader("time"), time); glDrawElements(GL_TRIANGLES,TOTAL_INDICES,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); }
In this recipe, the only attribute passed in is the per-vertex position (vVertex
). There are two uniforms: the combined modelview projection matrix (MVP
) and the current time (time
). We will use the time
uniform to allow progression of the deformer so we can observe the ripple movement. After these declarations are three constants, namely amplitude
(which controls how much the ripple moves up and down from the zero base line), frequency
(which controls the total number of waves), and PI
(a constant used in the wave formula). Note that we could have replaced the constants with uniforms and had them modified from the application code.
In our formula, we first find the distance (d
) of the vertex from the origin by using the Euclidean distance formula. This is given to us by the length
built-in GLSL function. Next, we input the distance into the sin
function multiplying the distance by the frequency (f
) and (π
). In our vertex shader, we replace the phase (φ
) with time.
Similar to the previous recipe, we declare the GLSLShader
object in the global scope to allow maximum visibility. Next, we initialize the GLSLShader
object in the OnInit()
function.
The only difference in this recipe is the addition of an additional uniform (time
).
This sort of topology can be generated using the following code:
In order to alternate the triangle directions and maintain their winding order, we take two different combinations, one for an even iteration and second for an odd iteration. This can be achieved using the following code:
After filling the vertices and indices arrays, we push this data to the GPU memory. We first create a vertex array object (vaoID
) and two buffer objects, the GL_ARRAY_BUFFER
binding for vertices and the GL_ELEMENT_ARRAY_BUFFER
binding for the indices array. These calls are exactly the same as in the previous recipe. The only difference is that now we only have a single per-vertex attribute, that is, the vertex position (vVertex
). The OnShutdown()
function is also unchanged as in the previous recipe.
Note that the matrix multiplication in glm
follows from right to left. So the order in which we generate the transformations will be applied in the reverse order. In our case the combined modelview matrix will be calculated as MV = (T*(Rx*Ry))
. The translation amount, dist
, and the rotation values, rX
and rY
, are calculated in the mouse input functions based on the user's input.
The OnMouseMove
function is defined as follows:
The OnMouseMove
function has only two parameters, the x
and y
screen location where the mouse currently is. The mouse move event is raised whenever the mouse enters and moves in the application window. Based on the state set in the OnMouseDown
function, we calculate the zoom amount (dist
) if the middle mouse button is pressed. Otherwise, we calculate the two rotation amounts (rX
and rY
). Next, we update the oldX
and oldY
positions for the next event. Finally we request the freeglut framework to repaint our application window by calling glutPostRedisplay()
function. This call sends the repaint event which re-renders our scene.
#version 330 core layout(location=0) in vec3 vVertex; uniform mat4 MVP; uniform float time; const float amplitude = 0.125; const float frequency = 4; const float PI = 3.14159; void main() { float distance = length(vVertex); float y = amplitude*sin(-PI*distance*frequency+time); gl_Position = MVP*vec4(vVertex.x, y, vVertex.z,1); }
- fragment shader 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 two shaders using the
GLSLShader
class in theOnInit()
function.shader.LoadFromFile(GL_VERTEX_SHADER, "shaders/shader.vert"); shader.LoadFromFile(GL_FRAGMENT_SHADER, "shaders/shader.frag"); shader.CreateAndLinkProgram(); shader.Use(); shader.AddAttribute("vVertex"); shader.AddUniform("MVP"); shader.AddUniform("time"); shader.UnUse();
- Create the geometry and topology.
int count = 0; int i=0, j=0; for( j=0;j<=NUM_Z;j++) { for( i=0;i<=NUM_X;i++) { vertices[count++] = glm::vec3( ((float(i)/(NUM_X-1)) *2-1)* HALF_SIZE_X, 0, ((float(j)/(NUM_Z-1))*2-1)*HALF_SIZE_Z); } } GLushort* id=&indices[0]; for (i = 0; i < NUM_Z; i++) { for (j = 0; j < NUM_X; j++) { int i0 = i * (NUM_X+1) + j; int i1 = i0 + 1; int i2 = i0 + (NUM_X+1); int i3 = i2 + 1; if ((j+i)%2) { *id++ = i0; *id++ = i2; *id++ = i1; *id++ = i1; *id++ = i2; *id++ = i3; } else { *id++ = i0; *id++ = i2; *id++ = i3; *id++ = i0; *id++ = i3; *id++ = i1; } } }
- Store the geometry and topology in the buffer object(s).
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);
- Set up the perspective projection matrix in the resize handler.
P = glm::perspective(45.0f, (GLfloat)w/h, 1.f, 1000.f);
- Set up the rendering code to bind the
GLSLShader
shader, pass the uniforms and then draw the geometry.void OnRender() { time = glutGet(GLUT_ELAPSED_TIME)/1000.0f * SPEED; 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)); glm::mat4 MVP= P*MV; shader.Use(); glUniformMatrix4fv(shader("MVP"), 1, GL_FALSE, glm::value_ptr(MVP)); glUniform1f(shader("time"), time); glDrawElements(GL_TRIANGLES,TOTAL_INDICES,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); }
In this recipe, the only attribute passed in is the per-vertex position (vVertex
). There are two uniforms: the combined modelview projection matrix (MVP
) and the current time (time
). We will use the time
uniform to allow progression of the deformer so we can observe the ripple movement. After these declarations are three constants, namely amplitude
(which controls how much the ripple moves up and down from the zero base line), frequency
(which controls the total number of waves), and PI
(a constant used in the wave formula). Note that we could have replaced the constants with uniforms and had them modified from the application code.
In our formula, we first find the distance (d
) of the vertex from the origin by using the Euclidean distance formula. This is given to us by the length
built-in GLSL function. Next, we input the distance into the sin
function multiplying the distance by the frequency (f
) and (π
). In our vertex shader, we replace the phase (φ
) with time.
Similar to the previous recipe, we declare the GLSLShader
object in the global scope to allow maximum visibility. Next, we initialize the GLSLShader
object in the OnInit()
function.
The only difference in this recipe is the addition of an additional uniform (time
).
This sort of topology can be generated using the following code:
In order to alternate the triangle directions and maintain their winding order, we take two different combinations, one for an even iteration and second for an odd iteration. This can be achieved using the following code:
After filling the vertices and indices arrays, we push this data to the GPU memory. We first create a vertex array object (vaoID
) and two buffer objects, the GL_ARRAY_BUFFER
binding for vertices and the GL_ELEMENT_ARRAY_BUFFER
binding for the indices array. These calls are exactly the same as in the previous recipe. The only difference is that now we only have a single per-vertex attribute, that is, the vertex position (vVertex
). The OnShutdown()
function is also unchanged as in the previous recipe.
Note that the matrix multiplication in glm
follows from right to left. So the order in which we generate the transformations will be applied in the reverse order. In our case the combined modelview matrix will be calculated as MV = (T*(Rx*Ry))
. The translation amount, dist
, and the rotation values, rX
and rY
, are calculated in the mouse input functions based on the user's input.
The OnMouseMove
function is defined as follows:
The OnMouseMove
function has only two parameters, the x
and y
screen location where the mouse currently is. The mouse move event is raised whenever the mouse enters and moves in the application window. Based on the state set in the OnMouseDown
function, we calculate the zoom amount (dist
) if the middle mouse button is pressed. Otherwise, we calculate the two rotation amounts (rX
and rY
). Next, we update the oldX
and oldY
positions for the next event. Finally we request the freeglut framework to repaint our application window by calling glutPostRedisplay()
function. This call sends the repaint event which re-renders our scene.
only attribute passed in is the per-vertex position (vVertex
). There are two uniforms: the combined modelview projection matrix (MVP
) and the current time (time
). We will use the time
uniform to allow progression of the deformer so we can observe the ripple movement. After these declarations are three constants, namely amplitude
(which controls how much the ripple moves up and down from the zero base line), frequency
(which controls the total number of waves), and PI
(a constant used in the wave formula). Note that we could have replaced the constants with uniforms and had them modified from the application code.
In our formula, we first find the distance (d
) of the vertex from the origin by using the Euclidean distance formula. This is given to us by the length
built-in GLSL function. Next, we input the distance into the sin
function multiplying the distance by the frequency (f
) and (π
). In our vertex shader, we replace the phase (φ
) with time.
Similar to the previous recipe, we declare the GLSLShader
object in the global scope to allow maximum visibility. Next, we initialize the GLSLShader
object in the OnInit()
function.
The only difference in this recipe is the addition of an additional uniform (time
).
This sort of topology can be generated using the following code:
In order to alternate the triangle directions and maintain their winding order, we take two different combinations, one for an even iteration and second for an odd iteration. This can be achieved using the following code:
After filling the vertices and indices arrays, we push this data to the GPU memory. We first create a vertex array object (vaoID
) and two buffer objects, the GL_ARRAY_BUFFER
binding for vertices and the GL_ELEMENT_ARRAY_BUFFER
binding for the indices array. These calls are exactly the same as in the previous recipe. The only difference is that now we only have a single per-vertex attribute, that is, the vertex position (vVertex
). The OnShutdown()
function is also unchanged as in the previous recipe.
Note that the matrix multiplication in glm
follows from right to left. So the order in which we generate the transformations will be applied in the reverse order. In our case the combined modelview matrix will be calculated as MV = (T*(Rx*Ry))
. The translation amount, dist
, and the rotation values, rX
and rY
, are calculated in the mouse input functions based on the user's input.
The OnMouseMove
function is defined as follows:
The OnMouseMove
function has only two parameters, the x
and y
screen location where the mouse currently is. The mouse move event is raised whenever the mouse enters and moves in the application window. Based on the state set in the OnMouseDown
function, we calculate the zoom amount (dist
) if the middle mouse button is pressed. Otherwise, we calculate the two rotation amounts (rX
and rY
). Next, we update the oldX
and oldY
positions for the next event. Finally we request the freeglut framework to repaint our application window by calling glutPostRedisplay()
function. This call sends the repaint event which re-renders our scene.
previous recipe, we declare the GLSLShader
object in the global scope to allow maximum visibility. Next, we initialize the GLSLShader
object in the OnInit()
function.
The only difference in this recipe is the addition of an additional uniform (time
).
This sort of topology can be generated using the following code:
In order to alternate the triangle directions and maintain their winding order, we take two different combinations, one for an even iteration and second for an odd iteration. This can be achieved using the following code:
After filling the vertices and indices arrays, we push this data to the GPU memory. We first create a vertex array object (vaoID
) and two buffer objects, the GL_ARRAY_BUFFER
binding for vertices and the GL_ELEMENT_ARRAY_BUFFER
binding for the indices array. These calls are exactly the same as in the previous recipe. The only difference is that now we only have a single per-vertex attribute, that is, the vertex position (vVertex
). The OnShutdown()
function is also unchanged as in the previous recipe.
Note that the matrix multiplication in glm
follows from right to left. So the order in which we generate the transformations will be applied in the reverse order. In our case the combined modelview matrix will be calculated as MV = (T*(Rx*Ry))
. The translation amount, dist
, and the rotation values, rX
and rY
, are calculated in the mouse input functions based on the user's input.
The OnMouseMove
function is defined as follows:
The OnMouseMove
function has only two parameters, the x
and y
screen location where the mouse currently is. The mouse move event is raised whenever the mouse enters and moves in the application window. Based on the state set in the OnMouseDown
function, we calculate the zoom amount (dist
) if the middle mouse button is pressed. Otherwise, we calculate the two rotation amounts (rX
and rY
). Next, we update the oldX
and oldY
positions for the next event. Finally we request the freeglut framework to repaint our application window by calling glutPostRedisplay()
function. This call sends the repaint event which re-renders our scene.
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.
In this recipe, we will dynamically subdivide a planar mesh using the geometry shader.
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.
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; }
Let's dissect the geometry shader.
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.
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
.
The fragment shader outputs a constant color (white: vec4(1,1,1,1)
).
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.
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.
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:
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.
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; }
Let's dissect the geometry shader.
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.
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
.
The fragment shader outputs a constant color (white: vec4(1,1,1,1)
).
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.
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.
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:
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); }
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; } } }
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); }
- 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; }
Let's dissect the geometry shader.
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.
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
.
The fragment shader outputs a constant color (white: vec4(1,1,1,1)
).
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.
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.
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:
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.
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
.
The fragment shader outputs a constant color (white: vec4(1,1,1,1)
).
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.
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.
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:
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.
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:
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.
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(); }
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]
).
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.
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.
Chapter1\SubdivisionGeometryShader_Instanced
directory.
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(); }
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]
).
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.
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.
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);
}
MVP
matrix with the PV
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; } } }
- 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(); }
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]
).
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.
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.
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.
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.
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.
We will wrap up this chapter with a recipe for creating a simple image viewer in the OpenGL v3.3 core profile using the SOIL
image loading library.
Let us now implement the image loader by following these steps:
- Load the image using the
SOIL
library. Since the loaded image fromSOIL
is inverted vertically, we flip the image on the Y axis.int texture_width = 0, texture_height = 0, channels=0; GLubyte* pData = SOIL_load_image(filename.c_str(), &texture_width, &texture_height, &channels, SOIL_LOAD_AUTO); if(pData == NULL) { cerr<<"Cannot load image: "<<filename.c_str()<<endl; exit(EXIT_FAILURE); } int i,j; for( j = 0; j*2 < texture_height; ++j ) { int index1 = j * texture_width * channels; int index2 = (texture_height - 1 - j) * texture_width * channels; for( i = texture_width * channels; i > 0; --i ) { GLubyte temp = pData[index1]; pData[index1] = pData[index2]; pData[index2] = temp; ++index1; ++index2; } }
- Set up the OpenGL texture object and free the data allocated by the
SOIL
library.glGenTextures(1, &textureID); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textureID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture_width, texture_height, 0, GL_RGB, GL_UNSIGNED_BYTE, pData); SOIL_free_image_data(pData);
- Set up the vertex shader to output the clip space position (
shaders/shader.vert
).#version 330 core layout(location=0) in vec2 vVertex; smooth out vec2 vUV; void main() { gl_Position = vec4(vVertex*2.0-1,0,1); vUV = vVertex; }
- Set up the fragment shader that samples our image texture (
shaders/shader.frag
).#version 330 core layout (location=0) out vec4 vFragColor; smooth in vec2 vUV; uniform sampler2D textureMap; void main() { vFragColor = texture(textureMap, vUV); }
- Set up the application code using the
GLSLShader
shader class.shader.LoadFromFile(GL_VERTEX_SHADER, "shaders/shader.vert"); shader.LoadFromFile(GL_FRAGMENT_SHADER,"shaders/shader.frag"); shader.CreateAndLinkProgram(); shader.Use(); shader.AddAttribute("vVertex"); shader.AddUniform("textureMap"); glUniform1i(shader("textureMap"), 0); shader.UnUse();
- Set up the geometry and topology and pass data to the GPU using buffer objects.
vertices[0] = glm::vec2(0.0,0.0); vertices[1] = glm::vec2(1.0,0.0); vertices[2] = glm::vec2(1.0,1.0); vertices[3] = glm::vec2(0.0,1.0); GLushort* id=&indices[0]; *id++ =0; *id++ =1; *id++ =2; *id++ =0; *id++ =2; *id++ =3; 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"], 2, GL_FLOAT, GL_FALSE,0,0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices[0], GL_STATIC_DRAW);
- Set the shader and render the geometry.
void OnRender() { glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); shader.Use(); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); shader.UnUse(); glutSwapBuffers(); }
- Release the allocated resources.
void OnShutdown() { shader.DeleteShaderProgram(); glDeleteBuffers(1, &vboVerticesID); glDeleteBuffers(1, &vboIndicesID); glDeleteVertexArrays(1, &vaoID); glDeleteTextures(1, &textureID); }
The first parameter is the image file name. The next three parameters return the texture width, texture height, and total color channels in the image. These are used when generating the OpenGL texture object. The final parameter is the flag which is used to control further processing on the image. For this simple example, we will use the SOIL_LOAD_AUTO
flag which keeps all of the loading settings set to default. If the function succeeds, it returns unsigned char*
to the image data. If it fails, the return value is NULL (0)
. Since the image data loaded by SOIL
is vertically flipped, we then use two nested loops to flip the image data on the Y axis.
After the image data is loaded, we generate an OpenGL texture object and pass this data to the texture memory.
The glTexImage2D
function is where the actual allocation of the texture object takes place. The first parameter is the texture target (in our case this is GL_TEXTURE_2D
). The second parameter is the mipmap level which we keep to 0
. The third parameter is the internal format. We can determine this by looking at the image properties. The fourth and fifth parameters store the texture width and height respectively. The sixth parameter is 0
for no border and 1
for border. The seventh parameter is the image format. The eighth parameter is the type of the image data pointer, and the final parameter is the pointer to the raw image data. After this function, we can safely release the image data allocated by SOIL
by calling SOIL_free_image_data(pData)
.
The fragment shader has the texture coordinates smoothly interpolated from the vertex shader stage through the rasterizer. The image that we loaded using SOIL
is passed to a texture sampler (uniform sampler2D textureMap
) which is then sampled using the input texture coordinates (vFragColor = texture(textureMap, vUV)
). So in the end, we get the image displayed on the screen.
The OnShutdown()
function is similar to the earlier recipes. In addition, this code adds deletion of the OpenGL texture object. The rendering code first clears the color and depth buffers. Next, it binds the shader program and then invokes the glDrawElement
call to render the triangles. Finally the shader is unbound and then the glutSwapBuffers
function is called to display the current back buffer as the next front buffer. Compiling and running this code displays the image in a window as shown in the following screenshot:
Chapter1/ImageLoader
directory.
Let us now implement the image loader by following these steps:
- Load the image using the
SOIL
library. Since the loaded image fromSOIL
is inverted vertically, we flip the image on the Y axis.int texture_width = 0, texture_height = 0, channels=0; GLubyte* pData = SOIL_load_image(filename.c_str(), &texture_width, &texture_height, &channels, SOIL_LOAD_AUTO); if(pData == NULL) { cerr<<"Cannot load image: "<<filename.c_str()<<endl; exit(EXIT_FAILURE); } int i,j; for( j = 0; j*2 < texture_height; ++j ) { int index1 = j * texture_width * channels; int index2 = (texture_height - 1 - j) * texture_width * channels; for( i = texture_width * channels; i > 0; --i ) { GLubyte temp = pData[index1]; pData[index1] = pData[index2]; pData[index2] = temp; ++index1; ++index2; } }
- Set up the OpenGL texture object and free the data allocated by the
SOIL
library.glGenTextures(1, &textureID); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textureID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture_width, texture_height, 0, GL_RGB, GL_UNSIGNED_BYTE, pData); SOIL_free_image_data(pData);
- Set up the vertex shader to output the clip space position (
shaders/shader.vert
).#version 330 core layout(location=0) in vec2 vVertex; smooth out vec2 vUV; void main() { gl_Position = vec4(vVertex*2.0-1,0,1); vUV = vVertex; }
- Set up the fragment shader that samples our image texture (
shaders/shader.frag
).#version 330 core layout (location=0) out vec4 vFragColor; smooth in vec2 vUV; uniform sampler2D textureMap; void main() { vFragColor = texture(textureMap, vUV); }
- Set up the application code using the
GLSLShader
shader class.shader.LoadFromFile(GL_VERTEX_SHADER, "shaders/shader.vert"); shader.LoadFromFile(GL_FRAGMENT_SHADER,"shaders/shader.frag"); shader.CreateAndLinkProgram(); shader.Use(); shader.AddAttribute("vVertex"); shader.AddUniform("textureMap"); glUniform1i(shader("textureMap"), 0); shader.UnUse();
- Set up the geometry and topology and pass data to the GPU using buffer objects.
vertices[0] = glm::vec2(0.0,0.0); vertices[1] = glm::vec2(1.0,0.0); vertices[2] = glm::vec2(1.0,1.0); vertices[3] = glm::vec2(0.0,1.0); GLushort* id=&indices[0]; *id++ =0; *id++ =1; *id++ =2; *id++ =0; *id++ =2; *id++ =3; 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"], 2, GL_FLOAT, GL_FALSE,0,0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices[0], GL_STATIC_DRAW);
- Set the shader and render the geometry.
void OnRender() { glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); shader.Use(); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); shader.UnUse(); glutSwapBuffers(); }
- Release the allocated resources.
void OnShutdown() { shader.DeleteShaderProgram(); glDeleteBuffers(1, &vboVerticesID); glDeleteBuffers(1, &vboIndicesID); glDeleteVertexArrays(1, &vaoID); glDeleteTextures(1, &textureID); }
The first parameter is the image file name. The next three parameters return the texture width, texture height, and total color channels in the image. These are used when generating the OpenGL texture object. The final parameter is the flag which is used to control further processing on the image. For this simple example, we will use the SOIL_LOAD_AUTO
flag which keeps all of the loading settings set to default. If the function succeeds, it returns unsigned char*
to the image data. If it fails, the return value is NULL (0)
. Since the image data loaded by SOIL
is vertically flipped, we then use two nested loops to flip the image data on the Y axis.
After the image data is loaded, we generate an OpenGL texture object and pass this data to the texture memory.
The glTexImage2D
function is where the actual allocation of the texture object takes place. The first parameter is the texture target (in our case this is GL_TEXTURE_2D
). The second parameter is the mipmap level which we keep to 0
. The third parameter is the internal format. We can determine this by looking at the image properties. The fourth and fifth parameters store the texture width and height respectively. The sixth parameter is 0
for no border and 1
for border. The seventh parameter is the image format. The eighth parameter is the type of the image data pointer, and the final parameter is the pointer to the raw image data. After this function, we can safely release the image data allocated by SOIL
by calling SOIL_free_image_data(pData)
.
The fragment shader has the texture coordinates smoothly interpolated from the vertex shader stage through the rasterizer. The image that we loaded using SOIL
is passed to a texture sampler (uniform sampler2D textureMap
) which is then sampled using the input texture coordinates (vFragColor = texture(textureMap, vUV)
). So in the end, we get the image displayed on the screen.
The OnShutdown()
function is similar to the earlier recipes. In addition, this code adds deletion of the OpenGL texture object. The rendering code first clears the color and depth buffers. Next, it binds the shader program and then invokes the glDrawElement
call to render the triangles. Finally the shader is unbound and then the glutSwapBuffers
function is called to display the current back buffer as the next front buffer. Compiling and running this code displays the image in a window as shown in the following screenshot:
- image using the
SOIL
library. Since the loaded image fromSOIL
is inverted vertically, we flip the image on the Y axis.int texture_width = 0, texture_height = 0, channels=0; GLubyte* pData = SOIL_load_image(filename.c_str(), &texture_width, &texture_height, &channels, SOIL_LOAD_AUTO); if(pData == NULL) { cerr<<"Cannot load image: "<<filename.c_str()<<endl; exit(EXIT_FAILURE); } int i,j; for( j = 0; j*2 < texture_height; ++j ) { int index1 = j * texture_width * channels; int index2 = (texture_height - 1 - j) * texture_width * channels; for( i = texture_width * channels; i > 0; --i ) { GLubyte temp = pData[index1]; pData[index1] = pData[index2]; pData[index2] = temp; ++index1; ++index2; } }
- Set up the OpenGL texture object and free the data allocated by the
SOIL
library.glGenTextures(1, &textureID); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, textureID); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture_width, texture_height, 0, GL_RGB, GL_UNSIGNED_BYTE, pData); SOIL_free_image_data(pData);
- Set up the vertex shader to output the clip space position (
shaders/shader.vert
).#version 330 core layout(location=0) in vec2 vVertex; smooth out vec2 vUV; void main() { gl_Position = vec4(vVertex*2.0-1,0,1); vUV = vVertex; }
- Set up the fragment shader that samples our image texture (
shaders/shader.frag
).#version 330 core layout (location=0) out vec4 vFragColor; smooth in vec2 vUV; uniform sampler2D textureMap; void main() { vFragColor = texture(textureMap, vUV); }
- Set up the application code using the
GLSLShader
shader class.shader.LoadFromFile(GL_VERTEX_SHADER, "shaders/shader.vert"); shader.LoadFromFile(GL_FRAGMENT_SHADER,"shaders/shader.frag"); shader.CreateAndLinkProgram(); shader.Use(); shader.AddAttribute("vVertex"); shader.AddUniform("textureMap"); glUniform1i(shader("textureMap"), 0); shader.UnUse();
- Set up the geometry and topology and pass data to the GPU using buffer objects.
vertices[0] = glm::vec2(0.0,0.0); vertices[1] = glm::vec2(1.0,0.0); vertices[2] = glm::vec2(1.0,1.0); vertices[3] = glm::vec2(0.0,1.0); GLushort* id=&indices[0]; *id++ =0; *id++ =1; *id++ =2; *id++ =0; *id++ =2; *id++ =3; 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"], 2, GL_FLOAT, GL_FALSE,0,0); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), &indices[0], GL_STATIC_DRAW);
- Set the shader and render the geometry.
void OnRender() { glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT); shader.Use(); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0); shader.UnUse(); glutSwapBuffers(); }
- Release the allocated resources.
void OnShutdown() { shader.DeleteShaderProgram(); glDeleteBuffers(1, &vboVerticesID); glDeleteBuffers(1, &vboIndicesID); glDeleteVertexArrays(1, &vaoID); glDeleteTextures(1, &textureID); }
The first parameter is the image file name. The next three parameters return the texture width, texture height, and total color channels in the image. These are used when generating the OpenGL texture object. The final parameter is the flag which is used to control further processing on the image. For this simple example, we will use the SOIL_LOAD_AUTO
flag which keeps all of the loading settings set to default. If the function succeeds, it returns unsigned char*
to the image data. If it fails, the return value is NULL (0)
. Since the image data loaded by SOIL
is vertically flipped, we then use two nested loops to flip the image data on the Y axis.
After the image data is loaded, we generate an OpenGL texture object and pass this data to the texture memory.
The glTexImage2D
function is where the actual allocation of the texture object takes place. The first parameter is the texture target (in our case this is GL_TEXTURE_2D
). The second parameter is the mipmap level which we keep to 0
. The third parameter is the internal format. We can determine this by looking at the image properties. The fourth and fifth parameters store the texture width and height respectively. The sixth parameter is 0
for no border and 1
for border. The seventh parameter is the image format. The eighth parameter is the type of the image data pointer, and the final parameter is the pointer to the raw image data. After this function, we can safely release the image data allocated by SOIL
by calling SOIL_free_image_data(pData)
.
The fragment shader has the texture coordinates smoothly interpolated from the vertex shader stage through the rasterizer. The image that we loaded using SOIL
is passed to a texture sampler (uniform sampler2D textureMap
) which is then sampled using the input texture coordinates (vFragColor = texture(textureMap, vUV)
). So in the end, we get the image displayed on the screen.
The OnShutdown()
function is similar to the earlier recipes. In addition, this code adds deletion of the OpenGL texture object. The rendering code first clears the color and depth buffers. Next, it binds the shader program and then invokes the glDrawElement
call to render the triangles. Finally the shader is unbound and then the glutSwapBuffers
function is called to display the current back buffer as the next front buffer. Compiling and running this code displays the image in a window as shown in the following screenshot:
SOIL
library provides a lot of functions but for now we are only interested in the SOIL_load_image
function.
processing on the image. For this simple example, we will use the SOIL_LOAD_AUTO
flag which keeps all of the loading settings set to default. If the function succeeds, it returns unsigned char*
to the image data. If it fails, the return value is NULL (0)
. Since the image data loaded by SOIL
is vertically flipped, we then use two nested loops to flip the image data on the Y axis.
After the image data is loaded, we generate an OpenGL texture object and pass this data to the texture memory.
The glTexImage2D
function is where the actual allocation of the texture object takes place. The first parameter is the texture target (in our case this is GL_TEXTURE_2D
). The second parameter is the mipmap level which we keep to 0
. The third parameter is the internal format. We can determine this by looking at the image properties. The fourth and fifth parameters store the texture width and height respectively. The sixth parameter is 0
for no border and 1
for border. The seventh parameter is the image format. The eighth parameter is the type of the image data pointer, and the final parameter is the pointer to the raw image data. After this function, we can safely release the image data allocated by SOIL
by calling SOIL_free_image_data(pData)
.
The fragment shader has the texture coordinates smoothly interpolated from the vertex shader stage through the rasterizer. The image that we loaded using SOIL
is passed to a texture sampler (uniform sampler2D textureMap
) which is then sampled using the input texture coordinates (vFragColor = texture(textureMap, vUV)
). So in the end, we get the image displayed on the screen.
The OnShutdown()
function is similar to the earlier recipes. In addition, this code adds deletion of the OpenGL texture object. The rendering code first clears the color and depth buffers. Next, it binds the shader program and then invokes the glDrawElement
call to render the triangles. Finally the shader is unbound and then the glutSwapBuffers
function is called to display the current back buffer as the next front buffer. Compiling and running this code displays the image in a window as shown in the following screenshot:
vVertex
) by simple arithmetic. Using the vertex positions, it also generates the texture coordinates (vUV
) for sampling of the texture in the fragment shader.
The OnShutdown()
function is similar to the earlier recipes. In addition, this code adds deletion of the OpenGL texture object. The rendering code first clears the color and depth buffers. Next, it binds the shader program and then invokes the glDrawElement
call to render the triangles. Finally the shader is unbound and then the glutSwapBuffers
function is called to display the current back buffer as the next front buffer. Compiling and running this code displays the image in a window as shown in the following screenshot: