@@ -12,13 +12,14 @@ namespace FlaxEngine.Tools
|
|||||||
{
|
{
|
||||||
partial struct Options
|
partial struct Options
|
||||||
{
|
{
|
||||||
private bool ShowGeometry => Type == ModelTool.ModelType.Model || Type == ModelTool.ModelType.SkinnedModel;
|
private bool ShowGeometry => Type == ModelType.Model || Type == ModelType.SkinnedModel || Type == ModelType.Prefab;
|
||||||
private bool ShowModel => Type == ModelTool.ModelType.Model;
|
private bool ShowModel => Type == ModelType.Model || Type == ModelType.Prefab;
|
||||||
private bool ShowSkinnedModel => Type == ModelTool.ModelType.SkinnedModel;
|
private bool ShowSkinnedModel => Type == ModelType.SkinnedModel || Type == ModelType.Prefab;
|
||||||
private bool ShowAnimation => Type == ModelTool.ModelType.Animation;
|
private bool ShowAnimation => Type == ModelType.Animation || Type == ModelType.Prefab;
|
||||||
private bool ShowSmoothingNormalsAngle => ShowGeometry && CalculateNormals;
|
private bool ShowSmoothingNormalsAngle => ShowGeometry && CalculateNormals;
|
||||||
private bool ShowSmoothingTangentsAngle => ShowGeometry && CalculateTangents;
|
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
|
// Do nothing
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else
|
else if (result != CreateAssetResult::Skip)
|
||||||
{
|
{
|
||||||
LOG(Error, "Cannot import file '{0}'! Result: {1}", inputPath, ::ToString(result));
|
LOG(Error, "Cannot import file '{0}'! Result: {1}", inputPath, ::ToString(result));
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -15,6 +15,10 @@
|
|||||||
#include "Engine/Content/Storage/ContentStorageManager.h"
|
#include "Engine/Content/Storage/ContentStorageManager.h"
|
||||||
#include "Engine/Content/Assets/Animation.h"
|
#include "Engine/Content/Assets/Animation.h"
|
||||||
#include "Engine/Content/Content.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/Platform/FileSystem.h"
|
||||||
#include "Engine/Utilities/RectPack.h"
|
#include "Engine/Utilities/RectPack.h"
|
||||||
#include "Engine/Profiler/ProfilerCPU.h"
|
#include "Engine/Profiler/ProfilerCPU.h"
|
||||||
@@ -50,6 +54,13 @@ bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct PrefabObject
|
||||||
|
{
|
||||||
|
int32 NodeIndex;
|
||||||
|
String Name;
|
||||||
|
String AssetPath;
|
||||||
|
};
|
||||||
|
|
||||||
void RepackMeshLightmapUVs(ModelData& data)
|
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
|
// 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;
|
ModelData dataThis;
|
||||||
Array<IGrouping<StringView, MeshData*>>* meshesByNamePtr = options.Cached ? (Array<IGrouping<StringView, MeshData*>>*)options.Cached->MeshesByName : nullptr;
|
Array<IGrouping<StringView, MeshData*>>* meshesByNamePtr = options.Cached ? (Array<IGrouping<StringView, MeshData*>>*)options.Cached->MeshesByName : nullptr;
|
||||||
Array<IGrouping<StringView, MeshData*>> meshesByNameThis;
|
Array<IGrouping<StringView, MeshData*>> meshesByNameThis;
|
||||||
|
String autoImportOutput;
|
||||||
if (!data)
|
if (!data)
|
||||||
{
|
{
|
||||||
String errorMsg;
|
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));
|
autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath));
|
||||||
if (ModelTool::ImportModel(context.InputPath, dataThis, options, errorMsg, autoImportOutput))
|
if (ModelTool::ImportModel(context.InputPath, dataThis, options, errorMsg, autoImportOutput))
|
||||||
{
|
{
|
||||||
@@ -246,14 +258,62 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
|
|||||||
Array<IGrouping<StringView, MeshData*>>& meshesByName = *meshesByNamePtr;
|
Array<IGrouping<StringView, MeshData*>>& meshesByName = *meshesByNamePtr;
|
||||||
|
|
||||||
// Import objects from file separately
|
// 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
|
// Import the first object within this call
|
||||||
options.SplitObjects = false;
|
options.SplitObjects = false;
|
||||||
options.ObjectIndex = 0;
|
options.ObjectIndex = 0;
|
||||||
|
|
||||||
// Import rest of the objects recursive but use current model data to skip loading file again
|
// 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;
|
options.Cached = &cached;
|
||||||
Function<bool(Options& splitOptions, const StringView& objectName)> splitImport;
|
Function<bool(Options& splitOptions, const StringView& objectName)> splitImport;
|
||||||
splitImport.Bind([&context](Options& splitOptions, const StringView& objectName)
|
splitImport.Bind([&context](Options& splitOptions, const StringView& objectName)
|
||||||
@@ -396,6 +456,9 @@ CreateAssetResult ImportModel::Import(CreateAssetContext& context)
|
|||||||
case ModelTool::ModelType::Animation:
|
case ModelTool::ModelType::Animation:
|
||||||
result = CreateAnimation(context, *data, &options);
|
result = CreateAnimation(context, *data, &options);
|
||||||
break;
|
break;
|
||||||
|
case ModelTool::ModelType::Prefab:
|
||||||
|
result = CreatePrefab(context, *data, options, prefabObjects);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
for (auto mesh : meshesToDelete)
|
for (auto mesh : meshesToDelete)
|
||||||
Delete(mesh);
|
Delete(mesh);
|
||||||
@@ -546,4 +609,115 @@ CreateAssetResult ImportModel::CreateAnimation(CreateAssetContext& context, Mode
|
|||||||
return CreateAssetResult::Ok;
|
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
|
#endif
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ private:
|
|||||||
static CreateAssetResult CreateModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr);
|
static CreateAssetResult CreateModel(CreateAssetContext& context, ModelData& data, const Options* options = nullptr);
|
||||||
static CreateAssetResult CreateSkinnedModel(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 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
|
#endif
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class CreateAssetContext;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create/Import new asset callback result
|
/// Create/Import new asset callback result
|
||||||
/// </summary>
|
/// </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>
|
/// <summary>
|
||||||
/// Create/Import new asset callback function
|
/// 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.FramesRange.Y = Math::Max(options.FramesRange.Y, options.FramesRange.X);
|
||||||
options.DefaultFrameRate = Math::Max(0.0f, options.DefaultFrameRate);
|
options.DefaultFrameRate = Math::Max(0.0f, options.DefaultFrameRate);
|
||||||
options.SamplingRate = Math::Max(0.0f, options.SamplingRate);
|
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
|
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?
|
// 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:
|
case ModelType::Animation:
|
||||||
options.ImportTypes = ImportDataTypes::Animations;
|
options.ImportTypes = ImportDataTypes::Animations;
|
||||||
break;
|
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:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1279,7 +1286,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& data, Options& option
|
|||||||
!
|
!
|
||||||
#endif
|
#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)
|
// Perform simple nodes mapping to single node (will transform meshes to model local space)
|
||||||
SkeletonMapping<ModelDataNode> skeletonMapping(data.Nodes, nullptr);
|
SkeletonMapping<ModelDataNode> skeletonMapping(data.Nodes, nullptr);
|
||||||
|
|||||||
@@ -114,6 +114,8 @@ public:
|
|||||||
SkinnedModel = 1,
|
SkinnedModel = 1,
|
||||||
// The animation asset.
|
// The animation asset.
|
||||||
Animation = 2,
|
Animation = 2,
|
||||||
|
// The prefab scene.
|
||||||
|
Prefab = 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -282,10 +284,10 @@ public:
|
|||||||
public: // Splitting
|
public: // Splitting
|
||||||
|
|
||||||
// If checked, the imported mesh/animations are split into separate assets. Used if ObjectIndex is set to -1.
|
// 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;
|
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.
|
// 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;
|
int32 ObjectIndex = -1;
|
||||||
|
|
||||||
public: // Other
|
public: // Other
|
||||||
|
|||||||
Reference in New Issue
Block a user