478 lines
13 KiB
Zig
478 lines
13 KiB
Zig
const std = @import("std");
|
|
|
|
const ArrayListUnmanaged = std.ArrayListUnmanaged;
|
|
|
|
pub fn loadObj(filename: []const u8, allocator: std.mem.Allocator) !ObjContents {
|
|
return try ObjContents.load(filename, allocator);
|
|
}
|
|
|
|
pub fn loadObjBytes(bytes: []const u8, allocator: std.mem.Allocator) !ObjContents {
|
|
return try ObjContents.loadFromBytes(bytes, allocator);
|
|
}
|
|
|
|
// Higher level file functions.
|
|
pub fn loadFileAlloc(filename: []const u8, comptime alignment: usize, allocator: std.mem.Allocator) ![]const u8 {
|
|
var file = try std.fs.cwd().openFile(filename, .{ .mode = .read_only });
|
|
const filesize = (try file.stat()).size;
|
|
const buffer: []u8 = try allocator.alignedAlloc(u8, alignment, filesize);
|
|
try file.reader().readNoEof(buffer);
|
|
return buffer;
|
|
}
|
|
|
|
pub const ObjVec = struct {
|
|
x: f32,
|
|
y: f32,
|
|
z: f32,
|
|
};
|
|
|
|
pub const ObjVec2 = struct {
|
|
x: f32,
|
|
y: f32,
|
|
};
|
|
|
|
pub const ObjColor = struct {
|
|
r: f32,
|
|
g: f32,
|
|
b: f32,
|
|
a: f32,
|
|
};
|
|
|
|
pub const ObjMesh = struct {
|
|
object_name: []u8,
|
|
v_positions: ArrayListUnmanaged(ObjVec) = .{},
|
|
v_colors: ArrayListUnmanaged(ObjColor) = .{},
|
|
v_normals: ArrayListUnmanaged(ObjVec) = .{},
|
|
v_uvs: ArrayListUnmanaged(ObjVec2) = .{},
|
|
v_faces: ArrayListUnmanaged(ObjFace) = .{},
|
|
allocator: std.mem.Allocator,
|
|
|
|
pub fn validate_mesh(self: ObjMesh) !void {
|
|
for (self.v_faces.items) |face| {
|
|
var i: u32 = 0;
|
|
while (i < face.count) {
|
|
if (face.vertex[i] > self.v_positions.items.len)
|
|
return error.FaceReferencesInvalidVertex;
|
|
if (face.normal[i] > self.v_normals.items.len)
|
|
return error.FaceReferencesInvalidNormal;
|
|
if (face.texture[i] > self.v_uvs.items.len)
|
|
return error.FaceReferencesInvalidTexture;
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn print_stats(self: ObjMesh) void {
|
|
std.debug.print("obj: {s}, Vertices count = {d} Normals count = {d}, faces = {d} texures = {d}\n", .{
|
|
self.object_name,
|
|
self.v_positions.items.len,
|
|
self.v_normals.items.len,
|
|
self.v_faces.items.len,
|
|
self.v_uvs.items.len,
|
|
});
|
|
}
|
|
|
|
pub fn init(_object_name: []const u8, allocator: std.mem.Allocator) !ObjMesh {
|
|
const object_name = try allocator.alloc(u8, _object_name.len);
|
|
|
|
std.mem.copyForwards(u8, object_name, _object_name);
|
|
|
|
const self = ObjMesh{
|
|
.object_name = object_name,
|
|
.allocator = allocator,
|
|
};
|
|
|
|
return self;
|
|
}
|
|
|
|
pub fn setName(self: *ObjMesh, name: []const u8) !void {
|
|
self.allocator.free(self.object_name);
|
|
self.object_name = try self.allocator.alloc(u8, name.len);
|
|
std.mem.copyForwards(u8, self.object_name, name);
|
|
}
|
|
|
|
pub fn deinit(self: *ObjMesh) void {
|
|
self.v_positions.deinit(self.allocator);
|
|
self.v_colors.deinit(self.allocator);
|
|
self.v_normals.deinit(self.allocator);
|
|
self.v_uvs.deinit(self.allocator);
|
|
self.v_faces.deinit(self.allocator);
|
|
self.allocator.free(self.object_name);
|
|
}
|
|
};
|
|
|
|
pub const ObjFace = struct {
|
|
vertex: [4]u32,
|
|
texture: [4]u32,
|
|
normal: [4]u32,
|
|
count: u32,
|
|
|
|
pub fn init() ObjFace {
|
|
return .{
|
|
.vertex = .{ 0, 0, 0, 0 },
|
|
.texture = .{ 0, 0, 0, 0 },
|
|
.normal = .{ 0, 0, 0, 0 },
|
|
.count = 0,
|
|
};
|
|
}
|
|
};
|
|
|
|
const ResultType = enum {
|
|
vertex,
|
|
color,
|
|
normal,
|
|
face,
|
|
object,
|
|
group,
|
|
comment,
|
|
texture,
|
|
};
|
|
|
|
const LineParseResult = union(ResultType) {
|
|
vertex: ObjVec,
|
|
color: ObjColor,
|
|
normal: ObjVec,
|
|
face: ObjFace,
|
|
object: []const u8,
|
|
group: []const u8,
|
|
comment: []const u8,
|
|
texture: ObjVec2,
|
|
};
|
|
|
|
fn iterIntoVec2(iter: anytype) !ObjVec2 {
|
|
var vec: ObjVec2 = undefined;
|
|
var next: u32 = 0;
|
|
while (iter.next()) |tok| {
|
|
switch (next) {
|
|
0 => {
|
|
vec.x = try std.fmt.parseFloat(f32, tok);
|
|
},
|
|
1 => {
|
|
vec.y = try std.fmt.parseFloat(f32, tok);
|
|
},
|
|
else => {
|
|
return error.UnexpectedVectorPosition;
|
|
},
|
|
}
|
|
next += 1;
|
|
}
|
|
|
|
return vec;
|
|
}
|
|
|
|
fn iterIntoVec3(iter: anytype) !ObjVec {
|
|
var vec: ObjVec = undefined;
|
|
var next: u32 = 0;
|
|
while (iter.next()) |tok| {
|
|
switch (next) {
|
|
0 => {
|
|
vec.x = try std.fmt.parseFloat(f32, tok);
|
|
},
|
|
1 => {
|
|
vec.y = try std.fmt.parseFloat(f32, tok);
|
|
},
|
|
2 => {
|
|
vec.z = try std.fmt.parseFloat(f32, tok);
|
|
},
|
|
else => {
|
|
return error.UnexpectedVectorPosition;
|
|
},
|
|
}
|
|
next += 1;
|
|
}
|
|
|
|
return vec;
|
|
}
|
|
|
|
pub fn toksIntoFace(toks: anytype) !ObjFace {
|
|
var rv = ObjFace.init();
|
|
var count: u32 = 0;
|
|
while (toks.next()) |tok| {
|
|
if (tok.len == 0)
|
|
continue;
|
|
var face_desc = std.mem.tokenize(u8, tok, "/");
|
|
var ic: u32 = 0; // ic= inner_count
|
|
if (count >= 4) {
|
|
continue;
|
|
}
|
|
while (face_desc.next()) |prop| {
|
|
switch (ic) {
|
|
0 => {
|
|
rv.vertex[count] = std.fmt.parseInt(u32, prop, 10) catch 0;
|
|
},
|
|
1 => {
|
|
rv.texture[count] = std.fmt.parseInt(u32, prop, 10) catch 0;
|
|
},
|
|
2 => {
|
|
rv.normal[count] = std.fmt.parseInt(u32, prop, 10) catch 0;
|
|
},
|
|
else => return error.TooManyfaceProperties,
|
|
}
|
|
|
|
ic += 1;
|
|
}
|
|
count += 1;
|
|
}
|
|
|
|
rv.count = count;
|
|
|
|
if (rv.count > 4) {
|
|
std.debug.print("We have a face larger than 4 polys? polyCount = {d}\n", .{rv.count});
|
|
}
|
|
|
|
return rv;
|
|
}
|
|
|
|
fn parse_line(lineIn: []const u8, allocator: std.mem.Allocator) !LineParseResult {
|
|
_ = allocator;
|
|
var line: []const u8 = lineIn;
|
|
|
|
if (line.len <= 0)
|
|
return LineParseResult{ .comment = "<<empty line>>" };
|
|
while (line[0] == ' ') {
|
|
line = line[1..line.len];
|
|
}
|
|
|
|
var tokens = std.mem.tokenize(u8, line, " ");
|
|
const first = tokens.next().?;
|
|
|
|
if (line[0] == '#') {
|
|
return LineParseResult{
|
|
.comment = if (line.len > 1) line[1..line.len] else line[0..line.len],
|
|
};
|
|
}
|
|
|
|
if (std.mem.eql(u8, "mtllib", first)) {
|
|
return LineParseResult{
|
|
.comment = "gonna ignore mtllibs for now.",
|
|
};
|
|
}
|
|
|
|
if (std.mem.eql(u8, "vt", first)) {
|
|
return LineParseResult{
|
|
.texture = try iterIntoVec2(&tokens),
|
|
};
|
|
}
|
|
|
|
if (std.mem.eql(u8, "vn", first)) {
|
|
return LineParseResult{
|
|
.normal = try iterIntoVec3(&tokens),
|
|
};
|
|
}
|
|
|
|
if (std.mem.eql(u8, "v", first)) {
|
|
return LineParseResult{
|
|
.vertex = try iterIntoVec3(&tokens),
|
|
};
|
|
}
|
|
if (std.mem.eql(u8, "f", first)) {
|
|
return LineParseResult{
|
|
.face = try toksIntoFace(&tokens),
|
|
};
|
|
} else if (std.mem.eql(u8, "g", first)) {
|
|
// get a slice of the entire group
|
|
const groupName = line[first.len + 1 .. line.len];
|
|
return LineParseResult{
|
|
.group = groupName,
|
|
};
|
|
} else if (std.mem.eql(u8, "o", first)) {
|
|
// get a slice of the entire group
|
|
const groupName = line[first.len + 1 .. line.len];
|
|
// std.debug.print("LOADING OBJECT: {s}\n", .{groupName});
|
|
return LineParseResult{
|
|
.object = groupName,
|
|
};
|
|
} else if (std.mem.eql(u8, "usemtl", first)) {
|
|
return LineParseResult{
|
|
.comment = "not yet implemented",
|
|
};
|
|
} else if (std.mem.eql(u8, "s", first)) {
|
|
return LineParseResult{
|
|
.comment = "not yet implemented",
|
|
};
|
|
}
|
|
|
|
// std.debug.print("error: unrecognized header: `{s}` in line: {s} \n", .{ first, line });
|
|
|
|
return error.NotImplemented;
|
|
}
|
|
|
|
test "parse_vector" {
|
|
{
|
|
const result = try parse_line("v 0.437500 0.765625 -0.164063", std.testing.allocator);
|
|
std.debug.print("\n", .{});
|
|
std.debug.print("{any}\n", .{result});
|
|
try std.testing.expect(result == .vertex);
|
|
}
|
|
|
|
{
|
|
const result = try parse_line("vn 0.437500 0.765625 -0.164063", std.testing.allocator);
|
|
std.debug.print("\n", .{});
|
|
std.debug.print("{any}\n", .{result});
|
|
try std.testing.expect(result == .normal);
|
|
}
|
|
|
|
{
|
|
const result = try parse_line("f 47//1 1//1 3//1 45//1", std.testing.allocator);
|
|
std.debug.print("\n", .{});
|
|
std.debug.print("{any}\n", .{result});
|
|
try std.testing.expect(result == .face);
|
|
}
|
|
|
|
{
|
|
const result = try parse_line("f 47//1 1//1 3//1 ", std.testing.allocator);
|
|
std.debug.print("\n", .{});
|
|
std.debug.print("{any}\n", .{result});
|
|
try std.testing.expect(result == .face);
|
|
}
|
|
|
|
{
|
|
const result = try parse_line("o Suzanne", std.testing.allocator);
|
|
std.debug.print("\n", .{});
|
|
std.debug.print("{any}\n", .{result});
|
|
try std.testing.expect(result == .object);
|
|
}
|
|
|
|
{
|
|
const result = try parse_line("g Suzanne", std.testing.allocator);
|
|
std.debug.print("\n", .{});
|
|
std.debug.print("{any}\n", .{result});
|
|
try std.testing.expect(result == .group);
|
|
}
|
|
|
|
{
|
|
const result = try parse_line(" # this is a comment ", std.testing.allocator);
|
|
std.debug.print("\n", .{});
|
|
try std.testing.expect(result == .comment);
|
|
std.debug.print("{s}\n", .{result.comment});
|
|
}
|
|
}
|
|
|
|
pub fn fileIntoLines(file_contents: []const u8) std.mem.SplitIterator(u8, .sequence) {
|
|
// find a \n and see if it has \r\n
|
|
var index: u32 = 0;
|
|
while (index < file_contents.len) : (index += 1) {
|
|
if (file_contents[index] == '\n') {
|
|
if (index > 0) {
|
|
if (file_contents[index - 1] == '\r') {
|
|
return std.mem.split(u8, file_contents, "\r\n");
|
|
} else {
|
|
return std.mem.split(u8, file_contents, "\n");
|
|
}
|
|
} else {
|
|
return std.mem.split(u8, file_contents, "\n");
|
|
}
|
|
}
|
|
}
|
|
return std.mem.split(u8, file_contents, "\n");
|
|
}
|
|
|
|
const ObjContents = struct {
|
|
meshes: std.ArrayList(ObjMesh),
|
|
|
|
pub fn load(file_path: []const u8, allocator: std.mem.Allocator) !ObjContents {
|
|
const file_contents = try loadFileAlloc(file_path, 1, allocator);
|
|
defer allocator.free(file_contents);
|
|
|
|
return try loadFromBytes(file_contents, allocator);
|
|
}
|
|
|
|
pub fn loadFromBytes(file_contents: []const u8, allocator: std.mem.Allocator) !ObjContents {
|
|
var self = ObjContents{
|
|
.meshes = std.ArrayList(ObjMesh).init(allocator),
|
|
};
|
|
|
|
var mesh = try ObjMesh.init("root", allocator);
|
|
var lines = fileIntoLines(file_contents);
|
|
|
|
while (lines.next()) |line| {
|
|
const result = parse_line(line, allocator) catch continue;
|
|
|
|
if (result == .object) {
|
|
if (mesh.v_positions.items.len > 0) {
|
|
try self.meshes.append(mesh);
|
|
mesh = try ObjMesh.init(result.object, allocator);
|
|
} else {
|
|
try mesh.setName(result.object);
|
|
}
|
|
}
|
|
|
|
if (result == .vertex) {
|
|
try mesh.v_positions.append(mesh.allocator, result.vertex);
|
|
}
|
|
|
|
if (result == .normal) {
|
|
try mesh.v_normals.append(mesh.allocator, result.normal);
|
|
}
|
|
|
|
if (result == .face) {
|
|
try mesh.v_faces.append(mesh.allocator, result.face);
|
|
}
|
|
|
|
if (result == .texture) {
|
|
try mesh.v_uvs.append(mesh.allocator, result.texture);
|
|
}
|
|
}
|
|
try self.meshes.append(mesh);
|
|
return self;
|
|
}
|
|
|
|
pub fn deinit(self: *ObjContents) void {
|
|
for (self.meshes.items, 0..) |_, i| {
|
|
self.meshes.items[i].deinit();
|
|
}
|
|
self.meshes.deinit();
|
|
}
|
|
};
|
|
|
|
test "load_monkey_full" {
|
|
const monkey_obj_path = "./content/monkey.obj";
|
|
var obj_contents = try ObjContents.load(monkey_obj_path, std.testing.allocator);
|
|
defer obj_contents.deinit();
|
|
try std.testing.expect(obj_contents.meshes.items.len == 1);
|
|
// obj_contents.meshes.items[0].print_stats();
|
|
}
|
|
|
|
test "parse_monkey" {
|
|
const monkey_obj_path = "./content/monkey.obj";
|
|
|
|
const file_contents = try loadFileAlloc(monkey_obj_path, 1, std.testing.allocator);
|
|
defer std.testing.allocator.free(file_contents);
|
|
var lines = fileIntoLines(file_contents);
|
|
var count: u32 = 0;
|
|
var vertex_count: u32 = 0;
|
|
var normal_count: u32 = 0;
|
|
var faces_count: u32 = 0;
|
|
var texture_count: u32 = 0;
|
|
|
|
while (lines.next()) |line| {
|
|
const result = parse_line(line, std.testing.allocator) catch {
|
|
continue;
|
|
};
|
|
if (result == .vertex)
|
|
vertex_count += 1;
|
|
|
|
if (result == .normal)
|
|
normal_count += 1;
|
|
|
|
if (result == .face)
|
|
faces_count += 1;
|
|
|
|
if (result == .texture)
|
|
texture_count += 1;
|
|
|
|
count += 1;
|
|
}
|
|
|
|
std.debug.print("{s} loaded, Vertices count = {d} Normals count = {d}, faces = {d}\n", .{
|
|
monkey_obj_path,
|
|
vertex_count,
|
|
normal_count,
|
|
faces_count,
|
|
});
|
|
}
|
|
|
|
test "ObjIndexMesh" {
|
|
const monkey_obj_path = "./content/monkey.obj";
|
|
_ = monkey_obj_path;
|
|
}
|