2. Loading OBJ Files
Loading an OBJ file is fairly simple, but a lot
of text parsing needs to be done. In the OBJ Models demo we will load
the sample cube and its material.
Two files specify the OBJ loading code:
objLoader.h and objLoader.cpp. In the header file there are two classes,
one used to store a mesh and one used to store all meshes in the file.
The mesh class is called ObjMesh, and it stores all of the
vertices, normals, and texture coordinates that are ready to be used to
create a Direct3D 10 vertex buffer and the texture file name in a
string. This mesh class is fairly straightforward and only has functions
for accessing the member variables. The ObjMesh class is shown in Listing 3.
Listing 3. The ObjMesh Class
class ObjMesh
{
public:
ObjMesh()
{
m_vertices = NULL;
m_normals = NULL;
m_texCoords = NULL;
m_totalVerts = 0;
}
~ObjMesh()
{
Release()
}
void Release()
void SetVertices(float *verts) { m_vertices = verts; }
void SetNormals(float *normals) { m_normals = normals; }
void SetTexCoords(float *coords) { m_texCoords = coords; }
void SetTotalVerts(int total) { m_totalVerts = total; }
void SetName(string name) { m_name = name; }
void SetTextureName(string name) { m_decalFile = name; }
float *GetVertices() { return m_vertices; }
float *GetNormals() { return m_normals; }
float *GetTexCoords() { return m_texCoords; }
int GetTotalVerts() { return m_totalVerts; }
string GetName() { return m_name; }
string GetTextureName() { return m_decalFile; }
private:
float *m_vertices;
float *m_normals;
float *m_texCoords;
int m_totalVerts;
string m_name;
string m_decalFile;
};
|
The model list class is called ObjModel, and it stores a list of ObjMesh objects. The model class is also going to be used to load the OBJ file, which is done by calling the function LoadOBJ(). All of the other functions except for Release()
in the class are used to access individual mesh information such as
getting a specific mesh’s texture file string, getting a specific mesh’s
vertex list, and so on. The Release() function that you will
see in the class is used to free all allocated memory. The mesh list
itself is a list of pointers that are allocated during the loading of
the model file. The ObjModel class is shown in Listing 4.
The model list class is only used to load all of the information from
an OBJ file and have its data ready so that vertex buffers can be
created out of the OBJ files. Once those vertex buffers are created, the
model list class object would not be needed. Later in the main source
file you’ll see how this class is used to temporarily hold the geometric
information until the vertex buffers are created.
Listing 4. The ObjModel Class
class ObjModel
{
public:
ObjModel() { }
~ObjModel() { Release() }
bool LoadOBJ(char *fileName);
void Release()
ObjMesh *GetMeshByIndex(int index);
float *GetMeshVertices(int index);
float *GetMeshNormals(int index);
float *GetMeshTexCoords(int index);
int GetMeshTotalVerts(int index);
int GetMeshCount();
string GetMeshTextureFile(int index);
private:
vector<ObjMesh*> m_meshList;
};
|
The first functions to look at in the objLoader.cpp file are the Release()
functions. For the mesh, this function deletes all allocated memory
(i.e., vertices, normals, and texture coordinates), and for the model
class a Standard Template Library (STL) algorithm is used to delete all
allocated memory from the container. The list of meshes is stored in an std::vector object that is part of the C++ standard and is part of the STL. By calling the STL algorithm function for_each() and sending it a user-defined structure that will operate on each of the elements in the std::vector
array, we can create a structure that will delete each element that
exists. This is a nice trick that can be used to delete all elements of a
container by taking advantage of the efficiency of the STL algorithms.
The Release() functions are shown in Listing 5.
Listing 5. The Release() Functions
// Used to delete allocated objects in an STL container.
struct DeleteMemObj
{
template<typename T>
void operator()(const T* ptr) const
{
if(ptr != NULL)
delete ptr;
ptr = NULL;
}
};
void ObjMesh::Release()
{
m_totalVerts = 0;
if(m_vertices != NULL)
{
delete[] m_vertices;
m_vertices = NULL;
}
if(m_normals != NULL)
{
delete[] m_normals;
m_normals = NULL;
}
if(m_texCoords != NULL)
{
delete[] m_texCoords;
m_texCoords = NULL;
}
}
void ObjModel::Release()
{
for_each(m_meshList.begin(), m_meshList.end(), DeleteMemObj());
}
|
The mesh-accessing functions of the ObjModel
class are fairly straightforward and use array indexes to return the
information of interest. The class has a function to return a mesh by
array index and functions to return a specific mesh’s vertex positions,
normals, texture coordinates, vertex count, and texture file name. These
accessing functions are shown in Listing 6 for the ObjModel class.
Listing 6. The ObjModel Class’s Accessing Functions
ObjMesh *ObjModel::GetMeshByIndex(int index)
{
if(index < 0 || index > (int)m_meshList.size())
return NULL;
return m_meshList[index];
}
float *ObjModel::GetMeshVertices(int index)
{
if(index < 0 || index > (int)m_meshList.size())
return 0;
return m_meshList[index]->GetVertices();
}
float *ObjModel::GetMeshNormals(int index)
{
if(index < 0 || index > (int)m_meshList.size())
return 0;
return m_meshList[index]->GetNormals();
}
float *ObjModel::GetMeshTexCoords(int index)
{
if(index < 0 || index > (int)m_meshList.size())
return 0;
return m_meshList[index]->GetTexCoords();
}
int ObjModel::GetMeshTotalVerts(int index)
{
if(index < 0 || index > (int)m_meshList.size())
return 0;
return m_meshList[index]->GetTotalVerts();
}
int ObjModel::GetMeshCount()
{
return (int)m_meshList.size();
}
string ObjModel::GetMeshTextureFile(int index)
{
if(index < 0 || index > (int)m_meshList.size())
return 0;
return m_meshList[index]->GetTextureName();
}
|
The last function in the objLoader.cpp source file is the LoadOBJ()
function. This function is the biggest, but it is all fairly
straightforward. To make it easier to understand we’ll look at the
function in sections.
In the first section the OBJ file is sent to a
token stream object. There are two token streams in the function, with
the first holding the OBJ file and the second being a temp stream used
to further parse individual lines. The main token stream that has the
entire file will extract each line from the OBJ file using the MoveToNextLine() function of the TokenStream class. The temp stream object will take that line and further break it down into individual tokens on a line-by-line basis.
A loop is used in the first section to read each
line from the file. The first token of each line that is read from the
OBJ file is examined to see what information is on that line of text. If
the line starts with a #, then it is a comment and can be ignored. If the line starts with a v,
then it is a vertex position, and we will need to read the next three
tokens and convert the strings to floats to extract that information.
The same is done for vertex normals (vn) and texture coordinates (vt). All read information is stored in temporary std::vector arrays and used later in the function. Also, the material file name is read and stored in a string called materialFile. The first section of the LoadOBJ() function is shown in Listing 7.
Listing 7. The First Section of the LoadOBJ() Function
bool ObjModel::LoadOBJ(char *fileName)
{
TokenStream tokenStream(NULL), tempStream(NULL);
std::string tempLine, token;
tokenStream.LoadTokenStream(fileName);
std::vector<float> verts, norms, texC;
// This will store the material file location
// so we can use it to read the texture file name later.
string materialFile;
// Loop through and read all positions, normals, tex coords.
while(tokenStream.MoveToNextLine(&tempLine))
{
tempStream.SetTokenStream((char*)tempLine.c_str());
tokenStream.GetNextToken(NULL);
if(!tempStream.GetNextToken(&token))
continue;
if(strcmp(token.c_str(), "v") == 0)
{
tempStream.GetNextToken(&token);
verts.push_back((float)atof(token.c_str()));
tempStream.GetNextToken(&token);
verts.push_back((float)atof(token.c_str()));
tempStream.GetNextToken(&token);
verts.push_back((float)atof(token.c_str()));
}
else if(strcmp(token.c_str(), "mtllib") == 0)
{
tempStream.GetNextToken(&materialFile);
}
else if(strcmp(token.c_str(), "vn") == 0)
{
tempStream.GetNextToken(&token);
norms.push_back((float)atof(token.c_str()));
tempStream.GetNextToken(&token);
norms.push_back((float)atof(token.c_str()));
tempStream.GetNextToken(&token);
norms.push_back((float)atof(token.c_str()));
}
else if(strcmp(token.c_str(), "vt") == 0)
{
tempStream.GetNextToken(&token);
texC.push_back((float)atof(token.c_str()));
tempStream.GetNextToken(&token);
texC.push_back((float)atof(token.c_str()));
}
token[0] = '\0;;
}
…
}
|
The second section of the LoadOBJ() function resets the stream, and this time it loops through and looks for mesh declarations by searching for g and triangle faces by searching for f. Every time a g is encountered, a new mesh is added to the ObjModel class’s mesh list. Every time an f
is encountered, a new face is added to the last mesh added to the mesh
list. That way all meshes receive their correct faces since each mesh in
an OBJ file is followed by its list of faces. This code assumes that
the file uses only three point triangles, so keep that in mind.
To store the information of a mesh that will be
read, the function creates a temporary structure to hold the data. This
structure holds the name of the mesh (optional), the material name the
mesh uses from the material file, and the face indexes. During this
second section of the LoadOBJ() function, all mesh information is stored in an array of these temporary structure objects.
When reading a mesh, a new object is pushed
(added) to the mesh list, and the current mesh index is saved. This
index is used for the face parsing, so we always know which mesh was the
last one added to the list. At the end of the mesh’s conditional
statement the name of the mesh is extracted, which always follows the g keyword.
When reading the faces, the three tokens are
extracted one at a time. Each token that is extracted is further broken
down, and each face index (the first being for the position, the second
for the normal, and the third for the texture coordinates) is stored in
the temporary mesh’s faces array. This is done by looping through the
token and reading the indexes until we come across a slash (/) that
marks the end of an index or until all indexes have been read for the
current face vertex.
The second section from the LoadOBJ() function is shown in Listing 8.
The face parsing looks complex, but it is nothing more than reading the
characters between the slashes, converting them to integers, and saving
them in the temporary face array for the current mesh.
Listing 8. The Second Section from the LoadOBJ() Function
bool ObjModel::LoadOBJ(char *fileName)
{
…
// Temp struct used to store file faces per-mesh.
struct TempOBJMesh
{
string name, material;
std::vector<int> faces;
};
std::vector<TempOBJMesh> tempMeshes;
int tempMeshlndex = 0;
// Start from the beginning.
tokenStream.ResetStream();
// Read each mesh.
while(tokenStream.MoveToNextLine(&tempLine))
{
tempStream.SetTokenStream((char*)tempLine.c_str());
tokenStream.GetNextToken(NULL);
if(!tempStream.GetNextToken(&token))
continue;
if(strcmp(token.c_str(), "g") == 0)
{
// Add a new mesh to the list.
TempOBJMesh tempMesh;
tempMeshes.push_back(tempMesh);
tempMeshIndex = (int)tempMeshes.size() - 1;
tempStream.GetNextToken(&tempMeshes[tempMeshIndex].name);
}
else if(strcmp(token.c_str(), "usemtl") == 0 &&
!tempMeshes.empty())
{
// Get the material for the current mesh.
tempStream.GetNextToken(
&tempMeshes[tempMeshIndex].material);
}
else if(strcmp(token.c_str(), "f") == 0 &&
!tempMeshes.empty())
{
// Add a new face to the current mesh.
int index = 0;
for(int i = 0; i < 3; i++)
{
tempStream.GetNextToken(&token);
int len = (int)strlen(token.c_str());
for(int s = 0; s < len + 1; s++)
{
char buff[24];
if(token[s] != '/' && s < len)
{
buff[index] = token[s];
index++;
}
else
{
buff[index] = '\0';
tempMeshes[tempMeshIndex].faces.push_back(
(int)atoi(buff));
index = 0;
}
}
}
}
token[0] = '\0';
}
…
}
|
The third and last section of the LoadOBJ() function takes all of the loaded OBJ data that is stored in the temporary arrays and creates each ObjMesh object out of them. This section starts by allocating enough room on the mesh list array by calling reserve().
The function then loops through each mesh in the temp mesh list,
allocates the real mesh we will be using, and allocates memory to store
the vertex positions, normals, and texture coordinates in triangle list
form. Inside the loop a triangle list mesh is essentially being created,
as that process is very straightforward. In the OBJ file the
information is specified in a way that can’t be sent directly to
Direct3D. In this section we are creating the ObjMesh that will have its data formatted in a way that can be sent to Direct3D as a triangle list model.
In an OBJ file only unique positions, texture
coordinates, and normals are used, and when you use index geometry in
Direct3D or OpenGL, the indexes for a face vertex have to be the same
for each attribute. So, for example, if you have 100 vertices, there
should be 100 positions, normals, and texture coordinates, even in an
index model where index 1 in all arrays references attributes for the
same vertex point. However, in an OBJ file you can have four texture
coordinates that are reused, six normals, and eight positions, which
wouldn’t be right for Direct3D since all attributes must have the same
index. You can even have one normal, 20 vertices, and so on in an OBJ
file. Since the attribute indexes are not the same across each array for
a single vertex point, we have to take this extra step to set things up
for rendering later on.
Once the face information has been expanded so
that we have a triangle list’s worth of information, this information is
set to the allocated ObjMesh object, and that object is added
to the mesh list. Since we already know the material’s file name and
since we know the name of the material the mesh uses, we create another
token stream object to load the material file, and we search for the
texture’s file name. This can be done by using the overloaded GetNextToken()
function to move to the start of the material information the mesh uses
and then calling the same function again to search for the token Kd_map. The token that follows Kd_map is the name of the texture file, which is also stored in the ObjMesh object.
The third and final section of the LoadOBJ() function is shown in Listing 9.
Listing 9. The Third Section of the LoadOBJ() Function
bool ObjModel::LoadOBJ(char *fileName)
{
…
// "Unroll" the loaded obj information into a list
// of triangles for each mesh.
m_meshList.reserve(tempMeshes.size());
for(int i = 0; i < (int)tempMeshes.size(); i++)
{
ObjMesh *mesh = new ObjMesh();
int vIndex = 0, nIndex = 0, tIndex = 0;
int numFaces = (int)tempMeshes[i].faces.size() / 9;
int totalVerts = numFaces * 3;
mesh->SetTotalVerts(totalVerts);
float *vertices = new float[totalVerts * 3];
float *normals = NULL, *texCoords = NULL;
if((int)norms.size() != 0)
normals = new float[totalVerts * 3];
if((int)texC.size() != 0)
texCoords = new float[totalVerts * 2];
// Generate triangle list.
for(int f = 0; f < (int)tempMeshes[i].faces.size(); f+=3)
{
vertices[vIndex + 0] =
verts[(tempMeshes[i].faces[f + 0] - 1) * 3 + 0];
vertices[vIndex + 1] =
verts[(tempMeshes[i].faces[f + 0] - 1) * 3 + 1];
vertices[vIndex + 2] =
verts[(tempMeshes[i].faces[f + 0] - 1) * 3 + 2];
vIndex += 3;
if(texCoords)
{
texCoords[tIndex + 0] =
texC[(tempMeshes[i].faces[f + 1] - 1) * 2 + 0];
texCoords[tIndex + 1] =
texC[(tempMeshes[i].faces[f + 1] - 1) * 2 + 1];
tIndex += 2;
}
if(normals)
{
normals[nIndex + 0] =
norms[(tempMeshes[i].faces[f + 2] - 1) * 3 + 0];
normals[nIndex + 1] =
norms[(tempMeshes[i].faces[f + 2] - 1) * 3 + 1];
normals[nIndex + 2] =
norms[(tempMeshes[i].faces[f + 2] - 1) * 3 + 2];
nIndex += 3;
}
}
// Set info to mesh object.
mesh->SetName(tempMeshes[i].name);
mesh->SetVertices(vertices);
mesh->SetNormals(normals);
mesh->SetTexCoords(texCoords);
TokenStream materialStream(NULL);
materialStream.LoadTokenStream((char*)materialFile.c_str());
string searchKeyword = "map_Kd";
string textureFile;
// Use the first call to move to the material's section.
if(materialStream.GetNextToken(&tempMeshes[i].material,
NULL))
{
// Then use this call to get the texture name.
// All mat info is kept in one section so once we find
// the mat's name we can just move right to the texture
// file name since that will appear before any other mat.
materialStream.GetNextToken(&searchKeyword, &textureFile);
mesh->SetTextureName(textureFile);
}
m_meshList.push_back(mesh);
}
verts.clear();
norms.clear();
texC.clear();
tempMeshes.clear();
return true;
}
|
The next file to examine
is the main.cpp source file for the OBJ Models demo. In this file the
global section has a new structure added to it called DX10Mesh,
as well as a list of these meshes. Inside this mesh are a D3D10 vertex
buffer, the total vertices count, and a texture. The global section from
the OBJ Models demo’s main.cpp source file is shown in Listing 10.
Each vertex from an OBJ file specifies a position, normal, and texture
coordinate. For models that do not use one or more of these, most 3D
modeling applications use a single value for those attributes even if
the attribute isn’t used. In our loader those arrays would have been
filled with that single value for attributes that are not specified in
the model, so the vertex structure specifies each.
Listing 10. Global Section from the OBJ Models Demo’s Main Source File
#include<d3d10.h>
#include<d3dx10.h>
#include<vector>
#include"objLoader.h"
#pragma comment(lib, "d3d10.lib")
#pragma comment(lib, "d3dx10.lib")
#define WINDOW_NAME "Loading OBJ Models"
#define WINDOW_CLASS "UPGCLASS"
#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600
// Global window handles.
HINSTANCE g_hInst = NULL;
HWND g_hwnd = NULL;
// Direct3D 10 objects.
ID3D10Device *g_d3dDevice = NULL;
IDXGISwapChain *g_swapChain = NULL;
ID3D10RenderTargetView *g_renderTargetView = NULL;
ID3D10DepthStencilView *g_depthStencilView = NULL;
ID3D10Texture2D *g_depthStencilTex = NULL;
struct DX10Vertex
{
D3DXVECTOR3 pos;
D3DXVECTOR3 normal;
D3DXVECTOR2 tex0;
};
ID3D10InputLayout *g_layout = NULL;
struct DX10Mesh
{
DX10Mesh()
{
m_vertices = NULL;
m_decal = NULL;
m_totalVerts = 0;
}
ID3D10Buffer *m_vertices;
ID3D10ShaderResourceView *m_decal;
int m_totalVerts;
};
vector<DX10Mesh> g_meshes;
ID3D10Effect *g_shader = NULL;
ID3D10EffectTechnique *g_textureMapTech = NULL;
ID3D10EffectShaderResourceVariable *g_decalEffectVar = NULL;
ID3D10EffectMatrixVariable *g_worldEffectVar = NULL;
ID3D10EffectMatrixVariable *g_viewEffectVar = NULL;
ID3D10EffectMatrixVariable *g_projEffectVar = NULL;
D3DXMATRIX g_worldMat, g_viewMat, g_projMat;
// Scene rotations.
float g_xRot = 0.0f;
float g_yRot = 0.0f;
|