In this recipe we will implement the Wavefront ® OBJ model. Instead of using separate buffer objects for storing positions, normals, and texture coordinates as in the previous recipe, we will use a single buffer object with interleaved data. This ensures that we have more chances of a cache hit since related attributes are stored next to each other in the buffer object memory.
Let us start the recipe by following these simple steps:
ObjLoader
object. Call the ObjLoader::Load
function, passing it the name of the OBJ file. Pass vectors to store the meshes, vertices, indices, and materials contained in the OBJ file.ObjLoader obj; if(!obj.Load(mesh_filename.c_str(), meshes, vertices, indices, materials)) { cout<<"Cannot load the 3ds mesh"<<endl; exit(EXIT_FAILURE); }
SOIL
library if the material has a texture map.for(size_t k=0;k<materials.size();k++) { if(materials[k]->map_Kd != "") { GLuint id = 0; glGenTextures(1, &id); glBindTexture(GL_TEXTURE_2D, id); 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_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); int texture_width = 0, texture_height = 0, channels=0; const string& filename = materials[k]->map_Kd; std::string full_filename = mesh_path; full_filename.append(filename); GLubyte* pData = SOIL_load_image(full_filename.c_str(), &texture_width, &texture_height, &channels, SOIL_LOAD_AUTO); if(pData == NULL) { cerr<<"Cannot load image: "<<full_filename.c_str()<<endl; exit(EXIT_FAILURE); } //… image flipping code GLenum format = GL_RGBA; switch(channels) { case 2: format = GL_RG32UI; break; case 3: format = GL_RGB; break; case 4: format = GL_RGBA; break; } glTexImage2D(GL_TEXTURE_2D, 0, format, texture_width, texture_height, 0, format, GL_UNSIGNED_BYTE, pData); SOIL_free_image_data(pData); textures.push_back(id); } }
glGenVertexArrays(1, &vaoID); glGenBuffers(1, &vboVerticesID); glGenBuffers(1, &vboIndicesID); glBindVertexArray(vaoID); glBindBuffer (GL_ARRAY_BUFFER, vboVerticesID); glBufferData (GL_ARRAY_BUFFER, sizeof(Vertex)*vertices.size(), &(vertices[0].pos.x), GL_STATIC_DRAW); glEnableVertexAttribArray(shader["vVertex"]); glVertexAttribPointer(shader["vVertex"], 3, GL_FLOAT, GL_FALSE,sizeof(Vertex),0); glEnableVertexAttribArray(shader["vNormal"]); glVertexAttribPointer(shader["vNormal"], 3, GL_FLOAT, GL_FALSE,sizeof(Vertex),(const GLvoid*)(offsetof( Vertex, normal)) ); glEnableVertexAttribArray(shader["vUV"]); glVertexAttribPointer(shader["vUV"], 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)(offsetof(Vertex, uv)) ); if(materials.size()==1) { glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID); glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort)*indices.size(), &(indices[0]), GL_STATIC_DRAW); }
MV
), projection (P
), normal matrices (N
) and light position, and so on.glBindVertexArray(vaoID); { shader.Use(); glUniformMatrix4fv(shader("MV"), 1, GL_FALSE, glm::value_ptr(MV)); glUniformMatrix3fv(shader("N"), 1, GL_FALSE, glm::value_ptr(glm::inverseTranspose(glm::mat3(MV)))); glUniformMatrix4fv(shader("P"), 1, GL_FALSE, glm::value_ptr(P)); glUniform3fv(shader("light_position"),1, &(lightPosOS.x));
GL_TEXTURE_2D
target if the material contains a texture map. Otherwise, use a default color for the mesh. Finally, call the glDrawElements
function to render the mesh/submesh.for(size_t i=0;i<materials.size();i++) { Material* pMat = materials[i]; if(pMat->map_Kd !="") { glUniform1f(shader("useDefault"), 0.0); GLint whichID[1]; glGetIntegerv(GL_TEXTURE_BINDING_2D, whichID); if(whichID[0] != textures[i]) glBindTexture(GL_TEXTURE_2D, textures[i]); } else glUniform1f(shader("useDefault"), 1.0); if(materials.size()==1) glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_SHORT, 0); else glDrawElements(GL_TRIANGLES, pMat->count, GL_UNSIGNED_SHORT, (const GLvoid*)(& indices[pMat->offset])); } shader.UnUse();
The main component of this recipe is the ObjLoader::Load
function defined in the Obj.cpp
file. The Wavefront® OBJ file is a text file which has different text descriptors for different mesh components. Usually, the mesh starts with the geometry definition, that is, vertices that begin with the letter v
followed by three floating point values. If there are normals, their definitions begin with vn
followed by three floating point values. If there are texture coordinates, their definitions begin with vt
, followed by two floating point values. Comments start with the #
character, so whenever a line with this character is encountered, it is ignored.
Following the geometry definition, the topology is defined. In this case, the line is prefixed with f
followed by the indices for the polygon vertices. In case of a triangle, three indices sections are given such that the vertex position indices are given first, followed by texture coordinates indices (if any), and finally the normal indices (if any). Note that the indices start from 1, not 0.
So, for example, say that we have a quad geometry having four position indices (1,2,3,4) having four texture coordinate indices (5,6,7,8), and four normal indices (1,1,1,1) then the topology would be stored as follows:
f 1/5/1 2/6/1 3/7/1 4/8/1
If the mesh is a triangular mesh with position vertices (1,2,3), texture coordinates (7,8,9), and normals (4,5,6) then the topology would be stored as follows:
f 1/7/4 2/8/5 3/9/6
Now, if the texture coordinates are omitted from the first example, then the topology would be stored as follows:
f 1//1 2//1 3//1 4//1
The OBJ file stores material information in a separate material (.mtl
) file. This file contains similar text descriptors that define different materials with their ambient, diffuse, and specular color values, texture maps, and so on. The details of the defined elements are given in the OBJ format specifications. The material file for the current OBJ file is declared using the mtllib
keyword followed by the name of the .mtl
file. Usually, the .mtl
file is stored in the same folder as the OBJ file. A polygon definition is preceded with a usemtl
keyword followed by the name of the material to use for the upcoming polygon definition. Several polygonal definitions can be grouped using the g
or o
prefix followed by the name of the group/object respectively.
The ObjLoader::Load
function first finds the current prefix. Then, the code branches to the appropriate section depending on the prefix. The suffix strings are then parsed and the extracted data is stored in the corresponding vectors. For efficiency, rather than storing the indices directly, we store them by material so that we can then sort and render the mesh by material. The associated material library file (.mtl
) is loaded using the ReadMaterialLibrary
function. Refer to the Obj.cpp
file for details.
The file parsing is the first piece of the puzzle. The second piece is the transfer of this data to the GPU memory. In this recipe, we use an interleaved buffer, that is, instead of storing each per-vertex attribute separately in its own vertex buffer object, we store them interleaved one after the other in a single buffer object. First positions are followed by normals and then texture coordinates. We achieve this by first defining our vertex format using a custom Vertex
struct. Our vertices
are a vector of this struct.
struct Vertex { glm::vec3 pos, normal; glm::vec2 uv; };
We generate the vertex array object and then the vertex buffer object. Next, we bind the buffer object passing it our vertices. In this case, we specify the stride of each attribute in the data stream separately as follows:
glBindBuffer (GL_ARRAY_BUFFER, vboVerticesID); glBufferData (GL_ARRAY_BUFFER, sizeof(Vertex)*vertices.size(), &(vertices[0].pos.x), GL_STATIC_DRAW); glEnableVertexAttribArray(shader["vVertex"]); glVertexAttribPointer(shader["vVertex"], 3, GL_FLOAT, GL_FALSE,sizeof(Vertex),0); glEnableVertexAttribArray(shader["vNormal"]); glVertexAttribPointer(shader["vNormal"], 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)(offsetof(Vertex, normal)) ); glEnableVertexAttribArray(shader["vUV"]); glVertexAttribPointer(shader["vUV"], 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)(offsetof(Vertex, uv)) );
If the mesh has a single material, we store the mesh indices into a GL_ELEMENT_ARRAY_BUFFER
target. Otherwise, we render the submeshes by material.
if(materials.size()==1) {
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vboIndicesID);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(GLushort) * indices.size(), &(indices[0]), GL_STATIC_DRAW);
}
At the time of rendering, if we have a single material, we render the whole mesh, otherwise we render the subset stored with the material.
if(materials.size()==1)
glDrawElements(GL_TRIANGLES,indices.size(),GL_UNSIGNED_SHORT,0);
else
glDrawElements(GL_TRIANGLES, pMat->count, GL_UNSIGNED_SHORT, (const GLvoid*)(&indices[pMat->offset]));
The demo application implementing this recipe shows a scene with three blocks on a planar quad. The camera view can be rotated with the left mouse button. The light source's position is shown by a 3D crosshair that can be moved by dragging the right mouse button. The output from this demo application is shown in the following figure:
You can see the OBJ file specification on Wikipedia at http://en.wikipedia.org/wiki/Wavefront_.obj_file.
3.135.219.166