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 color
Kd: Specifies a diffuse color
Ks: Specifies a specular color
Ns: Defines the focus of specular highlights
Ni: Defines the optical density (a.k.a index of refraction)
d: Specifies a factor for dissolve
illum: Specifies an illumination model
map_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 the cube_texture.mtl companion.
- There are additional lines starting with
vn. Each such line declares a normal vector – for example, the vn 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 with v, the color is defined in the first line starting with vt, and the normal vector is defined in the first line starting with vn:
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 and textures_idx fields of the faces variable all contain valid indices now instead of taking a -1 value.
- The
normals field of the aux variable is a PyTorch tensor now, instead of being None.
- The
verts_uvs field of the aux variable is a PyTorch tensor now, instead of being None.
- The
texture_images field of the aux variable is not an empty dictionary any longer. The texture_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 the wal67ar_small.jpg file, as defined in the mtl_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.