27#include <assimp/scene.h>
28#include <assimp/mesh.h>
29#include <assimp/material.h>
30#include <assimp/matrix4x4.h>
31#include <assimp/quaternion.h>
32#include <assimp/vector3.h>
33#include <assimp/texture.h>
34#include <assimp/light.h>
35#include <assimp/camera.h>
36#include <assimp/GltfMaterial.h>
39#include <glm/gtx/matrix_decompose.hpp>
40#include <glm/gtc/matrix_transform.hpp>
41#include <glm/gtc/quaternion.hpp>
45#include <simplesquirrel/simplesquirrel.hpp>
52#include <unordered_set>
55namespace sceneloader {
57SceneLoader::DecodedScene::DecodedScene() =
default;
58SceneLoader::DecodedScene::~DecodedScene() =
default;
59SceneLoader::DecodedScene::DecodedScene(DecodedScene &&) noexcept = default;
60SceneLoader::DecodedScene &SceneLoader::DecodedScene::operator=(DecodedScene &&) noexcept = default;
76constexpr float kEps = 1e-5f;
80 if (!result.
ok())
return nullptr;
81 return result.
value();
84std::string normPath(
const std::string &
p) {
86 out.reserve(
p.size());
88 if (
c ==
'\\') out.push_back(
'/');
89 else out.push_back(
c);
94graphics::Graphics *currentGraphics() {
95 return ModuleManager::getInstance<graphics::Graphics>(
"Graphics");
98model3d::ModelLoadOptions toModelOptions(
const LoadOptions &o) {
99 model3d::ModelLoadOptions
m;
100 m.triangulate = o.triangulate;
101 m.generateNormalsIfMissing = o.generateNormalsIfMissing;
102 m.joinIdenticalVertices = o.joinIdenticalVertices;
103 m.flipUVs = o.flipUVs;
104 m.improveCacheLocality = o.improveCacheLocality;
108bool approx(
float a,
float b) {
return std::fabs(
a -
b) < kEps; }
112void decomposeNode(
const aiMatrix4x4 &
m,
float &
x,
float &
y,
float &
z,
float &
yaw,
float &
pitch,
113 float &
roll,
float &
sx,
float &
sy,
float &
sz) {
123 glm::quat
q(rot.w, rot.x, rot.y, rot.z);
124 glm::vec3 e = glm::eulerAngles(
q);
130std::string uniqueId(
const std::string &base, std::unordered_map<std::string, int> &
counts) {
131 std::string
b = base.empty() ?
"node" : base;
134 if (
n > 0)
id =
b +
"_" + std::to_string(
n);
139bool startsWithInsensitive(
const std::string &
value,
const char *prefix) {
140 const size_t n = std::char_traits<char>::length(prefix);
141 if (
value.size() <
n)
return false;
142 for (
size_t i = 0; i <
n; ++i)
143 if (std::tolower(
static_cast<unsigned char>(
value[i])) !=
144 std::tolower(
static_cast<unsigned char>(prefix[i])))
149int lodFromName(
const std::string &
name) {
150 const size_t p =
name.rfind(
"_LOD");
151 if (
p == std::string::npos ||
p + 4 >=
name.size())
return -1;
153 const long value = std::strtol(
name.c_str() +
p + 4, &
end, 10);
157LoadOptions presetOptions(
const std::string &
name) {
159 if (
name ==
"quality" ||
name ==
"balanced" ||
name.empty())
return o;
160 if (
name ==
"mobile") {
161 o.importLights =
false;
162 o.importCameras =
false;
163 o.importAnimations =
false;
167 o.generateNormalsIfMissing =
false;
168 o.joinIdenticalVertices =
false;
170 o.improveCacheLocality =
false;
171 o.sharedMeshes =
false;
173 o.importLights =
false;
174 o.importCameras =
false;
175 o.importAnimations =
false;
192SamplerSpec samplerFor(
const aiMaterial *mat, aiTextureType
type,
bool wantMips) {
198 int modeU = aiTextureMapMode_Wrap;
199 int modeV = aiTextureMapMode_Wrap;
200 mat->Get(AI_MATKEY_MAPPINGMODE_U(
type, 0), modeU);
201 mat->Get(AI_MATKEY_MAPPINGMODE_V(
type, 0), modeV);
202 s.repeatU = (modeU != aiTextureMapMode_Clamp && modeU != aiTextureMapMode_Mirror);
203 s.repeatV = (modeV != aiTextureMapMode_Clamp && modeV != aiTextureMapMode_Mirror);
206 int mag = 0,
min = 0;
207 mat->Get(AI_MATKEY_GLTF_MAPPINGFILTER_MAG(
type, 0), mag);
208 mat->Get(AI_MATKEY_GLTF_MAPPINGFILTER_MIN(
type, 0),
min);
209 s.filter = (mag == 9728) ?
"nearest" :
"linear";
224 s.mipmap =
"nearest";
228 s.mipmap = wantMips ?
"linear" :
"none";
234graphics::Texture *resolveTexture(graphics::Graphics *gfx,
const aiScene *scene,
235 const aiMaterial *mat, aiTextureType
type,
238 if (!gfx || !scene || !mat)
return nullptr;
240 if (mat->GetTexture(
type, 0, &
path) != AI_SUCCESS)
return nullptr;
241 const char *
p =
path.C_Str();
242 if (!
p || !
p[0])
return nullptr;
244 const SamplerSpec
s = samplerFor(mat,
type, wantMips);
245 auto *
const imageModule = eve::image::Image::create();
247 const std::string keySuffix =
248 std::string(
s.repeatU ?
"|1" :
"|0") + (
s.
repeatV ?
"1" :
"0") +
"|" +
s.
filter +
"|" +
253 int idx = std::atoi(
p + 1);
254 if (
idx < 0 ||
static_cast<unsigned>(
idx) >=
scene->mNumTextures)
return nullptr;
255 const aiTexture *tex =
scene->mTextures[
idx];
256 if (!tex || !tex->pcData)
return nullptr;
257 const std::string
key =
"*" + std::to_string(
idx) + keySuffix;
258 auto it = cache.find(
key);
259 if (it != cache.end())
return it->second;
261 graphics::Texture *
t =
nullptr;
262 if (tex->mHeight == 0) {
266 t = gfx->newTextureWithSampler(img,
s.repeatU,
s.repeatV,
s.mips, 8.f,
s.filter,
273 const unsigned w = tex->mWidth;
274 const unsigned h = tex->mHeight;
275 std::vector<uint8_t> rgba(
size_t(
w) *
size_t(
h) * 4);
276 const aiTexel *src = tex->pcData;
277 for (
unsigned i = 0; i <
w *
h; ++i) {
278 rgba[i * 4 + 0] = src[i].r;
279 rgba[i * 4 + 1] = src[i].g;
280 rgba[i * 4 + 2] = src[i].b;
281 rgba[i * 4 + 3] = src[i].a;
284 t = gfx->newTextureWithSampler(&img,
s.repeatU,
s.repeatV,
s.mips, 8.f,
s.filter,
287 if (
t) cache[
key] =
t;
292 const std::string
key = normPath(
p) + keySuffix;
293 auto it = cache.find(
key);
294 if (it != cache.end())
return it->second;
298 auto pit = predecoded->find(
key);
299 if (pit != predecoded->end()) {
300 const SceneLoader::CpuImage &ci = pit->second;
302 const_cast<uint8_t *
>(ci.rgba.data()),
false);
303 graphics::Texture *
t = gfx->newTextureWithSampler(&img,
s.repeatU,
s.repeatV,
s.mips,
304 8.f,
s.filter,
s.mipmap);
305 if (
t) cache[
key] =
t;
311 auto *fs = eve::filesystem::Filesystem::create();
312 std::unique_ptr<eve::filesystem::FileData> fd(fs->read(
p));
313 if (fd && fd->getSize() > 0) {
315 graphics::Texture *
t = gfx->newTextureWithSampler(img,
s.repeatU,
s.repeatV,
s.mips,
316 8.f,
s.filter,
s.mipmap);
318 if (
t) cache[
key] =
t;
328void collectCpuImages(
const aiScene *scene,
const MeshSlotMap &
slots,
bool wantMips,
331 auto *
const imageModule = eve::image::Image::create();
332 const aiTextureType kTypes[4] = {aiTextureType_BASE_COLOR, aiTextureType_DIFFUSE,
333 aiTextureType_NORMALS, aiTextureType_HEIGHT};
334 for (
const auto &kv :
slots) {
335 for (
const MeshSlot &slot : kv.
second) {
336 if (!slot.scene || slot.materialIndex >=
scene->mNumMaterials)
continue;
337 const aiMaterial *mat =
scene->mMaterials[slot.materialIndex];
339 for (aiTextureType
type : kTypes) {
341 if (mat->GetTexture(
type, 0, &
p) != AI_SUCCESS)
continue;
342 const char *
c =
p.C_Str();
343 if (!
c || !
c[0] ||
c[0] ==
'*')
continue;
344 const SamplerSpec
s = samplerFor(mat,
type, wantMips);
345 const std::string
key = normPath(
c) + std::string(
s.repeatU ?
"|1" :
"|0") +
347 if (out.count(
key))
continue;
349 auto *fs = eve::filesystem::Filesystem::create();
350 std::unique_ptr<eve::filesystem::FileData> fd(fs->read(
c));
351 if (!fd || fd->getSize() == 0)
continue;
355 SceneLoader::CpuImage ci;
358 const size_t n = size_t(ci.w) * size_t(ci.h) * 4;
359 ci.rgba.assign(
reinterpret_cast<const uint8_t *
>(img->
getData()),
360 reinterpret_cast<const uint8_t *
>(img->
getData()) +
n);
361 out[
key] = std::move(ci);
373graphics::Renderable3D *makeRenderable(graphics::Graphics *gfx,
const MeshSlot &slot,
375 if (!gfx || !slot.mesh)
return nullptr;
376 graphics::Mesh *
mesh = gfx->newMeshFromAssimp(*slot.mesh);
377 if (!
mesh)
return nullptr;
378 auto *
r = graphics::Renderable3D::create();
379 r->meshRenderer()->visible =
true;
382 const aiScene *
scene = slot.scene;
383 const aiMaterial *mat =
nullptr;
384 if (scene &&
scene->mMaterials && slot.materialIndex <
scene->mNumMaterials)
385 mat =
scene->mMaterials[slot.materialIndex];
387 aiColor3D base(1.f, 1.f, 1.f);
389 if (mat->Get(AI_MATKEY_BASE_COLOR, base) != AI_SUCCESS)
390 mat->Get(AI_MATKEY_COLOR_DIFFUSE, base);
393 mat->Get(AI_MATKEY_METALLIC_FACTOR,
metallic);
394 mat->Get(AI_MATKEY_ROUGHNESS_FACTOR,
roughness);
399 graphics::Texture *
albedo =
400 resolveTexture(gfx, scene, mat, aiTextureType_BASE_COLOR, textures, mipmaps);
401 if (!
albedo)
albedo = resolveTexture(gfx, scene, mat, aiTextureType_DIFFUSE, textures, mipmaps);
402 graphics::Texture *
normal =
403 resolveTexture(gfx, scene, mat, aiTextureType_NORMALS, textures, mipmaps);
404 graphics::Texture *
height =
405 resolveTexture(gfx, scene, mat, aiTextureType_HEIGHT, textures, mipmaps);
407 r->setTint(base.r, base.g, base.b, 1.f);
414void destroyRenderable(graphics::Renderable3D *
r) {
415 if (
r) ecs::DestroyEntity(
r);
420glm::mat4 nodeLocalMatrix(
const scene::SceneNode &
n) {
422 m = glm::translate(
m, glm::vec3(
n.x,
n.y,
n.z));
423 if (
n.space ==
"2d") {
424 m = glm::rotate(
m,
n.roll, glm::vec3(0.f, 0.f, 1.f));
426 m = glm::rotate(
m,
n.yaw, glm::vec3(0.f, 1.f, 0.f));
427 m = glm::rotate(
m,
n.pitch, glm::vec3(1.f, 0.f, 0.f));
428 m = glm::rotate(
m,
n.roll, glm::vec3(0.f, 0.f, 1.f));
430 m = glm::scale(
m, glm::vec3(
n.sx,
n.sy,
n.sz));
434void fillMeshBoundsFromSlot(scene::SceneNode &
n,
const MeshSlot &slot) {
435 const aiMesh *
mesh = slot.mesh;
436 if (!
mesh || !
mesh->mVertices ||
mesh->mNumVertices == 0)
return;
437 float mn[3] = {std::numeric_limits<float>::max(),
438 std::numeric_limits<float>::max(),
439 std::numeric_limits<float>::max()};
440 float mx[3] = {std::numeric_limits<float>::lowest(),
441 std::numeric_limits<float>::lowest(),
442 std::numeric_limits<float>::lowest()};
443 for (
unsigned i = 0; i <
mesh->mNumVertices; ++i) {
444 const aiVector3D &
v =
mesh->mVertices[i];
445 for (
int c = 0;
c < 3; ++
c) {
446 const float f =
v[
c];
447 mn[
c] = std::min(mn[
c],
f);
448 mx[
c] = std::max(mx[
c],
f);
461void unionChildBounds(scene::SceneHost::Tree &tree,
int nodeIndex) {
462 scene::SceneNode &
n = tree.nodes[size_t(
nodeIndex)];
463 for (
int c =
n.firstChild;
c >= 0;
c = tree.nodes[size_t(
c)].nextSibling) {
464 unionChildBounds(tree,
c);
468 float mn[3] = {std::numeric_limits<float>::max(),
469 std::numeric_limits<float>::max(),
470 std::numeric_limits<float>::max()};
471 float mx[3] = {std::numeric_limits<float>::lowest(),
472 std::numeric_limits<float>::lowest(),
473 std::numeric_limits<float>::lowest()};
474 for (
int c =
n.firstChild;
c >= 0;
c = tree.nodes[size_t(
c)].nextSibling) {
475 scene::SceneNode &ch = tree.nodes[size_t(
c)];
476 if (!ch.hasBounds)
continue;
478 const glm::mat4 lm = nodeLocalMatrix(ch);
479 for (
int i = 0; i < 8; ++i) {
480 const glm::vec3
p((i & 1) ? ch.bmaxX : ch.bminX,
481 (i & 2) ? ch.bmaxY : ch.bminY,
482 (i & 4) ? ch.bmaxZ : ch.bminZ);
483 const glm::vec4
w = lm * glm::vec4(
p, 1.f);
484 for (
int k = 0; k < 3; ++k) {
485 mn[k] = std::min(mn[k],
w[k]);
486 mx[k] = std::max(mx[k],
w[k]);
500 n.bminX = std::min(
n.bminX, mn[0]);
501 n.bminY = std::min(
n.bminY, mn[1]);
502 n.bminZ = std::min(
n.bminZ, mn[2]);
503 n.bmaxX = std::max(
n.bmaxX, mx[0]);
504 n.bmaxY = std::max(
n.bmaxY, mx[1]);
505 n.bmaxZ = std::max(
n.bmaxZ, mx[2]);
511void buildNodeRecursive(
const aiScene *scene,
const aiNode *
node, scene::NodeDesc &out,
513 const std::string rawName = (
node &&
node->mName.length) ?
node->mName.C_Str() :
"<root>";
514 const std::string
id = uniqueId(rawName,
counts);
521 if (startsWithInsensitive(rawName,
"SOCKET_")) out.tags.push_back(
"model-socket");
522 if (startsWithInsensitive(rawName,
"UCX_") || startsWithInsensitive(rawName,
"UBX_") ||
523 startsWithInsensitive(rawName,
"USP_") || startsWithInsensitive(rawName,
"UCP_")) {
524 out.tags.push_back(
"collision");
527 const int lod = lodFromName(rawName);
529 out.tags.push_back(
"lod");
530 out.tags.push_back(
"lod:" + std::to_string(
lod));
531 out.visible =
lod == 0;
533 decomposeNode(
node->mTransformation, out.x, out.y, out.z, out.yaw, out.pitch, out.roll,
534 out.sx, out.sy, out.sz);
536 for (
unsigned i = 0;
node && i <
node->mNumMeshes; ++i) {
537 const unsigned mi =
node->mMeshes[i];
538 if (mi >=
scene->mNumMeshes)
continue;
540 if (!
mesh ||
mesh->mNumFaces == 0)
continue;
542 const std::string cid =
id +
"_mesh" + std::to_string(i);
543 scene::NodeDesc
child;
546 child.name = std::string(
"mesh") + std::to_string(i);
549 child.tags = out.tags;
550 out.
children.push_back(std::move(child));
556 s.materialIndex =
mesh->mMaterialIndex;
557 (*slots)[cid].push_back(std::move(
s));
561 for (
unsigned c = 0;
node &&
c <
node->mNumChildren; ++
c) {
562 scene::NodeDesc
child;
564 out.children.push_back(std::move(child));
568bool propsChanged(
const scene::SceneNode &
n,
const scene::NodeDesc &
d) {
569 if (
n.visible !=
d.visible)
return true;
570 return !(approx(
n.x,
d.x) && approx(
n.y,
d.y) && approx(
n.z,
d.z) && approx(
n.yaw,
d.yaw) &&
571 approx(
n.pitch,
d.pitch) && approx(
n.roll,
d.roll) && approx(
n.sx,
d.sx) &&
572 approx(
n.sy,
d.sy) && approx(
n.sz,
d.sz));
576void walkNew(
const scene::NodeDesc &
d,
const std::string &
parentId,
577 const std::unordered_map<std::string, const scene::SceneNode *> &oldNodes,
578 const std::unordered_map<std::string, std::string> &oldParent, SceneDiff &out) {
579 auto it = oldNodes.find(
d.id);
580 if (it == oldNodes.end()) {
584 auto pit = oldParent.find(
d.id);
585 const std::string op = (pit != oldParent.end()) ? pit->second :
"";
589 }
else if (propsChanged(*it->second,
d)) {
594 for (
const auto &child :
d.
children) walkNew(
child,
d.
id, oldNodes, oldParent, out);
599void importLights(
const aiScene *scene, std::vector<graphics::Light3D *> &out) {
601 for (
unsigned i = 0; i <
scene->mNumLights; ++i) {
602 const aiLight *l =
scene->mLights[i];
604 std::string
type =
"point";
606 case aiLightSource_DIRECTIONAL:
609 case aiLightSource_POINT:
610 case aiLightSource_SPOT:
611 case aiLightSource_AREA:
618 light->setDirection(l->mDirection.x, l->mDirection.y, l->mDirection.z);
620 light->setPosition(l->mPosition.x, l->mPosition.y, l->mPosition.z);
622 if (l->mAttenuationQuadratic > 1e-6f)
623 radius = 1.f / std::sqrt(l->mAttenuationQuadratic);
626 light->setColor(l->mColorDiffuse.r, l->mColorDiffuse.g, l->mColorDiffuse.b, 1.f);
627 out.push_back(
light);
631void importCameras(
const aiScene *scene, std::vector<graphics::Camera3D *> &out) {
633 for (
unsigned i = 0; i <
scene->mNumCameras; ++i) {
634 const aiCamera *
c =
scene->mCameras[i];
637 cam->setActive(
false);
638 cam->setEye(
c->mPosition.x,
c->mPosition.y,
c->mPosition.z);
639 cam->setTarget(
c->mLookAt.x,
c->mLookAt.y,
c->mLookAt.z);
640 cam->setUp(
c->mUp.x,
c->mUp.y,
c->mUp.z);
641 cam->setFov(glm::degrees(
c->mHorizontalFOV));
646void importAnimations(
const aiScene *scene,
const LoadOptions &
options,
647 animation::AnimSkeleton **skeletonOut,
648 std::vector<animation::AnimClip *> &clips) {
649 if (!scene || !
options.importAnimations ||
scene->mNumAnimations == 0)
return;
651 if (!skeleton)
return;
652 *skeletonOut = skeleton;
653 for (
unsigned i = 0; i <
scene->mNumAnimations; ++i) {
666 std::unordered_map<std::string, int>
counts;
673 std::unordered_map<std::string, const scene::SceneNode *> oldNodes;
674 std::unordered_map<std::string, std::string> oldParent;
676 auto t =
host->tree();
677 for (
size_t i = 0; i <
t->nodes.size(); ++i) {
680 oldParent[
n.id] = (
n.parent >= 0) ?
t->nodes[
size_t(
n.parent)].
id :
"";
685 std::unordered_set<std::string> newIds;
688 for (
const auto &
c :
d.children) collect(
c);
693 for (
const auto &kv : oldNodes) {
694 if (!newIds.count(kv.first)) {
701 walkNew(newRoot,
"", oldNodes, oldParent, out);
712 std::unordered_map<std::string, const scene::SceneNode *> oldNodes;
714 auto t =
host->tree();
715 for (
const auto &
n :
t->nodes) oldNodes[
n.id] = &
n;
719 std::unordered_set<std::string> newIds;
722 for (
const auto &
c :
d.children) collect(
c);
727 for (
const auto &kv : oldNodes) {
728 if (!newIds.count(kv.first)) {
737 std::vector<scene::SceneNode>
nodes;
738 nodes.reserve(newIds.size());
740 int parentIndex) ->
int {
744 n.key =
d.key.empty() ?
d.id :
d.key;
745 n.name =
d.name.empty() ?
d.id :
d.name;
746 n.space =
d.space.empty() ?
"3d" :
d.space;
747 n.visible =
d.visible;
758 n.world = glm::mat4(1.f);
761 n.parent = parentIndex;
763 auto oldIt = oldNodes.find(
d.id);
764 if (oldIt != oldNodes.end()) {
765 n.links = oldIt->second->links;
766 n.objectId = oldIt->second->objectId;
767 n.bminX = oldIt->second->bminX;
768 n.bminY = oldIt->second->bminY;
769 n.bminZ = oldIt->second->bminZ;
770 n.bmaxX = oldIt->second->bmaxX;
771 n.bmaxY = oldIt->second->bmaxY;
772 n.bmaxZ = oldIt->second->bmaxZ;
773 n.hasBounds = oldIt->second->hasBounds;
776 nodes.push_back(std::move(
n));
780 for (
const auto &
c :
d.children) {
781 const int ci = build(
c,
idx);
782 if (firstChild < 0) firstChild = ci;
783 if (prevChild >= 0)
nodes[size_t(prevChild)].nextSibling = ci;
786 nodes[size_t(
idx)].firstChild = firstChild;
789 const int root = build(newRoot, -1);
795 if (!
n.links.empty())
continue;
796 if (std::find(
n.tags.begin(),
n.tags.end(),
"collision") !=
n.tags.end())
continue;
797 auto sit =
slots->find(
n.id);
798 if (sit ==
slots->end() || sit->second.empty())
continue;
801 r = makeRenderable(gfx, sit->second[0], unused,
true);
813 host->invalidateIndex();
814 host->markTransformDirty();
821 auto t =
host->tree();
822 for (
auto &
n :
t->nodes) {
823 auto it =
slots.find(
n.id);
824 if (it ==
slots.end() || it->second.empty())
continue;
825 fillMeshBoundsFromSlot(
n, it->second[0]);
827 if (
t->root >= 0) unionChildBounds(*
t,
t->root);
835 TextureCache &textures, MeshCache &shared,
836 const CpuImageMap *predecoded) {
838 for (
const auto &kv :
slots) {
840 if (!
n || kv.second.empty())
continue;
841 if (std::find(
n->tags.begin(),
n->tags.end(),
"collision") !=
n->tags.end())
continue;
843 const MeshSlot &slot = kv.second[0];
845 auto it = shared.find(slot.
mesh);
846 if (
options.sharedMeshes && it != shared.end()) {
857 auto *
r = graphics::Renderable3D::create();
858 r->meshRenderer()->visible =
true;
862 const aiMaterial *mat =
nullptr;
865 aiColor3D base(1.f, 1.f, 1.f);
867 if (mat->Get(AI_MATKEY_BASE_COLOR, base) != AI_SUCCESS)
868 mat->Get(AI_MATKEY_COLOR_DIFFUSE, base);
871 mat->Get(AI_MATKEY_METALLIC_FACTOR,
metallic);
872 mat->Get(AI_MATKEY_ROUGHNESS_FACTOR,
roughness);
876 graphics::Texture *
albedo = resolveTexture(gfx, scene, mat, aiTextureType_BASE_COLOR,
877 textures,
options.mipmaps, predecoded);
879 albedo = resolveTexture(gfx, scene, mat, aiTextureType_DIFFUSE, textures,
881 graphics::Texture *
normal = resolveTexture(gfx, scene, mat, aiTextureType_NORMALS,
882 textures,
options.mipmaps, predecoded);
883 graphics::Texture *
height = resolveTexture(gfx, scene, mat, aiTextureType_HEIGHT,
884 textures,
options.mipmaps, predecoded);
885 r->setTint(base.r, base.g, base.b, 1.f);
893bool SceneLoader::decode(
const std::string &
path,
const LoadOptions &
options, DecodedScene *out) {
894 const std::string
key = normPath(
path);
900 auto *mod3d = ModuleManager::getInstance<model3d::Model3D>(
"Model3D");
901 if (!mod3d) mod3d = model3d::Model3D::create();
902 model3d::ModelData *md =
nullptr;
904 md = mod3d->newModelDataFromFile(
path, toModelOptions(
options));
905 }
catch (
const std::exception &e) {
906 std::lock_guard<std::mutex> lock(pendingMu_);
907 lastErrors_[
key] = e.what();
910 std::lock_guard<std::mutex> lock(pendingMu_);
911 lastErrors_[
key] =
"unknown model decode error";
915 std::lock_guard<std::mutex> lock(pendingMu_);
916 lastErrors_[
key] =
"model decoder returned no scene";
920 std::vector<std::string> importWarnings;
921 for (
int i = 0; i < md->getMeshCount(); ++i) {
922 if (!md->hasNormals(i))
923 importWarnings.push_back(
"mesh " + std::to_string(i) +
" has no normals");
924 if (!md->hasTexCoords(i))
925 importWarnings.push_back(
"mesh " + std::to_string(i) +
" has no UV0");
928 std::lock_guard<std::mutex> lock(pendingMu_);
929 lastErrors_.erase(
key);
930 warnings_[
key] = std::move(importWarnings);
933 out->path = normPath(
path);
936 out->root = std::make_unique<scene::NodeDesc>(
buildNodeDesc(md->getScene(), &out->slots));
937 collectCpuImages(md->getScene(), out->slots,
options.mipmaps, out->cpuImages);
941scene::SceneHost *SceneLoader::mount(DecodedScene &
d) {
943 if (!
host)
return nullptr;
944 host->setTree(std::move(*
d.root));
946 graphics::Graphics *gfx = currentGraphics();
951 linkMeshNodes(
host,
d.slots, gfx,
d.options, textures_, shared, &
d.cpuImages);
952 if (
d.options.importLights) importLights(
d.md->getScene(),
d.lights);
953 if (
d.options.importCameras) importCameras(
d.md->getScene(),
d.cameras);
954 importAnimations(
d.md->getScene(),
d.options, &
d.skeleton,
d.clips);
958 scenes_[
d.path] =
Loaded{
d.path,
host, gfx,
d.options, std::move(
d.lights),
959 std::move(
d.cameras),
d.skeleton, std::move(
d.clips)};
963void SceneLoader::clearTextures() {
964 for (
auto &kv : textures_) delete kv.
second;
972 const std::string
key = normPath(
path);
974 auto warm = prewarmed_.find(
key);
975 if (warm != prewarmed_.end()) {
976 d = std::move(warm->second);
977 prewarmed_.erase(warm);
981 if (!linkRenderables) {
983 if (!
host)
return nullptr;
984 host->setTree(std::move(*
d.root));
998 return load(
path, presetOptions(preset));
1002 const std::string
key = normPath(
path);
1003 auto it = scenes_.find(
key);
1004 if (it == scenes_.end()) {
1009 {},
"sceneloader.reload"));
1021 "sceneloader.reload"));
1025 if (out) *out =
diff;
1029 linkMeshNodes(ld.host,
d.slots, ld.gfx,
options, textures_, shared, &
d.cpuImages);
1037 const std::string
key = normPath(
path);
1039 auto *mod3d = ModuleManager::getInstance<model3d::Model3D>(
"Model3D");
1040 if (!mod3d) mod3d = model3d::Model3D::create();
1043 md = mod3d->newModelDataFromFile(
path);
1051 auto it = scenes_.find(
key);
1052 if (it != scenes_.end() && it->second.host) {
1056 std::function<void(
const scene::NodeDesc &,
const std::string &)> walk =
1060 for (
const auto &
c :
n.children) walk(
c,
n.id);
1069 DecodedScene decoded;
1074 message.empty() ?
"scene inspection failed to decode the source" :
message,
1075 normPath(
path), {},
"sceneloader.inspect"));
1078 const auto mounted = scenes_.find(normPath(
path));
1079 if (mounted != scenes_.end() && mounted->second.host)
1080 result.
diff =
diffTree(mounted->second.host, *decoded.root);
1086 for (
const auto& child :
node.children) add(child,
node.
id);
1088 add(*decoded.root,
"");
1093 if (std::find(
node.tags.begin(),
node.tags.end(),
"model-socket") !=
node.tags.end())
1095 if (std::find(
node.tags.begin(),
node.tags.end(),
"collision") !=
node.tags.end())
1097 for (
const auto& child :
node.children) collect(child);
1099 collect(*decoded.root);
1106 auto it = scenes_.find(normPath(
path));
1107 return (it != scenes_.end()) ? it->second.host :
nullptr;
1112 return h ?
h->getNodeCount() : 0;
1118 auto it = scenes_.find(normPath(
path));
1119 if (it == scenes_.end())
return;
1120 if (it->second.host) {
1121 auto t = it->second.host->tree();
1122 for (
auto &
n :
t->nodes) {
1128 ecs::DestroyEntity(it->second.host);
1132 delete it->second.skeleton;
1141 const std::string
key = normPath(
path);
1143 std::lock_guard<std::mutex> lock(pendingMu_);
1144 if (inFlight_.count(
key))
return false;
1145 inFlight_.insert(
key);
1147 if (!pool_) pool_ = std::make_shared<thread::ThreadPool>(2);
1153 std::lock_guard<std::mutex> lock(self->pendingMu_);
1155 self->pending_.push_back(std::move(d));
1157 std::lock_guard<std::mutex> lock(self->pendingMu_);
1158 self->inFlight_.erase(
key);
1168 std::vector<DecodedScene> ready;
1170 std::lock_guard<std::mutex> lock(pendingMu_);
1171 ready.swap(pending_);
1173 for (
auto &
d : ready) {
1174 if (!
d.md)
continue;
1175 if (
d.prewarmOnly) {
1176 prewarmed_[
d.path] = std::move(
d);
1180 if (
d.done)
d.done(
h);
1182 return static_cast<int>(ready.size());
1186 const std::string
key = normPath(
path);
1188 std::lock_guard<std::mutex> lock(pendingMu_);
1189 if (prewarmed_.count(
key) || inFlight_.count(
key))
return false;
1190 inFlight_.insert(
key);
1192 if (!pool_) pool_ = std::make_shared<thread::ThreadPool>(2);
1198 d.prewarmOnly = true;
1199 std::lock_guard<std::mutex> lock(self->pendingMu_);
1200 self->pending_.push_back(std::move(d));
1202 std::lock_guard<std::mutex> lock(self->pendingMu_);
1203 self->inFlight_.erase(
key);
1209 return prewarmed_.count(normPath(
path)) > 0;
1213 auto it = prewarmed_.find(normPath(
path));
1214 if (it == prewarmed_.end())
return;
1215 prewarmed_.erase(it);
1219 std::lock_guard<std::mutex> lock(pendingMu_);
1220 return static_cast<int>(pending_.size() + inFlight_.size());
1223std::string SceneLoader::decodeErrorFor(
const std::string &
path)
const {
1224 std::lock_guard<std::mutex> lock(pendingMu_);
1225 auto it = lastErrors_.find(normPath(
path));
1226 return it == lastErrors_.end() ? std::string{} : it->second;
1230 std::lock_guard<std::mutex> lock(pendingMu_);
1231 auto it = warnings_.find(normPath(
path));
1232 return it == warnings_.end() ? 0 :
static_cast<int>(it->second.size());
1236 std::lock_guard<std::mutex> lock(pendingMu_);
1237 auto it = warnings_.find(normPath(
path));
1238 if (it == warnings_.end() ||
index < 0 ||
static_cast<size_t>(
index) >= it->second.size())
1240 return it->second[
static_cast<size_t>(
index)];
1245 if (!
h ||
level < 0)
return false;
1247 auto tree =
h->tree();
1250 for (
const std::string &
tag :
node.tags)
1251 if (
tag.rfind(
"lod:", 0) == 0) nodeLod = std::atoi(
tag.c_str() + 4);
1261 auto it = scenes_.find(normPath(
path));
1262 if (it == scenes_.end() || !it->second.host)
return 0;
1263 return static_cast<int>(it->second.host->findAllByTag(
"model-socket").size());
1267 auto it = scenes_.find(normPath(
path));
1268 if (it == scenes_.end() || !it->second.host ||
index < 0)
return {};
1269 auto sockets = it->second.host->findAllByTag(
"model-socket");
1270 return static_cast<size_t>(
index) < sockets.size() ? sockets[
static_cast<size_t>(
index)]->
name
1275 auto it = scenes_.find(normPath(
path));
1276 if (it == scenes_.end() || !it->second.host)
return 0;
1277 return static_cast<int>(it->second.host->findAllByTag(
"collision").size());
1281 auto it = scenes_.find(normPath(
path));
1282 if (it == scenes_.end() || !it->second.host ||
index < 0)
return {};
1283 auto nodes = it->second.host->findAllByTag(
"collision");
1291 auto it = scenes_.find(normPath(
path));
1292 return (it != scenes_.end()) ?
static_cast<int>(it->second.lights.size()) : 0;
1296 auto it = scenes_.find(normPath(
path));
1297 if (it == scenes_.end() ||
index < 0 ||
1298 static_cast<size_t>(
index) >= it->second.lights.size())
1300 return it->second.lights[size_t(
index)];
1304 auto it = scenes_.find(normPath(
path));
1305 return (it != scenes_.end()) ?
static_cast<int>(it->second.cameras.size()) : 0;
1309 auto it = scenes_.find(normPath(
path));
1310 if (it == scenes_.end() ||
index < 0 ||
1311 static_cast<size_t>(
index) >= it->second.cameras.size())
1313 return it->second.cameras[size_t(
index)];
1317 auto it = scenes_.find(normPath(
path));
1318 return (it != scenes_.end()) ?
static_cast<int>(it->second.clips.size()) : 0;
1322 auto it = scenes_.find(normPath(
path));
1323 return (it != scenes_.end()) ? it->second.skeleton :
nullptr;
1327 auto it = scenes_.find(normPath(
path));
1328 if (it == scenes_.end() ||
index < 0 ||
1329 static_cast<size_t>(
index) >= it->second.clips.size())
1331 return it->second.clips[size_t(
index)];
1334void SceneLoader::expose(ssq::Table &table) {
1335 auto cls = table.addClass(
name, SceneLoader::create,
false);
1339void SceneLoader::expose(ssq::Class &
cls) {
1344 return sl ? sl->load(
path) : nullptr;
1354 "scene loader must not be null",
"scene")),
1357 [](
bool changed) { return eve::Value::boolean(changed); });
struct SQVM * HSQUIRRELVM
graphics::Texture * albedo
std::array< double, 10 > q
std::array< float, 3 > scale
#define Module_IMPL(ModuleName, newExpr)
std::weak_ptr< PrimitiveScene > scene
LocalPageCacheEntry slots[ShadowConfig::kLocalSlots]
const SquirrelValueOptions & options
The single Squirrel projection for common Result, Status and Value.
static Diagnostic error(DiagnosticCode code, std::string message, std::string path={}, DiagnosticDetails details={}, std::string source={})
Construct an error diagnostic with the standard error severity.
EVENGINE_API_FOUNDATION public API.
virtual std::string getName() const =0
Returns the name.
eve::Result< bool > reload(const std::string &normPath) override
Reloads .
static ResourceManager & getInstance()
Returns the instance.
Move-only operation result carrying either a value or Status.
static Result success(T value)
Construct a successful result owning value.
const T & value() const &
Borrow the value from a const lvalue after checking success.
bool ok() const noexcept
Whether this result represents a non-failure outcome.
static Result failure(Status status)
Construct a failed result from a structured status.
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
static Value boolean(bool value)
Compatibility factory for a boolean value.
Keyframed skeletal animation clip (local TRS tracks per bone). Script type: AnimClip.
static AnimClip * loadClip(const aiScene *scene, const AnimSkeleton *skeleton, int animIndex=0)
Load clip by index; maps node-name channels onto skeleton bones.
static AnimSkeleton * loadSkeleton(const aiScene *scene)
Build skeleton from the scene node hierarchy (depth-first).
3D bone hierarchy + bind-pose local TRS for skeletal animation. Independent of ik::Skeleton3D (FABRIK...
In-memory byte buffer implementing eve::Data (ref-counted).
EVENGINE_API_BACKENDS public API.
static Camera3D * createCamera()
virtual Mesh * newMeshFromAssimp(const ::aiMesh &mesh)=0
Creates a mesh from assimp. @ownership Caller deletes unless documented otherwise.
Declarative 3D light. Collected by RenderSystem3D (max 8 per frame). type: "point" | "dir" | "spot" (...
static Light3D * createLight(const std::string &type="point")
GPU mesh handle (+ optional CPU morph targets).
EVENGINE_API_BACKENDS public API.
Represents raw pixel data.
std::string getFormat() const
CPU-side decoded 3D model (Assimp scene owned via medialoader::ModelScene). Does not upload to GPU — ...
const aiScene * getScene() const
Returns the scene.
ECS mount point for one scene graph (full scene or nested subtree root). Isomorphic to eve::ui::UIHos...
static eve::Result< SceneHost * > createHost(const std::string &name="")
Creates an ECS-owned scene host with an explicit Result contract.
Loads a 3D scene (glTF / OBJ / FBX / GLB ... via Assimp/medialoader) into the ECS scene tree.
bool loadAsync(const std::string &path, const LoadOptions &options={}, std::function< void(scene::SceneHost *)> done=nullptr)
Decode path on a worker thread, then apply it on the main thread the next time pollAsync() is called ...
int pendingAsyncCount() const
Number of async loads still waiting to be mounted.
static bool applyTreeDiff(scene::SceneHost *host, const scene::NodeDesc &newRoot, const SceneDiff &diff, graphics::Graphics *gfx, const MeshSlotMap *slots)
Apply a diff to the host arena in place. Creates/destroys Renderable3D for added/removed mesh objects...
int collisionCount(const std::string &path) const
Number of imported UCX_/UBX_/USP_/UCP_ collision nodes.
std::unordered_map< const aiMesh *, graphics::Mesh * > MeshCache
eve::Result< SceneInspection > inspect(const std::string &path, const LoadOptions &options={})
Decode with explicit options and return copied metadata without mounting or GPU upload.
graphics::Light3D * light(const std::string &path, int index)
Light.
void clearPrewarm(const std::string &path)
Drop a decoded prewarm result and release its CPU scene.
int lightCount(const std::string &path)
Number of imported Light3D entities for path; 0 if none / disabled.
eve::Result< bool > reload(const std::string &path, SceneDiff *out=nullptr, const LoadOptions &options={})
Hot-reload path. Re-decodes, diffs, and applies only what changed.
bool prewarm(const std::string &path)
Script-friendly CPU prewarm using the default import preset.
int animationCount(const std::string &path)
Number of imported animation clips for path; 0 if none / disabled.
bool prewarmAsync(const std::string &path, const LoadOptions &options={})
Decode and retain a scene without creating ECS/GPU objects.
scene::SceneHost * load(const std::string &path, bool linkRenderables=true, const LoadOptions &options={})
Full load: build the GameObject tree from path, mount it, return host.
std::string collisionName(const std::string &path, int index) const
Name of an imported collision node, or an empty string.
std::unordered_map< std::string, CpuImage > CpuImageMap
SceneDiff diff(const std::string &path)
Dry-run diff for path against the currently mounted tree (no mutation).
scene::SceneHost * host(const std::string &path)
The mounted host for path, or nullptr if not loaded.
std::string socketName(const std::string &path, int index) const
Name of an imported SOCKET_ node, or an empty string.
animation::AnimSkeleton * skeleton(const std::string &path)
Imported skeleton for path (nullptr when the scene has no animations).
bool loaded(const std::string &path)
True if path is currently loaded.
int socketCount(const std::string &path) const
Number of imported SOCKET_ nodes.
animation::AnimClip * clip(const std::string &path, int index)
Clip.
static scene::NodeDesc buildNodeDesc(const aiScene *scene, MeshSlotMap *slotsOut=nullptr)
Flatten an Assimp scene into a scene::NodeDesc tree (id = object id).
bool setLod(const std::string &path, int level)
Select imported name-convention LOD (_LOD0, _LOD1, ...).
bool loadAsyncDefault(const std::string &path)
Script-friendly async load using the default import preset.
static void fillSceneBounds(scene::SceneHost *host, const MeshSlotMap &slots)
Fill node bounds from Assimp mesh AABBs (mesh GameObjects get the exact local-space AABB; ancestors g...
static SceneDiff diffTree(scene::SceneHost *host, const scene::NodeDesc &newRoot)
Diff a mounted host tree against a freshly built NodeDesc tree.
std::string warning(const std::string &path, int index) const
Non-fatal import warning at index, or an empty string.
~SceneLoader() override
Scene loader.
bool loadAsyncPreset(const std::string &path, const std::string &preset)
Loads async preset.
void unload(const std::string &path)
Drop the loaded scene for path, destroying linked Renderable3D objects.
bool prewarmed(const std::string &path) const
True when a decoded scene is ready for load() to consume.
int warningCount(const std::string &path) const
Number of non-fatal import warnings retained for path.
int nodeCount(const std::string &path)
Number of GameObjects (SceneNodes) currently mounted for path; 0 if none.
graphics::Camera3D * camera(const std::string &path, int index)
Camera.
SceneLoader()
Construct the loader and publish its optional action-block provider.
int pollAsync()
Mount every decoded-but-not-applied scene. Must be called on the main / render thread....
scene::SceneHost * loadPreset(const std::string &path, const std::string &preset)
Load with a built-in preset: quality, balanced, mobile, or raw.
std::unordered_map< std::string, graphics::Texture * > TextureCache
Caches shared across the loader: textures by source key, GPU meshes per aiMesh.
int cameraCount(const std::string &path)
Number of imported Camera3D entities for path; 0 if none / disabled.
int findLinkKind(const char *kind)
Finds link kind.
void shutdownPrefabActionCapabilities()
Release all provider-owned prefab instances during SceneLoader shutdown.
void registerPrefabActionCapabilities()
Register the SceneLoader-backed prefab action provider.
std::unordered_map< std::string, std::vector< MeshSlot > > MeshSlotMap
ssq::Table projectResult(HSQUIRRELVM vm, Result< void > &&result)
Consume and project a void native Result using the common schema.
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Build metadata (engine git commit, build time, third-party version).
Declarative scene-node description (build once / on dirty → flatten into SceneHost::Tree)....
Retained scene node (arena). Conceptual GameObject; isomorphic to eve::ui::UINode.
Options controlling how a 3D scene file is decoded and mounted.
A GPU mesh reference for one Assimp mesh referenced by an aiNode. The loader uploads one Renderable3D...
std::vector< SceneDiffEntry > entries
CPU-only import inspection result used by editor preflight.
std::vector< std::string > warnings
std::vector< std::string > collisions
std::vector< std::string > sockets