//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" // errno, for why a file that exists doesn't open. #include 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; // Primitive modes; those below kTriangles are points and lines. constexpr std::int64_t kTriangles = 4; constexpr std::int64_t kTriangleStrip = 5; constexpr std::int64_t kTriangleFan = 6; // The decode limit when Load isn't given one: 1 GiB, the largest image a // texture takes, and 64 bytes for each byte of the file, more than a // model's vertices and images decode to unless the same data is decoded // over and over. constexpr std::uint64_t kDecodeFloor = std::uint64_t{1} << 30; constexpr std::uint64_t kDecodePerByte = 64; // The extensions this loader implements. A file that requires another // can't be loaded right without it, so, as glTF asks, it isn't loaded. constexpr std::array kImplemented = { "KHR_lights_punctual", "KHR_materials_emissive_strength", "KHR_materials_ior", "KHR_materials_transmission", }; // An index the file gives that can't name anything, as GlbAsset's int32 // keeps it; CheckReferences turns it down. constexpr std::int32_t kBadIndex = std::numeric_limits::min(); // A texCoord past the two UV sets a primitive keeps, likewise. constexpr std::uint32_t kBadTexCoord = std::numeric_limits::max(); using Pixel = Vector; using Image = TextureAsset; std::unexpected Error(std::string message) { return std::unexpected("GLB: " + std::move(message)); } std::uint32_t U32(std::span bytes, std::size_t offset) { std::uint32_t value; std::memcpy(&value, bytes.data() + offset, sizeof(value)); return value; } // 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; } // A whole number: 24, but also 24.0 or 2.4e1, as JSON may write one. // One past int64, which no index or count reaches, comes back as // int64's limit of its sign, which range checks still turn down. std::optional AsInteger(const Json& value) { if (value.is_number_unsigned()) { return static_cast(std::min(value.get(), std::numeric_limits::max())); } if (value.is_number_integer()) return value.get(); if (!value.is_number_float()) return std::nullopt; const double number = value.get(); if (number != std::trunc(number)) return std::nullopt; // a fraction, or NaN constexpr double kTwoTo63 = 9223372036854775808.0; if (number >= kTwoTo63) return std::numeric_limits::max(); if (number < -kTwoTo63) return std::numeric_limits::min(); return static_cast(number); } std::int64_t Integer(const Json& object, const char* key, std::int64_t fallback) { const Json* value = Find(object, key); return value != nullptr ? AsInteger(*value).value_or(fallback) : fallback; } // An index into one of the file's arrays, as GlbAsset keeps it: -1 when // `key` is absent, kBadIndex when it can't name an element (negative, // not a whole number, past int32). Checked before it is narrowed, so // that 4294967296 can't wrap round to 0. std::int32_t Index(const Json& object, const char* key) { const Json* value = Find(object, key); if (value == nullptr) return -1; const std::optional index = AsInteger(*value); return index && *index >= 0 && *index <= std::numeric_limits::max() ? static_cast(*index) : kBadIndex; } // An element of a list of indices (children, joints, a scene's nodes) // that names one of `count`, or nullopt. std::optional Element(const Json& value, std::size_t count) { const std::optional index = AsInteger(value); if (!index || *index < 0 || static_cast(*index) >= count || *index > std::numeric_limits::max()) { return std::nullopt; } return static_cast(*index); } 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. Written // without recursion: Json::dump recurses once a level, and extras nested // deep enough run it out of stack, where the parser and the destructor // don't recurse. std::string Extras(const Json& object) { const Json* extras = Find(object, "extras"); if (extras == nullptr || extras->is_null()) return {}; std::string text; // The arrays and objects being written, each with its next element. std::vector> open; const Json* value = extras; while (true) { if (value != nullptr) { if (value->is_structured() && !value->empty()) { text += value->is_object() ? '{' : '['; open.emplace_back(value, value->cbegin()); } else { text += value->dump(); // a scalar, {} or []: nothing nested } } if (open.empty()) return text; auto& [container, next] = open.back(); if (next == container->cend()) { text += container->is_object() ? '}' : ']'; open.pop_back(); value = nullptr; continue; } if (next != container->cbegin()) text += ','; if (container->is_object()) { text += Json(next.key()).dump(); text += ':'; } value = &*next; ++next; } } // 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; } // Numbers from a JSON array into `out`, which keeps its values where the // array is shorter or holds something else. void Numbers(const Json& object, const char* key, std::span out) { const Json& values = Array(object, key); for (std::size_t i = 0; i < out.size() && i < values.size(); ++i) { if (values[i].is_number()) out[i] = values[i].get(); } } std::size_t ComponentSize(std::int64_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, or, for one that // is sparse or has no bufferView, in `dense`, where they were put // together. struct Accessor { const std::byte* data = nullptr; std::size_t count = 0; std::size_t stride = 0; std::uint32_t componentType = 0; std::uint32_t components = 0; bool normalized = false; std::vector dense; const std::byte* At(std::size_t element, std::uint32_t component) const { return (dense.empty() ? data : dense.data()) + element * stride + component * ComponentSize(componentType); } float Float(std::size_t element, std::uint32_t component) const { const std::byte* p = At(element, component); 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 = At(element, component); 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 decode limit, and what is left of it. std::uint64_t limit; std::uint64_t budget; }; // Takes `count` elements of `size` bytes out of the budget; false when // they don't fit in it, or in a size_t. bool Spend(Context& ctx, std::uint64_t count, std::uint64_t size) { if (count > ctx.budget / size || count > std::numeric_limits::max() / size) return false; ctx.budget -= count * size; return true; } std::unexpected OverLimit(const Context& ctx, std::string_view what) { return Error(std::format("{} decodes past the limit of {} bytes", what, ctx.limit)); } // Sizes `stream` to `count` elements, out of the budget. template bool Allocate(Context& ctx, std::vector& stream, std::size_t count) { if (!Spend(ctx, count, sizeof(T))) return false; stream.resize(count); return true; } // 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)); } // `count` elements of `size` bytes, packed at the bufferView and // byteOffset `object` names: a sparse accessor's indices or values. std::expected, std::string> Packed(const Context& ctx, const Json& object, std::uint64_t count, std::size_t size) { const auto view = ViewBytes(ctx, Integer(object, "bufferView", -1), nullptr); if (!view) return std::unexpected(view.error()); const std::int64_t offset = Integer(object, "byteOffset", 0); const std::uint64_t available = view->size(); if (offset < 0 || static_cast(offset) > available || count > (available - static_cast(offset)) / size) { return Error("sparse data reads past the end of its bufferView"); } return view->subspan(static_cast(offset), static_cast(count * size)); } // A sparse accessor's values, written over the elements its indices name // in `accessor`, whose elements are already in `dense`. std::expected ApplySparse(const Context& ctx, const Json& sparse, Accessor& accessor, std::int64_t index) { const std::int64_t count = Integer(sparse, "count", -1); const Json* indices = Find(sparse, "indices"); const Json* values = Find(sparse, "values"); const std::int64_t indexType = indices != nullptr ? Integer(*indices, "componentType", 0) : 0; if (count < 0 || static_cast(count) > accessor.count || values == nullptr || (indexType != kUnsignedByte && indexType != kUnsignedShort && indexType != kUnsignedInt)) { return Error(std::format("accessor {} has a malformed sparse part", index)); } const std::size_t indexSize = ComponentSize(indexType); const auto indexBytes = Packed(ctx, *indices, static_cast(count), indexSize); if (!indexBytes) return std::unexpected(std::format("{} (sparse indices of accessor {})", indexBytes.error(), index)); const std::size_t elementSize = accessor.stride; const auto valueBytes = Packed(ctx, *values, static_cast(count), elementSize); if (!valueBytes) return std::unexpected(std::format("{} (sparse values of accessor {})", valueBytes.error(), index)); const Accessor elements{ .data = indexBytes->data(), .count = static_cast(count), .stride = indexSize, .componentType = static_cast(indexType), .components = 1, }; std::int64_t previous = -1; for (std::size_t i = 0; i < elements.count; ++i) { const std::uint32_t element = elements.Unsigned(i, 0); // glTF has them strictly increase, which also keeps each one // inside the accessor once. if (element <= previous || element >= accessor.count) { return Error(std::format("accessor {}'s sparse indices don't rise within its {} elements", index, accessor.count)); } previous = element; std::memcpy(accessor.dense.data() + std::size_t{element} * elementSize, valueBytes->data() + i * elementSize, elementSize); } return {}; } std::expected ReadAccessor(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)]; // Compared as the file has it, so that 4294972422 can't wrap round // to FLOAT, 5126. const std::int64_t componentType = Integer(accessor, "componentType", 0); const std::size_t componentSize = ComponentSize(componentType); const std::uint32_t components = ComponentCount(String(accessor, "type")); if (componentSize == 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 != 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)); const std::size_t elementSize = componentSize * components; Accessor result{ .stride = elementSize, .componentType = static_cast(componentType), .components = components, .normalized = Boolean(accessor, "normalized", false), }; const bool hasView = Find(accessor, "bufferView") != nullptr; if (hasView) { std::size_t viewStride = 0; const auto view = ViewBytes(ctx, Integer(accessor, "bufferView", -1), &viewStride); if (!view) return std::unexpected(view.error()); 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)); } result.data = view->data() + offset; result.stride = stride; } const Json* sparse = Find(accessor, "sparse"); if (hasView && sparse == nullptr) { result.count = static_cast(count); // inside its view, so a size_t holds it return result; } // Put together in `dense`: the view's elements, or zeros without one, // as glTF says, with the sparse values written over them. if (!Spend(ctx, static_cast(count), elementSize)) return OverLimit(ctx, std::format("accessor {}", index)); result.count = static_cast(count); result.dense.resize(result.count * elementSize); if (hasView) { for (std::size_t i = 0; i < result.count; ++i) { std::memcpy(result.dense.data() + i * elementSize, result.data + i * result.stride, elementSize); } } result.stride = elementSize; if (sparse != nullptr) { if (auto applied = ApplySparse(ctx, *sparse, result, index); !applied) return std::unexpected(applied.error()); } return result; } // An accessor of `components` per element whose component type is one of // `allowed`, with exactly `count` elements when a count is given. std::expected Attribute(Context& ctx, const Json& attributes, const char* name, std::uint32_t components, std::initializer_list allowed, std::optional 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 && accessor->count != *count) { return Error(std::format("{} of {} has {} elements for {} vertices", name, where, accessor->count, *count)); } return accessor; } // The vertex stream `name` into `out`: `components` numbers for each of // the `count` vertices. Left empty when `attributes` doesn't have it. template std::expected Stream(Context& ctx, const Json& attributes, const char* name, std::uint32_t components, std::initializer_list allowed, std::size_t count, const std::string& where, std::vector& out) { if (Find(attributes, name) == nullptr) return {}; const auto accessor = Attribute(ctx, attributes, name, components, allowed, count, where); if (!accessor) return std::unexpected(accessor.error()); if (!Allocate(ctx, out, count)) return OverLimit(ctx, where); for (std::size_t i = 0; i < count; ++i) { for (std::uint32_t c = 0; c < components; ++c) out[i].v[c] = accessor->Float(i, c); } return {}; } // Per-vertex tangents from the gradients of `uv`, 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, const std::vector>& uv) { 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 = uv[i[1]].x - uv[i[0]].x, dv1 = uv[i[1]].y - uv[i[0]].y; const float du2 = uv[i[2]].x - uv[i[0]].x, dv2 = uv[i[2]].y - uv[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]); // Written so that NaN takes these branches too. if (!(tangent.LengthSq() >= 1e-20f)) { // No usable UV gradient: any direction perpendicular to the // normal keeps the frame valid, and any at all where the // normal is zero. tangent = V3::Cross(std::abs(normal.x) < 0.9f ? V3(1, 0, 0) : V3(0, 1, 0), normal); if (!(tangent.LengthSq() >= 1e-20f)) tangent = V3(1, 0, 0); } 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); } } // JOINTS_n and WEIGHTS_n, which come in pairs from n = 0 up, as up to // four influences a vertex: the heaviest, a joint named twice counted // once with its weights added, and the weights made to sum to 1. std::expected ReadInfluences(Context& ctx, const Json& attributes, std::size_t n, const std::string& where, GlbPrimitive& p) { std::size_t sets = 0; while (Find(attributes, std::format("JOINTS_{}", sets).c_str()) != nullptr && Find(attributes, std::format("WEIGHTS_{}", sets).c_str()) != nullptr) { ++sets; } // Every JOINTS_ and WEIGHTS_ is one of those pairs: no half of one, // and no gap before one. std::size_t named = 0; for (auto it = attributes.begin(); it != attributes.end(); ++it) { named += it.key().starts_with("JOINTS_") || it.key().starts_with("WEIGHTS_"); } if (named != 2 * sets) return Error(std::format("{}'s JOINTS_n and WEIGHTS_n don't pair up from n = 0", where)); if (sets == 0) return {}; std::vector> influences; for (std::size_t set = 0; set < sets; ++set) { const std::string joints = std::format("JOINTS_{}", set); const std::string weights = std::format("WEIGHTS_{}", set); auto j = Attribute(ctx, attributes, joints.c_str(), 4, {kUnsignedByte, kUnsignedShort}, n, where); if (!j) return std::unexpected(j.error()); auto w = Attribute(ctx, attributes, weights.c_str(), 4, {kFloat, kUnsignedByte, kUnsignedShort}, n, where); if (!w) return std::unexpected(w.error()); influences.emplace_back(std::move(*j), std::move(*w)); } // Each set is read for every vertex, so each beyond the first is // paid for as if it were kept too. if (!Spend(ctx, static_cast(n) * (sets - 1), sizeof(std::array) + sizeof(std::array)) || !Allocate(ctx, p.joints, n) || !Allocate(ctx, p.weights, n)) { return OverLimit(ctx, where); } using Influence = std::pair; std::vector heaviest; for (std::size_t v = 0; v < n; ++v) { heaviest.clear(); for (const auto& [joints, weights] : influences) { for (std::uint32_t c = 0; c < 4; ++c) { const float weight = weights.Float(v, c); // A weight of nothing: zero, negative, or not a number. if (!(weight > 0.0f) || !std::isfinite(weight)) continue; const auto joint = static_cast(joints.Unsigned(v, c)); const auto same = std::ranges::find(heaviest, joint, &Influence::first); if (same != heaviest.end()) same->second += weight; else heaviest.emplace_back(joint, weight); } } if (heaviest.empty()) { p.joints[v][0] = static_cast(influences[0].first.Unsigned(v, 0)); p.weights[v][0] = 1.0f; continue; } if (heaviest.size() > 4) { std::ranges::stable_sort(heaviest, std::ranges::greater(), &Influence::second); heaviest.resize(4); } float total = 0; for (const auto& [joint, weight] : heaviest) total += weight; for (std::size_t k = 0; k < heaviest.size(); ++k) { p.joints[v][k] = heaviest[k].first; p.weights[v][k] = heaviest[k].second / total; } } return {}; } std::expected ReadPrimitive(Context& ctx, const Json& primitive, const std::vector& materials, const std::string& where) { GlbPrimitive p; p.material = Index(primitive, "material"); const std::int64_t mode = Integer(primitive, "mode", kTriangles); if (mode < 0 || mode > kTriangleFan) return Error(std::format("{} has mode {}, which glTF doesn't define", where, mode)); // Points and lines: no triangles to read. if (mode < kTriangles) return p; const Json* attributes = Find(primitive, "attributes"); if (attributes == nullptr || Find(*attributes, "POSITION") == nullptr) { return Error(std::format("{} has no POSITION", where)); } const auto positions = Attribute(ctx, *attributes, "POSITION", 3, {kFloat}, std::nullopt, where); if (!positions) return std::unexpected(positions.error()); const std::size_t n = positions->count; if (n > std::numeric_limits::max()) return Error(std::format("{} has more vertices than 32-bit indices reach", where)); if (!Allocate(ctx, p.positions, n)) return OverLimit(ctx, where); for (std::size_t i = 0; i < n; ++i) { for (std::uint32_t c = 0; c < 3; ++c) p.positions[i].v[c] = positions->Float(i, c); } if (auto read = Stream(ctx, *attributes, "NORMAL", 3, {kFloat}, n, where, p.normals); !read) return std::unexpected(read.error()); for (const auto& [name, set] : { std::pair { "TEXCOORD_0", &p.uv0 }, std::pair { "TEXCOORD_1", &p.uv1 } }) { if (auto read = Stream(ctx, *attributes, name, 2, {kFloat, kUnsignedByte, kUnsignedShort}, n, where, *set); !read) { return std::unexpected(read.error()); } } if (auto read = Stream(ctx, *attributes, "TANGENT", 4, {kFloat}, n, where, p.tangents); !read) return std::unexpected(read.error()); if (auto read = ReadInfluences(ctx, *attributes, n, where, p); !read) return std::unexpected(read.error()); // The vertices in the order the file lists them, then as a list of // triangles. std::vector listed; 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)); } if (!Allocate(ctx, listed, indices->count)) return OverLimit(ctx, where); for (std::size_t i = 0; i < indices->count; ++i) listed[i] = indices->Unsigned(i, 0); } else { if (!Allocate(ctx, listed, n)) return OverLimit(ctx, where); std::iota(listed.begin(), listed.end(), 0u); } if (std::ranges::any_of(listed, [n](std::uint32_t i) { return i >= n; })) { return Error(std::format("{} indexes past its {} vertices", where, n)); } if (mode == kTriangles) { if (listed.size() % 3 != 0) { return Error(std::format("{} has {} indices, not whole triangles", where, listed.size())); } p.indices = std::move(listed); } else { // A strip's or a fan's triangles as glTF numbers them: a strip's // odd ones swap their last two corners to keep the winding, and // a fan's all end at its first vertex. const std::size_t triangles = listed.size() >= 3 ? listed.size() - 2 : 0; if (!Allocate(ctx, p.indices, triangles * 3)) return OverLimit(ctx, where); for (std::size_t t = 0; t < triangles; ++t) { const std::array corners = mode == kTriangleStrip ? std::array{t, t + 1 + t % 2, t + 2 - t % 2} : std::array{t + 1, t + 2, 0}; for (std::size_t c = 0; c < 3; ++c) p.indices[t * 3 + c] = listed[corners[c]]; } } const Json& targets = Array(primitive, "targets"); p.targets.resize(targets.size()); for (std::size_t t = 0; t < targets.size(); ++t) { const std::string target = std::format("{} morph target {}", where, t); GlbMorphTarget& morph = p.targets[t]; for (const auto& [name, stream] : { std::pair { "POSITION", &morph.positions }, std::pair { "NORMAL", &morph.normals }, std::pair { "TANGENT", &morph.tangents } }) { if (auto read = Stream(ctx, targets[t], name, 3, {kFloat}, n, target, *stream); !read) return std::unexpected(read.error()); } } // Tangents the file leaves out, from the UV set its normal map is // sampled with, as glTF asks. if (p.tangents.empty() && !p.normals.empty()) { std::uint32_t set = 0; if (p.material >= 0 && static_cast(p.material) < materials.size()) { const GlbTextureRef& normalTexture = materials[static_cast(p.material)].normalTexture; if (normalTexture.texture != -1) set = normalTexture.texCoord; } const std::vector>* uv = set == 0 ? &p.uv0 : set == 1 ? &p.uv1 : nullptr; if (uv != nullptr && !uv->empty()) { if (!Spend(ctx, n, sizeof(Vector))) return OverLimit(ctx, where); ComputeTangents(p, *uv); } } return p; } GlbTextureRef TextureRef(const Json& material, const char* key, const char* scaleKey) { GlbTextureRef ref; if (const Json* info = Find(material, key)) { ref.texture = Index(*info, "index"); const std::int64_t texCoord = Integer(*info, "texCoord", ref.texCoord); ref.texCoord = texCoord == 0 || texCoord == 1 ? static_cast(texCoord) : kBadTexCoord; if (scaleKey != nullptr) ref.scale = Number(*info, scaleKey, ref.scale); } return ref; } // Core glTF's image formats, PNG and JPEG, told by their first bytes. // stb_image reads more (GIF, PSD, TGA, ...), which have no business in a // GLB, so those don't reach it. bool PngOrJpeg(std::span bytes) { constexpr std::array kPng = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A, '\n'}; constexpr std::array kJpeg = {0xFF, 0xD8, 0xFF}; auto startsWith = [bytes](std::span magic) { return bytes.size() >= magic.size() && std::memcmp(bytes.data(), magic.data(), magic.size()) == 0; }; return startsWith(kPng) || startsWith(kJpeg); } // A sampler's enum, which must be one of `allowed`: `fallback` when the // file leaves it out, nullopt when it is anything else. std::optional Enum(const Json& sampler, const char* key, std::uint32_t fallback, std::initializer_list allowed) { if (Find(sampler, key) == nullptr) return fallback; const std::int64_t value = Integer(sampler, key, -1); if (std::ranges::find(allowed, value) == allowed.end()) return std::nullopt; return static_cast(value); } // Each index points at an element that exists, and what the interface // promises of them holds, 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) { const GlbNode& node = a.nodes[i]; if (!valid(node.mesh, a.meshes.size())) return Error(std::format("node {} uses a mesh that doesn't exist", i)); if (!valid(node.skin, a.skins.size())) return Error(std::format("node {} uses a skin that doesn't exist", i)); if (!valid(node.camera, a.cameras.size())) return Error(std::format("node {} uses a camera that doesn't exist", i)); if (!valid(node.light, a.lights.size())) return Error(std::format("node {} uses a light that doesn't exist", i)); if (node.skin != -1 && node.mesh == -1) return Error(std::format("node {} has a skin and no mesh to skin", i)); if (!node.weights.empty() && (node.mesh == -1 || node.weights.size() != a.meshes[static_cast(node.mesh)].weights.size())) { return Error(std::format("node {} has {} morph weights, not one for each of its mesh's targets", i, node.weights.size())); } } 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)); if (ref->texture != -1 && ref->texCoord == kBadTexCoord) { return Error(std::format("material '{}' samples a UV set other than TEXCOORD_0 and TEXCOORD_1, the ones the loader reads", m.name)); } } } // For each mesh, once however many nodes draw it: its highest joint // index, and whether every primitive with vertices has joints. std::vector highestJoint(a.meshes.size(), -1); std::vector skinnable(a.meshes.size(), true); for (std::size_t m = 0; m < a.meshes.size(); ++m) { for (std::size_t p = 0; p < a.meshes[m].primitives.size(); ++p) { const GlbPrimitive& primitive = a.meshes[m].primitives[p]; if (!valid(primitive.material, a.materials.size())) { return Error(std::format("mesh '{}' uses a material that doesn't exist", a.meshes[m].name)); } if (primitive.positions.empty()) continue; // points, lines, or nothing to draw if (primitive.material != -1) { const GlbMaterial& material = a.materials[static_cast(primitive.material)]; for (const GlbTextureRef* ref : {&material.baseColorTexture, &material.metallicRoughnessTexture, &material.normalTexture, &material.occlusionTexture, &material.emissiveTexture, &material.transmissionTexture}) { if (ref->texture != -1 && (ref->texCoord == 0 ? primitive.uv0 : primitive.uv1).empty()) { return Error(std::format("material '{}' samples TEXCOORD_{}, which mesh {} primitive {} doesn't have", material.name, ref->texCoord, m, p)); } } } skinnable[m] = skinnable[m] && !primitive.joints.empty(); for (const auto& joints : primitive.joints) { for (std::uint16_t joint : joints) highestJoint[m] = std::max(highestJoint[m], joint); } } } // 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 mesh = static_cast(node.mesh); if (!skinnable[mesh]) { return Error(std::format("node '{}' skins mesh '{}', which has a primitive without JOINTS_0 and WEIGHTS_0", node.name, a.meshes[mesh].name)); } const std::size_t jointCount = a.skins[static_cast(node.skin)].joints.size(); if (highestJoint[mesh] >= static_cast(jointCount)) { return Error(std::format("node '{}' skins mesh '{}' with joint indices beyond its skin's {} joints", node.name, a.meshes[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::size_t i = 0; i < a.textures.size(); ++i) { const GlbTexture& texture = a.textures[i]; if (!valid(texture.image, a.images.size())) return Error(std::format("texture {} uses an image that doesn't exist", i)); if (texture.image != -1 && a.images[static_cast(texture.image)].pixels.empty()) { return Error(std::format("texture {} uses image {}, which is neither a PNG nor a JPEG", i, texture.image)); } if (!valid(texture.sampler, a.samplers.size())) return Error(std::format("texture {} uses a sampler that doesn't exist", i)); } return {}; } // The 12-byte header: the length the file says it has, no less than the // header's own. std::expected Header(std::span glb) { if (glb.size() < 12) return Error("too short to be a GLB"); if (U32(glb, 0) != kMagic) return Error("bad magic, not a .glb"); if (U32(glb, 4) != 2) return Error(std::format("container version {}, only 2 is supported", U32(glb, 4))); if (U32(glb, 8) < 12) return Error(std::format("the header says the file has {} bytes, fewer than the header", U32(glb, 8))); return U32(glb, 8); } } std::expected GlbAsset::Load(const fs::path& path, std::optional decodeLimit) { std::error_code error; const fs::file_status status = fs::status(path, error); if (error) return Error(std::format("can't read {}: {}", path.string(), error.message())); // A directory or a pipe opens too, with no size to read. if (!fs::is_regular_file(status)) 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())); errno = 0; std::ifstream file(path, std::ios::binary); if (!file) { return Error(std::format("can't open {}: {}", path.string(), errno != 0 ? std::generic_category().message(errno) : "reason unknown")); } // The header first. A file that isn't a GLB is turned down unread, and // one that is says how much of it to read, which can't pass 4 GiB // however large the file. std::array header; if (size < header.size() || !file.read(reinterpret_cast(header.data()), header.size())) { return Error(std::format("too short to be a GLB ({})", path.string())); } const auto length = Header(header); if (!length) return std::unexpected(std::format("{} ({})", length.error(), path.string())); if (*length > size) return Error(std::format("truncated: the header says {} bytes, there are {} ({})", *length, size, path.string())); std::vector bytes(*length); std::memcpy(bytes.data(), header.data(), header.size()); if (!file.read(reinterpret_cast(bytes.data() + header.size()), static_cast(bytes.size() - header.size()))) { return Error(std::format("can't read {}", path.string())); } auto asset = Load(std::span(bytes), decodeLimit); if (!asset) return std::unexpected(std::format("{} ({})", asset.error(), path.string())); return asset; } std::expected GlbAsset::Load(std::span glb, std::optional decodeLimit) { const auto header = Header(glb); if (!header) return std::unexpected(header.error()); const std::uint32_t length = *header; 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. The arithmetic is in 32 bits, like the header's, and // ordered so that it can't wrap round. std::span jsonChunk; std::span binChunk; for (std::uint32_t offset = 12; length - offset >= 8;) { const std::uint32_t chunkLength = U32(glb, offset); const std::uint32_t chunkType = U32(glb, offset + 4); // Not offset + 8 + chunkLength > length, which wraps round. 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"); for (const Json& name : Array(root, "extensionsRequired")) { if (!name.is_string()) return Error("extensionsRequired names something that isn't an extension"); if (std::ranges::find(kImplemented, name.get_ref()) == kImplemented.end()) { return Error(std::format("the file requires {}, which this loader doesn't implement", name.get_ref())); } } const std::uint64_t limit = decodeLimit.value_or(kDecodeFloor + kDecodePerByte * length); Context ctx{.root = root, .bin = binChunk, .limit = limit, .budget = limit}; // Nodes, then the parent links their children lists imply. const Json& nodes = Array(root, "nodes"); for (std::size_t i = 0; i < nodes.size(); ++i) { const Json& node = nodes[i]; GlbNode& n = asset.nodes.emplace_back(); n.name = String(node, "name"); for (const Json& child : Array(node, "children")) { const std::optional index = Element(child, nodes.size()); if (!index) return Error(std::format("node {} has a child that isn't one of the {} nodes", i, nodes.size())); n.children.push_back(*index); } Numbers(node, "translation", n.translation.v); Numbers(node, "rotation", n.rotation.v); Numbers(node, "scale", n.scale.v); 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 = Index(node, "mesh"); n.skin = Index(node, "skin"); n.camera = Index(node, "camera"); if (const Json* punctual = Extension(node, "KHR_lights_punctual")) { n.light = Index(*punctual, "light"); } for (const Json& weight : Array(node, "weights")) n.weights.push_back(weight.is_number() ? weight.get() : 0.0f); 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 (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; } // The default scene, or the first when the file names none. const Json& scenes = Array(root, "scenes"); std::int64_t scene = Index(root, "scene"); if (scene == -1 && !scenes.empty()) scene = 0; if (scene != -1) { if (scene < 0 || static_cast(scene) >= scenes.size()) { return Error(std::format("the default scene isn't one of the {} scenes", scenes.size())); } std::vector isRoot(asset.nodes.size(), false); for (const Json& node : Array(scenes[static_cast(scene)], "nodes")) { const std::optional index = Element(node, asset.nodes.size()); if (!index) return Error("the default scene names a node that doesn't exist"); if (asset.nodes[*index].parent != -1) return Error(std::format("the default scene has node {}, a child, as a root", *index)); if (isRoot[*index]) return Error(std::format("the default scene names node {} twice", *index)); isRoot[*index] = true; asset.sceneRoots.push_back(*index); } 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)); } } // Materials before meshes: a primitive's tangents depend on its // material's normal texture. 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", m.metallicFactor); m.roughnessFactor = Number(*pbr, "roughnessFactor", m.roughnessFactor); 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", m.alphaCutoff); m.doubleSided = Boolean(material, "doubleSided", m.doubleSided); if (const Json* strength = Extension(material, "KHR_materials_emissive_strength")) { m.emissiveStrength = Number(*strength, "emissiveStrength", m.emissiveStrength); } if (const Json* transmission = Extension(material, "KHR_materials_transmission")) { m.transmissionFactor = Number(*transmission, "transmissionFactor", m.transmissionFactor); m.transmissionTexture = TextureRef(*transmission, "transmissionTexture", nullptr); } if (const Json* ior = Extension(material, "KHR_materials_ior")) { m.ior = Number(*ior, "ior", m.ior); } m.extras = Extras(material); } 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"); // glTF has every primitive of a mesh carry the same morph targets, // which the mesh's weights weigh. const std::size_t targets = primitives.empty() ? 0 : Array(primitives[0], "targets").size(); for (std::size_t p = 0; p < primitives.size(); ++p) { if (Array(primitives[p], "targets").size() != targets) { return Error(std::format("mesh {}'s primitives don't have the same number of morph targets", m)); } auto primitive = ReadPrimitive(ctx, primitives[p], asset.materials, std::format("mesh {} primitive {}", m, p)); if (!primitive) return std::unexpected(primitive.error()); mesh.primitives.push_back(std::move(*primitive)); } mesh.weights.assign(targets, 0.0f); if (Find(meshes[m], "weights") != nullptr && Array(meshes[m], "weights").size() != targets) { return Error(std::format("mesh {} has {} morph weights for {} targets", m, Array(meshes[m], "weights").size(), targets)); } Numbers(meshes[m], "weights", mesh.weights); mesh.extras = Extras(meshes[m]); } 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")) { const std::optional index = Element(joint, asset.nodes.size()); if (!index) return Error(std::format("skin '{}' has a joint that isn't one of the {} nodes", s.name, asset.nodes.size())); s.joints.push_back(*index); } s.skeleton = Index(skin, "skeleton"); 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& 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", c.xmag); c.ymag = Number(*orthographic, "ymag", c.ymag); c.znear = Number(*orthographic, "znear", c.znear); c.zfar = Number(*orthographic, "zfar", c.zfar); } else if (const Json* perspective = Find(camera, "perspective"); !c.orthographic && perspective != nullptr) { c.yfov = Number(*perspective, "yfov", c.yfov); c.aspectRatio = Number(*perspective, "aspectRatio", c.aspectRatio); c.znear = Number(*perspective, "znear", c.znear); c.zfar = Number(*perspective, "zfar", c.zfar); } 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", l.intensity); l.range = Number(light, "range", l.range); 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 = Index(texture, "source"), .sampler = Index(texture, "sampler"), }); } const Json& samplers = Array(root, "samplers"); for (std::size_t i = 0; i < samplers.size(); ++i) { GlbSampler& s = asset.samplers.emplace_back(); const auto magFilter = Enum(samplers[i], "magFilter", s.magFilter, {9728, 9729}); const auto minFilter = Enum(samplers[i], "minFilter", s.minFilter, {9728, 9729, 9984, 9985, 9986, 9987}); const auto wrapS = Enum(samplers[i], "wrapS", s.wrapS, {33071, 33648, 10497}); const auto wrapT = Enum(samplers[i], "wrapT", s.wrapT, {33071, 33648, 10497}); if (!magFilter || !minFilter || !wrapS || !wrapT) return Error(std::format("sampler {} has a filter or wrap mode glTF doesn't define", i)); s = {.magFilter = *magFilter, .minFilter = *minFilter, .wrapS = *wrapS, .wrapT = *wrapT}; } const Json& images = Array(root, "images"); // The image that decoded each bufferView first: another naming it is // copied rather than decoded again. std::unordered_map decodedFrom; 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 std::int64_t view = Integer(images[i], "bufferView", -1); const auto bytes = ViewBytes(ctx, view, nullptr); if (!bytes) return std::unexpected(std::format("{} (image {})", bytes.error(), i)); Image& image = asset.images.emplace_back(); // An extension's format (EXT_texture_webp's WebP, KHR_texture_basisu's // KTX2, ...) stays undecoded; CheckReferences turns down a texture // that has one as its image. if (!PngOrJpeg(*bytes)) continue; if (const auto same = decodedFrom.find(view); same != decodedFrom.end()) { const Image& decoded = asset.images[same->second]; if (!Spend(ctx, decoded.pixels.size(), sizeof(Pixel))) return OverLimit(ctx, std::format("image {}", i)); image = decoded; continue; } // Its size from its header, so that it is paid for before it is // decoded. const auto size = Image::SizeFromMemory(*bytes); if (!size) return Error(std::format("image {} doesn't decode: {}", i, size.error())); if (!Spend(ctx, std::uint64_t{size->first} * size->second, sizeof(Pixel))) return OverLimit(ctx, std::format("image {}", i)); auto decoded = Image::LoadPNGFromMemory(*bytes); if (!decoded) return Error(std::format("image {} doesn't decode: {}", i, decoded.error())); image = std::move(*decoded); decodedFrom.emplace(view, i); } if (auto valid = CheckReferences(asset); !valid) return std::unexpected(valid.error()); asset.bin.assign(binChunk.begin(), binChunk.end()); return asset; }