载入中...
搜索中...
未找到
SceneLoader.cpp
浏览该文件的文档.
2
3#include "scene/SceneHost.h"
4#include "scene/NodeDesc.h"
6#include "model3d/Model3D.h"
7#include "model3d/ModelData.h"
8#include "graphics/Graphics.h"
9#include "graphics/Light.h"
10#include "graphics/Mesh.h"
12#include "graphics/Texture.h"
14#include "filesystem/FileData.h"
15#include "data/ByteData.h"
16#include "image/Image.h"
17#include "image/ImageData.h"
19#include "thread/ThreadPool.h"
20#include "common/ECS.h"
21#include "common/Resource.h"
22
23#include <assimp/scene.h>
24#include <assimp/mesh.h>
25#include <assimp/material.h>
26#include <assimp/matrix4x4.h>
27#include <assimp/quaternion.h>
28#include <assimp/vector3.h>
29#include <assimp/texture.h>
30#include <assimp/light.h>
31#include <assimp/camera.h>
32#include <assimp/GltfMaterial.h>
33
34#include <glm/glm.hpp>
35#include <glm/gtx/matrix_decompose.hpp>
36#include <glm/gtc/matrix_transform.hpp>
37#include <glm/gtc/quaternion.hpp>
38
39#include <limits>
40
41#include <simplesquirrel/simplesquirrel.hpp>
42
43#include <algorithm>
44#include <cmath>
45#include <cstdlib>
46#include <functional>
47#include <memory>
48#include <unordered_set>
49
50namespace eve {
51namespace sceneloader {
52
54
56 // Decoded ModelData instances are owned by the unified resource cache
57 // (Model3D::newModelDataFromFile returns cache-shared resources), so no
58 // cleanup is needed here.
59 clearTextures();
60}
61
62namespace {
63
64constexpr float kEps = 1e-5f;
65
66std::string normPath(const std::string &p) {
67 std::string out;
68 out.reserve(p.size());
69 for (char c : p) {
70 if (c == '\\') out.push_back('/');
71 else out.push_back(c);
72 }
73 return out;
74}
75
76graphics::Graphics *currentGraphics() {
77 return ModuleManager::getInstance<graphics::Graphics>("Graphics");
78}
79
80model3d::ModelLoadOptions toModelOptions(const LoadOptions &o) {
81 model3d::ModelLoadOptions m;
82 m.triangulate = o.triangulate;
83 m.generateNormalsIfMissing = o.generateNormalsIfMissing;
84 m.joinIdenticalVertices = o.joinIdenticalVertices;
85 m.flipUVs = o.flipUVs;
86 m.improveCacheLocality = o.improveCacheLocality;
87 return m;
88}
89
90bool approx(float a, float b) { return std::fabs(a - b) < kEps; }
91
92// ---- transform helpers ----
93
94void decomposeNode(const aiMatrix4x4 &m, float &x, float &y, float &z, float &yaw, float &pitch,
95 float &roll, float &sx, float &sy, float &sz) {
96 aiVector3D pos, scale;
97 aiQuaternion rot;
98 m.Decompose(scale, rot, pos);
99 x = pos.x;
100 y = pos.y;
101 z = pos.z;
102 sx = scale.x;
103 sy = scale.y;
104 sz = scale.z;
105 glm::quat q(rot.w, rot.x, rot.y, rot.z);
106 glm::vec3 e = glm::eulerAngles(q);
107 yaw = e.y;
108 pitch = e.x;
109 roll = e.z;
110}
111
112std::string uniqueId(const std::string &base, std::unordered_map<std::string, int> &counts) {
113 std::string b = base.empty() ? "node" : base;
114 int &n = counts[b];
115 std::string id = b;
116 if (n > 0) id = b + "_" + std::to_string(n);
117 ++n;
118 return id;
119}
120
121// ---- texture helpers (embedded / VFS, cached by path) ----
122
124struct SamplerSpec {
125 bool repeatU = true;
126 bool repeatV = true;
127 bool mips = true;
128 std::string filter = "linear"; // "linear" | "nearest"
129 std::string mipmap = "linear"; // "none" | "linear" | "nearest"
130};
131
132SamplerSpec samplerFor(const aiMaterial *mat, aiTextureType type, bool wantMips) {
133 SamplerSpec s;
134 if (!mat) {
135 s.mips = wantMips;
136 return s;
137 }
138 int modeU = aiTextureMapMode_Wrap;
139 int modeV = aiTextureMapMode_Wrap;
140 mat->Get(AI_MATKEY_MAPPINGMODE_U(type, 0), modeU);
141 mat->Get(AI_MATKEY_MAPPINGMODE_V(type, 0), modeV);
142 s.repeatU = (modeU != aiTextureMapMode_Clamp && modeU != aiTextureMapMode_Mirror);
143 s.repeatV = (modeV != aiTextureMapMode_Clamp && modeV != aiTextureMapMode_Mirror);
144
145 // glTF sampler filter values (AI_MATKEY_GLTF_MAPPINGFILTER_*).
146 int mag = 0, min = 0;
147 mat->Get(AI_MATKEY_GLTF_MAPPINGFILTER_MAG(type, 0), mag);
148 mat->Get(AI_MATKEY_GLTF_MAPPINGFILTER_MIN(type, 0), min);
149 s.filter = (mag == 9728) ? "nearest" : "linear"; // 0 / 9729(linear) -> linear
150 switch (min) {
151 case 9728: // NEAREST
152 case 9729: // LINEAR — no mipmaps requested by the source
153 s.mips = false;
154 s.mipmap = "none";
155 break;
156 case 9986: // NEAREST_MIPMAP_LINEAR
157 case 9987: // LINEAR_MIPMAP_LINEAR
158 s.mips = wantMips;
159 s.mipmap = "linear";
160 break;
161 case 9984: // NEAREST_MIPMAP_NEAREST
162 case 9985: // LINEAR_MIPMAP_NEAREST
163 s.mips = wantMips;
164 s.mipmap = "nearest";
165 break;
166 default: // unspecified -> honor the global toggle
167 s.mips = wantMips;
168 s.mipmap = wantMips ? "linear" : "none";
169 break;
170 }
171 return s;
172}
173
174graphics::Texture *resolveTexture(graphics::Graphics *gfx, const aiScene *scene,
175 const aiMaterial *mat, aiTextureType type,
176 SceneLoader::TextureCache &cache, bool wantMips,
177 const SceneLoader::CpuImageMap *predecoded = nullptr) {
178 if (!gfx || !scene || !mat) return nullptr;
179 aiString path;
180 if (mat->GetTexture(type, 0, &path) != AI_SUCCESS) return nullptr;
181 const char *p = path.C_Str();
182 if (!p || !p[0]) return nullptr;
183
184 const SamplerSpec s = samplerFor(mat, type, wantMips);
185 eve::image::Image::create();
186
187 const std::string keySuffix =
188 std::string(s.repeatU ? "|1" : "|0") + (s.repeatV ? "1" : "0") + "|" + s.filter + "|" +
189 s.mipmap;
190
191 // Embedded texture ("*0", "*1", ...).
192 if (p[0] == '*') {
193 int idx = std::atoi(p + 1);
194 if (idx < 0 || static_cast<unsigned>(idx) >= scene->mNumTextures) return nullptr;
195 const aiTexture *tex = scene->mTextures[idx];
196 if (!tex || !tex->pcData) return nullptr;
197 const std::string key = "*" + std::to_string(idx) + keySuffix;
198 auto it = cache.find(key);
199 if (it != cache.end()) return it->second;
200
201 graphics::Texture *t = nullptr;
202 if (tex->mHeight == 0) {
203 eve::data::ByteData bytes(tex->pcData, static_cast<size_t>(tex->mWidth));
204 try {
205 eve::image::ImageData *img = eve::image::Image::create()->newImageData(&bytes);
206 t = gfx->newTextureWithSampler(img, s.repeatU, s.repeatV, s.mips, 8.f, s.filter,
207 s.mipmap);
208 delete img;
209 } catch (...) {
210 return nullptr;
211 }
212 } else {
213 const unsigned w = tex->mWidth;
214 const unsigned h = tex->mHeight;
215 std::vector<uint8_t> rgba(size_t(w) * size_t(h) * 4);
216 const aiTexel *src = tex->pcData;
217 for (unsigned i = 0; i < w * h; ++i) {
218 rgba[i * 4 + 0] = src[i].r;
219 rgba[i * 4 + 1] = src[i].g;
220 rgba[i * 4 + 2] = src[i].b;
221 rgba[i * 4 + 3] = src[i].a;
222 }
223 eve::image::ImageData img(int(w), int(h), "RGBA8", rgba.data(), false);
224 t = gfx->newTextureWithSampler(&img, s.repeatU, s.repeatV, s.mips, 8.f, s.filter,
225 s.mipmap);
226 }
227 if (t) cache[key] = t;
228 return t;
229 }
230
231 // External file through the VFS.
232 const std::string key = normPath(p) + keySuffix;
233 auto it = cache.find(key);
234 if (it != cache.end()) return it->second;
235
236 // Off-thread pre-decoded CPU image (async path): upload directly, no disk IO.
237 if (predecoded) {
238 auto pit = predecoded->find(key);
239 if (pit != predecoded->end()) {
240 const SceneLoader::CpuImage &ci = pit->second;
241 eve::image::ImageData img(ci.w, ci.h, "RGBA8",
242 const_cast<uint8_t *>(ci.rgba.data()), false);
243 graphics::Texture *t = gfx->newTextureWithSampler(&img, s.repeatU, s.repeatV, s.mips,
244 8.f, s.filter, s.mipmap);
245 if (t) cache[key] = t;
246 return t;
247 }
248 }
249
250 try {
251 auto *fs = eve::filesystem::Filesystem::create();
252 std::unique_ptr<eve::filesystem::FileData> fd(fs->read(p));
253 if (fd && fd->getSize() > 0) {
254 eve::image::ImageData *img = eve::image::Image::create()->newImageData(fd.get());
255 graphics::Texture *t = gfx->newTextureWithSampler(img, s.repeatU, s.repeatV, s.mips,
256 8.f, s.filter, s.mipmap);
257 delete img;
258 if (t) cache[key] = t;
259 return t;
260 }
261 } catch (...) {
262 }
263 return nullptr;
264}
265
266// Pre-decode external (non-embedded) texture files off the calling thread so the
267// async loader can skip disk IO + image decode on the render thread.
268void collectCpuImages(const aiScene *scene, const MeshSlotMap &slots, bool wantMips,
270 if (!scene) return;
271 eve::image::Image::create();
272 const aiTextureType kTypes[4] = {aiTextureType_BASE_COLOR, aiTextureType_DIFFUSE,
273 aiTextureType_NORMALS, aiTextureType_HEIGHT};
274 for (const auto &kv : slots) {
275 for (const MeshSlot &slot : kv.second) {
276 if (!slot.scene || slot.materialIndex >= scene->mNumMaterials) continue;
277 const aiMaterial *mat = scene->mMaterials[slot.materialIndex];
278 if (!mat) continue;
279 for (aiTextureType type : kTypes) {
280 aiString p;
281 if (mat->GetTexture(type, 0, &p) != AI_SUCCESS) continue;
282 const char *c = p.C_Str();
283 if (!c || !c[0] || c[0] == '*') continue; // embedded handled on main thread
284 const SamplerSpec s = samplerFor(mat, type, wantMips);
285 const std::string key = normPath(c) + std::string(s.repeatU ? "|1" : "|0") +
286 (s.repeatV ? "1" : "0") + "|" + s.filter + "|" + s.mipmap;
287 if (out.count(key)) continue;
288 try {
289 auto *fs = eve::filesystem::Filesystem::create();
290 std::unique_ptr<eve::filesystem::FileData> fd(fs->read(c));
291 if (!fd || fd->getSize() == 0) continue;
293 eve::image::Image::create()->newImageData(fd.get());
294 if (!img) continue;
295 if (img->getFormat() == "RGBA8") {
296 SceneLoader::CpuImage ci;
297 ci.w = img->getWidth();
298 ci.h = img->getHeight();
299 const size_t n = size_t(ci.w) * size_t(ci.h) * 4;
300 ci.rgba.assign(reinterpret_cast<const uint8_t *>(img->getData()),
301 reinterpret_cast<const uint8_t *>(img->getData()) + n);
302 out[key] = std::move(ci);
303 }
304 delete img;
305 } catch (...) {
306 }
307 }
308 }
309 }
310}
311
312// ---- renderable creation ----
313
314graphics::Renderable3D *makeRenderable(graphics::Graphics *gfx, const MeshSlot &slot,
315 SceneLoader::TextureCache &textures, bool mipmaps) {
316 if (!gfx || !slot.mesh) return nullptr;
317 graphics::Mesh *mesh = gfx->newMeshFromAssimp(*slot.mesh); // local-space (hierarchy transform)
318 if (!mesh) return nullptr;
319 auto *r = graphics::Renderable3D::create();
320 r->meshRenderer()->visible = true;
321 r->setMesh(mesh);
322
323 const aiScene *scene = slot.scene;
324 const aiMaterial *mat = nullptr;
325 if (scene && scene->mMaterials && slot.materialIndex < scene->mNumMaterials)
326 mat = scene->mMaterials[slot.materialIndex];
327
328 aiColor3D base(1.f, 1.f, 1.f);
329 if (mat) {
330 if (mat->Get(AI_MATKEY_BASE_COLOR, base) != AI_SUCCESS)
331 mat->Get(AI_MATKEY_COLOR_DIFFUSE, base);
332 float metallic = 0.f;
333 float roughness = 0.45f;
334 mat->Get(AI_MATKEY_METALLIC_FACTOR, metallic);
335 mat->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness);
336 r->setMetallic(metallic);
337 r->setRoughness(roughness);
338 }
339
340 graphics::Texture *albedo =
341 resolveTexture(gfx, scene, mat, aiTextureType_BASE_COLOR, textures, mipmaps);
342 if (!albedo) albedo = resolveTexture(gfx, scene, mat, aiTextureType_DIFFUSE, textures, mipmaps);
343 graphics::Texture *normal =
344 resolveTexture(gfx, scene, mat, aiTextureType_NORMALS, textures, mipmaps);
345 graphics::Texture *height =
346 resolveTexture(gfx, scene, mat, aiTextureType_HEIGHT, textures, mipmaps);
347
348 r->setTint(base.r, base.g, base.b, 1.f);
349 if (albedo) r->setTexture(albedo);
350 if (normal) r->setNormalTexture(normal);
351 if (height) r->setHeightTexture(height);
352 return r;
353}
354
355void destroyRenderable(graphics::Renderable3D *r) {
356 if (r) ecs::DestroyEntity(r);
357}
358
359// ---- mesh AABB bounds (picking / culling) ----
360
361glm::mat4 nodeLocalMatrix(const scene::SceneNode &n) {
362 glm::mat4 m(1.f);
363 m = glm::translate(m, glm::vec3(n.x, n.y, n.z));
364 if (n.space == "2d") {
365 m = glm::rotate(m, n.roll, glm::vec3(0.f, 0.f, 1.f));
366 } else {
367 m = glm::rotate(m, n.yaw, glm::vec3(0.f, 1.f, 0.f));
368 m = glm::rotate(m, n.pitch, glm::vec3(1.f, 0.f, 0.f));
369 m = glm::rotate(m, n.roll, glm::vec3(0.f, 0.f, 1.f));
370 }
371 m = glm::scale(m, glm::vec3(n.sx, n.sy, n.sz));
372 return m;
373}
374
375void fillMeshBoundsFromSlot(scene::SceneNode &n, const MeshSlot &slot) {
376 const aiMesh *mesh = slot.mesh;
377 if (!mesh || !mesh->mVertices || mesh->mNumVertices == 0) return;
378 float mn[3] = {std::numeric_limits<float>::max(),
379 std::numeric_limits<float>::max(),
380 std::numeric_limits<float>::max()};
381 float mx[3] = {std::numeric_limits<float>::lowest(),
382 std::numeric_limits<float>::lowest(),
383 std::numeric_limits<float>::lowest()};
384 for (unsigned i = 0; i < mesh->mNumVertices; ++i) {
385 const aiVector3D &v = mesh->mVertices[i];
386 for (int c = 0; c < 3; ++c) {
387 const float f = v[c];
388 mn[c] = std::min(mn[c], f);
389 mx[c] = std::max(mx[c], f);
390 }
391 }
392 n.bminX = mn[0];
393 n.bminY = mn[1];
394 n.bminZ = mn[2];
395 n.bmaxX = mx[0];
396 n.bmaxY = mx[1];
397 n.bmaxZ = mx[2];
398 n.hasBounds = true;
399}
400
402void unionChildBounds(scene::SceneHost::Tree &tree, int nodeIndex) {
403 scene::SceneNode &n = tree.nodes[size_t(nodeIndex)];
404 for (int c = n.firstChild; c >= 0; c = tree.nodes[size_t(c)].nextSibling) {
405 unionChildBounds(tree, c);
406 }
407
408 bool any = false;
409 float mn[3] = {std::numeric_limits<float>::max(),
410 std::numeric_limits<float>::max(),
411 std::numeric_limits<float>::max()};
412 float mx[3] = {std::numeric_limits<float>::lowest(),
413 std::numeric_limits<float>::lowest(),
414 std::numeric_limits<float>::lowest()};
415 for (int c = n.firstChild; c >= 0; c = tree.nodes[size_t(c)].nextSibling) {
416 scene::SceneNode &ch = tree.nodes[size_t(c)];
417 if (!ch.hasBounds) continue;
418 any = true;
419 const glm::mat4 lm = nodeLocalMatrix(ch);
420 for (int i = 0; i < 8; ++i) {
421 const glm::vec3 p((i & 1) ? ch.bmaxX : ch.bminX,
422 (i & 2) ? ch.bmaxY : ch.bminY,
423 (i & 4) ? ch.bmaxZ : ch.bminZ);
424 const glm::vec4 w = lm * glm::vec4(p, 1.f);
425 for (int k = 0; k < 3; ++k) {
426 mn[k] = std::min(mn[k], w[k]);
427 mx[k] = std::max(mx[k], w[k]);
428 }
429 }
430 }
431 if (!any) return;
432 if (!n.hasBounds) {
433 n.bminX = mn[0];
434 n.bminY = mn[1];
435 n.bminZ = mn[2];
436 n.bmaxX = mx[0];
437 n.bmaxY = mx[1];
438 n.bmaxZ = mx[2];
439 n.hasBounds = true;
440 } else {
441 n.bminX = std::min(n.bminX, mn[0]);
442 n.bminY = std::min(n.bminY, mn[1]);
443 n.bminZ = std::min(n.bminZ, mn[2]);
444 n.bmaxX = std::max(n.bmaxX, mx[0]);
445 n.bmaxY = std::max(n.bmaxY, mx[1]);
446 n.bmaxZ = std::max(n.bmaxZ, mx[2]);
447 }
448}
449
450// ---- NodeDesc build ----
451
452void buildNodeRecursive(const aiScene *scene, const aiNode *node, scene::NodeDesc &out,
453 MeshSlotMap *slots, std::unordered_map<std::string, int> &counts) {
454 const std::string rawName = (node && node->mName.length) ? node->mName.C_Str() : "<root>";
455 const std::string id = uniqueId(rawName, counts);
456
457 out.id = id;
458 out.key = id;
459 out.name = rawName;
460 out.space = "3d";
461 out.visible = true;
462 decomposeNode(node->mTransformation, out.x, out.y, out.z, out.yaw, out.pitch, out.roll,
463 out.sx, out.sy, out.sz);
464
465 for (unsigned i = 0; node && i < node->mNumMeshes; ++i) {
466 const unsigned mi = node->mMeshes[i];
467 if (mi >= scene->mNumMeshes) continue;
468 const aiMesh *mesh = scene->mMeshes[mi];
469 if (!mesh || mesh->mNumFaces == 0) continue;
470
471 const std::string cid = id + "_mesh" + std::to_string(i);
472 scene::NodeDesc child;
473 child.id = cid;
474 child.key = cid;
475 child.name = std::string("mesh") + std::to_string(i);
476 child.space = "3d";
477 child.visible = true;
478 out.children.push_back(std::move(child));
479
480 if (slots) {
481 MeshSlot s;
482 s.scene = scene;
483 s.mesh = mesh;
484 s.materialIndex = mesh->mMaterialIndex;
485 (*slots)[cid].push_back(std::move(s));
486 }
487 }
488
489 for (unsigned c = 0; node && c < node->mNumChildren; ++c) {
490 scene::NodeDesc child;
491 buildNodeRecursive(scene, node->mChildren[c], child, slots, counts);
492 out.children.push_back(std::move(child));
493 }
494}
495
496bool propsChanged(const scene::SceneNode &n, const scene::NodeDesc &d) {
497 if (n.visible != d.visible) return true;
498 return !(approx(n.x, d.x) && approx(n.y, d.y) && approx(n.z, d.z) && approx(n.yaw, d.yaw) &&
499 approx(n.pitch, d.pitch) && approx(n.roll, d.roll) && approx(n.sx, d.sx) &&
500 approx(n.sy, d.sy) && approx(n.sz, d.sz));
501}
502
503// Walk the new tree in DFS order, emitting Add / Move / Modify entries.
504void walkNew(const scene::NodeDesc &d, const std::string &parentId,
505 const std::unordered_map<std::string, const scene::SceneNode *> &oldNodes,
506 const std::unordered_map<std::string, std::string> &oldParent, SceneDiff &out) {
507 auto it = oldNodes.find(d.id);
508 if (it == oldNodes.end()) {
509 out.entries.push_back({SceneDiffEntry::Action::Add, d.id, parentId});
510 ++out.added;
511 } else {
512 auto pit = oldParent.find(d.id);
513 const std::string op = (pit != oldParent.end()) ? pit->second : "";
514 if (op != parentId) {
515 out.entries.push_back({SceneDiffEntry::Action::Move, d.id, parentId});
516 ++out.moved;
517 } else if (propsChanged(*it->second, d)) {
518 out.entries.push_back({SceneDiffEntry::Action::Modify, d.id, ""});
519 ++out.modified;
520 }
521 }
522 for (const auto &child : d.children) walkNew(child, d.id, oldNodes, oldParent, out);
523}
524
525// ---- lights / cameras / animation import ----
526
527void importLights(const aiScene *scene, std::vector<graphics::Light3D *> &out) {
528 if (!scene) return;
529 for (unsigned i = 0; i < scene->mNumLights; ++i) {
530 const aiLight *l = scene->mLights[i];
531 if (!l) continue;
532 std::string type = "point";
533 switch (l->mType) {
534 case aiLightSource_DIRECTIONAL:
535 type = "dir";
536 break;
537 case aiLightSource_POINT:
538 case aiLightSource_SPOT:
539 case aiLightSource_AREA:
540 default:
541 type = "point";
542 break;
543 }
544 graphics::Light3D *light = graphics::Light3D::createLight(type);
545 if (type == "dir") {
546 light->setDirection(l->mDirection.x, l->mDirection.y, l->mDirection.z);
547 } else {
548 light->setPosition(l->mPosition.x, l->mPosition.y, l->mPosition.z);
549 float radius = 8.f;
550 if (l->mAttenuationQuadratic > 1e-6f)
551 radius = 1.f / std::sqrt(l->mAttenuationQuadratic);
552 light->setRadius(radius);
553 }
554 light->setColor(l->mColorDiffuse.r, l->mColorDiffuse.g, l->mColorDiffuse.b, 1.f);
555 out.push_back(light);
556 }
557}
558
559void importCameras(const aiScene *scene, std::vector<graphics::Camera3D *> &out) {
560 if (!scene) return;
561 for (unsigned i = 0; i < scene->mNumCameras; ++i) {
562 const aiCamera *c = scene->mCameras[i];
563 if (!c) continue;
564 graphics::Camera3D *cam = graphics::Camera3D::createCamera();
565 cam->setActive(false);
566 cam->setEye(c->mPosition.x, c->mPosition.y, c->mPosition.z);
567 cam->setTarget(c->mLookAt.x, c->mLookAt.y, c->mLookAt.z);
568 cam->setUp(c->mUp.x, c->mUp.y, c->mUp.z);
569 cam->setFov(glm::degrees(c->mHorizontalFOV));
570 out.push_back(cam);
571 }
572}
573
574void importAnimations(const aiScene *scene, const LoadOptions &options,
575 animation::AnimSkeleton **skeletonOut,
576 std::vector<animation::AnimClip *> &clips) {
577 if (!scene || !options.importAnimations || scene->mNumAnimations == 0) return;
578 animation::AnimSkeleton *skeleton = animation::AnimImporter::loadSkeleton(scene);
579 if (!skeleton) return;
580 *skeletonOut = skeleton;
581 for (unsigned i = 0; i < scene->mNumAnimations; ++i) {
582 animation::AnimClip *clip = animation::AnimImporter::loadClip(scene, skeleton, int(i));
583 if (clip) clips.push_back(clip);
584 }
585}
586
587} // namespace
588
589// ---- public: pure helpers ----
590
592 scene::NodeDesc root;
593 if (!scene || !scene->mRootNode) return root;
594 std::unordered_map<std::string, int> counts;
595 buildNodeRecursive(scene, scene->mRootNode, root, slotsOut, counts);
596 return root;
597}
598
600 SceneDiff out;
601 std::unordered_map<std::string, const scene::SceneNode *> oldNodes;
602 std::unordered_map<std::string, std::string> oldParent;
603 if (host) {
604 auto t = host->tree();
605 for (size_t i = 0; i < t->nodes.size(); ++i) {
606 const scene::SceneNode &n = t->nodes[i];
607 oldNodes[n.id] = &n;
608 oldParent[n.id] = (n.parent >= 0) ? t->nodes[size_t(n.parent)].id : "";
609 }
610 }
611
612 // Collect the set of ids present in the new tree.
613 std::unordered_set<std::string> newIds;
614 std::function<void(const scene::NodeDesc &)> collect = [&](const scene::NodeDesc &d) {
615 newIds.insert(d.id);
616 for (const auto &c : d.children) collect(c);
617 };
618 collect(newRoot);
619
620 // Removed: present in old, absent in new.
621 for (const auto &kv : oldNodes) {
622 if (!newIds.count(kv.first)) {
623 out.entries.push_back({SceneDiffEntry::Action::Remove, kv.first, ""});
624 ++out.removed;
625 }
626 }
627
628 // Add / Move / Modify: walk the new tree (DFS so parents precede children).
629 walkNew(newRoot, "", oldNodes, oldParent, out);
630 return out;
631}
632
634 const SceneDiff &diff, graphics::Graphics *gfx,
635 const MeshSlotMap *slots) {
636 if (!host || diff.empty()) return false;
637
638 // Snapshot the mounted tree by id so kept GameObjects can keep their linked
639 // Renderable3D (no re-upload / no object rebuild) across the update.
640 std::unordered_map<std::string, const scene::SceneNode *> oldNodes;
641 {
642 auto t = host->tree();
643 for (const auto &n : t->nodes) oldNodes[n.id] = &n;
644 }
645
646 // Collect the ids present in the new tree.
647 std::unordered_set<std::string> newIds;
648 std::function<void(const scene::NodeDesc &)> collect = [&](const scene::NodeDesc &d) {
649 newIds.insert(d.id);
650 for (const auto &c : d.children) collect(c);
651 };
652 collect(newRoot);
653
654 // Destroy Renderable3D of removed GameObjects.
655 for (const auto &kv : oldNodes) {
656 if (!newIds.count(kv.first)) {
657 if (const auto *l = host->findLink(kv.second, scene::findLinkKind("renderable3d"))) {
658 destroyRenderable(static_cast<graphics::Renderable3D *>(l->target));
659 }
660 }
661 }
662
663 // Rebuild the arena from the new tree (DFS). Kept nodes copy their link from
664 // the old node (identity preserved); added mesh nodes get a fresh Renderable3D.
665 std::vector<scene::SceneNode> nodes;
666 nodes.reserve(newIds.size());
667 std::function<int(const scene::NodeDesc &, int)> build = [&](const scene::NodeDesc &d,
668 int parentIndex) -> int {
669 const int idx = int(nodes.size());
671 n.id = d.id;
672 n.key = d.key.empty() ? d.id : d.key;
673 n.name = d.name.empty() ? d.id : d.name;
674 n.space = d.space.empty() ? "3d" : d.space;
675 n.visible = d.visible;
676 n.x = d.x;
677 n.y = d.y;
678 n.z = d.z;
679 n.yaw = d.yaw;
680 n.pitch = d.pitch;
681 n.roll = d.roll;
682 n.sx = d.sx;
683 n.sy = d.sy;
684 n.sz = d.sz;
685 n.localDirty = true;
686 n.world = glm::mat4(1.f);
687 n.firstChild = -1;
688 n.nextSibling = -1;
689 n.parent = parentIndex;
690
691 auto oldIt = oldNodes.find(d.id);
692 if (oldIt != oldNodes.end()) {
693 n.links = oldIt->second->links;
694 n.objectId = oldIt->second->objectId;
695 n.bminX = oldIt->second->bminX;
696 n.bminY = oldIt->second->bminY;
697 n.bminZ = oldIt->second->bminZ;
698 n.bmaxX = oldIt->second->bmaxX;
699 n.bmaxY = oldIt->second->bmaxY;
700 n.bmaxZ = oldIt->second->bmaxZ;
701 n.hasBounds = oldIt->second->hasBounds;
702 }
703
704 nodes.push_back(std::move(n));
705
706 int prevChild = -1;
707 int firstChild = -1;
708 for (const auto &c : d.children) {
709 const int ci = build(c, idx);
710 if (firstChild < 0) firstChild = ci;
711 if (prevChild >= 0) nodes[size_t(prevChild)].nextSibling = ci;
712 prevChild = ci;
713 }
714 nodes[size_t(idx)].firstChild = firstChild;
715 return idx;
716 };
717 const int root = build(newRoot, -1);
718
719 // Fresh Renderable3D for newly added mesh GameObjects (only changed ones).
720 if (gfx && slots) {
721 TextureCache unused;
722 for (auto &n : nodes) {
723 if (!n.links.empty()) continue;
724 auto sit = slots->find(n.id);
725 if (sit == slots->end() || sit->second.empty()) continue;
726 graphics::Renderable3D *r = nullptr;
727 try {
728 r = makeRenderable(gfx, sit->second[0], unused, true);
729 } catch (...) {
730 r = nullptr;
731 }
732 if (r) {
733 n.links.push_back(scene::SceneLink{scene::findLinkKind("renderable3d"), r, 0});
734 }
735 }
736 }
737
738 host->tree()->nodes = std::move(nodes);
739 host->tree()->root = root;
742 if (slots) fillSceneBounds(host, *slots);
743 return true;
744}
745
747 if (!host) return;
748 auto t = host->tree();
749 for (auto &n : t->nodes) {
750 auto it = slots.find(n.id);
751 if (it == slots.end() || it->second.empty()) continue;
752 fillMeshBoundsFromSlot(n, it->second[0]);
753 }
754 if (t->root >= 0) unionChildBounds(*t, t->root);
755}
756
757// ---- public: file / lifecycle ----
758// ---- private: linking + async decode ----
759
760void SceneLoader::linkMeshNodes(scene::SceneHost *host, const MeshSlotMap &slots,
761 graphics::Graphics *gfx, const LoadOptions &options,
762 TextureCache &textures, MeshCache &shared,
763 const CpuImageMap *predecoded) {
764 if (!gfx) return;
765 for (const auto &kv : slots) {
766 scene::SceneNode *n = host->findById(kv.first);
767 if (!n || kv.second.empty()) continue;
768 if (host->findLink(n, scene::findLinkKind("renderable3d"))) continue;
769 const MeshSlot &slot = kv.second[0];
770 graphics::Mesh *mesh = nullptr;
771 auto it = shared.find(slot.mesh);
772 if (options.sharedMeshes && it != shared.end()) {
773 mesh = it->second;
774 } else {
775 try {
776 mesh = gfx->newMeshFromAssimp(*slot.mesh);
777 } catch (...) {
778 mesh = nullptr;
779 }
780 if (mesh && options.sharedMeshes) shared[slot.mesh] = mesh;
781 }
782 if (!mesh) continue;
783 auto *r = graphics::Renderable3D::create();
784 r->meshRenderer()->visible = true;
785 r->setMesh(mesh);
786
787 const aiScene *scene = slot.scene;
788 const aiMaterial *mat = nullptr;
789 if (scene && scene->mMaterials && slot.materialIndex < scene->mNumMaterials)
790 mat = scene->mMaterials[slot.materialIndex];
791 aiColor3D base(1.f, 1.f, 1.f);
792 if (mat) {
793 if (mat->Get(AI_MATKEY_BASE_COLOR, base) != AI_SUCCESS)
794 mat->Get(AI_MATKEY_COLOR_DIFFUSE, base);
795 float metallic = 0.f;
796 float roughness = 0.45f;
797 mat->Get(AI_MATKEY_METALLIC_FACTOR, metallic);
798 mat->Get(AI_MATKEY_ROUGHNESS_FACTOR, roughness);
799 r->setMetallic(metallic);
800 r->setRoughness(roughness);
801 }
802 graphics::Texture *albedo = resolveTexture(gfx, scene, mat, aiTextureType_BASE_COLOR,
803 textures, options.mipmaps, predecoded);
804 if (!albedo)
805 albedo = resolveTexture(gfx, scene, mat, aiTextureType_DIFFUSE, textures,
806 options.mipmaps, predecoded);
807 graphics::Texture *normal = resolveTexture(gfx, scene, mat, aiTextureType_NORMALS,
808 textures, options.mipmaps, predecoded);
809 graphics::Texture *height = resolveTexture(gfx, scene, mat, aiTextureType_HEIGHT,
810 textures, options.mipmaps, predecoded);
811 r->setTint(base.r, base.g, base.b, 1.f);
812 if (albedo) r->setTexture(albedo);
813 if (normal) r->setNormalTexture(normal);
814 if (height) r->setHeightTexture(height);
815 n->links.push_back(scene::SceneLink{scene::findLinkKind("renderable3d"), r, 0});
816 }
817}
818
819bool SceneLoader::decode(const std::string &path, const LoadOptions &options, DecodedScene *out) {
820 // The unified resource cache may hold a decode from an earlier load; a
821 // reload/diff must see the file as it is now, so refresh the cached entry
822 // before asking for it (no-op when nothing is cached yet).
824
825 auto *mod3d = ModuleManager::getInstance<model3d::Model3D>("Model3D");
826 if (!mod3d) mod3d = model3d::Model3D::create();
827 model3d::ModelData *md = nullptr;
828 try {
829 md = mod3d->newModelDataFromFile(path, toModelOptions(options));
830 } catch (...) {
831 return false;
832 }
833 if (!md) return false;
834
835 out->path = normPath(path);
836 out->md = md;
837 out->options = options;
838 out->root = buildNodeDesc(md->getScene(), &out->slots);
839 collectCpuImages(md->getScene(), out->slots, options.mipmaps, out->cpuImages);
840 return true;
841}
842
843scene::SceneHost *SceneLoader::mount(DecodedScene &d) {
844 scene::SceneHost *host = scene::SceneHost::createHost(d.path);
845 host->setTree(std::move(d.root));
846
847 graphics::Graphics *gfx = currentGraphics();
848 if (!d.slots.empty()) fillSceneBounds(host, d.slots);
849
850 if (gfx) {
851 MeshCache shared;
852 linkMeshNodes(host, d.slots, gfx, d.options, textures_, shared, &d.cpuImages);
853 if (d.options.importLights) importLights(d.md->getScene(), d.lights);
854 if (d.options.importCameras) importCameras(d.md->getScene(), d.cameras);
855 importAnimations(d.md->getScene(), d.options, &d.skeleton, d.clips);
856 }
857 fillSceneBounds(host, d.slots);
859 scenes_[d.path] = Loaded{d.path, host, gfx, d.options, std::move(d.lights),
860 std::move(d.cameras), d.skeleton, std::move(d.clips)};
861 return host;
862}
863
864void SceneLoader::clearTextures() {
865 for (auto &kv : textures_) delete kv.second;
866 textures_.clear();
867}
868
869// ---- public: file / lifecycle ----
870
871scene::SceneHost *SceneLoader::load(const std::string &path, bool linkRenderables,
872 const LoadOptions &options) {
873 const std::string key = normPath(path);
874 DecodedScene d;
875 auto warm = prewarmed_.find(key);
876 if (warm != prewarmed_.end()) {
877 d = std::move(warm->second);
878 prewarmed_.erase(warm);
879 } else if (!decode(path, options, &d)) {
880 return nullptr;
881 }
882 if (!linkRenderables) {
884 host->setTree(std::move(d.root));
886 scenes_[d.path] = Loaded{d.path, host, nullptr, options, {}, {}, nullptr, {}};
887 return host;
888 }
889 scene::SceneHost *host = mount(d);
890 return host;
891}
892
893scene::SceneHost *SceneLoader::load(const std::string &path, const LoadOptions &options) {
894 return load(path, true, options);
895}
896
897bool SceneLoader::reload(const std::string &path, SceneDiff *out, const LoadOptions &options) {
898 const std::string key = normPath(path);
899 auto it = scenes_.find(key);
900 if (it == scenes_.end()) {
901 load(path, options);
902 if (out) *out = SceneDiff{};
903 return true;
904 }
905 Loaded &ld = it->second;
906
907 DecodedScene d;
908 if (!decode(path, options, &d)) return false;
909
910 SceneDiff diff = diffTree(ld.host, d.root);
911 if (out) *out = diff;
912 if (!diff.empty()) {
913 applyTreeDiff(ld.host, d.root, diff, nullptr, nullptr);
914 MeshCache shared;
915 linkMeshNodes(ld.host, d.slots, ld.gfx, options, textures_, shared, &d.cpuImages);
917 }
918 return !diff.empty();
919}
920
921SceneDiff SceneLoader::diff(const std::string &path) {
922 const std::string key = normPath(path);
923 eve::ResourceManager::getInstance().reload(path); // fresh decode for diffing
924 auto *mod3d = ModuleManager::getInstance<model3d::Model3D>("Model3D");
925 if (!mod3d) mod3d = model3d::Model3D::create();
926 model3d::ModelData *md = nullptr;
927 try {
928 md = mod3d->newModelDataFromFile(path);
929 } catch (...) {
930 return SceneDiff{};
931 }
932 if (!md) return SceneDiff{};
933 MeshSlotMap slots;
934 scene::NodeDesc newRoot = buildNodeDesc(md->getScene(), &slots);
935 SceneDiff d;
936 auto it = scenes_.find(key);
937 if (it != scenes_.end() && it->second.host) {
938 d = diffTree(it->second.host, newRoot);
939 } else {
940 // Nothing mounted: every node in the new tree is an add.
941 std::function<void(const scene::NodeDesc &, const std::string &)> walk =
942 [&](const scene::NodeDesc &n, const std::string &parent) {
943 d.entries.push_back({SceneDiffEntry::Action::Add, n.id, parent});
944 ++d.added;
945 for (const auto &c : n.children) walk(c, n.id);
946 };
947 walk(newRoot, "");
948 }
949 return d;
950}
951
952scene::SceneHost *SceneLoader::host(const std::string &path) {
953 auto it = scenes_.find(normPath(path));
954 return (it != scenes_.end()) ? it->second.host : nullptr;
955}
956
957int SceneLoader::nodeCount(const std::string &path) {
958 scene::SceneHost *h = host(path);
959 return h ? h->getNodeCount() : 0;
960}
961
962bool SceneLoader::loaded(const std::string &path) { return scenes_.count(normPath(path)) > 0; }
963
964void SceneLoader::unload(const std::string &path) {
965 auto it = scenes_.find(normPath(path));
966 if (it == scenes_.end()) return;
967 if (it->second.host) {
968 auto t = it->second.host->tree();
969 for (auto &n : t->nodes) {
970 if (const auto *l = it->second.host->findLink(&n, scene::findLinkKind("renderable3d"))) {
971 destroyRenderable(static_cast<graphics::Renderable3D *>(l->target));
972 }
973 n.links.clear();
974 }
975 ecs::DestroyEntity(it->second.host);
976 }
977 for (graphics::Light3D *l : it->second.lights) ecs::DestroyEntity(l);
978 for (graphics::Camera3D *c : it->second.cameras) ecs::DestroyEntity(c);
979 delete it->second.skeleton;
980 for (animation::AnimClip *clip : it->second.clips) delete clip;
981 scenes_.erase(it);
982}
983
984// ---- async loading ----
985
986bool SceneLoader::loadAsync(const std::string &path, const LoadOptions &options,
987 std::function<void(scene::SceneHost *)> done) {
988 const std::string key = normPath(path);
989 {
990 std::lock_guard<std::mutex> lock(pendingMu_);
991 for (const auto &p : pending_)
992 if (p.path == key) return false;
993 }
994 if (!pool_) pool_ = std::make_shared<thread::ThreadPool>(2);
995
996 auto self = this;
997 pool_->submit([self, key, options, done]() {
998 DecodedScene d;
999 if (self->decode(key, options, &d)) {
1000 std::lock_guard<std::mutex> lock(self->pendingMu_);
1001 d.done = done;
1002 self->pending_.push_back(std::move(d));
1003 }
1004 });
1005 return true;
1006}
1007
1009 std::vector<DecodedScene> ready;
1010 {
1011 std::lock_guard<std::mutex> lock(pendingMu_);
1012 ready.swap(pending_);
1013 }
1014 for (auto &d : ready) {
1015 if (!d.md) continue;
1016 if (d.prewarmOnly) {
1017 prewarmed_[d.path] = std::move(d);
1018 continue;
1019 }
1020 scene::SceneHost *h = mount(d);
1021 if (d.done) d.done(h);
1022 }
1023 return static_cast<int>(ready.size());
1024}
1025
1026bool SceneLoader::prewarmAsync(const std::string &path, const LoadOptions &options) {
1027 const std::string key = normPath(path);
1028 {
1029 std::lock_guard<std::mutex> lock(pendingMu_);
1030 if (prewarmed_.count(key)) return false;
1031 for (const auto &p : pending_)
1032 if (p.path == key) return false;
1033 }
1034 if (!pool_) pool_ = std::make_shared<thread::ThreadPool>(2);
1035
1036 auto self = this;
1037 pool_->submit([self, key, options]() {
1038 DecodedScene d;
1039 if (!self->decode(key, options, &d)) return;
1040 d.prewarmOnly = true;
1041 std::lock_guard<std::mutex> lock(self->pendingMu_);
1042 self->pending_.push_back(std::move(d));
1043 });
1044 return true;
1045}
1046
1047bool SceneLoader::prewarmed(const std::string &path) const {
1048 return prewarmed_.count(normPath(path)) > 0;
1049}
1050
1051void SceneLoader::clearPrewarm(const std::string &path) {
1052 auto it = prewarmed_.find(normPath(path));
1053 if (it == prewarmed_.end()) return;
1054 prewarmed_.erase(it);
1055}
1056
1058 std::lock_guard<std::mutex> lock(pendingMu_);
1059 return static_cast<int>(pending_.size());
1060}
1061
1062// ---- imported scene extras ----
1063
1064int SceneLoader::lightCount(const std::string &path) {
1065 auto it = scenes_.find(normPath(path));
1066 return (it != scenes_.end()) ? static_cast<int>(it->second.lights.size()) : 0;
1067}
1068
1069graphics::Light3D *SceneLoader::light(const std::string &path, int index) {
1070 auto it = scenes_.find(normPath(path));
1071 if (it == scenes_.end() || index < 0 ||
1072 static_cast<size_t>(index) >= it->second.lights.size())
1073 return nullptr;
1074 return it->second.lights[size_t(index)];
1075}
1076
1077int SceneLoader::cameraCount(const std::string &path) {
1078 auto it = scenes_.find(normPath(path));
1079 return (it != scenes_.end()) ? static_cast<int>(it->second.cameras.size()) : 0;
1080}
1081
1082graphics::Camera3D *SceneLoader::camera(const std::string &path, int index) {
1083 auto it = scenes_.find(normPath(path));
1084 if (it == scenes_.end() || index < 0 ||
1085 static_cast<size_t>(index) >= it->second.cameras.size())
1086 return nullptr;
1087 return it->second.cameras[size_t(index)];
1088}
1089
1090int SceneLoader::animationCount(const std::string &path) {
1091 auto it = scenes_.find(normPath(path));
1092 return (it != scenes_.end()) ? static_cast<int>(it->second.clips.size()) : 0;
1093}
1094
1096 auto it = scenes_.find(normPath(path));
1097 return (it != scenes_.end()) ? it->second.skeleton : nullptr;
1098}
1099
1100animation::AnimClip *SceneLoader::clip(const std::string &path, int index) {
1101 auto it = scenes_.find(normPath(path));
1102 if (it == scenes_.end() || index < 0 ||
1103 static_cast<size_t>(index) >= it->second.clips.size())
1104 return nullptr;
1105 return it->second.clips[size_t(index)];
1106}
1107
1108void SceneLoader::expose(ssq::Table &table) {
1109 auto cls = table.addClass(name, SceneLoader::create, false);
1110 expose(cls);
1111}
1112
1113void SceneLoader::expose(ssq::Class &cls) {
1114 cls.addFunc("getName", &SceneLoader::getName);
1115 cls.addFunc("reloadChecked", &SceneLoader::reloadChecked);
1116 cls.addFunc("nodeCount", &SceneLoader::nodeCount);
1117 cls.addFunc("loaded", &SceneLoader::loaded);
1118 cls.addFunc("unload", &SceneLoader::unload);
1119 cls.addFunc("pollAsync", &SceneLoader::pollAsync);
1120 cls.addFunc("pendingAsyncCount", &SceneLoader::pendingAsyncCount);
1121 cls.addFunc("prewarmed", &SceneLoader::prewarmed);
1122 cls.addFunc("clearPrewarm", &SceneLoader::clearPrewarm);
1123 cls.addFunc("lightCount", &SceneLoader::lightCount);
1124 cls.addFunc("cameraCount", &SceneLoader::cameraCount);
1125 cls.addFunc("animationCount", &SceneLoader::animationCount);
1126}
1127
1128} // namespace sceneloader
1129} // namespace eve
HSQOBJECT cls
Definition ECS.cpp:21
std::string type
std::string id
int y
Definition Grass.cpp:135
int z
Definition Grass.cpp:135
float height
Definition Grass.cpp:235
int x
Definition Grass.cpp:135
glm::vec3 n
Definition Grass.cpp:64
int h
int w
uint32_t a
uint32_t b
uint32_t c
float roughness
Texture * normal
Texture * albedo
float metallic
#define Module_IMPL(ModuleName, newExpr)
Definition Module.h:24
int idx
float f
glm::vec4 p[6]
Mesh * mesh
const char * name
Definition RockMesh.cpp:21
bool repeatV
std::string filter
std::string mipmap
bool repeatU
bool mips
int d
int v
float scale
Definition TreeMesh.cpp:122
int parent
Definition TreeMesh.cpp:175
int children
Definition TreeMesh.cpp:177
float m[16]
uint32_t s
Definition Weather.cpp:28
virtual std::string getName() const =0
bool reload(const std::string &normPath) override
Definition Resource.cpp:126
static ResourceManager & getInstance()
Definition Resource.cpp:8
Keyframed skeletal animation clip (local TRS tracks per bone). Script type: AnimClip.
Definition AnimClip.h:17
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:11
static Camera3D * createCamera()
virtual Mesh * newMeshFromAssimp(const ::aiMesh &mesh)=0
Declarative 3D light. Collected by RenderSystem3D (max 8 per frame). type: "point" | "dir" (≤15 chars...
Definition Light.h:101
static Light3D * createLight(const std::string &type="point")
Definition Light.cpp:63
GPU mesh handle (+ optional CPU morph targets).
Definition Mesh.h:18
Represents raw pixel data.
Definition ImageData.h:26
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:23
const aiScene * getScene() const
Definition ModelData.cpp:65
ECS mount point for one scene graph (full scene or nested subtree root). Isomorphic to eve::ui::UIHos...
Definition SceneHost.h:80
void setTree(NodeDesc root)
Full replace.
SceneNode * findById(const std::string &id)
static SceneHost * createHost(const std::string &name="")
SceneLink * findLink(SceneNode *node, int kind)
static void updateHost(SceneHost *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...
std::unordered_map< const aiMesh *, graphics::Mesh * > MeshCache
graphics::Light3D * light(const std::string &path, int index)
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.
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::unordered_map< std::string, CpuImage > CpuImageMap
SceneDiff diff(const std::string &path)
Dry-run diff for path against the currently mounted tree (no mutation).
bool reload(const std::string &path, SceneDiff *out=nullptr, const LoadOptions &options={})
Hot-reload path. Re-decodes, diffs, and applies only what changed. Returns false if the file failed t...
scene::SceneHost * host(const std::string &path)
The mounted host for path, or nullptr if not loaded.
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.
animation::AnimClip * clip(const std::string &path, int index)
static scene::NodeDesc buildNodeDesc(const aiScene *scene, MeshSlotMap *slotsOut=nullptr)
Flatten an Assimp scene into a scene::NodeDesc tree (id = object id).
bool reloadChecked(const std::string &path)
Script-friendly reload: returns true when anything was updated.
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.
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 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)
int pollAsync()
Mount every decoded-but-not-applied scene. Must be called on the main / render thread....
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.
NodeDesc node(std::string id, std::vector< NodeDesc > children, std::string name)
Definition NodeDesc.cpp:214
int findLinkKind(const char *kind)
Definition SceneLink.cpp:39
std::unordered_map< std::string, std::vector< MeshSlot > > MeshSlotMap
Definition SceneLoader.h:87
WidgetDesc child(std::string id, std::vector< WidgetDesc > children, float width, float height)
Scrollable child region with an explicit size.
Definition Widget.cpp:387
Definition Build.cpp:11
Declarative scene-node description (build once / on dirty → flatten into SceneHost::Tree)....
Definition NodeDesc.h:15
Retained scene node (arena). Conceptual GameObject; isomorphic to eve::ui::UINode.
Definition SceneHost.h:39
Options controlling how a 3D scene file is decoded and mounted.
Definition SceneLoader.h:99
bool mipmaps
Generate mipmaps + anisotropic filtering for imported textures.
bool sharedMeshes
Reuse one GPU mesh when several nodes reference the same aiMesh.
A GPU mesh reference for one Assimp mesh referenced by an aiNode. The loader uploads one Renderable3D...
Definition SceneLoader.h:82
std::vector< SceneDiffEntry > entries
Definition SceneLoader.h:66
std::string key
Reconciliation key; defaults to id when empty.
Definition Widget.h:17
std::string id
Definition Widget.h:15