Skip to content

Instantly share code, notes, and snippets.

@samloeschen
Created July 21, 2025 12:25
Show Gist options
  • Save samloeschen/324b6a3176107e0d5b71998fa624a270 to your computer and use it in GitHub Desktop.
Save samloeschen/324b6a3176107e0d5b71998fa624a270 to your computer and use it in GitHub Desktop.
very basic obj loader in zig
const std = @import("std");
const math = @import("math.zig");
const ObjLineType = enum {
Vertex,
Normal,
Texcoord,
Face,
};
const ObjFaceElement = struct {
pos_idx: usize,
texcoord_idx: ?usize = null,
normal_idx: ?usize = null,
};
const ObjVertex = struct {
position: math.Vec3f,
color: ?math.Vec3f,
};
const ObjLine = union(ObjLineType) {
Vertex: ObjVertex,
Normal: math.Vec3f,
Texcoord: math.Vec2f,
Face: []const u8,
};
// TODO handle 's', 'o'
pub const ObjMeshData = struct {
positions_buffer: std.ArrayList(math.Vec3f),
index_buffer: std.ArrayList(u32),
colors_buffer: ?std.ArrayList(math.Vec3f),
texcoord_buffer: ?std.ArrayList(math.Vec2f),
normals_buffer: ?std.ArrayList(math.Vec3f),
};
pub fn parseData(data: []const u8, allocator: std.mem.Allocator) !ObjMeshData {
var lines_iter = std.mem.tokenizeScalar(u8, data, '\n');
var scratch_arena = std.heap.ArenaAllocator.init(allocator);
defer scratch_arena.deinit();
var obj_vertices = std.ArrayList(ObjVertex).init(scratch_arena.allocator());
var obj_normals = std.ArrayList(math.Vec3f).init(scratch_arena.allocator());
var obj_texcoords = std.ArrayList(math.Vec2f).init(scratch_arena.allocator());
var obj_faces = std.ArrayList([]const u8).init(scratch_arena.allocator());
var has_vertex_colors = false;
var has_normals = false;
var has_texcoords = false;
while (lines_iter.next()) |line| {
if (try parseLine(line)) |elem| switch (elem) {
.Vertex => |vertex| blk: {
if (vertex.color != null) {
has_vertex_colors = true;
}
try obj_vertices.append(vertex);
break :blk;
},
.Normal => |normal| try obj_normals.append(normal),
.Texcoord => |texcoord| try obj_texcoords.append(texcoord),
.Face => |slice| try obj_faces.append(slice),
};
}
// parse each face into a list of elements
var all_elements = std.ArrayList(ObjFaceElement).init(scratch_arena.allocator());
for (obj_faces.items) |slice| {
var face_elements = std.ArrayList(ObjFaceElement).init(scratch_arena.allocator());
var face_iter = std.mem.tokenizeAny(u8, slice, &std.ascii.whitespace);
while (face_iter.next()) |vertex| {
if (vertex.len == 0) continue;
var elem_iter = std.mem.tokenizeScalar(u8, vertex, '/');
if (elem_iter.next()) |pos_str| {
var elem = ObjFaceElement{
.pos_idx = try std.fmt.parseInt(usize, pos_str, 10),
};
if (elem_iter.next()) |tc| if (tc.len > 0) {
elem.texcoord_idx = try std.fmt.parseInt(usize, tc, 10);
has_texcoords = true;
};
if (elem_iter.next()) |norm| if (norm.len > 0) {
elem.normal_idx = try std.fmt.parseInt(usize, norm, 10);
has_normals = true;
};
try face_elements.append(elem);
}
}
// this face has more than three vertices, so we need to triangulate it
if (face_elements.items.len > 3) {
const first_element = face_elements.items[0];
const old_elements = try scratch_arena.allocator().dupe(ObjFaceElement, face_elements.items[1..]);
face_elements.clearRetainingCapacity();
for (0..old_elements.len - 1) |i| {
try face_elements.append(first_element);
try face_elements.append(old_elements[i]);
try face_elements.append(old_elements[i + 1]);
}
}
try all_elements.appendSlice(face_elements.items);
}
// make new buffers/indices from the face elements
var mesh_positions = std.ArrayList(math.Vec3f).init(allocator);
var mesh_indices = std.ArrayList(u32).init(allocator);
var mesh_colors: ?std.ArrayList(math.Vec3f) = null;
if (has_vertex_colors) {
mesh_colors = .init(allocator);
}
var mesh_texcoords: ?std.ArrayList(math.Vec2f) = null;
if (has_texcoords) {
mesh_texcoords = .init(allocator);
}
var mesh_normals: ?std.ArrayList(math.Vec3f) = null;
if (has_normals) {
mesh_normals = .init(allocator);
}
// could be smaller, but this allows for every index to point to unique data
var corrected_idx_map = try scratch_arena.allocator().alloc(?usize, all_elements.items.len + 1);
@memset(corrected_idx_map, null);
for (all_elements.items) |elem| {
const pos_idx = elem.pos_idx;
if (corrected_idx_map[pos_idx]) |corrected_idx| {
try mesh_indices.append(@as(u32, @intCast(corrected_idx)));
} else {
const corrected_idx = mesh_positions.items.len;
corrected_idx_map[elem.pos_idx] = corrected_idx;
try mesh_indices.append(@as(u32, @intCast(corrected_idx)));
const vertex = obj_vertices.items[elem.pos_idx - 1];
try mesh_positions.append(vertex.position);
if (mesh_colors) |*buf| {
const color: math.Vec3f = if (vertex.color) |c| c else .ZERO;
try buf.append(color);
}
if (mesh_normals) |*buf| {
const normal: math.Vec3f = if (elem.normal_idx) |n| obj_normals.items[n - 1] else .ZERO;
try buf.append(normal);
}
if (mesh_texcoords) |*buf| {
const tc: math.Vec2f = if (elem.texcoord_idx) |tc| obj_texcoords.items[tc - 1] else .ZERO;
try buf.append(tc);
}
}
}
return .{
.positions_buffer = mesh_positions,
.index_buffer = mesh_indices,
.colors_buffer = mesh_colors,
.texcoord_buffer = mesh_texcoords,
.normals_buffer = mesh_normals,
};
}
fn parseLine(line: []const u8) !?ObjLine {
if (line.len == 0) return null;
// trim leading whitespace
const slice = std.mem.trimLeft(u8, line, &std.ascii.whitespace);
var iter = std.mem.tokenizeAny(u8, slice, &std.ascii.whitespace);
errdefer {
std.debug.print("error on this line -> {s}\n", .{line});
}
if (iter.next()) |first| {
if (std.mem.eql(u8, first, "v")) {
var position = math.Vec3f.ZERO;
var color: ?math.Vec3f = null;
comptime var axis: i32 = 0;
inline while (axis < 3) : (axis += 1) {
if (iter.next()) |num_str| {
const value = try std.fmt.parseFloat(f32, num_str);
position.setAxis(axis, value);
} else {
break;
}
}
// vertices can optionally define a color
if (iter.peek() != null) {
color = .ZERO;
comptime var channel: i32 = 0;
inline while (channel < 3) : (channel += 1) {
if (iter.next()) |num_str| {
const value = try std.fmt.parseFloat(f32, num_str);
color.?.setAxis(channel, value);
}
}
}
return .{
.Vertex = .{
.position = position,
.color = color,
},
};
} else if (std.mem.eql(u8, first, "vt")) {
var texcoord = math.Vec2f.ZERO;
comptime var axis: i32 = 0;
inline while (axis < 2) : (axis += 1) {
if (iter.next()) |num_str| {
const value = try std.fmt.parseFloat(f32, num_str);
texcoord.setAxis(axis, value);
}
}
return .{
.Texcoord = texcoord,
};
} else if (std.mem.eql(u8, first, "vn")) {
var normal = math.Vec3f.ZERO;
comptime var axis: i32 = 0;
inline while (axis < 3) : (axis += 1) {
if (iter.next()) |num_str| {
const value = try std.fmt.parseFloat(f32, num_str);
normal.setAxis(axis, value);
}
}
return .{
.Normal = normal,
};
} else if (std.mem.eql(u8, first, "f")) {
return .{
.Face = iter.rest(),
};
}
}
return null;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment