// Copyright Epic Games, Inc. All Rights Reserved. #include "FbxAPI.h" #include "CoreMinimal.h" #include "FbxCamera.h" #include "FbxConvert.h" #include "FbxHelper.h" #include "FbxInclude.h" #include "FbxLight.h" #include "FbxMaterial.h" #include "FbxMesh.h" #include "FbxScene.h" #include "InterchangeHelper.h" #include "InterchangeTextureNode.h" #if WITH_ENGINE #include "Mesh/InterchangeMeshPayload.h" #endif #include "Misc/Paths.h" #include "Nodes/InterchangeBaseNodeContainer.h" #include "Nodes/InterchangeSourceNode.h" #include "Misc/SecureHash.h" #include "InterchangeCommonAnimationPayload.h" #include "Serialization/LargeMemoryWriter.h" #include "FbxAnimation.h" #define LOCTEXT_NAMESPACE "InterchangeFbxParser" #define DESTROY_FBX_OBJECT(Object) \ if(Object) \ { \ Object->Destroy(); \ Object = nullptr; \ } namespace UE { namespace Interchange { namespace Private { FFbxParser::~FFbxParser() { FbxHelper = nullptr; Reset(); } void FFbxParser::Reset() { PayloadContexts.Reset(); DESTROY_FBX_OBJECT(SDKImporter); DESTROY_FBX_OBJECT(SDKScene); if (SDKGeometryConverter) { delete SDKGeometryConverter; SDKGeometryConverter = nullptr; } DESTROY_FBX_OBJECT(SDKIoSettings); DESTROY_FBX_OBJECT(SDKManager); if (FbxHelper.IsValid()) { FbxHelper->Reset(); } } const TSharedPtr FFbxParser::GetFbxHelper() { if (!FbxHelper.IsValid()) { FbxHelper = MakeShared(bKeepFbxNamespace); } check(FbxHelper.IsValid()); return FbxHelper; } bool FFbxParser::LoadFbxFile(const FString& Filename, UInterchangeBaseNodeContainer& NodeContainer) { SourceFilename = Filename; int32 SDKMajor, SDKMinor, SDKRevision; //The first thing to do is to create the FBX Manager which is the object allocator for almost all the classes in the SDK SDKManager = FbxManager::Create(); if (!SDKManager) { UInterchangeResultError_Generic* Message = AddMessage(); Message->Text = LOCTEXT("CannotCreateFBXManager", "Cannot create FBX SDK manager."); return false; } //Create an IOSettings object. This object holds all import/export settings. SDKIoSettings = FbxIOSettings::Create(SDKManager, IOSROOT); SDKIoSettings->SetBoolProp(IMP_FBX_MATERIAL, true); SDKIoSettings->SetBoolProp(IMP_FBX_TEXTURE, true); SDKIoSettings->SetBoolProp(IMP_FBX_LINK, true); SDKIoSettings->SetBoolProp(IMP_FBX_SHAPE, true); SDKIoSettings->SetBoolProp(IMP_FBX_GOBO, true); SDKIoSettings->SetBoolProp(IMP_FBX_ANIMATION, true); SDKIoSettings->SetBoolProp(IMP_SKINS, true); SDKIoSettings->SetBoolProp(IMP_DEFORMATION, true); SDKIoSettings->SetBoolProp(IMP_FBX_GLOBAL_SETTINGS, true); SDKIoSettings->SetBoolProp(IMP_TAKE, true); SDKManager->SetIOSettings(SDKIoSettings); SDKGeometryConverter = new FbxGeometryConverter(SDKManager); //Create an FBX scene. This object holds most objects imported/exported from/to files. SDKScene = FbxScene::Create(SDKManager, "My Scene"); if (!SDKScene) { UInterchangeResultError_Generic* Message = AddMessage(); Message->Text = LOCTEXT("CannotCreateFBXScene", "Cannot create FBX SDK scene."); return false; } // Create an importer. SDKImporter = FbxImporter::Create(SDKManager, ""); // Get the version number of the FBX files generated by the // version of FBX SDK that you are using. FbxManager::GetFileFormatVersion(SDKMajor, SDKMinor, SDKRevision); // Initialize the importer by providing a filename. auto ReportOpenError = [this, &Filename]() { FFormatNamedArguments FilenameText { { TEXT("Filename"), FText::FromString(Filename) } }; UInterchangeResultError_Generic* Message = AddMessage(); Message->Text = FText::Format(LOCTEXT("CannotOpenFBXFile", "Cannot open FBX file '{Filename}'."), FilenameText); }; const bool bImportStatus = SDKImporter->Initialize(TCHAR_TO_UTF8(*Filename)); if (!bImportStatus) { ReportOpenError(); return false; } bool bStatus = SDKImporter->Import(SDKScene); if (!bStatus) { ReportOpenError(); return false; } //We always convert scene to UE axis and units FbxAMatrix AxisConversionInverseMatrix; FFbxConvert::ConvertScene(SDKScene, bConvertScene, bForceFrontXAxis, bConvertSceneUnit, FileDetails.AxisDirection, FileDetails.UnitSystem, AxisConversionInverseMatrix, JointOrientationMatrix); //Save the AxisConversionInverseTransform into InterchangeSourceNode (so that socket transport can use it accordingly). FTransform AxisConversionInverseTransform = FFbxConvert::ConvertTransform(AxisConversionInverseMatrix); UInterchangeSourceNode* SourceNode = UInterchangeSourceNode::FindOrCreateUniqueInstance(&NodeContainer); SourceNode->SetCustomAxisConversionInverseTransform(AxisConversionInverseTransform); //Store the fbx frame rate { FrameRate = FbxTime::GetFrameRate(SDKScene->GetGlobalSettings().GetTimeMode()); FileDetails.FrameRate = FString::Printf(TEXT("%.2f"), FrameRate); SourceNode->SetCustomSourceFrameRateNumerator(FrameRate); constexpr double Denominator = 1.0; SourceNode->SetCustomSourceFrameRateDenominator(Denominator); } // Fbx legacy has a special way to bake the skeletal mesh that do not fit the interchange standard // The interchange skeletal mesh factory will read this to use the proper bake transform so it match legacy behavior. // This fix the issue with blender armature bone skip SourceNode->SetCustomUseLegacySkeletalMeshBakeTransform(true); //Fbx legacy does not allow Scene Root Nodes to be part of the skeletons (to be joints). SourceNode->SetCustomAllowSceneRootAsJoint(false); // Get the version number of the FBX file format. int32 FileMajor, FileMinor, FileRevision; SDKImporter->GetFileVersion(FileMajor, FileMinor, FileRevision); FileDetails.FbxFileVersion = FString::Printf(TEXT("%d.%d.%d"), FileMajor, FileMinor, FileRevision); // Get The Creator of the FBX File. FileDetails.FbxFileCreator = UTF8_TO_TCHAR(SDKImporter->GetFileHeaderInfo()->mCreator.Buffer()); { //Example of creator file info string //Blender (stable FBX IO) - 2.78 (sub 0) - 3.7.7 //Maya and Max use the same string where they specify the fbx sdk version, so we cannot know it is coming from which software //We need blender creator when importing skeletal mesh containing the "armature" dummy node as the parent of the root joint. We want to remove this dummy "armature" node bCreatorIsBlender = FileDetails.FbxFileCreator.StartsWith(TEXT("Blender")); } FbxDocumentInfo* DocInfo = SDKImporter->GetSceneInfo(); if (DocInfo) { FString LastSavedVendor(UTF8_TO_TCHAR(DocInfo->LastSaved_ApplicationVendor.Get().Buffer())); FString LastSavedAppName(UTF8_TO_TCHAR(DocInfo->LastSaved_ApplicationName.Get().Buffer())); FString LastSavedAppVersion(UTF8_TO_TCHAR(DocInfo->LastSaved_ApplicationVersion.Get().Buffer())); FileDetails.ApplicationVendor = LastSavedVendor; FileDetails.ApplicationName = LastSavedAppName; FileDetails.ApplicationVersion = LastSavedAppVersion; } else { FileDetails.ApplicationVendor = TEXT(""); FileDetails.ApplicationName = TEXT(""); FileDetails.ApplicationVersion = TEXT(""); } return true; } void FFbxParser::FillContainerWithFbxScene(UInterchangeBaseNodeContainer& NodeContainer) { CleanupFbxData(); FFbxMaterial FbxMaterial(*this); FbxMaterial.AddAllTextures(SDKScene, NodeContainer); FbxMaterial.AddAllMaterials(SDKScene, NodeContainer); FFbxMesh FbxMesh(*this); FbxMesh.AddAllMeshes(SDKScene, SDKGeometryConverter, NodeContainer, PayloadContexts); FFbxLight FbxLight(*this); FbxLight.AddAllLights(SDKScene, NodeContainer); FFbxCamera FbxCamera(*this); FbxCamera.AddAllCameras(SDKScene, NodeContainer); FFbxScene FbxScene(*this); FbxScene.AddHierarchy(SDKScene, NodeContainer, PayloadContexts); FbxScene.AddAnimation(SDKScene, NodeContainer, PayloadContexts); FbxScene.AddMorphTargetAnimations(SDKScene, NodeContainer, PayloadContexts, FbxMesh.GetMorphTargetAnimationsBuildingData()); ProcessExtraInformation(NodeContainer); } bool FFbxParser::FetchPayloadData(const FString& PayloadKey, const FString& PayloadFilepath) { if (!PayloadContexts.Contains(PayloadKey)) { UInterchangeResultError_Generic* Message = AddMessage(); Message->Text = LOCTEXT("CannotRetrievePayload", "Cannot retrieve payload; payload key doesn't have any context."); return false; } { //Critical section to force payload to be fetch one by one with no concurrency. FScopeLock Lock(&PayloadCriticalSection); TSharedPtr& PayloadContext = PayloadContexts.FindChecked(PayloadKey); return PayloadContext->FetchPayloadToFile(*this, PayloadFilepath); } } bool FFbxParser::FetchMeshPayloadData(const FString& PayloadKey, const FTransform& MeshGlobalTransform, const FString& PayloadFilepath) { if (!PayloadContexts.Contains(PayloadKey)) { UInterchangeResultError_Generic* Message = AddMessage(); Message->Text = LOCTEXT("CannotRetrievePayload", "Cannot retrieve payload; payload key doesn't have any context."); return false; } { //Critical section to force payload to be fetch one by one with no concurrency. FScopeLock Lock(&PayloadCriticalSection); TSharedPtr& PayloadContext = PayloadContexts.FindChecked(PayloadKey); return PayloadContext->FetchMeshPayloadToFile(*this, MeshGlobalTransform, PayloadFilepath); } } #if WITH_ENGINE bool FFbxParser::FetchMeshPayloadData(const FString& PayloadKey, const FTransform& MeshGlobalTransform, FMeshPayloadData& OutMeshPayloadData) { if (!PayloadContexts.Contains(PayloadKey)) { UInterchangeResultError_Generic* Message = AddMessage(); Message->Text = LOCTEXT("CannotRetrievePayload", "Cannot retrieve payload; payload key doesn't have any context."); return false; } { //Critical section to force payload to be fetch one by one with no concurrency. FScopeLock Lock(&PayloadCriticalSection); TSharedPtr& PayloadContext = PayloadContexts.FindChecked(PayloadKey); return PayloadContext->FetchMeshPayload(*this, MeshGlobalTransform, OutMeshPayloadData); } } #endif bool FFbxParser::FetchAnimationBakeTransformPayload(const TArray& PayloadQueries, const FString& ResultFolder, FCriticalSection* ResultPayloadsCriticalSection, TAtomic& UniqueIdCounter, TMap& ResultPayloads/*PayloadUniqueID to FilePath*/) { //Critical section to force payload to be fetch one by one with no concurrency. FScopeLock Lock(&PayloadCriticalSection); TMap> PayloadQueriesGrouped; for (const UE::Interchange::FAnimationPayloadQuery& PayloadQuery : PayloadQueries) { TArray& PayloadQueriesForHash = PayloadQueriesGrouped.FindOrAdd(PayloadQuery.TimeDescription.GetHash()); PayloadQueriesForHash.Add(&PayloadQuery); } TArray OutErrorMessages; bool bResult = true; for (const TPair>& Group : PayloadQueriesGrouped) { bResult = FFbxAnimation::FetchAnimationBakeTransformPayload(*this, GetSDKScene(), PayloadContexts, Group.Value, ResultFolder, ResultPayloadsCriticalSection, UniqueIdCounter, ResultPayloads, OutErrorMessages) && bResult; } for (const FText& ErrorMessage : OutErrorMessages) { UInterchangeResultError_Generic* Message = AddMessage(); Message->Text = ErrorMessage; } return bResult; } void FFbxParser::CleanupFbxData() { auto ReportAndSetNoName = [this](FbxObject* Object, const FString& ObjectName) { Object->SetName(TCHAR_TO_UTF8(*ObjectName)); if (!GIsAutomationTesting) { UInterchangeResultDisplay_Generic* Message = AddMessage(); Message->Text = FText::Format(LOCTEXT("CleanupFbxData_NoObjectName", "Interchange FBX file Loading: Found object with no name, new object name is '{0}'"), FText::FromString(ObjectName)); } }; //bUseNodeRules: // - we don't manage the namespace as each namespace is expected to be in separate assets // - name sanitization will be part of the uniqueness check, but we don't set the sanitized name to the object, // as node names will have their namespace managed and sanitized when we create the names for Interchange nodes specifically. auto MakeFbxObjectNameUnique = [this](FbxObject* Object, TMap& Names, bool bUseNCL = true, bool bUseNodeRules = false) { FString NodeName = UTF8_TO_TCHAR(Object->GetName()); FString UniqueNodeName = NodeName; if (!bUseNodeRules) { //Manage namespaces should be done before sanitizing since it relies on the ":" character GetFbxHelper()->ManageNamespaceAndRenameObject(UniqueNodeName, Object); } //Make sure to use the sanitized name to avoid name collisions later on UE::Interchange::SanitizeName(UniqueNodeName); if (int32* Count = Names.Find(UniqueNodeName)) { (*Count)++; if (bUseNodeRules) { //if we are using node rules then its expected to fix the name facing towards Interchange, instead of right in place (in FBX struct). UniqueNodeName = NodeName + (bUseNCL ? TEXT("_ncl_") : TEXT("")) + FString::FromInt(*Count); Object->SetName(TCHAR_TO_UTF8(*UniqueNodeName)); } else { UniqueNodeName += (bUseNCL ? TEXT("_ncl_") : TEXT("")) + FString::FromInt(*Count); Object->SetName(TCHAR_TO_UTF8(*UniqueNodeName)); } if (!GIsAutomationTesting) { UInterchangeResultDisplay_Generic* Message = AddMessage(); Message->Text = FText::Format(LOCTEXT("CleanupFbxData_NodeNameClash", "FBX File Loading: Found name clash, object '{0}' was renamed to '{1}'"), FText::FromString(NodeName), FText::FromString(UniqueNodeName)); } } else { Names.Add(UniqueNodeName, 0); } }; ////////////////////////////////////////////////////////////////////////// // Ensure Node Name Validity (uniqueness) // Name clash must be global because unreal bones do not support name conflict (they are stored in an array, no hierarchy) TMap NodeNames; for (int32 NodeIndex = 0; NodeIndex < SDKScene->GetNodeCount(); ++NodeIndex) { FbxNode* Node = SDKScene->GetNode(NodeIndex); FString NodeName = UTF8_TO_TCHAR(Node->GetName()); bool bUseNCL = false; if (NodeName.IsEmpty()) { bUseNCL = true; ReportAndSetNoName(Node, TEXT("Node")); } MakeFbxObjectNameUnique(Node, NodeNames, bUseNCL, true); } ////////////////////////////////////////////////////////////////////////// // Ensure Mesh Name Validity (uniqueness) // Name clash must be global because we will build Unique ID from the mesh name TMap MeshNames; for (int32 GeometryIndex = 0; GeometryIndex < SDKScene->GetGeometryCount(); ++GeometryIndex) { FbxGeometry* Geometry = SDKScene->GetGeometry(GeometryIndex); if (Geometry->GetAttributeType() != FbxNodeAttribute::eMesh) { continue; } FbxMesh* Mesh = static_cast(Geometry); if (!Mesh) { continue; } FString MeshName = UTF8_TO_TCHAR(Mesh->GetName()); if (MeshName.IsEmpty()) { ReportAndSetNoName(Mesh, TEXT("Mesh")); } MakeFbxObjectNameUnique(Mesh, MeshNames); } ///////////////////////////////////////////////////////////////////////// // Ensure Material Name Validity (uniqueness) // Name clash must be global because we will build Unique ID from the material name TMap MaterialNames; for (int32 MaterialIndex = 0; MaterialIndex < SDKScene->GetMaterialCount(); ++MaterialIndex) { FbxSurfaceMaterial* Material = SDKScene->GetMaterial(MaterialIndex); FString MaterialName = UTF8_TO_TCHAR(Material->GetName()); if (MaterialName.IsEmpty()) { ReportAndSetNoName(Material, TEXT("Material")); } MakeFbxObjectNameUnique(Material, MaterialNames); } } void FFbxParser::ProcessExtraInformation(UInterchangeBaseNodeContainer& NodeContainer) { UInterchangeSourceNode* SourceNode = UInterchangeSourceNode::FindOrCreateUniqueInstance(&NodeContainer); SourceNode->SetExtraInformation(TEXT("File Version"), FileDetails.FbxFileVersion); SourceNode->SetExtraInformation(TEXT("File Creator"), FileDetails.FbxFileCreator); SourceNode->SetExtraInformation(TEXT("File Units"), FileDetails.UnitSystem); SourceNode->SetExtraInformation(TEXT("File Axis Direction"), FileDetails.AxisDirection); SourceNode->SetExtraInformation(TEXT("File Frame Rate"), FileDetails.FrameRate); // Analytics Data { using namespace UE::Interchange; if (!FileDetails.ApplicationVendor.IsEmpty()) { SourceNode->SetExtraInformation(FSourceNodeExtraInfoStaticData::GetApplicationVendorExtraInfoKey(), FileDetails.ApplicationVendor); } if (!FileDetails.ApplicationName.IsEmpty()) { SourceNode->SetExtraInformation(FSourceNodeExtraInfoStaticData::GetApplicationNameExtraInfoKey(), FileDetails.ApplicationName); } if (!FileDetails.ApplicationVersion.IsEmpty()) { SourceNode->SetExtraInformation(FSourceNodeExtraInfoStaticData::GetApplicationVersionExtraInfoKey(), FileDetails.ApplicationVersion); } } } } //ns Private } //ns Interchange } //ns UE #undef LOCTEXT_NAMESPACE