Sunday, August 23, 2015

Wavefront OBJ file format and Normal Mapping

Before going into more advanced render techniques I'd like to use more professional object designs than spheres, cubes or cylinders. It looks more professional with textures, but we shouldn't use so simple designs on our awesome framework. We won't be able to use all commands

Wavefront .obj file format

Wavefront OBJ is the most simple file format for storing 3D model informations. Every line starts with a command followed by a punch of numbers. With them we can store points, texture coordinates, normals and surfaces. Unfortunately lights are not a standard for the file format.

I mark variables between <...> (i.e. <x> <y> <z>, <"name">). Quotes for strings and no quotes for numbers:

Comment "#"

Every line starting with a # symbol is a comment

Syntax:
# <"comment">

Example:
# This is a comment

Material library "mtllib"

A line starting with "mtllib" tells us that we want to include a material file (.mtl) into our object. This has the advantage to store ambient, diffuse, specular and texture informations into an extern file. In generall the filename of both .obj and .mtl files are the same (classroom.obj and classroom.mtl). Even whitespace is possible in a filename, so be careful

Syntax:
mtllib <"filepath">

Examples:
mtllib classroom.mtl
mtllib genie lamb 05.mtl

Object "o"

A line starting with "o" tells us that a new object will be created. All lines afterwards and until the next "o" will belong to this object. This allows us to have more than one object in one file.

Syntax:
o <"object name">

Examples:
o Plane
o Lamb01
o Chair

It may happen that there is no "o" at all, so we need to create a new object anyway.

Group "g"

A line starting with "g" describes that the following lines belong to that group. It won't create a new object, but it is more like they belong together.

Syntax:
g <"group name">

Examples:
g window
g front

Vertex "v"

Finally something which contains important informations: "v" is a vertex or in our case a point. They normally appear in several lines in a row, especially because we need at least 3 points to create a surface.

Syntax:
v <x> <y> <z>

Example:
v -1 2 5.2
v -0.000000 0.114194 -0.315641

Texture vertex "vt"

Same as "v", but uses u, v and optional w. (w is used for bump mapping, explained later). In most cases they are between 0 and +1 like explained in 2D texture mapping. They also appear  in several lines in a row.

Syntax:
vt <u> <v>
vt <u> <v> <w>

Example:
vt 0.450580 0.776692
vt 0.382673 0.517478 0.247539

Normal vertex "vn"

A normal vertex is normally used for vertex interpolation like with smooth surfaces. Gouraud shading and Phong shading. The trick is that each surface point has (optionally) a normal vertex stored. The normal on a certain point on the surface can be interpolated in the same way like texture coordinates using u and v.

Syntax:
vn <x> <y> <z>

Material usage "usemtl"

Before we assign points and texture points to each surface we need to define the used material. Remember the line "mtllib"? Exactly: In this file we can find the material with the given name. (Material file syntax explained below).

Syntax:
usemtl <"material name">

Examples:
usemtl Material001
usemtl Rocky
usemtl gold

Smooth group "s"

This command puts all upcoming surfaces into the same smooth group. This allows us to use smooth shading along edges. There is also the option to set smooth shading "off".

Syntax:
s <1-32>
s off

Example:
s 7

Surface "f"

Now the usage of this nasty pointers in our framework will make sense. Wavefront OBJ numerates each "v", "vt" and "vn" like a list. In other words, after reading all vertexes, texture vertexes and normal vertexes we should have 3 lists, each of them starting with index 1, NOT WITH 0. A surface can have 3 or 4 points and both have 4 syntaxes each to take care of.

Syntax with vertex alone:
f <v1> <v2> <v3>
f <v1> <v2> <v3> <v4>

Syntax with vertex and texture:
f <v1>/<vt1> <v2>/<vt2> <v3>/<vt3>
f <v1>/<vt1> <v2>/<vt2> <v3>/<vt3> <v4>/<vt4>

Syntax with vertex, texture and normal:
f <v1>/<vt1>/<vn1> <v2>/<vt2>/<vn2> <v3>/<vt3>/<vn3>
f <v1>/<vt1>/<vn1> <v2>/<vt2>/<vn2> <v3>/<vt3>/<vn3> <v4>/<vt4>/<vn4>

Syntax with vertex and normal:
f <v1>//<vn1> <v2>//<vn2> <v3>//<vn3>
f <v1>//<vn1> <v2>//<vn2> <v3>//<vn3> <v4>//<vn4>

Examples:
f 146/27 8405/32 8406/33 147/28
f 146/27/8 8405/32/8 8406/33 147/28/8
f 1//6 4//6 8//6

Wavefront .mtl file format

In most cases, each OBJ file gives us a MTL (material) file, containing color and other informations we described in our Material class. Like in OBJ every line starts with a command for a syntax.

Comment "#"

I already described command "#", lets skip this

Material definition "newmtl"

This command tells us to create a new material object with the given name.

Syntax:
newmtl <"material name">

Examples:
newmtl Rocky
newmtl Material001

I used "Rocky" on purpose (see above ^^).

Ambient color "Ka"

The first component important for phong illumination model. Command is "ka" followed by 3 floating point numbers from 0 to 1 representing red, green and blue.

Syntax:
Ka <r> <g> <b>

Example:
Ka 0.000000 0.000000 0.000000

Diffuse color "Kd"

The second component important for phong illumination model. Same as ambient color.

Syntax:
Kd <r> <g> <b>

Example:
Kd 0.790599 0.552374 0.126178

Specular color "Ks"

The third component important for phong illumination model. Same as ambient and diffuse color.

Syntax:
Ks <r> <g> <b>

Example:
Ks 1.909807 1.671032 0.369581

Specular exponent "Ns"

Important to describe the specular's exponent for highlighting a spot. After command "Ns" there is a single floating point number from 0 to 1000.

Syntax:
Ns <exponent>

Example:
Ns 109.803922

Refraction coefficient "Ni"

Important for raytracing (explained later). If you are a physician, you know that transparent objects (i.e. glass and water) have a refraction coefficient where light rays get distracted from the surface when entering. It is not important now how to calculate it.

Syntyx:
Ni <refraction coefficient>

Example (no refraction):
Ni 1.000000

Disolve factor or Transparency "d"

Talking about transparent objects like glass. This command tells us how much of the color you will see from the object.

Syntax:
d <disolve factor>

Example:
d 1.000000

Illumination model "illum"

Not really something you can figure out yourself. It seems that OBJ files have around 10 different settings for illumination.

Syntax:
illum <setting>

Example (Highlight on):
illum 2

A list of possible illumination models can be found here.

Mapping "map_" + option

There is also a possibility to not just use material color but also use a texture color of a given point as component (ambient, diffuse and specular) color. That way we are more flexible in designing realistic scenes.

Syntax:
map_Kd <"texture file">
map_Ks 
<"texture file">
map_Ka 
<"texture file">
map_Bump 
<"texture file">
map_d 
<"scalar texture file">

Our problem is that most file names are of type PNG, JPEG or TGA. There are far too many for our simple framework. We have to figure it out ourself which image we took for texture mapping and add it in our framework by ourselves.

Reflection type "refl"

Also something we need for raytracing. This is quite difficult to figure out because it seems there are more than one possibility for a syntax. I believe it is more directed to reflection mapping.

Syntax:
refl -type <sphere/cube_"side"> <"filename">
refl -type <sphere/cube_"side"> -otions <"optoin"> -args <"args"> <"filename">

Example:
refl -type sphere chrome.rla

Normal vector transformation

Now that we know the contents of an OBJ file we need to update our surface class. It contains now:
  • 3x Vertex points
  • 3x Texture points
  • 3x Normal vectors
Normal vectors only show us the direction, they won't change when the surface is translated. You should consider normals like planes (see here). To get a normal transformed correctly we calculate:



where n is normal and M is the transformation matrix. Here, M is inverse and transpose.

It took me some time to figure that out myself, so I'll show you how I do that. In 3D we normally work with a 4x4 matrix looking like this:



From this transformation matrix we only need the components responsible for scale and rotation, not for translation. We cut the 4th row and 4th column, getting a 3x3 matrix M.



And from this, we first calculate the inverse matrix and afterwards calculate the transpose. We can use this matrix to multiply with a 3D vector normally OR turn M into a 4x4 matrix for a homogenous matrix transformation.

Phong interpolation

We can set up 3 (equal or different) normals (n0, n1 and n2) at a surface's corner, giving us the opportunity to manipulate a surface normal as we please. That way we can make a surface more smooth (like a sphere).

We are already able to use barycentric coordinates properly. We use this to test if a point is inside a surface and calculate texture coordinates for UV mapping. We will now do the same calculation for normals.

From barycentric coordinates we get α, β and γ. Our normal on point P is.



With that there is no need to simulate an object's normal like I did in the previous version. We can also scale a sphere as we please and it will keep the correct normals for interpolation. This is important at working with customized vertex normals.

Normal Mapping

With normal mapping we are able to simulate a terrain on a surface without manipulating the object's surfaces. In other words, A sphere will stay a sphere for the render engine, but illumination will get a different normal then usually.

To a color map (a.k.a. texture) we have for instance a picture of the earth and it's normal map (source)




This normal map saves a surface's normal as RGB from 0 to 255. For us, it means from 0 to 1. But vectors can also go into negative direction, which is why we need to calculate from 0 to 1 into -1 to +1. R is x, G is y and B is z. This map is considered to have a general normal of (0,0,1).

As you can see the normal map is mainly colored in blue. Well, normals don't face negative areas usually. This image always have more than 0.5 in his blue color channel.

We only know a normal on our surface (i.e. a sphere). We must find a way to get 2 additional vectors so that we can form something like a look at matrix (remember my post about 3D Viewing). This 2 new vectors are called tangent and bitangent (like right and up vectors for look at matrix). A way to calculate these two can be found on this website.

We have to prepare some values to get tangent:

  • Surface's edges (p1-p0 and p2-p0) in x,y,z
  • texture edges (t1-t0 and t2-t0) in u,v

You can also just calculate tangent. Bitangent can be calculated by using the cross product of normal and tangent. With that we can finally build our transform matrix. We can use either 3x3 or 4x4 (homogenous).



IMPORTANT: Inverse and transpose matrix M when used on normals which is the case in normal mapping

Previously our images looked like this, very boring, plane and with a specular.


And that's how the same sphere with the same lighting and the same view looks with a normal map:


As you can see, some areas on the dark side of the earth are still bright. That's because according to Phong's illumiation model the new normal is still at a small angel to the light source. We can avoid this by adding hard shadows.
Honestly, I almost fell from my chair when I saw this picture for the first time after running the framework.

Framework

Once again, I put in some minor changes in the framework
  • Surface3D
    • Added Texture object normalMap
    • interpolates normals with and without normal map
  • Polygon3D
    • Derived from Object3D
    • Is able to add extern created points and surfaces
  • OBJLoader
    • Text parser to read OBJ and MTL files
    • Only works with an existing Scene3D object
    • Splits each line into blocks (seperated by space ' ')
    • Freely expendable
    • Can read "f" commands with 3 (one triangle) and 4 (two triangles) parameters
    • Saves materials, objects and surfaces immediately into Scene3D
  • Scene3D
    • Method to load obj files from OBJLoader object
    • Method to add Object3D and Material objects
I first intended to add a bounding sphere algorithm into the framework, but I left that asside because this issue should focus on loading OBJ files and normal mapping. It shall demonstrate how slow raycasting really is without that.

Preparations for OBJ and MTL files

There are several 3D modeling softwares in WWW. It is up to you which one you take, as long as they are able to export your creations into OBJ files. Blender's homepage offers many demo files you can export into OBJ files.

For our framework, you need to consider the following options (i.e. in Blender v2.73)
  • What is IMPORTANT
    • Forward settings: +Z-Forward
    • Up settings: +Y-Up
    • Objects as OBJ objects (not group)
    • Write Materials
  • What is OPTIONAL
    • Include UVs (for texture mapping)
    • Triangulate Faces (3 instead of 4 points, file would be larger)
    • Write Normals (for correct phong interpolation)
Settings for File -> Export -> Wavefront (.obj) in Blender v2.73

Output

This Image was rendered in 8 minutes with size 640x480. It loads file "genie_lamp_05.obj" and creates a sphere with texture "earth.ppm" and normal map "earth_normalmap.ppm"

Left: an object loaded from "genie_lamp_05.obj" (blender file source)
Left: a sphere with normal mapped earth surface (texture and normal map source)

Conclusion

We are now able to load extern OBJ files from modern 3D modeling softwares like Blender, Maya and AutoCAD. But we are facing the harsh reality that our framework becomes slower and slower without better data structures.

In the next issue we will improve the OBJLoader so that it can use extern folders and we will talk about a special data structure to speed up raycasting and even later render algorithms. The one data structure I think will fit the best will be Octrees, I will change the structure a bit where a sub-octree can contain multiple leaves (or in our framework: Surface3Ds). Scene3D will also make some changes by turning all std::vector into std::map.

I do this before Raytracing because I don't want to wait 8 minutes everytime I want to test the framework. Having said that, see you in the next issue.

Repository

Tested with:
  • Visual Studio 2013 (Windows 8.1)
  • GNU Compiler 4.9.2 (Ubuntu 12.04)

No comments:

Post a Comment