From a808bcdbf6c0af5021b2c1e2ca8ddd3f8133f2e8 Mon Sep 17 00:00:00 2001 From: Wojtek Figat Date: Fri, 1 Dec 2023 13:57:08 +0100 Subject: [PATCH] Refactor objects splitting in models importing to be handled by `ModelTool` not the importer code itself --- .../Editor/Managed/ManagedEditor.Internal.cpp | 2 +- Source/Engine/Animations/AnimationData.h | 12 +- .../AssetsImportingManager.cpp | 56 +-- .../{ImportModelFile.cpp => ImportModel.cpp} | 213 +++++++-- Source/Engine/ContentImporters/ImportModel.h | 8 +- .../Engine/ContentImporters/ImportModelFile.h | 54 --- Source/Engine/Graphics/Models/ModelData.cpp | 43 +- Source/Engine/Graphics/Models/ModelData.h | 20 +- .../Tools/ModelTool/ModelTool.Assimp.cpp | 297 ++++++------- .../Tools/ModelTool/ModelTool.OpenFBX.cpp | 410 +++++++----------- Source/Engine/Tools/ModelTool/ModelTool.cpp | 162 ++++--- Source/Engine/Tools/ModelTool/ModelTool.h | 16 +- 12 files changed, 630 insertions(+), 663 deletions(-) rename Source/Engine/ContentImporters/{ImportModelFile.cpp => ImportModel.cpp} (51%) delete mode 100644 Source/Engine/ContentImporters/ImportModelFile.h diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp index 969608676..8855ba58a 100644 --- a/Source/Editor/Managed/ManagedEditor.Internal.cpp +++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp @@ -769,7 +769,7 @@ bool ManagedEditor::TryRestoreImportOptions(ModelTool::Options& options, String // Get options from model FileSystem::NormalizePath(assetPath); - return ImportModelFile::TryGetImportOptions(assetPath, options); + return ImportModel::TryGetImportOptions(assetPath, options); } bool ManagedEditor::Import(const String& inputPath, const String& outputPath, const AudioTool::Options& options) diff --git a/Source/Engine/Animations/AnimationData.h b/Source/Engine/Animations/AnimationData.h index 623d9ea6c..c69de8afa 100644 --- a/Source/Engine/Animations/AnimationData.h +++ b/Source/Engine/Animations/AnimationData.h @@ -98,7 +98,6 @@ public: /// struct AnimationData { -public: /// /// The duration of the animation (in frames). /// @@ -114,6 +113,11 @@ public: /// bool EnableRootMotion = false; + /// + /// The animation name. + /// + String Name; + /// /// The custom node name to be used as a root motion source. If not specified the actual root node will be used. /// @@ -131,14 +135,14 @@ public: FORCE_INLINE float GetLength() const { #if BUILD_DEBUG - ASSERT(FramesPerSecond != 0); + ASSERT(FramesPerSecond > ZeroTolerance); #endif return static_cast(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: /// void Dispose() { + Name.Clear(); Duration = 0.0; FramesPerSecond = 0.0; RootNodeName.Clear(); diff --git a/Source/Engine/ContentImporters/AssetsImportingManager.cpp b/Source/Engine/ContentImporters/AssetsImportingManager.cpp index cb366ffb3..91c391711 100644 --- a/Source/Engine/ContentImporters/AssetsImportingManager.cpp +++ b/Source/Engine/ContentImporters/AssetsImportingManager.cpp @@ -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 }, diff --git a/Source/Engine/ContentImporters/ImportModelFile.cpp b/Source/Engine/ContentImporters/ImportModel.cpp similarity index 51% rename from Source/Engine/ContentImporters/ImportModelFile.cpp rename to Source/Engine/ContentImporters/ImportModel.cpp index 64a24217e..5aa44ecc4 100644 --- a/Source/Engine/ContentImporters/ImportModelFile.cpp +++ b/Source/Engine/ContentImporters/ImportModel.cpp @@ -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& materials) +{ + Array 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 const& i1, IGrouping 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>* meshesByNamePtr = options.Cached ? (Array>*)options.Cached->MeshesByName : nullptr; + Array> 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 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>& 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 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 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 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 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; diff --git a/Source/Engine/ContentImporters/ImportModel.h b/Source/Engine/ContentImporters/ImportModel.h index b7af2789e..31a525852 100644 --- a/Source/Engine/ContentImporters/ImportModel.h +++ b/Source/Engine/ContentImporters/ImportModel.h @@ -11,7 +11,7 @@ /// /// Importing models utility /// -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 diff --git a/Source/Engine/ContentImporters/ImportModelFile.h b/Source/Engine/ContentImporters/ImportModelFile.h deleted file mode 100644 index a40109601..000000000 --- a/Source/Engine/ContentImporters/ImportModelFile.h +++ /dev/null @@ -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" - -/// -/// Enable/disable caching model import options -/// -#define IMPORT_MODEL_CACHE_OPTIONS 1 - -/// -/// Importing models utility -/// -class ImportModelFile -{ -public: - typedef ModelTool::Options Options; - -public: - /// - /// Tries the get model import options from the target location asset. - /// - /// The asset path. - /// The options. - /// True if success, otherwise false. - static bool TryGetImportOptions(String path, Options& options); - - /// - /// Imports the model file. - /// - /// The importing context. - /// Result. - static CreateAssetResult Import(CreateAssetContext& context); - - /// - /// Creates the model asset from the ModelData storage (input argument should be pointer to ModelData). - /// - /// The importing context. - /// Result. - 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 diff --git a/Source/Engine/Graphics/Models/ModelData.cpp b/Source/Engine/Graphics/Models/ModelData.cpp index 1d67f4737..3a31c5771 100644 --- a/Source/Engine/Graphics/Models/ModelData.cpp +++ b/Source/Engine/Graphics/Models/ModelData.cpp @@ -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 diff --git a/Source/Engine/Graphics/Models/ModelData.h b/Source/Engine/Graphics/Models/ModelData.h index 1516554fe..65bedf876 100644 --- a/Source/Engine/Graphics/Models/ModelData.h +++ b/Source/Engine/Graphics/Models/ModelData.h @@ -405,10 +405,7 @@ struct FLAXENGINE_API ModelLodData /// /// Finalizes an instance of the class. /// - ~ModelLodData() - { - Meshes.ClearDelete(); - } + ~ModelLodData(); /// /// Gets the bounding box combined for all meshes in this model LOD. @@ -455,15 +452,7 @@ public: /// /// The node animations. /// - AnimationData Animation; - -public: - /// - /// Initializes a new instance of the class. - /// - ModelData() - { - } + Array Animations; public: /// @@ -494,6 +483,7 @@ public: /// The matrix to use for the transformation. void TransformBuffer(const Matrix& matrix); +#if USE_EDITOR public: /// /// Pack mesh data to the header stream @@ -513,6 +503,8 @@ public: /// Pack animation data to the header stream /// /// Output stream + /// Index of animation. /// True if cannot save data, otherwise false - bool Pack2AnimationHeader(WriteStream* stream) const; + bool Pack2AnimationHeader(WriteStream* stream, int32 animIndex = 0) const; +#endif }; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp index ffcce99fc..e62d03b1c 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp @@ -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(); 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); } @@ -739,222 +740,166 @@ void ImportCurve(aiQuatKey* keys, uint32 keysCount, LinearCurve& 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(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 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(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(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; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp index b3e5ecdc8..a6e6225b1 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp @@ -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& 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 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 fileData; + if (File::ReadAllBytes(String(path), fileData)) { - // Import file - Array 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(scene->getGlobalSettings()); - ptr->UpAxis = (ofbx::UpVector)((int32)ptr->UpAxis + 1); - } - - // Process imported scene - context = New(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 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(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(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(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; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 38f010e28..4d6438522 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -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 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 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((endTime - startTime).GetTotalMilliseconds())); diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index e3da7e2c3..cec9bdcf2 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -296,13 +296,17 @@ public: public: // Internals - // Runtime data for objects splitting during import (used internally) - void* SplitContext = nullptr; - Function 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. /// /// The file path. - /// The output data. + /// The output data. /// The import options. /// The error message container. /// The output folder for the additional imported data - optional. Used to auto-import textures and material assets. /// True if fails, otherwise false. - 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);