top

Graphics 3D in C++

This tutorial performs a step by step decomposition of NaCl Graphics3D demo application in C++.

Introduction

This article will discuss the structure of the NaCl application written in C++ programming language that handles drawing a 3D graphics using OpenGL ES 2.0. The source code can be found in Tizen Studio or you can click here. The downloaded zip file can be imported into Tizen Studio or extracted and compiled using a separately downloaded NaCl toolchain. Build instructions are available in the demo package.

Note

Please note that you need at least pepper_42 toolchain version to compile this sample application.

We recommend to go through this tutorial along with analyzing the downloaded source code. This analysis will only focus on 3D graphics related code of the NaCl application, most of the web part (the .html and .js code) will be omitted because it’s trivial and almost identical for every demo. This article won’t teach you how to use OpenGL in general. It’s recommended to be familiar with Hello World in C++ and Input events in C++ tutorials. This tutorial assumes that you possess a basic knowledge of OpenGL.

There is also a cheat sheet for this demo available in Tizen Studio that explains the application functionality by highlighting most important code fragments. Cheat sheets are available from the Help menu.



The application is a simple 3D graphics example. It draws a 3D animated and textured cube in the NaCl embed element object on the web page. This cube can be rotated using mouse.

Code Analysis

In this article we will focus on specific functionalities you need to implement in the application to enable drawing in 3D.

In order to draw 3D graphics, an application has to:

  • initialize the instance,
  • initialize the Graphics3D context,
  • initialize the OpenGL ES 2.0 pipeline,
  • create the main loop routine.

Before implementing any function, we should take a look at Graphics3DCubeInstance class member variables definition. These are as follows:

  • callback_factory_ - used for simplifying the creation of CompletionCallback objects which help to establish main drawing loop,
  • context_- a pp::Graphics3D object,
  • width_, height_, scale_ - dimensions of the viewport,
  • set of OpenGL handles - specific OpenGL handles for shaders, program, attribute location, etc.,
  • mouse state variables - variables that describe mouse pointer location, and button state,
  • x_angle_, y_angle_ - current rotation state of the displayed cube.

Initialization

The initialization of the instance happens in the instance constructor where it initializes all members of the class and callback_factory_. Additionally it initializes mouse input events and a Logger that helps in logging information on the webpage.

RequestInputEvents(PP_INPUTEVENT_CLASS_MOUSE);
 Logger::InitializeInstance(this);

After the constructor finishes our Instance is initialized, but not the context, nor OpenGL program is ready.

Next, the DidChangeView()method is called, as always when the viewport is modified. Updated viewport dimensions are stored in the instance for future usage. If necessary, initialization of OpenGL takes place and the corresponding rendering context is created. Afterwards, shaders are compiled and linked to the graphics program, buffers are initialized and the main rendering loop starts. If the context is already initialized, then current buffers are resized to fit the viewport. The code looks as follows:

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 by 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;
    }
    // Everything is ok, prepare 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_);
}

The initialization of the graphics context is done inside the InitGL()function. Once the context is ready the GPU program is compiled and linked inside the InitShaders() function and InitBuffers() is called, which creates necessary vertex buffer objects. We also call the InitTexture() method to load the cube texture into the GPU. Now aforementioned functions will be described in greater detail.

Context Initialization

The InitGL() function creates a pp::Graphics3D context for rendering. Its implementation is given below:

bool Graphics3DCubeInstance::InitGL(int32_t new_width, int32_t new_height) {
  // Initialize OpenGL ES library and its connection with this NaCl module.
  // This must be called once before making any gl calls.
  // @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 with this <code>Instance</code>.
  context_ = pp::Graphics3D(this, attrib_list);
  if (!BindGraphics(context_)) {
    Logger::Error("Unable to bind 3D context!");
    context_ = pp::Graphics3D();
    glSetCurrentContextPPAPI(0);
    return false;
  }

  // Set context to be used for rendering.
  glSetCurrentContextPPAPI(context_.pp_resource());
  Logger::Log("Initialized GLES library.");
  return true;
}

The first step in the function is the initialization of OpenGL ES library by the glInitializePPAPI() function. Afterwards, the attrib_list array is created with viewport properties like alpha size (in bits), size of depth buffer, width and height of the viewport, etc. These attributes are then passed to the pp:Graphics3Dcontext constructor. The next step involves binding graphics context to the application viewport which is achieved by the BindGraphics() method. If the viewport binding was successful, then a newly created graphics context is set active and the InitGL() function returns true.

OpenGL Shader Initialization

OpenGL resources initialization happens in three major steps.

Shader Compilation

The first step is compilation of the OpenGL GPU program. This is done inside the InitShaders() function:

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 uniforms and attributes location for future usage.
  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");
}

As you can see above, CompileShader() function is called for both fragment and vertex shaders and the graphics program is linked which makes it ready to use.

When it succeeds, the location of all important attributes and uniforms is stored by calling glGetUniformLocation()and glGetAttribLocation().

TheCompileShader()function, responsible for compiling a shader is implemented as shown below:

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, let's see what the error is.
    char buffer[1024];
    GLsizei length;
    glGetShaderInfoLog(shader, sizeof(buffer), &length, &buffer[0]);
    Logger::Error("Shader failed to compile: %s", buffer);
    return 0;
  }

  return shader;
}

It allocates necessary program memory on GPU, loads the shader source code and compiles it. On success, the shader handle is returned. Shaders provided in the demo are the most basic shaders in OpenGL, they are responsible for processing the coordinates of the cube into the world coordinates and then painting the cube with the provided texture.

Shader Linking

After compiling the shaders it’s time to link them into executable graphics program. This is achieved by the LinkProgram()function which is presented below:

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, let's see what the error is.
    char buffer[1024];
    GLsizei length;
    glGetProgramInfoLog(program, sizeof(buffer), &length, &buffer[0]);
    Logger::Error("Program failed to link: %s", buffer);
    return 0;
  }

  return program;
}

It allocates space for the program in the GPU, then attaches both previously compiled shaders to it and then calls the glLinkProgram() function which performs the linking process. Next, if there were no linking errors, the program handle is returned and the program itself is ready to be executed.

Buffer Initialisation

The next step for the initialization of the OpenGL resources is allocation of VBO (Vertex Buffer Object) and IBO (Index Buffer Object) for the cube. This is done by the InitBuffers() function:

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);
}

It loads provided cube vertex data (position, UV mapping and color) into the VBO and populates IBO with the correct triangle indices.

Another straightforward step is loading of the cube texture in to the GPU memory:

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]);
}

The texture data can be obtained from the project directory or remote URL or even generated procedurally. In this demo it’s loaded from the file texture.cc.

The implementation of the main drawing routine is described below.

Main Rendering Loop

The main loop of the program is based on the callback mechanism. The Animate() function updates current x and y rotation values for the cube in each frame. The Render() function performs rendering routine actions. Finally, the SwapBuffers() method is called on the context. Upon finishing it runs the provided completion callback, which restarts the loop iteration. The code is given below:

void Graphics3DCubeInstance::MainLoopIteration(int32_t) {
  Animate();
  Render();
  // Swap the background buffer with the foreground buffer.
  context_.SwapBuffers(
      callback_factory_.NewCallback(&Graphics3DCubeInstance::MainLoopIteration));
}

The Render() function is most important, as it implements the main rendering routine:

void Graphics3DCubeInstance::Render() {
  // Clear 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 what program to use.
  glUseProgram(program_);
  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, texture_);
  glUniform1i(texture_loc_, 0);

  // Create our 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 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 it's specific data offset in the array,
  // to be able to iterate over it.
  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 buffer containing indices drawing order and draw all bound elements
  // to the background buffer.
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, index_buffer_);
  glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_BYTE, 0);
}

The Render() function performs following steps:

  1. Clears current frame buffer, depth buffer and resets all drawing flags.
  2. Creates arrays that contain transformation matrix data.
  3. Populates these arrays with rotation matrix data according to current cube rotation, translation matrix data to move camera backwards from the cube and perspective matrix data with the camera settings for perspective implementation. These steps are performed using helper functions defined in matrix.h file.
  4. Above three matrices are multiplied together which results in one matrix containing all of the above transformations. Next, that matrix is loaded to the uniform address in the GPU program.
  5. Next the VBO is bound to the context and all attributes are loaded into the GPU memory (position, color, UV mapping).
  6. IBO is bound to the current context and the glDrawElements() function is called which results in cube being drawn to the framebuffer.

Above steps conclude the drawing routine of the Graphics3D demo.

Input

Mouse movement when the left mouse button is pressed directly correlates with changes to the rotation about X and Z axis, which indirectly changes values of the rotation matrix. On clicking, the automatic rotation is stopped in favor of the rotation provided by the mouse. Clicking on the screen restores automatic rotation.

Summary

This application can be compiled using Tizen Studio or manually with the make program. The application compiled manually can be run in Google Chrome. To run it on Smart TV Emulator you have to build it with Tizen Studio.

After a successful load you should be presented with a webpage that looks like the one below and displays nice interactive 3D cube animation.