载入中...
搜索中...
未找到
SceneLoader.cpp
浏览该文件的文档.
3
7#include "common/ECS.h"
8#include "common/Resource.h"
10#include "data/ByteData.h"
11#include "filesystem/FileData.h"
13#include "graphics/Graphics.h"
14#include "graphics/Light.h"
15#include "graphics/Mesh.h"
17#include "graphics/Texture.h"
18#include "image/Image.h"
19#include "image/ImageData.h"
20#include "model3d/Model3D.h"
21#include "model3d/ModelData.h"
22#include "scene/NodeDesc.h"
23#include "scene/SceneHost.h"
25#include "thread/ThreadPool.h"
26
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>
37
38#include <glm/glm.hpp>
39#include <glm/gtx/matrix_decompose.hpp>
40#include <glm/gtc/matrix_transform.hpp>
41#include <glm/gtc/quaternion.hpp>
42
43#include <limits>
44
45#include <simplesquirrel/simplesquirrel.hpp>
46
47#include <algorithm>
48#include <cmath>
49#include <cstdlib>
50#include <functional>
51#include <memory>
52#include <unordered_set>
53
54namespace eve {
55namespace sceneloader {
56
57SceneLoader::DecodedScene::DecodedScene() = default;
58SceneLoader::DecodedScene::~DecodedScene() = default;
59SceneLoader::DecodedScene::DecodedScene(DecodedScene &&) noexcept = default;
60SceneLoader::DecodedScene &SceneLoader::DecodedScene::operator=(DecodedScene &&) noexcept = default;
61
63
65
68 // Decoded ModelData instances are owned by the unified resource cache
69 // (Model3D::newModelDataFromFile returns cache-shared resources), so no
70 // cleanup is needed here.
71 clearTextures();
72}
73
74namespace {
75
76constexpr float kEps = 1e-5f;
77
78template <class T>
79T *borrowResult(eve::Result<T *> result) {
80 if (!result.ok()) return nullptr;
81 return result.value();
82}
83
84std::string normPath(const std::string &p) {
85 std::string out;
86 out.reserve(p.size());
87 for (char c : p) {
88 if (c == '\\') out.push_back('/');
89 else out.push_back(c);
90 }
91 return out;
92}
93
94graphics::Graphics *currentGraphics() {
95 return ModuleManager::getInstance<graphics::Graphics>("Graphics");
96}
97
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;
105 return m;
106}
107
108bool approx(float a, float b) { return std::fabs(a - b) < kEps; }
109
110// ---- transform helpers ----
111
112void decomposeNode(const aiMatrix4x4 &m, float &x, float &y, float &z, float &yaw, float &pitch,
113 float &roll, float &sx, float &sy, float &sz) {
114 aiVector3D pos, scale;
115 aiQuaternion rot;
116 m.Decompose(scale, rot, pos);
117 x = pos.x;
118 y = pos.y;
119 z = pos.z;
120 sx = scale.x;
121 sy = scale.y;
122 sz = scale.z;
123 glm::quat q(rot.w, rot.x, rot.y, rot.z);
124 glm::vec3 e = glm::eulerAngles(q);
125 yaw = e.y;
126 pitch = e.x;
127 roll = e.z;
128}
129
130std::string uniqueId(const std::string &base, std::unordered_map<std::string, int> &counts) {
131 std::string b = base.empty() ? "node" : base;
132 int &n = counts[b];
133 std::string id = b;
134 if (n > 0) id = b + "_" + std::to_string(n);
135 ++n;
136 return id;
137}
138
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])))
145 return false;
146 return true;
147}
148
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;
152 char *end = nullptr;
153 const long value = std::strtol(name.c_str() + p + 4, &end, 10);
154 return end == name.c_str() + p + 4 || value < 0 ? -1 : static_cast<int>(value);
155}
156
157LoadOptions presetOptions(const std::string &name) {
158 LoadOptions o;
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;
164 return o;
165 }
166 if (name == "raw") {
167 o.generateNormalsIfMissing = false;
168 o.joinIdenticalVertices = false;
169 o.flipUVs = false;
170 o.improveCacheLocality = false;
171 o.sharedMeshes = false;
172 o.mipmaps = false;
173 o.importLights = false;
174 o.importCameras = false;
175 o.importAnimations = false;
176 return o;
177 }
178 throw eve::Exception("SceneLoader: unknown import preset '%s'", name.c_str());
179}
180
181// ---- texture helpers (embedded / VFS, cached by path) ----
182
184struct SamplerSpec {
185 bool repeatU = true;
186 bool repeatV = true;
187 bool mips = true;
188 std::string filter = "linear"; // "linear" | "nearest"
189 std::string mipmap = "linear"; // "none" | "linear" | "nearest"
190};
191
192SamplerSpec samplerFor(const aiMaterial *mat, aiTextureType type, bool wantMips) {
193 SamplerSpec s;
194 if (!mat) {
195 s.mips = wantMips;
196 return s;
197 }
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);
204
205 // glTF sampler filter values (AI_MATKEY_GLTF_MAPPINGFILTER_*).
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"; // 0 / 9729(linear) -> linear
210 switch (min) {
211 case 9728: // NEAREST
212 case 9729: // LINEAR — no mipmaps requested by the source
213 s.mips = false;
214 s.mipmap = "none";
215 break;
216 case 9986: // NEAREST_MIPMAP_LINEAR
217 case 9987: // LINEAR_MIPMAP_LINEAR
218 s.mips = wantMips;
219 s.mipmap = "linear";
220 break;
221 case 9984: // NEAREST_MIPMAP_NEAREST
222 case 9985: // LINEAR_MIPMAP_NEAREST
223 s.mips = wantMips;
224 s.mipmap = "nearest";
225 break;
226 default: // unspecified -> honor the global toggle
227 s.mips = wantMips;
228 s.mipmap = wantMips ? "linear" : "none";
229 break;
230 }
231 return s;
232}
233
234graphics::Texture *resolveTexture(graphics::Graphics *gfx, const aiScene *scene,
235 const aiMaterial *mat, aiTextureType type,
236 SceneLoader::TextureCache &cache, bool wantMips,
237 const SceneLoader::CpuImageMap *predecoded = nullptr) {
238 if (!gfx || !scene || !mat) return nullptr;
239 aiString path;
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;
243
244 const SamplerSpec s = samplerFor(mat, type, wantMips);
245 auto *const imageModule = eve::image::Image::create();
246
247 const std::string keySuffix =
248 std::string(s.repeatU ? "|1" : "|0") + (s.repeatV ? "1" : "0") + "|" + s.filter + "|" +
249 s.mipmap;
250
251 // Embedded texture ("*0", "*1", ...).
252 if (p[0] == '*') {
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;
260
261 graphics::Texture *t = nullptr;
262 if (tex->mHeight == 0) {
263 eve::data::ByteData bytes(tex->pcData, static_cast<size_t>(tex->mWidth));
264 try {
265 eve::image::ImageData *img = imageModule->newImageData(&bytes);
266 t = gfx->newTextureWithSampler(img, s.repeatU, s.repeatV, s.mips, 8.f, s.filter,
267 s.mipmap);
268 delete img;
269 } catch (...) {
270 return nullptr;
271 }
272 } else {
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;
282 }
283 eve::image::ImageData img(int(w), int(h), "RGBA8", rgba.data(), false);
284 t = gfx->newTextureWithSampler(&img, s.repeatU, s.repeatV, s.mips, 8.f, s.filter,
285 s.mipmap);
286 }
287 if (t) cache[key] = t;
288 return t;
289 }
290
291 // External file through the VFS.
292 const std::string key = normPath(p) + keySuffix;
293 auto it = cache.find(key);
294 if (it != cache.end()) return it->second;
295
296 // Off-thread pre-decoded CPU image (async path): upload directly, no disk IO.
297 if (predecoded) {
298 auto pit = predecoded->find(key);
299 if (pit != predecoded->end()) {
300 const SceneLoader::CpuImage &ci = pit->second;
301 eve::image::ImageData img(ci.w, ci.h, "RGBA8",
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;
306 return t;
307 }
308 }
309
310 try {
311 auto *fs = eve::filesystem::Filesystem::create();
312 std::unique_ptr<eve::filesystem::FileData> fd(fs->read(p));
313 if (fd && fd->getSize() > 0) {
314 eve::image::ImageData *img = imageModule->newImageData(fd.get());
315 graphics::Texture *t = gfx->newTextureWithSampler(img, s.repeatU, s.repeatV, s.mips,
316 8.f, s.filter, s.mipmap);
317 delete img;
318 if (t) cache[key] = t;
319 return t;
320 }
321 } catch (...) {
322 }
323 return nullptr;
324}
325
326// Pre-decode external (non-embedded) texture files off the calling thread so the
327// async loader can skip disk IO + image decode on the render thread.
328void collectCpuImages(const aiScene *scene, const MeshSlotMap &slots, bool wantMips,
330 if (!scene) return;
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];
338 if (!mat) continue;
339 for (aiTextureType type : kTypes) {
340 aiString p;
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; // embedded handled on main thread
344 const SamplerSpec s = samplerFor(mat, type, wantMips);
345 const std::string key = normPath(c) + std::string(s.repeatU ? "|1" : "|0") +
346 (s.repeatV ? "1" : "0") + "|" + s.filter + "|" + s.mipmap;
347 if (out.count(key)) continue;
348 try {
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;
352 eve::image::ImageData *img = imageModule->newImageData(fd.get());
353 if (!img) continue;
354 if (img->getFormat() == "RGBA8") {
355 SceneLoader::CpuImage ci;
356 ci.w = img->getWidth();
357 ci.h = img->getHeight();
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);
362 }
363 delete img;
364 } catch (...) {
365 }
366 }
367 }
368 }
369}
370
371// ---- renderable creation ----
372
373graphics::Renderable3D *makeRenderable(graphics::Graphics *gfx, const MeshSlot &slot,
374 SceneLoader::TextureCache &textures, bool mipmaps) {
375 if (!gfx || !slot.mesh) return nullptr;
376 graphics::Mesh *mesh = gfx->newMeshFromAssimp(*slot.mesh); // local-space (hierarchy transform)
377 if (!mesh) return nullptr;
378 auto *r = graphics::Renderable3D::create();
379 r->meshRenderer()->visible = true;
380 r->setMesh(mesh);
381
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];
386
387 aiColor3D base(1.f, 1.f, 1.f);
388 if (mat) {
389 if (mat->Get(AI_MATKEY_BASE_COLOR, base) != AI_SUCCESS)
390 mat->Get(AI_MATKEY_COLOR_DIFFUSE, base);
391 float metallic = 0.f;
392 float roughness = 0.45f;
393 mat->Get(AI_MATKEY_METALLIC_FACTOR, metallic);
394 mat->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness);
395 r->setMetallic(metallic);
396 r->setRoughness(roughness);
397 }
398
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);
406
407 r->setTint(base.r, base.g, base.b, 1.f);
408 if (albedo) r->setTexture(albedo);
409 if (normal) r->setNormalTexture(normal);
410 if (height) r->setHeightTexture(height);
411 return r;
412}
413
414void destroyRenderable(graphics::Renderable3D *r) {
415 if (r) ecs::DestroyEntity(r);
416}
417
418// ---- mesh AABB bounds (picking / culling) ----
419
420glm::mat4 nodeLocalMatrix(const scene::SceneNode &n) {
421 glm::mat4 m(1.f);
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));
425 } else {
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));
429 }
430 m = glm::scale(m, glm::vec3(n.sx, n.sy, n.sz));
431 return m;
432}
433
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);
449 }
450 }
451 n.bminX = mn[0];
452 n.bminY = mn[1];
453 n.bminZ = mn[2];
454 n.bmaxX = mx[0];
455 n.bmaxY = mx[1];
456 n.bmaxZ = mx[2];
457 n.hasBounds = true;
458}
459
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);
465 }
466
467 bool any = false;
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;
477 any = true;
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]);
487 }
488 }
489 }
490 if (!any) return;
491 if (!n.hasBounds) {
492 n.bminX = mn[0];
493 n.bminY = mn[1];
494 n.bminZ = mn[2];
495 n.bmaxX = mx[0];
496 n.bmaxY = mx[1];
497 n.bmaxZ = mx[2];
498 n.hasBounds = true;
499 } else {
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]);
506 }
507}
508
509// ---- NodeDesc build ----
510
511void buildNodeRecursive(const aiScene *scene, const aiNode *node, scene::NodeDesc &out,
512 MeshSlotMap *slots, std::unordered_map<std::string, int> &counts) {
513 const std::string rawName = (node && node->mName.length) ? node->mName.C_Str() : "<root>";
514 const std::string id = uniqueId(rawName, counts);
515
516 out.id = id;
517 out.key = id;
518 out.name = rawName;
519 out.space = "3d";
520 out.visible = true;
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");
525 out.visible = false;
526 }
527 const int lod = lodFromName(rawName);
528 if (lod >= 0) {
529 out.tags.push_back("lod");
530 out.tags.push_back("lod:" + std::to_string(lod));
531 out.visible = lod == 0;
532 }
533 decomposeNode(node->mTransformation, out.x, out.y, out.z, out.yaw, out.pitch, out.roll,
534 out.sx, out.sy, out.sz);
535
536 for (unsigned i = 0; node && i < node->mNumMeshes; ++i) {
537 const unsigned mi = node->mMeshes[i];
538 if (mi >= scene->mNumMeshes) continue;
539 const aiMesh *mesh = scene->mMeshes[mi];
540 if (!mesh || mesh->mNumFaces == 0) continue;
541
542 const std::string cid = id + "_mesh" + std::to_string(i);
543 scene::NodeDesc child;
544 child.id = cid;
545 child.key = cid;
546 child.name = std::string("mesh") + std::to_string(i);
547 child.space = "3d";
548 child.visible = out.visible;
549 child.tags = out.tags;
550 out.children.push_back(std::move(child));
551
552 if (slots) {
553 MeshSlot s;
554 s.scene = scene;
555 s.mesh = mesh;
556 s.materialIndex = mesh->mMaterialIndex;
557 (*slots)[cid].push_back(std::move(s));
558 }
559 }
560
561 for (unsigned c = 0; node && c < node->mNumChildren; ++c) {
562 scene::NodeDesc child;
563 buildNodeRecursive(scene, node->mChildren[c], child, slots, counts);
564 out.children.push_back(std::move(child));
565 }
566}
567
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));
573}
574
575// Walk the new tree in DFS order, emitting Add / Move / Modify entries.
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()) {
581 out.entries.push_back({SceneDiffEntry::Action::Add, d.id, parentId});
582 ++out.added;
583 } else {
584 auto pit = oldParent.find(d.id);
585 const std::string op = (pit != oldParent.end()) ? pit->second : "";
586 if (op != parentId) {
587 out.entries.push_back({SceneDiffEntry::Action::Move, d.id, parentId});
588 ++out.moved;
589 } else if (propsChanged(*it->second, d)) {
590 out.entries.push_back({SceneDiffEntry::Action::Modify, d.id, ""});
591 ++out.modified;
592 }
593 }
594 for (const auto &child : d.children) walkNew(child, d.id, oldNodes, oldParent, out);
595}
596
597// ---- lights / cameras / animation import ----
598
599void importLights(const aiScene *scene, std::vector<graphics::Light3D *> &out) {
600 if (!scene) return;
601 for (unsigned i = 0; i < scene->mNumLights; ++i) {
602 const aiLight *l = scene->mLights[i];
603 if (!l) continue;
604 std::string type = "point";
605 switch (l->mType) {
606 case aiLightSource_DIRECTIONAL:
607 type = "dir";
608 break;
609 case aiLightSource_POINT:
610 case aiLightSource_SPOT:
611 case aiLightSource_AREA:
612 default:
613 type = "point";
614 break;
615 }
616 graphics::Light3D *light = graphics::Light3D::createLight(type);
617 if (type == "dir") {
618 light->setDirection(l->mDirection.x, l->mDirection.y, l->mDirection.z);
619 } else {
620 light->setPosition(l->mPosition.x, l->mPosition.y, l->mPosition.z);
621 float radius = 8.f;
622 if (l->mAttenuationQuadratic > 1e-6f)
623 radius = 1.f / std::sqrt(l->mAttenuationQuadratic);
624 light->setRadius(radius);
625 }
626 light->setColor(l->mColorDiffuse.r, l->mColorDiffuse.g, l->mColorDiffuse.b, 1.f);
627 out.push_back(light);
628 }
629}
630
631void importCameras(const aiScene *scene, std::vector<graphics::Camera3D *> &out) {
632 if (!scene) return;
633 for (unsigned i = 0; i < scene->mNumCameras; ++i) {
634 const aiCamera *c = scene->mCameras[i];
635 if (!c) continue;
636 graphics::Camera3D *cam = graphics::Camera3D::createCamera();
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));
642 out.push_back(cam);
643 }
644}
645
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;
650 animation::AnimSkeleton *skeleton = animation::AnimImporter::loadSkeleton(scene);
651 if (!skeleton) return;
652 *skeletonOut = skeleton;
653 for (unsigned i = 0; i < scene->mNumAnimations; ++i) {
654 animation::AnimClip *clip = animation::AnimImporter::loadClip(scene, skeleton, int(i));
655 if (clip) clips.push_back(clip);
656 }
657}
658
659} // namespace
660
661// ---- public: pure helpers ----
662
665 if (!scene || !scene->mRootNode) return root;
666 std::unordered_map<std::string, int> counts;
667 buildNodeRecursive(scene, scene->mRootNode, root, slotsOut, counts);
668 return root;
669}
670
672 SceneDiff out;
673 std::unordered_map<std::string, const scene::SceneNode *> oldNodes;
674 std::unordered_map<std::string, std::string> oldParent;
675 if (host) {
676 auto t = host->tree();
677 for (size_t i = 0; i < t->nodes.size(); ++i) {
678 const scene::SceneNode &n = t->nodes[i];
679 oldNodes[n.id] = &n;
680 oldParent[n.id] = (n.parent >= 0) ? t->nodes[size_t(n.parent)].id : "";
681 }
682 }
683
684 // Collect the set of ids present in the new tree.
685 std::unordered_set<std::string> newIds;
686 std::function<void(const scene::NodeDesc &)> collect = [&](const scene::NodeDesc &d) {
687 newIds.insert(d.id);
688 for (const auto &c : d.children) collect(c);
689 };
690 collect(newRoot);
691
692 // Removed: present in old, absent in new.
693 for (const auto &kv : oldNodes) {
694 if (!newIds.count(kv.first)) {
695 out.entries.push_back({SceneDiffEntry::Action::Remove, kv.first, ""});
696 ++out.removed;
697 }
698 }
699
700 // Add / Move / Modify: walk the new tree (DFS so parents precede children).
701 walkNew(newRoot, "", oldNodes, oldParent, out);
702 return out;
703}
704
706 const SceneDiff &diff, graphics::Graphics *gfx,
707 const MeshSlotMap *slots) {
708 if (!host || diff.empty()) return false;
709
710 // Snapshot the mounted tree by id so kept GameObjects can keep their linked
711 // Renderable3D (no re-upload / no object rebuild) across the update.
712 std::unordered_map<std::string, const scene::SceneNode *> oldNodes;
713 {
714 auto t = host->tree();
715 for (const auto &n : t->nodes) oldNodes[n.id] = &n;
716 }
717
718 // Collect the ids present in the new tree.
719 std::unordered_set<std::string> newIds;
720 std::function<void(const scene::NodeDesc &)> collect = [&](const scene::NodeDesc &d) {
721 newIds.insert(d.id);
722 for (const auto &c : d.children) collect(c);
723 };
724 collect(newRoot);
725
726 // Destroy Renderable3D of removed GameObjects.
727 for (const auto &kv : oldNodes) {
728 if (!newIds.count(kv.first)) {
729 if (const auto *l = host->findLink(kv.second, scene::findLinkKind("renderable3d"))) {
730 destroyRenderable(static_cast<graphics::Renderable3D *>(l->target));
731 }
732 }
733 }
734
735 // Rebuild the arena from the new tree (DFS). Kept nodes copy their link from
736 // the old node (identity preserved); added mesh nodes get a fresh Renderable3D.
737 std::vector<scene::SceneNode> nodes;
738 nodes.reserve(newIds.size());
739 std::function<int(const scene::NodeDesc &, int)> build = [&](const scene::NodeDesc &d,
740 int parentIndex) -> int {
741 const int idx = int(nodes.size());
743 n.id = d.id;
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;
748 n.x = d.x;
749 n.y = d.y;
750 n.z = d.z;
751 n.yaw = d.yaw;
752 n.pitch = d.pitch;
753 n.roll = d.roll;
754 n.sx = d.sx;
755 n.sy = d.sy;
756 n.sz = d.sz;
757 n.localDirty = true;
758 n.world = glm::mat4(1.f);
759 n.firstChild = -1;
760 n.nextSibling = -1;
761 n.parent = parentIndex;
762
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;
774 }
775
776 nodes.push_back(std::move(n));
777
778 int prevChild = -1;
779 int firstChild = -1;
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;
784 prevChild = ci;
785 }
786 nodes[size_t(idx)].firstChild = firstChild;
787 return idx;
788 };
789 const int root = build(newRoot, -1);
790
791 // Fresh Renderable3D for newly added mesh GameObjects (only changed ones).
792 if (gfx && slots) {
793 TextureCache unused;
794 for (auto &n : nodes) {
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;
799 graphics::Renderable3D *r = nullptr;
800 try {
801 r = makeRenderable(gfx, sit->second[0], unused, true);
802 } catch (...) {
803 r = nullptr;
804 }
805 if (r) {
806 n.links.push_back(scene::SceneLink{scene::findLinkKind("renderable3d"), r, 0});
807 }
808 }
809 }
810
811 host->tree()->nodes = std::move(nodes);
812 host->tree()->root = root;
813 host->invalidateIndex();
814 host->markTransformDirty();
816 return true;
817}
818
820 if (!host) return;
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]);
826 }
827 if (t->root >= 0) unionChildBounds(*t, t->root);
828}
829
830// ---- public: file / lifecycle ----
831// ---- private: linking + async decode ----
832
833void SceneLoader::linkMeshNodes(scene::SceneHost *host, const MeshSlotMap &slots,
835 TextureCache &textures, MeshCache &shared,
836 const CpuImageMap *predecoded) {
837 if (!gfx) return;
838 for (const auto &kv : slots) {
839 scene::SceneNode *n = borrowResult(host->findById(kv.first));
840 if (!n || kv.second.empty()) continue;
841 if (std::find(n->tags.begin(), n->tags.end(), "collision") != n->tags.end()) continue;
842 if (host->findLink(n, scene::findLinkKind("renderable3d"))) continue;
843 const MeshSlot &slot = kv.second[0];
844 graphics::Mesh *mesh = nullptr;
845 auto it = shared.find(slot.mesh);
846 if (options.sharedMeshes && it != shared.end()) {
847 mesh = it->second;
848 } else {
849 try {
850 mesh = gfx->newMeshFromAssimp(*slot.mesh);
851 } catch (...) {
852 mesh = nullptr;
853 }
854 if (mesh && options.sharedMeshes) shared[slot.mesh] = mesh;
855 }
856 if (!mesh) continue;
857 auto *r = graphics::Renderable3D::create();
858 r->meshRenderer()->visible = true;
859 r->setMesh(mesh);
860
861 const aiScene *scene = slot.scene;
862 const aiMaterial *mat = nullptr;
863 if (scene && scene->mMaterials && slot.materialIndex < scene->mNumMaterials)
864 mat = scene->mMaterials[slot.materialIndex];
865 aiColor3D base(1.f, 1.f, 1.f);
866 if (mat) {
867 if (mat->Get(AI_MATKEY_BASE_COLOR, base) != AI_SUCCESS)
868 mat->Get(AI_MATKEY_COLOR_DIFFUSE, base);
869 float metallic = 0.f;
870 float roughness = 0.45f;
871 mat->Get(AI_MATKEY_METALLIC_FACTOR, metallic);
872 mat->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness);
873 r->setMetallic(metallic);
874 r->setRoughness(roughness);
875 }
876 graphics::Texture *albedo = resolveTexture(gfx, scene, mat, aiTextureType_BASE_COLOR,
877 textures, options.mipmaps, predecoded);
878 if (!albedo)
879 albedo = resolveTexture(gfx, scene, mat, aiTextureType_DIFFUSE, textures,
880 options.mipmaps, predecoded);
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);
886 if (albedo) r->setTexture(albedo);
887 if (normal) r->setNormalTexture(normal);
888 if (height) r->setHeightTexture(height);
889 n->links.push_back(scene::SceneLink{scene::findLinkKind("renderable3d"), r, 0});
890 }
891}
892
893bool SceneLoader::decode(const std::string &path, const LoadOptions &options, DecodedScene *out) {
894 const std::string key = normPath(path);
895 // The unified resource cache may hold a decode from an earlier load; a
896 // reload/diff must see the file as it is now, so refresh the cached entry
897 // before asking for it (no-op when nothing is cached yet).
898 eve::ResourceManager::getInstance().reload(path).ignore("scene reload");
899
900 auto *mod3d = ModuleManager::getInstance<model3d::Model3D>("Model3D");
901 if (!mod3d) mod3d = model3d::Model3D::create();
902 model3d::ModelData *md = nullptr;
903 try {
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();
908 return false;
909 } catch (...) {
910 std::lock_guard<std::mutex> lock(pendingMu_);
911 lastErrors_[key] = "unknown model decode error";
912 return false;
913 }
914 if (!md) {
915 std::lock_guard<std::mutex> lock(pendingMu_);
916 lastErrors_[key] = "model decoder returned no scene";
917 return false;
918 }
919
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");
926 }
927 {
928 std::lock_guard<std::mutex> lock(pendingMu_);
929 lastErrors_.erase(key);
930 warnings_[key] = std::move(importWarnings);
931 }
932
933 out->path = normPath(path);
934 out->md = md;
935 out->options = options;
936 out->root = std::make_unique<scene::NodeDesc>(buildNodeDesc(md->getScene(), &out->slots));
937 collectCpuImages(md->getScene(), out->slots, options.mipmaps, out->cpuImages);
938 return true;
939}
940
941scene::SceneHost *SceneLoader::mount(DecodedScene &d) {
942 scene::SceneHost *host = borrowResult(scene::SceneHost::createHost(d.path));
943 if (!host) return nullptr;
944 host->setTree(std::move(*d.root));
945
946 graphics::Graphics *gfx = currentGraphics();
947 if (!d.slots.empty()) fillSceneBounds(host, d.slots);
948
949 if (gfx) {
950 MeshCache shared;
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);
955 }
956 fillSceneBounds(host, d.slots);
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)};
960 return host;
961}
962
963void SceneLoader::clearTextures() {
964 for (auto &kv : textures_) delete kv.second;
965 textures_.clear();
966}
967
968// ---- public: file / lifecycle ----
969
970scene::SceneHost *SceneLoader::load(const std::string &path, bool linkRenderables,
971 const LoadOptions &options) {
972 const std::string key = normPath(path);
973 DecodedScene d;
974 auto warm = prewarmed_.find(key);
975 if (warm != prewarmed_.end()) {
976 d = std::move(warm->second);
977 prewarmed_.erase(warm);
978 } else if (!decode(path, options, &d)) {
979 return nullptr;
980 }
981 if (!linkRenderables) {
983 if (!host) return nullptr;
984 host->setTree(std::move(*d.root));
986 scenes_[d.path] = Loaded{d.path, host, nullptr, options, {}, {}, nullptr, {}};
987 return host;
988 }
989 scene::SceneHost *host = mount(d);
990 return host;
991}
992
994 return load(path, true, options);
995}
996
997scene::SceneHost *SceneLoader::loadPreset(const std::string &path, const std::string &preset) {
998 return load(path, presetOptions(preset));
999}
1000
1002 const std::string key = normPath(path);
1003 auto it = scenes_.find(key);
1004 if (it == scenes_.end()) {
1005 if (!load(path, options)) {
1006 const std::string message = decodeErrorFor(path);
1008 eve::DiagnosticCode::Failed, message.empty() ? "scene reload failed to load the scene" : message, key,
1009 {}, "sceneloader.reload"));
1010 }
1011 if (out) *out = SceneDiff{};
1013 }
1014 Loaded &ld = it->second;
1015
1016 DecodedScene d;
1017 if (!decode(path, options, &d)) {
1018 const std::string message = decodeErrorFor(path);
1020 eve::DiagnosticCode::Failed, message.empty() ? "scene reload failed to decode the scene" : message, key, {},
1021 "sceneloader.reload"));
1022 }
1023
1024 SceneDiff diff = diffTree(ld.host, *d.root);
1025 if (out) *out = diff;
1026 if (!diff.empty()) {
1027 applyTreeDiff(ld.host, *d.root, diff, nullptr, nullptr);
1028 MeshCache shared;
1029 linkMeshNodes(ld.host, d.slots, ld.gfx, options, textures_, shared, &d.cpuImages);
1031 }
1034}
1035
1036SceneDiff SceneLoader::diff(const std::string &path) {
1037 const std::string key = normPath(path);
1038 eve::ResourceManager::getInstance().reload(path).ignore("scene diff reload"); // fresh decode for diffing
1039 auto *mod3d = ModuleManager::getInstance<model3d::Model3D>("Model3D");
1040 if (!mod3d) mod3d = model3d::Model3D::create();
1041 model3d::ModelData *md = nullptr;
1042 try {
1043 md = mod3d->newModelDataFromFile(path);
1044 } catch (...) {
1045 return SceneDiff{};
1046 }
1047 if (!md) return SceneDiff{};
1049 scene::NodeDesc newRoot = buildNodeDesc(md->getScene(), &slots);
1050 SceneDiff d;
1051 auto it = scenes_.find(key);
1052 if (it != scenes_.end() && it->second.host) {
1053 d = diffTree(it->second.host, newRoot);
1054 } else {
1055 // Nothing mounted: every node in the new tree is an add.
1056 std::function<void(const scene::NodeDesc &, const std::string &)> walk =
1057 [&](const scene::NodeDesc &n, const std::string &parent) {
1058 d.entries.push_back({SceneDiffEntry::Action::Add, n.id, parent});
1059 ++d.added;
1060 for (const auto &c : n.children) walk(c, n.id);
1061 };
1062 walk(newRoot, "");
1063 }
1064 return d;
1065}
1066
1068 const LoadOptions &options) {
1069 DecodedScene decoded;
1070 if (!decode(path, options, &decoded)) {
1071 const std::string message = decodeErrorFor(path);
1074 message.empty() ? "scene inspection failed to decode the source" : message,
1075 normPath(path), {}, "sceneloader.inspect"));
1076 }
1077 SceneInspection result;
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);
1081 else {
1082 std::function<void(const scene::NodeDesc&, const std::string&)> add =
1083 [&](const scene::NodeDesc& node, const std::string& parent) {
1084 result.diff.entries.push_back({SceneDiffEntry::Action::Add, node.id, parent});
1085 ++result.diff.added;
1086 for (const auto& child : node.children) add(child, node.id);
1087 };
1088 add(*decoded.root, "");
1089 }
1090 std::function<void(const scene::NodeDesc&)> collect = [&](const scene::NodeDesc& node) {
1091 ++result.nodeCount;
1092 if (decoded.slots.find(node.id) != decoded.slots.end()) ++result.meshNodeCount;
1093 if (std::find(node.tags.begin(), node.tags.end(), "model-socket") != node.tags.end())
1094 result.sockets.push_back(node.name);
1095 if (std::find(node.tags.begin(), node.tags.end(), "collision") != node.tags.end())
1096 result.collisions.push_back(node.name);
1097 for (const auto& child : node.children) collect(child);
1098 };
1099 collect(*decoded.root);
1100 const int count = warningCount(path);
1101 for (int i = 0; i < count; ++i) result.warnings.push_back(warning(path, i));
1102 return eve::Result<SceneInspection>::success(std::move(result));
1103}
1104
1106 auto it = scenes_.find(normPath(path));
1107 return (it != scenes_.end()) ? it->second.host : nullptr;
1108}
1109
1110int SceneLoader::nodeCount(const std::string &path) {
1112 return h ? h->getNodeCount() : 0;
1113}
1114
1115bool SceneLoader::loaded(const std::string &path) { return scenes_.count(normPath(path)) > 0; }
1116
1117void SceneLoader::unload(const std::string &path) {
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) {
1123 if (const auto *l = it->second.host->findLink(&n, scene::findLinkKind("renderable3d"))) {
1124 destroyRenderable(static_cast<graphics::Renderable3D *>(l->target));
1125 }
1126 n.links.clear();
1127 }
1128 ecs::DestroyEntity(it->second.host);
1129 }
1130 for (graphics::Light3D *l : it->second.lights) ecs::DestroyEntity(l);
1131 for (graphics::Camera3D *c : it->second.cameras) ecs::DestroyEntity(c);
1132 delete it->second.skeleton;
1133 for (animation::AnimClip *clip : it->second.clips) delete clip;
1134 scenes_.erase(it);
1135}
1136
1137// ---- async loading ----
1138
1139bool SceneLoader::loadAsync(const std::string &path, const LoadOptions &options,
1140 std::function<void(scene::SceneHost *)> done) {
1141 const std::string key = normPath(path);
1142 {
1143 std::lock_guard<std::mutex> lock(pendingMu_);
1144 if (inFlight_.count(key)) return false;
1145 inFlight_.insert(key);
1146 }
1147 if (!pool_) pool_ = std::make_shared<thread::ThreadPool>(2);
1148
1149 auto self = this;
1150 pool_->submit([self, key, options, done]() {
1151 DecodedScene d;
1152 if (self->decode(key, options, &d)) {
1153 std::lock_guard<std::mutex> lock(self->pendingMu_);
1154 d.done = done;
1155 self->pending_.push_back(std::move(d));
1156 }
1157 std::lock_guard<std::mutex> lock(self->pendingMu_);
1158 self->inFlight_.erase(key);
1159 });
1160 return true;
1161}
1162
1163bool SceneLoader::loadAsyncPreset(const std::string &path, const std::string &preset) {
1164 return loadAsync(path, presetOptions(preset));
1165}
1166
1168 std::vector<DecodedScene> ready;
1169 {
1170 std::lock_guard<std::mutex> lock(pendingMu_);
1171 ready.swap(pending_);
1172 }
1173 for (auto &d : ready) {
1174 if (!d.md) continue;
1175 if (d.prewarmOnly) {
1176 prewarmed_[d.path] = std::move(d);
1177 continue;
1178 }
1179 scene::SceneHost *h = mount(d);
1180 if (d.done) d.done(h);
1181 }
1182 return static_cast<int>(ready.size());
1183}
1184
1185bool SceneLoader::prewarmAsync(const std::string &path, const LoadOptions &options) {
1186 const std::string key = normPath(path);
1187 {
1188 std::lock_guard<std::mutex> lock(pendingMu_);
1189 if (prewarmed_.count(key) || inFlight_.count(key)) return false;
1190 inFlight_.insert(key);
1191 }
1192 if (!pool_) pool_ = std::make_shared<thread::ThreadPool>(2);
1193
1194 auto self = this;
1195 pool_->submit([self, key, options]() {
1196 DecodedScene d;
1197 if (self->decode(key, options, &d)) {
1198 d.prewarmOnly = true;
1199 std::lock_guard<std::mutex> lock(self->pendingMu_);
1200 self->pending_.push_back(std::move(d));
1201 }
1202 std::lock_guard<std::mutex> lock(self->pendingMu_);
1203 self->inFlight_.erase(key);
1204 });
1205 return true;
1206}
1207
1208bool SceneLoader::prewarmed(const std::string &path) const {
1209 return prewarmed_.count(normPath(path)) > 0;
1210}
1211
1212void SceneLoader::clearPrewarm(const std::string &path) {
1213 auto it = prewarmed_.find(normPath(path));
1214 if (it == prewarmed_.end()) return;
1215 prewarmed_.erase(it);
1216}
1217
1219 std::lock_guard<std::mutex> lock(pendingMu_);
1220 return static_cast<int>(pending_.size() + inFlight_.size());
1221}
1222
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;
1227}
1228
1229int SceneLoader::warningCount(const std::string &path) const {
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());
1233}
1234
1235std::string SceneLoader::warning(const std::string &path, int index) const {
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())
1239 return {};
1240 return it->second[static_cast<size_t>(index)];
1241}
1242
1243bool SceneLoader::setLod(const std::string &path, int level) {
1245 if (!h || level < 0) return false;
1246 bool found = false;
1247 auto tree = h->tree();
1248 for (scene::SceneNode &node : tree->nodes) {
1249 int nodeLod = -1;
1250 for (const std::string &tag : node.tags)
1251 if (tag.rfind("lod:", 0) == 0) nodeLod = std::atoi(tag.c_str() + 4);
1252 if (nodeLod >= 0) {
1253 node.visible = nodeLod == level;
1254 found = found || node.visible;
1255 }
1256 }
1257 return found;
1258}
1259
1260int SceneLoader::socketCount(const std::string &path) const {
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());
1264}
1265
1266std::string SceneLoader::socketName(const std::string &path, int index) const {
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
1271 : std::string{};
1272}
1273
1274int SceneLoader::collisionCount(const std::string &path) const {
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());
1278}
1279
1280std::string SceneLoader::collisionName(const std::string &path, int index) const {
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");
1284 return static_cast<size_t>(index) < nodes.size() ? nodes[static_cast<size_t>(index)]->name
1285 : std::string{};
1286}
1287
1288// ---- imported scene extras ----
1289
1290int SceneLoader::lightCount(const std::string &path) {
1291 auto it = scenes_.find(normPath(path));
1292 return (it != scenes_.end()) ? static_cast<int>(it->second.lights.size()) : 0;
1293}
1294
1296 auto it = scenes_.find(normPath(path));
1297 if (it == scenes_.end() || index < 0 ||
1298 static_cast<size_t>(index) >= it->second.lights.size())
1299 return nullptr;
1300 return it->second.lights[size_t(index)];
1301}
1302
1303int SceneLoader::cameraCount(const std::string &path) {
1304 auto it = scenes_.find(normPath(path));
1305 return (it != scenes_.end()) ? static_cast<int>(it->second.cameras.size()) : 0;
1306}
1307
1309 auto it = scenes_.find(normPath(path));
1310 if (it == scenes_.end() || index < 0 ||
1311 static_cast<size_t>(index) >= it->second.cameras.size())
1312 return nullptr;
1313 return it->second.cameras[size_t(index)];
1314}
1315
1316int SceneLoader::animationCount(const std::string &path) {
1317 auto it = scenes_.find(normPath(path));
1318 return (it != scenes_.end()) ? static_cast<int>(it->second.clips.size()) : 0;
1319}
1320
1322 auto it = scenes_.find(normPath(path));
1323 return (it != scenes_.end()) ? it->second.skeleton : nullptr;
1324}
1325
1327 auto it = scenes_.find(normPath(path));
1328 if (it == scenes_.end() || index < 0 ||
1329 static_cast<size_t>(index) >= it->second.clips.size())
1330 return nullptr;
1331 return it->second.clips[size_t(index)];
1332}
1333
1334void SceneLoader::expose(ssq::Table &table) {
1335 auto cls = table.addClass(name, SceneLoader::create, false);
1336 expose(cls);
1337}
1338
1339void SceneLoader::expose(ssq::Class &cls) {
1340 cls.addFunc("getName", &SceneLoader::getName);
1341 cls.addFunc("load",
1342 std::function<scene::SceneHost *(SceneLoader *, const std::string &)>(
1343 [](SceneLoader *sl, const std::string &path) -> scene::SceneHost * {
1344 return sl ? sl->load(path) : nullptr;
1345 }));
1346 cls.addFunc("loadPreset", &SceneLoader::loadPreset);
1347 cls.addFunc("host", &SceneLoader::host);
1348 const HSQUIRRELVM vm = cls.getHandle();
1349 cls.addFunc("reload", [vm](SceneLoader *value, const std::string &path) {
1350 if (!value)
1352 vm,
1354 "scene loader must not be null", "scene")),
1355 [](bool changed) { return eve::Value::boolean(changed); });
1356 return eve::script::projectResult(vm, value->reload(path),
1357 [](bool changed) { return eve::Value::boolean(changed); });
1358 });
1359 cls.addFunc("nodeCount", &SceneLoader::nodeCount);
1360 cls.addFunc("loaded", &SceneLoader::loaded);
1361 cls.addFunc("unload", &SceneLoader::unload);
1362 cls.addFunc("pollAsync", &SceneLoader::pollAsync);
1363 cls.addFunc("loadAsync", &SceneLoader::loadAsyncDefault);
1364 cls.addFunc("loadAsyncPreset", &SceneLoader::loadAsyncPreset);
1365 cls.addFunc("pendingAsyncCount", &SceneLoader::pendingAsyncCount);
1366 cls.addFunc("prewarmed", &SceneLoader::prewarmed);
1367 cls.addFunc("clearPrewarm", &SceneLoader::clearPrewarm);
1368 cls.addFunc("prewarm", &SceneLoader::prewarm);
1369 cls.addFunc("warningCount", &SceneLoader::warningCount);
1370 cls.addFunc("warning", &SceneLoader::warning);
1371 cls.addFunc("setLod", &SceneLoader::setLod);
1372 cls.addFunc("socketCount", &SceneLoader::socketCount);
1373 cls.addFunc("socketName", &SceneLoader::socketName);
1374 cls.addFunc("collisionCount", &SceneLoader::collisionCount);
1375 cls.addFunc("collisionName", &SceneLoader::collisionName);
1376 cls.addFunc("lightCount", &SceneLoader::lightCount);
1377 cls.addFunc("cameraCount", &SceneLoader::cameraCount);
1378 cls.addFunc("animationCount", &SceneLoader::animationCount);
1379 cls.addFunc("light", &SceneLoader::light);
1380 cls.addFunc("camera", &SceneLoader::camera);
1381 cls.addFunc("skeleton", &SceneLoader::skeleton);
1382 cls.addFunc("clip", &SceneLoader::clip);
1383}
1384
1385} // namespace sceneloader
1386} // namespace eve
double value
float w
Definition AnimClip.cpp:738
float y
Definition AnimClip.cpp:738
float x
Definition AnimClip.cpp:738
float z
Definition AnimClip.cpp:738
int root
Definition AnimSmr.cpp:119
const std::string & s
struct SQVM * HSQUIRRELVM
graphics::Texture * albedo
glm::vec4 p[6]
HSQUIRRELVM vm
Definition ECS.cpp:20
HSQOBJECT cls
Definition ECS.cpp:21
std::uint64_t parentId
std::string message
std::uint32_t key
glm::vec4 clip
glm::vec3 n
Definition Grass.cpp:63
std::array< double, 10 > q
double r
float v
std::int32_t second
std::int32_t c
int h
std::uint32_t height
std::array< float, 3 > scale
std::int32_t parent
std::uint64_t bytes
std::string name
MeleePoint3 b
Definition MeleeHit.cpp:41
MeleePoint3 a
Definition MeleeHit.cpp:40
std::vector< BvhNode > nodes
float roughness
Texture * normal
float metallic
int level
const std::string * tag
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:26
int idx
float f
float radius
std::string path
Definition PlayHost.cpp:110
std::string id
Definition PlayHost.cpp:108
std::weak_ptr< PrimitiveScene > scene
float d
float t
Mesh * mesh
int lod
const RoadNode * node
bool found
bool repeatV
std::string filter
std::string mipmap
bool repeatU
bool mips
Light3D::Data * light
LocalPageCacheEntry slots[ShadowConfig::kLocalSlots]
std::uint32_t count
const SquirrelValueOptions & options
The single Squirrel projection for common Result, Status and Value.
int children
Definition TreeMesh.cpp:295
UIHostHandle host
uint32_t index
float m[16]
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.
Definition Diagnostic.h:125
EVENGINE_API_FOUNDATION public API.
Definition Exception.h:13
virtual std::string getName() const =0
Returns the name.
eve::Result< bool > reload(const std::string &normPath) override
Reloads .
Definition Resource.cpp:390
static ResourceManager & getInstance()
Returns the instance.
Definition Resource.cpp:15
Move-only operation result carrying either a value or Status.
Definition Result.h:155
static Result success(T value)
Construct a successful result owning value.
Definition Result.h:164
const T & value() const &
Borrow the value from a const lvalue after checking success.
Definition Result.h:308
bool ok() const noexcept
Whether this result represents a non-failure outcome.
Definition Result.h:255
static Result failure(Status status)
Construct a failed result from a structured status.
Definition Result.h:175
static Status success(StatusCode code=StatusCode::Ok)
Construct a successful status with an explicit non-error outcome.
Definition Status.h:81
static Value boolean(bool value)
Compatibility factory for a boolean value.
Definition Value.h:108
Keyframed skeletal animation clip (local TRS tracks per bone). Script type: AnimClip.
Definition AnimClip.h:145
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).
Definition ByteData.h:13
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" (...
Definition Light.h:191
static Light3D * createLight(const std::string &type="point")
Definition Light.cpp:90
GPU mesh handle (+ optional CPU morph targets).
Definition Mesh.h:25
EVENGINE_API_BACKENDS public API.
Represents raw pixel data.
Definition ImageData.h:38
void * getData() const
std::string getFormat() const
CPU-side decoded 3D model (Assimp scene owned via medialoader::ModelScene). Does not upload to GPU — ...
Definition ModelData.h:39
const aiScene * getScene() const
Returns the scene.
Definition ModelData.cpp:67
ECS mount point for one scene graph (full scene or nested subtree root). Isomorphic to eve::ui::UIHos...
Definition SceneHost.h:89
static eve::Result< SceneHost * > createHost(const std::string &name="")
Creates an ECS-owned scene host with an explicit Result contract.
static void updateHost(SceneHost *host)
Updates host.
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.
Definition SceneLink.cpp:39
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.
Definition Widget.cpp:635
Build metadata (engine git commit, build time, third-party version).
Definition Build.cpp:16
Declarative scene-node description (build once / on dirty → flatten into SceneHost::Tree)....
Definition NodeDesc.h:19
Retained scene node (arena). Conceptual GameObject; isomorphic to eve::ui::UINode.
Definition SceneHost.h:46
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...
Definition SceneLoader.h:98
SceneDiff public API.
Definition SceneLoader.h:70
bool empty() const
Empty.
Definition SceneLoader.h:78
std::vector< SceneDiffEntry > entries
Definition SceneLoader.h:71
CPU-only import inspection result used by editor preflight.
Definition SceneLoader.h:82
std::vector< std::string > warnings
Definition SceneLoader.h:86
std::vector< std::string > collisions
Definition SceneLoader.h:88
std::vector< std::string > sockets
Definition SceneLoader.h:87
std::vector< WidgetDesc > children
Definition Widget.h:111
std::string key
Reconciliation key; defaults to id when empty.
Definition Widget.h:19
std::string id
Definition Widget.h:17
glm::uvec4 counts