├── .envrc ├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.org ├── build.zig ├── build.zig.zon ├── examples ├── cube.mtl ├── cube.obj ├── empty.mtl ├── plateball.mtl ├── single.mtl ├── triangle.mtl ├── triangle.obj ├── triangle_error.obj ├── triangle_two.obj ├── triangle_windows.mtl └── triangle_windows.obj ├── flake.lock ├── flake.nix ├── src ├── main.zig ├── mtl.zig ├── obj.zig └── utils.zig └── test.zig /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: '0 12 * * *' 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: goto-bus-stop/setup-zig@v2 21 | with: 22 | version: master 23 | - run: zig build test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .zig-cache 2 | zig-cache 3 | zig-out 4 | 5 | .zigmod 6 | deps.zig -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2023 Andreas Arvidsson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * zig-obj 2 | [[https://github.com/chip2n/zig-obj/workflows/CI/badge.svg]] 3 | 4 | Minimal Zig parser for ~.obj~ and ~.mtl~ files. 5 | 6 | ** Features 7 | 8 | The following features are implemented: 9 | 10 | OBJ files: 11 | - Vertices 12 | - Texture coordinates 13 | - Normals 14 | - Objects 15 | 16 | MTL files: 17 | - Bump map 18 | - Diffuse map 19 | - Specular map 20 | - Ambient map 21 | - Roughness map 22 | - Metallic map 23 | - Sheen map 24 | - Emissive map 25 | - Normal map 26 | - Ambient color 27 | - Diffuse color 28 | - Specular color 29 | - Specular highlight 30 | - Emissive coefficient 31 | - Optical density 32 | - Dissolve 33 | - Illumination 34 | - Roughness 35 | - Metallic 36 | - Sheen 37 | - Clearcoat thickness 38 | - Clearcoat roughness 39 | - Anisotropy 40 | - Anisotropy rotation 41 | 42 | If something is missing or not working properly, feel free to open an issue/pull 43 | request and I'll take a look. 44 | 45 | ** Getting started 46 | 47 | Add module to your projects ~build.zig.zon~ file: 48 | 49 | #+begin_src bash 50 | zig fetch --save git+https://github.com/chip2n/zig-obj.git 51 | #+end_src 52 | 53 | Add the dependency to your executable in ~build.zig~: 54 | 55 | #+begin_src zig 56 | pub fn build(b: *std.build.Builder) void { 57 | ... 58 | const obj_mod = b.dependency("obj", .{ .target = target, .optimize = optimize }).module("obj"); 59 | exe_mod.addImport("obj", obj_mod); 60 | } 61 | #+end_src 62 | 63 | ** Building a static library 64 | 65 | Build a static library by running: 66 | 67 | #+begin_src bash 68 | zig build 69 | #+end_src 70 | 71 | ** Usage 72 | 73 | #+begin_src zig 74 | const obj = @import("obj"); 75 | 76 | var model = try obj.parseObj(allocator, @embedFile("cube.obj")); 77 | defer model.deinit(allocator); 78 | var material = try obj.parseMtl(allocator, @embedFile("cube.mtl")); 79 | defer material.deinit(allocator); 80 | #+end_src 81 | 82 | ** Running tests 83 | 84 | Tests are being ran automatically each day using the nightly Zig build. 85 | 86 | Run the test suite manually with: 87 | 88 | #+begin_src bash 89 | zig build test 90 | #+end_src 91 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const Mode = std.builtin.Mode; 3 | 4 | pub fn build(b: *std.Build) void { 5 | const target = b.standardTargetOptions(.{}); 6 | const optimize = b.standardOptimizeOption(.{}); 7 | 8 | _ = b.addModule( 9 | "obj", 10 | .{ .root_source_file = b.path("src/main.zig") }, 11 | ); 12 | 13 | const lib = b.addStaticLibrary(.{ 14 | .name = "zig-obj", 15 | .root_source_file = b.path("src/main.zig"), 16 | .target = target, 17 | .optimize = optimize, 18 | }); 19 | b.installArtifact(lib); 20 | 21 | const main_tests = b.addTest(.{ 22 | .root_source_file = b.path("test.zig"), 23 | .target = target, 24 | .optimize = optimize, 25 | }); 26 | 27 | const run_main_tests = b.addRunArtifact(main_tests); 28 | const test_step = b.step("test", "Run library tests"); 29 | test_step.dependOn(&run_main_tests.step); 30 | } 31 | 32 | inline fn rootDir() []const u8 { 33 | return comptime std.fs.path.dirname(@src().file) orelse "."; 34 | } 35 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | .name = .obj, 3 | .version = "3.0.0", 4 | .fingerprint = 0x4666d46c8a9fc3bf, // Changing this has security and trust implications. 5 | .dependencies = .{}, 6 | .paths = .{ 7 | "examples", 8 | "src", 9 | "build.zig", 10 | "build.zig.zon", 11 | "LICENSE", 12 | "README.org", 13 | }, 14 | } 15 | -------------------------------------------------------------------------------- /examples/cube.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'None' 2 | # Material Count: 1 3 | 4 | newmtl Material 5 | Ns 323.999994 6 | Ka 1.000000 1.000000 1.000000 7 | Kd 0.800000 0.800000 0.800000 8 | Ks 0.500000 0.500000 0.500000 9 | Ke 0.000000 0.000000 0.000000 10 | Ni 1.450000 11 | d 1.000000 12 | illum 2 13 | -------------------------------------------------------------------------------- /examples/cube.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.91.0 OBJ File: '' 2 | # www.blender.org 3 | mtllib cube.mtl 4 | o Cube 5 | v 1.000000 1.000000 -1.000000 6 | v 1.000000 -1.000000 -1.000000 7 | v 1.000000 1.000000 1.000000 8 | v 1.000000 -1.000000 1.000000 9 | v -1.000000 1.000000 -1.000000 10 | v -1.000000 -1.000000 -1.000000 11 | v -1.000000 1.000000 1.000000 12 | v -1.000000 -1.000000 1.000000 13 | vt 0.625000 0.500000 14 | vt 0.875000 0.500000 15 | vt 0.875000 0.750000 16 | vt 0.625000 0.750000 17 | vt 0.375000 0.750000 18 | vt 0.625000 1.000000 19 | vt 0.375000 1.000000 20 | vt 0.375000 0.000000 21 | vt 0.625000 0.000000 22 | vt 0.625000 0.250000 23 | vt 0.375000 0.250000 24 | vt 0.125000 0.500000 25 | vt 0.375000 0.500000 26 | vt 0.125000 0.750000 27 | vn 0.0000 1.0000 0.0000 28 | vn 0.0000 0.0000 1.0000 29 | vn -1.0000 0.0000 0.0000 30 | vn 0.0000 -1.0000 0.0000 31 | vn 1.0000 0.0000 0.0000 32 | vn 0.0000 0.0000 -1.0000 33 | usemtl Material 34 | s off 35 | f 1/1/1 5/2/1 7/3/1 3/4/1 36 | f 4/5/2 3/4/2 7/6/2 8/7/2 37 | f 8/8/3 7/9/3 5/10/3 6/11/3 38 | f 6/12/4 2/13/4 4/5/4 8/14/4 39 | f 2/13/5 1/1/5 3/4/5 4/5/5 40 | f 6/11/6 5/10/6 1/1/6 2/13/6 41 | -------------------------------------------------------------------------------- /examples/empty.mtl: -------------------------------------------------------------------------------- 1 | # Blender 3.4.1 MTL File: 'sphere.blend' 2 | # www.blender.org 3 | -------------------------------------------------------------------------------- /examples/plateball.mtl: -------------------------------------------------------------------------------- 1 | # Blender 4.3.1 MTL File: 'None' 2 | # www.blender.org 3 | 4 | newmtl Material.001 5 | Ks 0.500000 0.500000 0.500000 6 | Ke 0.000000 0.000000 0.000000 7 | Ni 1.500000 8 | d 1.000000 9 | illum 2 10 | Ps 0.000000 11 | Pc 0.000000 12 | Pcr 0.030000 13 | aniso 0.000000 14 | anisor 0.000000 15 | map_Kd metal_plate_diff_1k.jpg 16 | map_Pm metal_plate_rough_1k.jpg 17 | map_Pr metal_plate_metal_1k.jpg 18 | map_Bump -bm 1.000000 metal_plate_nor_gl_1k.jpg 19 | -------------------------------------------------------------------------------- /examples/single.mtl: -------------------------------------------------------------------------------- 1 | # Material Count: 8 2 | 3 | newmtl Material 4 | Ns 225.000000 5 | Ka 1.000000 1.000000 1.000000 6 | Kd 0.900000 0.900000 0.900000 7 | Ks 0.800000 0.800000 0.800000 8 | Ke 0.700000 0.700000 0.700000 9 | Ni 1.450000 10 | d 1.000000 11 | illum 2 12 | map_Bump /path/to/bump.png 13 | map_Kd /path/to/diffuse.png 14 | map_Ns /path/to/specular.png -------------------------------------------------------------------------------- /examples/triangle.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'None' 2 | # Material Count: 1 3 | 4 | newmtl None 5 | Ns 500 6 | Ka 0.8 0.8 0.8 7 | Kd 0.8 0.8 0.8 8 | Ks 0.8 0.8 0.8 9 | d 1 10 | illum 2 11 | -------------------------------------------------------------------------------- /examples/triangle.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.91.0 OBJ File: '' 2 | # www.blender.org 3 | mtllib triangle.mtl 4 | o Plane 5 | v -1.000000 0.000000 0.000000 6 | v 1.000000 0.000000 1.000000 7 | v 1.000000 0.000000 -1.000000 8 | vt 0.000000 0.000000 9 | vt 1.000000 0.000000 10 | vt 1.000000 1.000000 11 | vn 0.0000 1.0000 0.0000 12 | usemtl None 13 | s off 14 | f 1/1/1 2/2/1 3/3/1 15 | -------------------------------------------------------------------------------- /examples/triangle_error.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.91.0 OBJ File: '' 2 | # www.blender.org 3 | mtllib triangle.mtl 4 | o Plane 5 | v -1.000000 0.000000 0.000000 6 | v 1.000000 0.000000 1.000000 7 | v 1.000000 0.000000 -1.000000 8 | vt 0.000000 0.000000 9 | vt 1.000000 0.000000 10 | vt 1.000000 1.000000 11 | vn 0.0000 1.0000 0.0000 12 | usemtl None 13 | s off 14 | f 1/1/1 2/2/1 3/3/1 15 | invalid 16 | -------------------------------------------------------------------------------- /examples/triangle_two.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.91.0 OBJ File: '' 2 | # www.blender.org 3 | mtllib triangle.mtl 4 | o Plane 5 | v -1.000000 0.000000 0.000000 6 | v 1.000000 0.000000 1.000000 7 | v 1.000000 0.000000 -1.000000 8 | vt 0.000000 0.000000 9 | vt 1.000000 0.000000 10 | vt 1.000000 1.000000 11 | vn 0.0000 1.0000 0.0000 12 | usemtl None 13 | s off 14 | f 1/1/1 2/2/1 3/3/1 15 | 16 | o Plane2 17 | f 1/1/1 2/2/1 3/3/1 18 | -------------------------------------------------------------------------------- /examples/triangle_windows.mtl: -------------------------------------------------------------------------------- 1 | # Blender MTL File: 'None' 2 | # Material Count: 1 3 | 4 | newmtl None 5 | Ns 500 6 | Ka 0.8 0.8 0.8 7 | Kd 0.8 0.8 0.8 8 | Ks 0.8 0.8 0.8 9 | d 1 10 | illum 2 11 | -------------------------------------------------------------------------------- /examples/triangle_windows.obj: -------------------------------------------------------------------------------- 1 | # Blender v2.91.0 OBJ File: '' 2 | # www.blender.org 3 | mtllib triangle_windows.mtl 4 | o Plane 5 | v -1.000000 0.000000 0.000000 6 | v 1.000000 0.000000 1.000000 7 | v 1.000000 0.000000 -1.000000 8 | vt 0.000000 0.000000 9 | vt 1.000000 0.000000 10 | vt 1.000000 1.000000 11 | vn 0.0000 1.0000 0.0000 12 | usemtl None 13 | s off 14 | f 1/1/1 2/2/1 3/3/1 15 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "flake": false, 5 | "locked": { 6 | "lastModified": 1696426674, 7 | "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 8 | "owner": "edolstra", 9 | "repo": "flake-compat", 10 | "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 11 | "type": "github" 12 | }, 13 | "original": { 14 | "owner": "edolstra", 15 | "repo": "flake-compat", 16 | "type": "github" 17 | } 18 | }, 19 | "flake-utils": { 20 | "inputs": { 21 | "systems": "systems" 22 | }, 23 | "locked": { 24 | "lastModified": 1731533236, 25 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 26 | "owner": "numtide", 27 | "repo": "flake-utils", 28 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "numtide", 33 | "repo": "flake-utils", 34 | "type": "github" 35 | } 36 | }, 37 | "flake-utils_2": { 38 | "inputs": { 39 | "systems": "systems_2" 40 | }, 41 | "locked": { 42 | "lastModified": 1705309234, 43 | "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", 44 | "owner": "numtide", 45 | "repo": "flake-utils", 46 | "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "numtide", 51 | "repo": "flake-utils", 52 | "type": "github" 53 | } 54 | }, 55 | "nixpkgs": { 56 | "locked": { 57 | "lastModified": 1720535198, 58 | "narHash": "sha256-zwVvxrdIzralnSbcpghA92tWu2DV2lwv89xZc8MTrbg=", 59 | "owner": "nixos", 60 | "repo": "nixpkgs", 61 | "rev": "205fd4226592cc83fd4c0885a3e4c9c400efabb5", 62 | "type": "github" 63 | }, 64 | "original": { 65 | "owner": "nixos", 66 | "ref": "nixos-23.11", 67 | "repo": "nixpkgs", 68 | "type": "github" 69 | } 70 | }, 71 | "nixpkgs_2": { 72 | "locked": { 73 | "lastModified": 1708161998, 74 | "narHash": "sha256-6KnemmUorCvlcAvGziFosAVkrlWZGIc6UNT9GUYr0jQ=", 75 | "owner": "NixOS", 76 | "repo": "nixpkgs", 77 | "rev": "84d981bae8b5e783b3b548de505b22880559515f", 78 | "type": "github" 79 | }, 80 | "original": { 81 | "owner": "NixOS", 82 | "ref": "nixos-23.11", 83 | "repo": "nixpkgs", 84 | "type": "github" 85 | } 86 | }, 87 | "root": { 88 | "inputs": { 89 | "flake-utils": "flake-utils", 90 | "nixpkgs": "nixpkgs", 91 | "zig_overlay": "zig_overlay" 92 | } 93 | }, 94 | "systems": { 95 | "locked": { 96 | "lastModified": 1681028828, 97 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 98 | "owner": "nix-systems", 99 | "repo": "default", 100 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 101 | "type": "github" 102 | }, 103 | "original": { 104 | "owner": "nix-systems", 105 | "repo": "default", 106 | "type": "github" 107 | } 108 | }, 109 | "systems_2": { 110 | "locked": { 111 | "lastModified": 1681028828, 112 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 113 | "owner": "nix-systems", 114 | "repo": "default", 115 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 116 | "type": "github" 117 | }, 118 | "original": { 119 | "owner": "nix-systems", 120 | "repo": "default", 121 | "type": "github" 122 | } 123 | }, 124 | "zig_overlay": { 125 | "inputs": { 126 | "flake-compat": "flake-compat", 127 | "flake-utils": "flake-utils_2", 128 | "nixpkgs": "nixpkgs_2" 129 | }, 130 | "locked": { 131 | "lastModified": 1740831041, 132 | "narHash": "sha256-g/kh9kOWVfWQfxcQSWRgSUCAkOCJMqvEQecGA4S9bL4=", 133 | "owner": "mitchellh", 134 | "repo": "zig-overlay", 135 | "rev": "2e8e91407fc014324a46f2f20849772cf0dd2155", 136 | "type": "github" 137 | }, 138 | "original": { 139 | "owner": "mitchellh", 140 | "repo": "zig-overlay", 141 | "type": "github" 142 | } 143 | } 144 | }, 145 | "root": "root", 146 | "version": 7 147 | } 148 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs = { 3 | nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11"; 4 | flake-utils.url = "github:numtide/flake-utils"; 5 | zig_overlay.url = "github:mitchellh/zig-overlay"; 6 | }; 7 | 8 | outputs = { self, nixpkgs, flake-utils, zig_overlay, ...}: 9 | flake-utils.lib.eachDefaultSystem (system: let 10 | pkgs = import nixpkgs { inherit system; }; 11 | zig = zig_overlay.packages.${system}.master-2025-03-01; 12 | in { 13 | devShell = pkgs.mkShell { 14 | packages = [ 15 | zig 16 | ]; 17 | shellHook = '' 18 | echo "zig $(zig version)" 19 | ''; 20 | }; 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | const obj = @import("obj.zig"); 4 | const mtl = @import("mtl.zig"); 5 | 6 | pub const parseObj = obj.parse; 7 | pub const parseObjCustom = obj.parseCustom; 8 | pub const ObjData = obj.ObjData; 9 | pub const Mesh = obj.Mesh; 10 | 11 | pub const parseMtl = mtl.parse; 12 | pub const parseMtlCustom = mtl.parseCustom; 13 | pub const MaterialData = mtl.MaterialData; 14 | pub const Material = mtl.Material; 15 | 16 | test "zig-obj" { 17 | std.testing.refAllDecls(@This()); 18 | } 19 | -------------------------------------------------------------------------------- /src/mtl.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tokenizeAny = std.mem.tokenizeAny; 3 | const Allocator = std.mem.Allocator; 4 | 5 | const lineIterator = @import("utils.zig").lineIterator; 6 | 7 | pub const MaterialData = struct { 8 | materials: std.StringHashMapUnmanaged(Material), 9 | 10 | pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void { 11 | var iter = self.materials.iterator(); 12 | while (iter.next()) |m| { 13 | m.value_ptr.deinit(allocator); 14 | allocator.free(m.key_ptr.*); 15 | } 16 | self.materials.deinit(allocator); 17 | } 18 | 19 | const Builder = struct { 20 | allocator: Allocator, 21 | current_material: Material = .{}, 22 | current_name: ?[]const u8 = null, 23 | materials: std.StringHashMapUnmanaged(Material) = .{}, 24 | 25 | fn onError(self: *Builder) void { 26 | var iter = self.materials.iterator(); 27 | while (iter.next()) |m| { 28 | m.value_ptr.deinit(self.allocator); 29 | self.allocator.free(m.key_ptr.*); 30 | } 31 | self.materials.deinit(self.allocator); 32 | if (self.current_name) |n| 33 | self.allocator.free(n); 34 | } 35 | 36 | fn finish(self: *Builder) !MaterialData { 37 | if (self.current_name) |nm| 38 | try self.materials.put(self.allocator, nm, self.current_material); 39 | return MaterialData{ .materials = self.materials }; 40 | } 41 | 42 | fn new_material(self: *Builder, name: []const u8) !void { 43 | if (self.current_name) |n| { 44 | try self.materials.put( 45 | self.allocator, 46 | n, 47 | self.current_material, 48 | ); 49 | self.current_material = Material{}; 50 | } 51 | self.current_name = try self.allocator.dupe(u8, name); 52 | } 53 | 54 | fn dupeTextureMap(self: *Builder, map: TextureMap) !TextureMap { 55 | return .{ 56 | .path = try self.allocator.dupe(u8, map.path), 57 | .opts = try self.allocator.dupe(u8, map.opts), 58 | }; 59 | } 60 | 61 | fn ambient_color(self: *Builder, rgb: [3]f32) !void { 62 | self.current_material.ambient_color = rgb; 63 | } 64 | fn diffuse_color(self: *Builder, rgb: [3]f32) !void { 65 | self.current_material.diffuse_color = rgb; 66 | } 67 | fn specular_color(self: *Builder, rgb: [3]f32) !void { 68 | self.current_material.specular_color = rgb; 69 | } 70 | fn specular_highlight(self: *Builder, v: f32) !void { 71 | self.current_material.specular_highlight = v; 72 | } 73 | fn emissive_coefficient(self: *Builder, rgb: [3]f32) !void { 74 | self.current_material.emissive_coefficient = rgb; 75 | } 76 | fn optical_density(self: *Builder, v: f32) !void { 77 | self.current_material.optical_density = v; 78 | } 79 | fn dissolve(self: *Builder, v: f32) !void { 80 | self.current_material.dissolve = v; 81 | } 82 | fn illumination(self: *Builder, v: u8) !void { 83 | self.current_material.illumination = v; 84 | } 85 | fn roughness(self: *Builder, v: f32) !void { 86 | self.current_material.roughness = v; 87 | } 88 | fn metallic(self: *Builder, v: f32) !void { 89 | self.current_material.metallic = v; 90 | } 91 | fn sheen(self: *Builder, v: f32) !void { 92 | self.current_material.sheen = v; 93 | } 94 | fn clearcoat_thickness(self: *Builder, v: f32) !void { 95 | self.current_material.clearcoat_thickness = v; 96 | } 97 | fn clearcoat_roughness(self: *Builder, v: f32) !void { 98 | self.current_material.clearcoat_roughness = v; 99 | } 100 | fn anisotropy(self: *Builder, v: f32) !void { 101 | self.current_material.anisotropy = v; 102 | } 103 | fn anisotropy_rotation(self: *Builder, v: f32) !void { 104 | self.current_material.anisotropy_rotation = v; 105 | } 106 | fn ambient_map(self: *Builder, map: TextureMap) !void { 107 | self.current_material.ambient_map = try self.dupeTextureMap(map); 108 | } 109 | fn diffuse_map(self: *Builder, map: TextureMap) !void { 110 | self.current_material.diffuse_map = try self.dupeTextureMap(map); 111 | } 112 | fn specular_color_map(self: *Builder, map: TextureMap) !void { 113 | self.current_material.specular_color_map = try self.dupeTextureMap(map); 114 | } 115 | fn specular_highlight_map(self: *Builder, map: TextureMap) !void { 116 | self.current_material.specular_highlight_map = try self.dupeTextureMap(map); 117 | } 118 | fn bump_map(self: *Builder, map: TextureMap) !void { 119 | self.current_material.bump_map = try self.dupeTextureMap(map); 120 | } 121 | fn roughness_map(self: *Builder, map: TextureMap) !void { 122 | self.current_material.roughness_map = try self.dupeTextureMap(map); 123 | } 124 | fn metallic_map(self: *Builder, map: TextureMap) !void { 125 | self.current_material.metallic_map = try self.dupeTextureMap(map); 126 | } 127 | fn sheen_map(self: *Builder, map: TextureMap) !void { 128 | self.current_material.sheen_map = try self.dupeTextureMap(map); 129 | } 130 | fn emissive_map(self: *Builder, map: TextureMap) !void { 131 | self.current_material.emissive_map = try self.dupeTextureMap(map); 132 | } 133 | fn normal_map(self: *Builder, map: TextureMap) !void { 134 | self.current_material.normal_map = try self.dupeTextureMap(map); 135 | } 136 | }; 137 | }; 138 | 139 | // NOTE: I'm not sure which material statements are optional. For now, I'm 140 | // assuming all of them are. 141 | pub const Material = struct { 142 | ambient_color: ?[3]f32 = null, 143 | diffuse_color: ?[3]f32 = null, 144 | specular_color: ?[3]f32 = null, 145 | specular_highlight: ?f32 = null, 146 | emissive_coefficient: ?[3]f32 = null, 147 | optical_density: ?f32 = null, 148 | dissolve: ?f32 = null, 149 | illumination: ?u8 = null, 150 | roughness: ?f32 = null, 151 | metallic: ?f32 = null, 152 | sheen: ?f32 = null, 153 | clearcoat_thickness: ?f32 = null, 154 | clearcoat_roughness: ?f32 = null, 155 | anisotropy: ?f32 = null, 156 | anisotropy_rotation: ?f32 = null, 157 | 158 | ambient_map: ?TextureMap = null, 159 | diffuse_map: ?TextureMap = null, 160 | specular_color_map: ?TextureMap = null, 161 | specular_highlight_map: ?TextureMap = null, 162 | bump_map: ?TextureMap = null, 163 | roughness_map: ?TextureMap = null, 164 | metallic_map: ?TextureMap = null, 165 | sheen_map: ?TextureMap = null, 166 | emissive_map: ?TextureMap = null, 167 | normal_map: ?TextureMap = null, 168 | 169 | pub fn deinit(self: *Material, allocator: Allocator) void { 170 | if (self.bump_map) |m| freeTextureMap(allocator, m); 171 | if (self.diffuse_map) |m| freeTextureMap(allocator, m); 172 | if (self.specular_color_map) |m| freeTextureMap(allocator, m); 173 | if (self.specular_highlight_map) |m| freeTextureMap(allocator, m); 174 | if (self.ambient_map) |m| freeTextureMap(allocator, m); 175 | if (self.roughness_map) |m| freeTextureMap(allocator, m); 176 | if (self.metallic_map) |m| freeTextureMap(allocator, m); 177 | if (self.sheen_map) |m| freeTextureMap(allocator, m); 178 | if (self.emissive_map) |m| freeTextureMap(allocator, m); 179 | if (self.normal_map) |m| freeTextureMap(allocator, m); 180 | } 181 | 182 | fn freeTextureMap(allocator: Allocator, map: TextureMap) void { 183 | allocator.free(map.path); 184 | allocator.free(map.opts); 185 | } 186 | }; 187 | 188 | const Keyword = enum { 189 | comment, 190 | new_material, 191 | ambient_color, 192 | diffuse_color, 193 | specular_color, 194 | specular_highlight, 195 | emissive_coefficient, 196 | optical_density, 197 | dissolve, 198 | illumination, 199 | roughness, 200 | metallic, 201 | sheen, 202 | clearcoat_thickness, 203 | clearcoat_roughness, 204 | anisotropy, 205 | anisotropy_rotation, 206 | ambient_map, 207 | diffuse_map, 208 | specular_color_map, 209 | specular_highlight_map, 210 | bump_map, 211 | roughness_map, 212 | metallic_map, 213 | sheen_map, 214 | emissive_map, 215 | normal_map, 216 | }; 217 | 218 | pub const TextureMap = struct { path: []const u8, opts: []const u8 }; 219 | 220 | pub fn parse(allocator: Allocator, data: []const u8) !MaterialData { 221 | var b = MaterialData.Builder{ .allocator = allocator }; 222 | errdefer b.onError(); 223 | var fbs = std.io.fixedBufferStream(data); 224 | return try parseCustom(MaterialData, &b, fbs.reader()); 225 | } 226 | 227 | pub fn parseCustom(comptime T: type, b: *T.Builder, reader: anytype) !T { 228 | var buffer: [128]u8 = undefined; 229 | var lines = lineIterator(reader, &buffer); 230 | while (try lines.next()) |line| { 231 | var iter = tokenizeAny(u8, line, " "); 232 | const def_type = 233 | if (iter.next()) |tok| try parseKeyword(tok) else continue; 234 | switch (def_type) { 235 | .comment => {}, 236 | .new_material => try b.new_material(iter.next().?), 237 | .ambient_color => try b.ambient_color(try parseVec3(&iter)), 238 | .diffuse_color => try b.diffuse_color(try parseVec3(&iter)), 239 | .specular_color => try b.specular_color(try parseVec3(&iter)), 240 | .specular_highlight => try b.specular_highlight(try parseF32(&iter)), 241 | .emissive_coefficient => try b.emissive_coefficient(try parseVec3(&iter)), 242 | .optical_density => try b.optical_density(try parseF32(&iter)), 243 | .dissolve => try b.dissolve(try parseF32(&iter)), 244 | .illumination => try b.illumination(try parseU8(&iter)), 245 | .roughness => try b.roughness(try parseF32(&iter)), 246 | .metallic => try b.metallic(try parseF32(&iter)), 247 | .sheen => try b.sheen(try parseF32(&iter)), 248 | .clearcoat_thickness => try b.clearcoat_thickness(try parseF32(&iter)), 249 | .clearcoat_roughness => try b.clearcoat_roughness(try parseF32(&iter)), 250 | .anisotropy => try b.anisotropy(try parseF32(&iter)), 251 | .anisotropy_rotation => try b.anisotropy_rotation(try parseF32(&iter)), 252 | .ambient_map => try b.ambient_map(try parseTextureMap(&iter)), 253 | .diffuse_map => try b.diffuse_map(try parseTextureMap(&iter)), 254 | .specular_color_map => try b.specular_color_map(try parseTextureMap(&iter)), 255 | .specular_highlight_map => try b.specular_highlight_map(try parseTextureMap(&iter)), 256 | .bump_map => try b.bump_map(try parseTextureMap(&iter)), 257 | .roughness_map => try b.roughness_map(try parseTextureMap(&iter)), 258 | .metallic_map => try b.metallic_map(try parseTextureMap(&iter)), 259 | .sheen_map => try b.sheen_map(try parseTextureMap(&iter)), 260 | .emissive_map => try b.emissive_map(try parseTextureMap(&iter)), 261 | .normal_map => try b.normal_map(try parseTextureMap(&iter)), 262 | } 263 | } 264 | return try b.finish(); 265 | } 266 | 267 | fn parseU8(iter: *std.mem.TokenIterator(u8, .any)) !u8 { 268 | return try std.fmt.parseInt(u8, iter.next().?, 10); 269 | } 270 | 271 | fn parseF32(iter: *std.mem.TokenIterator(u8, .any)) !f32 { 272 | return try std.fmt.parseFloat(f32, iter.next().?); 273 | } 274 | 275 | fn parseVec3(iter: *std.mem.TokenIterator(u8, .any)) ![3]f32 { 276 | const x = try std.fmt.parseFloat(f32, iter.next().?); 277 | const y = try std.fmt.parseFloat(f32, iter.next().?); 278 | const z = try std.fmt.parseFloat(f32, iter.next().?); 279 | return [_]f32{ x, y, z }; 280 | } 281 | 282 | fn parseTextureMap(iter: *std.mem.TokenIterator(u8, .any)) !TextureMap { 283 | const start = iter.index; 284 | var end = iter.index; 285 | var path: []const u8 = ""; 286 | while (iter.next()) |s| { 287 | if (iter.peek() != null) { 288 | end = iter.index; 289 | } 290 | path = s; 291 | } 292 | return .{ 293 | .path = path, 294 | .opts = std.mem.trim(u8, iter.buffer[start..end], " "), 295 | }; 296 | } 297 | 298 | fn parseKeyword(s: []const u8) !Keyword { 299 | if (std.mem.eql(u8, s, "#")) { 300 | return .comment; 301 | } else if (std.mem.eql(u8, s, "newmtl")) { 302 | return .new_material; 303 | } else if (std.mem.eql(u8, s, "Ka")) { 304 | return .ambient_color; 305 | } else if (std.mem.eql(u8, s, "Kd")) { 306 | return .diffuse_color; 307 | } else if (std.mem.eql(u8, s, "Ks")) { 308 | return .specular_color; 309 | } else if (std.mem.eql(u8, s, "Ns")) { 310 | return .specular_highlight; 311 | } else if (std.mem.eql(u8, s, "Ke")) { 312 | return .emissive_coefficient; 313 | } else if (std.mem.eql(u8, s, "Ni")) { 314 | return .optical_density; 315 | } else if (std.mem.eql(u8, s, "d")) { 316 | return .dissolve; 317 | } else if (std.mem.eql(u8, s, "illum")) { 318 | return .illumination; 319 | } else if (std.mem.eql(u8, s, "Pr")) { 320 | return .roughness; 321 | } else if (std.mem.eql(u8, s, "Pm")) { 322 | return .metallic; 323 | } else if (std.mem.eql(u8, s, "Ps")) { 324 | return .sheen; 325 | } else if (std.mem.eql(u8, s, "Pc")) { 326 | return .clearcoat_thickness; 327 | } else if (std.mem.eql(u8, s, "Pcr")) { 328 | return .clearcoat_roughness; 329 | } else if (std.mem.eql(u8, s, "aniso")) { 330 | return .anisotropy; 331 | } else if (std.mem.eql(u8, s, "anisor")) { 332 | return .anisotropy_rotation; 333 | } else if (std.mem.eql(u8, s, "map_Ka")) { 334 | return .ambient_map; 335 | } else if (std.mem.eql(u8, s, "map_Kd")) { 336 | return .diffuse_map; 337 | } else if (std.mem.eql(u8, s, "map_Ks")) { 338 | return .specular_color_map; 339 | } else if (std.mem.eql(u8, s, "map_Ns")) { 340 | return .specular_highlight_map; 341 | } else if (std.mem.eql(u8, s, "map_Bump")) { 342 | return .bump_map; 343 | } else if (std.mem.eql(u8, s, "map_Pr")) { 344 | return .roughness_map; 345 | } else if (std.mem.eql(u8, s, "map_Pm")) { 346 | return .metallic_map; 347 | } else if (std.mem.eql(u8, s, "map_Ps")) { 348 | return .sheen_map; 349 | } else if (std.mem.eql(u8, s, "map_Ke")) { 350 | return .emissive_map; 351 | } else if (std.mem.eql(u8, s, "map_Norm")) { 352 | return .normal_map; 353 | } else { 354 | std.log.warn("Unknown keyword: {s}", .{s}); 355 | return error.UnknownKeyword; 356 | } 357 | } 358 | 359 | const test_allocator = std.testing.allocator; 360 | const expectEqual = std.testing.expectEqual; 361 | const expectEqualSlices = std.testing.expectEqualSlices; 362 | const expectEqualStrings = std.testing.expectEqualStrings; 363 | 364 | test "single material" { 365 | const data = @embedFile("../examples/single.mtl"); 366 | 367 | var result = try parse(test_allocator, data); 368 | defer result.deinit(test_allocator); 369 | 370 | const material = result.materials.get("Material").?; 371 | try expectEqualSlices(f32, &material.ambient_color.?, &.{ 1.0, 1.0, 1.0 }); 372 | try expectEqualSlices(f32, &material.diffuse_color.?, &.{ 0.9, 0.9, 0.9 }); 373 | try expectEqualSlices(f32, &material.specular_color.?, &.{ 0.8, 0.8, 0.8 }); 374 | try expectEqualSlices(f32, &material.emissive_coefficient.?, &.{ 0.7, 0.7, 0.7 }); 375 | try expectEqual(material.specular_highlight.?, 225.0); 376 | try expectEqual(material.optical_density.?, 1.45); 377 | try expectEqual(material.dissolve.?, 1.0); 378 | try expectEqual(material.illumination.?, 2); 379 | try expectEqualStrings(material.bump_map.?.path, "/path/to/bump.png"); 380 | try expectEqualStrings(material.diffuse_map.?.path, "/path/to/diffuse.png"); 381 | try expectEqualStrings(material.specular_highlight_map.?.path, "/path/to/specular.png"); 382 | } 383 | 384 | test "plateball (pbr)" { 385 | const data = @embedFile("../examples/plateball.mtl"); 386 | 387 | var result = try parse(test_allocator, data); 388 | defer result.deinit(test_allocator); 389 | 390 | const material = result.materials.get("Material.001").?; 391 | try expectEqualStrings(material.metallic_map.?.path, "metal_plate_rough_1k.jpg"); 392 | try expectEqualStrings(material.roughness_map.?.path, "metal_plate_metal_1k.jpg"); 393 | try expectEqualStrings(material.bump_map.?.path, "metal_plate_nor_gl_1k.jpg"); 394 | try expectEqualStrings(material.bump_map.?.opts, "-bm 1.000000"); 395 | } 396 | 397 | test "windows line endings" { 398 | const data = @embedFile("../examples/triangle_windows.mtl"); 399 | 400 | var result = try parse(test_allocator, data); 401 | defer result.deinit(test_allocator); 402 | 403 | const material = result.materials.get("None").?; 404 | try expectEqualSlices(f32, &material.ambient_color.?, &.{ 0.8, 0.8, 0.8 }); 405 | try expectEqualSlices(f32, &material.diffuse_color.?, &.{ 0.8, 0.8, 0.8 }); 406 | try expectEqualSlices(f32, &material.specular_color.?, &.{ 0.8, 0.8, 0.8 }); 407 | try expectEqual(material.dissolve.?, 1.0); 408 | try expectEqual(material.illumination.?, 2); 409 | } 410 | 411 | test "empty mtl" { 412 | const data = @embedFile("../examples/empty.mtl"); 413 | 414 | var result = try parse(test_allocator, data); 415 | defer result.deinit(test_allocator); 416 | 417 | try expectEqual(result.materials.size, 0); 418 | } 419 | -------------------------------------------------------------------------------- /src/obj.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const tokenizeAny = std.mem.tokenizeAny; 3 | const splitAny = std.mem.splitAny; 4 | const ArrayListUnmanaged = std.ArrayListUnmanaged; 5 | const Allocator = std.mem.Allocator; 6 | const assert = std.debug.assert; 7 | const parseFloat = std.fmt.parseFloat; 8 | const parseInt = std.fmt.parseInt; 9 | 10 | const lineIterator = @import("utils.zig").lineIterator; 11 | 12 | pub const ObjData = struct { 13 | material_libs: []const []const u8, 14 | 15 | vertices: []const f32, 16 | tex_coords: []const f32, 17 | normals: []const f32, 18 | 19 | meshes: []const Mesh, 20 | 21 | pub fn deinit(self: *@This(), allocator: Allocator) void { 22 | for (self.material_libs) |mlib| allocator.free(mlib); 23 | allocator.free(self.material_libs); 24 | 25 | allocator.free(self.vertices); 26 | allocator.free(self.tex_coords); 27 | allocator.free(self.normals); 28 | 29 | for (self.meshes) |mesh| mesh.deinit(allocator); 30 | allocator.free(self.meshes); 31 | } 32 | 33 | const Builder = struct { 34 | allocator: Allocator, 35 | material_libs: ArrayListUnmanaged([]const u8) = .{}, 36 | vertices: ArrayListUnmanaged(f32) = .{}, 37 | tex_coords: ArrayListUnmanaged(f32) = .{}, 38 | normals: ArrayListUnmanaged(f32) = .{}, 39 | meshes: ArrayListUnmanaged(Mesh) = .{}, 40 | 41 | // current mesh 42 | name: ?[]const u8 = null, 43 | num_verts: ArrayListUnmanaged(u32) = .{}, 44 | indices: ArrayListUnmanaged(Mesh.Index) = .{}, 45 | index_i: u32 = 0, 46 | 47 | // current mesh material 48 | current_material: ?MeshMaterial = null, 49 | mesh_materials: ArrayListUnmanaged(MeshMaterial) = .{}, 50 | num_processed_verts: usize = 0, 51 | 52 | fn onError(self: *Builder) void { 53 | for (self.material_libs.items) |mlib| self.allocator.free(mlib); 54 | for (self.meshes.items) |mesh| mesh.deinit(self.allocator); 55 | self.material_libs.deinit(self.allocator); 56 | self.vertices.deinit(self.allocator); 57 | self.tex_coords.deinit(self.allocator); 58 | self.normals.deinit(self.allocator); 59 | self.meshes.deinit(self.allocator); 60 | if (self.name) |n| self.allocator.free(n); 61 | self.num_verts.deinit(self.allocator); 62 | self.indices.deinit(self.allocator); 63 | if (self.current_material) |mat| self.allocator.free(mat.material); 64 | self.mesh_materials.deinit(self.allocator); 65 | } 66 | 67 | fn finish(self: *Builder) !ObjData { 68 | defer self.* = undefined; 69 | try self.use_material(null); // add last material if any 70 | try self.object(null); // add last mesh (as long as it is not empty) 71 | return ObjData{ 72 | .material_libs = try self.material_libs.toOwnedSlice(self.allocator), 73 | .vertices = try self.vertices.toOwnedSlice(self.allocator), 74 | .tex_coords = try self.tex_coords.toOwnedSlice(self.allocator), 75 | .normals = try self.normals.toOwnedSlice(self.allocator), 76 | .meshes = try self.meshes.toOwnedSlice(self.allocator), 77 | }; 78 | } 79 | 80 | fn vertex(self: *Builder, x: f32, y: f32, z: f32, w: ?f32) !void { 81 | _ = w; 82 | try self.vertices.appendSlice(self.allocator, &.{ x, y, z }); 83 | } 84 | 85 | fn tex_coord(self: *Builder, u: f32, v: ?f32, w: ?f32) !void { 86 | _ = w; 87 | try self.tex_coords.appendSlice(self.allocator, &.{ u, v.? }); 88 | } 89 | 90 | fn normal(self: *Builder, i: f32, j: f32, k: f32) !void { 91 | try self.normals.appendSlice(self.allocator, &.{ i, j, k }); 92 | } 93 | 94 | fn face_index(self: *Builder, vert: u32, tex: ?u32, norm: ?u32) !void { 95 | try self.indices.append( 96 | self.allocator, 97 | .{ .vertex = vert, .tex_coord = tex, .normal = norm }, 98 | ); 99 | self.index_i += 1; 100 | } 101 | 102 | fn face_end(self: *Builder) !void { 103 | try self.num_verts.append(self.allocator, self.index_i); 104 | self.num_processed_verts += self.index_i; 105 | self.index_i = 0; 106 | } 107 | 108 | fn object(self: *Builder, name: ?[]const u8) !void { 109 | if (0 < self.num_verts.items.len) { 110 | if (self.current_material) |*m| { 111 | m.end_index = self.num_processed_verts; 112 | try self.mesh_materials.append(self.allocator, m.*); 113 | } 114 | try self.meshes.append(self.allocator, .{ 115 | .name = self.name, 116 | .num_vertices = try self.num_verts.toOwnedSlice(self.allocator), 117 | .indices = try self.indices.toOwnedSlice(self.allocator), 118 | .materials = try self.mesh_materials.toOwnedSlice(self.allocator), 119 | }); 120 | } 121 | if (name) |n| { 122 | self.name = try self.allocator.dupe(u8, n); 123 | self.num_verts = .{}; 124 | self.indices = .{}; 125 | self.num_processed_verts = 0; 126 | self.current_material = null; 127 | } 128 | } 129 | 130 | fn use_material(self: *Builder, name: ?[]const u8) !void { 131 | if (self.current_material) |*m| { 132 | m.end_index = self.num_processed_verts; 133 | try self.mesh_materials.append(self.allocator, m.*); 134 | } 135 | if (name) |n| { 136 | self.current_material = MeshMaterial{ 137 | .material = try self.allocator.dupe(u8, n), 138 | .start_index = self.num_processed_verts, 139 | .end_index = self.num_processed_verts + 1, 140 | }; 141 | } else { 142 | self.current_material = null; 143 | } 144 | } 145 | 146 | fn material_lib(self: *Builder, name: []const u8) !void { 147 | try self.material_libs.append( 148 | self.allocator, 149 | try self.allocator.dupe(u8, name), 150 | ); 151 | } 152 | 153 | fn vertexCount(self: Builder) usize { 154 | return self.vertices.items.len; 155 | } 156 | 157 | fn texCoordCount(self: Builder) usize { 158 | return self.tex_coords.items.len; 159 | } 160 | 161 | fn normalCount(self: Builder) usize { 162 | return self.normals.items.len; 163 | } 164 | }; 165 | }; 166 | 167 | fn compareOpt(a: ?u32, b: ?u32) bool { 168 | if (a != null and b != null) { 169 | return a.? == b.?; 170 | } 171 | 172 | return a == null and b == null; 173 | } 174 | 175 | fn eqlZ(comptime T: type, a: ?[]const T, b: ?[]const T) bool { 176 | if (a != null and b != null) { 177 | return std.mem.eql(T, a.?, b.?); 178 | } 179 | 180 | return a == null and b == null; 181 | } 182 | 183 | pub const MeshMaterial = struct { 184 | material: []const u8, 185 | start_index: usize, 186 | end_index: usize, 187 | 188 | fn eq(self: MeshMaterial, other: MeshMaterial) bool { 189 | return std.mem.eql(u8, self.material, other.material) and 190 | self.start_index == other.start_index and 191 | self.end_index == other.end_index; 192 | } 193 | }; 194 | 195 | pub const Mesh = struct { 196 | pub const Index = struct { 197 | vertex: ?u32, 198 | tex_coord: ?u32, 199 | normal: ?u32, 200 | 201 | fn eq(self: Mesh.Index, other: Mesh.Index) bool { 202 | return compareOpt(self.vertex, other.vertex) and 203 | compareOpt(self.tex_coord, other.tex_coord) and 204 | compareOpt(self.normal, other.normal); 205 | } 206 | }; 207 | 208 | name: ?[]const u8, 209 | 210 | // Number of vertices for each face 211 | num_vertices: []const u32, 212 | indices: []const Mesh.Index, 213 | 214 | materials: []const MeshMaterial, 215 | 216 | pub fn deinit(self: Mesh, allocator: Allocator) void { 217 | if (self.name) |name| allocator.free(name); 218 | allocator.free(self.num_vertices); 219 | allocator.free(self.indices); 220 | for (self.materials) |mat| { 221 | allocator.free(mat.material); 222 | } 223 | allocator.free(self.materials); 224 | } 225 | 226 | // TODO Use std.meta magic? 227 | fn eq(self: Mesh, other: Mesh) bool { 228 | if (!eqlZ(u8, self.name, other.name)) return false; 229 | if (self.indices.len != other.indices.len) return false; 230 | if (!std.mem.eql(u32, self.num_vertices, other.num_vertices)) return false; 231 | for (self.indices, 0..) |index, i| { 232 | if (!index.eq(other.indices[i])) return false; 233 | } 234 | for (self.materials, 0..) |mat, i| { 235 | if (!mat.eq(other.materials[i])) return false; 236 | } 237 | return true; 238 | } 239 | }; 240 | 241 | const DefType = enum { 242 | comment, 243 | vertex, 244 | tex_coord, 245 | normal, 246 | face, 247 | object, 248 | group, 249 | material_lib, 250 | use_material, 251 | smoothing, 252 | line, 253 | param_vertex, 254 | }; 255 | 256 | pub fn parse(allocator: Allocator, data: []const u8) !ObjData { 257 | var b = ObjData.Builder{ .allocator = allocator }; 258 | errdefer b.onError(); 259 | var fbs = std.io.fixedBufferStream(data); 260 | return try parseCustom(ObjData, &b, fbs.reader()); 261 | } 262 | 263 | pub fn parseCustom(comptime T: type, b: *T.Builder, reader: anytype) !T { 264 | var buffer: [1024]u8 = undefined; 265 | var lines = lineIterator(reader, &buffer); 266 | while (try lines.next()) |line| { 267 | var iter = tokenizeAny(u8, line, " "); 268 | const def_type = 269 | if (iter.next()) |tok| try parseType(tok) else continue; 270 | switch (def_type) { 271 | .vertex => try b.vertex( 272 | try parseFloat(f32, iter.next().?), 273 | try parseFloat(f32, iter.next().?), 274 | try parseFloat(f32, iter.next().?), 275 | if (iter.next()) |w| (try parseFloat(f32, w)) else null, 276 | ), 277 | .tex_coord => try b.tex_coord( 278 | try parseFloat(f32, iter.next().?), 279 | if (iter.next()) |v| (try parseFloat(f32, v)) else null, 280 | if (iter.next()) |w| (try parseFloat(f32, w)) else null, 281 | ), 282 | .normal => try b.normal( 283 | try parseFloat(f32, iter.next().?), 284 | try parseFloat(f32, iter.next().?), 285 | try parseFloat(f32, iter.next().?), 286 | ), 287 | .face => { 288 | while (iter.next()) |entry| { 289 | var entry_iter = splitAny(u8, entry, "/"); 290 | try b.face_index( 291 | (try parseOptionalIndex(entry_iter.next().?, b.vertexCount())).?, 292 | if (entry_iter.next()) |e| (try parseOptionalIndex(e, b.texCoordCount())) else null, 293 | if (entry_iter.next()) |e| (try parseOptionalIndex(e, b.normalCount())) else null, 294 | ); 295 | } 296 | try b.face_end(); 297 | }, 298 | .object => try b.object(iter.next().?), 299 | .use_material => try b.use_material(iter.next().?), 300 | .material_lib => while (iter.next()) |lib| try b.material_lib(lib), 301 | else => {}, 302 | } 303 | } 304 | 305 | return try b.finish(); 306 | } 307 | 308 | fn parseOptionalIndex(v: []const u8, n_items: usize) !?u32 { 309 | if (std.mem.eql(u8, v, "")) return null; 310 | const i = try parseInt(i32, v, 10); 311 | 312 | if (i < 0) { 313 | // index is relative to end of indices list, -1 meaning the last element 314 | return @as(u32, @intCast(@as(i32, @intCast(n_items)) + i)); 315 | } else { 316 | // obj is one-indexed - let's make it zero-indexed 317 | return @as(u32, @intCast(i)) - 1; 318 | } 319 | } 320 | 321 | fn parseType(t: []const u8) !DefType { 322 | if (std.mem.eql(u8, t, "#")) { 323 | return .comment; 324 | } else if (std.mem.eql(u8, t, "v")) { 325 | return .vertex; 326 | } else if (std.mem.eql(u8, t, "vt")) { 327 | return .tex_coord; 328 | } else if (std.mem.eql(u8, t, "vn")) { 329 | return .normal; 330 | } else if (std.mem.eql(u8, t, "f")) { 331 | return .face; 332 | } else if (std.mem.eql(u8, t, "o")) { 333 | return .object; 334 | } else if (std.mem.eql(u8, t, "g")) { 335 | return .group; 336 | } else if (std.mem.eql(u8, t, "mtllib")) { 337 | return .material_lib; 338 | } else if (std.mem.eql(u8, t, "usemtl")) { 339 | return .use_material; 340 | } else if (std.mem.eql(u8, t, "s")) { 341 | return .smoothing; 342 | } else if (std.mem.eql(u8, t, "l")) { 343 | return .line; 344 | } else if (std.mem.eql(u8, t, "vp")) { 345 | return .param_vertex; 346 | } else { 347 | return error.UnknownDefType; 348 | } 349 | } 350 | 351 | // ------------------------------------------------------------------------------ 352 | 353 | const test_allocator = std.testing.allocator; 354 | 355 | const expect = std.testing.expect; 356 | const expectError = std.testing.expectError; 357 | const expectEqual = std.testing.expectEqual; 358 | const expectEqualSlices = std.testing.expectEqualSlices; 359 | const expectEqualStrings = std.testing.expectEqualStrings; 360 | 361 | test "unknown def" { 362 | try expectError(error.UnknownDefType, parse(test_allocator, "invalid 0 1 2")); 363 | } 364 | 365 | test "comment" { 366 | var result = try parse(test_allocator, "# this is a comment"); 367 | defer result.deinit(test_allocator); 368 | 369 | try expectEqual(0, result.vertices.len); 370 | try expectEqual(0, result.tex_coords.len); 371 | try expectEqual(0, result.normals.len); 372 | try expectEqual(0, result.meshes.len); 373 | } 374 | 375 | test "single vertex def xyz" { 376 | var result = try parse(test_allocator, "v 0.123 0.234 0.345"); 377 | defer result.deinit(test_allocator); 378 | 379 | try expectEqualSlices(f32, &.{ 0.123, 0.234, 0.345 }, result.vertices); 380 | try expectEqual(0, result.tex_coords.len); 381 | try expectEqual(0, result.normals.len); 382 | try expectEqual(0, result.meshes.len); 383 | } 384 | 385 | test "single vertex def xyzw" { 386 | var result = try parse(test_allocator, "v 0.123 0.234 0.345 0.456"); 387 | defer result.deinit(test_allocator); 388 | 389 | try expectEqualSlices(f32, &.{ 0.123, 0.234, 0.345 }, result.vertices); 390 | try expectEqual(0, result.tex_coords.len); 391 | try expectEqual(0, result.normals.len); 392 | try expectEqual(0, result.meshes.len); 393 | } 394 | 395 | test "single tex coord def uv" { 396 | var result = try parse(test_allocator, "vt 0.123 0.234"); 397 | defer result.deinit(test_allocator); 398 | 399 | try expectEqualSlices(f32, &.{ 0.123, 0.234 }, result.tex_coords); 400 | try expectEqual(0, result.vertices.len); 401 | try expectEqual(0, result.normals.len); 402 | try expectEqual(0, result.meshes.len); 403 | } 404 | 405 | test "single tex coord def uvw" { 406 | var result = try parse(test_allocator, "vt 0.123 0.234 0.345"); 407 | defer result.deinit(test_allocator); 408 | 409 | try expectEqualSlices(f32, &.{ 0.123, 0.234 }, result.tex_coords); 410 | try expectEqual(0, result.vertices.len); 411 | try expectEqual(0, result.normals.len); 412 | try expectEqual(0, result.meshes.len); 413 | } 414 | 415 | test "single normal def xyz" { 416 | var result = try parse(test_allocator, "vn 0.123 0.234 0.345"); 417 | defer result.deinit(test_allocator); 418 | 419 | try expectEqualSlices(f32, &.{ 0.123, 0.234, 0.345 }, result.normals); 420 | try expectEqual(0, result.vertices.len); 421 | try expectEqual(0, result.tex_coords.len); 422 | try expectEqual(0, result.meshes.len); 423 | } 424 | 425 | test "single face def vertex only" { 426 | var result = try parse(test_allocator, "f 1 2 3"); 427 | defer result.deinit(test_allocator); 428 | 429 | const mesh = Mesh{ 430 | .name = null, 431 | .num_vertices = &[_]u32{3}, 432 | .indices = &[_]Mesh.Index{ 433 | Mesh.Index{ .vertex = 0, .tex_coord = null, .normal = null }, 434 | Mesh.Index{ .vertex = 1, .tex_coord = null, .normal = null }, 435 | Mesh.Index{ .vertex = 2, .tex_coord = null, .normal = null }, 436 | }, 437 | .materials = &[_]MeshMaterial{}, 438 | }; 439 | try expectEqual(1, result.meshes.len); 440 | try expect(result.meshes[0].eq(mesh)); 441 | } 442 | 443 | test "single face def vertex + tex coord" { 444 | var result = try parse(test_allocator, "f 1/4 2/5 3/6"); 445 | defer result.deinit(test_allocator); 446 | 447 | const mesh = Mesh{ 448 | .name = null, 449 | .num_vertices = &[_]u32{3}, 450 | .indices = &[_]Mesh.Index{ 451 | .{ .vertex = 0, .tex_coord = 3, .normal = null }, 452 | .{ .vertex = 1, .tex_coord = 4, .normal = null }, 453 | .{ .vertex = 2, .tex_coord = 5, .normal = null }, 454 | }, 455 | .materials = &[_]MeshMaterial{}, 456 | }; 457 | try expectEqual(1, result.meshes.len); 458 | try expect(result.meshes[0].eq(mesh)); 459 | } 460 | 461 | test "single face def vertex + tex coord + normal" { 462 | var result = try parse(test_allocator, "f 1/4/7 2/5/8 3/6/9"); 463 | defer result.deinit(test_allocator); 464 | 465 | const mesh = Mesh{ 466 | .name = null, 467 | .num_vertices = &[_]u32{3}, 468 | .indices = &[_]Mesh.Index{ 469 | .{ .vertex = 0, .tex_coord = 3, .normal = 6 }, 470 | .{ .vertex = 1, .tex_coord = 4, .normal = 7 }, 471 | .{ .vertex = 2, .tex_coord = 5, .normal = 8 }, 472 | }, 473 | .materials = &[_]MeshMaterial{}, 474 | }; 475 | try expectEqual(1, result.meshes.len); 476 | try expect(result.meshes[0].eq(mesh)); 477 | } 478 | 479 | test "single face def vertex + normal" { 480 | var result = try parse(test_allocator, "f 1//7 2//8 3//9"); 481 | defer result.deinit(test_allocator); 482 | 483 | const expected = Mesh{ 484 | .name = null, 485 | .num_vertices = &[_]u32{3}, 486 | .indices = &[_]Mesh.Index{ 487 | .{ .vertex = 0, .tex_coord = null, .normal = 6 }, 488 | .{ .vertex = 1, .tex_coord = null, .normal = 7 }, 489 | .{ .vertex = 2, .tex_coord = null, .normal = 8 }, 490 | }, 491 | .materials = &[_]MeshMaterial{}, 492 | }; 493 | try expectEqual(1, result.meshes.len); 494 | try expect(result.meshes[0].eq(expected)); 495 | } 496 | 497 | test "multiple materials in one mesh" { 498 | var result = try parse(test_allocator, 499 | \\usemtl Mat1 500 | \\f 1/1/1 5/2/1 7/3/1 501 | \\usemtl Mat2 502 | \\f 4/5/2 3/4/2 7/6/2 503 | ); 504 | defer result.deinit(test_allocator); 505 | 506 | const expected = Mesh{ 507 | .name = null, 508 | .num_vertices = &[_]u32{ 3, 3 }, 509 | .indices = &[_]Mesh.Index{ 510 | .{ .vertex = 0, .tex_coord = 0, .normal = 0 }, 511 | .{ .vertex = 4, .tex_coord = 1, .normal = 0 }, 512 | .{ .vertex = 6, .tex_coord = 2, .normal = 0 }, 513 | .{ .vertex = 3, .tex_coord = 4, .normal = 1 }, 514 | .{ .vertex = 2, .tex_coord = 3, .normal = 1 }, 515 | .{ .vertex = 6, .tex_coord = 5, .normal = 1 }, 516 | }, 517 | .materials = &[_]MeshMaterial{ 518 | .{ .material = "Mat1", .start_index = 0, .end_index = 3 }, 519 | .{ .material = "Mat2", .start_index = 3, .end_index = 6 }, 520 | }, 521 | }; 522 | 523 | try expectEqual(1, result.meshes.len); 524 | try expect(result.meshes[0].eq(expected)); 525 | } 526 | 527 | test "triangle obj exported from blender" { 528 | const data = @embedFile("../examples/triangle.obj"); 529 | 530 | var result = try parse(test_allocator, data); 531 | defer result.deinit(test_allocator); 532 | 533 | const expected = ObjData{ 534 | .material_libs = &[_][]const u8{"triangle.mtl"}, 535 | .vertices = &[_]f32{ 536 | -1.0, 0.0, 0.0, 537 | 1.0, 0.0, 1.0, 538 | 1.0, 0.0, -1.0, 539 | }, 540 | .tex_coords = &[_]f32{ 541 | 0.0, 0.0, 542 | 1.0, 0.0, 543 | 1.0, 1.0, 544 | }, 545 | .normals = &[_]f32{ 0.0, 1.0, 0.0 }, 546 | .meshes = &[_]Mesh{ 547 | Mesh{ 548 | .name = "Plane", 549 | .num_vertices = &[_]u32{3}, 550 | .indices = &[_]Mesh.Index{ 551 | .{ .vertex = 0, .tex_coord = 0, .normal = 0 }, 552 | .{ .vertex = 1, .tex_coord = 1, .normal = 0 }, 553 | .{ .vertex = 2, .tex_coord = 2, .normal = 0 }, 554 | }, 555 | .materials = &[_]MeshMaterial{ 556 | .{ .material = "None", .start_index = 0, .end_index = 3 }, 557 | }, 558 | }, 559 | }, 560 | }; 561 | try expectEqual(1, result.material_libs.len); 562 | try expectEqualStrings(expected.material_libs[0], result.material_libs[0]); 563 | 564 | try expectEqualSlices(f32, expected.vertices, result.vertices); 565 | try expectEqualSlices(f32, expected.tex_coords, result.tex_coords); 566 | try expectEqualSlices(f32, expected.normals, result.normals); 567 | 568 | try expectEqual(1, result.meshes.len); 569 | try expect(result.meshes[0].eq(expected.meshes[0])); 570 | } 571 | 572 | test "triangle obj exported from blender (windows line endings)" { 573 | const data = @embedFile("../examples/triangle_windows.obj"); 574 | 575 | var result = try parse(test_allocator, data); 576 | defer result.deinit(test_allocator); 577 | 578 | const expected = ObjData{ 579 | .material_libs = &[_][]const u8{"triangle_windows.mtl"}, 580 | .vertices = &[_]f32{ 581 | -1.0, 0.0, 0.0, 582 | 1.0, 0.0, 1.0, 583 | 1.0, 0.0, -1.0, 584 | }, 585 | .tex_coords = &[_]f32{ 586 | 0.0, 0.0, 587 | 1.0, 0.0, 588 | 1.0, 1.0, 589 | }, 590 | .normals = &[_]f32{ 0.0, 1.0, 0.0 }, 591 | .meshes = &[_]Mesh{ 592 | Mesh{ 593 | .name = "Plane", 594 | .num_vertices = &[_]u32{3}, 595 | .indices = &[_]Mesh.Index{ 596 | .{ .vertex = 0, .tex_coord = 0, .normal = 0 }, 597 | .{ .vertex = 1, .tex_coord = 1, .normal = 0 }, 598 | .{ .vertex = 2, .tex_coord = 2, .normal = 0 }, 599 | }, 600 | .materials = &[_]MeshMaterial{ 601 | .{ .material = "None", .start_index = 0, .end_index = 3 }, 602 | }, 603 | }, 604 | }, 605 | }; 606 | try expectEqual(1, result.material_libs.len); 607 | try expectEqualStrings(expected.material_libs[0], result.material_libs[0]); 608 | 609 | try expectEqualSlices(f32, expected.vertices, result.vertices); 610 | try expectEqualSlices(f32, expected.tex_coords, result.tex_coords); 611 | try expectEqualSlices(f32, expected.normals, result.normals); 612 | 613 | try expectEqual(1, result.meshes.len); 614 | try expect(result.meshes[0].eq(expected.meshes[0])); 615 | } 616 | 617 | test "triangle obj exported from blender (two triangles)" { 618 | const data = @embedFile("../examples/triangle_two.obj"); 619 | 620 | var result = try parse(test_allocator, data); 621 | defer result.deinit(test_allocator); 622 | 623 | const expected = ObjData{ 624 | .material_libs = &[_][]const u8{"triangle.mtl"}, 625 | .vertices = &[_]f32{ 626 | -1.0, 0.0, 0.0, 627 | 1.0, 0.0, 1.0, 628 | 1.0, 0.0, -1.0, 629 | }, 630 | .tex_coords = &[_]f32{ 631 | 0.0, 0.0, 632 | 1.0, 0.0, 633 | 1.0, 1.0, 634 | }, 635 | .normals = &[_]f32{ 0.0, 1.0, 0.0 }, 636 | .meshes = &[_]Mesh{ 637 | Mesh{ 638 | .name = "Plane", 639 | .num_vertices = &[_]u32{3}, 640 | .indices = &[_]Mesh.Index{ 641 | .{ .vertex = 0, .tex_coord = 0, .normal = 0 }, 642 | .{ .vertex = 1, .tex_coord = 1, .normal = 0 }, 643 | .{ .vertex = 2, .tex_coord = 2, .normal = 0 }, 644 | }, 645 | .materials = &[_]MeshMaterial{ 646 | .{ .material = "None", .start_index = 0, .end_index = 3 }, 647 | }, 648 | }, 649 | Mesh{ 650 | .name = "Plane2", 651 | .num_vertices = &[_]u32{3}, 652 | .indices = &[_]Mesh.Index{ 653 | .{ .vertex = 0, .tex_coord = 0, .normal = 0 }, 654 | .{ .vertex = 1, .tex_coord = 1, .normal = 0 }, 655 | .{ .vertex = 2, .tex_coord = 2, .normal = 0 }, 656 | }, 657 | .materials = &[_]MeshMaterial{ 658 | .{ .material = "None", .start_index = 0, .end_index = 3 }, 659 | }, 660 | }, 661 | }, 662 | }; 663 | try expectEqual(1, result.material_libs.len); 664 | try expectEqualStrings(expected.material_libs[0], result.material_libs[0]); 665 | 666 | try expectEqualSlices(f32, expected.vertices, result.vertices); 667 | try expectEqualSlices(f32, expected.tex_coords, result.tex_coords); 668 | try expectEqualSlices(f32, expected.normals, result.normals); 669 | 670 | try expectEqual(2, result.meshes.len); 671 | try expect(result.meshes[0].eq(expected.meshes[0])); 672 | try expect(result.meshes[1].eq(expected.meshes[1])); 673 | } 674 | 675 | test "triangle obj exported from blender (with error)" { 676 | const data = @embedFile("../examples/triangle_error.obj"); 677 | try expectError(error.UnknownDefType, parse(test_allocator, data)); 678 | } 679 | 680 | test "cube obj exported from blender" { 681 | const data = @embedFile("../examples/cube.obj"); 682 | 683 | var result = try parse(test_allocator, data); 684 | defer result.deinit(test_allocator); 685 | 686 | const expected = ObjData{ 687 | .material_libs = &[_][]const u8{"cube.mtl"}, 688 | .vertices = &[_]f32{ 689 | 1.0, 1.0, -1.0, 690 | 1.0, -1.0, -1.0, 691 | 1.0, 1.0, 1.0, 692 | 1.0, -1.0, 1.0, 693 | -1.0, 1.0, -1.0, 694 | -1.0, -1.0, -1.0, 695 | -1.0, 1.0, 1.0, 696 | -1.0, -1.0, 1.0, 697 | }, 698 | .tex_coords = &[_]f32{ 699 | 0.625, 0.500, 700 | 0.875, 0.500, 701 | 0.875, 0.750, 702 | 0.625, 0.750, 703 | 0.375, 0.750, 704 | 0.625, 1.000, 705 | 0.375, 1.000, 706 | 0.375, 0.000, 707 | 0.625, 0.000, 708 | 0.625, 0.250, 709 | 0.375, 0.250, 710 | 0.125, 0.500, 711 | 0.375, 0.500, 712 | 0.125, 0.750, 713 | }, 714 | .normals = &[_]f32{ 715 | 0.0, 1.0, 0.0, 716 | 0.0, 0.0, 1.0, 717 | -1.0, 0.0, 0.0, 718 | 0.0, -1.0, 0.0, 719 | 1.0, 0.0, 0.0, 720 | 0.0, 0.0, -1.0, 721 | }, 722 | .meshes = &[_]Mesh{ 723 | Mesh{ 724 | .name = "Cube", 725 | .num_vertices = &[_]u32{ 4, 4, 4, 4, 4, 4 }, 726 | .indices = &[_]Mesh.Index{ 727 | .{ .vertex = 0, .tex_coord = 0, .normal = 0 }, 728 | .{ .vertex = 4, .tex_coord = 1, .normal = 0 }, 729 | .{ .vertex = 6, .tex_coord = 2, .normal = 0 }, 730 | .{ .vertex = 2, .tex_coord = 3, .normal = 0 }, 731 | .{ .vertex = 3, .tex_coord = 4, .normal = 1 }, 732 | .{ .vertex = 2, .tex_coord = 3, .normal = 1 }, 733 | .{ .vertex = 6, .tex_coord = 5, .normal = 1 }, 734 | .{ .vertex = 7, .tex_coord = 6, .normal = 1 }, 735 | .{ .vertex = 7, .tex_coord = 7, .normal = 2 }, 736 | .{ .vertex = 6, .tex_coord = 8, .normal = 2 }, 737 | .{ .vertex = 4, .tex_coord = 9, .normal = 2 }, 738 | .{ .vertex = 5, .tex_coord = 10, .normal = 2 }, 739 | .{ .vertex = 5, .tex_coord = 11, .normal = 3 }, 740 | .{ .vertex = 1, .tex_coord = 12, .normal = 3 }, 741 | .{ .vertex = 3, .tex_coord = 4, .normal = 3 }, 742 | .{ .vertex = 7, .tex_coord = 13, .normal = 3 }, 743 | .{ .vertex = 1, .tex_coord = 12, .normal = 4 }, 744 | .{ .vertex = 0, .tex_coord = 0, .normal = 4 }, 745 | .{ .vertex = 2, .tex_coord = 3, .normal = 4 }, 746 | .{ .vertex = 3, .tex_coord = 4, .normal = 4 }, 747 | .{ .vertex = 5, .tex_coord = 10, .normal = 5 }, 748 | .{ .vertex = 4, .tex_coord = 9, .normal = 5 }, 749 | .{ .vertex = 0, .tex_coord = 0, .normal = 5 }, 750 | .{ .vertex = 1, .tex_coord = 12, .normal = 5 }, 751 | }, 752 | .materials = &[_]MeshMaterial{.{ .material = "Material", .start_index = 0, .end_index = 24 }}, 753 | }, 754 | }, 755 | }; 756 | 757 | try expectEqual(1, result.material_libs.len); 758 | try expectEqualStrings(expected.material_libs[0], result.material_libs[0]); 759 | 760 | try expectEqualSlices(f32, expected.vertices, result.vertices); 761 | try expectEqualSlices(f32, expected.tex_coords, result.tex_coords); 762 | try expectEqualSlices(f32, expected.normals, result.normals); 763 | 764 | try expectEqual(1, result.meshes.len); 765 | try expect(result.meshes[0].eq(expected.meshes[0])); 766 | } 767 | 768 | // TODO add test for negative indices 769 | -------------------------------------------------------------------------------- /src/utils.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn LineIterator(comptime Reader: type) type { 4 | return struct { 5 | buffer: []u8, 6 | reader: Reader, 7 | 8 | pub fn next(self: *@This()) !?[]const u8 { 9 | var fbs = std.io.fixedBufferStream(self.buffer); 10 | self.reader.streamUntilDelimiter( 11 | fbs.writer(), 12 | '\n', 13 | fbs.buffer.len, 14 | ) catch |err| switch (err) { 15 | error.EndOfStream => if (fbs.getWritten().len == 0) return null, 16 | else => |e| return e, 17 | }; 18 | var line = fbs.getWritten(); 19 | if (0 < line.len and line[line.len - 1] == '\r') 20 | line = line[0 .. line.len - 1]; 21 | return line; 22 | } 23 | }; 24 | } 25 | 26 | pub fn lineIterator(rdr: anytype, buffer: []u8) LineIterator(@TypeOf(rdr)) { 27 | return .{ .buffer = buffer, .reader = rdr }; 28 | } 29 | -------------------------------------------------------------------------------- /test.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | test "zig-obj" { 4 | std.testing.refAllDecls(@import("src/main.zig")); 5 | } 6 | --------------------------------------------------------------------------------