3D data file format – OBJ files
In this section, we are going to discuss another widely used 3D data file format, the OBJ file format. The OBJ file format was first developed by Wavefront Technologies Inc. Like the PLY file format, the OBJ format also has both an ASCII version and a binary version. The binary version is proprietary and undocumented. So, we are going to discuss the ASCII version in this section.
Like the previous section, here we are going to learn the file format by looking at examples. The first example, cube.obj
, is shown as follows. As you can guess, the OBJ file defines a mesh of a cube.
The first line, mtlib ./cube.mtl
, declares the companion Material Template Library (MTL) file. The MTL file describes surface shading properties, which will be explained in the next code snippet.
For the o cube
line, the starting letter, o
, indicates that the line defines an object, where the name of the object is cube
. Any line starting with #
is a comment line – that is, the rest of the line will be ignored by a computer. Each line starts with v
, which indicates that each line defines a vertex. For example, v -0.5 -0.5 0.5
defines a vertex with an x coordinate of 0.5, a y coordinate of 0.5, and a z coordination of 0.5. For each line starting with f
, f
indicates that each line contains a definition for one face. For example, the f 1 2 3
line defines a face, with its three vertices being the vertices with indices 1, 2, and 3.
The usemtl Door
line declares that the surfaces declared after this line should be shaded using a material property defined in the MTL file, named Door
:
mtllib ./cube.mtl
o cube
# Vertex list
v -0.5 -0.5 0.5
v -0.5 -0.5 -0.5
v -0.5 0.5 -0.5
v -0.5 0.5 0.5
v 0.5 -0.5 0.5
v 0.5 -0.5 -0.5
v 0.5 0.5 -0.5
v 0.5 0.5 0.5
# Point/Line/Face list
usemtl Door
f 1 2 3
f 6 5 8
f 7 3 2
f 4 8 5
f 8 4 3
f 6 2 1
f 1 3 4
f 6 8 7
f 7 2 6
f 4 5 1
f 8 3 7
f 6 1 5
The cube.mtl
companion MTL file is shown as follows. The file defines a material property called Door
:
newmtl Door
Ka  0.8 0.6 0.4
Kd  0.8 0.6 0.4
Ks  0.9 0.9 0.9
d  1.0
Ns  0.0
illum 2
We will not discuss these material properties in detail except for map_Kd
. If you are curious, you can refer to a standard computer graphics textbook such as Computer Graphics: Principles and Practice. We will list some rough descriptions of these properties as follows, just for the sake of completeness:
Ka
: Specifies an ambient colorKd
: Specifies a diffuse colorKs
: Specifies a specular colorNs
: Defines the focus of specular highlightsNi
: Defines the optical density (a.k.a index of refraction)d
: Specifies a factor for dissolveillum
: Specifies an illumination modelmap_Kd
: Specifies a color texture file to be applied to the diffuse reflectivity of the material
The cube.obj
file can be opened by both Open3D and PyTorch3D. The following code snippet, obj_example1.py
, can be downloaded from our GitHub repository:
import open3d
from pytorch3d.io import load_obj
mesh_file = "cube.obj"
print('visualizing the mesh using open3D')
mesh = open3d.io.read_triangle_mesh(mesh_file)
open3d.visualization.draw_geometries([mesh],
                 mesh_show_wireframe = True,
                 mesh_show_back_face = True)
print("Loading the same file with PyTorch3D")
vertices, faces, aux = load_obj(mesh_file)
print('Type of vertices = ', type(vertices))
print("Type of faces = ", type(faces))
print("Type of aux = ", type(aux))
print('vertices = ', vertices)
print('faces = ', faces)
print('aux = ', aux)
In the preceding code snippet, the defined mesh of a cube can be interactively visualized by using the Open3D draw_geometries
function. The mesh will be shown in a window, and you can rotate, zoom into, and zoom out of the mesh using your mouse. The mesh can also be loaded using the PyTorch3D load_obj
function. The load_obj
function will return the vertices
, faces
, and aux
variables, either in the format of a PyTorch tensor or tuples of PyTorch tensors.
An example output of the obj_example1.py
code snippet is shown as follows:
visualizing the mesh using open3D
Loading the same file with PyTorch3D
Type of vertices =Â Â <class 'torch.Tensor'>
Type of faces =Â Â <class 'pytorch3d.io.obj_io.Faces'>
Type of aux =Â Â <class 'pytorch3d.io.obj_io.Properties'>
vertices =  tensor([[-0.5000, -0.5000,  0.5000],
        [-0.5000, -0.5000, -0.5000],
        [-0.5000,  0.5000, -0.5000],
        [-0.5000,  0.5000,  0.5000],
        [ 0.5000, -0.5000,  0.5000],
        [ 0.5000, -0.5000, -0.5000],
        [ 0.5000,  0.5000, -0.5000],
        [ 0.5000,  0.5000,  0.5000]])
faces =Â Â Faces(verts_idx=tensor([[0, 1, 2],
        [5, 4, 7],
        [6, 2, 1],
        ...
        [3, 4, 0],
        [7, 2, 6],
        [5, 0, 4]]), normals_idx=tensor([[-1, -1, -1],
        [-1, -1, -1],
        [-1, -1, -1],
        [-1, -1, -1],
        ...
        [-1, -1, -1],
        [-1, -1, -1]]), textures_idx=tensor([[-1, -1, -1],
        [-1, -1, -1],
        [-1, -1, -1],
        ...
        [-1, -1, -1],
        [-1, -1, -1]]), materials_idx=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
aux =Â Â Properties(normals=None, verts_uvs=None, material_colors={'Door': {'ambient_color': tensor([0.8000, 0.6000, 0.4000]), 'diffuse_color': tensor([0.8000, 0.6000, 0.4000]), 'specular_color': tensor([0.9000, 0.9000, 0.9000]), 'shininess': tensor([0.])}}, texture_images={}, texture_atlas=None)
From the code snippet output here, we know that the returned vertices variable is a PyTorch tensor with a shape of 8 x 3, where each row is a vertex with the x, y, and z coordinates. The returned variable, faces
, is a named tuple of three PyTorch tensors, verts_idx
, normals_idx
, and textures_idx
. In the preceding example, all the normals_idx
and textures_idx
tensors are invalid because cube.obj
does not include definitions for normal and textures. We will see in the next example how normals and textures can be defined in the OBJ file format. verts_idx
is the vertex indices for each face. Note that the vertex indices are 0-indexed here in PyTorch3D, where the indices start from 0. However, the vertex indices in OBJ files are 1-indexed, where the indices start from 1. PyTorch3D has already made the conversion between the two ways of vertex indexing for us.
The return variable, aux
, contains some extra mesh information. Note that the texture_image
field of the aux
variable is empty. The texture images are used in MTL files to define colors on vertices and faces. Again, we will show how to use this feature in our next example.
In the second example, we will use an example cube_texture.obj
file to highlight more OBJ file features. The file is shown as follows.
The cube_texture.obj
file is like the cube.obj
file, except for the following differences:
- There are some additional lines starting with
vt
. Each such line declares a texture vertex with x and y coordinates. Each texture vertex defines a color. The color is the pixel color at a so-called texture image, where the pixel location is the x coordinate of the texture vertex x width, and the y coordinate of the texture vertex x height. The texture image would be defined in thecube_texture.mtl
companion. - There are additional lines starting with
vn
. Each such line declares a normal vector – for example, thevn 0.000000 -1.000000 0.000000
line declares a normal vector pointing to the negative z axis. - Each face definition line now contains more information about each vertex. For example, the
f 2/1/1 3/2/1 4/3/1
line contains the definitions for the three vertices. The first triple,2/1/1
, defines the first vertex, the second triple,3/2/1
, defines the second vertex, and the third triple,4/3/1
, defines the third vertex. Each such triplet is the vertex index, texture vertex index, and normal vector index. For example,2/1/1
defines a vertex, where the vertex geometric location is defined in the second line starting withv
, the color is defined in the first line starting withvt
, and the normal vector is defined in the first line starting withvn
:
mtllib cube_texture.mtl
v 1.000000 -1.000000 -1.000000
v 1.000000 -1.000000 1.000000
v -1.000000 -1.000000 1.000000
v -1.000000 -1.000000 -1.000000
v 1.000000 1.000000 -0.999999
v 0.999999 1.000000 1.000001
v -1.000000 1.000000 1.000000
v -1.000000 1.000000 -1.000000
vt 1.000000 0.333333
vt 1.000000 0.666667
vt 0.666667 0.666667
vt 0.666667 0.333333
vt 0.666667 0.000000
vt 0.000000 0.333333
vt 0.000000 0.000000
vt 0.333333 0.000000
vt 0.333333 1.000000
vt 0.000000 1.000000
vt 0.000000 0.666667
vt 0.333333 0.333333
vt 0.333333 0.666667
vt 1.000000 0.000000
vn 0.000000 -1.000000 0.000000
vn 0.000000 1.000000 0.000000
vn 1.000000 0.000000 0.000000
vn -0.000000 0.000000 1.000000
vn -1.000000 -0.000000 -0.000000
vn 0.000000 0.000000 -1.000000
g main
usemtl Skin
s 1
f 2/1/1 3/2/1 4/3/1
f 8/1/2 7/4/2 6/5/2
f 5/6/3 6/7/3 2/8/3
f 6/8/4 7/5/4 3/4/4
f 3/9/5 7/10/5 8/11/5
f 1/12/6 4/13/6 8/11/6
f 1/4/1 2/1/1 4/3/1
f 5/14/2 8/1/2 6/5/2
f 1/12/3 5/6/3 2/8/3
f 2/12/4 6/8/4 3/4/4
f 4/13/5 3/9/5 8/11/5
f 5/6/6 1/12/6 8/11/6
The cube_texture.mtl
companion is as follows, where the line starting with map_Kd
declares the texture image. Here, wal67ar_small.jpg
is a 250 x 250 RGB image file in the same folder as the MTL file:
newmtl Skin
Ka 0.200000 0.200000 0.200000
Kd 0.827451 0.792157 0.772549
Ks 0.000000 0.000000 0.000000
Ns 0.000000
map_Kd ./wal67ar_small.jpg
Again, we can use Open3D and PyTorch3D to load the mesh in the cube_texture.obj
file – for example, by using the following obj_example2.py
file:
import open3d
from pytorch3d.io import load_obj
import torch
mesh_file = "cube_texture.obj"
print('visualizing the mesh using open3D')
mesh = open3d.io.read_triangle_mesh(mesh_file)
open3d.visualization.draw_geometries([mesh],
                  mesh_show_wireframe = True,
                  mesh_show_back_face = True)
print("Loading the same file with PyTorch3D")
vertices, faces, aux = load_obj(mesh_file)
print('Type of vertices = ', type(vertices))
print("Type of faces = ", type(faces))
print("Type of aux = ", type(aux))
print('vertices = ', vertices)
print('faces = ', faces)
print('aux = ', aux)
texture_images = getattr(aux, 'texture_images')
print('texture_images type = ', type(texture_images))
print(texture_images['Skin'].shape)
The output of the obj_example2.py
code snippet should be as follows:
visualizing the mesh using open3D
Loading the same file with PyTorch3D
Type of vertices =Â Â <class 'torch.Tensor'>
Type of faces =Â Â <class 'pytorch3d.io.obj_io.Faces'>
Type of aux =Â Â <class 'pytorch3d.io.obj_io.Properties'>
vertices =Â Â tensor([[ 1.0000, -1.0000, -1.0000],
        [ 1.0000, -1.0000,  1.0000],
        [-1.0000, -1.0000,  1.0000],
        [-1.0000, -1.0000, -1.0000],
        [ 1.0000,  1.0000, -1.0000],
        [ 1.0000,  1.0000,  1.0000],
        [-1.0000,  1.0000,  1.0000],
        [-1.0000,  1.0000, -1.0000]])
faces =Â Â Faces(verts_idx=tensor([[1, 2, 3],
        [7, 6, 5],
        [4, 5, 1],
        [5, 6, 2],
        [2, 6, 7],
        [0, 3, 7],
        [0, 1, 3],
        ...
        [3, 3, 3],
        [4, 4, 4],
        [5, 5, 5]]), textures_idx=tensor([[ 0,  1,  2],
        [ 0,  3,  4],
        [ 5,  6,  7],
        [ 7,  4,  3],
        [ 8,  9, 10],
        [11, 12, 10],
        ...
        [12,  8, 10],
        [ 5, 11, 10]]), materials_idx=tensor([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]))
aux =  Properties(normals=tensor([[ 0., -1.,  0.],
        [ 0.,  1.,  0.],
        [ 1.,  0.,  0.],
        [-0.,  0.,  1.],
        [-1., -0., -0.],
        [ 0.,  0., -1.]]), verts_uvs=tensor([[1.0000, 0.3333],
        ...
        [0.3333, 0.6667],
        [1.0000, 0.0000]]), material_colors={'Skin': {'ambient_color': tensor([0.2000, 0.2000, 0.2000]), 'diffuse_color': tensor([0.8275, 0.7922, 0.7725]), 'specular_color': tensor([0., 0., 0.]), 'shininess': tensor([0.])}}, texture_images={'Skin': tensor([[[0.2078, 0.1765, 0.1020],
         [0.2039, 0.1725, 0.0980],
         [0.1961, 0.1647, 0.0902],
         ...,
          [0.2235, 0.1882, 0.1294]]])}, texture_atlas=None)
texture_images type =Â Â <class 'dict'>
Skin
torch.Size([250, 250, 3])
Note
This is not the complete output; please check this while you run the code.
Compared with the output of the obj_example1.py
code snippet, the preceding output has the following differences.
- The
normals_idx
andtextures_idx
fields of thefaces
variable all contain valid indices now instead of taking a-
1
value. - The
normals
field of theaux
variable is a PyTorch tensor now, instead of beingNone
. - The
verts_uvs
field of theaux
variable is a PyTorch tensor now, instead of beingNone
. - The
texture_images
field of theaux
variable is not an empty dictionary any longer. Thetexture_images
dictionary contains one entry with a key,Skin
, and a PyTorch tensor with a shape of (250, 250, 3). This tensor is exactly the same as the image contained in thewal67ar_small.jpg
file, as defined in themtl_texture.mtl
file.
We have learned how to use basic 3D data file formats and PLY and OBJ files. In the next section, we will learn the basic concepts of 3D coordination systems.