4947 lines
186 KiB
C++
4947 lines
186 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
/*=============================================================================
|
|
Skeletal mesh creation from FBX data.
|
|
Largely based on SkeletalMeshImport.cpp
|
|
=============================================================================*/
|
|
|
|
#include "CoreMinimal.h"
|
|
#include "EngineDefines.h"
|
|
#include "Misc/MessageDialog.h"
|
|
#include "Containers/IndirectArray.h"
|
|
#include "Containers/UnrealString.h"
|
|
#include "Stats/Stats.h"
|
|
#include "Async/AsyncWork.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Misc/FeedbackContext.h"
|
|
#include "Misc/ScopedSlowTask.h"
|
|
#include "Modules/ModuleManager.h"
|
|
#include "UObject/ObjectMacros.h"
|
|
#include "UObject/Object.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "Animation/Skeleton.h"
|
|
#include "Materials/Material.h"
|
|
#include "Engine/SkeletalMesh.h"
|
|
#include "Engine/SkeletalMeshLODSettings.h"
|
|
#include "Engine/SkinnedAssetCommon.h"
|
|
#include "Components/SkinnedMeshComponent.h"
|
|
#include "Components/SkeletalMeshComponent.h"
|
|
#include "AnimEncoding.h"
|
|
#include "Factories/Factory.h"
|
|
#include "Factories/FbxSkeletalMeshImportData.h"
|
|
#include "Animation/AnimationSettings.h"
|
|
#include "Animation/MorphTarget.h"
|
|
#include "PhysicsAssetUtils.h"
|
|
#include "Rendering/SkeletalMeshModel.h"
|
|
#include "Rendering/SkeletalMeshRenderData.h"
|
|
#include "Rendering/SkeletalMeshLODRenderData.h"
|
|
#include "Misc/ScopedSlowTask.h"
|
|
|
|
#include "ImportUtils/SkeletalMeshImportUtils.h"
|
|
#include "ImportUtils/SkelImport.h"
|
|
#include "Logging/TokenizedMessage.h"
|
|
#include "FbxImporter.h"
|
|
|
|
#include "AssetRegistry/AssetData.h"
|
|
#include "AssetRegistry/ARFilter.h"
|
|
#include "AssetRegistry/AssetRegistryModule.h"
|
|
#include "AssetNotifications.h"
|
|
|
|
#include "ObjectTools.h"
|
|
|
|
#include "ApexClothingUtils.h"
|
|
#include "MeshUtilities.h"
|
|
#include "OverlappingCorners.h"
|
|
|
|
#include "IMessageLogListing.h"
|
|
#include "MessageLogModule.h"
|
|
#include "UObject/UObjectHash.h"
|
|
#include "UObject/UObjectIterator.h"
|
|
#include "ComponentReregisterContext.h"
|
|
|
|
#include "Misc/FbxErrors.h"
|
|
#include "PhysicsEngine/PhysicsAsset.h"
|
|
#include "Engine/SkeletalMeshSocket.h"
|
|
#include "ClothingAsset.h"
|
|
#include "LODUtilities.h"
|
|
#include "IMeshBuilderModule.h"
|
|
#include "Interfaces/ITargetPlatform.h"
|
|
#include "Interfaces/ITargetPlatformManagerModule.h"
|
|
#include "Misc/CoreMisc.h"
|
|
#include "Preferences/PersonaOptions.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "FBXImpoter"
|
|
|
|
using namespace UnFbx;
|
|
|
|
// Get the geometry deformation local to a node. It is never inherited by the
|
|
// children.
|
|
FbxAMatrix GetGeometry(FbxNode* pNode)
|
|
{
|
|
FbxVector4 lT, lR, lS;
|
|
FbxAMatrix lGeometry;
|
|
|
|
lT = pNode->GetGeometricTranslation(FbxNode::eSourcePivot);
|
|
lR = pNode->GetGeometricRotation(FbxNode::eSourcePivot);
|
|
lS = pNode->GetGeometricScaling(FbxNode::eSourcePivot);
|
|
|
|
lGeometry.SetT(lT);
|
|
lGeometry.SetR(lR);
|
|
lGeometry.SetS(lS);
|
|
|
|
return lGeometry;
|
|
}
|
|
|
|
// Scale all the elements of a matrix.
|
|
void MatrixScale(FbxAMatrix& pMatrix, double pValue)
|
|
{
|
|
int32 i,j;
|
|
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
for (j = 0; j < 4; j++)
|
|
{
|
|
pMatrix[i][j] *= pValue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add a value to all the elements in the diagonal of the matrix.
|
|
void MatrixAddToDiagonal(FbxAMatrix& pMatrix, double pValue)
|
|
{
|
|
pMatrix[0][0] += pValue;
|
|
pMatrix[1][1] += pValue;
|
|
pMatrix[2][2] += pValue;
|
|
pMatrix[3][3] += pValue;
|
|
}
|
|
|
|
|
|
// Sum two matrices element by element.
|
|
void MatrixAdd(FbxAMatrix& pDstMatrix, FbxAMatrix& pSrcMatrix)
|
|
{
|
|
int32 i,j;
|
|
|
|
for (i = 0; i < 4; i++)
|
|
{
|
|
for (j = 0; j < 4; j++)
|
|
{
|
|
pDstMatrix[i][j] += pSrcMatrix[i][j];
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Helper struct for the mesh component vert position octree */
|
|
struct FSkeletalMeshVertPosOctreeSemantics
|
|
{
|
|
enum { MaxElementsPerLeaf = 16 };
|
|
enum { MinInclusiveElementsPerNode = 7 };
|
|
enum { MaxNodeDepth = 12 };
|
|
|
|
typedef TInlineAllocator<MaxElementsPerLeaf> ElementAllocator;
|
|
|
|
/**
|
|
* Get the bounding box of the provided octree element. In this case, the box
|
|
* is merely the point specified by the element.
|
|
*
|
|
* @param Element Octree element to get the bounding box for
|
|
*
|
|
* @return Bounding box of the provided octree element
|
|
*/
|
|
FORCEINLINE static FBoxCenterAndExtent GetBoundingBox(const FSoftSkinVertex& Element)
|
|
{
|
|
return FBoxCenterAndExtent((FVector)Element.Position, FVector::ZeroVector);
|
|
}
|
|
|
|
/**
|
|
* Determine if two octree elements are equal
|
|
*
|
|
* @param A First octree element to check
|
|
* @param B Second octree element to check
|
|
*
|
|
* @return true if both octree elements are equal, false if they are not
|
|
*/
|
|
FORCEINLINE static bool AreElementsEqual(const FSoftSkinVertex& A, const FSoftSkinVertex& B)
|
|
{
|
|
return (A.Position == B.Position && A.UVs[0] == B.UVs[0]);
|
|
}
|
|
|
|
/** Ignored for this implementation */
|
|
FORCEINLINE static void SetElementId(const FSoftSkinVertex& Element, FOctreeElementId2 Id)
|
|
{
|
|
}
|
|
};
|
|
typedef TOctree2<FSoftSkinVertex, FSkeletalMeshVertPosOctreeSemantics> TSKCVertPosOctree;
|
|
|
|
void RemapSkeletalMeshVertexColorToImportData(const USkeletalMesh* SkeletalMesh, const int32 LODIndex, FSkeletalMeshImportData* SkelMeshImportData)
|
|
{
|
|
//Make sure we have all the source data we need to do the remap
|
|
if (!SkeletalMesh->GetImportedModel() || !SkeletalMesh->GetImportedModel()->LODModels.IsValidIndex(LODIndex) || !SkeletalMesh->GetHasVertexColors())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Find the extents formed by the cached vertex positions in order to optimize the octree used later
|
|
FBox Bounds(ForceInitToZero);
|
|
SkelMeshImportData->bHasVertexColors = true;
|
|
|
|
int32 WedgeNumber = SkelMeshImportData->Wedges.Num();
|
|
for (int32 WedgeIndex = 0; WedgeIndex < WedgeNumber; ++WedgeIndex)
|
|
{
|
|
SkeletalMeshImportData::FVertex& Wedge = SkelMeshImportData->Wedges[WedgeIndex];
|
|
const FVector& Position = (FVector)SkelMeshImportData->Points[Wedge.VertexIndex];
|
|
Bounds += Position;
|
|
}
|
|
|
|
TArray<FSoftSkinVertex> Vertices;
|
|
SkeletalMesh->GetImportedModel()->LODModels[LODIndex].GetVertices(Vertices);
|
|
for (int32 SkinVertexIndex = 0; SkinVertexIndex < Vertices.Num(); ++SkinVertexIndex)
|
|
{
|
|
const FSoftSkinVertex& SkinVertex = Vertices[SkinVertexIndex];
|
|
Bounds += (FVector)SkinVertex.Position;
|
|
}
|
|
|
|
TSKCVertPosOctree VertPosOctree(Bounds.GetCenter(), Bounds.GetExtent().GetMax());
|
|
|
|
// Add each old vertex to the octree
|
|
for (int32 SkinVertexIndex = 0; SkinVertexIndex < Vertices.Num(); ++SkinVertexIndex)
|
|
{
|
|
const FSoftSkinVertex& SkinVertex = Vertices[SkinVertexIndex];
|
|
VertPosOctree.AddElement(SkinVertex);
|
|
}
|
|
|
|
TMap<int32, FVector3f> WedgeIndexToNormal;
|
|
WedgeIndexToNormal.Reserve(WedgeNumber);
|
|
for (int32 FaceIndex = 0; FaceIndex < SkelMeshImportData->Faces.Num(); ++FaceIndex)
|
|
{
|
|
const SkeletalMeshImportData::FTriangle& Triangle = SkelMeshImportData->Faces[FaceIndex];
|
|
for (int32 Corner = 0; Corner < 3; ++Corner)
|
|
{
|
|
WedgeIndexToNormal.Add(Triangle.WedgeIndex[Corner], Triangle.TangentZ[Corner]);
|
|
}
|
|
}
|
|
|
|
// Iterate over each new vertex position, attempting to find the old vertex it is closest to, applying
|
|
// the color of the old vertex to the new position if possible.
|
|
for (int32 WedgeIndex = 0; WedgeIndex < WedgeNumber; ++WedgeIndex)
|
|
{
|
|
SkeletalMeshImportData::FVertex& Wedge = SkelMeshImportData->Wedges[WedgeIndex];
|
|
const FVector& Position = (FVector)SkelMeshImportData->Points[Wedge.VertexIndex];
|
|
const FVector2f UV = Wedge.UVs[0];
|
|
const FVector3f& Normal = WedgeIndexToNormal.FindChecked(WedgeIndex);
|
|
|
|
TArray<FSoftSkinVertex> PointsToConsider;
|
|
VertPosOctree.FindNearbyElements(Position, [&PointsToConsider](const FSoftSkinVertex& Vertex)
|
|
{
|
|
PointsToConsider.Add(Vertex);
|
|
});
|
|
|
|
if (PointsToConsider.Num() > 0)
|
|
{
|
|
//Get the closest position
|
|
float MaxNormalDot = -MAX_FLT;
|
|
float MinUVDistance = MAX_FLT;
|
|
int32 MatchIndex = INDEX_NONE;
|
|
for (int32 ConsiderationIndex = 0; ConsiderationIndex < PointsToConsider.Num(); ++ConsiderationIndex)
|
|
{
|
|
const FSoftSkinVertex& SkinVertex = PointsToConsider[ConsiderationIndex];
|
|
const FVector2f& SkinVertexUV = SkinVertex.UVs[0];
|
|
const float UVDistanceSqr = FVector2f::DistSquared(UV, SkinVertexUV);
|
|
if (UVDistanceSqr < MinUVDistance)
|
|
{
|
|
MinUVDistance = FMath::Min(MinUVDistance, UVDistanceSqr);
|
|
MatchIndex = ConsiderationIndex;
|
|
MaxNormalDot = Normal | SkinVertex.TangentZ;
|
|
}
|
|
else if (FMath::IsNearlyEqual(UVDistanceSqr, MinUVDistance, KINDA_SMALL_NUMBER))
|
|
{
|
|
//This case is useful when we have hard edge that shared vertice, somtime not all the shared wedge have the same paint color
|
|
//Think about a cube where each face have different vertex color.
|
|
float NormalDot = Normal | SkinVertex.TangentZ;
|
|
if (NormalDot > MaxNormalDot)
|
|
{
|
|
MaxNormalDot = NormalDot;
|
|
MatchIndex = ConsiderationIndex;
|
|
}
|
|
}
|
|
}
|
|
if (PointsToConsider.IsValidIndex(MatchIndex))
|
|
{
|
|
Wedge.Color = PointsToConsider[MatchIndex].Color;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Utility function for creating a unique material import name based on the node's initial name and its material/section index
|
|
FString MakeNodeMaterialIndexName(FbxNode* Node, int32 MaterialIndex)
|
|
{
|
|
FString NodeName = FFbxImporter::MakeName(Node->GetInitialName());
|
|
NodeName.Append("_");
|
|
NodeName.AppendInt(MaterialIndex);
|
|
|
|
return NodeName;
|
|
}
|
|
|
|
void FFbxImporter::SkinControlPointsToPose(FSkeletalMeshImportData& ImportData, FbxMesh* FbxMesh, FbxShape* FbxShape, bool bUseT0 )
|
|
{
|
|
FbxTime poseTime = FBXSDK_TIME_INFINITE;
|
|
if(bUseT0)
|
|
{
|
|
poseTime = 0;
|
|
}
|
|
|
|
int32 VertexCount = FbxMesh->GetControlPointsCount();
|
|
|
|
// Create a copy of the vertex array to receive vertex deformations.
|
|
FbxVector4* VertexArray = new FbxVector4[VertexCount];
|
|
|
|
// If a shape is provided, then it is the morphed version of the mesh.
|
|
// So we want to deform that instead of the base mesh vertices
|
|
if (FbxShape)
|
|
{
|
|
check(FbxShape->GetControlPointsCount() == VertexCount);
|
|
FMemory::Memcpy(VertexArray, FbxShape->GetControlPoints(), VertexCount * sizeof(FbxVector4));
|
|
}
|
|
else
|
|
{
|
|
FMemory::Memcpy(VertexArray, FbxMesh->GetControlPoints(), VertexCount * sizeof(FbxVector4));
|
|
}
|
|
|
|
|
|
int32 ClusterCount = 0;
|
|
int32 SkinCount = FbxMesh->GetDeformerCount(FbxDeformer::eSkin);
|
|
for( int32 i=0; i< SkinCount; i++)
|
|
{
|
|
ClusterCount += ((FbxSkin *)(FbxMesh->GetDeformer(i, FbxDeformer::eSkin)))->GetClusterCount();
|
|
}
|
|
|
|
// Deform the vertex array with the links contained in the mesh.
|
|
if (ClusterCount)
|
|
{
|
|
FbxAMatrix MeshMatrix = ComputeTotalMatrix(FbxMesh->GetNode());
|
|
// All the links must have the same link mode.
|
|
FbxCluster::ELinkMode lClusterMode = ((FbxSkin*)FbxMesh->GetDeformer(0, FbxDeformer::eSkin))->GetCluster(0)->GetLinkMode();
|
|
|
|
int32 i, j;
|
|
int32 lClusterCount=0;
|
|
|
|
int32 lSkinCount = FbxMesh->GetDeformerCount(FbxDeformer::eSkin);
|
|
|
|
TArray<FbxAMatrix> ClusterDeformations;
|
|
ClusterDeformations.AddZeroed( VertexCount );
|
|
|
|
TArray<double> ClusterWeights;
|
|
ClusterWeights.AddZeroed( VertexCount );
|
|
|
|
|
|
if (lClusterMode == FbxCluster::eAdditive)
|
|
{
|
|
for (i = 0; i < VertexCount; i++)
|
|
{
|
|
ClusterDeformations[i].SetIdentity();
|
|
}
|
|
}
|
|
|
|
for ( i=0; i<lSkinCount; ++i)
|
|
{
|
|
lClusterCount =( (FbxSkin *)FbxMesh->GetDeformer(i, FbxDeformer::eSkin))->GetClusterCount();
|
|
for (j=0; j<lClusterCount; ++j)
|
|
{
|
|
FbxCluster* Cluster =((FbxSkin *) FbxMesh->GetDeformer(i, FbxDeformer::eSkin))->GetCluster(j);
|
|
if (!Cluster->GetLink())
|
|
continue;
|
|
|
|
FbxNode* Link = Cluster->GetLink();
|
|
|
|
FbxAMatrix lReferenceGlobalInitPosition;
|
|
FbxAMatrix lReferenceGlobalCurrentPosition;
|
|
FbxAMatrix lClusterGlobalInitPosition;
|
|
FbxAMatrix lClusterGlobalCurrentPosition;
|
|
FbxAMatrix lReferenceGeometry;
|
|
FbxAMatrix lClusterGeometry;
|
|
|
|
FbxAMatrix lClusterRelativeInitPosition;
|
|
FbxAMatrix lClusterRelativeCurrentPositionInverse;
|
|
FbxAMatrix lVertexTransformMatrix;
|
|
|
|
if (lClusterMode == FbxCluster::eAdditive && Cluster->GetAssociateModel())
|
|
{
|
|
Cluster->GetTransformAssociateModelMatrix(lReferenceGlobalInitPosition);
|
|
lReferenceGlobalCurrentPosition = Scene->GetAnimationEvaluator()->GetNodeGlobalTransform(Cluster->GetAssociateModel(), poseTime);
|
|
// Geometric transform of the model
|
|
lReferenceGeometry = GetGeometry(Cluster->GetAssociateModel());
|
|
lReferenceGlobalCurrentPosition *= lReferenceGeometry;
|
|
}
|
|
else
|
|
{
|
|
Cluster->GetTransformMatrix(lReferenceGlobalInitPosition);
|
|
lReferenceGlobalCurrentPosition = MeshMatrix; //pGlobalPosition;
|
|
// Multiply lReferenceGlobalInitPosition by Geometric Transformation
|
|
lReferenceGeometry = GetGeometry(FbxMesh->GetNode());
|
|
lReferenceGlobalInitPosition *= lReferenceGeometry;
|
|
}
|
|
// Get the link initial global position and the link current global position.
|
|
Cluster->GetTransformLinkMatrix(lClusterGlobalInitPosition);
|
|
lClusterGlobalCurrentPosition = Link->GetScene()->GetAnimationEvaluator()->GetNodeGlobalTransform(Link, poseTime);
|
|
|
|
// Compute the initial position of the link relative to the reference.
|
|
lClusterRelativeInitPosition = lClusterGlobalInitPosition.Inverse() * lReferenceGlobalInitPosition;
|
|
|
|
// Compute the current position of the link relative to the reference.
|
|
lClusterRelativeCurrentPositionInverse = lReferenceGlobalCurrentPosition.Inverse() * lClusterGlobalCurrentPosition;
|
|
|
|
// Compute the shift of the link relative to the reference.
|
|
lVertexTransformMatrix = lClusterRelativeCurrentPositionInverse * lClusterRelativeInitPosition;
|
|
|
|
int32 k;
|
|
int32 lVertexIndexCount = Cluster->GetControlPointIndicesCount();
|
|
|
|
for (k = 0; k < lVertexIndexCount; ++k)
|
|
{
|
|
int32 lIndex = Cluster->GetControlPointIndices()[k];
|
|
|
|
// Sometimes, the mesh can have less points than at the time of the skinning
|
|
// because a smooth operator was active when skinning but has been deactivated during export.
|
|
if (lIndex >= VertexCount)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
double lWeight = Cluster->GetControlPointWeights()[k];
|
|
|
|
if (lWeight == 0.0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Compute the influence of the link on the vertex.
|
|
FbxAMatrix lInfluence = lVertexTransformMatrix;
|
|
MatrixScale(lInfluence, lWeight);
|
|
|
|
if (lClusterMode == FbxCluster::eAdditive)
|
|
{
|
|
// Multiply with to the product of the deformations on the vertex.
|
|
MatrixAddToDiagonal(lInfluence, 1.0 - lWeight);
|
|
ClusterDeformations[lIndex] = lInfluence * ClusterDeformations[lIndex];
|
|
|
|
// Set the link to 1.0 just to know this vertex is influenced by a link.
|
|
ClusterWeights[lIndex] = 1.0;
|
|
}
|
|
else // lLinkMode == KFbxLink::eNORMALIZE || lLinkMode == KFbxLink::eTOTAL1
|
|
{
|
|
// Add to the sum of the deformations on the vertex.
|
|
MatrixAdd(ClusterDeformations[lIndex], lInfluence);
|
|
|
|
// Add to the sum of weights to either normalize or complete the vertex.
|
|
ClusterWeights[lIndex] += lWeight;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < VertexCount; i++)
|
|
{
|
|
FbxVector4 lSrcVertex = VertexArray[i];
|
|
FbxVector4& lDstVertex = VertexArray[i];
|
|
double lWeight = ClusterWeights[i];
|
|
|
|
// Deform the vertex if there was at least a link with an influence on the vertex,
|
|
if (lWeight != 0.0)
|
|
{
|
|
lDstVertex = ClusterDeformations[i].MultT(lSrcVertex);
|
|
|
|
if (lClusterMode == FbxCluster::eNormalize)
|
|
{
|
|
// In the normalized link mode, a vertex is always totally influenced by the links.
|
|
lDstVertex /= lWeight;
|
|
}
|
|
else if (lClusterMode == FbxCluster::eTotalOne)
|
|
{
|
|
// In the total 1 link mode, a vertex can be partially influenced by the links.
|
|
lSrcVertex *= (1.0 - lWeight);
|
|
lDstVertex += lSrcVertex;
|
|
}
|
|
}
|
|
}
|
|
|
|
// change the vertex position
|
|
int32 ExistPointNum = ImportData.Points.Num();
|
|
int32 StartPointIndex = ExistPointNum - VertexCount;
|
|
for(int32 ControlPointsIndex = 0 ; ControlPointsIndex < VertexCount ;ControlPointsIndex++ )
|
|
{
|
|
ImportData.Points[ControlPointsIndex+StartPointIndex] = (FVector3f)Converter.ConvertPos(MeshMatrix.MultT(VertexArray[ControlPointsIndex]));
|
|
}
|
|
|
|
}
|
|
|
|
delete[] VertexArray;
|
|
}
|
|
|
|
// 3 "ProcessImportMesh..." functions outputing Unreal data from a filled FSkeletalMeshBinaryImport
|
|
// and a handfull of other minor stuff needed by these
|
|
// Fully taken from SkeletalMeshImport.cpp
|
|
|
|
|
|
struct tFaceRecord
|
|
{
|
|
int32 FaceIndex;
|
|
int32 HoekIndex;
|
|
int32 WedgeIndex;
|
|
uint32 SmoothFlags;
|
|
uint32 FanFlags;
|
|
};
|
|
|
|
struct VertsFans
|
|
{
|
|
TArray<tFaceRecord> FaceRecord;
|
|
int32 FanGroupCount;
|
|
};
|
|
|
|
struct tInfluences
|
|
{
|
|
TArray<int32> RawInfIndices;
|
|
};
|
|
|
|
struct tWedgeList
|
|
{
|
|
TArray<int32> WedgeList;
|
|
};
|
|
|
|
struct tFaceSet
|
|
{
|
|
TArray<int32> Faces;
|
|
};
|
|
|
|
// Check whether faces have at least two vertices in common. These must be POINTS - don't care about wedges.
|
|
bool UnFbx::FFbxImporter::FacesAreSmoothlyConnected( FSkeletalMeshImportData &ImportData, int32 Face1, int32 Face2 )
|
|
{
|
|
|
|
//if( ( Face1 >= Thing->SkinData.Faces.Num()) || ( Face2 >= Thing->SkinData.Faces.Num()) ) return false;
|
|
|
|
if( Face1 == Face2 )
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Smoothing groups match at least one bit in binary AND ?
|
|
if( ( ImportData.Faces[Face1].SmoothingGroups & ImportData.Faces[Face2].SmoothingGroups ) == 0 )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int32 VertMatches = 0;
|
|
for( int32 i=0; i<3; i++)
|
|
{
|
|
int32 Point1 = ImportData.Wedges[ ImportData.Faces[Face1].WedgeIndex[i] ].VertexIndex;
|
|
|
|
for( int32 j=0; j<3; j++)
|
|
{
|
|
int32 Point2 = ImportData.Wedges[ ImportData.Faces[Face2].WedgeIndex[j] ].VertexIndex;
|
|
if( Point2 == Point1 )
|
|
{
|
|
VertMatches ++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return ( VertMatches >= 2 );
|
|
}
|
|
|
|
int32 UnFbx::FFbxImporter::DoUnSmoothVerts(FSkeletalMeshImportData &ImportData, bool bDuplicateUnSmoothWedges)
|
|
{
|
|
//
|
|
// Connectivity: triangles with non-matching smoothing groups will be physically split.
|
|
//
|
|
// -> Splitting involves: the UV+material-contaning vertex AND the 3d point.
|
|
//
|
|
// -> Tally smoothing groups for each and every (textured) vertex.
|
|
//
|
|
// -> Collapse:
|
|
// -> start from a vertex and all its adjacent triangles - go over
|
|
// each triangle - if any connecting one (sharing more than one vertex) gives a smoothing match,
|
|
// accumulate it. Then IF more than one resulting section,
|
|
// ensure each boundary 'vert' is split _if not already_ to give each smoothing group
|
|
// independence from all others.
|
|
//
|
|
|
|
int32 DuplicatedVertCount = 0;
|
|
int32 RemappedHoeks = 0;
|
|
|
|
int32 TotalSmoothMatches = 0;
|
|
int32 TotalConnexChex = 0;
|
|
|
|
// Link _all_ faces to vertices.
|
|
TArray<VertsFans> Fans;
|
|
TArray<tInfluences> PointInfluences;
|
|
TArray<tWedgeList> PointWedges;
|
|
|
|
Fans.AddZeroed( ImportData.Points.Num() );//Fans.AddExactZeroed( Thing->SkinData.Points.Num() );
|
|
PointInfluences.AddZeroed( ImportData.Points.Num() );//PointInfluences.AddExactZeroed( Thing->SkinData.Points.Num() );
|
|
PointWedges.AddZeroed( ImportData.Points.Num() );//PointWedges.AddExactZeroed( Thing->SkinData.Points.Num() );
|
|
|
|
// Existing points map 1:1
|
|
ImportData.PointToRawMap.AddUninitialized( ImportData.Points.Num() );
|
|
for(int32 i=0; i<ImportData.Points.Num(); i++)
|
|
{
|
|
ImportData.PointToRawMap[i] = i;
|
|
}
|
|
|
|
for(int32 i=0; i< ImportData.Influences.Num(); i++)
|
|
{
|
|
if (PointInfluences.Num() <= ImportData.Influences[i].VertexIndex)
|
|
{
|
|
PointInfluences.AddZeroed(ImportData.Influences[i].VertexIndex - PointInfluences.Num() + 1);
|
|
}
|
|
PointInfluences[ImportData.Influences[i].VertexIndex ].RawInfIndices.Add( i );
|
|
}
|
|
|
|
for(int32 i=0; i< ImportData.Wedges.Num(); i++)
|
|
{
|
|
if (uint32(PointWedges.Num()) <= ImportData.Wedges[i].VertexIndex)
|
|
{
|
|
PointWedges.AddZeroed(ImportData.Wedges[i].VertexIndex - PointWedges.Num() + 1);
|
|
}
|
|
|
|
PointWedges[ImportData.Wedges[i].VertexIndex ].WedgeList.Add( i );
|
|
}
|
|
|
|
for(int32 f=0; f< ImportData.Faces.Num(); f++ )
|
|
{
|
|
// For each face, add a pointer to that face into the Fans[vertex].
|
|
for( int32 i=0; i<3; i++)
|
|
{
|
|
int32 WedgeIndex = ImportData.Faces[f].WedgeIndex[i];
|
|
int32 PointIndex = ImportData.Wedges[ WedgeIndex ].VertexIndex;
|
|
tFaceRecord NewFR;
|
|
|
|
NewFR.FaceIndex = f;
|
|
NewFR.HoekIndex = i;
|
|
NewFR.WedgeIndex = WedgeIndex; // This face touches the point courtesy of Wedges[Wedgeindex].
|
|
NewFR.SmoothFlags = ImportData.Faces[f].SmoothingGroups;
|
|
NewFR.FanFlags = 0;
|
|
Fans[ PointIndex ].FaceRecord.Add( NewFR );
|
|
Fans[ PointIndex ].FanGroupCount = 0;
|
|
}
|
|
}
|
|
|
|
// Investigate connectivity and assign common group numbers (1..+) to the fans' individual FanFlags.
|
|
for( int32 p=0; p< Fans.Num(); p++) // The fan of faces for each 3d point 'p'.
|
|
{
|
|
// All faces connecting.
|
|
if( Fans[p].FaceRecord.Num() > 0 )
|
|
{
|
|
int32 FacesProcessed = 0;
|
|
TArray<tFaceSet> FaceSets; // Sets with indices INTO FANS, not into face array.
|
|
|
|
// Digest all faces connected to this vertex (p) into one or more smooth sets. only need to check
|
|
// all faces MINUS one..
|
|
while( FacesProcessed < Fans[p].FaceRecord.Num() )
|
|
{
|
|
// One loop per group. For the current ThisFaceIndex, tally all truly connected ones
|
|
// and put them in a new TArray. Once no more can be connected, stop.
|
|
|
|
int32 NewSetIndex = FaceSets.Num(); // 0 to start
|
|
FaceSets.AddZeroed(1); // first one will be just ThisFaceIndex.
|
|
|
|
// Find the first non-processed face. There will be at least one.
|
|
int32 ThisFaceFanIndex = 0;
|
|
{
|
|
int32 SearchIndex = 0;
|
|
while( Fans[p].FaceRecord[SearchIndex].FanFlags == -1 ) // -1 indicates already processed.
|
|
{
|
|
SearchIndex++;
|
|
}
|
|
ThisFaceFanIndex = SearchIndex; //Fans[p].FaceRecord[SearchIndex].FaceIndex;
|
|
}
|
|
|
|
// Initial face.
|
|
FaceSets[ NewSetIndex ].Faces.Add( ThisFaceFanIndex ); // Add the unprocessed Face index to the "local smoothing group" [NewSetIndex].
|
|
Fans[p].FaceRecord[ThisFaceFanIndex].FanFlags = -1; // Mark as processed.
|
|
FacesProcessed++;
|
|
|
|
// Find all faces connected to this face, and if there's any
|
|
// smoothing group matches, put it in current face set and mark it as processed;
|
|
// until no more match.
|
|
int32 NewMatches = 0;
|
|
do
|
|
{
|
|
NewMatches = 0;
|
|
// Go over all current faces in this faceset and set if the FaceRecord (local smoothing groups) has any matches.
|
|
// there will be at least one face already in this faceset - the first face in the fan.
|
|
for( int32 n=0; n< FaceSets[NewSetIndex].Faces.Num(); n++)
|
|
{
|
|
int32 HookFaceIdx = Fans[p].FaceRecord[ FaceSets[NewSetIndex].Faces[n] ].FaceIndex;
|
|
|
|
//Go over the fan looking for matches.
|
|
for( int32 s=0; s< Fans[p].FaceRecord.Num(); s++)
|
|
{
|
|
// Skip if same face, skip if face already processed.
|
|
if( ( HookFaceIdx != Fans[p].FaceRecord[s].FaceIndex ) && ( Fans[p].FaceRecord[s].FanFlags != -1 ))
|
|
{
|
|
TotalConnexChex++;
|
|
// Process if connected with more than one vertex, AND smooth..
|
|
if( FacesAreSmoothlyConnected( ImportData, HookFaceIdx, Fans[p].FaceRecord[s].FaceIndex ) )
|
|
{
|
|
TotalSmoothMatches++;
|
|
Fans[p].FaceRecord[s].FanFlags = -1; // Mark as processed.
|
|
FacesProcessed++;
|
|
// Add
|
|
FaceSets[NewSetIndex].Faces.Add( s ); // Store FAN index of this face index into smoothing group's faces.
|
|
// Tally
|
|
NewMatches++;
|
|
}
|
|
} // not the same...
|
|
}// all faces in fan
|
|
} // all faces in FaceSet
|
|
}while( NewMatches );
|
|
|
|
}// Repeat until all faces processed.
|
|
|
|
// For the new non-initialized face sets,
|
|
// Create a new point, influences, and uv-vertex(-ices) for all individual FanFlag groups with an index of 2+ and also remap
|
|
// the face's vertex into those new ones.
|
|
if( FaceSets.Num() > 1 )
|
|
{
|
|
for( int32 f=1; f<FaceSets.Num(); f++ )
|
|
{
|
|
check(ImportData.Points.Num() == ImportData.PointToRawMap.Num());
|
|
|
|
// We duplicate the current vertex. (3d point)
|
|
int32 NewPointIndex = ImportData.Points.Num();
|
|
ImportData.Points.AddUninitialized();
|
|
ImportData.Points[NewPointIndex] = ImportData.Points[p] ;
|
|
|
|
ImportData.PointToRawMap.AddUninitialized();
|
|
ImportData.PointToRawMap[NewPointIndex] = p;
|
|
|
|
DuplicatedVertCount++;
|
|
|
|
// Duplicate all related weights.
|
|
for( int32 t=0; t< PointInfluences[p].RawInfIndices.Num(); t++ )
|
|
{
|
|
// Add new weight
|
|
int32 NewWeightIndex = ImportData.Influences.Num();
|
|
ImportData.Influences.AddUninitialized();
|
|
ImportData.Influences[NewWeightIndex] = ImportData.Influences[ PointInfluences[p].RawInfIndices[t] ];
|
|
ImportData.Influences[NewWeightIndex].VertexIndex = NewPointIndex;
|
|
}
|
|
|
|
// Duplicate any and all Wedges associated with it; and all Faces' wedges involved.
|
|
for( int32 w=0; w< PointWedges[p].WedgeList.Num(); w++)
|
|
{
|
|
int32 OldWedgeIndex = PointWedges[p].WedgeList[w];
|
|
int32 NewWedgeIndex = ImportData.Wedges.Num();
|
|
|
|
if( bDuplicateUnSmoothWedges )
|
|
{
|
|
ImportData.Wedges.AddUninitialized();
|
|
ImportData.Wedges[NewWedgeIndex] = ImportData.Wedges[ OldWedgeIndex ];
|
|
ImportData.Wedges[ NewWedgeIndex ].VertexIndex = NewPointIndex;
|
|
|
|
// Update relevant face's Wedges. Inelegant: just check all associated faces for every new wedge.
|
|
for( int32 s=0; s< FaceSets[f].Faces.Num(); s++)
|
|
{
|
|
int32 FanIndex = FaceSets[f].Faces[s];
|
|
if( Fans[p].FaceRecord[ FanIndex ].WedgeIndex == OldWedgeIndex )
|
|
{
|
|
// Update just the right one for this face (HoekIndex!)
|
|
ImportData.Faces[ Fans[p].FaceRecord[ FanIndex].FaceIndex ].WedgeIndex[ Fans[p].FaceRecord[ FanIndex ].HoekIndex ] = NewWedgeIndex;
|
|
RemappedHoeks++;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ImportData.Wedges[OldWedgeIndex].VertexIndex = NewPointIndex;
|
|
}
|
|
}
|
|
}
|
|
} // if FaceSets.Num(). -> duplicate stuff
|
|
}// while( FacesProcessed < Fans[p].FaceRecord.Num() )
|
|
} // Fans for each 3d point
|
|
|
|
check(ImportData.Points.Num() == ImportData.PointToRawMap.Num());
|
|
|
|
return DuplicatedVertCount;
|
|
}
|
|
|
|
bool UnFbx::FFbxImporter::IsUnrealBone(FbxNode* Link)
|
|
{
|
|
FbxNodeAttribute* Attr = Link->GetNodeAttribute();
|
|
if (Attr)
|
|
{
|
|
FbxNodeAttribute::EType AttrType = Attr->GetAttributeType();
|
|
if ( AttrType == FbxNodeAttribute::eSkeleton ||
|
|
AttrType == FbxNodeAttribute::eMesh ||
|
|
AttrType == FbxNodeAttribute::eNull )
|
|
{
|
|
return !IsUnrealTransformAttribute(Link);
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool UnFbx::FFbxImporter::IsUnrealTransformAttribute(FbxNode* Link)
|
|
{
|
|
FbxNodeAttribute* Attr = Link->GetNodeAttribute();
|
|
if (Attr)
|
|
{
|
|
FbxNodeAttribute::EType AttrType = Attr->GetAttributeType();
|
|
if (AttrType == FbxNodeAttribute::eNull)
|
|
{
|
|
FString AttributeName = FSkeletalMeshImportData::FixupBoneName(MakeName(Link->GetName()));
|
|
|
|
for (const FString& TransformAttributeName : UAnimationSettings::Get()->TransformAttributeNames)
|
|
{
|
|
if (AttributeName.MatchesWildcard(TransformAttributeName, ESearchCase::IgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::RecursiveBuildSkeleton(FbxNode* Link, TArray<FbxNode*>& OutSortedLinks)
|
|
{
|
|
if (IsUnrealBone(Link))
|
|
{
|
|
OutSortedLinks.Add(Link);
|
|
int32 ChildIndex;
|
|
for (ChildIndex=0; ChildIndex<Link->GetChildCount(); ChildIndex++)
|
|
{
|
|
RecursiveBuildSkeleton(Link->GetChild(ChildIndex),OutSortedLinks);
|
|
}
|
|
}
|
|
else if (IsUnrealTransformAttribute(Link))
|
|
{
|
|
OutSortedLinks.Add(Link);
|
|
}
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::BuildSkeletonSystem(TArray<FbxCluster*>& ClusterArray, TArray<FbxNode*>& OutSortedLinks)
|
|
{
|
|
FbxNode* Link;
|
|
TArray<FbxNode*> RootLinks;
|
|
int32 ClusterIndex;
|
|
for (ClusterIndex = 0; ClusterIndex < ClusterArray.Num(); ClusterIndex++)
|
|
{
|
|
Link = ClusterArray[ClusterIndex]->GetLink();
|
|
if (Link)
|
|
{
|
|
Link = GetRootSkeleton(Link);
|
|
int32 LinkIndex;
|
|
for (LinkIndex = 0; LinkIndex < RootLinks.Num(); LinkIndex++)
|
|
{
|
|
if (Link == RootLinks[LinkIndex])
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// this link is a new root, add it
|
|
if (LinkIndex == RootLinks.Num())
|
|
{
|
|
RootLinks.Add(Link);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32 LinkIndex=0; LinkIndex<RootLinks.Num(); LinkIndex++)
|
|
{
|
|
RecursiveBuildSkeleton(RootLinks[LinkIndex], OutSortedLinks);
|
|
}
|
|
}
|
|
|
|
bool UnFbx::FFbxImporter::RetrievePoseFromBindPose(const TArray<FbxNode*>& NodeArray, FbxArray<FbxPose*> & PoseArray) const
|
|
{
|
|
const int32 PoseCount = Scene->GetPoseCount();
|
|
for(int32 PoseIndex = 0; PoseIndex < PoseCount; PoseIndex++)
|
|
{
|
|
FbxPose* CurrentPose = Scene->GetPose(PoseIndex);
|
|
|
|
// current pose is bind pose,
|
|
if(CurrentPose && CurrentPose->IsBindPose())
|
|
{
|
|
// IsValidBindPose doesn't work reliably
|
|
// It checks all the parent chain(regardless root given), and if the parent doesn't have correct bind pose, it fails
|
|
// It causes more false positive issues than the real issue we have to worry about
|
|
// If you'd like to try this, set CHECK_VALID_BIND_POSE to 1, and try the error message
|
|
// when Autodesk fixes this bug, then we might be able to re-open this
|
|
FString PoseName = CurrentPose->GetName();
|
|
// all error report status
|
|
FbxStatus Status;
|
|
|
|
// it does not make any difference of checking with different node
|
|
// it is possible pose 0 -> node array 2, but isValidBindPose function returns true even with node array 0
|
|
for(auto Current : NodeArray)
|
|
{
|
|
FString CurrentName = Current->GetName();
|
|
NodeList pMissingAncestors, pMissingDeformers, pMissingDeformersAncestors, pWrongMatrices;
|
|
|
|
if(CurrentPose->IsValidBindPoseVerbose(Current, pMissingAncestors, pMissingDeformers, pMissingDeformersAncestors, pWrongMatrices, 0.0001, &Status))
|
|
{
|
|
PoseArray.Add(CurrentPose);
|
|
UE_LOG(LogFbx, Log, TEXT("Valid bind pose for Pose (%s) - %s"), *PoseName, *FString(Current->GetName()));
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// first try to fix up
|
|
// add missing ancestors
|
|
for(int i = 0; i < pMissingAncestors.GetCount(); i++)
|
|
{
|
|
FbxAMatrix mat = pMissingAncestors.GetAt(i)->EvaluateGlobalTransform(FBXSDK_TIME_ZERO);
|
|
CurrentPose->Add(pMissingAncestors.GetAt(i), mat);
|
|
}
|
|
|
|
pMissingAncestors.Clear();
|
|
pMissingDeformers.Clear();
|
|
pMissingDeformersAncestors.Clear();
|
|
pWrongMatrices.Clear();
|
|
|
|
// check it again
|
|
if(CurrentPose->IsValidBindPose(Current))
|
|
{
|
|
PoseArray.Add(CurrentPose);
|
|
UE_LOG(LogFbx, Log, TEXT("Valid bind pose for Pose (%s) - %s"), *PoseName, *FString(Current->GetName()));
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// first try to find parent who is null group and see if you can try test it again
|
|
FbxNode * ParentNode = Current->GetParent();
|
|
while(ParentNode)
|
|
{
|
|
FbxNodeAttribute* Attr = ParentNode->GetNodeAttribute();
|
|
if(Attr && Attr->GetAttributeType() == FbxNodeAttribute::eNull)
|
|
{
|
|
// found it
|
|
break;
|
|
}
|
|
|
|
// find next parent
|
|
ParentNode = ParentNode->GetParent();
|
|
}
|
|
|
|
if(ParentNode && CurrentPose->IsValidBindPose(ParentNode))
|
|
{
|
|
PoseArray.Add(CurrentPose);
|
|
UE_LOG(LogFbx, Log, TEXT("Valid bind pose for Pose (%s) - %s"), *PoseName, *FString(Current->GetName()));
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
FString ErrorString = Status.GetErrorString();
|
|
if (!GIsAutomationTesting)
|
|
UE_LOG(LogFbx, Warning, TEXT("Not valid bind pose for Pose (%s) - Node %s : %s"), *PoseName, *FString(Current->GetName()), *ErrorString);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return (PoseArray.Size() > 0);
|
|
}
|
|
|
|
bool UnFbx::FFbxImporter::ImportBones(TArray<FbxNode*>& NodeArray, FSkeletalMeshImportData &ImportData, UFbxSkeletalMeshImportData* TemplateData ,TArray<FbxNode*> &SortedLinks, bool& bOutDiffPose, bool bDisableMissingBindPoseWarning, bool & bUseTime0AsRefPose, FbxNode *SkeletalMeshNode, bool bIsReimport)
|
|
{
|
|
bOutDiffPose = false;
|
|
bool bIsARigidMesh = false;
|
|
FbxNode* Link = NULL;
|
|
FbxArray<FbxPose*> PoseArray;
|
|
TArray<FbxCluster*> ClusterArray;
|
|
|
|
bool AllowImportBoneErrorAndWarning = !(bIsReimport && ImportOptions->bImportAsSkeletalGeometry);
|
|
if (NodeArray[0]->GetMesh()->GetDeformerCount(FbxDeformer::eSkin) == 0)
|
|
{
|
|
bIsARigidMesh = true;
|
|
Link = NodeArray[0];
|
|
RecursiveBuildSkeleton(GetRootSkeleton(Link),SortedLinks);
|
|
}
|
|
else
|
|
{
|
|
// get bindpose and clusters from FBX skeleton
|
|
|
|
// let's put the elements to their bind pose! (and we restore them after
|
|
// we have built the ClusterInformation.
|
|
int32 Default_NbPoses = SdkManager->GetBindPoseCount(Scene);
|
|
// If there are no BindPoses, the following will generate them.
|
|
//SdkManager->CreateMissingBindPoses(Scene);
|
|
|
|
//if we created missing bind poses, update the number of bind poses
|
|
int32 NbPoses = SdkManager->GetBindPoseCount(Scene);
|
|
|
|
if ( NbPoses != Default_NbPoses && AllowImportBoneErrorAndWarning)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("FbxSkeletaLMeshimport_SceneMissingBinding", "The imported scene has no initial binding position (Bind Pose) for the skin. The plug-in will compute one automatically. However, it may create unexpected results.")), FFbxErrors::SkeletalMesh_NoBindPoseInScene);
|
|
}
|
|
|
|
//
|
|
// create the bones / skinning
|
|
//
|
|
|
|
for ( int32 i = 0; i < NodeArray.Num(); i++)
|
|
{
|
|
FbxMesh* FbxMesh = NodeArray[i]->GetMesh();
|
|
const int32 SkinDeformerCount = FbxMesh->GetDeformerCount(FbxDeformer::eSkin);
|
|
for (int32 DeformerIndex=0; DeformerIndex < SkinDeformerCount; DeformerIndex++)
|
|
{
|
|
FbxSkin* Skin = (FbxSkin*)FbxMesh->GetDeformer(DeformerIndex, FbxDeformer::eSkin);
|
|
for ( int32 ClusterIndex = 0; ClusterIndex < Skin->GetClusterCount(); ClusterIndex++)
|
|
{
|
|
ClusterArray.Add(Skin->GetCluster(ClusterIndex));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ClusterArray.Num() == 0)
|
|
{
|
|
if (AllowImportBoneErrorAndWarning)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("FbxSkeletaLMeshimport_NoAssociatedCluster", "No associated clusters")), FFbxErrors::SkeletalMesh_NoAssociatedCluster);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// get bind pose
|
|
if(RetrievePoseFromBindPose(NodeArray, PoseArray) == false)
|
|
{
|
|
if (!GIsAutomationTesting)
|
|
UE_LOG(LogFbx, Warning, TEXT("Getting valid bind pose failed. Try to recreate bind pose"));
|
|
// if failed, delete bind pose, and retry.
|
|
const int32 PoseCount = Scene->GetPoseCount();
|
|
for(int32 PoseIndex = PoseCount-1; PoseIndex >= 0; --PoseIndex)
|
|
{
|
|
FbxPose* CurrentPose = Scene->GetPose(PoseIndex);
|
|
|
|
// current pose is bind pose,
|
|
if(CurrentPose && CurrentPose->IsBindPose())
|
|
{
|
|
Scene->RemovePose(PoseIndex);
|
|
CurrentPose->Destroy();
|
|
}
|
|
}
|
|
|
|
SdkManager->CreateMissingBindPoses(Scene);
|
|
if ( RetrievePoseFromBindPose(NodeArray, PoseArray) == false)
|
|
{
|
|
if (!GIsAutomationTesting)
|
|
UE_LOG(LogFbx, Warning, TEXT("Recreating bind pose failed."));
|
|
}
|
|
else
|
|
{
|
|
if (!GIsAutomationTesting)
|
|
UE_LOG(LogFbx, Warning, TEXT("Recreating bind pose succeeded."));
|
|
}
|
|
}
|
|
|
|
// recurse through skeleton and build ordered table
|
|
BuildSkeletonSystem(ClusterArray, SortedLinks);
|
|
}
|
|
|
|
// error check
|
|
// if no bond is found
|
|
if(SortedLinks.Num() == 0)
|
|
{
|
|
if (AllowImportBoneErrorAndWarning)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_NoBone", "'{0}' has no bones"), FText::FromString(NodeArray[0]->GetName()))), FFbxErrors::SkeletalMesh_NoBoneFound);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// if no bind pose is found
|
|
if (!bUseTime0AsRefPose && PoseArray.GetCount() == 0)
|
|
{
|
|
// add to tokenized error message
|
|
if (ImportOptions->bImportScene)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_InvalidBindPose", "Skeletal Mesh '{0}' dont have a bind pose. Scene import do not support yet time 0 as bind pose, there will be no bind pose import"), FText::FromString(NodeArray[0]->GetName()))), FFbxErrors::SkeletalMesh_InvalidBindPose);
|
|
}
|
|
else
|
|
{
|
|
if (!GIsAutomationTesting)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("FbxSkeletaLMeshimport_MissingBindPose", "Could not find the bind pose. It will use time 0 as bind pose.")), FFbxErrors::SkeletalMesh_InvalidBindPose);
|
|
}
|
|
bUseTime0AsRefPose = true;
|
|
}
|
|
}
|
|
|
|
int32 LinkIndex;
|
|
|
|
// Check for duplicate bone names and issue a warning if found
|
|
for(LinkIndex = 0; LinkIndex < SortedLinks.Num(); ++LinkIndex)
|
|
{
|
|
Link = SortedLinks[LinkIndex];
|
|
|
|
for(int32 AltLinkIndex = LinkIndex+1; AltLinkIndex < SortedLinks.Num(); ++AltLinkIndex)
|
|
{
|
|
FbxNode* AltLink = SortedLinks[AltLinkIndex];
|
|
|
|
if(FCStringAnsi::Strcmp(Link->GetName(), AltLink->GetName()) == 0)
|
|
{
|
|
if (AllowImportBoneErrorAndWarning)
|
|
{
|
|
FString RawBoneName = UTF8_TO_TCHAR(Link->GetName());
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("Error_DuplicateBoneName", "Error, Could not import {0}.\nDuplicate bone name found ('{1}'). Each bone must have a unique name."),
|
|
FText::FromString(NodeArray[0]->GetName()), FText::FromString(RawBoneName))), FFbxErrors::SkeletalMesh_DuplicateBones);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
FbxArray<FbxAMatrix> GlobalsPerLink;
|
|
GlobalsPerLink.Grow(SortedLinks.Num());
|
|
GlobalsPerLink[0].SetIdentity();
|
|
|
|
bool GlobalLinkFoundFlag;
|
|
FbxVector4 LocalLinkT;
|
|
FbxQuaternion LocalLinkQ;
|
|
FbxVector4 LocalLinkS;
|
|
|
|
bool bAnyLinksNotInBindPose = false;
|
|
FString LinksWithoutBindPoses;
|
|
|
|
int32 RootIdx = INDEX_NONE;
|
|
|
|
for (LinkIndex=0; LinkIndex<SortedLinks.Num(); LinkIndex++)
|
|
{
|
|
// Add a bone for each FBX Link
|
|
ImportData.RefBonesBinary.Emplace(SkeletalMeshImportData::FBone());
|
|
Link = SortedLinks[LinkIndex];
|
|
int32 ParentIndex = INDEX_NONE; // base value for root if no parent found
|
|
|
|
if (LinkIndex == 0)
|
|
{
|
|
RootIdx = LinkIndex;
|
|
}
|
|
else
|
|
{
|
|
const FbxNode* LinkParent = Link->GetParent();
|
|
// get the link parent index.
|
|
for (int32 ll = 0; ll < LinkIndex; ++ll) // <LinkIndex because parent is guaranteed to be before child in sortedLink
|
|
{
|
|
FbxNode* Otherlink = SortedLinks[ll];
|
|
if (Otherlink == LinkParent)
|
|
{
|
|
ParentIndex = ll;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (ParentIndex == INDEX_NONE)//We found another root inside the hierarchy, this is not supported
|
|
{
|
|
if (AllowImportBoneErrorAndWarning)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, LOCTEXT("MultipleRootsFound", "Multiple roots are found in the bone hierarchy. We only support single root bone.")), FFbxErrors::SkeletalMesh_MultipleRoots);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
GlobalLinkFoundFlag = false;
|
|
if (!bIsARigidMesh) //skeletal mesh
|
|
{
|
|
// there are some links, they have no cluster, but in bindpose
|
|
if (PoseArray.GetCount())
|
|
{
|
|
for (int32 PoseIndex = 0; PoseIndex < PoseArray.GetCount(); PoseIndex++)
|
|
{
|
|
int32 PoseLinkIndex = PoseArray[PoseIndex]->Find(Link);
|
|
if (PoseLinkIndex>=0)
|
|
{
|
|
FbxMatrix NoneAffineMatrix = PoseArray[PoseIndex]->GetMatrix(PoseLinkIndex);
|
|
FbxAMatrix Matrix = *(FbxAMatrix*)(double*)&NoneAffineMatrix;
|
|
GlobalsPerLink[LinkIndex] = Matrix;
|
|
GlobalLinkFoundFlag = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!GlobalLinkFoundFlag)
|
|
{
|
|
// since now we set use time 0 as ref pose this won't unlikely happen
|
|
// but leaving it just in case it still has case where it's missing partial bind pose
|
|
if(!bUseTime0AsRefPose && !bDisableMissingBindPoseWarning)
|
|
{
|
|
bAnyLinksNotInBindPose = true;
|
|
LinksWithoutBindPoses += UTF8_TO_TCHAR(Link->GetName());
|
|
LinksWithoutBindPoses += TEXT(" \n");
|
|
}
|
|
|
|
for (int32 ClusterIndex=0; ClusterIndex<ClusterArray.Num(); ClusterIndex++)
|
|
{
|
|
FbxCluster* Cluster = ClusterArray[ClusterIndex];
|
|
if (Link == Cluster->GetLink())
|
|
{
|
|
Cluster->GetTransformLinkMatrix(GlobalsPerLink[LinkIndex]);
|
|
GlobalLinkFoundFlag = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!GlobalLinkFoundFlag)
|
|
{
|
|
GlobalsPerLink[LinkIndex] = Link->EvaluateGlobalTransform();
|
|
}
|
|
|
|
if (bUseTime0AsRefPose && !ImportOptions->bImportScene)
|
|
{
|
|
FbxAMatrix& T0Matrix = Scene->GetAnimationEvaluator()->GetNodeGlobalTransform(Link, 0);
|
|
if (GlobalsPerLink[LinkIndex] != T0Matrix)
|
|
{
|
|
bOutDiffPose = true;
|
|
}
|
|
|
|
GlobalsPerLink[LinkIndex] = T0Matrix;
|
|
}
|
|
|
|
//Add the join orientation
|
|
GlobalsPerLink[LinkIndex] = GlobalsPerLink[LinkIndex] * FFbxDataConverter::GetJointPostConversionMatrix();
|
|
if (LinkIndex)
|
|
{
|
|
FbxAMatrix Matrix;
|
|
Matrix = GlobalsPerLink[ParentIndex].Inverse() * GlobalsPerLink[LinkIndex];
|
|
LocalLinkT = Matrix.GetT();
|
|
LocalLinkQ = Matrix.GetQ();
|
|
LocalLinkS = Matrix.GetS();
|
|
}
|
|
else // skeleton root
|
|
{
|
|
// for root, this is global coordinate
|
|
LocalLinkT = GlobalsPerLink[LinkIndex].GetT();
|
|
LocalLinkQ = GlobalsPerLink[LinkIndex].GetQ();
|
|
LocalLinkS = GlobalsPerLink[LinkIndex].GetS();
|
|
}
|
|
|
|
// set bone
|
|
SkeletalMeshImportData::FBone& Bone = ImportData.RefBonesBinary[LinkIndex];
|
|
FString BoneName = MakeName( Link->GetName() );
|
|
Bone.Name = BoneName;
|
|
|
|
//Check for nan and for zero scale
|
|
if (AllowImportBoneErrorAndWarning)
|
|
{
|
|
bool bFoundNan = false;
|
|
bool bFoundZeroScale = false;
|
|
for (int32 i = 0; i < 4; ++i)
|
|
{
|
|
if (i < 3)
|
|
{
|
|
if (FMath::IsNaN(LocalLinkT[i]) || FMath::IsNaN(LocalLinkS[i]))
|
|
{
|
|
bFoundNan = true;
|
|
}
|
|
if (FMath::IsNearlyZero(LocalLinkS[i]))
|
|
{
|
|
bFoundZeroScale = true;
|
|
}
|
|
}
|
|
if (FMath::IsNaN(LocalLinkQ[i]))
|
|
{
|
|
bFoundNan = true;
|
|
}
|
|
}
|
|
|
|
if (bFoundNan)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("BoneTransformNAN", "Found NAN value in bone transform. Bone name: [{0}]"), FText::FromString(BoneName))), FFbxErrors::SkeletalMesh_InvalidBindPose);
|
|
}
|
|
|
|
if (bFoundZeroScale)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("BoneTransformZeroScale", "Found zero scale value in bone transform. Bone name: [{0}]"), FText::FromString(BoneName))), FFbxErrors::SkeletalMesh_InvalidBindPose);
|
|
}
|
|
}
|
|
|
|
SkeletalMeshImportData::FJointPos& JointMatrix = Bone.BonePos;
|
|
FbxSkeleton* Skeleton = Link->GetSkeleton();
|
|
if (Skeleton)
|
|
{
|
|
JointMatrix.Length = Converter.ConvertDist(Skeleton->LimbLength.Get());
|
|
JointMatrix.XSize = Converter.ConvertDist(Skeleton->Size.Get());
|
|
JointMatrix.YSize = Converter.ConvertDist(Skeleton->Size.Get());
|
|
JointMatrix.ZSize = Converter.ConvertDist(Skeleton->Size.Get());
|
|
}
|
|
else
|
|
{
|
|
JointMatrix.Length = 1. ;
|
|
JointMatrix.XSize = 100. ;
|
|
JointMatrix.YSize = 100. ;
|
|
JointMatrix.ZSize = 100. ;
|
|
}
|
|
|
|
// get the link parent and children
|
|
Bone.ParentIndex = ParentIndex;
|
|
Bone.NumChildren = 0;
|
|
for (int32 ChildIndex=0; ChildIndex<Link->GetChildCount(); ChildIndex++)
|
|
{
|
|
FbxNode* Child = Link->GetChild(ChildIndex);
|
|
if (IsUnrealBone(Child))
|
|
{
|
|
Bone.NumChildren++;
|
|
}
|
|
}
|
|
|
|
JointMatrix.Transform.SetTranslation(FVector3f(Converter.ConvertPos(LocalLinkT)));
|
|
JointMatrix.Transform.SetRotation(FQuat4f(Converter.ConvertRotToQuat(LocalLinkQ)));
|
|
JointMatrix.Transform.SetScale3D(FVector3f(Converter.ConvertScale(LocalLinkS)));
|
|
}
|
|
|
|
//In case we do a scene import we need a relative to skeletal mesh transform instead of a global
|
|
if (ImportOptions->bImportScene && !ImportOptions->bTransformVertexToAbsolute)
|
|
{
|
|
FbxAMatrix GlobalSkeletalNodeFbx = Scene->GetAnimationEvaluator()->GetNodeGlobalTransform(SkeletalMeshNode, 0);
|
|
FTransform3f GlobalSkeletalNode;
|
|
GlobalSkeletalNode.SetFromMatrix(FMatrix44f(Converter.ConvertMatrix(GlobalSkeletalNodeFbx.Inverse())));
|
|
|
|
SkeletalMeshImportData::FBone& RootBone = ImportData.RefBonesBinary[RootIdx];
|
|
FTransform3f& RootTransform = RootBone.BonePos.Transform;
|
|
RootTransform.SetFromMatrix(RootTransform.ToMatrixWithScale() * GlobalSkeletalNode.ToMatrixWithScale());
|
|
}
|
|
|
|
if(TemplateData)
|
|
{
|
|
FbxAMatrix FbxAddedMatrix;
|
|
BuildFbxMatrixForImportTransform(FbxAddedMatrix, TemplateData);
|
|
FMatrix44f AddedMatrix = FMatrix44f(Converter.ConvertMatrix(FbxAddedMatrix));
|
|
|
|
SkeletalMeshImportData::FBone& RootBone = ImportData.RefBonesBinary[RootIdx];
|
|
FTransform3f& RootTransform = RootBone.BonePos.Transform;
|
|
RootTransform.SetFromMatrix(RootTransform.ToMatrixWithScale() * AddedMatrix);
|
|
}
|
|
|
|
if(bAnyLinksNotInBindPose && AllowImportBoneErrorAndWarning && !GIsAutomationTesting)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_BonesAreMissingFromBindPose", "The following bones are missing from the bind pose:\n{0}\nThis can happen for bones that are not vert weighted. If they are not in the correct orientation after importing,\nplease set the \"Use T0 as ref pose\" option or add them to the bind pose and reimport the skeletal mesh."), FText::FromString(LinksWithoutBindPoses))), FFbxErrors::SkeletalMesh_BonesAreMissingFromBindPose);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UnFbx::FFbxImporter::FillSkeletalMeshImportData(TArray<FbxNode*>& NodeArray, UFbxSkeletalMeshImportData* TemplateImportData, TArray<FbxShape*> *FbxShapeArray, FSkeletalMeshImportData* OutData, TArray<FbxNode*>& OutImportedSkeletonLinkNodes, TArray<FName> &LastImportedMaterialNames, const bool bIsReimport, const TMap<FVector3f, FColor>& ExistingVertexColorData, bool& bMapMorphTargetToTimeZero)
|
|
{
|
|
if (NodeArray.Num() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FbxNode* Node = NodeArray[0];
|
|
// find the mesh by its name
|
|
FbxMesh* FbxMesh = Node->GetMesh();
|
|
|
|
if (OutData == nullptr)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Fill with data from buffer - contains the full .FBX file.
|
|
FSkeletalMeshImportData* SkelMeshImportDataPtr = OutData;
|
|
|
|
OutImportedSkeletonLinkNodes.Empty();
|
|
TArray<FbxNode*>& SortedLinkArray = OutImportedSkeletonLinkNodes;
|
|
FbxArray<FbxAMatrix> GlobalsPerLink;
|
|
|
|
bool bDiffPose = false;
|
|
bool bUseT0AsRefPose = ImportOptions->bUseT0AsRefPose;
|
|
|
|
// Note: importing morph data causes additional passes through this function, so disable the warning dialogs
|
|
// from popping up again on each additional pass.
|
|
if (!ImportBones(NodeArray, *SkelMeshImportDataPtr, TemplateImportData, SortedLinkArray, bDiffPose, (FbxShapeArray != nullptr), bUseT0AsRefPose, Node, bIsReimport))
|
|
{
|
|
if (!(bIsReimport && ImportOptions->bImportAsSkeletalGeometry)) //Do not import bone if we import only the geometry and we are reimporting
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
bMapMorphTargetToTimeZero = bUseT0AsRefPose & bDiffPose;
|
|
|
|
FbxNode* SceneRootNode = Scene->GetRootNode();
|
|
if(SceneRootNode && TemplateImportData)
|
|
{
|
|
ApplyTransformSettingsToFbxNode(SceneRootNode, TemplateImportData);
|
|
}
|
|
|
|
// Create a list of all unique fbx materials. This needs to be done as a separate pass before reading geometry
|
|
// so that we know about all possible materials before assigning material indices to each triangle
|
|
TArray<FbxSurfaceMaterial*> FbxMaterials;
|
|
for (int32 NodeIndex = 0; NodeIndex < NodeArray.Num(); ++NodeIndex)
|
|
{
|
|
Node = NodeArray[NodeIndex];
|
|
|
|
int32 MaterialCount = Node->GetMaterialCount();
|
|
|
|
for (int32 MaterialIndex = 0; MaterialIndex < MaterialCount; ++MaterialIndex)
|
|
{
|
|
FbxSurfaceMaterial* FbxMaterial = Node->GetMaterial(MaterialIndex);
|
|
|
|
// Store unique FbxNode materials resulting in sections being combined.
|
|
if (!ImportOptions->bKeepSectionsSeparate)
|
|
{
|
|
if (!FbxMaterials.Contains(FbxMaterial))
|
|
{
|
|
FbxMaterials.Add(FbxMaterial);
|
|
|
|
SkeletalMeshImportData::FMaterial NewMaterial;
|
|
|
|
NewMaterial.MaterialImportName = MakeName(FbxMaterial->GetName());
|
|
// Add an entry for each unique material
|
|
SkelMeshImportDataPtr->Materials.Add(NewMaterial);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Store each FbxNode material regardless of duplicates to avoid sections being combined.
|
|
FbxMaterials.Add(FbxMaterial);
|
|
|
|
SkeletalMeshImportData::FMaterial NewMaterial;
|
|
|
|
NewMaterial.MaterialImportName = MakeNodeMaterialIndexName(Node, MaterialIndex);
|
|
// Add an entry for each unique material
|
|
SkelMeshImportDataPtr->Materials.Add(NewMaterial);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32 NodeIndex = 0; NodeIndex < NodeArray.Num(); ++NodeIndex)
|
|
{
|
|
Node = NodeArray[NodeIndex];
|
|
FbxNode *RootNode = NodeArray[0];
|
|
FbxMesh = Node->GetMesh();
|
|
FbxSkin* Skin = (FbxSkin*)FbxMesh->GetDeformer(0, FbxDeformer::eSkin);
|
|
FbxShape* FbxShape = nullptr;
|
|
if (FbxShapeArray)
|
|
{
|
|
FbxShape = (*FbxShapeArray)[NodeIndex];
|
|
}
|
|
|
|
// NOTE: This function may invalidate FbxMesh and set it to point to a an updated version
|
|
if (!FillSkelMeshImporterFromFbx( *SkelMeshImportDataPtr, FbxMesh, Skin, FbxShape, SortedLinkArray, FbxMaterials, RootNode, ExistingVertexColorData) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (bMapMorphTargetToTimeZero && !ImportOptions->bImportScene)
|
|
{
|
|
// deform skin vertex to the frame 0 from bind pose
|
|
SkinControlPointsToPose(*SkelMeshImportDataPtr, FbxMesh, FbxShape, true);
|
|
}
|
|
}
|
|
|
|
CleanUpUnusedMaterials(*SkelMeshImportDataPtr);
|
|
|
|
for (SkeletalMeshImportData::FMaterial& MaterialInfo : SkelMeshImportDataPtr->Materials)
|
|
{
|
|
if (UMaterial* Material = Cast<UMaterial>(MaterialInfo.Material))
|
|
{
|
|
Material->bUsedWithSkeletalMesh = true;
|
|
}
|
|
}
|
|
|
|
|
|
if (LastImportedMaterialNames.Num() > 0)
|
|
{
|
|
SetMaterialOrderByName(*SkelMeshImportDataPtr, LastImportedMaterialNames);
|
|
}
|
|
else
|
|
{
|
|
// reorder material according to "SKinXX" in material name
|
|
SetMaterialSkinXXOrder(*SkelMeshImportDataPtr);
|
|
}
|
|
|
|
if (ImportOptions->bPreserveSmoothingGroups)
|
|
{
|
|
bool bDuplicateUnSmoothWedges = false; //We deprecate legacy build
|
|
DoUnSmoothVerts(*SkelMeshImportDataPtr, bDuplicateUnSmoothWedges);
|
|
}
|
|
else
|
|
{
|
|
SkelMeshImportDataPtr->PointToRawMap.AddUninitialized(SkelMeshImportDataPtr->Points.Num());
|
|
for (int32 PointIdx = 0; PointIdx < SkelMeshImportDataPtr->Points.Num(); PointIdx++)
|
|
{
|
|
SkelMeshImportDataPtr->PointToRawMap[PointIdx] = PointIdx;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UnFbx::FFbxImporter::ReplaceSkeletalMeshGeometryImportData(const USkeletalMesh* SkeletalMesh, FSkeletalMeshImportData* ImportData, int32 LodIndex)
|
|
{
|
|
return FSkeletalMeshImportData::ReplaceSkeletalMeshGeometryImportData(SkeletalMesh, ImportData, LodIndex);
|
|
}
|
|
|
|
bool UnFbx::FFbxImporter::ReplaceSkeletalMeshSkinningImportData(const USkeletalMesh* SkeletalMesh, FSkeletalMeshImportData* ImportData, int32 LodIndex)
|
|
{
|
|
return FSkeletalMeshImportData::ReplaceSkeletalMeshRigImportData(SkeletalMesh, ImportData, LodIndex);
|
|
}
|
|
|
|
bool UnFbx::FFbxImporter::FillSkeletalMeshImportPoints(FSkeletalMeshImportData* OutData, FbxNode* RootNode, FbxNode* Node, FbxShape* FbxShape)
|
|
{
|
|
FbxMesh* FbxMesh = Node->GetMesh();
|
|
|
|
// Extract the name.
|
|
FString MeshName = MakeName(FbxMesh->GetName());
|
|
if (MeshName.IsEmpty())
|
|
{
|
|
MeshName = MakeName(Node->GetName());
|
|
}
|
|
|
|
// Add the mesh info
|
|
OutData->MeshInfos.AddDefaulted();
|
|
SkeletalMeshImportData::FMeshInfo& MeshInfo = OutData->MeshInfos.Last();
|
|
MeshInfo.Name = *MeshName;
|
|
|
|
const int32 ControlPointsCount = FbxMesh->GetControlPointsCount();
|
|
const int32 ExistPointNum = OutData->Points.Num();
|
|
OutData->Points.AddUninitialized(ControlPointsCount);
|
|
|
|
MeshInfo.StartImportedVertex = ExistPointNum;
|
|
MeshInfo.NumVertices = ControlPointsCount;
|
|
|
|
// Construct the matrices for the conversion from right handed to left handed system
|
|
FbxAMatrix TotalMatrix = ComputeSkeletalMeshTotalMatrix(Node, RootNode);
|
|
|
|
int32 ControlPointsIndex;
|
|
bool bInvalidPositionFound = false;
|
|
for( ControlPointsIndex = 0 ; ControlPointsIndex < ControlPointsCount ;ControlPointsIndex++ )
|
|
{
|
|
FbxVector4 Position;
|
|
if (FbxShape)
|
|
{
|
|
Position = FbxShape->GetControlPoints()[ControlPointsIndex];
|
|
}
|
|
else
|
|
{
|
|
Position = FbxMesh->GetControlPoints()[ControlPointsIndex];
|
|
}
|
|
|
|
FbxVector4 FinalPosition;
|
|
FinalPosition = TotalMatrix.MultT(Position);
|
|
FVector ConvertedPosition = Converter.ConvertPos(FinalPosition);
|
|
|
|
// ensure user when this happens if attached to debugger
|
|
if (!ensure(ConvertedPosition.ContainsNaN() == false))
|
|
{
|
|
if (!bInvalidPositionFound)
|
|
{
|
|
bInvalidPositionFound = true;
|
|
}
|
|
|
|
ConvertedPosition = FVector::ZeroVector;
|
|
}
|
|
|
|
OutData->Points[ ControlPointsIndex + ExistPointNum ] = (FVector3f)ConvertedPosition;
|
|
}
|
|
|
|
if (bInvalidPositionFound)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning,
|
|
FText::Format(LOCTEXT("FbxSkeletaLMeshimport_InvalidPosition", "Invalid position (NaN or Inf) found from source position for mesh '{0}'. Please verify if the source asset contains valid position. "),
|
|
FText::FromString(FbxMesh->GetName()))), FFbxErrors::SkeletalMesh_InvalidPosition);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool UnFbx::FFbxImporter::GatherPointsForMorphTarget(FSkeletalMeshImportData* OutData, TArray<FbxNode*>& NodeArray, TArray< FbxShape* >* FbxShapeArray, TSet<uint32>& ModifiedPoints, const bool bMapMorphTargetToTimeZero)
|
|
{
|
|
check(OutData);
|
|
TArray<FVector3f> CompressPoints;
|
|
CompressPoints.Reserve(OutData->Points.Num());
|
|
FSkeletalMeshImportData NewImportData = *OutData;
|
|
NewImportData.Points.Empty();
|
|
|
|
FbxNode* const RootNode = NodeArray[0];
|
|
|
|
for ( int32 NodeIndex = 0; NodeIndex < NodeArray.Num(); ++NodeIndex )
|
|
{
|
|
FbxNode* Node = NodeArray[NodeIndex];
|
|
FbxMesh* FbxMesh = Node->GetMesh();
|
|
|
|
FbxShape* FbxShape = nullptr;
|
|
if (FbxShapeArray && FbxShapeArray->IsValidIndex(NodeIndex))
|
|
{
|
|
FbxShape = (*FbxShapeArray)[NodeIndex];
|
|
}
|
|
|
|
FillSkeletalMeshImportPoints( &NewImportData, RootNode, Node, FbxShape );
|
|
|
|
if (bMapMorphTargetToTimeZero && !ImportOptions->bImportScene)
|
|
{
|
|
// deform skin vertex to the frame 0 from bind pose
|
|
SkinControlPointsToPose(NewImportData, FbxMesh, FbxShape, true);
|
|
}
|
|
}
|
|
|
|
for ( int32 PointIdx = 0; PointIdx < OutData->Points.Num(); ++PointIdx )
|
|
{
|
|
int32 OriginalPointIdx = OutData->PointToRawMap[ PointIdx ];
|
|
//Rebuild the data with only the modified point
|
|
if ( ( NewImportData.Points[ OriginalPointIdx ] - OutData->Points[ PointIdx ] ).SizeSquared() > FMath::Square(THRESH_POINTS_ARE_SAME) )
|
|
{
|
|
ModifiedPoints.Add( PointIdx );
|
|
CompressPoints.Add(NewImportData.Points[OriginalPointIdx]);
|
|
}
|
|
}
|
|
OutData->Points = CompressPoints;
|
|
return true;
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::FillLastImportMaterialNames(TArray<FName> &LastImportedMaterialNames, USkeletalMesh* BaseSkelMesh, TArray<FName> *OrderedMaterialNames)
|
|
{
|
|
if (OrderedMaterialNames == nullptr && BaseSkelMesh)
|
|
{
|
|
int32 NoneNameCount = 0;
|
|
for (const FSkeletalMaterial &Material : BaseSkelMesh->GetMaterials())
|
|
{
|
|
if (Material.ImportedMaterialSlotName == NAME_None)
|
|
{
|
|
NoneNameCount++;
|
|
}
|
|
LastImportedMaterialNames.Add(Material.ImportedMaterialSlotName);
|
|
}
|
|
if (NoneNameCount >= LastImportedMaterialNames.Num())
|
|
{
|
|
LastImportedMaterialNames.Empty();
|
|
}
|
|
}
|
|
else if (OrderedMaterialNames)
|
|
{
|
|
//Copy the ordered material name parameter
|
|
LastImportedMaterialNames = (*OrderedMaterialNames);
|
|
}
|
|
|
|
//If the imported model is using skinxx workflow just empty LastImportedMaterialNames array
|
|
if (LastImportedMaterialNames.Num() > 0)
|
|
{
|
|
int32 SkinXXNameCount = 0;
|
|
for (FName MaterialName : LastImportedMaterialNames)
|
|
{
|
|
if (MaterialName == NAME_None)
|
|
{
|
|
continue;
|
|
}
|
|
FString ImportedMaterialName = MaterialName.ToString();
|
|
int32 Offset = ImportedMaterialName.Find(TEXT("_SKIN"), ESearchCase::IgnoreCase, ESearchDir::FromEnd);
|
|
if (Offset != INDEX_NONE)
|
|
{
|
|
FString SkinXXNumber = ImportedMaterialName.Right(ImportedMaterialName.Len() - (Offset + 1)).RightChop(4);
|
|
|
|
if (SkinXXNumber.IsNumeric())
|
|
{
|
|
SkinXXNameCount++;
|
|
}
|
|
}
|
|
}
|
|
//If we have some skinxx suffixe we don't use the name to reorder
|
|
if (SkinXXNameCount == LastImportedMaterialNames.Num())
|
|
{
|
|
LastImportedMaterialNames.Empty();
|
|
}
|
|
}
|
|
}
|
|
|
|
//Small helper macro helping us to do an early return when the import is canceled.
|
|
#define EARLY_RETURN_ON_CANCEL(bForce, CancelOperation) \
|
|
if(bForce || (ImportOptions->bIsImportCancelable && SlowTask.ShouldCancel())) \
|
|
{ \
|
|
CancelOperation(); \
|
|
return nullptr; \
|
|
}
|
|
|
|
USkeletalMesh* UnFbx::FFbxImporter::ImportSkeletalMesh(FImportSkeletalMeshArgs &ImportSkeletalMeshArgs)
|
|
{
|
|
if (ImportSkeletalMeshArgs.NodeArray.Num() == 0 || !CanImportClass(USkeletalMesh::StaticClass()))
|
|
{
|
|
return nullptr;
|
|
}
|
|
FFbxScopedOperation ScopedImportOperation(this);
|
|
|
|
FScopedSlowTask SlowTask(1);
|
|
SlowTask.EnterProgressFrame(1);
|
|
USkeletalMesh* SkeletalMesh = nullptr;
|
|
|
|
// We canceled the import, let the factory delete the assets.
|
|
auto CancelCleanup = [&]()
|
|
{
|
|
bImportOperationCanceled = true;
|
|
};
|
|
|
|
// The import failed, we are marking the imported meshes for deletion in the next GC.
|
|
auto FailureCleanup = [&]()
|
|
{
|
|
if (SkeletalMesh)
|
|
{
|
|
SkeletalMesh->ClearFlags(RF_Standalone);
|
|
SkeletalMesh->Rename(NULL, GetTransientPackage(), REN_DontCreateRedirectors);
|
|
}
|
|
};
|
|
|
|
EARLY_RETURN_ON_CANCEL(false, CancelCleanup);
|
|
|
|
int32 SafeLODIndex = ImportSkeletalMeshArgs.LodIndex < 0 ? 0 : ImportSkeletalMeshArgs.LodIndex;
|
|
FbxNode* Node = ImportSkeletalMeshArgs.NodeArray[0];
|
|
// find the mesh by its name
|
|
FbxMesh* FbxMesh = Node->GetMesh();
|
|
|
|
if( !FbxMesh )
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_NodeInvalidSkeletalMesh", "Fbx node: '{0}' is not a valid skeletal mesh"), FText::FromString(Node->GetName()))), FFbxErrors::Generic_Mesh_MeshNotFound);
|
|
return nullptr;
|
|
}
|
|
|
|
//Make sure the render thread is done
|
|
FlushRenderingCommands();
|
|
|
|
// warning for missing smoothing group info
|
|
CheckSmoothingInfo(FbxMesh);
|
|
|
|
Parent = ImportSkeletalMeshArgs.InParent;
|
|
|
|
USkeletalMesh* ExistingSkelMesh = nullptr;
|
|
if ( !ImportSkeletalMeshArgs.FbxShapeArray )
|
|
{
|
|
UObject* ExistingObject = StaticFindObjectFast(UObject::StaticClass(), ImportSkeletalMeshArgs.InParent, ImportSkeletalMeshArgs.Name, false, RF_NoFlags, EInternalObjectFlags::Garbage);
|
|
ExistingSkelMesh = Cast<USkeletalMesh>(ExistingObject);
|
|
|
|
if (!ExistingSkelMesh && ExistingObject)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_OverlappingName", "Same name but different class: '{0}' already exists"), FText::FromString(ExistingObject->GetName()))), FFbxErrors::Generic_SameNameAssetExists);
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
TMap<FVector3f, FColor> ExistingVertexColorData;
|
|
if (!ExistingSkelMesh)
|
|
{
|
|
// When we are not re-importing we want to create the mesh here to be sure there is no material
|
|
// or texture that will be create with the same name
|
|
SkeletalMesh = NewObject<USkeletalMesh>(ImportSkeletalMeshArgs.InParent, ImportSkeletalMeshArgs.Name, ImportSkeletalMeshArgs.Flags);
|
|
|
|
//This should not happen
|
|
if (!ensure(SkeletalMesh))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
CreatedObjects.Add(SkeletalMesh);
|
|
}
|
|
else
|
|
{
|
|
ExistingVertexColorData = ExistingSkelMesh->GetVertexColorData();
|
|
}
|
|
|
|
FSkeletalMeshImportData TempData;
|
|
// Fill with data from buffer - contains the full .FBX file.
|
|
FSkeletalMeshImportData* SkelMeshImportDataPtr = &TempData;
|
|
if( ImportSkeletalMeshArgs.OutData )
|
|
{
|
|
SkelMeshImportDataPtr = ImportSkeletalMeshArgs.OutData;
|
|
}
|
|
|
|
TArray<FName> LastImportedMaterialNames;
|
|
FillLastImportMaterialNames(LastImportedMaterialNames, ExistingSkelMesh, ImportSkeletalMeshArgs.OrderedMaterialNames);
|
|
|
|
//////////////////////////////////////////////////////////////////////////
|
|
// We must do a maximum of fail test before backing up the data since the backup is destructive on the existing skeletal mesh.
|
|
// See the comment later when we call the following function (SaveExistingSkelMeshData)
|
|
TArray<FbxNode*> ImportedSkeletonLinkNodes;
|
|
if (FillSkeletalMeshImportData(ImportSkeletalMeshArgs.NodeArray, ImportSkeletalMeshArgs.TemplateImportData, ImportSkeletalMeshArgs.FbxShapeArray, SkelMeshImportDataPtr, ImportedSkeletonLinkNodes, LastImportedMaterialNames, ExistingSkelMesh != nullptr, ExistingVertexColorData, ImportSkeletalMeshArgs.bMapMorphTargetToTimeZero) == false)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("FbxSkeletaLMeshimport_FillupImportData", "Get Import Data has failed.")), FFbxErrors::SkeletalMesh_FillImportDataFailed);
|
|
EARLY_RETURN_ON_CANCEL(true, FailureCleanup);
|
|
}
|
|
|
|
EARLY_RETURN_ON_CANCEL(false, CancelCleanup);
|
|
|
|
//When reimporting we must verify the import skeleton is valid before applying any modification to the skeletalmesh. For sure if we reimport only the geometry we do not need to do that
|
|
if (ExistingSkelMesh && !ImportOptions->bImportAsSkeletalGeometry)
|
|
{
|
|
const TArray <SkeletalMeshImportData::FBone>& RefBonesBinary = SkelMeshImportDataPtr->RefBonesBinary;
|
|
int32 BoneNumber = RefBonesBinary.Num();
|
|
TArray<FString> UniqueBoneNames;
|
|
UniqueBoneNames.Reserve(BoneNumber);
|
|
for (int32 BoneIndex = 0; BoneIndex < BoneNumber; BoneIndex++)
|
|
{
|
|
const SkeletalMeshImportData::FBone& BinaryBone = RefBonesBinary[BoneIndex];
|
|
const FString BoneName = FSkeletalMeshImportData::FixupBoneName(BinaryBone.Name);
|
|
//FString == operator and <= or >= are all case insensitive
|
|
//We want to check with case insensitive so this is perfect to use contains. No need to iterate
|
|
if (UniqueBoneNames.Contains(BoneName))
|
|
{
|
|
UnFbx::FFbxImporter* FFbxImporter = UnFbx::FFbxImporter::GetInstance();
|
|
FFbxImporter->AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("SkeletonHasDuplicateBones", "Skeleton has non-unique bone names.\nBone named '{0}' encountered more than once."), FText::FromString(BoneName))), FFbxErrors::SkeletalMesh_DuplicateBones);
|
|
if (SkeletalMesh)
|
|
{
|
|
SkeletalMesh->ClearFlags(RF_Standalone);
|
|
SkeletalMesh->Rename(NULL, GetTransientPackage(), REN_DontCreateRedirectors);
|
|
}
|
|
return nullptr;
|
|
}
|
|
UniqueBoneNames.Add(BoneName);
|
|
}
|
|
}
|
|
|
|
//Stack the PostEditChange call, it will call post edit change when it will go out of scope
|
|
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(ExistingSkelMesh);
|
|
|
|
//Adjust the import data from the import options
|
|
if (ExistingSkelMesh != nullptr)
|
|
{
|
|
if (ImportOptions->bImportAsSkeletalSkinning)
|
|
{
|
|
//Replace geometry import data by original existing skel mesh geometry data
|
|
ReplaceSkeletalMeshGeometryImportData(ExistingSkelMesh, SkelMeshImportDataPtr, SafeLODIndex);
|
|
}
|
|
else if (ImportOptions->bImportAsSkeletalGeometry)
|
|
{
|
|
//Replace skinning import data by original existing skel mesh skinning data
|
|
ReplaceSkeletalMeshSkinningImportData(ExistingSkelMesh, SkelMeshImportDataPtr, SafeLODIndex);
|
|
}
|
|
|
|
//Reuse the vertex color in case we are not re-importing weights only and there is some vertexcolor in the existing skeleMesh
|
|
if (ExistingSkelMesh->GetHasVertexColors() && ImportOptions->VertexColorImportOption == EVertexColorImportOption::Ignore)
|
|
{
|
|
RemapSkeletalMeshVertexColorToImportData(ExistingSkelMesh, SafeLODIndex, SkelMeshImportDataPtr);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Create initial bounding box based on expanded version of reference pose for meshes without physics assets. Can be overridden by artist.
|
|
FBox3f BoundingBox(SkelMeshImportDataPtr->Points.GetData(), SkelMeshImportDataPtr->Points.Num());
|
|
const FVector3f BoundingBoxSize = BoundingBox.GetSize();
|
|
|
|
if (SkelMeshImportDataPtr->Points.Num() > 2 && BoundingBoxSize.X < THRESH_POINTS_ARE_SAME && BoundingBoxSize.Y < THRESH_POINTS_ARE_SAME && BoundingBoxSize.Z < THRESH_POINTS_ARE_SAME)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_ErrorMeshTooSmall", "Cannot import this mesh, the bounding box of this mesh is smaller than the supported threshold[{0}]."), FText::FromString(FString::Printf(TEXT("%f"), THRESH_POINTS_ARE_SAME)))), FFbxErrors::SkeletalMesh_FillImportDataFailed);
|
|
EARLY_RETURN_ON_CANCEL(true, FailureCleanup);
|
|
}
|
|
|
|
FSkeletalMeshBuildSettings BuildOptions;
|
|
//Make sure the build option change in the re-import ui is reconduct
|
|
BuildOptions.bUseFullPrecisionUVs = false;
|
|
BuildOptions.bUseBackwardsCompatibleF16TruncUVs = false;
|
|
BuildOptions.bUseHighPrecisionTangentBasis = false;
|
|
BuildOptions.bRecomputeNormals = !ImportOptions->ShouldImportNormals() || !SkelMeshImportDataPtr->bHasNormals;
|
|
BuildOptions.bRecomputeTangents = !ImportOptions->ShouldImportTangents() || !SkelMeshImportDataPtr->bHasTangents;
|
|
BuildOptions.bUseMikkTSpace = (ImportOptions->NormalGenerationMethod == EFBXNormalGenerationMethod::MikkTSpace) && (!ImportOptions->ShouldImportNormals() || !ImportOptions->ShouldImportTangents());
|
|
BuildOptions.bComputeWeightedNormals = ImportOptions->bComputeWeightedNormals;
|
|
BuildOptions.bRemoveDegenerates = ImportOptions->bRemoveDegenerates;
|
|
BuildOptions.ThresholdPosition = ImportOptions->OverlappingThresholds.ThresholdPosition;
|
|
BuildOptions.ThresholdTangentNormal = ImportOptions->OverlappingThresholds.ThresholdTangentNormal;
|
|
BuildOptions.ThresholdUV = ImportOptions->OverlappingThresholds.ThresholdUV;
|
|
BuildOptions.MorphThresholdPosition = ImportOptions->OverlappingThresholds.MorphThresholdPosition;
|
|
|
|
TSharedPtr<FExistingSkelMeshData> ExistSkelMeshDataPtr;
|
|
if (ExistingSkelMesh)
|
|
{
|
|
ExistingSkelMesh->PreEditChange(NULL);
|
|
//We update the build settings before saving the asset data we want to restore after the re-import
|
|
int32 SafeReimportLODIndex = ImportSkeletalMeshArgs.LodIndex < 0 ? 0 : ImportSkeletalMeshArgs.LodIndex;
|
|
FSkeletalMeshLODInfo* LODInfoPtr = ExistingSkelMesh->GetLODInfo(SafeReimportLODIndex);
|
|
if (LODInfoPtr)
|
|
{
|
|
// Full precision UV and High precision tangent cannot be change in the re-import options, it must not be change from the original data.
|
|
BuildOptions.bUseFullPrecisionUVs = LODInfoPtr->BuildSettings.bUseFullPrecisionUVs;
|
|
BuildOptions.bUseHighPrecisionTangentBasis = LODInfoPtr->BuildSettings.bUseHighPrecisionTangentBasis;
|
|
BuildOptions.bUseBackwardsCompatibleF16TruncUVs = LODInfoPtr->BuildSettings.bUseBackwardsCompatibleF16TruncUVs;
|
|
|
|
//Copy all the build option to reflect any change in the setting using the re-import UI
|
|
LODInfoPtr->BuildSettings = BuildOptions;
|
|
}
|
|
|
|
// Before calling SaveExistingSkelMeshData() we must remove the existing fbx metadata as we don't want to restore those.
|
|
RemoveFBXMetaData(ExistingSkelMesh);
|
|
|
|
//The backup of the skeletal mesh data empty the LOD array in the ImportedResource of the skeletal mesh
|
|
//If the import fail after this step the editor can crash when updating the bone later since the LODModel will not exist anymore
|
|
ExistSkelMeshDataPtr = SkeletalMeshImportUtils::SaveExistingSkelMeshData(ExistingSkelMesh, !ImportOptions->bImportMaterials, ImportSkeletalMeshArgs.LodIndex);
|
|
}
|
|
|
|
if (SkeletalMesh == nullptr)
|
|
{
|
|
if (!ensure(ExistingSkelMesh != nullptr))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
SkeletalMesh = ExistingSkelMesh;
|
|
|
|
int32 PostEditChangeStackCounter = SkeletalMesh->GetPostEditChangeStackCounter();
|
|
SkeletalMesh->SetPostEditChangeStackCounter(0);
|
|
|
|
for (int32 LODIndex = 0, LODCount = SkeletalMesh->GetLODNum(); LODIndex < LODCount; ++LODIndex)
|
|
{
|
|
SkeletalMesh->ClearMeshDescriptionAndBulkData(LODIndex);
|
|
}
|
|
|
|
FSkeletalMeshModel* ImportedResource = SkeletalMesh->GetImportedModel();
|
|
ImportedResource->EmptyOriginalReductionSourceMeshData();
|
|
ImportedResource->LODModels.Empty();
|
|
|
|
SkeletalMesh->ResetLODInfo();
|
|
SkeletalMesh->GetMaterials().Empty();
|
|
SkeletalMesh->GetRefSkeleton().Empty();
|
|
SkeletalMesh->SetSkeleton(nullptr);
|
|
SkeletalMesh->SetPhysicsAsset(nullptr);
|
|
SkeletalMesh->UnregisterAllMorphTarget();
|
|
|
|
SkeletalMesh->ReleaseResources();
|
|
|
|
SkeletalMesh->PostEditChange();
|
|
SkeletalMesh->SetPostEditChangeStackCounter(PostEditChangeStackCounter);
|
|
}
|
|
|
|
|
|
SkeletalMesh->PreEditChange(NULL);
|
|
//Dirty the DDC Key for any imported Skeletal Mesh
|
|
SkeletalMesh->InvalidateDeriveDataCacheGUID();
|
|
|
|
FSkeletalMeshModel *ImportedResource = SkeletalMesh->GetImportedModel();
|
|
check(ImportedResource->LODModels.Num() == 0);
|
|
ImportedResource->LODModels.Empty();
|
|
ImportedResource->LODModels.Add(new FSkeletalMeshLODModel());
|
|
const int32 ImportLODModelIndex = 0;
|
|
FSkeletalMeshLODModel& LODModel = ImportedResource->LODModels[ImportLODModelIndex];
|
|
|
|
// process materials from import data
|
|
SkeletalMeshImportUtils::ProcessImportMeshMaterials(SkeletalMesh->GetMaterials(), *SkelMeshImportDataPtr);
|
|
|
|
// process reference skeleton from import data
|
|
int32 SkeletalDepth = 0;
|
|
USkeleton* ExistingSkeleton = ExistSkelMeshDataPtr ? ExistSkelMeshDataPtr->ExistingSkeleton : ImportOptions->SkeletonForAnimation;
|
|
if (!SkeletalMeshImportUtils::ProcessImportMeshSkeleton(ExistingSkeleton, SkeletalMesh->GetRefSkeleton(), SkeletalDepth, *SkelMeshImportDataPtr))
|
|
{
|
|
EARLY_RETURN_ON_CANCEL(true, FailureCleanup);
|
|
}
|
|
|
|
EARLY_RETURN_ON_CANCEL(false, CancelCleanup);
|
|
|
|
if (!GIsAutomationTesting)
|
|
{
|
|
UE_LOG(LogFbx, Log, TEXT("Bones digested - %i Depth of hierarchy - %i"), SkeletalMesh->GetRefSkeleton().GetNum(), SkeletalDepth);
|
|
}
|
|
|
|
// process bone influences from import data
|
|
SkeletalMeshImportUtils::ProcessImportMeshInfluences(*SkelMeshImportDataPtr, SkeletalMesh->GetPathName());
|
|
|
|
SkeletalMesh->ResetLODInfo();
|
|
FSkeletalMeshLODInfo& NewLODInfo = SkeletalMesh->AddLODInfo();
|
|
NewLODInfo.ReductionSettings.NumOfTrianglesPercentage = 1.0f;
|
|
NewLODInfo.ReductionSettings.NumOfVertPercentage = 1.0f;
|
|
NewLODInfo.ReductionSettings.MaxDeviationPercentage = 0.0f;
|
|
NewLODInfo.LODHysteresis = 0.02f;
|
|
|
|
//Store the original fbx import data the SkelMeshImportDataPtr should not be modified after this
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
SkeletalMesh->SaveLODImportedData(ImportLODModelIndex, *SkelMeshImportDataPtr);
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
|
|
SkeletalMesh->SetImportedBounds(FBoxSphereBounds((FBox)BoundingBox));
|
|
|
|
// Store whether or not this mesh has vertex colors
|
|
SkeletalMesh->SetHasVertexColors(SkelMeshImportDataPtr->bHasVertexColors);
|
|
SkeletalMesh->SetVertexColorGuid(SkeletalMesh->GetHasVertexColors() ? FGuid::NewGuid() : FGuid());
|
|
|
|
// Pass the number of texture coordinate sets to the LODModel. Ensure there is at least one UV coord
|
|
LODModel.NumTexCoords = FMath::Max<uint32>(1, SkelMeshImportDataPtr->NumTexCoords);
|
|
|
|
if(ImportSkeletalMeshArgs.bCreateRenderData )
|
|
{
|
|
TArray<FVector3f> LODPoints;
|
|
TArray<SkeletalMeshImportData::FMeshWedge> LODWedges;
|
|
TArray<SkeletalMeshImportData::FMeshFace> LODFaces;
|
|
TArray<SkeletalMeshImportData::FVertInfluence> LODInfluences;
|
|
TArray<int32> LODPointToRawMap;
|
|
SkelMeshImportDataPtr->CopyLODImportData(LODPoints,LODWedges,LODFaces,LODInfluences,LODPointToRawMap);
|
|
|
|
bool bUseLegacyMeshUtilities = false;
|
|
bool bBuildSuccess = false;
|
|
if (bUseLegacyMeshUtilities)
|
|
{
|
|
IMeshUtilities::MeshBuildOptions LegacyBuildOptions;
|
|
LegacyBuildOptions.FillOptions(BuildOptions);
|
|
LegacyBuildOptions.TargetPlatform = GetTargetPlatformManagerRef().GetRunningTargetPlatform();
|
|
|
|
IMeshUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked<IMeshUtilities>("MeshUtilities");
|
|
|
|
TArray<FText> WarningMessages;
|
|
TArray<FName> WarningNames;
|
|
// Create actual rendering data.
|
|
bBuildSuccess = MeshUtilities.BuildSkeletalMesh(ImportedResource->LODModels[ImportLODModelIndex], SkeletalMesh->GetPathName(), SkeletalMesh->GetRefSkeleton(), LODInfluences, LODWedges, LODFaces, LODPoints, LODPointToRawMap, LegacyBuildOptions, &WarningMessages, &WarningNames);
|
|
|
|
//Cache the vertex/triangle count in the InlineReductionCacheData so we can know if the LODModel need reduction or not.
|
|
TArray<FInlineReductionCacheData>& InlineReductionCacheDatas = ImportedResource->InlineReductionCacheDatas;
|
|
if (!InlineReductionCacheDatas.IsValidIndex(ImportLODModelIndex))
|
|
{
|
|
InlineReductionCacheDatas.AddDefaulted((ImportLODModelIndex + 1) - InlineReductionCacheDatas.Num());
|
|
}
|
|
if (ensure(InlineReductionCacheDatas.IsValidIndex(ImportLODModelIndex)))
|
|
{
|
|
InlineReductionCacheDatas[ImportLODModelIndex].SetCacheGeometryInfo(ImportedResource->LODModels[ImportLODModelIndex]);
|
|
}
|
|
|
|
// temporary hack of message/names, should be one token or a struct
|
|
if (WarningMessages.Num() > 0 && WarningNames.Num() == WarningMessages.Num())
|
|
{
|
|
EMessageSeverity::Type MessageSeverity = bBuildSuccess ? EMessageSeverity::Warning : EMessageSeverity::Error;
|
|
|
|
for (int32 MessageIdx = 0; MessageIdx < WarningMessages.Num(); ++MessageIdx)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(MessageSeverity, WarningMessages[MessageIdx]), WarningNames[MessageIdx]);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//The imported LOD is always 0 here, the LOD custom import will import the LOD alone(in a temporary skeletalmesh) and add it to the base skeletal mesh later
|
|
check(SkeletalMesh->GetLODInfo(ImportLODModelIndex) != nullptr);
|
|
//Set the build options
|
|
SkeletalMesh->GetLODInfo(ImportLODModelIndex)->BuildSettings = BuildOptions;
|
|
//New MeshDescription build process
|
|
IMeshBuilderModule& MeshBuilderModule = IMeshBuilderModule::GetForRunningPlatform();
|
|
//We must build the LODModel so we can restore properly the mesh, but we do not have to regenerate LODs
|
|
const bool bRegenDepLODs = false;
|
|
FSkeletalMeshBuildParameters SkeletalMeshBuildParameters(SkeletalMesh, GetTargetPlatformManagerRef().GetRunningTargetPlatform(), ImportLODModelIndex, bRegenDepLODs);
|
|
// JIRA UE-250536: Change of API to IMeshBuilderModule::BuildSkeletalMesh now requires a rendering resource
|
|
// Solution: Allocate an uninitialized rendering resource then release it after the call to IMeshBuilderModule::BuildSkeletalMesh
|
|
// Although the legacy importer does not support Nanite for skeletal mesh, the call succeeds even if Nanite is manually set to true
|
|
// Hopefully, this solution will last until the legacy FBX importer is deprecated.
|
|
SkeletalMesh->AllocateResourceForRendering();
|
|
bBuildSuccess = MeshBuilderModule.BuildSkeletalMesh(*SkeletalMesh->GetResourceForRendering(), SkeletalMeshBuildParameters);
|
|
SkeletalMesh->ReleaseResources();
|
|
}
|
|
|
|
if( !bBuildSuccess )
|
|
{
|
|
SkeletalMesh->MarkAsGarbage();
|
|
return NULL;
|
|
}
|
|
|
|
// Presize the per-section shadow casting array with the number of sections in the imported LOD.
|
|
const int32 NumSections = LODModel.Sections.Num();
|
|
|
|
//Get the last fbx file data need for reimport
|
|
if (ImportSkeletalMeshArgs.ImportMaterialOriginalNameData)
|
|
{
|
|
for (FSkeletalMaterial SkeletalMaterial : SkeletalMesh->GetMaterials())
|
|
{
|
|
ImportSkeletalMeshArgs.ImportMaterialOriginalNameData->Add(SkeletalMaterial.ImportedMaterialSlotName);
|
|
}
|
|
}
|
|
if (ImportSkeletalMeshArgs.ImportMeshSectionsData)
|
|
{
|
|
const TArray<FSkeletalMaterial>& Materials = SkeletalMesh->GetMaterials();
|
|
for (int32 SectionIndex = 0; SectionIndex < ImportedResource->LODModels[ImportLODModelIndex].Sections.Num(); ++SectionIndex)
|
|
{
|
|
const FSkelMeshSection &SkelMeshSection = ImportedResource->LODModels[ImportLODModelIndex].Sections[SectionIndex];
|
|
int32 MaterialIndex = SkelMeshSection.MaterialIndex;
|
|
if (SkeletalMesh->GetLODInfo(0)->LODMaterialMap.IsValidIndex(SectionIndex) && SkeletalMesh->GetLODInfo(0)->LODMaterialMap[SectionIndex] != INDEX_NONE)
|
|
{
|
|
MaterialIndex = SkeletalMesh->GetLODInfo(0)->LODMaterialMap[SectionIndex];
|
|
}
|
|
|
|
if (ensure(Materials.IsValidIndex(MaterialIndex)))
|
|
{
|
|
ImportSkeletalMeshArgs.ImportMeshSectionsData->SectionOriginalMaterialName.Add(Materials[MaterialIndex].ImportedMaterialSlotName);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Store the current file path and timestamp for re-import purposes
|
|
UFbxSkeletalMeshImportData* ImportData = UFbxSkeletalMeshImportData::GetImportDataForSkeletalMesh(SkeletalMesh, ImportSkeletalMeshArgs.TemplateImportData);
|
|
|
|
if (ExistingSkelMesh)
|
|
{
|
|
//In case of a re-import we update only the type we re-import
|
|
SkeletalMesh->GetAssetImportData()->AddFileName(UFactory::GetCurrentFilename(), ImportOptions->bImportAsSkeletalGeometry ? 1 : ImportOptions->bImportAsSkeletalSkinning ? 2 : 0);
|
|
}
|
|
else
|
|
{
|
|
//When we create a new skeletal mesh asset we create 1 or 3 source files. 1 in case we import all the content, 3 in case we import geo or skinning
|
|
int32 SourceFileIndex = ImportOptions->bImportAsSkeletalGeometry ? 1 : ImportOptions->bImportAsSkeletalSkinning ? 2 : 0;
|
|
//Always add the base sourcefile
|
|
SkeletalMesh->GetAssetImportData()->AddFileName(UFactory::GetCurrentFilename(), 0, NSSkeletalMeshSourceFileLabels::GeoAndSkinningText().ToString());
|
|
if (SourceFileIndex != 0)
|
|
{
|
|
//If the user import geo or skinning, we add both entries to allow reimport of them
|
|
SkeletalMesh->GetAssetImportData()->AddFileName(UFactory::GetCurrentFilename(), 1, NSSkeletalMeshSourceFileLabels::GeometryText().ToString());
|
|
SkeletalMesh->GetAssetImportData()->AddFileName(UFactory::GetCurrentFilename(), 2, NSSkeletalMeshSourceFileLabels::SkinningText().ToString());
|
|
}
|
|
}
|
|
|
|
if (ExistSkelMeshDataPtr)
|
|
{
|
|
SkeletalMeshImportUtils::RestoreExistingSkelMeshData(ExistSkelMeshDataPtr, SkeletalMesh, ImportSkeletalMeshArgs.LodIndex, ImportOptions->bCanShowDialog, ImportOptions->bImportAsSkeletalSkinning, ImportOptions->bResetToFbxOnMaterialConflict);
|
|
}
|
|
|
|
SkeletalMesh->CalculateInvRefMatrices();
|
|
|
|
EARLY_RETURN_ON_CANCEL(false, CancelCleanup);
|
|
|
|
if ((!SkeletalMesh->GetResourceForRendering() || !SkeletalMesh->GetResourceForRendering()->LODRenderData.IsValidIndex(0)) && ImportOptions->bCreatePhysicsAsset && CanImportClass(UPhysicsAsset::StaticClass()))
|
|
{
|
|
//We need to have a valid render data to create physic asset
|
|
SkeletalMesh->Build();
|
|
}
|
|
SkeletalMesh->MarkPackageDirty();
|
|
}
|
|
|
|
if(ImportSkeletalMeshArgs.LodIndex == 0)
|
|
{
|
|
// see if we have skeleton set up
|
|
// if creating skeleton, create skeleton
|
|
USkeleton* Skeleton = ImportOptions->SkeletonForAnimation;
|
|
if (Skeleton == NULL)
|
|
{
|
|
FString ObjectName = FString::Printf(TEXT("%s_Skeleton"), *SkeletalMesh->GetName());
|
|
Skeleton = CreateAsset<USkeleton>(ImportSkeletalMeshArgs.InParent->GetName(), ObjectName, true);
|
|
if (!Skeleton)
|
|
{
|
|
// same object exists, try to see if it's skeleton, if so, load
|
|
Skeleton = LoadObject<USkeleton>(ImportSkeletalMeshArgs.InParent, *ObjectName);
|
|
|
|
// if not skeleton, we're done, we can't create skeleton with same name
|
|
// @todo in the future, we'll allow them to rename
|
|
if (!Skeleton)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_SkeletonRecreateError", "'{0}' already exists. It fails to recreate it."), FText::FromString(ObjectName))), FFbxErrors::SkeletalMesh_SkeletonRecreateError);
|
|
return SkeletalMesh;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CreatedObjects.Add(Skeleton);
|
|
}
|
|
}
|
|
|
|
// merge bones to the selected skeleton
|
|
if ((!ImportOptions->bImportAsSkeletalSkinning || ExistingSkelMesh == nullptr) && !Skeleton->MergeAllBonesToBoneTree( SkeletalMesh ) )
|
|
{
|
|
// We should only show the skeleton save toast once, not as many times as we have nodes to import
|
|
bool bToastSaveMessage = false;
|
|
if(bFirstMesh || (LastMergeBonesChoice != EAppReturnType::NoAll && LastMergeBonesChoice != EAppReturnType::YesAll))
|
|
{
|
|
LastMergeBonesChoice = FMessageDialog::Open(EAppMsgType::YesNoYesAllNoAllCancel,
|
|
LOCTEXT("SkeletonFailed_BoneMerge", "FAILED TO MERGE BONES:\n\n This could happen if significant hierarchical changes have been made\ne.g. inserting a bone between nodes.\nWould you like to regenerate the Skeleton from this mesh?\n\n***WARNING: THIS MAY INVALIDATE OR REQUIRE RECOMPRESSION OF ANIMATION DATA.***\n"));
|
|
bToastSaveMessage = true;
|
|
}
|
|
|
|
if(LastMergeBonesChoice == EAppReturnType::Cancel)
|
|
{
|
|
// User wants to cancel further importing
|
|
bImportOperationCanceled = true;
|
|
return nullptr;
|
|
}
|
|
|
|
if (LastMergeBonesChoice == EAppReturnType::Yes || LastMergeBonesChoice == EAppReturnType::YesAll)
|
|
{
|
|
if ( Skeleton->RecreateBoneTree( SkeletalMesh ) && bToastSaveMessage)
|
|
{
|
|
// @todo: this is a lot of message box but this requires user input and it can be very annoying to miss
|
|
// make sure to go through all skeletalmesh and merge them also to recreate the issue.
|
|
if (FMessageDialog::Open(EAppMsgType::YesNo,
|
|
LOCTEXT("Skeleton_ReAddAllMeshes", "Would you like to merge all SkeletalMeshes using this skeleton to ensure all bones are merged? This will require to load those SkeletalMeshes.")) == EAppReturnType::Yes)
|
|
{
|
|
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
|
|
TArray<FAssetData> SkeletalMeshAssetData;
|
|
|
|
FARFilter ARFilter;
|
|
ARFilter.ClassPaths.Add(USkeletalMesh::StaticClass()->GetClassPathName());
|
|
ARFilter.TagsAndValues.Add(TEXT("Skeleton"), FAssetData(Skeleton).GetExportTextName());
|
|
|
|
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
|
|
if (AssetRegistry.GetAssets(ARFilter, SkeletalMeshAssetData))
|
|
{
|
|
// look through all skeletalmeshes that uses this skeleton
|
|
for (int32 AssetId = 0; AssetId < SkeletalMeshAssetData.Num(); ++AssetId)
|
|
{
|
|
FAssetData& CurAssetData = SkeletalMeshAssetData[AssetId];
|
|
const USkeletalMesh* ExtraSkeletalMesh = Cast<USkeletalMesh>(CurAssetData.GetAsset());
|
|
if (SkeletalMesh != ExtraSkeletalMesh && IsValid(ExtraSkeletalMesh))
|
|
{
|
|
// merge still can fail, then print message box
|
|
if (Skeleton->MergeAllBonesToBoneTree(ExtraSkeletalMesh) == false)
|
|
{
|
|
// print warning
|
|
FMessageDialog::Open(EAppMsgType::Ok,
|
|
FText::Format(LOCTEXT("SkeletonRegenError_RemergingBones", "Failed to merge SkeletalMesh '{0}'."), FText::FromString(ExtraSkeletalMesh->GetName())));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
FAssetNotifications::SkeletonNeedsToBeSaved(Skeleton);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// ask if they'd like to update their position form this mesh
|
|
if ( ImportOptions->SkeletonForAnimation && ImportOptions->bUpdateSkeletonReferencePose )
|
|
{
|
|
Skeleton->UpdateReferencePoseFromMesh(SkeletalMesh);
|
|
FAssetNotifications::SkeletonNeedsToBeSaved(Skeleton);
|
|
}
|
|
}
|
|
|
|
EARLY_RETURN_ON_CANCEL(false, CancelCleanup);
|
|
|
|
if (SkeletalMesh->GetSkeleton() != Skeleton)
|
|
{
|
|
SkeletalMesh->SetSkeleton(Skeleton);
|
|
SkeletalMesh->MarkPackageDirty();
|
|
}
|
|
// Create PhysicsAsset if requested and if physics asset is null
|
|
// We create the physic asset after we create the skeleton since we need the skeleton to correctly build it
|
|
if (ImportOptions->bCreatePhysicsAsset)
|
|
{
|
|
if (SkeletalMesh->GetPhysicsAsset() == NULL && CanImportClass(UPhysicsAsset::StaticClass()))
|
|
{
|
|
FString ObjectName = FString::Printf(TEXT("%s_PhysicsAsset"), *SkeletalMesh->GetName());
|
|
UPhysicsAsset * NewPhysicsAsset = CreateAsset<UPhysicsAsset>(ImportSkeletalMeshArgs.InParent->GetName(), ObjectName, true);
|
|
if (!NewPhysicsAsset)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_CouldNotCreatePhysicsAsset", "Could not create Physics Asset ('{0}') for '{1}'"), FText::FromString(ObjectName), FText::FromString(SkeletalMesh->GetName()))), FFbxErrors::SkeletalMesh_FailedToCreatePhyscisAsset);
|
|
}
|
|
else
|
|
{
|
|
FPhysAssetCreateParams NewBodyData;
|
|
FText CreationErrorMessage;
|
|
CreatedObjects.Add(NewPhysicsAsset);
|
|
bool bSuccess = FPhysicsAssetUtils::CreateFromSkeletalMesh(NewPhysicsAsset, SkeletalMesh, NewBodyData, CreationErrorMessage);
|
|
if (!bSuccess)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, CreationErrorMessage), FFbxErrors::SkeletalMesh_FailedToCreatePhyscisAsset);
|
|
// delete the asset since we could not have create physics asset
|
|
TArray<UObject*> ObjectsToDelete;
|
|
ObjectsToDelete.Add(NewPhysicsAsset);
|
|
ObjectTools::DeleteObjects(ObjectsToDelete, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// if physics asset is selected
|
|
else if (ImportOptions->PhysicsAsset)
|
|
{
|
|
SkeletalMesh->SetPhysicsAsset(ImportOptions->PhysicsAsset);
|
|
}
|
|
}
|
|
|
|
// Import mesh metadata to SkeletalMesh
|
|
for (FbxNode* MeshNode : ImportSkeletalMeshArgs.NodeArray)
|
|
{
|
|
ImportNodeCustomProperties(SkeletalMesh, MeshNode, true);
|
|
}
|
|
|
|
// Import bone metadata to Skeleton
|
|
for (FbxNode* SkeletonNode : ImportedSkeletonLinkNodes)
|
|
{
|
|
ImportNodeCustomProperties(SkeletalMesh->GetSkeleton(), SkeletonNode, true);
|
|
}
|
|
|
|
return SkeletalMesh;
|
|
}
|
|
#undef EARLY_RETURN_ON_CANCEL
|
|
|
|
void UnFbx::FFbxImporter::UpdateSkeletalMeshImportData(USkeletalMesh *SkeletalMesh, UFbxSkeletalMeshImportData* SkeletalMeshImportData, int32 SpecificLod, TArray<FName> *ImportMaterialOriginalNameData, TArray<FImportMeshLodSectionsData> *ImportMeshLodData)
|
|
{
|
|
if (SkeletalMesh == nullptr)
|
|
{
|
|
return;
|
|
}
|
|
|
|
UFbxSkeletalMeshImportData* ImportData = Cast<UFbxSkeletalMeshImportData>(SkeletalMesh->GetAssetImportData());
|
|
if (!ImportData && SkeletalMeshImportData)
|
|
{
|
|
ImportData = UFbxSkeletalMeshImportData::GetImportDataForSkeletalMesh(SkeletalMesh, SkeletalMeshImportData);
|
|
}
|
|
if (ImportData)
|
|
{
|
|
//Set the last content type import
|
|
ImportData->LastImportContentType = ImportData->ImportContentType;
|
|
|
|
ImportData->ImportMaterialOriginalNameData.Empty();
|
|
if (ImportMaterialOriginalNameData && ImportMeshLodData)
|
|
{
|
|
if (SpecificLod == INDEX_NONE && ImportMeshLodData->Num() == SkeletalMesh->GetLODNum())
|
|
{
|
|
//Copy the material array
|
|
ImportData->ImportMaterialOriginalNameData = (*ImportMaterialOriginalNameData);
|
|
|
|
ImportData->ImportMeshLodData.Empty();
|
|
for (const FImportMeshLodSectionsData &ImportMeshLodSectionsData : (*ImportMeshLodData))
|
|
{
|
|
ImportData->ImportMeshLodData.Add(ImportMeshLodSectionsData);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (FName MaterialImportNameLOD : (*ImportMaterialOriginalNameData))
|
|
{
|
|
bool bFoundMaterial = false;
|
|
for (FName MaterialImportName : ImportData->ImportMaterialOriginalNameData)
|
|
{
|
|
if (MaterialImportNameLOD == MaterialImportName)
|
|
{
|
|
bFoundMaterial = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!bFoundMaterial)
|
|
{
|
|
//Add the LOD material at the end of the original array
|
|
ImportData->ImportMaterialOriginalNameData.Add(MaterialImportNameLOD);
|
|
}
|
|
}
|
|
|
|
if (SpecificLod == INDEX_NONE)
|
|
{
|
|
for (int32 UpdateLodIndex = 0; UpdateLodIndex < (*ImportMeshLodData).Num(); ++UpdateLodIndex)
|
|
{
|
|
if (ImportData->ImportMeshLodData.Num() <= UpdateLodIndex)
|
|
{
|
|
ImportData->ImportMeshLodData.AddZeroed();
|
|
}
|
|
ImportData->ImportMeshLodData[UpdateLodIndex] = (*ImportMeshLodData)[UpdateLodIndex];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (ImportData->ImportMeshLodData.Num() <= SpecificLod)
|
|
{
|
|
ImportData->ImportMeshLodData.AddZeroed(1 + SpecificLod - ImportData->ImportMeshLodData.Num());
|
|
}
|
|
ImportData->ImportMeshLodData[SpecificLod] = (*ImportMeshLodData)[0];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//This is not a re-import or we reimport an old asset containing no data
|
|
//In this case we update from the skeletal mesh import
|
|
ImportData->ImportMaterialOriginalNameData.Empty();
|
|
ImportData->ImportMeshLodData.Empty();
|
|
for (const FSkeletalMaterial &Material : SkeletalMesh->GetMaterials())
|
|
{
|
|
ImportData->ImportMaterialOriginalNameData.Add(Material.ImportedMaterialSlotName);
|
|
}
|
|
FSkeletalMeshModel* ImportedResource = SkeletalMesh->GetImportedModel();
|
|
for (int32 LODResoureceIndex = 0; LODResoureceIndex < ImportedResource->LODModels.Num(); ++LODResoureceIndex)
|
|
{
|
|
ImportData->ImportMeshLodData.AddZeroed();
|
|
const FSkeletalMeshLODInfo& LODInfo = *(SkeletalMesh->GetLODInfo(LODResoureceIndex));
|
|
const FSkeletalMeshLODModel& LODModel = ImportedResource->LODModels[LODResoureceIndex];
|
|
int32 NumSections = LODModel.Sections.Num();
|
|
for (int32 SectionIndex = 0; SectionIndex < NumSections; ++SectionIndex)
|
|
{
|
|
int32 MaterialLodSectionIndex = LODModel.Sections[SectionIndex].MaterialIndex;
|
|
//Is this LOD use the LODMaterialMap override
|
|
if (LODInfo.LODMaterialMap.IsValidIndex(SectionIndex) && LODInfo.LODMaterialMap[SectionIndex] != INDEX_NONE)
|
|
{
|
|
MaterialLodSectionIndex = LODInfo.LODMaterialMap[SectionIndex];
|
|
}
|
|
|
|
if (ImportData->ImportMaterialOriginalNameData.IsValidIndex(MaterialLodSectionIndex))
|
|
{
|
|
ImportData->ImportMeshLodData[LODResoureceIndex].SectionOriginalMaterialName.Add(ImportData->ImportMaterialOriginalNameData[MaterialLodSectionIndex]);
|
|
}
|
|
else
|
|
{
|
|
ImportData->ImportMeshLodData[LODResoureceIndex].SectionOriginalMaterialName.Add(TEXT("InvalidMaterialIndex"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Note: For 36.00, the forward axis is either Y or X. A more complete solution will be implemented later.
|
|
SkeletalMesh->SetForwardAxis(ImportData->bConvertScene ? (ImportData->bForceFrontXAxis ? EAxis::X : EAxis::Y) : EAxis::Y);
|
|
}
|
|
}
|
|
|
|
|
|
UObject* UnFbx::FFbxImporter::CreateAssetOfClass(UClass* AssetClass, FString ParentPackageName, FString ObjectName, bool bAllowReplace)
|
|
{
|
|
// See if this sequence already exists.
|
|
UObject* ParentPkg = CreatePackage( *ParentPackageName);
|
|
FString ParentPath = FString::Printf(TEXT("%s/%s"), *FPackageName::GetLongPackagePath(*ParentPackageName), *ObjectName);
|
|
UObject* Parent = CreatePackage( *ParentPath);
|
|
// See if an object with this name exists
|
|
UObject* Object = LoadObject<UObject>(Parent, *ObjectName, NULL, LOAD_NoWarn | LOAD_Quiet, NULL);
|
|
|
|
// if object with same name but different class exists, warn user
|
|
if ((Object != NULL) && (Object->GetClass() != AssetClass))
|
|
{
|
|
UnFbx::FFbxImporter* FFbxImporter = UnFbx::FFbxImporter::GetInstance();
|
|
FFbxImporter->AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, LOCTEXT("Error_AssetExist", "Asset with same name exists. Can't overwrite another asset")), FFbxErrors::Generic_SameNameAssetExists);
|
|
return NULL;
|
|
}
|
|
|
|
// if object with same name exists, warn user
|
|
if ( Object != NULL && !bAllowReplace )
|
|
{
|
|
// until we have proper error message handling so we don't ask every time, but once, I'm disabling it.
|
|
// if ( EAppReturnType::Yes != FMessageDialog::Open( EAppMsgType::YesNo, LocalizeSecure(NSLOCTEXT("UnrealEd", "Error_AssetExistAsk", "Asset with the name (`~) exists. Would you like to overwrite?").ToString(), *ParentPath) ) )
|
|
// {
|
|
// return NULL;
|
|
// }
|
|
UnFbx::FFbxImporter* FFbxImporter = UnFbx::FFbxImporter::GetInstance();
|
|
FFbxImporter->AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_SameNameExist", "Asset with the name ('{0}') exists. Overwriting..."), FText::FromString(ParentPath))), FFbxErrors::Generic_SameNameAssetOverriding);
|
|
}
|
|
|
|
if (Object == NULL)
|
|
{
|
|
// add it to the set
|
|
// do not add to the set, now create independent asset
|
|
Object = NewObject<UObject>(Parent, AssetClass, *ObjectName, RF_Public | RF_Standalone);
|
|
Object->MarkPackageDirty();
|
|
// Notify the asset registry
|
|
FAssetRegistryModule::AssetCreated(Object);
|
|
}
|
|
|
|
return Object;
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::SetupAnimationDataFromMesh(USkeletalMesh* SkeletalMesh, UObject* InParent, TArray<FbxNode*>& NodeArray, UFbxAnimSequenceImportData* TemplateImportData, const FString& Name)
|
|
{
|
|
if (!CanImportClass(UAnimSequence::StaticClass()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
USkeleton* Skeleton = SkeletalMesh->GetSkeleton();
|
|
|
|
if (Scene->GetSrcObjectCount<FbxAnimStack>() > 0)
|
|
{
|
|
if ( ensure ( Skeleton != NULL ) )
|
|
{
|
|
TArray<FbxNode*> FBXMeshNodeArray;
|
|
FbxNode* SkeletonRoot = FindFBXMeshesByBone(SkeletalMesh->GetSkeleton()->GetReferenceSkeleton().GetBoneName(0), true, FBXMeshNodeArray);
|
|
|
|
if (SkeletonRoot != nullptr)
|
|
{
|
|
TArray<FbxNode*> SortedLinks;
|
|
RecursiveBuildSkeleton(SkeletonRoot, SortedLinks);
|
|
|
|
// when importing animation from SkeletalMesh, add new Group Anim, a lot of times they're same name
|
|
UPackage * OuterPackage = InParent->GetOutermost();
|
|
FString AnimName = (ImportOptions->AnimationName != "") ? ImportOptions->AnimationName : Name + TEXT("_Anim");
|
|
// give animouter as outer
|
|
ImportAnimations(Skeleton, OuterPackage, SortedLinks, AnimName, TemplateImportData, NodeArray);
|
|
}
|
|
else
|
|
{
|
|
//Cannot import animations if the skeleton do not match
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_SkeletonNotMatching_no_anim_import", "Specified Skeleton '{0}' do not match fbx imported skeleton. Cannot import animations with this skeleton."), FText::FromName(Skeleton->GetFName()) )), FFbxErrors::Animation_InvalidData);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
USkeletalMesh* UnFbx::FFbxImporter::ReimportSkeletalMesh(USkeletalMesh* Mesh, UFbxSkeletalMeshImportData* TemplateImportData, uint64 SkeletalMeshFbxUID, TArray<FbxNode*> *OutSkeletalMeshArray)
|
|
{
|
|
FFbxScopedOperation ScopedImportOperation(this);
|
|
|
|
if ( !ensure(Mesh) )
|
|
{
|
|
// You need a mesh in order to reimport
|
|
return NULL;
|
|
}
|
|
|
|
if ( !ensure(TemplateImportData) )
|
|
{
|
|
// You need import data in order to reimport
|
|
return NULL;
|
|
}
|
|
|
|
TArray<FbxNode*>* FbxNodes = NULL;
|
|
USkeletalMesh* NewMesh = NULL;
|
|
|
|
bool Old_ImportRigidMesh = ImportOptions->bImportRigidMesh;
|
|
bool Old_ImportMaterials = ImportOptions->bImportMaterials;
|
|
bool Old_ImportTextures = ImportOptions->bImportTextures;
|
|
bool Old_ImportAnimations = ImportOptions->bImportAnimations;
|
|
|
|
auto CleanUpImportOptionsData = [&]()
|
|
{
|
|
ImportOptions->bImportRigidMesh = Old_ImportRigidMesh;
|
|
ImportOptions->bImportMaterials = Old_ImportMaterials;
|
|
ImportOptions->bImportTextures = Old_ImportTextures;
|
|
ImportOptions->bImportAnimations = Old_ImportAnimations;
|
|
};
|
|
|
|
// support to update rigid animation mesh
|
|
ImportOptions->bImportRigidMesh = true;
|
|
|
|
UFbxSkeletalMeshImportData* SKImportData = Cast<UFbxSkeletalMeshImportData>(Mesh->GetAssetImportData());
|
|
if (SKImportData)
|
|
{
|
|
constexpr int32 LODIndex = 0;
|
|
FSkeletalMeshLODInfo* LODInfo = Mesh->GetLODInfo(LODIndex);
|
|
if(LODInfo && Mesh->GetImportedModel() && Mesh->GetImportedModel()->LODModels.IsValidIndex(LODIndex))
|
|
{
|
|
if (!Mesh->HasMeshDescription(LODIndex))
|
|
{
|
|
//Set the build settings
|
|
LODInfo->BuildSettings.bComputeWeightedNormals = SKImportData->bComputeWeightedNormals;
|
|
LODInfo->BuildSettings.bRecomputeNormals = SKImportData->NormalImportMethod == EFBXNormalImportMethod::FBXNIM_ComputeNormals;
|
|
LODInfo->BuildSettings.bRecomputeTangents = SKImportData->NormalImportMethod != EFBXNormalImportMethod::FBXNIM_ImportNormalsAndTangents;
|
|
LODInfo->BuildSettings.bRemoveDegenerates = true;
|
|
LODInfo->BuildSettings.bUseMikkTSpace = SKImportData->NormalGenerationMethod == EFBXNormalGenerationMethod::MikkTSpace;
|
|
LODInfo->BuildSettings.ThresholdPosition = SKImportData->ThresholdPosition;
|
|
LODInfo->BuildSettings.ThresholdTangentNormal = SKImportData->ThresholdTangentNormal;
|
|
LODInfo->BuildSettings.ThresholdUV = SKImportData->ThresholdUV;
|
|
LODInfo->BuildSettings.MorphThresholdPosition = SKImportData->MorphThresholdPosition;
|
|
}
|
|
}
|
|
}
|
|
|
|
// get meshes in Fbx file
|
|
//the function also fill the collision models, so we can update collision models correctly
|
|
TArray< TArray<FbxNode*>* > FbxSkelMeshArray;
|
|
FillFbxSkelMeshArrayInScene(Scene->GetRootNode(), FbxSkelMeshArray, false, ImportOptions->bImportAsSkeletalGeometry || ImportOptions->bImportAsSkeletalSkinning, ImportOptions->bImportScene);
|
|
|
|
if(SkeletalMeshFbxUID != 0xFFFFFFFFFFFFFFFF)
|
|
{
|
|
//Scene reimport know which skeletal mesh we want to reimport
|
|
for (TArray<FbxNode*>*SkeletalMeshNodes : FbxSkelMeshArray)
|
|
{
|
|
if (SkeletalMeshNodes->Num() > 0)
|
|
{
|
|
FbxNode *Node = (*SkeletalMeshNodes)[0];
|
|
FbxNode *SkeletalMeshNode = Node;
|
|
if (Node->GetNodeAttribute() && Node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
|
|
{
|
|
TArray<FbxNode*> NodeInLod;
|
|
FindAllLODGroupNode(NodeInLod, Node, 0);
|
|
for (FbxNode *MeshNode : NodeInLod)
|
|
{
|
|
if (MeshNode != nullptr && MeshNode->GetNodeAttribute() && MeshNode->GetNodeAttribute()->GetUniqueID() == SkeletalMeshFbxUID)
|
|
{
|
|
FbxNodes = SkeletalMeshNodes;
|
|
if (OutSkeletalMeshArray != nullptr)
|
|
{
|
|
for (FbxNode *NodeReimport : (*SkeletalMeshNodes))
|
|
{
|
|
OutSkeletalMeshArray->Add(NodeReimport);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (SkeletalMeshNode != nullptr && SkeletalMeshNode->GetNodeAttribute() && SkeletalMeshNode->GetNodeAttribute()->GetUniqueID() == SkeletalMeshFbxUID)
|
|
{
|
|
FbxNodes = SkeletalMeshNodes;
|
|
if (OutSkeletalMeshArray != nullptr)
|
|
{
|
|
for (FbxNode *NodeReimport : (*SkeletalMeshNodes))
|
|
{
|
|
OutSkeletalMeshArray->Add(NodeReimport);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (FbxNodes != nullptr)
|
|
break;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if there is only one mesh, use it without name checking
|
|
// (because the "Used As Full Name" option enables users name the Unreal mesh by themselves
|
|
if (FbxSkelMeshArray.Num() == 1)
|
|
{
|
|
FbxNodes = FbxSkelMeshArray[0];
|
|
}
|
|
else if (FbxSkelMeshArray.Num() > 1)
|
|
{
|
|
//Search a set corresponding
|
|
for (TArray<FbxNode*>*SkeletalMeshNodes : FbxSkelMeshArray)
|
|
{
|
|
FbxNode* Node = GetMeshNodesFromName(Mesh->GetName(), *SkeletalMeshNodes);
|
|
if (Node != nullptr)
|
|
{
|
|
FbxNodes = SkeletalMeshNodes;
|
|
break;
|
|
}
|
|
}
|
|
if (FbxNodes == nullptr)
|
|
{
|
|
//Re import the first skeletalmesh found in the fbx file
|
|
FbxNodes = FbxSkelMeshArray[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
if (FbxNodes)
|
|
{
|
|
//Posteditchange will be call at the end of the reimport which will reallocate the render ressource
|
|
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(Mesh);
|
|
|
|
FScopedSlowTask SlowTask(100.0f, NSLOCTEXT("SkeltalMeshReimportFactory", "SkeletalMeshReimportTask", "Reimporting skeletal mesh..."));
|
|
SlowTask.MakeDialog();
|
|
|
|
//The fbx file is valid we can reimport the skeletal mesh
|
|
Mesh->PreEditChange(nullptr);
|
|
|
|
// Unbind any existing clothing assets before we reimport the geometry
|
|
TArray<ClothingAssetUtils::FClothingAssetMeshBinding> ClothingBindings;
|
|
FLODUtilities::UnbindClothingAndBackup(Mesh, ClothingBindings);
|
|
|
|
//Reimport must force a new guid
|
|
Mesh->InvalidateDeriveDataCacheGUID();
|
|
|
|
for (int32 LODCounter = 1; LODCounter < Mesh->GetLODNum(); ++LODCounter)
|
|
{
|
|
//Reset all import with base mesh flags
|
|
Mesh->GetLODInfo(LODCounter)->bImportWithBaseMesh = false;
|
|
}
|
|
//Empty the morph target before re-importing, it will prevent to have old data that can point on random vertex
|
|
//Do not empty the morph if we import weights only
|
|
if (Mesh->GetMorphTargets().Num() > 0 && !ImportOptions->bImportAsSkeletalSkinning)
|
|
{
|
|
Mesh->UnregisterAllMorphTarget();
|
|
}
|
|
|
|
// set import options, how about others?
|
|
if (!ImportOptions->bImportScene)
|
|
{
|
|
ImportOptions->bImportMaterials = false;
|
|
ImportOptions->bImportTextures = false;
|
|
}
|
|
//In case of a scene reimport animations are reimport later so its ok to hardcode animation to false here
|
|
ImportOptions->bImportAnimations = false;
|
|
// check if there is LODGroup for this skeletal mesh
|
|
int32 MaxLODLevel = 1;
|
|
int32 NumPrevLODs = Mesh->GetLODNum();
|
|
|
|
for (int32 j = 0; j < (*FbxNodes).Num(); j++)
|
|
{
|
|
FbxNode* Node = (*FbxNodes)[j];
|
|
if (Node->GetNodeAttribute() && Node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
|
|
{
|
|
// get max LODgroup level
|
|
if (MaxLODLevel < Node->GetChildCount())
|
|
{
|
|
MaxLODLevel = Node->GetChildCount();
|
|
}
|
|
}
|
|
}
|
|
|
|
//Original fbx data storage
|
|
TArray<FName> ImportMaterialOriginalNameData;
|
|
TArray<FImportMeshLodSectionsData> ImportMeshLodData;
|
|
|
|
int32 LODIndex;
|
|
int32 SuccessfulLodIndex = 0;
|
|
float ProgressStep = static_cast<float>(90.0f / MaxLODLevel);
|
|
for (LODIndex = 0; LODIndex < MaxLODLevel; LODIndex++)
|
|
{
|
|
SlowTask.EnterProgressFrame(ProgressStep);
|
|
int32 ImportedSuccessfulLodIndex = INDEX_NONE;
|
|
TArray<FbxNode*> SkelMeshNodeArray;
|
|
for (int32 j = 0; j < (*FbxNodes).Num(); j++)
|
|
{
|
|
FbxNode* Node = (*FbxNodes)[j];
|
|
if (Node->GetNodeAttribute() && Node->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eLODGroup)
|
|
{
|
|
TArray<FbxNode*> NodeInLod;
|
|
if (Node->GetChildCount() > LODIndex)
|
|
{
|
|
FindAllLODGroupNode(NodeInLod, Node, LODIndex);
|
|
}
|
|
else // in less some LODGroups have less level, use the last level
|
|
{
|
|
FindAllLODGroupNode(NodeInLod, Node, Node->GetChildCount() - 1);
|
|
}
|
|
for (FbxNode *MeshNode : NodeInLod)
|
|
{
|
|
SkelMeshNodeArray.Add(MeshNode);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SkelMeshNodeArray.Add(Node);
|
|
}
|
|
}
|
|
FSkeletalMeshImportData OutData;
|
|
bool bMapMorphTargetToTimeZero = false;
|
|
if (LODIndex == 0)
|
|
{
|
|
ImportMeshLodData.AddZeroed();
|
|
UnFbx::FFbxImporter::FImportSkeletalMeshArgs ImportSkeletalMeshArgs;
|
|
ImportSkeletalMeshArgs.InParent = Mesh->GetOuter();
|
|
ImportSkeletalMeshArgs.NodeArray = SkelMeshNodeArray;
|
|
ImportSkeletalMeshArgs.Name = *Mesh->GetName();
|
|
ImportSkeletalMeshArgs.Flags = RF_Public | RF_Standalone;
|
|
ImportSkeletalMeshArgs.TemplateImportData = TemplateImportData;
|
|
ImportSkeletalMeshArgs.LodIndex = LODIndex;
|
|
ImportSkeletalMeshArgs.ImportMaterialOriginalNameData = &ImportMaterialOriginalNameData;
|
|
ImportSkeletalMeshArgs.ImportMeshSectionsData = &ImportMeshLodData[0];
|
|
ImportSkeletalMeshArgs.OutData = &OutData;
|
|
|
|
NewMesh = ImportSkeletalMesh( ImportSkeletalMeshArgs );
|
|
if (bImportOperationCanceled)
|
|
{
|
|
// User cancel, clean up and return
|
|
NewMesh = nullptr;
|
|
break;
|
|
}
|
|
if (NewMesh)
|
|
{
|
|
bMapMorphTargetToTimeZero = ImportSkeletalMeshArgs.bMapMorphTargetToTimeZero;
|
|
ImportedSuccessfulLodIndex = SuccessfulLodIndex;
|
|
SuccessfulLodIndex++;
|
|
}
|
|
}
|
|
else if (NewMesh && ImportOptions->bImportSkeletalMeshLODs && SkelMeshNodeArray[0]->GetMesh() == nullptr)
|
|
{
|
|
FSkeletalMeshUpdateContext UpdateContext;
|
|
UpdateContext.SkeletalMesh = NewMesh;
|
|
//Add a autogenerated LOD to the BaseSkeletalMesh
|
|
if(SuccessfulLodIndex >= NewMesh->GetLODNum())
|
|
{
|
|
NewMesh->AddLODInfo();
|
|
check(SuccessfulLodIndex == NewMesh->GetLODNum() - 1);
|
|
}
|
|
ImportMeshLodData.AddZeroed();
|
|
FSkeletalMeshLODInfo* LODInfo = NewMesh->GetLODInfo(SuccessfulLodIndex);
|
|
LODInfo->ReductionSettings.NumOfTrianglesPercentage = FMath::Pow(0.5f, (float)(SuccessfulLodIndex));
|
|
LODInfo->ReductionSettings.BaseLOD = 0;
|
|
FLODUtilities::SimplifySkeletalMeshLOD(UpdateContext, SuccessfulLodIndex, GetTargetPlatformManagerRef().GetRunningTargetPlatform(), false);
|
|
LODInfo->bImportWithBaseMesh = true;
|
|
LODInfo->SourceImportFilename = FString(TEXT(""));
|
|
ImportedSuccessfulLodIndex = SuccessfulLodIndex;
|
|
SuccessfulLodIndex++;
|
|
}
|
|
else if (NewMesh && ImportOptions->bImportSkeletalMeshLODs) // the base skeletal mesh is imported successfully
|
|
{
|
|
TArray<FName> ImportMaterialOriginalNameDataLOD;
|
|
ImportMeshLodData.AddZeroed();
|
|
USkeletalMesh* BaseSkeletalMesh = NewMesh;
|
|
|
|
UnFbx::FFbxImporter::FImportSkeletalMeshArgs ImportSkeletalMeshArgs;
|
|
ImportSkeletalMeshArgs.InParent = NewMesh->GetOutermost();
|
|
ImportSkeletalMeshArgs.NodeArray = SkelMeshNodeArray;
|
|
ImportSkeletalMeshArgs.Name = NAME_None;
|
|
ImportSkeletalMeshArgs.Flags = RF_Transient;
|
|
ImportSkeletalMeshArgs.TemplateImportData = TemplateImportData;
|
|
ImportSkeletalMeshArgs.LodIndex = SuccessfulLodIndex;
|
|
ImportSkeletalMeshArgs.ImportMaterialOriginalNameData = &ImportMaterialOriginalNameDataLOD;
|
|
ImportSkeletalMeshArgs.ImportMeshSectionsData = &ImportMeshLodData[SuccessfulLodIndex];
|
|
ImportSkeletalMeshArgs.OutData = &OutData;
|
|
|
|
UObject *LODObject = ImportSkeletalMesh( ImportSkeletalMeshArgs );
|
|
bool bImportSucceeded = !bImportOperationCanceled && ImportSkeletalMeshLOD( Cast<USkeletalMesh>(LODObject), BaseSkeletalMesh, SuccessfulLodIndex, TemplateImportData);
|
|
|
|
if (bImportSucceeded)
|
|
{
|
|
FSkeletalMeshLODInfo* LODInfo = BaseSkeletalMesh->GetLODInfo(SuccessfulLodIndex);
|
|
LODInfo->bImportWithBaseMesh = true;
|
|
LODInfo->SourceImportFilename = FString(TEXT(""));
|
|
bMapMorphTargetToTimeZero = ImportSkeletalMeshArgs.bMapMorphTargetToTimeZero;
|
|
ImportedSuccessfulLodIndex = SuccessfulLodIndex;
|
|
SuccessfulLodIndex++;
|
|
for (FName MaterialImportNameLOD : ImportMaterialOriginalNameDataLOD)
|
|
{
|
|
bool bFoundMaterial = false;
|
|
for (FName MaterialImportName : ImportMaterialOriginalNameData)
|
|
{
|
|
if (MaterialImportNameLOD == MaterialImportName)
|
|
{
|
|
bFoundMaterial = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!bFoundMaterial)
|
|
{
|
|
//Add the LOD material at the end of the original array
|
|
ImportMaterialOriginalNameData.Add(MaterialImportNameLOD);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, LOCTEXT("FailedToImport_SkeletalMeshLOD", "Failed to import Skeletal mesh LOD.")), FFbxErrors::SkeletalMesh_LOD_FailedToImport);
|
|
}
|
|
|
|
}
|
|
|
|
if (NewMesh)
|
|
{
|
|
if ((ImportOptions->bImportSkeletalMeshLODs || LODIndex == 0) &&
|
|
ImportOptions->bImportMorph &&
|
|
ImportedSuccessfulLodIndex != INDEX_NONE &&
|
|
NewMesh->GetImportedModel() &&
|
|
NewMesh->GetImportedModel()->LODModels.IsValidIndex(ImportedSuccessfulLodIndex))
|
|
{
|
|
ImportFbxMorphTarget(SkelMeshNodeArray, NewMesh, ImportedSuccessfulLodIndex, OutData, bMapMorphTargetToTimeZero);
|
|
}
|
|
}
|
|
}
|
|
|
|
auto RebindClothing = [&ClothingBindings](USkeletalMesh* SkelMesh)
|
|
{
|
|
FSkeletalMeshModel* MeshResource = SkelMesh->GetImportedModel();
|
|
if (MeshResource && ClothingBindings.Num() > 0)
|
|
{
|
|
FLODUtilities::RestoreClothingFromBackup(SkelMesh, ClothingBindings);
|
|
}
|
|
};
|
|
|
|
|
|
if (!bImportOperationCanceled && NewMesh)
|
|
{
|
|
RebindClothing(NewMesh);
|
|
|
|
//Update the import data so we can re-import correctly
|
|
UpdateSkeletalMeshImportData(NewMesh, TemplateImportData, INDEX_NONE, &ImportMaterialOriginalNameData, &ImportMeshLodData);
|
|
}
|
|
else if(bImportOperationCanceled && Mesh != nullptr)
|
|
{
|
|
//Operation was canceled
|
|
//rebind the clothing on the original mesh
|
|
RebindClothing(Mesh);
|
|
}
|
|
SlowTask.EnterProgressFrame(10.0f);
|
|
}
|
|
else
|
|
{
|
|
// no mesh found in the FBX file
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_NoFBXMeshMatch", "No FBX mesh matches the Unreal mesh '{0}'."), FText::FromString(Mesh->GetName()))), FFbxErrors::Generic_Mesh_MeshNotFound);
|
|
}
|
|
|
|
CleanUpImportOptionsData();
|
|
|
|
return NewMesh;
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::SetMaterialSkinXXOrder(FSkeletalMeshImportData& ImportData)
|
|
{
|
|
TArray<int32> MaterialIndexToSkinIndex;
|
|
TMap<int32, int32> SkinIndexToMaterialIndex;
|
|
TArray<int32> MissingSkinSuffixMaterial;
|
|
TMap<int32, int32> SkinIndexGreaterThenMaterialArraySize;
|
|
{
|
|
int32 MaterialCount = ImportData.Materials.Num();
|
|
|
|
bool bNeedsReorder = false;
|
|
for (int32 MaterialIndex = 0; MaterialIndex < MaterialCount; ++MaterialIndex)
|
|
{
|
|
// get skin index
|
|
FString MatName = ImportData.Materials[MaterialIndex].MaterialImportName;
|
|
|
|
if (MatName.Len() > 6)
|
|
{
|
|
int32 Offset = MatName.Find(TEXT("_SKIN"), ESearchCase::IgnoreCase, ESearchDir::FromEnd);
|
|
if (Offset != INDEX_NONE)
|
|
{
|
|
// Chop off the material name so we are left with the number in _SKINXX
|
|
FString SkinXXNumber = MatName.Right(MatName.Len() - (Offset + 1)).RightChop(4);
|
|
|
|
if (SkinXXNumber.IsNumeric())
|
|
{
|
|
bNeedsReorder = true;
|
|
int32 TmpIndex = FPlatformString::Atoi(*SkinXXNumber);
|
|
if (TmpIndex < MaterialCount)
|
|
{
|
|
SkinIndexToMaterialIndex.Add(TmpIndex, MaterialIndex);
|
|
}
|
|
else
|
|
{
|
|
SkinIndexGreaterThenMaterialArraySize.Add(TmpIndex, MaterialIndex);
|
|
}
|
|
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MissingSkinSuffixMaterial.Add(MaterialIndex);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MissingSkinSuffixMaterial.Add(MaterialIndex);
|
|
}
|
|
}
|
|
|
|
if (bNeedsReorder && MissingSkinSuffixMaterial.Num() > 0)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, LOCTEXT("FbxSkeletaLMeshimport_Skinxx_missing", "Cannot mix skinxx suffix materials with no skinxx material, mesh section order will not be right.")), FFbxErrors::Generic_Mesh_SkinxxNameError);
|
|
return;
|
|
}
|
|
|
|
//Add greater then material array skinxx at the end sorted by integer the index will be remap correctly in the case of a LOD import
|
|
if (SkinIndexGreaterThenMaterialArraySize.Num() > 0)
|
|
{
|
|
int32 MaxAvailableKey = SkinIndexToMaterialIndex.Num();
|
|
for (int32 AvailableKey = 0; AvailableKey < MaxAvailableKey; ++AvailableKey)
|
|
{
|
|
if (SkinIndexToMaterialIndex.Contains(AvailableKey))
|
|
continue;
|
|
|
|
TMap<int32, int32> TempSkinIndexToMaterialIndex;
|
|
for (auto KvpSkinToMat : SkinIndexToMaterialIndex)
|
|
{
|
|
if (KvpSkinToMat.Key > AvailableKey)
|
|
{
|
|
TempSkinIndexToMaterialIndex.Add(KvpSkinToMat.Key - 1, KvpSkinToMat.Value);
|
|
}
|
|
else
|
|
{
|
|
TempSkinIndexToMaterialIndex.Add(KvpSkinToMat.Key, KvpSkinToMat.Value);
|
|
}
|
|
}
|
|
//move all the later key of the array to fill the available index
|
|
SkinIndexToMaterialIndex = TempSkinIndexToMaterialIndex;
|
|
AvailableKey--; //We need to retest the same index it can be empty
|
|
}
|
|
//Reorder the array
|
|
SkinIndexGreaterThenMaterialArraySize.KeySort(TLess<int32>());
|
|
for (auto Kvp : SkinIndexGreaterThenMaterialArraySize)
|
|
{
|
|
SkinIndexToMaterialIndex.Add(SkinIndexToMaterialIndex.Num(), Kvp.Value);
|
|
}
|
|
}
|
|
|
|
//Fill the array MaterialIndexToSkinIndex so we order material by _skinXX order
|
|
//This ensure we support skinxx suffixe that are not increment by one like _skin00, skin_01, skin_03, skin_04, skin_08...
|
|
for (auto kvp : SkinIndexToMaterialIndex)
|
|
{
|
|
int32 MatIndexToInsert = 0;
|
|
for (MatIndexToInsert = 0; MatIndexToInsert < MaterialIndexToSkinIndex.Num(); ++MatIndexToInsert)
|
|
{
|
|
if (*(SkinIndexToMaterialIndex.Find(MaterialIndexToSkinIndex[MatIndexToInsert])) >= kvp.Value)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
MaterialIndexToSkinIndex.Insert(kvp.Key, MatIndexToInsert);
|
|
}
|
|
|
|
if (bNeedsReorder)
|
|
{
|
|
// re-order the materials
|
|
TArray< SkeletalMeshImportData::FMaterial > ExistingMatList = ImportData.Materials;
|
|
for (int32 MissingIndex : MissingSkinSuffixMaterial)
|
|
{
|
|
MaterialIndexToSkinIndex.Insert(MaterialIndexToSkinIndex.Num(), MissingIndex);
|
|
}
|
|
for (int32 MaterialIndex = 0; MaterialIndex < MaterialCount; ++MaterialIndex)
|
|
{
|
|
if (MaterialIndex < MaterialIndexToSkinIndex.Num())
|
|
{
|
|
int32 NewIndex = MaterialIndexToSkinIndex[MaterialIndex];
|
|
if (ExistingMatList.IsValidIndex(NewIndex))
|
|
{
|
|
ImportData.Materials[NewIndex] = ExistingMatList[MaterialIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
// remapping the material index for each triangle
|
|
int32 FaceNum = ImportData.Faces.Num();
|
|
for (int32 TriangleIndex = 0; TriangleIndex < FaceNum; TriangleIndex++)
|
|
{
|
|
SkeletalMeshImportData::FTriangle& Triangle = ImportData.Faces[TriangleIndex];
|
|
if (Triangle.MatIndex < MaterialIndexToSkinIndex.Num())
|
|
{
|
|
Triangle.MatIndex = MaterialIndexToSkinIndex[Triangle.MatIndex];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::SetMaterialOrderByName(FSkeletalMeshImportData& ImportData, TArray<FName> LastImportedMaterialNames)
|
|
{
|
|
TArray<int32> MaterialIndexToNameIndex;
|
|
TMap<int32, int32> NameIndexToMaterialIndex;
|
|
TArray<int32> MissingNameSuffixMaterial;
|
|
TMap<int32, int32> NameIndexGreaterThenMaterialArraySize;
|
|
{
|
|
int32 MaterialCount = ImportData.Materials.Num();
|
|
int32 MaxMaterialOrderedCount = 0;
|
|
bool bNeedsReorder = false;
|
|
for (int32 MaterialIndex = 0; MaterialIndex < MaterialCount; ++MaterialIndex)
|
|
{
|
|
FName MatName = FName(*(ImportData.Materials[MaterialIndex].MaterialImportName));
|
|
bool bFoundValidName = false;
|
|
for (int32 OrderedIndex = 0; OrderedIndex < LastImportedMaterialNames.Num(); ++OrderedIndex)
|
|
{
|
|
FName OrderedMaterialName = LastImportedMaterialNames[OrderedIndex];
|
|
if (OrderedMaterialName == NAME_None)
|
|
{
|
|
continue;
|
|
}
|
|
if (OrderedMaterialName == MatName)
|
|
{
|
|
if (OrderedIndex < MaterialCount)
|
|
{
|
|
MaxMaterialOrderedCount = FMath::Max(MaxMaterialOrderedCount, OrderedIndex+1);
|
|
NameIndexToMaterialIndex.Add(OrderedIndex, MaterialIndex);
|
|
}
|
|
else
|
|
{
|
|
NameIndexGreaterThenMaterialArraySize.Add(OrderedIndex, MaterialIndex);
|
|
}
|
|
bFoundValidName = true;
|
|
bNeedsReorder = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!bFoundValidName)
|
|
{
|
|
MissingNameSuffixMaterial.Add(MaterialIndex);
|
|
MaxMaterialOrderedCount = FMath::Max(MaxMaterialOrderedCount, MaterialIndex+1);
|
|
}
|
|
}
|
|
|
|
if (bNeedsReorder && MissingNameSuffixMaterial.Num() > 0)
|
|
{
|
|
//Add the missing name material at the end to not disturb the existing order
|
|
TArray<int32> OrderedListMissing;
|
|
OrderedListMissing.AddZeroed(MaxMaterialOrderedCount);
|
|
for (auto Kvp : NameIndexToMaterialIndex)
|
|
{
|
|
OrderedListMissing[Kvp.Key] = -1;
|
|
}
|
|
for (int32 OrderedListMissingIndex = 0; OrderedListMissingIndex < OrderedListMissing.Num(); ++OrderedListMissingIndex)
|
|
{
|
|
if (MissingNameSuffixMaterial.Num() <= 0)
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (OrderedListMissing[OrderedListMissingIndex] != 0)
|
|
continue;
|
|
|
|
NameIndexToMaterialIndex.Add(OrderedListMissingIndex, MissingNameSuffixMaterial.Pop());
|
|
}
|
|
}
|
|
|
|
//Add greater then material array slot index at the end sorted by integer the index will be remap correctly in the case of a LOD import
|
|
if (NameIndexGreaterThenMaterialArraySize.Num() > 0)
|
|
{
|
|
int32 MaxAvailableKey = NameIndexToMaterialIndex.Num();
|
|
for (int32 AvailableKey = 0; AvailableKey < MaxAvailableKey; ++AvailableKey)
|
|
{
|
|
if (NameIndexToMaterialIndex.Contains(AvailableKey))
|
|
continue;
|
|
|
|
TMap<int32, int32> TempSkinIndexToMaterialIndex;
|
|
for (auto KvpSkinToMat : NameIndexToMaterialIndex)
|
|
{
|
|
if (KvpSkinToMat.Key > AvailableKey)
|
|
{
|
|
TempSkinIndexToMaterialIndex.Add(KvpSkinToMat.Key - 1, KvpSkinToMat.Value);
|
|
}
|
|
else
|
|
{
|
|
TempSkinIndexToMaterialIndex.Add(KvpSkinToMat.Key, KvpSkinToMat.Value);
|
|
}
|
|
}
|
|
//move all the later key of the array to fill the available index
|
|
NameIndexToMaterialIndex = TempSkinIndexToMaterialIndex;
|
|
AvailableKey--; //We need to retest the same index it can be empty
|
|
}
|
|
//Reorder the array
|
|
NameIndexGreaterThenMaterialArraySize.KeySort(TLess<int32>());
|
|
for (auto Kvp : NameIndexGreaterThenMaterialArraySize)
|
|
{
|
|
NameIndexToMaterialIndex.Add(NameIndexToMaterialIndex.Num(), Kvp.Value);
|
|
}
|
|
}
|
|
|
|
//Fill the array MaterialIndexToNameIndex so we order material by ordered index
|
|
for (auto kvp : NameIndexToMaterialIndex)
|
|
{
|
|
int32 MatIndexToInsert = 0;
|
|
for (MatIndexToInsert = 0; MatIndexToInsert < MaterialIndexToNameIndex.Num(); ++MatIndexToInsert)
|
|
{
|
|
if (*(NameIndexToMaterialIndex.Find(MaterialIndexToNameIndex[MatIndexToInsert])) >= kvp.Value)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
MaterialIndexToNameIndex.Insert(kvp.Key, MatIndexToInsert);
|
|
}
|
|
|
|
if (bNeedsReorder)
|
|
{
|
|
// re-order the materials
|
|
TArray< SkeletalMeshImportData::FMaterial > ExistingMatList = ImportData.Materials;
|
|
|
|
for (int32 MaterialIndex = 0; MaterialIndex < MaterialCount; ++MaterialIndex)
|
|
{
|
|
if (MaterialIndex < MaterialIndexToNameIndex.Num())
|
|
{
|
|
int32 NewIndex = MaterialIndexToNameIndex[MaterialIndex];
|
|
if (ExistingMatList.IsValidIndex(NewIndex))
|
|
{
|
|
ImportData.Materials[NewIndex] = ExistingMatList[MaterialIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
// remapping the material index for each triangle
|
|
int32 FaceNum = ImportData.Faces.Num();
|
|
for (int32 TriangleIndex = 0; TriangleIndex < FaceNum; TriangleIndex++)
|
|
{
|
|
SkeletalMeshImportData::FTriangle& Triangle = ImportData.Faces[TriangleIndex];
|
|
if (Triangle.MatIndex < MaterialIndexToNameIndex.Num())
|
|
{
|
|
Triangle.MatIndex = MaterialIndexToNameIndex[Triangle.MatIndex];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::CleanUpUnusedMaterials(FSkeletalMeshImportData& ImportData)
|
|
{
|
|
if (ImportData.Materials.Num() <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
TArray< SkeletalMeshImportData::FMaterial > ExistingMatList = ImportData.Materials;
|
|
|
|
TArray<uint8> UsedMaterialIndex;
|
|
// Find all material that are use by the mesh faces
|
|
int32 FaceNum = ImportData.Faces.Num();
|
|
for (int32 TriangleIndex = 0; TriangleIndex < FaceNum; TriangleIndex++)
|
|
{
|
|
SkeletalMeshImportData::FTriangle& Triangle = ImportData.Faces[TriangleIndex];
|
|
UsedMaterialIndex.AddUnique(Triangle.MatIndex);
|
|
}
|
|
//Remove any unused material.
|
|
if (UsedMaterialIndex.Num() < ExistingMatList.Num())
|
|
{
|
|
TArray<int32> RemapIndex;
|
|
TArray< SkeletalMeshImportData::FMaterial > &NewMatList = ImportData.Materials;
|
|
NewMatList.Empty();
|
|
for (int32 ExistingMatIndex = 0; ExistingMatIndex < ExistingMatList.Num(); ++ExistingMatIndex)
|
|
{
|
|
if (UsedMaterialIndex.Contains((uint8)ExistingMatIndex))
|
|
{
|
|
RemapIndex.Add(NewMatList.Add(ExistingMatList[ExistingMatIndex]));
|
|
}
|
|
else
|
|
{
|
|
RemapIndex.Add(INDEX_NONE);
|
|
}
|
|
}
|
|
ImportData.MaxMaterialIndex = 0;
|
|
//Remap the face material index
|
|
for (int32 TriangleIndex = 0; TriangleIndex < FaceNum; TriangleIndex++)
|
|
{
|
|
SkeletalMeshImportData::FTriangle& Triangle = ImportData.Faces[TriangleIndex];
|
|
check(RemapIndex[Triangle.MatIndex] != INDEX_NONE);
|
|
Triangle.MatIndex = RemapIndex[Triangle.MatIndex];
|
|
ImportData.MaxMaterialIndex = FMath::Max<uint32>(ImportData.MaxMaterialIndex, Triangle.MatIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UnFbx::FFbxImporter::FillSkelMeshImporterFromFbx( FSkeletalMeshImportData& ImportData, FbxMesh*& Mesh, FbxSkin* Skin, FbxShape* FbxShape, TArray<FbxNode*> &SortedLinks, const TArray<FbxSurfaceMaterial*>& FbxMaterials, FbxNode *RootNode, const TMap<FVector3f, FColor>& ExistingVertexColorData)
|
|
{
|
|
FbxNode* Node = Mesh->GetNode();
|
|
|
|
//remove the bad polygons before getting any data from mesh
|
|
Mesh->RemoveBadPolygons();
|
|
|
|
//Get the base layer of the mesh
|
|
FbxLayer* BaseLayer = Mesh->GetLayer(0);
|
|
if (BaseLayer == NULL)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, LOCTEXT("FbxSkeletaLMeshimport_NoGeometry", "There is no geometry information in mesh")), FFbxErrors::Generic_Mesh_NoGeometry);
|
|
return false;
|
|
}
|
|
|
|
// Do some checks before proceeding, check to make sure the number of bones does not exceed the maximum supported
|
|
if(SortedLinks.Num() > MAX_BONES)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_ExceedsMaxBoneCount", "'{0}' mesh has '{1}' bones which exceeds the maximum allowed bone count of {2}."), FText::FromString(Node->GetName()), FText::AsNumber(SortedLinks.Num()), FText::AsNumber(MAX_BONES))), FFbxErrors::SkeletalMesh_ExceedsMaxBoneCount);
|
|
return false;
|
|
}
|
|
|
|
//
|
|
// store the UVs in arrays for fast access in the later looping of triangles
|
|
//
|
|
// mapping from UVSets to Fbx LayerElementUV
|
|
// Fbx UVSets may be duplicated, remove the duplicated UVSets in the mapping
|
|
int32 LayerCount = Mesh->GetLayerCount();
|
|
TArray<FString> UVSets;
|
|
|
|
UVSets.Empty();
|
|
if (LayerCount > 0)
|
|
{
|
|
int32 UVLayerIndex;
|
|
for (UVLayerIndex = 0; UVLayerIndex<LayerCount; UVLayerIndex++)
|
|
{
|
|
FbxLayer* lLayer = Mesh->GetLayer(UVLayerIndex);
|
|
int32 UVSetCount = lLayer->GetUVSetCount();
|
|
if(UVSetCount)
|
|
{
|
|
FbxArray<FbxLayerElementUV const*> EleUVs = lLayer->GetUVSets();
|
|
for (int32 UVIndex = 0; UVIndex<UVSetCount; UVIndex++)
|
|
{
|
|
FbxLayerElementUV const* ElementUV = EleUVs[UVIndex];
|
|
if (ElementUV)
|
|
{
|
|
const char* UVSetName = ElementUV->GetName();
|
|
FString LocalUVSetName = UTF8_TO_TCHAR(UVSetName);
|
|
if (LocalUVSetName.IsEmpty())
|
|
{
|
|
LocalUVSetName = TEXT("UVmap_") + FString::FromInt(UVLayerIndex);
|
|
}
|
|
UVSets.AddUnique(LocalUVSetName);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the the UV sets are named using the following format (UVChannel_X; where X ranges from 1 to 4)
|
|
// we will re-order them based on these names. Any UV sets that do not follow this naming convention
|
|
// will be slotted into available spaces.
|
|
if( UVSets.Num() > 0 )
|
|
{
|
|
for(int32 ChannelNumIdx = 0; ChannelNumIdx < 4; ChannelNumIdx++)
|
|
{
|
|
FString ChannelName = FString::Printf( TEXT("UVChannel_%d"), ChannelNumIdx+1 );
|
|
int32 SetIdx = UVSets.Find( ChannelName );
|
|
|
|
// If the specially formatted UVSet name appears in the list and it is in the wrong spot,
|
|
// we will swap it into the correct spot.
|
|
if( SetIdx != INDEX_NONE && SetIdx != ChannelNumIdx )
|
|
{
|
|
// If we are going to swap to a position that is outside the bounds of the
|
|
// array, then we pad out to that spot with empty data.
|
|
for(int32 ArrSize = UVSets.Num(); ArrSize < ChannelNumIdx+1; ArrSize++)
|
|
{
|
|
UVSets.Add( FString(TEXT("")) );
|
|
}
|
|
//Swap the entry into the appropriate spot.
|
|
UVSets.Swap( SetIdx, ChannelNumIdx );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
TArray<UMaterialInterface*> Materials;
|
|
const bool bForSkeletalMesh = true;
|
|
|
|
FindOrImportMaterialsFromNode(Node, Materials, UVSets, bForSkeletalMesh);
|
|
if (!ImportOptions->bImportMaterials && ImportOptions->bImportTextures)
|
|
{
|
|
//If we are not importing any new material, we might still want to import new textures.
|
|
ImportTexturesFromNode(Node);
|
|
}
|
|
|
|
// Maps local mesh material index to global material index
|
|
const int32 MaterialCount = Node->GetMaterialCount();
|
|
TArray<int32> MaterialMapping;
|
|
MaterialMapping.AddUninitialized(MaterialCount);
|
|
|
|
for( int32 MaterialIndex = 0; MaterialIndex < MaterialCount; ++MaterialIndex )
|
|
{
|
|
FbxSurfaceMaterial* FbxMaterial = Node->GetMaterial( MaterialIndex );
|
|
|
|
int32 ExistingMatIndex = INDEX_NONE;
|
|
|
|
// Default behavior finds the first material in FbxMaterials that matches the node's materials.
|
|
// This results in combining nodes/sections that use the same material.
|
|
if (!ImportOptions->bKeepSectionsSeparate)
|
|
{
|
|
FbxMaterials.Find(FbxMaterial, ExistingMatIndex);
|
|
}
|
|
else
|
|
{
|
|
// Otherwise find the material in FbxMaterials specific to that node's section/surface
|
|
for (int32 FbxMatIndex = 0; FbxMatIndex < FbxMaterials.Num(); FbxMatIndex++)
|
|
{
|
|
FString NodeMaterialIndexName = MakeNodeMaterialIndexName(Node, MaterialIndex);
|
|
bool MaterialImportNameMatches = ImportData.Materials[FbxMatIndex].MaterialImportName == NodeMaterialIndexName;
|
|
bool MaterialInitialNameMatches = FbxMaterials[FbxMatIndex]->GetInitialName() == FbxMaterial->GetInitialName();
|
|
|
|
if (MaterialImportNameMatches && MaterialInitialNameMatches)
|
|
{
|
|
ExistingMatIndex = FbxMatIndex;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (ExistingMatIndex != INDEX_NONE)
|
|
{
|
|
// Reuse existing material
|
|
MaterialMapping[MaterialIndex] = ExistingMatIndex;
|
|
|
|
if (!ImportOptions->bKeepSectionsSeparate)
|
|
{
|
|
if (Materials.IsValidIndex(MaterialIndex))
|
|
{
|
|
ImportData.Materials[ExistingMatIndex].Material = Materials[MaterialIndex];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (Materials.IsValidIndex(MaterialIndex) && ImportData.Materials.IsValidIndex(ExistingMatIndex))
|
|
{
|
|
ImportData.Materials[ExistingMatIndex].MaterialImportName = MakeNodeMaterialIndexName(Node, MaterialIndex);
|
|
ImportData.Materials[ExistingMatIndex].Material = Materials[MaterialIndex];
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
MaterialMapping[MaterialIndex] = 0;
|
|
}
|
|
}
|
|
|
|
if( LayerCount > 0 && ImportOptions->bPreserveSmoothingGroups )
|
|
{
|
|
// Check and see if the smooothing data is valid. If not generate it from the normals
|
|
BaseLayer = Mesh->GetLayer(0);
|
|
if( BaseLayer )
|
|
{
|
|
const FbxLayerElementSmoothing* SmoothingLayer = BaseLayer->GetSmoothing();
|
|
|
|
if( SmoothingLayer )
|
|
{
|
|
bool bValidSmoothingData = false;
|
|
FbxLayerElementArrayTemplate<int32>& Array = SmoothingLayer->GetDirectArray();
|
|
for( int32 SmoothingIndex = 0; SmoothingIndex < Array.GetCount(); ++SmoothingIndex )
|
|
{
|
|
if( Array[SmoothingIndex] != 0 )
|
|
{
|
|
bValidSmoothingData = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( !bValidSmoothingData && Mesh->GetPolygonVertexCount() > 0 )
|
|
{
|
|
GeometryConverter->ComputeEdgeSmoothingFromNormals(Mesh);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Must do this before triangulating the mesh due to an FBX bug in TriangulateMeshAdvance
|
|
int32 LayerSmoothingCount = Mesh->GetLayerCount(FbxLayerElement::eSmoothing);
|
|
for(int32 i = 0; i < LayerSmoothingCount; i++)
|
|
{
|
|
FbxLayerElementSmoothing const* SmoothingInfo = Mesh->GetLayer(i)->GetSmoothing();
|
|
if (SmoothingInfo && SmoothingInfo->GetMappingMode() != FbxLayerElement::eByPolygon)
|
|
{
|
|
GeometryConverter->ComputePolygonSmoothingFromEdgeSmoothing(Mesh, i);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Convert data format to unreal-compatible
|
|
//
|
|
|
|
if (!Mesh->IsTriangleMesh())
|
|
{
|
|
UE_LOG(LogFbx, Log, TEXT("Triangulating skeletal mesh %s"), UTF8_TO_TCHAR(Node->GetName()));
|
|
|
|
const bool bReplace = true;
|
|
FbxNodeAttribute* ConvertedNode = GeometryConverter->Triangulate(Mesh, bReplace);
|
|
if( ConvertedNode != NULL && ConvertedNode->GetAttributeType() == FbxNodeAttribute::eMesh )
|
|
{
|
|
Mesh = ConvertedNode->GetNode()->GetMesh();
|
|
}
|
|
else
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_TriangulatingFailed", "Unable to triangulate mesh '{0}'. Check detail for Ouput Log."), FText::FromString(Node->GetName()))), FFbxErrors::Generic_Mesh_TriangulationFailed);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// renew the base layer
|
|
BaseLayer = Mesh->GetLayer(0);
|
|
Skin = (FbxSkin*)static_cast<FbxGeometry*>(Mesh)->GetDeformer(0, FbxDeformer::eSkin);
|
|
|
|
//
|
|
// store the UVs in arrays for fast access in the later looping of triangles
|
|
//
|
|
uint32 UniqueUVCount = UVSets.Num();
|
|
FbxLayerElementUV** LayerElementUV = NULL;
|
|
FbxLayerElement::EReferenceMode* UVReferenceMode = NULL;
|
|
FbxLayerElement::EMappingMode* UVMappingMode = NULL;
|
|
if (UniqueUVCount > 0)
|
|
{
|
|
LayerElementUV = new FbxLayerElementUV*[UniqueUVCount];
|
|
UVReferenceMode = new FbxLayerElement::EReferenceMode[UniqueUVCount];
|
|
UVMappingMode = new FbxLayerElement::EMappingMode[UniqueUVCount];
|
|
}
|
|
else
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_NoUVSet", "Mesh '{0}' has no UV set. Creating a default set."), FText::FromString(Node->GetName()))), FFbxErrors::SkeletalMesh_NoUVSet);
|
|
}
|
|
|
|
LayerCount = Mesh->GetLayerCount();
|
|
for (uint32 UVIndex = 0; UVIndex < UniqueUVCount; UVIndex++)
|
|
{
|
|
LayerElementUV[UVIndex] = NULL;
|
|
for (int32 UVLayerIndex = 0; UVLayerIndex<LayerCount; UVLayerIndex++)
|
|
{
|
|
FbxLayer* lLayer = Mesh->GetLayer(UVLayerIndex);
|
|
int32 UVSetCount = lLayer->GetUVSetCount();
|
|
if(UVSetCount)
|
|
{
|
|
FbxArray<FbxLayerElementUV const*> EleUVs = lLayer->GetUVSets();
|
|
for (int32 FbxUVIndex = 0; FbxUVIndex<UVSetCount; FbxUVIndex++)
|
|
{
|
|
FbxLayerElementUV const* ElementUV = EleUVs[FbxUVIndex];
|
|
if (ElementUV)
|
|
{
|
|
const char* UVSetName = ElementUV->GetName();
|
|
FString LocalUVSetName = UTF8_TO_TCHAR(UVSetName);
|
|
if (LocalUVSetName.IsEmpty())
|
|
{
|
|
LocalUVSetName = TEXT("UVmap_") + FString::FromInt(UVLayerIndex);
|
|
}
|
|
if (LocalUVSetName == UVSets[UVIndex])
|
|
{
|
|
LayerElementUV[UVIndex] = const_cast<FbxLayerElementUV*>(ElementUV);
|
|
UVReferenceMode[UVIndex] = LayerElementUV[UVIndex]->GetReferenceMode();
|
|
UVMappingMode[UVIndex] = LayerElementUV[UVIndex]->GetMappingMode();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// get the smoothing group layer
|
|
//
|
|
bool bSmoothingAvailable = false;
|
|
|
|
FbxLayerElementSmoothing const* SmoothingInfo = BaseLayer->GetSmoothing();
|
|
FbxLayerElement::EReferenceMode SmoothingReferenceMode(FbxLayerElement::eDirect);
|
|
FbxLayerElement::EMappingMode SmoothingMappingMode(FbxLayerElement::eByEdge);
|
|
if (SmoothingInfo)
|
|
{
|
|
if( SmoothingInfo->GetMappingMode() == FbxLayerElement::eByEdge )
|
|
{
|
|
if (!GeometryConverter->ComputePolygonSmoothingFromEdgeSmoothing(Mesh))
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_ConvertSmoothingGroupFailed", "Unable to fully convert the smoothing groups for mesh '{0}'"), FText::FromString(Mesh->GetName()))), FFbxErrors::Generic_Mesh_ConvertSmoothingGroupFailed);
|
|
bSmoothingAvailable = false;
|
|
}
|
|
else
|
|
{
|
|
//After using the geometry converter we always have to get the Layer and the smoothing info
|
|
BaseLayer = Mesh->GetLayer(0);
|
|
SmoothingInfo = BaseLayer->GetSmoothing();
|
|
}
|
|
}
|
|
|
|
if( SmoothingInfo->GetMappingMode() == FbxLayerElement::eByPolygon )
|
|
{
|
|
bSmoothingAvailable = true;
|
|
}
|
|
|
|
|
|
SmoothingReferenceMode = SmoothingInfo->GetReferenceMode();
|
|
SmoothingMappingMode = SmoothingInfo->GetMappingMode();
|
|
}
|
|
|
|
|
|
//
|
|
// get the "material index" layer
|
|
//
|
|
FbxLayerElementMaterial* LayerElementMaterial = BaseLayer->GetMaterials();
|
|
FbxLayerElement::EMappingMode MaterialMappingMode = LayerElementMaterial ?
|
|
LayerElementMaterial->GetMappingMode() : FbxLayerElement::eByPolygon;
|
|
|
|
UniqueUVCount = FMath::Min<uint32>( UniqueUVCount, MAX_TEXCOORDS );
|
|
|
|
// One UV set is required but only import up to MAX_TEXCOORDS number of uv layers
|
|
ImportData.NumTexCoords = FMath::Max<uint32>( ImportData.NumTexCoords, UniqueUVCount );
|
|
|
|
//
|
|
// get the first vertex color layer
|
|
//
|
|
|
|
FbxLayerElementVertexColor* LayerElementVertexColor = BaseLayer->GetVertexColors();
|
|
FbxLayerElement::EReferenceMode VertexColorReferenceMode(FbxLayerElement::eDirect);
|
|
FbxLayerElement::EMappingMode VertexColorMappingMode(FbxLayerElement::eByControlPoint);
|
|
if (LayerElementVertexColor && ImportOptions->VertexColorImportOption == EVertexColorImportOption::Replace)
|
|
{
|
|
VertexColorReferenceMode = LayerElementVertexColor->GetReferenceMode();
|
|
VertexColorMappingMode = LayerElementVertexColor->GetMappingMode();
|
|
ImportData.bHasVertexColors = true;
|
|
}
|
|
else if(ImportOptions->VertexColorImportOption == EVertexColorImportOption::Override)
|
|
{
|
|
ImportData.bHasVertexColors = true;
|
|
}
|
|
else if (ImportOptions->VertexColorImportOption == EVertexColorImportOption::Ignore && ExistingVertexColorData.Num() != 0)
|
|
{
|
|
ImportData.bHasVertexColors = true;
|
|
}
|
|
|
|
//
|
|
// get the first normal layer
|
|
//
|
|
FbxLayerElementNormal* LayerElementNormal = BaseLayer->GetNormals();
|
|
FbxLayerElementTangent* LayerElementTangent = BaseLayer->GetTangents();
|
|
FbxLayerElementBinormal* LayerElementBinormal = BaseLayer->GetBinormals();
|
|
|
|
//whether there is normal, tangent and binormal data in this mesh
|
|
bool bHasNormalInformation = LayerElementNormal != NULL;
|
|
bool bHasTangentInformation = LayerElementTangent != NULL && LayerElementBinormal != NULL;
|
|
|
|
ImportData.bHasNormals = bHasNormalInformation;
|
|
ImportData.bHasTangents = bHasTangentInformation;
|
|
|
|
FbxLayerElement::EReferenceMode NormalReferenceMode(FbxLayerElement::eDirect);
|
|
FbxLayerElement::EMappingMode NormalMappingMode(FbxLayerElement::eByControlPoint);
|
|
if (LayerElementNormal)
|
|
{
|
|
NormalReferenceMode = LayerElementNormal->GetReferenceMode();
|
|
NormalMappingMode = LayerElementNormal->GetMappingMode();
|
|
}
|
|
|
|
FbxLayerElement::EReferenceMode TangentReferenceMode(FbxLayerElement::eDirect);
|
|
FbxLayerElement::EMappingMode TangentMappingMode(FbxLayerElement::eByControlPoint);
|
|
if (LayerElementTangent)
|
|
{
|
|
TangentReferenceMode = LayerElementTangent->GetReferenceMode();
|
|
TangentMappingMode = LayerElementTangent->GetMappingMode();
|
|
}
|
|
|
|
FbxLayerElement::EReferenceMode BinormalReferenceMode(FbxLayerElement::eDirect);
|
|
FbxLayerElement::EMappingMode BinormalMappingMode(FbxLayerElement::eByControlPoint);
|
|
if (LayerElementBinormal)
|
|
{
|
|
BinormalReferenceMode = LayerElementBinormal->GetReferenceMode();
|
|
BinormalMappingMode = LayerElementBinormal->GetMappingMode();
|
|
}
|
|
|
|
//
|
|
// create the points / wedges / faces
|
|
//
|
|
int32 ControlPointsCount = Mesh->GetControlPointsCount();
|
|
int32 ExistPointNum = ImportData.Points.Num();
|
|
FillSkeletalMeshImportPoints( &ImportData, RootNode, Node, FbxShape );
|
|
|
|
// Construct the matrices for the conversion from right handed to left handed system
|
|
FbxAMatrix TotalMatrix;
|
|
FbxAMatrix TotalMatrixForNormal;
|
|
TotalMatrix = ComputeSkeletalMeshTotalMatrix(Node, RootNode);
|
|
TotalMatrixForNormal = TotalMatrix.Inverse();
|
|
TotalMatrixForNormal = TotalMatrixForNormal.Transpose();
|
|
|
|
bool OddNegativeScale = IsOddNegativeScale(TotalMatrix);
|
|
|
|
int32 TriangleCount = Mesh->GetPolygonCount();
|
|
int32 ExistFaceNum = ImportData.Faces.Num();
|
|
ImportData.Faces.AddUninitialized( TriangleCount );
|
|
int32 ExistWedgesNum = ImportData.Wedges.Num();
|
|
SkeletalMeshImportData::FVertex TmpWedges[3];
|
|
|
|
bool bUnsupportedSmoothingGroupErrorDisplayed = false;
|
|
bool bFaceMaterialIndexInconsistencyErrorDisplayed = false;
|
|
for( int32 TriangleIndex = ExistFaceNum, LocalIndex = 0 ; TriangleIndex < ExistFaceNum+TriangleCount ; TriangleIndex++, LocalIndex++ )
|
|
{
|
|
|
|
SkeletalMeshImportData::FTriangle& Triangle = ImportData.Faces[TriangleIndex];
|
|
|
|
//
|
|
// smoothing mask
|
|
//
|
|
// set the face smoothing by default. It could be any number, but not zero
|
|
Triangle.SmoothingGroups = 255;
|
|
if ( bSmoothingAvailable)
|
|
{
|
|
if (SmoothingInfo)
|
|
{
|
|
if (SmoothingMappingMode == FbxLayerElement::eByPolygon)
|
|
{
|
|
int32 lSmoothingIndex = (SmoothingReferenceMode == FbxLayerElement::eDirect) ? LocalIndex : SmoothingInfo->GetIndexArray().GetAt(LocalIndex);
|
|
Triangle.SmoothingGroups = SmoothingInfo->GetDirectArray().GetAt(lSmoothingIndex);
|
|
}
|
|
else if(!bUnsupportedSmoothingGroupErrorDisplayed)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FbxSkeletaLMeshimport_Unsupportingsmoothinggroup", "Unsupported Smoothing group mapping mode on mesh '{0}'"), FText::FromString(Mesh->GetName()))), FFbxErrors::Generic_Mesh_UnsupportingSmoothingGroup);
|
|
bUnsupportedSmoothingGroupErrorDisplayed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int32 VertexIndex=0; VertexIndex<3; VertexIndex++)
|
|
{
|
|
// If there are odd number negative scale, invert the vertex order for triangles
|
|
int32 UnrealVertexIndex = OddNegativeScale ? 2 - VertexIndex : VertexIndex;
|
|
|
|
int32 ControlPointIndex = Mesh->GetPolygonVertex(LocalIndex, VertexIndex);
|
|
//
|
|
// normals, tangents and binormals
|
|
//
|
|
if( bHasNormalInformation )
|
|
{
|
|
const int32 TmpIndex = LocalIndex*3 + VertexIndex;
|
|
// normals may have different reference and mapping mode than tangents and binormals
|
|
const int32 NormalMapIndex = (NormalMappingMode == FbxLayerElement::eByControlPoint) ?
|
|
ControlPointIndex : TmpIndex;
|
|
const int32 NormalValueIndex = (NormalReferenceMode == FbxLayerElement::eDirect) ?
|
|
NormalMapIndex : LayerElementNormal->GetIndexArray().GetAt(NormalMapIndex);
|
|
|
|
// tangents and binormals share the same reference, mapping mode and index array
|
|
FbxVector4 TempValue;
|
|
|
|
if( bHasTangentInformation )
|
|
{
|
|
const int32 TangentMapIndex = (TangentMappingMode == FbxLayerElement::eByControlPoint) ? ControlPointIndex : TmpIndex;
|
|
const int32 TangentValueIndex = (TangentReferenceMode == FbxLayerElement::eDirect) ? TangentMapIndex : LayerElementTangent->GetIndexArray().GetAt(TangentMapIndex);
|
|
TempValue = LayerElementTangent->GetDirectArray().GetAt(TangentValueIndex);
|
|
TempValue = TotalMatrixForNormal.MultT(TempValue);
|
|
Triangle.TangentX[ UnrealVertexIndex ] = (FVector3f)Converter.ConvertDir(TempValue);
|
|
Triangle.TangentX[ UnrealVertexIndex ].Normalize();
|
|
|
|
const int32 BinormalMapIndex = (BinormalMappingMode == FbxLayerElement::eByControlPoint) ? ControlPointIndex : TmpIndex;
|
|
const int32 BinormalValueIndex = (BinormalReferenceMode == FbxLayerElement::eDirect) ? BinormalMapIndex : LayerElementBinormal->GetIndexArray().GetAt(BinormalMapIndex);
|
|
TempValue = LayerElementBinormal->GetDirectArray().GetAt(BinormalValueIndex);
|
|
TempValue = TotalMatrixForNormal.MultT(TempValue);
|
|
Triangle.TangentY[ UnrealVertexIndex ] = (FVector3f)-Converter.ConvertDir(TempValue);
|
|
Triangle.TangentY[ UnrealVertexIndex ].Normalize();
|
|
}
|
|
|
|
TempValue = LayerElementNormal->GetDirectArray().GetAt(NormalValueIndex);
|
|
TempValue = TotalMatrixForNormal.MultT(TempValue);
|
|
Triangle.TangentZ[ UnrealVertexIndex ] = (FVector3f)Converter.ConvertDir(TempValue);
|
|
Triangle.TangentZ[ UnrealVertexIndex ].Normalize();
|
|
|
|
}
|
|
else
|
|
{
|
|
for( int32 NormalIndex = 0; NormalIndex < 3; ++NormalIndex )
|
|
{
|
|
Triangle.TangentX[ NormalIndex ] = FVector3f::ZeroVector;
|
|
Triangle.TangentY[ NormalIndex ] = FVector3f::ZeroVector;
|
|
Triangle.TangentZ[ NormalIndex ] = FVector3f::ZeroVector;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// material index
|
|
//
|
|
Triangle.MatIndex = 0; // default value
|
|
if (MaterialCount>0)
|
|
{
|
|
if (LayerElementMaterial)
|
|
{
|
|
switch(MaterialMappingMode)
|
|
{
|
|
// material index is stored in the IndexArray, not the DirectArray (which is irrelevant with 2009.1)
|
|
case FbxLayerElement::eAllSame:
|
|
{
|
|
Triangle.MatIndex = MaterialMapping[ LayerElementMaterial->GetIndexArray().GetAt(0) ];
|
|
}
|
|
break;
|
|
case FbxLayerElement::eByPolygon:
|
|
{
|
|
int32 Index = LayerElementMaterial->GetIndexArray().GetAt(LocalIndex);
|
|
if (!MaterialMapping.IsValidIndex(Index))
|
|
{
|
|
if (!bFaceMaterialIndexInconsistencyErrorDisplayed)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("FbxSkeletaLMeshimport_MaterialIndexInconsistency", "Face material index inconsistency - forcing to 0")), FFbxErrors::Generic_Mesh_MaterialIndexInconsistency);
|
|
bFaceMaterialIndexInconsistencyErrorDisplayed = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Triangle.MatIndex = MaterialMapping[Index];
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
// When import morph, we don't check the material index
|
|
// because we don't import material for morph, so the ImportData.Materials contains zero material
|
|
if ( !FbxShape && (Triangle.MatIndex < 0 || Triangle.MatIndex >= FbxMaterials.Num() ) )
|
|
{
|
|
if (!bFaceMaterialIndexInconsistencyErrorDisplayed)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("FbxSkeletaLMeshimport_MaterialIndexInconsistency", "Face material index inconsistency - forcing to 0")), FFbxErrors::Generic_Mesh_MaterialIndexInconsistency);
|
|
bFaceMaterialIndexInconsistencyErrorDisplayed = true;
|
|
}
|
|
Triangle.MatIndex = 0;
|
|
}
|
|
}
|
|
ImportData.MaxMaterialIndex = FMath::Max<uint32>( ImportData.MaxMaterialIndex, Triangle.MatIndex );
|
|
|
|
Triangle.AuxMatIndex = 0;
|
|
for (int32 VertexIndex=0; VertexIndex<3; VertexIndex++)
|
|
{
|
|
// If there are odd number negative scale, invert the vertex order for triangles
|
|
int32 UnrealVertexIndex = OddNegativeScale ? 2 - VertexIndex : VertexIndex;
|
|
|
|
TmpWedges[UnrealVertexIndex].MatIndex = Triangle.MatIndex;
|
|
TmpWedges[UnrealVertexIndex].VertexIndex = ExistPointNum + Mesh->GetPolygonVertex(LocalIndex,VertexIndex);
|
|
// Initialize all colors to white.
|
|
TmpWedges[UnrealVertexIndex].Color = FColor::White;
|
|
}
|
|
|
|
//
|
|
// uvs
|
|
//
|
|
uint32 UVLayerIndex;
|
|
// Some FBX meshes can have no UV sets, so also check the UniqueUVCount
|
|
for ( UVLayerIndex = 0; UVLayerIndex< UniqueUVCount; UVLayerIndex++ )
|
|
{
|
|
// ensure the layer has data
|
|
if (LayerElementUV[UVLayerIndex] != NULL)
|
|
{
|
|
// Get each UV from the layer
|
|
for (int32 VertexIndex=0;VertexIndex<3;VertexIndex++)
|
|
{
|
|
// If there are odd number negative scale, invert the vertex order for triangles
|
|
int32 UnrealVertexIndex = OddNegativeScale ? 2 - VertexIndex : VertexIndex;
|
|
|
|
int32 lControlPointIndex = Mesh->GetPolygonVertex(LocalIndex, VertexIndex);
|
|
int32 UVMapIndex = (UVMappingMode[UVLayerIndex] == FbxLayerElement::eByControlPoint) ?
|
|
lControlPointIndex : LocalIndex*3+VertexIndex;
|
|
int32 UVIndex = (UVReferenceMode[UVLayerIndex] == FbxLayerElement::eDirect) ?
|
|
UVMapIndex : LayerElementUV[UVLayerIndex]->GetIndexArray().GetAt(UVMapIndex);
|
|
FbxVector2 UVVector = LayerElementUV[UVLayerIndex]->GetDirectArray().GetAt(UVIndex);
|
|
|
|
TmpWedges[UnrealVertexIndex].UVs[ UVLayerIndex ].X = static_cast<float>(UVVector[0]);
|
|
TmpWedges[UnrealVertexIndex].UVs[ UVLayerIndex ].Y = 1.f - static_cast<float>(UVVector[1]);
|
|
}
|
|
}
|
|
else if( UVLayerIndex == 0 )
|
|
{
|
|
// Set all UV's to zero. If we are here the mesh had no UV sets so we only need to do this for the
|
|
// first UV set which always exists.
|
|
|
|
for (int32 VertexIndex=0; VertexIndex<3; VertexIndex++)
|
|
{
|
|
TmpWedges[VertexIndex].UVs[UVLayerIndex].X = 0.0f;
|
|
TmpWedges[VertexIndex].UVs[UVLayerIndex].Y = 0.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read vertex colors if they exist.
|
|
if( LayerElementVertexColor && ImportOptions->VertexColorImportOption == EVertexColorImportOption::Replace)
|
|
{
|
|
auto ConvertFbxColorToFColor = [](const FbxColor& VertexColor)
|
|
{
|
|
// Clamp manually to avoid wrapping around because of truncation for values outside of the 0.0 to 1.0 range
|
|
return FColor(uint8(255.f * FMath::Clamp(VertexColor.mRed, 0.0, 1.0)),
|
|
uint8(255.f * FMath::Clamp(VertexColor.mGreen, 0.0, 1.0)),
|
|
uint8(255.f * FMath::Clamp(VertexColor.mBlue, 0.0, 1.0)),
|
|
uint8(255.f * FMath::Clamp(VertexColor.mAlpha, 0.0, 1.0)));
|
|
};
|
|
|
|
switch(VertexColorMappingMode)
|
|
{
|
|
case FbxLayerElement::eByControlPoint:
|
|
{
|
|
for (int32 VertexIndex=0;VertexIndex<3;VertexIndex++)
|
|
{
|
|
int32 UnrealVertexIndex = OddNegativeScale ? 2 - VertexIndex : VertexIndex;
|
|
|
|
FbxColor VertexColor = (VertexColorReferenceMode == FbxLayerElement::eDirect)
|
|
? LayerElementVertexColor->GetDirectArray().GetAt(Mesh->GetPolygonVertex(LocalIndex,VertexIndex))
|
|
: LayerElementVertexColor->GetDirectArray().GetAt(LayerElementVertexColor->GetIndexArray().GetAt(Mesh->GetPolygonVertex(LocalIndex,VertexIndex)));
|
|
|
|
|
|
TmpWedges[UnrealVertexIndex].Color = ConvertFbxColorToFColor(VertexColor);
|
|
}
|
|
}
|
|
break;
|
|
case FbxLayerElement::eByPolygonVertex:
|
|
{
|
|
for (int32 VertexIndex=0;VertexIndex<3;VertexIndex++)
|
|
{
|
|
int32 UnrealVertexIndex = OddNegativeScale ? 2 - VertexIndex : VertexIndex;
|
|
|
|
FbxColor VertexColor = (VertexColorReferenceMode == FbxLayerElement::eDirect)
|
|
? LayerElementVertexColor->GetDirectArray().GetAt(LocalIndex*3+VertexIndex)
|
|
: LayerElementVertexColor->GetDirectArray().GetAt(LayerElementVertexColor->GetIndexArray().GetAt(LocalIndex*3+VertexIndex));
|
|
|
|
TmpWedges[UnrealVertexIndex].Color = ConvertFbxColorToFColor(VertexColor);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
else if (ImportOptions->VertexColorImportOption == EVertexColorImportOption::Override)
|
|
{
|
|
for (int32 VertexIndex = 0; VertexIndex < 3; VertexIndex++)
|
|
{
|
|
int32 UnrealVertexIndex = OddNegativeScale ? 2 - VertexIndex : VertexIndex;
|
|
TmpWedges[UnrealVertexIndex].Color = ImportOptions->VertexOverrideColor;
|
|
}
|
|
}
|
|
else if (ImportOptions->VertexColorImportOption == EVertexColorImportOption::Ignore && ImportData.bHasVertexColors)
|
|
{
|
|
for (int32 VertexIndex = 0; VertexIndex < 3; VertexIndex++)
|
|
{
|
|
const FVector3f& VertexPosition = ImportData.Points[TmpWedges[VertexIndex].VertexIndex];
|
|
const FColor* PaintedColor = ExistingVertexColorData.Find(VertexPosition);
|
|
|
|
// try to match this wedge current vertex with one that existed in the previous mesh.
|
|
// This is a find in a tmap which uses a fast hash table lookup.
|
|
if (PaintedColor)
|
|
{
|
|
TmpWedges[VertexIndex].Color = *PaintedColor;
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// basic wedges matching : 3 unique per face. TODO Can we do better ?
|
|
//
|
|
for (int32 VertexIndex=0; VertexIndex<3; VertexIndex++)
|
|
{
|
|
int32 w;
|
|
|
|
w = ImportData.Wedges.AddUninitialized();
|
|
ImportData.Wedges[w].VertexIndex = TmpWedges[VertexIndex].VertexIndex;
|
|
ImportData.Wedges[w].MatIndex = TmpWedges[VertexIndex].MatIndex;
|
|
ImportData.Wedges[w].Color = TmpWedges[VertexIndex].Color;
|
|
ImportData.Wedges[w].Reserved = 0;
|
|
FMemory::Memcpy( ImportData.Wedges[w].UVs, TmpWedges[VertexIndex].UVs, sizeof(FVector2f)*MAX_TEXCOORDS );
|
|
|
|
Triangle.WedgeIndex[VertexIndex] = w;
|
|
}
|
|
|
|
}
|
|
|
|
// now we can work on a per-cluster basis with good ordering
|
|
if (Skin) // skeletal mesh
|
|
{
|
|
// create influences for each cluster
|
|
for (int32 ClusterIndex = 0; ClusterIndex < Skin->GetClusterCount(); ClusterIndex++)
|
|
{
|
|
FbxCluster* Cluster = Skin->GetCluster(ClusterIndex);
|
|
// When Maya plug-in exports rigid binding, it will generate "CompensationCluster" for each ancestor links.
|
|
// FBX writes these "CompensationCluster" out. The CompensationCluster also has weight 1 for vertices.
|
|
// Unreal importer should skip these clusters.
|
|
if(!Cluster || (FCStringAnsi::Strcmp(Cluster->GetUserDataID(), "Maya_ClusterHint") == 0 && FCStringAnsi::Strcmp(Cluster->GetUserData(), "CompensationCluster") == 0))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FbxNode* Link = Cluster->GetLink();
|
|
// find the bone index
|
|
int32 BoneIndex = -1;
|
|
SortedLinks.Find(Link, BoneIndex);
|
|
|
|
// get the vertex indices
|
|
int32 ControlPointIndicesCount = Cluster->GetControlPointIndicesCount();
|
|
int32* ControlPointIndices = Cluster->GetControlPointIndices();
|
|
double* Weights = Cluster->GetControlPointWeights();
|
|
|
|
// for each vertex index in the cluster
|
|
for (int32 ControlPointIndex = 0; ControlPointIndex < ControlPointIndicesCount; ++ControlPointIndex)
|
|
{
|
|
ImportData.Influences.AddUninitialized();
|
|
ImportData.Influences.Last().BoneIndex = BoneIndex;
|
|
ImportData.Influences.Last().Weight = static_cast<float>(Weights[ControlPointIndex]);
|
|
ImportData.Influences.Last().VertexIndex = ExistPointNum + ControlPointIndices[ControlPointIndex];
|
|
}
|
|
}
|
|
}
|
|
else // for rigid mesh
|
|
{
|
|
// find the bone index, the bone is the node itself
|
|
int32 BoneIndex = -1;
|
|
SortedLinks.Find(Node, BoneIndex);
|
|
|
|
if (BoneIndex == -1 && ImportOptions->bImportAsSkeletalGeometry && SortedLinks.Num() > 0)
|
|
{
|
|
//When we import geometry only, we want to hook all the influence to a particular bone
|
|
BoneIndex = 0;
|
|
}
|
|
|
|
// for each vertex in the mesh
|
|
for (int32 ControlPointIndex = 0; ControlPointIndex < ControlPointsCount; ++ControlPointIndex)
|
|
{
|
|
ImportData.Influences.AddUninitialized();
|
|
ImportData.Influences.Last().BoneIndex = BoneIndex;
|
|
ImportData.Influences.Last().Weight = 1.0;
|
|
ImportData.Influences.Last().VertexIndex = ExistPointNum + ControlPointIndex;
|
|
}
|
|
}
|
|
|
|
//
|
|
// Get the vertex attribute layers from all layers, even the first layer which may be used as a vertex color layer.
|
|
// Currently we're only interested in alpha-only layers, since those are the only layer types the engine
|
|
// currently exposes for vertex attributes. Internally we can store 1-4 components, but there's no tooling for that
|
|
// 2-4 channels as of yet.
|
|
//
|
|
struct FNamedVertexAttribute : SkeletalMeshImportData::FVertexAttribute
|
|
{
|
|
FNamedVertexAttribute(FString&& InAttributeName, TArray<float>&& InAttributeValues, const int32 InComponentCount)
|
|
: FVertexAttribute(MoveTemp(InAttributeValues), InComponentCount)
|
|
, AttributeName(InAttributeName)
|
|
{}
|
|
|
|
FString AttributeName;
|
|
};
|
|
|
|
if (ImportOptions->bImportVertexAttributes)
|
|
{
|
|
TArray<FNamedVertexAttribute> NamedVertexAttributes;
|
|
|
|
for (int32 LayerIndex = 0; LayerIndex < LayerCount; LayerIndex++)
|
|
{
|
|
FbxLayerElementVertexColor* LayerElementVertexAttribute = Mesh->GetLayer(LayerIndex)->GetVertexColors();
|
|
if (!LayerElementVertexAttribute)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Check if this is an alpha-only attribute, by ensuring the RGB values are all zero, otherwise skip.
|
|
bool bIsValidAttribute = true;
|
|
const FbxLayerElementArrayTemplate<FbxColor>& AttributeValues = LayerElementVertexAttribute->GetDirectArray();
|
|
for (int32 Index = 0; Index < AttributeValues.GetCount(); Index++)
|
|
{
|
|
// We do an exact comparison, since that's how empty channels would be represented in the FBX file.
|
|
const FbxColor& Value = AttributeValues.GetAt(Index);
|
|
if (Value.mRed != 0.0 || Value.mGreen != 0.0 || Value.mBlue != 0.0)
|
|
{
|
|
bIsValidAttribute = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// We can only do attributes that are mapped per-vertex.
|
|
if (!bIsValidAttribute)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const int32 AttributeComponentCount = 1; // Number of component values per vertex. See comment above.
|
|
TArray<float> AttributeComponentValues;
|
|
|
|
switch(LayerElementVertexAttribute->GetMappingMode())
|
|
{
|
|
case FbxLayerElement::eByControlPoint:
|
|
{
|
|
AttributeComponentValues.AddZeroed(ControlPointsCount);
|
|
|
|
if (LayerElementVertexAttribute->GetReferenceMode() == FbxLayerElement::eDirect)
|
|
{
|
|
for (int32 Index = 0; Index < AttributeValues.GetCount(); Index++)
|
|
{
|
|
AttributeComponentValues[Index] = AttributeValues.GetAt(Index).mAlpha;
|
|
}
|
|
}
|
|
else // LayerElementVertexAttribute->GetReferenceMode() == FbxLayerElement::eIndexToDirect
|
|
{
|
|
const FbxLayerElementArrayTemplate<int>& IndexArray = LayerElementVertexAttribute->GetIndexArray();
|
|
for (int32 Index = 0; Index < IndexArray.GetCount(); Index++)
|
|
{
|
|
AttributeComponentValues[Index] = AttributeValues.GetAt(IndexArray[Index]).mAlpha;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case FbxLayerElement::eByPolygonVertex:
|
|
{
|
|
// Vertex attributes are stored per-vertex, not per-vertex instance. To work around this we average
|
|
// together values that share a vertex.
|
|
TArray<int32> SharedVertexCount;
|
|
SharedVertexCount.AddZeroed(ControlPointsCount);
|
|
AttributeComponentValues.AddZeroed(ControlPointsCount);
|
|
|
|
const FbxLayerElementArrayTemplate<int>* IndexArray = nullptr;
|
|
if (LayerElementVertexAttribute->GetReferenceMode() == FbxLayerElement::eIndexToDirect)
|
|
{
|
|
IndexArray = &LayerElementVertexAttribute->GetIndexArray();
|
|
}
|
|
|
|
const int* PolygonControlPointIndexes = Mesh->GetPolygonVertices();
|
|
|
|
for(int32 TriangleIndex = 0; TriangleIndex < TriangleCount; TriangleIndex++)
|
|
{
|
|
for (int32 InnerIndex = 0; InnerIndex < 3; InnerIndex++)
|
|
{
|
|
const int32 PolygonVertexIndex = TriangleIndex * 3 + InnerIndex;;
|
|
const int32 PointIndex = PolygonControlPointIndexes[PolygonVertexIndex];
|
|
|
|
AttributeComponentValues[PointIndex] +=
|
|
AttributeValues.GetAt(IndexArray ? IndexArray->GetAt(PolygonVertexIndex) : PolygonVertexIndex).mAlpha;
|
|
SharedVertexCount[PointIndex]++;
|
|
}
|
|
}
|
|
|
|
for (int32 PointIndex = 0; PointIndex < ControlPointsCount; PointIndex++)
|
|
{
|
|
if (SharedVertexCount[PointIndex] > 1)
|
|
{
|
|
AttributeComponentValues[PointIndex] /= static_cast<float>(SharedVertexCount[PointIndex]);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
if (!AttributeComponentValues.IsEmpty())
|
|
{
|
|
FString AttributeName(UTF8_TO_TCHAR(LayerElementVertexAttribute->GetName()));
|
|
NamedVertexAttributes.Emplace(MoveTemp(AttributeName), MoveTemp(AttributeComponentValues), AttributeComponentCount);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Add in the attributes that we received, and pad any non-matching existing attributes with zero values to
|
|
// match the new point count.
|
|
//
|
|
if (ExistFaceNum)
|
|
{
|
|
// TODO: In the future, once we support attributes with non-unity component counts, we need to decide how to deal
|
|
// with attributes that share the same name but differing component counts. Two options:
|
|
// 1) We create a new attribute with a new name that differs from the existing name. Preferably with the component
|
|
// count somehow embedded in the name so that we can deal with appending again.
|
|
// 2) Widen the existing attribute's component count to match the new one (or vice versa, widening the new attribute
|
|
// so that we can append).
|
|
|
|
const int32 VertexCount = Mesh->GetControlPointsCount();
|
|
|
|
for (int32 ExistingAttributeIndex = 0; ExistingAttributeIndex < ImportData.VertexAttributes.Num(); ExistingAttributeIndex++)
|
|
{
|
|
const FString& ExistingAttributeName = ImportData.VertexAttributeNames[ExistingAttributeIndex];
|
|
const int32 ExistingAttributeComponentCount = ImportData.VertexAttributes[ExistingAttributeIndex].ComponentCount;
|
|
|
|
int32 NewAttributeIndex = NamedVertexAttributes.IndexOfByPredicate(
|
|
[ExistingAttributeName](const FNamedVertexAttribute& InAttribute)
|
|
{
|
|
return InAttribute.AttributeName == ExistingAttributeName && ensure(InAttribute.ComponentCount == 1);
|
|
});
|
|
|
|
if (ExistingAttributeComponentCount != 1)
|
|
{
|
|
// For now, if an existing attribute has a non-unity component count, we don't append the new attribute values
|
|
// on top, just pad with zeroes.
|
|
const FText ErrorMsg = FText::Format(LOCTEXT("FbxSkeletalMeshimport_AttributeComponentCountMismatch", "Existing attribute '{0}' has more than one component. Ignoring imported attribute of same name."),
|
|
FText::FromString(ExistingAttributeName));
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, ErrorMsg), FFbxErrors::SkeletalMesh_AttributeComponentCountMismatch);
|
|
|
|
NewAttributeIndex = INDEX_NONE;
|
|
}
|
|
|
|
SkeletalMeshImportData::FVertexAttribute& ExistingAttribute = ImportData.VertexAttributes[ExistingAttributeIndex];
|
|
if (NewAttributeIndex != INDEX_NONE)
|
|
{
|
|
ExistingAttribute.AttributeValues.Append(NamedVertexAttributes[NewAttributeIndex].AttributeValues);
|
|
NamedVertexAttributes.RemoveAt(NewAttributeIndex);
|
|
}
|
|
else
|
|
{
|
|
ExistingAttribute.AttributeValues.AddZeroed(VertexCount * ExistingAttribute.ComponentCount);
|
|
}
|
|
}
|
|
|
|
// Any remaining attributes we add + padding for existing points.
|
|
for (FNamedVertexAttribute& NamedVertexAttribute: NamedVertexAttributes)
|
|
{
|
|
ImportData.VertexAttributeNames.Emplace(MoveTemp(NamedVertexAttribute.AttributeName));
|
|
|
|
SkeletalMeshImportData::FVertexAttribute& NewAttribute = ImportData.VertexAttributes.AddDefaulted_GetRef();
|
|
NewAttribute.ComponentCount = NamedVertexAttribute.ComponentCount;
|
|
NewAttribute.AttributeValues.AddZeroed(ExistPointNum * NamedVertexAttribute.ComponentCount);
|
|
NewAttribute.AttributeValues.Append(NamedVertexAttribute.AttributeValues);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (FNamedVertexAttribute& NamedVertexAttribute: NamedVertexAttributes)
|
|
{
|
|
ImportData.VertexAttributes.Emplace(MoveTemp(NamedVertexAttribute.AttributeValues), NamedVertexAttribute.ComponentCount);
|
|
ImportData.VertexAttributeNames.Emplace(MoveTemp(NamedVertexAttribute.AttributeName));
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// clean up
|
|
//
|
|
if (LayerElementUV)
|
|
{
|
|
delete[] LayerElementUV;
|
|
}
|
|
if (UVReferenceMode)
|
|
{
|
|
delete[] UVReferenceMode;
|
|
}
|
|
if (UVMappingMode)
|
|
{
|
|
delete[] UVMappingMode;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::InsertNewLODToBaseSkeletalMesh(USkeletalMesh* InSkeletalMesh, USkeletalMesh* BaseSkeletalMesh, int32 DesiredLOD, UFbxSkeletalMeshImportData* TemplateImportData)
|
|
{
|
|
//Scope a post edit change
|
|
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(BaseSkeletalMesh);
|
|
|
|
FSkeletalMeshModel* ImportedResource = InSkeletalMesh->GetImportedModel();
|
|
FSkeletalMeshModel* DestImportedResource = BaseSkeletalMesh->GetImportedModel();
|
|
|
|
FSkeletalMeshLODModel& NewLODModel = ImportedResource->LODModels[0];
|
|
|
|
|
|
//Fill the data we need to recover the user section material slot assignation
|
|
TArray<FName> ExistingMeshSectionSlotNames;
|
|
TArray<FName> OriginalImportMeshSectionSlotNames;
|
|
UFbxSkeletalMeshImportData* ImportData = nullptr;
|
|
bool HasReimportData = (TemplateImportData != nullptr && DesiredLOD != 0 && BaseSkeletalMesh->GetLODNum() > DesiredLOD);
|
|
if (HasReimportData)
|
|
{
|
|
ImportData = UFbxSkeletalMeshImportData::GetImportDataForSkeletalMesh(BaseSkeletalMesh, TemplateImportData);
|
|
HasReimportData = (ImportData != nullptr && ImportData->ImportMeshLodData.Num() > DesiredLOD);
|
|
if (HasReimportData)
|
|
{
|
|
const FImportMeshLodSectionsData& OriginalImportMeshLodSectionsData = ImportData->ImportMeshLodData[DesiredLOD];
|
|
const FSkeletalMeshLODInfo &ExistingSkelMeshLodInfo = *(BaseSkeletalMesh->GetLODInfo(DesiredLOD));
|
|
//Restore the section changes from the old import data
|
|
for (int32 SectionIndex = 0; SectionIndex < ExistingSkelMeshLodInfo.LODMaterialMap.Num(); SectionIndex++)
|
|
{
|
|
if (ExistingSkelMeshLodInfo.LODMaterialMap.Num() <= SectionIndex || OriginalImportMeshLodSectionsData.SectionOriginalMaterialName.Num() <= SectionIndex)
|
|
{
|
|
break;
|
|
}
|
|
|
|
//Get the current skelmesh section slot import name
|
|
int32 ExistRemapMaterialIndex = ExistingSkelMeshLodInfo.LODMaterialMap[SectionIndex];
|
|
if (!BaseSkeletalMesh->GetMaterials().IsValidIndex(ExistRemapMaterialIndex))
|
|
{
|
|
//If we have an INDEX_NONE it mean we have to use the section material index. If we have a bad value we will use the section material index if possible
|
|
if (DestImportedResource->LODModels.IsValidIndex(DesiredLOD) && DestImportedResource->LODModels[DesiredLOD].Sections.IsValidIndex(SectionIndex))
|
|
{
|
|
ExistRemapMaterialIndex = DestImportedResource->LODModels[DesiredLOD].Sections[SectionIndex].MaterialIndex;
|
|
}
|
|
else
|
|
{
|
|
//If the material is wrong log and pick the zero index
|
|
UE_LOG(LogFbx, Display, TEXT("Insert LOD, invalid material rematch: LOD [%d] Section[%d]- Skeletalmesh:[%s]"), DesiredLOD, SectionIndex, *BaseSkeletalMesh->GetFullName());
|
|
ExistRemapMaterialIndex = 0;
|
|
}
|
|
}
|
|
check(BaseSkeletalMesh->GetMaterials().IsValidIndex(ExistRemapMaterialIndex));
|
|
|
|
ExistingMeshSectionSlotNames.Add(BaseSkeletalMesh->GetMaterials()[ExistRemapMaterialIndex].ImportedMaterialSlotName);
|
|
|
|
//Get the Last imported skelmesh section slot import name
|
|
OriginalImportMeshSectionSlotNames.Add(OriginalImportMeshLodSectionsData.SectionOriginalMaterialName[SectionIndex]);
|
|
}
|
|
|
|
if (DestImportedResource->LODModels.IsValidIndex(DesiredLOD))
|
|
{
|
|
const FSkeletalMeshLODModel& ExistLODModel = DestImportedResource->LODModels[DesiredLOD];
|
|
for (int32 NewSectionIndex = 0; NewSectionIndex < NewLODModel.Sections.Num(); NewSectionIndex++)
|
|
{
|
|
FSkelMeshSection& NewSection = NewLODModel.Sections[NewSectionIndex];
|
|
int32 MatIdx = NewSection.MaterialIndex;
|
|
for (int32 ExistSectionIndex = 0; ExistSectionIndex < ExistLODModel.Sections.Num(); ExistSectionIndex++)
|
|
{
|
|
if (OriginalImportMeshSectionSlotNames.IsValidIndex(ExistSectionIndex) && OriginalImportMeshSectionSlotNames[ExistSectionIndex] == InSkeletalMesh->GetMaterials()[MatIdx].ImportedMaterialSlotName)
|
|
{
|
|
const FSkelMeshSection& ExistSection = ExistLODModel.Sections[ExistSectionIndex];
|
|
//We found a match, restore the data
|
|
NewSection.bCastShadow = ExistSection.bCastShadow;
|
|
NewSection.bVisibleInRayTracing = ExistSection.bVisibleInRayTracing;
|
|
NewSection.bRecomputeTangent = ExistSection.bRecomputeTangent;
|
|
NewSection.RecomputeTangentsVertexMaskChannel = ExistSection.RecomputeTangentsVertexMaskChannel;
|
|
NewSection.bDisabled = ExistSection.bDisabled;
|
|
NewSection.GenerateUpToLodIndex = ExistSection.GenerateUpToLodIndex;
|
|
bool bBoneChunkedSection = NewSection.ChunkedParentSectionIndex >= 0;
|
|
int32 ParentOriginalSectionIndex = NewSection.OriginalDataSectionIndex;
|
|
if (!bBoneChunkedSection)
|
|
{
|
|
//Set the new Parent Index
|
|
FSkelMeshSourceSectionUserData& UserSectionData = NewLODModel.UserSectionsData.FindOrAdd(ParentOriginalSectionIndex);
|
|
UserSectionData.bDisabled = NewSection.bDisabled;
|
|
UserSectionData.bCastShadow = NewSection.bCastShadow;
|
|
UserSectionData.bVisibleInRayTracing = NewSection.bVisibleInRayTracing;
|
|
UserSectionData.bRecomputeTangent = NewSection.bRecomputeTangent;
|
|
UserSectionData.RecomputeTangentsVertexMaskChannel = NewSection.RecomputeTangentsVertexMaskChannel;
|
|
UserSectionData.GenerateUpToLodIndex = NewSection.GenerateUpToLodIndex;
|
|
//The cloth will be rebind later after the reimport is done
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we want to add this as a new LOD to this mesh - add to LODModels/LODInfo array.
|
|
if (DesiredLOD == DestImportedResource->LODModels.Num())
|
|
{
|
|
DestImportedResource->LODModels.Add(new FSkeletalMeshLODModel());
|
|
|
|
// Add element to LODInfo array.
|
|
BaseSkeletalMesh->AddLODInfo();
|
|
check(BaseSkeletalMesh->GetLODNum() == DestImportedResource->LODModels.Num());
|
|
}
|
|
|
|
// Set up LODMaterialMap to number of materials in new mesh.
|
|
FSkeletalMeshLODInfo& LODInfo = *(BaseSkeletalMesh->GetLODInfo(DesiredLOD));
|
|
|
|
//Copy the build settings
|
|
if(InSkeletalMesh->GetLODInfo(0))
|
|
{
|
|
const FSkeletalMeshLODInfo& ImportedLODInfo = *(InSkeletalMesh->GetLODInfo(0));
|
|
LODInfo.BuildSettings = ImportedLODInfo.BuildSettings;
|
|
}
|
|
|
|
TArray<FSkeletalMaterial>& BaseMaterials = BaseSkeletalMesh->GetMaterials();
|
|
LODInfo.LODMaterialMap.Empty();
|
|
// Now set up the material mapping array.
|
|
for (int32 SectionIndex = 0; SectionIndex < NewLODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
int32 MatIdx = NewLODModel.Sections[SectionIndex].MaterialIndex;
|
|
// Try and find the auto-assigned material in the array.
|
|
int32 LODMatIndex = INDEX_NONE;
|
|
//First try to match by name
|
|
for (int32 BaseMaterialIndex = 0; BaseMaterialIndex < BaseMaterials.Num(); ++BaseMaterialIndex)
|
|
{
|
|
const FSkeletalMaterial& SkeletalMaterial = BaseMaterials[BaseMaterialIndex];
|
|
if (SkeletalMaterial.ImportedMaterialSlotName != NAME_None && SkeletalMaterial.ImportedMaterialSlotName == InSkeletalMesh->GetMaterials()[MatIdx].ImportedMaterialSlotName)
|
|
{
|
|
LODMatIndex = BaseMaterialIndex;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// If we dont have a match, add a new entry to the material list.
|
|
if (LODMatIndex == INDEX_NONE)
|
|
{
|
|
LODMatIndex = BaseMaterials.Add(InSkeletalMesh->GetMaterials()[MatIdx]);
|
|
}
|
|
|
|
LODInfo.LODMaterialMap.Add(LODMatIndex);
|
|
}
|
|
|
|
// Release all resources before replacing the model
|
|
BaseSkeletalMesh->PreEditChange(nullptr);
|
|
|
|
// Assign new FSkeletalMeshLODModel to desired slot in selected skeletal mesh.
|
|
FSkeletalMeshLODModel::CopyStructure(&(DestImportedResource->LODModels[DesiredLOD]), &NewLODModel);
|
|
//Copy the import data into the base skeletalmesh for the imported LOD
|
|
FMeshDescription SourceMeshDescription;
|
|
if (InSkeletalMesh->CloneMeshDescription(0, SourceMeshDescription))
|
|
{
|
|
BaseSkeletalMesh->CreateMeshDescription(DesiredLOD, MoveTemp(SourceMeshDescription));
|
|
BaseSkeletalMesh->CommitMeshDescription(DesiredLOD);
|
|
}
|
|
|
|
|
|
// If this LOD had been generated previously by automatic mesh reduction, clear that flag.
|
|
LODInfo.bHasBeenSimplified = false;
|
|
if (BaseSkeletalMesh->GetLODSettings() == nullptr || !BaseSkeletalMesh->GetLODSettings()->HasValidSettings() || BaseSkeletalMesh->GetLODSettings()->GetNumberOfSettings() <= DesiredLOD)
|
|
{
|
|
//Make sure any custom LOD have correct settings (no reduce)
|
|
LODInfo.ReductionSettings.NumOfTrianglesPercentage = 1.0f;
|
|
LODInfo.ReductionSettings.MaxNumOfTriangles = MAX_uint32;
|
|
LODInfo.ReductionSettings.MaxNumOfTrianglesPercentage = MAX_uint32;
|
|
LODInfo.ReductionSettings.NumOfVertPercentage = 1.0f;
|
|
LODInfo.ReductionSettings.MaxNumOfVerts = MAX_uint32;
|
|
LODInfo.ReductionSettings.MaxNumOfVertsPercentage = MAX_uint32;
|
|
LODInfo.ReductionSettings.MaxDeviationPercentage = 0.0f;
|
|
}
|
|
|
|
//Set back the user data
|
|
if (HasReimportData && BaseSkeletalMesh->GetLODNum() > DesiredLOD)
|
|
{
|
|
FSkeletalMeshLODInfo &NewSkelMeshLodInfo = *(BaseSkeletalMesh->GetLODInfo(DesiredLOD));
|
|
//Restore the section changes from the old import data
|
|
for (int32 SectionIndex = 0; SectionIndex < NewLODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
if (ExistingMeshSectionSlotNames.Num() <= SectionIndex || OriginalImportMeshSectionSlotNames.Num() <= SectionIndex)
|
|
{
|
|
break;
|
|
}
|
|
//Get the current skelmesh section slot import name
|
|
FName ExistMeshSectionSlotName = ExistingMeshSectionSlotNames[SectionIndex];
|
|
//Get the new skelmesh section slot import name
|
|
int32 NewRemapMaterialIndex = NewSkelMeshLodInfo.LODMaterialMap[SectionIndex];
|
|
if (!BaseMaterials.IsValidIndex(NewRemapMaterialIndex))
|
|
{
|
|
int32 RematchMaterialIndex = NewSkelMeshLodInfo.LODMaterialMap[SectionIndex];
|
|
if (RematchMaterialIndex == INDEX_NONE)
|
|
{
|
|
RematchMaterialIndex = NewLODModel.Sections[SectionIndex].MaterialIndex;
|
|
}
|
|
NewSkelMeshLodInfo.LODMaterialMap[SectionIndex] = FMath::Clamp<int32>(RematchMaterialIndex, 0, BaseMaterials.Num()-1);
|
|
continue;
|
|
}
|
|
FName NewMeshSectionSlotName = BaseMaterials[NewRemapMaterialIndex].ImportedMaterialSlotName;
|
|
//Get the Last imported skelmesh section slot import name
|
|
FName OriginalImportMeshSectionSlotName = OriginalImportMeshSectionSlotNames[SectionIndex];
|
|
|
|
if (OriginalImportMeshSectionSlotName == NewMeshSectionSlotName && ExistMeshSectionSlotName != OriginalImportMeshSectionSlotName)
|
|
{
|
|
//The last import slot name match the New import slot name, but the Exist slot name is different then the last import slot name.
|
|
//This mean the user has change the section assign slot and the fbx file did not change it
|
|
//Override the new section material index to use the one that the user set
|
|
for (int32 RemapMaterialIndex = 0; RemapMaterialIndex < BaseMaterials.Num(); ++RemapMaterialIndex)
|
|
{
|
|
const FSkeletalMaterial &NewSectionMaterial = BaseMaterials[RemapMaterialIndex];
|
|
if (NewSectionMaterial.ImportedMaterialSlotName == ExistMeshSectionSlotName)
|
|
{
|
|
NewSkelMeshLodInfo.LODMaterialMap[SectionIndex] = RemapMaterialIndex;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool UnFbx::FFbxImporter::ImportSkeletalMeshLOD(USkeletalMesh* InSkeletalMesh, USkeletalMesh* BaseSkeletalMesh, int32 DesiredLOD, UFbxSkeletalMeshImportData* TemplateImportData)
|
|
{
|
|
check(InSkeletalMesh);
|
|
check(BaseSkeletalMesh);
|
|
|
|
FSkeletalMeshModel* ImportedResource = InSkeletalMesh->GetImportedModel();
|
|
FSkeletalMeshModel* DestImportedResource = BaseSkeletalMesh->GetImportedModel();
|
|
|
|
// Now we copy the base FSkeletalMeshLODModel from the imported skeletal mesh as the new LOD in the selected mesh.
|
|
check(ImportedResource->LODModels.Num() == 1);
|
|
|
|
// Names of root bones must match.
|
|
// If the names of root bones don't match, the LOD Mesh does not share skeleton with base Mesh.
|
|
if (InSkeletalMesh->GetRefSkeleton().GetBoneName(0) != BaseSkeletalMesh->GetRefSkeleton().GetBoneName(0) && !ImportOptions->bImportAsSkeletalGeometry)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("LODRootNameIncorrect", "Root bone in LOD is '{0}' instead of '{1}'.\nImport failed."),
|
|
FText::FromName(InSkeletalMesh->GetRefSkeleton().GetBoneName(0)), FText::FromName(BaseSkeletalMesh->GetRefSkeleton().GetBoneName(0)))), FFbxErrors::SkeletalMesh_LOD_RootNameIncorrect);
|
|
|
|
return false;
|
|
}
|
|
bool bApplyBaseSkinning = false;
|
|
// We do some checking here that for every bone in the mesh we just imported, it's in our base ref skeleton, and the parent is the same.
|
|
for (int32 i = 0; i < InSkeletalMesh->GetRefSkeleton().GetRawBoneNum(); i++)
|
|
{
|
|
int32 LODBoneIndex = i;
|
|
FName LODBoneName = InSkeletalMesh->GetRefSkeleton().GetBoneName(LODBoneIndex);
|
|
int32 BaseBoneIndex = BaseSkeletalMesh->GetRefSkeleton().FindBoneIndex(LODBoneName);
|
|
if (BaseBoneIndex == INDEX_NONE)
|
|
{
|
|
if (!ImportOptions->bImportAsSkeletalGeometry)
|
|
{
|
|
// If we could not find the bone from this LOD in base mesh - we fail.
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("LODBoneDoesNotMatch", "Bone '{0}' not found in base SkeletalMesh '{1}'.\nImport failed."),
|
|
FText::FromName(LODBoneName), FText::FromString(BaseSkeletalMesh->GetName()))), FFbxErrors::SkeletalMesh_LOD_BonesDoNotMatch);
|
|
return false;
|
|
}
|
|
|
|
bApplyBaseSkinning = true;
|
|
break;
|
|
}
|
|
|
|
if (i > 0)
|
|
{
|
|
int32 LODParentIndex = InSkeletalMesh->GetRefSkeleton().GetParentIndex(LODBoneIndex);
|
|
FName LODParentName = InSkeletalMesh->GetRefSkeleton().GetBoneName(LODParentIndex);
|
|
|
|
int32 BaseParentIndex = BaseSkeletalMesh->GetRefSkeleton().GetParentIndex(BaseBoneIndex);
|
|
FName BaseParentName = BaseSkeletalMesh->GetRefSkeleton().GetBoneName(BaseParentIndex);
|
|
|
|
if (LODParentName != BaseParentName)
|
|
{
|
|
if (!ImportOptions->bImportAsSkeletalGeometry)
|
|
{
|
|
// If bone has different parents, display an error and don't allow import.
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("LODBoneHasIncorrectParent", "Bone '{0}' in LOD has parent '{1}' instead of '{2}'"),
|
|
FText::FromName(LODBoneName), FText::FromName(LODParentName), FText::FromName(BaseParentName))), FFbxErrors::SkeletalMesh_LOD_IncorrectParent);
|
|
return false;
|
|
}
|
|
bApplyBaseSkinning = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(BaseSkeletalMesh);
|
|
|
|
//Skeleton do not match and we are importing skeletal geometry only so apply the base LOD Skinning
|
|
if (bApplyBaseSkinning)
|
|
{
|
|
check(ImportOptions->bImportAsSkeletalGeometry);
|
|
//Apply the base LOD skinning to the new LOD geometry, DestImportedResource here is the base lod and is the source for the apply skinning.
|
|
//The Dest here mean we merge the just import LOD to the base LodModel.
|
|
SkeletalMeshImportUtils::ApplySkinning(InSkeletalMesh, DestImportedResource->LODModels[0], ImportedResource->LODModels[0]);
|
|
|
|
//Excluded bones will be remove when we rebuild the asset
|
|
//IMeshUtilities& MeshUtilities = FModuleManager::Get().LoadModuleChecked<IMeshUtilities>("MeshUtilities");
|
|
//MeshUtilities.RemoveBonesFromMesh(BaseSkeletalMesh, DesiredLOD, NULL);
|
|
}
|
|
|
|
FSkeletalMeshLODModel& NewLODModel = ImportedResource->LODModels[0];
|
|
|
|
// Enforce LODs having only single-influence vertices.
|
|
bool bCheckSingleInfluence;
|
|
GConfig->GetBool(TEXT("ImportSetting"), TEXT("CheckSingleInfluenceLOD"), bCheckSingleInfluence, GEditorIni);
|
|
if (bCheckSingleInfluence &&
|
|
DesiredLOD > 0)
|
|
{
|
|
for (int32 SectionIndex = 0; SectionIndex < NewLODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
if (NewLODModel.Sections[SectionIndex].SoftVertices.Num() > 0)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText(LOCTEXT("LODHasSoftVertices", "Warning: The mesh LOD you are importing has some vertices with more than one influence."))), FFbxErrors::SkeletalMesh_LOD_HasSoftVerts);
|
|
}
|
|
}
|
|
}
|
|
|
|
// If this LOD is going to be the lowest one, we check all bones we have sockets on are present in it.
|
|
if (DesiredLOD == DestImportedResource->LODModels.Num() ||
|
|
DesiredLOD == DestImportedResource->LODModels.Num() - 1)
|
|
{
|
|
const TArray<USkeletalMeshSocket*>& Sockets = BaseSkeletalMesh->GetMeshOnlySocketList();
|
|
|
|
for (int32 i = 0; i < Sockets.Num(); i++)
|
|
{
|
|
// Find bone index the socket is attached to.
|
|
USkeletalMeshSocket* Socket = Sockets[i];
|
|
int32 SocketBoneIndex = InSkeletalMesh->GetRefSkeleton().FindBoneIndex(Socket->BoneName);
|
|
|
|
// If this LOD does not contain the socket bone, abort import.
|
|
if (SocketBoneIndex == INDEX_NONE)
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("LODMissingSocketBone", "This LOD is missing bone '{0}' used by socket '{1}'.\nAborting import."),
|
|
FText::FromName(Socket->BoneName), FText::FromName(Socket->SocketName))), FFbxErrors::SkeletalMesh_LOD_MissingSocketBone);
|
|
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!bApplyBaseSkinning)
|
|
{
|
|
//The imported LOD is always in LOD 0 of the InSkeletalMesh
|
|
const int32 SourceLODIndex = 0;
|
|
if(InSkeletalMesh->HasMeshDescription(SourceLODIndex))
|
|
{
|
|
// Fix up the imported data bone indexes
|
|
FSkeletalMeshImportData LODImportData;
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
InSkeletalMesh->LoadLODImportedData(SourceLODIndex, LODImportData);
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
const int32 LODImportDataBoneNumber = LODImportData.RefBonesBinary.Num();
|
|
//We want to create a remap array so we can fix all influence easily
|
|
TArray<int32> ImportDataBoneRemap;
|
|
ImportDataBoneRemap.AddZeroed(LODImportDataBoneNumber);
|
|
//We generate a new RefBonesBinary array to replace the existing one
|
|
TArray<SkeletalMeshImportData::FBone> RemapedRefBonesBinary;
|
|
RemapedRefBonesBinary.AddZeroed(BaseSkeletalMesh->GetRefSkeleton().GetNum());
|
|
for (int32 ImportBoneIndex = 0; ImportBoneIndex < LODImportDataBoneNumber; ++ImportBoneIndex)
|
|
{
|
|
SkeletalMeshImportData::FBone& ImportedBone = LODImportData.RefBonesBinary[ImportBoneIndex];
|
|
int32 LODBoneIndex = ImportBoneIndex;
|
|
FName LODBoneName = FName(*FSkeletalMeshImportData::FixupBoneName(ImportedBone.Name));
|
|
int32 BaseBoneIndex = BaseSkeletalMesh->GetRefSkeleton().FindBoneIndex(LODBoneName);
|
|
ImportDataBoneRemap[ImportBoneIndex] = BaseBoneIndex;
|
|
if (BaseBoneIndex != INDEX_NONE)
|
|
{
|
|
RemapedRefBonesBinary[BaseBoneIndex] = ImportedBone;
|
|
if(RemapedRefBonesBinary[BaseBoneIndex].ParentIndex != INDEX_NONE)
|
|
{
|
|
RemapedRefBonesBinary[BaseBoneIndex].ParentIndex = ImportDataBoneRemap[RemapedRefBonesBinary[BaseBoneIndex].ParentIndex];
|
|
}
|
|
}
|
|
}
|
|
//Copy the new RefBonesBinary over the existing one
|
|
LODImportData.RefBonesBinary = RemapedRefBonesBinary;
|
|
|
|
//Fix the influences
|
|
bool bNeedShrinking = false;
|
|
const int32 InfluenceNumber = LODImportData.Influences.Num();
|
|
for (int32 InfluenceIndex = InfluenceNumber-1; InfluenceIndex >= 0; --InfluenceIndex)
|
|
{
|
|
SkeletalMeshImportData::FRawBoneInfluence& Influence = LODImportData.Influences[InfluenceIndex];
|
|
Influence.BoneIndex = ImportDataBoneRemap.IsValidIndex(Influence.BoneIndex) ? ImportDataBoneRemap[Influence.BoneIndex] : INDEX_NONE;
|
|
if (Influence.BoneIndex == INDEX_NONE)
|
|
{
|
|
const int32 DeleteCount = 1;
|
|
LODImportData.Influences.RemoveAt(InfluenceIndex, DeleteCount, EAllowShrinking::No);
|
|
bNeedShrinking = true;
|
|
}
|
|
}
|
|
//Shrink the array if we have deleted at least one entry
|
|
if(bNeedShrinking)
|
|
{
|
|
LODImportData.Influences.Shrink();
|
|
}
|
|
//Save the fix up remap bone index
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
InSkeletalMesh->SaveLODImportedData(SourceLODIndex, LODImportData);
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
}
|
|
|
|
// Fix up the ActiveBoneIndices array.
|
|
for (int32 ActiveIndex = 0; ActiveIndex < NewLODModel.ActiveBoneIndices.Num(); ActiveIndex++)
|
|
{
|
|
int32 LODBoneIndex = NewLODModel.ActiveBoneIndices[ActiveIndex];
|
|
FName LODBoneName = InSkeletalMesh->GetRefSkeleton().GetBoneName(LODBoneIndex);
|
|
int32 BaseBoneIndex = BaseSkeletalMesh->GetRefSkeleton().FindBoneIndex(LODBoneName);
|
|
NewLODModel.ActiveBoneIndices[ActiveIndex] = BaseBoneIndex;
|
|
}
|
|
|
|
// Fix up the chunk BoneMaps.
|
|
for (int32 SectionIndex = 0; SectionIndex < NewLODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
FSkelMeshSection& Section = NewLODModel.Sections[SectionIndex];
|
|
for (int32 BoneMapIndex = 0; BoneMapIndex < Section.BoneMap.Num(); BoneMapIndex++)
|
|
{
|
|
int32 LODBoneIndex = Section.BoneMap[BoneMapIndex];
|
|
FName LODBoneName = InSkeletalMesh->GetRefSkeleton().GetBoneName(LODBoneIndex);
|
|
int32 BaseBoneIndex = BaseSkeletalMesh->GetRefSkeleton().FindBoneIndex(LODBoneName);
|
|
Section.BoneMap[BoneMapIndex] = BaseBoneIndex;
|
|
}
|
|
}
|
|
|
|
// Create the RequiredBones array in the LODModel from the ref skeleton.
|
|
for (int32 RequiredBoneIndex = 0; RequiredBoneIndex < NewLODModel.RequiredBones.Num(); RequiredBoneIndex++)
|
|
{
|
|
FName LODBoneName = InSkeletalMesh->GetRefSkeleton().GetBoneName(NewLODModel.RequiredBones[RequiredBoneIndex]);
|
|
int32 BaseBoneIndex = BaseSkeletalMesh->GetRefSkeleton().FindBoneIndex(LODBoneName);
|
|
if (BaseBoneIndex != INDEX_NONE)
|
|
{
|
|
NewLODModel.RequiredBones[RequiredBoneIndex] = BaseBoneIndex;
|
|
}
|
|
else
|
|
{
|
|
NewLODModel.RequiredBones.RemoveAt(RequiredBoneIndex--);
|
|
}
|
|
}
|
|
|
|
// Also sort the RequiredBones array to be strictly increasing.
|
|
NewLODModel.RequiredBones.Sort();
|
|
BaseSkeletalMesh->GetRefSkeleton().EnsureParentsExistAndSort(NewLODModel.ActiveBoneIndices);
|
|
}
|
|
// To be extra-nice, we apply the difference between the root transform of the meshes to the verts.
|
|
FMatrix44f LODToBaseTransform = FMatrix44f(InSkeletalMesh->GetRefPoseMatrix(0).InverseFast() * BaseSkeletalMesh->GetRefPoseMatrix(0));
|
|
|
|
for (int32 SectionIndex = 0; SectionIndex < NewLODModel.Sections.Num(); SectionIndex++)
|
|
{
|
|
FSkelMeshSection& Section = NewLODModel.Sections[SectionIndex];
|
|
|
|
// Fix up soft verts.
|
|
for (int32 i = 0; i < Section.SoftVertices.Num(); i++)
|
|
{
|
|
Section.SoftVertices[i].Position = LODToBaseTransform.TransformPosition(Section.SoftVertices[i].Position);
|
|
Section.SoftVertices[i].TangentX = LODToBaseTransform.TransformVector(Section.SoftVertices[i].TangentX);
|
|
Section.SoftVertices[i].TangentY = LODToBaseTransform.TransformVector(Section.SoftVertices[i].TangentY);
|
|
Section.SoftVertices[i].TangentZ = LODToBaseTransform.TransformVector(Section.SoftVertices[i].TangentZ);
|
|
}
|
|
}
|
|
|
|
//Restore the LOD section data in case this LOD was reimport and some material match
|
|
if (DestImportedResource->LODModels.IsValidIndex(DesiredLOD) && ImportedResource->LODModels.IsValidIndex(0))
|
|
{
|
|
const TArray<FSkelMeshSection>& ExistingSections = DestImportedResource->LODModels[DesiredLOD].Sections;
|
|
const FSkeletalMeshLODInfo& ExistingInfo = *(BaseSkeletalMesh->GetLODInfo(DesiredLOD));
|
|
|
|
TArray<FSkelMeshSection>& ImportedSections = ImportedResource->LODModels[0].Sections;
|
|
const FSkeletalMeshLODInfo& ImportedInfo = *(InSkeletalMesh->GetLODInfo(0));
|
|
|
|
auto GetImportMaterialSlotName = [](const USkeletalMesh* SkelMesh, const FSkelMeshSection& Section, int32 SectionIndex, const FSkeletalMeshLODInfo& Info, int32& OutMaterialIndex)->FName
|
|
{
|
|
const TArray<FSkeletalMaterial>& MeshMaterials = SkelMesh->GetMaterials();
|
|
check(MeshMaterials.Num() > 0);
|
|
OutMaterialIndex = Section.MaterialIndex;
|
|
if (Info.LODMaterialMap.IsValidIndex(SectionIndex) && Info.LODMaterialMap[SectionIndex] != INDEX_NONE)
|
|
{
|
|
OutMaterialIndex = Info.LODMaterialMap[SectionIndex];
|
|
}
|
|
FName ImportedMaterialSlotName = NAME_None;
|
|
if (MeshMaterials.IsValidIndex(OutMaterialIndex))
|
|
{
|
|
ImportedMaterialSlotName = MeshMaterials[OutMaterialIndex].ImportedMaterialSlotName;
|
|
}
|
|
else
|
|
{
|
|
ImportedMaterialSlotName = MeshMaterials[0].ImportedMaterialSlotName;
|
|
OutMaterialIndex = 0;
|
|
}
|
|
return ImportedMaterialSlotName;
|
|
};
|
|
|
|
for(int32 ExistingSectionIndex = 0; ExistingSectionIndex < ExistingSections.Num(); ++ExistingSectionIndex)
|
|
{
|
|
const FSkelMeshSection& ExistingSection = ExistingSections[ExistingSectionIndex];
|
|
int32 ExistingMaterialIndex = 0;
|
|
FName ExistingImportedMaterialSlotName = GetImportMaterialSlotName(BaseSkeletalMesh, ExistingSection, ExistingSectionIndex, ExistingInfo, ExistingMaterialIndex);
|
|
|
|
for(int32 ImportedSectionIndex = 0; ImportedSectionIndex < ImportedSections.Num(); ++ImportedSectionIndex)
|
|
{
|
|
FSkelMeshSection& ImportedSection = ImportedSections[ImportedSectionIndex];
|
|
int32 ImportedMaterialIndex = 0;
|
|
FName ImportedImportedMaterialSlotName = GetImportMaterialSlotName(InSkeletalMesh, ImportedSection, ImportedSectionIndex, ImportedInfo, ImportedMaterialIndex);
|
|
if (ExistingImportedMaterialSlotName != NAME_None)
|
|
{
|
|
if (ImportedImportedMaterialSlotName == ExistingImportedMaterialSlotName)
|
|
{
|
|
//Set the value and exit
|
|
ImportedSection.bCastShadow = ExistingSection.bCastShadow;
|
|
ImportedSection.bVisibleInRayTracing = ExistingSection.bVisibleInRayTracing;
|
|
ImportedSection.bRecomputeTangent = ExistingSection.bRecomputeTangent;
|
|
ImportedSection.RecomputeTangentsVertexMaskChannel = ExistingSection.RecomputeTangentsVertexMaskChannel;
|
|
break;
|
|
}
|
|
}
|
|
else if(InSkeletalMesh->GetMaterials()[ImportedMaterialIndex] == BaseSkeletalMesh->GetMaterials()[ExistingMaterialIndex]) //Use material slot compare to match in case the name is none
|
|
{
|
|
//Set the value and exit
|
|
ImportedSection.bCastShadow = ExistingSection.bCastShadow;
|
|
ImportedSection.bVisibleInRayTracing = ExistingSection.bVisibleInRayTracing;
|
|
ImportedSection.bRecomputeTangent = ExistingSection.bRecomputeTangent;
|
|
ImportedSection.RecomputeTangentsVertexMaskChannel = ExistingSection.RecomputeTangentsVertexMaskChannel;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
InsertNewLODToBaseSkeletalMesh(InSkeletalMesh, BaseSkeletalMesh, DesiredLOD, TemplateImportData);
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
void UnFbx::FFbxImporter::ImportMorphTargetsInternal( TArray<FbxNode*>& SkelMeshNodeArray, USkeletalMesh* BaseSkelMesh, int32 LODIndex, FSkeletalMeshImportData &BaseImportData, const bool bMapMorphTargetToTimeZero)
|
|
{
|
|
FbxString ShapeNodeName;
|
|
TMap<FString, TArray<FbxShape*>> ShapeNameToShapeArray;
|
|
FScopedSlowTask ImportMorphTargetSlowTask(0, NSLOCTEXT("FbxImporter", "BeginGeneratingMorphModelsTask", "Generating Morph Models"), true);
|
|
|
|
// For each morph in FBX geometries, we create one morph target for the Unreal skeletal mesh
|
|
for (int32 NodeIndex = 0; NodeIndex < SkelMeshNodeArray.Num(); NodeIndex++)
|
|
{
|
|
FbxGeometry* Geometry = (FbxGeometry*)SkelMeshNodeArray[NodeIndex]->GetNodeAttribute();
|
|
if (Geometry)
|
|
{
|
|
const int32 BlendShapeDeformerCount = Geometry->GetDeformerCount(FbxDeformer::eBlendShape);
|
|
|
|
/************************************************************************/
|
|
/* collect all the shapes */
|
|
/************************************************************************/
|
|
for(int32 BlendShapeIndex = 0; BlendShapeIndex<BlendShapeDeformerCount; ++BlendShapeIndex)
|
|
{
|
|
FbxBlendShape* BlendShape = (FbxBlendShape*)Geometry->GetDeformer(BlendShapeIndex, FbxDeformer::eBlendShape);
|
|
const int32 BlendShapeChannelCount = BlendShape->GetBlendShapeChannelCount();
|
|
|
|
FString BlendShapeName = MakeName(BlendShape->GetName());
|
|
|
|
// see below where this is used for explanation...
|
|
const bool bMightBeBadMAXFile = (BlendShapeName == FString("Morpher"));
|
|
|
|
for(int32 ChannelIndex = 0; ChannelIndex<BlendShapeChannelCount; ++ChannelIndex)
|
|
{
|
|
FbxBlendShapeChannel* Channel = BlendShape->GetBlendShapeChannel(ChannelIndex);
|
|
if(Channel)
|
|
{
|
|
//Find which shape should we use according to the weight.
|
|
const int32 CurrentChannelShapeCount = Channel->GetTargetShapeCount();
|
|
|
|
FString ChannelName = MakeName(Channel->GetName());
|
|
|
|
// Maya adds the name of the blendshape and an underscore to the front of the channel name, so remove it
|
|
if(ChannelName.StartsWith(BlendShapeName))
|
|
{
|
|
ChannelName.RightInline(ChannelName.Len() - (BlendShapeName.Len()+1), EAllowShrinking::No);
|
|
}
|
|
|
|
for(int32 ShapeIndex = 0; ShapeIndex<CurrentChannelShapeCount; ++ShapeIndex)
|
|
{
|
|
FbxShape* Shape = Channel->GetTargetShape(ShapeIndex);
|
|
|
|
FString ShapeName;
|
|
if( CurrentChannelShapeCount > 1 )
|
|
{
|
|
ShapeName = MakeName(Shape->GetName());
|
|
}
|
|
else
|
|
{
|
|
if (bMightBeBadMAXFile)
|
|
{
|
|
ShapeName = MakeName(Shape->GetName());
|
|
}
|
|
else
|
|
{
|
|
// Maya concatenates the number of the shape to the end of its name, so instead use the name of the channel
|
|
ShapeName = ChannelName;
|
|
}
|
|
}
|
|
|
|
TArray<FbxShape*> & ShapeArray = ShapeNameToShapeArray.FindOrAdd(ShapeName);
|
|
if (ShapeArray.Num() == 0)
|
|
{
|
|
ShapeArray.AddZeroed(SkelMeshNodeArray.Num());
|
|
}
|
|
|
|
ShapeArray[NodeIndex] = Shape;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} // for NodeIndex
|
|
|
|
for (auto Iter = ShapeNameToShapeArray.CreateIterator(); Iter && !bImportOperationCanceled; ++Iter)
|
|
{
|
|
FString ShapeName = Iter.Key();
|
|
TArray< FbxShape* >& ShapeArray = Iter.Value();
|
|
FSkeletalMeshImportData ShapeImportData;
|
|
BaseImportData.CopyDataNeedByMorphTargetImport(ShapeImportData);
|
|
|
|
//Store the rebuild morph data into the base import data, this will allow us to rebuild the morph data in case the mesh is rebuild and the vertex count change because of options (max bone per section, normals compute...)
|
|
int32 MorphTargetIndex;
|
|
if (BaseImportData.MorphTargetNames.Find(ShapeName, MorphTargetIndex))
|
|
{
|
|
BaseImportData.MorphTargetNames.RemoveAt(MorphTargetIndex);
|
|
BaseImportData.MorphTargetModifiedPoints.RemoveAt(MorphTargetIndex);
|
|
BaseImportData.MorphTargets.RemoveAt(MorphTargetIndex);
|
|
}
|
|
BaseImportData.MorphTargetNames.Add(ShapeName);
|
|
TSet<uint32>& ModifiedPoints = BaseImportData.MorphTargetModifiedPoints.AddDefaulted_GetRef();
|
|
GatherPointsForMorphTarget(&ShapeImportData, SkelMeshNodeArray, &ShapeArray, ModifiedPoints, bMapMorphTargetToTimeZero);
|
|
//We do not need this data anymore empty it so we reduce the size of what we save into memory
|
|
ShapeImportData.PointToRawMap.Empty();
|
|
BaseImportData.MorphTargets.Add(ShapeImportData);
|
|
check(BaseImportData.MorphTargetNames.Num() == BaseImportData.MorphTargets.Num() && BaseImportData.MorphTargetNames.Num() == BaseImportData.MorphTargetModifiedPoints.Num());
|
|
|
|
// Ensure that we have curve metadata for this morph target (either skeleton or mesh)
|
|
FName CurveName = *ShapeName;
|
|
if(ImportOptions->bAddCurveMetadataToSkeleton)
|
|
{
|
|
if(USkeleton* Skeleton = BaseSkelMesh->GetSkeleton())
|
|
{
|
|
Skeleton->AddCurveMetaData(CurveName);
|
|
|
|
// Ensure we have a morph flag set
|
|
FCurveMetaData* CurveMetaData = Skeleton->GetCurveMetaData(CurveName);
|
|
check(CurveMetaData);
|
|
CurveMetaData->Type.bMorphtarget = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
UAnimCurveMetaData* AnimCurveMetaData = BaseSkelMesh->GetAssetUserData<UAnimCurveMetaData>();
|
|
if(AnimCurveMetaData == nullptr)
|
|
{
|
|
AnimCurveMetaData = NewObject<UAnimCurveMetaData>(BaseSkelMesh, NAME_None, RF_Transactional);
|
|
BaseSkelMesh->AddAssetUserData(AnimCurveMetaData);
|
|
}
|
|
|
|
AnimCurveMetaData->AddCurveMetaData(CurveName);
|
|
|
|
// Ensure we have a morph flag set
|
|
FCurveMetaData* CurveMetaData = AnimCurveMetaData->GetCurveMetaData(CurveName);
|
|
check(CurveMetaData);
|
|
CurveMetaData->Type.bMorphtarget = true;
|
|
}
|
|
|
|
if (ImportOptions->bIsImportCancelable && ImportMorphTargetSlowTask.ShouldCancel())
|
|
{
|
|
bImportOperationCanceled = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (BaseSkelMesh->GetImportedModel() && BaseSkelMesh->GetImportedModel()->LODModels.IsValidIndex(LODIndex))
|
|
{
|
|
//If we can build the skeletal mesh there is no need to build the morph target now, all the necessary build morph target data was copied before.
|
|
if (!BaseSkelMesh->HasMeshDescription(LODIndex))
|
|
{
|
|
//Build MorphTargets
|
|
FLODUtilities::BuildMorphTargets(
|
|
BaseSkelMesh,
|
|
BaseImportData,
|
|
LODIndex,
|
|
ImportOptions->ShouldImportNormals(),
|
|
ImportOptions->ShouldImportTangents(),
|
|
(ImportOptions->NormalGenerationMethod == EFBXNormalGenerationMethod::MikkTSpace),
|
|
ImportOptions->OverlappingThresholds
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Import Morph target
|
|
void UnFbx::FFbxImporter::ImportFbxMorphTarget(TArray<FbxNode*> &SkelMeshNodeArray, USkeletalMesh* BaseSkelMesh, int32 LODIndex, FSkeletalMeshImportData &BaseSkeletalMeshImportData, const bool bMapMorphTargetToTimeZero)
|
|
{
|
|
if (!CanImportClass(UMorphTarget::StaticClass()))
|
|
{
|
|
return;
|
|
}
|
|
|
|
//Stack the PostEditChange call, it will call post edit change when it will go out of scope
|
|
FScopedSkeletalMeshPostEditChange ScopedPostEditChange(BaseSkelMesh);
|
|
FFbxScopedOperation ScopedImportOperation(this);
|
|
|
|
bool bHasMorph = false;
|
|
int32 NodeIndex;
|
|
// check if there are morph in this geometry
|
|
for (NodeIndex = 0; NodeIndex < SkelMeshNodeArray.Num(); NodeIndex++)
|
|
{
|
|
FbxGeometry* Geometry = (FbxGeometry*)SkelMeshNodeArray[NodeIndex]->GetNodeAttribute();
|
|
if (Geometry)
|
|
{
|
|
bHasMorph = Geometry->GetDeformerCount(FbxDeformer::eBlendShape) > 0;
|
|
if (bHasMorph)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bHasMorph)
|
|
{
|
|
ImportMorphTargetsInternal( SkelMeshNodeArray, BaseSkelMesh, LODIndex, BaseSkeletalMeshImportData, bMapMorphTargetToTimeZero);
|
|
//Save the rawMesh
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
BaseSkelMesh->SaveLODImportedData(LODIndex, BaseSkeletalMeshImportData);
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
}
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::AddTokenizedErrorMessage(TSharedRef<FTokenizedMessage> Error, FName FbxErrorName)
|
|
{
|
|
// check to see if Logger exists, this way, we guarantee only prints to FBX import
|
|
// when we meant to print
|
|
if (Logger)
|
|
{
|
|
Logger->TokenizedErrorMessages.Add(Error);
|
|
|
|
if(FbxErrorName != NAME_None)
|
|
{
|
|
Error->AddToken(FFbxErrorToken::Create(FbxErrorName));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// if not found, use normal log
|
|
UE_LOG(LogFbx, Warning, TEXT("%s"), *(Error->ToText().ToString()));
|
|
}
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::ClearTokenizedErrorMessages()
|
|
{
|
|
if(Logger)
|
|
{
|
|
Logger->TokenizedErrorMessages.Empty();
|
|
}
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::FlushToTokenizedErrorMessage(EMessageSeverity::Type Severity)
|
|
{
|
|
if (!ErrorMessage.IsEmpty())
|
|
{
|
|
AddTokenizedErrorMessage(FTokenizedMessage::Create(Severity, FText::Format(FText::FromString("{0}"), FText::FromString(ErrorMessage))), NAME_None);
|
|
}
|
|
}
|
|
|
|
void UnFbx::FFbxImporter::SetLogger(class FFbxLogger * InLogger)
|
|
{
|
|
// this should be only called by top level functions
|
|
// if you set it you can't set it again. Otherwise, you'll lose all log information
|
|
check(Logger == NULL);
|
|
Logger = InLogger;
|
|
}
|
|
|
|
// just in case if DeleteScene/CleanUp is getting called too late
|
|
void UnFbx::FFbxImporter::ClearLogger()
|
|
{
|
|
Logger = NULL;
|
|
}
|
|
|
|
FFbxLogger::FFbxLogger()
|
|
{
|
|
}
|
|
|
|
FFbxLogger::~FFbxLogger()
|
|
{
|
|
bool ShowLogMessage = !ShowLogMessageOnlyIfError;
|
|
if (ShowLogMessageOnlyIfError)
|
|
{
|
|
for (TSharedRef<FTokenizedMessage> TokenMessage : TokenizedErrorMessages)
|
|
{
|
|
if (TokenMessage->GetSeverity() == EMessageSeverity::Error)
|
|
{
|
|
ShowLogMessage = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
//Always clear the old message after an import or re-import
|
|
const TCHAR* LogTitle = TEXT("FBXImport");
|
|
FMessageLogModule& MessageLogModule = FModuleManager::LoadModuleChecked<FMessageLogModule>("MessageLog");
|
|
TSharedPtr<class IMessageLogListing> LogListing = MessageLogModule.GetLogListing(LogTitle);
|
|
LogListing->SetLabel(FText::FromString("FBX Import"));
|
|
LogListing->ClearMessages();
|
|
|
|
if(TokenizedErrorMessages.Num() > 0)
|
|
{
|
|
LogListing->AddMessages(TokenizedErrorMessages);
|
|
if (ShowLogMessage)
|
|
{
|
|
MessageLogModule.OpenMessageLog(LogTitle);
|
|
}
|
|
}
|
|
}
|
|
|
|
UnFbx::FFbxScopedOperation::FFbxScopedOperation(FFbxImporter* FbxImporter) : Importer(FbxImporter)
|
|
{
|
|
check(Importer);
|
|
|
|
if (Importer->ImportOperationStack++ == 0)
|
|
{
|
|
Importer->bImportOperationCanceled = false;
|
|
}
|
|
}
|
|
|
|
UnFbx::FFbxScopedOperation::~FFbxScopedOperation()
|
|
{
|
|
check(Importer->ImportOperationStack > 0);
|
|
Importer->ImportOperationStack--;
|
|
}
|
|
#undef LOCTEXT_NAMESPACE
|