981 lines
32 KiB
C++
981 lines
32 KiB
C++
// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
|
|
|
|
#if COMPILE_WITH_MODEL_TOOL && USE_AUTODESK_FBX_SDK
|
|
|
|
#include "ModelTool.h"
|
|
#include "Engine/Core/Log.h"
|
|
#include "Engine/Core/Collections/Array.h"
|
|
#include "Engine/Core/Math/Matrix.h"
|
|
#include "Engine/Threading/Threading.h"
|
|
|
|
// Import Autodesk FBX SDK
|
|
#define FBXSDK_NEW_API
|
|
#include <fbxsdk.h>
|
|
|
|
class FbxSdkManager
|
|
{
|
|
public:
|
|
|
|
static FbxManager* Manager;
|
|
static CriticalSection Locker;
|
|
|
|
static void Init()
|
|
{
|
|
if (Manager == nullptr)
|
|
{
|
|
LOG_STR(Info, String("Autodesk FBX SDK " FBXSDK_VERSION_STRING_FULL));
|
|
|
|
Manager = FbxManager::Create();
|
|
if (Manager == nullptr)
|
|
{
|
|
LOG(Fatal, "Autodesk FBX SDK failed to initialize.");
|
|
return;
|
|
}
|
|
|
|
FbxIOSettings* ios = FbxIOSettings::Create(Manager, IOSROOT);
|
|
ios->SetBoolProp(IMP_FBX_TEXTURE, false);
|
|
ios->SetBoolProp(IMP_FBX_GOBO, false);
|
|
Manager->SetIOSettings(ios);
|
|
}
|
|
}
|
|
};
|
|
|
|
FbxManager* FbxSdkManager::Manager = nullptr;
|
|
CriticalSection FbxSdkManager::Locker;
|
|
|
|
Matrix ToFlaxType(const FbxAMatrix& value)
|
|
{
|
|
Matrix native;
|
|
for (int32 row = 0; row < 4; row++)
|
|
for (int32 col = 0; col < 4; col++)
|
|
native.Values[row][col] = (float)value[col][row];
|
|
|
|
return native;
|
|
}
|
|
|
|
Vector3 ToFlaxType(const FbxVector4& value)
|
|
{
|
|
Vector3 native;
|
|
native.X = (float)value[0];
|
|
native.Y = (float)value[1];
|
|
native.Z = (float)value[2];
|
|
|
|
return native;
|
|
}
|
|
|
|
Vector3 ToFlaxType(const FbxDouble3& value)
|
|
{
|
|
Vector3 native;
|
|
native.X = (float)value[0];
|
|
native.Y = (float)value[1];
|
|
native.Z = (float)value[2];
|
|
|
|
return native;
|
|
}
|
|
|
|
Vector2 ToFlaxType(const FbxVector2& value)
|
|
{
|
|
Vector2 native;
|
|
native.X = (float)value[0];
|
|
native.Y = 1 - (float)value[1];
|
|
|
|
return native;
|
|
}
|
|
|
|
Color ToFlaxType(const FbxColor& value)
|
|
{
|
|
Color native;
|
|
native.R = (float)value[0];
|
|
native.G = (float)value[1];
|
|
native.B = (float)value[2];
|
|
native.A = (float)value[3];
|
|
|
|
return native;
|
|
}
|
|
|
|
int ToFlaxType(const int& value)
|
|
{
|
|
return value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a single node in the FBX transform hierarchy.
|
|
/// </summary>
|
|
struct Node
|
|
{
|
|
/// <summary>
|
|
/// The parent index. The root node uses value -1.
|
|
/// </summary>
|
|
int32 ParentIndex;
|
|
|
|
/// <summary>
|
|
/// The local transformation of the node, relative to parent node.
|
|
/// </summary>
|
|
Transform LocalTransform;
|
|
|
|
/// <summary>
|
|
/// The name of this node.
|
|
/// </summary>
|
|
String Name;
|
|
|
|
/// <summary>
|
|
/// The LOD index of the data in this node (used to separate meshes across different level of details).
|
|
/// </summary>
|
|
int32 LodIndex;
|
|
|
|
Matrix GeomTransform;
|
|
Matrix WorldTransform;
|
|
FbxNode* FbxNode;
|
|
};
|
|
|
|
struct Bone
|
|
{
|
|
/// <summary>
|
|
/// The index of the related node.
|
|
/// </summary>
|
|
int32 NodeIndex;
|
|
|
|
/// <summary>
|
|
/// The parent bone index. The root bone uses value -1.
|
|
/// </summary>
|
|
int32 ParentBoneIndex;
|
|
|
|
/// <summary>
|
|
/// The name of this bone.
|
|
/// </summary>
|
|
String Name;
|
|
|
|
/// <summary>
|
|
/// The matrix that transforms from mesh space to bone space in bind pose.
|
|
/// </summary>
|
|
Matrix OffsetMatrix;
|
|
};
|
|
|
|
struct ImporterData
|
|
{
|
|
ImportedModelData& Model;
|
|
const FbxScene* Scene;
|
|
const ModelTool::Options& Options;
|
|
|
|
Array<Node> Nodes;
|
|
Array<Bone> Bones;
|
|
|
|
Dictionary<FbxMesh*, MeshData*> Meshes;
|
|
Array<FbxSurfaceMaterial*> Materials;
|
|
|
|
ImporterData(ImportedModelData& model, const ModelTool::Options& options, const FbxScene* scene)
|
|
: Model(model)
|
|
, Scene(scene)
|
|
, Options(options)
|
|
, Nodes(256)
|
|
, Meshes(256)
|
|
, Materials(64)
|
|
{
|
|
}
|
|
|
|
int32 FindNode(FbxNode* fbxNode)
|
|
{
|
|
for (int32 i = 0; i < Nodes.Count(); i++)
|
|
{
|
|
if (Nodes[i].FbxNode == fbxNode)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int32 FindNode(const String& name, StringSearchCase caseSensitivity = StringSearchCase::CaseSensitive)
|
|
{
|
|
for (int32 i = 0; i < Nodes.Count(); i++)
|
|
{
|
|
if (Nodes[i].Name.Compare(name, caseSensitivity) == 0)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int32 FindBone(const String& name, StringSearchCase caseSensitivity = StringSearchCase::CaseSensitive)
|
|
{
|
|
for (int32 i = 0; i < Bones.Count(); i++)
|
|
{
|
|
if (Bones[i].Name.Compare(name, caseSensitivity) == 0)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
int32 FindBone(const int32 nodeIndex)
|
|
{
|
|
for (int32 i = 0; i < Bones.Count(); i++)
|
|
{
|
|
if (Bones[i].NodeIndex == nodeIndex)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
};
|
|
|
|
void ProcessNodes(ImporterData& data, FbxNode* fbxNode, int32 parentIndex)
|
|
{
|
|
const int32 nodeIndex = data.Nodes.Count();
|
|
|
|
Vector3 translation = ToFlaxType(fbxNode->EvaluateLocalTranslation(FbxTime(0)));
|
|
Vector3 rotationEuler = ToFlaxType(fbxNode->EvaluateLocalRotation(FbxTime(0)));
|
|
Vector3 scale = ToFlaxType(fbxNode->EvaluateLocalScaling(FbxTime(0)));
|
|
Quaternion rotation = Quaternion::Euler(rotationEuler);
|
|
|
|
// Create node
|
|
Node node;
|
|
node.ParentIndex = parentIndex;
|
|
node.Name = String(fbxNode->GetNameWithoutNameSpacePrefix().Buffer());
|
|
node.LocalTransform = Transform(translation, rotation, scale);
|
|
node.FbxNode = fbxNode;
|
|
|
|
// Geometry transform is applied to geometry (mesh data) only, it is not inherited by children, so we store it separately
|
|
Vector3 geomTrans = ToFlaxType(fbxNode->GeometricTranslation.Get());
|
|
Vector3 geomRotEuler = ToFlaxType(fbxNode->GeometricRotation.Get());
|
|
Vector3 geomScale = ToFlaxType(fbxNode->GeometricScaling.Get());
|
|
Quaternion geomRotation = Quaternion::Euler(geomRotEuler);
|
|
Transform(geomTrans, geomRotation, geomScale).GetWorld(node.GeomTransform);
|
|
|
|
// Pick node LOD index
|
|
if (parentIndex == -1 || !data.Options.ImportLODs)
|
|
{
|
|
node.LodIndex = 0;
|
|
}
|
|
else
|
|
{
|
|
node.LodIndex = data.Nodes[parentIndex].LodIndex;
|
|
if (node.LodIndex == 0)
|
|
{
|
|
node.LodIndex = ModelTool::DetectLodIndex(node.Name);
|
|
}
|
|
ASSERT(Math::IsInRange(node.LodIndex, 0, MODEL_MAX_LODS - 1));
|
|
}
|
|
|
|
if (parentIndex == -1)
|
|
{
|
|
node.LocalTransform.GetWorld(node.WorldTransform);
|
|
}
|
|
else
|
|
{
|
|
node.WorldTransform = data.Nodes[parentIndex].WorldTransform * node.LocalTransform.GetWorld();
|
|
}
|
|
data.Nodes.Add(node);
|
|
|
|
// Process the children
|
|
for (int i = 0; i < fbxNode->GetChildCount(); i++)
|
|
{
|
|
ProcessNodes(data, fbxNode->GetChild(i), nodeIndex);
|
|
}
|
|
}
|
|
|
|
template<class TFBX, class TNative>
|
|
void ReadLayerData(FbxMesh* fbxMesh, FbxLayerElementTemplate<TFBX>& layer, Array<TNative>& output)
|
|
{
|
|
if (layer.GetDirectArray().GetCount() == 0)
|
|
return;
|
|
|
|
int32 vertexCount = fbxMesh->GetControlPointsCount();
|
|
int32 triangleCount = fbxMesh->GetPolygonCount();
|
|
output.Resize(vertexCount);
|
|
|
|
switch (layer.GetMappingMode())
|
|
{
|
|
case FbxLayerElement::eByControlPoint:
|
|
{
|
|
for (int vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++)
|
|
{
|
|
int index = 0;
|
|
if (layer.GetReferenceMode() == FbxGeometryElement::eDirect)
|
|
index = vertexIndex;
|
|
else if (layer.GetReferenceMode() == FbxGeometryElement::eIndexToDirect)
|
|
index = layer.GetIndexArray().GetAt(vertexIndex);
|
|
|
|
output[vertexIndex] = ToFlaxType(layer.GetDirectArray().GetAt(index));
|
|
}
|
|
}
|
|
break;
|
|
case FbxLayerElement::eByPolygonVertex:
|
|
{
|
|
int indexByPolygonVertex = 0;
|
|
|
|
for (int polygonIndex = 0; polygonIndex < triangleCount; polygonIndex++)
|
|
{
|
|
const int polygonSize = fbxMesh->GetPolygonSize(polygonIndex);
|
|
for (int i = 0; i < polygonSize; i++)
|
|
{
|
|
int index = 0;
|
|
if (layer.GetReferenceMode() == FbxGeometryElement::eDirect)
|
|
index = indexByPolygonVertex;
|
|
else if (layer.GetReferenceMode() == FbxGeometryElement::eIndexToDirect)
|
|
index = layer.GetIndexArray().GetAt(indexByPolygonVertex);
|
|
|
|
int vertexIndex = fbxMesh->GetPolygonVertex(polygonIndex, i);
|
|
output[vertexIndex] = ToFlaxType(layer.GetDirectArray().GetAt(index));
|
|
|
|
indexByPolygonVertex++;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case FbxLayerElement::eAllSame:
|
|
{
|
|
output[0] = ToFlaxType(layer.GetDirectArray().GetAt(0));
|
|
for (int vertexIndex = 1; vertexIndex < vertexCount; vertexIndex++)
|
|
{
|
|
output[vertexIndex] = output[0];
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
LOG(Warning, "Unsupported layer mapping mode.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool IsGroupMappingModeByEdge(FbxLayerElement* layerElement)
|
|
{
|
|
return layerElement->GetMappingMode() == FbxLayerElement::eByEdge;
|
|
}
|
|
|
|
bool ProcessMesh(ImporterData& data, FbxMesh* fbxMesh, MeshData& mesh, String& errorMsg)
|
|
{
|
|
// Properties
|
|
mesh.Name = fbxMesh->GetName();
|
|
mesh.MaterialSlotIndex = -1;
|
|
if (fbxMesh->GetElementMaterial())
|
|
{
|
|
const auto materialIndices = &(fbxMesh->GetElementMaterial()->GetIndexArray());
|
|
if (materialIndices)
|
|
{
|
|
mesh.MaterialSlotIndex = materialIndices->GetAt(0);
|
|
}
|
|
}
|
|
|
|
int32 vertexCount = fbxMesh->GetControlPointsCount();
|
|
int32 triangleCount = fbxMesh->GetPolygonCount();
|
|
FbxVector4* controlPoints = fbxMesh->GetControlPoints();
|
|
FbxGeometryElementNormal* normalElement = fbxMesh->GetElementNormal();
|
|
FbxGeometryElementTangent* tangentElement = fbxMesh->GetElementTangent();
|
|
|
|
// Regenerate data if necessary
|
|
if (normalElement == nullptr || data.Options.CalculateNormals)
|
|
{
|
|
fbxMesh->GenerateNormals(true, false, false);
|
|
normalElement = fbxMesh->GetElementNormal();
|
|
}
|
|
if (tangentElement == nullptr || data.Options.CalculateTangents)
|
|
{
|
|
fbxMesh->GenerateTangentsData(0, true);
|
|
tangentElement = fbxMesh->GetElementTangent();
|
|
}
|
|
|
|
bool needEdgeIndexing = false;
|
|
if (normalElement)
|
|
needEdgeIndexing |= IsGroupMappingModeByEdge(normalElement);
|
|
|
|
// Vertex positions
|
|
mesh.Positions.Resize(vertexCount, false);
|
|
for (int32 i = 0; i < vertexCount; i++)
|
|
{
|
|
mesh.Positions[i] = ToFlaxType(controlPoints[i]);
|
|
}
|
|
|
|
// Indices
|
|
const int32 indexCount = triangleCount * 3;
|
|
mesh.Indices.Resize(indexCount, false);
|
|
int* fbxIndices = fbxMesh->GetPolygonVertices();
|
|
for (int32 i = 0; i < indexCount; i++)
|
|
{
|
|
mesh.Indices[i] = fbxIndices[i];
|
|
}
|
|
|
|
// Texture coordinates
|
|
FbxGeometryElementUV* texcoords = fbxMesh->GetElementUV(0);
|
|
if (texcoords)
|
|
{
|
|
ReadLayerData(fbxMesh, *texcoords, mesh.UVs);
|
|
}
|
|
|
|
// Normals
|
|
if (normalElement)
|
|
{
|
|
ReadLayerData(fbxMesh, *normalElement, mesh.Normals);
|
|
}
|
|
|
|
// Tangents
|
|
if (tangentElement)
|
|
{
|
|
ReadLayerData(fbxMesh, *tangentElement, mesh.Tangents);
|
|
}
|
|
|
|
// Lightmap UVs
|
|
if (data.Options.LightmapUVsSource == ModelLightmapUVsSource::Disable)
|
|
{
|
|
// No lightmap UVs
|
|
}
|
|
else if (data.Options.LightmapUVsSource == ModelLightmapUVsSource::Generate)
|
|
{
|
|
// Generate lightmap UVs
|
|
if (mesh.GenerateLightmapUVs())
|
|
{
|
|
// TODO: we could propagate this message to Debug Console in editor? or create interface to gather some msgs from importing service
|
|
LOG(Warning, "Failed to generate lightmap uvs");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Select input channel index
|
|
int32 inputChannelIndex;
|
|
switch (data.Options.LightmapUVsSource)
|
|
{
|
|
case ModelLightmapUVsSource::Channel0:
|
|
inputChannelIndex = 0;
|
|
break;
|
|
case ModelLightmapUVsSource::Channel1:
|
|
inputChannelIndex = 1;
|
|
break;
|
|
case ModelLightmapUVsSource::Channel2:
|
|
inputChannelIndex = 2;
|
|
break;
|
|
case ModelLightmapUVsSource::Channel3:
|
|
inputChannelIndex = 3;
|
|
break;
|
|
default:
|
|
inputChannelIndex = INVALID_INDEX;
|
|
break;
|
|
}
|
|
|
|
// Check if has that channel texcoords
|
|
if (inputChannelIndex >= 0 && inputChannelIndex < fbxMesh->GetElementUVCount() && fbxMesh->GetElementUV(inputChannelIndex))
|
|
{
|
|
ReadLayerData(fbxMesh, *fbxMesh->GetElementUV(inputChannelIndex), mesh.LightmapUVs);
|
|
}
|
|
else
|
|
{
|
|
// TODO: we could propagate this message to Debug Console in editor? or create interface to gather some msgs from importing service
|
|
LOG(Warning, "Cannot import model lightmap uvs. Missing texcoords channel {0}.", inputChannelIndex);
|
|
}
|
|
}
|
|
|
|
// Vertex Colors
|
|
if (data.Options.ImportVertexColors && fbxMesh->GetElementVertexColorCount() > 0)
|
|
{
|
|
auto vertexColorElement = fbxMesh->GetElementVertexColor(0);
|
|
ReadLayerData(fbxMesh, *vertexColorElement, mesh.Colors);
|
|
}
|
|
|
|
// Blend Indices and Blend Weights
|
|
const int skinDeformerCount = fbxMesh->GetDeformerCount(FbxDeformer::eSkin);
|
|
if (skinDeformerCount > 0)
|
|
{
|
|
const int32 vertexCount = mesh.Positions.Count();
|
|
mesh.BlendIndices.Resize(vertexCount);
|
|
mesh.BlendWeights.Resize(vertexCount);
|
|
mesh.BlendIndices.SetAll(Int4::Zero);
|
|
mesh.BlendWeights.SetAll(Vector4::Zero);
|
|
|
|
for (int deformerIndex = 0; deformerIndex < skinDeformerCount; deformerIndex++)
|
|
{
|
|
FbxSkin* skin = FbxCast<FbxSkin>(fbxMesh->GetDeformer(deformerIndex, FbxDeformer::eSkin));
|
|
int totalClusterCount = skin->GetClusterCount();
|
|
|
|
for (int clusterIndex = 0; clusterIndex < totalClusterCount; ++clusterIndex)
|
|
{
|
|
FbxCluster* cluster = skin->GetCluster(clusterIndex);
|
|
int indexCount = cluster->GetControlPointIndicesCount();
|
|
if (indexCount == 0)
|
|
continue;
|
|
FbxNode* link = cluster->GetLink();
|
|
const String boneName(link->GetName());
|
|
|
|
// Find the node where the bone is mapped - based on the name
|
|
const int32 nodeIndex = data.FindNode(link);
|
|
if (nodeIndex == -1)
|
|
{
|
|
LOG(Warning, "Invalid mesh bone linkage. Mesh: {0}, bone: {1}. Skipping...", mesh.Name, boneName);
|
|
continue;
|
|
}
|
|
|
|
// Create bone if missing
|
|
int32 boneIndex = data.FindBone(boneName);
|
|
if (boneIndex == -1)
|
|
{
|
|
// Find the parent bone
|
|
int32 parentBoneIndex = -1;
|
|
for (int32 i = nodeIndex; i != -1; i = data.Nodes[i].ParentIndex)
|
|
{
|
|
parentBoneIndex = data.FindBone(i);
|
|
if (parentBoneIndex != -1)
|
|
break;
|
|
}
|
|
|
|
// Add bone
|
|
boneIndex = data.Bones.Count();
|
|
data.Bones.EnsureCapacity(Math::Max(128, boneIndex + 16));
|
|
data.Bones.Resize(boneIndex + 1);
|
|
auto& bone = data.Bones[boneIndex];
|
|
|
|
FbxAMatrix transformMatrix;
|
|
FbxAMatrix transformLinkMatrix;
|
|
cluster->GetTransformMatrix(transformMatrix);
|
|
cluster->GetTransformLinkMatrix(transformLinkMatrix);
|
|
const auto globalBindposeInverseMatrix = transformLinkMatrix.Inverse() * transformMatrix;
|
|
|
|
// Setup bone
|
|
bone.Name = boneName;
|
|
bone.NodeIndex = nodeIndex;
|
|
bone.ParentBoneIndex = parentBoneIndex;
|
|
bone.OffsetMatrix = ToFlaxType(globalBindposeInverseMatrix);
|
|
}
|
|
|
|
// Apply the bone influences
|
|
int* cluserIndices = cluster->GetControlPointIndices();
|
|
double* cluserWeights = cluster->GetControlPointWeights();
|
|
for (int j = 0; j < indexCount; j++)
|
|
{
|
|
const int vtxWeightId = cluserIndices[j];
|
|
|
|
if (vtxWeightId >= vertexCount)
|
|
continue;
|
|
|
|
const auto vtxWeight = (float)cluserWeights[j];
|
|
|
|
if (vtxWeight <= 0 || isnan(vtxWeight) || isinf(vtxWeight))
|
|
continue;
|
|
|
|
auto& indices = mesh.BlendIndices[vtxWeightId];
|
|
auto& weights = mesh.BlendWeights[vtxWeightId];
|
|
|
|
for (int32 k = 0; k < 4; k++)
|
|
{
|
|
if (vtxWeight >= weights.Raw[k])
|
|
{
|
|
for (int32 l = 2; l >= k; l--)
|
|
{
|
|
indices.Raw[l + 1] = indices.Raw[l];
|
|
weights.Raw[l + 1] = weights.Raw[l];
|
|
}
|
|
|
|
indices.Raw[k] = boneIndex;
|
|
weights.Raw[k] = vtxWeight;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
mesh.NormalizeBlendWeights();
|
|
}
|
|
|
|
// Blend Shapes
|
|
const int blendShapeDeformerCount = fbxMesh->GetDeformerCount(FbxDeformer::eBlendShape);
|
|
if (blendShapeDeformerCount > 0 && data.Model.Types & ImportDataTypes::Skeleton && data.Options.ImportBlendShapes)
|
|
{
|
|
mesh.BlendShapes.EnsureCapacity(blendShapeDeformerCount);
|
|
for (int deformerIndex = 0; deformerIndex < skinDeformerCount; deformerIndex++)
|
|
{
|
|
FbxBlendShape* blendShape = FbxCast<FbxBlendShape>(fbxMesh->GetDeformer(deformerIndex, FbxDeformer::eBlendShape));
|
|
|
|
const int blendShapeChannelCount = blendShape->GetBlendShapeChannelCount();
|
|
for (int32 channelIndex = 0; channelIndex < blendShapeChannelCount; channelIndex++)
|
|
{
|
|
FbxBlendShapeChannel* blendShapeChannel = blendShape->GetBlendShapeChannel(channelIndex);
|
|
|
|
// Use last shape
|
|
const int shapeCount = blendShapeChannel->GetTargetShapeCount();
|
|
if (shapeCount == 0)
|
|
continue;
|
|
FbxShape* shape = blendShapeChannel->GetTargetShape(shapeCount - 1);
|
|
|
|
int shapeControlPointsCount = shape->GetControlPointsCount();
|
|
if (shapeControlPointsCount != vertexCount)
|
|
continue;
|
|
|
|
BlendShape& blendShapeData = mesh.BlendShapes.AddOne();
|
|
blendShapeData.Name = blendShapeChannel->GetName();
|
|
const auto dotPos = blendShapeData.Name.Find('.');
|
|
if (dotPos != -1)
|
|
blendShapeData.Name = blendShapeData.Name.Substring(dotPos + 1);
|
|
blendShapeData.Weight = blendShapeChannel->GetTargetShapeCount() > 1 ? (float)(blendShapeChannel->DeformPercent.Get() / 100.0) : 1.0f;
|
|
|
|
FbxVector4* shapeControlPoints = shape->GetControlPoints();
|
|
blendShapeData.Vertices.Resize(shapeControlPointsCount);
|
|
for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
|
|
blendShapeData.Vertices[i].VertexIndex = i;
|
|
for (int i = 0; i < blendShapeData.Vertices.Count(); i++)
|
|
blendShapeData.Vertices[i].PositionDelta = ToFlaxType(shapeControlPoints[i] - controlPoints[i]);
|
|
// TODO: support importing normals from blend shape
|
|
for (int32 i = 0; i < blendShapeData.Vertices.Count(); i++)
|
|
blendShapeData.Vertices[i].NormalDelta = Vector3::Zero;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Flip the Y in texcoords
|
|
for (int32 i = 0; i < mesh.UVs.Count(); i++)
|
|
mesh.UVs[i].Y = 1.0f - mesh.UVs[i].Y;
|
|
for (int32 i = 0; i < mesh.LightmapUVs.Count(); i++)
|
|
mesh.LightmapUVs[i].Y = 1.0f - mesh.LightmapUVs[i].Y;
|
|
|
|
// Handle missing material case (could never happen but it's better to be sure it will work)
|
|
if (mesh.MaterialSlotIndex == -1)
|
|
{
|
|
mesh.MaterialSlotIndex = 0;
|
|
LOG(Warning, "Mesh \'{0}\' has missing material slot.", mesh.Name);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ImportMesh(ImporterData& data, int32 nodeIndex, FbxMesh* fbxMesh, String& errorMsg)
|
|
{
|
|
auto& model = data.Model;
|
|
|
|
// Skip invalid meshes
|
|
if (!fbxMesh->IsTriangleMesh() || fbxMesh->GetControlPointsCount() == 0 || fbxMesh->GetPolygonCount() == 0)
|
|
return false;
|
|
|
|
// Check if that mesh has been already imported (instanced geometry)
|
|
MeshData* meshData = nullptr;
|
|
if (data.Meshes.TryGet(fbxMesh, meshData) && meshData)
|
|
{
|
|
// Clone mesh
|
|
meshData = New<MeshData>(*meshData);
|
|
}
|
|
else
|
|
{
|
|
// Import mesh data
|
|
meshData = New<MeshData>();
|
|
if (ProcessMesh(data, fbxMesh, *meshData, errorMsg))
|
|
return true;
|
|
}
|
|
|
|
// Link mesh
|
|
meshData->NodeIndex = nodeIndex;
|
|
auto& node = data.Nodes[nodeIndex];
|
|
const auto lodIndex = node.LodIndex;
|
|
if (model.LODs.Count() <= lodIndex)
|
|
model.LODs.Resize(lodIndex + 1);
|
|
model.LODs[lodIndex].Meshes.Add(meshData);
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ImportMesh(ImporterData& data, int32 nodeIndex, String& errorMsg)
|
|
{
|
|
auto fbxNode = data.Nodes[nodeIndex].FbxNode;
|
|
|
|
// Process the node's attributes
|
|
for (int i = 0; i < fbxNode->GetNodeAttributeCount(); i++)
|
|
{
|
|
auto attribute = fbxNode->GetNodeAttributeByIndex(i);
|
|
if (!attribute)
|
|
continue;
|
|
|
|
switch (attribute->GetAttributeType())
|
|
{
|
|
case FbxNodeAttribute::eNurbs:
|
|
case FbxNodeAttribute::eNurbsSurface:
|
|
case FbxNodeAttribute::ePatch:
|
|
{
|
|
FbxGeometryConverter geomConverter(FbxSdkManager::Manager);
|
|
attribute = geomConverter.Triangulate(attribute, true);
|
|
|
|
if (attribute->GetAttributeType() == FbxNodeAttribute::eMesh)
|
|
{
|
|
FbxMesh* mesh = static_cast<FbxMesh*>(attribute);
|
|
mesh->RemoveBadPolygons();
|
|
|
|
if (ImportMesh(data, nodeIndex, mesh, errorMsg))
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
case FbxNodeAttribute::eMesh:
|
|
{
|
|
FbxMesh* mesh = static_cast<FbxMesh*>(attribute);
|
|
mesh->RemoveBadPolygons();
|
|
|
|
if (!mesh->IsTriangleMesh())
|
|
{
|
|
FbxGeometryConverter geomConverter(FbxSdkManager::Manager);
|
|
geomConverter.Triangulate(mesh, true);
|
|
attribute = fbxNode->GetNodeAttribute();
|
|
mesh = static_cast<FbxMesh*>(attribute);
|
|
}
|
|
|
|
if (ImportMesh(data, nodeIndex, mesh, errorMsg))
|
|
return true;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
bool ImportMeshes(ImporterData& data, String& errorMsg)
|
|
{
|
|
for (int32 i = 0; i < data.Nodes.Count(); i++)
|
|
{
|
|
if (ImportMesh(data, i, errorMsg))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
void ImportCurve(aiVectorKey* keys, uint32 keysCount, LinearCurve<Vector3>& curve)
|
|
{
|
|
if (keys == nullptr || keysCount == 0)
|
|
return;
|
|
|
|
const auto keyframes = curve.Resize(keysCount);
|
|
|
|
for (uint32 i = 0; i < keysCount; i++)
|
|
{
|
|
auto& aKey = keys[i];
|
|
auto& key = keyframes[i];
|
|
|
|
key.Time = (float)aKey.mTime;
|
|
key.Value = ToVector3(aKey.mValue);
|
|
}
|
|
}
|
|
|
|
void ImportCurve(aiQuatKey* keys, uint32 keysCount, LinearCurve<Quaternion>& curve)
|
|
{
|
|
if (keys == nullptr || keysCount == 0)
|
|
return;
|
|
|
|
const auto keyframes = curve.Resize(keysCount);
|
|
|
|
for (uint32 i = 0; i < keysCount; i++)
|
|
{
|
|
auto& aKey = keys[i];
|
|
auto& key = keyframes[i];
|
|
|
|
key.Time = (float)aKey.mTime;
|
|
key.Value = ToQuaternion(aKey.mValue);
|
|
}
|
|
}
|
|
*/
|
|
|
|
/// <summary>
|
|
/// Bakes the node transformations.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// FBX stores transforms in a more complex way than just translation-rotation-scale as used by Flax Engine.
|
|
/// Instead they also support rotations offsets and pivots, scaling pivots and more. We wish to bake all this data
|
|
/// into a standard transform so we can access it using node's local TRS properties (e.g. FbxNode::LclTranslation).
|
|
/// </remarks>
|
|
/// <param name="scene">The FBX scene.</param>
|
|
void BakeTransforms(FbxScene* scene)
|
|
{
|
|
double frameRate = FbxTime::GetFrameRate(scene->GetGlobalSettings().GetTimeMode());
|
|
|
|
Array<FbxNode*> todo;
|
|
todo.Push(scene->GetRootNode());
|
|
|
|
while (todo.HasItems())
|
|
{
|
|
FbxNode* node = todo.Pop();
|
|
|
|
FbxVector4 zero(0, 0, 0);
|
|
FbxVector4 one(1, 1, 1);
|
|
|
|
// Activate pivot converting
|
|
node->SetPivotState(FbxNode::eSourcePivot, FbxNode::ePivotActive);
|
|
node->SetPivotState(FbxNode::eDestinationPivot, FbxNode::ePivotActive);
|
|
|
|
// We want to set all these to 0 (1 for scale) and bake them into the transforms
|
|
node->SetPostRotation(FbxNode::eDestinationPivot, zero);
|
|
node->SetPreRotation(FbxNode::eDestinationPivot, zero);
|
|
node->SetRotationOffset(FbxNode::eDestinationPivot, zero);
|
|
node->SetScalingOffset(FbxNode::eDestinationPivot, zero);
|
|
node->SetRotationPivot(FbxNode::eDestinationPivot, zero);
|
|
node->SetScalingPivot(FbxNode::eDestinationPivot, zero);
|
|
|
|
// We account for geometric properties separately during node traversal
|
|
node->SetGeometricTranslation(FbxNode::eDestinationPivot, node->GetGeometricTranslation(FbxNode::eSourcePivot));
|
|
node->SetGeometricRotation(FbxNode::eDestinationPivot, node->GetGeometricRotation(FbxNode::eSourcePivot));
|
|
node->SetGeometricScaling(FbxNode::eDestinationPivot, node->GetGeometricScaling(FbxNode::eSourcePivot));
|
|
|
|
// Flax assumes euler angles are in YXZ order
|
|
node->SetRotationOrder(FbxNode::eDestinationPivot, FbxEuler::eOrderYXZ);
|
|
|
|
// Keep interpolation as is
|
|
node->SetQuaternionInterpolation(FbxNode::eDestinationPivot, node->GetQuaternionInterpolation(FbxNode::eSourcePivot));
|
|
|
|
for (int i = 0; i < node->GetChildCount(); i++)
|
|
{
|
|
FbxNode* childNode = node->GetChild(i);
|
|
todo.Push(childNode);
|
|
}
|
|
}
|
|
|
|
scene->GetRootNode()->ConvertPivotAnimationRecursive(nullptr, FbxNode::eDestinationPivot, frameRate, false);
|
|
}
|
|
|
|
bool ModelTool::ImportDataAutodeskFbxSdk(const char* path, ImportedModelData& data, const Options& options, String& errorMsg)
|
|
{
|
|
ScopeLock lock(FbxSdkManager::Locker);
|
|
|
|
// Initialize
|
|
FbxSdkManager::Init();
|
|
auto scene = FbxScene::Create(FbxSdkManager::Manager, "Scene");
|
|
if (scene == nullptr)
|
|
{
|
|
errorMsg = TEXT("Failed to create FBX scene");
|
|
return false;
|
|
}
|
|
|
|
// Import file
|
|
bool importMeshes = (data.Types & ImportDataTypes::Geometry) != 0;
|
|
bool importAnimations = (data.Types & ImportDataTypes::Animations) != 0;
|
|
FbxImporter* importer = FbxImporter::Create(FbxSdkManager::Manager, "");
|
|
auto ios = FbxSdkManager::Manager->GetIOSettings();
|
|
ios->SetBoolProp(IMP_FBX_MODEL, importMeshes);
|
|
ios->SetBoolProp(IMP_FBX_ANIMATION, importAnimations);
|
|
if (!importer->Initialize(path, -1, ios))
|
|
{
|
|
errorMsg = String::Format(TEXT("Failed to initialize FBX importer. {0}"), String(importer->GetStatus().GetErrorString()));
|
|
return false;
|
|
}
|
|
if (!importer->Import(scene))
|
|
{
|
|
errorMsg = String::Format(TEXT("Failed to import FBX scene. {0}"), String(importer->GetStatus().GetErrorString()));
|
|
importer->Destroy();
|
|
return false;
|
|
}
|
|
{
|
|
const FbxAxisSystem fileCoordSystem = scene->GetGlobalSettings().GetAxisSystem();
|
|
FbxAxisSystem bsCoordSystem(FbxAxisSystem::eDirectX);
|
|
if (fileCoordSystem != bsCoordSystem)
|
|
bsCoordSystem.ConvertScene(scene);
|
|
}
|
|
importer->Destroy();
|
|
importer = nullptr;
|
|
|
|
BakeTransforms(scene);
|
|
|
|
// TODO: optimizeMeshes
|
|
|
|
// Process imported scene nodes
|
|
ImporterData importerData(data, options, scene);
|
|
ProcessNodes(importerData, scene->GetRootNode(), -1);
|
|
|
|
// Add all materials
|
|
for (int i = 0; i < scene->GetMaterialCount(); i++)
|
|
{
|
|
importerData.Materials.Add(scene->GetMaterial(i));
|
|
}
|
|
|
|
// Import geometry (meshes and materials)
|
|
if (data.Types & ImportDataTypes::Geometry)
|
|
{
|
|
if (ImportMeshes(importerData, errorMsg))
|
|
{
|
|
LOG(Warning, "Failed to import meshes.");
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// TODO: remove unused materials if meshes merging is disabled
|
|
|
|
// Import skeleton
|
|
if (data.Types & ImportDataTypes::Skeleton)
|
|
{
|
|
data.Skeleton.Nodes.Resize(importerData.Nodes.Count(), false);
|
|
for (int32 i = 0; i < importerData.Nodes.Count(); i++)
|
|
{
|
|
auto& node = data.Skeleton.Nodes[i];
|
|
auto& fbxNode = importerData.Nodes[i];
|
|
|
|
node.Name = fbxNode.Name;
|
|
node.ParentIndex = fbxNode.ParentIndex;
|
|
node.LocalTransform = fbxNode.LocalTransform;
|
|
}
|
|
|
|
data.Skeleton.Bones.Resize(importerData.Bones.Count(), false);
|
|
for (int32 i = 0; i < importerData.Bones.Count(); i++)
|
|
{
|
|
auto& bone = data.Skeleton.Bones[i];
|
|
auto& fbxBone = importerData.Bones[i];
|
|
|
|
const auto boneNodeIndex = fbxBone.NodeIndex;
|
|
const auto parentBoneNodeIndex = fbxBone.ParentBoneIndex == -1 ? -1 : importerData.Bones[fbxBone.ParentBoneIndex].NodeIndex;
|
|
|
|
bone.ParentIndex = fbxBone.ParentBoneIndex;
|
|
bone.NodeIndex = fbxBone.NodeIndex;
|
|
bone.LocalTransform = CombineTransformsFromNodeIndices(importerData.Nodes, parentBoneNodeIndex, boneNodeIndex);
|
|
bone.OffsetMatrix = fbxBone.OffsetMatrix;
|
|
}
|
|
}
|
|
/*
|
|
// Import animations
|
|
if (data.Types & ImportDataTypes::Animations)
|
|
{
|
|
if (scene->HasAnimations())
|
|
{
|
|
const auto animations = scene->mAnimations[0];
|
|
data.Animation.Channels.Resize(animations->mNumChannels, false);
|
|
data.Animation.Duration = animations->mDuration;
|
|
data.Animation.FramesPerSecond = animations->mTicksPerSecond != 0.0 ? animations->mTicksPerSecond : 25.0;
|
|
|
|
for (unsigned i = 0; i < animations->mNumChannels; i++)
|
|
{
|
|
const auto aAnim = animations->mChannels[i];
|
|
auto& anim = data.Animation.Channels[i];
|
|
|
|
anim.NodeName = aAnim->mNodeName.C_Str();
|
|
|
|
ImportCurve(aAnim->mPositionKeys, aAnim->mNumPositionKeys, anim.Position);
|
|
ImportCurve(aAnim->mRotationKeys, aAnim->mNumRotationKeys, anim.Rotation);
|
|
ImportCurve(aAnim->mScalingKeys, aAnim->mNumScalingKeys, anim.Scale);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
LOG(Warning, "Loaded scene has no animations");
|
|
}
|
|
}
|
|
*/
|
|
// Import nodes
|
|
if (data.Types & ImportDataTypes::Nodes)
|
|
{
|
|
data.Nodes.Resize(importerData.Nodes.Count());
|
|
for (int32 i = 0; i < importerData.Nodes.Count(); i++)
|
|
{
|
|
auto& node = data.Nodes[i];
|
|
auto& aNode = importerData.Nodes[i];
|
|
|
|
node.Name = aNode.Name;
|
|
node.ParentIndex = aNode.ParentIndex;
|
|
node.LocalTransform = aNode.LocalTransform;
|
|
}
|
|
}
|
|
|
|
// Export materials info
|
|
const int32 materialsCount = importerData.Materials.Count();
|
|
data.Materials.Resize(materialsCount, false);
|
|
for (int32 i = 0; i < importerData.Materials.Count(); i++)
|
|
{
|
|
auto& material = data.Materials[i];
|
|
const auto fbxMaterial = importerData.Materials[i];
|
|
|
|
material.Name = String(fbxMaterial->GetName()).TrimTrailing();
|
|
material.MaterialID = Guid::Empty;
|
|
}
|
|
|
|
scene->Clear();
|
|
|
|
return false;
|
|
}
|
|
|
|
#endif
|