//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® module; // nlohmann/json stays inside this unit. Included from the interface, it // would be parsed again by everything that imports Crafter.Asset. #include "../lib/json.hpp" module Crafter.Asset; import std; import Crafter.Math; namespace fs = std::filesystem; // Json, not json: GlbAsset has a member called json, which would hide it // inside GlbAsset::Load. using Json = nlohmann::json; using namespace Crafter; namespace { constexpr std::uint32_t kMagic = 0x46546C67; // "glTF" constexpr std::uint32_t kChunkJson = 0x4E4F534A; // "JSON" constexpr std::uint32_t kChunkBin = 0x004E4942; // "BIN\0" constexpr std::uint32_t kByte = 5120; constexpr std::uint32_t kUnsignedByte = 5121; constexpr std::uint32_t kShort = 5122; constexpr std::uint32_t kUnsignedShort = 5123; constexpr std::uint32_t kUnsignedInt = 5125; constexpr std::uint32_t kFloat = 5126; std::unexpected Error(std::string message) { return std::unexpected("GLB: " + std::move(message)); } // Reads from the parsed JSON that never throw (a -fno-exceptions build // aborts on a throw): a missing value or one of the wrong type reads as // the fallback. const Json* Find(const Json& object, const char* key) { if (!object.is_object()) return nullptr; const auto it = object.find(key); return it == object.end() ? nullptr : &*it; } std::int64_t Integer(const Json& object, const char* key, std::int64_t fallback) { const Json* value = Find(object, key); return value != nullptr && value->is_number_integer() ? value->get() : fallback; } float Number(const Json& object, const char* key, float fallback) { const Json* value = Find(object, key); return value != nullptr && value->is_number() ? value->get() : fallback; } bool Boolean(const Json& object, const char* key, bool fallback) { const Json* value = Find(object, key); return value != nullptr && value->is_boolean() ? value->get() : fallback; } std::string String(const Json& object, const char* key) { const Json* value = Find(object, key); return value != nullptr && value->is_string() ? value->get() : std::string(); } const Json& Array(const Json& object, const char* key) { static const Json empty = Json::array(); const Json* value = Find(object, key); return value != nullptr && value->is_array() ? *value : empty; } // An object's extras as JSON text, or empty when it has none. std::string Extras(const Json& object) { const Json* value = Find(object, "extras"); return value != nullptr ? value->dump() : std::string(); } // The object an extension keeps in `object`'s extensions, or null. const Json* Extension(const Json& object, const char* name) { const Json* extensions = Find(object, "extensions"); return extensions != nullptr ? Find(*extensions, name) : nullptr; } // Up to N numbers from a JSON array into `out`, which keeps its values // where the array is shorter or holds something else. template void Numbers(const Json& object, const char* key, std::array& out) { const Json& values = Array(object, key); for (std::size_t i = 0; i < N && i < values.size(); ++i) { if (values[i].is_number()) out[i] = values[i].get(); } } std::size_t ComponentSize(std::uint32_t componentType) { switch (componentType) { case kByte: case kUnsignedByte: return 1; case kShort: case kUnsignedShort: return 2; case kUnsignedInt: case kFloat: return 4; default: return 0; } } std::uint32_t ComponentCount(const std::string& type) { if (type == "SCALAR") return 1; if (type == "VEC2") return 2; if (type == "VEC3") return 3; if (type == "VEC4") return 4; if (type == "MAT4") return 16; return 0; } // An accessor resolved to its bytes in the BIN chunk. struct Accessor { const std::byte* data; std::size_t count; std::size_t stride; std::uint32_t componentType; std::uint32_t components; bool normalized; float Float(std::size_t element, std::uint32_t component) const { const std::byte* p = data + element * stride + component * ComponentSize(componentType); switch (componentType) { case kFloat: { float v; std::memcpy(&v, p, sizeof(v)); return v; } case kUnsignedByte: { std::uint8_t v; std::memcpy(&v, p, sizeof(v)); return normalized ? v / 255.0f : v; } case kUnsignedShort: { std::uint16_t v; std::memcpy(&v, p, sizeof(v)); return normalized ? v / 65535.0f : v; } case kByte: { std::int8_t v; std::memcpy(&v, p, sizeof(v)); return normalized ? std::max(v / 127.0f, -1.0f) : v; } case kShort: { std::int16_t v; std::memcpy(&v, p, sizeof(v)); return normalized ? std::max(v / 32767.0f, -1.0f) : v; } case kUnsignedInt: { std::uint32_t v; std::memcpy(&v, p, sizeof(v)); return static_cast(v); } default: return 0.0f; } } std::uint32_t Unsigned(std::size_t element, std::uint32_t component) const { const std::byte* p = data + element * stride + component * ComponentSize(componentType); switch (componentType) { case kUnsignedByte: { std::uint8_t v; std::memcpy(&v, p, sizeof(v)); return v; } case kUnsignedShort: { std::uint16_t v; std::memcpy(&v, p, sizeof(v)); return v; } case kUnsignedInt: { std::uint32_t v; std::memcpy(&v, p, sizeof(v)); return v; } default: return 0; } } }; struct Context { const Json& root; std::span bin; }; // The bytes of buffer view `index`, which must lie in the BIN chunk. std::expected, std::string> ViewBytes(const Context& ctx, std::int64_t index, std::size_t* strideOut) { const Json& views = Array(ctx.root, "bufferViews"); if (index < 0 || static_cast(index) >= views.size()) { return Error(std::format("bufferView {} doesn't exist", index)); } const Json& view = views[static_cast(index)]; if (Integer(view, "buffer", -1) != 0) { return Error(std::format("bufferView {} uses an external buffer, which a GLB loader doesn't read", index)); } const std::int64_t offset = Integer(view, "byteOffset", 0); const std::int64_t length = Integer(view, "byteLength", -1); // Compared so that nothing can overflow. const std::uint64_t size = ctx.bin.size(); if (offset < 0 || length < 0 || static_cast(offset) > size || static_cast(length) > size - static_cast(offset)) { return Error(std::format("bufferView {} lies outside the BIN chunk", index)); } // glTF allows 4 to 252 bytes between elements; 0 or none packs them. const std::int64_t stride = Integer(view, "byteStride", 0); if (stride != 0 && (stride < 4 || stride > 252)) { return Error(std::format("bufferView {} has a byteStride of {}, outside 4..252", index, stride)); } if (strideOut != nullptr) *strideOut = static_cast(stride); return ctx.bin.subspan(static_cast(offset), static_cast(length)); } std::expected ReadAccessor(const Context& ctx, std::int64_t index) { const Json& accessors = Array(ctx.root, "accessors"); if (index < 0 || static_cast(index) >= accessors.size()) { return Error(std::format("accessor {} doesn't exist", index)); } const Json& accessor = accessors[static_cast(index)]; if (Find(accessor, "sparse") != nullptr) { return Error(std::format("accessor {} is sparse, which isn't supported", index)); } const std::uint32_t componentType = static_cast(Integer(accessor, "componentType", 0)); const std::uint32_t components = ComponentCount(String(accessor, "type")); if (ComponentSize(componentType) == 0 || components == 0) { return Error(std::format("accessor {} has an unsupported type or componentType", index)); } // Matrices of 1- and 2-byte components pad their columns; nothing // here reads those. if (components == 16 && ComponentSize(componentType) != 4) { return Error(std::format("accessor {} is a MAT4 of small components, which isn't supported", index)); } const std::int64_t count = Integer(accessor, "count", -1); if (count < 0) return Error(std::format("accessor {} has no count", index)); if (Find(accessor, "bufferView") == nullptr) { return Error(std::format("accessor {} has no bufferView", index)); } std::size_t viewStride = 0; const auto view = ViewBytes(ctx, Integer(accessor, "bufferView", -1), &viewStride); if (!view) return std::unexpected(view.error()); const std::size_t elementSize = ComponentSize(componentType) * components; if (viewStride != 0 && viewStride < elementSize) { return Error(std::format("accessor {}'s elements overlap: {} bytes in a stride of {}", index, elementSize, viewStride)); } const std::size_t stride = viewStride != 0 ? viewStride : elementSize; const std::int64_t offset = Integer(accessor, "byteOffset", 0); // The last element ends inside the view, compared so that nothing // can overflow, whatever the count. const std::uint64_t size = view->size(); const bool inside = offset >= 0 && static_cast(offset) <= size && (count == 0 || (elementSize <= size - static_cast(offset) && static_cast(count - 1) <= (size - static_cast(offset) - elementSize) / stride)); if (!inside) { return Error(std::format("accessor {} reads past the end of its bufferView", index)); } return Accessor{ .data = view->data() + offset, .count = static_cast(count), .stride = stride, .componentType = componentType, .components = components, .normalized = Boolean(accessor, "normalized", false), }; } // An accessor of `components` per element whose component type is one of // `allowed`, with exactly `count` elements unless count is 0. std::expected Attribute(const Context& ctx, const Json& attributes, const char* name, std::uint32_t components, std::initializer_list allowed, std::size_t count, const std::string& where) { auto accessor = ReadAccessor(ctx, Integer(attributes, name, -1)); if (!accessor) return std::unexpected(std::format("{} ({} of {})", accessor.error(), name, where)); if (accessor->components != components || std::ranges::find(allowed, accessor->componentType) == allowed.end()) { return Error(std::format("{} of {} has the wrong type", name, where)); } if (count != 0 && accessor->count != count) { return Error(std::format("{} of {} has {} elements, POSITION has {}", name, where, accessor->count, count)); } return accessor; } // Per-vertex tangents from the UV gradients, accumulated over the // triangles a vertex is in and made orthogonal to its normal. glTF's v // runs down the image while a normal map's +Y points up, so its // bitangent is the one along -v: hence the sign below, the same // correction three.js applies to MikkTSpace output for glTF. void ComputeTangents(GlbPrimitive& p) { using V3 = Vector; const std::size_t n = p.positions.size(); std::vector tangentSum(n, V3(0, 0, 0)); std::vector bitangentSum(n, V3(0, 0, 0)); for (std::size_t t = 0; t + 2 < p.indices.size(); t += 3) { const std::uint32_t i[3] = {p.indices[t], p.indices[t + 1], p.indices[t + 2]}; const V3 p0(p.positions[i[0]].x, p.positions[i[0]].y, p.positions[i[0]].z); const V3 e1 = V3(p.positions[i[1]].x, p.positions[i[1]].y, p.positions[i[1]].z) - p0; const V3 e2 = V3(p.positions[i[2]].x, p.positions[i[2]].y, p.positions[i[2]].z) - p0; const float du1 = p.uv0[i[1]].x - p.uv0[i[0]].x, dv1 = p.uv0[i[1]].y - p.uv0[i[0]].y; const float du2 = p.uv0[i[2]].x - p.uv0[i[0]].x, dv2 = p.uv0[i[2]].y - p.uv0[i[0]].y; const float determinant = du1 * dv2 - du2 * dv1; if (std::abs(determinant) < 1e-12f) continue; // degenerate UVs carry no direction const float f = 1.0f / determinant; const V3 tangent = (e1 * dv2 - e2 * dv1) * f; const V3 bitangent = (e2 * du1 - e1 * du2) * f; for (std::uint32_t v : i) { tangentSum[v] += tangent; bitangentSum[v] += bitangent; } } p.tangents.resize(n); for (std::size_t v = 0; v < n; ++v) { const V3 normal(p.normals[v].x, p.normals[v].y, p.normals[v].z); V3 tangent = tangentSum[v] - normal * V3::Dot(normal, tangentSum[v]); if (tangent.LengthSq() < 1e-20f) { // No usable UV gradient: any direction perpendicular to the // normal keeps the frame valid. tangent = V3::Cross(std::abs(normal.x) < 0.9f ? V3(1, 0, 0) : V3(0, 1, 0), normal); } tangent.Normalize(); const float sign = V3::Dot(V3::Cross(normal, tangent), bitangentSum[v]) < 0 ? 1.0f : -1.0f; p.tangents[v] = Vector(tangent.x, tangent.y, tangent.z, sign); } } std::expected ReadPrimitive(const Context& ctx, const Json& primitive, const std::string& where) { if (Integer(primitive, "mode", 4) != 4) { return Error(std::format("{} is not a triangle list (mode {})", where, Integer(primitive, "mode", 4))); } const Json* attributes = Find(primitive, "attributes"); if (attributes == nullptr || Find(*attributes, "POSITION") == nullptr) { return Error(std::format("{} has no POSITION", where)); } GlbPrimitive p; const auto positions = Attribute(ctx, *attributes, "POSITION", 3, {kFloat}, 0, where); if (!positions) return std::unexpected(positions.error()); const std::size_t n = positions->count; p.positions.reserve(n); for (std::size_t v = 0; v < n; ++v) { p.positions.emplace_back(positions->Float(v, 0), positions->Float(v, 1), positions->Float(v, 2)); } if (Find(*attributes, "NORMAL") != nullptr) { const auto normals = Attribute(ctx, *attributes, "NORMAL", 3, {kFloat}, n, where); if (!normals) return std::unexpected(normals.error()); p.normals.reserve(n); for (std::size_t v = 0; v < n; ++v) { p.normals.emplace_back(normals->Float(v, 0), normals->Float(v, 1), normals->Float(v, 2)); } } if (Find(*attributes, "TEXCOORD_0") != nullptr) { const auto uv = Attribute(ctx, *attributes, "TEXCOORD_0", 2, {kFloat, kUnsignedByte, kUnsignedShort}, n, where); if (!uv) return std::unexpected(uv.error()); p.uv0.reserve(n); for (std::size_t v = 0; v < n; ++v) { p.uv0.emplace_back(uv->Float(v, 0), uv->Float(v, 1)); } } if (Find(*attributes, "TANGENT") != nullptr) { const auto tangents = Attribute(ctx, *attributes, "TANGENT", 4, {kFloat}, n, where); if (!tangents) return std::unexpected(tangents.error()); p.tangents.reserve(n); for (std::size_t v = 0; v < n; ++v) { p.tangents.emplace_back(tangents->Float(v, 0), tangents->Float(v, 1), tangents->Float(v, 2), tangents->Float(v, 3)); } } if (Find(*attributes, "JOINTS_0") != nullptr) { const auto joints = Attribute(ctx, *attributes, "JOINTS_0", 4, {kUnsignedByte, kUnsignedShort}, n, where); if (!joints) return std::unexpected(joints.error()); p.joints.resize(n); for (std::size_t v = 0; v < n; ++v) { for (std::uint32_t c = 0; c < 4; ++c) p.joints[v][c] = static_cast(joints->Unsigned(v, c)); } } if (Find(*attributes, "WEIGHTS_0") != nullptr) { const auto weights = Attribute(ctx, *attributes, "WEIGHTS_0", 4, {kFloat, kUnsignedByte, kUnsignedShort}, n, where); if (!weights) return std::unexpected(weights.error()); p.weights.resize(n); for (std::size_t v = 0; v < n; ++v) { float sum = 0; for (std::uint32_t c = 0; c < 4; ++c) { p.weights[v][c] = weights->Float(v, c); sum += p.weights[v][c]; } if (sum > 0) { for (float& w : p.weights[v]) w /= sum; } } } if (Find(primitive, "indices") != nullptr) { const auto indices = ReadAccessor(ctx, Integer(primitive, "indices", -1)); if (!indices) return std::unexpected(std::format("{} (indices of {})", indices.error(), where)); if (indices->components != 1 || (indices->componentType != kUnsignedByte && indices->componentType != kUnsignedShort && indices->componentType != kUnsignedInt)) { return Error(std::format("indices of {} have the wrong type", where)); } p.indices.reserve(indices->count); for (std::size_t i = 0; i < indices->count; ++i) p.indices.push_back(indices->Unsigned(i, 0)); } else { p.indices.resize(n); std::iota(p.indices.begin(), p.indices.end(), 0u); } if (p.indices.size() % 3 != 0) { return Error(std::format("{} has {} indices, not whole triangles", where, p.indices.size())); } if (std::ranges::any_of(p.indices, [n](std::uint32_t i) { return i >= n; })) { return Error(std::format("{} indexes past its {} vertices", where, n)); } p.material = static_cast(Integer(primitive, "material", -1)); if (p.tangents.empty() && !p.normals.empty() && !p.uv0.empty()) ComputeTangents(p); return p; } GlbTextureRef TextureRef(const Json& material, const char* key, const char* scaleKey) { GlbTextureRef ref; if (const Json* info = Find(material, key)) { ref.texture = static_cast(Integer(*info, "index", -1)); ref.texCoord = static_cast(Integer(*info, "texCoord", 0)); if (scaleKey != nullptr) ref.scale = Number(*info, scaleKey, 1.0f); } return ref; } // Each index points at an element that exists, so callers can index // without checking. std::expected CheckReferences(const GlbAsset& a) { auto valid = [](std::int64_t index, std::size_t size) { return index == -1 || (index >= 0 && static_cast(index) < size); }; for (std::size_t i = 0; i < a.nodes.size(); ++i) { if (!valid(a.nodes[i].mesh, a.meshes.size())) return Error(std::format("node {} uses a mesh that doesn't exist", i)); if (!valid(a.nodes[i].skin, a.skins.size())) return Error(std::format("node {} uses a skin that doesn't exist", i)); if (!valid(a.nodes[i].camera, a.cameras.size())) return Error(std::format("node {} uses a camera that doesn't exist", i)); if (!valid(a.nodes[i].light, a.lights.size())) return Error(std::format("node {} uses a light that doesn't exist", i)); } for (const GlbMesh& mesh : a.meshes) { for (const GlbPrimitive& p : mesh.primitives) { if (!valid(p.material, a.materials.size())) return Error(std::format("mesh '{}' uses a material that doesn't exist", mesh.name)); } } // A vertex's joint indices pick from the joint list of the skin on // the node that draws its mesh. for (const GlbNode& node : a.nodes) { if (node.mesh == -1 || node.skin == -1) continue; const std::size_t jointCount = a.skins[static_cast(node.skin)].joints.size(); for (const GlbPrimitive& p : a.meshes[static_cast(node.mesh)].primitives) { for (const auto& joints : p.joints) { if (std::ranges::any_of(joints, [jointCount](std::uint16_t j) { return j >= jointCount; })) { return Error(std::format("node '{}' skins mesh '{}' with joint indices beyond its skin's {} joints", node.name, a.meshes[static_cast(node.mesh)].name, jointCount)); } } } } for (const GlbSkin& skin : a.skins) { if (!valid(skin.skeleton, a.nodes.size())) return Error(std::format("skin '{}' has a skeleton node that doesn't exist", skin.name)); for (std::uint32_t joint : skin.joints) { if (joint >= a.nodes.size()) return Error(std::format("skin '{}' uses node {}, which doesn't exist", skin.name, joint)); } } for (std::size_t i = 0; i < a.textures.size(); ++i) { if (!valid(a.textures[i].image, a.images.size())) return Error(std::format("texture {} uses an image that doesn't exist", i)); if (!valid(a.textures[i].sampler, a.samplers.size())) return Error(std::format("texture {} uses a sampler that doesn't exist", i)); } for (const GlbMaterial& m : a.materials) { for (const GlbTextureRef* ref : {&m.baseColorTexture, &m.metallicRoughnessTexture, &m.normalTexture, &m.occlusionTexture, &m.emissiveTexture, &m.transmissionTexture}) { if (!valid(ref->texture, a.textures.size())) return Error(std::format("material '{}' uses a texture that doesn't exist", m.name)); } } return {}; } } std::expected GlbAsset::Load(const fs::path& path) { // A directory or a pipe opens too, with no size to read. std::error_code error; if (!fs::is_regular_file(path, error)) return Error(std::format("{} isn't a file", path.string())); const std::uintmax_t size = fs::file_size(path, error); if (error) return Error(std::format("can't read {}: {}", path.string(), error.message())); std::ifstream file(path, std::ios::binary); if (!file) return Error(std::format("can't open {}", path.string())); std::vector bytes(static_cast(size)); if (!file.read(reinterpret_cast(bytes.data()), static_cast(bytes.size()))) { return Error(std::format("can't read {}", path.string())); } auto asset = Load(std::span(bytes)); if (!asset) return std::unexpected(std::format("{} ({})", asset.error(), path.string())); return asset; } std::expected GlbAsset::Load(std::span glb) { auto u32 = [&](std::size_t offset) { std::uint32_t value; std::memcpy(&value, glb.data() + offset, sizeof(value)); return value; }; if (glb.size() < 12) return Error("too short to be a GLB"); if (u32(0) != kMagic) return Error("bad magic, not a .glb"); if (u32(4) != 2) return Error(std::format("container version {}, only 2 is supported", u32(4))); const std::size_t length = u32(8); if (length > glb.size()) return Error(std::format("truncated: the header says {} bytes, there are {}", length, glb.size())); // Chunks follow the header back to back. Unknown chunk types are skipped, // as the spec asks. std::span jsonChunk; std::span binChunk; for (std::size_t offset = 12; offset + 8 <= length;) { const std::size_t chunkLength = u32(offset); const std::uint32_t chunkType = u32(offset + 4); // Not offset + 8 + chunkLength: that wraps round where size_t has 32 bits. if (chunkLength > length - offset - 8) return Error("truncated chunk"); const std::span chunk = glb.subspan(offset + 8, chunkLength); if (chunkType == kChunkJson && jsonChunk.empty()) jsonChunk = chunk; else if (chunkType == kChunkBin && binChunk.empty()) binChunk = chunk; offset += 8 + chunkLength; } if (jsonChunk.empty()) return Error("no JSON chunk"); GlbAsset asset; asset.json.assign(reinterpret_cast(jsonChunk.data()), jsonChunk.size()); const Json root = Json::parse(asset.json, nullptr, false); if (root.is_discarded() || !root.is_object()) return Error("the JSON chunk doesn't parse"); const Json* info = Find(root, "asset"); if (info == nullptr || !String(*info, "version").starts_with("2")) return Error("not glTF 2.x"); const Context ctx{root, binChunk}; // Nodes, then the parent links their children lists imply. for (const Json& node : Array(root, "nodes")) { GlbNode& n = asset.nodes.emplace_back(); n.name = String(node, "name"); for (const Json& child : Array(node, "children")) { if (!child.is_number_unsigned()) return Error(std::format("node '{}' has a child that isn't a node index", n.name)); n.children.push_back(child.get()); } std::array t = {0, 0, 0}; std::array r = {0, 0, 0, 1}; std::array s = {1, 1, 1}; Numbers(node, "translation", t); Numbers(node, "rotation", r); Numbers(node, "scale", s); n.translation = Vector(t[0], t[1], t[2]); n.rotation = Vector(r[0], r[1], r[2], r[3]); n.scale = Vector(s[0], s[1], s[2]); if (Array(node, "matrix").size() == 16) { std::array m = {}; Numbers(node, "matrix", m); n.hasMatrix = true; n.matrix = MatrixRowMajor(m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9], m[10], m[11], m[12], m[13], m[14], m[15]); } n.mesh = static_cast(Integer(node, "mesh", -1)); n.skin = static_cast(Integer(node, "skin", -1)); n.camera = static_cast(Integer(node, "camera", -1)); if (const Json* punctual = Extension(node, "KHR_lights_punctual")) { n.light = static_cast(Integer(*punctual, "light", -1)); } n.extras = Extras(node); } for (std::size_t i = 0; i < asset.nodes.size(); ++i) { for (std::uint32_t child : asset.nodes[i].children) { if (child >= asset.nodes.size()) return Error(std::format("node {} has child {}, which doesn't exist", i, child)); if (asset.nodes[child].parent != -1) return Error(std::format("node {} has two parents", child)); asset.nodes[child].parent = static_cast(i); } } // Every node reaches a root, or the children lists form a cycle. Each // walk up stops at a node an earlier one found reaching a root, so every // node is stepped through once, however deep the hierarchy. enum class Seen : std::uint8_t { No, OnThisWalk, ReachesRoot }; std::vector seen(asset.nodes.size(), Seen::No); std::vector walked; for (std::size_t i = 0; i < asset.nodes.size(); ++i) { walked.clear(); for (std::int32_t at = static_cast(i); at != -1 && seen[static_cast(at)] != Seen::ReachesRoot; at = asset.nodes[static_cast(at)].parent) { if (seen[static_cast(at)] == Seen::OnThisWalk) return Error("the node hierarchy has a cycle"); seen[static_cast(at)] = Seen::OnThisWalk; walked.push_back(static_cast(at)); } for (std::size_t node : walked) seen[node] = Seen::ReachesRoot; } const Json& scenes = Array(root, "scenes"); const std::int64_t scene = Integer(root, "scene", 0); if (scene >= 0 && static_cast(scene) < scenes.size()) { for (const Json& node : Array(scenes[static_cast(scene)], "nodes")) { if (!node.is_number_unsigned() || node.get() >= asset.nodes.size()) { return Error("the default scene names a node that doesn't exist"); } asset.sceneRoots.push_back(node.get()); } asset.extras = Extras(scenes[static_cast(scene)]); } else { for (std::size_t i = 0; i < asset.nodes.size(); ++i) { if (asset.nodes[i].parent == -1) asset.sceneRoots.push_back(static_cast(i)); } } const Json& meshes = Array(root, "meshes"); for (std::size_t m = 0; m < meshes.size(); ++m) { GlbMesh& mesh = asset.meshes.emplace_back(); mesh.name = String(meshes[m], "name"); const Json& primitives = Array(meshes[m], "primitives"); for (std::size_t p = 0; p < primitives.size(); ++p) { auto primitive = ReadPrimitive(ctx, primitives[p], std::format("mesh {} primitive {}", m, p)); if (!primitive) return std::unexpected(primitive.error()); mesh.primitives.push_back(std::move(*primitive)); } } for (const Json& skin : Array(root, "skins")) { GlbSkin& s = asset.skins.emplace_back(); s.name = String(skin, "name"); for (const Json& joint : Array(skin, "joints")) { if (!joint.is_number_unsigned()) return Error(std::format("skin '{}' has a joint that isn't a node index", s.name)); s.joints.push_back(joint.get()); } s.skeleton = static_cast(Integer(skin, "skeleton", -1)); if (Find(skin, "inverseBindMatrices") != nullptr) { const auto ibm = ReadAccessor(ctx, Integer(skin, "inverseBindMatrices", -1)); if (!ibm) return std::unexpected(std::format("{} (inverseBindMatrices of skin '{}')", ibm.error(), s.name)); if (ibm->components != 16 || ibm->componentType != kFloat || ibm->count < s.joints.size()) { return Error(std::format("inverseBindMatrices of skin '{}' aren't one float MAT4 per joint", s.name)); } for (std::size_t j = 0; j < s.joints.size(); ++j) { float m[16]; for (std::uint32_t c = 0; c < 16; ++c) m[c] = ibm->Float(j, c); // glTF's column-major array as the rows: the row-vector form. s.inverseBindMatrices.emplace_back(m[0], m[1], m[2], m[3], m[4], m[5], m[6], m[7], m[8], m[9], m[10], m[11], m[12], m[13], m[14], m[15]); } } else { s.inverseBindMatrices.assign(s.joints.size(), MatrixRowMajor::Identity()); } } for (const Json& material : Array(root, "materials")) { GlbMaterial& m = asset.materials.emplace_back(); m.name = String(material, "name"); if (const Json* pbr = Find(material, "pbrMetallicRoughness")) { Numbers(*pbr, "baseColorFactor", m.baseColorFactor); m.metallicFactor = Number(*pbr, "metallicFactor", 1.0f); m.roughnessFactor = Number(*pbr, "roughnessFactor", 1.0f); m.baseColorTexture = TextureRef(*pbr, "baseColorTexture", nullptr); m.metallicRoughnessTexture = TextureRef(*pbr, "metallicRoughnessTexture", nullptr); } m.normalTexture = TextureRef(material, "normalTexture", "scale"); m.occlusionTexture = TextureRef(material, "occlusionTexture", "strength"); m.emissiveTexture = TextureRef(material, "emissiveTexture", nullptr); Numbers(material, "emissiveFactor", m.emissiveFactor); const std::string alphaMode = String(material, "alphaMode"); m.alphaMode = alphaMode == "MASK" ? GlbAlphaMode::Mask : alphaMode == "BLEND" ? GlbAlphaMode::Blend : GlbAlphaMode::Opaque; m.alphaCutoff = Number(material, "alphaCutoff", 0.5f); m.doubleSided = Boolean(material, "doubleSided", false); if (const Json* strength = Extension(material, "KHR_materials_emissive_strength")) { m.emissiveStrength = Number(*strength, "emissiveStrength", 1.0f); } if (const Json* transmission = Extension(material, "KHR_materials_transmission")) { m.transmissionFactor = Number(*transmission, "transmissionFactor", 0.0f); m.transmissionTexture = TextureRef(*transmission, "transmissionTexture", nullptr); } if (const Json* ior = Extension(material, "KHR_materials_ior")) { m.ior = Number(*ior, "ior", 1.5f); } m.extras = Extras(material); } for (const Json& camera : Array(root, "cameras")) { GlbCamera& c = asset.cameras.emplace_back(); c.name = String(camera, "name"); c.orthographic = String(camera, "type") == "orthographic"; if (const Json* orthographic = Find(camera, "orthographic"); c.orthographic && orthographic != nullptr) { c.xmag = Number(*orthographic, "xmag", 0.0f); c.ymag = Number(*orthographic, "ymag", 0.0f); c.znear = Number(*orthographic, "znear", 0.0f); c.zfar = Number(*orthographic, "zfar", 0.0f); } else if (const Json* perspective = Find(camera, "perspective"); !c.orthographic && perspective != nullptr) { c.yfov = Number(*perspective, "yfov", 0.0f); c.aspectRatio = Number(*perspective, "aspectRatio", 0.0f); c.znear = Number(*perspective, "znear", 0.0f); c.zfar = Number(*perspective, "zfar", 0.0f); } else { return Error(std::format("camera '{}' has no {} properties", c.name, c.orthographic ? "orthographic" : "perspective")); } } if (const Json* punctual = Extension(root, "KHR_lights_punctual")) { for (const Json& light : Array(*punctual, "lights")) { GlbLight& l = asset.lights.emplace_back(); l.name = String(light, "name"); const std::string type = String(light, "type"); if (type == "directional") l.type = GlbLightType::Directional; else if (type == "point") l.type = GlbLightType::Point; else if (type == "spot") l.type = GlbLightType::Spot; else return Error(std::format("light '{}' is of type '{}', which KHR_lights_punctual doesn't define", l.name, type)); Numbers(light, "color", l.color); l.intensity = Number(light, "intensity", 1.0f); l.range = Number(light, "range", 0.0f); if (const Json* spot = Find(light, "spot")) { l.innerConeAngle = Number(*spot, "innerConeAngle", l.innerConeAngle); l.outerConeAngle = Number(*spot, "outerConeAngle", l.outerConeAngle); } l.extras = Extras(light); } } for (const Json& texture : Array(root, "textures")) { asset.textures.push_back({ .image = static_cast(Integer(texture, "source", -1)), .sampler = static_cast(Integer(texture, "sampler", -1)), }); } for (const Json& sampler : Array(root, "samplers")) { asset.samplers.push_back({ .magFilter = static_cast(Integer(sampler, "magFilter", 0)), .minFilter = static_cast(Integer(sampler, "minFilter", 0)), .wrapS = static_cast(Integer(sampler, "wrapS", 10497)), .wrapT = static_cast(Integer(sampler, "wrapT", 10497)), }); } const Json& images = Array(root, "images"); for (std::size_t i = 0; i < images.size(); ++i) { if (Find(images[i], "uri") != nullptr) { return Error(std::format("image {} is an external file or data URI, which a GLB loader doesn't read", i)); } const auto bytes = ViewBytes(ctx, Integer(images[i], "bufferView", -1), nullptr); if (!bytes) return std::unexpected(std::format("{} (image {})", bytes.error(), i)); auto image = TextureAsset::LoadPNGFromMemory(*bytes); if (!image) return Error(std::format("image {} doesn't decode: {}", i, image.error())); asset.images.push_back(std::move(*image)); } if (auto valid = CheckReferences(asset); !valid) return std::unexpected(valid.error()); return asset; }