MULTIMEDIA

iPhone 3D Programming : Adding Depth and Realism - Loading Geometry from OBJ Files

1/17/2011 2:47:50 PM
So far we’ve been dealing exclusively with a gallery of parametric surfaces. They make a great teaching tool, but parametric surfaces probably aren’t what you’ll be rendering in your app. More likely, you’ll have 3D assets coming from artists who use modeling software such as Maya or Blender.

The first thing to decide on is the file format we’ll use for storing geometry. The COLLADA format was devised to solve the problem of interchange between various 3D packages, but COLLADA is quite complex; it’s capable of conveying much more than just geometry, including effects, physics, and animation.

A more suitable format for our modest purposes is the simple OBJ format, first developed by Wavefront Technologies in the 1980s and still in use today. We won’t go into its full specification here (there are plenty of relevant sources on the Web), but we’ll cover how to load a conformant file that uses a subset of OBJ features.


Warning:

Even though the OBJ format is simple and portable, I don’t recommend using it in a production game or application. The parsing overhead can be avoided by inventing your own raw binary format, slurping up the entire file in a single I/O call, and then directly uploading its contents into a vertex buffer. This type of blitz loading can greatly improve the start-up time of your iPhone app.



Note:

Another popular geometry file format for the iPhone is PowerVR’s POD format. The PowerVR Insider SDK  includes tools and code samples for generating and reading POD files.


Without further ado, Example 1 shows an example OBJ file.

Example 1. Insanely simple OBJ file
# This is a comment.

v 0.0 1.0 1.0
v 0.0 -1.0 1.0
v 0.0 -1.0 -1.0
v -1.0 1.0 1.0

f 1 2 3
f 2 3 4

Lines that start with a v specify a vertex position using three floats separated by spaces. Lines that start with f specify a “face” with a list of indices into the vertex list. If the OBJ consists of triangles only, then every face has exactly three indices, which makes it a breeze to render with OpenGL. Watch out, though: in OBJ files, indices are one-based, not zero-based as they are in OpenGL.

OBJ also supports vertex normals with lines that start with vn. For a face to refer to a vertex normal, it references it using an index that’s separate from the vertex index, as shown in Example 2. The slashes are doubled because the format is actually f v/vt/vn; this example doesn’t use texture coordinates (vt), so it’s blank.

Example 2. An OBJ file with vertex normals
v 0.0 1.0 1.0
v 0.0 -1.0 1.0
v 0.0 -1.0 -1.0
vn 1 0 0
f 1//1 2//1 3//1

One thing that’s a bit awkward about this (from an OpenGL standpoint) is that each face specifies separate position indices and normal indices. In OpenGL ES, you only specify a single list of indices; each index simultaneously refers to both a normal and a position.

Because of this complication, the normals found in OBJ files are often ignored in many tools. It’s fairly easy to compute the normals yourself analytically, which we’ll demonstrate soon.

3D artist Christopher Desse has graciously donated some models to the public domain, two of which we’ll be using in ModelViewer: a character named MicroNapalm (the selected model in Figure 1) and a ninja character (far left in the tab bar). This greatly enhances the cool factor when you want to show off to your 4-year-old; why have cones and spheres when you can have ninjas?

Figure 1. ModelViewer with two OBJ models



Note:

I should also mention that I processed Christopher’s OBJ files so that they contain only v lines and f lines with three indices each and that I scaled the models to fit inside a unit cube.


1. Managing Resource Files

Note that we’ll be loading resources from external files for the first time. Adding file resources to a project is easy in Xcode. Download the two files (micronapalmv2.obj and Ninja.obj) from the examples site, and put them on your desktop or in your Downloads folder.

Create a new folder called Models by right-clicking the ModelViewer root in the Overview pane, and choose AddNew Group. Right-click the new folder, and choose AddExisting Files. Select the two OBJ files  by holding the Command key, and then click Add. In the next dialog box, check the box labeled “Copy items...”, accept the defaults, and then click Add. Done!

The iPhone differs from other platforms in how it handles bundled resources, so it makes sense to create a new interface to shield this from the application engine. Let’s call it IResourceManager, shown in Example 3. For now it has a single method that simply returns the absolute path to the folder that has resource files. This may seem too simple to merit its own interface at the moment, such as loading image files. Add these lines, and make the change shown in bold to Interface.hpp.

Example 3. Adding IResourceManager to Interface.hpp
#include <string>
using std::string;

// ...
struct IResourceManager {
virtual string GetResourcePath() const = 0;
virtual ~IResourceManager() {}
};

IResourceManager* CreateResourceManager();

IApplicationEngine* CreateApplicationEngine(IRenderingEngine* renderingEngine,
IResourceManager* resourceManager);

// ...


We added a new argument to CreateApplicationEngine to allow the platform-specific layer to pass in its implementation class. In our case the implementation class needs to be a mixture of C++ and Objective-C. Add a new C++ file to your Xcode project called ResourceManager.mm (don’t create the corresponding .h file), shown in Example 4.

Example 4. ResourceManager implementation
#import <UIKit/UIKit.h>
#import <QuartzCore/QuartzCore.h>
#import <string>
#import <iostream>
#import "Interfaces.hpp"

using namespace std;

class ResourceManager : public IResourceManager {
public:
string GetResourcePath() const
{
NSString* bundlePath = [[NSBundle mainBundle] resourcePath];
return [bundlePath UTF8String];
}
};

IResourceManager* CreateResourceManager()
{
return new ResourceManager();
}

The resource manager should be instanced within the GLView class and passed to the application engine. GLView.h has a field called m_resourceManager, which gets instanced somewhere in initWithFrame and gets passed to CreateApplicationEngine. (This is similar to how we’re already handling the rendering engine.) So, you’ll need to do the following:

  1. In GLView.h, add the line IResourceManager* m_resourceManager; to the @private section.

  2. In GLView.mm, add the line m_resourceManager = CreateResourceManager(); to initWithFrame (you can add it just above the line if (api == kEAGLRenderingAPIOpenGLES1). Next, add m_resourceManager as the second argument to CreateApplicationEngine.

Next we need to make a few small changes to the application engine per Example 5. The lines in bold show how we’re reusing the ISurface interface to avoid changing any code in the rendering engine. Modified/new lines in ApplicationEngine.cpp are shown in bold (make sure you replace the existing assignments to surfaces[0] and surfaces[0] in Initialize):

Example 5. Consuming IResourceManager from ApplicationEngine
#include "Interfaces.hpp"
#include "ObjSurface.hpp"

...

class ApplicationEngine : public IApplicationEngine {
public:
ApplicationEngine(IRenderingEngine* renderingEngine,
IResourceManager* resourceManager);
...
private:
...
IResourceManager* m_resourceManager;
};

IApplicationEngine* CreateApplicationEngine(IRenderingEngine* renderingEngine,
IResourceManager* resourceManager)
{
return new ApplicationEngine(renderingEngine, resourceManager);
}


ApplicationEngine::ApplicationEngine(IRenderingEngine* renderingEngine,
IResourceManager* resourceManager) :
m_spinning(false),
m_pressedButton(-1),
m_renderingEngine(renderingEngine),
m_resourceManager(resourceManager)
{
...
}

void ApplicationEngine::Initialize(int width, int height)
{
...

string path = m_resourceManager->GetResourcePath();
surfaces[0] = new ObjSurface(path + "/micronapalmv2.obj");
surfaces[1] = new ObjSurface(path + "/Ninja.obj");
surfaces[2] = new Torus(1.4, 0.3);
surfaces[3] = new TrefoilKnot(1.8f);
surfaces[4] = new KleinBottle(0.2f);
surfaces[5] = new MobiusStrip(1);

...
}

2. Implementing ISurface

The next step is creating the ObjSurface class, which implements all the ISurface methods and is responsible for parsing the OBJ file. This class will be more than just a dumb loader; recall that we want to compute surface normals analytically. Doing so allows us to reduce the size of the app, but at the cost of a slightly longer startup time.

We’ll compute the vertex normals by first finding the facet normal of every face and then averaging together the normals from adjoining faces. The C++ implementation of this algorithm is fairly rote, and you can get it from this website (http://oreilly.com/catalog/9780596804831); for brevity’s sake, Example 6 shows the pseudocode.

Example 6. Pseudocode to compute vertex normals from facets
ivec3 faces[faceCount] = read from OBJ
vec3 positions[vertexCount] = read from OBJ
vec3 normals[vertexCount] = { (0,0,0), (0,0,0), ... }

for each face in faces:
vec3 a = positions[face.Vertex0]
vec3 b = positions[face.Vertex1]
vec3 c = positions[face.Vertex2]
vec3 facetNormal = (a - b) × (c - b)



normals[face.Vertex0] += facetNormal
normals[face.Vertex1] += facetNormal
normals[face.Vertex2] += facetNormal

for each normal in normals:
normal = normalize(normal)

The mechanics of loading face indices and vertex positions from the OBJ file are somewhat tedious, so you should download ObjSurface.cpp and ObjSurface.hpp from this website  and add them to your Xcode project. Example 7 shows the ObjSurface constructor, which loads the vertex indices using the fstream facility in C++. Note that I subtracted one from all vertex indices; watch out for the one-based pitfall!

Example 7. ObjSurface constructor
ObjSurface::ObjSurface(const string& name) :
m_name(name),
m_faceCount(0),
m_vertexCount(0)
{
m_faces.resize(this->GetTriangleIndexCount() / 3);
ifstream objFile(m_name.c_str());
vector<ivec3>::iterator face = m_faces.begin();
while (objFile) {
char c = objFile.get();
if (c == 'f') {
assert(face != m_faces.end() && "parse error");
objFile >> face->x >> face->y >> face->z;
*face++ -= ivec3(1, 1, 1);
}
objFile.ignore(MaxLineSize, '\n');
}
assert(face == m_faces.end() && "parse error");
}

Other  
  •  iPhone 3D Programming : Adding Depth and Realism - Better Wireframes Using Polygon Offset
  •  Programming with DirectX : Textures in Direct3D 10 (part 2)
  •  Programming with DirectX : Textures in Direct3D 10 (part 1) - Textures Coordinates
  •  Programming with DirectX : Shading and Surfaces - Types of Textures
  •  iPhone 3D Programming : Adding Shaders to ModelViewer (part 2)
  •  iPhone 3D Programming : Adding Shaders to ModelViewer (part 1) - New Rendering Engine
  •  iPhone 3D Programming : Adding Depth and Realism - Shaders Demystified
  •  Programming with DirectX : Transformation Demo
  •  Programming with DirectX : View Transformations
  •  Programming with DirectX : World Transformations
  •  Programming with DirectX : Projection Transformations
  •  iPhone 3D Programming : Adding Depth and Realism - Lighting Up (part 2)
  •  iPhone 3D Programming : Adding Depth and Realism - Lighting Up (part 1)
  •  iPhone 3D Programming : Adding Depth and Realism - Surface Normals (part 2)
  •  iPhone 3D Programming : Adding Depth and Realism - Surface Normals (part 1)
  •  iPhone 3D Programming : Adding Depth and Realism - Filling the Wireframe with Triangles
  •  iPhone 3D Programming : Adding Depth and Realism - Creating and Using the Depth Buffer
  •  iPhone 3D Programming : Adding Depth and Realism - Examining the Depth Buffer
  •  iPhone 3D Programming : HelloCone with Fixed Function
  •  iPhone 3D Programming : Vector Beautification with C++
  •  
    Most View
    HTC One X+ Review - Highlighting Almost All Power Of HTC One X (Part 2)
    Skype For Windows 8 - A New Way Of Using Skype
    ASP.NET 4 : Data Source Controls (part 2) - Parameterized Commands
    Ten Stellar Keyboard Shortcuts
    Windows Phone 8 Group Test – June 2013 (Part 1) : HTC Windows Phone 8S, HTC Windows Phone 8X
    Microsoft Surface With Windows RT Review (Part 1)
    Improve Your Mac (Part 1) - Import Pictures into iPhoto
    Apple iPod Nano – The Trendy Compactness
    Sony Vaio Tap 20 Mobile Desktop - Meet The Laptablet
    Affordable Strong-Bass Denon Headphones
    Top 10
    The NZXT Kraken X40 Compact Liquid Cooler Review (Part 3)
    The NZXT Kraken X40 Compact Liquid Cooler Review (Part 2)
    T-Mobile’s Samsung Galaxy Note II Review (Part 6)
    T-Mobile’s Samsung Galaxy Note II Review (Part 5)
    T-Mobile’s Samsung Galaxy Note II Review (Part 4)
    T-Mobile’s Samsung Galaxy Note II Review (Part 3)
    T-Mobile’s Samsung Galaxy Note II Review (Part 2)
    T-Mobile’s Samsung Galaxy Note II Review (Part 1)
    Sony Cybershot DSC-TF1 - Affordable Water-Resistant Camera
    Buffalo MiniStation Slim 500GB External Hard Drive