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.

Figure 1. 3D graphics in C++ application

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:

  1. 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 a pp::Graphics3D object.
    • width_, height_, and scale_ 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_ and y_angle_ describe the current rotation state of the rendered cube.
  2. Enable mouse input events and initialize the Loggerclass:

    RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE);
    Logger::InitializeInstance(this);
    
  3. 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 the DidChangeView() 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_);
    }
    
  4. In the InitGL() function, create the pp::Graphics3D rendering context:

    1. Initialize the OpenGL® ES library using the glInitializePPAPI() function.
    2. Create the attrib_list array with the viewport properties, such as the alpha size (in bits), depth buffer size, and viewport dimensions.
    3. Pass the viewport properties to the pp:Graphics3D context constructor.
    4. 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 the InitGL() function returns true.
    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;
    }
    
  5. 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");
    }
    
    1. 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;
      }
      
    2. 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;
      }
      
  6. 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);
    }
    
  7. 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:

  1. Clear the current frame buffer, depth buffer and reset all drawing flags.
  2. Create arrays that contain transformation matrix data.
  3. 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.
  4. Multiply the rotation, translation, and perspective matrices to obtain a single matrix.
  5. Load the matrix to the uniform address in the GPU program.
  6. Bind the VBO to the context and load all attributes, such as position, color, and UV mapping) into GPU memory.
  7. Bind the IBO to the current context.
  8. 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);
}