Created
July 21, 2025 12:25
-
-
Save samloeschen/324b6a3176107e0d5b71998fa624a270 to your computer and use it in GitHub Desktop.
very basic obj loader in zig
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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