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, andvndata. - 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:
- 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). - 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
malloccall for each array to allocate all the necessary memory at once. - Second Pass (Parsing): Read through the file a second time. This time, parse the floating-point numbers from the
v,vt, andvnlines 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:
- Read a line from the file using
fgets. - Check the first few characters to identify the tag (e.g., "v ", "vt ", "vn ").
- 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.
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.

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
realloccalls. - The C standard library provides powerful tools for this task:
fopen/fgets/fclosefor file reading andsscanffor 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.