Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/Fbx/FbxMainImport.cpp
2025-05-18 13:04:45 +08:00

3499 lines
124 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
Main implementation of FFbxImporter : import FBX data to Unreal
=============================================================================*/
#include "CoreMinimal.h"
#include "Misc/Paths.h"
#include "Misc/FeedbackContext.h"
#include "Modules/ModuleManager.h"
#include "Widgets/DeclarativeSyntaxSupport.h"
#include "Widgets/SWindow.h"
#include "Framework/Application/SlateApplication.h"
#include "Misc/SecureHash.h"
#include "Factories/FbxSkeletalMeshImportData.h"
#include "Factories/FbxTextureImportData.h"
#include "Materials/MaterialInterface.h"
#include "Rendering/SkeletalMeshLODImporterData.h"
#include "Logging/TokenizedMessage.h"
#include "Misc/FbxErrors.h"
#include "FbxImporter.h"
#include "FbxOptionWindow.h"
#include "Interfaces/IMainFrameModule.h"
#include "EngineAnalytics.h"
#include "AnalyticsEventAttribute.h"
#include "Interfaces/IAnalyticsProvider.h"
#include "UObject/MetaData.h"
#include "UObject/UObjectGlobals.h"
#include "UObject/Package.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/ARFilter.h"
#include "Animation/Skeleton.h"
#include "HAL/PlatformApplicationMisc.h"
#include "Editor/EditorPerProjectUserSettings.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/SkinnedAssetCommon.h"
#include "Engine/StaticMesh.h"
#include "IMeshReductionInterfaces.h"
#include "ObjectTools.h"
#include "Misc/AutomationTest.h"
#include "Misc/AxisDisplayInfo.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "Misc/NamePermissionList.h"
#include "PhysicsEngine/PhysicsAsset.h"
DEFINE_LOG_CATEGORY(LogFbx);
#define LOCTEXT_NAMESPACE "FbxMainImport"
#define GeneratedLODNameSuffix "_GeneratedLOD_"
namespace UnFbx
{
TSharedPtr<FFbxImporter> FFbxImporter::StaticInstance;
TSharedPtr<FFbxImporter> FFbxImporter::StaticPreviewInstance;
template<typename TMaterialType>
void PrepareAndShowMaterialConflictPreviewDialog(UFbxImportUI* ImportUI)
{
TArray<TMaterialType> CurrentMaterial;
TArray<TMaterialType> ResultMaterial;
TArray<int32> RemapMaterial;
TArray<FName> RemapMaterialName;
RemapMaterial.AddZeroed(ImportUI->MaterialCompareData.ResultAsset.Num());
RemapMaterialName.AddZeroed(ImportUI->MaterialCompareData.ResultAsset.Num());
CurrentMaterial.AddDefaulted(ImportUI->MaterialCompareData.CurrentAsset.Num());
for (int32 Materialindex = 0; Materialindex < ImportUI->MaterialCompareData.CurrentAsset.Num(); ++Materialindex)
{
CurrentMaterial[Materialindex].MaterialSlotName = ImportUI->MaterialCompareData.CurrentAsset[Materialindex].MaterialSlotName;
CurrentMaterial[Materialindex].ImportedMaterialSlotName = ImportUI->MaterialCompareData.CurrentAsset[Materialindex].ImportedMaterialSlotName;
}
ResultMaterial.AddDefaulted(ImportUI->MaterialCompareData.ResultAsset.Num());
for (int32 Materialindex = 0; Materialindex < ImportUI->MaterialCompareData.ResultAsset.Num(); ++Materialindex)
{
ResultMaterial[Materialindex].MaterialSlotName = ImportUI->MaterialCompareData.ResultAsset[Materialindex].MaterialSlotName;
ResultMaterial[Materialindex].ImportedMaterialSlotName = ImportUI->MaterialCompareData.ResultAsset[Materialindex].ImportedMaterialSlotName;
}
UnFbx::EFBXReimportDialogReturnOption OutReturnOption;
UnFbx::FFbxImporter::PrepareAndShowMaterialConflictDialog<TMaterialType>(CurrentMaterial, ResultMaterial, RemapMaterial, RemapMaterialName, true, true, false, OutReturnOption);
}
void PrepareAndShowSkeletonConflictPreviewDialog(UFbxImportUI* ImportUI)
{
USkeletalMesh* SkeletalMesh = Cast<USkeletalMesh>(ImportUI->ReimportMesh);
UnFbx::FFbxImporter::ShowFbxSkeletonConflictWindow(SkeletalMesh, ImportUI->Skeleton, ImportUI->SkeletonCompareData);
}
FBXImportOptions* GetImportOptions( UnFbx::FFbxImporter* FbxImporter, UFbxImportUI* ImportUI, bool bShowOptionDialog, bool bIsAutomated, const FString& FullPath, bool& OutOperationCanceled, bool& bOutImportAll, bool bIsObjFormat, const FString& InFilename, bool bForceImportType, EFBXImportType ImportType)
{
OutOperationCanceled = false;
if ( bShowOptionDialog )
{
bOutImportAll = false;
UnFbx::FBXImportOptions* ImportOptions = FbxImporter->GetImportOptions();
// if Skeleton was set by outside, please make sure copy back to UI
if ( ImportOptions->SkeletonForAnimation )
{
ImportUI->Skeleton = ImportOptions->SkeletonForAnimation;
}
else
{
// Look in the current target directory to see if we have a skeleton
FARFilter Filter;
Filter.PackagePaths.Add(*FPaths::GetPath(FullPath));
Filter.ClassPaths.Add(USkeleton::StaticClass()->GetClassPathName());
IAssetRegistry& AssetRegistry = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
TArray<FAssetData> SkeletonAssets;
AssetRegistry.GetAssets(Filter, SkeletonAssets);
if(SkeletonAssets.Num() > 0)
{
ImportUI->Skeleton = CastChecked<USkeleton>(SkeletonAssets[0].GetAsset());
}
else
{
ImportUI->Skeleton = NULL;
}
}
if ( ImportOptions->PhysicsAsset )
{
ImportUI->PhysicsAsset = ImportOptions->PhysicsAsset;
}
else
{
ImportUI->PhysicsAsset = NULL;
}
if(bForceImportType)
{
ImportUI->MeshTypeToImport = ImportType;
ImportUI->OriginalImportType = ImportType;
}
ImportUI->bImportAsSkeletal = ImportUI->MeshTypeToImport == FBXIT_SkeletalMesh;
ImportUI->bImportMesh = ImportUI->MeshTypeToImport != FBXIT_Animation;
ImportUI->bIsObjImport = bIsObjFormat;
//This option must always be the same value has the skeletalmesh one.
ImportUI->AnimSequenceImportData->bImportMeshesInBoneHierarchy = ImportUI->SkeletalMeshImportData->bImportMeshesInBoneHierarchy;
//////////////////////////////////////////////////////////////////////////
// Set the information section data
//Make sure the file is open to be able to read the header before showing the options
//If the file is already open it will simply return false.
if (FbxImporter->ReadHeaderFromFile(InFilename, true))
{
ImportUI->FileVersion = FbxImporter->GetFbxFileVersion();
ImportUI->FileCreator = FbxImporter->GetFileCreator();
// do analytics on getting Fbx data
FbxDocumentInfo* DocInfo = FbxImporter->Scene->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()));
ImportUI->FileCreatorApplication = LastSavedVendor + TEXT(" ") + LastSavedAppName + TEXT(" ") + LastSavedAppVersion;
}
else
{
ImportUI->FileCreatorApplication = TEXT("");
}
ImportUI->FileUnits = FbxImporter->GetFileUnitSystem();
ImportUI->FileAxisDirection = FbxImporter->GetFileAxisDirection();
}
if (ImportUI->MeshTypeToImport != FBXIT_Animation && ImportUI->ReimportMesh != nullptr)
{
ImportUI->OnUpdateCompareFbx = FOnUpdateCompareFbx::CreateLambda([&ImportUI, &FbxImporter]
{
//Fill the importUI compare
ImportUI->UpdateCompareData(FbxImporter);
});
ImportUI->OnShowMaterialConflictDialog = FOnShowConflictDialog::CreateLambda([&ImportUI, &FbxImporter]
{
if (!ImportUI->MaterialCompareData.bHasConflict)
{
return;
}
if (ImportUI->MeshTypeToImport == FBXIT_SkeletalMesh)
{
PrepareAndShowMaterialConflictPreviewDialog<FSkeletalMaterial>(ImportUI);
}
else if (ImportUI->MeshTypeToImport == FBXIT_StaticMesh)
{
PrepareAndShowMaterialConflictPreviewDialog<FStaticMaterial>(ImportUI);
}
});
ImportUI->OnShowSkeletonConflictDialog = FOnShowConflictDialog::CreateLambda([&ImportUI, &FbxImporter]()
{
if (ImportUI->SkeletonCompareData.CompareResult == ImportCompareHelper::ECompareResult::SCR_None)
{
return;
}
if (ImportUI->MeshTypeToImport == FBXIT_SkeletalMesh)
{
PrepareAndShowSkeletonConflictPreviewDialog(ImportUI);
}
});
}
auto UpdateCoordinateSystemPolicy = [](UFbxAssetImportData* FbxAssetImportData)
{
if (FbxAssetImportData)
{
FbxAssetImportData->bUsingLUFCoordinateSysem = AxisDisplayInfo::GetAxisDisplayCoordinateSystem() == EAxisList::LeftUpForward;
FbxAssetImportData->CoordinateSystemPolicy = FbxAssetImportData->bConvertScene ? (FbxAssetImportData->bForceFrontXAxis ? ECoordinateSystemPolicy::MatchUpForwardAxes : ECoordinateSystemPolicy::MatchUpAxis) : ECoordinateSystemPolicy::KeepXYZAxes;
}
};
UpdateCoordinateSystemPolicy(ImportUI->StaticMeshImportData);
UpdateCoordinateSystemPolicy(ImportUI->SkeletalMeshImportData);
UpdateCoordinateSystemPolicy(ImportUI->AnimSequenceImportData);
TSharedPtr<SWindow> ParentWindow;
if( FModuleManager::Get().IsModuleLoaded( "MainFrame" ) )
{
IMainFrameModule& MainFrame = FModuleManager::LoadModuleChecked<IMainFrameModule>( "MainFrame" );
ParentWindow = MainFrame.GetParentWindow();
}
// Compute centered window position based on max window size, which include when all categories are expanded
const float FbxImportWindowWidth = 450.0f;
const float FbxImportWindowHeight = 750.0f;
FVector2D FbxImportWindowSize = FVector2D(FbxImportWindowWidth, FbxImportWindowHeight); // Max window size it can get based on current slate
FSlateRect WorkAreaRect = FSlateApplicationBase::Get().GetPreferredWorkArea();
FVector2D DisplayTopLeft(WorkAreaRect.Left, WorkAreaRect.Top);
FVector2D DisplaySize(WorkAreaRect.Right - WorkAreaRect.Left, WorkAreaRect.Bottom - WorkAreaRect.Top);
float ScaleFactor = FPlatformApplicationMisc::GetDPIScaleFactorAtPoint(DisplayTopLeft.X, DisplayTopLeft.Y);
FbxImportWindowSize *= ScaleFactor;
FVector2D WindowPosition = (DisplayTopLeft + (DisplaySize - FbxImportWindowSize) / 2.0f) / ScaleFactor;
TSharedRef<SWindow> Window = SNew(SWindow)
.Title(NSLOCTEXT("UnrealEd", "FBXImportOpionsTitle", "FBX Import Options"))
.SizingRule(ESizingRule::Autosized)
.AutoCenter(EAutoCenter::None)
.ClientSize(FbxImportWindowSize)
.ScreenPosition(WindowPosition);
TSharedPtr<SFbxOptionWindow> FbxOptionWindow;
Window->SetContent
(
SAssignNew(FbxOptionWindow, SFbxOptionWindow)
.ImportUI(ImportUI)
.WidgetWindow(Window)
.FullPath(FText::FromString(FullPath))
.ForcedImportType( bForceImportType ? TOptional<EFBXImportType>( ImportType ) : TOptional<EFBXImportType>() )
.IsObjFormat( bIsObjFormat )
.MaxWindowHeight(FbxImportWindowHeight)
.MaxWindowWidth(FbxImportWindowWidth)
);
// @todo: we can make this slow as showing progress bar later
FSlateApplication::Get().AddModalWindow(Window, ParentWindow, false);
if (ImportUI->MeshTypeToImport == FBXIT_SkeletalMesh || ImportUI->MeshTypeToImport == FBXIT_Animation)
{
//Set some hardcoded options for skeletal mesh
ImportUI->SkeletalMeshImportData->bBakePivotInVertex = false;
ImportOptions->bBakePivotInVertex = false;
ImportUI->SkeletalMeshImportData->bTransformVertexToAbsolute = true;
ImportOptions->bTransformVertexToAbsolute = true;
//when user import animation only we must get duplicate "bImportMeshesInBoneHierarchy" option from ImportUI anim sequence data
if (!ImportUI->bImportMesh && ImportUI->bImportAnimations)
{
ImportUI->SkeletalMeshImportData->bImportMeshesInBoneHierarchy = ImportUI->AnimSequenceImportData->bImportMeshesInBoneHierarchy;
}
else
{
ImportUI->AnimSequenceImportData->bImportMeshesInBoneHierarchy = ImportUI->SkeletalMeshImportData->bImportMeshesInBoneHierarchy;
}
}
if (!FbxImporter->CanImportClass(UPhysicsAsset::StaticClass()))
{
ImportUI->bCreatePhysicsAsset = false;
}
UFbxImportUI::SaveOptions(ImportUI);
auto SaveImportData = [](UFbxAssetImportData* FbxAssetImportData)
{
if (FbxAssetImportData)
{
if (FbxAssetImportData->bUsingLUFCoordinateSysem)
{
FbxAssetImportData->bConvertScene = FbxAssetImportData->CoordinateSystemPolicy != ECoordinateSystemPolicy::KeepXYZAxes;
FbxAssetImportData->bForceFrontXAxis = FbxAssetImportData->CoordinateSystemPolicy == ECoordinateSystemPolicy::MatchUpForwardAxes;
}
UFbxImportUI::SaveOptions(FbxAssetImportData);
}
};
SaveImportData(ImportUI->StaticMeshImportData);
SaveImportData(ImportUI->SkeletalMeshImportData);
SaveImportData(ImportUI->AnimSequenceImportData);
if( ImportUI->TextureImportData )
{
UFbxImportUI::SaveOptions(ImportUI->TextureImportData);
}
if (FbxOptionWindow->ShouldImport())
{
bOutImportAll = FbxOptionWindow->ShouldImportAll();
// open dialog
// see if it's canceled
ApplyImportUIToImportOptions(ImportUI, *ImportOptions);
return ImportOptions;
}
else
{
OutOperationCanceled = true;
}
}
else if (bIsAutomated)
{
//Automation tests set ImportUI settings directly. Just copy them over
UnFbx::FBXImportOptions* ImportOptions = FbxImporter->GetImportOptions();
//Clean up the options
UnFbx::FBXImportOptions::ResetOptions(ImportOptions);
ApplyImportUIToImportOptions(ImportUI, *ImportOptions);
if (!FbxImporter->CanImportClass(UPhysicsAsset::StaticClass()))
{
ImportOptions->bCreatePhysicsAsset = false;
}
return ImportOptions;
}
else
{
if (!FbxImporter->CanImportClass(UPhysicsAsset::StaticClass()))
{
FbxImporter->GetImportOptions()->bCreatePhysicsAsset = false;
}
return FbxImporter->GetImportOptions();
}
return NULL;
}
void ApplyImportUIToImportOptions(UFbxImportUI* ImportUI, FBXImportOptions& InOutImportOptions)
{
check(ImportUI);
//General options
{
InOutImportOptions.bUsedAsFullName = ImportUI->bOverrideFullName;
InOutImportOptions.ImportType = ImportUI->MeshTypeToImport;
InOutImportOptions.bResetToFbxOnMaterialConflict = ImportUI->bResetToFbxOnMaterialConflict;
InOutImportOptions.bAutoComputeLodDistances = ImportUI->bAutoComputeLodDistances;
InOutImportOptions.LodDistances.Empty(8);
InOutImportOptions.LodDistances.Add(ImportUI->LodDistance0);
InOutImportOptions.LodDistances.Add(ImportUI->LodDistance1);
InOutImportOptions.LodDistances.Add(ImportUI->LodDistance2);
InOutImportOptions.LodDistances.Add(ImportUI->LodDistance3);
InOutImportOptions.LodDistances.Add(ImportUI->LodDistance4);
InOutImportOptions.LodDistances.Add(ImportUI->LodDistance5);
InOutImportOptions.LodDistances.Add(ImportUI->LodDistance6);
InOutImportOptions.LodDistances.Add(ImportUI->LodDistance7);
InOutImportOptions.LodNumber = ImportUI->LodNumber;
InOutImportOptions.MinimumLodNumber = ImportUI->MinimumLodNumber;
}
InOutImportOptions.bBuildNanite = ImportUI->StaticMeshImportData->bBuildNanite;
//Animation and skeletal mesh options
{
InOutImportOptions.bImportAnimations = ImportUI->bImportAnimations;
InOutImportOptions.SkeletonForAnimation = ImportUI->Skeleton;
InOutImportOptions.bCreatePhysicsAsset = ImportUI->bCreatePhysicsAsset;
InOutImportOptions.PhysicsAsset = ImportUI->PhysicsAsset;
InOutImportOptions.AnimationName = ImportUI->OverrideAnimationName;
}
//Material options
{
InOutImportOptions.bImportMaterials = ImportUI->bImportMaterials;
InOutImportOptions.bInvertNormalMap = ImportUI->TextureImportData->bInvertNormalMaps;
InOutImportOptions.MaterialSearchLocation = ImportUI->TextureImportData->MaterialSearchLocation;
UMaterialInterface* BaseMaterialInterface = Cast<UMaterialInterface>(ImportUI->TextureImportData->BaseMaterialName.TryLoad());
if (BaseMaterialInterface) {
InOutImportOptions.BaseMaterial = BaseMaterialInterface;
InOutImportOptions.BaseColorName = ImportUI->TextureImportData->BaseColorName;
InOutImportOptions.BaseDiffuseTextureName = ImportUI->TextureImportData->BaseDiffuseTextureName;
InOutImportOptions.BaseNormalTextureName = ImportUI->TextureImportData->BaseNormalTextureName;
InOutImportOptions.BaseEmmisiveTextureName = ImportUI->TextureImportData->BaseEmmisiveTextureName;
InOutImportOptions.BaseSpecularTextureName = ImportUI->TextureImportData->BaseSpecularTextureName;
InOutImportOptions.BaseOpacityTextureName = ImportUI->TextureImportData->BaseOpacityTextureName;
InOutImportOptions.BaseEmissiveColorName = ImportUI->TextureImportData->BaseEmissiveColorName;
}
InOutImportOptions.bImportTextures = ImportUI->bImportTextures;
}
//Some options are overlap between static mesh, skeletal mesh and anim sequence. We must set them according to the import type to set them only once
if ( ImportUI->MeshTypeToImport == FBXIT_StaticMesh )
{
UFbxStaticMeshImportData* StaticMeshData = ImportUI->StaticMeshImportData;
InOutImportOptions.NormalImportMethod = StaticMeshData->NormalImportMethod;
InOutImportOptions.NormalGenerationMethod = StaticMeshData->NormalGenerationMethod;
InOutImportOptions.bComputeWeightedNormals = StaticMeshData->bComputeWeightedNormals;
InOutImportOptions.ImportTranslation = StaticMeshData->ImportTranslation;
InOutImportOptions.ImportRotation = StaticMeshData->ImportRotation;
InOutImportOptions.ImportUniformScale = StaticMeshData->ImportUniformScale;
InOutImportOptions.bTransformVertexToAbsolute = StaticMeshData->bTransformVertexToAbsolute;
InOutImportOptions.bBakePivotInVertex = StaticMeshData->bBakePivotInVertex;
InOutImportOptions.bImportStaticMeshLODs = StaticMeshData->bImportMeshLODs;
InOutImportOptions.bConvertScene = StaticMeshData->bConvertScene;
InOutImportOptions.bForceFrontXAxis = StaticMeshData->bForceFrontXAxis;
InOutImportOptions.bConvertSceneUnit = StaticMeshData->bConvertSceneUnit;
InOutImportOptions.VertexColorImportOption = StaticMeshData->VertexColorImportOption;
InOutImportOptions.VertexOverrideColor = StaticMeshData->VertexOverrideColor;
InOutImportOptions.bReorderMaterialToFbxOrder = StaticMeshData->bReorderMaterialToFbxOrder;
InOutImportOptions.DistanceFieldResolutionScale = StaticMeshData->DistanceFieldResolutionScale;
}
else if ( ImportUI->MeshTypeToImport == FBXIT_SkeletalMesh )
{
UFbxSkeletalMeshImportData* SkeletalMeshData = ImportUI->SkeletalMeshImportData;
InOutImportOptions.NormalImportMethod = SkeletalMeshData->NormalImportMethod;
InOutImportOptions.NormalGenerationMethod = SkeletalMeshData->NormalGenerationMethod;
InOutImportOptions.bComputeWeightedNormals = SkeletalMeshData->bComputeWeightedNormals;
InOutImportOptions.ImportTranslation = SkeletalMeshData->ImportTranslation;
InOutImportOptions.ImportRotation = SkeletalMeshData->ImportRotation;
InOutImportOptions.ImportUniformScale = SkeletalMeshData->ImportUniformScale;
InOutImportOptions.bTransformVertexToAbsolute = SkeletalMeshData->bTransformVertexToAbsolute;
InOutImportOptions.bBakePivotInVertex = SkeletalMeshData->bBakePivotInVertex;
InOutImportOptions.bImportSkeletalMeshLODs = SkeletalMeshData->bImportMeshLODs;
InOutImportOptions.bConvertScene = SkeletalMeshData->bConvertScene;
InOutImportOptions.bForceFrontXAxis = SkeletalMeshData->bForceFrontXAxis;
InOutImportOptions.bConvertSceneUnit = SkeletalMeshData->bConvertSceneUnit;
InOutImportOptions.VertexColorImportOption = SkeletalMeshData->VertexColorImportOption;
InOutImportOptions.VertexOverrideColor = SkeletalMeshData->VertexOverrideColor;
InOutImportOptions.bReorderMaterialToFbxOrder = SkeletalMeshData->bReorderMaterialToFbxOrder;
InOutImportOptions.bImportVertexAttributes = SkeletalMeshData->bImportVertexAttributes;
if(ImportUI->bImportAnimations)
{
// Copy the transform information into the animation data to match the mesh.
UFbxAnimSequenceImportData* AnimData = ImportUI->AnimSequenceImportData;
AnimData->ImportTranslation = SkeletalMeshData->ImportTranslation;
AnimData->ImportRotation = SkeletalMeshData->ImportRotation;
AnimData->ImportUniformScale = SkeletalMeshData->ImportUniformScale;
AnimData->bConvertScene = SkeletalMeshData->bConvertScene;
AnimData->bForceFrontXAxis = SkeletalMeshData->bForceFrontXAxis;
AnimData->bConvertSceneUnit = SkeletalMeshData->bConvertSceneUnit;
}
}
else
{
UFbxAnimSequenceImportData* AnimData = ImportUI->AnimSequenceImportData;
InOutImportOptions.NormalImportMethod = FBXNIM_ComputeNormals;
InOutImportOptions.bComputeWeightedNormals = true;
InOutImportOptions.ImportTranslation = AnimData->ImportTranslation;
InOutImportOptions.ImportRotation = AnimData->ImportRotation;
InOutImportOptions.ImportUniformScale = AnimData->ImportUniformScale;
InOutImportOptions.bConvertScene = AnimData->bConvertScene;
InOutImportOptions.bForceFrontXAxis = AnimData->bForceFrontXAxis;
InOutImportOptions.bConvertSceneUnit = AnimData->bConvertSceneUnit;
}
//Skeletal mesh unshared options
{
InOutImportOptions.bImportAsSkeletalGeometry = ImportUI->SkeletalMeshImportData->ImportContentType == EFBXImportContentType::FBXICT_Geometry;
InOutImportOptions.bImportAsSkeletalSkinning = ImportUI->SkeletalMeshImportData->ImportContentType == EFBXImportContentType::FBXICT_SkinningWeights;
InOutImportOptions.bImportMorph = ImportUI->SkeletalMeshImportData->bImportMorphTargets;
InOutImportOptions.bUpdateSkeletonReferencePose = ImportUI->SkeletalMeshImportData->bUpdateSkeletonReferencePose;
InOutImportOptions.bImportRigidMesh = ImportUI->OriginalImportType == FBXIT_StaticMesh && ImportUI->MeshTypeToImport == FBXIT_SkeletalMesh;
InOutImportOptions.bUseT0AsRefPose = ImportUI->SkeletalMeshImportData->bUseT0AsRefPose;
InOutImportOptions.bPreserveSmoothingGroups = ImportUI->SkeletalMeshImportData->bPreserveSmoothingGroups;
InOutImportOptions.bKeepSectionsSeparate = ImportUI->SkeletalMeshImportData->bKeepSectionsSeparate;
InOutImportOptions.OverlappingThresholds.ThresholdPosition = ImportUI->SkeletalMeshImportData->ThresholdPosition;
InOutImportOptions.OverlappingThresholds.ThresholdTangentNormal = ImportUI->SkeletalMeshImportData->ThresholdTangentNormal;
InOutImportOptions.OverlappingThresholds.ThresholdUV = ImportUI->SkeletalMeshImportData->ThresholdUV;
InOutImportOptions.OverlappingThresholds.MorphThresholdPosition = ImportUI->SkeletalMeshImportData->MorphThresholdPosition;
InOutImportOptions.bImportMeshesInBoneHierarchy = ImportUI->SkeletalMeshImportData->bImportMeshesInBoneHierarchy;
}
//Static mesh unshared options
{
InOutImportOptions.bCombineToSingle = ImportUI->StaticMeshImportData->bCombineMeshes;
InOutImportOptions.bRemoveDegenerates = ImportUI->StaticMeshImportData->bRemoveDegenerates;
InOutImportOptions.bBuildReversedIndexBuffer = ImportUI->StaticMeshImportData->bBuildReversedIndexBuffer;
InOutImportOptions.bGenerateLightmapUVs = ImportUI->StaticMeshImportData->bGenerateLightmapUVs;
InOutImportOptions.bOneConvexHullPerUCX = ImportUI->StaticMeshImportData->bOneConvexHullPerUCX;
InOutImportOptions.bAutoGenerateCollision = ImportUI->StaticMeshImportData->bAutoGenerateCollision;
InOutImportOptions.StaticMeshLODGroup = ImportUI->StaticMeshImportData->StaticMeshLODGroup;
}
// animation unshared options
{
InOutImportOptions.AnimationLengthImportType = ImportUI->AnimSequenceImportData->AnimationLength;
InOutImportOptions.AnimationRange.X = ImportUI->AnimSequenceImportData->FrameImportRange.Min;
InOutImportOptions.AnimationRange.Y = ImportUI->AnimSequenceImportData->FrameImportRange.Max;
// only re-sample if they don't want to use default sample rate
InOutImportOptions.bResample = !ImportUI->AnimSequenceImportData->bUseDefaultSampleRate;
InOutImportOptions.ResampleRate = ImportUI->AnimSequenceImportData->CustomSampleRate;
InOutImportOptions.bSnapToClosestFrameBoundary = ImportUI->AnimSequenceImportData->bSnapToClosestFrameBoundary;
InOutImportOptions.bPreserveLocalTransform = ImportUI->AnimSequenceImportData->bPreserveLocalTransform;
InOutImportOptions.bDeleteExistingMorphTargetCurves = ImportUI->AnimSequenceImportData->bDeleteExistingMorphTargetCurves;
InOutImportOptions.bRemoveRedundantKeys = ImportUI->AnimSequenceImportData->bRemoveRedundantKeys;
InOutImportOptions.bDoNotImportCurveWithZero = ImportUI->AnimSequenceImportData->bDoNotImportCurveWithZero;
InOutImportOptions.bImportCustomAttribute = ImportUI->AnimSequenceImportData->bImportCustomAttribute;
InOutImportOptions.bDeleteExistingCustomAttributeCurves = ImportUI->AnimSequenceImportData->bDeleteExistingCustomAttributeCurves;
InOutImportOptions.bDeleteExistingNonCurveCustomAttributes = ImportUI->AnimSequenceImportData->bDeleteExistingNonCurveCustomAttributes;
InOutImportOptions.bImportBoneTracks = ImportUI->AnimSequenceImportData->bImportBoneTracks;
InOutImportOptions.bSetMaterialDriveParameterOnCustomAttribute = ImportUI->AnimSequenceImportData->bSetMaterialDriveParameterOnCustomAttribute;
InOutImportOptions.bAddCurveMetadataToSkeleton = ImportUI->AnimSequenceImportData->bAddCurveMetadataToSkeleton;
InOutImportOptions.MaterialCurveSuffixes = ImportUI->AnimSequenceImportData->MaterialCurveSuffixes;
}
}
static bool AssetClassPassesFilter(UClass* Class, EAssetClassAction AssetClassAction)
{
IAssetTools& AssetTools = FAssetToolsModule::GetModule().Get();
TSharedPtr<FPathPermissionList> AssetClassPermissionList = AssetTools.GetAssetClassPathPermissionList(AssetClassAction);
if (Class && AssetClassPermissionList && AssetClassPermissionList->HasFiltering())
{
return AssetClassPermissionList->PassesFilter(Class->GetPathName());
}
return true;
}
void FImportedMaterialData::AddImportedMaterial( const FbxSurfaceMaterial& FbxMaterial, UMaterialInterface& UnrealMaterial )
{
FbxToUnrealMaterialMap.Add( &FbxMaterial, &UnrealMaterial );
ImportedMaterialNames.Add( *UnrealMaterial.GetPathName() );
}
bool FImportedMaterialData::IsAlreadyImported( const FbxSurfaceMaterial& FbxMaterial, FName ImportedMaterialName ) const
{
const UMaterialInterface* FoundMaterial = GetUnrealMaterial( FbxMaterial );
return FoundMaterial != NULL || ImportedMaterialNames.Contains( ImportedMaterialName );
}
UMaterialInterface* FImportedMaterialData::GetUnrealMaterial( const FbxSurfaceMaterial& FbxMaterial ) const
{
return FbxToUnrealMaterialMap.FindRef( &FbxMaterial ).Get();
}
void FImportedMaterialData::Clear()
{
FbxToUnrealMaterialMap.Empty();
ImportedMaterialNames.Empty();
}
FFbxImporter::FFbxImporter()
: Scene(NULL)
, ImportOptions(NULL)
, GeometryConverter(NULL)
, SdkManager(NULL)
, Importer(NULL)
, bFirstMesh(true)
, FbxCreator(UnFbx::EFbxCreator::Unknow)
, Logger(NULL)
{
// Create the SdkManager
SdkManager = FbxManager::Create();
// create an IOSettings object
FbxIOSettings * ios = FbxIOSettings::Create(SdkManager, IOSROOT );
SdkManager->SetIOSettings(ios);
// Create the geometry converter
GeometryConverter = new FbxGeometryConverter(SdkManager);
Scene = NULL;
ImportOptions = new FBXImportOptions();
FMemory::Memzero(*ImportOptions);
ImportOptions->MaterialBasePath = NAME_None;
CurPhase = NOTSTARTED;
//The FFbxImporter is a singleton is constructor is protected
//We must release the resource in the pre-exit delegate because in some cases the
//Instance is not valid anymore when the destructor get called (i.e. when we build the editor in monolithic)
FCoreDelegates::OnPreExit.AddLambda([]()
{
FFbxImporter::GetInstance()->CleanUp();
});
}
//-------------------------------------------------------------------------
//
//-------------------------------------------------------------------------
FFbxImporter::~FFbxImporter()
{
//The clean up should have been done in the pre-exit core delegate implement in the FFbxImporter constructor
}
//-------------------------------------------------------------------------
//
//-------------------------------------------------------------------------
FFbxImporter* FFbxImporter::GetInstance(bool bDoNotCreate /*= false*/)
{
if (!StaticInstance.IsValid())
{
//Return nullptr if we cannot create the instance
if (bDoNotCreate)
{
return nullptr;
}
StaticInstance = MakeShareable( new FFbxImporter() );
}
return StaticInstance.Get();
}
void FFbxImporter::DeleteInstance()
{
StaticInstance.Reset();
}
//-------------------------------------------------------------------------
//
//-------------------------------------------------------------------------
void FFbxImporter::CleanUp()
{
ClearTokenizedErrorMessages();
ReleaseScene();
delete GeometryConverter;
GeometryConverter = NULL;
delete ImportOptions;
ImportOptions = NULL;
if (SdkManager)
{
SdkManager->Destroy();
}
SdkManager = NULL;
Logger = NULL;
}
void FFbxImporter::PartialCleanUp()
{
ClearTokenizedErrorMessages();
ReleaseScene();
}
//-------------------------------------------------------------------------
//
//-------------------------------------------------------------------------
void FFbxImporter::ReleaseScene()
{
TRACE_CPUPROFILER_EVENT_SCOPE(FFbxImporter::ReleaseScene);
if (Importer)
{
Importer->Destroy();
Importer = NULL;
}
if (Scene)
{
Scene->Destroy();
Scene = NULL;
}
ImportedMaterialData.Clear();
// reset
FbxTextureToUniqueNameMap.Empty();
NodeUniqueNameToOriginalNameMap.Clear();
CollisionModels.Clear();
CreatedObjects.Empty();
CurPhase = NOTSTARTED;
bFirstMesh = true;
LastMergeBonesChoice = EAppReturnType::Ok;
}
bool FFbxImporter::CanImportClass(UClass* Class) const
{
return AssetClassPassesFilter(Class, EAssetClassAction::ImportAsset);
}
bool FFbxImporter::CanCreateClass(UClass* Class) const
{
return AssetClassPassesFilter(Class, EAssetClassAction::CreateAsset);
}
FBXImportOptions* UnFbx::FFbxImporter::GetImportOptions() const
{
return ImportOptions;
}
int32 FFbxImporter::GetImportType(const FString& InFilename)
{
int32 Result = -1; // Default to invalid
FString Filename = InFilename;
// Prioritized in the order of SkeletalMesh > StaticMesh > Animation (only if animation data is found)
if (OpenFile(Filename))
{
bool bHasAnimation = false;
bool bHasAnimationOnSkeletalMesh = false;
FbxSceneInfo SceneInfo;
if (GetSceneInfo(Filename, SceneInfo, true))
{
if (SceneInfo.SkinnedMeshNum > 0)
{
Result = 1;
}
else if (SceneInfo.TotalGeometryNum > 0)
{
Result = 0;
}
bHasAnimation = SceneInfo.bHasAnimation;
bHasAnimationOnSkeletalMesh = SceneInfo.bHasAnimationOnSkeletalMesh;
}
// In case no Geometry was found, check for animation (FBX can still contain mesh data though)
if (bHasAnimation)
{
if ( Result == -1)
{
Result = 2;
}
else if (Result == 0)
{
// by default detects as skeletalmesh since it has animation curves
if (bHasAnimationOnSkeletalMesh)
{
Result = 1;
}
else
{
Result = 0;
}
}
}
}
return Result;
}
bool FFbxImporter::GetSceneInfo(FString Filename, FbxSceneInfo& SceneInfo, bool bPreventMaterialNameClash /*= false*/)
{
bool Result = true;
GWarn->BeginSlowTask( NSLOCTEXT("FbxImporter", "BeginGetSceneInfoTask", "Parse FBX file to get scene info"), true );
bool bSceneInfo = true;
switch (CurPhase)
{
case NOTSTARTED:
if (!OpenFile(Filename))
{
Result = false;
break;
}
GWarn->UpdateProgress( 40, 100 );
case FILEOPENED:
if (!ImportFile(Filename, bPreventMaterialNameClash))
{
Result = false;
break;
}
GWarn->UpdateProgress( 90, 100 );
case IMPORTED:
case FIXEDANDCONVERTED:
default:
break;
}
if (Result)
{
FbxTimeSpan GlobalTimeSpan(FBXSDK_TIME_INFINITE,FBXSDK_TIME_MINUS_INFINITE);
SceneInfo.TotalMaterialNum = Scene->GetMaterialCount();
SceneInfo.TotalTextureNum = Scene->GetTextureCount();
SceneInfo.TotalGeometryNum = 0;
SceneInfo.NonSkinnedMeshNum = 0;
SceneInfo.SkinnedMeshNum = 0;
for ( int32 GeometryIndex = 0; GeometryIndex < Scene->GetGeometryCount(); GeometryIndex++ )
{
FbxGeometry * Geometry = Scene->GetGeometry(GeometryIndex);
if (Geometry->GetAttributeType() == FbxNodeAttribute::eMesh)
{
FbxNode* GeoNode = Geometry->GetNode();
FbxMesh* Mesh = (FbxMesh*)Geometry;
//Skip staticmesh sub LOD group that will be merge with the other same lod index mesh
if (GeoNode && Mesh->GetDeformerCount(FbxDeformer::eSkin) <= 0)
{
FbxNode* ParentNode = RecursiveFindParentLodGroup(GeoNode->GetParent());
if (ParentNode != nullptr && ParentNode->GetNodeAttribute() && ParentNode->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
{
bool IsLodRoot = false;
for (int32 ChildIndex = 0; ChildIndex < ParentNode->GetChildCount(); ++ChildIndex)
{
FbxNode *MeshNode = FindLODGroupNode(ParentNode, ChildIndex);
if (GeoNode == MeshNode)
{
IsLodRoot = true;
break;
}
}
if (!IsLodRoot)
{
//Skip static mesh sub LOD
continue;
}
}
}
SceneInfo.TotalGeometryNum++;
SceneInfo.MeshInfo.AddZeroed(1);
FbxMeshInfo& MeshInfo = SceneInfo.MeshInfo.Last();
if(Geometry->GetName()[0] != '\0')
MeshInfo.Name = MakeName(Geometry->GetName());
else
MeshInfo.Name = MakeString(GeoNode ? GeoNode->GetName() : "None");
MeshInfo.bTriangulated = Mesh->IsTriangleMesh();
MeshInfo.MaterialNum = GeoNode? GeoNode->GetMaterialCount() : 0;
MeshInfo.FaceNum = Mesh->GetPolygonCount();
MeshInfo.VertexNum = Mesh->GetControlPointsCount();
// LOD info
MeshInfo.LODGroup = NULL;
if (GeoNode)
{
FbxNode* ParentNode = RecursiveFindParentLodGroup(GeoNode->GetParent());
if (ParentNode != nullptr && ParentNode->GetNodeAttribute() && ParentNode->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
{
MeshInfo.LODGroup = MakeString(ParentNode->GetName());
for (int32 LODIndex = 0; LODIndex < ParentNode->GetChildCount(); LODIndex++)
{
FbxNode *MeshNode = FindLODGroupNode(ParentNode, LODIndex, GeoNode);
if (GeoNode == MeshNode)
{
MeshInfo.LODLevel = LODIndex;
break;
}
}
}
}
// skeletal mesh
if (Mesh->GetDeformerCount(FbxDeformer::eSkin) > 0)
{
SceneInfo.SkinnedMeshNum++;
MeshInfo.bIsSkelMesh = true;
MeshInfo.MorphNum = Mesh->GetShapeCount();
// skeleton root
FbxSkin* Skin = (FbxSkin*)Mesh->GetDeformer(0, FbxDeformer::eSkin);
int32 ClusterCount = Skin->GetClusterCount();
FbxNode* Link = NULL;
for (int32 ClusterId = 0; ClusterId < ClusterCount; ++ClusterId)
{
FbxCluster* Cluster = Skin->GetCluster(ClusterId);
Link = Cluster->GetLink();
while (Link && Link->GetParent() && Link->GetParent()->GetSkeleton())
{
Link = Link->GetParent();
}
if (Link != NULL)
{
break;
}
}
MeshInfo.SkeletonRoot = MakeString(Link ? Link->GetName() : ("None"));
MeshInfo.SkeletonElemNum = Link ? Link->GetChildCount(true) : 0;
if (Link)
{
FbxTimeSpan AnimTimeSpan(FBXSDK_TIME_INFINITE, FBXSDK_TIME_MINUS_INFINITE);
Link->GetAnimationInterval(AnimTimeSpan);
GlobalTimeSpan.UnionAssignment(AnimTimeSpan);
}
}
else
{
SceneInfo.NonSkinnedMeshNum++;
MeshInfo.bIsSkelMesh = false;
MeshInfo.SkeletonRoot = NULL;
}
MeshInfo.UniqueId = Mesh->GetUniqueID();
}
}
SceneInfo.bHasAnimation = false;
SceneInfo.bHasAnimationOnSkeletalMesh = false;
int32 AnimCurveNodeCount = Scene->GetSrcObjectCount<FbxAnimCurveNode>();
// sadly Max export with animation curve node by default without any change, so
// we'll have to skip the first two curves, which is translation/rotation
// if there is a valid animation, we'd expect there are more curve nodes than 2.
for (int32 AnimCurveNodeIndex = 2; AnimCurveNodeIndex < AnimCurveNodeCount; AnimCurveNodeIndex++)
{
FbxAnimCurveNode* CurAnimCruveNode = Scene->GetSrcObject<FbxAnimCurveNode>(AnimCurveNodeIndex);
if (CurAnimCruveNode->IsAnimated(true))
{
SceneInfo.bHasAnimation = true;
const FbxProperty DstProperty = CurAnimCruveNode->GetDstProperty();
const FbxObject* AnimatedObject = DstProperty.GetFbxObject();
if (AnimatedObject && (AnimatedObject->Is<FbxGeometry>() || (AnimatedObject->Is<FbxNode>() && IsUnrealBone((FbxNode*)AnimatedObject))))
{
SceneInfo.bHasAnimationOnSkeletalMesh = true;
}
}
if (SceneInfo.bHasAnimation && SceneInfo.bHasAnimationOnSkeletalMesh)
{
break;
}
}
SceneInfo.FrameRate = FbxTime::GetFrameRate(Scene->GetGlobalSettings().GetTimeMode());
if ( GlobalTimeSpan.GetDirection() == FBXSDK_TIME_FORWARD)
{
SceneInfo.TotalTime = (GlobalTimeSpan.GetDuration().GetMilliSeconds())/1000.f * SceneInfo.FrameRate;
}
else
{
SceneInfo.TotalTime = 0;
}
FbxNode* RootNode = Scene->GetRootNode();
FbxNodeInfo RootInfo;
RootInfo.ObjectName = MakeName(RootNode->GetName());
RootInfo.UniqueId = RootNode->GetUniqueID();
RootInfo.Transform = RootNode->EvaluateGlobalTransform();
RootInfo.AttributeName = NULL;
RootInfo.AttributeUniqueId = 0;
RootInfo.AttributeType = NULL;
RootInfo.ParentName = NULL;
RootInfo.ParentUniqueId = 0;
//Add the rootnode to the SceneInfo
SceneInfo.HierarchyInfo.Add(RootInfo);
//Fill the hierarchy info
TraverseHierarchyNodeRecursively(SceneInfo, RootNode, RootInfo);
}
GWarn->EndSlowTask();
return Result;
}
void FFbxImporter::TraverseHierarchyNodeRecursively(FbxSceneInfo& SceneInfo, FbxNode *ParentNode, FbxNodeInfo &ParentInfo)
{
int32 NodeCount = ParentNode->GetChildCount();
for (int32 NodeIndex = 0; NodeIndex < NodeCount; ++NodeIndex)
{
FbxNode* ChildNode = ParentNode->GetChild(NodeIndex);
FbxNodeInfo ChildInfo;
ChildInfo.ObjectName = MakeName(ChildNode->GetName());
ChildInfo.UniqueId = ChildNode->GetUniqueID();
ChildInfo.ParentName = ParentInfo.ObjectName;
ChildInfo.ParentUniqueId = ParentInfo.UniqueId;
ChildInfo.RotationPivot = ChildNode->RotationPivot.Get();
ChildInfo.ScalePivot = ChildNode->ScalingPivot.Get();
ChildInfo.Transform = ChildNode->EvaluateLocalTransform();
if (ChildNode->GetNodeAttribute())
{
FbxNodeAttribute *ChildAttribute = ChildNode->GetNodeAttribute();
ChildInfo.AttributeUniqueId = ChildAttribute->GetUniqueID();
if (ChildAttribute->GetName()[0] != '\0')
{
ChildInfo.AttributeName = MakeName(ChildAttribute->GetName());
}
else
{
//Get the name of the first node that link this attribute
ChildInfo.AttributeName = MakeName(ChildAttribute->GetNode()->GetName());
}
switch (ChildAttribute->GetAttributeType())
{
case FbxNodeAttribute::eUnknown:
ChildInfo.AttributeType = "eUnknown";
break;
case FbxNodeAttribute::eNull:
ChildInfo.AttributeType = "eNull";
break;
case FbxNodeAttribute::eMarker:
ChildInfo.AttributeType = "eMarker";
break;
case FbxNodeAttribute::eSkeleton:
ChildInfo.AttributeType = "eSkeleton";
break;
case FbxNodeAttribute::eMesh:
ChildInfo.AttributeType = "eMesh";
break;
case FbxNodeAttribute::eNurbs:
ChildInfo.AttributeType = "eNurbs";
break;
case FbxNodeAttribute::ePatch:
ChildInfo.AttributeType = "ePatch";
break;
case FbxNodeAttribute::eCamera:
ChildInfo.AttributeType = "eCamera";
break;
case FbxNodeAttribute::eCameraStereo:
ChildInfo.AttributeType = "eCameraStereo";
break;
case FbxNodeAttribute::eCameraSwitcher:
ChildInfo.AttributeType = "eCameraSwitcher";
break;
case FbxNodeAttribute::eLight:
ChildInfo.AttributeType = "eLight";
break;
case FbxNodeAttribute::eOpticalReference:
ChildInfo.AttributeType = "eOpticalReference";
break;
case FbxNodeAttribute::eOpticalMarker:
ChildInfo.AttributeType = "eOpticalMarker";
break;
case FbxNodeAttribute::eNurbsCurve:
ChildInfo.AttributeType = "eNurbsCurve";
break;
case FbxNodeAttribute::eTrimNurbsSurface:
ChildInfo.AttributeType = "eTrimNurbsSurface";
break;
case FbxNodeAttribute::eBoundary:
ChildInfo.AttributeType = "eBoundary";
break;
case FbxNodeAttribute::eNurbsSurface:
ChildInfo.AttributeType = "eNurbsSurface";
break;
case FbxNodeAttribute::eShape:
ChildInfo.AttributeType = "eShape";
break;
case FbxNodeAttribute::eLODGroup:
ChildInfo.AttributeType = "eLODGroup";
break;
case FbxNodeAttribute::eSubDiv:
ChildInfo.AttributeType = "eSubDiv";
break;
case FbxNodeAttribute::eCachedEffect:
ChildInfo.AttributeType = "eCachedEffect";
break;
case FbxNodeAttribute::eLine:
ChildInfo.AttributeType = "eLine";
break;
}
}
else
{
ChildInfo.AttributeUniqueId = INVALID_UNIQUE_ID;
ChildInfo.AttributeType = "eNull";
ChildInfo.AttributeName = NULL;
}
SceneInfo.HierarchyInfo.Add(ChildInfo);
TraverseHierarchyNodeRecursively(SceneInfo, ChildNode, ChildInfo);
}
}
bool FFbxImporter::OpenFile(FString Filename)
{
bool Result = true;
if (CurPhase != NOTSTARTED)
{
// something went wrong
return false;
}
TRACE_CPUPROFILER_EVENT_SCOPE(FFbxImporter::OpenFile);
GWarn->BeginSlowTask( LOCTEXT("OpeningFile", "Reading File"), true);
GWarn->StatusForceUpdate(20, 100, LOCTEXT("OpeningFile", "Reading File"));
ClearAllCaches();
int32 SDKMajor, SDKMinor, SDKRevision;
// Create an importer.
Importer = 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);
if (SdkManager->GetIOSettings())
{
SdkManager->GetIOSettings()->SetBoolProp(IMP_RELAXED_FBX_CHECK, true);
}
// Initialize the importer by providing a filename.
const bool bImportStatus = Importer->Initialize(TCHAR_TO_UTF8(*Filename), -1, SdkManager->GetIOSettings());
FbxCreator = EFbxCreator::Unknow;
FbxIOFileHeaderInfo *FileHeaderInfo = Importer->GetFileHeaderInfo();
if (FileHeaderInfo)
{
//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
FString CreatorStr(FileHeaderInfo->mCreator.Buffer());
if (CreatorStr.StartsWith(TEXT("Blender")))
{
FbxCreator = EFbxCreator::Blender;
}
}
GWarn->StatusForceUpdate(100, 100, LOCTEXT("OpeningFile", "Reading File"));
GWarn->EndSlowTask();
if( !bImportStatus ) // Problem with the file to be imported
{
UE_LOG(LogFbx, Error,TEXT("Call to FbxImporter::Initialize() failed."));
UE_LOG(LogFbx, Warning, TEXT("Error returned: %s"), UTF8_TO_TCHAR(Importer->GetStatus().GetErrorString()));
if (Importer->GetStatus().GetCode() == FbxStatus::eInvalidFileVersion )
{
UE_LOG(LogFbx, Warning, TEXT("FBX version number for this FBX SDK is %d.%d.%d"),
SDKMajor, SDKMinor, SDKRevision);
}
return false;
}
// Version out of date warning
int32 FileMajor = 0, FileMinor = 0, FileRevision = 0;
Importer->GetFileVersion(FileMajor, FileMinor, FileRevision);
int32 FileVersion = (FileMajor << 16 | FileMinor << 8 | FileRevision);
int32 SDKVersion = (SDKMajor << 16 | SDKMinor << 8 | SDKRevision);
if( FileVersion != SDKVersion )
{
// Appending the SDK version to the config key causes the warning to automatically reappear even if previously suppressed when the SDK version we use changes.
FString ConfigStr = FString::Printf( TEXT("Warning_OutOfDateFBX_%d"), SDKVersion );
FString FileVerStr = FString::Printf( TEXT("%d.%d.%d"), FileMajor, FileMinor, FileRevision );
FString SDKVerStr = FString::Printf( TEXT("%d.%d.%d"), SDKMajor, SDKMinor, SDKRevision );
const FText WarningText = FText::Format(
NSLOCTEXT("UnrealEd", "Warning_OutOfDateFBX", "An out of date FBX has been detected.\nImporting different versions of FBX files than the SDK version can cause undesirable results.\n\nFile Version: {0}\nSDK Version: {1}" ),
FText::FromString(FileVerStr), FText::FromString(SDKVerStr) );
}
//Cache the current file hash
Md5Hash = FMD5Hash::HashFile(*Filename);
CurPhase = FILEOPENED;
// Destroy the importer
//Importer->Destroy();
return Result;
}
TSet<FbxFileTexture*> GetFbxMaterialTextures(const FbxSurfaceMaterial& Material)
{
TSet<FbxFileTexture*> TextureSet;
int32 TextureIndex;
FBXSDK_FOR_EACH_TEXTURE(TextureIndex)
{
FbxProperty Property = Material.FindProperty(FbxLayerElement::sTextureChannelNames[TextureIndex]);
if (Property.IsValid())
{
//We use auto as the parameter type to allow for a generic lambda accepting both FbxProperty and FbxLayeredTexture
auto AddSrcTextureToSet = [&TextureSet](const auto& InObject) {
int32 NbTextures = InObject.template GetSrcObjectCount<FbxTexture>();
for (int32 TexIndex = 0; TexIndex < NbTextures; ++TexIndex)
{
FbxFileTexture* Texture = InObject.template GetSrcObject<FbxFileTexture>(TexIndex);
if (Texture)
{
TextureSet.Add(Texture);
}
}
};
//Here we have to check if it's layered textures, or just textures:
const int32 LayeredTextureCount = Property.GetSrcObjectCount<FbxLayeredTexture>();
if (LayeredTextureCount > 0)
{
for (int32 LayerIndex = 0; LayerIndex < LayeredTextureCount; ++LayerIndex)
{
if (const FbxLayeredTexture* lLayeredTexture = Property.GetSrcObject<FbxLayeredTexture>(LayerIndex))
{
AddSrcTextureToSet(*lLayeredTexture);
}
}
}
else
{
//no layered texture simply get on the property
AddSrcTextureToSet(Property);
}
}
}
return TextureSet;
}
void FFbxImporter::FixMaterialClashName()
{
const bool bKeepNamespace = GetDefault<UEditorPerProjectUserSettings>()->bKeepFbxNamespace;
FbxArray<FbxSurfaceMaterial*> MaterialArray;
Scene->FillMaterialArray(MaterialArray);
TSet<FString> AllMaterialAndTextureNames;
TSet<FbxFileTexture*> MaterialTextures;
auto FixNameIfNeeded = [this, &AllMaterialAndTextureNames](const FString& AssetName,
TFunctionRef<void(const FString& /*UniqueName*/)> ApplyUniqueNameFunction,
TFunctionRef<FText(const FString& /*UniqueName*/)> GetErrorTextFunction){
FString UniqueName(AssetName);
if (AllMaterialAndTextureNames.Contains(UniqueName))
{
//Use the fbx nameclash 1 convention: NAMECLASH1_KEY
//This will add _ncl1_
FString AssetBaseName = UniqueName + TEXT(NAMECLASH1_KEY);
int32 NameIndex = 1;
do
{
UniqueName = AssetBaseName + FString::FromInt(NameIndex++);
} while (AllMaterialAndTextureNames.Contains(UniqueName));
//Apply the unique name.
ApplyUniqueNameFunction(UniqueName);
if (!GIsAutomationTesting)
{
AddTokenizedErrorMessage(
FTokenizedMessage::Create(EMessageSeverity::Info, GetErrorTextFunction(UniqueName)),
FFbxErrors::Generic_LoadingSceneFailed);
}
}
AllMaterialAndTextureNames.Add(UniqueName);
};
// First rename materials to unique names and gather their texture.
for (int32 MaterialIndex = 0; MaterialIndex < MaterialArray.Size(); ++MaterialIndex)
{
FbxSurfaceMaterial *Material = MaterialArray[MaterialIndex];
FString MaterialName = MakeName(Material->GetName());
MaterialTextures.Append(GetFbxMaterialTextures(*Material));
if (!bKeepNamespace)
{
Material->SetName(TCHAR_TO_UTF8(*MaterialName));
}
FixNameIfNeeded(MaterialName,
[&](const FString& UniqueName) { Material->SetName(TCHAR_TO_UTF8(*UniqueName)); },
[&](const FString& UniqueName) {
return FText::Format(LOCTEXT("FbxImport_MaterialNameClash", "FBX Scene Loading: Found material name clash, name clash can be wrongly reassign at reimport , material '{0}' was renamed '{1}'"), FText::FromString(MaterialName), FText::FromString(UniqueName));
}
);
}
// Then rename make sure the texture have unique names as well.
for (FbxFileTexture* CurrentTexture : MaterialTextures)
{
FString AbsoluteFilename = UTF8_TO_TCHAR(CurrentTexture->GetFileName());
FString TextureName = FPaths::GetBaseFilename(AbsoluteFilename);
TextureName = ObjectTools::SanitizeObjectName(TextureName);
FixNameIfNeeded(TextureName,
[&](const FString& UniqueName) { FbxTextureToUniqueNameMap.Add(CurrentTexture, UniqueName); },
[&](const FString& UniqueName) {
return FText::Format(LOCTEXT("FbxImport_TextureNameClash", "FBX Scene Loading: Found texture name clash, name clash can be wrongly reassign at reimport , texture '{0}' was renamed '{1}'"), FText::FromString(TextureName), FText::FromString(UniqueName));
}
);
}
}
void FFbxImporter::EnsureNodeNameAreValid(const FString& BaseFilename)
{
const bool bKeepNamespace = GetDefault<UEditorPerProjectUserSettings>()->bKeepFbxNamespace;
TSet<FString> AllNodeName;
int32 CurrentNameIndex = 1;
for (int32 NodeIndex = 0; NodeIndex < Scene->GetNodeCount(); ++NodeIndex)
{
FbxNode* Node = Scene->GetNode(NodeIndex);
FString NodeName = UTF8_TO_TCHAR(Node->GetName());
if (NodeName.IsEmpty())
{
do
{
NodeName = TEXT("ncl1_") + FString::FromInt(CurrentNameIndex++);
} while (AllNodeName.Contains(NodeName));
Node->SetName(TCHAR_TO_UTF8(*NodeName));
if (!GIsAutomationTesting)
{
AddTokenizedErrorMessage(
FTokenizedMessage::Create(EMessageSeverity::Warning,
FText::Format(LOCTEXT("FbxImport_NoNodeName", "FBX File Loading: Found node with no name, new node name is '{0}'"), FText::FromString(NodeName))),
FFbxErrors::Generic_LoadingSceneFailed);
}
}
if (bKeepNamespace)
{
if (NodeName.Contains(TEXT(":")))
{
NodeName = NodeName.Replace(TEXT(":"), TEXT("_"));
Node->SetName(TCHAR_TO_UTF8(*NodeName));
}
}
// Do not allow node to be named same as filename as this creates problems later on (reimport)
if (AllNodeName.Contains(NodeName) || (ImportOptions->bImportScene && 0 == NodeName.Compare(BaseFilename, ESearchCase::IgnoreCase)))
{
FString UniqueNodeName;
do
{
UniqueNodeName = NodeName + FString::FromInt(CurrentNameIndex++);
} while (AllNodeName.Contains(UniqueNodeName));
FbxString UniqueName(TCHAR_TO_UTF8(*UniqueNodeName));
NodeUniqueNameToOriginalNameMap[UniqueName] = Node->GetName();
Node->SetName(UniqueName);
if (!GIsAutomationTesting)
{
AddTokenizedErrorMessage(
FTokenizedMessage::Create(EMessageSeverity::Info,
FText::Format(LOCTEXT("FbxImport_NodeNameClash", "FBX File Loading: Found name clash, node '{0}' was renamed to '{1}'"), FText::FromString(NodeName), FText::FromString(UniqueNodeName))),
FFbxErrors::Generic_LoadingSceneFailed);
}
}
AllNodeName.Add(NodeName);
}
}
void FFbxImporter::RemoveFBXMetaData(const UObject* Object)
{
TArray<FName> KeysToRemove;
if (const TMap<FName, FString>* ExistingUMetaDataTagValues = FMetaData::GetMapForObject(Object))
{
for (const TPair<FName, FString>& KeyValue : *ExistingUMetaDataTagValues)
{
if (KeyValue.Key.ToString().StartsWith(FBX_METADATA_PREFIX, ESearchCase::IgnoreCase))
{
KeysToRemove.Add(KeyValue.Key);
}
}
}
if (KeysToRemove.Num() > 0)
{
FMetaData& PackageMetaData = Object->GetPackage()->GetMetaData();
for (const FName& KeyToRemove : KeysToRemove)
{
PackageMetaData.RemoveValue(Object, KeyToRemove);
}
}
}
FString FFbxImporter::GetFileAxisDirection()
{
FString AxisDirection;
int32 Sign = 1;
switch (FileAxisSystem.GetUpVector(Sign))
{
case FbxAxisSystem::eXAxis:
{
AxisDirection += TEXT("X");
}
break;
case FbxAxisSystem::eYAxis:
{
AxisDirection += TEXT("Y");
}
break;
case FbxAxisSystem::eZAxis:
{
AxisDirection += TEXT("Z");
}
break;
}
//Negative sign mean down instead of up
AxisDirection += Sign == 1 ? TEXT("-UP") : TEXT("-DOWN");
switch (FileAxisSystem.GetCoorSystem())
{
case FbxAxisSystem::eLeftHanded:
{
AxisDirection += TEXT(" (LH)");
}
break;
case FbxAxisSystem::eRightHanded:
{
AxisDirection += TEXT(" (RH)");
}
break;
}
return AxisDirection;
}
#ifdef IOS_REF
#undef IOS_REF
#define IOS_REF (*(SdkManager->GetIOSettings()))
#endif
bool FFbxImporter::ImportFile(FString Filename, bool bPreventMaterialNameClash /*=false*/)
{
if (Scene)
{
UE_LOG(LogFbx, Error, TEXT("FBX Scene already loaded from %s"), *Filename);
return false;
}
TRACE_CPUPROFILER_EVENT_SCOPE(FFbxImporter::ImportFile);
bool Result = true;
bool bStatus;
FileBasePath = FPaths::GetPath(Filename);
// Create the Scene
Scene = FbxScene::Create(SdkManager,"");
UE_LOG(LogFbx, Log, TEXT("Loading FBX Scene from %s"), *Filename);
int32 FileMajor, FileMinor, FileRevision;
IOS_REF.SetBoolProp(IMP_FBX_MATERIAL, true);
IOS_REF.SetBoolProp(IMP_FBX_TEXTURE, true);
IOS_REF.SetBoolProp(IMP_FBX_LINK, true);
IOS_REF.SetBoolProp(IMP_FBX_SHAPE, true);
IOS_REF.SetBoolProp(IMP_FBX_GOBO, true);
IOS_REF.SetBoolProp(IMP_FBX_ANIMATION, true);
IOS_REF.SetBoolProp(IMP_SKINS, true);
IOS_REF.SetBoolProp(IMP_DEFORMATION, true);
IOS_REF.SetBoolProp(IMP_FBX_GLOBAL_SETTINGS, true);
IOS_REF.SetBoolProp(IMP_TAKE, true);
// Import the scene.
bStatus = Importer->Import(Scene);
const bool bRemovePath = true;
EnsureNodeNameAreValid(FPaths::GetBaseFilename(Filename, bRemovePath));
//Make sure we don't have name clash for materials
if (bPreventMaterialNameClash)
{
FixMaterialClashName();
}
// Get the version number of the FBX file format.
Importer->GetFileVersion(FileMajor, FileMinor, FileRevision);
FbxFileVersion = FString::Printf(TEXT("%d.%d.%d"), FileMajor, FileMinor, FileRevision);
FbxFileCreator = UTF8_TO_TCHAR(Importer->GetFileHeaderInfo()->mCreator.Buffer());
// output result
if(bStatus)
{
UE_LOG(LogFbx, Log, TEXT("FBX Scene Loaded Succesfully"));
CurPhase = IMPORTED;
}
else
{
ErrorMessage = UTF8_TO_TCHAR(Importer->GetStatus().GetErrorString());
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_FileLoadingFailed", "FBX Scene Loading Failed : '{0}'"), FText::FromString(ErrorMessage))), FFbxErrors::Generic_LoadingSceneFailed);
// ReleaseScene will also release the importer if it was initialized
ReleaseScene();
Result = false;
CurPhase = NOTSTARTED;
return Result;
}
const FbxGlobalSettings& GlobalSettings = Scene->GetGlobalSettings();
FbxTime::EMode TimeMode = GlobalSettings.GetTimeMode();
//Set the original framerate from the current fbx file
OriginalFbxFramerate = FbxTime::GetFrameRate(TimeMode);
return Result;
}
void FFbxImporter::ConvertScene()
{
TRACE_CPUPROFILER_EVENT_SCOPE(FFbxImporter::ConvertScene);
//Merge the anim stack before the conversion since the above 0 layer will not be converted
int32 AnimStackCount = Scene->GetSrcObjectCount<FbxAnimStack>();
//Merge the animation stack layer before converting the scene
for (int32 AnimStackIndex = 0; AnimStackIndex < AnimStackCount; AnimStackIndex++)
{
FbxAnimStack* CurAnimStack = Scene->GetSrcObject<FbxAnimStack>(AnimStackIndex);
if (CurAnimStack->GetMemberCount() > 1)
{
int32 ResampleRate = GetGlobalAnimStackSampleRate(CurAnimStack);
MergeAllLayerAnimation(CurAnimStack, ResampleRate);
}
}
//Set the original file information
FileAxisSystem = Scene->GetGlobalSettings().GetAxisSystem();
FileUnitSystem = Scene->GetGlobalSettings().GetSystemUnit();
FbxAMatrix AxisConversionMatrix;
AxisConversionMatrix.SetIdentity();
FbxAMatrix JointOrientationMatrix;
JointOrientationMatrix.SetIdentity();
if (GetImportOptions()->bConvertScene)
{
// we use -Y as forward axis here when we import. This is odd considering our forward axis is technically +X
// but this is to mimic Maya/Max behavior where if you make a model facing +X facing,
// when you import that mesh, you want +X facing in engine.
// only thing that doesn't work is hand flipping because Max/Maya is RHS but UE is LHS
// On the positive note, we now have import transform set up you can do to rotate mesh if you don't like default setting
FbxAxisSystem::ECoordSystem CoordSystem = FbxAxisSystem::eRightHanded;
FbxAxisSystem::EUpVector UpVector = FbxAxisSystem::eZAxis;
FbxAxisSystem::EFrontVector FrontVector = (FbxAxisSystem::EFrontVector) - FbxAxisSystem::eParityOdd;
if (GetImportOptions()->bForceFrontXAxis)
{
FrontVector = FbxAxisSystem::eParityEven;
}
FbxAxisSystem UnrealImportAxis(UpVector, FrontVector, CoordSystem);
FbxAxisSystem SourceSetup = Scene->GetGlobalSettings().GetAxisSystem();
if (SourceSetup != UnrealImportAxis)
{
FbxRootNodeUtility::RemoveAllFbxRoots(Scene);
UnrealImportAxis.ConvertScene(Scene);
FbxAMatrix SourceMatrix;
SourceSetup.GetMatrix(SourceMatrix);
FbxAMatrix UE4Matrix;
UnrealImportAxis.GetMatrix(UE4Matrix);
AxisConversionMatrix = SourceMatrix.Inverse() * UE4Matrix;
if (GetImportOptions()->bForceFrontXAxis)
{
JointOrientationMatrix.SetR(FbxVector4(-90.0, -90.0, 0.0));
}
}
}
FFbxDataConverter::SetJointPostConversionMatrix(JointOrientationMatrix);
FFbxDataConverter::SetAxisConversionMatrix(AxisConversionMatrix);
// Convert the scene's units to what is used in this program, if needed.
// The base unit used in both FBX and Unreal is centimeters. So unless the units
// are already in centimeters (ie: scalefactor 1.0) then it needs to be converted
if (GetImportOptions()->bConvertSceneUnit && Scene->GetGlobalSettings().GetSystemUnit() != FbxSystemUnit::cm)
{
FbxSystemUnit::cm.ConvertScene(Scene);
}
//Reset all the transform evaluation cache since we change some node transform
Scene->GetAnimationEvaluator()->Reset();
}
//-------------------------------------------------------------------------
//
//-------------------------------------------------------------------------
bool FFbxImporter::ReadHeaderFromFile(const FString& Filename, bool bPreventMaterialNameClash /*= false*/)
{
bool Result = true;
switch (CurPhase)
{
case NOTSTARTED:
if (!OpenFile(FString(Filename)))
{
Result = false;
break;
}
case FILEOPENED:
if (!ImportFile(FString(Filename), bPreventMaterialNameClash))
{
Result = false;
CurPhase = NOTSTARTED;
break;
}
}
return Result;
}
//-------------------------------------------------------------------------
//
//-------------------------------------------------------------------------
bool FFbxImporter::ImportFromFile(const FString& Filename, const FString& Type, bool bPreventMaterialNameClash /*= false*/)
{
FFbxScopedOperation ScopedImportOperation(this);
bool Result = true;
switch (CurPhase)
{
case NOTSTARTED:
if (!OpenFile(FString(Filename)))
{
Result = false;
break;
}
case FILEOPENED:
if (!ImportFile(FString(Filename), bPreventMaterialNameClash))
{
Result = false;
CurPhase = NOTSTARTED;
break;
}
case IMPORTED:
{
static const FString Obj(TEXT("obj"));
// The imported axis system is unknown for obj files
if( !Type.Equals( Obj, ESearchCase::IgnoreCase ) )
{
//Convert the scene
ConvertScene();
// Run Analytics for FBX Import data
/**
* @EventName Editor.Usage.FBX
* @Trigger Fires when the user clicks OK in the FBX Import Dialog
* @Type Editor
* @EventParam LastSavedVendor string Returns the name of the vendor that manufactures the last application used to modify the imported FBX
* @EventParam LastSavedAppName string Returns the name of the last application used to modify the imported FBX
* @EventParam LastSavedAppVersion string Returns the revision of the last application used to modify the imported FBX
* @EventParam FBXFileVersion string Returns the FBX SDK used to generate the imported FBX
* @EventParam ImportType string Returns the mesh data type (Static, Skeletal, Animation) being imported from the FBX
* @EventParam ConvertScene boolean Returns whether the import fbx should be converted to unreal axis system
* @EventParam ConvertSceneUnit boolean Returns whether the import fbx should converted the unit to unreal unit (cm)
* @EventParam ForceFrontXAxis boolean Returns whether the import fbx should be converted to unreal axis system with front axis being X
* @EventParam ImportMaterials boolean Returns whether the importer should create the missing materials
* @EventParam ImportTextures boolean Returns whether the importer should create the missing textures
* @EventParam InvertNormalMap boolean Returns whether the importer should inverse the incoming normal map textures
* @EventParam RemoveNameSpace boolean Returns whether the importer should remove namespace on all nodes name
* @EventParam UsedAsFullName boolean Returns whether the importer should use the filename to name the imported mesh
* @EventParam ImportTranslation string Returns the translation vector apply on the import data
* @EventParam ImportRotation string Returns the FRotator vector apply on the import data
* @EventParam ImportUniformScale float Returns the uniform scale apply on the import data
* @EventParam MaterialBasePath string Returns the path pointing on the base material use to import material instance
* @EventParam MaterialSearchLocation string Returns the scope of the search for existing materials (if material not found it can create one depending on bImportMaterials value)
* @EventParam ReorderMaterialToFbxOrder boolean returns weather the importer should reorder the materials in the same order has the fbx file
* @EventParam AutoGenerateCollision boolean Returns whether the importer should create collision primitive
* @EventParam CombineToSingle boolean Returns whether the importer should combine all mesh part together or import many meshes
* @EventParam BakePivotInVertex boolean Returns whether the importer should bake the fbx mesh pivot into the vertex position
* @EventParam TransformVertexToAbsolute boolean Returns whether the importer should bake the global fbx node transform into the vertex position
* @EventParam ImportRigidMesh boolean Returns whether the importer should try to create a rigid mesh (static mesh import as skeletal mesh)
* @EventParam NormalImportMethod string Return if the tangents or normal should be imported or compute
* @EventParam NormalGenerationMethod string Return tangents generation method
* @EventParam CreatePhysicsAsset boolean Returns whether the importer should create the physic asset
* @EventParam ImportAnimations boolean Returns whether the importer should import also the animation
* @EventParam ImportAsSkeletalGeometry boolean Returns whether the importer should import only the geometry
* @EventParam ImportAsSkeletalSkinning boolean Returns whether the importer should import only the skinning
* @EventParam ImportMeshesInBoneHierarchy boolean Returns whether the importer should import also the mesh found in the bone hierarchy
* @EventParam ImportMorph boolean Returns whether the importer should import the morph targets
* @EventParam ImportSkeletalMeshLODs boolean Returns whether the importer should import the LODs
* @EventParam PreserveSmoothingGroups boolean Returns whether the importer should import the smoothing groups
* @EventParam UpdateSkeletonReferencePose boolean Returns whether the importer should update the skeleton reference pose
* @EventParam UseT0AsRefPose boolean Returns whether the importer should use the the animation 0 time has the reference pose
* @EventParam ThresholdPosition float Returns the threshold delta to weld vertices
* @EventParam ThresholdTangentNormal float Returns the threshold delta to weld tangents and normals
* @EventParam ThresholdUV float Returns the threshold delta to weld UVs
* @EventParam MorphThresholdPosition float Returns the morph target threshold delta to compute deltas
* @EventParam AutoComputeLodDistances boolean Returns whether the importer should set the auto compute LOD distance
* @EventParam LodNumber integer Returns the LOD number we should have after the import
* @EventParam BuildReversedIndexBuffer boolean Returns whether the importer should fill the reverse index buffer when building the static mesh
* @EventParam GenerateLightmapUVs boolean Returns whether the importer should generate light map UVs
* @EventParam ImportStaticMeshLODs boolean Returns whether the importer should import the LODs
* @EventParam RemoveDegenerates boolean Returns whether the importer should remove the degenerated triangles when building the static mesh
* @EventParam MinimumLodNumber integer Returns the minimum LOD use by the rendering
* @EventParam StaticMeshLODGroup string Returns the LOD Group settings we use to build this imported static mesh
* @EventParam VertexColorImportOption string Returns how the importer should import the vertex color
* @EventParam VertexOverrideColor string Returns the color use if we need to override the vertex color
* @EventParam AnimationLengthImportType string Returns how we choose the animation time span
* @EventParam DeleteExistingMorphTargetCurves boolean Returns whether the importer should delete the existing morph target curves
* @EventParam AnimationRange string Returns the range of animation the importer should sample if the time span is custom
* @EventParam DoNotImportCurveWithZero boolean Returns whether the importer should import curves containing only zero value
* @EventParam ImportBoneTracks boolean Returns whether the importer should import the bone tracks
* @EventParam ImportCustomAttribute boolean Returns whether the importer should import the custom attribute curves
* @EventParam PreserveLocalTransform boolean Returns whether the importer should preserve the local transform when importing the animation
* @EventParam RemoveRedundantKeys boolean Returns whether the importer should remove all redundant key in an animation
* @EventParam Resample boolean Returns whether the importer should re-sample the animation
* @EventParam SetMaterialDriveParameterOnCustomAttribute boolean Returns whether the importer should hook all custom attribute curve to unreal material attribute
* @EventParam SetMaterialDriveParameterOnCustomAttribute boolean Returns whether the importer should hook some custom attribute (having the suffix) curve to unreal material attribute
* @EventParam ResampleRate float Returns the rate the exporter is suppose to re-sample any imported animations
*
* @Owner Alexis.Matte
*/
FbxDocumentInfo* DocInfo = Scene->GetSceneInfo();
if (DocInfo)
{
if( FEngineAnalytics::IsAvailable() )
{
const static UEnum* FBXImportTypeEnum = StaticEnum<EFBXImportType>();
const static UEnum* FBXAnimationLengthImportTypeEnum = StaticEnum<EFBXAnimationLengthImportType>();
const static UEnum* MaterialSearchLocationEnum = StaticEnum<EMaterialSearchLocation>();
const static UEnum* FBXNormalGenerationMethodEnum = StaticEnum<EFBXNormalGenerationMethod::Type>();
const static UEnum* FBXNormalImportMethodEnum = StaticEnum<EFBXNormalImportMethod>();
const static UEnum* VertexColorImportOptionEnum = StaticEnum<EVertexColorImportOption::Type>();
TArray<FAnalyticsEventAttribute> Attribs;
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()));
Attribs.Add(FAnalyticsEventAttribute(TEXT("LastSaved Application Vendor"), LastSavedVendor));
Attribs.Add(FAnalyticsEventAttribute(TEXT("LastSaved Application Name"), LastSavedAppName));
Attribs.Add(FAnalyticsEventAttribute(TEXT("LastSaved Application Version"), LastSavedAppVersion));
Attribs.Add(FAnalyticsEventAttribute(TEXT("FBX Version"), FbxFileVersion));
//////////////////////////////////////////////////////////////////////////
//FBX import options
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt ImportType"), FBXImportTypeEnum->GetNameStringByValue(ImportOptions->ImportType)));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt ConvertScene"), ImportOptions->bConvertScene));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt ConvertSceneUnit"), ImportOptions->bConvertSceneUnit));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt ForceFrontXAxis"), ImportOptions->bForceFrontXAxis));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt ImportMaterials"), ImportOptions->bImportMaterials));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt ImportTextures"), ImportOptions->bImportTextures));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt InvertNormalMap"), ImportOptions->bInvertNormalMap));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt RemoveNameSpace"), ImportOptions->bRemoveNameSpace));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt UsedAsFullName"), ImportOptions->bUsedAsFullName));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt ImportTranslation"), ImportOptions->ImportTranslation.ToString()));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt ImportRotation"), ImportOptions->ImportRotation.ToString()));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt ImportUniformScale"), ImportOptions->ImportUniformScale));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt MaterialBasePath"), ImportOptions->MaterialBasePath));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt MaterialSearchLocation"), MaterialSearchLocationEnum->GetNameStringByValue((uint64)(ImportOptions->MaterialSearchLocation))));
Attribs.Add(FAnalyticsEventAttribute(TEXT("GenOpt ReorderMaterialToFbxOrder"), ImportOptions->bReorderMaterialToFbxOrder));
//We cant capture a this member, so just assign the pointer here
FBXImportOptions* CaptureImportOptions = ImportOptions;
auto AddMeshAnalytic = [&Attribs, &CaptureImportOptions]()
{
Attribs.Add(FAnalyticsEventAttribute(TEXT("MeshOpt AutoGenerateCollision"), CaptureImportOptions->bAutoGenerateCollision));
Attribs.Add(FAnalyticsEventAttribute(TEXT("MeshOpt CombineToSingle"), CaptureImportOptions->bCombineToSingle));
Attribs.Add(FAnalyticsEventAttribute(TEXT("MeshOpt BakePivotInVertex"), CaptureImportOptions->bBakePivotInVertex));
Attribs.Add(FAnalyticsEventAttribute(TEXT("MeshOpt TransformVertexToAbsolute"), CaptureImportOptions->bTransformVertexToAbsolute));
Attribs.Add(FAnalyticsEventAttribute(TEXT("MeshOpt ImportRigidMesh"), CaptureImportOptions->bImportRigidMesh));
Attribs.Add(FAnalyticsEventAttribute(TEXT("MeshOpt NormalGenerationMethod"), FBXNormalGenerationMethodEnum->GetNameStringByValue(CaptureImportOptions->NormalGenerationMethod)));
Attribs.Add(FAnalyticsEventAttribute(TEXT("MeshOpt NormalImportMethod"), FBXNormalImportMethodEnum->GetNameStringByValue(CaptureImportOptions->NormalImportMethod)));
Attribs.Add(FAnalyticsEventAttribute(TEXT("MeshOpt ComputeWeightedNormals"), CaptureImportOptions->bComputeWeightedNormals));
};
auto AddSKAnalytic = [&Attribs, &CaptureImportOptions]()
{
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt CreatePhysicsAsset"), CaptureImportOptions->bCreatePhysicsAsset));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt ImportAnimations"), CaptureImportOptions->bImportAnimations));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt ImportAsSkeletalGeometry"), CaptureImportOptions->bImportAsSkeletalGeometry));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt ImportAsSkeletalSkinning"), CaptureImportOptions->bImportAsSkeletalSkinning));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt ImportMeshesInBoneHierarchy"), CaptureImportOptions->bImportMeshesInBoneHierarchy));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt ImportMorph"), CaptureImportOptions->bImportMorph));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt ImportSkeletalMeshLODs"), CaptureImportOptions->bImportSkeletalMeshLODs));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt PreserveSmoothingGroups"), CaptureImportOptions->bPreserveSmoothingGroups));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt KeepSectionsSeparate"), CaptureImportOptions->bKeepSectionsSeparate));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt UpdateSkeletonReferencePose"), CaptureImportOptions->bUpdateSkeletonReferencePose));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt UseT0AsRefPose"), CaptureImportOptions->bUseT0AsRefPose));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt OverlappingThresholds.ThresholdPosition"), CaptureImportOptions->OverlappingThresholds.ThresholdPosition));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt OverlappingThresholds.ThresholdTangentNormal"), CaptureImportOptions->OverlappingThresholds.ThresholdTangentNormal));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt OverlappingThresholds.ThresholdUV"), CaptureImportOptions->OverlappingThresholds.ThresholdUV));
Attribs.Add(FAnalyticsEventAttribute(TEXT("SkeletalMeshOpt OverlappingThresholds.MorphThresholdPosition"), CaptureImportOptions->OverlappingThresholds.MorphThresholdPosition));
};
auto AddSMAnalytic = [&Attribs, &CaptureImportOptions]()
{
Attribs.Add(FAnalyticsEventAttribute(TEXT("StaticMeshOpt AutoComputeLodDistances"), CaptureImportOptions->bAutoComputeLodDistances));
Attribs.Add(FAnalyticsEventAttribute(TEXT("StaticMeshOpt LodNumber"), CaptureImportOptions->LodNumber));
Attribs.Add(FAnalyticsEventAttribute(TEXT("StaticMeshOpt BuildReversedIndexBuffer"), CaptureImportOptions->bBuildReversedIndexBuffer));
Attribs.Add(FAnalyticsEventAttribute(TEXT("StaticMeshOpt GenerateLightmapUVs"), CaptureImportOptions->bGenerateLightmapUVs));
Attribs.Add(FAnalyticsEventAttribute(TEXT("StaticMeshOpt ImportStaticMeshLODs"), CaptureImportOptions->bImportStaticMeshLODs));
Attribs.Add(FAnalyticsEventAttribute(TEXT("StaticMeshOpt RemoveDegenerates"), CaptureImportOptions->bRemoveDegenerates));
Attribs.Add(FAnalyticsEventAttribute(TEXT("StaticMeshOpt MinimumLodNumber"), CaptureImportOptions->MinimumLodNumber));
Attribs.Add(FAnalyticsEventAttribute(TEXT("StaticMeshOpt StaticMeshLODGroup"), CaptureImportOptions->StaticMeshLODGroup));
Attribs.Add(FAnalyticsEventAttribute(TEXT("StaticMeshOpt VertexColorImportOption"), VertexColorImportOptionEnum->GetNameStringByValue(CaptureImportOptions->VertexColorImportOption)));
Attribs.Add(FAnalyticsEventAttribute(TEXT("StaticMeshOpt VertexOverrideColor"), CaptureImportOptions->VertexOverrideColor.ToString()));
};
auto AddAnimAnalytic = [&Attribs, &CaptureImportOptions]()
{
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt AnimationLengthImportType"), FBXAnimationLengthImportTypeEnum->GetNameStringByValue(CaptureImportOptions->AnimationLengthImportType)));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt DeleteExistingMorphTargetCurves"), CaptureImportOptions->bDeleteExistingMorphTargetCurves));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt AnimationRange"), CaptureImportOptions->AnimationRange.ToString()));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt DoNotImportCurveWithZero"), CaptureImportOptions->bDoNotImportCurveWithZero));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt ImportBoneTracks"), CaptureImportOptions->bImportBoneTracks));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt ImportCustomAttribute"), CaptureImportOptions->bImportCustomAttribute));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt DeleteExistingCustomAttributeCurves"), CaptureImportOptions->bDeleteExistingCustomAttributeCurves));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt DeleteExistingNonCurveCustomAttributes"), CaptureImportOptions->bDeleteExistingNonCurveCustomAttributes));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt PreserveLocalTransform"), CaptureImportOptions->bPreserveLocalTransform));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt RemoveRedundantKeys"), CaptureImportOptions->bRemoveRedundantKeys));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt Resample"), CaptureImportOptions->bResample));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt SetMaterialDriveParameterOnCustomAttribute"), CaptureImportOptions->bSetMaterialDriveParameterOnCustomAttribute));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt MaterialCurveSuffixes"), CaptureImportOptions->MaterialCurveSuffixes));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt ResampleRate"), CaptureImportOptions->ResampleRate));
Attribs.Add(FAnalyticsEventAttribute(TEXT("AnimOpt SnapToClosestFrameBoundary"), CaptureImportOptions->bSnapToClosestFrameBoundary));
};
if (ImportOptions->ImportType == FBXIT_SkeletalMesh)
{
AddMeshAnalytic();
AddSKAnalytic();
if (ImportOptions->bImportAnimations)
{
AddAnimAnalytic();
}
}
else if (ImportOptions->ImportType == FBXIT_StaticMesh)
{
AddMeshAnalytic();
AddSMAnalytic();
}
else if (ImportOptions->ImportType == FBXIT_Animation)
{
AddAnimAnalytic();
}
FString EventString = FString::Printf(TEXT("Editor.Usage.FBX.Import"));
FEngineAnalytics::GetProvider().RecordEvent(EventString, Attribs);
}
}
}
//Warn the user if there is some geometry that cannot be imported because they are not reference by any scene node attribute
ValidateAllMeshesAreReferenceByNodeAttribute();
ConvertLodPrefixToLodGroup();
MeshNamesCache.Empty();
CurPhase = FIXEDANDCONVERTED;
break;
}
case FIXEDANDCONVERTED:
default:
break;
}
return Result;
}
void FFbxImporter::SetScene(FbxScene* InScene)
{
ClearAllCaches();
Scene = InScene;
FbxCreator = EFbxCreator::Unknow;
}
FString FFbxImporter::MakeName(const ANSICHAR* Name)
{
const TCHAR SpecialChars[] = {TEXT('.'), TEXT(','), TEXT('/'), TEXT('`'), TEXT('%')};
FString TmpName = MakeString(Name);
// Remove namespaces
int32 LastNamespaceTokenIndex = INDEX_NONE;
if (TmpName.FindLastChar(TEXT(':'), LastNamespaceTokenIndex))
{
//+1 to remove the ':' character we found
TmpName.RightChopInline(LastNamespaceTokenIndex + 1, EAllowShrinking::Yes);
}
//Remove the special chars
for ( int32 i = 0; i < UE_ARRAY_COUNT(SpecialChars); i++ )
{
TmpName.ReplaceCharInline(SpecialChars[i], TEXT('_'), ESearchCase::CaseSensitive);
}
return TmpName;
}
FString FFbxImporter::MakeString(const ANSICHAR* Name)
{
return FString(UTF8_TO_TCHAR(Name));
}
FName FFbxImporter::MakeNameForMesh(FString InName, FbxObject* FbxObject)
{
FName OutputName;
//Cant name the mesh if the object is null and there InName arguments is None.
check(FbxObject != nullptr || InName != TEXT("None"))
if ((ImportOptions->bUsedAsFullName || FbxObject == nullptr) && InName != TEXT("None"))
{
OutputName = *InName;
}
else
{
check(FbxObject);
char Name[MAX_SPRINTF];
int SpecialChars[] = {'.', ',', '/', '`', '%'};
FCStringAnsi::Sprintf(Name, "%s", FbxObject->GetName());
for ( int32 i = 0; i < 5; i++ )
{
char* CharPtr = Name;
while ( (CharPtr = FCStringAnsi::Strchr(CharPtr,SpecialChars[i])) != nullptr )
{
CharPtr[0] = '_';
}
}
// for mesh, replace ':' with '_' because Unreal doesn't support ':' in mesh name
char* NewName = FCStringAnsi::Strchr(Name, ':');
if (NewName)
{
char* Tmp;
Tmp = NewName;
while (Tmp)
{
// Always remove namespaces
NewName = Tmp + 1;
// there may be multiple namespace, so find the last ':'
Tmp = FCStringAnsi::Strchr(NewName + 1, ':');
}
}
else
{
NewName = Name;
}
int32 NameCount = 0;
FString ComposeName;
do
{
if ( InName == FString("None"))
{
ComposeName = FString::Printf(TEXT("%s"), UTF8_TO_TCHAR(NewName ));
}
else
{
ComposeName = FString::Printf(TEXT("%s_%s"), *InName,UTF8_TO_TCHAR(NewName));
}
if (NameCount > 0)
{
ComposeName += TEXT("_") + FString::FromInt(NameCount);
}
NameCount++;
} while (MeshNamesCache.Contains(ComposeName));
OutputName = FName(*ComposeName);
}
MeshNamesCache.Add(OutputName.ToString());
return OutputName;
}
/* The score is the difference between name lower score is better*/
int32 GetNodeNameDiffScore(const char* MeshName, const FbxNode* FbxMeshNode)
{
if (FbxMeshNode == nullptr || MeshName == nullptr)
{
return INDEX_NONE;
}
int32 MeshNameLen = FCStringAnsi::Strlen(MeshName);
if (MeshNameLen == 0)
{
return INDEX_NONE;
}
const char* FbxNodeName = FbxMeshNode->GetName();
int32 FbxNodeNameLen = FCStringAnsi::Strlen(FbxNodeName);
if (FbxNodeNameLen == 0 || FbxNodeNameLen > MeshNameLen)
{
return INDEX_NONE;
}
// The name of Unreal mesh may have a prefix, so we match from end
int32 i = 0;
const char* MeshNamePtr = MeshName + MeshNameLen - 1;
const char* FbxMeshPtr = FbxNodeName + FbxNodeNameLen - 1;
while (i < FbxNodeNameLen)
{
bool bIsPointAndUnderscore = *FbxMeshPtr == '.' && *MeshNamePtr == '_';
if (*MeshNamePtr != *FbxMeshPtr && !bIsPointAndUnderscore)
{
break;
}
else
{
i++;
MeshNamePtr--;
FbxMeshPtr--;
}
}
if (i == FbxNodeNameLen) // matched
{
if (FbxNodeNameLen == MeshNameLen) // the name of Unreal mesh is full match
{
return 0;
}
// The name of Unreal mesh has a prefix
if (*MeshNamePtr == '_')
{
return (MeshNameLen - i);
}
}
return INDEX_NONE;
}
FbxNode* FFbxImporter::GetMeshNodesFromName(const FString& ReimportMeshName, TArray<FbxNode*>& FbxMeshArray)
{
char MeshName[2048];
FCStringAnsi::Strncpy(MeshName, TCHAR_TO_UTF8(*ReimportMeshName), UE_ARRAY_COUNT(MeshName));
// find the Fbx mesh node that the Unreal Mesh matches according to name
int32 BestMatchIndex = INDEX_NONE;
int32 BestDiffScore = MAX_int32;
for (int32 MeshIndex = 0; MeshIndex < FbxMeshArray.Num(); MeshIndex++)
{
int32 DiffScore = GetNodeNameDiffScore(MeshName, FbxMeshArray[MeshIndex]);
if (DiffScore != INDEX_NONE && DiffScore < BestDiffScore)
{
BestMatchIndex = MeshIndex;
BestDiffScore = DiffScore;
}
}
return FbxMeshArray.IsValidIndex(BestMatchIndex) ? FbxMeshArray[BestMatchIndex] : nullptr;
}
FbxAMatrix FFbxImporter::ComputeSkeletalMeshTotalMatrix(FbxNode* Node, FbxNode *RootSkeletalNode)
{
if (ImportOptions->bImportScene && !ImportOptions->bTransformVertexToAbsolute && RootSkeletalNode != nullptr && RootSkeletalNode != Node)
{
FbxAMatrix GlobalTransform = Scene->GetAnimationEvaluator()->GetNodeGlobalTransform(Node);
FbxAMatrix GlobalSkeletalMeshRootTransform = Scene->GetAnimationEvaluator()->GetNodeGlobalTransform(RootSkeletalNode);
FbxAMatrix TotalMatrix = GlobalSkeletalMeshRootTransform.Inverse() * GlobalTransform;
return TotalMatrix;
}
return ComputeTotalMatrix(Node);
}
FbxAMatrix FFbxImporter::ComputeTotalMatrix(FbxNode* Node)
{
FbxAMatrix Geometry;
FbxVector4 Translation, Rotation, Scaling;
Translation = Node->GetGeometricTranslation(FbxNode::eSourcePivot);
Rotation = Node->GetGeometricRotation(FbxNode::eSourcePivot);
Scaling = Node->GetGeometricScaling(FbxNode::eSourcePivot);
Geometry.SetT(Translation);
Geometry.SetR(Rotation);
Geometry.SetS(Scaling);
//For Single Matrix situation, obtain transfrom matrix from eDESTINATION_SET, which include pivot offsets and pre/post rotations.
FbxAMatrix& GlobalTransform = Scene->GetAnimationEvaluator()->GetNodeGlobalTransform(Node);
//We can bake the pivot only if we don't transform the vertex to the absolute position
if (!ImportOptions->bTransformVertexToAbsolute)
{
if (ImportOptions->bBakePivotInVertex)
{
FbxAMatrix PivotGeometry;
FbxVector4 RotationPivot = Node->GetRotationPivot(FbxNode::eSourcePivot);
FbxVector4 FullPivot;
FullPivot[0] = -RotationPivot[0];
FullPivot[1] = -RotationPivot[1];
FullPivot[2] = -RotationPivot[2];
PivotGeometry.SetT(FullPivot);
Geometry = Geometry * PivotGeometry;
}
else
{
//No Vertex transform and no bake pivot, it will be the mesh as-is.
Geometry.SetIdentity();
}
}
//We must always add the geometric transform. Only Max use the geometric transform which is an offset to the local transform of the node
FbxAMatrix TotalMatrix = ImportOptions->bTransformVertexToAbsolute ? GlobalTransform * Geometry : Geometry;
return TotalMatrix;
}
bool FFbxImporter::IsOddNegativeScale(FbxAMatrix& TotalMatrix)
{
FbxVector4 Scale = TotalMatrix.GetS();
int32 NegativeNum = 0;
if (Scale[0] < 0) NegativeNum++;
if (Scale[1] < 0) NegativeNum++;
if (Scale[2] < 0) NegativeNum++;
return NegativeNum == 1 || NegativeNum == 3;
}
/**
* Recursively get skeletal mesh count
*
* @param Node Root node to find skeletal meshes
* @return int32 skeletal mesh count
*/
int32 GetFbxSkeletalMeshCount(FbxNode* Node)
{
int32 SkeletalMeshCount = 0;
if (Node->GetMesh() && (Node->GetMesh()->GetDeformerCount(FbxDeformer::eSkin)>0))
{
SkeletalMeshCount = 1;
}
int32 ChildIndex;
for (ChildIndex=0; ChildIndex<Node->GetChildCount(); ++ChildIndex)
{
SkeletalMeshCount += GetFbxSkeletalMeshCount(Node->GetChild(ChildIndex));
}
return SkeletalMeshCount;
}
/**
* Get mesh count (including static mesh and skeletal mesh, except collision models) and find collision models
*
* @param Node Root node to find meshes
* @return int32 mesh count
*/
int32 FFbxImporter::GetFbxMeshCount( FbxNode* Node, bool bCountLODs, int32& OutNumLODGroups )
{
// Is this node an LOD group
bool bLODGroup = Node->GetNodeAttribute() && Node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup;
if( bLODGroup )
{
++OutNumLODGroups;
}
int32 MeshCount = 0;
// Don't count LOD group nodes unless we are ignoring them
if( !bLODGroup || bCountLODs )
{
if (Node->GetMesh())
{
if (!FillCollisionModelList(Node))
{
MeshCount = 1;
}
}
int32 ChildIndex;
for (ChildIndex=0; ChildIndex<Node->GetChildCount(); ++ChildIndex)
{
MeshCount += GetFbxMeshCount(Node->GetChild(ChildIndex),bCountLODs,OutNumLODGroups);
}
}
else
{
// An LOD group should count as one mesh
MeshCount = 1;
}
return MeshCount;
}
/**
* Fill the collision models array by going through all mesh node recursively
*
* @param Node Root node to find collision meshes
*/
void FFbxImporter::FillFbxCollisionMeshArray(FbxNode* Node)
{
if (Node->GetMesh())
{
FillCollisionModelList(Node);
}
int32 ChildIndex;
for (ChildIndex = 0; ChildIndex < Node->GetChildCount(); ++ChildIndex)
{
FillFbxCollisionMeshArray(Node->GetChild(ChildIndex));
}
}
/**
* Get all Fbx mesh objects
*
* @param Node Root node to find meshes
* @param outMeshArray return Fbx meshes
*/
void FFbxImporter::FillFbxMeshArray(FbxNode* Node, TArray<FbxNode*>& outMeshArray, UnFbx::FFbxImporter* FFbxImporter)
{
if (Node->GetMesh())
{
if (!FFbxImporter->FillCollisionModelList(Node) && Node->GetMesh()->GetPolygonVertexCount() > 0)
{
outMeshArray.Add(Node);
}
}
int32 ChildIndex;
for (ChildIndex=0; ChildIndex<Node->GetChildCount(); ++ChildIndex)
{
FillFbxMeshArray(Node->GetChild(ChildIndex), outMeshArray, FFbxImporter);
}
}
void FFbxImporter::FillFbxMeshAndLODGroupArray(FbxNode* Node, TArray<FbxNode*>& outLODGroupArray, TArray<FbxNode*>& outMeshArray)
{
// Is this node an LOD group
bool bLODGroup = Node->GetNodeAttribute() && Node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup;
if (bLODGroup)
{
outLODGroupArray.Add(Node);
//Do not do LOD group childrens
return;
}
if (Node->GetMesh())
{
if (!FillCollisionModelList(Node) && Node->GetMesh()->GetPolygonVertexCount() > 0)
{
outMeshArray.Add(Node);
}
}
// Cycle the childrens
int32 ChildIndex;
for (ChildIndex = 0; ChildIndex < Node->GetChildCount(); ++ChildIndex)
{
FillFbxMeshAndLODGroupArray(Node->GetChild(ChildIndex), outLODGroupArray, outMeshArray);
}
}
/**
* Get all Fbx skeletal mesh objects
*
* @param Node Root node to find skeletal meshes
* @param outSkelMeshArray return Fbx meshes
*/
void FillFbxSkelMeshArray(FbxNode* Node, TArray<FbxNode*>& outSkelMeshArray)
{
if (Node->GetMesh() && Node->GetMesh()->GetDeformerCount(FbxDeformer::eSkin) > 0 )
{
outSkelMeshArray.Add(Node);
}
int32 ChildIndex;
for (ChildIndex=0; ChildIndex<Node->GetChildCount(); ++ChildIndex)
{
FillFbxSkelMeshArray(Node->GetChild(ChildIndex), outSkelMeshArray);
}
}
void FFbxImporter::ValidateAllMeshesAreReferenceByNodeAttribute()
{
TSet< FbxUInt64 > NodeGeometryIds;
NodeGeometryIds.Reserve( Scene->GetNodeCount() );
for (int NodeIndex = 0; NodeIndex < Scene->GetNodeCount(); ++NodeIndex)
{
FbxNode* SceneNode = Scene->GetNode(NodeIndex);
FbxGeometry* NodeGeometry = static_cast<FbxGeometry*>(SceneNode->GetMesh());
if ( NodeGeometry )
{
NodeGeometryIds.Add( NodeGeometry->GetUniqueID() );
}
}
for (int GeoIndex = 0; GeoIndex < Scene->GetGeometryCount(); ++GeoIndex)
{
FbxGeometry* Geometry = Scene->GetGeometry(GeoIndex);
if ( !NodeGeometryIds.Contains( Geometry->GetUniqueID() ) )
{
FString GeometryName = (Geometry->GetName() && Geometry->GetName()[0] != '\0') ? UTF8_TO_TCHAR(Geometry->GetName()) : TEXT("[Geometry have no name]");
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning,
FText::Format(LOCTEXT("FailedToImport_NoObjectLinkToNode", "Mesh {0} in the fbx file is not reference by any hierarchy node."), FText::FromString(GeometryName))),
FFbxErrors::Generic_ImportingNewObjectFailed);
}
}
}
void FFbxImporter::ConvertLodPrefixToLodGroup()
{
IMeshReductionModule& ReductionModule = FModuleManager::Get().LoadModuleChecked<IMeshReductionModule>("MeshReductionInterface");
IMeshReduction* SkeletalMeshReduction = ReductionModule.GetSkeletalMeshReductionInterface();
IMeshReduction* StaticMeshReduction = ReductionModule.GetStaticMeshReductionInterface();
bool bCanReduce = true;
bool bWarnUserNoReduction = false;
if (ImportOptions->ImportType == FBXIT_SkeletalMesh && !SkeletalMeshReduction)
{
bCanReduce = false;
}
if (ImportOptions->ImportType == FBXIT_StaticMesh && !StaticMeshReduction)
{
bCanReduce = false;
}
const FString LodPrefix = TEXT("LOD");
TMap<FString, TArray<uint64>> LodPrefixNodeMap;
TMap<uint64, FbxNode*> NodeMap;
for (int NodeIndex = 0; NodeIndex < Scene->GetNodeCount(); ++NodeIndex)
{
FbxNode *SceneNode = Scene->GetNode(NodeIndex);
if (SceneNode == nullptr)
{
continue;
}
FbxGeometry *NodeGeometry = static_cast<FbxGeometry*>(SceneNode->GetMesh());
if (NodeGeometry && NodeGeometry->GetUniqueID() != SceneNode->GetUniqueID())
{
FString SceneNodeName = UTF8_TO_TCHAR(SceneNode->GetName());
if (SceneNodeName.Len() > 5 && SceneNodeName.StartsWith(LodPrefix, ESearchCase::CaseSensitive) && SceneNodeName[4] == '_')
{
FString LODXNumber = SceneNodeName.RightChop(3).Left(1);
if (LODXNumber.IsNumeric())
{
NodeMap.FindOrAdd(SceneNode->GetUniqueID()) = SceneNode;
int32 LodNumber = FPlatformString::Atoi(*FString(&SceneNodeName[3]));
FString MatchName = SceneNodeName.RightChop(5);
if (SceneNode->GetParent())
{
uint64 ParentUniqueID = SceneNode->GetParent()->GetUniqueID();
FString ParentID = FString::FromInt((int32)ParentUniqueID);
if (ParentUniqueID > MAX_int32)
{
ParentID = FString::FromInt((int32)(ParentUniqueID >> 32)) + FString::FromInt((int32)ParentUniqueID);
}
MatchName += TEXT("_") + ParentID;
}
TArray<uint64>& LodPrefixNodeValues = LodPrefixNodeMap.FindOrAdd(MatchName);
//Add LOD in the correct order
if (LodNumber >= LodPrefixNodeValues.Num())
{
int32 AddCount = LodNumber + 1 - LodPrefixNodeValues.Num();
for (int32 AddIndex = 0; AddIndex < AddCount; ++AddIndex)
{
LodPrefixNodeValues.Add(MAX_uint64);
}
}
LodPrefixNodeValues[LodNumber] = SceneNode->GetUniqueID();
}
}
}
}
for (const auto& Kvp : LodPrefixNodeMap)
{
if (Kvp.Value.Num() <= 1)
{
continue;
}
//Find the first valid node to be able to discover the parent of this LOD Group
const TArray<uint64>& LodGroupNodes = Kvp.Value;
FbxNode* FirstNode = nullptr;
int32 ValidNodeCount = 0;
for (int CurrentLodIndex = 0; CurrentLodIndex < LodGroupNodes.Num(); ++CurrentLodIndex)
{
if (LodGroupNodes[CurrentLodIndex] != MAX_uint64)
{
if (FirstNode == nullptr)
{
FirstNode = NodeMap[LodGroupNodes[CurrentLodIndex]];
}
ValidNodeCount++;
}
}
//Do not create LODGroup with less then two child
if (ValidNodeCount <= 1)
{
continue;
}
check(FirstNode != nullptr);
//Set the parent node, we assume all node in LodGroupNodes have the same parent
FbxNode* ParentNode = FirstNode->GetParent() == nullptr ? Scene->GetRootNode() : FirstNode->GetParent();
if (ParentNode->GetNodeAttribute() && ParentNode->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
{
//LODGroup already exist no need to create one
continue;
}
//Get a valid name for the LODGroup actor
FString FbxNodeName = UTF8_TO_TCHAR(FirstNode->GetName());
FbxNodeName.RightChopInline(5, EAllowShrinking::No);
FbxNodeName += TEXT("_LodGroup");
//Create a LodGroup and child all fbx node to the Group
FbxNode* ActorNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*FbxNodeName));
FString FbxLODGroupName = FbxNodeName + TEXT("Attribute");
FbxLODGroup *FbxLodGroupAttribute = FbxLODGroup::Create(Scene, TCHAR_TO_UTF8(*FbxLODGroupName));
ActorNode->AddNodeAttribute(FbxLodGroupAttribute);
for (int CurrentLodIndex = 0; CurrentLodIndex < LodGroupNodes.Num(); ++CurrentLodIndex)
{
if (LodGroupNodes[CurrentLodIndex] == MAX_uint64)
{
if (bCanReduce)
{
FString FbxGeneratedNodeName = UTF8_TO_TCHAR(FirstNode->GetName());
FbxGeneratedNodeName.RightChopInline(5, EAllowShrinking::No);
FbxGeneratedNodeName += TEXT(GeneratedLODNameSuffix) + FString::FromInt(CurrentLodIndex);
//Generated LOD add dummy FbxNode to tell the import to add such a LOD
FbxNode* DummyGeneratedLODActorNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*FbxGeneratedNodeName));
ActorNode->AddChild(DummyGeneratedLODActorNode);
}
else
{
bWarnUserNoReduction = true;
}
continue;
}
FbxNode* CurrentNode = NodeMap[LodGroupNodes[CurrentLodIndex]];
if (CurrentNode->GetParent() != nullptr)
{
//All parent should be the same for a LOD group
check(ParentNode == CurrentNode->GetParent());
ParentNode->RemoveChild(CurrentNode);
}
ActorNode->AddChild(CurrentNode);
}
//We must have a parent node
check(ParentNode != nullptr);
ParentNode->AddChild(ActorNode);
}
if (bWarnUserNoReduction)
{
FText WarningMessage;
if (ImportOptions->ImportType == FBXIT_SkeletalMesh && !SkeletalMeshReduction)
{
WarningMessage = FText(LOCTEXT("FBX_ImportSkeletalMeshNoReductionModule", "No skeletal mesh reduction module available. Cannot add generated LOD between fbx node LOD prefix."));
}
if (ImportOptions->ImportType == FBXIT_StaticMesh && !StaticMeshReduction)
{
WarningMessage = FText(LOCTEXT("FBX_ImportStaticMeshNoReductionModule", "No static mesh reduction module available. Cannot add generated LOD between fbx node LOD prefix."));
}
AddTokenizedErrorMessage( FTokenizedMessage::Create( EMessageSeverity::Warning, WarningMessage ), FFbxErrors::Generic_Mesh_NoReductionModuleAvailable );
}
}
FbxNode *FFbxImporter::RecursiveGetFirstMeshNode(FbxNode* Node, FbxNode* NodeToFind)
{
if (Node == nullptr)
{
return nullptr;
}
if(Node->GetMesh() != nullptr)
return Node;
for (int32 ChildIndex = 0; ChildIndex < Node->GetChildCount(); ++ChildIndex)
{
FbxNode *MeshNode = RecursiveGetFirstMeshNode(Node->GetChild(ChildIndex), NodeToFind);
if (NodeToFind == nullptr)
{
if (MeshNode != nullptr)
{
return MeshNode;
}
}
else if (MeshNode == NodeToFind)
{
return MeshNode;
}
}
return nullptr;
}
void FFbxImporter::RecursiveGetAllMeshNode(TArray<FbxNode *> &OutAllNode, FbxNode* Node)
{
if (Node == nullptr)
{
return;
}
if (Node->GetMesh() != nullptr)
{
OutAllNode.Add(Node);
return;
}
//Look if its a generated LOD
FString FbxGeneratedNodeName = UTF8_TO_TCHAR(Node->GetName());
if (FbxGeneratedNodeName.Contains(TEXT(GeneratedLODNameSuffix)))
{
FString SuffixSearch = TEXT(GeneratedLODNameSuffix);
int32 SuffixIndex = FbxGeneratedNodeName.Find(SuffixSearch, ESearchCase::CaseSensitive, ESearchDir::FromEnd);
SuffixIndex += SuffixSearch.Len();
FString LODXNumber = FbxGeneratedNodeName.RightChop(SuffixIndex).Left(1);
if (LODXNumber.IsNumeric())
{
OutAllNode.Add(Node);
return;
}
}
for (int32 ChildIndex = 0; ChildIndex < Node->GetChildCount(); ++ChildIndex)
{
RecursiveGetAllMeshNode(OutAllNode, Node->GetChild(ChildIndex));
}
}
FbxNode* FFbxImporter::FindLODGroupNode(FbxNode* NodeLodGroup, int32 LodIndex, FbxNode *NodeToFind)
{
check(NodeLodGroup->GetChildCount() >= LodIndex);
FbxNode *ChildNode = NodeLodGroup->GetChild(LodIndex);
if (ChildNode == nullptr)
{
return nullptr;
}
return RecursiveGetFirstMeshNode(ChildNode, NodeToFind);
}
void FFbxImporter::FindAllLODGroupNode(TArray<FbxNode*> &OutNodeInLod, FbxNode* NodeLodGroup, int32 LodIndex)
{
check(NodeLodGroup->GetChildCount() >= LodIndex);
FbxNode *ChildNode = NodeLodGroup->GetChild(LodIndex);
if (ChildNode == nullptr)
{
return;
}
RecursiveGetAllMeshNode(OutNodeInLod, ChildNode);
}
FbxNode *FFbxImporter::RecursiveFindParentLodGroup(FbxNode *ParentNode)
{
if (ParentNode == nullptr)
return nullptr;
if (ParentNode->GetNodeAttribute() && ParentNode->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
return ParentNode;
return RecursiveFindParentLodGroup(ParentNode->GetParent());
}
void FFbxImporter::RecursiveFixSkeleton(FbxNode* Node, TArray<FbxNode*> &SkelMeshes, bool bImportNestedMeshes )
{
FbxNodeAttribute* Attr = Node->GetNodeAttribute();
bool NodeIsLodGroup = (Attr && (Attr->GetAttributeType() == FbxNodeAttribute::eLODGroup));
if (!NodeIsLodGroup)
{
for (int32 i = 0; i < Node->GetChildCount(); i++)
{
RecursiveFixSkeleton(Node->GetChild(i), SkelMeshes, bImportNestedMeshes);
}
}
if ( Attr && (Attr->GetAttributeType() == FbxNodeAttribute::eMesh || Attr->GetAttributeType() == FbxNodeAttribute::eNull ) )
{
if( bImportNestedMeshes && Attr->GetAttributeType() == FbxNodeAttribute::eMesh )
{
// for leaf mesh, keep them as mesh
int32 ChildCount = Node->GetChildCount();
int32 ChildIndex;
for (ChildIndex = 0; ChildIndex < ChildCount; ChildIndex++)
{
FbxNode* Child = Node->GetChild(ChildIndex);
if (Child->GetMesh() == NULL)
{
break;
}
}
if (ChildIndex != ChildCount)
{
// Remove from the mesh list it is no longer a mesh
SkelMeshes.Remove(Node);
//replace with skeleton
FbxSkeleton* lSkeleton = FbxSkeleton::Create(SdkManager,"");
Node->SetNodeAttribute(lSkeleton);
lSkeleton->SetSkeletonType(FbxSkeleton::eLimbNode);
}
else // this mesh may be not in skeleton mesh list. If not, add it.
{
if( !SkelMeshes.Contains( Node ) )
{
SkelMeshes.Add(Node);
}
}
}
else
{
// Remove from the mesh list it is no longer a mesh
SkelMeshes.Remove(Node);
//replace with skeleton
FbxSkeleton* lSkeleton = FbxSkeleton::Create(SdkManager,"");
Node->SetNodeAttribute(lSkeleton);
lSkeleton->SetSkeletonType(FbxSkeleton::eLimbNode);
}
}
}
FbxNode* FFbxImporter::GetRootSkeleton(FbxNode* Link)
{
FbxNode* RootBone = Link;
// get Unreal skeleton root
// mesh and dummy are used as bone if they are in the skeleton hierarchy
while (RootBone && RootBone->GetParent())
{
bool bIsBlenderArmatureBone = false;
if (FbxCreator == EFbxCreator::Blender)
{
//Hack to support armature dummy node from blender
//Users do not want the null attribute node named armature which is the parent of the real root bone in blender fbx file
//This is a hack since if a rigid mesh group root node is named "armature" it will be skip
const FString RootBoneParentName(RootBone->GetParent()->GetName());
FbxNode *GrandFather = RootBone->GetParent()->GetParent();
bIsBlenderArmatureBone = (GrandFather == nullptr || GrandFather == Scene->GetRootNode()) && (RootBoneParentName.Compare(TEXT("armature"), ESearchCase::IgnoreCase) == 0);
}
FbxNodeAttribute* Attr = RootBone->GetParent()->GetNodeAttribute();
if (Attr &&
(Attr->GetAttributeType() == FbxNodeAttribute::eMesh ||
(Attr->GetAttributeType() == FbxNodeAttribute::eNull && !bIsBlenderArmatureBone) ||
Attr->GetAttributeType() == FbxNodeAttribute::eSkeleton) &&
RootBone->GetParent() != Scene->GetRootNode())
{
// in some case, skeletal mesh can be ancestor of bones
// this avoids this situation
if (Attr->GetAttributeType() == FbxNodeAttribute::eMesh )
{
FbxMesh* Mesh = (FbxMesh*)Attr;
if (Mesh->GetDeformerCount(FbxDeformer::eSkin) > 0)
{
break;
}
}
RootBone = RootBone->GetParent();
}
else
{
break;
}
}
return RootBone;
}
void FFbxImporter::ApplyTransformSettingsToFbxNode(FbxNode* Node, UFbxAssetImportData* AssetData)
{
check(Node);
check(AssetData);
if (TransformSettingsToFbxApply.Contains(Node))
{
return;
}
TransformSettingsToFbxApply.Add(Node);
FbxAMatrix TransformMatrix;
BuildFbxMatrixForImportTransform(TransformMatrix, AssetData);
FbxDouble3 ExistingTranslation = Node->LclTranslation.Get();
FbxDouble3 ExistingRotation = Node->LclRotation.Get();
FbxDouble3 ExistingScaling = Node->LclScaling.Get();
// A little slower to build up this information from the matrix, but it means we get
// the same result across all import types, as other areas can use the matrix directly
FbxVector4 AddedTranslation = TransformMatrix.GetT();
FbxVector4 AddedRotation = TransformMatrix.GetR();
FbxVector4 AddedScaling = TransformMatrix.GetS();
FbxDouble3 NewTranslation = FbxDouble3(ExistingTranslation[0] + AddedTranslation[0], ExistingTranslation[1] + AddedTranslation[1], ExistingTranslation[2] + AddedTranslation[2]);
FbxDouble3 NewRotation = FbxDouble3(ExistingRotation[0] + AddedRotation[0], ExistingRotation[1] + AddedRotation[1], ExistingRotation[2] + AddedRotation[2]);
FbxDouble3 NewScaling = FbxDouble3(ExistingScaling[0] * AddedScaling[0], ExistingScaling[1] * AddedScaling[1], ExistingScaling[2] * AddedScaling[2]);
Node->LclTranslation.Set(NewTranslation);
Node->LclRotation.Set(NewRotation);
Node->LclScaling.Set(NewScaling);
//Reset all the transform evaluation cache since we change some node transform
Scene->GetAnimationEvaluator()->Reset();
}
void FFbxImporter::RemoveTransformSettingsFromFbxNode(FbxNode* Node, UFbxAssetImportData* AssetData)
{
check(Node);
check(AssetData);
if (!TransformSettingsToFbxApply.Contains(Node))
{
return;
}
TransformSettingsToFbxApply.Remove(Node);
FbxAMatrix TransformMatrix;
BuildFbxMatrixForImportTransform(TransformMatrix, AssetData);
FbxDouble3 ExistingTranslation = Node->LclTranslation.Get();
FbxDouble3 ExistingRotation = Node->LclRotation.Get();
FbxDouble3 ExistingScaling = Node->LclScaling.Get();
// A little slower to build up this information from the matrix, but it means we get
// the same result across all import types, as other areas can use the matrix directly
FbxVector4 AddedTranslation = TransformMatrix.GetT();
FbxVector4 AddedRotation = TransformMatrix.GetR();
FbxVector4 AddedScaling = TransformMatrix.GetS();
FbxDouble3 NewTranslation = FbxDouble3(ExistingTranslation[0] - AddedTranslation[0], ExistingTranslation[1] - AddedTranslation[1], ExistingTranslation[2] - AddedTranslation[2]);
FbxDouble3 NewRotation = FbxDouble3(ExistingRotation[0] - AddedRotation[0], ExistingRotation[1] - AddedRotation[1], ExistingRotation[2] - AddedRotation[2]);
FbxDouble3 NewScaling = FbxDouble3(ExistingScaling[0] / AddedScaling[0], ExistingScaling[1] / AddedScaling[1], ExistingScaling[2] / AddedScaling[2]);
Node->LclTranslation.Set(NewTranslation);
Node->LclRotation.Set(NewRotation);
Node->LclScaling.Set(NewScaling);
//Reset all the transform evaluation cache since we change some node transform
Scene->GetAnimationEvaluator()->Reset();
}
void FFbxImporter::BuildFbxMatrixForImportTransform(FbxAMatrix& OutMatrix, UFbxAssetImportData* AssetData)
{
if(!AssetData)
{
OutMatrix.SetIdentity();
return;
}
FbxVector4 FbxAddedTranslation = Converter.ConvertToFbxPos(AssetData->ImportTranslation);
FbxVector4 FbxAddedScale = Converter.ConvertToFbxScale(FVector(AssetData->ImportUniformScale));
FbxVector4 FbxAddedRotation = Converter.ConvertToFbxRot(AssetData->ImportRotation.Euler());
OutMatrix = FbxAMatrix(FbxAddedTranslation, FbxAddedRotation, FbxAddedScale);
}
/**
* Get all Fbx skeletal mesh objects which are grouped by skeleton they bind to
*
* @param Node Root node to find skeletal meshes
* @param outSkelMeshArray return Fbx meshes they are grouped by skeleton
* @param SkeletonArray
* @param ExpandLOD flag of expanding LOD to get each mesh
*/
void FFbxImporter::RecursiveFindFbxSkelMesh(FbxNode* Node, TArray< TArray<FbxNode*>* >& outSkelMeshArray, TArray<FbxNode*>& SkeletonArray, bool ExpandLOD)
{
FbxNode* SkelMeshNode = nullptr;
FbxNode* NodeToAdd = Node;
if (Node->GetMesh() && Node->GetMesh()->GetDeformerCount(FbxDeformer::eSkin) > 0 )
{
SkelMeshNode = Node;
}
else if (Node->GetNodeAttribute() && Node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
{
// for LODgroup, add the LODgroup to OutSkelMeshArray according to the skeleton that the first child bind to
SkelMeshNode = FindLODGroupNode(Node, 0);
// check if the first child is skeletal mesh
if (SkelMeshNode != nullptr && !(SkelMeshNode->GetMesh() && SkelMeshNode->GetMesh()->GetDeformerCount(FbxDeformer::eSkin) > 0))
{
SkelMeshNode = nullptr;
}
else if (ExpandLOD)
{
// if ExpandLOD is true, only add the first LODGroup level node
NodeToAdd = SkelMeshNode;
}
// else NodeToAdd = Node;
}
if (SkelMeshNode)
{
// find root skeleton
check(SkelMeshNode->GetMesh() != nullptr);
const int32 fbxDeformerCount = SkelMeshNode->GetMesh()->GetDeformerCount();
FbxSkin* Deformer = static_cast<FbxSkin*>( SkelMeshNode->GetMesh()->GetDeformer(0, FbxDeformer::eSkin) );
if (Deformer != NULL )
{
int32 ClusterCount = Deformer->GetClusterCount();
bool bFoundCorrectLink = false;
for (int32 ClusterId = 0; ClusterId < ClusterCount; ++ClusterId)
{
FbxNode* RootBoneLink = Deformer->GetCluster(ClusterId)->GetLink(); //Get the bone influences by this first cluster
RootBoneLink = GetRootSkeleton(RootBoneLink); // Get the skeleton root itself
if (RootBoneLink)
{
bool bAddedToExistingSkeleton = false;
for (int32 SkeletonIndex = 0; SkeletonIndex < SkeletonArray.Num(); ++SkeletonIndex)
{
if (RootBoneLink == SkeletonArray[SkeletonIndex])
{
// append to existed outSkelMeshArray element
TArray<FbxNode*>* TempArray = outSkelMeshArray[SkeletonIndex];
TempArray->Add(NodeToAdd);
bAddedToExistingSkeleton = true;
break;
}
}
// if there is no outSkelMeshArray element that is bind to this skeleton
// create new element for outSkelMeshArray
if (!bAddedToExistingSkeleton)
{
TArray<FbxNode*>* TempArray = new TArray<FbxNode*>();
TempArray->Add(NodeToAdd);
outSkelMeshArray.Add(TempArray);
SkeletonArray.Add(RootBoneLink);
if (ImportOptions->bImportScene && !ImportOptions->bTransformVertexToAbsolute)
{
FbxVector4 NodeScaling = NodeToAdd->EvaluateLocalScaling();
FbxVector4 NoScale(1.0, 1.0, 1.0);
if (NodeScaling != NoScale)
{
//Scene import cannot import correctly a skeletal mesh with a root node containing scale
//Warn the user is skeletal mesh can be wrong
AddTokenizedErrorMessage(
FTokenizedMessage::Create(
EMessageSeverity::Warning,
FText::Format(LOCTEXT("FBX_ImportSceneSkeletalMeshRootNodeScaling", "Importing skeletal mesh {0} that dont have a mesh node with no scale is not supported when doing an import scene."), FText::FromString(UTF8_TO_TCHAR(NodeToAdd->GetName())))
),
FFbxErrors::SkeletalMesh_InvalidRoot
);
}
}
}
bFoundCorrectLink = true;
break;
}
}
// we didn't find the correct link
if (!bFoundCorrectLink)
{
AddTokenizedErrorMessage(
FTokenizedMessage::Create(
EMessageSeverity::Warning,
FText::Format( LOCTEXT("FBX_NoWeightsOnDeformer", "Ignoring mesh {0} because it has no weights."), FText::FromString( UTF8_TO_TCHAR(SkelMeshNode->GetName()) ) )
),
FFbxErrors::SkeletalMesh_NoWeightsOnDeformer
);
}
}
}
//Skeletalmesh node can have child so let's always iterate trough child
{
TArray<FbxNode*> ChildScaled;
//Sort the node to have the one with no scaling first so we have more chance
//to have a root skeletal mesh with no scale. Because scene import do not support
//root skeletal mesh containing scale
for (int32 ChildIndex = 0; ChildIndex < Node->GetChildCount(); ++ChildIndex)
{
FbxNode *ChildNode = Node->GetChild(ChildIndex);
if(!Node->GetNodeAttribute() || Node->GetNodeAttribute()->GetAttributeType() != FbxNodeAttribute::eLODGroup)
{
FbxVector4 NoScale(1.0, 1.0, 1.0);
if(ChildNode->EvaluateLocalScaling() == NoScale)
{
RecursiveFindFbxSkelMesh(ChildNode, outSkelMeshArray, SkeletonArray, ExpandLOD);
}
else
{
ChildScaled.Add(ChildNode);
}
}
}
for (FbxNode *ChildNode : ChildScaled)
{
RecursiveFindFbxSkelMesh(ChildNode, outSkelMeshArray, SkeletonArray, ExpandLOD);
}
}
}
void FFbxImporter::RecursiveFindRigidMesh(FbxNode* Node, TArray< TArray<FbxNode*>* >& outSkelMeshArray, TArray<FbxNode*>& SkeletonArray, bool ExpandLOD)
{
bool bRigidNodeFound = false;
FbxNode* RigidMeshNode = nullptr;
DEBUG_FBX_NODE("", Node);
if (Node->GetMesh())
{
// ignore skeletal mesh
if (Node->GetMesh()->GetDeformerCount(FbxDeformer::eSkin) == 0 )
{
RigidMeshNode = Node;
bRigidNodeFound = true;
}
}
else if (Node->GetNodeAttribute() && Node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
{
// for LODgroup, add the LODgroup to OutSkelMeshArray according to the skeleton that the first child bind to
FbxNode* FirstLOD = FindLODGroupNode(Node, 0);
// check if the first child is skeletal mesh
if (FirstLOD != nullptr && FirstLOD->GetMesh())
{
if (FirstLOD->GetMesh()->GetDeformerCount(FbxDeformer::eSkin) == 0 )
{
bRigidNodeFound = true;
}
}
if (bRigidNodeFound)
{
if (ExpandLOD)
{
RigidMeshNode = FirstLOD;
}
else
{
RigidMeshNode = Node;
}
}
}
if (bRigidNodeFound)
{
// find root skeleton
FbxNode* Link = GetRootSkeleton(RigidMeshNode);
int32 i;
for (i = 0; i < SkeletonArray.Num(); i++)
{
if ( Link == SkeletonArray[i])
{
// append to existed outSkelMeshArray element
TArray<FbxNode*>* TempArray = outSkelMeshArray[i];
TempArray->Add(RigidMeshNode);
break;
}
}
// if there is no outSkelMeshArray element that is bind to this skeleton
// create new element for outSkelMeshArray
if ( i == SkeletonArray.Num() )
{
TArray<FbxNode*>* TempArray = new TArray<FbxNode*>();
TempArray->Add(RigidMeshNode);
outSkelMeshArray.Add(TempArray);
SkeletonArray.Add(Link);
}
}
// for LODGroup, we will not deep in.
if (!(Node->GetNodeAttribute() && Node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup))
{
int32 ChildIndex;
for (ChildIndex=0; ChildIndex<Node->GetChildCount(); ++ChildIndex)
{
RecursiveFindRigidMesh(Node->GetChild(ChildIndex), outSkelMeshArray, SkeletonArray, ExpandLOD);
}
}
}
/**
* Get all Fbx skeletal mesh objects in the scene. these meshes are grouped by skeleton they bind to
*
* @param Node Root node to find skeletal meshes
* @param outSkelMeshArray return Fbx meshes they are grouped by skeleton
*/
void FFbxImporter::FillFbxSkelMeshArrayInScene(FbxNode* Node, TArray< TArray<FbxNode*>* >& outSkelMeshArray, bool ExpandLOD, bool bCombineSkeletalMesh, bool bForceFindRigid /*= false*/)
{
TArray<FbxNode*> SkeletonArray;
// a) find skeletal meshes
RecursiveFindFbxSkelMesh(Node, outSkelMeshArray, SkeletonArray, ExpandLOD);
// for skeletal mesh, we convert the skeleton system to skeleton
// in less we recognize bone mesh as rigid mesh if they are textured
for ( int32 SkelIndex = 0; SkelIndex < SkeletonArray.Num(); SkelIndex++)
{
RecursiveFixSkeleton(SkeletonArray[SkelIndex], *outSkelMeshArray[SkelIndex], ImportOptions->bImportMeshesInBoneHierarchy );
}
// b) find rigid mesh
// If we are attempting to import a skeletal mesh but we have no hierarchy attempt to find a rigid mesh.
if (bForceFindRigid || outSkelMeshArray.Num() == 0)
{
RecursiveFindRigidMesh(Node, outSkelMeshArray, SkeletonArray, ExpandLOD);
if (bForceFindRigid)
{
//Cleanup the rigid mesh, We want to remove any real static mesh from the outSkelMeshArray
//Any non skinned mesh that contain no animation should be part of this array.
int32 AnimStackCount = Scene->GetSrcObjectCount<FbxAnimStack>();
TArray<int32> SkeletalMeshArrayToRemove;
for (int32 i = 0; i < outSkelMeshArray.Num(); i++)
{
bool bIsValidSkeletal = false;
TArray<FbxNode*> NodeArray = *outSkelMeshArray[i];
for (FbxNode *InspectedNode : NodeArray)
{
FbxMesh* Mesh = InspectedNode->GetMesh();
FbxLODGroup* LodGroup = InspectedNode->GetLodGroup();
if (LodGroup != nullptr)
{
FbxNode* SkelMeshNode = FindLODGroupNode(InspectedNode, 0);
if (SkelMeshNode != nullptr)
{
Mesh = SkelMeshNode->GetMesh();
}
}
if (Mesh == nullptr)
{
continue;
}
if (Mesh->GetDeformerCount(FbxDeformer::eSkin) > 0)
{
bIsValidSkeletal = true;
break;
}
//If there is some anim object we count this as a valid skeletal mesh imported as rigid mesh
for (int32 AnimStackIndex = 0; AnimStackIndex < AnimStackCount; AnimStackIndex++)
{
FbxAnimStack* CurAnimStack = Scene->GetSrcObject<FbxAnimStack>(AnimStackIndex);
// set current anim stack
Scene->SetCurrentAnimationStack(CurAnimStack);
FbxTimeSpan AnimTimeSpan(FBXSDK_TIME_INFINITE, FBXSDK_TIME_MINUS_INFINITE);
InspectedNode->GetAnimationInterval(AnimTimeSpan, CurAnimStack);
if (AnimTimeSpan.GetDuration() > 0)
{
bIsValidSkeletal = true;
break;
}
}
if (bIsValidSkeletal)
{
break;
}
}
if (!bIsValidSkeletal)
{
SkeletalMeshArrayToRemove.Add(i);
}
}
for (int32 i = SkeletalMeshArrayToRemove.Num() - 1; i >= 0; --i)
{
if (!SkeletalMeshArrayToRemove.IsValidIndex(i) || !outSkelMeshArray.IsValidIndex(SkeletalMeshArrayToRemove[i]))
continue;
int32 IndexToRemove = SkeletalMeshArrayToRemove[i];
outSkelMeshArray[IndexToRemove]->Empty();
outSkelMeshArray.RemoveAt(IndexToRemove);
}
}
}
//Empty the skeleton array
SkeletonArray.Empty();
if (bCombineSkeletalMesh)
{
//Merge all the skeletal mesh arrays into one combine mesh
TArray<FbxNode*>* CombineNodes = new TArray<FbxNode*>();
for (TArray<FbxNode*> *Parts : outSkelMeshArray)
{
CombineNodes->Append(*Parts);
delete Parts;
}
outSkelMeshArray.Empty(1);
outSkelMeshArray.Add(CombineNodes);
}
}
FbxNode* FFbxImporter::FindFBXMeshesByBone(const FName& RootBoneName, bool bExpandLOD, TArray<FbxNode*>& OutFBXMeshNodeArray)
{
// get the root bone of Unreal skeletal mesh
const FString BoneNameString = RootBoneName.ToString();
// we do not need to check if the skeleton root node is a skeleton
// because the animation may be a rigid animation
FbxNode* SkeletonRoot = NULL;
// find the FBX skeleton node according to name
SkeletonRoot = Scene->FindNodeByName(TCHAR_TO_UTF8(*BoneNameString));
// SinceFBX bone names are changed on import, it's possible that the
// bone name in the engine doesn't match that of the one in the FBX file and
// would not be found by FindNodeByName(). So apply the same changes to the
// names of the nodes before checking them against the name of the Unreal bone
if (!SkeletonRoot)
{
for (int32 NodeIndex = 0; NodeIndex < Scene->GetNodeCount(); NodeIndex++)
{
FbxNode* FbxNode = Scene->GetNode(NodeIndex);
FString FbxBoneName = FSkeletalMeshImportData::FixupBoneName(MakeName(FbxNode->GetName()));
if (FbxBoneName == BoneNameString)
{
SkeletonRoot = FbxNode;
break;
}
}
}
// return if do not find matched FBX skeleton
if (!SkeletonRoot)
{
return NULL;
}
// Get Mesh nodes array that bind to the skeleton system
// 1, get all skeltal meshes in the FBX file
TArray< TArray<FbxNode*>* > SkelMeshArray;
FillFbxSkelMeshArrayInScene(Scene->GetRootNode(), SkelMeshArray, false, ImportOptions->bImportAsSkeletalGeometry || ImportOptions->bImportAsSkeletalSkinning, ImportOptions->bImportScene);
// 2, then get skeletal meshes that bind to this skeleton
for (int32 SkelMeshIndex = 0; SkelMeshIndex < SkelMeshArray.Num(); SkelMeshIndex++)
{
FbxNode* MeshNode = NULL;
if((*SkelMeshArray[SkelMeshIndex]).IsValidIndex(0))
{
FbxNode* Node = (*SkelMeshArray[SkelMeshIndex])[0];
if (Node->GetNodeAttribute() && Node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
{
MeshNode = FindLODGroupNode(Node, 0);
}
else
{
MeshNode = Node;
}
}
if( !ensure( MeshNode && MeshNode->GetMesh() ) )
{
return NULL;
}
// 3, get the root bone that the mesh bind to
FbxSkin* Deformer = (FbxSkin*)MeshNode->GetMesh()->GetDeformer(0, FbxDeformer::eSkin);
FbxNode* Link = nullptr;
// If there is no deformer this is likely rigid animation
if( Deformer )
{
Link = Deformer->GetCluster(0)->GetLink();
Link = GetRootSkeleton(Link);
}
else
{
Link = GetRootSkeleton(SkeletonRoot);
}
// 4, fill in the mesh node
if (Link == SkeletonRoot)
{
// copy meshes
if (bExpandLOD)
{
TArray<FbxNode*> SkelMeshes = *SkelMeshArray[SkelMeshIndex];
for (int32 NodeIndex = 0; NodeIndex < SkelMeshes.Num(); NodeIndex++)
{
FbxNode* Node = SkelMeshes[NodeIndex];
if (Node->GetNodeAttribute() && Node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
{
FbxNode *InnerMeshNode = FindLODGroupNode(Node, 0);
if (InnerMeshNode != nullptr)
OutFBXMeshNodeArray.Add(InnerMeshNode);
}
else
{
OutFBXMeshNodeArray.Add(Node);
}
}
}
else
{
OutFBXMeshNodeArray.Append(*SkelMeshArray[SkelMeshIndex]);
}
break;
}
}
for (int32 i = 0; i < SkelMeshArray.Num(); i++)
{
delete SkelMeshArray[i];
}
return SkeletonRoot;
}
/**
* Get the first Fbx mesh node.
*
* @param Node Root node
* @param bIsSkelMesh if we want a skeletal mesh
* @return FbxNode* the node containing the first mesh
*/
FbxNode* GetFirstFbxMesh(FbxNode* Node, bool bIsSkelMesh)
{
if (Node->GetMesh())
{
if (bIsSkelMesh)
{
if (Node->GetMesh()->GetDeformerCount(FbxDeformer::eSkin)>0)
{
return Node;
}
}
else
{
return Node;
}
}
int32 ChildIndex;
for (ChildIndex=0; ChildIndex<Node->GetChildCount(); ++ChildIndex)
{
FbxNode* FirstMesh;
FirstMesh = GetFirstFbxMesh(Node->GetChild(ChildIndex), bIsSkelMesh);
if (FirstMesh)
{
return FirstMesh;
}
}
return NULL;
}
void FFbxImporter::CheckSmoothingInfo(FbxMesh* FbxMesh)
{
if (FbxMesh && bFirstMesh)
{
bFirstMesh = false; // don't check again
FbxLayer* LayerSmoothing = FbxMesh->GetLayer(0, FbxLayerElement::eSmoothing);
if (!LayerSmoothing && !GIsAutomationTesting)
{
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("Prompt_NoSmoothgroupForFBXScene", "No smoothing group information was found in this FBX scene. Please make sure to enable the 'Export Smoothing Groups' option in the FBX Exporter plug-in before exporting the file. Even for tools that don't support smoothing groups, the FBX Exporter will generate appropriate smoothing data at export-time so that correct vertex normals can be inferred while importing.")), FFbxErrors::Generic_Mesh_NoSmoothingGroup);
}
}
}
//-------------------------------------------------------------------------
//
//-------------------------------------------------------------------------
FbxNode* FFbxImporter::RetrieveObjectFromName(const TCHAR* ObjectName, FbxNode* Root)
{
if (Scene)
{
if (!Root)
{
Root = Scene->GetRootNode();
}
for (int32 ChildIndex=0;ChildIndex<Root->GetChildCount();++ChildIndex)
{
FbxNode* Node = Root->GetChild(ChildIndex);
FbxMesh* FbxMesh = Node->GetMesh();
if (FbxMesh && 0 == FCString::Strcmp(ObjectName,UTF8_TO_TCHAR(Node->GetName())))
{
return Node;
}
if (FbxNode* NextNode = RetrieveObjectFromName(ObjectName,Node))
{
return NextNode;
}
}
}
return nullptr;
}
FString GetFbxPropertyStringValue(const FbxProperty& Property)
{
FString ValueStr(TEXT("Unsupported type"));
FbxDataType DataType = Property.GetPropertyDataType();
switch (DataType.GetType())
{
case eFbxBool:
{
FbxBool BoolValue = Property.Get<FbxBool>();
ValueStr = LexToString(BoolValue);
}
break;
case eFbxChar:
{
FbxChar CharValue = Property.Get<FbxChar>();
ValueStr = LexToString(CharValue);
}
break;
case eFbxUChar:
{
FbxUChar UCharValue = Property.Get<FbxUChar>();
ValueStr = LexToString(UCharValue);
}
break;
case eFbxShort:
{
FbxShort ShortValue = Property.Get<FbxShort>();
ValueStr = LexToString(ShortValue);
}
break;
case eFbxUShort:
{
FbxUShort UShortValue = Property.Get<FbxUShort>();
ValueStr = LexToString(UShortValue);
}
break;
case eFbxInt:
{
FbxInt IntValue = Property.Get<FbxInt>();
ValueStr = LexToString(IntValue);
}
break;
case eFbxUInt:
{
FbxUInt UIntValue = Property.Get<FbxUInt>();
ValueStr = LexToString(UIntValue);
}
break;
case eFbxEnum:
{
FbxEnum EnumValue = Property.Get<FbxEnum>();
ValueStr = LexToString(EnumValue);
}
break;
case eFbxFloat:
{
FbxFloat FloatValue = Property.Get<FbxFloat>();
ValueStr = LexToString(FloatValue);
}
break;
case eFbxDouble:
{
FbxDouble DoubleValue = Property.Get<FbxDouble>();
ValueStr = LexToString(DoubleValue);
}
break;
case eFbxDouble2:
{
FbxDouble2 Vec = Property.Get<FbxDouble2>();
ValueStr = FString::Printf(TEXT("(%f, %f)"), Vec[0], Vec[1]);
}
break;
case eFbxDouble3:
{
FbxDouble3 Vec = Property.Get<FbxDouble3>();
ValueStr = FString::Printf(TEXT("(%f, %f, %f)"), Vec[0], Vec[1], Vec[2]);
}
break;
case eFbxDouble4:
{
FbxDouble4 Vec = Property.Get<FbxDouble4>();
ValueStr = FString::Printf(TEXT("(%f, %f, %f, %f)"), Vec[0], Vec[1], Vec[2], Vec[3]);
}
break;
case eFbxString:
{
FbxString StringValue = Property.Get<FbxString>();
ValueStr = UTF8_TO_TCHAR(StringValue.Buffer());
}
break;
default:
break;
}
return ValueStr;
}
void FFbxImporter::ImportNodeCustomProperties(UObject* Object, FbxNode* Node, bool bPrefixTagWithNodeName)
{
if (!Object || !Node)
{
return;
}
// Import all custom user-defined FBX properties from the FBX node to the object metadata
FbxProperty CurrentProperty = Node->GetFirstProperty();
FString NodeName = UTF8_TO_TCHAR(Node->GetName());
static const FString MetadataPrefix(FBX_METADATA_PREFIX);
while (CurrentProperty.IsValid())
{
if (CurrentProperty.GetFlag(FbxPropertyFlags::eUserDefined))
{
// Prefix the FBX metadata tag to make it distinguishable from other metadata
// so that it can be exportable through FBX export
FString MetadataTag = UTF8_TO_TCHAR(CurrentProperty.GetName());
if (bPrefixTagWithNodeName && !MetadataTag.StartsWith(NodeName))
{
// Append the node name in the tag since all the metadata will be flattened on the Object
MetadataTag = NodeName + TEXT(".") + MetadataTag;
}
MetadataTag = MetadataPrefix + MetadataTag;
FString MetadataValue = GetFbxPropertyStringValue(CurrentProperty);
Object->GetPackage()->GetMetaData().SetValue(Object, *MetadataTag, *MetadataValue);
}
CurrentProperty = Node->GetNextProperty(CurrentProperty);
}
int NumChildren = Node->GetChildCount();
for (int i = 0; i < NumChildren; ++i)
{
ImportNodeCustomProperties(Object, Node->GetChild(i), bPrefixTagWithNodeName);
}
}
} // namespace UnFbx
#if WITH_DEV_AUTOMATION_TESTS
IMPLEMENT_SIMPLE_AUTOMATION_TEST(FFbxUnitTest, "Editor.Import.Fbx.UnitTests", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter)
bool FFbxUnitTest::RunTest(const FString& Parameters)
{
const ANSICHAR TestData[][64] = { "SimpleTest"
, "SimpleTest_1"
, "SimpleTest.1"
, "SimpleTest,1"
, "SimpleTest/1"
, "SimpleTest`1"
, "SimpleTest%1"
, "One:SimpleTest"
, "One::SimpleTest"
, "One:Two:SimpleTest"
, "One::Two::SimpleTest"
, "O.n,e::/Two:%:Si`mpleTest" };
const FString Results[3] = { FString(TEXT("SimpleTest"))
, FString(TEXT("SimpleTest_1"))
, FString(TEXT("Si_mpleTest")) };
auto RunOneTest = [&TestData, &Results, this](int32 DataIndex, int32 ResultIndex)
{
FString ResultInternal = UnFbx::FFbxImporter::MakeName(TestData[DataIndex]);
if (!ResultInternal.Equals(Results[ResultIndex]))
{
FStringFormatOrderedArguments OrderedArguments;
OrderedArguments.Add(FStringFormatArg(UnFbx::FFbxImporter::MakeString(TestData[DataIndex])));
OrderedArguments.Add(FStringFormatArg(ResultInternal));
OrderedArguments.Add(FStringFormatArg(Results[ResultIndex]));
FString ErrorMessage = FString::Format(TEXT("Fbx importer MakeName function transform name [{0}] into [{1}], but expected [{2}]."), OrderedArguments);
AddError(ErrorMessage);
}
};
//////////////////////////////////////////////////////////////////////////
// String with no change test
RunOneTest(0, 0);
//////////////////////////////////////////////////////////////////////////
// Special character tests return all TestData_2
RunOneTest(1, 1);
RunOneTest(2, 1);
RunOneTest(3, 1);
RunOneTest(4, 1);
RunOneTest(5, 1);
RunOneTest(6, 1);
//////////////////////////////////////////////////////////////////////////
//namespace tests return all TestData_1
RunOneTest(7, 0);
RunOneTest(8, 0);
RunOneTest(9, 0);
RunOneTest(10, 0);
//////////////////////////////////////////////////////////////////////////
// Mix test
RunOneTest(11, 2);
return true;
}
#endif //WITH_DEV_AUTOMATION_TESTS
#undef LOCTEXT_NAMESPACE