Add **option to import model file as Prefab**

#1909 #1329 #1973
This commit is contained in:
Wojtek Figat
2023-12-06 11:19:42 +01:00
parent 3f632b7d15
commit 63773f2ddf
7 changed files with 199 additions and 14 deletions

View File

@@ -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;
}
}
}

View File

@@ -382,7 +382,7 @@ bool AssetsImportingManager::Create(const Function<CreateAssetResult(CreateAsset
// Do nothing
return true;
}
else
else if (result != CreateAssetResult::Skip)
{
LOG(Error, "Cannot import file '{0}'! Result: {1}", inputPath, ::ToString(result));
return true;

View File

@@ -15,6 +15,10 @@
#include "Engine/Content/Storage/ContentStorageManager.h"
#include "Engine/Content/Assets/Animation.h"
#include "Engine/Content/Content.h"
#include "Engine/Level/Actors/EmptyActor.h"
#include "Engine/Level/Actors/StaticModel.h"
#include "Engine/Level/Prefabs/Prefab.h"
#include "Engine/Level/Prefabs/PrefabManager.h"
#include "Engine/Platform/FileSystem.h"
#include "Engine/Utilities/RectPack.h"
#include "Engine/Profiler/ProfilerCPU.h"
@@ -50,6 +54,13 @@ bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
return false;
}
struct PrefabObject
{
int32 NodeIndex;
String Name;
String AssetPath;
};
void RepackMeshLightmapUVs(ModelData& data)
{
// Use weight-based coordinates space placement and rect-pack to allocate more space for bigger meshes in the model lightmap chart
@@ -219,10 +230,11 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
ModelData dataThis;
Array<IGrouping<StringView, MeshData*>>* meshesByNamePtr = options.Cached ? (Array<IGrouping<StringView, MeshData*>>*)options.Cached->MeshesByName : nullptr;
Array<IGrouping<StringView, MeshData*>> 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<IGrouping<StringView, MeshData*>>& meshesByName = *meshesByNamePtr;
// Import objects from file separately
if (options.SplitObjects)
ModelTool::Options::CachedData cached = { data, (void*)meshesByNamePtr };
Array<PrefabObject> 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<bool(Options& splitOptions, const StringView& objectName, String& outputPath)> 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<bool(Options& splitOptions, const StringView& objectName)> 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<PrefabObject>& 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<Prefab>(outputPath) : nullptr;
if (prefab)
{
// Ensure that prefab has Default Instance so ObjectsCache is valid (used below)
prefab->GetDefaultInstance();
}
// Create prefab structure
Dictionary<int32, Actor*> nodeToActor;
Array<Actor*> 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<StaticModel>();
actor->SetName(e.Name);
if (auto* model = Content::LoadAsync<Model>(e.AssetPath))
{
actor->Model = model;
}
nodeActors.Add(actor);
}
}
Actor* nodeActor = nodeActors.Count() == 1 ? nodeActors[0] : New<EmptyActor>();
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

View File

@@ -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<struct PrefabObject>& prefabObjects);
};
#endif

View File

@@ -18,7 +18,7 @@ class CreateAssetContext;
/// <summary>
/// Create/Import new asset callback result
/// </summary>
DECLARE_ENUM_7(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID);
DECLARE_ENUM_8(CreateAssetResult, Ok, Abort, Error, CannotSaveFile, InvalidPath, CannotAllocateChunk, InvalidTypeID, Skip);
/// <summary>
/// Create/Import new asset callback function

View File

@@ -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<ModelDataNode> skeletonMapping(data.Nodes, nullptr);

View File

@@ -114,6 +114,8 @@ public:
SkinnedModel = 1,
// The animation asset.
Animation = 2,
// The prefab scene.
Prefab = 3,
};
/// <summary>
@@ -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