// Copyright Epic Games, Inc. All Rights Reserved. #include "SkinWeightsUtilities.h" #include "LODUtilities.h" #include "Modules/ModuleManager.h" #include "Components/SkinnedMeshComponent.h" #include "Animation/DebugSkelMeshComponent.h" #include "Rendering/SkeletalMeshModel.h" #include "Rendering/SkeletalMeshLODModel.h" #include "Engine/SkeletalMesh.h" #include "EditorFramework/AssetImportData.h" #include "MeshUtilities.h" #include "IAssetTools.h" #include "AssetToolsModule.h" #include "Factories/FbxFactory.h" #include "Factories/FbxAnimSequenceImportData.h" #include "Factories/FbxSkeletalMeshImportData.h" #include "Factories/FbxStaticMeshImportData.h" #include "Factories/FbxTextureImportData.h" #include "Factories/FbxImportUI.h" #include "AssetRegistryModule.h" #include "ObjectTools.h" #include "AssetImportTask.h" #include "FbxImporter.h" #include "ScopedTransaction.h" #include "ComponentReregisterContext.h" #include "Animation/SkinWeightProfile.h" #include "IDesktopPlatform.h" #include "DesktopPlatformModule.h" #include "EditorDirectories.h" #include "Framework/Application/SlateApplication.h" #include "Interfaces/ITargetPlatform.h" #include "Interfaces/ITargetPlatformManagerModule.h" #include "Misc/CoreMisc.h" DEFINE_LOG_CATEGORY_STATIC(LogSkinWeightsUtilities, Log, All); bool FSkinWeightsUtilities::ImportAlternateSkinWeight(USkeletalMesh* SkeletalMesh, const FString& Path, int32 TargetLODIndex, const FName& ProfileName) { check(SkeletalMesh); check(SkeletalMesh->GetLODInfo(TargetLODIndex)); FSkeletalMeshLODInfo* LODInfo = SkeletalMesh->GetLODInfo(TargetLODIndex); if (LODInfo && LODInfo->bHasBeenSimplified && LODInfo->ReductionSettings.BaseLOD != TargetLODIndex) { // We cannot import alternate skin weights profile for a generated LOD UE_LOG(LogSkinWeightsUtilities, Error, TEXT("Cannot import Skin Weight Profile for a generated LOD.")); return false; } FString AbsoluteFilePath = UAssetImportData::ResolveImportFilename(Path, SkeletalMesh->GetOutermost()); if (!FPaths::FileExists(AbsoluteFilePath)) { UE_LOG(LogSkinWeightsUtilities, Error, TEXT("Path containing Skin Weight Profile data does not exist (%s)."), *Path); return false; } FScopedSuspendAlternateSkinWeightPreview ScopedSuspendAlternateSkinnWeightPreview(SkeletalMesh); FScopedSkeletalMeshPostEditChange ScopePostEditChange(SkeletalMesh); UnFbx::FBXImportOptions ImportOptions; // Import the alternate fbx into a temporary skeletal mesh using the same import options UFbxFactory* FbxFactory = NewObject(UFbxFactory::StaticClass()); FbxFactory->AddToRoot(); FbxFactory->ImportUI = NewObject(FbxFactory); UFbxSkeletalMeshImportData* OriginalSkeletalMeshImportData = UFbxSkeletalMeshImportData::GetImportDataForSkeletalMesh(SkeletalMesh, nullptr); if (OriginalSkeletalMeshImportData != nullptr) { // Copy the skeletal mesh import data options FbxFactory->ImportUI->SkeletalMeshImportData = DuplicateObject(OriginalSkeletalMeshImportData, FbxFactory); } // Skip the auto detect type on import, the test set a specific value FbxFactory->SetDetectImportTypeOnImport(false); FbxFactory->ImportUI->bImportAsSkeletal = true; FbxFactory->ImportUI->MeshTypeToImport = FBXIT_SkeletalMesh; FbxFactory->ImportUI->bIsReimport = false; FbxFactory->ImportUI->ReimportMesh = nullptr; FbxFactory->ImportUI->bAllowContentTypeImport = true; FbxFactory->ImportUI->bImportAnimations = false; FbxFactory->ImportUI->bAutomatedImportShouldDetectType = false; FbxFactory->ImportUI->bCreatePhysicsAsset = false; FbxFactory->ImportUI->bImportMaterials = false; FbxFactory->ImportUI->bImportTextures = false; FbxFactory->ImportUI->bImportMesh = true; FbxFactory->ImportUI->bImportRigidMesh = false; FbxFactory->ImportUI->bIsObjImport = false; FbxFactory->ImportUI->bOverrideFullName = true; FbxFactory->ImportUI->Skeleton = nullptr; // Force some skeletal mesh import options if (FbxFactory->ImportUI->SkeletalMeshImportData) { FbxFactory->ImportUI->SkeletalMeshImportData->bImportMeshLODs = false; FbxFactory->ImportUI->SkeletalMeshImportData->bImportMorphTargets = false; FbxFactory->ImportUI->SkeletalMeshImportData->bUpdateSkeletonReferencePose = false; FbxFactory->ImportUI->SkeletalMeshImportData->ImportContentType = EFBXImportContentType::FBXICT_All; // We need geo and skinning, so we can match the weights } // Force some material options if (FbxFactory->ImportUI->TextureImportData) { FbxFactory->ImportUI->TextureImportData->MaterialSearchLocation = EMaterialSearchLocation::DoNotSearch; FbxFactory->ImportUI->TextureImportData->BaseMaterialName.Reset(); } FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); FString ImportAssetPath = TEXT("/Engine/TempEditor/SkeletalMeshTool"); // Empty the temporary path FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); auto DeletePathAssets = [&AssetRegistryModule, &ImportAssetPath]() { TArray AssetsToDelete; AssetRegistryModule.Get().GetAssetsByPath(FName(*ImportAssetPath), AssetsToDelete, true); for (FAssetData AssetData: AssetsToDelete) { UObject* ObjToDelete = AssetData.GetAsset(); if (ObjToDelete) { // Avoid temporary package to be saved UPackage* Package = ObjToDelete->GetOutermost(); Package->SetDirtyFlag(false); // Avoid temporary asset to be saved by setting the RF_Transient flag ObjToDelete->SetFlags(RF_Transient); } } ObjectTools::DeleteAssets(AssetsToDelete, false); CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS); }; DeletePathAssets(); ApplyImportUIToImportOptions(FbxFactory->ImportUI, ImportOptions); TArray ImportFilePaths; ImportFilePaths.Add(AbsoluteFilePath); UAssetImportTask* Task = NewObject(); Task->AddToRoot(); Task->bAutomated = true; Task->bReplaceExisting = true; Task->DestinationPath = ImportAssetPath; Task->bSave = false; Task->DestinationName = FGuid::NewGuid().ToString(EGuidFormats::Digits); Task->Options = FbxFactory->ImportUI; Task->Filename = AbsoluteFilePath; Task->Factory = FbxFactory; FbxFactory->SetAssetImportTask(Task); TArray Tasks; Tasks.Add(Task); AssetToolsModule.Get().ImportAssetTasks(Tasks); UObject* ImportedObject = nullptr; for (FString AssetPath: Task->ImportedObjectPaths) { FAssetData AssetData = AssetRegistryModule.Get().GetAssetByObjectPath(FName(*AssetPath)); ImportedObject = AssetData.GetAsset(); if (ImportedObject != nullptr) { break; } } // Factory and task can now be garbage collected Task->RemoveFromRoot(); FbxFactory->RemoveFromRoot(); USkeletalMesh* TmpSkeletalMesh = Cast(ImportedObject); if (TmpSkeletalMesh == nullptr || TmpSkeletalMesh->GetSkeleton() == nullptr) { UE_LOG(LogSkinWeightsUtilities, Error, TEXT("Failed to import Skin Weight Profile from provided FBX file (%s)."), *Path); DeletePathAssets(); return false; } // The LOD index of the source is always 0, const int32 SrcLodIndex = 0; bool bResult = false; if (SkeletalMesh && TmpSkeletalMesh) { if (FSkeletalMeshModel* TargetModel = SkeletalMesh->GetImportedModel()) { if (TargetModel->LODModels.IsValidIndex(TargetLODIndex)) { // Prepare the profile data FSkeletalMeshLODModel& TargetLODModel = TargetModel->LODModels[TargetLODIndex]; // Prepare the profile data FSkinWeightProfileInfo* Profile = SkeletalMesh->GetSkinWeightProfiles().FindByPredicate([ProfileName](FSkinWeightProfileInfo Profile) { return Profile.Name == ProfileName; }); const bool bIsReimport = Profile != nullptr; FText TransactionName = bIsReimport ? NSLOCTEXT("UnrealEd", "UpdateAlternateSkinningWeight", "Update Alternate Skinning Weight") : NSLOCTEXT("UnrealEd", "ImportAlternateSkinningWeight", "Import Alternate Skinning Weight"); FScopedTransaction ScopedTransaction(TransactionName); SkeletalMesh->Modify(); if (bIsReimport) { // Update source file path FString& StoredPath = Profile->PerLODSourceFiles.FindOrAdd(TargetLODIndex); StoredPath = UAssetImportData::SanitizeImportFilename(AbsoluteFilePath, SkeletalMesh->GetOutermost()); Profile->PerLODSourceFiles.KeySort([](int32 A, int32 B) { return A < B; }); } // Clear profile data before import FImportedSkinWeightProfileData& ProfileData = TargetLODModel.SkinWeightProfiles.FindOrAdd(ProfileName); ProfileData.SkinWeights.Empty(); ProfileData.SourceModelInfluences.Empty(); FImportedSkinWeightProfileData PreviousProfileData = ProfileData; FOverlappingThresholds OverlappingThresholds = ImportOptions.OverlappingThresholds; bool ShouldImportNormals = ImportOptions.ShouldImportNormals(); bool ShouldImportTangents = ImportOptions.ShouldImportTangents(); bool bUseMikkTSpace = ImportOptions.NormalGenerationMethod == EFBXNormalGenerationMethod::MikkTSpace; bResult = FLODUtilities::UpdateAlternateSkinWeights(SkeletalMesh, ProfileName, TmpSkeletalMesh, TargetLODIndex, SrcLodIndex, OverlappingThresholds, ShouldImportNormals, ShouldImportTangents, bUseMikkTSpace, ImportOptions.bComputeWeightedNormals); if (!bResult) { // Remove invalid profile data due to failed import if (!bIsReimport) { TargetLODModel.SkinWeightProfiles.Remove(ProfileName); } else { // Otherwise restore previous data ProfileData = PreviousProfileData; } } // Only add if it is an initial import and it was successful if (!bIsReimport && bResult) { FSkinWeightProfileInfo SkeletalMeshProfile; SkeletalMeshProfile.DefaultProfile = (SkeletalMesh->GetNumSkinWeightProfiles() == 0); SkeletalMeshProfile.DefaultProfileFromLODIndex = TargetLODIndex; SkeletalMeshProfile.Name = ProfileName; SkeletalMeshProfile.PerLODSourceFiles.Add(TargetLODIndex, UAssetImportData::SanitizeImportFilename(AbsoluteFilePath, SkeletalMesh->GetOutermost())); SkeletalMesh->AddSkinWeightProfile(SkeletalMeshProfile); Profile = &SkeletalMeshProfile; } } } } // Make sure all created objects are gone DeletePathAssets(); return bResult; } bool FSkinWeightsUtilities::ReimportAlternateSkinWeight(USkeletalMesh* SkeletalMesh, int32 TargetLODIndex) { bool bResult = false; const TArray& SkinWeightProfiles = SkeletalMesh->GetSkinWeightProfiles(); if (SkinWeightProfiles.Num() <= 0) { return bResult; } FScopedSuspendAlternateSkinWeightPreview ScopedSuspendAlternateSkinnWeightPreview(SkeletalMesh); FScopedSkeletalMeshPostEditChange ScopePostEditChange(SkeletalMesh); for (int32 ProfileIndex = 0; ProfileIndex < SkinWeightProfiles.Num(); ++ProfileIndex) { const FSkinWeightProfileInfo& ProfileInfo = SkinWeightProfiles[ProfileIndex]; const FString* PathNamePtr = ProfileInfo.PerLODSourceFiles.Find(TargetLODIndex); // Skip profile that do not have data for TargetLODIndex if (!PathNamePtr) { continue; } const FString& PathName = *PathNamePtr; FString AbsoluteFilePath = UAssetImportData::ResolveImportFilename(PathName, SkeletalMesh->GetOutermost()); if (FPaths::FileExists(AbsoluteFilePath)) { bResult |= FSkinWeightsUtilities::ImportAlternateSkinWeight(SkeletalMesh, AbsoluteFilePath, TargetLODIndex, ProfileInfo.Name); } else { const FString PickedFileName = FSkinWeightsUtilities::PickSkinWeightFBXPath(TargetLODIndex, SkeletalMesh); if (!PickedFileName.IsEmpty() && FPaths::FileExists(PickedFileName)) { bResult |= FSkinWeightsUtilities::ImportAlternateSkinWeight(SkeletalMesh, PickedFileName, TargetLODIndex, ProfileInfo.Name); } } } if (bResult) { FLODUtilities::RegenerateDependentLODs(SkeletalMesh, TargetLODIndex, GetTargetPlatformManagerRef().GetRunningTargetPlatform()); } return bResult; } bool FSkinWeightsUtilities::RemoveSkinnedWeightProfileData(USkeletalMesh* SkeletalMesh, const FName& ProfileName, int32 LODIndex) { check(SkeletalMesh); check(SkeletalMesh->GetImportedModel()); check(SkeletalMesh->GetImportedModel()->LODModels.IsValidIndex(LODIndex)); FSkeletalMeshLODModel& LODModelDest = SkeletalMesh->GetImportedModel()->LODModels[LODIndex]; LODModelDest.SkinWeightProfiles.Remove(ProfileName); FSkeletalMeshImportData ImportDataDest; SkeletalMesh->LoadLODImportedData(LODIndex, ImportDataDest); // Rechunk the skeletal mesh since we remove it, we rebuild the skeletal mesh to achieve rechunking UFbxSkeletalMeshImportData* OriginalSkeletalMeshImportData = UFbxSkeletalMeshImportData::GetImportDataForSkeletalMesh(SkeletalMesh, nullptr); TArray LODPointsDest; TArray LODWedgesDest; TArray LODFacesDest; TArray LODInfluencesDest; TArray LODPointToRawMapDest; ImportDataDest.CopyLODImportData(LODPointsDest, LODWedgesDest, LODFacesDest, LODInfluencesDest, LODPointToRawMapDest); const bool bShouldImportNormals = OriginalSkeletalMeshImportData->NormalImportMethod == FBXNIM_ImportNormals || OriginalSkeletalMeshImportData->NormalImportMethod == FBXNIM_ImportNormalsAndTangents; const bool bShouldImportTangents = OriginalSkeletalMeshImportData->NormalImportMethod == FBXNIM_ImportNormalsAndTangents; // Set the options with the current asset build options IMeshUtilities::MeshBuildOptions BuildOptions; BuildOptions.OverlappingThresholds.ThresholdPosition = OriginalSkeletalMeshImportData->ThresholdPosition; BuildOptions.OverlappingThresholds.ThresholdTangentNormal = OriginalSkeletalMeshImportData->ThresholdTangentNormal; BuildOptions.OverlappingThresholds.ThresholdUV = OriginalSkeletalMeshImportData->ThresholdUV; BuildOptions.OverlappingThresholds.MorphThresholdPosition = OriginalSkeletalMeshImportData->MorphThresholdPosition; BuildOptions.bComputeNormals = !bShouldImportNormals || !ImportDataDest.bHasNormals; BuildOptions.bComputeTangents = !bShouldImportTangents || !ImportDataDest.bHasTangents; BuildOptions.bUseMikkTSpace = (OriginalSkeletalMeshImportData->NormalGenerationMethod == EFBXNormalGenerationMethod::MikkTSpace) && (!bShouldImportNormals || !bShouldImportTangents); BuildOptions.bComputeWeightedNormals = OriginalSkeletalMeshImportData->bComputeWeightedNormals; BuildOptions.bRemoveDegenerateTriangles = false; BuildOptions.TargetPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); // Build the skeletal mesh asset IMeshUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked("MeshUtilities"); TArray WarningMessages; TArray WarningNames; // BaseLOD need to make sure the source data fit with the skeletalmesh materials array before using meshutilities.BuildSkeletalMesh FLODUtilities::AdjustImportDataFaceMaterialIndex(SkeletalMesh->GetMaterials(), ImportDataDest.Materials, LODFacesDest, LODIndex); // Build the destination mesh with the Alternate influences, so the chunking is done properly. const bool bBuildSuccess = MeshUtilities.BuildSkeletalMesh(LODModelDest, SkeletalMesh->GetPathName(), SkeletalMesh->GetRefSkeleton(), LODInfluencesDest, LODWedgesDest, LODFacesDest, LODPointsDest, LODPointToRawMapDest, BuildOptions, &WarningMessages, &WarningNames); FLODUtilities::RegenerateAllImportSkinWeightProfileData(LODModelDest); return bBuildSuccess; } FString FSkinWeightsUtilities::PickSkinWeightFBXPath(int32 LODIndex, USkeletalMesh* SkeletalMesh) { FString PickedFileName(""); FString ExtensionStr; ExtensionStr += TEXT("FBX files|*.fbx|"); // First, display the file open dialog for selecting the file. TArray OpenFilenames; IDesktopPlatform* DesktopPlatform = FDesktopPlatformModule::Get(); bool bOpen = false; if (DesktopPlatform) { // Try and retrieve the path containing the original skeletal mesh source data, and set it as default path for the file dialog UFbxSkeletalMeshImportData* ImportData = SkeletalMesh ? Cast(SkeletalMesh->GetAssetImportData()) : nullptr; FString DefaultPath; FString TempString; if (ImportData) { ImportData->GetImportContentFilename(DefaultPath, TempString); DefaultPath = FPaths::GetPath(DefaultPath); } // Otherwise resort back to last FBX directory if (!FPaths::DirectoryExists(DefaultPath)) { DefaultPath = FEditorDirectories::Get().GetLastDirectory(ELastDirectory::FBX); } const FString DialogTitle = TEXT("Pick FBX file containing Skin Weight data for LOD ") + FString::FormatAsNumber(LODIndex); bOpen = DesktopPlatform->OpenFileDialog( FSlateApplication::Get().FindBestParentWindowHandleForDialogs(nullptr), DialogTitle, *DefaultPath, TEXT(""), *ExtensionStr, EFileDialogFlags::None, OpenFilenames); } if (bOpen) { if (OpenFilenames.Num() == 1) { PickedFileName = OpenFilenames[0]; // Set last directory path for FBX files FEditorDirectories::Get().SetLastDirectory(ELastDirectory::FBX, FPaths::GetPath(PickedFileName)); } else { // Error } } return PickedFileName; }