Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 57

Parsing OBJ Files for 3D Model Data

Recommended resources

Introduction

Hello! In our last lesson, you learned to describe the structure of a Wavefront OBJ file. You now understand the roles of the key data tagsโ€”v for vertex positions, vt for texture coordinates, vn for vertex normals, and f for facesโ€”and the crucial detail that OBJ files use 1-based indexing.

Today, we transition from theory to practice. Your learning outcome is to write a parser to load vertex positions, normals, and texture coordinates from an OBJ file. This is a significant step towards rendering complex models in our engine. We'll be writing a C function that can:

  • Open and read an OBJ text file.
  • Process the file line by line.
  • Identify lines containing v, vt, and vn data.
  • Parse the floating-point values from these lines.
  • Store this data into dynamically allocated arrays in memory.

This lesson will draw upon your C programming skills from Module 1, particularly file I/O and dynamic memory allocation (malloc). Let's get started.


1. The Parser's Strategy: A Two-Pass Approach

Before writing any code, we need a strategy. A common challenge when parsing files is not knowing how much data you'll need to store. How many vertices will a model have? We could use realloc to grow our arrays as we find more data, but this can be inefficient, as it may involve repeatedly copying large blocks of memory.

A more efficient and common strategy for this task is the two-pass approach:

  1. First Pass (Counting): Read through the entire file once with the sole purpose of counting the number of vertices (v), texture coordinates (vt), and normals (vn).
  2. Memory Allocation: Once the first pass is complete, you know exactly how many of each data type you need. You can now make a single malloc call for each array to allocate all the necessary memory at once.
  3. Second Pass (Parsing): Read through the file a second time. This time, parse the floating-point numbers from the v, vt, and vn lines and store them in the arrays you just allocated.

This method avoids the overhead of dynamic resizing and leads to cleaner, more performant code.

OpenGL with C++ 7: Loading Models

The following video from the GetIntoGameDev channel explains and implements this two-pass strategy. Although the code is in C++, the logic is identical to what we'll do in C.

Watch the segment from 14:48 to 16:35. The presenter first outlines the plan to read the file twice, then shows the first pass loop where they simply count up the occurrences of each data type. Focus on the why and how of this counting phase.


2. Reading and Tokenizing Lines in C

Now let's get into the C implementation details. The process for each line will be:

  1. Read a line from the file using fgets.
  2. Check the first few characters to identify the tag (e.g., "v ", "vt ", "vn ").
  3. If it's a tag we care about, parse the rest of the line to get the numeric values.

A standard C function for parsing formatted strings is sscanf. It's like printf but for reading from a string. For a vertex line like v 1.0 2.0 3.0, we could use it like this:

float x, y, z;
// We start reading from `line + 2` to skip the "v " tag
sscanf(line + 2, "%f %f %f", &x, &y, &z);

This is a very effective way to handle the parsing of the numbers.

Another key part of parsing is handling lines you don't know about. A robust parser shouldn't crash if it sees an unexpected tag like g or usemtl. It should simply ignore it and move on.

The following video is a long, live-coding session, but it wonderfully demonstrates the practical, iterative process of building a parser in C. You'll see the developer encounter unknown tags and decide how to handle them.

Parsing 3D File in C

This video by Tsoding Daily shows a C programmer parsing an OBJ file. It's a raw, unedited look at the process, which is very instructive. We'll watch a few separate clips.

Watch the following segments to see how a parser is built up to handle different line types: Encountering Unknown Tags (15:58 - 20:10): The developer's initial parser fails on mtllib and o tags. Watch how he adds simple if statements to detect these tags, print a warning, and continue parsing, rather than crashing. Identifying vn and vt (31:18 - 35:10): Here, he adds logic to identify and count vertex normals (vn) and texture coordinates (vt). Notice how he's building up the logic of the first pass we discussed. Handling More Unknowns (40:03 - 47:13): The parser then hits s and usemtl. Again, watch how he identifies them, researches their meaning, and adds logic to ignore them for now. Focus on the pattern: read a line, check its prefix, and either process it or ignore it. This is the core loop of our parser.

This iterative process of identifying what you need (v, vt, vn) and explicitly ignoring what you don't is fundamental to writing a robust parser.


3. Implementing the OBJ Parser Function

Let's bring these ideas together into a complete C function. We'll use a high-quality C programming article as our main reference. It presents a robust, from-scratch implementation that aligns perfectly with your goal of building an engine from the ground up.

Robust Wavefront OBJ model parsing in C

The article "Robust Wavefront OBJ model parsing in C" by Chris Wellons is a fantastic resource. We'll focus on the parts that construct the parser's main logic.

Read the section named "Putting it together". It clearly shows the two-pass structure in C. First, it shows a loop to count elements (m.nverts++, etc.). Then, it shows how memory is allocated (new(...) in his example is just a wrapper for malloc). Finally, it shows the second loop where it calls parsing functions (parsevert, parseface) to fill the allocated memory.

Let's break down how we can structure our own load_obj function based on this approach.

Step 1: Data Structures

First, you'll need structs to hold the data. You likely already have vec2 and vec3 from the math module, but if not, they are simple:

// In a suitable header file, e.g., "math_types.h"
typedef struct {
    float x, y, z;
} vec3;

typedef struct {
    float x, y;
} vec2;

// We'll also define a struct to hold all our parsed model data
typedef struct {
    vec3 *positions;
    int position_count;

    vec3 *normals;
    int normal_count;

    vec2 *texcoords;
    int texcoord_count;

    // We will populate this in the next lesson!
    // int *indices;
    // int index_count;
} model_data;

Step 2: The Parser Function Shell

Our function will take a filename and a pointer to our model_data struct to fill.




#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "math_types.h"

int load_obj(const char* filename, model_data* out_model) {
    FILE* file = fopen(filename, "r");
    if (!file) {
        fprintf(stderr, "Error: Could not open file %s\n", filename);
        return 0; // Failure
    }

    // Temporary counts
    int pos_cap = 0;
    int norm_cap = 0;
    int tex_cap = 0;

    char line[1024]; // Assume a max line length for now

    // --- FIRST PASS: COUNT ELEMENTS ---
    while (fgets(line, sizeof(line), file)) {
        if (strncmp(line, "v ", 2) == 0) {
            pos_cap++;
        } else if (strncmp(line, "vn ", 3) == 0) {
            norm_cap++;
        } else if (strncmp(line, "vt ", 3) == 0) {
            tex_cap++;
        }
    }

    // --- ALLOCATE MEMORY ---
    out_model->positions = malloc(pos_cap * sizeof(vec3));
    out_model->normals = malloc(norm_cap * sizeof(vec3));
    out_model->texcoords = malloc(tex_cap * sizeof(vec2));
    // Check if malloc failed...

    out_model->position_count = 0;
    out_model->normal_count = 0;
    out_model->texcoord_count = 0;

    // --- SECOND PASS: PARSE AND STORE DATA ---
    rewind(file); // Go back to the beginning of the file

    while (fgets(line, sizeof(line), file)) {
        if (strncmp(line, "v ", 2) == 0) {
            vec3* p = &out_model->positions[out_model->position_count++];
            sscanf(line, "v %f %f %f", &p->x, &p->y, &p->z);
        } else if (strncmp(line, "vn ", 3) == 0) {
            vec3* n = &out_model->normals[out_model->normal_count++];
            sscanf(line, "vn %f %f %f", &n->x, &n->y, &n->z);
        } else if (strncmp(line, "vt ", 3) == 0) {
            vec2* t = &out_model->texcoords[out_model->texcoord_count++];
            sscanf(line, "vt %f %f", &t->x, &t->y);
        }
        // We are ignoring 'f' lines for now.
    }

    fclose(file);
    return 1; // Success
}

This code skeleton directly implements the two-pass strategy. It is simple, readable, and efficient. It reads the file, counts the elements, allocates the exact amount of memory needed, rewinds the file, and then reads it again, this time using sscanf to parse the data into the allocated arrays.

Notice how we ignore f lines and any other line type. We now have three independent arrays of data, just as the learning outcome requires.

Vertex, Texture, and Face Mapping in OpenGL
This diagram shows how vertex positions, texture coordinates (UVs), and face definitions work together. Our parser has successfully loaded the `positions` and `texcoords`. The next step is to use the `f` lines to connect them.

Conclusion

Congratulations! You have now designed a complete C function to parse the geometric data from a Wavefront OBJ file. This is a non-trivial and essential piece of any game engine.

Key Takeaways:

  • A two-pass parsing strategy (count then parse) is highly efficient for files with unknown data counts, as it avoids expensive realloc calls.
  • The C standard library provides powerful tools for this task: fopen/fgets/fclose for file reading and sscanf for parsing formatted data from strings.
  • A robust parser should gracefully ignore lines and tags it doesn't recognize, preventing crashes from malformed or complex files.
  • The result of our parser is three separate, dynamically-allocated arrays containing all the positions, normals, and texture coordinates from the model file.

Preview of the Next Lesson:
Our parser successfully loads the "pools" of raw data, but it doesn't know how they connect to form triangles. The f (face) lines contain the indices that define this connectivity. In the next lesson, "Process face data from the OBJ file to generate an indexed vertex buffer for rendering," we will parse these f lines. This is the final and most complex step, as we will need to handle the v/vt/vn format and use those indices to assemble a final, unified vertex buffer that OpenGL can render.