-
Notifications
You must be signed in to change notification settings - Fork 15
3.1 OBJ Files
In all the scenes so far we've been extensively abusing our little container friend in many ways, but over time even our best friends could get a little boring. In practical graphics applications there are usually lots of complicated and interesting models that are much prettier to look at than a static container. However, unlike the container object, we can't really manually define all the vertices, normals and texture coordinates of complicated shapes like houses, vehicles or human-like characters. What we want instead is to import these models into the application; models that were carefully designed by 3D artists in tools like Blender, 3DS Max or Maya.
These so called 3D modeling tools allow artists to create complicated shapes and apply textures to them via a process called uv-mapping. The tools then automatically generate all the vertex coordinates, vertex normals and texture coordinates while exporting them to a model file format. This way, artists have an extensive toolkit to create high quality models without having to care too much about the technical details. All the technical aspects are hidden in the exported model file. We, as graphics programmers, do have to care about these technical details though.
It is thus our job to parse these exported model files and extract all the relevant information so we can store them in a format that OpenGL understands. A common issue is however that there are dozens of different file formats where each exports the model data in its own unique way. Model formats like the Wavefront .OBJ only contains model data with minor material information like model colors and diffuse/specular maps, while model formats like the XML-based Collada file format are extremely extensive and contain models, lights, many types of materials, animation data, cameras, complete scene information and much more. The Wavefront object format is generally considered to be an easy-to-parse model format. It is recommended to visit the Wavefront's wiki page at least once to see how such a file format's data is structured. This should give you a basic perception of how model file formats are generally structured.
All by all, there are many different file formats where a common general structure between them usually does not exist. So if we want to import a model from these file formats we'd have to write an importer ourselves for each of the file formats we want to import. Or we could use a 3rd party model importing library like Assimp. Assimp stands for Open Asset Import Library. Assimp is able to import dozens of different model file formats (and export to some as well) by loading all the model's data into Assimp's generalized data structures. As soon as Assimp has loaded the model, we can retrieve all the data we need from Assimp's data structures. Because the data structure of Assimp stays the same, regardless of the type of file format we imported, it abstracts us from all the different file formats out there.
However, in practice you may not want to use Assimp in your application. Importing models with Assimp can be slow, especially for text and XML based model file formats. You probably want to export your models to a binary format that is more compact and faster to load. You could use Assimp in a tool that exports models into a binary format that is most suited for your specific application.
However, in these tutorial series, we are going to stick to the Wavefront OBJ format since it is a very common format and relatively easy to parse ourselves.
OBJ files are commonly text files and support a wide variety of features. However, to keep things simple, we are only going to support a small subset. In particular, we only support OBJ files that define models as a list of vertex positions, texture coordinates, normals and triangle faces. We don't support faces containing more than 3 vertices (like quads) and we don't support other structures like lines, curves and surfaces. What we are left with is a file format that looks like this:
# Blender v2.71 (sub 0) OBJ File: 'nanosuit.blend'
# www.blender.org
mtllib nanosuit.mtl
o Visor
v 0.320384 14.057541 0.507779
v 0.385196 13.984534 0.445066
v 0.416643 14.114325 0.462461
...
vt 0.439941 0.453613
vt 0.541992 0.372070
vt 0.564941 0.521973
...
vn 0.496900 -0.240800 0.833700
vn 0.524600 -0.236700 0.817800
vn 0.581700 -0.161900 0.797100
...
usemtl Glass
f 1/1/1 2/2/2 3/3/3
f 4/4/4 5/5/5 6/6/6
f 6/6/6 7/7/7 4/4/4
...
o Legs
v 1.899165 2.317634 -0.120600
...
At the most basic level, every line in a OBJ file contains a command. The first word in the line is the name of the command. The rest of the line contains a space separated list of arguments for the command. Lines that start with a hash symbol (#) are comments and are ignored. Blank lines are ignored as well.
An OBJ file contains one or more meshes (or objects). Each mesh starts with an o
command (this command is optional if the file contains a single mesh). The example above, taken from the "nanosuit.obj" file, shows two meshes named "Visor" and "Legs". That file contains 5 more meshes.
ℹ️ Mesh
When modelling objects in modelling toolkits, artists generally do not create an entire model out of a single shape. Usually each model has several sub-models/shapes that it consists of. Each of those single shapes that a model is composed of is called a mesh. Think of a human-like character: artists usually model the head, limbs, clothes, weapons all as separate components and the combined result of all these meshes represents the final model. A single mesh is the minimal representation of what we need to draw an object in OpenGL (vertex data, indices and material properties). A model (usually) consists of several meshes.
In the [next](3.2 Mesh) tutorials we'll create our own TModel
and TMesh
class that can load models from OBJ files.
We support the following commands in OBJ files (any other commands are ignored):
-
mtllib
: load a material library from another file. This command usually appears near the top of the OBJ file and has a single argument with the name of the material library file to load. The format of this file (MTL) is discussed below. -
o
: begins a new mesh/object. Is followed by a name, which we ignore. -
v
: defines a position vertex. Is followed by at least 3 arguments with the X, Y and Z coordinates of the vertex. May be followed by a W coordinate, but we ignore that. -
vt
: defines a texture coordinate. Is followed by at least 2 texture coordinates (U and V). May be followed by a 3rd coordinate for 3D textures, but we ignore that. -
vn
: defines a vertex normal. Must be followed by 3 arguments (X, Y and Z). -
usemtl
: assigns a material to the current mesh. Is followed by the name of the material to use. This material must be available in the material library that is loaded with themtllib
command. -
f
: defines a single face of the mesh. A face is defined by 3 or more vertices. However, our implementation only supports 3 vertices (a triangle face). Each vertex is composed of 3 indices separated by forward slashes (/). Note that all indices start at 1 (instead of the more common 0):- The first number is an index in the list of position vertices (as defined by the
v
command). - The second number is an index in the list of texture coordinates (as defined by the
vt
command). - The third number is an index in the list of vertex normals (as defined by the
vn
command).
- The first number is an index in the list of position vertices (as defined by the
For example, the line:
f 1/1/1 2/1/2 3/2/2
means the triangle is composed of these 3 vertices:
- The first position, first texture coordinate and first normal
- The second position, first texture coordinate and second normal
- The third position, second texture coordinate and second normal
As you can see, a mesh is defined by lists of positions, texture coordinates, normals and faces/triangles. It is rendered using a material from a material library.
The format of material library (MTL) files is similar to those of OBJ files: it is a text file where each line represents a command with arguments. Again, we only support a subset of features of the MTL format:
# Blender MTL File: 'nanosuit.blend'
# Material Count: 6
newmtl Arm
map_Bump arm_showroom_ddn.png
map_Ka arm_showroom_refl.png
map_Kd arm_dif.png
map_Ks arm_showroom_spec.png
...
newmtl Body
map_Ka body_showroom_refl.png
map_Kd body_dif.png
map_Ks body_showroom_spec.png
map_Bump body_showroom_ddn.png
...
An MTL file contains one or more material definitions. Each material starts with a newmtl
command. The example above (taken from "nanosuit.mtl") shows two materials called "Arm" and "Body", and contains 4 additional materials.
In our implementation, the MTL file must be in the same directory (inside assets.zip
) as the corresponding OBJ file.
We support the following commands in MTL files (any other commands are ignored):
-
newmtl
: start the definition of a new material. Is followed by the name of the material. This material can later be referenced using this name by theusemtl
command in the OBJ file. -
map_Ka
: texture map to use for the ambient color component. Is followed by a single argument with the name of the file containing the texture image. In our implementation, this image file must be in the same directory (insideassets.zip
) as the OBJ and MTL file. -
map_Kd
: likemap_Ka
, but used for the diffuse color component. -
map_Ks
: likemap_Ks
, but used for the specular color component. -
map_Bump
: likemap_Ka
, but used for a bump map (or normal map). Normal maps are explained in a [later tutorial](5.4 Normal Mapping).
At its core, OBJ and MTL files have the same format: a text file where each line contains a command and up to 3 arguments. So we can parse them with a generic parser. Let's create a TParser
class that simplifies the parsing of commands and arguments:
type
TParser = class
private
FData: String;
FCurrent: PChar;
public
constructor Create(const APath: String);
{ Parses a line in the file.
Parameters:
ACommand: is set to command that starts the line (first word in the
string).
AArg1: is set to the first argument of the command
AArg2: is set to the second argument of the command (if any)
AArg3: is set to the third argument of the command (if any)
Returns:
True if a line was parsed, or False if the end of the file has been
reached.
This method will ignore empty lines and comments. }
function ReadLine(out ACommand, AArg1, AArg2, AArg3: String): Boolean;
end;
The API is very simple: the constructor is used to load a file. After that, you call ReadLine
repeatedly until it returns False
(when the end of the file has been reached):
var
Parser: TParser;
Command, Arg1, Arg2, Arg3: String;
begin
Parser := TParser.Create('models/mymodel.obj');
try
while (Parser.ReadLine(Command, Arg1, Arg2, Arg3)) do
begin
{ Handle Command }
end;
finally
Parser.Free;
end;
end;
The constructor loads the contents of the file into a single UnicodeString
that is parsed later. Because the file is probably in ANSI or UTF8 format, it needs to be converted to Unicode. We can use the TEncoding
class for this:
constructor TParser.Create(const APath: String);
var
Data: TBytes;
Encoding: TEncoding;
PreambleLength: Integer;
begin
inherited Create;
Data := TAssets.Load(APath);
{ Try to determine encoding of file }
Encoding := nil;
PreambleLength := TEncoding.GetBufferEncoding(Data, Encoding);
{ Convert to UnicodeString }
FData := Encoding.GetString(Data, PreambleLength, Length(Data) - PreambleLength);
FCurrent := PChar(FData);
end;
First, we load the raw file data from the assets.zip
file. Next, we use TEncoding.GetBufferEncoding
to try to determine the encoding used for the file. If the file contains a Byte Order Mark (BOM), then Encoding
will be set to corresponding encoding (like UTF8) and PreambleLength
will be set to the length of the BOM. We then use the Encoding
the convert the data to Unicode, skipping the BOM.
Finally, we set FCurrent
to point to the first character in the string. Later, when we parse the file, we keep advancing FCurrent
until it points to a #0
character, which means that the end of the string has been reached.
Parsing the next line then looks like this:
function TParser.ReadLine(out ACommand, AArg1, AArg2, AArg3: String): Boolean;
var
P: PChar;
function ParseString: String;
var
Start: PChar;
begin
if (P^ = #0) or (P^ = #10) then
{ End of line/file }
Exit('');
Start := P;
{ Advance to next whitespace }
while (P^ > ' ') do
Inc(P);
{ Extract string }
SetString(Result, Start, P - Start);
{ Skip whitespace }
while (P^ = #9) or (P^ = #13) or (P^ = ' ') do
Inc(P);
end;
begin
P := FCurrent;
{ Repeat until line has been read or end of file has been reached }
while True do
begin
{ Skip whitespace }
while (P^ <> #0) and (P^ <= ' ') do
Inc(P);
if (P^ = #0) then
begin
{ End of file reached }
FCurrent := P;
Exit(False);
end;
if (P^ = '#') then
begin
{ Ignore comment }
Inc(P);
while (P^ <> #0) and (P^ <> #10) do
Inc(P);
{ Next line }
Continue;
end;
ACommand := ParseString;
AArg1 := ParseString;
AArg2 := ParseString;
AArg3 := ParseString;
FCurrent := P;
Exit(True);
end;
end;
This is some typical parsing code that skips comments and empty lines and extracts the ACommand
and up to 3 arguments from a line of text.
We will use this parser in a [later tutorial](3.3 Model) to parse the actual OBJ and MTL files.
Next step: [importing](3.2 Mesh) fancy 3D stuff!
⬅️ [Lighting Review](Lighting Review) | Contents | [3.2 Mesh](3.2 Mesh) ➡️ |
---|
Learn OpenGL(ES) with Delphi
-
- Getting Started
- OpenGL (ES)
- [Creating an OpenGL App](Creating an OpenGL App)
- [1.1 Hello Window](1.1 Hello Window)
- [1.2 Hello Triangle](1.2 Hello Triangle)
- [1.3 Shaders](1.3 Shaders)
- [1.4 Textures](1.4 Textures)
- [1.5 Transformations](1.5 Transformations)
- [1.6 Coordinate Systems](1.6 Coordinate Systems)
- [1.7 Camera](1.7 Camera)
- [Review](Getting Started Review)
-
- Lighting
- [2.1 Colors](2.1 Colors)
- [2.2 Basic Lighting](2.2 Basic Lighting)
- [2.3 Materials](2.3 Materials)
- [2.4 Lighting Maps](2.4 Lighting Maps)
- [2.5 Light Casters](2.5 Light Casters)
- [2.6 Multiple Lights](2.6 Multiple Lights)
- [Review](Lighting Review)
-
- Model Loading
- [3.1 OBJ Files](3.1 OBJ Files)
- [3.2 Mesh](3.2 Mesh)
- [3.3 Model](3.3 Model)
-
- Advanced OpenGL
- [4.1 Depth Testing](4.1 Depth Testing)
- [4.2 Stencil Testing](4.2 Stencil Testing)
- [4.3 Blending](4.3 Blending)
- [4.4 Face Culling](4.4 Face Culling)
- [4.5 Framebuffers](4.5 Framebuffers)
- [4.6 Cubemaps](4.6 Cubemaps)
- [4.7 Advanced Data](4.7 Advanced Data)
- [4.8 Advanced GLSL](4.8 Advanced GLSL)
- [4.9 Geometry Shader](4.9 Geometry Shader)
- 4.10Instancing
- [4.11 Anti Aliasing](4.11 Anti Aliasing)
-
- Advanced Lighting
- [5.1 Advanced Lighting](5.1 Advanced Lighting)
- [5.2 Gamma Correction](5.2 Gamma Correction)
- [5.3 Shadows](5.3 Shadows)
- [5.3.1 Shadow Mapping](5.3.1 Shadow Mapping)
- [5.3.2 Point Shadows](5.3.2 Point Shadows)
- [5.3.3 CSM](5.3.3 CSM)
- [5.4 Normal Mapping](5.4 Normal Mapping)
- [5.5 Parallax Mapping](5.5 Parallax Mapping)
- [5.6 HDR](5.6 HDR)
- [5.7 Bloom](5.7 Bloom)
- [5.8 Deferred Shading](5.8 Deferred Shading)
- [5.9 SSAO](5.9 SSAO)
-
- PBR
-
- In Practice
- [7.1 Debugging](7.1 Debugging)
- [Text Rendering](Text Rendering)
- [2D Game](2D Game)
- Breakout
- [Setting Up](Setting Up)
- [Rendering Sprites](Rendering Sprites)
- Levels
-
Collisions
- Ball
- [Collision Detection](Collision Detection)
- [Collision Resolution](Collision Resolution)
- Particles
- Postprocessing
- Powerups
- Audio
- [Render Text](Render Text)
- [Final Thoughts](Final Thoughts)