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

783 lines
27 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
Implementation of Skeletal Mesh export related functionality from FbxExporter
=============================================================================*/
#include "CoreMinimal.h"
#include "Animation/Skeleton.h"
#include "BoneWeights.h"
#include "GPUSkinPublicDefs.h"
#include "Components/SkeletalMeshComponent.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/SkinnedAssetCommon.h"
#include "Animation/AnimSequence.h"
#include "Rendering/SkeletalMeshModel.h"
#include "FbxExporter.h"
#include "Exporters/FbxExportOption.h"
#include "UObject/MetaData.h"
#include "FbxMaterialExportUtilities.h"
DEFINE_LOG_CATEGORY_STATIC(LogFbxSkeletalMeshExport, Log, All);
namespace UnFbx
{
/**
* Adds FBX skeleton nodes to the FbxScene based on the skeleton in the given USkeletalMesh, and fills
* the given array with the nodes created
*/
FbxNode* FFbxExporter::CreateSkeleton(const USkeletalMesh* SkelMesh, TArray<FbxNode*>& BoneNodes)
{
const FReferenceSkeleton& RefSkeleton= SkelMesh->GetRefSkeleton();
if(RefSkeleton.GetRawBoneNum() == 0)
{
return nullptr;
}
// Create a list of the nodes we create for each bone, so that children can
// later look up their parent
BoneNodes.Reserve(RefSkeleton.GetRawBoneNum());
for(int32 BoneIndex = 0; BoneIndex < RefSkeleton.GetRawBoneNum(); ++BoneIndex)
{
const FMeshBoneInfo& CurrentBone = RefSkeleton.GetRefBoneInfo()[BoneIndex];
const FTransform& BoneTransform = RefSkeleton.GetRefBonePose()[BoneIndex];
FbxString BoneName = Converter.ConvertToFbxString(CurrentBone.ExportName);
// Create the node's attributes
FbxSkeleton* SkeletonAttribute = FbxSkeleton::Create(Scene, BoneName.Buffer());
if(BoneIndex)
{
SkeletonAttribute->SetSkeletonType(FbxSkeleton::eLimbNode);
//SkeletonAttribute->Size.Set(1.0);
}
else
{
SkeletonAttribute->SetSkeletonType(FbxSkeleton::eRoot);
//SkeletonAttribute->Size.Set(1.0);
}
// Create the node
FbxNode* BoneNode = FbxNode::Create(Scene, BoneName.Buffer());
BoneNode->SetNodeAttribute(SkeletonAttribute);
// Set the bone node's local orientation
FVector UnrealRotation = BoneTransform.GetRotation().Euler();
FbxVector4 LocalPos = Converter.ConvertToFbxPos(BoneTransform.GetTranslation());
FbxVector4 LocalRot = Converter.ConvertToFbxRot(UnrealRotation);
FbxVector4 LocalScale = Converter.ConvertToFbxScale(BoneTransform.GetScale3D());
BoneNode->LclTranslation.Set(LocalPos);
BoneNode->LclRotation.Set(LocalRot);
BoneNode->LclScaling.Set(LocalScale);
// If this is not the root bone, attach it to its parent
if(BoneIndex)
{
BoneNodes[CurrentBone.ParentIndex]->AddChild(BoneNode);
}
// Add the node to the list of nodes, in bone order
BoneNodes.Push(BoneNode);
}
return BoneNodes[0];
}
void FFbxExporter::GetSkeleton(FbxNode* RootNode, TArray<FbxNode*>& BoneNodes)
{
if (RootNode->GetSkeleton())
{
BoneNodes.Add(RootNode);
}
for (int32 ChildIndex=0; ChildIndex<RootNode->GetChildCount(); ++ChildIndex)
{
GetSkeleton(RootNode->GetChild(ChildIndex), BoneNodes);
}
}
/**
* Adds an Fbx Mesh to the FBX scene based on the data in the given FSkeletalMeshLODModel
*/
FbxNode* FFbxExporter::CreateMesh(const USkeletalMesh* SkelMesh, const TCHAR* MeshName, int32 LODIndex, const UAnimSequence* AnimSeq /*= nullptr*/, const TArray<UMaterialInterface*>* OverrideMaterials /*= nullptr*/)
{
const FSkeletalMeshModel* SkelMeshResource = SkelMesh->GetImportedModel();
if (!SkelMeshResource->LODModels.IsValidIndex(LODIndex))
{
//Return an empty node
return FbxNode::Create(Scene, TCHAR_TO_UTF8(MeshName));
}
const FSkeletalMeshLODModel& SourceModel = SkelMeshResource->LODModels[LODIndex];
const int32 VertexCount = SourceModel.GetNumNonClothingVertices();
// Verify the integrity of the mesh.
if (VertexCount == 0) return nullptr;
// Copy all the vertex data from the various chunks to a single buffer.
// Makes the rest of the code in this function cleaner and easier to maintain.
TArray<FSoftSkinVertex> Vertices;
SourceModel.GetNonClothVertices(Vertices);
if (Vertices.Num() != VertexCount) return nullptr;
FbxMesh* Mesh = FbxMesh::Create(Scene, TCHAR_TO_UTF8(MeshName));
// Create and fill in the vertex position data source.
Mesh->InitControlPoints(VertexCount);
FbxVector4* ControlPoints = Mesh->GetControlPoints();
for (int32 VertIndex = 0; VertIndex < VertexCount; ++VertIndex)
{
FVector Position = (FVector)Vertices[VertIndex].Position;
ControlPoints[VertIndex] = Converter.ConvertToFbxPos(Position);
}
// Create Layer 0 to hold the normals
FbxLayer* LayerZero = Mesh->GetLayer(0);
if (LayerZero == nullptr)
{
Mesh->CreateLayer();
LayerZero = Mesh->GetLayer(0);
}
// Create and fill in the per-face-vertex normal data source.
// We extract the Z-tangent and drop the X/Y-tangents which are also stored in the render mesh.
FbxLayerElementNormal* LayerElementNormal= FbxLayerElementNormal::Create(Mesh, "");
LayerElementNormal->SetMappingMode(FbxLayerElement::eByControlPoint);
// Set the normal values for every control point.
LayerElementNormal->SetReferenceMode(FbxLayerElement::eDirect);
for (int32 VertIndex = 0; VertIndex < VertexCount; ++VertIndex)
{
FVector Normal = (FVector4)Vertices[VertIndex].TangentZ;
FbxVector4 FbxNormal = Converter.ConvertToFbxPos(Normal);
LayerElementNormal->GetDirectArray().Add(FbxNormal);
}
LayerZero->SetNormals(LayerElementNormal);
// Create and fill in the per-face-vertex texture coordinate data source(s).
// Create UV for Diffuse channel.
const int32 TexCoordSourceCount = SourceModel.NumTexCoords;
TCHAR UVChannelName[32];
for (int32 TexCoordSourceIndex = 0; TexCoordSourceIndex < TexCoordSourceCount; ++TexCoordSourceIndex)
{
FbxLayer* Layer = Mesh->GetLayer(TexCoordSourceIndex);
if (Layer == nullptr)
{
Mesh->CreateLayer();
Layer = Mesh->GetLayer(TexCoordSourceIndex);
}
if (TexCoordSourceIndex == 1)
{
FCString::Sprintf(UVChannelName, TEXT("LightMapUV"));
}
else
{
FCString::Sprintf(UVChannelName, TEXT("DiffuseUV"));
}
FbxLayerElementUV* UVDiffuseLayer = FbxLayerElementUV::Create(Mesh, TCHAR_TO_UTF8(UVChannelName));
UVDiffuseLayer->SetMappingMode(FbxLayerElement::eByControlPoint);
UVDiffuseLayer->SetReferenceMode(FbxLayerElement::eDirect);
// Create the texture coordinate data source.
for (int32 TexCoordIndex = 0; TexCoordIndex < VertexCount; ++TexCoordIndex)
{
const FVector2D& TexCoord = FVector2D(Vertices[TexCoordIndex].UVs[TexCoordSourceIndex]);
UVDiffuseLayer->GetDirectArray().Add(FbxVector2(TexCoord.X, -TexCoord.Y + 1.0));
}
Layer->SetUVs(UVDiffuseLayer, FbxLayerElement::eTextureDiffuse);
}
FbxLayerElementMaterial* MatLayer = FbxLayerElementMaterial::Create(Mesh, "");
MatLayer->SetMappingMode(FbxLayerElement::eByPolygon);
MatLayer->SetReferenceMode(FbxLayerElement::eIndexToDirect);
LayerZero->SetMaterials(MatLayer);
// Create the per-material polygons sets.
int32 SectionCount = SourceModel.Sections.Num();
int32 ClothSectionVertexRemoveOffset = 0;
TArray<TPair<uint32, uint32>> VertexIndexOffsetPairArray{ TPair<uint32, uint32>(0,0) };
for (int32 SectionIndex = 0; SectionIndex < SectionCount; ++SectionIndex)
{
const FSkelMeshSection& Section = SourceModel.Sections[SectionIndex];
if (Section.HasClothingData())
{
ClothSectionVertexRemoveOffset += Section.GetNumVertices();
VertexIndexOffsetPairArray.Emplace(Section.BaseVertexIndex, ClothSectionVertexRemoveOffset);
continue;
}
int32 MatIndex = Section.MaterialIndex;
// Static meshes contain one triangle list per element.
int32 TriangleCount = Section.NumTriangles;
// Copy over the index buffer into the FBX polygons set.
for (int32 TriangleIndex = 0; TriangleIndex < TriangleCount; ++TriangleIndex)
{
Mesh->BeginPolygon(MatIndex);
for (int32 PointIndex = 0; PointIndex < 3; PointIndex++)
{
int32 VertexPositionIndex = SourceModel.IndexBuffer[Section.BaseIndex + ((TriangleIndex * 3) + PointIndex)] - ClothSectionVertexRemoveOffset;
check(VertexPositionIndex >= 0);
Mesh->AddPolygon(VertexPositionIndex);
}
Mesh->EndPolygon();
}
}
if (GetExportOptions()->VertexColor)
{
// Create and fill in the vertex color data source.
FbxLayerElementVertexColor* VertexColor = FbxLayerElementVertexColor::Create(Mesh, "");
VertexColor->SetMappingMode(FbxLayerElement::eByControlPoint);
VertexColor->SetReferenceMode(FbxLayerElement::eDirect);
FbxLayerElementArrayTemplate<FbxColor>& VertexColorArray = VertexColor->GetDirectArray();
LayerZero->SetVertexColors(VertexColor);
for (int32 VertIndex = 0; VertIndex < VertexCount; ++VertIndex)
{
FLinearColor VertColor = Vertices[VertIndex].Color.ReinterpretAsLinear();
VertexColorArray.Add(FbxColor(VertColor.R, VertColor.G, VertColor.B, VertColor.A));
}
}
if (GetExportOptions()->bExportMorphTargets && SkelMesh->GetSkeleton()) //The skeleton can be null if this is a destructible mesh.
{
TMap<FName, FbxAnimCurve*> BlendShapeCurvesMap;
if (SkelMesh->GetMorphTargets().Num())
{
// The original BlendShape Name was not saved during import, so we need to come up with a new one.
const FString BlendShapeName(SkelMesh->GetName() + TEXT("_blendShapes"));
FbxBlendShape* BlendShape = FbxBlendShape::Create(Mesh, TCHAR_TO_UTF8(*BlendShapeName));
bool bHasBadMorphTarget = false;
for (UMorphTarget* MorphTarget : SkelMesh->GetMorphTargets())
{
int32 DeformerIndex = Mesh->AddDeformer(BlendShape);
FbxBlendShapeChannel* BlendShapeChannel = FbxBlendShapeChannel::Create(BlendShape, TCHAR_TO_UTF8(*MorphTarget->GetName()));
if (BlendShape->AddBlendShapeChannel(BlendShapeChannel))
{
FbxShape* Shape = FbxShape::Create(BlendShapeChannel, TCHAR_TO_UTF8(*MorphTarget->GetName()));
Shape->InitControlPoints(VertexCount);
FbxVector4* ShapeControlPoints = Shape->GetControlPoints();
// Replicate the base mesh in the shape control points to set up the data.
for (int32 VertIndex = 0; VertIndex < VertexCount; ++VertIndex)
{
FVector Position = (FVector)Vertices[VertIndex].Position;
ShapeControlPoints[VertIndex] = Converter.ConvertToFbxPos(Position);
}
int32 NumberOfDeltas = 0;
const FMorphTargetDelta* MorphTargetDeltas = MorphTarget->GetMorphTargetDelta(LODIndex, NumberOfDeltas);
for (int32 MorphTargetDeltaIndex = 0; MorphTargetDeltaIndex < NumberOfDeltas; ++MorphTargetDeltaIndex)
{
// Apply the morph target deltas to the control points.
const FMorphTargetDelta& CurrentDelta = MorphTargetDeltas[MorphTargetDeltaIndex];
uint32 RemappedSourceIndex = CurrentDelta.SourceIdx;
if (VertexIndexOffsetPairArray.Num() > 1)
{
//If the skeletal mesh contains clothing we need to remap the morph target index too.
int32 UpperBoundIndex = Algo::UpperBoundBy(VertexIndexOffsetPairArray, RemappedSourceIndex,
[](const auto& CurrentPair) { return CurrentPair.Key; }); //Value functor
RemappedSourceIndex -= VertexIndexOffsetPairArray[UpperBoundIndex - 1].Value;
}
if ( RemappedSourceIndex < static_cast<uint32>( VertexCount ) )
{
ShapeControlPoints[RemappedSourceIndex] = Converter.ConvertToFbxPos(FVector(Vertices[RemappedSourceIndex].Position + CurrentDelta.PositionDelta));
}
else
{
bHasBadMorphTarget = true;
}
}
BlendShapeChannel->AddTargetShape(Shape);
FName MorphTargetName = MorphTarget->GetFName();
const FCurveMetaData* CurveMetaData = SkelMesh->GetSkeleton()->GetCurveMetaData(MorphTargetName);
if (AnimSeq && CurveMetaData && CurveMetaData->Type.bMorphtarget)
{
FbxAnimCurve* AnimCurve = Mesh->GetShapeChannel(DeformerIndex, BlendShape->GetBlendShapeChannelCount() - 1, AnimLayer, true);
BlendShapeCurvesMap.Add(MorphTargetName, AnimCurve);
}
}
}
if ( bHasBadMorphTarget )
{
UE_LOG( LogFbx, Warning, TEXT( "Encountered corrupted morphtarget(s) during export of SkeletalMesh %s, bad vertices were ignored." ), *SkelMesh->GetName() );
}
}
if(AnimSeq && BlendShapeCurvesMap.Num() > 0)
{
ExportCustomAnimCurvesToFbx(BlendShapeCurvesMap, AnimSeq,
FFrameTime(0), // Start frame to export
FFrameTime(AnimSeq->GetDataModel()->GetNumberOfFrames()), // Final frame to export
1.f, // Frame rate scale
0.f, // FBX StartTime
100.f); // ValueScale, for some reason we need to scale BlendShape curves by a factor of 100.
}
}
FbxNode* MeshNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(MeshName));
MeshNode->SetNodeAttribute(Mesh);
// Add the materials for the mesh
const TArray<FSkeletalMaterial>& SkelMeshMaterials = SkelMesh->GetMaterials();
int32 MaterialCount = SkelMeshMaterials.Num();
//TODO: move the init of MaterialBakingMeshData up a callstack step:
FFbxMaterialBakingMeshData MaterialBakingMeshData(SkelMesh, nullptr, 0);
for(int32 MaterialIndex = 0; MaterialIndex < MaterialCount; ++MaterialIndex)
{
UMaterialInterface* MatInterface = nullptr;
if (OverrideMaterials && OverrideMaterials->IsValidIndex(MaterialIndex))
{
MatInterface = (*OverrideMaterials)[MaterialIndex];
}
else
{
MatInterface = SkelMeshMaterials[MaterialIndex].MaterialInterface;
}
FbxSurfaceMaterial* FbxMaterial = nullptr;
if (LODIndex == 0)
{
if (MatInterface)
{
FbxMaterial = ExportMaterial(MatInterface, MaterialIndex, MaterialBakingMeshData);
}
}
else if(MatInterface)
{
TMap<int32, FbxSurfaceMaterial*>* MaterialIndexToFbxMaterials = FbxMaterials.Find(MatInterface);
if (MaterialIndexToFbxMaterials && MaterialIndexToFbxMaterials->Find(MaterialIndex))
{
if (FbxSurfaceMaterial** FbxMaterialPtr = MaterialIndexToFbxMaterials->Find(MaterialIndex))
{
FbxMaterial = *FbxMaterialPtr;
}
}
}
if(!FbxMaterial)
{
// Note: The vertex data relies on there being a set number of Materials.
// If you try to add the same material again it will not be added, so create a
// default material with a unique name to ensure the proper number of materials
TCHAR NewMaterialName[MAX_SPRINTF] = TEXT("");
FCString::Sprintf(NewMaterialName, TEXT("Fbx Default Material %i"), MaterialIndex);
FbxMaterial = FbxSurfaceLambert::Create(Scene, TCHAR_TO_UTF8(NewMaterialName));
((FbxSurfaceLambert*)FbxMaterial)->Diffuse.Set(FbxDouble3(0.72, 0.72, 0.72));
}
MeshNode->AddMaterial(FbxMaterial);
}
int32 SavedMaterialCount = MeshNode->GetMaterialCount();
check(SavedMaterialCount == MaterialCount);
return MeshNode;
}
/**
* Adds Fbx Clusters necessary to skin a skeletal mesh to the bones in the BoneNodes list
*/
void FFbxExporter::BindMeshToSkeleton(const USkeletalMesh* SkelMesh, FbxNode* MeshRootNode, TArray<FbxNode*>& BoneNodes, int32 LODIndex)
{
const FSkeletalMeshModel* SkelMeshResource = SkelMesh->GetImportedModel();
if (!SkelMeshResource->LODModels.IsValidIndex(LODIndex))
{
//We cannot bind the LOD if its not valid
return;
}
const FSkeletalMeshLODModel& SourceModel = SkelMeshResource->LODModels[LODIndex];
const int32 VertexCount = SourceModel.NumVertices;
FbxAMatrix MeshMatrix;
FbxScene* lScene = MeshRootNode->GetScene();
if( lScene )
{
MeshMatrix = MeshRootNode->EvaluateGlobalTransform();
}
FbxGeometry* MeshAttribute = (FbxGeometry*) MeshRootNode->GetNodeAttribute();
FbxSkin* Skin = FbxSkin::Create(Scene, "");
const int32 BoneCount = BoneNodes.Num();
for(int32 BoneIndex = 0; BoneIndex < BoneCount; ++BoneIndex)
{
FbxNode* BoneNode = BoneNodes[BoneIndex];
// Create the deforming cluster
FbxCluster *CurrentCluster = FbxCluster::Create(Scene,"");
CurrentCluster->SetLink(BoneNode);
CurrentCluster->SetLinkMode(FbxCluster::eTotalOne);
// Add all the vertices that are weighted to the current skeletal bone to the cluster
// NOTE: the bone influence indices contained in the vertex data are based on a per-chunk
// list of verts. The convert the chunk bone index to the mesh bone index, the chunk's
// boneMap is needed
int32 VertIndex = 0;
const int32 SectionCount = SourceModel.Sections.Num();
for(int32 SectionIndex = 0; SectionIndex < SectionCount; ++SectionIndex)
{
const FSkelMeshSection& Section = SourceModel.Sections[SectionIndex];
if (Section.HasClothingData())
{
continue;
}
for(int32 SoftIndex = 0; SoftIndex < Section.SoftVertices.Num(); ++SoftIndex)
{
const FSoftSkinVertex& Vert = Section.SoftVertices[SoftIndex];
for(int32 InfluenceIndex = 0; InfluenceIndex < MAX_TOTAL_INFLUENCES; ++InfluenceIndex)
{
const int32 InfluenceBone = Section.BoneMap[ Vert.InfluenceBones[InfluenceIndex] ];
const float InfluenceWeight = Vert.InfluenceWeights[InfluenceIndex] / UE::AnimationCore::MaxRawBoneWeightFloat;
if(InfluenceBone == BoneIndex && InfluenceWeight > 0.f)
{
CurrentCluster->AddControlPointIndex(VertIndex, InfluenceWeight);
}
}
++VertIndex;
}
}
// Now we have the Patch and the skeleton correctly positioned,
// set the Transform and TransformLink matrix accordingly.
CurrentCluster->SetTransformMatrix(MeshMatrix);
FbxAMatrix LinkMatrix;
if( lScene )
{
LinkMatrix = BoneNode->EvaluateGlobalTransform();
}
CurrentCluster->SetTransformLinkMatrix(LinkMatrix);
// Add the clusters to the mesh by creating a skin and adding those clusters to that skin.
Skin->AddCluster(CurrentCluster);
}
// Add the skin to the mesh after the clusters have been added
MeshAttribute->AddDeformer(Skin);
}
// Add the specified node to the node array. Also, add recursively
// all the parent node of the specified node to the array.
void AddNodeRecursively(FbxArray<FbxNode*>& pNodeArray, FbxNode* pNode)
{
if (pNode)
{
AddNodeRecursively(pNodeArray, pNode->GetParent());
if (pNodeArray.Find(pNode) == -1)
{
// Node not in the list, add it
pNodeArray.Add(pNode);
}
}
}
/**
* Add a bind pose to the scene based on the FbxMesh and skinning settings of the given node
*/
void FFbxExporter::CreateBindPose(FbxNode* MeshRootNode)
{
if (!MeshRootNode)
{
return;
}
// In the bind pose, we must store all the link's global matrix at the time of the bind.
// Plus, we must store all the parent(s) global matrix of a link, even if they are not
// themselves deforming any model.
// Create a bind pose with the link list
FbxArray<FbxNode*> lClusteredFbxNodes;
int i, j;
if (MeshRootNode->GetNodeAttribute())
{
int lSkinCount=0;
int lClusterCount=0;
switch (MeshRootNode->GetNodeAttribute()->GetAttributeType())
{
case FbxNodeAttribute::eMesh:
case FbxNodeAttribute::eNurbs:
case FbxNodeAttribute::ePatch:
lSkinCount = ((FbxGeometry*)MeshRootNode->GetNodeAttribute())->GetDeformerCount(FbxDeformer::eSkin);
//Go through all the skins and count them
//then go through each skin and get their cluster count
for(i=0; i<lSkinCount; ++i)
{
FbxSkin *lSkin=(FbxSkin*)((FbxGeometry*)MeshRootNode->GetNodeAttribute())->GetDeformer(i, FbxDeformer::eSkin);
lClusterCount+=lSkin->GetClusterCount();
}
break;
}
//if we found some clusters we must add the node
if (lClusterCount)
{
//Again, go through all the skins get each cluster link and add them
for (i=0; i<lSkinCount; ++i)
{
FbxSkin *lSkin=(FbxSkin*)((FbxGeometry*)MeshRootNode->GetNodeAttribute())->GetDeformer(i, FbxDeformer::eSkin);
lClusterCount=lSkin->GetClusterCount();
for (j=0; j<lClusterCount; ++j)
{
FbxNode* lClusterNode = lSkin->GetCluster(j)->GetLink();
AddNodeRecursively(lClusteredFbxNodes, lClusterNode);
}
}
// Add the patch to the pose
lClusteredFbxNodes.Add(MeshRootNode);
}
}
// Now create a bind pose with the link list
if (lClusteredFbxNodes.GetCount())
{
// A pose must be named. Arbitrarily use the name of the patch node.
FbxPose* lPose = FbxPose::Create(Scene, MeshRootNode->GetName());
// default pose type is rest pose, so we need to set the type as bind pose
lPose->SetIsBindPose(true);
for (i=0; i<lClusteredFbxNodes.GetCount(); i++)
{
FbxNode* lKFbxNode = lClusteredFbxNodes.GetAt(i);
FbxMatrix lBindMatrix = lKFbxNode->EvaluateGlobalTransform();
lPose->Add(lKFbxNode, lBindMatrix);
}
// Add the pose to the scene
Scene->AddPose(lPose);
}
}
void FFbxExporter::ExportSkeletalMeshComponent(USkeletalMeshComponent* SkelMeshComp, const TCHAR* MeshName, FbxNode* ActorRootNode, INodeNameAdapter& NodeNameAdapter, bool bSaveAnimSeq)
{
if (SkelMeshComp && SkelMeshComp->GetSkeletalMeshAsset())
{
UAnimSequence* AnimSeq = (bSaveAnimSeq && SkelMeshComp->GetAnimationMode() == EAnimationMode::AnimationSingleNode) ?
Cast<UAnimSequence>(SkelMeshComp->AnimationData.AnimToPlay) : nullptr;
FbxNode* SkeletonRootNode = ExportSkeletalMeshToFbx(SkelMeshComp->GetSkeletalMeshAsset(), AnimSeq, MeshName, ActorRootNode, &ToRawPtrTArrayUnsafe(SkelMeshComp->OverrideMaterials));
if(SkeletonRootNode)
{
FbxSkeletonRoots.Add(SkelMeshComp, SkeletonRootNode);
NodeNameAdapter.AddFbxNode(SkelMeshComp, SkeletonRootNode);
}
}
}
void ExportObjectMetadataToBones(const UObject* ObjectToExport, const TArray<FbxNode*>& Nodes)
{
if (!ObjectToExport || Nodes.Num() == 0)
{
return;
}
// Retrieve the metadata map without creating it
const TMap<FName, FString>* MetadataMap = FMetaData::GetMapForObject(ObjectToExport);
if (MetadataMap)
{
// Map the nodes to their names for fast access
TMap<FString, FbxNode*> NameToNode;
for (FbxNode* Node : Nodes)
{
NameToNode.Add(FString(Node->GetName()), Node);
}
static const FString MetadataPrefix(FBX_METADATA_PREFIX);
for (const auto& MetadataIt : *MetadataMap)
{
// Export object metadata tags that are prefixed as FBX custom user-defined properties
// Remove the prefix since it's for Unreal use only (and '.' is considered an invalid character for user property names in DCC like Maya)
FString TagAsString = MetadataIt.Key.ToString();
if (TagAsString.RemoveFromStart(MetadataPrefix))
{
// Extract the node name from the metadata tag
FString NodeName;
int32 CharPos = INDEX_NONE;
if (TagAsString.FindChar(TEXT('.'), CharPos))
{
NodeName = TagAsString.Left(CharPos);
// The remaining part is the actual metadata tag
TagAsString.RightChopInline(CharPos + 1, EAllowShrinking::No); // exclude the period
}
// Try to attach the metadata to its associated node by name
FbxNode** Node = NameToNode.Find(NodeName);
if (Node)
{
if (MetadataIt.Value == TEXT("true") || MetadataIt.Value == TEXT("false"))
{
FbxProperty Property = FbxProperty::Create(*Node, FbxBoolDT, TCHAR_TO_UTF8(*TagAsString));
FbxBool ValueBool = MetadataIt.Value == TEXT("true") ? true : false;
Property.Set(ValueBool);
Property.ModifyFlag(FbxPropertyFlags::eUserDefined, true);
}
else
{
FbxProperty Property = FbxProperty::Create(*Node, FbxStringDT, TCHAR_TO_UTF8(*TagAsString));
FbxString ValueString(TCHAR_TO_UTF8(*MetadataIt.Value));
Property.Set(ValueString);
Property.ModifyFlag(FbxPropertyFlags::eUserDefined, true);
}
}
}
}
}
}
/**
* Add the given skeletal mesh to the Fbx scene in preparation for exporting. Makes all new nodes a child of the given node
*/
FbxNode* FFbxExporter::ExportSkeletalMeshToFbx(const USkeletalMesh* SkeletalMesh, const UAnimSequence* AnimSeq, const TCHAR* MeshName, FbxNode* ActorRootNode, const TArray<UMaterialInterface*>* OverrideMaterials /*= nullptr*/)
{
if(AnimSeq)
{
return ExportAnimSequence(AnimSeq, SkeletalMesh, GetExportOptions()->bExportPreviewMesh, MeshName, ActorRootNode, OverrideMaterials);
}
else
{
//Create a temporary node attach to the scene root.
//This will allow us to do the binding without the scene transform (non uniform scale is not supported when binding the skeleton)
//We then detach from the temp node and attach to the parent and remove the temp node
FString FbxNodeName = FGuid::NewGuid().ToString(EGuidFormats::Digits);
FbxNode* TmpNodeNoTransform = FbxNode::Create(Scene, TCHAR_TO_UTF8(*FbxNodeName));
Scene->GetRootNode()->AddChild(TmpNodeNoTransform);
TArray<FbxNode*> BoneNodes;
// Add the skeleton to the scene
FbxNode* SkeletonRootNode = CreateSkeleton(SkeletalMesh, BoneNodes);
if(SkeletonRootNode)
{
TmpNodeNoTransform->AddChild(SkeletonRootNode);
}
FbxNode* MeshRootNode = nullptr;
if (GetExportOptions()->LevelOfDetail && SkeletalMesh->GetLODNum() > 1)
{
FString LodGroup_MeshName = FString(MeshName) + TEXT("_LodGroup");
MeshRootNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*LodGroup_MeshName));
TmpNodeNoTransform->AddChild(MeshRootNode);
LodGroup_MeshName = FString(MeshName) + TEXT("_LodGroupAttribute");
FbxLODGroup *FbxLodGroupAttribute = FbxLODGroup::Create(Scene, TCHAR_TO_UTF8(*LodGroup_MeshName));
MeshRootNode->AddNodeAttribute(FbxLodGroupAttribute);
FbxLodGroupAttribute->ThresholdsUsedAsPercentage = true;
//Export an Fbx Mesh Node for every LOD and child them to the fbx node (LOD Group)
for (int CurrentLodIndex = 0; CurrentLodIndex < SkeletalMesh->GetLODNum(); ++CurrentLodIndex)
{
FString FbxLODNodeName = FString(MeshName) + TEXT("_LOD") + FString::FromInt(CurrentLodIndex);
if (CurrentLodIndex + 1 < SkeletalMesh->GetLODNum())
{
//Convert the screen size to a threshold, it is just to be sure that we set some threshold, there is no way to convert this precisely
double LodScreenSize = (double)(10.0f / SkeletalMesh->GetLODInfo(CurrentLodIndex)->ScreenSize.Default);
FbxLodGroupAttribute->AddThreshold(LodScreenSize);
}
const UAnimSequence* NullAnimSeq = nullptr;
FbxNode* FbxActorLOD = CreateMesh(SkeletalMesh, *FbxLODNodeName, CurrentLodIndex, NullAnimSeq, OverrideMaterials);
if (FbxActorLOD)
{
MeshRootNode->AddChild(FbxActorLOD);
if (SkeletonRootNode)
{
// Bind the mesh to the skeleton
BindMeshToSkeleton(SkeletalMesh, FbxActorLOD, BoneNodes, CurrentLodIndex);
// Add the bind pose
CreateBindPose(FbxActorLOD);
}
}
}
}
else
{
const int32 LODIndex = 0;
const UAnimSequence* NullAnimSeq = nullptr;
MeshRootNode = CreateMesh(SkeletalMesh, MeshName, LODIndex, NullAnimSeq, OverrideMaterials);
if (MeshRootNode)
{
TmpNodeNoTransform->AddChild(MeshRootNode);
if (SkeletonRootNode)
{
// Bind the mesh to the skeleton
BindMeshToSkeleton(SkeletalMesh, MeshRootNode, BoneNodes, 0);
// Add the bind pose
CreateBindPose(MeshRootNode);
}
}
}
if (SkeletonRootNode)
{
TmpNodeNoTransform->RemoveChild(SkeletonRootNode);
ActorRootNode->AddChild(SkeletonRootNode);
}
ExportObjectMetadataToBones(SkeletalMesh->GetSkeleton(), BoneNodes);
if (MeshRootNode)
{
TmpNodeNoTransform->RemoveChild(MeshRootNode);
ActorRootNode->AddChild(MeshRootNode);
ExportObjectMetadata(SkeletalMesh, MeshRootNode);
}
Scene->GetRootNode()->RemoveChild(TmpNodeNoTransform);
Scene->RemoveNode(TmpNodeNoTransform);
return SkeletonRootNode;
}
}
} // namespace UnFbx