@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user