Loading the first 3D model in the Wavefront Object (.obj) format

Now, we are ready to integrate a 3D object loader into our code. The first step is to create an empty class called ObjLoader along with the source (.cpp) and header (.h) files. This class handles all the functions related to 3D object loading, parsing, and drawing using the OpenGL and Assimp libraries. The headers of the class will include the Assimp core functions for the handling of the data structures and all I/O mechanisms of the 3D data format:

#include <cimport.h>
#include <scene.h>
#include <postprocess.h>

In the ObjLoader.h file, we provide interfaces for the main program to create, destroy, load, and display the 3D data. In the ObjLoader.cpp file, we implement a set of functions to parse the scene (a hierarchical representation of the 3D objects in terms of meshes and faces) using the built-in functions from Assimp.

The Assimp library can support various 3D model data formats; however, in our example, we will focus on the Wavefront Object (.obj) format due to its simplicity. The .obj file is a simple geometric definition file that was first developed by Wavefront Technologies. The file contains the core elements of graphics, such as vertex, vertex position, normal face and so on, and is stored in a simple text format. Since the files are stored in ASCII text, we can easily open and examine the files without any parsers. For example, the following is the .obj file of a front-facing square:

# This is a comment.
# Front facing square.
# vertices [x, y, z]
v 0 0 0   # Bottom left.
v 1 0 0   # Bottom right.
v 1 1 0   # Top    right.
v 0 1 0   # Top    left.
# List of faces: 
f 1 2 3 4       # Square.

As we can see from the preceding example, the representation is quite simple and intuitive for beginners. The vertices can be read and extracted one line at a time, and then they can be modified.

In the next section, we will show the full implementation, which allows users to load the .obj file, store the scene in a vertex buffer object, and display the scene.

How to do it...

First, we create the ObjLoader.h file in the common folder and append the class function definitions and variables that will be used in our implementation:

#ifndef OBJLOADER_H_
#define OBJLOADER_H_
/* Assimp include files. These three are usually needed. */
#include <cimport.h>
#include <scene.h>
#include <postprocess.h>
#include <common.h>
#define aisgl_min(x,y) (x<y?x:y)
#define aisgl_max(x,y) (y>x?y:x)
class ObjLoader {
  public:
  ObjLoader();
  virtual ~ObjLoader();
  int loadAsset(const char* path);
  void setScale(float scale);
  unsigned int getNumVertices();
  void draw(const GLenum draw_mode);
  void loadVertices(GLfloat *g_vertex_buffer_data);
private:
  //helper functions and variables
  const struct aiScene* scene;
  GLuint scene_list;
  aiVector3D scene_min, scene_max, scene_center;
  float g_scale;
  unsigned int num_vertices;
  unsigned int recursiveDrawing(const struct aiNode* nd, unsigned int v_count, const GLenum);
  unsigned int recursiveVertexLoading(const struct aiNode *nd, GLfloat *g_vertex_buffer_data, unsigned int v_counter);
  unsigned int recursiveGetNumVertices(const struct aiNode *nd);
  void get_bounding_box (aiVector3D* min, aiVector3D* max);
  void get_bounding_box_for_node (const struct aiNode* nd, aiVector3D* min, aiVector3D* max, aiMatrix4x4* trafo);
};
#endif

The names of classes from the Assimp library are preceded by the prefix ai- (for example, aiScene and aiVector3D). The ObjLoader file provides ways to dynamically load and draw the object loaded into the memory. It also handles simple dynamic scaling so that the object will fit on the screen.

In the source file, ObjLoader.cpp, we start by adding the constructor for the class:

#include <ObjLoader.h>
ObjLoader::ObjLoader() {
  g_scale=1.0f;
  scene = NULL; //empty scene
  scene_list = 0;
  num_vertices = 0;
}

Then, we implement the file-loading mechanism with the aiImportFile function. The scene is processed to extract the bounding box size for proper scaling to fit the screen. The number of vertices of the scene is then used to allow dynamic vertex buffer creation in later steps:

int ObjLoader::loadAsset(const char *path){
  scene = aiImportFile(path, aiProcessPreset_TargetRealtime_MaxQuality);
  if (scene) {
    get_bounding_box(&scene_min,&scene_max);
    scene_center.x = (scene_min.x + scene_max.x) / 2.0f;
    scene_center.y = (scene_min.y + scene_max.y) / 2.0f;
    scene_center.z = (scene_min.z + scene_max.z) / 2.0f;
    printf("Loaded file %s
", path);
    g_scale =4.0/(scene_max.x-scene_min.x);

    printf("Scaling: %lf", g_scale);
    num_vertices = recursiveGetNumVertices(scene->mRootNode);
    printf("This Scene has %d vertices.
", num_vertices);
    return 0;
  }
  return 1;
}

To extract the total number of vertices required to draw the scene, we recursively walk through every node in the tree hierarchy. The implementation requires a simple recursive function that returns the number of vertices in each node, and then the total is calculated based on the summation of all nodes upon the return of the function:

unsigned int ObjLoader::recursiveGetNumVertices(const struct aiNode *nd){
  unsigned int counter=0;
  unsigned int i;
  unsigned int n = 0, t;
  // draw all meshes assigned to this node
  for (; n < nd->mNumMeshes; ++n) {
    const struct aiMesh* mesh = scene-> mMeshes[nd->mMeshes[n]];
    for (t = 0; t < mesh->mNumFaces; ++t) {
      const struct aiFace* face = &mesh-> mFaces[t];
      counter+=3*face->mNumIndices;
    }
    printf("recursiveGetNumVertices: mNumFaces 	%d
", mesh->mNumFaces);
  }
  //traverse all children nodes
  for (n = 0; n < nd->mNumChildren; ++n) {
    counter+=recursiveGetNumVertices(nd-> mChildren[n]);
  }
  printf("recursiveGetNumVertices: counter %d
", counter);
  return counter;
}

Similarly, to calculate the size of the bounding box (that is, the minimum volume that is required to contain the scene), we recursively examine each node and extract the points that are farthest away from the center of the object:

void ObjLoader::get_bounding_box (aiVector3D* min, aiVector3D* max)
{
  aiMatrix4x4 trafo;
  aiIdentityMatrix4(&trafo);
  min->x = min->y = min->z =  1e10f;
  max->x = max->y = max->z = -1e10f;
  get_bounding_box_for_node(scene-> mRootNode,min,max,&trafo);
}
void ObjLoader::get_bounding_box_for_node (const struct aiNode* nd, aiVector3D* min, aiVector3D* max, aiMatrix4x4* trafo) 
{
  aiMatrix4x4 prev;
  unsigned int n = 0, t;
  prev = *trafo;
  aiMultiplyMatrix4(trafo,&nd->mTransformation);
  for (; n < nd->mNumMeshes; ++n) {
    const struct aiMesh* mesh = scene-> mMeshes[nd->mMeshes[n]];
    for (t = 0; t < mesh->mNumVertices; ++t) {
      aiVector3D tmp = mesh->mVertices[t];
      aiTransformVecByMatrix4(&tmp,trafo);
      min->x = aisgl_min(min->x,tmp.x);
      min->y = aisgl_min(min->y,tmp.y);
      min->z = aisgl_min(min->z,tmp.z);
      max->x = aisgl_max(max->x,tmp.x);
      max->y = aisgl_max(max->y,tmp.y);
      max->z = aisgl_max(max->z,tmp.z);
    }
  }
  for (n = 0; n < nd->mNumChildren; ++n) {
    get_bounding_box_for_node(nd-> mChildren[n],min,max,trafo);
  }
  *trafo = prev;
}

The resulting bounding box allows us to calculate the scaling factor and recenter the object coordinate to fit within the viewable screen.

In the main.cpp file, we integrate the code by first inserting the header file:

#include <ObjLoader.h>

Then, we create the ObjLoader object and load the model with the given filename in the main function:

ObjLoader *obj_loader = new ObjLoader();
int result = 0;
if(argc > 1){
  result = obj_loader->loadAsset(argv[1]);
}
else{
  result = obj_loader-> loadAsset("dragon.obj");
}
if(result){
  fprintf(stderr, "Final to Load the 3D file
");
  glfwTerminate();
  exit(EXIT_FAILURE);
}

The ObjLoader contains an algorithm that recursively examines each mesh and computes the bounding box and the number of vertices in the scene. Then, we dynamically allocate the vertex buffer based on the number of vertices and load the vertices into the buffer:

GLfloat *g_vertex_buffer_data = (GLfloat*) 
malloc (obj_loader->getNumVertices()*sizeof(GLfloat));
//load the scene data to the vertex buffer
obj_loader->loadVertices(g_vertex_buffer_data);

Now, we have all the necessary vertex information for display with our custom shader program written in OpenGL.

How it works...

Assimp provides the mechanism to load and parse the 3D data format efficiently. The key feature we utilized is the hierarchical way to import 3D objects, which allows us to unify our rendering pipeline regardless of the 3D format. The aiImportFile function reads the given file and returns its content in the aiScene structure. The second parameter of this function specifies the optional postprocessing steps to be executed after a successful import. The aiProcessPreset_TargetRealtime_MaxQuality flag is a predefined variable, which combines the following set of parameters:

( 
  aiProcessPreset_TargetRealtime_Quality | 
  aiProcess_FindInstances | 
  aiProcess_ValidateDataStructure | 
  aiProcess_OptimizeMeshes | 
  aiProcess_Debone | 
0 )

These postprocessing options are described in further detail at http://assimp.sourceforge.net/lib_html/postprocess_8h.html#a64795260b95f5a4b3f3dc1be4f52e410. Advanced users can look into each option and understand whether these functions need to be enabled or disabled based on the content.

At this point, we have a simple mechanism to load graphics into the Assimp aiScene object, present the bounding box size, as well as extract the number of vertices required to render the scene. Next, we will create a simple shader program as well as various drawing functions to visualize the content with different styles. In short, by integrating this with the OpenGL graphics rendering engine, we now have a flexible way to visualize 3D models using the various tools we developed in the previous chapters.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.19.30.232