-
Book Overview & Buying
-
Table Of Contents
-
Feedback & Rating

3D Graphics Rendering Cookbook
By :

Every 3D graphics application needs some sort of math utility functions, such as basic linear algebra or computational geometry. This book uses the OpenGL Mathematics (GLM) header-only C++ mathematics library for graphics, which is based on the GLSL specification. The official documentation (https://glm.g-truc.net) describes GLM as follows:
GLM provides classes and functions designed and implemented with the same naming conventions and functionalities as GLSL so that anyone who knows GLSL, can use GLM as well as C++.
Get the latest version of GLM using the Bootstrap script. We use version 0.9.9.8:
{ "name": "glm", "source": { "type": "git", "url": "https://github.com/g-truc/glm.git", "revision": "0.9.9.8" } }
Let's make use of some linear algebra and create a more complicated 3D graphics example. There are no lonely triangles this time. The full source code for this recipe can be found in Chapter2/02_GLM
.
Let's augment the example from the previous recipe using a simple animation and a 3D cube. The model and projection matrices can be calculated inside the main loop based on the window aspect ratio, as follows:
(1, 1, 1)
axis, and the angle of rotation is based on the current system time returned by glfwGetTime()
:const float ratio = width / (float)height; const mat4 m = glm::rotate( glm::translate(mat4(1.0f), vec3(0.0f, 0.0f, -3.5f)), (float)glfwGetTime(), vec3(1.0f, 1.0f, 1.0f)); const mat4 p = glm::perspective( 45.0f, ratio, 0.1f, 1000.0f);
struct PerFrameData { mat4 mvp; int isWireframe; };
The first field, mvp
, will store the premultiplied model-view-projection matrix. The isWireframe
field will be used to set the color of the wireframe rendering to make the example more interesting.
const GLsizeiptr kBufferSize = sizeof(PerFrameData); GLuint perFrameDataBuf; glCreateBuffers(1, &perFrameDataBuf); glNamedBufferStorage(perFrameDataBuf, kBufferSize, nullptr, GL_DYNAMIC_STORAGE_BIT); glBindBufferRange(GL_UNIFORM_BUFFER, 0, perFrameDataBuf, 0, kBufferSize);
The GL_DYNAMIC_STORAGE_BIT
parameter tells the OpenGL implementation that the content of the data store might be updated after creation through calls to glBufferSubData()
. The glBindBufferRange()
function binds a range within a buffer object to an indexed buffer target. The buffer is bound to the indexed target of 0
. This value should be used in the shader code to read data from the buffer.
glEnable(GL_DEPTH_TEST); glEnable(GL_POLYGON_OFFSET_LINE); glPolygonOffset(-1.0f, -1.0f);
Polygon offset is needed to render a wireframe image of the cube on top of the solid image without Z-fighting. The values of -1.0
will move the wireframe rendering slightly toward the camera.
Let's write the GLSL shaders that are needed for this recipe:
PerFrameData
input structure in the following vertex shader reflects the PerFrameData
structure in the C++ code that was written earlier:static const char* shaderCodeVertex = R"( #version 460 core layout(std140, binding = 0) uniform PerFrameData { uniform mat4 MVP; uniform int isWireframe; }; layout (location=0) out vec3 color;
8
vertices among all the 6
adjacent faces of the cube:const vec3 pos[8] = vec3[8]( vec3(-1.0,-1.0, 1.0), vec3( 1.0,-1.0, 1.0), vec3(1.0, 1.0, 1.0), vec3(-1.0, 1.0, 1.0), vec3(-1.0,-1.0,-1.0), vec3(1.0,-1.0,-1.0), vec3( 1.0, 1.0,-1.0), vec3(-1.0, 1.0,-1.0) ); const vec3 col[8] = vec3[8]( vec3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0), vec3(1.0, 1.0, 0.0), vec3(1.0, 1.0, 0.0), vec3(0.0, 0.0, 1.0), vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0) );
const int indices[36] = int[36]( // front 0, 1, 2, 2, 3, 0, // right 1, 5, 6, 6, 2, 1, // back 7, 6, 5, 5, 4, 7, // left 4, 0, 3, 3, 7, 4, // bottom 4, 5, 1, 1, 0, 4, // top 3, 2, 6, 6, 7, 3 );
main()
function of the vertex shader looks similar to the following code block. The gl_VertexID
input variable is used to retrieve an index from indices[]
, which is used to get corresponding values for the position and color. If we are rendering a wireframe pass, set the vertex color to black:void main() { int idx = indices[gl_VertexID]; gl_Position = MVP * vec4(pos[idx], 1.0); color = isWireframe > 0 ? vec3(0.0) : col[idx]; } )";
static const char* shaderCodeFragment = R"( #version 460 core layout (location=0) in vec3 color; layout (location=0) out vec4 out_FragColor; void main() { out_FragColor = vec4(color, 1.0); }; )";
The only thing we are missing now is how we update the uniform buffer and submit actual draw calls. We update the buffer twice per frame, that is, once per each draw call:
GL_FILL
:PerFrameData perFrameData = { .mvp = p * m, .isWireframe = false }; glNamedBufferSubData( perFrameDataBuf, 0, kBufferSize, &perFrameData); glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); glDrawArrays(GL_TRIANGLES, 0, 36);
GL_LINE
polygon mode and the -1.0
polygon offset that we set up earlier with glPolygonOffset()
:perFrameData.isWireframe = true; glNamedBufferSubData( perFrameDataBuf, 0, kBufferSize, &perFrameData); glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); glDrawArrays(GL_TRIANGLES, 0, 36);
The resulting image should look similar to the following screenshot:
Figure 2.2 – The rotating 3D cube with wireframe contours
As you might have noticed in the preceding code, the glBindBufferRange()
function takes an offset into the buffer as one of its input parameters. That means we can make the buffer twice as large and store two different copies of PerFrameData
in it. One with isWireframe
set to true
and another one set to false
. Then, we can update the entire buffer with just one call to glNamedBufferSubData()
, instead of updating the buffer twice, and use the offset parameter of glBindBufferRange()
to feed the correct instance of PerFrameData
into the shader. This is the correct and most attractive approach, too.
The reason we decided not to use it in this recipe is that the OpenGL implementation might impose alignment restrictions on the value of offset
. For example, many implementations require offset
to be a multiple of 256
. Then, the actual required alignment can be queued as follows:
GLint alignment; glGetIntegerv( GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT, &alignment);
The alignment requirement would make the simple and straightforward code of this recipe more complicated and difficult to follow without providing any meaningful performance improvements. In more complicated real-world use cases, particularly as the number of different values in the buffer goes up, this approach becomes more useful.