694 lines
37 KiB
C++
694 lines
37 KiB
C++
|
|
//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<std::string> 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<std::int64_t>() : 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<float>() : 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<bool>() : 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>() : 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 <std::size_t N>
|
||
|
|
void Numbers(const Json& object, const char* key, std::array<float, N>& 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<float>();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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<float>(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<const std::byte> bin;
|
||
|
|
};
|
||
|
|
|
||
|
|
// The bytes of buffer view `index`, which must lie in the BIN chunk.
|
||
|
|
std::expected<std::span<const std::byte>, 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<std::size_t>(index) >= views.size()) {
|
||
|
|
return Error(std::format("bufferView {} doesn't exist", index));
|
||
|
|
}
|
||
|
|
const Json& view = views[static_cast<std::size_t>(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<std::uint64_t>(offset) > size || static_cast<std::uint64_t>(length) > size - static_cast<std::uint64_t>(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<std::size_t>(stride);
|
||
|
|
return ctx.bin.subspan(static_cast<std::size_t>(offset), static_cast<std::size_t>(length));
|
||
|
|
}
|
||
|
|
|
||
|
|
std::expected<Accessor, std::string> ReadAccessor(const Context& ctx, std::int64_t index) {
|
||
|
|
const Json& accessors = Array(ctx.root, "accessors");
|
||
|
|
if (index < 0 || static_cast<std::size_t>(index) >= accessors.size()) {
|
||
|
|
return Error(std::format("accessor {} doesn't exist", index));
|
||
|
|
}
|
||
|
|
const Json& accessor = accessors[static_cast<std::size_t>(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<std::uint32_t>(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<std::uint64_t>(offset) <= size
|
||
|
|
&& (count == 0 || (elementSize <= size - static_cast<std::uint64_t>(offset)
|
||
|
|
&& static_cast<std::uint64_t>(count - 1) <= (size - static_cast<std::uint64_t>(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<std::size_t>(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<Accessor, std::string> Attribute(const Context& ctx, const Json& attributes, const char* name, std::uint32_t components,
|
||
|
|
std::initializer_list<std::uint32_t> 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<float, 3, 0>;
|
||
|
|
const std::size_t n = p.positions.size();
|
||
|
|
std::vector<V3> tangentSum(n, V3(0, 0, 0));
|
||
|
|
std::vector<V3> 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<float, 4, 4>(tangent.x, tangent.y, tangent.z, sign);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
std::expected<GlbPrimitive, std::string> 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<std::uint16_t>(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<std::int32_t>(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<std::int32_t>(Integer(*info, "index", -1));
|
||
|
|
ref.texCoord = static_cast<std::uint32_t>(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<void, std::string> CheckReferences(const GlbAsset& a) {
|
||
|
|
auto valid = [](std::int64_t index, std::size_t size) { return index == -1 || (index >= 0 && static_cast<std::size_t>(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<std::size_t>(node.skin)].joints.size();
|
||
|
|
for (const GlbPrimitive& p : a.meshes[static_cast<std::size_t>(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<std::size_t>(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, std::string> 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<std::byte> bytes(static_cast<std::size_t>(size));
|
||
|
|
if (!file.read(reinterpret_cast<char*>(bytes.data()), static_cast<std::streamsize>(bytes.size()))) {
|
||
|
|
return Error(std::format("can't read {}", path.string()));
|
||
|
|
}
|
||
|
|
auto asset = Load(std::span<const std::byte>(bytes));
|
||
|
|
if (!asset) return std::unexpected(std::format("{} ({})", asset.error(), path.string()));
|
||
|
|
return asset;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::expected<GlbAsset, std::string> GlbAsset::Load(std::span<const std::byte> 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<const std::byte> jsonChunk;
|
||
|
|
std::span<const std::byte> 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<const std::byte> 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<const char*>(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::uint32_t>());
|
||
|
|
}
|
||
|
|
std::array<float, 3> t = {0, 0, 0};
|
||
|
|
std::array<float, 4> r = {0, 0, 0, 1};
|
||
|
|
std::array<float, 3> s = {1, 1, 1};
|
||
|
|
Numbers(node, "translation", t);
|
||
|
|
Numbers(node, "rotation", r);
|
||
|
|
Numbers(node, "scale", s);
|
||
|
|
n.translation = Vector<float, 3, 0>(t[0], t[1], t[2]);
|
||
|
|
n.rotation = Vector<float, 4, 0>(r[0], r[1], r[2], r[3]);
|
||
|
|
n.scale = Vector<float, 3, 0>(s[0], s[1], s[2]);
|
||
|
|
if (Array(node, "matrix").size() == 16) {
|
||
|
|
std::array<float, 16> m = {};
|
||
|
|
Numbers(node, "matrix", m);
|
||
|
|
n.hasMatrix = true;
|
||
|
|
n.matrix = MatrixRowMajor<float, 4, 4, 1>(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<std::int32_t>(Integer(node, "mesh", -1));
|
||
|
|
n.skin = static_cast<std::int32_t>(Integer(node, "skin", -1));
|
||
|
|
n.camera = static_cast<std::int32_t>(Integer(node, "camera", -1));
|
||
|
|
if (const Json* punctual = Extension(node, "KHR_lights_punctual")) {
|
||
|
|
n.light = static_cast<std::int32_t>(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<std::int32_t>(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> seen(asset.nodes.size(), Seen::No);
|
||
|
|
std::vector<std::size_t> walked;
|
||
|
|
for (std::size_t i = 0; i < asset.nodes.size(); ++i) {
|
||
|
|
walked.clear();
|
||
|
|
for (std::int32_t at = static_cast<std::int32_t>(i); at != -1 && seen[static_cast<std::size_t>(at)] != Seen::ReachesRoot;
|
||
|
|
at = asset.nodes[static_cast<std::size_t>(at)].parent) {
|
||
|
|
if (seen[static_cast<std::size_t>(at)] == Seen::OnThisWalk) return Error("the node hierarchy has a cycle");
|
||
|
|
seen[static_cast<std::size_t>(at)] = Seen::OnThisWalk;
|
||
|
|
walked.push_back(static_cast<std::size_t>(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<std::size_t>(scene) < scenes.size()) {
|
||
|
|
for (const Json& node : Array(scenes[static_cast<std::size_t>(scene)], "nodes")) {
|
||
|
|
if (!node.is_number_unsigned() || node.get<std::size_t>() >= asset.nodes.size()) {
|
||
|
|
return Error("the default scene names a node that doesn't exist");
|
||
|
|
}
|
||
|
|
asset.sceneRoots.push_back(node.get<std::uint32_t>());
|
||
|
|
}
|
||
|
|
asset.extras = Extras(scenes[static_cast<std::size_t>(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<std::uint32_t>(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<std::uint32_t>());
|
||
|
|
}
|
||
|
|
s.skeleton = static_cast<std::int32_t>(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<float, 4, 4, 1>::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<std::int32_t>(Integer(texture, "source", -1)),
|
||
|
|
.sampler = static_cast<std::int32_t>(Integer(texture, "sampler", -1)),
|
||
|
|
});
|
||
|
|
}
|
||
|
|
for (const Json& sampler : Array(root, "samplers")) {
|
||
|
|
asset.samplers.push_back({
|
||
|
|
.magFilter = static_cast<std::uint32_t>(Integer(sampler, "magFilter", 0)),
|
||
|
|
.minFilter = static_cast<std::uint32_t>(Integer(sampler, "minFilter", 0)),
|
||
|
|
.wrapS = static_cast<std::uint32_t>(Integer(sampler, "wrapS", 10497)),
|
||
|
|
.wrapT = static_cast<std::uint32_t>(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<GlbRGBA8>::LoadPNGFromMemory<std::uint8_t>(*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;
|
||
|
|
}
|