1884 lines
74 KiB
C++
1884 lines
74 KiB
C++
// glTF 2.0 -> .asp / .prs / .dds (gltf.h). Port of tools/ds2mesh.py's import path: Gltf (reader), gltf_geometry,
|
|
// gltf_skeleton, _sample, gltf_animation_prs, build_static_asp, write_asp / write_prs (only the chunks a 5.0 file
|
|
// written by the importer has), material_image. Function names below follow the Python ones.
|
|
//
|
|
// Arithmetic: everything is double, in the same order as the Python (sums start from +0.0 like Python's sum(),
|
|
// matrix products accumulate k = 0..3), and rounds to float32 only where struct.pack('<f') does. Python's
|
|
// round(x, n) is reproduced exactly (decimal rounding, half to even). That is what makes the outputs byte-identical
|
|
// (tools/gltf_cpp_check.py).
|
|
//
|
|
// Robustness: every read is bounds-checked; any problem throws Error, which ConvertMesh/ConvertAnimation turn into a
|
|
// false return with a message. Inputs are limited in size (a glTF that needs more than kMaxBytes is refused).
|
|
#include "gltf.h"
|
|
|
|
#ifndef WIN32_LEAN_AND_MEAN
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#endif
|
|
#ifndef NOMINMAX
|
|
#define NOMINMAX
|
|
#endif
|
|
#include <windows.h>
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <charconv>
|
|
#include <cstdarg>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <set>
|
|
#include <stdexcept>
|
|
#include <tuple>
|
|
|
|
namespace gltf {
|
|
namespace {
|
|
|
|
struct Error : std::runtime_error {
|
|
explicit Error(const std::string& s) : std::runtime_error(s) {}
|
|
};
|
|
|
|
const size_t kMaxBytes = 256u << 20; // biggest glTF / buffer / image file accepted (32-bit game process)
|
|
const size_t kMaxItems = 4u << 20; // biggest accessor (elements) and most corners per material group
|
|
|
|
std::string Fmt(const char* f, ...) {
|
|
char buf[512];
|
|
va_list a;
|
|
va_start(a, f);
|
|
vsnprintf(buf, sizeof(buf), f, a);
|
|
va_end(a);
|
|
return buf;
|
|
}
|
|
|
|
// --- files -----------------------------------------------------------------------------------------------------------
|
|
Bytes ReadFileW(const std::wstring& path) {
|
|
HANDLE h = CreateFileW(path.c_str(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr);
|
|
if (h == INVALID_HANDLE_VALUE) throw Error(Fmt("cannot open file (error %lu)", GetLastError()));
|
|
LARGE_INTEGER size{};
|
|
GetFileSizeEx(h, &size);
|
|
if (size.QuadPart > static_cast<LONGLONG>(kMaxBytes)) {
|
|
CloseHandle(h);
|
|
throw Error("file too large");
|
|
}
|
|
Bytes out(static_cast<size_t>(size.QuadPart));
|
|
DWORD got = 0;
|
|
bool ok = out.empty() || (ReadFile(h, out.data(), static_cast<DWORD>(out.size()), &got, nullptr) && got == out.size());
|
|
CloseHandle(h);
|
|
if (!ok) throw Error("read error");
|
|
return out;
|
|
}
|
|
|
|
std::wstring WidenU8(const std::string& s) {
|
|
if (s.empty()) return std::wstring();
|
|
int n = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), nullptr, 0);
|
|
if (n <= 0) return std::wstring();
|
|
std::wstring w(n, L'\0');
|
|
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), static_cast<int>(s.size()), &w[0], n);
|
|
return w;
|
|
}
|
|
|
|
// UTF-8 -> Latin-1 (the .asp/.prs text fields are Latin-1; ds2mesh encodes with 'latin1'); other characters -> '_'.
|
|
std::string Latin1(const std::string& s) {
|
|
std::wstring w = WidenU8(s);
|
|
std::string out;
|
|
for (wchar_t c : w) out += c < 256 ? static_cast<char>(c) : '_';
|
|
return out;
|
|
}
|
|
|
|
// --- Python numbers ----------------------------------------------------------------------------------------------------
|
|
// ds2mesh computes with Python ints and floats. The difference matters in two places: sum() (CPython 3.12+ adds floats
|
|
// with Neumaier compensation but ints exactly, see PySum) and the sign of zero (ints have none). N carries a value
|
|
// and whether Python would hold it as an int (JSON integers, integer accessors, literals like the identity matrix);
|
|
// the ints met here are small, so double arithmetic on them is exact.
|
|
struct N {
|
|
double v = 0;
|
|
bool i = false;
|
|
};
|
|
inline N Fl(double v) { return {v, false}; }
|
|
inline N In(double v) { return {v == 0 ? 0.0 : v, true}; }
|
|
inline N operator+(N a, N b) { return a.i && b.i ? In(a.v + b.v) : Fl(a.v + b.v); }
|
|
inline N operator-(N a, N b) { return a.i && b.i ? In(a.v - b.v) : Fl(a.v - b.v); }
|
|
inline N operator*(N a, N b) { return a.i && b.i ? In(a.v * b.v) : Fl(a.v * b.v); }
|
|
inline N operator/(N a, N b) { return Fl(a.v / b.v); } // Python's true division
|
|
inline N operator-(N a) { return a.i ? In(-a.v) : Fl(-a.v); }
|
|
|
|
// CPython 3.12+ builtin sum() with start 0: ints are added exactly while only ints came; the first float is added
|
|
// to that int; later floats use Neumaier's compensated summation, later ints are added without compensation; the
|
|
// compensation is added at the end if it is non-zero and finite.
|
|
N PySum(const N* it, size_t n) {
|
|
size_t k = 0;
|
|
double ires = 0;
|
|
while (k < n && it[k].i) ires += it[k++].v;
|
|
if (k == n) return In(ires);
|
|
double f = ires + it[k++].v, c = 0.0;
|
|
for (; k < n; ++k) {
|
|
if (it[k].i) {
|
|
f += it[k].v;
|
|
continue;
|
|
}
|
|
double x = it[k].v, t = f + x;
|
|
if (std::fabs(f) >= std::fabs(x)) c += (f - t) + x;
|
|
else c += (x - t) + f;
|
|
f = t;
|
|
}
|
|
if (c != 0 && std::isfinite(c)) f += c;
|
|
return Fl(f);
|
|
}
|
|
|
|
// --- JSON ------------------------------------------------------------------------------------------------------------
|
|
struct Json {
|
|
enum Type { Null, Bool, Num, Str, Arr, Obj } type = Null;
|
|
bool b = false;
|
|
double num = 0;
|
|
bool isInt = false; // written without '.', 'e' or 'E' (Python's json makes an int of it)
|
|
std::string str;
|
|
std::vector<Json> arr;
|
|
std::vector<std::pair<std::string, Json>> obj;
|
|
|
|
const Json* get(const char* key) const { // duplicate keys: the last one wins (like Python's json)
|
|
if (type != Obj) return nullptr;
|
|
for (size_t i = obj.size(); i-- > 0;)
|
|
if (obj[i].first == key) return &obj[i].second;
|
|
return nullptr;
|
|
}
|
|
bool has(const char* key) const { return get(key) != nullptr; }
|
|
const Json& at(const char* key) const {
|
|
const Json* j = get(key);
|
|
if (!j) throw Error(std::string("missing \"") + key + "\"");
|
|
return *j;
|
|
}
|
|
const Json& at(size_t i) const {
|
|
if (type != Arr || i >= arr.size()) throw Error("index out of range");
|
|
return arr[i];
|
|
}
|
|
size_t size() const { return type == Arr ? arr.size() : 0; }
|
|
double number() const {
|
|
if (type == Num) return num;
|
|
if (type == Bool) return b ? 1.0 : 0.0;
|
|
throw Error("number expected");
|
|
}
|
|
N pynum() const {
|
|
double v = number();
|
|
return type == Bool || isInt ? In(v) : Fl(v);
|
|
}
|
|
int integer() const {
|
|
double v = number();
|
|
if (v != std::floor(v) || v < -2147483647.0 || v > 2147483647.0) throw Error("integer expected");
|
|
return static_cast<int>(v);
|
|
}
|
|
size_t index(size_t limit) const {
|
|
int v = integer();
|
|
if (v < 0 || static_cast<size_t>(v) >= limit) throw Error("index out of range");
|
|
return static_cast<size_t>(v);
|
|
}
|
|
const std::string& string() const {
|
|
if (type != Str) throw Error("string expected");
|
|
return str;
|
|
}
|
|
bool truthy() const {
|
|
switch (type) {
|
|
case Null: return false;
|
|
case Bool: return b;
|
|
case Num: return num != 0;
|
|
case Str: return !str.empty();
|
|
case Arr: return !arr.empty();
|
|
default: return !obj.empty();
|
|
}
|
|
}
|
|
};
|
|
|
|
class JsonParser {
|
|
public:
|
|
JsonParser(const char* p, const char* e) : p_(p), e_(e) {}
|
|
Json Parse() {
|
|
Json j = Value(0);
|
|
Ws();
|
|
if (p_ != e_) throw Error("JSON: trailing data");
|
|
return j;
|
|
}
|
|
|
|
private:
|
|
const char *p_, *e_;
|
|
void Ws() {
|
|
while (p_ < e_ && (*p_ == ' ' || *p_ == '\t' || *p_ == '\n' || *p_ == '\r')) ++p_;
|
|
}
|
|
bool Lit(const char* s) {
|
|
size_t n = strlen(s);
|
|
if (static_cast<size_t>(e_ - p_) >= n && !memcmp(p_, s, n)) {
|
|
p_ += n;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
static void PutU8(std::string& o, unsigned c) {
|
|
if (c < 0x80) {
|
|
o += static_cast<char>(c);
|
|
} else if (c < 0x800) {
|
|
o += static_cast<char>(0xc0 | (c >> 6));
|
|
o += static_cast<char>(0x80 | (c & 0x3f));
|
|
} else if (c < 0x10000) {
|
|
o += static_cast<char>(0xe0 | (c >> 12));
|
|
o += static_cast<char>(0x80 | ((c >> 6) & 0x3f));
|
|
o += static_cast<char>(0x80 | (c & 0x3f));
|
|
} else {
|
|
o += static_cast<char>(0xf0 | (c >> 18));
|
|
o += static_cast<char>(0x80 | ((c >> 12) & 0x3f));
|
|
o += static_cast<char>(0x80 | ((c >> 6) & 0x3f));
|
|
o += static_cast<char>(0x80 | (c & 0x3f));
|
|
}
|
|
}
|
|
unsigned Hex4() {
|
|
if (e_ - p_ < 4) throw Error("JSON: bad \\u escape");
|
|
unsigned v = 0;
|
|
for (int i = 0; i < 4; ++i) {
|
|
char c = *p_++;
|
|
v <<= 4;
|
|
if (c >= '0' && c <= '9') v |= c - '0';
|
|
else if (c >= 'a' && c <= 'f') v |= c - 'a' + 10;
|
|
else if (c >= 'A' && c <= 'F') v |= c - 'A' + 10;
|
|
else throw Error("JSON: bad \\u escape");
|
|
}
|
|
return v;
|
|
}
|
|
std::string String() {
|
|
++p_; // opening quote
|
|
std::string o;
|
|
while (true) {
|
|
if (p_ >= e_) throw Error("JSON: unterminated string");
|
|
char c = *p_++;
|
|
if (c == '"') break;
|
|
if (c != '\\') {
|
|
o += c;
|
|
continue;
|
|
}
|
|
if (p_ >= e_) throw Error("JSON: bad escape");
|
|
c = *p_++;
|
|
switch (c) {
|
|
case '"': o += '"'; break;
|
|
case '\\': o += '\\'; break;
|
|
case '/': o += '/'; break;
|
|
case 'b': o += '\b'; break;
|
|
case 'f': o += '\f'; break;
|
|
case 'n': o += '\n'; break;
|
|
case 'r': o += '\r'; break;
|
|
case 't': o += '\t'; break;
|
|
case 'u': {
|
|
unsigned u = Hex4();
|
|
if (u >= 0xd800 && u < 0xdc00 && e_ - p_ >= 6 && p_[0] == '\\' && p_[1] == 'u') {
|
|
const char* save = p_;
|
|
p_ += 2;
|
|
unsigned lo = Hex4();
|
|
if (lo >= 0xdc00 && lo < 0xe000) u = 0x10000 + ((u - 0xd800) << 10) + (lo - 0xdc00);
|
|
else p_ = save;
|
|
}
|
|
PutU8(o, u);
|
|
break;
|
|
}
|
|
default: throw Error("JSON: bad escape");
|
|
}
|
|
}
|
|
return o;
|
|
}
|
|
Json Value(int depth) {
|
|
if (depth > 200) throw Error("JSON: nested too deeply");
|
|
Ws();
|
|
if (p_ >= e_) throw Error("JSON: unexpected end");
|
|
Json j;
|
|
char c = *p_;
|
|
if (c == '{') {
|
|
++p_;
|
|
j.type = Json::Obj;
|
|
Ws();
|
|
if (p_ < e_ && *p_ == '}') {
|
|
++p_;
|
|
return j;
|
|
}
|
|
while (true) {
|
|
Ws();
|
|
if (p_ >= e_ || *p_ != '"') throw Error("JSON: key expected");
|
|
std::string k = String();
|
|
Ws();
|
|
if (p_ >= e_ || *p_ != ':') throw Error("JSON: ':' expected");
|
|
++p_;
|
|
j.obj.emplace_back(k, Value(depth + 1));
|
|
Ws();
|
|
if (p_ < e_ && *p_ == ',') {
|
|
++p_;
|
|
continue;
|
|
}
|
|
if (p_ < e_ && *p_ == '}') {
|
|
++p_;
|
|
return j;
|
|
}
|
|
throw Error("JSON: ',' or '}' expected");
|
|
}
|
|
}
|
|
if (c == '[') {
|
|
++p_;
|
|
j.type = Json::Arr;
|
|
Ws();
|
|
if (p_ < e_ && *p_ == ']') {
|
|
++p_;
|
|
return j;
|
|
}
|
|
while (true) {
|
|
j.arr.push_back(Value(depth + 1));
|
|
Ws();
|
|
if (p_ < e_ && *p_ == ',') {
|
|
++p_;
|
|
continue;
|
|
}
|
|
if (p_ < e_ && *p_ == ']') {
|
|
++p_;
|
|
return j;
|
|
}
|
|
throw Error("JSON: ',' or ']' expected");
|
|
}
|
|
}
|
|
if (c == '"') {
|
|
j.type = Json::Str;
|
|
j.str = String();
|
|
return j;
|
|
}
|
|
if (Lit("true")) {
|
|
j.type = Json::Bool;
|
|
j.b = true;
|
|
return j;
|
|
}
|
|
if (Lit("false")) {
|
|
j.type = Json::Bool;
|
|
return j;
|
|
}
|
|
if (Lit("null")) return j;
|
|
if (Lit("NaN")) {
|
|
j.type = Json::Num;
|
|
j.num = std::nan("");
|
|
return j;
|
|
}
|
|
if (Lit("Infinity")) {
|
|
j.type = Json::Num;
|
|
j.num = HUGE_VAL;
|
|
return j;
|
|
}
|
|
if (Lit("-Infinity")) {
|
|
j.type = Json::Num;
|
|
j.num = -HUGE_VAL;
|
|
return j;
|
|
}
|
|
const char* s = p_;
|
|
if (p_ < e_ && *p_ == '-') ++p_;
|
|
while (p_ < e_ && ((*p_ >= '0' && *p_ <= '9') || *p_ == '.' || *p_ == 'e' || *p_ == 'E' || *p_ == '+' || *p_ == '-')) ++p_;
|
|
if (p_ == s) throw Error("JSON: unexpected character");
|
|
j.type = Json::Num;
|
|
j.isInt = std::find_if(s, p_, [](char ch) { return ch == '.' || ch == 'e' || ch == 'E'; }) == p_;
|
|
auto r = std::from_chars(s, p_, j.num);
|
|
if (r.ec != std::errc() || r.ptr != p_) {
|
|
if (r.ec == std::errc::result_out_of_range) j.num = (*s == '-') ? -HUGE_VAL : HUGE_VAL;
|
|
else throw Error("JSON: bad number");
|
|
}
|
|
if (j.isInt && j.num == 0) j.num = 0.0; // "-0" is the int 0
|
|
return j;
|
|
}
|
|
};
|
|
|
|
// --- Python's round(x, n): correctly rounded decimal, ties to even --------------------------------------------------
|
|
double PyRound(double x, int n) {
|
|
if (!std::isfinite(x) || x == 0) return x;
|
|
char buf[512];
|
|
int len = snprintf(buf, sizeof(buf), "%.*f", n + 40, x); // exact digits (UCRT prints the exact expansion)
|
|
if (len <= 0 || len >= static_cast<int>(sizeof(buf))) return x;
|
|
std::string s(buf);
|
|
bool neg = s[0] == '-';
|
|
if (neg) s.erase(0, 1);
|
|
size_t dot = s.find('.');
|
|
std::string ip = s.substr(0, dot), fp = s.substr(dot + 1);
|
|
std::string keep = ip + fp.substr(0, n), rest = fp.substr(n);
|
|
bool up = false;
|
|
if (!rest.empty() && rest[0] > '5') {
|
|
up = true;
|
|
} else if (!rest.empty() && rest[0] == '5') {
|
|
bool more = rest.find_first_not_of('0', 1) != std::string::npos;
|
|
up = more || ((keep.back() - '0') & 1);
|
|
}
|
|
if (up) {
|
|
size_t i = keep.size();
|
|
while (i > 0) {
|
|
--i;
|
|
if (keep[i] == '9') {
|
|
keep[i] = '0';
|
|
} else {
|
|
++keep[i];
|
|
break;
|
|
}
|
|
if (i == 0) keep.insert(keep.begin(), '1');
|
|
}
|
|
}
|
|
std::string txt = keep.substr(0, keep.size() - n) + "." + keep.substr(keep.size() - n);
|
|
double v = 0;
|
|
std::from_chars(txt.data(), txt.data() + txt.size(), v);
|
|
return neg ? -v : v;
|
|
}
|
|
|
|
float F32(double v) { return static_cast<float>(v); }
|
|
|
|
// --- math (ds2mesh: 4x4 row-major for column vectors, quaternions x, y, z, w) ----------------------------------------
|
|
struct M4 {
|
|
N m[4][4];
|
|
};
|
|
|
|
M4 Q2M(const N q[4], const N t[3], const N s[3]) { // m4(q, t, s) via q_mat3
|
|
N x = q[0], y = q[1], z = q[2], w = q[3];
|
|
N n = x * x + y * y + z * z + w * w;
|
|
N k = n.v != 0 ? Fl(2.0 / n.v) : Fl(0.0);
|
|
N one = In(1);
|
|
N r[3][3] = {{one - k * (y * y + z * z), k * (x * y - z * w), k * (x * z + y * w)},
|
|
{k * (x * y + z * w), one - k * (x * x + z * z), k * (y * z - x * w)},
|
|
{k * (x * z - y * w), k * (y * z + x * w), one - k * (x * x + y * y)}};
|
|
M4 o;
|
|
for (int i = 0; i < 3; ++i) {
|
|
o.m[i][0] = r[i][0] * s[0];
|
|
o.m[i][1] = r[i][1] * s[1];
|
|
o.m[i][2] = r[i][2] * s[2];
|
|
o.m[i][3] = t[i];
|
|
}
|
|
o.m[3][0] = o.m[3][1] = o.m[3][2] = In(0);
|
|
o.m[3][3] = In(1);
|
|
return o;
|
|
}
|
|
|
|
M4 Identity() {
|
|
const N q[4] = {In(0), In(0), In(0), In(1)}, t[3] = {In(0), In(0), In(0)}, s[3] = {In(1), In(1), In(1)};
|
|
return Q2M(q, t, s);
|
|
}
|
|
|
|
M4 Mul(const M4& a, const M4& b) {
|
|
M4 o;
|
|
for (int i = 0; i < 4; ++i)
|
|
for (int j = 0; j < 4; ++j) {
|
|
N p[4];
|
|
for (int k = 0; k < 4; ++k) p[k] = a.m[i][k] * b.m[k][j];
|
|
o.m[i][j] = PySum(p, 4);
|
|
}
|
|
return o;
|
|
}
|
|
|
|
M4 Inv(const M4& m) { // Gauss-Jordan with partial pivoting, as m4_inv
|
|
N a[4][8];
|
|
for (int i = 0; i < 4; ++i)
|
|
for (int j = 0; j < 8; ++j) a[i][j] = j < 4 ? m.m[i][j] : Fl(i == j - 4 ? 1.0 : 0.0);
|
|
for (int c = 0; c < 4; ++c) {
|
|
int p = c;
|
|
for (int r = c + 1; r < 4; ++r)
|
|
if (std::fabs(a[r][c].v) > std::fabs(a[p][c].v)) p = r;
|
|
for (int j = 0; j < 8; ++j) std::swap(a[c][j], a[p][j]);
|
|
N d = a[c][c];
|
|
if (std::fabs(d.v) < 1e-12) throw Error("singular matrix");
|
|
for (int j = 0; j < 8; ++j) a[c][j] = a[c][j] / d;
|
|
for (int r = 0; r < 4; ++r) {
|
|
if (r == c || a[r][c].v == 0) continue;
|
|
N f = a[r][c];
|
|
for (int j = 0; j < 8; ++j) a[r][j] = a[r][j] - f * a[c][j];
|
|
}
|
|
}
|
|
M4 o;
|
|
for (int i = 0; i < 4; ++i)
|
|
for (int j = 0; j < 4; ++j) o.m[i][j] = a[i][j + 4];
|
|
return o;
|
|
}
|
|
|
|
void Point(const M4& m, const N p[3], N o[3]) {
|
|
for (int i = 0; i < 3; ++i) o[i] = m.m[i][0] * p[0] + m.m[i][1] * p[1] + m.m[i][2] * p[2] + m.m[i][3];
|
|
}
|
|
|
|
void Dir(const M4& m, const N v[3], N o[3]) {
|
|
for (int i = 0; i < 3; ++i) o[i] = m.m[i][0] * v[0] + m.m[i][1] * v[1] + m.m[i][2] * v[2];
|
|
}
|
|
|
|
double Det3(const M4& a) { // only its sign is used
|
|
const N(*m)[4] = a.m;
|
|
return ((m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) +
|
|
m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]))).v;
|
|
}
|
|
|
|
template <int K>
|
|
void Normalize(const N v[K], N o[K]) {
|
|
N sq[K];
|
|
for (int i = 0; i < K; ++i) sq[i] = v[i] * v[i];
|
|
double n = std::sqrt(PySum(sq, K).v);
|
|
if (n > 1e-20) {
|
|
for (int i = 0; i < K; ++i) o[i] = v[i] / Fl(n);
|
|
} else { // ds2mesh returns (0, 1, 0) here
|
|
for (int i = 0; i < K; ++i) o[i] = Fl(0.0);
|
|
if (K > 1) o[1] = Fl(1.0);
|
|
}
|
|
}
|
|
|
|
// m4_to_qt: rotation quaternion, translation and scale of an affine matrix.
|
|
void ToQt(const M4& a, N q[4], N t[3], double sc[3]) {
|
|
const N(*m)[4] = a.m;
|
|
double s[3];
|
|
for (int c = 0; c < 3; ++c) {
|
|
N sq[3];
|
|
for (int r = 0; r < 3; ++r) sq[r] = m[r][c] * m[r][c];
|
|
s[c] = std::sqrt(PySum(sq, 3).v);
|
|
}
|
|
if (Det3(a) < 0) s[0] = -s[0];
|
|
double r[3][3];
|
|
for (int i = 0; i < 3; ++i)
|
|
for (int j = 0; j < 3; ++j) r[i][j] = m[i][j].v / s[j];
|
|
double tr = r[0][0] + r[1][1] + r[2][2];
|
|
N qq[4];
|
|
if (tr > 0) {
|
|
double k = std::sqrt(tr + 1.0) * 2;
|
|
qq[0] = Fl((r[2][1] - r[1][2]) / k);
|
|
qq[1] = Fl((r[0][2] - r[2][0]) / k);
|
|
qq[2] = Fl((r[1][0] - r[0][1]) / k);
|
|
qq[3] = Fl(0.25 * k);
|
|
} else if (r[0][0] > r[1][1] && r[0][0] > r[2][2]) {
|
|
double k = std::sqrt(1.0 + r[0][0] - r[1][1] - r[2][2]) * 2;
|
|
qq[0] = Fl(0.25 * k);
|
|
qq[1] = Fl((r[0][1] + r[1][0]) / k);
|
|
qq[2] = Fl((r[0][2] + r[2][0]) / k);
|
|
qq[3] = Fl((r[2][1] - r[1][2]) / k);
|
|
} else if (r[1][1] > r[2][2]) {
|
|
double k = std::sqrt(1.0 + r[1][1] - r[0][0] - r[2][2]) * 2;
|
|
qq[0] = Fl((r[0][1] + r[1][0]) / k);
|
|
qq[1] = Fl(0.25 * k);
|
|
qq[2] = Fl((r[1][2] + r[2][1]) / k);
|
|
qq[3] = Fl((r[0][2] - r[2][0]) / k);
|
|
} else {
|
|
double k = std::sqrt(1.0 + r[2][2] - r[0][0] - r[1][1]) * 2;
|
|
qq[0] = Fl((r[0][2] + r[2][0]) / k);
|
|
qq[1] = Fl((r[1][2] + r[2][1]) / k);
|
|
qq[2] = Fl(0.25 * k);
|
|
qq[3] = Fl((r[1][0] - r[0][1]) / k);
|
|
}
|
|
Normalize<4>(qq, q);
|
|
t[0] = m[0][3];
|
|
t[1] = m[1][3];
|
|
t[2] = m[2][3];
|
|
sc[0] = s[0];
|
|
sc[1] = s[1];
|
|
sc[2] = s[2];
|
|
}
|
|
|
|
// --- base64 / URIs ---------------------------------------------------------------------------------------------------
|
|
Bytes Base64(const std::string& s) { // like Python's base64.b64decode: characters outside the alphabet are skipped
|
|
Bytes out;
|
|
unsigned acc = 0;
|
|
int bits = 0;
|
|
for (char c : s) {
|
|
int v;
|
|
if (c >= 'A' && c <= 'Z') v = c - 'A';
|
|
else if (c >= 'a' && c <= 'z') v = c - 'a' + 26;
|
|
else if (c >= '0' && c <= '9') v = c - '0' + 52;
|
|
else if (c == '+') v = 62;
|
|
else if (c == '/') v = 63;
|
|
else if (c == '=') break;
|
|
else continue;
|
|
acc = (acc << 6) | static_cast<unsigned>(v);
|
|
bits += 6;
|
|
if (bits >= 8) {
|
|
bits -= 8;
|
|
out.push_back(static_cast<uint8_t>(acc >> bits));
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
std::string Unquote(const std::string& s) {
|
|
std::string o;
|
|
for (size_t i = 0; i < s.size(); ++i) {
|
|
auto hex = [](char c) { return c >= '0' && c <= '9' ? c - '0' : c >= 'a' && c <= 'f' ? c - 'a' + 10 : c >= 'A' && c <= 'F' ? c - 'A' + 10 : -1; };
|
|
if (s[i] == '%' && i + 2 < s.size() && hex(s[i + 1]) >= 0 && hex(s[i + 2]) >= 0) {
|
|
o += static_cast<char>(hex(s[i + 1]) * 16 + hex(s[i + 2]));
|
|
i += 2;
|
|
} else {
|
|
o += s[i];
|
|
}
|
|
}
|
|
return o;
|
|
}
|
|
|
|
// --- the glTF document (class Gltf) ----------------------------------------------------------------------------------
|
|
using Row = std::vector<N>;
|
|
|
|
// An accessor's elements (class Gltf.accessor): count x n numbers, ints for integer component types that are not
|
|
// normalized (as Python's struct.unpack gives them), floats otherwise.
|
|
struct Acc {
|
|
size_t count = 0, n = 0;
|
|
std::vector<double> v;
|
|
bool ints = false;
|
|
size_t size() const { return count; }
|
|
N at(size_t k, size_t c) const { return ints ? In(v[k * n + c]) : Fl(v[k * n + c]); }
|
|
Row row(size_t k) const {
|
|
Row r(n);
|
|
for (size_t c = 0; c < n; ++c) r[c] = at(k, c);
|
|
return r;
|
|
}
|
|
};
|
|
|
|
struct Doc {
|
|
Json doc;
|
|
std::wstring base;
|
|
std::vector<std::shared_ptr<Bytes>> buffers; // null: a GLB buffer without a BIN chunk
|
|
std::vector<std::wstring>* deps = nullptr;
|
|
|
|
void Load(const std::wstring& path) {
|
|
Bytes data = ReadFileW(path);
|
|
size_t slash = path.find_last_of(L"\\/");
|
|
base = slash == std::wstring::npos ? L"." : path.substr(0, slash);
|
|
std::shared_ptr<Bytes> glbBin;
|
|
if (data.size() >= 4 && !memcmp(data.data(), "glTF", 4)) {
|
|
size_t p = 12;
|
|
bool json = false;
|
|
while (p < data.size()) {
|
|
if (p + 8 > data.size()) throw Error("GLB: truncated chunk header");
|
|
uint32_t n;
|
|
memcpy(&n, &data[p], 4);
|
|
const uint8_t* kind = &data[p + 4];
|
|
size_t end = std::min(data.size(), p + 8 + static_cast<size_t>(n));
|
|
if (!memcmp(kind, "JSON", 4)) {
|
|
doc = JsonParser(reinterpret_cast<const char*>(&data[p + 8]), reinterpret_cast<const char*>(data.data() + end)).Parse();
|
|
json = true;
|
|
} else if (!memcmp(kind, "BIN\0", 4)) {
|
|
glbBin = std::make_shared<Bytes>(data.begin() + p + 8, data.begin() + end);
|
|
}
|
|
p += 8 + static_cast<size_t>(n);
|
|
}
|
|
if (!json) throw Error("GLB: no JSON chunk");
|
|
} else {
|
|
size_t skip = data.size() >= 3 && !memcmp(data.data(), "\xEF\xBB\xBF", 3) ? 3 : 0;
|
|
const char* s = reinterpret_cast<const char*>(data.data());
|
|
doc = JsonParser(s + skip, s + data.size()).Parse();
|
|
}
|
|
if (doc.type != Json::Obj) throw Error("glTF: the JSON is not an object");
|
|
if (const Json* need = doc.get("extensionsRequired")) {
|
|
if (need->size()) {
|
|
std::string list;
|
|
for (const Json& e : need->arr) list += (list.empty() ? "" : ", ") + (e.type == Json::Str ? e.str : "?");
|
|
throw Error("glTF requires unsupported extensions: " + list);
|
|
}
|
|
}
|
|
if (const Json* bufs = doc.get("buffers")) {
|
|
for (const Json& b : bufs->arr) {
|
|
const Json* uri = b.get("uri");
|
|
if (!uri) {
|
|
buffers.push_back(glbBin);
|
|
} else if (uri->string().compare(0, 5, "data:") == 0) {
|
|
size_t comma = uri->str.find(',');
|
|
if (comma == std::string::npos) throw Error("bad data URI");
|
|
buffers.push_back(std::make_shared<Bytes>(Base64(uri->str.substr(comma + 1))));
|
|
} else {
|
|
buffers.push_back(std::make_shared<Bytes>(ReadExternal(uri->str)));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Bytes ReadExternal(const std::string& uri) {
|
|
std::wstring rel = WidenU8(Unquote(uri));
|
|
for (auto& c : rel)
|
|
if (c == L'/') c = L'\\';
|
|
std::wstring p = (rel.size() > 1 && (rel[1] == L':' || rel[0] == L'\\')) ? rel : base + L"\\" + rel;
|
|
if (deps) deps->push_back(p);
|
|
try {
|
|
return ReadFileW(p);
|
|
} catch (const Error& e) {
|
|
throw Error("external file \"" + uri + "\": " + e.what());
|
|
}
|
|
}
|
|
|
|
const Json& Arr(const char* key) const {
|
|
static const Json empty = [] {
|
|
Json j;
|
|
j.type = Json::Arr;
|
|
return j;
|
|
}();
|
|
const Json* j = doc.get(key);
|
|
return j ? *j : empty;
|
|
}
|
|
|
|
// bytes of a buffer view: (pointer, length, stride or 0)
|
|
void View(size_t i, const uint8_t*& p, size_t& n, size_t& stride) const {
|
|
const Json& v = Arr("bufferViews").at(i);
|
|
size_t bi = v.at("buffer").index(buffers.size());
|
|
const std::shared_ptr<Bytes>& b = buffers[bi];
|
|
if (!b) throw Error("buffer without data (GLB without a BIN chunk)");
|
|
double o = v.has("byteOffset") ? v.at("byteOffset").number() : 0, len = v.at("byteLength").number();
|
|
if (o < 0 || len < 0) throw Error("bad buffer view");
|
|
size_t oo = static_cast<size_t>(std::min(o, static_cast<double>(b->size())));
|
|
size_t end = static_cast<size_t>(std::min(o + len, static_cast<double>(b->size()))); // Python slices clamp
|
|
p = b->data() + oo;
|
|
n = end > oo ? end - oo : 0;
|
|
stride = v.has("byteStride") && v.at("byteStride").truthy() ? static_cast<size_t>(v.at("byteStride").integer()) : 0;
|
|
}
|
|
|
|
mutable std::map<size_t, std::shared_ptr<Acc>> cache;
|
|
const Acc& Cached(size_t i) const { // animation samplers read the same accessors many times
|
|
auto it = cache.find(i);
|
|
if (it == cache.end()) it = cache.emplace(i, std::make_shared<Acc>(Accessor(i))).first;
|
|
return *it->second;
|
|
}
|
|
|
|
Acc Accessor(size_t i) const {
|
|
const Json& a = Arr("accessors").at(i);
|
|
if (a.has("sparse")) throw Error("sparse accessors are not supported");
|
|
int ct = a.at("componentType").integer();
|
|
size_t size;
|
|
double norm = 0;
|
|
switch (ct) {
|
|
case 5120: size = 1; norm = 127.0; break;
|
|
case 5121: size = 1; norm = 255.0; break;
|
|
case 5122: size = 2; norm = 32767.0; break;
|
|
case 5123: size = 2; norm = 65535.0; break;
|
|
case 5125: size = 4; break;
|
|
case 5126: size = 4; break;
|
|
default: throw Error(Fmt("unknown component type %d", ct));
|
|
}
|
|
const std::string& type = a.at("type").string();
|
|
size_t n = type == "SCALAR" ? 1 : type == "VEC2" ? 2 : type == "VEC3" ? 3 : type == "VEC4" ? 4 : type == "MAT2" ? 4
|
|
: type == "MAT3" ? 9 : type == "MAT4" ? 16 : 0;
|
|
if (!n) throw Error("unknown accessor type " + type);
|
|
double cnt = a.at("count").number();
|
|
if (cnt < 0 || cnt > kMaxItems) throw Error("accessor too large");
|
|
size_t count = static_cast<size_t>(cnt);
|
|
Acc out;
|
|
out.count = count;
|
|
out.n = n;
|
|
if (!a.has("bufferView")) { // ds2mesh: floats 0.0
|
|
out.v.assign(count * n, 0.0);
|
|
return out;
|
|
}
|
|
const uint8_t* raw;
|
|
size_t len, stride;
|
|
View(a.at("bufferView").index(Arr("bufferViews").size()), raw, len, stride);
|
|
double off = a.has("byteOffset") ? a.at("byteOffset").number() : 0;
|
|
if (off < 0) throw Error("bad accessor offset");
|
|
if (!stride) stride = size * n;
|
|
bool normalized = a.has("normalized") && a.at("normalized").truthy() && norm != 0;
|
|
out.ints = ct != 5126 && !normalized;
|
|
out.v.reserve(count * n);
|
|
for (size_t k = 0; k < count; ++k) {
|
|
double at = off + static_cast<double>(k) * stride;
|
|
if (at + size * n > len) throw Error("accessor reads past its buffer view");
|
|
const uint8_t* q = raw + static_cast<size_t>(at);
|
|
for (size_t c = 0; c < n; ++c, q += size) {
|
|
double v;
|
|
switch (ct) {
|
|
case 5120: v = static_cast<int8_t>(*q); break;
|
|
case 5121: v = *q; break;
|
|
case 5122: { int16_t x; memcpy(&x, q, 2); v = x; break; }
|
|
case 5123: { uint16_t x; memcpy(&x, q, 2); v = x; break; }
|
|
case 5125: { uint32_t x; memcpy(&x, q, 4); v = x; break; }
|
|
default: { float x; memcpy(&x, q, 4); v = x; break; }
|
|
}
|
|
if (normalized) v = std::max(-1.0, v / norm);
|
|
out.v.push_back(v);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
Bytes ImageBytes(size_t i) {
|
|
const Json& im = Arr("images").at(i);
|
|
if (im.has("bufferView")) {
|
|
const uint8_t* p;
|
|
size_t n, stride;
|
|
View(im.at("bufferView").index(Arr("bufferViews").size()), p, n, stride);
|
|
return Bytes(p, p + n);
|
|
}
|
|
const std::string& uri = im.at("uri").string();
|
|
if (uri.compare(0, 5, "data:") == 0) {
|
|
size_t comma = uri.find(',');
|
|
if (comma == std::string::npos) throw Error("bad data URI");
|
|
return Base64(uri.substr(comma + 1));
|
|
}
|
|
return ReadExternal(uri);
|
|
}
|
|
|
|
M4 NodeMatrix(const Json& nd) const {
|
|
if (const Json* mj = nd.get("matrix")) {
|
|
if (mj->size() < 16) throw Error("node matrix needs 16 numbers");
|
|
M4 o;
|
|
for (int r = 0; r < 4; ++r)
|
|
for (int k = 0; k < 4; ++k) o.m[r][k] = mj->at(r + 4 * k).pynum();
|
|
return o;
|
|
}
|
|
N q[4] = {In(0), In(0), In(0), In(1)}, t[3] = {In(0), In(0), In(0)}, s[3] = {In(1), In(1), In(1)};
|
|
Vec(nd, "rotation", q, 4);
|
|
Vec(nd, "translation", t, 3);
|
|
Vec(nd, "scale", s, 3);
|
|
return Q2M(q, t, s);
|
|
}
|
|
|
|
static void Vec(const Json& nd, const char* key, N* v, size_t n) {
|
|
if (const Json* j = nd.get(key)) {
|
|
if (j->size() < n) throw Error(std::string("node ") + key + ": too few numbers");
|
|
for (size_t i = 0; i < n; ++i) v[i] = j->at(i).pynum();
|
|
}
|
|
}
|
|
|
|
std::map<size_t, size_t> Parents() const { // child -> parent (the last parent listed wins)
|
|
std::map<size_t, size_t> out;
|
|
const Json& nodes = Arr("nodes");
|
|
for (size_t i = 0; i < nodes.size(); ++i)
|
|
if (const Json* ch = nodes.arr[i].get("children"))
|
|
for (const Json& c : ch->arr) out[c.index(nodes.size())] = i;
|
|
return out;
|
|
}
|
|
|
|
struct Instance {
|
|
size_t mesh;
|
|
M4 world;
|
|
long node; // -1: none
|
|
};
|
|
|
|
std::vector<Instance> MeshInstances() const {
|
|
const Json& nodes = Arr("nodes");
|
|
std::vector<size_t> roots;
|
|
const Json& scenes = Arr("scenes");
|
|
if (scenes.size()) {
|
|
size_t si = doc.has("scene") ? doc.at("scene").index(scenes.size()) : 0;
|
|
if (si >= scenes.size()) throw Error("bad scene index");
|
|
if (const Json* ns = scenes.arr[si].get("nodes"))
|
|
for (const Json& r : ns->arr) roots.push_back(r.index(nodes.size()));
|
|
} else {
|
|
std::set<size_t> kids;
|
|
for (const Json& nd : nodes.arr)
|
|
if (const Json* ch = nd.get("children"))
|
|
for (const Json& c : ch->arr) kids.insert(c.index(nodes.size()));
|
|
for (size_t i = 0; i < nodes.size(); ++i)
|
|
if (!kids.count(i)) roots.push_back(i);
|
|
}
|
|
std::vector<Instance> out;
|
|
size_t visits = 0;
|
|
struct Walker {
|
|
const Doc& d;
|
|
const Json& nodes;
|
|
std::vector<Instance>& out;
|
|
size_t& visits;
|
|
void Walk(size_t i, const M4& parent, int depth) {
|
|
if (depth > 256 || ++visits > 1000000) throw Error("node hierarchy too deep (cycle?)");
|
|
const Json& nd = nodes.at(i);
|
|
M4 mat = Mul(parent, d.NodeMatrix(nd));
|
|
if (const Json* m = nd.get("mesh")) out.push_back({m->index(d.Arr("meshes").size()), mat, static_cast<long>(i)});
|
|
if (const Json* ch = nd.get("children"))
|
|
for (const Json& c : ch->arr) Walk(c.index(nodes.size()), mat, depth + 1);
|
|
}
|
|
} w{*this, nodes, out, visits};
|
|
for (size_t r : roots) w.Walk(r, Identity(), 0);
|
|
if (out.empty() && Arr("meshes").size())
|
|
for (size_t i = 0; i < Arr("meshes").size(); ++i) out.push_back({i, Identity(), -1});
|
|
return out;
|
|
}
|
|
};
|
|
|
|
// --- skeleton (gltf_skeleton) ------------------------------------------------------------------------------------------
|
|
struct Bone {
|
|
std::string name;
|
|
int parent = -1;
|
|
long node = -1; // -1: the added root bone
|
|
M4 abs, local;
|
|
N q[4], t[3];
|
|
};
|
|
|
|
struct Skeleton {
|
|
std::vector<Bone> bones;
|
|
size_t skin = 0;
|
|
std::vector<int> skinBone; // DS2 bone of each joint of the skin
|
|
std::map<size_t, int> nodeBone; // glTF node -> DS2 bone
|
|
};
|
|
|
|
bool BuildSkeleton(const Doc& g, double scale, Skeleton& sk, std::vector<std::string>& warnings) {
|
|
const Json& skins = g.Arr("skins");
|
|
if (!skins.size()) return false;
|
|
const Json& nodes = g.Arr("nodes");
|
|
size_t skin = 0;
|
|
for (const Json& nd : nodes.arr)
|
|
if (nd.has("mesh") && nd.has("skin")) {
|
|
skin = nd.at("skin").index(skins.size());
|
|
break;
|
|
}
|
|
const Json& s = skins.at(skin);
|
|
std::vector<size_t> joints;
|
|
for (const Json& j : s.at("joints").arr) joints.push_back(j.index(nodes.size()));
|
|
std::vector<Row> ibm;
|
|
if (s.has("inverseBindMatrices")) {
|
|
Acc acc = g.Accessor(s.at("inverseBindMatrices").index(g.Arr("accessors").size()));
|
|
for (size_t k = 0; k < acc.size(); ++k) ibm.push_back(acc.row(k));
|
|
} else { // m4_colmajor(IDENTITY)
|
|
M4 id = Identity();
|
|
Row col;
|
|
for (int c = 0; c < 4; ++c)
|
|
for (int r = 0; r < 4; ++r) col.push_back(id.m[r][c]);
|
|
ibm.assign(joints.size(), col);
|
|
}
|
|
auto parents = g.Parents();
|
|
std::set<size_t> jset(joints.begin(), joints.end());
|
|
std::map<size_t, long> jparent;
|
|
for (size_t j : joints) {
|
|
auto it = parents.find(j);
|
|
long k = it == parents.end() ? -1 : static_cast<long>(it->second);
|
|
int guard = 0;
|
|
while (k >= 0 && !jset.count(static_cast<size_t>(k))) {
|
|
if (++guard > 100000) throw Error("node hierarchy has a cycle");
|
|
auto p = parents.find(static_cast<size_t>(k));
|
|
k = p == parents.end() ? -1 : static_cast<long>(p->second);
|
|
}
|
|
jparent[j] = k;
|
|
}
|
|
std::vector<size_t> order, roots;
|
|
for (size_t j : joints)
|
|
if (jparent[j] < 0) roots.push_back(j);
|
|
struct Visitor {
|
|
const std::vector<size_t>& joints;
|
|
std::map<size_t, long>& jparent;
|
|
std::vector<size_t>& order;
|
|
void Visit(size_t j, int depth) {
|
|
if (depth > 256 || order.size() > 100000) throw Error("joint hierarchy too deep");
|
|
order.push_back(j);
|
|
for (size_t c : joints)
|
|
if (jparent[c] == static_cast<long>(j)) Visit(c, depth + 1);
|
|
}
|
|
} v{joints, jparent, order};
|
|
for (size_t r : roots) v.Visit(r, 0);
|
|
bool synthetic = roots.size() > 1;
|
|
if (synthetic) {
|
|
warnings.push_back(Fmt("%u root joints: added a root bone", static_cast<unsigned>(roots.size())));
|
|
Bone b;
|
|
b.name = "root";
|
|
b.abs = Identity();
|
|
sk.bones.push_back(b);
|
|
}
|
|
for (size_t k = 0; k < order.size(); ++k) sk.nodeBone[order[k]] = static_cast<int>(k) + (synthetic ? 1 : 0);
|
|
for (size_t j : order) {
|
|
size_t ji = std::find(joints.begin(), joints.end(), j) - joints.begin();
|
|
if (ji >= ibm.size() || ibm[ji].size() < 16) throw Error("too few inverse bind matrices");
|
|
const Row& col = ibm[ji];
|
|
M4 mat;
|
|
for (int r = 0; r < 4; ++r)
|
|
for (int c = 0; c < 4; ++c) mat.m[r][c] = col[r + 4 * c];
|
|
M4 a = Inv(mat);
|
|
for (int r = 0; r < 3; ++r) a.m[r][3] = a.m[r][3] * Fl(scale);
|
|
Bone b;
|
|
const Json& nd = nodes.at(j);
|
|
b.name = nd.has("name") && nd.at("name").truthy() ? Latin1(nd.at("name").string()) : Fmt("joint%u", static_cast<unsigned>(j));
|
|
b.node = static_cast<long>(j);
|
|
long p = jparent[j];
|
|
b.parent = p >= 0 ? sk.nodeBone.at(static_cast<size_t>(p)) : (synthetic ? 0 : -1);
|
|
b.abs = a;
|
|
sk.bones.push_back(b);
|
|
}
|
|
for (Bone& b : sk.bones) {
|
|
b.local = b.parent < 0 ? b.abs : Mul(Inv(sk.bones[b.parent].abs), b.abs);
|
|
double sc[3];
|
|
ToQt(b.local, b.q, b.t, sc);
|
|
double dev = 0;
|
|
for (double x : sc) dev = std::max(dev, std::fabs(std::fabs(x) - 1));
|
|
if (dev > 1e-3) warnings.push_back("bone " + b.name + " has scale in its bind pose (dropped)");
|
|
}
|
|
std::set<std::string> seen;
|
|
for (const Bone& b : sk.bones) {
|
|
std::string lo = b.name;
|
|
for (char& c : lo) c = static_cast<char>(tolower(static_cast<unsigned char>(c)));
|
|
if (!seen.insert(lo).second) warnings.push_back("duplicate bone name " + b.name);
|
|
}
|
|
if (sk.bones.size() > 255) throw Error("more than 255 bones");
|
|
sk.skin = skin;
|
|
for (size_t j : joints) {
|
|
auto it = sk.nodeBone.find(j);
|
|
if (it == sk.nodeBone.end()) throw Error("joint outside the joint hierarchy");
|
|
sk.skinBone.push_back(it->second);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// --- geometry (gltf_geometry) ----------------------------------------------------------------------------------------
|
|
struct Corner {
|
|
double pos[3], nrm[3], uv[2];
|
|
uint32_t col;
|
|
bool hasBw = false;
|
|
int bones[4] = {0, 0, 0, 0};
|
|
double weights[4] = {0, 0, 0, 0};
|
|
};
|
|
|
|
struct Material {
|
|
std::string name;
|
|
bool hasImage = false;
|
|
Bytes image;
|
|
double color[4] = {1, 1, 1, 1};
|
|
};
|
|
|
|
struct Group {
|
|
std::vector<Corner> corners;
|
|
std::vector<std::array<size_t, 3>> faces;
|
|
};
|
|
|
|
struct Geometry {
|
|
std::vector<Material> materials;
|
|
std::map<size_t, Group> groups;
|
|
};
|
|
|
|
uint32_t RoundByte(double x) {
|
|
double v = std::nearbyint(x * 255); // Python round(): half to even
|
|
return static_cast<uint32_t>(std::max(0.0, std::min(255.0, v)));
|
|
}
|
|
|
|
Geometry BuildGeometry(Doc& g, double scale, const Skeleton* skel) {
|
|
Geometry geo;
|
|
const Json& doc = g.doc;
|
|
auto parents = g.Parents();
|
|
const Json& mats = g.Arr("materials");
|
|
for (size_t i = 0; i < mats.size(); ++i) {
|
|
const Json& mt = mats.arr[i];
|
|
Material m;
|
|
const Json* pbr = mt.get("pbrMetallicRoughness");
|
|
if (pbr && pbr->has("baseColorTexture")) {
|
|
const Json& tex = g.Arr("textures").at(pbr->at("baseColorTexture").at("index").index(g.Arr("textures").size()));
|
|
if (tex.has("source")) {
|
|
m.image = g.ImageBytes(tex.at("source").index(g.Arr("images").size()));
|
|
m.hasImage = true;
|
|
}
|
|
}
|
|
m.name = mt.has("name") && mt.at("name").truthy() ? mt.at("name").string() : Fmt("material%u", static_cast<unsigned>(i));
|
|
if (pbr && pbr->has("baseColorFactor")) {
|
|
const Json& f = pbr->at("baseColorFactor");
|
|
for (size_t k = 0; k < 4; ++k) m.color[k] = k < f.size() ? f.at(k).number() : 1.0;
|
|
}
|
|
geo.materials.push_back(m);
|
|
}
|
|
long defaultMat = -1;
|
|
const Json& nodes = g.Arr("nodes");
|
|
for (const Doc::Instance& inst : g.MeshInstances()) {
|
|
const Json& mesh = g.Arr("meshes").at(inst.mesh);
|
|
M4 world = inst.world;
|
|
long skin = -1;
|
|
if (skel && inst.node >= 0 && nodes.at(static_cast<size_t>(inst.node)).has("skin"))
|
|
skin = static_cast<long>(nodes.at(static_cast<size_t>(inst.node)).at("skin").index(g.Arr("skins").size()));
|
|
int rigidBone = -1;
|
|
const std::vector<int>* jmap = nullptr;
|
|
if (skel && skin < 0) {
|
|
long k = inst.node;
|
|
int guard = 0;
|
|
while (k >= 0 && !skel->nodeBone.count(static_cast<size_t>(k))) {
|
|
if (++guard > 100000) throw Error("node hierarchy has a cycle");
|
|
auto p = parents.find(static_cast<size_t>(k));
|
|
k = p == parents.end() ? -1 : static_cast<long>(p->second);
|
|
}
|
|
auto it = k >= 0 ? skel->nodeBone.find(static_cast<size_t>(k)) : skel->nodeBone.end();
|
|
rigidBone = it == skel->nodeBone.end() ? 0 : it->second;
|
|
} else if (skin >= 0) {
|
|
world = Identity(); // skinned meshes ignore their node transform (glTF 2.0, 3.7.3)
|
|
if (static_cast<size_t>(skin) != skel->skin)
|
|
throw Error(Fmt("mesh %u uses skin %ld; only one skin per model is supported", static_cast<unsigned>(inst.mesh), skin));
|
|
jmap = &skel->skinBone;
|
|
}
|
|
bool flip = Det3(world) < 0;
|
|
M4 inv = Inv(world), nworld;
|
|
for (int r = 0; r < 4; ++r)
|
|
for (int c = 0; c < 4; ++c) nworld.m[r][c] = inv.m[c][r]; // inverse transpose
|
|
for (const Json& prim : mesh.at("primitives").arr) {
|
|
if (prim.has("mode") && prim.at("mode").integer() != 4) continue;
|
|
const Json& at = prim.at("attributes");
|
|
size_t accs = g.Arr("accessors").size();
|
|
Acc pos = g.Accessor(at.at("POSITION").index(accs));
|
|
size_t n = pos.size();
|
|
if (pos.n < 3) throw Error("POSITION must be VEC3");
|
|
std::vector<size_t> idx;
|
|
if (prim.has("indices")) {
|
|
Acc ia = g.Accessor(prim.at("indices").index(accs));
|
|
for (size_t k = 0; k < ia.size(); ++k) {
|
|
double r = ia.at(k, 0).v;
|
|
if (r < 0 || r >= static_cast<double>(n) || r != std::floor(r)) throw Error("vertex index out of range");
|
|
idx.push_back(static_cast<size_t>(r));
|
|
}
|
|
} else {
|
|
for (size_t k = 0; k < n; ++k) idx.push_back(k);
|
|
}
|
|
std::vector<std::array<size_t, 3>> tris;
|
|
for (size_t k = 0; k + 2 < idx.size(); k += 3) tris.push_back({idx[k], idx[k + 1], idx[k + 2]});
|
|
if (flip)
|
|
for (auto& t : tris) std::swap(t[1], t[2]);
|
|
std::vector<std::array<N, 3>> wpos(n), nrm(n);
|
|
for (size_t k = 0; k < n; ++k) {
|
|
N p[3] = {pos.at(k, 0), pos.at(k, 1), pos.at(k, 2)}, w[3];
|
|
Point(world, p, w);
|
|
for (int c = 0; c < 3; ++c) wpos[k][c] = w[c] * Fl(scale);
|
|
}
|
|
if (at.has("NORMAL")) {
|
|
Acc nr = g.Accessor(at.at("NORMAL").index(accs));
|
|
if (nr.size() < n || nr.n < 3) throw Error("NORMAL must be VEC3, one per vertex");
|
|
for (size_t k = 0; k < n; ++k) {
|
|
N v[3] = {nr.at(k, 0), nr.at(k, 1), nr.at(k, 2)}, d[3];
|
|
Dir(nworld, v, d);
|
|
Normalize<3>(d, nrm[k].data());
|
|
}
|
|
} else { // smooth normals from the faces
|
|
std::vector<std::array<N, 3>> acc(n, {Fl(0.0), Fl(0.0), Fl(0.0)});
|
|
for (const auto& t : tris) {
|
|
const auto &a = wpos[t[0]], &b = wpos[t[1]], &c = wpos[t[2]];
|
|
N u[3] = {b[0] - a[0], b[1] - a[1], b[2] - a[2]}, v[3] = {c[0] - a[0], c[1] - a[1], c[2] - a[2]};
|
|
N fn[3] = {u[1] * v[2] - u[2] * v[1], u[2] * v[0] - u[0] * v[2], u[0] * v[1] - u[1] * v[0]};
|
|
for (size_t k : t)
|
|
for (int j = 0; j < 3; ++j) acc[k][j] = acc[k][j] + fn[j];
|
|
}
|
|
for (size_t k = 0; k < n; ++k) Normalize<3>(acc[k].data(), nrm[k].data());
|
|
}
|
|
std::vector<std::array<double, 2>> uv(n, {0.0, 0.0});
|
|
if (at.has("TEXCOORD_0")) {
|
|
Acc t = g.Accessor(at.at("TEXCOORD_0").index(accs));
|
|
if (t.size() < n || t.n < 2) throw Error("TEXCOORD_0 must be VEC2, one per vertex");
|
|
for (size_t k = 0; k < n; ++k) uv[k] = {t.at(k, 0).v, t.at(k, 1).v};
|
|
}
|
|
std::vector<uint32_t> cols(n, 0xffffffffu);
|
|
if (at.has("COLOR_0")) {
|
|
Acc cs = g.Accessor(at.at("COLOR_0").index(accs));
|
|
if (cs.size() < n) throw Error("COLOR_0 has too few elements");
|
|
for (size_t k = 0; k < n; ++k) {
|
|
double c[4] = {1.0, 1.0, 1.0, 1.0};
|
|
for (size_t j = 0; j < std::min<size_t>(4, cs.n); ++j) c[j] = cs.at(k, j).v;
|
|
cols[k] = (RoundByte(c[3]) << 24) | (RoundByte(c[0]) << 16) | (RoundByte(c[1]) << 8) | RoundByte(c[2]);
|
|
}
|
|
}
|
|
std::vector<Corner> bw; // bones/weights only
|
|
bool haveBw = false;
|
|
if (skin >= 0 && at.has("JOINTS_0") && at.has("WEIGHTS_0")) {
|
|
Acc js = g.Accessor(at.at("JOINTS_0").index(accs)), ws = g.Accessor(at.at("WEIGHTS_0").index(accs));
|
|
if (std::min(js.size(), ws.size()) < n) throw Error("JOINTS_0/WEIGHTS_0 have too few elements");
|
|
bw.resize(n);
|
|
for (size_t k = 0; k < n; ++k) {
|
|
std::vector<N> w;
|
|
for (size_t c = 0; c < ws.n; ++c) {
|
|
N x = ws.at(k, c);
|
|
w.push_back(x.v > 0.0 ? x : Fl(0.0)); // max(0.0, x)
|
|
}
|
|
N tot = PySum(w.data(), w.size());
|
|
if (tot.v == 0) tot = Fl(1.0);
|
|
std::vector<std::pair<double, int>> pairs;
|
|
for (size_t c = 0; c < std::min(js.n, w.size()); ++c) {
|
|
if (!(w[c].v > 0)) continue;
|
|
double jr = std::nearbyint(js.at(k, c).v);
|
|
if (jr < 0 || jr >= static_cast<double>(jmap->size())) throw Error("joint index out of range");
|
|
pairs.push_back({(w[c] / tot).v, (*jmap)[static_cast<size_t>(jr)]});
|
|
}
|
|
std::sort(pairs.begin(), pairs.end(), [](const std::pair<double, int>& a, const std::pair<double, int>& b) {
|
|
return a.first != b.first ? a.first > b.first : a.second > b.second;
|
|
});
|
|
if (pairs.size() > 4) throw Error("more than 4 weights per vertex");
|
|
bw[k].hasBw = true;
|
|
for (size_t c = 0; c < pairs.size(); ++c) {
|
|
bw[k].weights[c] = pairs[c].first;
|
|
bw[k].bones[c] = pairs[c].second;
|
|
}
|
|
}
|
|
haveBw = n > 0;
|
|
} else if (skel) {
|
|
bw.assign(n, Corner());
|
|
for (Corner& c : bw) {
|
|
c.hasBw = true;
|
|
c.bones[0] = rigidBone < 0 ? 0 : rigidBone;
|
|
c.weights[0] = 1.0;
|
|
}
|
|
haveBw = n > 0;
|
|
}
|
|
size_t mat;
|
|
if (prim.has("material")) {
|
|
mat = prim.at("material").index(geo.materials.size());
|
|
} else {
|
|
if (defaultMat < 0) {
|
|
Material d;
|
|
d.name = "default";
|
|
d.color[0] = d.color[1] = d.color[2] = 0.8;
|
|
d.color[3] = 1.0;
|
|
geo.materials.push_back(d);
|
|
defaultMat = static_cast<long>(geo.materials.size()) - 1;
|
|
}
|
|
mat = static_cast<size_t>(defaultMat);
|
|
}
|
|
Group& grp = geo.groups[mat];
|
|
size_t base = grp.corners.size();
|
|
for (size_t k = 0; k < n; ++k) {
|
|
Corner c = haveBw ? bw[k] : Corner();
|
|
for (int j = 0; j < 3; ++j) {
|
|
c.pos[j] = wpos[k][j].v;
|
|
c.nrm[j] = nrm[k][j].v;
|
|
}
|
|
c.uv[0] = uv[k][0];
|
|
c.uv[1] = uv[k][1];
|
|
c.col = cols[k];
|
|
grp.corners.push_back(c);
|
|
if (grp.corners.size() > kMaxItems) throw Error("too many corners");
|
|
}
|
|
for (const auto& t : tris) grp.faces.push_back({t[0] + base, t[1] + base, t[2] + base});
|
|
}
|
|
}
|
|
(void)doc;
|
|
return geo;
|
|
}
|
|
|
|
// ds2mesh _srgb_to_linear / _linear_to_srgb: glTF colour factors are linear, textures sRGB (glTF 2.0 3.9.2).
|
|
// std::min/std::max with these argument orders give Python's min(1.0, c) / max(0.0, c), NaN included.
|
|
double SrgbToLinear(double c) { return c <= 0.04045 ? c / 12.92 : std::pow((c + 0.055) / 1.055, 2.4); }
|
|
|
|
double LinearToSrgb(double c) {
|
|
c = std::max(0.0, std::min(1.0, c));
|
|
return c <= 0.0031308 ? c * 12.92 : 1.055 * std::pow(c, 1 / 2.4) - 0.055;
|
|
}
|
|
|
|
// Python int(round(x)) for 0 <= x <= 255: ties to even, as std::nearbyint in the default rounding mode.
|
|
uint8_t RoundU8(double x) { return static_cast<uint8_t>(std::max(0.0, std::min(255.0, std::nearbyint(x)))); }
|
|
|
|
// ds2mesh material_image: the base colour texture times the base colour factor (colour channels multiplied in
|
|
// linear space and written back as sRGB, alpha multiplied as it is), or a flat 8x8 sRGB image of the factor.
|
|
Image MaterialImage(const Material& m, std::vector<std::string>& warnings) {
|
|
(void)warnings;
|
|
const double* f = m.color;
|
|
const double alpha = std::max(0.0, std::min(1.0, f[3]));
|
|
Image im;
|
|
if (!m.hasImage) {
|
|
uint8_t c[4];
|
|
for (int k = 0; k < 3; ++k) c[k] = RoundU8(LinearToSrgb(f[k]) * 255);
|
|
c[3] = RoundU8(alpha * 255);
|
|
im.w = im.h = 8;
|
|
for (int i = 0; i < 64; ++i)
|
|
for (int k = 0; k < 4; ++k) im.rgba.push_back(c[k]);
|
|
return im;
|
|
}
|
|
std::string err;
|
|
if (!DecodeImage(m.image.data(), m.image.size(), im, err)) throw Error("material \"" + m.name + "\": " + err);
|
|
if (f[0] == 1 && f[1] == 1 && f[2] == 1 && f[3] == 1) return im;
|
|
uint8_t lut[4][256];
|
|
for (int i = 0; i < 256; ++i) {
|
|
for (int k = 0; k < 3; ++k) lut[k][i] = RoundU8(255 * LinearToSrgb(f[k] * SrgbToLinear(i / 255.0)));
|
|
lut[3][i] = RoundU8(i * alpha);
|
|
}
|
|
for (size_t p = 0; p < im.rgba.size(); ++p) im.rgba[p] = lut[p & 3][im.rgba[p]];
|
|
return im;
|
|
}
|
|
|
|
// --- writers -----------------------------------------------------------------------------------------------------------
|
|
struct W {
|
|
Bytes b;
|
|
void u32(uint32_t v) {
|
|
for (int i = 0; i < 4; ++i) b.push_back(static_cast<uint8_t>(v >> (8 * i)));
|
|
}
|
|
void i32(int32_t v) { u32(static_cast<uint32_t>(v)); }
|
|
void f32(double v) {
|
|
float f = F32(v);
|
|
uint32_t u;
|
|
memcpy(&u, &f, 4);
|
|
u32(u);
|
|
}
|
|
void tag(const char* t) { b.insert(b.end(), t, t + 4); }
|
|
void raw(const std::string& s) { b.insert(b.end(), s.begin(), s.end()); }
|
|
};
|
|
|
|
std::string TextField(const std::vector<std::string>& strings) {
|
|
std::string out;
|
|
for (const std::string& s : strings) {
|
|
out += s;
|
|
out += '\0';
|
|
while (out.size() % 4) out += '\0';
|
|
}
|
|
return out;
|
|
}
|
|
|
|
size_t Padded(const std::string& s) { return (s.size() + 1 + 3) / 4 * 4; }
|
|
|
|
// --- build_static_asp + write_asp ---------------------------------------------------------------------------------------
|
|
struct CKey {
|
|
double p[3], n[3], uv[2];
|
|
uint32_t col;
|
|
bool hasBw;
|
|
int bones[4];
|
|
double w[4];
|
|
bool operator<(const CKey& o) const {
|
|
for (int i = 0; i < 3; ++i)
|
|
if (p[i] != o.p[i]) return p[i] < o.p[i];
|
|
for (int i = 0; i < 3; ++i)
|
|
if (n[i] != o.n[i]) return n[i] < o.n[i];
|
|
for (int i = 0; i < 2; ++i)
|
|
if (uv[i] != o.uv[i]) return uv[i] < o.uv[i];
|
|
if (col != o.col) return col < o.col;
|
|
if (hasBw != o.hasBw) return hasBw < o.hasBw;
|
|
for (int i = 0; i < 4; ++i)
|
|
if (bones[i] != o.bones[i]) return bones[i] < o.bones[i];
|
|
for (int i = 0; i < 4; ++i)
|
|
if (w[i] != o.w[i]) return w[i] < o.w[i];
|
|
return false;
|
|
}
|
|
};
|
|
|
|
struct VKey {
|
|
double p[3];
|
|
bool hasBw;
|
|
int bones[4];
|
|
double w[4];
|
|
bool operator<(const VKey& o) const {
|
|
for (int i = 0; i < 3; ++i)
|
|
if (p[i] != o.p[i]) return p[i] < o.p[i];
|
|
if (hasBw != o.hasBw) return hasBw < o.hasBw;
|
|
for (int i = 0; i < 4; ++i)
|
|
if (bones[i] != o.bones[i]) return bones[i] < o.bones[i];
|
|
for (int i = 0; i < 4; ++i)
|
|
if (w[i] != o.w[i]) return w[i] < o.w[i];
|
|
return false;
|
|
}
|
|
};
|
|
|
|
Bytes BuildAsp(const Geometry& geo, const std::string& meshName, const std::vector<std::string>& texNames,
|
|
const Skeleton* skel, std::vector<std::string>& warnings) {
|
|
struct Out {
|
|
double p[3], n[3], uv[2];
|
|
uint32_t col;
|
|
bool hasBw;
|
|
int bones[4];
|
|
double w[4];
|
|
};
|
|
std::vector<Out> corners;
|
|
std::vector<std::array<uint32_t, 3>> faces;
|
|
std::vector<std::pair<uint32_t, uint32_t>> materials, spans;
|
|
size_t tex = 0;
|
|
for (const auto& kv : geo.groups) {
|
|
const Group& grp = kv.second;
|
|
size_t start = corners.size();
|
|
std::map<CKey, size_t> ckey;
|
|
std::vector<size_t> remap(grp.corners.size());
|
|
for (size_t i = 0; i < grp.corners.size(); ++i) {
|
|
const Corner& c = grp.corners[i];
|
|
Out o;
|
|
if (skel) {
|
|
for (int j = 0; j < 3; ++j) {
|
|
o.p[j] = c.pos[j];
|
|
o.n[j] = c.nrm[j];
|
|
}
|
|
} else {
|
|
o.p[0] = c.pos[0];
|
|
o.p[1] = -c.pos[2];
|
|
o.p[2] = c.pos[1];
|
|
o.n[0] = c.nrm[0];
|
|
o.n[1] = -c.nrm[2];
|
|
o.n[2] = c.nrm[1];
|
|
}
|
|
for (double& x : o.p) x = static_cast<double>(F32(x)); // float32 identity for de-duplication
|
|
o.uv[0] = c.uv[0];
|
|
o.uv[1] = c.uv[1];
|
|
o.col = c.col;
|
|
o.hasBw = skel != nullptr;
|
|
for (int j = 0; j < 4; ++j) {
|
|
o.bones[j] = skel ? c.bones[j] : 0;
|
|
o.w[j] = skel ? PyRound(c.weights[j], 6) : 0.0;
|
|
}
|
|
CKey k;
|
|
for (int j = 0; j < 3; ++j) {
|
|
k.p[j] = o.p[j];
|
|
k.n[j] = PyRound(o.n[j], 5);
|
|
}
|
|
k.uv[0] = PyRound(o.uv[0], 6);
|
|
k.uv[1] = PyRound(o.uv[1], 6);
|
|
k.col = o.col;
|
|
k.hasBw = o.hasBw;
|
|
for (int j = 0; j < 4; ++j) {
|
|
k.bones[j] = o.bones[j];
|
|
k.w[j] = o.w[j];
|
|
}
|
|
auto it = ckey.find(k);
|
|
if (it == ckey.end()) {
|
|
it = ckey.emplace(k, corners.size() - start).first;
|
|
corners.push_back(o);
|
|
}
|
|
remap[i] = it->second;
|
|
}
|
|
size_t span = corners.size() - start;
|
|
uint32_t nf = 0;
|
|
for (const auto& f : grp.faces) {
|
|
if (f[0] >= remap.size() || f[1] >= remap.size() || f[2] >= remap.size()) throw Error("face index out of range");
|
|
uint32_t a = static_cast<uint32_t>(remap[f[0]]), b = static_cast<uint32_t>(remap[f[1]]), c = static_cast<uint32_t>(remap[f[2]]);
|
|
if (a == b || b == c || a == c) continue;
|
|
faces.push_back({a, b, c});
|
|
++nf;
|
|
}
|
|
materials.push_back({static_cast<uint32_t>(tex++), nf});
|
|
spans.push_back({static_cast<uint32_t>(start), static_cast<uint32_t>(span)});
|
|
}
|
|
// vertices: one per distinct (position, bones/weights)
|
|
std::map<VKey, size_t> vkey;
|
|
std::vector<size_t> vfirst; // a corner of each vertex (its position and weights)
|
|
std::vector<std::vector<uint32_t>> bvmp;
|
|
for (size_t ci = 0; ci < corners.size(); ++ci) {
|
|
const Out& c = corners[ci];
|
|
VKey k;
|
|
for (int j = 0; j < 3; ++j) k.p[j] = c.p[j];
|
|
k.hasBw = c.hasBw;
|
|
for (int j = 0; j < 4; ++j) {
|
|
k.bones[j] = c.bones[j];
|
|
k.w[j] = c.w[j];
|
|
}
|
|
auto it = vkey.find(k);
|
|
if (it == vkey.end()) {
|
|
it = vkey.emplace(k, vfirst.size()).first;
|
|
vfirst.push_back(ci);
|
|
bvmp.emplace_back();
|
|
}
|
|
bvmp[it->second].push_back(static_cast<uint32_t>(ci));
|
|
}
|
|
if (corners.size() > 65535) warnings.push_back(Fmt("%u corners in one sub-mesh", static_cast<unsigned>(corners.size())));
|
|
for (const auto& s : spans)
|
|
if (s.second > 65535) throw Error("more than 65535 corners in one material (the game keeps 16-bit indices)");
|
|
|
|
std::vector<std::string> boneNames;
|
|
std::vector<int> boneParents;
|
|
if (skel) {
|
|
for (const Bone& b : skel->bones) {
|
|
boneNames.push_back(b.name);
|
|
boneParents.push_back(b.parent);
|
|
}
|
|
} else {
|
|
boneNames.push_back(meshName);
|
|
boneParents.push_back(-1);
|
|
}
|
|
std::vector<std::string> strings(texNames);
|
|
strings.insert(strings.end(), boneNames.begin(), boneNames.end());
|
|
std::string tf = TextField(strings);
|
|
|
|
W w;
|
|
w.tag("BMSH");
|
|
w.u32(5);
|
|
w.u32(static_cast<uint32_t>(tf.size()));
|
|
w.u32(static_cast<uint32_t>(boneNames.size()));
|
|
w.u32(static_cast<uint32_t>(texNames.size()));
|
|
w.u32(static_cast<uint32_t>(vfirst.size()));
|
|
w.u32(1);
|
|
w.u32(0);
|
|
w.raw(tf);
|
|
w.tag("BONH");
|
|
w.u32(5);
|
|
size_t off = 0;
|
|
for (const std::string& t : texNames) off += Padded(t);
|
|
for (size_t i = 0; i < boneNames.size(); ++i) {
|
|
w.i32(static_cast<int32_t>(i));
|
|
w.i32(boneParents[i]);
|
|
w.u32(static_cast<uint32_t>(off));
|
|
off += Padded(boneNames[i]);
|
|
}
|
|
w.tag("BSUB");
|
|
w.u32(5);
|
|
w.i32(0);
|
|
w.u32(static_cast<uint32_t>(texNames.size()));
|
|
w.u32(static_cast<uint32_t>(vfirst.size()));
|
|
w.u32(static_cast<uint32_t>(corners.size()));
|
|
w.u32(static_cast<uint32_t>(faces.size()));
|
|
w.tag("BSMM");
|
|
w.u32(5);
|
|
w.u32(static_cast<uint32_t>(materials.size()));
|
|
for (const auto& m : materials) {
|
|
w.u32(m.first);
|
|
w.u32(m.second);
|
|
}
|
|
w.tag("BVTX");
|
|
w.u32(5);
|
|
w.u32(static_cast<uint32_t>(vfirst.size()));
|
|
for (size_t v : vfirst)
|
|
for (int j = 0; j < 3; ++j) w.f32(corners[v].p[j]);
|
|
w.tag("WCRN");
|
|
w.u32(5);
|
|
w.u32(static_cast<uint32_t>(corners.size()));
|
|
for (const Out& c : corners) {
|
|
for (int j = 0; j < 3; ++j) w.f32(c.p[j]);
|
|
for (int j = 0; j < 4; ++j) w.f32(c.w[j]);
|
|
for (int j = 0; j < 4; ++j) {
|
|
if (c.bones[j] < 0 || c.bones[j] > 255) throw Error("bone index above 255");
|
|
w.b.push_back(static_cast<uint8_t>(c.bones[j]));
|
|
}
|
|
for (int j = 0; j < 3; ++j) w.f32(c.n[j]);
|
|
w.u32(c.col);
|
|
w.f32(c.uv[0]);
|
|
w.f32(c.uv[1]);
|
|
}
|
|
w.tag("BVMP");
|
|
w.u32(5);
|
|
for (const auto& l : bvmp) {
|
|
w.u32(static_cast<uint32_t>(l.size()));
|
|
for (uint32_t c : l) w.u32(c);
|
|
}
|
|
w.tag("BTRI");
|
|
w.u32(5);
|
|
w.u32(static_cast<uint32_t>(faces.size()));
|
|
for (const auto& s : spans) {
|
|
w.u32(s.first);
|
|
w.u32(s.second);
|
|
}
|
|
for (const auto& f : faces) {
|
|
w.u32(f[0]);
|
|
w.u32(f[1]);
|
|
w.u32(f[2]);
|
|
}
|
|
w.tag("BVWL");
|
|
w.u32(5);
|
|
if (skel) {
|
|
std::vector<std::vector<std::pair<uint32_t, double>>> bvwl(skel->bones.size());
|
|
for (size_t vi = 0; vi < vfirst.size(); ++vi) {
|
|
const Out& c = corners[vfirst[vi]];
|
|
for (int j = 0; j < 4; ++j)
|
|
if (c.w[j] > 0) {
|
|
if (static_cast<size_t>(c.bones[j]) >= bvwl.size()) throw Error("bone index out of range");
|
|
bvwl[c.bones[j]].push_back({static_cast<uint32_t>(vi), c.w[j]});
|
|
}
|
|
}
|
|
for (const auto& l : bvwl) {
|
|
w.u32(static_cast<uint32_t>(l.size()));
|
|
for (const auto& e : l) {
|
|
w.u32(e.first);
|
|
w.f32(e.second);
|
|
}
|
|
}
|
|
} else {
|
|
w.i32(-1);
|
|
}
|
|
w.tag("STCH");
|
|
w.u32(5);
|
|
w.u32(0);
|
|
w.tag("RPOS");
|
|
w.u32(5);
|
|
w.u32(static_cast<uint32_t>(boneNames.size()));
|
|
if (skel) {
|
|
for (const Bone& b : skel->bones) {
|
|
N iq[4], it[3];
|
|
double sc[3];
|
|
ToQt(Inv(b.abs), iq, it, sc);
|
|
for (const N& x : iq) w.f32(x.v);
|
|
for (const N& x : it) w.f32(x.v);
|
|
for (const N& x : b.q) w.f32(x.v);
|
|
for (const N& x : b.t) w.f32(x.v);
|
|
}
|
|
} else {
|
|
double r2 = std::sqrt(0.5);
|
|
const double rpos[14] = {r2, 0.0, 0.0, r2, 0.0, 0.0, 0.0, -r2, 0.0, 0.0, r2, 0.0, 0.0, 0.0};
|
|
for (double x : rpos) w.f32(x);
|
|
}
|
|
w.tag("BBOX");
|
|
w.u32(5);
|
|
w.u32(0);
|
|
w.tag("BEND");
|
|
w.tag("INFO");
|
|
w.u32(1);
|
|
w.raw("SiegeMax");
|
|
return w.b;
|
|
}
|
|
|
|
// --- animations (_sample, gltf_animation_prs, write_prs) -----------------------------------------------------------
|
|
Row Sample(const Doc& g, const Json& sampler, N t) {
|
|
const Acc& in = g.Cached(sampler.at("input").index(g.Arr("accessors").size()));
|
|
const Acc& out = g.Cached(sampler.at("output").index(g.Arr("accessors").size()));
|
|
std::string interp = sampler.has("interpolation") ? sampler.at("interpolation").string() : "LINEAR";
|
|
std::vector<size_t> rows; // the output elements that are values (CUBICSPLINE: every third, from 1)
|
|
for (size_t i = interp == "CUBICSPLINE" ? 1 : 0; i < out.size(); i += interp == "CUBICSPLINE" ? 3 : 1) rows.push_back(i);
|
|
if (in.size() == 0 || in.n < 1) throw Error("empty animation sampler");
|
|
auto val = [&](size_t i) -> Row {
|
|
if (i >= rows.size()) throw Error("animation sampler: too few output values");
|
|
return out.row(rows[i]);
|
|
};
|
|
if (t.v <= in.at(0, 0).v) return val(0);
|
|
if (t.v >= in.at(in.size() - 1, 0).v) {
|
|
if (rows.empty()) throw Error("animation sampler: no output values");
|
|
return val(rows.size() - 1);
|
|
}
|
|
size_t k = 0;
|
|
for (size_t i = 0; i < in.size(); ++i)
|
|
if (in.at(i, 0).v <= t.v) k = i;
|
|
if (interp == "STEP") return val(k);
|
|
if (k + 1 >= in.size() || in.at(k + 1, 0).v == in.at(k, 0).v) throw Error("animation sampler: bad key times");
|
|
N u = (t - in.at(k, 0)) / (in.at(k + 1, 0) - in.at(k, 0));
|
|
Row a = val(k), b = val(k + 1);
|
|
if (a.size() != b.size()) throw Error("animation sampler: mixed value sizes");
|
|
if (a.size() == 4) {
|
|
N d[4];
|
|
for (int i = 0; i < 4; ++i) d[i] = a[i] * b[i];
|
|
if (PySum(d, 4).v < 0)
|
|
for (N& x : b) x = -x;
|
|
N l[4], o[4];
|
|
for (int i = 0; i < 4; ++i) l[i] = a[i] + (b[i] - a[i]) * u;
|
|
Normalize<4>(l, o);
|
|
return Row(o, o + 4);
|
|
}
|
|
Row o(a.size());
|
|
for (size_t i = 0; i < a.size(); ++i) o[i] = a[i] + (b[i] - a[i]) * u;
|
|
return o;
|
|
}
|
|
|
|
Bytes BuildPrs(const Doc& g, const Skeleton& skel, size_t animIndex, double scale, bool loop, std::vector<std::string>& warnings) {
|
|
const Json& a = g.Arr("animations").at(animIndex);
|
|
auto parents = g.Parents();
|
|
const Json& samplers = a.at("samplers");
|
|
std::map<size_t, std::map<std::string, const Json*>> chans;
|
|
for (const Json& ch : a.at("channels").arr) {
|
|
const Json& tgt = ch.at("target");
|
|
if (tgt.has("node"))
|
|
chans[tgt.at("node").index(g.Arr("nodes").size())][tgt.at("path").string()] =
|
|
&samplers.at(ch.at("sampler").index(samplers.size()));
|
|
}
|
|
if (!samplers.size()) throw Error("animation without samplers");
|
|
N tEnd, tStart;
|
|
bool first = true;
|
|
for (const Json& sm : samplers.arr) {
|
|
Acc in = g.Accessor(sm.at("input").index(g.Arr("accessors").size()));
|
|
if (in.size() == 0 || in.n < 1) throw Error("empty animation input");
|
|
N mx = in.at(0, 0), mn = in.at(0, 0);
|
|
for (size_t k = 1; k < in.size(); ++k) {
|
|
if (in.at(k, 0).v > mx.v) mx = in.at(k, 0);
|
|
if (in.at(k, 0).v < mn.v) mn = in.at(k, 0);
|
|
}
|
|
if (first || mx.v > tEnd.v) tEnd = mx;
|
|
if (first || mn.v < tStart.v) tStart = mn;
|
|
first = false;
|
|
}
|
|
N span = tEnd - tStart, dur = 1e-3 > span.v ? Fl(1e-3) : span; // max(t_end - t_start, 1e-3)
|
|
struct Track {
|
|
std::vector<std::pair<double, std::array<N, 4>>> rot;
|
|
std::vector<std::pair<double, std::array<double, 3>>> pos;
|
|
};
|
|
std::vector<Track> tracks(skel.bones.size());
|
|
std::vector<std::string> names;
|
|
const Json& nodes = g.Arr("nodes");
|
|
for (size_t bi = 0; bi < skel.bones.size(); ++bi) {
|
|
const Bone& b = skel.bones[bi];
|
|
names.push_back(b.name);
|
|
if (b.node < 0) {
|
|
tracks[bi].rot.push_back({0.0, {Fl(0.0), Fl(0.0), Fl(0.0), Fl(1.0)}});
|
|
tracks[bi].pos.push_back({0.0, {0.0, 0.0, 0.0}});
|
|
continue;
|
|
}
|
|
M4 pre = Identity();
|
|
auto pit = parents.find(static_cast<size_t>(b.node));
|
|
long k = pit == parents.end() ? -1 : static_cast<long>(pit->second);
|
|
long stop = b.parent >= 0 ? skel.bones[b.parent].node : -1;
|
|
int guard = 0;
|
|
while (k >= 0 && k != stop) {
|
|
if (++guard > 100000) throw Error("node hierarchy has a cycle");
|
|
if (chans.count(static_cast<size_t>(k))) warnings.push_back("a node between bones is animated (ignored)");
|
|
pre = Mul(g.NodeMatrix(nodes.at(static_cast<size_t>(k))), pre);
|
|
auto p = parents.find(static_cast<size_t>(k));
|
|
k = p == parents.end() ? -1 : static_cast<long>(p->second);
|
|
}
|
|
const Json& nd = nodes.at(static_cast<size_t>(b.node));
|
|
N restQ[4] = {In(0), In(0), In(0), In(1)}, restT[3] = {In(0), In(0), In(0)};
|
|
if (nd.has("matrix")) {
|
|
double restS[3];
|
|
ToQt(g.NodeMatrix(nd), restQ, restT, restS);
|
|
} else {
|
|
Doc::Vec(nd, "rotation", restQ, 4);
|
|
Doc::Vec(nd, "translation", restT, 3);
|
|
}
|
|
static const std::map<std::string, const Json*> none;
|
|
auto cit = chans.find(static_cast<size_t>(b.node));
|
|
const std::map<std::string, const Json*>& ch = cit == chans.end() ? none : cit->second;
|
|
if (ch.count("scale")) warnings.push_back("bone " + b.name + ": scale keys dropped");
|
|
std::vector<N> times;
|
|
std::set<double> seen; // a Python set: the first of equal values stays
|
|
for (const char* path : {"rotation", "translation"}) {
|
|
auto it = ch.find(path);
|
|
if (it == ch.end()) continue;
|
|
const Acc& in = g.Cached(it->second->at("input").index(g.Arr("accessors").size()));
|
|
for (size_t r = 0; r < in.size(); ++r)
|
|
if (seen.insert(in.at(r, 0).v).second) times.push_back(in.at(r, 0));
|
|
}
|
|
std::stable_sort(times.begin(), times.end(), [](const N& x, const N& y) { return x.v < y.v; });
|
|
if (times.empty()) times.push_back(tStart);
|
|
for (const N& t : times) {
|
|
N q[4], tr[3];
|
|
auto r = ch.find("rotation");
|
|
if (r != ch.end()) {
|
|
Row v = Sample(g, *r->second, t);
|
|
if (v.size() < 4) throw Error("rotation output must be VEC4");
|
|
for (int i = 0; i < 4; ++i) q[i] = v[i];
|
|
} else {
|
|
for (int i = 0; i < 4; ++i) q[i] = restQ[i];
|
|
}
|
|
auto tt = ch.find("translation");
|
|
if (tt != ch.end()) {
|
|
Row v = Sample(g, *tt->second, t);
|
|
if (v.size() < 3) throw Error("translation output must be VEC3");
|
|
for (int i = 0; i < 3; ++i) tr[i] = v[i];
|
|
} else {
|
|
for (int i = 0; i < 3; ++i) tr[i] = restT[i];
|
|
}
|
|
const N one[3] = {In(1), In(1), In(1)};
|
|
M4 mat = Mul(pre, Q2M(q, tr, one));
|
|
N q2[4], t2[3];
|
|
double sc[3];
|
|
ToQt(mat, q2, t2, sc);
|
|
if (!tracks[bi].rot.empty()) {
|
|
const auto& prev = tracks[bi].rot.back().second;
|
|
N d[4];
|
|
for (int i = 0; i < 4; ++i) d[i] = q2[i] * prev[i];
|
|
if (PySum(d, 4).v < 0)
|
|
for (N& x : q2) x = -x;
|
|
}
|
|
double u = ((t - tStart) / dur).v;
|
|
tracks[bi].rot.push_back({u, {q2[0], q2[1], q2[2], q2[3]}});
|
|
tracks[bi].pos.push_back({u, {(t2[0] * Fl(scale)).v, (t2[1] * Fl(scale)).v, (t2[2] * Fl(scale)).v}});
|
|
}
|
|
}
|
|
std::string tf = TextField(names);
|
|
W w;
|
|
w.tag("ANIM");
|
|
w.u32(5);
|
|
w.u32(static_cast<uint32_t>(tf.size()));
|
|
w.u32(static_cast<uint32_t>(names.size()));
|
|
w.f32(dur.v);
|
|
for (int i = 0; i < 3; ++i) w.f32(0.0);
|
|
for (int k = 0; k < 2; ++k) {
|
|
w.f32(0.0);
|
|
w.f32(0.0);
|
|
w.f32(0.0);
|
|
w.f32(1.0);
|
|
}
|
|
w.f32(0.0);
|
|
w.raw(tf);
|
|
w.tag("NOTE");
|
|
w.u32(5);
|
|
if (loop) {
|
|
w.u32(2);
|
|
w.f32(0.0);
|
|
w.tag("BEGL");
|
|
w.f32(1.0);
|
|
w.tag("ENDL");
|
|
} else {
|
|
w.u32(0);
|
|
}
|
|
size_t off = 0;
|
|
for (size_t bi = 0; bi < names.size(); ++bi) {
|
|
w.tag("KLST");
|
|
w.u32(5);
|
|
w.u32(static_cast<uint32_t>(bi));
|
|
w.u32(static_cast<uint32_t>(off));
|
|
off += Padded(names[bi]);
|
|
const Track& t = tracks[bi];
|
|
w.u32(static_cast<uint32_t>(t.rot.size()));
|
|
w.u32(static_cast<uint32_t>(t.pos.size()));
|
|
for (const auto& r : t.rot) {
|
|
w.f32(r.first);
|
|
for (const N& x : r.second) w.f32(x.v);
|
|
}
|
|
for (const auto& p : t.pos) {
|
|
w.f32(p.first);
|
|
for (double x : p.second) w.f32(x);
|
|
}
|
|
}
|
|
w.tag("AEND");
|
|
w.tag("INFO");
|
|
w.u32(1);
|
|
w.raw("SiegeMax");
|
|
return w.b;
|
|
}
|
|
|
|
std::string AnimName(const Json& an, size_t ai) {
|
|
std::string n = an.has("name") && an.at("name").truthy() ? Latin1(an.at("name").string()) : Fmt("anim%u", static_cast<unsigned>(ai));
|
|
for (char& c : n)
|
|
if (c == ' ' || c == '|') c = '_';
|
|
return n;
|
|
}
|
|
|
|
void ConvertMeshImpl(const std::wstring& path, const Options& opt, bool withAnims, Result& out) {
|
|
Doc g;
|
|
g.deps = &out.dependencies;
|
|
g.Load(path);
|
|
Skeleton skel;
|
|
bool skinned = !opt.forceStatic && BuildSkeleton(g, opt.scale, skel, out.warnings);
|
|
Geometry geo = BuildGeometry(g, opt.scale, skinned ? &skel : nullptr);
|
|
if (geo.groups.empty()) throw Error("no triangles in the default scene");
|
|
std::vector<std::string> tex = opt.textureNames;
|
|
if (opt.textureNamer) {
|
|
std::vector<std::string> matNames;
|
|
for (const auto& kv : geo.groups) matNames.push_back(Latin1(geo.materials.at(kv.first).name));
|
|
tex = opt.textureNamer(matNames);
|
|
}
|
|
if (tex.size() < geo.groups.size()) {
|
|
std::vector<std::string> def = DefaultTextureNames(opt.meshName, geo.groups.size());
|
|
for (size_t i = tex.size(); i < def.size(); ++i) tex.push_back(def[i]);
|
|
}
|
|
tex.resize(geo.groups.size());
|
|
out.asp = BuildAsp(geo, Latin1(opt.meshName), tex, skinned ? &skel : nullptr, out.warnings);
|
|
out.skinned = skinned;
|
|
size_t k = 0;
|
|
for (const auto& kv : geo.groups) {
|
|
const Material& m = geo.materials.at(kv.first);
|
|
out.textures.push_back({tex[k++], WriteDds(MaterialImage(m, out.warnings), opt.maxTexture)});
|
|
}
|
|
if (skinned && withAnims) {
|
|
const Json& anims = g.Arr("animations");
|
|
for (size_t ai = 0; ai < anims.size(); ++ai)
|
|
out.anims.push_back({AnimName(anims.arr[ai], ai), BuildPrs(g, skel, ai, opt.scale, opt.loop, out.warnings)});
|
|
}
|
|
}
|
|
|
|
void ConvertAnimationImpl(const std::wstring& path, const std::string& name, const Options& opt, Result& out) {
|
|
Doc g;
|
|
g.deps = &out.dependencies;
|
|
g.Load(path);
|
|
Skeleton skel;
|
|
if (!BuildSkeleton(g, opt.scale, skel, out.warnings)) throw Error("no skin in the file (an animation needs the skeleton)");
|
|
const Json& anims = g.Arr("animations");
|
|
if (!anims.size()) throw Error("no animations in the file");
|
|
size_t idx = anims.size();
|
|
for (size_t i = 0; i < anims.size(); ++i)
|
|
if (anims.arr[i].has("name") && anims.arr[i].at("name").type == Json::Str && anims.arr[i].at("name").str == name) {
|
|
idx = i;
|
|
break;
|
|
}
|
|
if (idx == anims.size() && opt.animFallbackFirst) {
|
|
for (size_t i = 0; i < anims.size() && idx == anims.size(); ++i)
|
|
if (anims.arr[i].has("name") && anims.arr[i].at("name").type == Json::Str &&
|
|
_stricmp(anims.arr[i].at("name").str.c_str(), name.c_str()) == 0)
|
|
idx = i;
|
|
if (idx == anims.size()) idx = 0;
|
|
}
|
|
if (idx == anims.size()) {
|
|
if (name.empty()) {
|
|
idx = 0;
|
|
} else if (name.find_first_not_of("0123456789") == std::string::npos) {
|
|
idx = static_cast<size_t>(std::stoul(name));
|
|
if (idx >= anims.size()) throw Error("no animation " + name);
|
|
} else {
|
|
throw Error("no animation \"" + name + "\"");
|
|
}
|
|
}
|
|
out.anims.push_back({AnimName(anims.arr[idx], idx), BuildPrs(g, skel, idx, opt.scale, opt.loop, out.warnings)});
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::vector<std::string> DefaultTextureNames(const std::string& meshName, size_t count) {
|
|
std::string base = meshName.compare(0, 2, "m_") == 0 ? "b" + meshName.substr(1) : "b_" + meshName;
|
|
std::vector<std::string> out;
|
|
for (size_t i = 0; i < count; ++i) out.push_back(count == 1 ? base : base + Fmt("-%02u", static_cast<unsigned>(i + 1)));
|
|
return out;
|
|
}
|
|
|
|
bool ConvertMesh(const std::wstring& path, const Options& opt, bool withAnims, Result& out, std::string& error) {
|
|
try {
|
|
ConvertMeshImpl(path, opt, withAnims, out);
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
error = e.what();
|
|
} catch (...) {
|
|
error = "unknown error";
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool ConvertAnimation(const std::wstring& path, const std::string& name, const Options& opt, Result& out, std::string& error) {
|
|
try {
|
|
ConvertAnimationImpl(path, name, opt, out);
|
|
return true;
|
|
} catch (const std::exception& e) {
|
|
error = e.what();
|
|
} catch (...) {
|
|
error = "unknown error";
|
|
}
|
|
return false;
|
|
}
|
|
|
|
} // namespace gltf
|