3D Graphics in C++
This topic describes the "3D Graphics in C++" sample application implementation.
Related Info
Samples
This tutorial describes how to use OpenGL® ES 2.0 to implement an interactive 3D animation in C++ as a Native Client (NaCl) embed
object. The sample application renders a rotating, textured cube, whose rotation can be controlled by clicking and dragging the mouse.
When the cube is clicked, automatic cube rotation stops. The user can drag the cube to rotate it; mouse movement is correlated to changes in the rotation around the X and Z axes, indirectly changing the rotation matrix values. Clicking the cube again resumes automatic rotation.
For information on how to access the sample application cheat sheet and run the application, see Sample-based Tutorials.
Initializing the Instance and Context
To implement 3D graphics with OpenGL® ES 2.0, you must initialize the instance, the rendering context, and the rendering pipeline:
-
In the class constructor, initialize the class members:
callback_factory_
simplifies the creation of completion callbacks used to implement the main drawing loop.context_
is app::Graphics3D
object.width_
,height_
, andscale_
define the size and scale of the viewport.- Various OpenGL handles, such as for shaders, program, and attribute locations.
- Mouse state variables store the mouse pointer location and button state.
x_angle_
andy_angle_
describe the current rotation state of the rendered cube.
-
Enable mouse input events and initialize the
Logger
class:RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE); Logger::InitializeInstance(this);
-
The
DidChangeView()
function is called whenever a viewport is created or changed, and it provides information about the viewport, such as its size and location.
In theDidChangeView()
function, retrieve the device scale and size, initialize OpenGL if necessary, and create the rendering context. Compile and link the shaders to the graphic program, initialize the buffers, and start the rendering loop. If the context was already initialized, resize the buffers to fit the viewport.void Graphics3DCubeInstance::DidChangeView(const pp::View& view) { // Pepper specifies dimensions in DIPs (device-independent pixels) // To generate a context that is at device-pixel resolution on HiDPI devices, // scale the dimensions using view.GetDeviceScale() device_scale_ = view.GetDeviceScale(); int32_t new_width = view.GetRect().width() * device_scale_; int32_t new_height = view.GetRect().height() * device_scale_; if (context_.is_null()) { if (!InitGL(new_width, new_height)) { Logger::Error("Couldn't initialize GLES library!"); return; } // Prepare the pipeline and start drawing InitShaders(); InitBuffers(); InitTexture(); MainLoopIteration(0); } else { // Resize the buffers to the new size of the module int32_t result = context_.ResizeBuffers(new_width, new_height); if (result < 0) { Logger::Error("Unable to resize buffers to %d x %d!", new_width, new_height); return; } } width_ = new_width; height_ = new_height; glViewport(0, 0, width_, height_); Logger::Log("Initialized module's view with resolution %dx%d", width_, height_); }
-
In the
InitGL()
function, create thepp::Graphics3D
rendering context:- Initialize the OpenGL® ES library using the
glInitializePPAPI()
function. - Create the
attrib_list
array with the viewport properties, such as the alpha size (in bits), depth buffer size, and viewport dimensions. - Pass the viewport properties to the
pp:Graphics3D
context constructor. - Bind the graphics context to the application viewport using the
BindGraphics()
function. If viewport binding is successful, the new graphics context is set to active and theInitGL()
function returnstrue
.
bool Graphics3DCubeInstance::InitGL(int32_t new_width, int32_t new_height) { // Initialize the OpenGL ES library and its connection with this NaCl module // This must be called once before making any GL calls // For more information, see ppapi/lib/gl/gles2/gl2ext_ppapi.h if (!glInitializePPAPI(pp::Module::Get()->get_browser_interface())) { Logger::Error("Unable to initialize GLES PPAPI!"); return false; } const int32_t attrib_list[] = { PP_GRAPHICS3DATTRIB_ALPHA_SIZE, 8, PP_GRAPHICS3DATTRIB_DEPTH_SIZE, 24, PP_GRAPHICS3DATTRIB_WIDTH, new_width, PP_GRAPHICS3DATTRIB_HEIGHT, new_height, PP_GRAPHICS3DATTRIB_NONE }; // Create a 3D graphics context and bind it to this instance context_ = pp::Graphics3D(this, attrib_list); if (!BindGraphics(context_)) { Logger::Error("Unable to bind 3D context!"); context_ = pp::Graphics3D(); glSetCurrentContextPPAPI(0); return false; } // Set the rendering context as current glSetCurrentContextPPAPI(context_.pp_resource()); Logger::Log("Initialized GLES library."); return true; }
- Initialize the OpenGL® ES library using the
-
In the
InitShaders()
function, compile the fragment and vertex shaders, link them to the graphics program, and save the attribute and uniform locations:void Graphics3DCubeInstance::InitShaders() { frag_shader_ = CompileShader(GL_FRAGMENT_SHADER, kFragShaderSource); if (!frag_shader_) return; vertex_shader_ = CompileShader(GL_VERTEX_SHADER, kVertexShaderSource); if (!vertex_shader_) return; program_ = LinkProgram(frag_shader_, vertex_shader_); if (!program_) return; // Save uniform and attribute locations for future use texture_loc_ = glGetUniformLocation(program_, "u_texture"); position_loc_ = glGetAttribLocation(program_, "a_position"); texcoord_loc_ = glGetAttribLocation(program_, "a_texcoord"); color_loc_ = glGetAttribLocation(program_, "a_color"); mvp_loc_ = glGetUniformLocation(program_, "u_mvp"); }
-
The basic shaders in the sample application convert the cube coordinates into world coordinates and paint the cube with the provided textures.
To compile a shader, allocate the needed program memory on the GPU, and load and compile the shader source code. On success, return the shader handle.GLuint CompileShader(GLenum type, const char* data) { GLuint shader = glCreateShader(type); glShaderSource(shader, 1, &data, NULL); glCompileShader(shader); GLint compile_status; glGetShaderiv(shader, GL_COMPILE_STATUS, &compile_status); if (compile_status != GL_TRUE) { // Shader failed to compile; show the error message char buffer[1024]; GLsizei length; glGetShaderInfoLog(shader, sizeof(buffer), &length, &buffer[0]); Logger::Error("Shader failed to compile: %s", buffer); return 0; } return shader; }
-
To link the shaders to the graphics program, allocate space for the program in the GPU, attach the shaders to it, and call the
glLinkProgram()
function to initiate the link. On success, return the program handle.GLuint LinkProgram(GLuint frag_shader, GLuint vert_shader) { GLuint program = glCreateProgram(); glAttachShader(program, frag_shader); glAttachShader(program, vert_shader); glLinkProgram(program); GLint link_status; glGetProgramiv(program, GL_LINK_STATUS, &link_status); if (link_status != GL_TRUE) { // Program failed to link; show the error message char buffer[1024]; GLsizei length; glGetProgramInfoLog(program, sizeof(buffer), &length, &buffer[0]); Logger::Error("Program failed to link: %s", buffer); return 0; } return program; }
-
-
In the
InitBuffers()
function, allocate the VBO (Vertex Buffer Object) and IBO (Index Buffer Object) for the cube. Load the cube vertex data (position, UV mapping, and color) into the VBO and populate the IBO with the correct triangle indices.void Graphics3DCubeInstance::InitBuffers() { glGenBuffers(1, &vertex_buffer_); glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_); glBufferData(GL_ARRAY_BUFFER, sizeof(kCubeVerts), &kCubeVerts[0], GL_STATIC_DRAW); glGenBuffers(1, &index_buffer_); glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer_); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(kCubeIndexes), &kCubeIndexes[0], GL_STATIC_DRAW); }
-
In the
InitTexture()
function, load the cube texture into the GPU memory. You can load the texture data from the project directory or a remote URL, or you can generate it procedurally. In the sample application, the texture data is loaded from the "texture.cc" file.void Graphics3DCubeInstance::InitTexture() { glGenTextures(1, &texture_); glBindTexture(GL_TEXTURE_2D, texture_); 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_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 128, 128, 0, GL_RGB, GL_UNSIGNED_BYTE, &kTextureData[0]); }
Implementing the Rendering Loop
The MainLoopIteration()
function represents a single iteration of the rendering loop. The Animate()
function updates the current X and Y rotation values for the cube in each frame. The Render()
function renders the graphic. At the end of the MainLoopIteration()
function, the SwapBuffers()
function is called on the context. It triggers a completion callback, which calls the MainLoopIteration()
function again.
void Graphics3DCubeInstance::MainLoopIteration(int32_t) {
Animate();
Render();
// Swap the background and foreground buffers
context_.SwapBuffers(
callback_factory_.NewCallback(&Graphics3DCubeInstance::MainLoopIteration));
}
Implement the main rendering routine in the Render()
function:
- Clear the current frame buffer, depth buffer and reset all drawing flags.
- Create arrays that contain transformation matrix data.
- Using the helper functions defined in the "matrix.h" file, populate the arrays with a rotation matrix based on the current cube rotation, a translation matrix to move the camera away from the cube, and a perspective matrix with the camera perspective settings.
- Multiply the rotation, translation, and perspective matrices to obtain a single matrix.
- Load the matrix to the uniform address in the GPU program.
- Bind the VBO to the context and load all attributes, such as position, color, and UV mapping) into GPU memory.
- Bind the IBO to the current context.
- Call the
glDrawElements()
function to draw the cube to the frame buffer.
void Graphics3DCubeInstance::Render() {
// Clear the current buffer
glClearColor(0.5, 0.5, 0.5, 1);
glClearDepthf(1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
// Set the graphics program to use
glUseProgram(program_);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texture_);
glUniform1i(texture_loc_, 0);
// Create the perspective matrix
float mvp[16];
float trs[16];
float rot[16];
identity_matrix(mvp);
const float aspect_ratio = static_cast<float>(width_) / height_;
glhPerspectivef2(&mvp[0], kFovY, aspect_ratio, kZNear, kZFar);
// Prepare the transformation matrix
translate_matrix(0, 0, kCameraZ, trs);
rotate_matrix(x_angle_, y_angle_, 0.0f, rot);
multiply_matrix(trs, rot, trs);
multiply_matrix(mvp, trs, mvp);
glUniformMatrix4fv(mvp_loc_, 1, GL_FALSE, mvp);
// Define the attributes of the vertex
// Each attribute has information on its specific data offset in the array
glBindBuffer(GL_ARRAY_BUFFER, vertex_buffer_);
glVertexAttribPointer(position_loc_, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<void*>(offsetof(Vertex, loc)));
glEnableVertexAttribArray(position_loc_);
glVertexAttribPointer(color_loc_, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<void*>(offsetof(Vertex, color)));
glEnableVertexAttribArray(color_loc_);
glVertexAttribPointer(texcoord_loc_, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex),
reinterpret_cast<void*>(offsetof(Vertex, tex)));
glEnableVertexAttribArray(texcoord_loc_);
// Bind the buffer containing the index drawing order and draw the bound elements
// to the background buffer
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer_);
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, 0);
}