Backlog/lib/zgltf/src/main.zig

1613 lines
56 KiB
Zig

///
/// 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);
}