diff --git a/lib/zgltf/.gitignore b/lib/zgltf/.gitignore index cd21511..cd3a172 100644 --- a/lib/zgltf/.gitignore +++ b/lib/zgltf/.gitignore @@ -1,3 +1,4 @@ .zig-cache/ build_runner.zig .DS_Store +zig-out diff --git a/lib/zgltf/README.md b/lib/zgltf/README.md index 65a0f42..1733032 100644 --- a/lib/zgltf/README.md +++ b/lib/zgltf/README.md @@ -9,11 +9,13 @@ Note: It's not as complete as the glTF specification yet, but because it's strai If you would like to contribute, don't hesitate! :) +Note: The main branch is for the latest Zig release (0.15.1). + ## Examples ```zig const std = @import("std"); -const Gltf = @import("zgltf"); +const Gltf = @import("zgltf").Gltf; const allocator = std.heap.page_allocator; const print = std.debug.print; @@ -29,7 +31,7 @@ pub fn main() void { ); defer allocator.free(buf); - var gltf = Self.init(allocator); + var gltf = Gltf.init(allocator); defer gltf.deinit(); try gltf.parse(buf); @@ -42,7 +44,7 @@ pub fn main() void { ; print(message, .{ - node.name, + node.name orelse "Unnamed Node", node.children.items.len, node.skin != null, }); @@ -122,11 +124,15 @@ for (primitive.attributes.items) |attribute| { ## Install -Note: **Zig 0.11.x is required.** +Note: **Zig 0.15.1 is required.** + +```sh +zig fetch --save git+https://github.com/kooparse/zgltf +``` ```zig -const zgltf = @import("path-to-zgltf/build.zig"); -exe.addModule("zgltf", zgltf.module(b)); +const zgltf = b.dependency("zgltf", .{}); +exe.addModule("zgltf", zgltf.module("zgltf")); ``` ## Features diff --git a/lib/zgltf/build.zig b/lib/zgltf/build.zig index f500770..7da460d 100644 --- a/lib/zgltf/build.zig +++ b/lib/zgltf/build.zig @@ -3,19 +3,20 @@ const std = @import("std"); pub fn build(b: *std.Build) void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); - const static_build = b.option(bool, "static_build", "builds backlog dependencies for static linking") orelse false; - _ = static_build; - _ = b.addModule("zgltf", .{ - .root_source_file = b.path("src/main.zig"), - }); - - var tests = b.addTest(.{ - .root_source_file = b.path("src/main.zig"), + const mod = b.addModule("zgltf", .{ + .root_source_file = b.path("src/zgltf.zig"), .target = target, .optimize = optimize, }); + const tests = b.addTest(.{ + .root_module = mod, + }); + b.installArtifact(tests); + + var run_tests = b.addRunArtifact(tests); + const test_step = b.step("test", "Run tests"); - test_step.dependOn(&tests.step); + test_step.dependOn(&run_tests.step); } diff --git a/lib/zgltf/build.zig.zon b/lib/zgltf/build.zig.zon index 1acfc40..cb0d639 100644 --- a/lib/zgltf/build.zig.zon +++ b/lib/zgltf/build.zig.zon @@ -1,9 +1,12 @@ .{ .name = .zgltf, .version = "0.1.0", - .minimum_zig_version = "0.11.0", + .fingerprint = 0x7dfe8a1202907eb1, + .minimum_zig_version = "0.15.1", .dependencies = .{}, .paths = .{ + ".github", + ".gitignore", "build.zig", "build.zig.zon", "src", @@ -11,5 +14,4 @@ "LICENSE", "README.md", }, - .fingerprint = 0x7dfe8a12f6d6c124, } diff --git a/lib/zgltf/src/main.zig b/lib/zgltf/src/main.zig deleted file mode 100644 index 2095c9f..0000000 --- a/lib/zgltf/src/main.zig +++ /dev/null @@ -1,1612 +0,0 @@ -/// -/// glTF™ 2.0 Specification is available here: -/// https://www.khronos.org/registry/glTF/specs/2.0/glTF-2.0.html -/// -const Self = @This(); - -const std = @import("std"); -const helpers = @import("helpers.zig"); -const types = @import("types.zig"); - -const mem = std.mem; -const math = std.math; -const json = std.json; -const fmt = std.fmt; -const panic = std.debug.panic; -const print = std.debug.print; -const assert = std.debug.assert; -const ArrayList = std.ArrayList; -const Allocator = std.mem.Allocator; -const ArenaAllocator = std.heap.ArenaAllocator; -const Mat4 = helpers.Mat4; -const Vec3 = helpers.Vec3; -const Quat = helpers.Quat; - -pub const Scene = types.Scene; -pub const Node = types.Node; -pub const Index = types.Index; -pub const Mesh = types.Mesh; -pub const Material = types.Material; -pub const Skin = types.Skin; -pub const TextureSampler = types.TextureSampler; -pub const Image = types.Image; -pub const Camera = types.Camera; -pub const Animation = types.Animation; -pub const Texture = types.Texture; -pub const Accessor = types.Accessor; -pub const AccessorType = types.AccessorType; -pub const AccessorIterator = types.AccessorIterator; -pub const BufferView = types.BufferView; -pub const Buffer = types.Buffer; -pub const Primitive = types.Primitive; -pub const Attribute = types.Attribute; -pub const Mode = types.Mode; -pub const ComponentType = types.ComponentType; -pub const Target = types.Target; -pub const MetallicRoughness = types.MetallicRoughness; -pub const AnimationSampler = types.AnimationSampler; -pub const Channel = types.Channel; -pub const MagFilter = types.MagFilter; -pub const MinFilter = types.MinFilter; -pub const WrapMode = types.WrapMode; -pub const TargetProperty = types.TargetProperty; -pub const Asset = types.Asset; -pub const LightType = types.LightType; -pub const Light = types.Light; -pub const LightSpot = types.LightSpot; - -pub const Data = struct { - asset: Asset, - scene: ?Index = null, - scenes: ArrayList(Scene), - cameras: ArrayList(Camera), - nodes: ArrayList(Node), - meshes: ArrayList(Mesh), - materials: ArrayList(Material), - skins: ArrayList(Skin), - samplers: ArrayList(TextureSampler), - images: ArrayList(Image), - animations: ArrayList(Animation), - textures: ArrayList(Texture), - accessors: ArrayList(Accessor), - buffer_views: ArrayList(BufferView), - buffers: ArrayList(Buffer), - lights: ArrayList(Light), -}; - -arena: *ArenaAllocator, -data: Data, - -glb_binary: ?[]align(4) const u8 = null, - -pub fn init(allocator: Allocator) Self { - var arena = allocator.create(ArenaAllocator) catch { - panic("Error while allocating memory for gltf arena.", .{}); - }; - - arena.* = ArenaAllocator.init(allocator); - - const alloc = arena.allocator(); - return Self{ - .arena = arena, - .data = .{ - .asset = Asset{ .version = "Undefined" }, - .scenes = ArrayList(Scene).init(alloc), - .nodes = ArrayList(Node).init(alloc), - .cameras = ArrayList(Camera).init(alloc), - .meshes = ArrayList(Mesh).init(alloc), - .materials = ArrayList(Material).init(alloc), - .skins = ArrayList(Skin).init(alloc), - .samplers = ArrayList(TextureSampler).init(alloc), - .images = ArrayList(Image).init(alloc), - .animations = ArrayList(Animation).init(alloc), - .textures = ArrayList(Texture).init(alloc), - .accessors = ArrayList(Accessor).init(alloc), - .buffer_views = ArrayList(BufferView).init(alloc), - .buffers = ArrayList(Buffer).init(alloc), - .lights = ArrayList(Light).init(alloc), - }, - }; -} - -/// Fill data by parsing a glTF file's buffer. -pub fn parse(self: *Self, file_buffer: []align(4) const u8) !void { - if (isGlb(file_buffer)) { - try self.parseGlb(file_buffer); - } else { - try self.parseGltfJson(file_buffer); - } -} - -pub fn debugPrint(self: *const Self) void { - const msg = - \\ - \\ glTF file info: - \\ - \\ Node {} - \\ Mesh {} - \\ Skin {} - \\ Animation {} - \\ Texture {} - \\ Material {} - \\ - \\ - ; - - print(msg, .{ - self.data.nodes.items.len, - self.data.meshes.items.len, - self.data.skins.items.len, - self.data.animations.items.len, - self.data.textures.items.len, - self.data.materials.items.len, - }); - - print(" Details:\n\n", .{}); - - if (self.data.skins.items.len > 0) { - print(" Skins found:\n", .{}); - - for (self.data.skins.items) |skin| { - print(" '{s}' found with {} joint(s).\n", .{ - skin.name, - skin.joints.items.len, - }); - } - - print("\n", .{}); - } - - if (self.data.animations.items.len > 0) { - print(" Animations found:\n", .{}); - - for (self.data.animations.items) |anim| { - print( - " '{s}' found with {} sampler(s) and {} channel(s).\n", - .{ anim.name, anim.samplers.items.len, anim.channels.items.len }, - ); - } - - print("\n", .{}); - } -} - -/// Retrieve actual data from a glTF BufferView through a given glTF Accessor. -/// Note: This library won't pull to memory the binary buffer corresponding -/// to the BufferView. -pub fn getDataFromBufferView( - self: *const Self, - comptime T: type, - /// List that will be fill with data. - list: *ArrayList(T), - accessor: Accessor, - binary: []const u8, -) void { - if (switch (accessor.component_type) { - .byte => T != i8, - .unsigned_byte => T != u8, - .short => T != i16, - .unsigned_short => T != u16, - .unsigned_integer => T != u32, - .float => T != f32, - }) { - panic( - "Mismatch between gltf component '{}' and given type '{}'.", - .{ accessor.component_type, T }, - ); - } - - if (accessor.buffer_view == null) { - panic("Accessors without buffer_view are not supported yet.", .{}); - } - - const buffer_view = self.data.buffer_views.items[accessor.buffer_view.?]; - - const comp_size = @sizeOf(T); - const offset = (accessor.byte_offset + buffer_view.byte_offset) / comp_size; - - const stride = blk: { - if (buffer_view.byte_stride) |byte_stride| { - break :blk byte_stride / comp_size; - } else { - break :blk accessor.stride / comp_size; - } - }; - - const total_count = accessor.count; - const datum_count: usize = switch (accessor.type) { - // Scalar. - .scalar => 1, - // Vec2. - .vec2 => 2, - // Vec3. - .vec3 => 3, - // Vec4. - .vec4 => 4, - // Vec4. - .mat4x4 => 16, - else => { - panic("Accessor type '{}' not implemented.", .{accessor.type}); - }, - }; - - const data = @as([*]const T, @ptrCast(@alignCast(binary.ptr))); - - var current_count: usize = 0; - while (current_count < total_count) : (current_count += 1) { - const slice = (data + offset + current_count * stride)[0..datum_count]; - list.appendSlice(slice) catch unreachable; - } -} - -pub fn deinit(self: *Self) void { - self.arena.deinit(); - self.arena.child_allocator.destroy(self.arena); -} - -pub fn getLocalTransform(node: Node) Mat4 { - return blk: { - if (node.matrix) |mat4x4| { - break :blk .{ - mat4x4[0..4].*, - mat4x4[4..8].*, - mat4x4[8..12].*, - mat4x4[12..16].*, - }; - } - - break :blk helpers.recompose( - node.translation, - node.rotation, - node.scale, - ); - }; -} - -pub fn getGlobalTransform(data: *const Data, node: Node) Mat4 { - var parent_index = node.parent; - var node_transform: Mat4 = getLocalTransform(node); - - while (parent_index != null) { - const parent = data.nodes.items[parent_index.?]; - const parent_transform = getLocalTransform(parent); - - node_transform = helpers.mul(parent_transform, node_transform); - parent_index = parent.parent; - } - - return node_transform; -} - -fn isGlb(glb_buffer: []align(4) const u8) bool { - const GLB_MAGIC_NUMBER: u32 = 0x46546C67; // 'gltf' in ASCII. - const fields = @as([*]const u32, @ptrCast(glb_buffer)); - - return fields[0] == GLB_MAGIC_NUMBER; -} - -fn parseGlb(self: *Self, glb_buffer: []align(4) const u8) !void { - const GLB_CHUNK_TYPE_JSON: u32 = 0x4E4F534A; // 'JSON' in ASCII. - const GLB_CHUNK_TYPE_BIN: u32 = 0x004E4942; // 'BIN' in ASCII. - - // Keep track of the moving index in the glb buffer. - var index: usize = 0; - - // 'cause most of the interesting fields are u32s in the buffer, it's - // easier to read them with a pointer cast. - const fields = @as([*]const u32, @ptrCast(glb_buffer)); - - // The 12-byte header consists of three 4-byte entries: - // u32 magic - // u32 version - // u32 length - const total_length = blk: { - const header = fields[0..3]; - - const version = header[1]; - const length = header[2]; - - if (!isGlb(glb_buffer)) { - panic("First 32 bits are not equal to magic number.", .{}); - } - - if (version != 2) { - panic("Only glTF spec v2 is supported.", .{}); - } - - index = header.len * @sizeOf(u32); - break :blk length; - }; - - // Each chunk has the following structure: - // u32 chunkLength - // u32 chunkType - // ubyte[] chunkData - const json_buffer = blk: { - const json_chunk = fields[3..6]; - - if (json_chunk[1] != GLB_CHUNK_TYPE_JSON) { - panic("First GLB chunk must be JSON data.", .{}); - } - - const json_bytes: u32 = fields[3]; - const start = index + 2 * @sizeOf(u32); - const end = start + json_bytes; - - const json_buffer = glb_buffer[start..end]; - - index = end; - break :blk json_buffer; - }; - - const binary_buffer = blk: { - const fields_index = index / @sizeOf(u32); - - const binary_bytes = fields[fields_index]; - const start = index + 2 * @sizeOf(u32); - const end = start + binary_bytes; - - assert(end == total_length); - - std.debug.assert(start % 4 == 0); - std.debug.assert(end % 4 == 0); - const binary: []align(4) const u8 = @alignCast(glb_buffer[start..end]); - - if (fields[fields_index + 1] != GLB_CHUNK_TYPE_BIN) { - panic("Second GLB chunk must be binary data.", .{}); - } - - index = end; - break :blk binary; - }; - - try self.parseGltfJson(json_buffer); - self.glb_binary = binary_buffer; - - const buffer_views = self.data.buffer_views.items; - - for (self.data.images.items) |*image| { - if (image.buffer_view) |buffer_view_index| { - const buffer_view = buffer_views[buffer_view_index]; - const start = buffer_view.byte_offset; - const end = start + buffer_view.byte_length; - image.data = binary_buffer[start..end]; - } - } -} - -fn parseGltfJson(self: *Self, gltf_json: []const u8) !void { - const alloc = self.arena.allocator(); - - var gltf_parsed = try json.parseFromSlice(json.Value, alloc, gltf_json, .{}); - defer gltf_parsed.deinit(); - - const gltf: *json.Value = &gltf_parsed.value; - - if (gltf.object.get("asset")) |json_value| { - var asset = &self.data.asset; - - if (json_value.object.get("version")) |version| { - asset.version = try alloc.dupe(u8, version.string); - } else { - panic("Asset's version is missing.", .{}); - } - - if (json_value.object.get("generator")) |generator| { - asset.generator = try alloc.dupe(u8, generator.string); - } - - if (json_value.object.get("copyright")) |copyright| { - asset.copyright = try alloc.dupe(u8, copyright.string); - } - } - - if (gltf.object.get("nodes")) |nodes| { - for (nodes.array.items, 0..) |item, index| { - const object = item.object; - - var node = Node{ - .name = undefined, - .children = ArrayList(Index).init(alloc), - }; - - if (object.get("name")) |name| { - node.name = try alloc.dupe(u8, name.string); - } else { - node.name = try fmt.allocPrint(alloc, "Node_{}", .{index}); - } - - if (object.get("mesh")) |mesh| { - node.mesh = parseIndex(mesh); - } - - if (object.get("camera")) |camera_index| { - node.camera = parseIndex(camera_index); - } - - if (object.get("skin")) |skin| { - node.skin = parseIndex(skin); - } - - if (object.get("children")) |children| { - for (children.array.items) |value| { - try node.children.append(parseIndex(value)); - } - } - - if (object.get("rotation")) |rotation| { - for (rotation.array.items, 0..) |component, i| { - node.rotation[i] = parseFloat(f32, component); - } - } - - if (object.get("translation")) |translation| { - for (translation.array.items, 0..) |component, i| { - node.translation[i] = parseFloat(f32, component); - } - } - - if (object.get("scale")) |scale| { - for (scale.array.items, 0..) |component, i| { - node.scale[i] = parseFloat(f32, component); - } - } - - if (object.get("matrix")) |matrix| { - node.matrix = [16]f32{ - 1, 0, 0, 0, - 0, 1, 0, 0, - 0, 0, 1, 0, - 0, 0, 0, 1, - }; - - for (matrix.array.items, 0..) |component, i| { - node.matrix.?[i] = parseFloat(f32, component); - } - } - - if (object.get("extensions")) |extensions| { - if (extensions.object.get("KHR_lights_punctual")) |lights_punctual| { - if (lights_punctual.object.get("light")) |light| { - node.light = @as(Index, @intCast(light.integer)); - } - } - } - - try self.data.nodes.append(node); - } - } - - if (gltf.object.get("cameras")) |cameras| { - for (cameras.array.items, 0..) |item, index| { - const object = item.object; - - var camera = Camera{ - .name = undefined, - .type = undefined, - }; - - if (object.get("name")) |name| { - camera.name = try alloc.dupe(u8, name.string); - } else { - camera.name = try fmt.allocPrint(alloc, "Camera_{}", .{index}); - } - - if (object.get("type")) |name| { - if (mem.eql(u8, name.string, "perspective")) { - if (object.get("perspective")) |perspective| { - var value = perspective.object; - - camera.type = .{ - .perspective = .{ - .aspect_ratio = if (value.get("aspectRatio")) |aspect_ratio| parseFloat( - f32, - aspect_ratio, - ) else null, - .yfov = parseFloat(f32, value.get("yfov").?), - .zfar = if (value.get("zfar")) |zfar| parseFloat( - f32, - zfar, - ) else null, - .znear = parseFloat(f32, value.get("znear").?), - }, - }; - } else { - panic("Camera's perspective value is missing.", .{}); - } - } else if (mem.eql(u8, name.string, "orthographic")) { - if (object.get("orthographic")) |orthographic| { - var value = orthographic.object; - - camera.type = .{ - .orthographic = .{ - .xmag = parseFloat(f32, value.get("xmag").?), - .ymag = parseFloat(f32, value.get("ymag").?), - .zfar = parseFloat(f32, value.get("zfar").?), - .znear = parseFloat(f32, value.get("znear").?), - }, - }; - } else { - panic("Camera's orthographic value is missing.", .{}); - } - } else { - panic( - "Camera's type must be perspective or orthographic.", - .{}, - ); - } - } - - try self.data.cameras.append(camera); - } - } - - if (gltf.object.get("skins")) |skins| { - for (skins.array.items, 0..) |item, index| { - const object = item.object; - - var skin = Skin{ - .name = undefined, - .joints = ArrayList(Index).init(alloc), - }; - - if (object.get("name")) |name| { - skin.name = try alloc.dupe(u8, name.string); - } else { - skin.name = try fmt.allocPrint(alloc, "Skin_{}", .{index}); - } - - if (object.get("joints")) |joints| { - for (joints.array.items) |join| { - try skin.joints.append(parseIndex(join)); - } - } - - if (object.get("skeleton")) |skeleton| { - skin.skeleton = parseIndex(skeleton); - } - - if (object.get("inverseBindMatrices")) |inv_bind_mat4| { - skin.inverse_bind_matrices = parseIndex(inv_bind_mat4); - } - - try self.data.skins.append(skin); - } - } - - if (gltf.object.get("meshes")) |meshes| { - for (meshes.array.items, 0..) |item, index| { - const object = item.object; - - var mesh: Mesh = .{ - .name = undefined, - .primitives = ArrayList(Primitive).init(alloc), - }; - - if (object.get("name")) |name| { - mesh.name = try alloc.dupe(u8, name.string); - } else { - mesh.name = try fmt.allocPrint(alloc, "Mesh_{}", .{index}); - } - - if (object.get("primitives")) |primitives| { - for (primitives.array.items) |prim_item| { - var primitive: Primitive = .{ - .attributes = ArrayList(Attribute).init(alloc), - }; - - if (prim_item.object.get("mode")) |mode| { - primitive.mode = @as(Mode, @enumFromInt(mode.integer)); - } - - if (prim_item.object.get("indices")) |indices| { - primitive.indices = parseIndex(indices); - } - - if (prim_item.object.get("material")) |material| { - primitive.material = parseIndex(material); - } - - if (prim_item.object.get("attributes")) |attributes| { - if (attributes.object.get("POSITION")) |position| { - try primitive.attributes.append( - .{ - .position = parseIndex(position), - }, - ); - } - - if (attributes.object.get("NORMAL")) |normal| { - try primitive.attributes.append( - .{ - .normal = parseIndex(normal), - }, - ); - } - - if (attributes.object.get("TANGENT")) |tangent| { - try primitive.attributes.append( - .{ - .tangent = parseIndex(tangent), - }, - ); - } - - const texcoords = [_][]const u8{ - "TEXCOORD_0", - "TEXCOORD_1", - "TEXCOORD_2", - "TEXCOORD_3", - "TEXCOORD_4", - "TEXCOORD_5", - "TEXCOORD_6", - }; - - for (texcoords) |tex_name| { - if (attributes.object.get(tex_name)) |texcoord| { - try primitive.attributes.append( - .{ - .texcoord = parseIndex(texcoord), - }, - ); - } - } - - const joints = [_][]const u8{ - "JOINTS_0", - "JOINTS_1", - "JOINTS_2", - "JOINTS_3", - "JOINTS_4", - "JOINTS_5", - "JOINTS_6", - }; - - for (joints) |join_count| { - if (attributes.object.get(join_count)) |joint| { - try primitive.attributes.append( - .{ - .joints = parseIndex(joint), - }, - ); - } - } - - const weights = [_][]const u8{ - "WEIGHTS_0", - "WEIGHTS_1", - "WEIGHTS_2", - "WEIGHTS_3", - "WEIGHTS_4", - "WEIGHTS_5", - "WEIGHTS_6", - }; - - for (weights) |weight_count| { - if (attributes.object.get(weight_count)) |weight| { - try primitive.attributes.append( - .{ - .weights = parseIndex(weight), - }, - ); - } - } - } - - try mesh.primitives.append(primitive); - } - } - - try self.data.meshes.append(mesh); - } - } - - if (gltf.object.get("accessors")) |accessors| { - for (accessors.array.items) |item| { - const object = item.object; - - var accessor = Accessor{ - .component_type = undefined, - .type = undefined, - .count = undefined, - .stride = undefined, - }; - - if (object.get("componentType")) |component_type| { - accessor.component_type = @as(ComponentType, @enumFromInt(component_type.integer)); - } else { - panic("Accessor's componentType is missing.", .{}); - } - - if (object.get("count")) |count| { - accessor.count = @as(i32, @intCast(count.integer)); - } else { - panic("Accessor's count is missing.", .{}); - } - - if (object.get("type")) |accessor_type| { - if (mem.eql(u8, accessor_type.string, "SCALAR")) { - accessor.type = .scalar; - } else if (mem.eql(u8, accessor_type.string, "VEC2")) { - accessor.type = .vec2; - } else if (mem.eql(u8, accessor_type.string, "VEC3")) { - accessor.type = .vec3; - } else if (mem.eql(u8, accessor_type.string, "VEC4")) { - accessor.type = .vec4; - } else if (mem.eql(u8, accessor_type.string, "MAT2")) { - accessor.type = .mat2x2; - } else if (mem.eql(u8, accessor_type.string, "MAT3")) { - accessor.type = .mat3x3; - } else if (mem.eql(u8, accessor_type.string, "MAT4")) { - accessor.type = .mat4x4; - } else { - panic("Accessor's type '{s}' is invalid.", .{accessor_type.string}); - } - } else { - panic("Accessor's type is missing.", .{}); - } - - if (object.get("normalized")) |normalized| { - accessor.normalized = normalized.bool; - } - - if (object.get("bufferView")) |buffer_view| { - accessor.buffer_view = parseIndex(buffer_view); - } - - if (object.get("byteOffset")) |byte_offset| { - accessor.byte_offset = @as(usize, @intCast(byte_offset.integer)); - } - - const component_size: usize = switch (accessor.component_type) { - .byte => @sizeOf(i8), - .unsigned_byte => @sizeOf(u8), - .short => @sizeOf(i16), - .unsigned_short => @sizeOf(u16), - .unsigned_integer => @sizeOf(u32), - .float => @sizeOf(f32), - }; - - accessor.stride = switch (accessor.type) { - .scalar => component_size, - .vec2 => 2 * component_size, - .vec3 => 3 * component_size, - .vec4 => 4 * component_size, - .mat2x2 => 4 * component_size, - .mat3x3 => 9 * component_size, - .mat4x4 => 16 * component_size, - }; - - try self.data.accessors.append(accessor); - } - } - - if (gltf.object.get("bufferViews")) |buffer_views| { - for (buffer_views.array.items) |item| { - const object = item.object; - - var buffer_view = BufferView{ - .buffer = undefined, - .byte_length = undefined, - }; - - if (object.get("buffer")) |buffer| { - buffer_view.buffer = parseIndex(buffer); - } - - if (object.get("byteLength")) |byte_length| { - buffer_view.byte_length = @as(usize, @intCast(byte_length.integer)); - } - - if (object.get("byteOffset")) |byte_offset| { - buffer_view.byte_offset = @as(usize, @intCast(byte_offset.integer)); - } - - if (object.get("byteStride")) |byte_stride| { - buffer_view.byte_stride = @as(usize, @intCast(byte_stride.integer)); - } - - if (object.get("target")) |target| { - buffer_view.target = @as(Target, @enumFromInt(target.integer)); - } - - try self.data.buffer_views.append(buffer_view); - } - } - - if (gltf.object.get("buffers")) |buffers| { - for (buffers.array.items) |item| { - const object = item.object; - - var buffer = Buffer{ - .byte_length = undefined, - }; - - if (object.get("uri")) |uri| { - buffer.uri = uri.string; - } - - if (object.get("byteLength")) |byte_length| { - buffer.byte_length = @as(usize, @intCast(byte_length.integer)); - } else { - panic("Buffer's byteLength is missing.", .{}); - } - - try self.data.buffers.append(buffer); - } - } - - if (gltf.object.get("scene")) |default_scene| { - self.data.scene = parseIndex(default_scene); - } - - if (gltf.object.get("scenes")) |scenes| { - for (scenes.array.items, 0..) |item, index| { - const object = item.object; - - var scene = Scene{ - .name = undefined, - }; - - if (object.get("name")) |name| { - scene.name = try alloc.dupe(u8, name.string); - } else { - scene.name = try fmt.allocPrint(alloc, "Scene_{}", .{index}); - } - - if (object.get("nodes")) |nodes| { - scene.nodes = ArrayList(Index).init(alloc); - - for (nodes.array.items) |node| { - try scene.nodes.?.append(parseIndex(node)); - } - } - - try self.data.scenes.append(scene); - } - } - - if (gltf.object.get("materials")) |materials| { - for (materials.array.items, 0..) |item, m_index| { - const object = item.object; - - var material = Material{ - .name = undefined, - }; - - if (object.get("name")) |name| { - material.name = try alloc.dupe(u8, name.string); - } else { - material.name = try fmt.allocPrint(alloc, "Material_{}", .{m_index}); - } - - if (object.get("pbrMetallicRoughness")) |pbrMetallicRoughness| { - var metallic_roughness: MetallicRoughness = .{}; - if (pbrMetallicRoughness.object.get("baseColorFactor")) |color_factor| { - for (color_factor.array.items, 0..) |factor, i| { - metallic_roughness.base_color_factor[i] = parseFloat(f32, factor); - } - } - - if (pbrMetallicRoughness.object.get("metallicFactor")) |factor| { - metallic_roughness.metallic_factor = parseFloat(f32, factor); - } - - if (pbrMetallicRoughness.object.get("roughnessFactor")) |factor| { - metallic_roughness.roughness_factor = parseFloat(f32, factor); - } - - if (pbrMetallicRoughness.object.get("baseColorTexture")) |texture_info| { - metallic_roughness.base_color_texture = .{ - .index = undefined, - }; - - if (texture_info.object.get("index")) |index| { - metallic_roughness.base_color_texture.?.index = parseIndex(index); - } - - if (texture_info.object.get("texCoord")) |texcoord| { - metallic_roughness.base_color_texture.?.texcoord = @as(i32, @intCast(texcoord.integer)); - } - } - - if (pbrMetallicRoughness.object.get("metallicRoughnessTexture")) |texture_info| { - metallic_roughness.metallic_roughness_texture = .{ - .index = undefined, - }; - - if (texture_info.object.get("index")) |index| { - metallic_roughness.metallic_roughness_texture.?.index = parseIndex(index); - } - - if (texture_info.object.get("texCoord")) |texcoord| { - metallic_roughness.metallic_roughness_texture.?.texcoord = @as(i32, @intCast(texcoord.integer)); - } - } - - material.metallic_roughness = metallic_roughness; - } - - if (object.get("normalTexture")) |normal_texture| { - material.normal_texture = .{ - .index = undefined, - }; - - if (normal_texture.object.get("index")) |index| { - material.normal_texture.?.index = parseIndex(index); - } - - if (normal_texture.object.get("texCoord")) |index| { - material.normal_texture.?.texcoord = @as(i32, @intCast(index.integer)); - } - - if (normal_texture.object.get("scale")) |scale| { - material.normal_texture.?.scale = parseFloat(f32, scale); - } - } - - if (object.get("emissiveTexture")) |emissive_texture| { - material.emissive_texture = .{ - .index = undefined, - }; - - if (emissive_texture.object.get("index")) |index| { - material.emissive_texture.?.index = parseIndex(index); - } - - if (emissive_texture.object.get("texCoord")) |index| { - material.emissive_texture.?.texcoord = @as(i32, @intCast(index.integer)); - } - } - - if (object.get("occlusionTexture")) |occlusion_texture| { - material.occlusion_texture = .{ - .index = undefined, - }; - - if (occlusion_texture.object.get("index")) |index| { - material.occlusion_texture.?.index = parseIndex(index); - } - - if (occlusion_texture.object.get("texCoord")) |index| { - material.occlusion_texture.?.texcoord = @as(i32, @intCast(index.integer)); - } - - if (occlusion_texture.object.get("strength")) |strength| { - material.occlusion_texture.?.strength = parseFloat(f32, strength); - } - } - - if (object.get("alphaMode")) |alpha_mode| { - if (mem.eql(u8, alpha_mode.string, "OPAQUE")) { - material.alpha_mode = .@"opaque"; - } - if (mem.eql(u8, alpha_mode.string, "MASK")) { - material.alpha_mode = .mask; - } - if (mem.eql(u8, alpha_mode.string, "BLEND")) { - material.alpha_mode = .blend; - } - } - - if (object.get("doubleSided")) |double_sided| { - material.is_double_sided = double_sided.bool; - } - - if (object.get("alphaCutoff")) |alpha_cutoff| { - material.alpha_cutoff = parseFloat(f32, alpha_cutoff); - } - - if (object.get("emissiveFactor")) |emissive_factor| { - for (emissive_factor.array.items, 0..) |factor, i| { - material.emissive_factor[i] = parseFloat(f32, factor); - } - } - - if (object.get("extensions")) |extensions| { - if (extensions.object.get("KHR_materials_emissive_strength")) |materials_emissive_strength| { - if (materials_emissive_strength.object.get("emissiveStrength")) |emissive_strength| { - material.emissive_strength = parseFloat(f32, emissive_strength); - } - } - - if (extensions.object.get("KHR_materials_ior")) |materials_ior| { - if (materials_ior.object.get("ior")) |ior| { - material.ior = parseFloat(f32, ior); - } - } - - if (extensions.object.get("KHR_materials_transmission")) |materials_transmission| { - if (materials_transmission.object.get("transmissionFactor")) |transmission_factor| { - material.transmission_factor = parseFloat(f32, transmission_factor); - } - - if (materials_transmission.object.get("transmissionTexture")) |transmission_texture| { - material.transmission_texture = .{ - .index = undefined, - }; - - if (transmission_texture.object.get("index")) |index| { - material.transmission_texture.?.index = parseIndex(index); - } - - if (transmission_texture.object.get("texCoord")) |index| { - material.transmission_texture.?.texcoord = @as(i32, @intCast(index.integer)); - } - } - } - - if (extensions.object.get("KHR_materials_volume")) |materials_volume| { - if (materials_volume.object.get("thicknessFactor")) |thickness_factor| { - material.thickness_factor = parseFloat(f32, thickness_factor); - } - - if (materials_volume.object.get("thicknessTexture")) |thickness_texture| { - material.thickness_texture = .{ - .index = undefined, - }; - - if (thickness_texture.object.get("index")) |index| { - material.thickness_texture.?.index = parseIndex(index); - } - - if (thickness_texture.object.get("texCoord")) |index| { - material.thickness_texture.?.texcoord = @as(i32, @intCast(index.integer)); - } - } - - if (materials_volume.object.get("attenuationDistance")) |attenuation_distance| { - material.attenuation_distance = parseFloat(f32, attenuation_distance); - } - - if (materials_volume.object.get("attenuationColor")) |attenuation_color| { - for (&material.attenuation_color, attenuation_color.array.items) |*dst, src| { - dst.* = parseFloat(f32, src); - } - } - } - - if (extensions.object.get("KHR_materials_dispersion")) |materials_dispersion| { - if (materials_dispersion.object.get("dispersion")) |dispersion| { - material.dispersion = parseFloat(f32, dispersion); - } - } - } - - try self.data.materials.append(material); - } - } - - if (gltf.object.get("textures")) |textures| { - for (textures.array.items) |item| { - var texture = Texture{}; - - if (item.object.get("source")) |source| { - texture.source = parseIndex(source); - } - - if (item.object.get("sampler")) |sampler| { - texture.sampler = parseIndex(sampler); - } - - try self.data.textures.append(texture); - } - } - - if (gltf.object.get("animations")) |animations| { - for (animations.array.items, 0..) |item, index| { - const object = item.object; - - var animation = Animation{ - .samplers = ArrayList(AnimationSampler).init(alloc), - .channels = ArrayList(Channel).init(alloc), - .name = undefined, - }; - - if (item.object.get("name")) |name| { - animation.name = try alloc.dupe(u8, name.string); - } else { - animation.name = try fmt.allocPrint(alloc, "Animation_{}", .{index}); - } - - if (object.get("samplers")) |samplers| { - for (samplers.array.items) |sampler_item| { - var sampler: AnimationSampler = .{ - .input = undefined, - .output = undefined, - }; - - if (sampler_item.object.get("input")) |input| { - sampler.input = parseIndex(input); - } else { - panic("Animation sampler's input is missing.", .{}); - } - - if (sampler_item.object.get("output")) |output| { - sampler.output = parseIndex(output); - } else { - panic("Animation sampler's output is missing.", .{}); - } - - if (sampler_item.object.get("interpolation")) |interpolation| { - if (mem.eql(u8, interpolation.string, "LINEAR")) { - sampler.interpolation = .linear; - } - - if (mem.eql(u8, interpolation.string, "STEP")) { - sampler.interpolation = .step; - } - - if (mem.eql(u8, interpolation.string, "CUBICSPLINE")) { - sampler.interpolation = .cubicspline; - } - } - - try animation.samplers.append(sampler); - } - } - - if (object.get("channels")) |channels| { - for (channels.array.items) |channel_item| { - var channel: Channel = .{ .sampler = undefined, .target = .{ - .node = undefined, - .property = undefined, - } }; - - if (channel_item.object.get("sampler")) |sampler_index| { - channel.sampler = parseIndex(sampler_index); - } else { - panic("Animation channel's sampler is missing.", .{}); - } - - if (channel_item.object.get("target")) |target_item| { - if (target_item.object.get("node")) |node_index| { - channel.target.node = parseIndex(node_index); - } else { - panic("Animation target's node is missing.", .{}); - } - - if (target_item.object.get("path")) |path| { - if (mem.eql(u8, path.string, "translation")) { - channel.target.property = .translation; - } else if (mem.eql(u8, path.string, "rotation")) { - channel.target.property = .rotation; - } else if (mem.eql(u8, path.string, "scale")) { - channel.target.property = .scale; - } else if (mem.eql(u8, path.string, "weights")) { - channel.target.property = .weights; - } else { - panic("Animation path/property is invalid.", .{}); - } - } else { - panic("Animation target's path/property is missing.", .{}); - } - } else { - panic("Animation channel's target is missing.", .{}); - } - - try animation.channels.append(channel); - } - } - - try self.data.animations.append(animation); - } - } - - if (gltf.object.get("samplers")) |samplers| { - for (samplers.array.items) |item| { - const object = item.object; - var sampler = TextureSampler{}; - - if (object.get("magFilter")) |mag_filter| { - sampler.mag_filter = @as(MagFilter, @enumFromInt(mag_filter.integer)); - } - - if (object.get("minFilter")) |min_filter| { - sampler.min_filter = @as(MinFilter, @enumFromInt(min_filter.integer)); - } - - if (object.get("wrapS")) |wrap_s| { - sampler.wrap_s = @as(WrapMode, @enumFromInt(wrap_s.integer)); - } - - if (object.get("wrapt")) |wrap_t| { - sampler.wrap_t = @as(WrapMode, @enumFromInt(wrap_t.integer)); - } - - try self.data.samplers.append(sampler); - } - } - - if (gltf.object.get("images")) |images| { - for (images.array.items) |item| { - const object = item.object; - var image = Image{}; - - if (object.get("uri")) |uri| { - image.uri = try alloc.dupe(u8, uri.string); - } - - if (object.get("mimeType")) |mime_type| { - image.mime_type = try alloc.dupe(u8, mime_type.string); - } - - if (object.get("bufferView")) |buffer_view| { - image.buffer_view = parseIndex(buffer_view); - } - - try self.data.images.append(image); - } - } - - if (gltf.object.get("extensions")) |extensions| { - if (extensions.object.get("KHR_lights_punctual")) |lights_punctual| { - if (lights_punctual.object.get("lights")) |lights| { - for (lights.array.items) |item| { - const object: json.ObjectMap = item.object; - - var light = Light{ - .name = null, - .type = undefined, - .range = math.inf(f32), - .spot = null, - }; - - if (object.get("name")) |name| { - light.name = try alloc.dupe(u8, name.string); - } - - if (object.get("color")) |color| { - for (color.array.items, 0..) |component, i| { - light.color[i] = parseFloat(f32, component); - } - } - - if (object.get("intensity")) |intensity| { - light.intensity = parseFloat(f32, intensity); - } - - if (object.get("type")) |@"type"| { - if (std.meta.stringToEnum(LightType, @"type".string)) |light_type| { - light.type = light_type; - } else panic("Light's type invalid", .{}); - } - - if (object.get("range")) |range| { - light.range = parseFloat(f32, range); - } - - if (object.get("spot")) |spot| { - light.spot = .{}; - - if (spot.object.get("innerConeAngle")) |inner_cone_angle| { - light.spot.?.inner_cone_angle = parseFloat(f32, inner_cone_angle); - } - - if (spot.object.get("outerConeAngle")) |outer_cone_angle| { - light.spot.?.outer_cone_angle = parseFloat(f32, outer_cone_angle); - } - } - - try self.data.lights.append(light); - } - } - } - } - - // For each node, fill parent indexes. - for (self.data.scenes.items) |scene| { - if (scene.nodes) |nodes| { - for (nodes.items) |node_index| { - const node = &self.data.nodes.items[node_index]; - fillParents(&self.data, node, node_index); - } - } - } -} - -// In 'gltf' files, often values are array indexes; -// this function casts Integer to 'usize'. -fn parseIndex(component: json.Value) usize { - return switch (component) { - .integer => |val| @as(usize, @intCast(val)), - else => panic( - "The json component '{any}' is not valid number.", - .{component}, - ), - }; -} - -// Exact values could be interpreted as Integer, often we want only -// floating numbers. -fn parseFloat(comptime T: type, component: json.Value) T { - const type_info = @typeInfo(T); - if (type_info != .float) { - panic( - "Given type '{any}' is not a floating number.", - .{type_info}, - ); - } - - return switch (component) { - .float => |val| @as(T, @floatCast(val)), - .integer => |val| @as(T, @floatFromInt(val)), - else => panic( - "The json component '{any}' is not a number.", - .{component}, - ), - }; -} - -fn fillParents(data: *Data, node: *Node, parent_index: Index) void { - for (node.children.items) |child_index| { - var child_node = &data.nodes.items[child_index]; - child_node.parent = parent_index; - fillParents(data, child_node, child_index); - } -} - -test "gltf.parseGlb" { - const allocator = std.testing.allocator; - const expectEqualSlices = std.testing.expectEqualSlices; - - // This is the '.glb' file. - const glb_buf = try std.fs.cwd().readFileAllocOptions(allocator, "test-samples/box_binary/Box.glb", 512_000, null, 4, null); - defer allocator.free(glb_buf); - - var gltf = Self.init(allocator); - defer gltf.deinit(); - - try expectEqualSlices(u8, gltf.data.asset.version, "Undefined"); - - try gltf.parseGlb(glb_buf); - - const mesh = gltf.data.meshes.items[0]; - for (mesh.primitives.items) |primitive| { - for (primitive.attributes.items) |attribute| { - switch (attribute) { - .position => |accessor_index| { - var tmp = ArrayList(f32).init(allocator); - defer tmp.deinit(); - - const accessor = gltf.data.accessors.items[accessor_index]; - gltf.getDataFromBufferView(f32, &tmp, accessor, gltf.glb_binary.?); - - try expectEqualSlices(f32, tmp.items, &[72]f32{ - // zig fmt: off - -0.50, -0.50, 0.50, 0.50, -0.50, 0.50, -0.50, 0.50, 0.50, - 0.50, 0.50, 0.50, 0.50, -0.50, 0.50, -0.50, -0.50, 0.50, - 0.50, -0.50, -0.50, -0.50, -0.50, -0.50, 0.50, 0.50, 0.50, - 0.50, -0.50, 0.50, 0.50, 0.50, -0.50, 0.50, -0.50, -0.50, - -0.50, 0.50, 0.50, 0.50, 0.50, 0.50, -0.50, 0.50, -0.50, - 0.50, 0.50, -0.50, -0.50, -0.50, 0.50, -0.50, 0.50, 0.50, - -0.50, -0.50, -0.50, -0.50, 0.50, -0.50, -0.50, -0.50, -0.50, - -0.50, 0.50, -0.50, 0.50, -0.50, -0.50, 0.50, 0.50, -0.50, - }); - }, - else => {}, - } - } - } -} - -test "gltf.parseGlbTextured" { - const allocator = std.testing.allocator; - const expectEqualSlices = std.testing.expectEqualSlices; - - // This is the '.glb' file. - const glb_buf = try std.fs.cwd().readFileAllocOptions( - allocator, - "test-samples/box_binary_textured/BoxTextured.glb", - 512_000, - null, - 4, - null - ); - defer allocator.free(glb_buf); - - var gltf = Self.init(allocator); - defer gltf.deinit(); - - try gltf.parseGlb(glb_buf); - - const test_to_check = try std.fs.cwd().readFileAlloc( - allocator, - "test-samples/box_binary_textured/test.png", - 512_000 - ); - defer allocator.free(test_to_check); - - const data = gltf.data.images.items[0].data.?; - try expectEqualSlices(u8, test_to_check, data); -} - -test "gltf.parse" { - const allocator = std.testing.allocator; - const expectEqualSlices = std.testing.expectEqualSlices; - const expectEqual = std.testing.expectEqual; - - // This is the '.gltf' file, a json specifying what information is in the - // model and how to retrieve it inside binary file(s). - const buf = try std.fs.cwd().readFileAllocOptions( - allocator, - "test-samples/rigged_simple/RiggedSimple.gltf", - 512_000, - null, - 4, - null - ); - defer allocator.free(buf); - - var gltf = Self.init(allocator); - defer gltf.deinit(); - - try expectEqualSlices(u8, gltf.data.asset.version, "Undefined"); - - try gltf.parse(buf); - - try expectEqualSlices(u8, gltf.data.asset.version, "2.0"); - try expectEqualSlices(u8, gltf.data.asset.generator.?, "COLLADA2GLTF"); - - try expectEqual(gltf.data.scene, 0); - - // Nodes. - const nodes = gltf.data.nodes.items; - try expectEqualSlices(u8, nodes[0].name, "Z_UP"); - try expectEqualSlices(usize, nodes[0].children.items, &[_]usize{1}); - try expectEqualSlices(u8, nodes[2].name, "Cylinder"); - try expectEqual(nodes[2].skin, 0); - - try expectEqual(gltf.data.buffers.items.len > 0, true); - - // Skin - const skin = gltf.data.skins.items[0]; - try expectEqualSlices(u8, skin.name, "Armature"); -} - -test "gltf.parse (cameras)" { - const allocator = std.testing.allocator; - const expectEqual = std.testing.expectEqual; - - const buf = try std.fs.cwd().readFileAllocOptions( - allocator, - "test-samples/cameras/Cameras.gltf", - 512_000, - null, - 4, - null - ); - defer allocator.free(buf); - - var gltf = Self.init(allocator); - defer gltf.deinit(); - - try gltf.parse(buf); - - try expectEqual(gltf.data.nodes.items[1].camera, 0); - try expectEqual(gltf.data.nodes.items[2].camera, 1); - - const camera_0 = gltf.data.cameras.items[0]; - try expectEqual(camera_0.type.perspective, Camera.Perspective{ - .aspect_ratio = 1.0, - .yfov = 0.7, - .zfar = 100, - .znear = 0.01, - }); - - const camera_1 = gltf.data.cameras.items[1]; - try expectEqual(camera_1.type.orthographic, Camera.Orthographic{ - .xmag = 1.0, - .ymag = 1.0, - .zfar = 100, - .znear = 0.01, - }); -} - -test "gltf.getDataFromBufferView" { - const allocator = std.testing.allocator; - const expectEqualSlices = std.testing.expectEqualSlices; - - const buf = try std.fs.cwd().readFileAllocOptions( - allocator, - "test-samples/box/Box.gltf", - 512_000, - null, - 4, - null - ); - defer allocator.free(buf); - - // This is the '.bin' file containing all the gltf underneath data. - const binary = try std.fs.cwd().readFileAllocOptions( - allocator, - "test-samples/box/Box0.bin", - 5_000_000, - null, - // From gltf spec, data from BufferView should be 4 bytes aligned. - 4, - null, - ); - defer allocator.free(binary); - - var gltf = Self.init(allocator); - defer gltf.deinit(); - - try gltf.parse(buf); - - const mesh = gltf.data.meshes.items[0]; - for (mesh.primitives.items) |primitive| { - for (primitive.attributes.items) |attribute| { - switch (attribute) { - .position => |accessor_index| { - var tmp = ArrayList(f32).init(allocator); - defer tmp.deinit(); - - const accessor = gltf.data.accessors.items[accessor_index]; - gltf.getDataFromBufferView(f32, &tmp, accessor, binary); - - try expectEqualSlices(f32, tmp.items, &[72]f32{ - // zig fmt: off - -0.50, -0.50, 0.50, 0.50, -0.50, 0.50, -0.50, 0.50, 0.50, - 0.50, 0.50, 0.50, 0.50, -0.50, 0.50, -0.50, -0.50, 0.50, - 0.50, -0.50, -0.50, -0.50, -0.50, -0.50, 0.50, 0.50, 0.50, - 0.50, -0.50, 0.50, 0.50, 0.50, -0.50, 0.50, -0.50, -0.50, - -0.50, 0.50, 0.50, 0.50, 0.50, 0.50, -0.50, 0.50, -0.50, - 0.50, 0.50, -0.50, -0.50, -0.50, 0.50, -0.50, 0.50, 0.50, - -0.50, -0.50, -0.50, -0.50, 0.50, -0.50, -0.50, -0.50, -0.50, - -0.50, 0.50, -0.50, 0.50, -0.50, -0.50, 0.50, 0.50, -0.50, - }); - }, - else => {}, - } - } - } -} - -test "gltf.parse (lights)" { - const allocator = std.testing.allocator; - const expect = std.testing.expect; - const expectEqual = std.testing.expectEqual; - - const buf = try std.fs.cwd().readFileAllocOptions( - allocator, - "test-samples/khr_lights_punctual/Lights.gltf", - 512_000, - null, - 4, - null - ); - defer allocator.free(buf); - - var gltf = Self.init(allocator); - defer gltf.deinit(); - - try gltf.parse(buf); - - try expectEqual(@as(usize, 3), gltf.data.lights.items.len); - - try expect(gltf.data.lights.items[0].name != null); - try expect(std.mem.eql(u8, "Light", gltf.data.lights.items[0].name.?)); - try expectEqual([3]f32 { 1, 1, 1 }, gltf.data.lights.items[0].color); - try expectEqual(@as(f32, 1000), gltf.data.lights.items[0].intensity); - try expectEqual(LightType.point, gltf.data.lights.items[0].type); - - try expect(gltf.data.lights.items[1].name != null); - try expect(std.mem.eql(u8, "Light.001", gltf.data.lights.items[1].name.?)); - try expectEqual([3]f32 { 1, 1, 1 }, gltf.data.lights.items[1].color); - try expectEqual(@as(f32, 1000), gltf.data.lights.items[1].intensity); - try expectEqual(LightType.spot, gltf.data.lights.items[1].type); - - try expect(gltf.data.lights.items[1].spot != null); - try expectEqual(@as(f32, 0), gltf.data.lights.items[1].spot.?.inner_cone_angle); - try expectEqual(@as(f32, 1), gltf.data.lights.items[1].spot.?.outer_cone_angle); - - try expect(gltf.data.lights.items[2].name != null); - try expect(std.mem.eql(u8, "Light.002", gltf.data.lights.items[2].name.?)); - try expectEqual([3]f32 { 1, 1, 1 }, gltf.data.lights.items[2].color); - try expectEqual(@as(f32, 1000), gltf.data.lights.items[2].intensity); - try expectEqual(LightType.directional, gltf.data.lights.items[2].type); - - try expect(gltf.data.nodes.items[0].light != null); - try expectEqual(@as(?Index, 0), gltf.data.nodes.items[0].light); -} diff --git a/lib/zgltf/src/types.zig b/lib/zgltf/src/types.zig index 6a2351d..c434a7c 100644 --- a/lib/zgltf/src/types.zig +++ b/lib/zgltf/src/types.zig @@ -1,8 +1,8 @@ const std = @import("std"); -const Gltf = @import("main.zig"); +const Gltf = @import("Gltf.zig"); const pi = std.math.pi; -const ArrayList = std.ArrayList; const panic = std.debug.panic; +const json = @import("std").json; /// Index of element in data arrays. pub const Index = usize; @@ -20,8 +20,7 @@ pub const Index = usize; /// an animation.channel.target), matrix must not be present. pub const Node = struct { /// The user-defined name of this object. - /// Default to `Node_{index}`. - name: []const u8, + name: ?[]const u8 = null, /// The index of the node's parent. /// A node is called a root node when it doesn’t have a parent. parent: ?Index = null, @@ -32,7 +31,7 @@ pub const Node = struct { /// The index of the skin referenced by this node. skin: ?Index = null, /// The indices of this node’s children. - children: ArrayList(Index), + children: []Index = &[_]Index{}, /// A floating-point 4x4 transformation matrix stored in column-major order. matrix: ?[16]f32 = null, /// The node’s unit quaternion rotation in the order (x, y, z, w), @@ -49,6 +48,8 @@ pub const Node = struct { weights: ?[]usize = null, ///The index of the light referenced by this node. light: ?Index = null, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// A buffer points to binary geometry, animation, or skins. @@ -59,6 +60,8 @@ pub const Buffer = struct { uri: ?[]const u8 = null, /// The length of the buffer in bytes. byte_length: usize, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// A view into a buffer generally representing a subset of the buffer. @@ -74,6 +77,8 @@ pub const BufferView = struct { /// The hint representing the intended GPU buffer type /// to use with this buffer view. target: ?Target = null, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// A typed view into a buffer view that contains raw binary data. @@ -86,12 +91,12 @@ pub const Accessor = struct { component_type: ComponentType, /// Specifies if the accessor’s elements are scalars, vectors, or matrices. type: AccessorType, - /// Computed stride: @sizeOf(component_type) * type. - stride: usize, /// The number of elements referenced by this accessor. - count: i32, + count: usize, /// Specifies whether integer data values are normalized before usage. normalized: bool = false, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, pub fn iterator( accessor: Accessor, @@ -99,14 +104,7 @@ pub const Accessor = struct { gltf: *const Gltf, binary: []align(4) const u8, ) AccessorIterator(T) { - if (switch (accessor.component_type) { - .byte => T != i8, - .unsigned_byte => T != u8, - .short => T != i16, - .unsigned_short => T != u16, - .unsigned_integer => T != u32, - .float => T != f32, - }) { + if (ComponentType.fromType(T) != accessor.component_type) { panic( "Mismatch between gltf component '{}' and given type '{}'.", .{ accessor.component_type, T }, @@ -117,30 +115,14 @@ pub const Accessor = struct { panic("Accessors without buffer_view are not supported yet.", .{}); } - const buffer_view = gltf.data.buffer_views.items[accessor.buffer_view.?]; + const buffer_view = gltf.data.buffer_views[accessor.buffer_view.?]; - const comp_size = @sizeOf(T); - const offset = (accessor.byte_offset + buffer_view.byte_offset) / comp_size; - - const stride = blk: { - if (buffer_view.byte_stride) |byte_stride| { - break :blk byte_stride / comp_size; - } else { - break :blk accessor.stride / comp_size; - } - }; + const offset = (accessor.byte_offset + buffer_view.byte_offset) / @sizeOf(T); + const datum_count: usize = accessor.type.componentCount(); + // When byte_stride is null, data is tightly packed, so stride = datum_count + const stride = if (buffer_view.byte_stride) |byte_stride| (byte_stride / @sizeOf(T)) else datum_count; const total_count: usize = @intCast(accessor.count); - const datum_count: usize = switch (accessor.type) { - .scalar => 1, - .vec2 => 2, - .vec3 => 3, - .vec4 => 4, - .mat4x4 => 16, - else => { - panic("Accessor type '{}' not implemented.", .{accessor.type}); - }, - }; const data: [*]const T = @ptrCast(@alignCast(binary.ptr)); @@ -191,22 +173,26 @@ pub fn AccessorIterator(comptime T: type) type { /// The root nodes of a scene. pub const Scene = struct { /// The user-defined name of this object. - name: []const u8, + name: ?[]const u8 = null, /// The indices of each root node. - nodes: ?ArrayList(Index) = null, + nodes: ?[]Index = null, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// Joints and matrices defining a skin. pub const Skin = struct { /// The user-defined name of this object. - name: []const u8, + name: ?[]const u8 = null, /// The index of the accessor containing the floating-point /// 4x4 inverse-bind matrices. inverse_bind_matrices: ?Index = null, /// The index of the node used as a skeleton root. skeleton: ?Index = null, /// Indices of skeleton nodes, used as joints in this skin. - joints: ArrayList(Index), + joints: []Index = &[_]Index{}, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// Reference to a texture. @@ -260,7 +246,7 @@ pub const MetallicRoughness = struct { /// The material appearance of a primitive. pub const Material = struct { /// The user-defined name of this object. - name: []const u8, + name: ?[]const u8 = null, /// A set of parameter values that are used to define /// the metallic-roughness material model /// from Physically Based Rendering methodology. @@ -309,11 +295,13 @@ pub const Material = struct { /// The strength of the dispersion effect. /// Note: from khr_materials_dispersion extension. dispersion: f32 = 0.0, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// The material’s alpha rendering mode enumeration specifying /// the interpretation of the alpha value of the base color. -const AlphaMode = enum { +pub const AlphaMode = enum { /// The alpha value is ignored, and the rendered output is fully opaque. @"opaque", /// The rendered output is either fully opaque or fully transparent @@ -337,11 +325,22 @@ pub const Texture = struct { /// When undefined, an extension or other mechanism should supply /// an alternate texture source, otherwise behavior is undefined. source: ?Index = null, + /// Extension object with extension-specific objects. + extensions: struct { + EXT_texture_webp: ?struct { + /// The index of the WebP image used by this texture. + source: Index, + } = null, + } = .{}, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// Image data used to create a texture. /// Image may be referenced by an uri or a buffer view index. pub const Image = struct { + /// The user-defined name of this object. + name: ?[]const u8 = null, /// The URI (or IRI) of the image. uri: ?[]const u8 = null, /// The image’s media type. @@ -353,6 +352,8 @@ pub const Image = struct { /// The image's data calculated from the buffer/buffer_view. /// Only there if glb file is loaded. data: ?[]const u8 = null, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; pub const WrapMode = enum(u32) { @@ -385,6 +386,8 @@ pub const TextureSampler = struct { wrap_s: WrapMode = .repeat, /// T (U) wrapping mode. wrap_t: WrapMode = .repeat, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// Values are Accessor's index. @@ -406,6 +409,18 @@ pub const AccessorType = enum { mat2x2, mat3x3, mat4x4, + + pub fn componentCount(self: AccessorType) usize { + return switch (self) { + .scalar => 1, + .vec2 => 2, + .vec3 => 3, + .vec4 => 4, + .mat2x2 => 4, + .mat3x3 => 9, + .mat4x4 => 16, + }; + } }; /// Enum values from GLTF 2.0 spec. @@ -416,18 +431,35 @@ pub const Target = enum(u32) { /// Enum values from GLTF 2.0 spec. pub const ComponentType = enum(u32) { - /// i8. byte = 5120, - /// u8. unsigned_byte = 5121, - /// i16. short = 5122, - /// u16. unsigned_short = 5123, - /// u32. unsigned_integer = 5125, - /// f32. float = 5126, + + pub fn fromType(T: type) ComponentType { + return switch (T) { + i8 => .byte, + u8 => .unsigned_byte, + i16 => .short, + u16 => .unsigned_short, + u32 => .unsigned_integer, + f32 => .float, + else => @compileError("invalid type " ++ @typeName(T) ++ " for ComponentType.fromType"), + }; + } + + pub fn byteSize(self: ComponentType) usize { + return switch (self) { + .byte => @sizeOf(i8), + .unsigned_byte => @sizeOf(u8), + .short => @sizeOf(i16), + .unsigned_short => @sizeOf(u16), + .unsigned_integer => @sizeOf(u32), + .float => @sizeOf(f32), + }; + } }; /// The topology type of primitives to render. @@ -471,6 +503,8 @@ pub const Channel = struct { /// of the Morph Targets it instantiates. property: TargetProperty, }, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// Interpolation algorithm. @@ -496,41 +530,49 @@ pub const AnimationSampler = struct { output: Index, /// Interpolation algorithm. interpolation: Interpolation = .linear, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// A keyframe animation. pub const Animation = struct { /// The user-defined name of this object. - name: []const u8, + name: ?[]const u8 = null, /// An array of animation channels. /// An animation channel combines an animation sampler with a target /// property being animated. /// Different channels of the same animation must not have the same targets. - channels: ArrayList(Channel), + channels: []Channel = &[_]Channel{}, /// An array of animation samplers. /// An animation sampler combines timestamps with a sequence of output /// values and defines an interpolation algorithm. - samplers: ArrayList(AnimationSampler), + samplers: []AnimationSampler = &[_]AnimationSampler{}, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// Geometry to be rendered with the given material. pub const Primitive = struct { - attributes: ArrayList(Attribute), + attributes: []Attribute = &[_]Attribute{}, /// The topology type of primitives to render. mode: Mode = .triangles, /// The index of the accessor that contains the vertex indices. indices: ?Index = null, /// The index of the material to apply to this primitive when rendering. material: ?Index = null, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// A set of primitives to be rendered. /// Its global transform is defined by a node that references it. pub const Mesh = struct { /// The user-defined name of this object. - name: []const u8, + name: ?[]const u8 = null, /// An array of primitives, each defining geometry to be rendered. - primitives: ArrayList(Primitive), + primitives: []Primitive = &[_]Primitive{}, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// Metadata about the glTF asset. @@ -541,6 +583,8 @@ pub const Asset = struct { generator: ?[]const u8 = null, /// A copyright message suitable for display to credit the content creator. copyright: ?[]const u8 = null, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// A camera’s projection. @@ -580,11 +624,13 @@ pub const Camera = struct { znear: f32, }; - name: []const u8, + name: ?[]const u8 = null, type: union(enum) { perspective: Perspective, orthographic: Orthographic, }, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; /// Specifies the light type. @@ -614,7 +660,7 @@ pub const LightType = enum { /// A directional, point or spot light. pub const Light = struct { - name: ?[]const u8, + name: ?[]const u8 = null, /// Color of the light source. color: [3]f32 = .{ 1, 1, 1 }, /// Intensity of the light source. `point` and `spot` lights use luminous intensity in candela (lm/sr) @@ -626,6 +672,8 @@ pub const Light = struct { spot: ?LightSpot, /// A distance cutoff at which the light's intensity may be considered to have reached zero. range: f32, + /// Any extra, custom attributes. + extras: ?json.ObjectMap = null, }; pub const LightSpot = struct {