diff --git a/Source/Editor/Content/Import/ModelImportEntry.cs b/Source/Editor/Content/Import/ModelImportEntry.cs index bbb384562..484efb1d9 100644 --- a/Source/Editor/Content/Import/ModelImportEntry.cs +++ b/Source/Editor/Content/Import/ModelImportEntry.cs @@ -12,13 +12,14 @@ namespace FlaxEngine.Tools { partial struct Options { - private bool ShowGeometry => Type == ModelTool.ModelType.Model || Type == ModelTool.ModelType.SkinnedModel; - private bool ShowModel => Type == ModelTool.ModelType.Model; - private bool ShowSkinnedModel => Type == ModelTool.ModelType.SkinnedModel; - private bool ShowAnimation => Type == ModelTool.ModelType.Animation; + private bool ShowGeometry => Type == ModelType.Model || Type == ModelType.SkinnedModel || Type == ModelType.Prefab; + private bool ShowModel => Type == ModelType.Model || Type == ModelType.Prefab; + private bool ShowSkinnedModel => Type == ModelType.SkinnedModel || Type == ModelType.Prefab; + private bool ShowAnimation => Type == ModelType.Animation || Type == ModelType.Prefab; private bool ShowSmoothingNormalsAngle => ShowGeometry && CalculateNormals; private bool ShowSmoothingTangentsAngle => ShowGeometry && CalculateTangents; - private bool ShowFramesRange => ShowAnimation && Duration == ModelTool.AnimationDuration.Custom; + private bool ShowFramesRange => ShowAnimation && Duration == AnimationDuration.Custom; + private bool ShowSplitting => Type != ModelType.Prefab; } } } diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.cpp b/Source/Engine/ContentImporters/AssetsImportingManager.cpp index 76b9c211d..4bad26599 100644 --- a/Source/Engine/ContentImporters/AssetsImportingManager.cpp +++ b/Source/Engine/ContentImporters/AssetsImportingManager.cpp @@ -382,7 +382,7 @@ bool AssetsImportingManager::Create(const Function>* meshesByNamePtr = options.Cached ? (Array>*)options.Cached->MeshesByName : nullptr; Array> meshesByNameThis; + String autoImportOutput; if (!data) { String errorMsg; - String autoImportOutput(StringUtils::GetDirectoryName(context.TargetAssetPath)); + autoImportOutput = StringUtils::GetDirectoryName(context.TargetAssetPath); autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath)); if (ModelTool::ImportModel(context.InputPath, dataThis, options, errorMsg, autoImportOutput)) { @@ -246,14 +258,62 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context) Array>& meshesByName = *meshesByNamePtr; // Import objects from file separately - if (options.SplitObjects) + ModelTool::Options::CachedData cached = { data, (void*)meshesByNamePtr }; + Array prefabObjects; + if (options.Type == ModelTool::ModelType::Prefab) + { + // Normalize options + options.SplitObjects = false; + options.ObjectIndex = -1; + + // Import all of the objects recursive but use current model data to skip loading file again + options.Cached = &cached; + Function splitImport = [&context, &autoImportOutput](Options& splitOptions, const StringView& objectName, String& outputPath) + { + // Recursive importing of the split object + String postFix = objectName; + const int32 splitPos = postFix.FindLast(TEXT('|')); + if (splitPos != -1) + postFix = postFix.Substring(splitPos + 1); + // TODO: check for name collisions with material/texture assets + outputPath = autoImportOutput / String(StringUtils::GetFileNameWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax"); + splitOptions.SubAssetFolder = TEXT(" "); // Use the same folder as asset as they all are imported to the subdir for the prefab (see SubAssetFolder usage above) + return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions); + }; + auto splitOptions = options; + LOG(Info, "Splitting imported {0} meshes", meshesByName.Count()); + PrefabObject prefabObject; + for (int32 groupIndex = 0; groupIndex < meshesByName.Count(); groupIndex++) + { + auto& group = meshesByName[groupIndex]; + + // Cache object options (nested sub-object import removes the meshes) + prefabObject.NodeIndex = group.First()->NodeIndex; + prefabObject.Name = group.First()->Name; + + splitOptions.Type = ModelTool::ModelType::Model; + splitOptions.ObjectIndex = groupIndex; + if (!splitImport(splitOptions, group.GetKey(), prefabObject.AssetPath)) + { + prefabObjects.Add(prefabObject); + } + } + LOG(Info, "Splitting imported {0} animations", data->Animations.Count()); + for (int32 i = 0; i < data->Animations.Count(); i++) + { + auto& animation = data->Animations[i]; + splitOptions.Type = ModelTool::ModelType::Animation; + splitOptions.ObjectIndex = i; + splitImport(splitOptions, animation.Name, prefabObject.AssetPath); + } + } + else if (options.SplitObjects) { // Import the first object within this call options.SplitObjects = false; options.ObjectIndex = 0; // Import rest of the objects recursive but use current model data to skip loading file again - ModelTool::Options::CachedData cached = { data, (void*)meshesByNamePtr }; options.Cached = &cached; Function splitImport; splitImport.Bind([&context](Options& splitOptions, const StringView& objectName) @@ -396,6 +456,9 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context) case ModelTool::ModelType::Animation: result = CreateAnimation(context, *data, &options); break; + case ModelTool::ModelType::Prefab: + result = CreatePrefab(context, *data, options, prefabObjects); + break; } for (auto mesh : meshesToDelete) Delete(mesh); @@ -546,4 +609,115 @@ CreateAssetResult ImportModel::CreateAnimation(CreateAssetContext& context, Mode return CreateAssetResult::Ok; } +CreateAssetResult ImportModel::CreatePrefab(CreateAssetContext& context, ModelData& data, const Options& options, const Array& prefabObjects) +{ + PROFILE_CPU(); + if (data.Nodes.Count() == 0) + return CreateAssetResult::Error; + + // If that prefab already exists then we need to use it as base to preserve object IDs and local changes applied by user + const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + DEFAULT_PREFAB_EXTENSION_DOT; + auto* prefab = FileSystem::FileExists(outputPath) ? Content::Load(outputPath) : nullptr; + if (prefab) + { + // Ensure that prefab has Default Instance so ObjectsCache is valid (used below) + prefab->GetDefaultInstance(); + } + + // Create prefab structure + Dictionary nodeToActor; + Array nodeActors; + Actor* rootActor = nullptr; + for (int32 nodeIndex = 0; nodeIndex < data.Nodes.Count(); nodeIndex++) + { + const auto& node = data.Nodes[nodeIndex]; + + // Create actor(s) for this node + nodeActors.Clear(); + for (const PrefabObject& e : prefabObjects) + { + if (e.NodeIndex == nodeIndex) + { + auto* actor = New(); + actor->SetName(e.Name); + if (auto* model = Content::LoadAsync(e.AssetPath)) + { + actor->Model = model; + } + nodeActors.Add(actor); + } + } + Actor* nodeActor = nodeActors.Count() == 1 ? nodeActors[0] : New(); + if (nodeActors.Count() > 1) + { + for (Actor* e : nodeActors) + { + e->SetParent(nodeActor); + } + } + if (nodeActors.Count() != 1) + { + // Include default actor to iterate over it properly in code below + nodeActors.Add(nodeActor); + } + + // Setup node in hierarchy + nodeToActor.Add(nodeIndex, nodeActor); + nodeActor->SetName(node.Name); + nodeActor->SetLocalTransform(node.LocalTransform); + if (nodeIndex == 0) + { + // Special case for root actor to link any unlinked nodes + nodeToActor.Add(-1, nodeActor); + rootActor = nodeActor; + } + else + { + Actor* parentActor; + if (nodeToActor.TryGet(node.ParentIndex, parentActor)) + nodeActor->SetParent(parentActor); + } + + // Link with object from prefab (if reimporting) + if (prefab) + { + for (Actor* a : nodeActors) + { + for (const auto& i : prefab->ObjectsCache) + { + if (i.Value->GetTypeHandle() != a->GetTypeHandle()) // Type match + continue; + auto* o = (Actor*)i.Value; + if (o->GetName() != a->GetName()) // Name match + continue; + + // Mark as this object already exists in prefab so will be preserved when updating it + a->LinkPrefab(o->GetPrefabID(), o->GetPrefabObjectID()); + break; + } + } + } + } + ASSERT_LOW_LAYER(rootActor); + // TODO: add PrefabModel script for asset reimporting + + // Create prefab instead of native asset + bool failed; + if (prefab) + { + failed = prefab->ApplyAll(rootActor); + } + else + { + failed = PrefabManager::CreatePrefab(rootActor, outputPath, false); + } + + // Cleanup objects from memory + rootActor->DeleteObjectNow(); + + if (failed) + return CreateAssetResult::Error; + return CreateAssetResult::Skip; +} + #endif diff --git a/Source/Engine/ContentImporters/ImportModel.h b/Source/Engine/ContentImporters/ImportModel.h index 31a525852..710971fca 100644 --- a/Source/Engine/ContentImporters/ImportModel.h +++ b/Source/Engine/ContentImporters/ImportModel.h @@ -43,6 +43,7 @@ private: static CreateAssetResult CreateModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); static CreateAssetResult CreateSkinnedModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); static CreateAssetResult CreateAnimation(CreateAssetContext& context, ModelData& data, const Options* options = nullptr); + static CreateAssetResult CreatePrefab(CreateAssetContext& context, ModelData& data, const Options& options, const Array& prefabObjects); }; #endif diff --git a/Source/Engine/ContentImporters/Types.h b/Source/Engine/ContentImporters/Types.h index 4ebf55583..388c533a8 100644 --- a/Source/Engine/ContentImporters/Types.h +++ b/Source/Engine/ContentImporters/Types.h @@ -18,7 +18,7 @@ class CreateAssetContext; /// /// Create/Import new asset callback result /// -DECLARE_ENUM_7(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID); +DECLARE_ENUM_8(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID, Skip); /// /// Create/Import new asset callback function diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 2bd923f20..1257d49cc 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -453,7 +453,7 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options options.FramesRange.Y = Math::Max(options.FramesRange.Y, options.FramesRange.X); options.DefaultFrameRate = Math::Max(0.0f, options.DefaultFrameRate); options.SamplingRate = Math::Max(0.0f, options.SamplingRate); - if (options.SplitObjects) + if (options.SplitObjects || options.Type == ModelType::Prefab) options.MergeMeshes = false; // Meshes merging doesn't make sense when we want to import each mesh individually // TODO: maybe we could update meshes merger to collapse meshes within the same name if splitting is enabled? @@ -808,6 +808,13 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option case ModelType::Animation: options.ImportTypes = ImportDataTypes::Animations; break; + case ModelType::Prefab: + options.ImportTypes = ImportDataTypes::Geometry | ImportDataTypes::Nodes | ImportDataTypes::Animations; + if (options.ImportMaterials) + options.ImportTypes |= ImportDataTypes::Materials; + if (options.ImportTextures) + options.ImportTypes |= ImportDataTypes::Textures; + break; default: return true; } @@ -1279,7 +1286,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option ! #endif } - if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry)) + if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && options.Type != ModelType::Prefab) { // Perform simple nodes mapping to single node (will transform meshes to model local space) SkeletonMapping skeletonMapping(data.Nodes, nullptr); diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index cec9bdcf2..c0099448d 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -114,6 +114,8 @@ public: SkinnedModel = 1, // The animation asset. Animation = 2, + // The prefab scene. + Prefab = 3, }; /// @@ -282,10 +284,10 @@ public: public: // Splitting // If checked, the imported mesh/animations are split into separate assets. Used if ObjectIndex is set to -1. - API_FIELD(Attributes="EditorOrder(2000), EditorDisplay(\"Splitting\")") + API_FIELD(Attributes="EditorOrder(2000), EditorDisplay(\"Splitting\"), VisibleIf(nameof(ShowSplitting))") bool SplitObjects = false; // The zero-based index for the mesh/animation clip to import. If the source file has more than one mesh/animation it can be used to pick a desired object. Default -1 imports all objects. - API_FIELD(Attributes="EditorOrder(2010), EditorDisplay(\"Splitting\")") + API_FIELD(Attributes="EditorOrder(2010), EditorDisplay(\"Splitting\"), VisibleIf(nameof(ShowSplitting))") int32 ObjectIndex = -1; public: // Other