diff --git a/Source/Editor/Content/Import/ModelImportEntry.cs b/Source/Editor/Content/Import/ModelImportEntry.cs index efcf1844f..3b5ba83a7 100644 --- a/Source/Editor/Content/Import/ModelImportEntry.cs +++ b/Source/Editor/Content/Import/ModelImportEntry.cs @@ -243,12 +243,6 @@ namespace FlaxEditor.Content.Import [EditorOrder(1070), DefaultValue(typeof(string), ""), EditorDisplay("Animation"), Tooltip("The custom node name to be used as a root motion source. If not specified the actual root node will be used.")] public string RootNodeName { get; set; } - /// - /// The zero-based index for the animation clip to import. If the source file has more than one animation it can be used to pick a desire clip. - /// - [EditorOrder(1080), DefaultValue(-1), EditorDisplay("Animation"), Tooltip("The zero-based index for the animation clip to import. If the source file has more than one animation it can be used to pick a desire clip.")] - public int AnimationIndex { get; set; } = -1; - /// /// If checked, the importer will generate a sequence of LODs based on the base LOD index. /// @@ -290,6 +284,18 @@ namespace FlaxEditor.Content.Import /// [EditorOrder(420), DefaultValue(true), EditorDisplay("Materials", "Restore Materials On Reimport"), Tooltip("If checked, the importer will try to restore the assigned materials to the model slots.")] public bool RestoreMaterialsOnReimport { get; set; } = true; + + /// + /// If checked, the imported mesh/animations are splitted into separate assets. Used if ObjectIndex is set to -1. + /// + [EditorOrder(2000), DefaultValue(false), EditorDisplay("Splitting"), Tooltip("If checked, the imported mesh/animations are splitted into separate assets. Used if ObjectIndex is set to -1.")] + public bool SplitObjects { get; set; } = false; + + /// + /// The zero-based index for the mesh/animation clip to import. If the source file has more than one mesh/animation it can be used to pick a desire object. + /// + [EditorOrder(2010), DefaultValue(-1), EditorDisplay("Splitting"), Tooltip("The zero-based index for the mesh/animation clip to import. If the source file has more than one mesh/animation it can be used to pick a desire object.")] + public int ObjectIndex { get; set; } = -1; [StructLayout(LayoutKind.Sequential)] internal struct InternalOptions @@ -325,7 +331,6 @@ namespace FlaxEditor.Content.Import public byte OptimizeKeyframes; public byte EnableRootMotion; public string RootNodeName; - public int AnimationIndex; // Level Of Detail public byte GenerateLODs; @@ -337,6 +342,10 @@ namespace FlaxEditor.Content.Import public byte ImportMaterials; public byte ImportTextures; public byte RestoreMaterialsOnReimport; + + // Splitting + public byte SplitObjects; + public int ObjectIndex; } internal void ToInternal(out InternalOptions options) @@ -368,7 +377,6 @@ namespace FlaxEditor.Content.Import OptimizeKeyframes = (byte)(OptimizeKeyframes ? 1 : 0), EnableRootMotion = (byte)(EnableRootMotion ? 1 : 0), RootNodeName = RootNodeName, - AnimationIndex = AnimationIndex, GenerateLODs = (byte)(GenerateLODs ? 1 : 0), BaseLOD = BaseLOD, LODCount = LODCount, @@ -376,6 +384,8 @@ namespace FlaxEditor.Content.Import ImportMaterials = (byte)(ImportMaterials ? 1 : 0), ImportTextures = (byte)(ImportTextures ? 1 : 0), RestoreMaterialsOnReimport = (byte)(RestoreMaterialsOnReimport ? 1 : 0), + SplitObjects = (byte)(SplitObjects ? 1 : 0), + ObjectIndex = ObjectIndex, }; } @@ -405,7 +415,6 @@ namespace FlaxEditor.Content.Import OptimizeKeyframes = options.OptimizeKeyframes != 0; EnableRootMotion = options.EnableRootMotion != 0; RootNodeName = options.RootNodeName; - AnimationIndex = options.AnimationIndex; GenerateLODs = options.GenerateLODs != 0; BaseLOD = options.BaseLOD; LODCount = options.LODCount; @@ -413,6 +422,8 @@ namespace FlaxEditor.Content.Import ImportMaterials = options.ImportMaterials != 0; ImportTextures = options.ImportTextures != 0; RestoreMaterialsOnReimport = options.RestoreMaterialsOnReimport != 0; + SplitObjects = options.SplitObjects != 0; + ObjectIndex = options.ObjectIndex; } /// diff --git a/Source/Editor/Managed/ManagedEditor.Internal.cpp b/Source/Editor/Managed/ManagedEditor.Internal.cpp index abe5ace18..efdb20c0b 100644 --- a/Source/Editor/Managed/ManagedEditor.Internal.cpp +++ b/Source/Editor/Managed/ManagedEditor.Internal.cpp @@ -183,7 +183,6 @@ struct InternalModelOptions byte OptimizeKeyframes; byte EnableRootMotion; MonoString* RootNodeName; - int32 AnimationIndex; // Level Of Detail byte GenerateLODs; @@ -196,6 +195,10 @@ struct InternalModelOptions byte ImportTextures; byte RestoreMaterialsOnReimport; + // Splitting + byte SplitObjects; + int32 ObjectIndex; + static void Convert(InternalModelOptions* from, ImportModelFile::Options* to) { to->Type = from->Type; @@ -223,7 +226,6 @@ struct InternalModelOptions to->OptimizeKeyframes = from->OptimizeKeyframes; to->EnableRootMotion = from->EnableRootMotion; to->RootNodeName = MUtils::ToString(from->RootNodeName); - to->AnimationIndex = from->AnimationIndex; to->GenerateLODs = from->GenerateLODs; to->BaseLOD = from->BaseLOD; to->LODCount = from->LODCount; @@ -231,6 +233,8 @@ struct InternalModelOptions to->ImportMaterials = from->ImportMaterials; to->ImportTextures = from->ImportTextures; to->RestoreMaterialsOnReimport = from->RestoreMaterialsOnReimport; + to->SplitObjects = from->SplitObjects; + to->ObjectIndex = from->ObjectIndex; } static void Convert(ImportModelFile::Options* from, InternalModelOptions* to) @@ -260,7 +264,6 @@ struct InternalModelOptions to->OptimizeKeyframes = from->OptimizeKeyframes; to->EnableRootMotion = from->EnableRootMotion; to->RootNodeName = MUtils::ToString(from->RootNodeName); - to->AnimationIndex = from->AnimationIndex; to->GenerateLODs = from->GenerateLODs; to->BaseLOD = from->BaseLOD; to->LODCount = from->LODCount; @@ -268,6 +271,8 @@ struct InternalModelOptions to->ImportMaterials = from->ImportMaterials; to->ImportTextures = from->ImportTextures; to->RestoreMaterialsOnReimport = from->RestoreMaterialsOnReimport; + to->SplitObjects = from->SplitObjects; + to->ObjectIndex = from->ObjectIndex; } }; diff --git a/Source/Engine/ContentImporters/ImportModelFile.cpp b/Source/Engine/ContentImporters/ImportModelFile.cpp index 32fd38e4d..53dcc4731 100644 --- a/Source/Engine/ContentImporters/ImportModelFile.cpp +++ b/Source/Engine/ContentImporters/ImportModelFile.cpp @@ -14,6 +14,7 @@ #include "Engine/Content/Assets/Animation.h" #include "Engine/Content/Content.h" #include "Engine/Platform/FileSystem.h" +#include "AssetsImportingManager.h" bool ImportModelFile::TryGetImportOptions(String path, Options& options) { @@ -111,6 +112,19 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) LOG(Warning, "Missing model import options. Using default values."); } } + if (options.SplitObjects) + { + options.OnSplitImport.Bind([&context](Options& splitOptions, const String& objectName) + { + // Recursive importing of the split object + String postFix = objectName; + const int32 splitPos = postFix.FindLast(TEXT('|')); + if (splitPos != -1) + postFix = postFix.Substring(splitPos + 1); + const String outputPath = String(StringUtils::GetPathWithoutExtension(context.TargetAssetPath)) + TEXT(" ") + postFix + TEXT(".flax"); + return AssetsImportingManager::Import(context.InputPath, outputPath, &splitOptions); + }); + } // Import model file ModelData modelData; @@ -149,7 +163,6 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) return result; #if IMPORT_MODEL_CACHE_OPTIONS - // Create json with import context rapidjson_flax::StringBuffer importOptionsMetaBuffer; importOptionsMetaBuffer.Reserve(256); @@ -162,7 +175,6 @@ CreateAssetResult ImportModelFile::Import(CreateAssetContext& context) } importOptionsMeta.EndObject(); context.Data.Metadata.Copy((const byte*)importOptionsMetaBuffer.GetString(), (uint32)importOptionsMetaBuffer.GetSize()); - #endif return CreateAssetResult::Ok; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp index 04101bc0b..c63d9ca93 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.Assimp.cpp @@ -4,6 +4,7 @@ #include "ModelTool.h" #include "Engine/Core/Log.h" +#include "Engine/Core/DeleteMe.h" #include "Engine/Core/Math/Matrix.h" #include "Engine/Platform/FileSystem.h" #include "Engine/Tools/TextureTool/TextureTool.h" @@ -38,8 +39,6 @@ public: DefaultLogger::kill(); } -public: - void write(const char* message) override { String s(message); @@ -139,22 +138,19 @@ struct AssimpBone struct AssimpImporterData { - ImportedModelData& Model; + Importer AssimpImporter; + AssimpLogStream AssimpLogStream; const String Path; - const aiScene* Scene; + const aiScene* Scene = nullptr; const ModelTool::Options& Options; Array Nodes; Array Bones; Dictionary> MeshIndexToNodeIndex; - AssimpImporterData(const char* path, ImportedModelData& model, const ModelTool::Options& options, const aiScene* scene) - : Model(model) - , Path(path) - , Scene(scene) + AssimpImporterData(const char* path, const ModelTool::Options& options) + : Path(path) , Options(options) - , Nodes(static_cast(scene->mNumMeshes * 4.0f)) - , MeshIndexToNodeIndex(static_cast(scene->mNumMeshes * 8.0f)) { } @@ -231,7 +227,7 @@ void ProcessNodes(AssimpImporterData& data, aiNode* aNode, int32 parentIndex) } } -bool ProcessMesh(AssimpImporterData& data, const aiMesh* aMesh, MeshData& mesh, String& errorMsg) +bool ProcessMesh(ImportedModelData& result, AssimpImporterData& data, const aiMesh* aMesh, MeshData& mesh, String& errorMsg) { // Properties mesh.Name = aMesh->mName.C_Str(); @@ -330,7 +326,7 @@ bool ProcessMesh(AssimpImporterData& data, const aiMesh* aMesh, MeshData& mesh, } else { - LOG(Warning, "Cannot import model lightmap uvs. Missing texcoords channel {0}.", inputChannelIndex); + LOG(Warning, "Cannot import result lightmap uvs. Missing texcoords channel {0}.", inputChannelIndex); } } @@ -347,7 +343,7 @@ bool ProcessMesh(AssimpImporterData& data, const aiMesh* aMesh, MeshData& mesh, } // Blend Indices and Blend Weights - if (aMesh->mNumBones > 0 && aMesh->mBones && data.Model.Types & ImportDataTypes::Skeleton) + if (aMesh->mNumBones > 0 && aMesh->mBones && result.Types & ImportDataTypes::Skeleton) { const int32 vertexCount = mesh.Positions.Count(); mesh.BlendIndices.Resize(vertexCount); @@ -428,7 +424,7 @@ bool ProcessMesh(AssimpImporterData& data, const aiMesh* aMesh, MeshData& mesh, } // Blend Shapes - if (aMesh->mNumAnimMeshes > 0 && data.Model.Types & ImportDataTypes::Skeleton && data.Options.ImportBlendShapes) + if (aMesh->mNumAnimMeshes > 0 && result.Types & ImportDataTypes::Skeleton && data.Options.ImportBlendShapes) { mesh.BlendShapes.EnsureCapacity(aMesh->mNumAnimMeshes); for (unsigned int animMeshIndex = 0; animMeshIndex < aMesh->mNumAnimMeshes; animMeshIndex++) @@ -473,7 +469,7 @@ bool ProcessMesh(AssimpImporterData& data, const aiMesh* aMesh, MeshData& mesh, return false; } -bool ImportTexture(AssimpImporterData& data, aiString& aFilename, int32& textureIndex, TextureEntry::TypeHint type) +bool ImportTexture(ImportedModelData& result, AssimpImporterData& data, aiString& aFilename, int32& textureIndex, TextureEntry::TypeHint type) { // Find texture file path const String filename = String(aFilename.C_Str()).TrimTrailing(); @@ -483,35 +479,35 @@ bool ImportTexture(AssimpImporterData& data, aiString& aFilename, int32& texture // Check if already used textureIndex = 0; - while (textureIndex < data.Model.Textures.Count()) + while (textureIndex < result.Textures.Count()) { - if (data.Model.Textures[textureIndex].FilePath == path) + if (result.Textures[textureIndex].FilePath == path) return true; textureIndex++; } // Import texture - auto& texture = data.Model.Textures.AddOne(); + auto& texture = result.Textures.AddOne(); texture.FilePath = path; texture.Type = type; texture.AssetID = Guid::Empty; return true; } -bool ImportMaterialTexture(AssimpImporterData& data, const aiMaterial* aMaterial, aiTextureType aTextureType, int32& textureIndex, TextureEntry::TypeHint type) +bool ImportMaterialTexture(ImportedModelData& result, AssimpImporterData& data, const aiMaterial* aMaterial, aiTextureType aTextureType, int32& textureIndex, TextureEntry::TypeHint type) { aiString aFilename; return aMaterial->GetTexture(aTextureType, 0, &aFilename, nullptr, nullptr, nullptr, nullptr) == AI_SUCCESS && - ImportTexture(data, aFilename, textureIndex, type); + ImportTexture(result, data, aFilename, textureIndex, type); } -bool ImportMaterials(AssimpImporterData& data, String& errorMsg) +bool ImportMaterials(ImportedModelData& result, AssimpImporterData& data, String& errorMsg) { - const uint32 materialsCount = (uint32)data.Scene->mNumMaterials; - data.Model.Materials.Resize(materialsCount, false); + const uint32 materialsCount = data.Scene->mNumMaterials; + result.Materials.Resize(materialsCount, false); for (uint32 i = 0; i < materialsCount; i++) { - auto& materialSlot = data.Model.Materials[i]; + auto& materialSlot = result.Materials[i]; const aiMaterial* aMaterial = data.Scene->mMaterials[i]; aiString aName; @@ -519,7 +515,7 @@ bool ImportMaterials(AssimpImporterData& data, String& errorMsg) materialSlot.Name = String(aName.C_Str()).TrimTrailing(); materialSlot.AssetID = Guid::Empty; - if (data.Model.Types & ImportDataTypes::Materials) + if (result.Types & ImportDataTypes::Materials) { aiColor3D aColor; if (aMaterial->Get(AI_MATKEY_COLOR_DIFFUSE, aColor) == AI_SUCCESS) @@ -531,19 +527,19 @@ bool ImportMaterials(AssimpImporterData& data, String& errorMsg) if (aMaterial->Get(AI_MATKEY_OPACITY, aFloat) == AI_SUCCESS) materialSlot.Opacity.Value = aFloat; - if (data.Model.Types & ImportDataTypes::Textures) + if (result.Types & ImportDataTypes::Textures) { - ImportMaterialTexture(data, aMaterial, aiTextureType_DIFFUSE, materialSlot.Diffuse.TextureIndex, TextureEntry::TypeHint::ColorRGB); - ImportMaterialTexture(data, aMaterial, aiTextureType_EMISSIVE, materialSlot.Emissive.TextureIndex, TextureEntry::TypeHint::ColorRGB); - ImportMaterialTexture(data, aMaterial, aiTextureType_NORMALS, materialSlot.Normals.TextureIndex, TextureEntry::TypeHint::Normals); - ImportMaterialTexture(data, aMaterial, aiTextureType_OPACITY, materialSlot.Opacity.TextureIndex, TextureEntry::TypeHint::ColorRGBA); + ImportMaterialTexture(result, data, aMaterial, aiTextureType_DIFFUSE, materialSlot.Diffuse.TextureIndex, TextureEntry::TypeHint::ColorRGB); + ImportMaterialTexture(result, data, aMaterial, aiTextureType_EMISSIVE, materialSlot.Emissive.TextureIndex, TextureEntry::TypeHint::ColorRGB); + ImportMaterialTexture(result, data, aMaterial, aiTextureType_NORMALS, materialSlot.Normals.TextureIndex, TextureEntry::TypeHint::Normals); + ImportMaterialTexture(result, data, aMaterial, aiTextureType_OPACITY, materialSlot.Opacity.TextureIndex, TextureEntry::TypeHint::ColorRGBA); if (materialSlot.Diffuse.TextureIndex != -1) { // Detect using alpha mask in diffuse texture - materialSlot.Diffuse.HasAlphaMask = TextureTool::HasAlpha(data.Model.Textures[materialSlot.Diffuse.TextureIndex].FilePath); + materialSlot.Diffuse.HasAlphaMask = TextureTool::HasAlpha(result.Textures[materialSlot.Diffuse.TextureIndex].FilePath); if (materialSlot.Diffuse.HasAlphaMask) - data.Model.Textures[materialSlot.Diffuse.TextureIndex].Type = TextureEntry::TypeHint::ColorRGBA; + result.Textures[materialSlot.Diffuse.TextureIndex].Type = TextureEntry::TypeHint::ColorRGBA; } } } @@ -552,46 +548,42 @@ bool ImportMaterials(AssimpImporterData& data, String& errorMsg) return false; } -bool ImportMeshes(AssimpImporterData& data, String& errorMsg) +bool ImportMesh(int32 i, ImportedModelData& result, AssimpImporterData& data, String& errorMsg) { - for (unsigned i = 0; i < data.Scene->mNumMeshes; i++) + const auto aMesh = data.Scene->mMeshes[i]; + + // Skip invalid meshes + if (aMesh->mPrimitiveTypes != aiPrimitiveType_TRIANGLE || aMesh->mNumVertices == 0 || aMesh->mNumFaces == 0 || aMesh->mFaces[0].mNumIndices != 3) + return false; + + // Skip unused meshes + if (!data.MeshIndexToNodeIndex.ContainsKey(i)) + 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 aMesh = data.Scene->mMeshes[i]; + const auto nodeIndex = nodesWithMesh[j]; + auto& node = data.Nodes[nodeIndex]; + const int32 lodIndex = node.LodIndex; - // Skip invalid meshes - if (aMesh->mPrimitiveTypes != aiPrimitiveType_TRIANGLE || aMesh->mNumVertices == 0 || aMesh->mNumFaces == 0 || aMesh->mFaces[0].mNumIndices != 3) - continue; - - // Skip unused meshes - if (!data.MeshIndexToNodeIndex.ContainsKey(i)) - continue; - - // Import mesh data - MeshData* meshData = New(); - if (ProcessMesh(data, aMesh, *meshData, errorMsg)) - return true; - - auto& nodesWithMesh = data.MeshIndexToNodeIndex[i]; - for (int32 j = 0; j < nodesWithMesh.Count(); j++) + // The first mesh instance uses meshData directly while others have to clone it + if (j != 0) { - const auto nodeIndex = nodesWithMesh[j]; - 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) - { - meshData = New(*meshData); - } - - // Link mesh - meshData->NodeIndex = nodeIndex; - if (data.Model.LODs.Count() <= lodIndex) - data.Model.LODs.Resize(lodIndex + 1); - data.Model.LODs[lodIndex].Meshes.Add(meshData); + meshData = New(*meshData); } - } + // Link mesh + meshData->NodeIndex = nodeIndex; + if (result.LODs.Count() <= lodIndex) + result.LODs.Resize(lodIndex + 1); + result.LODs[lodIndex].Meshes.Add(meshData); + } return false; } @@ -629,125 +621,181 @@ void ImportCurve(aiQuatKey* keys, uint32 keysCount, LinearCurve& cur } } -static bool AssimpInited = false; - -bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, const Options& options, String& errorMsg) +bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, Options& options, String& errorMsg) { - // Prepare - if (!AssimpInited) + auto context = (AssimpImporterData*)options.SplitContext; + if (!context) { - AssimpInited = true; + static bool AssimpInited = false; + if (!AssimpInited) + { + AssimpInited = true; + LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision()); + } + bool importMeshes = (data.Types & ImportDataTypes::Geometry) != 0; + bool importAnimations = (data.Types & ImportDataTypes::Animations) != 0; + context = New(path, options); - // Log Assimp version - LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision()); + // Setup import flags + unsigned int flags = + aiProcess_JoinIdenticalVertices | + aiProcess_LimitBoneWeights | + aiProcess_Triangulate | + 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; + } + + // Process imported scene nodes + ProcessNodes(*context, context->Scene->mRootNode, -1); } - Importer importer; - AssimpLogStream assimpLogStream; - bool importMeshes = (data.Types & ImportDataTypes::Geometry) != 0; - bool importAnimations = (data.Types & ImportDataTypes::Animations) != 0; - - // Setup import flags - unsigned int flags = - aiProcess_JoinIdenticalVertices | - aiProcess_LimitBoneWeights | - aiProcess_Triangulate | - 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 - importer.SetPropertyFloat(AI_CONFIG_PP_GSN_MAX_SMOOTHING_ANGLE, options.SmoothingNormalsAngle); - importer.SetPropertyFloat(AI_CONFIG_PP_CT_MAX_SMOOTHING_ANGLE, options.SmoothingTangentsAngle); - //importer.SetPropertyInteger(AI_CONFIG_PP_SLM_TRIANGLE_LIMIT, MAX_uint16); - importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false); - importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false); - importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, false); - importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, importAnimations); - //importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); // TODO: optimize pivots when https://github.com/assimp/assimp/issues/1068 gets fixed - importer.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true); - - // Import file - const auto scene = importer.ReadFile(path, flags); - if (scene == nullptr) - { - LOG_STR(Warning, String(importer.GetErrorString())); - LOG_STR(Warning, String(path)); - LOG_STR(Warning, StringUtils::ToString(flags)); - errorMsg = importer.GetErrorString(); - return true; - } - - // Process imported scene nodes - AssimpImporterData assimpData(path, data, options, scene); - ProcessNodes(assimpData, scene->mRootNode, -1); + DeleteMe contextCleanup(options.SplitContext ? nullptr : context); // Import materials - if (ImportMaterials(assimpData, errorMsg)) + if (ImportMaterials(data, *context, errorMsg)) { LOG(Warning, "Failed to import materials."); return true; } // Import geometry - if (data.Types & ImportDataTypes::Geometry) + if (data.Types & ImportDataTypes::Geometry && context->Scene->HasMeshes()) { - if (ImportMeshes(assimpData, errorMsg)) + const int meshCount = context->Scene->mNumMeshes; + if (options.SplitObjects && options.ObjectIndex == -1) { - LOG(Warning, "Failed to import meshes."); - return true; + // Import the first object within this call + options.SplitObjects = false; + options.ObjectIndex = 0; + + if (meshCount > 1 && 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)) + return true; + } + else + { + // Import all meshes + for (int32 meshIndex = 0; meshIndex < meshCount; meshIndex++) + { + if (ImportMesh(meshIndex, data, *context, errorMsg)) + return true; + } } } // Import skeleton if (data.Types & ImportDataTypes::Skeleton) { - data.Skeleton.Nodes.Resize(assimpData.Nodes.Count(), false); - for (int32 i = 0; i < assimpData.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 = assimpData.Nodes[i]; + auto& aNode = context->Nodes[i]; node.Name = aNode.Name; node.ParentIndex = aNode.ParentIndex; node.LocalTransform = aNode.LocalTransform; } - data.Skeleton.Bones.Resize(assimpData.Bones.Count(), false); - for (int32 i = 0; i < assimpData.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 = assimpData.Bones[i]; + auto& aBone = context->Bones[i]; const auto boneNodeIndex = aBone.NodeIndex; - const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : assimpData.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(assimpData.Nodes, parentBoneNodeIndex, boneNodeIndex); + bone.LocalTransform = CombineTransformsFromNodeIndices(context->Nodes, parentBoneNodeIndex, boneNodeIndex); bone.OffsetMatrix = aBone.OffsetMatrix; } } // Import animations - if (data.Types & ImportDataTypes::Animations) + if (data.Types & ImportDataTypes::Animations && context->Scene->HasAnimations()) { - if (scene->HasAnimations()) + const int32 animCount = (int32)context->Scene->mNumAnimations; + if (options.SplitObjects && options.ObjectIndex == -1) { - const auto animIndex = Math::Clamp(options.AnimationIndex, 0, scene->mNumAnimations - 1); - const auto animations = scene->mAnimations[animIndex]; + // Import the first object within this call + options.SplitObjects = false; + options.ObjectIndex = 0; + + if (animCount > 1 && 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 != 0.0 ? animations->mTicksPerSecond : 25.0; @@ -764,20 +812,16 @@ bool ModelTool::ImportDataAssimp(const char* path, ImportedModelData& data, cons ImportCurve(aAnim->mScalingKeys, aAnim->mNumScalingKeys, anim.Scale); } } - else - { - LOG(Warning, "Loaded scene has no animations"); - } } // Import nodes if (data.Types & ImportDataTypes::Nodes) { - data.Nodes.Resize(assimpData.Nodes.Count()); - for (int32 i = 0; i < assimpData.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 = assimpData.Nodes[i]; + auto& aNode = context->Nodes[i]; node.Name = aNode.Name; node.ParentIndex = aNode.ParentIndex; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.AutodeskFbxSdk.cpp b/Source/Engine/Tools/ModelTool/ModelTool.AutodeskFbxSdk.cpp index 8cb3cb2b1..a81054baa 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.AutodeskFbxSdk.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.AutodeskFbxSdk.cpp @@ -821,7 +821,7 @@ void BakeTransforms(FbxScene* scene) scene->GetRootNode()->ConvertPivotAnimationRecursive(nullptr, FbxNode::eDestinationPivot, frameRate, false); } -bool ModelTool::ImportDataAutodeskFbxSdk(const char* path, ImportedModelData& data, const Options& options, String& errorMsg) +bool ModelTool::ImportDataAutodeskFbxSdk(const char* path, ImportedModelData& data, Options& options, String& errorMsg) { ScopeLock lock(FbxSdkManager::Locker); diff --git a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp index b6b55d951..b31f78f1a 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.OpenFBX.cpp @@ -4,6 +4,7 @@ #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" @@ -78,8 +79,8 @@ struct FbxBone struct OpenFbxImporterData { - ImportedModelData& Model; const ofbx::IScene* Scene; + std::unique_ptr ScenePtr; const String Path; const ModelTool::Options& Options; @@ -94,10 +95,11 @@ struct OpenFbxImporterData Array Nodes; Array Bones; Array Materials; + Array ImportedMaterials; - OpenFbxImporterData(const char* path, ImportedModelData& model, const ModelTool::Options& options, const ofbx::IScene* scene) - : Model(model) - , Scene(scene) + OpenFbxImporterData(const char* path, const ModelTool::Options& options, ofbx::IScene* scene) + : Scene(scene) + , ScenePtr(scene) , Path(path) , Options(options) , GlobalSettings(*scene->getGlobalSettings()) @@ -170,7 +172,7 @@ struct OpenFbxImporterData } } - bool ImportMaterialTexture(const ofbx::Material* mat, ofbx::Texture::TextureType textureType, int32& textureIndex, TextureEntry::TypeHint type) const + bool ImportMaterialTexture(ImportedModelData& result, const ofbx::Material* mat, ofbx::Texture::TextureType textureType, int32& textureIndex, TextureEntry::TypeHint type) const { const ofbx::Texture* tex = mat->getTexture(textureType); if (tex) @@ -188,15 +190,15 @@ struct OpenFbxImporterData // Check if already used textureIndex = 0; - while (textureIndex < Model.Textures.Count()) + while (textureIndex < result.Textures.Count()) { - if (Model.Textures[textureIndex].FilePath == path) + if (result.Textures[textureIndex].FilePath == path) return true; textureIndex++; } // Import texture - auto& texture = Model.Textures.AddOne(); + auto& texture = result.Textures.AddOne(); texture.FilePath = path; texture.Type = type; texture.AssetID = Guid::Empty; @@ -205,39 +207,46 @@ struct OpenFbxImporterData return false; } - int32 AddMaterial(const ofbx::Material* mat) + int32 AddMaterial(ImportedModelData& result, const ofbx::Material* mat) { int32 index = Materials.Find(mat); - if (index == INVALID_INDEX) + if (index == -1) { index = Materials.Count(); Materials.Add(mat); - auto& material = Model.Materials.AddOne(); + auto& material = ImportedMaterials.AddOne(); material.AssetID = Guid::Empty; if (mat) material.Name = String(mat->name).TrimTrailing(); - if (mat && Model.Types & ImportDataTypes::Materials) + if (mat && result.Types & ImportDataTypes::Materials) { material.Diffuse.Color = ToColor(mat->getDiffuseColor()); - if (Model.Types & ImportDataTypes::Textures) + if (result.Types & ImportDataTypes::Textures) { - ImportMaterialTexture(mat, ofbx::Texture::DIFFUSE, material.Diffuse.TextureIndex, TextureEntry::TypeHint::ColorRGB); - ImportMaterialTexture(mat, ofbx::Texture::EMISSIVE, material.Emissive.TextureIndex, TextureEntry::TypeHint::ColorRGB); - ImportMaterialTexture(mat, ofbx::Texture::NORMAL, material.Normals.TextureIndex, TextureEntry::TypeHint::Normals); + ImportMaterialTexture(result, mat, ofbx::Texture::DIFFUSE, material.Diffuse.TextureIndex, TextureEntry::TypeHint::ColorRGB); + ImportMaterialTexture(result, mat, ofbx::Texture::EMISSIVE, material.Emissive.TextureIndex, TextureEntry::TypeHint::ColorRGB); + ImportMaterialTexture(result, mat, ofbx::Texture::NORMAL, material.Normals.TextureIndex, TextureEntry::TypeHint::Normals); if (material.Diffuse.TextureIndex != -1) { // Detect using alpha mask in diffuse texture - material.Diffuse.HasAlphaMask = TextureTool::HasAlpha(Model.Textures[material.Diffuse.TextureIndex].FilePath); + material.Diffuse.HasAlphaMask = TextureTool::HasAlpha(result.Textures[material.Diffuse.TextureIndex].FilePath); if (material.Diffuse.HasAlphaMask) - Model.Textures[material.Diffuse.TextureIndex].Type = TextureEntry::TypeHint::ColorRGBA; + result.Textures[material.Diffuse.TextureIndex].Type = TextureEntry::TypeHint::ColorRGBA; } } } } - return index; + const auto& importedMaterial = ImportedMaterials[index]; + for (int32 i = 0; i < result.Materials.Count(); i++) + { + if (result.Materials[i].Name == importedMaterial.Name) + return i; + } + result.Materials.Add(importedMaterial); + return result.Materials.Count() - 1; } int32 FindNode(const ofbx::Object* link) @@ -450,7 +459,7 @@ bool ImportBones(OpenFbxImporterData& data, String& errorMsg) return false; } -bool ProcessMesh(OpenFbxImporterData& data, const ofbx::Mesh* aMesh, MeshData& mesh, String& errorMsg, int32 triangleStart, int32 triangleEnd) +bool ProcessMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* aMesh, MeshData& mesh, String& errorMsg, int32 triangleStart, int32 triangleEnd) { // Prepare const int32 firstVertexOffset = triangleStart * 3; @@ -476,7 +485,7 @@ bool ProcessMesh(OpenFbxImporterData& data, const ofbx::Mesh* aMesh, MeshData& m else aMaterial = aMesh->getMaterial(0); } - mesh.MaterialSlotIndex = data.AddMaterial(aMaterial); + mesh.MaterialSlotIndex = data.AddMaterial(result, aMaterial); // Vertex positions mesh.Positions.Resize(vertexCount, false); @@ -632,7 +641,7 @@ bool ProcessMesh(OpenFbxImporterData& data, const ofbx::Mesh* aMesh, MeshData& m } // Blend Indices and Blend Weights - if (skin && skin->getClusterCount() > 0 && data.Model.Types & ImportDataTypes::Skeleton) + if (skin && skin->getClusterCount() > 0 && result.Types & ImportDataTypes::Skeleton) { mesh.BlendIndices.Resize(vertexCount); mesh.BlendWeights.Resize(vertexCount); @@ -698,7 +707,7 @@ bool ProcessMesh(OpenFbxImporterData& data, const ofbx::Mesh* aMesh, MeshData& m } // Blend Shapes - if (blendShape && blendShape->getBlendShapeChannelCount() > 0 && data.Model.Types & ImportDataTypes::Skeleton && data.Options.ImportBlendShapes) + if (blendShape && blendShape->getBlendShapeChannelCount() > 0 && result.Types & ImportDataTypes::Skeleton && data.Options.ImportBlendShapes) { mesh.BlendShapes.EnsureCapacity(blendShape->getBlendShapeChannelCount()); for (int32 channelIndex = 0; channelIndex < blendShape->getBlendShapeChannelCount(); channelIndex++) @@ -799,7 +808,7 @@ bool ProcessMesh(OpenFbxImporterData& data, const ofbx::Mesh* aMesh, MeshData& m return false; } -bool ImportMesh(OpenFbxImporterData& data, const ofbx::Mesh* aMesh, String& errorMsg, int32 triangleStart, int32 triangleEnd) +bool ImportMesh(ImportedModelData& result, OpenFbxImporterData& data, const ofbx::Mesh* aMesh, String& errorMsg, int32 triangleStart, int32 triangleEnd) { // Find the parent node int32 nodeIndex = data.FindNode(aMesh); @@ -840,62 +849,57 @@ bool ImportMesh(OpenFbxImporterData& data, const ofbx::Mesh* aMesh, String& erro // Import mesh data MeshData* meshData = New(); - if (ProcessMesh(data, aMesh, *meshData, errorMsg, triangleStart, triangleEnd)) + if (ProcessMesh(result, data, aMesh, *meshData, errorMsg, triangleStart, triangleEnd)) return true; // Link mesh auto& node = data.Nodes[nodeIndex]; const int32 lodIndex = node.LodIndex; meshData->NodeIndex = nodeIndex; - if (data.Model.LODs.Count() <= lodIndex) - data.Model.LODs.Resize(lodIndex + 1); - data.Model.LODs[lodIndex].Meshes.Add(meshData); + if (result.LODs.Count() <= lodIndex) + result.LODs.Resize(lodIndex + 1); + result.LODs[lodIndex].Meshes.Add(meshData); return false; } -bool ImportMeshes(OpenFbxImporterData& data, String& errorMsg) +bool ImportMesh(int32 index, ImportedModelData& result, OpenFbxImporterData& data, String& errorMsg) { - const int meshCount = data.Scene->getMeshCount(); - for (int i = 0; i < meshCount; i++) + const auto aMesh = data.Scene->getMesh(index); + const auto aGeometry = aMesh->getGeometry(); + const auto trianglesCount = aGeometry->getVertexCount() / 3; + + // Skip invalid meshes + if (IsMeshInvalid(aMesh)) + return false; + + if (aMesh->getMaterialCount() < 2 || !aGeometry->getMaterials()) { - const auto aMesh = data.Scene->getMesh(i); - const auto aGeometry = aMesh->getGeometry(); - const auto trianglesCount = aGeometry->getVertexCount() / 3; - - // Skip invalid meshes - if (IsMeshInvalid(aMesh)) - continue; - - if (aMesh->getMaterialCount() < 2 || !aGeometry->getMaterials()) - { - // Fast path if mesh is using single material for all triangles - if (ImportMesh(data, aMesh, errorMsg, 0, trianglesCount - 1)) - return true; - } - else - { - // Create mesh for each sequence of triangles that share the same material - const auto materials = aGeometry->getMaterials(); - int32 rangeStart = 0; - int32 rangeStartVal = materials[rangeStart]; - for (int32 triangleIndex = 1; triangleIndex < trianglesCount; triangleIndex++) - { - if (rangeStartVal != materials[triangleIndex]) - { - if (ImportMesh(data, aMesh, errorMsg, rangeStart, triangleIndex - 1)) - return true; - - // Start a new range - rangeStart = triangleIndex; - rangeStartVal = materials[triangleIndex]; - } - } - if (ImportMesh(data, aMesh, errorMsg, rangeStart, trianglesCount - 1)) - return true; - } + // Fast path if mesh is using single material for all triangles + if (ImportMesh(result, data, aMesh, errorMsg, 0, trianglesCount - 1)) + return true; } + else + { + // Create mesh for each sequence of triangles that share the same material + const auto materials = aGeometry->getMaterials(); + int32 rangeStart = 0; + int32 rangeStartVal = materials[rangeStart]; + for (int32 triangleIndex = 1; triangleIndex < trianglesCount; triangleIndex++) + { + if (rangeStartVal != materials[triangleIndex]) + { + if (ImportMesh(result, data, aMesh, errorMsg, rangeStart, triangleIndex - 1)) + return true; + // Start a new range + rangeStart = triangleIndex; + rangeStartVal = materials[triangleIndex]; + } + } + if (ImportMesh(result, data, aMesh, errorMsg, rangeStart, trianglesCount - 1)) + return true; + } return false; } @@ -962,11 +966,11 @@ void ImportCurve(const ofbx::AnimationCurveNode* curveNode, LinearCurve& curv } } -bool ImportAnimation(int32 index, ofbx::IScene* scene, ImportedModelData& data, OpenFbxImporterData& importerData) +bool ImportAnimation(int32 index, ImportedModelData& data, OpenFbxImporterData& importerData) { - const ofbx::AnimationStack* stack = scene->getAnimationStack(index); + const ofbx::AnimationStack* stack = importerData.Scene->getAnimationStack(index); const ofbx::AnimationLayer* layer = stack->getLayer(0); - const ofbx::TakeInfo* takeInfo = scene->getTakeInfo(stack->name); + const ofbx::TakeInfo* takeInfo = importerData.Scene->getTakeInfo(stack->name); if (takeInfo == nullptr) return true; @@ -1019,6 +1023,27 @@ bool ImportAnimation(int32 index, ofbx::IScene* scene, ImportedModelData& data, //ImportCurve(scalingNode, anim.Scale, info, ExtractKeyframeScale); } + if (importerData.ConvertRH) + { + for (int32 i = 0; i < data.Animation.Channels.Count(); i++) + { + auto& anim = data.Animation.Channels[i]; + auto& posKeys = anim.Position.GetKeyframes(); + auto& rotKeys = anim.Rotation.GetKeyframes(); + + for (int32 k = 0; k < posKeys.Count(); k++) + { + posKeys[k].Value.Z *= -1.0f; + } + + for (int32 k = 0; k < rotKeys.Count(); k++) + { + rotKeys[k].Value.X *= -1.0f; + rotKeys[k].Value.Y *= -1.0f; + } + } + } + return false; } @@ -1037,171 +1062,208 @@ static Vector3 FbxVectorFromAxisAndSign(int axis, int sign) return { 0.f, 0.f, 0.f }; } -bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, const Options& options, String& errorMsg) +bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, Options& options, String& errorMsg) { - // Import file - Array fileData; - if (File::ReadAllBytes(String(path), fileData)) + auto context = (OpenFbxImporterData*)options.SplitContext; + if (!context) { - errorMsg = TEXT("Cannot load file."); - return true; - } - ofbx::u64 loadFlags = 0; - if (data.Types & ImportDataTypes::Geometry) - loadFlags |= (ofbx::u64)ofbx::LoadFlags::TRIANGULATE; - else - loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_GEOMETRY; - if (!options.ImportBlendShapes) - loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES; - ofbx::IScene* scene = ofbx::load(fileData.Get(), fileData.Count(), loadFlags); - if (!scene) - { - errorMsg = ofbx::getError(); - return true; - } - std::unique_ptr scenePtr(scene); - fileData.Resize(0); - - // Process imported scene - OpenFbxImporterData importerData(path, data, options, scene); - auto& globalSettings = importerData.GlobalSettings; - ProcessNodes(importerData, scene->getRoot(), -1); - - // Apply model scene global scale factor - importerData.Nodes[0].LocalTransform = Transform(Vector3::Zero, Quaternion::Identity, globalSettings.UnitScaleFactor) * importerData.Nodes[0].LocalTransform; - - // Log scene info - LOG(Info, "Loaded FBX model, Frame Rate: {0}, Unit Scale Factor: {1}", importerData.FrameRate, globalSettings.UnitScaleFactor); - 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)")); - LOG(Info, "Imported scene: Up={0}, Front={1}, Right={2}", importerData.Up, importerData.Front, importerData.Right); - - // Extract embedded textures - if (data.Types & ImportDataTypes::Textures) - { - String outputPath; - for (int i = 0, c = scene->getEmbeddedDataCount(); i < c; i++) + // Import file + Array fileData; + if (File::ReadAllBytes(String(path), fileData)) { - const ofbx::DataView aEmbedded = scene->getEmbeddedData(i); - ofbx::DataView aFilename = scene->getEmbeddedFilename(i); - char filenameData[256]; - aFilename.toString(filenameData); - if (outputPath.IsEmpty()) + errorMsg = TEXT("Cannot load file."); + return true; + } + ofbx::u64 loadFlags = 0; + if (data.Types & ImportDataTypes::Geometry) + loadFlags |= (ofbx::u64)ofbx::LoadFlags::TRIANGULATE; + else + loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_GEOMETRY; + if (!options.ImportBlendShapes) + loadFlags |= (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); + + // 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, "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)")); + LOG(Info, "Imported scene: Up={0}, Front={1}, Right={2}", context->Up, context->Front, context->Right); + + // Extract embedded textures + if (data.Types & ImportDataTypes::Textures) + { + String outputPath; + for (int i = 0, c = scene->getEmbeddedDataCount(); i < c; i++) { - String pathStr(path); - outputPath = String(StringUtils::GetDirectoryName(pathStr)) / TEXT("textures"); - FileSystem::CreateDirectory(outputPath); + 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"); + } } - 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))) + } + + // Transform nodes to match the engine coordinates system - DirectX (UpVector = +Y, FrontVector = +Z, CoordSystem = -X (LeftHanded)) + if (context->Up == Vector3(1, 0, 0) && context->Front == Vector3(0, 0, 1) && context->Right == Vector3(0, 1, 0)) + { + context->RootConvertRotation = Quaternion::Euler(0, 180, 0); + } + else if (context->Up == Vector3(0, 1, 0) && context->Front == Vector3(-1, 0, 0) && context->Right == Vector3(0, 0, 1)) + { + context->RootConvertRotation = Quaternion::Euler(90, -90, 0); + } + /*Vector3 engineUp(0, 1, 0); + Vector3 engineFront(0, 0, 1); + Vector3 engineRight(-1, 0, 0);*/ + /*Vector3 engineUp(1, 0, 0); + Vector3 engineFront(0, 0, 1); + Vector3 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); + }*/ + /*Vector3 hackUp = FbxVectorFromAxisAndSign(globalSettings.UpAxis, globalSettings.UpAxisSign); + if (hackUp == Vector3::UnitX) + context->RootConvertRotation = Quaternion::Euler(-90, 0, 0); + else if (hackUp == Vector3::UnitZ) + context->RootConvertRotation = Quaternion::Euler(90, 0, 0);*/ + if (!context->RootConvertRotation.IsIdentity()) + { + for (int32 i = 0; i < context->Nodes.Count(); i++) { - LOG(Error, "Failed to write data to file"); - } - } - } - - // Transform nodes to match the engine coordinates system - DirectX (UpVector = +Y, FrontVector = +Z, CoordSystem = -X (LeftHanded)) - if (importerData.Up == Vector3(1, 0, 0) && importerData.Front == Vector3(0, 0, 1) && importerData.Right == Vector3(0, 1, 0)) - { - importerData.RootConvertRotation = Quaternion::Euler(0, 180, 0); - } - else if (importerData.Up == Vector3(0, 1, 0) && importerData.Front == Vector3(-1, 0, 0) && importerData.Right == Vector3(0, 0, 1)) - { - importerData.RootConvertRotation = Quaternion::Euler(90, -90, 0); - } - /*Vector3 engineUp(0, 1, 0); - Vector3 engineFront(0, 0, 1); - Vector3 engineRight(-1, 0, 0);*/ - /*Vector3 engineUp(1, 0, 0); - Vector3 engineFront(0, 0, 1); - Vector3 engineRight(0, 1, 0); - if (importerData.Up != engineUp || importerData.Front != engineFront || importerData.Right != engineRight) - { - LOG(Info, "Converting imported scene nodes to match engine coordinates system"); - importerData.RootConvertRotation = Quaternion::GetRotationFromTo(importerData.Up, engineUp, engineUp); - //importerData.RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * importerData.Right, engineRight, engineRight); - //importerData.RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * importerData.Front, engineFront, engineFront); - }*/ - /*Vector3 hackUp = FbxVectorFromAxisAndSign(globalSettings.UpAxis, globalSettings.UpAxisSign); - if (hackUp == Vector3::UnitX) - importerData.RootConvertRotation = Quaternion::Euler(-90, 0, 0); - else if (hackUp == Vector3::UnitZ) - importerData.RootConvertRotation = Quaternion::Euler(90, 0, 0);*/ - if (!importerData.RootConvertRotation.IsIdentity()) - { - for (int32 i = 0; i < importerData.Nodes.Count(); i++) - { - if (importerData.Nodes[i].ParentIndex == -1) - { - importerData.Nodes[i].LocalTransform.Orientation = importerData.RootConvertRotation * importerData.Nodes[i].LocalTransform.Orientation; - break; + if (context->Nodes[i].ParentIndex == -1) + { + context->Nodes[i].LocalTransform.Orientation = context->RootConvertRotation * context->Nodes[i].LocalTransform.Orientation; + break; + } } } } + DeleteMe contextCleanup(options.SplitContext ? nullptr : context); // Build final skeleton bones hierarchy before importing meshes if (data.Types & ImportDataTypes::Skeleton) { - if (ImportBones(importerData, errorMsg)) + if (ImportBones(*context, errorMsg)) { LOG(Warning, "Failed to import skeleton bones."); return true; } - Sorting::QuickSort(importerData.Bones.Get(), importerData.Bones.Count()); + Sorting::QuickSort(context->Bones.Get(), context->Bones.Count()); } // Import geometry (meshes and materials) - if (data.Types & ImportDataTypes::Geometry) + if (data.Types & ImportDataTypes::Geometry && context->Scene->getMeshCount() > 0) { - if (ImportMeshes(importerData, errorMsg)) + const int meshCount = context->Scene->getMeshCount(); + if (options.SplitObjects && options.ObjectIndex == -1) { - LOG(Warning, "Failed to import meshes."); - return true; + // Import the first object within this call + options.SplitObjects = false; + options.ObjectIndex = 0; + + if (meshCount > 1 && 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)) + return true; + } + else + { + // Import all meshes + for (int32 meshIndex = 0; meshIndex < meshCount; meshIndex++) + { + if (ImportMesh(meshIndex, data, *context, errorMsg)) + return true; + } } } // Import skeleton if (data.Types & ImportDataTypes::Skeleton) { - data.Skeleton.Nodes.Resize(importerData.Nodes.Count(), false); - for (int32 i = 0; i < importerData.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 = importerData.Nodes[i]; + auto& aNode = context->Nodes[i]; node.Name = aNode.Name; node.ParentIndex = aNode.ParentIndex; node.LocalTransform = aNode.LocalTransform; } - data.Skeleton.Bones.Resize(importerData.Bones.Count(), false); - for (int32 i = 0; i < importerData.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 = importerData.Bones[i]; + auto& aBone = context->Bones[i]; const auto boneNodeIndex = aBone.NodeIndex; // Find the parent bone int32 parentBoneIndex = -1; - for (int32 j = importerData.Nodes[boneNodeIndex].ParentIndex; j != -1; j = importerData.Nodes[j].ParentIndex) + for (int32 j = context->Nodes[boneNodeIndex].ParentIndex; j != -1; j = context->Nodes[j].ParentIndex) { - parentBoneIndex = importerData.FindBone(j); + parentBoneIndex = context->FindBone(j); if (parentBoneIndex != -1) break; } aBone.ParentBoneIndex = parentBoneIndex; - const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : importerData.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(importerData.Nodes, parentBoneNodeIndex, boneNodeIndex); + bone.LocalTransform = CombineTransformsFromNodeIndices(context->Nodes, parentBoneNodeIndex, boneNodeIndex); bone.OffsetMatrix = aBone.OffsetMatrix; } } @@ -1209,57 +1271,54 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ImportedModelData& data, con // Import animations if (data.Types & ImportDataTypes::Animations) { - const int animCount = scene->getAnimationStackCount(); - if (options.AnimationIndex != -1) + const int animCount = context->Scene->getAnimationStackCount(); + if (options.SplitObjects && options.ObjectIndex == -1) + { + // Import the first object within this call + options.SplitObjects = false; + options.ObjectIndex = 0; + + if (animCount > 1 && 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.AnimationIndex, 0, animCount - 1); - ImportAnimation(animIndex, scene, data, importerData); + 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, scene, data, importerData)) + if (!ImportAnimation(animIndex, data, *context)) break; } } - if (data.Animation.Channels.IsEmpty()) - { - LOG(Warning, "Loaded scene has no animations"); - } - - if (importerData.ConvertRH) - { - for (int32 i = 0; i < data.Animation.Channels.Count(); i++) - { - auto& anim = data.Animation.Channels[i]; - auto& posKeys = anim.Position.GetKeyframes(); - auto& rotKeys = anim.Rotation.GetKeyframes(); - - for (int32 k = 0; k < posKeys.Count(); k++) - { - posKeys[k].Value.Z *= -1.0f; - } - - for (int32 k = 0; k < rotKeys.Count(); k++) - { - rotKeys[k].Value.X *= -1.0f; - rotKeys[k].Value.Y *= -1.0f; - } - } - } } // Import nodes if (data.Types & ImportDataTypes::Nodes) { - data.Nodes.Resize(importerData.Nodes.Count()); - for (int32 i = 0; i < importerData.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 = importerData.Nodes[i]; + auto& aNode = context->Nodes[i]; node.Name = aNode.Name; node.ParentIndex = aNode.ParentIndex; diff --git a/Source/Engine/Tools/ModelTool/ModelTool.Options.cpp b/Source/Engine/Tools/ModelTool/ModelTool.Options.cpp index 0d8fb459f..3c276a8d9 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.Options.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.Options.cpp @@ -54,7 +54,6 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(OptimizeKeyframes); SERIALIZE(EnableRootMotion); SERIALIZE(RootNodeName); - SERIALIZE(AnimationIndex); SERIALIZE(GenerateLODs); SERIALIZE(BaseLOD); SERIALIZE(LODCount); @@ -62,6 +61,8 @@ void ModelTool::Options::Serialize(SerializeStream& stream, const void* otherObj SERIALIZE(ImportMaterials); SERIALIZE(ImportTextures); SERIALIZE(RestoreMaterialsOnReimport); + SERIALIZE(SplitObjects); + SERIALIZE(ObjectIndex); } void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifier* modifier) @@ -90,7 +91,6 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(OptimizeKeyframes); DESERIALIZE(EnableRootMotion); DESERIALIZE(RootNodeName); - DESERIALIZE(AnimationIndex); DESERIALIZE(GenerateLODs); DESERIALIZE(BaseLOD); DESERIALIZE(LODCount); @@ -98,6 +98,14 @@ void ModelTool::Options::Deserialize(DeserializeStream& stream, ISerializeModifi DESERIALIZE(ImportMaterials); DESERIALIZE(ImportTextures); DESERIALIZE(RestoreMaterialsOnReimport); + DESERIALIZE(SplitObjects); + DESERIALIZE(ObjectIndex); + + // [Deprecated on 23.11.2021, expires on 21.11.2023] + int32 AnimationIndex = -1; + DESERIALIZE(AnimationIndex); + if (AnimationIndex != -1) + ObjectIndex = AnimationIndex; } #endif diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 06c5bac13..c6a6d42f5 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -26,7 +26,7 @@ void RemoveNamespace(String& name) name = name.Substring(namespaceStart + 1); } -bool ModelTool::ImportData(const String& path, ImportedModelData& data, Options options, String& errorMsg) +bool ModelTool::ImportData(const String& path, ImportedModelData& data, Options& options, String& errorMsg) { // Validate options options.Scale = Math::Clamp(options.Scale, 0.0001f, 100000.0f); @@ -313,7 +313,7 @@ void MeshOptDeallocate(void* ptr) Allocator::Free(ptr); } -bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options options, String& errorMsg, const String& autoImportOutput) +bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& options, String& errorMsg, const String& autoImportOutput) { LOG(Info, "Importing model from \'{0}\'", path); const auto startTime = DateTime::NowUTC(); diff --git a/Source/Engine/Tools/ModelTool/ModelTool.h b/Source/Engine/Tools/ModelTool/ModelTool.h index 525f8f1ba..042c0081e 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.h +++ b/Source/Engine/Tools/ModelTool/ModelTool.h @@ -193,7 +193,6 @@ public: bool OptimizeKeyframes = true; bool EnableRootMotion = false; String RootNodeName; - int32 AnimationIndex = -1; // Level Of Detail bool GenerateLODs = false; @@ -206,6 +205,14 @@ public: bool ImportTextures = true; bool RestoreMaterialsOnReimport = true; + // Splitting + bool SplitObjects = false; + int32 ObjectIndex = -1; + + // Runtime data for objects splitting during import (used internally) + void* SplitContext = nullptr; + Function OnSplitImport; + public: // [ISerializable] @@ -223,7 +230,7 @@ public: /// The import options. /// The error message container. /// True if fails, otherwise false. - static bool ImportData(const String& path, ImportedModelData& data, Options options, String& errorMsg); + static bool ImportData(const String& path, ImportedModelData& data, Options& options, String& errorMsg); /// /// Imports the model. @@ -234,7 +241,7 @@ public: /// 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& meshData, Options& options, String& errorMsg, const String& autoImportOutput = String::Empty); public: @@ -267,13 +274,13 @@ public: private: #if USE_ASSIMP - static bool ImportDataAssimp(const char* path, ImportedModelData& data, const Options& options, String& errorMsg); + static bool ImportDataAssimp(const char* path, ImportedModelData& data, Options& options, String& errorMsg); #endif #if USE_AUTODESK_FBX_SDK - static bool ImportDataAutodeskFbxSdk(const char* path, ImportedModelData& data, const Options& options, String& errorMsg); + static bool ImportDataAutodeskFbxSdk(const char* path, ImportedModelData& data, Options& options, String& errorMsg); #endif #if USE_OPEN_FBX - static bool ImportDataOpenFBX(const char* path, ImportedModelData& data, const Options& options, String& errorMsg); + static bool ImportDataOpenFBX(const char* path, ImportedModelData& data, Options& options, String& errorMsg); #endif };