1 /*
2  * Copyright (c) 2024 Huawei Device Co., Ltd.
3  * Licensed under the Apache License, Version 2.0 (the "License");
4  * you may not use this file except in compliance with the License.
5  * You may obtain a copy of the License at
6  *
7  *     http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software
10  * distributed under the License is distributed on an "AS IS" BASIS,
11  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12  * See the License for the specific language governing permissions and
13  * limitations under the License.
14  */
15 
16 #include "asset_loader.h"
17 
18 #include <algorithm>
19 
20 #include <3d/ecs/components/animation_component.h>
21 #include <3d/ecs/components/animation_track_component.h>
22 #include <3d/ecs/components/name_component.h>
23 #include <3d/ecs/components/node_component.h>
24 #include <3d/ecs/components/render_handle_component.h>
25 #include <3d/ecs/components/uri_component.h>
26 #include <3d/ecs/systems/intf_animation_system.h>
27 #include <3d/ecs/systems/intf_node_system.h>
28 #include <base/containers/fixed_string.h>
29 #include <base/containers/vector.h>
30 #include <core/image/intf_image_loader_manager.h>
31 #include <core/intf_engine.h>
32 #include <core/io/intf_file_manager.h>
33 #include <render/device/intf_gpu_resource_manager.h>
34 #include <render/intf_render_context.h>
35 
36 #include "asset_manager.h"
37 #include "asset_migration.h"
38 
39 using namespace BASE_NS;
40 using namespace CORE_NS;
41 using namespace RENDER_NS;
42 using namespace CORE3D_NS;
43 
44 SCENE_BEGIN_NAMESPACE()
45 
46 namespace {
47 
48 struct IoUtil {
LoadTextFile__anon5335af650110::IoUtil49     static bool LoadTextFile(string_view fileUri, string& fileContentsOut, IFileManager& fileManager)
50     {
51         auto file = fileManager.OpenFile(fileUri);
52         if (file) {
53             const size_t length = file->GetLength();
54             fileContentsOut.resize(length);
55             return file->Read(fileContentsOut.data(), length) == length;
56         }
57         return false;
58     }
59 };
60 
61 // Add a node and all of its children recursively to a collection
AddNodeToCollectionRecursive(IEntityCollection & ec,ISceneNode & node,string_view path)62 void AddNodeToCollectionRecursive(IEntityCollection& ec, ISceneNode& node, string_view path)
63 {
64     const auto entity = node.GetEntity();
65     const auto currentPath = path + node.GetName();
66 
67     EntityReference ref = ec.GetEcs().GetEntityManager().GetReferenceCounted(entity);
68     ec.AddEntity(ref);
69     ec.SetId(currentPath, ref);
70 
71     const auto childBasePath = currentPath + "/";
72     for (auto* child : node.GetChildren()) {
73         AddNodeToCollectionRecursive(ec, *child, childBasePath);
74     }
75 }
76 } // namespace
77 
78 // A class that executes asset load operation over multiple frames.
79 class AssetLoader final : public IAssetLoader, private IAssetLoader::IListener, private IGLTF2Importer::Listener {
80 public:
AssetLoader(AssetManager & assetManager,IRenderContext & renderContext,IGraphicsContext & graphicsContext,IEntityCollection & ec,string_view src,string_view contextUri)81     AssetLoader(AssetManager& assetManager, IRenderContext& renderContext, IGraphicsContext& graphicsContext,
82         IEntityCollection& ec, string_view src, string_view contextUri)
83         : assetManager_(assetManager), renderContext_(renderContext), graphicsContext_(graphicsContext),
84           ecs_(ec.GetEcs()), ec_(ec), src_(src), contextUri_(contextUri),
85           fileManager_(renderContext_.GetEngine().GetFileManager())
86     {}
87 
~AssetLoader()88     ~AssetLoader() override
89     {
90         Cancel();
91     }
92 
GetEntityCollection() const93     IEntityCollection& GetEntityCollection() const override
94     {
95         return ec_;
96     }
97 
GetSrc() const98     string GetSrc() const override
99     {
100         return src_;
101     }
102 
GetContextUri() const103     string GetContextUri() const override
104     {
105         return contextUri_;
106     }
107 
GetUri() const108     string GetUri() const override
109     {
110         return PathUtil::ResolveUri(contextUri_, src_, true);
111     }
112 
AddListener(IAssetLoader::IListener & listener)113     void AddListener(IAssetLoader::IListener& listener) override
114     {
115         CORE_ASSERT(&listener);
116         listeners_.emplace_back(&listener);
117     }
118 
RemoveListener(IAssetLoader::IListener & listener)119     void RemoveListener(IAssetLoader::IListener& listener) override
120     {
121         CORE_ASSERT(&listener);
122         for (size_t i = 0; i < listeners_.size(); ++i) {
123             if (&listener == listeners_[i]) {
124                 listeners_.erase(listeners_.begin() + i);
125                 return;
126             }
127         }
128 
129         // trying to remove a non-existent listener.
130         CORE_ASSERT(true);
131     }
132 
LoadAsset()133     void LoadAsset() override
134     {
135         async_ = false;
136         StartLoading();
137     }
138 
LoadAssetAsync()139     void LoadAssetAsync() override
140     {
141         async_ = true;
142         StartLoading();
143     }
144 
GetNextDependency() const145     IAssetLoader* GetNextDependency() const
146     {
147         if (dependencies_.empty()) {
148             return nullptr;
149         }
150         for (const auto& dep : dependencies_) {
151             if (dep && !dep->IsCompleted()) {
152                 return dep.get();
153             }
154         }
155         return nullptr;
156     }
157 
158     //
159     // From IAssetLoader::Listener
160     //
OnLoadFinished(const IAssetLoader & loader)161     void OnLoadFinished(const IAssetLoader& loader) override
162     {
163         // This will be called when a dependency has finished loading.
164 
165         // Hide cached data.
166         loader.GetEntityCollection().SetActive(false);
167 
168         auto* dep = GetNextDependency();
169         if (dep) {
170             // Load next dependency.
171             ContinueLoading();
172         } else {
173             // There are no more dependencies. Try loading the main asset again but now with the dependencies loaded.
174             StartLoading();
175         }
176     }
177 
178     //
179     // From CORE_NS::IGLTF2Importer::Listener
180     //
OnImportStarted()181     void OnImportStarted() override {}
OnImportFinished()182     void OnImportFinished() override
183     {
184         GltfImportFinished();
185         if (async_) {
186             ContinueLoading();
187         }
188     }
OnImportProgressed(size_t taskIndex,size_t taskCount)189     void OnImportProgressed(size_t taskIndex, size_t taskCount) override {}
190 
Execute(uint32_t timeBudget)191     bool Execute(uint32_t timeBudget) override
192     {
193         // NOTE: Currently actually only one import will be active at a time so we don't need to worry about the time
194         // budget.
195         bool done = true;
196 
197         for (auto& dep : dependencies_) {
198             if (dep) {
199                 done &= dep->Execute(timeBudget);
200             }
201         }
202         if (importer_) {
203             done &= importer_->Execute(timeBudget);
204         }
205         return done;
206     }
207 
Cancel()208     void Cancel() override
209     {
210         cancelled_ = true;
211         for (auto& dep : dependencies_) {
212             if (dep) {
213                 dep->Cancel();
214             }
215         }
216 
217         if (importer_) {
218             importer_->Cancel();
219             importer_.reset();
220         }
221     }
222 
IsCompleted() const223     bool IsCompleted() const override
224     {
225         return done_;
226     }
227 
GetResult() const228     Result GetResult() const override
229     {
230         return result_;
231     }
232 
Destroy()233     void Destroy() override
234     {
235         delete this;
236     }
237 
238 private:
StartLoading()239     void StartLoading()
240     {
241         if (src_.empty()) {
242             result_ = { false, "Ivalid uri" };
243             done_ = true;
244             return;
245         }
246 
247         const auto resolvedUri = GetUri();
248 
249         const auto resolvedFile = PathUtil::ResolveUri(contextUri_, src_, false);
250         const auto ext = PathUtil::GetExtension(resolvedFile);
251         const auto type = assetManager_.GetExtensionType(ext);
252         // TODO: Separate different loaders and make it possible to register more.
253         switch (type) {
254             case IAssetManager::ExtensionType::COLLECTION: {
255                 ec_.SetType("entitycollection");
256                 if (LoadJsonEntityCollection()) {
257                     MigrateAnimation(ec_);
258                 }
259                 break;
260             }
261 
262             case IAssetManager::ExtensionType::SCENE: {
263                 ec_.SetType("scene");
264                 if (LoadJsonEntityCollection()) {
265                     MigrateAnimation(ec_);
266                 }
267                 break;
268             }
269 
270             case IAssetManager::ExtensionType::ANIMATION: {
271                 ec_.SetType("animation");
272                 if (LoadJsonEntityCollection()) {
273                     MigrateAnimation(ec_);
274                 }
275                 break;
276             }
277 
278             case IAssetManager::ExtensionType::MATERIAL: {
279                 ec_.SetType("material");
280                 LoadJsonEntityCollection();
281                 break;
282             }
283 
284             case IAssetManager::ExtensionType::GLTF:
285             case IAssetManager::ExtensionType::GLB: {
286                 ec_.SetType("gltf");
287                 LoadGltfEntityCollection();
288                 break;
289             }
290 
291             case IAssetManager::ExtensionType::JPG:
292             case IAssetManager::ExtensionType::PNG:
293             case IAssetManager::ExtensionType::KTX: {
294                 ec_.SetType("image");
295                 LoadImageEntityCollection();
296                 break;
297             }
298 
299             case IAssetManager::ExtensionType::NOT_SUPPORTED:
300             default: {
301                 CORE_LOG_E("Unsupported asset format: '%s'", src_.c_str());
302                 result_ = { false, "Unsupported asset format" };
303                 done_ = true;
304                 break;
305             }
306         }
307 
308         ContinueLoading();
309     }
310 
ContinueLoading()311     void ContinueLoading()
312     {
313         if (done_) {
314             ec_.MarkModified(false, true);
315 
316             for (auto* listener : listeners_) {
317                 listener->OnLoadFinished(*this);
318             }
319             return;
320         }
321 
322         auto* dep = GetNextDependency();
323         if (dep) {
324             if (async_) {
325                 dep->LoadAssetAsync();
326             } else {
327                 dep->LoadAsset();
328             }
329             return;
330         }
331     }
332 
CreateDummyEntity()333     void CreateDummyEntity()
334     {
335         // Create a dummy root entity as a placeholder for a missing asset.
336         auto dummyEntity = ecs_.GetEntityManager().CreateReferenceCounted();
337         // Note: adding a node component for now so it will be displayed in the hierarchy pane.
338         // This is wrong as the failed asset might not be a 3D node in reality. this could be removed after combining
339         // the Entity pane functionlity to the hierarchy pane and when there is better handling for missing references.
340         auto* nodeComponentManager = GetManager<INodeComponentManager>(ecs_);
341         CORE_ASSERT(nodeComponentManager);
342         if (nodeComponentManager) {
343             nodeComponentManager->Create(dummyEntity);
344         }
345         if (!GetUri().empty()) {
346             auto* nameComponentManager = GetManager<INameComponentManager>(ecs_);
347             CORE_ASSERT(nameComponentManager);
348             if (nameComponentManager) {
349                 nameComponentManager->Create(dummyEntity);
350                 nameComponentManager->Write(dummyEntity)->name = GetUri();
351             }
352         }
353         ec_.AddEntity(dummyEntity);
354         ec_.SetId("/", dummyEntity);
355     }
356 
LoadJsonEntityCollection()357     bool LoadJsonEntityCollection()
358     {
359         const auto resolvedUri = GetUri();
360         const auto resolvedFile = PathUtil::ResolveUri(contextUri_, src_, false);
361 
362         const auto params = PathUtil::GetUriParameters(src_);
363         const auto targetParam = params.find("target");
364         string entityTarget = (targetParam != params.cend()) ? targetParam->second : "";
365         auto targetEntity = ec_.GetEntity(entityTarget);
366         size_t subcollectionCount = ec_.GetSubCollectionCount();
367         while (!targetEntity && subcollectionCount) {
368             targetEntity = ec_.GetSubCollection(--subcollectionCount)->GetEntity(entityTarget);
369         }
370 
371         string textIn;
372         if (IoUtil::LoadTextFile(resolvedFile, textIn, fileManager_)) {
373             // TODO: Check file version here.
374 
375             auto json = json::parse(textIn.data());
376             if (!json) {
377                 CORE_LOG_E("Parsing json failed: '%s':\n%s", resolvedUri.c_str(), textIn.c_str());
378             } else {
379                 if (!dependencies_.empty()) {
380                     // There were dependencies loaded earlier and now we want to load the actual asset.
381                     // No dependencies to load. Just load the entity collection itself and this loading is done.
382                     auto result = assetManager_.GetEcsSerializer().ReadEntityCollection(ec_, json, resolvedUri);
383                     done_ = true;
384                     return result;
385                 }
386 
387                 vector<IEcsSerializer::ExternalCollection> dependencies;
388                 assetManager_.GetEcsSerializer().GatherExternalCollections(json, resolvedUri, dependencies);
389 
390                 for (const auto& dep : dependencies) {
391                     BASE_NS::string patchedDepUri = dep.src;
392                     if (!entityTarget.empty())
393                         patchedDepUri.append("?target=").append(entityTarget);
394                     if (!assetManager_.IsCachedCollection(patchedDepUri, dep.contextUri)) {
395                         auto* cacheEc =
396                             assetManager_.CreateCachedCollection(ec_.GetEcs(), patchedDepUri, dep.contextUri);
397                         dependencies_.emplace_back(
398                             assetManager_.CreateAssetLoader(*cacheEc, patchedDepUri, dep.contextUri));
399                         auto& dep = *dependencies_.back();
400                         dep.AddListener(*this);
401                     }
402                 }
403 
404                 if (GetNextDependency() == nullptr) {
405                     // No dependencies to load. Just load the entity collection itself and this loading is done.
406                     auto result = assetManager_.GetEcsSerializer().ReadEntityCollection(ec_, json, resolvedUri);
407                     done_ = true;
408                     return result;
409                 }
410 
411                 // There are dependencies that need to be parsed in a next step.
412                 return true;
413             }
414         }
415 
416         CreateDummyEntity();
417         result_ = { false, "collection loading failed." };
418         done_ = true;
419         return false;
420     }
421 
LoadGltfEntityCollection()422     void LoadGltfEntityCollection()
423     {
424         const auto resolvedFile = PathUtil::ResolveUri(contextUri_, src_, false);
425 
426         auto& gltf = graphicsContext_.GetGltf();
427 
428         loadResult_ = gltf.LoadGLTF(resolvedFile);
429         if (!loadResult_.success) {
430             CORE_LOG_E("Loaded '%s' with errors:\n%s", resolvedFile.c_str(), loadResult_.error.c_str());
431         }
432         if (!loadResult_.data) {
433             CreateDummyEntity();
434             result_ = { false, "glTF load failed." };
435             done_ = true;
436             return;
437         }
438 
439         importer_ = gltf.CreateGLTF2Importer(ec_.GetEcs());
440 
441         if (async_) {
442             importer_->ImportGLTFAsync(*loadResult_.data, CORE_GLTF_IMPORT_RESOURCE_FLAG_BITS_ALL, this);
443         } else {
444             importer_->ImportGLTF(*loadResult_.data, CORE_GLTF_IMPORT_RESOURCE_FLAG_BITS_ALL);
445             OnImportFinished();
446         }
447     }
448 
ImportSceneFromGltf(EntityReference root)449     Entity ImportSceneFromGltf(EntityReference root)
450     {
451         auto& gltf = graphicsContext_.GetGltf();
452 
453         // Import the default scene, or first scene if there is no default scene set.
454         size_t sceneIndex = loadResult_.data->GetDefaultSceneIndex();
455         if (sceneIndex == CORE_GLTF_INVALID_INDEX && loadResult_.data->GetSceneCount() > 0) {
456             // Use first scene.
457             sceneIndex = 0;
458         }
459 
460         const CORE3D_NS::GLTFResourceData& resourceData = importer_->GetResult().data;
461         Entity importedSceneEntity {};
462         if (sceneIndex != CORE_GLTF_INVALID_INDEX) {
463             GltfSceneImportFlags importFlags = CORE_GLTF_IMPORT_COMPONENT_FLAG_BITS_ALL;
464             importedSceneEntity =
465                 gltf.ImportGltfScene(sceneIndex, *loadResult_.data, resourceData, ecs_, root, importFlags);
466         }
467         if (!EntityUtil::IsValid(importedSceneEntity)) {
468             return {};
469         }
470 
471         // make sure everyone has a name (rest of the system fails if there are unnamed nodes)
472         auto* nodeComponentManager = GetManager<INodeComponentManager>(ecs_);
473         for (auto i = 0; i < nodeComponentManager->GetComponentCount(); i++) {
474             auto ent = nodeComponentManager->GetEntity(i);
475             auto* ncm = GetManager<INameComponentManager>(ecs_);
476             bool setName = true;
477             if (ncm->HasComponent(ent)) {
478                 // check if empty
479                 auto name = ncm->Get(ent).name;
480                 if (!name.empty()) {
481                     // it has a non empty name..
482                     setName = false;
483                 }
484             }
485             if (setName) {
486                 // no name component, so create one create one.
487                 char buf[256];
488                 // possibly not unique enough. user created names could conflict.
489                 sprintf(buf, "Unnamed Node %d", i);
490                 ncm->Set(ent, { buf });
491             }
492         }
493 
494         // Link animation tracks to targets
495         if (!resourceData.animations.empty()) {
496             INodeSystem* nodeSystem = GetSystem<INodeSystem>(ecs_);
497             if (auto animationRootNode = nodeSystem->GetNode(importedSceneEntity); animationRootNode) {
498                 for (const auto& animationEntity : resourceData.animations) {
499                     UpdateTrackTargets(animationEntity, animationRootNode);
500                 }
501             }
502         }
503 
504         return importedSceneEntity;
505     }
506 
UpdateTrackTargets(Entity animationEntity,ISceneNode * node)507     void UpdateTrackTargets(Entity animationEntity, ISceneNode* node)
508     {
509         auto& nameManager_ = *GetManager<INameComponentManager>(ecs_);
510         auto animationManager = GetManager<IAnimationComponentManager>(ecs_);
511         auto animationTrackManager = GetManager<IAnimationTrackComponentManager>(ecs_);
512         auto& entityManager = ecs_.GetEntityManager();
513 
514         if (const ScopedHandle<const AnimationComponent> animationData = animationManager->Read(animationEntity);
515             animationData) {
516             vector<Entity> targetEntities;
517             targetEntities.reserve(animationData->tracks.size());
518             std::transform(animationData->tracks.begin(), animationData->tracks.end(),
519                 std::back_inserter(targetEntities), [&nameManager = nameManager_, &node](const Entity& trackEntity) {
520                     if (auto nameHandle = nameManager.Read(trackEntity); nameHandle) {
521                         if (auto targetNode = node->LookupNodeByPath(nameHandle->name); targetNode) {
522                             return targetNode->GetEntity();
523                         }
524                     }
525                     return Entity {};
526                 });
527             if (animationData->tracks.size() == targetEntities.size()) {
528                 auto targetIt = targetEntities.begin();
529                 for (const auto& trackEntity : animationData->tracks) {
530                     if (auto track = animationTrackManager->Write(trackEntity); track) {
531                         if (track->target) {
532                             SCENE_PLUGIN_VERBOSE_LOG("AnimationTrack %s already targetted",
533                                 to_hex(static_cast<const Entity&>(track->target).id).data());
534                         }
535                         track->target = entityManager.GetReferenceCounted(*targetIt);
536                     }
537                     ++targetIt;
538                 }
539             }
540         }
541     }
542 
GltfImportFinished()543     void GltfImportFinished()
544     {
545         auto* nodeSystem = GetSystem<INodeSystem>(ecs_);
546         CORE_ASSERT(nodeSystem);
547         auto* nodeComponentManager = GetManager<INodeComponentManager>(ecs_);
548         CORE_ASSERT(nodeComponentManager);
549         if (!nodeSystem || !nodeComponentManager) {
550             result_ = { false, {} };
551             done_ = true;
552             return;
553         }
554 
555         const auto params = PathUtil::GetUriParameters(src_);
556         const auto rootParam = params.find("root");
557         string entityPath = (rootParam != params.cend()) ? rootParam->second : "";
558 
559         const auto targetParam = params.find("target");
560         string entityTarget = (targetParam != params.cend()) ? targetParam->second : "";
561         auto targetEntity = ec_.GetEntity(entityTarget);
562         size_t subcollectionCount = ec_.GetSubCollectionCount();
563         while (!targetEntity && subcollectionCount) {
564             targetEntity = ec_.GetSubCollection(--subcollectionCount)->GetEntity(entityTarget);
565         }
566 
567         if (importer_) {
568             auto const gltfImportResult = importer_->GetResult();
569             if (!gltfImportResult.success) {
570                 CORE_LOG_E("Importing of '%s' failed: %s", GetUri().c_str(), gltfImportResult.error.c_str());
571                 CreateDummyEntity();
572                 result_ = { false, "glTF import failed." };
573                 done_ = true;
574                 return;
575 
576             } else if (cancelled_) {
577                 CORE_LOG_V("Importing of '%s' cancelled", GetUri().c_str());
578                 CreateDummyEntity();
579                 result_ = { false, "glTF import cancelled." };
580                 done_ = true;
581                 return;
582             }
583 
584             // Loading and importing of glTF was done successfully. Fill the collection with all the gltf entities.
585             const auto originalRootEntity = ImportSceneFromGltf(targetEntity);
586             auto* originalRootNode = nodeSystem->GetNode(originalRootEntity);
587 
588             // It's possible to only add some specific node from the gltf.
589             auto* loadNode = originalRootNode;
590             if (!entityPath.empty()) {
591                 loadNode = originalRootNode->LookupNodeByPath(entityPath);
592                 if (!loadNode || loadNode->GetEntity() == Entity {}) {
593                     CORE_LOG_E("Entity '%s' not found from '%s'", entityPath.c_str(), GetUri().c_str());
594                 }
595             }
596 
597             if (!loadNode || loadNode->GetEntity() == Entity {}) {
598                 CreateDummyEntity();
599                 result_ = { false, "Ivalid uri" };
600                 done_ = true;
601                 return;
602             }
603 
604             Entity entity = loadNode->GetEntity();
605             if (entity != Entity {}) {
606                 EntityReference ref = ecs_.GetEntityManager().GetReferenceCounted(entity);
607                 ec_.AddEntity(ref);
608                 ec_.SetId("/", ref);
609 
610                 if (!targetEntity) {
611                     loadNode->SetParent(nodeSystem->GetRootNode());
612                 }
613                 for (auto* child : loadNode->GetChildren()) {
614                     AddNodeToCollectionRecursive(ec_, *child, "/");
615                 }
616             }
617 
618             // TODO: a little backwards to first create everything and then delete the extra.
619             if (entity != originalRootEntity) {
620                 auto* oldRoot = nodeSystem->GetNode(originalRootEntity);
621                 CORE_ASSERT(oldRoot);
622                 if (oldRoot) {
623                     nodeSystem->DestroyNode(*oldRoot);
624                 }
625             }
626 
627             // Add all resources in separate sub-collections. Not just 3D nodes.
628             {
629                 const auto& importResult = importer_->GetResult();
630                 ec_.AddSubCollection("images", {}).AddEntities(importResult.data.images);
631                 ec_.AddSubCollection("materials", {}).AddEntities(importResult.data.materials);
632                 ec_.AddSubCollection("meshes", {}).AddEntities(importResult.data.meshes);
633                 ec_.AddSubCollection("skins", {}).AddEntities(importResult.data.skins);
634                 ec_.AddSubCollection("animations", {}).AddEntities(importResult.data.animations);
635 
636                 // TODO: don't list duplicates
637                 vector<EntityReference> animationTracks;
638                 auto* acm = GetManager<IAnimationComponentManager>(ecs_);
639                 if (acm) {
640                     for (auto& entity : importResult.data.animations) {
641                         if (auto handle = acm->Read(entity); handle) {
642                             const auto& tracks = handle->tracks;
643                             for (auto& entityRef : tracks) {
644                                 animationTracks.emplace_back(entityRef);
645                             }
646                         }
647                     }
648                 }
649                 ec_.AddSubCollection("animationTracks", {}).AddEntities(animationTracks);
650             }
651 
652             // Load finished successfully.
653             done_ = true;
654         }
655     }
656 
LoadImageEntityCollection()657     bool LoadImageEntityCollection()
658     {
659         auto uri = GetUri();
660         auto imageHandle = assetManager_.GetEcsSerializer().LoadImageResource(uri);
661 
662         // NOTE: Creating the entity even when the image load failed to load so we can detect a missing resource.
663         EntityReference entity = ecs_.GetEntityManager().CreateReferenceCounted();
664         ec_.AddEntity(entity);
665 
666         auto* ncm = GetManager<INameComponentManager>(ecs_);
667         auto* ucm = GetManager<IUriComponentManager>(ecs_);
668         auto* gcm = GetManager<IRenderHandleComponentManager>(ecs_);
669         if (ncm && ucm && gcm) {
670             {
671                 ncm->Create(entity);
672                 auto nc = ncm->Get(entity);
673                 nc.name = PathUtil::GetFilename(uri);
674                 ec_.SetUniqueIdentifier(nc.name, entity);
675                 ncm->Set(entity, nc);
676             }
677 
678             {
679                 // TODO: maybe need to save uri + contextUri
680                 ucm->Create(entity);
681                 auto uc = ucm->Get(entity);
682                 uc.uri = uri;
683                 ucm->Set(entity, uc);
684             }
685 
686             gcm->Create(entity);
687             if (imageHandle) {
688                 auto ic = gcm->Get(entity);
689                 ic.reference = imageHandle;
690                 gcm->Set(entity, ic);
691 
692                 done_ = true;
693                 return true;
694             }
695         }
696 
697         // NOTE: Always returning true as even when this fails a placeholder entity is created.
698         CORE_LOG_E("Error creating image '%s'", uri.c_str());
699         CreateDummyEntity();
700         result_ = { false, "Error creating image" };
701         done_ = true;
702         return true;
703     }
704 
705 private:
706     AssetManager& assetManager_;
707     RENDER_NS::IRenderContext& renderContext_;
708     CORE3D_NS::IGraphicsContext& graphicsContext_;
709 
710     CORE_NS::IEcs& ecs_;
711     IEntityCollection& ec_;
712     BASE_NS::string src_;
713     BASE_NS::string contextUri_;
714 
715     IAssetLoader::Result result_ {};
716 
717     vector<AssetLoader::Ptr> dependencies_;
718 
719     bool done_ { false };
720     bool cancelled_ { false };
721     bool async_ { false };
722 
723     GLTFLoadResult loadResult_ {};
724     IGLTF2Importer::Ptr importer_ {};
725 
726     vector<IAssetLoader::IListener*> listeners_;
727     IFileManager& fileManager_;
728 };
729 
CreateAssetLoader(AssetManager & assetManager,IRenderContext & renderContext,IGraphicsContext & graphicsContext,IEntityCollection & ec,string_view src,string_view contextUri)730 IAssetLoader::Ptr CreateAssetLoader(AssetManager& assetManager, IRenderContext& renderContext,
731     IGraphicsContext& graphicsContext, IEntityCollection& ec, string_view src, string_view contextUri)
732 {
733     return IAssetLoader::Ptr { new AssetLoader(assetManager, renderContext, graphicsContext, ec, src, contextUri) };
734 }
735 
736 SCENE_END_NAMESPACE()
737