#include "anim_bake.h" #include "bake_io.h" #include "loaders/opa_loader.h" #include "loaders/gltf_loader.h" #include #include #include #include #include namespace { using bakeio::Reader; using bakeio::Writer; constexpr u8 kClay = 1, kGltf = 2; // Which nodes the animated meshes hang from: those nodes and every ancestor the evaluation reaches from them, the // rest dropped (kept in their order, so each still follows its parents). `parentOf(i)` is the parent the evaluation // uses (-1: none); out[i] = the node's new index, -1 when it isn't kept. template std::vector keptNodes(size_t n, const std::vector& used, ParentOf parentOf) { std::vector keep(n, 0); for (int u : used) { for (int i = u, steps = 0; i >= 0 && static_cast(i) < n && !keep[i] && steps <= static_cast(n); ++steps) { keep[i] = 1; i = parentOf(i); } } std::vector out(n, -1); int k = 0; for (size_t i = 0; i < n; ++i) if (keep[i]) out[i] = k++; return out; } // ── Clay (OpaLoader) ── void writeTrack(Writer& w, const OpaLoader::Track& t) { w.pod(t.comps); w.pod(t.nFrames); w.pod(t.effFrames); w.vec(t.v); } OpaLoader::Track readTrack(Reader& r) { OpaLoader::Track t; t.comps = r.pod(); t.nFrames = r.pod(); t.effFrames = r.pod(); t.v = r.vec(); return t; } // sampleTrack reads nFrames x comps values when the track has frames and its comps are what it asks for (3 or 4) bool trackOk(const OpaLoader::Track& t) { return t.nFrames <= 0 || (t.comps != 3 && t.comps != 4) || t.v.size() >= static_cast(t.nFrames) * t.comps; } void saveClay(const OpaLoader& L, Writer& w) { w.f(L.animFps); w.pod(L.animMaxFrames); w.f(L.vatFps); w.f(L.uvMaxLoopSec); // the node hierarchy, as far as the animated meshes need it (evalAnimNodes: a parent at or after its child is none) std::vector used; for (const auto& ar : L.animRecs) used.push_back(static_cast(ar.nodeIdx)); const std::vector nodeMap = keptNodes(L.animNodes.size(), used, [&](int i) { const int p = L.animNodes[i].parent; return p >= 0 && p < i ? p : -1; }); std::vector nodes; for (size_t i = 0; i < L.animNodes.size(); ++i) if (nodeMap[i] >= 0) nodes.push_back(&L.animNodes[i]); w.u(static_cast(nodes.size())); for (const OpaLoader::Node* nd : nodes) { const int i = static_cast(nd - L.animNodes.data()), p = nd->parent; w.str(nd->name); w.pod(p >= 0 && p < i ? nodeMap[p] : -1); w.fa(nd->t, 3); w.fa(nd->r, 4); w.fa(nd->s, 3); } { // their keyed tracks (by node name, as evalAnimNodes looks them up) std::vector> tracks; std::set seen; for (const OpaLoader::Node* nd : nodes) { const auto it = L.nodeAnim.find(nd->name); if (it != L.nodeAnim.end() && seen.insert(nd->name).second) tracks.emplace_back(&it->first, &it->second); } w.u(static_cast(tracks.size())); for (const auto& t : tracks) { w.str(*t.first); writeTrack(w, t.second->t); writeTrack(w, t.second->r); writeTrack(w, t.second->s); } } w.u(static_cast(L.animRecs.size())); for (const auto& ar : L.animRecs) { w.u(static_cast(ar.meshIdx)); w.pod(ar.nodeIdx < nodeMap.size() ? nodeMap[ar.nodeIdx] : INT_MAX); // (out of range: evaluated as no node) w.vec(ar.basePos); w.pod(ar.rigid); w.pod(ar.rest); w.fa(ar.restInv, 16); w.b(ar.atRest); } // skin clips the skinned meshes play (animate() evaluates every clip each frame: the unused ones go) std::vector clipMap(L.clips.size(), -1); std::vector clips; for (const auto& sr : L.skinRecs) if (sr.clipIdx >= 0 && static_cast(sr.clipIdx) < L.clips.size() && clipMap[sr.clipIdx] < 0) clipMap[sr.clipIdx] = 0; for (size_t c = 0; c < L.clips.size(); ++c) if (clipMap[c] >= 0) { clipMap[c] = static_cast(clips.size()); clips.push_back(&L.clips[c]); } w.u(static_cast(clips.size())); for (const OpaLoader::AnimClip* c : clips) { w.pod(c->numJoints); w.pod(c->numFrames); w.vec(c->parents); w.vec(c->mats); } w.u(static_cast(L.skinRecs.size())); for (const auto& sr : L.skinRecs) { w.u(static_cast(sr.meshIdx)); w.pod(sr.clipIdx >= 0 && static_cast(sr.clipIdx) < clipMap.size() ? clipMap[sr.clipIdx] : -1); w.pod(sr.nJoints); w.b(sr.gpu); // skinned on the GPU: the bind pose and weights are the mesh's own (skinBindPos, boneIndices / boneWeights) static const std::vector noF; static const std::vector noI; w.vec(sr.gpu ? noF : sr.basePos); w.vec(sr.gpu ? noI : sr.jidx); w.vec(sr.gpu ? noF : sr.jw); w.vec(sr.boneClip); w.vec(sr.invBind); w.vec(sr.jointBox); w.fa(sr.unriggedBox, 6); } { // UV tracks the UV-animated meshes play, each with its scroll-or-flipbook answer std::vector> tracks; std::set seen; for (const auto& ur : L.uvAnimRecs) { const auto it = L.matUVAnim.find(ur.node); if (it != L.matUVAnim.end() && seen.insert(ur.node).second) tracks.emplace_back(&it->first, &it->second); } w.u(static_cast(tracks.size())); for (const auto& t : tracks) { const OpaLoader::UVTrack& tr = *t.second; int scroll = tr.scroll; float ru = tr.ru, rv = tr.rv; if (scroll < 0) scroll = L.uvScrollRate(tr, ru, rv) ? 1 : 0; w.str(*t.first); w.pod(tr.nFrames); w.vec(tr.m); w.pod(scroll); w.f(ru); w.f(rv); } } w.u(static_cast(L.uvAnimRecs.size())); for (const auto& ur : L.uvAnimRecs) { w.u(static_cast(ur.meshIdx)); w.str(ur.node); w.vec(ur.baseUV); w.b(ur.atRest); } { std::vector> tracks; std::set seen; for (const auto& tr : L.tintRecs) { const auto it = L.matTintAnim.find(tr.node); if (it != L.matTintAnim.end() && seen.insert(tr.node).second) tracks.emplace_back(&it->first, &it->second); } w.u(static_cast(tracks.size())); for (const auto& t : tracks) { w.str(*t.first); w.pod(t.second->nFrames); w.vec(t.second->rgba); } } w.u(static_cast(L.tintRecs.size())); for (const auto& tr : L.tintRecs) { w.u(static_cast(tr.meshIdx)); w.str(tr.node); } std::vector vats; for (const auto& vr : L.vatRecs) if (vr.vd && std::find(vats.begin(), vats.end(), vr.vd) == vats.end()) vats.push_back(vr.vd); w.u(static_cast(vats.size())); for (const OpaLoader::VatData* vd : vats) { w.pod(vd->cols); w.pod(vd->frames); w.vec(vd->off); } w.u(static_cast(L.vatRecs.size())); for (const auto& vr : L.vatRecs) { w.u(static_cast(vr.meshIdx)); w.vec(vr.basePos); w.vec(vr.col); w.pod(vr.world); w.pod(vr.vd ? static_cast(std::find(vats.begin(), vats.end(), vr.vd) - vats.begin()) : -1); } } std::shared_ptr loadClay(Reader& r, size_t meshes) { auto L = std::make_shared(); L->verbose = false; L->animFps = r.f(); L->animMaxFrames = r.pod(); L->vatFps = r.f(); L->uvMaxLoopSec = r.f(); L->animNodes.resize(r.count(44)); for (auto& nd : L->animNodes) { if (r.bad) return nullptr; nd.name = r.str(); nd.parent = r.pod(); r.fa(nd.t, 3); r.fa(nd.r, 4); r.fa(nd.s, 3); } for (u32 n = r.count(40); n-- && !r.bad;) { const std::string name = r.str(); OpaLoader::NodeTracks nt; nt.t = readTrack(r); nt.r = readTrack(r); nt.s = readTrack(r); if (!trackOk(nt.t) || !trackOk(nt.r) || !trackOk(nt.s)) return nullptr; L->nodeAnim[name] = std::move(nt); } L->animRecs.resize(r.count(145)); for (auto& ar : L->animRecs) { if (r.bad) return nullptr; ar.meshIdx = r.u(); ar.nodeIdx = static_cast(r.pod()); ar.basePos = r.vec(); ar.rigid = r.pod(); ar.rest = r.pod(); r.fa(ar.restInv, 16); ar.atRest = r.b(); ar.parentWorld = OpaLoader::identity(); if (ar.meshIdx >= meshes) return nullptr; } L->clips.resize(r.count(16)); for (auto& c : L->clips) { if (r.bad) return nullptr; c.numJoints = r.pod(); c.numFrames = r.pod(); c.parents = r.vec(); c.mats = r.vec(); if (c.numJoints < 0 || c.numFrames < 0 || c.mats.size() < static_cast(c.numFrames) * c.numJoints * 16) return nullptr; } L->skinRecs.resize(r.count(57)); for (auto& sr : L->skinRecs) { if (r.bad) return nullptr; sr.meshIdx = r.u(); sr.clipIdx = r.pod(); sr.nJoints = r.pod(); sr.gpu = r.b(); sr.basePos = r.vec(); sr.jidx = r.vec(); sr.jw = r.vec(); sr.boneClip = r.vec(); sr.invBind = r.vec(); sr.jointBox = r.vec(); r.fa(sr.unriggedBox, 6); const size_t nv = sr.basePos.size() / 3; if (sr.meshIdx >= meshes || (!sr.gpu && (sr.jidx.size() < nv * 4 || sr.jw.size() < nv * 4)) || (sr.gpu && sr.jointBox.size() < static_cast(std::max(0, sr.nJoints)) * 6)) return nullptr; } for (u32 n = r.count(24); n-- && !r.bad;) { const std::string name = r.str(); OpaLoader::UVTrack tr; tr.nFrames = r.pod(); tr.m = r.vec(); tr.scroll = r.pod(); tr.ru = r.f(); tr.rv = r.f(); if (tr.nFrames < 0 || tr.m.size() < static_cast(tr.nFrames) * 6) return nullptr; L->matUVAnim[name] = std::move(tr); } L->uvAnimRecs.resize(r.count(13)); for (auto& ur : L->uvAnimRecs) { if (r.bad) return nullptr; ur.meshIdx = r.u(); ur.node = r.str(); ur.baseUV = r.vec(); ur.atRest = r.b(); if (ur.meshIdx >= meshes) return nullptr; } for (u32 n = r.count(12); n-- && !r.bad;) { const std::string name = r.str(); OpaLoader::TintTrack tt; tt.nFrames = r.pod(); tt.rgba = r.vec(); if (tt.nFrames < 0 || tt.rgba.size() < static_cast(tt.nFrames) * 4) return nullptr; L->matTintAnim[name] = std::move(tt); } L->tintRecs.resize(r.count(8)); for (auto& tr : L->tintRecs) { if (r.bad) return nullptr; tr.meshIdx = r.u(); tr.node = r.str(); if (tr.meshIdx >= meshes) return nullptr; } std::vector vatNames(r.count(12)); for (size_t i = 0; i < vatNames.size() && !r.bad; ++i) { vatNames[i] = std::to_string(i); // (keyed by the geo name when parsed; by order here) OpaLoader::VatData& vd = L->vatByBase[vatNames[i]]; vd.cols = r.pod(); vd.frames = r.pod(); vd.off = r.vec(); if (vd.cols < 0 || vd.frames < 0 || vd.off.size() < static_cast(vd.frames) * vd.cols * 3) return nullptr; } L->vatRecs.resize(r.count(80)); for (auto& vr : L->vatRecs) { if (r.bad) return nullptr; vr.meshIdx = r.u(); vr.basePos = r.vec(); vr.col = r.vec(); vr.world = r.pod(); const int vd = r.pod(); vr.vd = vd >= 0 && static_cast(vd) < vatNames.size() ? &L->vatByBase[vatNames[vd]] : nullptr; if (vr.meshIdx >= meshes || (vr.vd && vr.col.size() < vr.basePos.size() / 3)) return nullptr; } return r.bad ? nullptr : L; } // ── glTF (GltfLoader) ── void saveGltf(const GltfLoader& L, Writer& w) { w.f(L.animDuration); w.fa(L.reanchor, 3); // the nodes the animated and skinned meshes follow, with their ancestors (nodeWorlds: an out-of-range parent is // evaluated as the identity, so it stays out of range) std::vector used; for (const auto& rec : L.nodeAnimRecs) used.push_back(rec.nodeIdx); for (const auto& rec : L.skinned) if (rec.skin >= 0 && static_cast(rec.skin) < L.gskins.size()) for (int j : L.gskins[rec.skin].joints) used.push_back(j); const size_t nn = L.gnodes.size(); const std::vector nodeMap = keptNodes(nn, used, [&](int i) { return L.gnodes[i].parent; }); auto mapNode = [&](int n) { return n >= 0 && static_cast(n) < nn ? (nodeMap[n] >= 0 ? nodeMap[n] : INT_MAX) : (n < 0 ? n : INT_MAX); }; u32 kept = 0; for (int m : nodeMap) kept += m >= 0; w.u(kept); for (size_t i = 0; i < nn; ++i) { if (nodeMap[i] < 0) continue; const GltfLoader::GNode& g = L.gnodes[i]; w.fa(g.t, 3); w.fa(g.r, 4); w.fa(g.s, 3); w.pod(mapNode(g.parent)); } w.u(static_cast(L.gskins.size())); for (const auto& sk : L.gskins) { std::vector joints; for (int j : sk.joints) joints.push_back(mapNode(j)); w.vec(joints); w.vec(sk.ibm); } // the channels on kept nodes (in their order: a later one on the same node wins), and the samplers they read std::vector samplerMap(L.gsamplers.size(), -1); std::vector channels; for (const auto& ch : L.gchannels) if (ch.node >= 0 && static_cast(ch.node) < nn && nodeMap[ch.node] >= 0 && ch.sampler >= 0 && static_cast(ch.sampler) < L.gsamplers.size()) { channels.push_back(&ch); samplerMap[ch.sampler] = 0; } std::vector samplers; for (size_t s = 0; s < L.gsamplers.size(); ++s) if (samplerMap[s] >= 0) { samplerMap[s] = static_cast(samplers.size()); samplers.push_back(&L.gsamplers[s]); } w.u(static_cast(samplers.size())); for (const GltfLoader::GSampler* s : samplers) { w.vec(s->times); w.vec(s->vals); w.pod(s->comps); w.b(s->step); } w.u(static_cast(channels.size())); for (const GltfLoader::GChannel* ch : channels) { w.pod(nodeMap[ch->node]); w.pod(ch->path); w.pod(samplerMap[ch->sampler]); } w.u(static_cast(L.skinned.size())); for (const auto& rec : L.skinned) { w.pod(rec.meshIdx); w.pod(rec.skin); w.u(rec.nv); w.vec(rec.basePos); w.vec(rec.jidx); w.vec(rec.jw); } w.u(static_cast(L.nodeAnimRecs.size())); for (const auto& rec : L.nodeAnimRecs) { w.pod(rec.meshIdx); w.pod(mapNode(rec.nodeIdx)); w.vec(rec.basePos); w.pod(rec.rigid); w.fa(rec.rest, 16); w.fa(rec.restInv, 16); w.b(rec.atRest); } } std::shared_ptr loadGltf(Reader& r, size_t meshes) { auto L = std::make_shared(); L->verbose = false; L->animDuration = r.f(); r.fa(L->reanchor, 3); L->gnodes.resize(r.count(44)); for (auto& g : L->gnodes) { if (r.bad) return nullptr; r.fa(g.t, 3); r.fa(g.r, 4); r.fa(g.s, 3); g.parent = r.pod(); } L->gskins.resize(r.count(8)); for (auto& sk : L->gskins) { if (r.bad) return nullptr; sk.joints = r.vec(); sk.ibm = r.vec(); } L->gsamplers.resize(r.count(13)); for (auto& s : L->gsamplers) { if (r.bad) return nullptr; s.times = r.vec(); s.vals = r.vec(); s.comps = r.pod(); s.step = r.b(); // (sampleSampler: up to 4 values a key, a key's worth even without keys) if (s.comps < 0 || s.comps > 4 || (s.times.empty() ? !s.vals.empty() && s.vals.size() < static_cast(s.comps) : s.vals.size() < s.times.size() * s.comps)) return nullptr; } L->gchannels.resize(r.count(12)); for (auto& ch : L->gchannels) { if (r.bad) return nullptr; ch.node = r.pod(); ch.path = r.pod(); ch.sampler = r.pod(); } L->skinned.resize(r.count(24)); for (auto& rec : L->skinned) { if (r.bad) return nullptr; rec.meshIdx = r.pod(); rec.skin = r.pod(); rec.nv = r.u(); rec.basePos = r.vec(); rec.jidx = r.vec(); rec.jw = r.vec(); if (rec.meshIdx < 0 || static_cast(rec.meshIdx) >= meshes || rec.basePos.size() < static_cast(rec.nv) * 3 || rec.jidx.size() < static_cast(rec.nv) * 4 || rec.jw.size() < static_cast(rec.nv) * 4) return nullptr; } L->nodeAnimRecs.resize(r.count(145)); for (auto& rec : L->nodeAnimRecs) { if (r.bad) return nullptr; rec.meshIdx = r.pod(); rec.nodeIdx = r.pod(); rec.basePos = r.vec(); rec.rigid = r.pod(); r.fa(rec.rest, 16); r.fa(rec.restInv, 16); rec.atRest = r.b(); if (rec.meshIdx < 0 || static_cast(rec.meshIdx) >= meshes) return nullptr; } return r.bad ? nullptr : L; } // The per-frame play of a loader's animation, parsed or rebuilt: it works on the scene's meshes (the loader indexes them // as its own, same order). template std::function&)> player(std::shared_ptr L) { return [L](float t, std::vector& m) { std::swap(L->meshes, m); L->animateRuntime(t); std::swap(L->meshes, m); }; } // One stored animation, rebuilt: the loader holding it. struct Animator { std::shared_ptr clay; std::shared_ptr gltf; std::function&)> animate() const { return clay ? player(clay) : gltf ? player(gltf) : nullptr; } void save(std::vector& out) const { if (clay) animbake::save(*clay, out); else if (gltf) animbake::save(*gltf, out); } }; bool rebuild(const std::vector& bytes, size_t meshes, Animator& a) { Reader r{bytes.data(), bytes.data() + bytes.size()}; const u8 kind = r.pod(); a = Animator(); if (kind == kClay) a.clay = loadClay(r, meshes); else if (kind == kGltf) a.gltf = loadGltf(r, meshes); return (a.clay || a.gltf) && !r.bad && r.p == r.end; } } // namespace namespace animbake { void adopt(HslScene& scene, std::shared_ptr loader) { scene.animate = player(loader); scene.saveAnimation = [loader](std::vector& out) { save(*loader, out); }; } void adopt(HslScene& scene, std::shared_ptr loader) { scene.animate = player(loader); scene.saveAnimation = [loader](std::vector& out) { save(*loader, out); }; } void save(const OpaLoader& loader, std::vector& out) { out.clear(); Writer w{out}; w.pod(kClay); saveClay(loader, w); } void save(const GltfLoader& loader, std::vector& out) { out.clear(); Writer w{out}; w.pod(kGltf); saveGltf(loader, w); } bool load(const std::vector& bytes, size_t meshes, HslScene& scene) { Animator a; if (!rebuild(bytes, meshes, a)) return false; scene.animate = a.animate(); scene.saveAnimation = [bytes](std::vector& out) { out = bytes; }; return true; } std::vector targets(const std::vector& bytes) { Animator a; std::vector out; if (!rebuild(bytes, SIZE_MAX, a)) return out; if (a.clay) { const OpaLoader& L = *a.clay; for (const auto& x : L.animRecs) out.push_back(static_cast(x.meshIdx)); for (const auto& x : L.skinRecs) out.push_back(static_cast(x.meshIdx)); for (const auto& x : L.uvAnimRecs) out.push_back(static_cast(x.meshIdx)); for (const auto& x : L.tintRecs) out.push_back(static_cast(x.meshIdx)); for (const auto& x : L.vatRecs) out.push_back(static_cast(x.meshIdx)); } else { for (const auto& x : a.gltf->skinned) out.push_back(x.meshIdx); for (const auto& x : a.gltf->nodeAnimRecs) out.push_back(x.meshIdx); } std::sort(out.begin(), out.end()); out.erase(std::unique(out.begin(), out.end()), out.end()); return out; } bool startPose(std::vector& bytes, const std::vector& meshes, std::vector>& posed) { posed.clear(); Animator a; if (!rebuild(bytes, meshes.size(), a)) return false; // only the meshes it writes are copied; the others stay empty (animate() never looks at them) std::vector work(meshes.size()); const std::vector written = targets(bytes); for (int m : written) work[m] = meshes[m]; a.animate()(0.0f, work); for (int m : written) posed.emplace_back(static_cast(m), std::move(work[m])); a.save(bytes); return true; } bool toStart(HslScene& scene) { if (!scene.saveAnimation) return false; std::vector bytes; scene.saveAnimation(bytes); std::vector> posed; if (!startPose(bytes, scene.meshes, posed)) return false; for (auto& [m, md] : posed) scene.meshes[m] = std::move(md); return true; } } // namespace animbake