2024-09-02 14:50:30 +02:00
|
|
|
#pragma once
|
|
|
|
|
|
|
|
#include <json/json.h>
|
2024-09-05 17:16:55 +02:00
|
|
|
#include "util/base64.h"
|
|
|
|
|
|
|
|
#include <cstdint>
|
2024-09-02 14:50:30 +02:00
|
|
|
#include <functional>
|
|
|
|
#include <stack>
|
|
|
|
#include <string>
|
|
|
|
#include <string_view>
|
|
|
|
#include <variant>
|
|
|
|
#include <vector>
|
|
|
|
#include <array>
|
|
|
|
#include <optional>
|
|
|
|
#include <limits>
|
2024-11-03 15:10:39 +01:00
|
|
|
#include <memory> // unique_ptr
|
2024-09-02 14:50:30 +02:00
|
|
|
#include <stdexcept>
|
|
|
|
#include <unordered_map>
|
|
|
|
#include <unordered_set>
|
|
|
|
|
2024-11-03 15:10:39 +01:00
|
|
|
#if JSONCPP_VERSION_HEXA < 0x01090000 /* 1.9.0 */
|
|
|
|
namespace Json {
|
|
|
|
using String = JSONCPP_STRING; // Polyfill
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
|
2024-09-02 14:50:30 +02:00
|
|
|
namespace tiniergltf {
|
|
|
|
|
|
|
|
static inline void check(bool cond) {
|
|
|
|
if (!cond)
|
|
|
|
throw std::runtime_error("invalid glTF");
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
static inline void checkIndex(const std::optional<std::vector<T>> &vec,
|
|
|
|
const std::optional<std::size_t> &i) {
|
|
|
|
if (!i.has_value()) return;
|
|
|
|
check(vec.has_value());
|
|
|
|
check(i < vec->size());
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
static inline void checkIndex(const std::vector<T> &vec,
|
|
|
|
const std::optional<std::size_t> &i) {
|
|
|
|
if (!i.has_value()) return;
|
|
|
|
check(i < vec.size());
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T, typename F>
|
|
|
|
static inline void checkForall(const std::optional<std::vector<T>> &vec, const F &cond) {
|
|
|
|
if (!vec.has_value())
|
|
|
|
return;
|
|
|
|
for (const T &v : vec.value())
|
|
|
|
cond(v);
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
static inline void checkDuplicateFree(const std::vector<T> &vec) {
|
|
|
|
check(std::unordered_set<T>(vec.begin(), vec.end()).size() == vec.size());
|
|
|
|
}
|
|
|
|
|
|
|
|
template <typename T>
|
|
|
|
static inline T as(const Json::Value &o);
|
|
|
|
|
|
|
|
template<>
|
|
|
|
bool as(const Json::Value &o) {
|
|
|
|
check(o.isBool());
|
|
|
|
return o.asBool();
|
|
|
|
}
|
|
|
|
|
|
|
|
template<>
|
|
|
|
double as (const Json::Value &o) {
|
|
|
|
check(o.isDouble());
|
|
|
|
return o.asDouble();
|
|
|
|
}
|
|
|
|
|
|
|
|
template<>
|
|
|
|
std::size_t as(const Json::Value &o) {
|
|
|
|
check(o.isUInt64());
|
|
|
|
auto u = o.asUInt64();
|
|
|
|
check(u <= std::numeric_limits<std::size_t>::max());
|
|
|
|
return u;
|
|
|
|
}
|
|
|
|
|
|
|
|
template<>
|
|
|
|
std::string as(const Json::Value &o) {
|
|
|
|
check(o.isString());
|
|
|
|
return o.asString();
|
|
|
|
}
|
|
|
|
|
|
|
|
template<typename U>
|
|
|
|
std::vector<U> asVec(const Json::Value &o) {
|
|
|
|
check(o.isArray());
|
|
|
|
std::vector<U> res;
|
|
|
|
res.reserve(o.size());
|
|
|
|
for (Json::ArrayIndex i = 0; i < o.size(); ++i) {
|
|
|
|
res.push_back(as<U>(o[i]));
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
template<typename U, std::size_t n>
|
|
|
|
std::array<U, n> asArr(const Json::Value &o) {
|
|
|
|
check(o.isArray());
|
|
|
|
check(o.size() == n);
|
|
|
|
std::array<U, n> res;
|
|
|
|
for (Json::ArrayIndex i = 0; i < n; ++i) {
|
|
|
|
res[i] = as<U>(o[i]);
|
|
|
|
}
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct AccessorSparseIndices {
|
|
|
|
std::size_t bufferView;
|
|
|
|
std::size_t byteOffset;
|
|
|
|
// as defined in the glTF specification
|
|
|
|
enum class ComponentType {
|
|
|
|
UNSIGNED_BYTE,
|
|
|
|
UNSIGNED_SHORT,
|
|
|
|
UNSIGNED_INT,
|
|
|
|
};
|
|
|
|
ComponentType componentType;
|
|
|
|
std::size_t componentSize() const {
|
|
|
|
switch (componentType) {
|
|
|
|
case ComponentType::UNSIGNED_BYTE:
|
|
|
|
return 1;
|
|
|
|
case ComponentType::UNSIGNED_SHORT:
|
|
|
|
return 2;
|
|
|
|
case ComponentType::UNSIGNED_INT:
|
|
|
|
return 4;
|
|
|
|
}
|
|
|
|
throw std::logic_error("invalid component type");
|
|
|
|
}
|
|
|
|
std::size_t elementSize() const {
|
|
|
|
return componentSize();
|
|
|
|
}
|
|
|
|
AccessorSparseIndices(const Json::Value &o)
|
|
|
|
: bufferView(as<std::size_t>(o["bufferView"]))
|
|
|
|
, byteOffset(0)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("byteOffset")) {
|
|
|
|
byteOffset = as<std::size_t>(o["byteOffset"]);
|
|
|
|
check(byteOffset >= 0);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
static std::unordered_map<Json::UInt64, ComponentType> map = {
|
|
|
|
{5121, ComponentType::UNSIGNED_BYTE},
|
|
|
|
{5123, ComponentType::UNSIGNED_SHORT},
|
|
|
|
{5125, ComponentType::UNSIGNED_INT},
|
|
|
|
};
|
|
|
|
const auto &v = o["componentType"]; check(v.isUInt64());
|
|
|
|
componentType = map.at(v.asUInt64());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> AccessorSparseIndices as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct AccessorSparseValues {
|
|
|
|
std::size_t bufferView;
|
|
|
|
std::size_t byteOffset;
|
|
|
|
AccessorSparseValues(const Json::Value &o)
|
|
|
|
: bufferView(as<std::size_t>(o["bufferView"]))
|
|
|
|
, byteOffset(0)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("byteOffset")) {
|
|
|
|
byteOffset = as<std::size_t>(o["byteOffset"]);
|
|
|
|
check(byteOffset >= 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> AccessorSparseValues as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct AccessorSparse {
|
|
|
|
std::size_t count;
|
|
|
|
AccessorSparseIndices indices;
|
|
|
|
AccessorSparseValues values;
|
|
|
|
AccessorSparse(const Json::Value &o)
|
|
|
|
: count(as<std::size_t>(o["count"]))
|
|
|
|
, indices(as<AccessorSparseIndices>(o["indices"]))
|
|
|
|
, values(as<AccessorSparseValues>(o["values"]))
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
check(count >= 1);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> AccessorSparse as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Accessor {
|
|
|
|
std::optional<std::size_t> bufferView;
|
|
|
|
std::size_t byteOffset;
|
|
|
|
// as defined in the glTF specification
|
|
|
|
enum class ComponentType {
|
|
|
|
BYTE,
|
|
|
|
UNSIGNED_BYTE,
|
|
|
|
SHORT,
|
|
|
|
UNSIGNED_SHORT,
|
|
|
|
UNSIGNED_INT,
|
|
|
|
FLOAT,
|
|
|
|
};
|
|
|
|
ComponentType componentType;
|
|
|
|
std::size_t componentSize() const {
|
|
|
|
switch (componentType) {
|
|
|
|
case ComponentType::BYTE:
|
|
|
|
case ComponentType::UNSIGNED_BYTE:
|
|
|
|
return 1;
|
|
|
|
case ComponentType::SHORT:
|
|
|
|
case ComponentType::UNSIGNED_SHORT:
|
|
|
|
return 2;
|
|
|
|
case ComponentType::UNSIGNED_INT:
|
|
|
|
case ComponentType::FLOAT:
|
|
|
|
return 4;
|
|
|
|
}
|
|
|
|
throw std::logic_error("invalid component type");
|
|
|
|
}
|
|
|
|
std::size_t count;
|
|
|
|
std::optional<std::vector<double>> max;
|
|
|
|
std::optional<std::vector<double>> min;
|
|
|
|
std::optional<std::string> name;
|
|
|
|
bool normalized;
|
|
|
|
std::optional<AccessorSparse> sparse;
|
|
|
|
enum class Type {
|
|
|
|
MAT2,
|
|
|
|
MAT3,
|
|
|
|
MAT4,
|
|
|
|
SCALAR,
|
|
|
|
VEC2,
|
|
|
|
VEC3,
|
|
|
|
VEC4,
|
|
|
|
};
|
|
|
|
std::size_t typeCount() const {
|
|
|
|
switch (type) {
|
|
|
|
case Type::SCALAR:
|
|
|
|
return 1;
|
|
|
|
case Type::VEC2:
|
|
|
|
return 2;
|
|
|
|
case Type::VEC3:
|
|
|
|
return 3;
|
|
|
|
case Type::MAT2:
|
|
|
|
case Type::VEC4:
|
|
|
|
return 4;
|
|
|
|
case Type::MAT3:
|
|
|
|
return 9;
|
|
|
|
case Type::MAT4:
|
|
|
|
return 16;
|
|
|
|
}
|
|
|
|
throw std::logic_error("invalid type");
|
|
|
|
}
|
|
|
|
Type type;
|
|
|
|
std::size_t elementSize() const {
|
|
|
|
return componentSize() * typeCount();
|
|
|
|
}
|
|
|
|
Accessor(const Json::Value &o)
|
|
|
|
: byteOffset(0)
|
|
|
|
, count(as<std::size_t>(o["count"]))
|
|
|
|
, normalized(false)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("bufferView")) {
|
|
|
|
bufferView = as<std::size_t>(o["bufferView"]);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
static std::unordered_map<Json::UInt64, ComponentType> map = {
|
|
|
|
{5120, ComponentType::BYTE},
|
|
|
|
{5121, ComponentType::UNSIGNED_BYTE},
|
|
|
|
{5122, ComponentType::SHORT},
|
|
|
|
{5123, ComponentType::UNSIGNED_SHORT},
|
|
|
|
{5125, ComponentType::UNSIGNED_INT},
|
|
|
|
{5126, ComponentType::FLOAT},
|
|
|
|
};
|
|
|
|
const auto &v = o["componentType"]; check(v.isUInt64());
|
|
|
|
componentType = map.at(v.asUInt64());
|
|
|
|
}
|
|
|
|
if (o.isMember("byteOffset")) {
|
|
|
|
byteOffset = as<std::size_t>(o["byteOffset"]);
|
|
|
|
check(byteOffset >= 0);
|
|
|
|
check(byteOffset % componentSize() == 0);
|
|
|
|
}
|
|
|
|
check(count >= 1);
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("normalized")) {
|
|
|
|
normalized = as<bool>(o["normalized"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("sparse")) {
|
|
|
|
sparse = as<AccessorSparse>(o["sparse"]);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
static std::unordered_map<Json::String, Type> map = {
|
|
|
|
{"MAT2", Type::MAT2},
|
|
|
|
{"MAT3", Type::MAT3},
|
|
|
|
{"MAT4", Type::MAT4},
|
|
|
|
{"SCALAR", Type::SCALAR},
|
|
|
|
{"VEC2", Type::VEC2},
|
|
|
|
{"VEC3", Type::VEC3},
|
|
|
|
{"VEC4", Type::VEC4},
|
|
|
|
};
|
|
|
|
const auto &v = o["type"]; check(v.isString());
|
|
|
|
type = map.at(v.asString());
|
|
|
|
}
|
|
|
|
if (o.isMember("max")) {
|
|
|
|
max = asVec<double>(o["max"]);
|
|
|
|
check(max->size() == typeCount());
|
|
|
|
}
|
|
|
|
if (o.isMember("min")) {
|
|
|
|
min = asVec<double>(o["min"]);
|
|
|
|
check(min->size() == typeCount());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Accessor as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct AnimationChannelTarget {
|
|
|
|
std::optional<std::size_t> node;
|
|
|
|
enum class Path {
|
|
|
|
ROTATION,
|
|
|
|
SCALE,
|
|
|
|
TRANSLATION,
|
|
|
|
WEIGHTS,
|
|
|
|
};
|
|
|
|
Path path;
|
|
|
|
AnimationChannelTarget(const Json::Value &o)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("node")) {
|
|
|
|
node = as<std::size_t>(o["node"]);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
static std::unordered_map<Json::String, Path> map = {
|
|
|
|
{"rotation", Path::ROTATION},
|
|
|
|
{"scale", Path::SCALE},
|
|
|
|
{"translation", Path::TRANSLATION},
|
|
|
|
{"weights", Path::WEIGHTS},
|
|
|
|
};
|
|
|
|
const auto &v = o["path"]; check(v.isString());
|
|
|
|
path = map.at(v.asString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> AnimationChannelTarget as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct AnimationChannel {
|
|
|
|
std::size_t sampler;
|
|
|
|
AnimationChannelTarget target;
|
|
|
|
AnimationChannel(const Json::Value &o)
|
|
|
|
: sampler(as<std::size_t>(o["sampler"]))
|
|
|
|
, target(as<AnimationChannelTarget>(o["target"]))
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> AnimationChannel as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct AnimationSampler {
|
|
|
|
std::size_t input;
|
|
|
|
enum class Interpolation {
|
|
|
|
CUBICSPLINE,
|
|
|
|
LINEAR,
|
|
|
|
STEP,
|
|
|
|
};
|
|
|
|
Interpolation interpolation;
|
|
|
|
std::size_t output;
|
|
|
|
AnimationSampler(const Json::Value &o)
|
|
|
|
: input(as<std::size_t>(o["input"]))
|
|
|
|
, interpolation(Interpolation::LINEAR)
|
|
|
|
, output(as<std::size_t>(o["output"]))
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("interpolation")) {
|
|
|
|
static std::unordered_map<Json::String, Interpolation> map = {
|
|
|
|
{"CUBICSPLINE", Interpolation::CUBICSPLINE},
|
|
|
|
{"LINEAR", Interpolation::LINEAR},
|
|
|
|
{"STEP", Interpolation::STEP},
|
|
|
|
};
|
|
|
|
const auto &v = o["interpolation"]; check(v.isString());
|
|
|
|
interpolation = map.at(v.asString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> AnimationSampler as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Animation {
|
|
|
|
std::vector<AnimationChannel> channels;
|
|
|
|
std::optional<std::string> name;
|
|
|
|
std::vector<AnimationSampler> samplers;
|
|
|
|
Animation(const Json::Value &o)
|
|
|
|
: channels(asVec<AnimationChannel>(o["channels"]))
|
|
|
|
, samplers(asVec<AnimationSampler>(o["samplers"]))
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
check(channels.size() >= 1);
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
|
|
|
check(samplers.size() >= 1);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Animation as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Asset {
|
|
|
|
std::optional<std::string> copyright;
|
|
|
|
std::optional<std::string> generator;
|
|
|
|
std::optional<std::string> minVersion;
|
|
|
|
std::string version;
|
|
|
|
Asset(const Json::Value &o)
|
|
|
|
: version(as<std::string>(o["version"]))
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("copyright")) {
|
|
|
|
copyright = as<std::string>(o["copyright"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("generator")) {
|
|
|
|
generator = as<std::string>(o["generator"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("minVersion")) {
|
|
|
|
minVersion = as<std::string>(o["minVersion"]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Asset as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct BufferView {
|
|
|
|
std::size_t buffer;
|
|
|
|
std::size_t byteLength;
|
|
|
|
std::size_t byteOffset;
|
|
|
|
std::optional<std::size_t> byteStride;
|
|
|
|
std::optional<std::string> name;
|
|
|
|
enum class Target {
|
|
|
|
ARRAY_BUFFER,
|
|
|
|
ELEMENT_ARRAY_BUFFER,
|
|
|
|
};
|
|
|
|
std::optional<Target> target;
|
|
|
|
BufferView(const Json::Value &o)
|
|
|
|
: buffer(as<std::size_t>(o["buffer"]))
|
|
|
|
, byteLength(as<std::size_t>(o["byteLength"]))
|
|
|
|
, byteOffset(0)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
check(byteLength >= 1);
|
|
|
|
if (o.isMember("byteOffset")) {
|
|
|
|
byteOffset = as<std::size_t>(o["byteOffset"]);
|
|
|
|
check(byteOffset >= 0);
|
|
|
|
}
|
|
|
|
if (o.isMember("byteStride")) {
|
|
|
|
byteStride = as<std::size_t>(o["byteStride"]);
|
|
|
|
check(byteStride.value() >= 4);
|
|
|
|
check(byteStride.value() <= 252);
|
|
|
|
check(byteStride.value() % 4 == 0);
|
|
|
|
}
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("target")) {
|
|
|
|
static std::unordered_map<Json::UInt64, Target> map = {
|
|
|
|
{34962, Target::ARRAY_BUFFER},
|
|
|
|
{34963, Target::ELEMENT_ARRAY_BUFFER},
|
|
|
|
};
|
|
|
|
const auto &v = o["target"]; check(v.isUInt64());
|
|
|
|
target = map.at(v.asUInt64());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> BufferView as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Buffer {
|
|
|
|
std::size_t byteLength;
|
|
|
|
std::optional<std::string> name;
|
|
|
|
std::string data;
|
|
|
|
Buffer(const Json::Value &o,
|
2024-09-05 17:16:55 +02:00
|
|
|
const std::function<std::string(const std::string &uri)> &resolveURI,
|
|
|
|
std::optional<std::string> &&glbData = std::nullopt)
|
2024-09-02 14:50:30 +02:00
|
|
|
: byteLength(as<std::size_t>(o["byteLength"]))
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
check(byteLength >= 1);
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
2024-09-05 17:16:55 +02:00
|
|
|
if (glbData.has_value()) {
|
|
|
|
check(!o.isMember("uri"));
|
|
|
|
data = *std::move(glbData);
|
|
|
|
// GLB allows padding, which need not be reflected in the JSON
|
|
|
|
check(byteLength + 3 >= data.size());
|
|
|
|
check(data.size() >= byteLength);
|
|
|
|
} else {
|
|
|
|
check(o.isMember("uri"));
|
|
|
|
bool dataURI = false;
|
|
|
|
const std::string uri = as<std::string>(o["uri"]);
|
|
|
|
for (auto &prefix : std::array<std::string, 2> {
|
|
|
|
"data:application/octet-stream;base64,",
|
|
|
|
"data:application/gltf-buffer;base64,"
|
|
|
|
}) {
|
|
|
|
if (std::string_view(uri).substr(0, prefix.length()) == prefix) {
|
|
|
|
auto view = std::string_view(uri).substr(prefix.length());
|
|
|
|
check(base64_is_valid(view));
|
|
|
|
data = base64_decode(view);
|
|
|
|
dataURI = true;
|
|
|
|
break;
|
|
|
|
}
|
2024-09-02 14:50:30 +02:00
|
|
|
}
|
2024-09-05 17:16:55 +02:00
|
|
|
if (!dataURI)
|
|
|
|
data = resolveURI(uri);
|
|
|
|
check(data.size() >= byteLength);
|
2024-09-02 14:50:30 +02:00
|
|
|
}
|
|
|
|
data.resize(byteLength);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
struct CameraOrthographic {
|
|
|
|
double xmag;
|
|
|
|
double ymag;
|
|
|
|
double zfar;
|
|
|
|
double znear;
|
|
|
|
CameraOrthographic(const Json::Value &o)
|
|
|
|
: xmag(as<double>(o["xmag"]))
|
|
|
|
, ymag(as<double>(o["ymag"]))
|
|
|
|
, zfar(as<double>(o["zfar"]))
|
|
|
|
, znear(as<double>(o["znear"]))
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
check(zfar > 0);
|
|
|
|
check(znear >= 0);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> CameraOrthographic as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct CameraPerspective {
|
|
|
|
std::optional<double> aspectRatio;
|
|
|
|
double yfov;
|
|
|
|
std::optional<double> zfar;
|
|
|
|
double znear;
|
|
|
|
CameraPerspective(const Json::Value &o)
|
|
|
|
: yfov(as<double>(o["yfov"]))
|
|
|
|
, znear(as<double>(o["znear"]))
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("aspectRatio")) {
|
|
|
|
aspectRatio = as<double>(o["aspectRatio"]);
|
|
|
|
check(aspectRatio.value() > 0);
|
|
|
|
}
|
|
|
|
check(yfov > 0);
|
|
|
|
if (o.isMember("zfar")) {
|
|
|
|
zfar = as<double>(o["zfar"]);
|
|
|
|
check(zfar.value() > 0);
|
|
|
|
}
|
|
|
|
check(znear > 0);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> CameraPerspective as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Camera {
|
|
|
|
std::optional<std::string> name;
|
|
|
|
std::optional<CameraOrthographic> orthographic;
|
|
|
|
std::optional<CameraPerspective> perspective;
|
|
|
|
enum class Type {
|
|
|
|
ORTHOGRAPHIC,
|
|
|
|
PERSPECTIVE,
|
|
|
|
};
|
|
|
|
Type type;
|
|
|
|
Camera(const Json::Value &o)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("orthographic")) {
|
|
|
|
orthographic = as<CameraOrthographic>(o["orthographic"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("perspective")) {
|
|
|
|
perspective = as<CameraPerspective>(o["perspective"]);
|
|
|
|
}
|
|
|
|
{
|
|
|
|
static std::unordered_map<Json::String, Type> map = {
|
|
|
|
{"orthographic", Type::ORTHOGRAPHIC},
|
|
|
|
{"perspective", Type::PERSPECTIVE},
|
|
|
|
};
|
|
|
|
const auto &v = o["type"]; check(v.isString());
|
|
|
|
type = map.at(v.asString());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Camera as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Image {
|
|
|
|
std::optional<std::size_t> bufferView;
|
|
|
|
enum class MimeType {
|
|
|
|
IMAGE_JPEG,
|
|
|
|
IMAGE_PNG,
|
|
|
|
};
|
|
|
|
std::optional<MimeType> mimeType;
|
|
|
|
std::optional<std::string> name;
|
|
|
|
std::optional<std::string> uri;
|
|
|
|
Image(const Json::Value &o)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("bufferView")) {
|
|
|
|
bufferView = as<std::size_t>(o["bufferView"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("mimeType")) {
|
|
|
|
static std::unordered_map<Json::String, MimeType> map = {
|
|
|
|
{"image/jpeg", MimeType::IMAGE_JPEG},
|
|
|
|
{"image/png", MimeType::IMAGE_PNG},
|
|
|
|
};
|
|
|
|
const auto &v = o["mimeType"]; check(v.isString());
|
|
|
|
mimeType = map.at(v.asString());
|
|
|
|
}
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("uri")) {
|
|
|
|
uri = as<std::string>(o["uri"]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Image as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct TextureInfo {
|
|
|
|
std::size_t index;
|
|
|
|
std::size_t texCoord;
|
|
|
|
TextureInfo(const Json::Value &o)
|
|
|
|
: index(as<std::size_t>(o["index"]))
|
|
|
|
, texCoord(0)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("texCoord")) {
|
|
|
|
texCoord = as<std::size_t>(o["texCoord"]);
|
|
|
|
check(texCoord >= 0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> TextureInfo as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct MaterialNormalTextureInfo {
|
|
|
|
std::size_t index;
|
|
|
|
double scale;
|
|
|
|
std::size_t texCoord;
|
|
|
|
MaterialNormalTextureInfo(const Json::Value &o)
|
|
|
|
: index(as<std::size_t>(o["index"]))
|
|
|
|
, scale(1)
|
|
|
|
, texCoord(0)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("scale")) {
|
|
|
|
scale = as<double>(o["scale"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("texCoord")) {
|
|
|
|
texCoord = as<std::size_t>(o["texCoord"]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> MaterialNormalTextureInfo as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct MaterialOcclusionTextureInfo {
|
|
|
|
std::size_t index;
|
|
|
|
double strength;
|
|
|
|
std::size_t texCoord;
|
|
|
|
MaterialOcclusionTextureInfo(const Json::Value &o)
|
|
|
|
: index(as<std::size_t>(o["index"]))
|
|
|
|
, strength(1)
|
|
|
|
, texCoord(0)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("strength")) {
|
|
|
|
strength = as<double>(o["strength"]);
|
|
|
|
check(strength >= 0);
|
|
|
|
check(strength <= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("texCoord")) {
|
|
|
|
texCoord = as<std::size_t>(o["texCoord"]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> MaterialOcclusionTextureInfo as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct MaterialPbrMetallicRoughness {
|
|
|
|
std::array<double, 4> baseColorFactor;
|
|
|
|
std::optional<TextureInfo> baseColorTexture;
|
|
|
|
double metallicFactor;
|
|
|
|
std::optional<TextureInfo> metallicRoughnessTexture;
|
|
|
|
double roughnessFactor;
|
|
|
|
MaterialPbrMetallicRoughness(const Json::Value &o)
|
|
|
|
: baseColorFactor{1, 1, 1, 1}
|
|
|
|
, metallicFactor(1)
|
|
|
|
, roughnessFactor(1)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("baseColorFactor")) {
|
|
|
|
baseColorFactor = asArr<double, 4>(o["baseColorFactor"]);
|
|
|
|
for (auto v: baseColorFactor) {
|
|
|
|
check(v >= 0);
|
|
|
|
check(v <= 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (o.isMember("baseColorTexture")) {
|
|
|
|
baseColorTexture = as<TextureInfo>(o["baseColorTexture"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("metallicFactor")) {
|
|
|
|
metallicFactor = as<double>(o["metallicFactor"]);
|
|
|
|
check(metallicFactor >= 0);
|
|
|
|
check(metallicFactor <= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("metallicRoughnessTexture")) {
|
|
|
|
metallicRoughnessTexture = as<TextureInfo>(o["metallicRoughnessTexture"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("roughnessFactor")) {
|
|
|
|
roughnessFactor = as<double>(o["roughnessFactor"]);
|
|
|
|
check(roughnessFactor >= 0);
|
|
|
|
check(roughnessFactor <= 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> MaterialPbrMetallicRoughness as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Material {
|
|
|
|
double alphaCutoff;
|
|
|
|
enum class AlphaMode {
|
|
|
|
BLEND,
|
|
|
|
MASK,
|
|
|
|
OPAQUE,
|
|
|
|
};
|
|
|
|
AlphaMode alphaMode;
|
|
|
|
bool doubleSided;
|
|
|
|
std::array<double, 3> emissiveFactor;
|
|
|
|
std::optional<TextureInfo> emissiveTexture;
|
|
|
|
std::optional<std::string> name;
|
|
|
|
std::optional<MaterialNormalTextureInfo> normalTexture;
|
|
|
|
std::optional<MaterialOcclusionTextureInfo> occlusionTexture;
|
|
|
|
std::optional<MaterialPbrMetallicRoughness> pbrMetallicRoughness;
|
|
|
|
Material(const Json::Value &o)
|
|
|
|
: alphaCutoff(0.5)
|
|
|
|
, alphaMode(AlphaMode::OPAQUE)
|
|
|
|
, doubleSided(false)
|
|
|
|
, emissiveFactor{0, 0, 0}
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("alphaCutoff")) {
|
|
|
|
alphaCutoff = as<double>(o["alphaCutoff"]);
|
|
|
|
check(alphaCutoff >= 0);
|
|
|
|
}
|
|
|
|
if (o.isMember("alphaMode")){
|
|
|
|
static std::unordered_map<Json::String, AlphaMode> map = {
|
|
|
|
{"BLEND", AlphaMode::BLEND},
|
|
|
|
{"MASK", AlphaMode::MASK},
|
|
|
|
{"OPAQUE", AlphaMode::OPAQUE},
|
|
|
|
};
|
|
|
|
const auto &v = o["alphaMode"]; check(v.isString());
|
|
|
|
alphaMode = map.at(v.asString());
|
|
|
|
}
|
|
|
|
if (o.isMember("doubleSided")) {
|
|
|
|
doubleSided = as<bool>(o["doubleSided"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("emissiveFactor")) {
|
|
|
|
emissiveFactor = asArr<double, 3>(o["emissiveFactor"]);
|
|
|
|
for (const auto &v: emissiveFactor) {
|
|
|
|
check(v >= 0);
|
|
|
|
check(v <= 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (o.isMember("emissiveTexture")) {
|
|
|
|
emissiveTexture = as<TextureInfo>(o["emissiveTexture"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("normalTexture")) {
|
|
|
|
normalTexture = as<MaterialNormalTextureInfo>(o["normalTexture"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("occlusionTexture")) {
|
|
|
|
occlusionTexture = as<MaterialOcclusionTextureInfo>(o["occlusionTexture"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("pbrMetallicRoughness")) {
|
|
|
|
pbrMetallicRoughness = as<MaterialPbrMetallicRoughness>(o["pbrMetallicRoughness"]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Material as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct MeshPrimitive {
|
|
|
|
static void enumeratedProps(const Json::Value &o, const std::string &name, std::optional<std::vector<std::size_t>> &attr) {
|
|
|
|
for (std::size_t i = 0;; ++i) {
|
|
|
|
const std::string s = name + "_" + std::to_string(i);
|
|
|
|
if (!o.isMember(s)) break;
|
|
|
|
if (i == 0) {
|
|
|
|
attr = std::vector<std::size_t>();
|
|
|
|
}
|
|
|
|
attr->push_back(as<std::size_t>(o[s]));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
struct Attributes {
|
|
|
|
std::optional<std::size_t> position, normal, tangent;
|
|
|
|
std::optional<std::vector<std::size_t>> texcoord, color, joints, weights;
|
|
|
|
Attributes(const Json::Value &o) {
|
|
|
|
if (o.isMember("POSITION"))
|
|
|
|
position = as<std::size_t>(o["POSITION"]);
|
|
|
|
if (o.isMember("NORMAL"))
|
|
|
|
normal = as<std::size_t>(o["NORMAL"]);
|
|
|
|
if (o.isMember("TANGENT"))
|
|
|
|
tangent = as<std::size_t>(o["TANGENT"]);
|
|
|
|
enumeratedProps(o, "TEXCOORD", texcoord);
|
|
|
|
enumeratedProps(o, "COLOR", color);
|
|
|
|
enumeratedProps(o, "JOINTS", joints);
|
|
|
|
enumeratedProps(o, "WEIGHTS", weights);
|
|
|
|
check(joints.has_value() == weights.has_value());
|
|
|
|
if (joints.has_value()) {
|
|
|
|
check(joints->size() == weights->size());
|
|
|
|
}
|
|
|
|
check(position.has_value()
|
|
|
|
|| normal.has_value()
|
|
|
|
|| tangent.has_value()
|
|
|
|
|| texcoord.has_value()
|
|
|
|
|| color.has_value()
|
|
|
|
|| joints.has_value()
|
|
|
|
|| weights.has_value());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
Attributes attributes;
|
|
|
|
std::optional<std::size_t> indices;
|
|
|
|
std::optional<std::size_t> material;
|
|
|
|
enum class Mode {
|
|
|
|
POINTS,
|
|
|
|
LINES,
|
|
|
|
LINE_LOOP,
|
|
|
|
LINE_STRIP,
|
|
|
|
TRIANGLES,
|
|
|
|
TRIANGLE_STRIP,
|
|
|
|
TRIANGLE_FAN,
|
|
|
|
};
|
|
|
|
Mode mode;
|
|
|
|
struct MorphTargets {
|
|
|
|
std::optional<std::size_t> position, normal, tangent;
|
|
|
|
std::optional<std::vector<std::size_t>> texcoord, color;
|
|
|
|
MorphTargets(const Json::Value &o) {
|
|
|
|
if (o.isMember("POSITION"))
|
|
|
|
position = as<std::size_t>(o["POSITION"]);
|
|
|
|
if (o.isMember("NORMAL"))
|
|
|
|
normal = as<std::size_t>(o["NORMAL"]);
|
|
|
|
if (o.isMember("TANGENT"))
|
|
|
|
tangent = as<std::size_t>(o["TANGENT"]);
|
|
|
|
enumeratedProps(o, "TEXCOORD", texcoord);
|
|
|
|
enumeratedProps(o, "COLOR", color);
|
|
|
|
check(position.has_value()
|
|
|
|
|| normal.has_value()
|
|
|
|
|| tangent.has_value()
|
|
|
|
|| texcoord.has_value()
|
|
|
|
|| color.has_value());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
std::optional<std::vector<MorphTargets>> targets;
|
|
|
|
MeshPrimitive(const Json::Value &o)
|
|
|
|
: attributes(Attributes(o["attributes"]))
|
|
|
|
, mode(Mode::TRIANGLES)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("indices")) {
|
|
|
|
indices = as<std::size_t>(o["indices"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("material")) {
|
|
|
|
material = as<std::size_t>(o["material"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("mode")) {
|
|
|
|
static std::unordered_map<Json::UInt64, Mode> map = {
|
|
|
|
{0, Mode::POINTS},
|
|
|
|
{1, Mode::LINES},
|
|
|
|
{2, Mode::LINE_LOOP},
|
|
|
|
{3, Mode::LINE_STRIP},
|
|
|
|
{4, Mode::TRIANGLES},
|
|
|
|
{5, Mode::TRIANGLE_STRIP},
|
|
|
|
{6, Mode::TRIANGLE_FAN},
|
|
|
|
};
|
|
|
|
const auto &v = o["mode"]; check(v.isUInt64());
|
|
|
|
mode = map.at(v.asUInt64());
|
|
|
|
}
|
|
|
|
if (o.isMember("targets")) {
|
|
|
|
targets = asVec<MorphTargets>(o["targets"]);
|
|
|
|
check(targets->size() >= 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> MeshPrimitive::MorphTargets as(const Json::Value &o) { return o; }
|
|
|
|
template<> MeshPrimitive as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Mesh {
|
|
|
|
std::optional<std::string> name;
|
|
|
|
std::vector<MeshPrimitive> primitives;
|
|
|
|
std::optional<std::vector<double>> weights;
|
|
|
|
Mesh(const Json::Value &o)
|
|
|
|
: primitives(asVec<MeshPrimitive>(o["primitives"]))
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
|
|
|
check(primitives.size() >= 1);
|
|
|
|
if (o.isMember("weights")) {
|
|
|
|
weights = asVec<double>(o["weights"]);
|
|
|
|
check(weights->size() >= 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Mesh as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Node {
|
|
|
|
std::optional<std::size_t> camera;
|
|
|
|
std::optional<std::vector<std::size_t>> children;
|
|
|
|
typedef std::array<double, 16> Matrix;
|
|
|
|
struct TRS {
|
|
|
|
std::array<double, 3> translation = {0, 0, 0};
|
|
|
|
std::array<double, 4> rotation = {0, 0, 0, 1};
|
|
|
|
std::array<double, 3> scale = {1, 1, 1};
|
|
|
|
};
|
|
|
|
std::variant<Matrix, TRS> transform;
|
|
|
|
std::optional<std::size_t> mesh;
|
|
|
|
std::optional<std::string> name;
|
|
|
|
std::optional<std::size_t> skin;
|
|
|
|
std::optional<std::vector<double>> weights;
|
|
|
|
Node(const Json::Value &o)
|
|
|
|
: transform(Matrix {
|
|
|
|
1, 0, 0, 0,
|
|
|
|
0, 1, 0, 0,
|
|
|
|
0, 0, 1, 0,
|
|
|
|
0, 0, 0, 1
|
|
|
|
})
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("camera")) {
|
|
|
|
camera = as<std::size_t>(o["camera"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("children")) {
|
|
|
|
children = asVec<std::size_t>(o["children"]);
|
|
|
|
check(children->size() >= 1);
|
|
|
|
checkDuplicateFree(*children);
|
|
|
|
}
|
|
|
|
bool hasTRS = o.isMember("translation") || o.isMember("rotation") || o.isMember("scale");
|
|
|
|
if (o.isMember("matrix")) {
|
|
|
|
check(!hasTRS);
|
|
|
|
transform = asArr<double, 16>(o["matrix"]);
|
|
|
|
} else if (hasTRS) {
|
|
|
|
TRS trs;
|
|
|
|
if (o.isMember("translation")) {
|
|
|
|
trs.translation = asArr<double, 3>(o["translation"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("rotation")) {
|
|
|
|
trs.rotation = asArr<double, 4>(o["rotation"]);
|
|
|
|
for (auto v: trs.rotation) {
|
|
|
|
check(v >= -1);
|
|
|
|
check(v <= 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (o.isMember("scale")) {
|
|
|
|
trs.scale = asArr<double, 3>(o["scale"]);
|
|
|
|
}
|
|
|
|
transform = trs;
|
|
|
|
}
|
|
|
|
if (o.isMember("mesh")) {
|
|
|
|
mesh = as<std::size_t>(o["mesh"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("skin")) {
|
|
|
|
check(mesh.has_value());
|
|
|
|
skin = as<std::size_t>(o["skin"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("weights")) {
|
|
|
|
weights = asVec<double>(o["weights"]);
|
|
|
|
check(weights->size() >= 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Node as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Sampler {
|
|
|
|
enum class MagFilter {
|
|
|
|
NEAREST,
|
|
|
|
LINEAR,
|
|
|
|
};
|
|
|
|
std::optional<MagFilter> magFilter;
|
|
|
|
enum class MinFilter {
|
|
|
|
NEAREST,
|
|
|
|
LINEAR,
|
|
|
|
NEAREST_MIPMAP_NEAREST,
|
|
|
|
LINEAR_MIPMAP_NEAREST,
|
|
|
|
NEAREST_MIPMAP_LINEAR,
|
|
|
|
LINEAR_MIPMAP_LINEAR,
|
|
|
|
};
|
|
|
|
std::optional<MinFilter> minFilter;
|
|
|
|
std::optional<std::string> name;
|
2024-10-08 20:34:16 +02:00
|
|
|
enum class Wrap {
|
2024-09-02 14:50:30 +02:00
|
|
|
REPEAT,
|
|
|
|
CLAMP_TO_EDGE,
|
|
|
|
MIRRORED_REPEAT,
|
|
|
|
};
|
2024-10-08 20:34:16 +02:00
|
|
|
Wrap wrapS;
|
|
|
|
Wrap wrapT;
|
2024-09-02 14:50:30 +02:00
|
|
|
Sampler(const Json::Value &o)
|
2024-10-08 20:34:16 +02:00
|
|
|
: wrapS(Wrap::REPEAT)
|
|
|
|
, wrapT(Wrap::REPEAT)
|
2024-09-02 14:50:30 +02:00
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("magFilter")) {
|
|
|
|
static std::unordered_map<Json::UInt64, MagFilter> map = {
|
|
|
|
{9728, MagFilter::NEAREST},
|
|
|
|
{9729, MagFilter::LINEAR},
|
|
|
|
};
|
|
|
|
const auto &v = o["magFilter"]; check(v.isUInt64());
|
|
|
|
magFilter = map.at(v.asUInt64());
|
|
|
|
}
|
|
|
|
if (o.isMember("minFilter")) {
|
|
|
|
static std::unordered_map<Json::UInt64, MinFilter> map = {
|
|
|
|
{9728, MinFilter::NEAREST},
|
|
|
|
{9729, MinFilter::LINEAR},
|
|
|
|
{9984, MinFilter::NEAREST_MIPMAP_NEAREST},
|
|
|
|
{9985, MinFilter::LINEAR_MIPMAP_NEAREST},
|
|
|
|
{9986, MinFilter::NEAREST_MIPMAP_LINEAR},
|
|
|
|
{9987, MinFilter::LINEAR_MIPMAP_LINEAR},
|
|
|
|
};
|
|
|
|
const auto &v = o["minFilter"]; check(v.isUInt64());
|
|
|
|
minFilter = map.at(v.asUInt64());
|
|
|
|
}
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
2024-10-08 20:34:16 +02:00
|
|
|
static std::unordered_map<Json::UInt64, Wrap> map = {
|
|
|
|
{10497, Wrap::REPEAT},
|
|
|
|
{33071, Wrap::CLAMP_TO_EDGE},
|
|
|
|
{33648, Wrap::MIRRORED_REPEAT},
|
|
|
|
};
|
2024-09-02 14:50:30 +02:00
|
|
|
if (o.isMember("wrapS")) {
|
|
|
|
const auto &v = o["wrapS"]; check(v.isUInt64());
|
|
|
|
wrapS = map.at(v.asUInt64());
|
|
|
|
}
|
|
|
|
if (o.isMember("wrapT")) {
|
|
|
|
const auto &v = o["wrapT"]; check(v.isUInt64());
|
|
|
|
wrapT = map.at(v.asUInt64());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Sampler as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Scene {
|
|
|
|
std::optional<std::string> name;
|
|
|
|
std::optional<std::vector<std::size_t>> nodes;
|
|
|
|
Scene(const Json::Value &o)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("nodes")) {
|
|
|
|
nodes = asVec<std::size_t>(o["nodes"]);
|
|
|
|
check(nodes->size() >= 1);
|
|
|
|
checkDuplicateFree(*nodes);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Scene as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Skin {
|
|
|
|
std::optional<std::size_t> inverseBindMatrices;
|
|
|
|
std::vector<std::size_t> joints;
|
|
|
|
std::optional<std::string> name;
|
|
|
|
std::optional<std::size_t> skeleton;
|
|
|
|
Skin(const Json::Value &o)
|
|
|
|
: joints(asVec<std::size_t>(o["joints"]))
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("inverseBindMatrices")) {
|
|
|
|
inverseBindMatrices = as<std::size_t>(o["inverseBindMatrices"]);
|
|
|
|
}
|
|
|
|
check(joints.size() >= 1);
|
|
|
|
checkDuplicateFree(joints);
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("skeleton")) {
|
|
|
|
skeleton = as<std::size_t>(o["skeleton"]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Skin as(const Json::Value &o) { return o; }
|
|
|
|
|
|
|
|
struct Texture {
|
|
|
|
std::optional<std::string> name;
|
|
|
|
std::optional<std::size_t> sampler;
|
|
|
|
std::optional<std::size_t> source;
|
|
|
|
Texture(const Json::Value &o)
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("name")) {
|
|
|
|
name = as<std::string>(o["name"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("sampler")) {
|
|
|
|
sampler = as<std::size_t>(o["sampler"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("source")) {
|
|
|
|
source = as<std::size_t>(o["source"]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
template<> Texture as(const Json::Value &o) { return o; }
|
|
|
|
|
2024-09-05 17:16:55 +02:00
|
|
|
using UriResolver = std::function<std::string(const std::string &uri)>;
|
|
|
|
static inline std::string uriError(const std::string &uri) {
|
|
|
|
// only base64 data URI support by default
|
|
|
|
throw std::runtime_error("unsupported URI: " + uri);
|
|
|
|
}
|
|
|
|
|
2024-09-02 14:50:30 +02:00
|
|
|
struct GlTF {
|
|
|
|
std::optional<std::vector<Accessor>> accessors;
|
|
|
|
std::optional<std::vector<Animation>> animations;
|
|
|
|
Asset asset;
|
|
|
|
std::optional<std::vector<BufferView>> bufferViews;
|
|
|
|
std::optional<std::vector<Buffer>> buffers;
|
|
|
|
std::optional<std::vector<Camera>> cameras;
|
|
|
|
std::optional<std::vector<std::string>> extensionsRequired;
|
|
|
|
std::optional<std::vector<std::string>> extensionsUsed;
|
|
|
|
std::optional<std::vector<Image>> images;
|
|
|
|
std::optional<std::vector<Material>> materials;
|
|
|
|
std::optional<std::vector<Mesh>> meshes;
|
|
|
|
std::optional<std::vector<Node>> nodes;
|
|
|
|
std::optional<std::vector<Sampler>> samplers;
|
|
|
|
std::optional<std::size_t> scene;
|
|
|
|
std::optional<std::vector<Scene>> scenes;
|
|
|
|
std::optional<std::vector<Skin>> skins;
|
|
|
|
std::optional<std::vector<Texture>> textures;
|
2024-09-05 17:16:55 +02:00
|
|
|
|
2024-09-02 14:50:30 +02:00
|
|
|
GlTF(const Json::Value &o,
|
2024-09-05 17:16:55 +02:00
|
|
|
const UriResolver &resolveUri = uriError,
|
|
|
|
std::optional<std::string> &&glbData = std::nullopt)
|
2024-09-02 14:50:30 +02:00
|
|
|
: asset(as<Asset>(o["asset"]))
|
|
|
|
{
|
|
|
|
check(o.isObject());
|
|
|
|
if (o.isMember("accessors")) {
|
|
|
|
accessors = asVec<Accessor>(o["accessors"]);
|
|
|
|
check(accessors->size() >= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("animations")) {
|
|
|
|
animations = asVec<Animation>(o["animations"]);
|
|
|
|
check(animations->size() >= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("bufferViews")) {
|
|
|
|
bufferViews = asVec<BufferView>(o["bufferViews"]);
|
|
|
|
check(bufferViews->size() >= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("buffers")) {
|
|
|
|
auto b = o["buffers"];
|
|
|
|
check(b.isArray());
|
|
|
|
std::vector<Buffer> bufs;
|
|
|
|
bufs.reserve(b.size());
|
|
|
|
for (Json::ArrayIndex i = 0; i < b.size(); ++i) {
|
2024-09-05 17:16:55 +02:00
|
|
|
bufs.emplace_back(b[i], resolveUri,
|
|
|
|
i == 0 ? std::move(glbData) : std::nullopt);
|
2024-09-02 14:50:30 +02:00
|
|
|
}
|
|
|
|
check(bufs.size() >= 1);
|
|
|
|
buffers = std::move(bufs);
|
|
|
|
}
|
|
|
|
if (o.isMember("cameras")) {
|
|
|
|
cameras = asVec<Camera>(o["cameras"]);
|
|
|
|
check(cameras->size() >= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("extensionsRequired")) {
|
|
|
|
extensionsRequired = asVec<std::string>(o["extensionsRequired"]);
|
|
|
|
check(extensionsRequired->size() >= 1);
|
|
|
|
checkDuplicateFree(*extensionsRequired);
|
|
|
|
}
|
|
|
|
if (o.isMember("extensionsUsed")) {
|
|
|
|
extensionsUsed = asVec<std::string>(o["extensionsUsed"]);
|
|
|
|
check(extensionsUsed->size() >= 1);
|
|
|
|
checkDuplicateFree(*extensionsUsed);
|
|
|
|
}
|
|
|
|
if (o.isMember("images")) {
|
|
|
|
images = asVec<Image>(o["images"]);
|
|
|
|
check(images->size() >= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("materials")) {
|
|
|
|
materials = asVec<Material>(o["materials"]);
|
|
|
|
check(materials->size() >= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("meshes")) {
|
|
|
|
meshes = asVec<Mesh>(o["meshes"]);
|
|
|
|
check(meshes->size() >= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("nodes")) {
|
|
|
|
nodes = asVec<Node>(o["nodes"]);
|
|
|
|
check(nodes->size() >= 1);
|
|
|
|
// Nodes must be a forest:
|
|
|
|
// 1. Each node should have indegree 0 or 1:
|
|
|
|
std::vector<std::size_t> indeg(nodes->size());
|
|
|
|
for (std::size_t i = 0; i < nodes->size(); ++i) {
|
|
|
|
auto children = nodes->at(i).children;
|
|
|
|
if (!children.has_value()) continue;
|
|
|
|
for (auto child : children.value()) {
|
|
|
|
++indeg.at(child);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const auto deg : indeg) {
|
|
|
|
check(deg <= 1);
|
|
|
|
}
|
|
|
|
// 2. There should be no cycles:
|
|
|
|
std::vector<bool> visited(nodes->size());
|
|
|
|
std::stack<std::size_t, std::vector<std::size_t>> toVisit;
|
|
|
|
for (std::size_t i = 0; i < nodes->size(); ++i) {
|
|
|
|
// Only start DFS in roots.
|
|
|
|
if (indeg[i] > 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
toVisit.push(i);
|
|
|
|
do {
|
|
|
|
std::size_t j = toVisit.top();
|
|
|
|
check(!visited.at(j));
|
|
|
|
visited[j] = true;
|
|
|
|
toVisit.pop();
|
|
|
|
auto children = nodes->at(j).children;
|
|
|
|
if (!children.has_value())
|
|
|
|
continue;
|
|
|
|
for (auto child : *children) {
|
|
|
|
toVisit.push(child);
|
|
|
|
}
|
|
|
|
} while (!toVisit.empty());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (o.isMember("samplers")) {
|
|
|
|
samplers = asVec<Sampler>(o["samplers"]);
|
|
|
|
check(samplers->size() >= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("scene")) {
|
|
|
|
scene = as<std::size_t>(o["scene"]);
|
|
|
|
}
|
|
|
|
if (o.isMember("scenes")) {
|
|
|
|
scenes = asVec<Scene>(o["scenes"]);
|
|
|
|
check(scenes->size() >= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("skins")) {
|
|
|
|
skins = asVec<Skin>(o["skins"]);
|
|
|
|
check(skins->size() >= 1);
|
|
|
|
}
|
|
|
|
if (o.isMember("textures")) {
|
|
|
|
textures = asVec<Texture>(o["textures"]);
|
|
|
|
check(textures->size() >= 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Validation
|
|
|
|
|
|
|
|
checkForall(bufferViews, [&](const BufferView &view) {
|
|
|
|
check(buffers.has_value());
|
|
|
|
const Buffer &buf = buffers->at(view.buffer);
|
|
|
|
// Be careful because of possible integer overflows.
|
|
|
|
check(view.byteOffset < buf.byteLength);
|
|
|
|
check(view.byteLength <= buf.byteLength);
|
|
|
|
check(view.byteOffset <= buf.byteLength - view.byteLength);
|
|
|
|
});
|
|
|
|
|
|
|
|
const auto checkAccessor = [&](const auto &accessor,
|
|
|
|
std::size_t bufferView, std::size_t byteOffset, std::size_t count) {
|
|
|
|
const BufferView &view = bufferViews->at(bufferView);
|
|
|
|
if (view.byteStride.has_value())
|
|
|
|
check(*view.byteStride % accessor.componentSize() == 0);
|
|
|
|
check(byteOffset < view.byteLength);
|
|
|
|
// Use division to avoid overflows.
|
|
|
|
const auto effective_byte_stride = view.byteStride.value_or(accessor.elementSize());
|
|
|
|
check(count <= (view.byteLength - byteOffset) / effective_byte_stride);
|
|
|
|
};
|
|
|
|
checkForall(accessors, [&](const Accessor &accessor) {
|
|
|
|
if (accessor.bufferView.has_value())
|
|
|
|
checkAccessor(accessor, *accessor.bufferView, accessor.byteOffset, accessor.count);
|
|
|
|
if (accessor.sparse.has_value()) {
|
|
|
|
const auto &indices = accessor.sparse->indices;
|
|
|
|
checkAccessor(indices, indices.bufferView, indices.byteOffset, accessor.sparse->count);
|
|
|
|
const auto &values = accessor.sparse->values;
|
|
|
|
checkAccessor(accessor, values.bufferView, values.byteOffset, accessor.sparse->count);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
checkForall(images, [&](const Image &image) {
|
|
|
|
checkIndex(bufferViews, image.bufferView);
|
|
|
|
});
|
|
|
|
|
|
|
|
checkForall(meshes, [&](const Mesh &mesh) {
|
|
|
|
for (const auto &primitive : mesh.primitives) {
|
|
|
|
checkIndex(accessors, primitive.indices);
|
|
|
|
checkIndex(materials, primitive.material);
|
|
|
|
checkIndex(accessors, primitive.attributes.normal);
|
|
|
|
checkIndex(accessors, primitive.attributes.position);
|
|
|
|
checkIndex(accessors, primitive.attributes.tangent);
|
|
|
|
checkForall(primitive.attributes.texcoord, [&](const std::size_t &i) {
|
|
|
|
checkIndex(accessors, i);
|
|
|
|
});
|
|
|
|
checkForall(primitive.attributes.color, [&](const std::size_t &i) {
|
|
|
|
checkIndex(accessors, i);
|
|
|
|
});
|
|
|
|
checkForall(primitive.attributes.joints, [&](const std::size_t &i) {
|
|
|
|
checkIndex(accessors, i);
|
|
|
|
});
|
|
|
|
checkForall(primitive.attributes.weights, [&](const std::size_t &i) {
|
|
|
|
checkIndex(accessors, i);
|
|
|
|
});
|
|
|
|
if (primitive.material.has_value()) {
|
|
|
|
const Material &material = materials->at(primitive.material.value());
|
|
|
|
if (material.emissiveTexture.has_value()) {
|
|
|
|
check(primitive.attributes.texcoord.has_value());
|
|
|
|
check(material.emissiveTexture->texCoord < primitive.attributes.texcoord->size());
|
|
|
|
}
|
|
|
|
if (material.normalTexture.has_value()) {
|
|
|
|
check(primitive.attributes.texcoord.has_value());
|
|
|
|
check(material.normalTexture->texCoord < primitive.attributes.texcoord->size());
|
|
|
|
}
|
|
|
|
if (material.occlusionTexture.has_value()) {
|
|
|
|
check(primitive.attributes.texcoord.has_value());
|
|
|
|
check(material.occlusionTexture->texCoord < primitive.attributes.texcoord->size());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
checkForall(primitive.targets, [&](const MeshPrimitive::MorphTargets &target) {
|
|
|
|
checkIndex(accessors, target.normal);
|
|
|
|
checkIndex(accessors, target.position);
|
|
|
|
checkIndex(accessors, target.tangent);
|
|
|
|
checkForall(target.texcoord, [&](const std::size_t &i) {
|
|
|
|
checkIndex(accessors, i);
|
|
|
|
});
|
|
|
|
checkForall(target.color, [&](const std::size_t &i) {
|
|
|
|
checkIndex(accessors, i);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
checkForall(nodes, [&](const Node &node) {
|
|
|
|
checkIndex(cameras, node.camera);
|
|
|
|
checkIndex(meshes, node.mesh);
|
|
|
|
checkIndex(skins, node.skin);
|
|
|
|
});
|
|
|
|
|
|
|
|
checkForall(scenes, [&](const Scene &scene) {
|
|
|
|
checkForall(scene.nodes, [&](const size_t &i) {
|
|
|
|
checkIndex(nodes, i);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
checkForall(skins, [&](const Skin &skin) {
|
|
|
|
checkIndex(accessors, skin.inverseBindMatrices);
|
|
|
|
for (const std::size_t &i : skin.joints)
|
|
|
|
checkIndex(nodes, i);
|
|
|
|
checkIndex(nodes, skin.skeleton);
|
|
|
|
});
|
|
|
|
|
|
|
|
checkForall(textures, [&](const Texture &texture) {
|
|
|
|
checkIndex(samplers, texture.sampler);
|
|
|
|
checkIndex(images, texture.source);
|
|
|
|
});
|
|
|
|
|
|
|
|
checkForall(animations, [&](const Animation &animation) {
|
|
|
|
for (const auto &sampler : animation.samplers) {
|
|
|
|
checkIndex(accessors, sampler.input);
|
|
|
|
const auto &accessor = accessors->at(sampler.input);
|
|
|
|
check(accessor.type == Accessor::Type::SCALAR);
|
|
|
|
check(accessor.componentType == Accessor::ComponentType::FLOAT);
|
|
|
|
checkIndex(accessors, sampler.output);
|
|
|
|
}
|
|
|
|
for (const auto &channel : animation.channels) {
|
|
|
|
checkIndex(nodes, channel.target.node);
|
|
|
|
checkIndex(animation.samplers, channel.sampler);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
checkIndex(scenes, scene);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2024-09-05 17:16:55 +02:00
|
|
|
// std::span is C++ 20, so we roll our own little struct here.
|
|
|
|
template <typename T>
|
|
|
|
struct Span {
|
|
|
|
T *ptr;
|
|
|
|
uint32_t len;
|
|
|
|
bool empty() const {
|
|
|
|
return len == 0;
|
|
|
|
}
|
|
|
|
T *end() const {
|
|
|
|
return ptr + len;
|
|
|
|
}
|
|
|
|
template <typename U>
|
|
|
|
Span<U> cast() const {
|
|
|
|
return {(U *) ptr, len};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
static Json::Value readJson(Span<const char> span) {
|
|
|
|
Json::CharReaderBuilder builder;
|
|
|
|
const std::unique_ptr<Json::CharReader> reader(builder.newCharReader());
|
|
|
|
Json::Value json;
|
2024-11-03 15:10:39 +01:00
|
|
|
Json::String err;
|
2024-09-05 17:16:55 +02:00
|
|
|
if (!reader->parse(span.ptr, span.end(), &json, &err))
|
|
|
|
throw std::runtime_error(std::string("invalid JSON: ") + err);
|
|
|
|
return json;
|
|
|
|
}
|
|
|
|
|
|
|
|
inline GlTF readGlb(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) {
|
|
|
|
struct Chunk {
|
|
|
|
uint32_t type;
|
|
|
|
Span<const uint8_t> span;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct Stream {
|
|
|
|
Span<const uint8_t> span;
|
|
|
|
|
|
|
|
bool eof() const {
|
|
|
|
return span.empty();
|
|
|
|
}
|
|
|
|
|
|
|
|
void advance(uint32_t n) {
|
|
|
|
span.len -= n;
|
|
|
|
span.ptr += n;
|
|
|
|
}
|
|
|
|
|
|
|
|
uint32_t readUint32() {
|
|
|
|
if (span.len < 4)
|
|
|
|
throw std::runtime_error("premature EOF");
|
|
|
|
uint32_t res = 0;
|
|
|
|
for (int i = 0; i < 4; ++i)
|
|
|
|
res += span.ptr[i] << (i * 8);
|
|
|
|
advance(4);
|
|
|
|
return res;
|
|
|
|
}
|
|
|
|
|
|
|
|
Chunk readChunk() {
|
|
|
|
const auto chunkLen = readUint32();
|
|
|
|
if (chunkLen % 4 != 0)
|
|
|
|
throw std::runtime_error("chunk length must be multiple of 4");
|
|
|
|
const auto chunkType = readUint32();
|
|
|
|
|
|
|
|
auto chunkPtr = span.ptr;
|
|
|
|
if (span.len < chunkLen)
|
|
|
|
throw std::runtime_error("premature EOF");
|
|
|
|
advance(chunkLen);
|
|
|
|
return {chunkType, {chunkPtr, chunkLen}};
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
constexpr uint32_t MAGIC_GLTF = 0x46546C67;
|
|
|
|
constexpr uint32_t MAGIC_JSON = 0x4E4F534A;
|
|
|
|
constexpr uint32_t MAGIC_BIN = 0x004E4942;
|
|
|
|
|
|
|
|
if (len > std::numeric_limits<uint32_t>::max())
|
|
|
|
throw std::runtime_error("too large");
|
|
|
|
|
|
|
|
Stream is{{(const uint8_t *) data, static_cast<uint32_t>(len)}};
|
|
|
|
|
|
|
|
const auto magic = is.readUint32();
|
|
|
|
if (magic != MAGIC_GLTF)
|
|
|
|
throw std::runtime_error("wrong magic number");
|
|
|
|
const auto version = is.readUint32();
|
|
|
|
if (version != 2)
|
|
|
|
throw std::runtime_error("wrong version");
|
|
|
|
const auto length = is.readUint32();
|
|
|
|
if (length != len)
|
|
|
|
throw std::runtime_error("wrong length");
|
|
|
|
|
|
|
|
const auto json = is.readChunk();
|
|
|
|
if (json.type != MAGIC_JSON)
|
|
|
|
throw std::runtime_error("expected JSON chunk");
|
|
|
|
|
|
|
|
std::optional<std::string> buffer;
|
|
|
|
if (!is.eof()) {
|
|
|
|
const auto chunk = is.readChunk();
|
|
|
|
if (chunk.type == MAGIC_BIN)
|
|
|
|
buffer = std::string((const char *) chunk.span.ptr, chunk.span.len);
|
|
|
|
else if (chunk.type == MAGIC_JSON)
|
|
|
|
throw std::runtime_error("unexpected chunk");
|
|
|
|
// Ignore all other chunks. We still want to validate that
|
|
|
|
// 1. These chunks are valid;
|
|
|
|
// 2. These chunks are *not* JSON or BIN chunks
|
|
|
|
while (!is.eof()) {
|
|
|
|
const auto type = is.readChunk().type;
|
|
|
|
if (type == MAGIC_JSON || type == MAGIC_BIN)
|
|
|
|
throw std::runtime_error("unexpected chunk");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return GlTF(readJson(json.span.cast<const char>()), resolveUri, std::move(buffer));
|
|
|
|
}
|
|
|
|
|
|
|
|
inline GlTF readGlTF(const char *data, std::size_t len, const UriResolver &resolveUri = uriError) {
|
|
|
|
if (len > std::numeric_limits<uint32_t>::max())
|
|
|
|
throw std::runtime_error("too large");
|
|
|
|
|
|
|
|
return GlTF(readJson({data, static_cast<uint32_t>(len)}), resolveUri);
|
|
|
|
}
|
|
|
|
|
2024-09-02 14:50:30 +02:00
|
|
|
}
|