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