51namespace sceneloader {
64constexpr float kEps = 1e-5f;
66std::string normPath(
const std::string &
p) {
68 out.reserve(
p.size());
70 if (
c ==
'\\') out.push_back(
'/');
71 else out.push_back(
c);
76graphics::Graphics *currentGraphics() {
77 return ModuleManager::getInstance<graphics::Graphics>(
"Graphics");
80model3d::ModelLoadOptions toModelOptions(
const LoadOptions &o) {
81 model3d::ModelLoadOptions
m;
82 m.triangulate = o.triangulate;
83 m.generateNormalsIfMissing = o.generateNormalsIfMissing;
84 m.joinIdenticalVertices = o.joinIdenticalVertices;
85 m.flipUVs = o.flipUVs;
86 m.improveCacheLocality = o.improveCacheLocality;
90bool approx(
float a,
float b) {
return std::fabs(
a -
b) < kEps; }
94void decomposeNode(
const aiMatrix4x4 &
m,
float &
x,
float &
y,
float &
z,
float &yaw,
float &pitch,
95 float &roll,
float &sx,
float &sy,
float &sz) {
105 glm::quat q(rot.w, rot.x, rot.y, rot.z);
106 glm::vec3 e = glm::eulerAngles(q);
112std::string uniqueId(
const std::string &base, std::unordered_map<std::string, int> &counts) {
113 std::string
b = base.empty() ?
"node" : base;
116 if (
n > 0)
id =
b +
"_" + std::to_string(
n);
132SamplerSpec samplerFor(
const aiMaterial *mat, aiTextureType
type,
bool wantMips) {
138 int modeU = aiTextureMapMode_Wrap;
139 int modeV = aiTextureMapMode_Wrap;
140 mat->Get(AI_MATKEY_MAPPINGMODE_U(
type, 0), modeU);
141 mat->Get(AI_MATKEY_MAPPINGMODE_V(
type, 0), modeV);
142 s.repeatU = (modeU != aiTextureMapMode_Clamp && modeU != aiTextureMapMode_Mirror);
143 s.repeatV = (modeV != aiTextureMapMode_Clamp && modeV != aiTextureMapMode_Mirror);
146 int mag = 0, min = 0;
147 mat->Get(AI_MATKEY_GLTF_MAPPINGFILTER_MAG(
type, 0), mag);
148 mat->Get(AI_MATKEY_GLTF_MAPPINGFILTER_MIN(
type, 0), min);
149 s.filter = (mag == 9728) ?
"nearest" :
"linear";
164 s.mipmap =
"nearest";
168 s.mipmap = wantMips ?
"linear" :
"none";
174graphics::Texture *resolveTexture(graphics::Graphics *gfx,
const aiScene *scene,
175 const aiMaterial *mat, aiTextureType
type,
178 if (!gfx || !scene || !mat)
return nullptr;
180 if (mat->GetTexture(
type, 0, &path) != AI_SUCCESS)
return nullptr;
181 const char *
p = path.C_Str();
182 if (!
p || !
p[0])
return nullptr;
184 const SamplerSpec
s = samplerFor(mat,
type, wantMips);
185 eve::image::Image::create();
187 const std::string keySuffix =
188 std::string(
s.repeatU ?
"|1" :
"|0") + (
s.
repeatV ?
"1" :
"0") +
"|" +
s.
filter +
"|" +
193 int idx = std::atoi(
p + 1);
194 if (
idx < 0 ||
static_cast<unsigned>(
idx) >= scene->mNumTextures)
return nullptr;
195 const aiTexture *tex = scene->mTextures[
idx];
196 if (!tex || !tex->pcData)
return nullptr;
197 const std::string key =
"*" + std::to_string(
idx) + keySuffix;
198 auto it = cache.find(key);
199 if (it != cache.end())
return it->second;
201 graphics::Texture *t =
nullptr;
202 if (tex->mHeight == 0) {
206 t = gfx->newTextureWithSampler(img,
s.repeatU,
s.repeatV,
s.mips, 8.f,
s.filter,
213 const unsigned w = tex->mWidth;
214 const unsigned h = tex->mHeight;
215 std::vector<uint8_t> rgba(
size_t(
w) *
size_t(
h) * 4);
216 const aiTexel *src = tex->pcData;
217 for (
unsigned i = 0; i <
w *
h; ++i) {
218 rgba[i * 4 + 0] = src[i].r;
219 rgba[i * 4 + 1] = src[i].g;
220 rgba[i * 4 + 2] = src[i].b;
221 rgba[i * 4 + 3] = src[i].a;
224 t = gfx->newTextureWithSampler(&img,
s.repeatU,
s.repeatV,
s.mips, 8.f,
s.filter,
227 if (t) cache[key] = t;
232 const std::string key = normPath(
p) + keySuffix;
233 auto it = cache.find(key);
234 if (it != cache.end())
return it->second;
238 auto pit = predecoded->find(key);
239 if (pit != predecoded->end()) {
240 const SceneLoader::CpuImage &ci = pit->second;
242 const_cast<uint8_t *
>(ci.rgba.data()),
false);
243 graphics::Texture *t = gfx->newTextureWithSampler(&img,
s.repeatU,
s.repeatV,
s.mips,
244 8.f,
s.filter,
s.mipmap);
245 if (t) cache[key] = t;
251 auto *fs = eve::filesystem::Filesystem::create();
252 std::unique_ptr<eve::filesystem::FileData> fd(fs->read(
p));
253 if (fd && fd->getSize() > 0) {
255 graphics::Texture *t = gfx->newTextureWithSampler(img,
s.repeatU,
s.repeatV,
s.mips,
256 8.f,
s.filter,
s.mipmap);
258 if (t) cache[key] = t;
268void collectCpuImages(
const aiScene *scene,
const MeshSlotMap &slots,
bool wantMips,
271 eve::image::Image::create();
272 const aiTextureType kTypes[4] = {aiTextureType_BASE_COLOR, aiTextureType_DIFFUSE,
273 aiTextureType_NORMALS, aiTextureType_HEIGHT};
274 for (
const auto &kv : slots) {
275 for (
const MeshSlot &slot : kv.second) {
276 if (!slot.scene || slot.materialIndex >= scene->mNumMaterials)
continue;
277 const aiMaterial *mat = scene->mMaterials[slot.materialIndex];
279 for (aiTextureType
type : kTypes) {
281 if (mat->GetTexture(
type, 0, &
p) != AI_SUCCESS)
continue;
282 const char *
c =
p.C_Str();
283 if (!
c || !
c[0] ||
c[0] ==
'*')
continue;
284 const SamplerSpec
s = samplerFor(mat,
type, wantMips);
285 const std::string key = normPath(
c) + std::string(
s.repeatU ?
"|1" :
"|0") +
287 if (out.count(key))
continue;
289 auto *fs = eve::filesystem::Filesystem::create();
290 std::unique_ptr<eve::filesystem::FileData> fd(fs->read(
c));
291 if (!fd || fd->getSize() == 0)
continue;
293 eve::image::Image::create()->newImageData(fd.get());
296 SceneLoader::CpuImage ci;
299 const size_t n = size_t(ci.w) * size_t(ci.h) * 4;
300 ci.rgba.assign(
reinterpret_cast<const uint8_t *
>(img->
getData()),
301 reinterpret_cast<const uint8_t *
>(img->
getData()) +
n);
302 out[key] = std::move(ci);
314graphics::Renderable3D *makeRenderable(graphics::Graphics *gfx,
const MeshSlot &slot,
316 if (!gfx || !slot.mesh)
return nullptr;
317 graphics::Mesh *
mesh = gfx->newMeshFromAssimp(*slot.mesh);
318 if (!
mesh)
return nullptr;
319 auto *r = graphics::Renderable3D::create();
320 r->meshRenderer()->visible =
true;
323 const aiScene *scene = slot.scene;
324 const aiMaterial *mat =
nullptr;
325 if (scene && scene->mMaterials && slot.materialIndex < scene->mNumMaterials)
326 mat = scene->mMaterials[slot.materialIndex];
328 aiColor3D base(1.f, 1.f, 1.f);
330 if (mat->Get(AI_MATKEY_BASE_COLOR, base) != AI_SUCCESS)
331 mat->Get(AI_MATKEY_COLOR_DIFFUSE, base);
334 mat->Get(AI_MATKEY_METALLIC_FACTOR,
metallic);
335 mat->Get(AI_MATKEY_ROUGHNESS_FACTOR,
roughness);
340 graphics::Texture *
albedo =
341 resolveTexture(gfx, scene, mat, aiTextureType_BASE_COLOR, textures, mipmaps);
342 if (!
albedo)
albedo = resolveTexture(gfx, scene, mat, aiTextureType_DIFFUSE, textures, mipmaps);
343 graphics::Texture *
normal =
344 resolveTexture(gfx, scene, mat, aiTextureType_NORMALS, textures, mipmaps);
345 graphics::Texture *
height =
346 resolveTexture(gfx, scene, mat, aiTextureType_HEIGHT, textures, mipmaps);
348 r->setTint(base.r, base.g, base.b, 1.f);
355void destroyRenderable(graphics::Renderable3D *r) {
356 if (r) ecs::DestroyEntity(r);
361glm::mat4 nodeLocalMatrix(
const scene::SceneNode &
n) {
363 m = glm::translate(
m, glm::vec3(
n.x,
n.y,
n.z));
364 if (
n.space ==
"2d") {
365 m = glm::rotate(
m,
n.roll, glm::vec3(0.f, 0.f, 1.f));
367 m = glm::rotate(
m,
n.yaw, glm::vec3(0.f, 1.f, 0.f));
368 m = glm::rotate(
m,
n.pitch, glm::vec3(1.f, 0.f, 0.f));
369 m = glm::rotate(
m,
n.roll, glm::vec3(0.f, 0.f, 1.f));
371 m = glm::scale(
m, glm::vec3(
n.sx,
n.sy,
n.sz));
375void fillMeshBoundsFromSlot(scene::SceneNode &
n,
const MeshSlot &slot) {
376 const aiMesh *
mesh = slot.mesh;
377 if (!
mesh || !
mesh->mVertices ||
mesh->mNumVertices == 0)
return;
378 float mn[3] = {std::numeric_limits<float>::max(),
379 std::numeric_limits<float>::max(),
380 std::numeric_limits<float>::max()};
381 float mx[3] = {std::numeric_limits<float>::lowest(),
382 std::numeric_limits<float>::lowest(),
383 std::numeric_limits<float>::lowest()};
384 for (
unsigned i = 0; i <
mesh->mNumVertices; ++i) {
385 const aiVector3D &
v =
mesh->mVertices[i];
386 for (
int c = 0;
c < 3; ++
c) {
387 const float f =
v[
c];
388 mn[
c] = std::min(mn[
c],
f);
389 mx[
c] = std::max(mx[
c],
f);
402void unionChildBounds(scene::SceneHost::Tree &tree,
int nodeIndex) {
403 scene::SceneNode &
n = tree.nodes[size_t(nodeIndex)];
404 for (
int c =
n.firstChild;
c >= 0;
c = tree.nodes[size_t(
c)].nextSibling) {
405 unionChildBounds(tree,
c);
409 float mn[3] = {std::numeric_limits<float>::max(),
410 std::numeric_limits<float>::max(),
411 std::numeric_limits<float>::max()};
412 float mx[3] = {std::numeric_limits<float>::lowest(),
413 std::numeric_limits<float>::lowest(),
414 std::numeric_limits<float>::lowest()};
415 for (
int c =
n.firstChild;
c >= 0;
c = tree.nodes[size_t(
c)].nextSibling) {
416 scene::SceneNode &ch = tree.nodes[size_t(
c)];
417 if (!ch.hasBounds)
continue;
419 const glm::mat4 lm = nodeLocalMatrix(ch);
420 for (
int i = 0; i < 8; ++i) {
421 const glm::vec3
p((i & 1) ? ch.bmaxX : ch.bminX,
422 (i & 2) ? ch.bmaxY : ch.bminY,
423 (i & 4) ? ch.bmaxZ : ch.bminZ);
424 const glm::vec4
w = lm * glm::vec4(
p, 1.f);
425 for (
int k = 0; k < 3; ++k) {
426 mn[k] = std::min(mn[k],
w[k]);
427 mx[k] = std::max(mx[k],
w[k]);
441 n.bminX = std::min(
n.bminX, mn[0]);
442 n.bminY = std::min(
n.bminY, mn[1]);
443 n.bminZ = std::min(
n.bminZ, mn[2]);
444 n.bmaxX = std::max(
n.bmaxX, mx[0]);
445 n.bmaxY = std::max(
n.bmaxY, mx[1]);
446 n.bmaxZ = std::max(
n.bmaxZ, mx[2]);
452void buildNodeRecursive(
const aiScene *scene,
const aiNode *node, scene::NodeDesc &out,
453 MeshSlotMap *slots, std::unordered_map<std::string, int> &counts) {
454 const std::string rawName = (
node &&
node->mName.length) ?
node->mName.C_Str() :
"<root>";
455 const std::string
id = uniqueId(rawName, counts);
462 decomposeNode(
node->mTransformation, out.
x, out.y, out.z, out.yaw, out.pitch, out.roll,
463 out.sx, out.sy, out.sz);
465 for (
unsigned i = 0;
node && i <
node->mNumMeshes; ++i) {
466 const unsigned mi =
node->mMeshes[i];
467 if (mi >= scene->mNumMeshes)
continue;
468 const aiMesh *
mesh = scene->mMeshes[mi];
469 if (!
mesh ||
mesh->mNumFaces == 0)
continue;
471 const std::string cid =
id +
"_mesh" + std::to_string(i);
472 scene::NodeDesc
child;
475 child.name = std::string(
"mesh") + std::to_string(i);
478 out.children.push_back(std::move(child));
484 s.materialIndex =
mesh->mMaterialIndex;
485 (*slots)[cid].push_back(std::move(
s));
489 for (
unsigned c = 0;
node &&
c <
node->mNumChildren; ++
c) {
490 scene::NodeDesc
child;
491 buildNodeRecursive(scene,
node->mChildren[
c], child, slots, counts);
492 out.children.push_back(std::move(child));
496bool propsChanged(
const scene::SceneNode &
n,
const scene::NodeDesc &
d) {
497 if (
n.visible !=
d.visible)
return true;
498 return !(approx(
n.x,
d.x) && approx(
n.y,
d.y) && approx(
n.z,
d.z) && approx(
n.yaw,
d.yaw) &&
499 approx(
n.pitch,
d.pitch) && approx(
n.roll,
d.roll) && approx(
n.sx,
d.sx) &&
500 approx(
n.sy,
d.sy) && approx(
n.sz,
d.sz));
504void walkNew(
const scene::NodeDesc &
d,
const std::string &parentId,
505 const std::unordered_map<std::string, const scene::SceneNode *> &oldNodes,
506 const std::unordered_map<std::string, std::string> &oldParent, SceneDiff &out) {
507 auto it = oldNodes.find(
d.id);
508 if (it == oldNodes.end()) {
512 auto pit = oldParent.find(
d.id);
513 const std::string op = (pit != oldParent.end()) ? pit->second :
"";
514 if (op != parentId) {
517 }
else if (propsChanged(*it->second,
d)) {
522 for (
const auto &child :
d.
children) walkNew(
child,
d.
id, oldNodes, oldParent, out);
527void importLights(
const aiScene *scene, std::vector<graphics::Light3D *> &out) {
529 for (
unsigned i = 0; i < scene->mNumLights; ++i) {
530 const aiLight *l = scene->mLights[i];
532 std::string
type =
"point";
534 case aiLightSource_DIRECTIONAL:
537 case aiLightSource_POINT:
538 case aiLightSource_SPOT:
539 case aiLightSource_AREA:
546 light->setDirection(l->mDirection.x, l->mDirection.y, l->mDirection.z);
548 light->setPosition(l->mPosition.x, l->mPosition.y, l->mPosition.z);
550 if (l->mAttenuationQuadratic > 1e-6f)
551 radius = 1.f / std::sqrt(l->mAttenuationQuadratic);
552 light->setRadius(radius);
554 light->setColor(l->mColorDiffuse.r, l->mColorDiffuse.g, l->mColorDiffuse.b, 1.f);
555 out.push_back(light);
559void importCameras(
const aiScene *scene, std::vector<graphics::Camera3D *> &out) {
561 for (
unsigned i = 0; i < scene->mNumCameras; ++i) {
562 const aiCamera *
c = scene->mCameras[i];
565 cam->setActive(
false);
566 cam->setEye(
c->mPosition.x,
c->mPosition.y,
c->mPosition.z);
567 cam->setTarget(
c->mLookAt.x,
c->mLookAt.y,
c->mLookAt.z);
568 cam->setUp(
c->mUp.x,
c->mUp.y,
c->mUp.z);
569 cam->setFov(glm::degrees(
c->mHorizontalFOV));
574void importAnimations(
const aiScene *scene,
const LoadOptions &options,
575 animation::AnimSkeleton **skeletonOut,
576 std::vector<animation::AnimClip *> &clips) {
577 if (!scene || !options.importAnimations || scene->mNumAnimations == 0)
return;
579 if (!skeleton)
return;
580 *skeletonOut = skeleton;
581 for (
unsigned i = 0; i < scene->mNumAnimations; ++i) {
583 if (clip) clips.push_back(clip);
593 if (!scene || !scene->mRootNode)
return root;
594 std::unordered_map<std::string, int> counts;
595 buildNodeRecursive(scene, scene->mRootNode, root, slotsOut, counts);
601 std::unordered_map<std::string, const scene::SceneNode *> oldNodes;
602 std::unordered_map<std::string, std::string> oldParent;
604 auto t =
host->tree();
605 for (
size_t i = 0; i < t->nodes.size(); ++i) {
608 oldParent[
n.id] = (
n.parent >= 0) ? t->nodes[
size_t(
n.parent)].
id :
"";
613 std::unordered_set<std::string> newIds;
616 for (
const auto &
c :
d.children) collect(
c);
621 for (
const auto &kv : oldNodes) {
622 if (!newIds.count(kv.first)) {
629 walkNew(newRoot,
"", oldNodes, oldParent, out);
640 std::unordered_map<std::string, const scene::SceneNode *> oldNodes;
642 auto t =
host->tree();
643 for (
const auto &
n : t->nodes) oldNodes[
n.id] = &
n;
647 std::unordered_set<std::string> newIds;
650 for (
const auto &
c :
d.children) collect(
c);
655 for (
const auto &kv : oldNodes) {
656 if (!newIds.count(kv.first)) {
665 std::vector<scene::SceneNode> nodes;
666 nodes.reserve(newIds.size());
668 int parentIndex) ->
int {
669 const int idx = int(nodes.size());
672 n.key =
d.key.empty() ?
d.id :
d.key;
673 n.name =
d.name.empty() ?
d.id :
d.name;
674 n.space =
d.space.empty() ?
"3d" :
d.space;
675 n.visible =
d.visible;
686 n.world = glm::mat4(1.f);
689 n.parent = parentIndex;
691 auto oldIt = oldNodes.find(
d.id);
692 if (oldIt != oldNodes.end()) {
693 n.links = oldIt->second->links;
694 n.objectId = oldIt->second->objectId;
695 n.bminX = oldIt->second->bminX;
696 n.bminY = oldIt->second->bminY;
697 n.bminZ = oldIt->second->bminZ;
698 n.bmaxX = oldIt->second->bmaxX;
699 n.bmaxY = oldIt->second->bmaxY;
700 n.bmaxZ = oldIt->second->bmaxZ;
701 n.hasBounds = oldIt->second->hasBounds;
704 nodes.push_back(std::move(
n));
708 for (
const auto &
c :
d.children) {
709 const int ci = build(
c,
idx);
710 if (firstChild < 0) firstChild = ci;
711 if (prevChild >= 0) nodes[size_t(prevChild)].nextSibling = ci;
714 nodes[size_t(
idx)].firstChild = firstChild;
717 const int root = build(newRoot, -1);
722 for (
auto &
n : nodes) {
723 if (!
n.links.empty())
continue;
724 auto sit = slots->find(
n.id);
725 if (sit == slots->end() || sit->second.empty())
continue;
728 r = makeRenderable(gfx, sit->second[0], unused,
true);
738 host->tree()->nodes = std::move(nodes);
739 host->tree()->root = root;
748 auto t =
host->tree();
749 for (
auto &
n : t->nodes) {
750 auto it = slots.find(
n.id);
751 if (it == slots.end() || it->second.empty())
continue;
752 fillMeshBoundsFromSlot(
n, it->second[0]);
754 if (t->root >= 0) unionChildBounds(*t, t->root);
762 TextureCache &textures, MeshCache &shared,
763 const CpuImageMap *predecoded) {
765 for (
const auto &kv : slots) {
767 if (!
n || kv.second.empty())
continue;
769 const MeshSlot &slot = kv.second[0];
771 auto it = shared.find(slot.
mesh);
783 auto *r = graphics::Renderable3D::create();
784 r->meshRenderer()->visible =
true;
787 const aiScene *scene = slot.
scene;
788 const aiMaterial *mat =
nullptr;
789 if (scene && scene->mMaterials && slot.
materialIndex < scene->mNumMaterials)
791 aiColor3D base(1.f, 1.f, 1.f);
793 if (mat->Get(AI_MATKEY_BASE_COLOR, base) != AI_SUCCESS)
794 mat->Get(AI_MATKEY_COLOR_DIFFUSE, base);
797 mat->Get(AI_MATKEY_METALLIC_FACTOR,
metallic);
798 mat->Get(AI_MATKEY_ROUGHNESS_FACTOR,
roughness);
802 graphics::Texture *
albedo = resolveTexture(gfx, scene, mat, aiTextureType_BASE_COLOR,
803 textures, options.
mipmaps, predecoded);
805 albedo = resolveTexture(gfx, scene, mat, aiTextureType_DIFFUSE, textures,
807 graphics::Texture *
normal = resolveTexture(gfx, scene, mat, aiTextureType_NORMALS,
808 textures, options.
mipmaps, predecoded);
809 graphics::Texture *
height = resolveTexture(gfx, scene, mat, aiTextureType_HEIGHT,
810 textures, options.
mipmaps, predecoded);
811 r->setTint(base.r, base.g, base.b, 1.f);
819bool SceneLoader::decode(
const std::string &path,
const LoadOptions &options, DecodedScene *out) {
825 auto *mod3d = ModuleManager::getInstance<model3d::Model3D>(
"Model3D");
826 if (!mod3d) mod3d = model3d::Model3D::create();
827 model3d::ModelData *md =
nullptr;
829 md = mod3d->newModelDataFromFile(path, toModelOptions(options));
833 if (!md)
return false;
835 out->path = normPath(path);
837 out->options = options;
839 collectCpuImages(md->getScene(), out->slots, options.mipmaps, out->cpuImages);
843scene::SceneHost *SceneLoader::mount(DecodedScene &
d) {
847 graphics::Graphics *gfx = currentGraphics();
852 linkMeshNodes(
host,
d.slots, gfx,
d.options, textures_, shared, &
d.cpuImages);
853 if (
d.options.importLights) importLights(
d.md->getScene(),
d.lights);
854 if (
d.options.importCameras) importCameras(
d.md->getScene(),
d.cameras);
855 importAnimations(
d.md->getScene(),
d.options, &
d.skeleton,
d.clips);
859 scenes_[
d.path] =
Loaded{
d.path,
host, gfx,
d.options, std::move(
d.lights),
860 std::move(
d.cameras),
d.skeleton, std::move(
d.clips)};
864void SceneLoader::clearTextures() {
865 for (
auto &kv : textures_) delete kv.second;
873 const std::string key = normPath(path);
875 auto warm = prewarmed_.find(key);
876 if (warm != prewarmed_.end()) {
877 d = std::move(warm->second);
878 prewarmed_.erase(warm);
879 }
else if (!decode(path, options, &
d)) {
882 if (!linkRenderables) {
886 scenes_[
d.path] =
Loaded{
d.path,
host,
nullptr, options, {}, {},
nullptr, {}};
894 return load(path,
true, options);
898 const std::string key = normPath(path);
899 auto it = scenes_.find(key);
900 if (it == scenes_.end()) {
908 if (!decode(path, options, &
d))
return false;
911 if (out) *out =
diff;
915 linkMeshNodes(ld.host,
d.slots, ld.gfx, options, textures_, shared, &
d.cpuImages);
922 const std::string key = normPath(path);
924 auto *mod3d = ModuleManager::getInstance<model3d::Model3D>(
"Model3D");
925 if (!mod3d) mod3d = model3d::Model3D::create();
928 md = mod3d->newModelDataFromFile(path);
936 auto it = scenes_.find(key);
937 if (it != scenes_.end() && it->second.host) {
941 std::function<void(
const scene::NodeDesc &,
const std::string &)> walk =
945 for (
const auto &
c :
n.children) walk(
c,
n.id);
953 auto it = scenes_.find(normPath(path));
954 return (it != scenes_.end()) ? it->second.host :
nullptr;
959 return h ?
h->getNodeCount() : 0;
965 auto it = scenes_.find(normPath(path));
966 if (it == scenes_.end())
return;
967 if (it->second.host) {
968 auto t = it->second.host->tree();
969 for (
auto &
n : t->nodes) {
975 ecs::DestroyEntity(it->second.host);
979 delete it->second.skeleton;
988 const std::string key = normPath(path);
990 std::lock_guard<std::mutex> lock(pendingMu_);
991 for (
const auto &
p : pending_)
992 if (
p.path == key)
return false;
994 if (!pool_) pool_ = std::make_shared<thread::ThreadPool>(2);
997 pool_->submit([self, key, options, done]() {
999 if (self->decode(key, options, &
d)) {
1000 std::lock_guard<std::mutex> lock(self->pendingMu_);
1002 self->pending_.push_back(std::move(d));
1009 std::vector<DecodedScene> ready;
1011 std::lock_guard<std::mutex> lock(pendingMu_);
1012 ready.swap(pending_);
1014 for (
auto &
d : ready) {
1015 if (!
d.md)
continue;
1016 if (
d.prewarmOnly) {
1017 prewarmed_[
d.path] = std::move(
d);
1021 if (
d.done)
d.done(
h);
1023 return static_cast<int>(ready.size());
1027 const std::string key = normPath(path);
1029 std::lock_guard<std::mutex> lock(pendingMu_);
1030 if (prewarmed_.count(key))
return false;
1031 for (
const auto &
p : pending_)
1032 if (
p.path == key)
return false;
1034 if (!pool_) pool_ = std::make_shared<thread::ThreadPool>(2);
1037 pool_->submit([self, key, options]() {
1039 if (!self->decode(key, options, &
d))
return;
1040 d.prewarmOnly =
true;
1041 std::lock_guard<std::mutex> lock(self->pendingMu_);
1042 self->pending_.push_back(std::move(
d));
1048 return prewarmed_.count(normPath(path)) > 0;
1052 auto it = prewarmed_.find(normPath(path));
1053 if (it == prewarmed_.end())
return;
1054 prewarmed_.erase(it);
1058 std::lock_guard<std::mutex> lock(pendingMu_);
1059 return static_cast<int>(pending_.size());
1065 auto it = scenes_.find(normPath(path));
1066 return (it != scenes_.end()) ?
static_cast<int>(it->second.lights.size()) : 0;
1070 auto it = scenes_.find(normPath(path));
1071 if (it == scenes_.end() || index < 0 ||
1072 static_cast<size_t>(index) >= it->second.lights.size())
1074 return it->second.lights[size_t(index)];
1078 auto it = scenes_.find(normPath(path));
1079 return (it != scenes_.end()) ?
static_cast<int>(it->second.cameras.size()) : 0;
1083 auto it = scenes_.find(normPath(path));
1084 if (it == scenes_.end() || index < 0 ||
1085 static_cast<size_t>(index) >= it->second.cameras.size())
1087 return it->second.cameras[size_t(index)];
1091 auto it = scenes_.find(normPath(path));
1092 return (it != scenes_.end()) ?
static_cast<int>(it->second.clips.size()) : 0;
1096 auto it = scenes_.find(normPath(path));
1097 return (it != scenes_.end()) ? it->second.skeleton :
nullptr;
1101 auto it = scenes_.find(normPath(path));
1102 if (it == scenes_.end() || index < 0 ||
1103 static_cast<size_t>(index) >= it->second.clips.size())
1105 return it->second.clips[size_t(index)];
1108void SceneLoader::expose(ssq::Table &table) {
1109 auto cls = table.addClass(
name, SceneLoader::create,
false);
1113void SceneLoader::expose(ssq::Class &
cls) {