Refactor objects splitting in models importing to be handled by ModelTool not the importer code itself

This commit is contained in:
Wojtek Figat
2023-12-01 13:57:08 +01:00
parent 6e92d3103c
commit a808bcdbf6
12 changed files with 630 additions and 663 deletions

View File

@@ -98,7 +98,6 @@ public:
/// </summary>
struct AnimationData
{
public:
/// <summary>
/// The duration of the animation (in frames).
/// </summary>
@@ -114,6 +113,11 @@ public:
/// </summary>
bool EnableRootMotion = false;
/// <summary>
/// The animation name.
/// </summary>
String Name;
/// <summary>
/// The custom node name to be used as a root motion source. If not specified the actual root node will be used.
/// </summary>
@@ -131,14 +135,14 @@ public:
FORCE_INLINE float GetLength() const
{
#if BUILD_DEBUG
ASSERT(FramesPerSecond != 0);
ASSERT(FramesPerSecond > ZeroTolerance);
#endif
return static_cast<float>(Duration / FramesPerSecond);
}
uint64 GetMemoryUsage() const
{
uint64 result = RootNodeName.Length() * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData);
uint64 result = (Name.Length() + RootNodeName.Length()) * sizeof(Char) + Channels.Capacity() * sizeof(NodeAnimationData);
for (const auto& e : Channels)
result += e.GetMemoryUsage();
return result;
@@ -164,6 +168,7 @@ public:
::Swap(Duration, other.Duration);
::Swap(FramesPerSecond, other.FramesPerSecond);
::Swap(EnableRootMotion, other.EnableRootMotion);
::Swap(Name, other.Name);
::Swap(RootNodeName, other.RootNodeName);
Channels.Swap(other.Channels);
}
@@ -173,6 +178,7 @@ public:
/// </summary>
void Dispose()
{
Name.Clear();
Duration = 0.0;
FramesPerSecond = 0.0;
RootNodeName.Clear();

View File

@@ -15,7 +15,7 @@
#include "Engine/Platform/Platform.h"
#include "Engine/Engine/Globals.h"
#include "ImportTexture.h"
#include "ImportModelFile.h"
#include "ImportModel.h"
#include "ImportAudio.h"
#include "ImportShader.h"
#include "ImportFont.h"
@@ -425,37 +425,37 @@ bool AssetsImportingManagerService::Init()
{ TEXT("otf"), ASSET_FILES_EXTENSION, ImportFont::Import },
// Models
{ TEXT("obj"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("fbx"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("x"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("dae"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("gltf"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("glb"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("obj"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("fbx"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("x"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("dae"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("gltf"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("glb"), ASSET_FILES_EXTENSION, ImportModel::Import },
// gettext PO files
{ TEXT("po"), TEXT("json"), CreateJson::ImportPo },
// Models (untested formats - may fail :/)
{ TEXT("blend"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("bvh"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("ase"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("ply"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("dxf"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("ifc"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("nff"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("smd"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("vta"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("mdl"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("md2"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("md3"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("md5mesh"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("q3o"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("q3s"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("ac"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("stl"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("lwo"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("lws"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("lxo"), ASSET_FILES_EXTENSION, ImportModelFile::Import },
{ TEXT("blend"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("bvh"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("ase"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("ply"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("dxf"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("ifc"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("nff"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("smd"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("vta"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("mdl"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("md2"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("md3"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("md5mesh"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("q3o"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("q3s"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("ac"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("stl"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("lwo"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("lws"), ASSET_FILES_EXTENSION, ImportModel::Import },
{ TEXT("lxo"), ASSET_FILES_EXTENSION, ImportModel::Import },
};
AssetsImportingManager::Importers.Add(InBuildImporters, ARRAY_COUNT(InBuildImporters));
@@ -473,7 +473,7 @@ bool AssetsImportingManagerService::Init()
{ AssetsImportingManager::CreateMaterialInstanceTag, CreateMaterialInstance::Create },
// Models
{ AssetsImportingManager::CreateModelTag, ImportModelFile::Create },
{ AssetsImportingManager::CreateModelTag, ImportModel::Create },
// Other
{ AssetsImportingManager::CreateRawDataTag, CreateRawData::Create },

View File

@@ -5,6 +5,8 @@
#if COMPILE_WITH_ASSETS_IMPORTER
#include "Engine/Core/Log.h"
#include "Engine/Core/Collections/Sorting.h"
#include "Engine/Core/Collections/ArrayExtensions.h"
#include "Engine/Serialization/MemoryWriteStream.h"
#include "Engine/Serialization/JsonWriters.h"
#include "Engine/Graphics/Models/ModelData.h"
@@ -16,7 +18,7 @@
#include "Engine/Platform/FileSystem.h"
#include "AssetsImportingManager.h"
bool ImportModelFile::TryGetImportOptions(const StringView& path, Options& options)
bool ImportModel::TryGetImportOptions(const StringView& path, Options& options)
{
if (FileSystem::FileExists(path))
{
@@ -88,7 +90,32 @@ void TryRestoreMaterials(CreateAssetContext& context, ModelData& modelData)
}
}
CreateAssetResult ImportModelFile::Import(CreateAssetContext& context)
void SetupMaterialSlots(ModelData& data, const Array<MaterialSlotEntry>& materials)
{
Array<int32> materialSlotsTable;
materialSlotsTable.Resize(materials.Count());
materialSlotsTable.SetAll(-1);
for (auto& lod : data.LODs)
{
for (MeshData* mesh : lod.Meshes)
{
int32 newSlotIndex = materialSlotsTable[mesh->MaterialSlotIndex];
if (newSlotIndex == -1)
{
newSlotIndex = data.Materials.Count();
data.Materials.AddOne() = materials[mesh->MaterialSlotIndex];
}
mesh->MaterialSlotIndex = newSlotIndex;
}
}
}
bool SortMeshGroups(IGrouping<StringView, MeshData*> const& i1, IGrouping<StringView, MeshData*> const& i2)
{
return i1.GetKey().Compare(i2.GetKey()) < 0;
}
CreateAssetResult ImportModel::Import(CreateAssetContext& context)
{
// Get import options
Options options;
@@ -105,9 +132,50 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context)
LOG(Warning, "Missing model import options. Using default values.");
}
}
// Import model file
ModelData* data = options.Cached ? options.Cached->Data : nullptr;
ModelData dataThis;
Array<IGrouping<StringView, MeshData*>>* meshesByNamePtr = options.Cached ? (Array<IGrouping<StringView, MeshData*>>*)options.Cached->MeshesByName : nullptr;
Array<IGrouping<StringView, MeshData*>> meshesByNameThis;
if (!data)
{
String errorMsg;
String 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))
{
LOG(Error, "Cannot import model file. {0}", errorMsg);
return CreateAssetResult::Error;
}
data = &dataThis;
// Group meshes by the name (the same mesh name can be used by multiple meshes that use different materials)
if (data->LODs.Count() != 0)
{
const Function<StringView(MeshData* const&)> f = [](MeshData* const& x) -> StringView
{
return x->Name;
};
ArrayExtensions::GroupBy(data->LODs[0].Meshes, f, meshesByNameThis);
Sorting::QuickSort(meshesByNameThis.Get(), meshesByNameThis.Count(), &SortMeshGroups);
}
meshesByNamePtr = &meshesByNameThis;
}
Array<IGrouping<StringView, MeshData*>>& meshesByName = *meshesByNamePtr;
// Import objects from file separately
if (options.SplitObjects)
{
options.OnSplitImport.Bind([&context](Options& splitOptions, const String& objectName)
// 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)
{
// Recursive importing of the split object
String postFix = objectName;
@@ -117,42 +185,132 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context)
const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax");
return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions);
});
auto splitOptions = options;
switch (options.Type)
{
case ModelTool::ModelType::Model:
case ModelTool::ModelType::SkinnedModel:
LOG(Info, "Splitting imported {0} meshes", meshesByName.Count());
for (int32 groupIndex = 1; groupIndex < meshesByName.Count(); groupIndex++)
{
auto& group = meshesByName[groupIndex];
splitOptions.ObjectIndex = groupIndex;
splitImport(splitOptions, group.GetKey());
}
break;
case ModelTool::ModelType::Animation:
LOG(Info, "Splitting imported {0} animations", data->Animations.Count());
for (int32 i = 1; i < data->Animations.Count(); i++)
{
auto& animation = data->Animations[i];
splitOptions.ObjectIndex = i;
splitImport(splitOptions, animation.Name);
}
break;
}
}
// Import model file
ModelData modelData;
String errorMsg;
String autoImportOutput(StringUtils::GetDirectoryName(context.TargetAssetPath));
autoImportOutput /= options.SubAssetFolder.HasChars() ? options.SubAssetFolder.TrimTrailing() : String(StringUtils::GetFileNameWithoutExtension(context.InputPath));
if (ModelTool::ImportModel(context.InputPath, modelData, options, errorMsg, autoImportOutput))
// When importing a single object as model asset then select a specific mesh group
Array<MeshData*> meshesToDelete;
if (options.ObjectIndex >= 0 &&
options.ObjectIndex < meshesByName.Count() &&
(options.Type == ModelTool::ModelType::Model || options.Type == ModelTool::ModelType::SkinnedModel))
{
LOG(Error, "Cannot import model file. {0}", errorMsg);
return CreateAssetResult::Error;
auto& group = meshesByName[options.ObjectIndex];
if (&dataThis == data)
{
// Use meshes only from the the grouping (others will be removed manually)
{
auto& lod = dataThis.LODs[0];
meshesToDelete.Add(lod.Meshes);
lod.Meshes.Clear();
for (MeshData* mesh : group)
{
lod.Meshes.Add(mesh);
meshesToDelete.Remove(mesh);
}
}
for (int32 lodIndex = 1; lodIndex < dataThis.LODs.Count(); lodIndex++)
{
auto& lod = dataThis.LODs[lodIndex];
Array<MeshData*> lodMeshes = lod.Meshes;
lod.Meshes.Clear();
for (MeshData* lodMesh : lodMeshes)
{
if (lodMesh->Name == group.GetKey())
lod.Meshes.Add(lodMesh);
else
meshesToDelete.Add(lodMesh);
}
}
// Use only materials references by meshes from the first grouping
{
auto materials = dataThis.Materials;
dataThis.Materials.Clear();
SetupMaterialSlots(dataThis, materials);
}
}
else
{
// Copy data from others data
dataThis.Skeleton = data->Skeleton;
dataThis.Nodes = data->Nodes;
// Move meshes from this group (including any LODs of them)
{
auto& lod = dataThis.LODs.AddOne();
lod.ScreenSize = data->LODs[0].ScreenSize;
lod.Meshes.Add(group);
for (MeshData* mesh : group)
data->LODs[0].Meshes.Remove(mesh);
}
for (int32 lodIndex = 1; lodIndex < data->LODs.Count(); lodIndex++)
{
Array<MeshData*> lodMeshes = data->LODs[lodIndex].Meshes;
for (int32 i = lodMeshes.Count() - 1; i >= 0; i--)
{
MeshData* lodMesh = lodMeshes[i];
if (lodMesh->Name == group.GetKey())
data->LODs[lodIndex].Meshes.Remove(lodMesh);
else
lodMeshes.RemoveAtKeepOrder(i);
}
if (lodMeshes.Count() == 0)
break; // No meshes of that name in this LOD so skip further ones
auto& lod = dataThis.LODs.AddOne();
lod.ScreenSize = data->LODs[lodIndex].ScreenSize;
lod.Meshes.Add(lodMeshes);
}
// Copy materials used by the meshes
SetupMaterialSlots(dataThis, data->Materials);
}
data = &dataThis;
}
// Check if restore materials on model reimport
if (options.RestoreMaterialsOnReimport && modelData.Materials.HasItems())
if (options.RestoreMaterialsOnReimport && data->Materials.HasItems())
{
TryRestoreMaterials(context, modelData);
TryRestoreMaterials(context, *data);
}
// Auto calculate LODs transition settings
modelData.CalculateLODsScreenSizes();
// Create destination asset type
CreateAssetResult result = CreateAssetResult::InvalidTypeID;
switch (options.Type)
{
case ModelTool::ModelType::Model:
result = ImportModel(context, modelData, &options);
result = CreateModel(context, *data, &options);
break;
case ModelTool::ModelType::SkinnedModel:
result = ImportSkinnedModel(context, modelData, &options);
result = CreateSkinnedModel(context, *data, &options);
break;
case ModelTool::ModelType::Animation:
result = ImportAnimation(context, modelData, &options);
result = CreateAnimation(context, *data, &options);
break;
}
for (auto mesh : meshesToDelete)
Delete(mesh);
if (result != CreateAssetResult::Ok)
return result;
@@ -172,7 +330,7 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context)
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModelFile::Create(CreateAssetContext& context)
CreateAssetResult ImportModel::Create(CreateAssetContext& context)
{
ASSERT(context.CustomArg != nullptr);
auto& modelData = *(ModelData*)context.CustomArg;
@@ -187,13 +345,11 @@ CreateAssetResult ImportModelFile::Create(CreateAssetContext& context)
// Auto calculate LODs transition settings
modelData.CalculateLODsScreenSizes();
// Import
return ImportModel(context, modelData);
return CreateModel(context, modelData);
}
CreateAssetResult ImportModelFile::ImportModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
CreateAssetResult ImportModel::CreateModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
// Base
IMPORT_SETUP(Model, Model::SerializedVersion);
// Save model header
@@ -242,9 +398,8 @@ CreateAssetResult ImportModelFile::ImportModel(CreateAssetContext& context, Mode
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModelFile::ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
CreateAssetResult ImportModel::CreateSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
// Base
IMPORT_SETUP(SkinnedModel, SkinnedModel::SerializedVersion);
// Save skinned model header
@@ -284,14 +439,14 @@ CreateAssetResult ImportModelFile::ImportSkinnedModel(CreateAssetContext& contex
return CreateAssetResult::Ok;
}
CreateAssetResult ImportModelFile::ImportAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options)
CreateAssetResult ImportModel::CreateAnimation(CreateAssetContext& context, ModelData& modelData, const Options* options)
{
// Base
IMPORT_SETUP(Animation, Animation::SerializedVersion);
// Save animation data
MemoryWriteStream stream(8182);
if (modelData.Pack2AnimationHeader(&stream))
const int32 animIndex = options && options->ObjectIndex != -1 ? options->ObjectIndex : 0; // Single animation per asset
if (modelData.Pack2AnimationHeader(&stream, animIndex))
return CreateAssetResult::Error;
if (context.AllocateChunk(0))
return CreateAssetResult::CannotAllocateChunk;

View File

@@ -11,7 +11,7 @@
/// <summary>
/// Importing models utility
/// </summary>
class ImportModelFile
class ImportModel
{
public:
typedef ModelTool::Options Options;
@@ -40,9 +40,9 @@ public:
static CreateAssetResult Create(CreateAssetContext& context);
private:
static CreateAssetResult ImportModel(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr);
static CreateAssetResult ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData, const Options* options = nullptr);
static CreateAssetResult ImportAnimation(CreateAssetContext& context, ModelData& modelData, 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 CreateAnimation(CreateAssetContext& context, ModelData& data, const Options* options = nullptr);
};
#endif

View File

@@ -1,54 +0,0 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#include "Types.h"
#if COMPILE_WITH_ASSETS_IMPORTER
#include "Engine/Content/Assets/Model.h"
#include "Engine/Tools/ModelTool/ModelTool.h"
/// <summary>
/// Enable/disable caching model import options
/// </summary>
#define IMPORT_MODEL_CACHE_OPTIONS 1
/// <summary>
/// Importing models utility
/// </summary>
class ImportModelFile
{
public:
typedef ModelTool::Options Options;
public:
/// <summary>
/// Tries the get model import options from the target location asset.
/// </summary>
/// <param name="path">The asset path.</param>
/// <param name="options">The options.</param>
/// <returns>True if success, otherwise false.</returns>
static bool TryGetImportOptions(String path, Options& options);
/// <summary>
/// Imports the model file.
/// </summary>
/// <param name="context">The importing context.</param>
/// <returns>Result.</returns>
static CreateAssetResult Import(CreateAssetContext& context);
/// <summary>
/// Creates the model asset from the ModelData storage (input argument should be pointer to ModelData).
/// </summary>
/// <param name="context">The importing context.</param>
/// <returns>Result.</returns>
static CreateAssetResult Create(CreateAssetContext& context);
private:
static CreateAssetResult ImportModel(CreateAssetContext& context, ModelData& modelData);
static CreateAssetResult ImportSkinnedModel(CreateAssetContext& context, ModelData& modelData);
static CreateAssetResult ImportAnimation(CreateAssetContext& context, ModelData& modelData);
};
#endif

View File

@@ -625,6 +625,11 @@ bool MaterialSlotEntry::UsesProperties() const
Normals.TextureIndex != -1;
}
ModelLodData::~ModelLodData()
{
Meshes.ClearDelete();
}
BoundingBox ModelLodData::GetBox() const
{
if (Meshes.IsEmpty())
@@ -644,11 +649,9 @@ void ModelData::CalculateLODsScreenSizes()
{
const float autoComputeLodPowerBase = 0.5f;
const int32 lodCount = LODs.Count();
for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
{
auto& lod = LODs[lodIndex];
if (lodIndex == 0)
{
lod.ScreenSize = 1.0f;
@@ -675,6 +678,8 @@ void ModelData::TransformBuffer(const Matrix& matrix)
}
}
#if USE_EDITOR
bool ModelData::Pack2ModelHeader(WriteStream* stream) const
{
// Validate input
@@ -880,20 +885,21 @@ bool ModelData::Pack2SkinnedModelHeader(WriteStream* stream) const
return false;
}
bool ModelData::Pack2AnimationHeader(WriteStream* stream) const
bool ModelData::Pack2AnimationHeader(WriteStream* stream, int32 animIndex) const
{
// Validate input
if (stream == nullptr)
if (stream == nullptr || animIndex < 0 || animIndex >= Animations.Count())
{
Log::ArgumentNullException();
return true;
}
if (Animation.Duration <= ZeroTolerance || Animation.FramesPerSecond <= ZeroTolerance)
auto& anim = Animations.Get()[animIndex];
if (anim.Duration <= ZeroTolerance || anim.FramesPerSecond <= ZeroTolerance)
{
Log::InvalidOperationException(TEXT("Invalid animation duration."));
return true;
}
if (Animation.Channels.IsEmpty())
if (anim.Channels.IsEmpty())
{
Log::ArgumentOutOfRangeException(TEXT("Channels"), TEXT("Animation channels collection cannot be empty."));
return true;
@@ -901,22 +907,23 @@ bool ModelData::Pack2AnimationHeader(WriteStream* stream) const
// Info
stream->WriteInt32(100); // Header version (for fast version upgrades without serialization format change)
stream->WriteDouble(Animation.Duration);
stream->WriteDouble(Animation.FramesPerSecond);
stream->WriteBool(Animation.EnableRootMotion);
stream->WriteString(Animation.RootNodeName, 13);
stream->WriteDouble(anim.Duration);
stream->WriteDouble(anim.FramesPerSecond);
stream->WriteBool(anim.EnableRootMotion);
stream->WriteString(anim.RootNodeName, 13);
// Animation channels
stream->WriteInt32(Animation.Channels.Count());
for (int32 i = 0; i < Animation.Channels.Count(); i++)
stream->WriteInt32(anim.Channels.Count());
for (int32 i = 0; i < anim.Channels.Count(); i++)
{
auto& anim = Animation.Channels[i];
stream->WriteString(anim.NodeName, 172);
Serialization::Serialize(*stream, anim.Position);
Serialization::Serialize(*stream, anim.Rotation);
Serialization::Serialize(*stream, anim.Scale);
auto& channel = anim.Channels[i];
stream->WriteString(channel.NodeName, 172);
Serialization::Serialize(*stream, channel.Position);
Serialization::Serialize(*stream, channel.Rotation);
Serialization::Serialize(*stream, channel.Scale);
}
return false;
}
#endif

View File

@@ -405,10 +405,7 @@ struct FLAXENGINE_API ModelLodData
/// <summary>
/// Finalizes an instance of the <see cref="ModelLodData"/> class.
/// </summary>
~ModelLodData()
{
Meshes.ClearDelete();
}
~ModelLodData();
/// <summary>
/// Gets the bounding box combined for all meshes in this model LOD.
@@ -455,15 +452,7 @@ public:
/// <summary>
/// The node animations.
/// </summary>
AnimationData Animation;
public:
/// <summary>
/// Initializes a new instance of the <see cref="ModelData"/> class.
/// </summary>
ModelData()
{
}
Array<AnimationData> Animations;
public:
/// <summary>
@@ -494,6 +483,7 @@ public:
/// <param name="matrix">The matrix to use for the transformation.</param>
void TransformBuffer(const Matrix& matrix);
#if USE_EDITOR
public:
/// <summary>
/// Pack mesh data to the header stream
@@ -513,6 +503,8 @@ public:
/// Pack animation data to the header stream
/// </summary>
/// <param name="stream">Output stream</param>
/// <param name="animIndex">Index of animation.</param>
/// <returns>True if cannot save data, otherwise false</returns>
bool Pack2AnimationHeader(WriteStream* stream) const;
bool Pack2AnimationHeader(WriteStream* stream, int32 animIndex = 0) const;
#endif
};

View File

@@ -4,7 +4,6 @@
#include "ModelTool.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/DeleteMe.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Platform/FileSystem.h"
@@ -28,7 +27,6 @@ using namespace Assimp;
class AssimpLogStream : public LogStream
{
public:
AssimpLogStream()
{
DefaultLogger::create("");
@@ -612,32 +610,35 @@ bool IsMeshInvalid(const aiMesh* aMesh)
return aMesh->mPrimitiveTypes != aiPrimitiveType_TRIANGLE || aMesh->mNumVertices == 0 || aMesh->mNumFaces == 0 || aMesh->mFaces[0].mNumIndices != 3;
}
bool ImportMesh(int32 i, ModelData& result, AssimpImporterData& data, String& errorMsg)
bool ImportMesh(int32 index, ModelData& result, AssimpImporterData& data, String& errorMsg)
{
const auto aMesh = data.Scene->mMeshes[i];
const auto aMesh = data.Scene->mMeshes[index];
// Skip invalid meshes
if (IsMeshInvalid(aMesh))
return false;
// Skip unused meshes
if (!data.MeshIndexToNodeIndex.ContainsKey(i))
if (!data.MeshIndexToNodeIndex.ContainsKey(index))
return false;
// Import mesh data
MeshData* meshData = New<MeshData>();
if (ProcessMesh(result, data, aMesh, *meshData, errorMsg))
return true;
auto& nodesWithMesh = data.MeshIndexToNodeIndex[i];
for (int32 j = 0; j < nodesWithMesh.Count(); j++)
{
const auto nodeIndex = nodesWithMesh[j];
Delete(meshData);
return true;
}
auto& nodesWithMesh = data.MeshIndexToNodeIndex[index];
for (int32 i = 0; i < nodesWithMesh.Count(); i++)
{
const auto nodeIndex = nodesWithMesh[i];
auto& node = data.Nodes[nodeIndex];
const int32 lodIndex = node.LodIndex;
// The first mesh instance uses meshData directly while others have to clone it
if (j != 0)
if (i != 0)
{
meshData = New<MeshData>(*meshData);
}
@@ -739,222 +740,166 @@ void ImportCurve(aiQuatKey* keys, uint32 keysCount, LinearCurve<Quaternion>& cur
}
}
void ImportAnimation(int32 index, ModelData& data, AssimpImporterData& importerData)
{
const auto animations = importerData.Scene->mAnimations[index];
auto& anim = data.Animations.AddOne();
anim.Channels.Resize(animations->mNumChannels, false);
anim.Duration = animations->mDuration;
anim.FramesPerSecond = animations->mTicksPerSecond;
if (anim.FramesPerSecond <= 0)
{
anim.FramesPerSecond = importerData.Options.DefaultFrameRate;
if (anim.FramesPerSecond <= 0)
anim.FramesPerSecond = 30.0f;
}
anim.Name = animations->mName.C_Str();
for (unsigned i = 0; i < animations->mNumChannels; i++)
{
const auto aAnim = animations->mChannels[i];
auto& channel = anim.Channels[i];
channel.NodeName = aAnim->mNodeName.C_Str();
ImportCurve(aAnim->mPositionKeys, aAnim->mNumPositionKeys, channel.Position);
ImportCurve(aAnim->mRotationKeys, aAnim->mNumRotationKeys, channel.Rotation);
if (importerData.Options.ImportScaleTracks)
ImportCurve(aAnim->mScalingKeys, aAnim->mNumScalingKeys, channel.Scale);
}
}
bool ModelTool::ImportDataAssimp(const char* path, ModelData& data, Options& options, String& errorMsg)
{
auto context = (AssimpImporterData*)options.SplitContext;
if (!context)
static bool AssimpInited = false;
if (!AssimpInited)
{
static bool AssimpInited = false;
if (!AssimpInited)
{
AssimpInited = true;
LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision());
}
bool importMeshes = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry);
bool importAnimations = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations);
context = New<AssimpImporterData>(path, options);
// Setup import flags
unsigned int flags =
aiProcess_JoinIdenticalVertices |
aiProcess_LimitBoneWeights |
aiProcess_Triangulate |
aiProcess_SortByPType |
aiProcess_GenUVCoords |
aiProcess_FindDegenerates |
aiProcess_FindInvalidData |
//aiProcess_ValidateDataStructure |
aiProcess_ConvertToLeftHanded;
if (importMeshes)
{
if (options.CalculateNormals)
flags |= aiProcess_FixInfacingNormals | aiProcess_GenSmoothNormals;
if (options.CalculateTangents)
flags |= aiProcess_CalcTangentSpace;
if (options.OptimizeMeshes)
flags |= aiProcess_OptimizeMeshes | aiProcess_SplitLargeMeshes | aiProcess_ImproveCacheLocality;
if (options.MergeMeshes)
flags |= aiProcess_RemoveRedundantMaterials;
}
// Setup import options
context->AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_GSN_MAX_SMOOTHING_ANGLE, options.SmoothingNormalsAngle);
context->AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_CT_MAX_SMOOTHING_ANGLE, options.SmoothingTangentsAngle);
//context->AssimpImporter.SetPropertyInteger(AI_CONFIG_PP_SLM_TRIANGLE_LIMIT, MAX_uint16);
context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false);
context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false);
context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, false);
context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, importAnimations);
//context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); // TODO: optimize pivots when https://github.com/assimp/assimp/issues/1068 gets fixed
context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true);
// Import file
context->Scene = context->AssimpImporter.ReadFile(path, flags);
if (context->Scene == nullptr)
{
LOG_STR(Warning, String(context->AssimpImporter.GetErrorString()));
LOG_STR(Warning, String(path));
LOG_STR(Warning, StringUtils::ToString(flags));
errorMsg = context->AssimpImporter.GetErrorString();
Delete(context);
return true;
}
// Create root node
AssimpNode& rootNode = context->Nodes.AddOne();
rootNode.ParentIndex = -1;
rootNode.LodIndex = 0;
rootNode.Name = TEXT("Root");
rootNode.LocalTransform = Transform::Identity;
// Process imported scene nodes
ProcessNodes(*context, context->Scene->mRootNode, 0);
AssimpInited = true;
LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision());
}
DeleteMe<AssimpImporterData> contextCleanup(options.SplitContext ? nullptr : context);
bool importMeshes = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry);
bool importAnimations = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations);
AssimpImporterData context(path, options);
// Setup import flags
unsigned int flags =
aiProcess_JoinIdenticalVertices |
aiProcess_LimitBoneWeights |
aiProcess_Triangulate |
aiProcess_SortByPType |
aiProcess_GenUVCoords |
aiProcess_FindDegenerates |
aiProcess_FindInvalidData |
//aiProcess_ValidateDataStructure |
aiProcess_ConvertToLeftHanded;
if (importMeshes)
{
if (options.CalculateNormals)
flags |= aiProcess_FixInfacingNormals | aiProcess_GenSmoothNormals;
if (options.CalculateTangents)
flags |= aiProcess_CalcTangentSpace;
if (options.OptimizeMeshes)
flags |= aiProcess_OptimizeMeshes | aiProcess_SplitLargeMeshes | aiProcess_ImproveCacheLocality;
if (options.MergeMeshes)
flags |= aiProcess_RemoveRedundantMaterials;
}
// Setup import options
context.AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_GSN_MAX_SMOOTHING_ANGLE, options.SmoothingNormalsAngle);
context.AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_CT_MAX_SMOOTHING_ANGLE, options.SmoothingTangentsAngle);
//context.AssimpImporter.SetPropertyInteger(AI_CONFIG_PP_SLM_TRIANGLE_LIMIT, MAX_uint16);
context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false);
context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false);
context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, false);
context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, importAnimations);
//context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); // TODO: optimize pivots when https://github.com/assimp/assimp/issues/1068 gets fixed
context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true);
// Import file
context.Scene = context.AssimpImporter.ReadFile(path, flags);
if (context.Scene == nullptr)
{
LOG_STR(Warning, String(context.AssimpImporter.GetErrorString()));
LOG_STR(Warning, String(path));
LOG_STR(Warning, StringUtils::ToString(flags));
errorMsg = context.AssimpImporter.GetErrorString();
return true;
}
// Create root node
AssimpNode& rootNode = context.Nodes.AddOne();
rootNode.ParentIndex = -1;
rootNode.LodIndex = 0;
rootNode.Name = TEXT("Root");
rootNode.LocalTransform = Transform::Identity;
// Process imported scene nodes
ProcessNodes(context, context.Scene->mRootNode, 0);
// Import materials
if (ImportMaterials(data, *context, errorMsg))
if (ImportMaterials(data, context, errorMsg))
{
LOG(Warning, "Failed to import materials.");
return true;
}
// Import geometry
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context->Scene->HasMeshes())
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context.Scene->HasMeshes())
{
const int meshCount = context->Scene->mNumMeshes;
if (options.SplitObjects && options.ObjectIndex == -1 && meshCount > 1)
for (unsigned meshIndex = 0; meshIndex < context.Scene->mNumMeshes; meshIndex++)
{
// Import the first object within this call
options.SplitObjects = false;
options.ObjectIndex = 0;
if (options.OnSplitImport.IsBinded())
{
// Split all animations into separate assets
LOG(Info, "Splitting imported {0} meshes", meshCount);
for (int32 i = 1; i < meshCount; i++)
{
auto splitOptions = options;
splitOptions.ObjectIndex = i;
splitOptions.SplitContext = context;
const auto aMesh = context->Scene->mMeshes[i];
const String objectName(aMesh->mName.C_Str());
options.OnSplitImport(splitOptions, objectName);
}
}
}
if (options.ObjectIndex != -1)
{
// Import the selected mesh
const auto meshIndex = Math::Clamp<int32>(options.ObjectIndex, 0, meshCount - 1);
if (ImportMesh(meshIndex, data, *context, errorMsg))
if (ImportMesh((int32)meshIndex, data, context, errorMsg))
return true;
}
else
{
// Import all meshes
for (int32 meshIndex = 0; meshIndex < meshCount; meshIndex++)
{
if (ImportMesh(meshIndex, data, *context, errorMsg))
return true;
}
}
}
// Import skeleton
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton))
{
data.Skeleton.Nodes.Resize(context->Nodes.Count(), false);
for (int32 i = 0; i < context->Nodes.Count(); i++)
data.Skeleton.Nodes.Resize(context.Nodes.Count(), false);
for (int32 i = 0; i < context.Nodes.Count(); i++)
{
auto& node = data.Skeleton.Nodes[i];
auto& aNode = context->Nodes[i];
auto& aNode = context.Nodes[i];
node.Name = aNode.Name;
node.ParentIndex = aNode.ParentIndex;
node.LocalTransform = aNode.LocalTransform;
}
data.Skeleton.Bones.Resize(context->Bones.Count(), false);
for (int32 i = 0; i < context->Bones.Count(); i++)
data.Skeleton.Bones.Resize(context.Bones.Count(), false);
for (int32 i = 0; i < context.Bones.Count(); i++)
{
auto& bone = data.Skeleton.Bones[i];
auto& aBone = context->Bones[i];
auto& aBone = context.Bones[i];
const auto boneNodeIndex = aBone.NodeIndex;
const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context->Bones[aBone.ParentBoneIndex].NodeIndex;
const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context.Bones[aBone.ParentBoneIndex].NodeIndex;
bone.ParentIndex = aBone.ParentBoneIndex;
bone.NodeIndex = aBone.NodeIndex;
bone.LocalTransform = CombineTransformsFromNodeIndices(context->Nodes, parentBoneNodeIndex, boneNodeIndex);
bone.LocalTransform = CombineTransformsFromNodeIndices(context.Nodes, parentBoneNodeIndex, boneNodeIndex);
bone.OffsetMatrix = aBone.OffsetMatrix;
}
}
// Import animations
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations) && context->Scene->HasAnimations())
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations))
{
const int32 animCount = (int32)context->Scene->mNumAnimations;
if (options.SplitObjects && options.ObjectIndex == -1 && animCount > 1)
for (unsigned animIndex = 0; animIndex < context.Scene->mNumAnimations; animIndex++)
{
// Import the first object within this call
options.SplitObjects = false;
options.ObjectIndex = 0;
if (options.OnSplitImport.IsBinded())
{
// Split all animations into separate assets
LOG(Info, "Splitting imported {0} animations", animCount);
for (int32 i = 1; i < animCount; i++)
{
auto splitOptions = options;
splitOptions.ObjectIndex = i;
splitOptions.SplitContext = context;
const auto animations = context->Scene->mAnimations[i];
const String objectName(animations->mName.C_Str());
options.OnSplitImport(splitOptions, objectName);
}
}
}
// Import the animation
{
const auto animIndex = Math::Clamp<int32>(options.ObjectIndex, 0, context->Scene->mNumAnimations - 1);
const auto animations = context->Scene->mAnimations[animIndex];
data.Animation.Channels.Resize(animations->mNumChannels, false);
data.Animation.Duration = animations->mDuration;
data.Animation.FramesPerSecond = animations->mTicksPerSecond;
if (data.Animation.FramesPerSecond <= 0)
{
data.Animation.FramesPerSecond = context->Options.DefaultFrameRate;
if (data.Animation.FramesPerSecond <= 0)
data.Animation.FramesPerSecond = 30.0f;
}
for (unsigned i = 0; i < animations->mNumChannels; i++)
{
const auto aAnim = animations->mChannels[i];
auto& anim = data.Animation.Channels[i];
anim.NodeName = aAnim->mNodeName.C_Str();
ImportCurve(aAnim->mPositionKeys, aAnim->mNumPositionKeys, anim.Position);
ImportCurve(aAnim->mRotationKeys, aAnim->mNumRotationKeys, anim.Rotation);
if (options.ImportScaleTracks)
ImportCurve(aAnim->mScalingKeys, aAnim->mNumScalingKeys, anim.Scale);
}
ImportAnimation((int32)animIndex, data, context);
}
}
// Import nodes
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Nodes))
{
data.Nodes.Resize(context->Nodes.Count());
for (int32 i = 0; i < context->Nodes.Count(); i++)
data.Nodes.Resize(context.Nodes.Count());
for (int32 i = 0; i < context.Nodes.Count(); i++)
{
auto& node = data.Nodes[i];
auto& aNode = context->Nodes[i];
auto& aNode = context.Nodes[i];
node.Name = aNode.Name;
node.ParentIndex = aNode.ParentIndex;

View File

@@ -4,7 +4,6 @@
#include "ModelTool.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/DeleteMe.h"
#include "Engine/Core/Math/Mathd.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Core/Collections/Sorting.h"
@@ -1006,27 +1005,19 @@ void ImportCurve(const ofbx::AnimationCurveNode* curveNode, LinearCurve<T>& curv
}
}
bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importerData)
void ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importerData)
{
const ofbx::AnimationStack* stack = importerData.Scene->getAnimationStack(index);
const ofbx::AnimationLayer* layer = stack->getLayer(0);
const ofbx::TakeInfo* takeInfo = importerData.Scene->getTakeInfo(stack->name);
if (takeInfo == nullptr)
return true;
return;
// Initialize animation animation keyframes sampling
const float frameRate = importerData.FrameRate;
data.Animation.FramesPerSecond = frameRate;
const double localDuration = takeInfo->local_time_to - takeInfo->local_time_from;
if (localDuration <= ZeroTolerance)
return true;
data.Animation.Duration = (double)(int32)(localDuration * frameRate + 0.5f);
AnimInfo info;
info.TimeStart = takeInfo->local_time_from;
info.TimeEnd = takeInfo->local_time_to;
info.Duration = localDuration;
info.FramesCount = (int32)data.Animation.Duration;
info.SamplingPeriod = 1.0f / frameRate;
return;
// Count valid animation channels
Array<int32> animatedNodes(importerData.Nodes.Count());
@@ -1042,15 +1033,32 @@ bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importer
animatedNodes.Add(nodeIndex);
}
if (animatedNodes.IsEmpty())
return true;
data.Animation.Channels.Resize(animatedNodes.Count(), false);
return;
// Setup animation descriptor
auto& animation = data.Animations.AddOne();
animation.Duration = (double)(int32)(localDuration * frameRate + 0.5f);
animation.FramesPerSecond = frameRate;
char nameData[256];
takeInfo->name.toString(nameData);
animation.Name = nameData;
animation.Name = animation.Name.TrimTrailing();
if (animation.Name.IsEmpty())
animation.Name = String(layer->name);
animation.Channels.Resize(animatedNodes.Count(), false);
AnimInfo info;
info.TimeStart = takeInfo->local_time_from;
info.TimeEnd = takeInfo->local_time_to;
info.Duration = localDuration;
info.FramesCount = (int32)animation.Duration;
info.SamplingPeriod = 1.0f / frameRate;
// Import curves
for (int32 i = 0; i < animatedNodes.Count(); i++)
{
const int32 nodeIndex = animatedNodes[i];
auto& aNode = importerData.Nodes[nodeIndex];
auto& anim = data.Animation.Channels[i];
auto& anim = animation.Channels[i];
const ofbx::AnimationCurveNode* translationNode = layer->getCurveNode(*aNode.FbxObj, "Lcl Translation");
const ofbx::AnimationCurveNode* rotationNode = layer->getCurveNode(*aNode.FbxObj, "Lcl Rotation");
@@ -1066,9 +1074,8 @@ bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importer
if (importerData.ConvertRH)
{
for (int32 i = 0; i < data.Animation.Channels.Count(); i++)
for (auto& anim : animation.Channels)
{
auto& anim = data.Animation.Channels[i];
auto& posKeys = anim.Position.GetKeyframes();
auto& rotKeys = anim.Rotation.GetKeyframes();
@@ -1084,8 +1091,6 @@ bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importer
}
}
}
return false;
}
static Float3 FbxVectorFromAxisAndSign(int axis, int sign)
@@ -1105,239 +1110,185 @@ static Float3 FbxVectorFromAxisAndSign(int axis, int sign)
bool ModelTool::ImportDataOpenFBX(const char* path, ModelData& data, Options& options, String& errorMsg)
{
auto context = (OpenFbxImporterData*)options.SplitContext;
if (!context)
// Import file
Array<byte> fileData;
if (File::ReadAllBytes(String(path), fileData))
{
// Import file
Array<byte> fileData;
if (File::ReadAllBytes(String(path), fileData))
{
errorMsg = TEXT("Cannot load file.");
return true;
}
ofbx::u64 loadFlags = 0;
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry))
{
loadFlags |= (ofbx::u64)ofbx::LoadFlags::TRIANGULATE;
if (!options.ImportBlendShapes)
loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES;
}
else
{
loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_GEOMETRY | (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES;
}
ofbx::IScene* scene = ofbx::load(fileData.Get(), fileData.Count(), loadFlags);
if (!scene)
{
errorMsg = ofbx::getError();
return true;
}
fileData.Resize(0);
// Tweak scene if exported by Blender
auto& globalInfo = *scene->getGlobalInfo();
if (StringAnsiView(globalInfo.AppName).StartsWith(StringAnsiView("Blender"), StringSearchCase::IgnoreCase))
{
auto ptr = const_cast<ofbx::GlobalSettings*>(scene->getGlobalSettings());
ptr->UpAxis = (ofbx::UpVector)((int32)ptr->UpAxis + 1);
}
// Process imported scene
context = New<OpenFbxImporterData>(path, options, scene);
auto& globalSettings = context->GlobalSettings;
ProcessNodes(*context, scene->getRoot(), -1);
// Apply model scene global scale factor
context->Nodes[0].LocalTransform = Transform(Vector3::Zero, Quaternion::Identity, globalSettings.UnitScaleFactor) * context->Nodes[0].LocalTransform;
// Log scene info
LOG(Info, "Loaded FBX model, Frame Rate: {0}, Unit Scale Factor: {1}", context->FrameRate, globalSettings.UnitScaleFactor);
LOG(Info, "{0}, {1}, {2}", String(globalInfo.AppName), String(globalInfo.AppVersion), String(globalInfo.AppVendor));
LOG(Info, "Up: {1}{0}", globalSettings.UpAxis == ofbx::UpVector_AxisX ? TEXT("X") : globalSettings.UpAxis == ofbx::UpVector_AxisY ? TEXT("Y") : TEXT("Z"), globalSettings.UpAxisSign == 1 ? TEXT("+") : TEXT("-"));
LOG(Info, "Front: {1}{0}", globalSettings.FrontAxis == ofbx::FrontVector_ParityEven ? TEXT("ParityEven") : TEXT("ParityOdd"), globalSettings.FrontAxisSign == 1 ? TEXT("+") : TEXT("-"));
LOG(Info, "{0} Handed{1}", globalSettings.CoordAxis == ofbx::CoordSystem_RightHanded ? TEXT("Right") : TEXT("Left"), globalSettings.CoordAxisSign == 1 ? TEXT("") : TEXT(" (negative)"));
#if OPEN_FBX_CONVERT_SPACE
LOG(Info, "Imported scene: Up={0}, Front={1}, Right={2}", context->Up, context->Front, context->Right);
#endif
// Extract embedded textures
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Textures))
{
String outputPath;
for (int i = 0, c = scene->getEmbeddedDataCount(); i < c; i++)
{
const ofbx::DataView aEmbedded = scene->getEmbeddedData(i);
ofbx::DataView aFilename = scene->getEmbeddedFilename(i);
char filenameData[256];
aFilename.toString(filenameData);
if (outputPath.IsEmpty())
{
String pathStr(path);
outputPath = String(StringUtils::GetDirectoryName(pathStr)) / TEXT("textures");
FileSystem::CreateDirectory(outputPath);
}
const String filenameStr(filenameData);
String embeddedPath = outputPath / StringUtils::GetFileName(filenameStr);
if (FileSystem::FileExists(embeddedPath))
continue;
LOG(Info, "Extracing embedded resource to {0}", embeddedPath);
if (File::WriteAllBytes(embeddedPath, aEmbedded.begin + 4, (int32)(aEmbedded.end - aEmbedded.begin - 4)))
{
LOG(Error, "Failed to write data to file");
}
}
}
#if OPEN_FBX_CONVERT_SPACE
// Transform nodes to match the engine coordinates system - DirectX (UpVector = +Y, FrontVector = +Z, CoordSystem = -X (LeftHanded))
if (context->Up == Float3(1, 0, 0) && context->Front == Float3(0, 0, 1) && context->Right == Float3(0, 1, 0))
{
context->RootConvertRotation = Quaternion::Euler(0, 180, 0);
}
else if (context->Up == Float3(0, 1, 0) && context->Front == Float3(-1, 0, 0) && context->Right == Float3(0, 0, 1))
{
context->RootConvertRotation = Quaternion::Euler(90, -90, 0);
}
/*Float3 engineUp(0, 1, 0);
Float3 engineFront(0, 0, 1);
Float3 engineRight(-1, 0, 0);*/
/*Float3 engineUp(1, 0, 0);
Float3 engineFront(0, 0, 1);
Float3 engineRight(0, 1, 0);
if (context->Up != engineUp || context->Front != engineFront || context->Right != engineRight)
{
LOG(Info, "Converting imported scene nodes to match engine coordinates system");
context->RootConvertRotation = Quaternion::GetRotationFromTo(context->Up, engineUp, engineUp);
//context->RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context->Right, engineRight, engineRight);
//context->RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context->Front, engineFront, engineFront);
}*/
/*Float3 hackUp = FbxVectorFromAxisAndSign(globalSettings.UpAxis, globalSettings.UpAxisSign);
if (hackUp == Float3::UnitX)
context->RootConvertRotation = Quaternion::Euler(-90, 0, 0);
else if (hackUp == Float3::UnitZ)
context->RootConvertRotation = Quaternion::Euler(90, 0, 0);*/
if (!context->RootConvertRotation.IsIdentity())
{
for (auto& node : context->Nodes)
{
if (node.ParentIndex == -1)
{
node.LocalTransform.Orientation = context->RootConvertRotation * node.LocalTransform.Orientation;
break;
}
}
}
#endif
errorMsg = TEXT("Cannot load file.");
return true;
}
DeleteMe<OpenFbxImporterData> contextCleanup(options.SplitContext ? nullptr : context);
ofbx::u64 loadFlags = 0;
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry))
{
loadFlags |= (ofbx::u64)ofbx::LoadFlags::TRIANGULATE;
if (!options.ImportBlendShapes)
loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES;
}
else
{
loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_GEOMETRY | (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES;
}
ofbx::IScene* scene = ofbx::load(fileData.Get(), fileData.Count(), loadFlags);
if (!scene)
{
errorMsg = ofbx::getError();
return true;
}
fileData.Resize(0);
// Tweak scene if exported by Blender
auto& globalInfo = *scene->getGlobalInfo();
if (StringAnsiView(globalInfo.AppName).StartsWith(StringAnsiView("Blender"), StringSearchCase::IgnoreCase))
{
auto ptr = const_cast<ofbx::GlobalSettings*>(scene->getGlobalSettings());
ptr->UpAxis = (ofbx::UpVector)((int32)ptr->UpAxis + 1);
}
// Process imported scene
OpenFbxImporterData context(path, options, scene);
auto& globalSettings = context.GlobalSettings;
ProcessNodes(context, scene->getRoot(), -1);
// Apply model scene global scale factor
context.Nodes[0].LocalTransform = Transform(Vector3::Zero, Quaternion::Identity, globalSettings.UnitScaleFactor) * context.Nodes[0].LocalTransform;
// Log scene info
LOG(Info, "Loaded FBX model, Frame Rate: {0}, Unit Scale Factor: {1}", context.FrameRate, globalSettings.UnitScaleFactor);
LOG(Info, "{0}, {1}, {2}", String(globalInfo.AppName), String(globalInfo.AppVersion), String(globalInfo.AppVendor));
LOG(Info, "Up: {1}{0}", globalSettings.UpAxis == ofbx::UpVector_AxisX ? TEXT("X") : globalSettings.UpAxis == ofbx::UpVector_AxisY ? TEXT("Y") : TEXT("Z"), globalSettings.UpAxisSign == 1 ? TEXT("+") : TEXT("-"));
LOG(Info, "Front: {1}{0}", globalSettings.FrontAxis == ofbx::FrontVector_ParityEven ? TEXT("ParityEven") : TEXT("ParityOdd"), globalSettings.FrontAxisSign == 1 ? TEXT("+") : TEXT("-"));
LOG(Info, "{0} Handed{1}", globalSettings.CoordAxis == ofbx::CoordSystem_RightHanded ? TEXT("Right") : TEXT("Left"), globalSettings.CoordAxisSign == 1 ? TEXT("") : TEXT(" (negative)"));
#if OPEN_FBX_CONVERT_SPACE
LOG(Info, "Imported scene: Up={0}, Front={1}, Right={2}", context.Up, context.Front, context.Right);
#endif
// Extract embedded textures
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Textures))
{
String outputPath;
for (int i = 0, c = scene->getEmbeddedDataCount(); i < c; i++)
{
const ofbx::DataView aEmbedded = scene->getEmbeddedData(i);
ofbx::DataView aFilename = scene->getEmbeddedFilename(i);
char filenameData[256];
aFilename.toString(filenameData);
if (outputPath.IsEmpty())
{
String pathStr(path);
outputPath = String(StringUtils::GetDirectoryName(pathStr)) / TEXT("textures");
FileSystem::CreateDirectory(outputPath);
}
const String filenameStr(filenameData);
String embeddedPath = outputPath / StringUtils::GetFileName(filenameStr);
if (FileSystem::FileExists(embeddedPath))
continue;
LOG(Info, "Extracing embedded resource to {0}", embeddedPath);
if (File::WriteAllBytes(embeddedPath, aEmbedded.begin + 4, (int32)(aEmbedded.end - aEmbedded.begin - 4)))
{
LOG(Error, "Failed to write data to file");
}
}
}
#if OPEN_FBX_CONVERT_SPACE
// Transform nodes to match the engine coordinates system - DirectX (UpVector = +Y, FrontVector = +Z, CoordSystem = -X (LeftHanded))
if (context.Up == Float3(1, 0, 0) && context.Front == Float3(0, 0, 1) && context.Right == Float3(0, 1, 0))
{
context.RootConvertRotation = Quaternion::Euler(0, 180, 0);
}
else if (context.Up == Float3(0, 1, 0) && context.Front == Float3(-1, 0, 0) && context.Right == Float3(0, 0, 1))
{
context.RootConvertRotation = Quaternion::Euler(90, -90, 0);
}
/*Float3 engineUp(0, 1, 0);
Float3 engineFront(0, 0, 1);
Float3 engineRight(-1, 0, 0);*/
/*Float3 engineUp(1, 0, 0);
Float3 engineFront(0, 0, 1);
Float3 engineRight(0, 1, 0);
if (context.Up != engineUp || context.Front != engineFront || context.Right != engineRight)
{
LOG(Info, "Converting imported scene nodes to match engine coordinates system");
context.RootConvertRotation = Quaternion::GetRotationFromTo(context.Up, engineUp, engineUp);
//context.RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context.Right, engineRight, engineRight);
//context.RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context.Front, engineFront, engineFront);
}*/
/*Float3 hackUp = FbxVectorFromAxisAndSign(globalSettings.UpAxis, globalSettings.UpAxisSign);
if (hackUp == Float3::UnitX)
context.RootConvertRotation = Quaternion::Euler(-90, 0, 0);
else if (hackUp == Float3::UnitZ)
context.RootConvertRotation = Quaternion::Euler(90, 0, 0);*/
if (!context.RootConvertRotation.IsIdentity())
{
for (auto& node : context.Nodes)
{
if (node.ParentIndex == -1)
{
node.LocalTransform.Orientation = context.RootConvertRotation * node.LocalTransform.Orientation;
break;
}
}
}
#endif
// Build final skeleton bones hierarchy before importing meshes
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton))
{
if (ImportBones(*context, errorMsg))
if (ImportBones(context, errorMsg))
{
LOG(Warning, "Failed to import skeleton bones.");
return true;
}
Sorting::QuickSort(context->Bones.Get(), context->Bones.Count());
Sorting::QuickSort(context.Bones.Get(), context.Bones.Count());
}
// Import geometry (meshes and materials)
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context->Scene->getMeshCount() > 0)
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context.Scene->getMeshCount() > 0)
{
const int meshCount = context->Scene->getMeshCount();
if (options.SplitObjects && options.ObjectIndex == -1 && meshCount > 1)
const int meshCount = context.Scene->getMeshCount();
for (int32 meshIndex = 0; meshIndex < meshCount; meshIndex++)
{
// Import the first object within this call
options.SplitObjects = false;
options.ObjectIndex = 0;
if (options.OnSplitImport.IsBinded())
{
// Split all animations into separate assets
LOG(Info, "Splitting imported {0} meshes", meshCount);
for (int32 i = 1; i < meshCount; i++)
{
auto splitOptions = options;
splitOptions.ObjectIndex = i;
splitOptions.SplitContext = context;
const auto aMesh = context->Scene->getMesh(i);
const String objectName(aMesh->name);
options.OnSplitImport(splitOptions, objectName);
}
}
}
if (options.ObjectIndex != -1)
{
// Import the selected mesh
const auto meshIndex = Math::Clamp<int32>(options.ObjectIndex, 0, meshCount - 1);
if (ImportMesh(meshIndex, data, *context, errorMsg))
if (ImportMesh(meshIndex, data, context, errorMsg))
return true;
// Let the firstly imported mesh import all materials from all meshes (index 0 is importing all following ones before itself during splitting - see code above)
if (options.ObjectIndex == 1)
{
for (int32 i = 0; i < meshCount; i++)
{
const auto aMesh = context->Scene->getMesh(i);
if (i == 1 || IsMeshInvalid(aMesh))
continue;
for (int32 j = 0; j < aMesh->getMaterialCount(); j++)
{
const ofbx::Material* aMaterial = aMesh->getMaterial(j);
context->AddMaterial(data, aMaterial);
}
}
}
}
else
{
// Import all meshes
for (int32 meshIndex = 0; meshIndex < meshCount; meshIndex++)
{
if (ImportMesh(meshIndex, data, *context, errorMsg))
return true;
}
}
}
// Import skeleton
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton))
{
data.Skeleton.Nodes.Resize(context->Nodes.Count(), false);
for (int32 i = 0; i < context->Nodes.Count(); i++)
data.Skeleton.Nodes.Resize(context.Nodes.Count(), false);
for (int32 i = 0; i < context.Nodes.Count(); i++)
{
auto& node = data.Skeleton.Nodes[i];
auto& aNode = context->Nodes[i];
auto& aNode = context.Nodes[i];
node.Name = aNode.Name;
node.ParentIndex = aNode.ParentIndex;
node.LocalTransform = aNode.LocalTransform;
}
data.Skeleton.Bones.Resize(context->Bones.Count(), false);
for (int32 i = 0; i < context->Bones.Count(); i++)
data.Skeleton.Bones.Resize(context.Bones.Count(), false);
for (int32 i = 0; i < context.Bones.Count(); i++)
{
auto& bone = data.Skeleton.Bones[i];
auto& aBone = context->Bones[i];
auto& aBone = context.Bones[i];
const auto boneNodeIndex = aBone.NodeIndex;
// Find the parent bone
int32 parentBoneIndex = -1;
for (int32 j = context->Nodes[boneNodeIndex].ParentIndex; j != -1; j = context->Nodes[j].ParentIndex)
for (int32 j = context.Nodes[boneNodeIndex].ParentIndex; j != -1; j = context.Nodes[j].ParentIndex)
{
parentBoneIndex = context->FindBone(j);
parentBoneIndex = context.FindBone(j);
if (parentBoneIndex != -1)
break;
}
aBone.ParentBoneIndex = parentBoneIndex;
const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context->Bones[aBone.ParentBoneIndex].NodeIndex;
const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context.Bones[aBone.ParentBoneIndex].NodeIndex;
bone.ParentIndex = aBone.ParentBoneIndex;
bone.NodeIndex = aBone.NodeIndex;
bone.LocalTransform = CombineTransformsFromNodeIndices(context->Nodes, parentBoneNodeIndex, boneNodeIndex);
bone.LocalTransform = CombineTransformsFromNodeIndices(context.Nodes, parentBoneNodeIndex, boneNodeIndex);
bone.OffsetMatrix = aBone.OffsetMatrix;
}
}
@@ -1345,54 +1296,21 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ModelData& data, Options& op
// Import animations
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations))
{
const int animCount = context->Scene->getAnimationStackCount();
if (options.SplitObjects && options.ObjectIndex == -1 && animCount > 1)
const int animCount = context.Scene->getAnimationStackCount();
for (int32 animIndex = 0; animIndex < animCount; animIndex++)
{
// Import the first object within this call
options.SplitObjects = false;
options.ObjectIndex = 0;
if (options.OnSplitImport.IsBinded())
{
// Split all animations into separate assets
LOG(Info, "Splitting imported {0} animations", animCount);
for (int32 i = 1; i < animCount; i++)
{
auto splitOptions = options;
splitOptions.ObjectIndex = i;
splitOptions.SplitContext = context;
const ofbx::AnimationStack* stack = context->Scene->getAnimationStack(i);
const ofbx::AnimationLayer* layer = stack->getLayer(0);
const String objectName(layer->name);
options.OnSplitImport(splitOptions, objectName);
}
}
}
if (options.ObjectIndex != -1)
{
// Import selected animation
const auto animIndex = Math::Clamp<int32>(options.ObjectIndex, 0, animCount - 1);
ImportAnimation(animIndex, data, *context);
}
else
{
// Import first valid animation
for (int32 animIndex = 0; animIndex < animCount; animIndex++)
{
if (!ImportAnimation(animIndex, data, *context))
break;
}
ImportAnimation(animIndex, data, context);
}
}
// Import nodes
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Nodes))
{
data.Nodes.Resize(context->Nodes.Count());
for (int32 i = 0; i < context->Nodes.Count(); i++)
data.Nodes.Resize(context.Nodes.Count());
for (int32 i = 0; i < context.Nodes.Count(); i++)
{
auto& node = data.Nodes[i];
auto& aNode = context->Nodes[i];
auto& aNode = context.Nodes[i];
node.Name = aNode.Name;
node.ParentIndex = aNode.ParentIndex;

View File

@@ -452,6 +452,9 @@ 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)
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?
// Validate path
// Note: Assimp/Autodesk supports only ANSI characters in imported file path
@@ -511,8 +514,6 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options
FileSystem::DeleteFile(tmpPath);
}
// TODO: check model LODs sequence (eg. {LOD0, LOD2, LOD5} is invalid)
// Remove namespace prefixes from the nodes names
{
for (auto& node : data.Nodes)
@@ -523,9 +524,10 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options
{
RemoveNamespace(node.Name);
}
for (auto& channel : data.Animation.Channels)
for (auto& animation : data.Animations)
{
RemoveNamespace(channel.NodeName);
for (auto& channel : animation.Channels)
RemoveNamespace(channel.NodeName);
}
for (auto& lod : data.LODs)
{
@@ -533,18 +535,19 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options
{
RemoveNamespace(mesh->Name);
for (auto& blendShape : mesh->BlendShapes)
{
RemoveNamespace(blendShape.Name);
}
}
}
}
// Validate the animation channels
if (data.Animation.Channels.HasItems())
for (auto& animation : data.Animations)
{
auto& channels = animation.Channels;
if (channels.IsEmpty())
continue;
// Validate bone animations uniqueness
auto& channels = data.Animation.Channels;
for (int32 i = 0; i < channels.Count(); i++)
{
for (int32 j = i + 1; j < channels.Count(); j++)
@@ -742,7 +745,7 @@ void TrySetupMaterialParameter(MaterialInstance* instance, Span<const Char*> par
{
if (type == MaterialParameterType::Color)
{
if (paramType != MaterialParameterType::Vector3 ||
if (paramType != MaterialParameterType::Vector3 ||
paramType != MaterialParameterType::Vector4)
continue;
}
@@ -757,7 +760,7 @@ void TrySetupMaterialParameter(MaterialInstance* instance, Span<const Char*> par
}
}
bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& options, String& errorMsg, const String& autoImportOutput)
bool ModelTool::ImportModel(const String& path, ModelData& data, Options& options, String& errorMsg, const String& autoImportOutput)
{
LOG(Info, "Importing model from \'{0}\'", path);
const auto startTime = DateTime::NowUTC();
@@ -785,7 +788,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
default:
return true;
}
ModelData data;
if (ImportData(path, data, options, errorMsg))
return true;
@@ -926,13 +928,20 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
case ModelType::Animation:
{
// Validate
if (data.Animation.Channels.IsEmpty())
if (data.Animations.IsEmpty())
{
errorMsg = TEXT("Imported file has no valid animations.");
return true;
}
LOG(Info, "Imported animation has {0} channels, duration: {1} frames, frames per second: {2}", data.Animation.Channels.Count(), data.Animation.Duration, data.Animation.FramesPerSecond);
for (auto& animation : data.Animations)
{
LOG(Info, "Imported animation '{}' has {} channels, duration: {} frames, frames per second: {}", animation.Name, animation.Channels.Count(), animation.Duration, animation.FramesPerSecond);
if (animation.Duration <= ZeroTolerance || animation.FramesPerSecond <= ZeroTolerance)
{
errorMsg = TEXT("Invalid animation duration.");
return true;
}
}
break;
}
}
@@ -976,7 +985,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
case TextureEntry::TypeHint::Normals:
textureOptions.Type = TextureFormatType::NormalMap;
break;
default: ;
}
AssetsImportingManager::ImportIfEdited(texture.FilePath, assetPath, texture.AssetID, &textureOptions);
#endif
@@ -1461,65 +1469,67 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
}
else if (options.Type == ModelType::Animation)
{
// Trim the animation keyframes range if need to
if (options.Duration == AnimationDuration::Custom)
for (auto& animation : data.Animations)
{
// Custom animation import, frame index start and end
const float start = options.FramesRange.X;
const float end = options.FramesRange.Y;
for (int32 i = 0; i < data.Animation.Channels.Count(); i++)
// Trim the animation keyframes range if need to
if (options.Duration == AnimationDuration::Custom)
{
auto& anim = data.Animation.Channels[i];
anim.Position.Trim(start, end);
anim.Rotation.Trim(start, end);
anim.Scale.Trim(start, end);
}
data.Animation.Duration = end - start;
}
// Change the sampling rate if need to
if (!Math::IsZero(options.SamplingRate))
{
const float timeScale = (float)(data.Animation.FramesPerSecond / options.SamplingRate);
if (!Math::IsOne(timeScale))
{
data.Animation.FramesPerSecond = options.SamplingRate;
for (int32 i = 0; i < data.Animation.Channels.Count(); i++)
// Custom animation import, frame index start and end
const float start = options.FramesRange.X;
const float end = options.FramesRange.Y;
for (int32 i = 0; i < animation.Channels.Count(); i++)
{
auto& anim = data.Animation.Channels[i];
auto& anim = animation.Channels[i];
anim.Position.Trim(start, end);
anim.Rotation.Trim(start, end);
anim.Scale.Trim(start, end);
}
animation.Duration = end - start;
}
anim.Position.TransformTime(timeScale, 0.0f);
anim.Rotation.TransformTime(timeScale, 0.0f);
anim.Scale.TransformTime(timeScale, 0.0f);
// Change the sampling rate if need to
if (!Math::IsZero(options.SamplingRate))
{
const float timeScale = (float)(animation.FramesPerSecond / options.SamplingRate);
if (!Math::IsOne(timeScale))
{
animation.FramesPerSecond = options.SamplingRate;
for (int32 i = 0; i < animation.Channels.Count(); i++)
{
auto& anim = animation.Channels[i];
anim.Position.TransformTime(timeScale, 0.0f);
anim.Rotation.TransformTime(timeScale, 0.0f);
anim.Scale.TransformTime(timeScale, 0.0f);
}
}
}
}
// Optimize the keyframes
if (options.OptimizeKeyframes)
{
const int32 before = data.Animation.GetKeyframesCount();
for (int32 i = 0; i < data.Animation.Channels.Count(); i++)
// Optimize the keyframes
if (options.OptimizeKeyframes)
{
auto& anim = data.Animation.Channels[i];
// Optimize keyframes
OptimizeCurve(anim.Position);
OptimizeCurve(anim.Rotation);
OptimizeCurve(anim.Scale);
// Remove empty channels
if (anim.GetKeyframesCount() == 0)
const int32 before = animation.GetKeyframesCount();
for (int32 i = 0; i < animation.Channels.Count(); i++)
{
data.Animation.Channels.RemoveAt(i--);
}
}
const int32 after = data.Animation.GetKeyframesCount();
LOG(Info, "Optimized {0} animation keyframe(s). Before: {1}, after: {2}, Ratio: {3}%", before - after, before, after, Utilities::RoundTo2DecimalPlaces((float)after / before));
}
auto& anim = animation.Channels[i];
data.Animation.EnableRootMotion = options.EnableRootMotion;
data.Animation.RootNodeName = options.RootNodeName;
// Optimize keyframes
OptimizeCurve(anim.Position);
OptimizeCurve(anim.Rotation);
OptimizeCurve(anim.Scale);
// Remove empty channels
if (anim.GetKeyframesCount() == 0)
{
animation.Channels.RemoveAt(i--);
}
}
const int32 after = animation.GetKeyframesCount();
LOG(Info, "Optimized {0} animation keyframe(s). Before: {1}, after: {2}, Ratio: {3}%", before - after, before, after, Utilities::RoundTo2DecimalPlaces((float)after / before));
}
animation.EnableRootMotion = options.EnableRootMotion;
animation.RootNodeName = options.RootNodeName;
}
}
// Merge meshes with the same parent nodes, material and skinning
@@ -1696,27 +1706,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
}
}
// Export imported data to the output container (we reduce vertex data copy operations to minimum)
{
meshData.Textures.Swap(data.Textures);
meshData.Materials.Swap(data.Materials);
meshData.LODs.Resize(data.LODs.Count(), false);
for (int32 i = 0; i < data.LODs.Count(); i++)
{
auto& dst = meshData.LODs[i];
auto& src = data.LODs[i];
dst.Meshes = src.Meshes;
}
meshData.Skeleton.Swap(data.Skeleton);
meshData.Animation.Swap(data.Animation);
// Clear meshes from imported data (we link them to result model data). This reduces amount of allocations.
data.LODs.Resize(0);
}
// Calculate blend shapes vertices ranges
for (auto& lod : meshData.LODs)
for (auto& lod : data.LODs)
{
for (auto& mesh : lod.Meshes)
{
@@ -1737,6 +1728,9 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
}
}
// Auto calculate LODs transition settings
data.CalculateLODsScreenSizes();
const auto endTime = DateTime::NowUTC();
LOG(Info, "Model file imported in {0} ms", static_cast<int32>((endTime - startTime).GetTotalMilliseconds()));

View File

@@ -296,13 +296,17 @@ public:
public: // Internals
// Runtime data for objects splitting during import (used internally)
void* SplitContext = nullptr;
Function<bool(Options& splitOptions, const String& objectName)> OnSplitImport;
// Internal flags for objects to import.
ImportDataTypes ImportTypes = ImportDataTypes::None;
struct CachedData
{
ModelData* Data = nullptr;
void* MeshesByName = nullptr;
};
// Cached model data - used when performing nested importing (eg. via objects splitting). Allows to read and process source file only once and use those results for creation of multiple assets (permutation via ObjectIndex).
CachedData* Cached = nullptr;
public:
// [ISerializable]
void Serialize(SerializeStream& stream, const void* otherObj) override;
@@ -324,12 +328,12 @@ public:
/// Imports the model.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="meshData">The output data.</param>
/// <param name="data">The output data.</param>
/// <param name="options">The import options.</param>
/// <param name="errorMsg">The error message container.</param>
/// <param name="autoImportOutput">The output folder for the additional imported data - optional. Used to auto-import textures and material assets.</param>
/// <returns>True if fails, otherwise false.</returns>
static bool ImportModel(const String& path, ModelData& meshData, Options& options, String& errorMsg, const String& autoImportOutput = String::Empty);
static bool ImportModel(const String& path, ModelData& data, Options& options, String& errorMsg, const String& autoImportOutput = String::Empty);
public:
static int32 DetectLodIndex(const String& nodeName);