Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Loading OBJ's
#1
Getting OBJ files to load isn’t the hardest thing in the world, but it does present a few challenges.
OBJ is a fairly standard format, but it’s been around long enough for it to have had a lot of additions and updates to make variations a problem. So if you write a reader for your own very specific types of OBJ’s you may find it lacks the flexibility to load other supposedly standard OBJ’s.
OBJ’s are important though, they represent the simplest kind of multi face model you are likely to load into your project, once you get to the realization that typing in anything more than a cube is a major pain.
Key to the OBJ standard is that it is text based and readable meaning you can check in an editor what kind of data your OBJ file has, make sure it has no strange extra features and get a reasonable assessment of how many sub objects it has and what their names are, which can be useful if you want to do work on specific sub objects, like a car wheel for example.
OBJ files are also usually (but not always) associated with a MTL or material file, which provides additional information that relates to textures and lighting features your renderer has to accommodate.
I use a software library to load OBJ’s,  TinyOBJLoader, and I do that for 2 reasons

1 it works almost 100%
2 it gets updated to cope with additions to OBJ standard.
 
Getting TinyOBJLoader installed and working, is not in itself very hard, but understanding what it provides can be a bit of a leap. TinyOBJLoader is a parser for data, it reads the text and stores the resulting numerical data….somewhere.
It’s important to note that not all the data is parsed, only the data it currently understands, and also it’s stored in data structures which are supposed to be passed to it, and supplied by the users app. But for us, just wanting info on the vertices/uv and textures we can extract all we need, if we also need normal and lighting info it’s there too.
So to help in understanding what TinyOBJLoader does, we need to look at what it provides. Data…but in what format? Fortunately for us there is a very nice example of its use in the github repos which proves a very simple loader and viewer. The viewer itself is old OpenGL1.0 format so not much use to us, but it does give us a good guide how to use TinyOBJLoader
The Viewer.cc file is a simple C++ system that loads and renders a model. The key part we want is the LoadObjAndConvert function which has these input values

static bool LoadObjAndConvert(float bmin[3], float bmax[3],
                              std::vector<DrawObject>* drawObjects,
                              std::vector<tinyobj::material_t>& materials,
                              std::map<std:: string, GLuint>& textures,
                              const char* filename)

For coders that should be all we need to know about… the bmin[3] and bmax[3] are simple arrays we can pass that will provide box min and max xyz values to allow us to have bounding boxes.
Then we have 2 vectors containing pointers to DrawObjects which is a simple data structure defined beforehand, and the other is the location vector of a material structures, also defined in TinyOBJLoader.
Next up is the location of a std::map, which will associate texture names with texture handles.. allowing us to load a texture one time (per model)
All these things are defined in the example for us to use in our own code.
  float bmin[3], bmax[3];
  std::vector<tinyobj::material_t> materials;
  std::map<std:: string, GLuint> textures;
 
and DrawObject is a structure and looks like this
typedef struct {
  GLuint vb_id;  // vertex buffer id
  int numTriangles;
  size_t material_id;
} DrawObject;
So a vector of DrawObject’s looks like
std::vector<DrawObject> gDrawObjects;
 
Finally of course the file name, as long as the file is correctly referenced LoadOBJAndConvert, will do exactly what it has been named to do and can be called.
Each DrawObject is essentially a sub object of an OBJ Model, some models may only have 1 sub object, but more complex models will often consist of several, A car model for example may have a body and 4 wheels, making 5 sub objects. So the vector’s size will tell us how many objects there are.
Now, when we call this function, we load the model, in all its parts, its material info goes into the material vector, the gDrawObject vector contains all the sub objects needed (with a VB(O) rather than a vertex list) and there it’s a simple process to render the object.
We will have to change the location of the vectors and map, so that our individual object can access its own data, but overall that’s not making any changes to the function itself.
 
The Key point is to think about what the function needs to work and what it returns.

When it comes to rendering, the old OpenGL1.0 draw function isn't something we can plug in directly, but we should be able to read the draw function and take note that it is working out how many shapes (sub objects) to draw, each with their own material ID to get access to the texture,(and other lighting info), and the number of triangles needed to draw,

In its raw form, the conversion produced VBO's (with id's held in the DrawObject structures) with 3 floats for verts, 3 for normals, 3 for colours(RGBA) and 2 for UV's You may not need or want to use all these attributes, so you can adapt the function if memory is tight, but you can certainly work out your stride pattern to enable your attribute buffers and send the required attributes to your shaders.
Once you set up your attributes, a simple draw call for triangles gets your models on screen.

have fun.
Brian Beuken
Lecturer in Game Programming at Breda University of Applied Sciences.
Author of The Fundamentals of C/C++ Game Programming: Using Target-based Development on SBC's 



Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)