// Copyright Epic Games, Inc. All Rights Reserved. #include "StaticToSkeletalMeshConverter.h" #if WITH_EDITOR #include "Animation/Skeleton.h" #include "EditorFramework/AssetImportData.h" #include "Engine/SkeletalMesh.h" #include "Engine/SkinnedAssetCommon.h" #include "Engine/StaticMesh.h" #include "InterchangeHelper.h" #include "Interfaces/ITargetPlatformManagerModule.h" #include "LODUtilities.h" #include "MeshDescription.h" #include "MeshUtilities.h" #include "Misc/Guid.h" #include "Modules/ModuleManager.h" #include "ReferenceSkeleton.h" #include "Rendering/SkeletalMeshLODImporterData.h" #include "Rendering/SkeletalMeshModel.h" #include "SkeletalMeshAttributes.h" DEFINE_LOG_CATEGORY_STATIC(LogStaticToSkeletalMeshConverter, Log, All); static const FName RootBoneName("Root"); static const TCHAR* JointBaseName(TEXT("Joint")); bool FStaticToSkeletalMeshConverter::InitializeSkeletonFromStaticMesh( USkeleton* InSkeleton, const UStaticMesh* InStaticMesh, const FVector& InRelativeRootPosition ) { if (!ensure(InSkeleton)) { return false; } if (InSkeleton->GetReferenceSkeleton().GetNum() != 0) { UE_LOG(LogStaticToSkeletalMeshConverter, Error, TEXT("Skeleton '%s' is not empty"), *InSkeleton->GetPathName()); return false; } if (!ensure(InStaticMesh)) { return false; } const FBox Bounds = InStaticMesh->GetBoundingBox(); const FVector RootPosition = Bounds.Min + (Bounds.Max - Bounds.Min) * InRelativeRootPosition; FTransform RootTransform(FTransform::Identity); RootTransform.SetTranslation(RootPosition); FReferenceSkeletonModifier Modifier(InSkeleton); Modifier.Add(FMeshBoneInfo(RootBoneName, RootBoneName.ToString(), INDEX_NONE), RootTransform); return true; } bool FStaticToSkeletalMeshConverter::InitializeSkeletonFromStaticMesh( USkeleton* InSkeleton, const UStaticMesh* InStaticMesh, const FVector& InRelativeRootPosition, const FVector& InRelativeEndEffectorPosition, const int32 InIntermediaryJointCount ) { if (!ensure(InSkeleton)) { return false; } if (InSkeleton->GetReferenceSkeleton().GetNum() != 0) { UE_LOG(LogStaticToSkeletalMeshConverter, Error, TEXT("Skeleton '%s' is not empty"), *InSkeleton->GetPathName()); return false; } if (!ensure(InStaticMesh)) { return false; } if (FMath::IsNearlyZero(FVector::DistSquared(InRelativeEndEffectorPosition, InRelativeRootPosition))) { return InitializeSkeletonFromStaticMesh(InSkeleton, InStaticMesh, InRelativeRootPosition); } const FBox Bounds = InStaticMesh->GetBoundingBox(); const FVector RootPosition = Bounds.Min + (Bounds.Max - Bounds.Min) * InRelativeRootPosition; const FVector EndEffectorPosition = Bounds.Min + (Bounds.Max - Bounds.Min) * InRelativeEndEffectorPosition; // Find a rough rotation we can use const FQuat Rotation = FQuat::FindBetweenVectors(FVector::ZAxisVector, EndEffectorPosition - RootPosition).GetNormalized(); FTransform ParentTransform(FTransform::Identity); ParentTransform.SetTranslation(RootPosition); ParentTransform.SetRotation(Rotation); FReferenceSkeletonModifier Modifier(InSkeleton); Modifier.Add(FMeshBoneInfo(RootBoneName, RootBoneName.ToString(), INDEX_NONE), ParentTransform); for (int32 JointIndex = 0; JointIndex <= InIntermediaryJointCount; JointIndex++) { const double T = (JointIndex + 1.0) / (InIntermediaryJointCount + 2.0); FTransform PointTransform(ParentTransform); PointTransform.SetTranslation(RootPosition + (EndEffectorPosition - RootPosition) * T); FString JointName = FString::Printf(TEXT("%s_%d"), JointBaseName, JointIndex + 1); Modifier.Add(FMeshBoneInfo(FName(JointName), JointName, JointIndex), PointTransform * ParentTransform.Inverse()); ParentTransform = PointTransform; } return true; } static void CopyBuildSettings( const FMeshBuildSettings& InStaticMeshBuildSettings, FSkeletalMeshBuildSettings& OutSkeletalMeshBuildSettings ) { OutSkeletalMeshBuildSettings.bRecomputeNormals = InStaticMeshBuildSettings.bRecomputeNormals; OutSkeletalMeshBuildSettings.bRecomputeTangents = InStaticMeshBuildSettings.bRecomputeTangents; OutSkeletalMeshBuildSettings.bUseMikkTSpace = InStaticMeshBuildSettings.bUseMikkTSpace; OutSkeletalMeshBuildSettings.bComputeWeightedNormals = InStaticMeshBuildSettings.bComputeWeightedNormals; OutSkeletalMeshBuildSettings.bRemoveDegenerates = InStaticMeshBuildSettings.bRemoveDegenerates; OutSkeletalMeshBuildSettings.bUseHighPrecisionTangentBasis = InStaticMeshBuildSettings.bUseHighPrecisionTangentBasis; OutSkeletalMeshBuildSettings.bUseFullPrecisionUVs = InStaticMeshBuildSettings.bUseFullPrecisionUVs; OutSkeletalMeshBuildSettings.bUseBackwardsCompatibleF16TruncUVs = InStaticMeshBuildSettings.bUseBackwardsCompatibleF16TruncUVs; // The rest we leave at defaults. } static SkeletalMeshOptimizationImportance ConvertOptimizationImportance( EMeshFeatureImportance::Type InStaticMeshImportance) { switch(InStaticMeshImportance) { default: case EMeshFeatureImportance::Off: return SMOI_Highest; case EMeshFeatureImportance::Lowest: return SMOI_Lowest; case EMeshFeatureImportance::Low: return SMOI_Low; case EMeshFeatureImportance::Normal: return SMOI_Normal; case EMeshFeatureImportance::High: return SMOI_High; case EMeshFeatureImportance::Highest: return SMOI_Highest; } } static void CopyReductionSettings( const FMeshReductionSettings& InStaticMeshReductionSettings, FSkeletalMeshOptimizationSettings& OutSkeletalMeshReductionSettings ) { // Copy the reduction settings as closely as we can. OutSkeletalMeshReductionSettings.NumOfTrianglesPercentage = InStaticMeshReductionSettings.PercentTriangles; OutSkeletalMeshReductionSettings.NumOfVertPercentage = InStaticMeshReductionSettings.PercentVertices; OutSkeletalMeshReductionSettings.WeldingThreshold = InStaticMeshReductionSettings.WeldingThreshold; OutSkeletalMeshReductionSettings.NormalsThreshold = InStaticMeshReductionSettings.HardAngleThreshold; OutSkeletalMeshReductionSettings.bRecalcNormals = InStaticMeshReductionSettings.bRecalculateNormals; OutSkeletalMeshReductionSettings.BaseLOD = InStaticMeshReductionSettings.BaseLODModel; OutSkeletalMeshReductionSettings.SilhouetteImportance = ConvertOptimizationImportance(InStaticMeshReductionSettings.SilhouetteImportance); OutSkeletalMeshReductionSettings.TextureImportance = ConvertOptimizationImportance(InStaticMeshReductionSettings.TextureImportance); OutSkeletalMeshReductionSettings.ShadingImportance = ConvertOptimizationImportance(InStaticMeshReductionSettings.ShadingImportance); switch(InStaticMeshReductionSettings.TerminationCriterion) { case EStaticMeshReductionTerimationCriterion::Triangles: OutSkeletalMeshReductionSettings.TerminationCriterion = SMTC_NumOfTriangles; break; case EStaticMeshReductionTerimationCriterion::Vertices: OutSkeletalMeshReductionSettings.TerminationCriterion = SMTC_NumOfVerts; break; case EStaticMeshReductionTerimationCriterion::Any: OutSkeletalMeshReductionSettings.TerminationCriterion = SMTC_TriangleOrVert; break; } } static bool AddLODFromMeshDescription( FMeshDescription&& InMeshDescription, USkeletalMesh* InSkeletalMesh, IMeshUtilities& InMeshUtilities, const bool bCacheOptimize = true) { FSkeletalMeshModel* ImportedModels = InSkeletalMesh->GetImportedModel(); const int32 LODIndex = ImportedModels->LODModels.Num(); ImportedModels->LODModels.Add(new FSkeletalMeshLODModel); if (!ensure(ImportedModels->LODModels.Num() == InSkeletalMesh->GetLODNum())) { return false; } FSkeletalMeshImportData SkeletalMeshImportGeometry = FSkeletalMeshImportData::CreateFromMeshDescription(InMeshDescription); InSkeletalMesh->CreateMeshDescription(LODIndex, MoveTemp(InMeshDescription)); InSkeletalMesh->CommitMeshDescription(LODIndex); FSkeletalMeshLODModel& SkeletalMeshModel = ImportedModels->LODModels.Last(); // We need at least one set of texture coordinates. Always. SkeletalMeshModel.NumTexCoords = FMath::Max(1, SkeletalMeshImportGeometry.NumTexCoords); // Data needed by BuildSkeletalMesh TArray LODPoints; TArray LODWedges; TArray LODFaces; TArray LODInfluences; TArray LODPointToRawMap; SkeletalMeshImportGeometry.CopyLODImportData( LODPoints, LODWedges, LODFaces, LODInfluences, LODPointToRawMap ); IMeshUtilities::MeshBuildOptions BuildOptions; BuildOptions.TargetPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform(); BuildOptions.FillOptions(InSkeletalMesh->GetLODInfo(InSkeletalMesh->GetLODNum() - 1)->BuildSettings); BuildOptions.bCacheOptimize = bCacheOptimize; TArray WarningMessages; if (!InMeshUtilities.BuildSkeletalMesh(SkeletalMeshModel, InSkeletalMesh->GetPathName(), InSkeletalMesh->GetRefSkeleton(), LODInfluences, LODWedges, LODFaces, LODPoints, LODPointToRawMap, BuildOptions, &WarningMessages, nullptr)) { for(const FText& Message: WarningMessages) { UE_LOG(LogStaticToSkeletalMeshConverter, Warning, TEXT("%s"), *Message.ToString()); } return false; } return true; } static bool AddLODFromStaticMeshSourceModel( const FStaticMeshSourceModel& InStaticMeshSourceModel, USkeletalMesh* InSkeletalMesh, const FBoneIndexType InBoneIndex, IMeshUtilities& InMeshUtilities ) { // Always copy the build and reduction settings. FSkeletalMeshLODInfo& SkeletalLODInfo = InSkeletalMesh->AddLODInfo(); SkeletalLODInfo.ScreenSize = InStaticMeshSourceModel.ScreenSize; CopyBuildSettings(InStaticMeshSourceModel.BuildSettings, SkeletalLODInfo.BuildSettings); CopyReductionSettings(InStaticMeshSourceModel.ReductionSettings, SkeletalLODInfo.ReductionSettings); FSkeletalMeshModel* ImportedModels = InSkeletalMesh->GetImportedModel(); const int32 LODIndex = ImportedModels->LODModels.Num(); if (InStaticMeshSourceModel.IsMeshDescriptionValid()) { FMeshDescription SkeletalMeshGeometry; if (!InStaticMeshSourceModel.CloneMeshDescription(SkeletalMeshGeometry)) { return false; } FSkeletalMeshAttributes SkeletalMeshAttributes(SkeletalMeshGeometry); SkeletalMeshAttributes.Register(); // Fill Bones data. const FReferenceSkeleton RefSkeleton = InSkeletalMesh->GetRefSkeleton(); const int32 NumRefBones = InSkeletalMesh->GetRefSkeleton().GetRawBoneNum(); FSkeletalMeshAttributes::FBoneArray& Bones = SkeletalMeshAttributes.Bones(); Bones.Reset(NumRefBones); FSkeletalMeshAttributes::FBoneNameAttributesRef BoneNames = SkeletalMeshAttributes.GetBoneNames(); FSkeletalMeshAttributes::FBoneParentIndexAttributesRef BoneParentIndices = SkeletalMeshAttributes.GetBoneParentIndices(); FSkeletalMeshAttributes::FBonePoseAttributesRef BonePoses = SkeletalMeshAttributes.GetBonePoses(); for (int Index = 0; Index < NumRefBones; ++Index) { const FMeshBoneInfo& BoneInfo = RefSkeleton.GetRawRefBoneInfo()[Index]; const FTransform& BoneTransform = RefSkeleton.GetRawRefBonePose()[Index]; const FBoneID BoneID = SkeletalMeshAttributes.CreateBone(); BoneNames.Set(BoneID, BoneInfo.Name); BoneParentIndices.Set(BoneID, BoneInfo.ParentIndex); BonePoses.Set(BoneID, BoneTransform); } // Full binding to the root bone. FSkinWeightsVertexAttributesRef SkinWeights = SkeletalMeshAttributes.GetVertexSkinWeights(); UE::AnimationCore::FBoneWeight RootInfluence(InBoneIndex, 1.0f); UE::AnimationCore::FBoneWeights RootBinding = UE::AnimationCore::FBoneWeights::Create({RootInfluence}); for (const FVertexID VertexID: SkeletalMeshGeometry.Vertices().GetElementIDs()) { SkinWeights.Set(VertexID, RootBinding); } // Convert weird static mesh inverse sRGB gamma to linear. // FIXME: Remove once static mesh color space has been fixed to be linear again. TVertexInstanceAttributesRef VertexInstanceColors = SkeletalMeshAttributes.GetVertexInstanceColors(); auto ConvertLinearToSRGBGamma = [](float V) { V = FMath::Clamp(V, 0.0f, 1.0f); if (V <= 0.0031308) { return V * 12.92f; } else { return 1.055f * FMath::Pow(V, 1.0f / 2.4f) - 0.055f; } }; for (FVertexInstanceID VertexInstanceID: SkeletalMeshGeometry.VertexInstances().GetElementIDs()) { FLinearColor VertexColor = VertexInstanceColors.Get(VertexInstanceID); VertexColor.R = ConvertLinearToSRGBGamma(VertexColor.R); VertexColor.G = ConvertLinearToSRGBGamma(VertexColor.G); VertexColor.B = ConvertLinearToSRGBGamma(VertexColor.B); VertexInstanceColors.Set(VertexInstanceID, VertexColor); } if (!AddLODFromMeshDescription(MoveTemp(SkeletalMeshGeometry), InSkeletalMesh, InMeshUtilities)) { return false; } } else { ImportedModels->LODModels.Add(new FSkeletalMeshLODModel); FSkeletalMeshUpdateContext UpdateContext; UpdateContext.SkeletalMesh = InSkeletalMesh; FLODUtilities::SimplifySkeletalMeshLOD(UpdateContext, LODIndex, GetTargetPlatformManagerRef().GetRunningTargetPlatform()); } return true; } static bool HasVertexColors( const USkeletalMesh* InSkeletalMesh ) { for (const FSkeletalMeshLODModel& LODModel: InSkeletalMesh->GetImportedModel()->LODModels) { for (const FSkelMeshSection& Section: LODModel.Sections) { for (const FSoftSkinVertex& Vertex: Section.SoftVertices) { if (Vertex.Color != FColor::White) { return true; } } } } return false; } bool FStaticToSkeletalMeshConverter::InitializeSkeletalMeshFromStaticMesh( USkeletalMesh* InSkeletalMesh, const UStaticMesh* InStaticMesh, const FReferenceSkeleton& InReferenceSkeleton, const FName InBindBone ) { if (!ensure(InSkeletalMesh)) { return false; } if (!InSkeletalMesh->GetImportedModel()->LODModels.IsEmpty()) { UE_LOG(LogStaticToSkeletalMeshConverter, Error, TEXT("Skeletal mesh '%s' is not empty"), *InSkeletalMesh->GetPathName()); return false; } if (!ensure(InStaticMesh)) { return false; } int32 BoneIndex = 0; if (!InBindBone.IsNone()) { BoneIndex = InReferenceSkeleton.FindRawBoneIndex(InBindBone); if (BoneIndex == INDEX_NONE) { UE_LOG(LogStaticToSkeletalMeshConverter, Error, TEXT("Bone '%s' not found in skeleton."), *InBindBone.ToString()); return false; } } // This ensures that the render data gets built before we return, by calling PostEditChange when we fall out of scope. FScopedSkeletalMeshPostEditChange ScopedPostEditChange( InSkeletalMesh ); InSkeletalMesh->PreEditChange( nullptr ); InSkeletalMesh->SetRefSkeleton(InReferenceSkeleton); // Calculate the initial pose from the reference skeleton. InSkeletalMesh->CalculateInvRefMatrices(); IMeshUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked( "MeshUtilities" ); // Copy the LODs and LOD settings over (as close as we can). bool bFirstSourceModel = true; for (const FStaticMeshSourceModel& StaticMeshSourceModel: InStaticMesh->GetSourceModels()) { if (!AddLODFromStaticMeshSourceModel( StaticMeshSourceModel, InSkeletalMesh, static_cast(BoneIndex), MeshUtilities)) { // If we didn't get a model for LOD index 0, we don't have a mesh. Bail out. if (bFirstSourceModel) { return false; } // Otherwise, we have a model, so let's continue with what we have. break; } bFirstSourceModel = false; } // Convert the materials over. TArray Materials; for (const FStaticMaterial& StaticMaterial: InStaticMesh->GetStaticMaterials()) { FSkeletalMaterial Material( StaticMaterial.MaterialInterface, StaticMaterial.MaterialSlotName, StaticMaterial.ImportedMaterialSlotName); Materials.Add(Material); } InSkeletalMesh->SetMaterials(Materials); if (HasVertexColors(InSkeletalMesh)) { InSkeletalMesh->SetHasVertexColors(true); InSkeletalMesh->SetVertexColorGuid(FGuid::NewGuid()); } // Set the bounds from the static mesh, including the extensions, otherwise it won't render properly (among other things). InSkeletalMesh->SetImportedBounds( InStaticMesh->GetBounds() ); InSkeletalMesh->SetPositiveBoundsExtension(InStaticMesh->GetPositiveBoundsExtension()); InSkeletalMesh->SetNegativeBoundsExtension(InStaticMesh->GetNegativeBoundsExtension()); //Create some import data so we can re-import this new skeletalmesh UAssetImportData* OriginalAssetImportData = InStaticMesh->GetAssetImportData(); if (OriginalAssetImportData) { UAssetImportData* DuplicateAssetImportData = DuplicateObject(OriginalAssetImportData, InSkeletalMesh); DuplicateAssetImportData->ConvertAssetImportDataToNewOwner(InSkeletalMesh); InSkeletalMesh->SetAssetImportData(DuplicateAssetImportData); } return true; } static bool ValidateSkinWeightAttribute( const FMeshDescription& InMeshDescription, const FReferenceSkeleton& InReferenceSkeleton ) { using namespace UE::AnimationCore; FSkeletalMeshConstAttributes MeshAttributes{InMeshDescription}; TArray Profiles = MeshAttributes.GetSkinWeightProfileNames(); if (Profiles.IsEmpty()) { UE_LOG(LogStaticToSkeletalMeshConverter, Error, TEXT("Mesh description doesn't have a skin weight attribute.")); return false; } FBoneIndexType BoneIndexMax = static_cast(InReferenceSkeleton.GetRawBoneNum()); // We use the first profile. Usually that's the default profile, unless we have nothing but alternate profiles. FSkinWeightsVertexAttributesConstRef VertexSkinWeights = MeshAttributes.GetVertexSkinWeights(Profiles[0]); for (const FVertexID VertexID: InMeshDescription.Vertices().GetElementIDs()) { for (FBoneWeight BoneWeight: VertexSkinWeights.Get(VertexID)) { if (BoneWeight.GetBoneIndex() >= BoneIndexMax) { UE_LOG(LogStaticToSkeletalMeshConverter, Error, TEXT("Mesh description's skin weight refers to a non-existent bone (%d of %d)."), BoneWeight.GetBoneIndex(), BoneIndexMax); return false; } } } return true; } bool FStaticToSkeletalMeshConverter::InitializeSkeletalMeshFromMeshDescriptions( USkeletalMesh* InSkeletalMesh, TArrayView InMeshDescriptions, TConstArrayView InMaterials, const FReferenceSkeleton& InReferenceSkeleton, const bool bInRecomputeNormals, const bool bInRecomputeTangents, const bool bCacheOptimize ) { if (!ensure(InSkeletalMesh)) { return false; } if (!InSkeletalMesh->GetImportedModel()->LODModels.IsEmpty()) { UE_LOG(LogStaticToSkeletalMeshConverter, Error, TEXT("Skeletal mesh '%s' is not empty"), *InSkeletalMesh->GetPathName()); return false; } if (InMeshDescriptions.IsEmpty()) { UE_LOG(LogStaticToSkeletalMeshConverter, Error, TEXT("No mesh descriptions given")); return false; } // Ensure all mesh descriptions have a skin weight attribute. for (const FMeshDescription* MeshDescription: InMeshDescriptions) { if (!ValidateSkinWeightAttribute(*MeshDescription, InReferenceSkeleton)) { return false; } } // Set the materials before we start converting. We'll add dummy materials afterward if there are more sections // than materials in any of the LODs. Not the best system, but the best we have for now. InSkeletalMesh->SetMaterials(TArray{InMaterials}); TSet ValidMaterialSlotNames; for (int32 Index = 0; Index < InMaterials.Num(); Index++) { const FSkeletalMaterial& Material = InMaterials[Index]; if (!Material.MaterialSlotName.IsNone()) { ValidMaterialSlotNames.Add(Material.MaterialSlotName); } } // This ensures that the render data gets built before we return, by calling PostEditChange when we fall out of scope. { FScopedSkeletalMeshPostEditChange ScopedPostEditChange( InSkeletalMesh ); InSkeletalMesh->PreEditChange( nullptr ); InSkeletalMesh->SetRefSkeleton(InReferenceSkeleton); // Calculate the initial pose from the reference skeleton. InSkeletalMesh->CalculateInvRefMatrices(); IMeshUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked( "MeshUtilities" ); bool bFirstSourceModel = true; for (const FMeshDescription* MeshDescription: InMeshDescriptions) { // Add default LOD build settings. FSkeletalMeshLODInfo& SkeletalLODInfo = InSkeletalMesh->AddLODInfo(); SkeletalLODInfo.ReductionSettings.NumOfTrianglesPercentage = 1.0f; SkeletalLODInfo.ReductionSettings.NumOfVertPercentage = 1.0f; SkeletalLODInfo.ReductionSettings.MaxDeviationPercentage = 0.0f; SkeletalLODInfo.LODHysteresis = 0.02f; SkeletalLODInfo.BuildSettings.bRecomputeNormals = bInRecomputeNormals; SkeletalLODInfo.BuildSettings.bRecomputeTangents = bInRecomputeTangents; FMeshDescription ClonedDescription(*MeshDescription); // Fix up the material slot names on the mesh to match the ones in the material list. If the name is // either NAME_None, or doesn't exist in the material list, we use the group index to index into the // material list to resolve the name. FSkeletalMeshAttributes Attributes(ClonedDescription); TPolygonGroupAttributesRef MaterialSlotNamesAttribute = Attributes.GetPolygonGroupMaterialSlotNames(); for (FPolygonGroupID PolygonGroupID: ClonedDescription.PolygonGroups().GetElementIDs()) { if (!ValidMaterialSlotNames.Contains(MaterialSlotNamesAttribute.Get(PolygonGroupID))) { int32 MaterialIndex = PolygonGroupID.GetValue(); MaterialIndex = FMath::Clamp(MaterialIndex, 0, InMaterials.Num() - 1); MaterialSlotNamesAttribute.Set(PolygonGroupID, InMaterials[MaterialIndex].MaterialSlotName); } } if (!AddLODFromMeshDescription(MoveTemp(ClonedDescription), InSkeletalMesh, MeshUtilities, bCacheOptimize)) { // If we didn't get a model for LOD index 0, we don't have a mesh. Bail out. if (bFirstSourceModel) { return false; } // Otherwise, we have a model, so let's continue with what we have. break; } bFirstSourceModel = false; } } // Compute the bbox, now that we have the model mesh generated. FBox3f BoundingBox{ForceInit}; int32 MaxSectionCount = 0; for (const FSkeletalMeshLODModel& MeshModel: InSkeletalMesh->GetImportedModel()->LODModels) { MaxSectionCount = FMath::Max(MaxSectionCount, MeshModel.Sections.Num()); // Compute the overall bbox. for (const FSkelMeshSection& Section: MeshModel.Sections) { for (const FSoftSkinVertex& Vertex: Section.SoftVertices) { BoundingBox += Vertex.Position; } } } // If we're short on materials, compared to sections, add dummy materials to fill in the gap. Not ideal, but // best we can do for now. const TArray& ExistingMaterials = InSkeletalMesh->GetMaterials(); if (MaxSectionCount > ExistingMaterials.Num()) { TArray NewMaterials{ExistingMaterials}; for (int32 Index = ExistingMaterials.Num(); Index < MaxSectionCount; Index++) { NewMaterials.Add(FSkeletalMaterial{}); } InSkeletalMesh->SetMaterials(NewMaterials); } InSkeletalMesh->SetImportedBounds( FBox3d{BoundingBox} ); return true; } #endif