Ray Tracing Devlog 2 - Parsing an OBJ file

2020-02-19 // 4 minutes

As mentioned in a previous blog post, one of my first major goals for this iteration of the project is to add OBJ file support. While I could use an existing library, such as tinyobjloader, I wanted to write my own for practice. That said, I will likely benchmark my performance against external libraries as a way of checking if my code is performing reasonably well.

In order to keep the first pass as simple as possible, I wrote out an obj file with a single triangle. The file does define a material, but for the first pass we will just make a pixel magenta if it hits the triangle. You can view that file below:

o triangle
mtllib triangle.mtl

v 0.00000 0.00000 1.000000
v 0.50000 0.00000 1.000000
v 0.00000 0.50000 1.000000
v 1.00000 1.00000 0.500000

vt 0.000000 0.000000
vt 1.000000 0.000000
vt 0.000000 1.000000
vt 1.000000 1.000000

vn 0.000000 0.000000 1.000000
vn 0.000000 1.000000 0.000000
vn 0.000000 0.000000 -1.000000
vn 0.000000 -1.000000 0.000000

g triangle
usemtl triangle
s 1
f 1/1/1 2/2/1 3/3/1

If you checked out the OBJ file specification, you'll notice that there are a lot of features the above test file doesn't use. This blog series will likely use those later, but I am omitting those for now in the interest of building the simplest thing first and iterating from there. To that end, we will just handle parsing geometric vertices, texture vertices, and vertex normals for now.

The rest of the blog post will be language specific. Please use menu in the sidebar to navigate to your language of choice while following along.


  • If you wish to follow along, you can either copy and paste the code below or use this link to the repo with the code for this day's entry.

    First, we're going to set up some basic structs to track the information we're pulling from the obj file. The macro for allowing dead code is just to suppress warnings about it not being used elsewhere. I'll remove these later after we actually start using the point data, not just create an array of them.

    #[allow(dead_code)]
    struct Vec2f {
    	x: f32,
    	y: f32
    }
    
    #[allow(dead_code)]
    struct Vec3f {
    	x: f32,
    	y: f32,
    	z: f32
    }
    
    struct OBJFile {
    	vertex_buffer : Vec,
    	uv_buffer : Vec,
    	normal_buffer : Vec
    }

    Rust is helpful in tracking down potential type safety bugs during compile time. However, this means code to safely parse a string into a float can be quite verbose. I wrote a quick helper function as the length quickly got out of control without it.

    fn parse_float(s : &str) -> f32 {
        match s.parse::() {
            Ok(f) => return f,
            Err(err) => {
                println!("Error: {:?}", err);
                return 0 as f32
            },
        }
    }}

    Now we're on to the main portion. A quick note here about style. Rust will infer the type for you in many cases. However, I prefer my code to be as explicit as possible. Having that information on hand isn't immediately useful when writing code, but I found that it saves time during later debugging.

    use std::fs::File;
    use std::io::{BufRead, BufReader};
    
    fn parse_obj_file(reader : BufReader<File>) -> OBJFile {
    	let mut obj_file : OBJFile = OBJFile {
    		vertex_buffer : Vec::new(),
    		uv_buffer : Vec::new(),
    		normal_buffer : Vec::new()
    	};
    
    	for (_index, line) in reader.lines().enumerate() {
            let line = line.unwrap(); // TODO(JR): Handle errors
            let split : Vec<&str> = line.split(" ").collect::<Vec<&str>>();
            
            if split[0] == "v" {
            	let vertex : Vec3f = Vec3f{ 
                    x : parse_float(&split[1]), 
                    y : parse_float(&split[2]), 
                    z : parse_float(&split[3])
                };
    
            	obj_file.vertex_buffer.push(vertex);
            }
            else if split[0] == "vt" {
            	let uv : Vec2f = Vec2f{
                    x : parse_float(&split[1]),
                    y : parse_float(&split[2])
                };
    
                obj_file.uv_buffer.push(uv);
            }
            else if split[0] == "vn" {
            	let normal : Vec3f = Vec3f{
                    x : parse_float(&split[1]), 
                    y : parse_float(&split[2]), 
                    z : parse_float(&split[3])
                };
    
                obj_file.normal_buffer.push(normal);
            }
        }
    
        obj_file
    }
    
    fn main() {
    	let filename = "images/obj/triangle.obj";
    	let file = File::open(filename).unwrap();
        let reader : BufReader = BufReader::new(file);
        let _data : OBJFile = parse_obj_file(reader);
    }
  • #include <windows.h>
    #include <iostream>