Refactor objects splitting in models importing to be handled by ModelTool not the importer code itself

This commit is contained in:
Wojtek Figat
2023-12-01 13:57:08 +01:00
parent 6e92d3103c
commit a808bcdbf6
12 changed files with 630 additions and 663 deletions

View File

@@ -4,7 +4,6 @@
#include "ModelTool.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/DeleteMe.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Platform/FileSystem.h"
@@ -28,7 +27,6 @@ using namespace Assimp;
class AssimpLogStream : public LogStream
{
public:
AssimpLogStream()
{
DefaultLogger::create("");
@@ -612,32 +610,35 @@ bool IsMeshInvalid(const aiMesh* aMesh)
return aMesh->mPrimitiveTypes != aiPrimitiveType_TRIANGLE || aMesh->mNumVertices == 0 || aMesh->mNumFaces == 0 || aMesh->mFaces[0].mNumIndices != 3;
}
bool ImportMesh(int32 i, ModelData& result, AssimpImporterData& data, String& errorMsg)
bool ImportMesh(int32 index, ModelData& result, AssimpImporterData& data, String& errorMsg)
{
const auto aMesh = data.Scene->mMeshes[i];
const auto aMesh = data.Scene->mMeshes[index];
// Skip invalid meshes
if (IsMeshInvalid(aMesh))
return false;
// Skip unused meshes
if (!data.MeshIndexToNodeIndex.ContainsKey(i))
if (!data.MeshIndexToNodeIndex.ContainsKey(index))
return false;
// Import mesh data
MeshData* meshData = New<MeshData>();
if (ProcessMesh(result, data, aMesh, *meshData, errorMsg))
return true;
auto& nodesWithMesh = data.MeshIndexToNodeIndex[i];
for (int32 j = 0; j < nodesWithMesh.Count(); j++)
{
const auto nodeIndex = nodesWithMesh[j];
Delete(meshData);
return true;
}
auto& nodesWithMesh = data.MeshIndexToNodeIndex[index];
for (int32 i = 0; i < nodesWithMesh.Count(); i++)
{
const auto nodeIndex = nodesWithMesh[i];
auto& node = data.Nodes[nodeIndex];
const int32 lodIndex = node.LodIndex;
// The first mesh instance uses meshData directly while others have to clone it
if (j != 0)
if (i != 0)
{
meshData = New<MeshData>(*meshData);
}
@@ -739,222 +740,166 @@ void ImportCurve(aiQuatKey* keys, uint32 keysCount, LinearCurve<Quaternion>& cur
}
}
void ImportAnimation(int32 index, ModelData& data, AssimpImporterData& importerData)
{
const auto animations = importerData.Scene->mAnimations[index];
auto& anim = data.Animations.AddOne();
anim.Channels.Resize(animations->mNumChannels, false);
anim.Duration = animations->mDuration;
anim.FramesPerSecond = animations->mTicksPerSecond;
if (anim.FramesPerSecond <= 0)
{
anim.FramesPerSecond = importerData.Options.DefaultFrameRate;
if (anim.FramesPerSecond <= 0)
anim.FramesPerSecond = 30.0f;
}
anim.Name = animations->mName.C_Str();
for (unsigned i = 0; i < animations->mNumChannels; i++)
{
const auto aAnim = animations->mChannels[i];
auto& channel = anim.Channels[i];
channel.NodeName = aAnim->mNodeName.C_Str();
ImportCurve(aAnim->mPositionKeys, aAnim->mNumPositionKeys, channel.Position);
ImportCurve(aAnim->mRotationKeys, aAnim->mNumRotationKeys, channel.Rotation);
if (importerData.Options.ImportScaleTracks)
ImportCurve(aAnim->mScalingKeys, aAnim->mNumScalingKeys, channel.Scale);
}
}
bool ModelTool::ImportDataAssimp(const char* path, ModelData& data, Options& options, String& errorMsg)
{
auto context = (AssimpImporterData*)options.SplitContext;
if (!context)
static bool AssimpInited = false;
if (!AssimpInited)
{
static bool AssimpInited = false;
if (!AssimpInited)
{
AssimpInited = true;
LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision());
}
bool importMeshes = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry);
bool importAnimations = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations);
context = New<AssimpImporterData>(path, options);
// Setup import flags
unsigned int flags =
aiProcess_JoinIdenticalVertices |
aiProcess_LimitBoneWeights |
aiProcess_Triangulate |
aiProcess_SortByPType |
aiProcess_GenUVCoords |
aiProcess_FindDegenerates |
aiProcess_FindInvalidData |
//aiProcess_ValidateDataStructure |
aiProcess_ConvertToLeftHanded;
if (importMeshes)
{
if (options.CalculateNormals)
flags |= aiProcess_FixInfacingNormals | aiProcess_GenSmoothNormals;
if (options.CalculateTangents)
flags |= aiProcess_CalcTangentSpace;
if (options.OptimizeMeshes)
flags |= aiProcess_OptimizeMeshes | aiProcess_SplitLargeMeshes | aiProcess_ImproveCacheLocality;
if (options.MergeMeshes)
flags |= aiProcess_RemoveRedundantMaterials;
}
// Setup import options
context->AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_GSN_MAX_SMOOTHING_ANGLE, options.SmoothingNormalsAngle);
context->AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_CT_MAX_SMOOTHING_ANGLE, options.SmoothingTangentsAngle);
//context->AssimpImporter.SetPropertyInteger(AI_CONFIG_PP_SLM_TRIANGLE_LIMIT, MAX_uint16);
context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false);
context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false);
context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, false);
context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, importAnimations);
//context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); // TODO: optimize pivots when https://github.com/assimp/assimp/issues/1068 gets fixed
context->AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true);
// Import file
context->Scene = context->AssimpImporter.ReadFile(path, flags);
if (context->Scene == nullptr)
{
LOG_STR(Warning, String(context->AssimpImporter.GetErrorString()));
LOG_STR(Warning, String(path));
LOG_STR(Warning, StringUtils::ToString(flags));
errorMsg = context->AssimpImporter.GetErrorString();
Delete(context);
return true;
}
// Create root node
AssimpNode& rootNode = context->Nodes.AddOne();
rootNode.ParentIndex = -1;
rootNode.LodIndex = 0;
rootNode.Name = TEXT("Root");
rootNode.LocalTransform = Transform::Identity;
// Process imported scene nodes
ProcessNodes(*context, context->Scene->mRootNode, 0);
AssimpInited = true;
LOG(Info, "Assimp {0}.{1}.{2}", aiGetVersionMajor(), aiGetVersionMinor(), aiGetVersionRevision());
}
DeleteMe<AssimpImporterData> contextCleanup(options.SplitContext ? nullptr : context);
bool importMeshes = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry);
bool importAnimations = EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations);
AssimpImporterData context(path, options);
// Setup import flags
unsigned int flags =
aiProcess_JoinIdenticalVertices |
aiProcess_LimitBoneWeights |
aiProcess_Triangulate |
aiProcess_SortByPType |
aiProcess_GenUVCoords |
aiProcess_FindDegenerates |
aiProcess_FindInvalidData |
//aiProcess_ValidateDataStructure |
aiProcess_ConvertToLeftHanded;
if (importMeshes)
{
if (options.CalculateNormals)
flags |= aiProcess_FixInfacingNormals | aiProcess_GenSmoothNormals;
if (options.CalculateTangents)
flags |= aiProcess_CalcTangentSpace;
if (options.OptimizeMeshes)
flags |= aiProcess_OptimizeMeshes | aiProcess_SplitLargeMeshes | aiProcess_ImproveCacheLocality;
if (options.MergeMeshes)
flags |= aiProcess_RemoveRedundantMaterials;
}
// Setup import options
context.AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_GSN_MAX_SMOOTHING_ANGLE, options.SmoothingNormalsAngle);
context.AssimpImporter.SetPropertyFloat(AI_CONFIG_PP_CT_MAX_SMOOTHING_ANGLE, options.SmoothingTangentsAngle);
//context.AssimpImporter.SetPropertyInteger(AI_CONFIG_PP_SLM_TRIANGLE_LIMIT, MAX_uint16);
context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_CAMERAS, false);
context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_LIGHTS, false);
context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_TEXTURES, false);
context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_READ_ANIMATIONS, importAnimations);
//context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, false); // TODO: optimize pivots when https://github.com/assimp/assimp/issues/1068 gets fixed
context.AssimpImporter.SetPropertyBool(AI_CONFIG_IMPORT_FBX_OPTIMIZE_EMPTY_ANIMATION_CURVES, true);
// Import file
context.Scene = context.AssimpImporter.ReadFile(path, flags);
if (context.Scene == nullptr)
{
LOG_STR(Warning, String(context.AssimpImporter.GetErrorString()));
LOG_STR(Warning, String(path));
LOG_STR(Warning, StringUtils::ToString(flags));
errorMsg = context.AssimpImporter.GetErrorString();
return true;
}
// Create root node
AssimpNode& rootNode = context.Nodes.AddOne();
rootNode.ParentIndex = -1;
rootNode.LodIndex = 0;
rootNode.Name = TEXT("Root");
rootNode.LocalTransform = Transform::Identity;
// Process imported scene nodes
ProcessNodes(context, context.Scene->mRootNode, 0);
// Import materials
if (ImportMaterials(data, *context, errorMsg))
if (ImportMaterials(data, context, errorMsg))
{
LOG(Warning, "Failed to import materials.");
return true;
}
// Import geometry
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context->Scene->HasMeshes())
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context.Scene->HasMeshes())
{
const int meshCount = context->Scene->mNumMeshes;
if (options.SplitObjects && options.ObjectIndex == -1 && meshCount > 1)
for (unsigned meshIndex = 0; meshIndex < context.Scene->mNumMeshes; meshIndex++)
{
// Import the first object within this call
options.SplitObjects = false;
options.ObjectIndex = 0;
if (options.OnSplitImport.IsBinded())
{
// Split all animations into separate assets
LOG(Info, "Splitting imported {0} meshes", meshCount);
for (int32 i = 1; i < meshCount; i++)
{
auto splitOptions = options;
splitOptions.ObjectIndex = i;
splitOptions.SplitContext = context;
const auto aMesh = context->Scene->mMeshes[i];
const String objectName(aMesh->mName.C_Str());
options.OnSplitImport(splitOptions, objectName);
}
}
}
if (options.ObjectIndex != -1)
{
// Import the selected mesh
const auto meshIndex = Math::Clamp<int32>(options.ObjectIndex, 0, meshCount - 1);
if (ImportMesh(meshIndex, data, *context, errorMsg))
if (ImportMesh((int32)meshIndex, data, context, errorMsg))
return true;
}
else
{
// Import all meshes
for (int32 meshIndex = 0; meshIndex < meshCount; meshIndex++)
{
if (ImportMesh(meshIndex, data, *context, errorMsg))
return true;
}
}
}
// Import skeleton
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton))
{
data.Skeleton.Nodes.Resize(context->Nodes.Count(), false);
for (int32 i = 0; i < context->Nodes.Count(); i++)
data.Skeleton.Nodes.Resize(context.Nodes.Count(), false);
for (int32 i = 0; i < context.Nodes.Count(); i++)
{
auto& node = data.Skeleton.Nodes[i];
auto& aNode = context->Nodes[i];
auto& aNode = context.Nodes[i];
node.Name = aNode.Name;
node.ParentIndex = aNode.ParentIndex;
node.LocalTransform = aNode.LocalTransform;
}
data.Skeleton.Bones.Resize(context->Bones.Count(), false);
for (int32 i = 0; i < context->Bones.Count(); i++)
data.Skeleton.Bones.Resize(context.Bones.Count(), false);
for (int32 i = 0; i < context.Bones.Count(); i++)
{
auto& bone = data.Skeleton.Bones[i];
auto& aBone = context->Bones[i];
auto& aBone = context.Bones[i];
const auto boneNodeIndex = aBone.NodeIndex;
const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context->Bones[aBone.ParentBoneIndex].NodeIndex;
const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context.Bones[aBone.ParentBoneIndex].NodeIndex;
bone.ParentIndex = aBone.ParentBoneIndex;
bone.NodeIndex = aBone.NodeIndex;
bone.LocalTransform = CombineTransformsFromNodeIndices(context->Nodes, parentBoneNodeIndex, boneNodeIndex);
bone.LocalTransform = CombineTransformsFromNodeIndices(context.Nodes, parentBoneNodeIndex, boneNodeIndex);
bone.OffsetMatrix = aBone.OffsetMatrix;
}
}
// Import animations
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations) && context->Scene->HasAnimations())
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations))
{
const int32 animCount = (int32)context->Scene->mNumAnimations;
if (options.SplitObjects && options.ObjectIndex == -1 && animCount > 1)
for (unsigned animIndex = 0; animIndex < context.Scene->mNumAnimations; animIndex++)
{
// Import the first object within this call
options.SplitObjects = false;
options.ObjectIndex = 0;
if (options.OnSplitImport.IsBinded())
{
// Split all animations into separate assets
LOG(Info, "Splitting imported {0} animations", animCount);
for (int32 i = 1; i < animCount; i++)
{
auto splitOptions = options;
splitOptions.ObjectIndex = i;
splitOptions.SplitContext = context;
const auto animations = context->Scene->mAnimations[i];
const String objectName(animations->mName.C_Str());
options.OnSplitImport(splitOptions, objectName);
}
}
}
// Import the animation
{
const auto animIndex = Math::Clamp<int32>(options.ObjectIndex, 0, context->Scene->mNumAnimations - 1);
const auto animations = context->Scene->mAnimations[animIndex];
data.Animation.Channels.Resize(animations->mNumChannels, false);
data.Animation.Duration = animations->mDuration;
data.Animation.FramesPerSecond = animations->mTicksPerSecond;
if (data.Animation.FramesPerSecond <= 0)
{
data.Animation.FramesPerSecond = context->Options.DefaultFrameRate;
if (data.Animation.FramesPerSecond <= 0)
data.Animation.FramesPerSecond = 30.0f;
}
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);
if (options.ImportScaleTracks)
ImportCurve(aAnim->mScalingKeys, aAnim->mNumScalingKeys, anim.Scale);
}
ImportAnimation((int32)animIndex, data, context);
}
}
// Import nodes
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Nodes))
{
data.Nodes.Resize(context->Nodes.Count());
for (int32 i = 0; i < context->Nodes.Count(); i++)
data.Nodes.Resize(context.Nodes.Count());
for (int32 i = 0; i < context.Nodes.Count(); i++)
{
auto& node = data.Nodes[i];
auto& aNode = context->Nodes[i];
auto& aNode = context.Nodes[i];
node.Name = aNode.Name;
node.ParentIndex = aNode.ParentIndex;

View File

@@ -4,7 +4,6 @@
#include "ModelTool.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/DeleteMe.h"
#include "Engine/Core/Math/Mathd.h"
#include "Engine/Core/Math/Matrix.h"
#include "Engine/Core/Collections/Sorting.h"
@@ -1006,27 +1005,19 @@ void ImportCurve(const ofbx::AnimationCurveNode* curveNode, LinearCurve<T>& curv
}
}
bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importerData)
void ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importerData)
{
const ofbx::AnimationStack* stack = importerData.Scene->getAnimationStack(index);
const ofbx::AnimationLayer* layer = stack->getLayer(0);
const ofbx::TakeInfo* takeInfo = importerData.Scene->getTakeInfo(stack->name);
if (takeInfo == nullptr)
return true;
return;
// Initialize animation animation keyframes sampling
const float frameRate = importerData.FrameRate;
data.Animation.FramesPerSecond = frameRate;
const double localDuration = takeInfo->local_time_to - takeInfo->local_time_from;
if (localDuration <= ZeroTolerance)
return true;
data.Animation.Duration = (double)(int32)(localDuration * frameRate + 0.5f);
AnimInfo info;
info.TimeStart = takeInfo->local_time_from;
info.TimeEnd = takeInfo->local_time_to;
info.Duration = localDuration;
info.FramesCount = (int32)data.Animation.Duration;
info.SamplingPeriod = 1.0f / frameRate;
return;
// Count valid animation channels
Array<int32> animatedNodes(importerData.Nodes.Count());
@@ -1042,15 +1033,32 @@ bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importer
animatedNodes.Add(nodeIndex);
}
if (animatedNodes.IsEmpty())
return true;
data.Animation.Channels.Resize(animatedNodes.Count(), false);
return;
// Setup animation descriptor
auto& animation = data.Animations.AddOne();
animation.Duration = (double)(int32)(localDuration * frameRate + 0.5f);
animation.FramesPerSecond = frameRate;
char nameData[256];
takeInfo->name.toString(nameData);
animation.Name = nameData;
animation.Name = animation.Name.TrimTrailing();
if (animation.Name.IsEmpty())
animation.Name = String(layer->name);
animation.Channels.Resize(animatedNodes.Count(), false);
AnimInfo info;
info.TimeStart = takeInfo->local_time_from;
info.TimeEnd = takeInfo->local_time_to;
info.Duration = localDuration;
info.FramesCount = (int32)animation.Duration;
info.SamplingPeriod = 1.0f / frameRate;
// Import curves
for (int32 i = 0; i < animatedNodes.Count(); i++)
{
const int32 nodeIndex = animatedNodes[i];
auto& aNode = importerData.Nodes[nodeIndex];
auto& anim = data.Animation.Channels[i];
auto& anim = animation.Channels[i];
const ofbx::AnimationCurveNode* translationNode = layer->getCurveNode(*aNode.FbxObj, "Lcl Translation");
const ofbx::AnimationCurveNode* rotationNode = layer->getCurveNode(*aNode.FbxObj, "Lcl Rotation");
@@ -1066,9 +1074,8 @@ bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importer
if (importerData.ConvertRH)
{
for (int32 i = 0; i < data.Animation.Channels.Count(); i++)
for (auto& anim : animation.Channels)
{
auto& anim = data.Animation.Channels[i];
auto& posKeys = anim.Position.GetKeyframes();
auto& rotKeys = anim.Rotation.GetKeyframes();
@@ -1084,8 +1091,6 @@ bool ImportAnimation(int32 index, ModelData& data, OpenFbxImporterData& importer
}
}
}
return false;
}
static Float3 FbxVectorFromAxisAndSign(int axis, int sign)
@@ -1105,239 +1110,185 @@ static Float3 FbxVectorFromAxisAndSign(int axis, int sign)
bool ModelTool::ImportDataOpenFBX(const char* path, ModelData& data, Options& options, String& errorMsg)
{
auto context = (OpenFbxImporterData*)options.SplitContext;
if (!context)
// Import file
Array<byte> fileData;
if (File::ReadAllBytes(String(path), fileData))
{
// Import file
Array<byte> fileData;
if (File::ReadAllBytes(String(path), fileData))
{
errorMsg = TEXT("Cannot load file.");
return true;
}
ofbx::u64 loadFlags = 0;
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry))
{
loadFlags |= (ofbx::u64)ofbx::LoadFlags::TRIANGULATE;
if (!options.ImportBlendShapes)
loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES;
}
else
{
loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_GEOMETRY | (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES;
}
ofbx::IScene* scene = ofbx::load(fileData.Get(), fileData.Count(), loadFlags);
if (!scene)
{
errorMsg = ofbx::getError();
return true;
}
fileData.Resize(0);
// Tweak scene if exported by Blender
auto& globalInfo = *scene->getGlobalInfo();
if (StringAnsiView(globalInfo.AppName).StartsWith(StringAnsiView("Blender"), StringSearchCase::IgnoreCase))
{
auto ptr = const_cast<ofbx::GlobalSettings*>(scene->getGlobalSettings());
ptr->UpAxis = (ofbx::UpVector)((int32)ptr->UpAxis + 1);
}
// Process imported scene
context = New<OpenFbxImporterData>(path, options, scene);
auto& globalSettings = context->GlobalSettings;
ProcessNodes(*context, scene->getRoot(), -1);
// Apply model scene global scale factor
context->Nodes[0].LocalTransform = Transform(Vector3::Zero, Quaternion::Identity, globalSettings.UnitScaleFactor) * context->Nodes[0].LocalTransform;
// Log scene info
LOG(Info, "Loaded FBX model, Frame Rate: {0}, Unit Scale Factor: {1}", context->FrameRate, globalSettings.UnitScaleFactor);
LOG(Info, "{0}, {1}, {2}", String(globalInfo.AppName), String(globalInfo.AppVersion), String(globalInfo.AppVendor));
LOG(Info, "Up: {1}{0}", globalSettings.UpAxis == ofbx::UpVector_AxisX ? TEXT("X") : globalSettings.UpAxis == ofbx::UpVector_AxisY ? TEXT("Y") : TEXT("Z"), globalSettings.UpAxisSign == 1 ? TEXT("+") : TEXT("-"));
LOG(Info, "Front: {1}{0}", globalSettings.FrontAxis == ofbx::FrontVector_ParityEven ? TEXT("ParityEven") : TEXT("ParityOdd"), globalSettings.FrontAxisSign == 1 ? TEXT("+") : TEXT("-"));
LOG(Info, "{0} Handed{1}", globalSettings.CoordAxis == ofbx::CoordSystem_RightHanded ? TEXT("Right") : TEXT("Left"), globalSettings.CoordAxisSign == 1 ? TEXT("") : TEXT(" (negative)"));
#if OPEN_FBX_CONVERT_SPACE
LOG(Info, "Imported scene: Up={0}, Front={1}, Right={2}", context->Up, context->Front, context->Right);
#endif
// Extract embedded textures
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Textures))
{
String outputPath;
for (int i = 0, c = scene->getEmbeddedDataCount(); i < c; i++)
{
const ofbx::DataView aEmbedded = scene->getEmbeddedData(i);
ofbx::DataView aFilename = scene->getEmbeddedFilename(i);
char filenameData[256];
aFilename.toString(filenameData);
if (outputPath.IsEmpty())
{
String pathStr(path);
outputPath = String(StringUtils::GetDirectoryName(pathStr)) / TEXT("textures");
FileSystem::CreateDirectory(outputPath);
}
const String filenameStr(filenameData);
String embeddedPath = outputPath / StringUtils::GetFileName(filenameStr);
if (FileSystem::FileExists(embeddedPath))
continue;
LOG(Info, "Extracing embedded resource to {0}", embeddedPath);
if (File::WriteAllBytes(embeddedPath, aEmbedded.begin + 4, (int32)(aEmbedded.end - aEmbedded.begin - 4)))
{
LOG(Error, "Failed to write data to file");
}
}
}
#if OPEN_FBX_CONVERT_SPACE
// Transform nodes to match the engine coordinates system - DirectX (UpVector = +Y, FrontVector = +Z, CoordSystem = -X (LeftHanded))
if (context->Up == Float3(1, 0, 0) && context->Front == Float3(0, 0, 1) && context->Right == Float3(0, 1, 0))
{
context->RootConvertRotation = Quaternion::Euler(0, 180, 0);
}
else if (context->Up == Float3(0, 1, 0) && context->Front == Float3(-1, 0, 0) && context->Right == Float3(0, 0, 1))
{
context->RootConvertRotation = Quaternion::Euler(90, -90, 0);
}
/*Float3 engineUp(0, 1, 0);
Float3 engineFront(0, 0, 1);
Float3 engineRight(-1, 0, 0);*/
/*Float3 engineUp(1, 0, 0);
Float3 engineFront(0, 0, 1);
Float3 engineRight(0, 1, 0);
if (context->Up != engineUp || context->Front != engineFront || context->Right != engineRight)
{
LOG(Info, "Converting imported scene nodes to match engine coordinates system");
context->RootConvertRotation = Quaternion::GetRotationFromTo(context->Up, engineUp, engineUp);
//context->RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context->Right, engineRight, engineRight);
//context->RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context->Front, engineFront, engineFront);
}*/
/*Float3 hackUp = FbxVectorFromAxisAndSign(globalSettings.UpAxis, globalSettings.UpAxisSign);
if (hackUp == Float3::UnitX)
context->RootConvertRotation = Quaternion::Euler(-90, 0, 0);
else if (hackUp == Float3::UnitZ)
context->RootConvertRotation = Quaternion::Euler(90, 0, 0);*/
if (!context->RootConvertRotation.IsIdentity())
{
for (auto& node : context->Nodes)
{
if (node.ParentIndex == -1)
{
node.LocalTransform.Orientation = context->RootConvertRotation * node.LocalTransform.Orientation;
break;
}
}
}
#endif
errorMsg = TEXT("Cannot load file.");
return true;
}
DeleteMe<OpenFbxImporterData> contextCleanup(options.SplitContext ? nullptr : context);
ofbx::u64 loadFlags = 0;
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry))
{
loadFlags |= (ofbx::u64)ofbx::LoadFlags::TRIANGULATE;
if (!options.ImportBlendShapes)
loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES;
}
else
{
loadFlags |= (ofbx::u64)ofbx::LoadFlags::IGNORE_GEOMETRY | (ofbx::u64)ofbx::LoadFlags::IGNORE_BLEND_SHAPES;
}
ofbx::IScene* scene = ofbx::load(fileData.Get(), fileData.Count(), loadFlags);
if (!scene)
{
errorMsg = ofbx::getError();
return true;
}
fileData.Resize(0);
// Tweak scene if exported by Blender
auto& globalInfo = *scene->getGlobalInfo();
if (StringAnsiView(globalInfo.AppName).StartsWith(StringAnsiView("Blender"), StringSearchCase::IgnoreCase))
{
auto ptr = const_cast<ofbx::GlobalSettings*>(scene->getGlobalSettings());
ptr->UpAxis = (ofbx::UpVector)((int32)ptr->UpAxis + 1);
}
// Process imported scene
OpenFbxImporterData context(path, options, scene);
auto& globalSettings = context.GlobalSettings;
ProcessNodes(context, scene->getRoot(), -1);
// Apply model scene global scale factor
context.Nodes[0].LocalTransform = Transform(Vector3::Zero, Quaternion::Identity, globalSettings.UnitScaleFactor) * context.Nodes[0].LocalTransform;
// Log scene info
LOG(Info, "Loaded FBX model, Frame Rate: {0}, Unit Scale Factor: {1}", context.FrameRate, globalSettings.UnitScaleFactor);
LOG(Info, "{0}, {1}, {2}", String(globalInfo.AppName), String(globalInfo.AppVersion), String(globalInfo.AppVendor));
LOG(Info, "Up: {1}{0}", globalSettings.UpAxis == ofbx::UpVector_AxisX ? TEXT("X") : globalSettings.UpAxis == ofbx::UpVector_AxisY ? TEXT("Y") : TEXT("Z"), globalSettings.UpAxisSign == 1 ? TEXT("+") : TEXT("-"));
LOG(Info, "Front: {1}{0}", globalSettings.FrontAxis == ofbx::FrontVector_ParityEven ? TEXT("ParityEven") : TEXT("ParityOdd"), globalSettings.FrontAxisSign == 1 ? TEXT("+") : TEXT("-"));
LOG(Info, "{0} Handed{1}", globalSettings.CoordAxis == ofbx::CoordSystem_RightHanded ? TEXT("Right") : TEXT("Left"), globalSettings.CoordAxisSign == 1 ? TEXT("") : TEXT(" (negative)"));
#if OPEN_FBX_CONVERT_SPACE
LOG(Info, "Imported scene: Up={0}, Front={1}, Right={2}", context.Up, context.Front, context.Right);
#endif
// Extract embedded textures
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Textures))
{
String outputPath;
for (int i = 0, c = scene->getEmbeddedDataCount(); i < c; i++)
{
const ofbx::DataView aEmbedded = scene->getEmbeddedData(i);
ofbx::DataView aFilename = scene->getEmbeddedFilename(i);
char filenameData[256];
aFilename.toString(filenameData);
if (outputPath.IsEmpty())
{
String pathStr(path);
outputPath = String(StringUtils::GetDirectoryName(pathStr)) / TEXT("textures");
FileSystem::CreateDirectory(outputPath);
}
const String filenameStr(filenameData);
String embeddedPath = outputPath / StringUtils::GetFileName(filenameStr);
if (FileSystem::FileExists(embeddedPath))
continue;
LOG(Info, "Extracing embedded resource to {0}", embeddedPath);
if (File::WriteAllBytes(embeddedPath, aEmbedded.begin + 4, (int32)(aEmbedded.end - aEmbedded.begin - 4)))
{
LOG(Error, "Failed to write data to file");
}
}
}
#if OPEN_FBX_CONVERT_SPACE
// Transform nodes to match the engine coordinates system - DirectX (UpVector = +Y, FrontVector = +Z, CoordSystem = -X (LeftHanded))
if (context.Up == Float3(1, 0, 0) && context.Front == Float3(0, 0, 1) && context.Right == Float3(0, 1, 0))
{
context.RootConvertRotation = Quaternion::Euler(0, 180, 0);
}
else if (context.Up == Float3(0, 1, 0) && context.Front == Float3(-1, 0, 0) && context.Right == Float3(0, 0, 1))
{
context.RootConvertRotation = Quaternion::Euler(90, -90, 0);
}
/*Float3 engineUp(0, 1, 0);
Float3 engineFront(0, 0, 1);
Float3 engineRight(-1, 0, 0);*/
/*Float3 engineUp(1, 0, 0);
Float3 engineFront(0, 0, 1);
Float3 engineRight(0, 1, 0);
if (context.Up != engineUp || context.Front != engineFront || context.Right != engineRight)
{
LOG(Info, "Converting imported scene nodes to match engine coordinates system");
context.RootConvertRotation = Quaternion::GetRotationFromTo(context.Up, engineUp, engineUp);
//context.RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context.Right, engineRight, engineRight);
//context.RootConvertRotation *= Quaternion::GetRotationFromTo(rotation * context.Front, engineFront, engineFront);
}*/
/*Float3 hackUp = FbxVectorFromAxisAndSign(globalSettings.UpAxis, globalSettings.UpAxisSign);
if (hackUp == Float3::UnitX)
context.RootConvertRotation = Quaternion::Euler(-90, 0, 0);
else if (hackUp == Float3::UnitZ)
context.RootConvertRotation = Quaternion::Euler(90, 0, 0);*/
if (!context.RootConvertRotation.IsIdentity())
{
for (auto& node : context.Nodes)
{
if (node.ParentIndex == -1)
{
node.LocalTransform.Orientation = context.RootConvertRotation * node.LocalTransform.Orientation;
break;
}
}
}
#endif
// Build final skeleton bones hierarchy before importing meshes
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton))
{
if (ImportBones(*context, errorMsg))
if (ImportBones(context, errorMsg))
{
LOG(Warning, "Failed to import skeleton bones.");
return true;
}
Sorting::QuickSort(context->Bones.Get(), context->Bones.Count());
Sorting::QuickSort(context.Bones.Get(), context.Bones.Count());
}
// Import geometry (meshes and materials)
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context->Scene->getMeshCount() > 0)
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Geometry) && context.Scene->getMeshCount() > 0)
{
const int meshCount = context->Scene->getMeshCount();
if (options.SplitObjects && options.ObjectIndex == -1 && meshCount > 1)
const int meshCount = context.Scene->getMeshCount();
for (int32 meshIndex = 0; meshIndex < meshCount; meshIndex++)
{
// Import the first object within this call
options.SplitObjects = false;
options.ObjectIndex = 0;
if (options.OnSplitImport.IsBinded())
{
// Split all animations into separate assets
LOG(Info, "Splitting imported {0} meshes", meshCount);
for (int32 i = 1; i < meshCount; i++)
{
auto splitOptions = options;
splitOptions.ObjectIndex = i;
splitOptions.SplitContext = context;
const auto aMesh = context->Scene->getMesh(i);
const String objectName(aMesh->name);
options.OnSplitImport(splitOptions, objectName);
}
}
}
if (options.ObjectIndex != -1)
{
// Import the selected mesh
const auto meshIndex = Math::Clamp<int32>(options.ObjectIndex, 0, meshCount - 1);
if (ImportMesh(meshIndex, data, *context, errorMsg))
if (ImportMesh(meshIndex, data, context, errorMsg))
return true;
// Let the firstly imported mesh import all materials from all meshes (index 0 is importing all following ones before itself during splitting - see code above)
if (options.ObjectIndex == 1)
{
for (int32 i = 0; i < meshCount; i++)
{
const auto aMesh = context->Scene->getMesh(i);
if (i == 1 || IsMeshInvalid(aMesh))
continue;
for (int32 j = 0; j < aMesh->getMaterialCount(); j++)
{
const ofbx::Material* aMaterial = aMesh->getMaterial(j);
context->AddMaterial(data, aMaterial);
}
}
}
}
else
{
// Import all meshes
for (int32 meshIndex = 0; meshIndex < meshCount; meshIndex++)
{
if (ImportMesh(meshIndex, data, *context, errorMsg))
return true;
}
}
}
// Import skeleton
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Skeleton))
{
data.Skeleton.Nodes.Resize(context->Nodes.Count(), false);
for (int32 i = 0; i < context->Nodes.Count(); i++)
data.Skeleton.Nodes.Resize(context.Nodes.Count(), false);
for (int32 i = 0; i < context.Nodes.Count(); i++)
{
auto& node = data.Skeleton.Nodes[i];
auto& aNode = context->Nodes[i];
auto& aNode = context.Nodes[i];
node.Name = aNode.Name;
node.ParentIndex = aNode.ParentIndex;
node.LocalTransform = aNode.LocalTransform;
}
data.Skeleton.Bones.Resize(context->Bones.Count(), false);
for (int32 i = 0; i < context->Bones.Count(); i++)
data.Skeleton.Bones.Resize(context.Bones.Count(), false);
for (int32 i = 0; i < context.Bones.Count(); i++)
{
auto& bone = data.Skeleton.Bones[i];
auto& aBone = context->Bones[i];
auto& aBone = context.Bones[i];
const auto boneNodeIndex = aBone.NodeIndex;
// Find the parent bone
int32 parentBoneIndex = -1;
for (int32 j = context->Nodes[boneNodeIndex].ParentIndex; j != -1; j = context->Nodes[j].ParentIndex)
for (int32 j = context.Nodes[boneNodeIndex].ParentIndex; j != -1; j = context.Nodes[j].ParentIndex)
{
parentBoneIndex = context->FindBone(j);
parentBoneIndex = context.FindBone(j);
if (parentBoneIndex != -1)
break;
}
aBone.ParentBoneIndex = parentBoneIndex;
const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context->Bones[aBone.ParentBoneIndex].NodeIndex;
const auto parentBoneNodeIndex = aBone.ParentBoneIndex == -1 ? -1 : context.Bones[aBone.ParentBoneIndex].NodeIndex;
bone.ParentIndex = aBone.ParentBoneIndex;
bone.NodeIndex = aBone.NodeIndex;
bone.LocalTransform = CombineTransformsFromNodeIndices(context->Nodes, parentBoneNodeIndex, boneNodeIndex);
bone.LocalTransform = CombineTransformsFromNodeIndices(context.Nodes, parentBoneNodeIndex, boneNodeIndex);
bone.OffsetMatrix = aBone.OffsetMatrix;
}
}
@@ -1345,54 +1296,21 @@ bool ModelTool::ImportDataOpenFBX(const char* path, ModelData& data, Options& op
// Import animations
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Animations))
{
const int animCount = context->Scene->getAnimationStackCount();
if (options.SplitObjects && options.ObjectIndex == -1 && animCount > 1)
const int animCount = context.Scene->getAnimationStackCount();
for (int32 animIndex = 0; animIndex < animCount; animIndex++)
{
// Import the first object within this call
options.SplitObjects = false;
options.ObjectIndex = 0;
if (options.OnSplitImport.IsBinded())
{
// Split all animations into separate assets
LOG(Info, "Splitting imported {0} animations", animCount);
for (int32 i = 1; i < animCount; i++)
{
auto splitOptions = options;
splitOptions.ObjectIndex = i;
splitOptions.SplitContext = context;
const ofbx::AnimationStack* stack = context->Scene->getAnimationStack(i);
const ofbx::AnimationLayer* layer = stack->getLayer(0);
const String objectName(layer->name);
options.OnSplitImport(splitOptions, objectName);
}
}
}
if (options.ObjectIndex != -1)
{
// Import selected animation
const auto animIndex = Math::Clamp<int32>(options.ObjectIndex, 0, animCount - 1);
ImportAnimation(animIndex, data, *context);
}
else
{
// Import first valid animation
for (int32 animIndex = 0; animIndex < animCount; animIndex++)
{
if (!ImportAnimation(animIndex, data, *context))
break;
}
ImportAnimation(animIndex, data, context);
}
}
// Import nodes
if (EnumHasAnyFlags(options.ImportTypes, ImportDataTypes::Nodes))
{
data.Nodes.Resize(context->Nodes.Count());
for (int32 i = 0; i < context->Nodes.Count(); i++)
data.Nodes.Resize(context.Nodes.Count());
for (int32 i = 0; i < context.Nodes.Count(); i++)
{
auto& node = data.Nodes[i];
auto& aNode = context->Nodes[i];
auto& aNode = context.Nodes[i];
node.Name = aNode.Name;
node.ParentIndex = aNode.ParentIndex;

View File

@@ -452,6 +452,9 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options
options.FramesRange.Y = Math::Max(options.FramesRange.Y, options.FramesRange.X);
options.DefaultFrameRate = Math::Max(0.0f, options.DefaultFrameRate);
options.SamplingRate = Math::Max(0.0f, options.SamplingRate);
if (options.SplitObjects)
options.MergeMeshes = false; // Meshes merging doesn't make sense when we want to import each mesh individually
// TODO: maybe we could update meshes merger to collapse meshes within the same name if splitting is enabled?
// Validate path
// Note: Assimp/Autodesk supports only ANSI characters in imported file path
@@ -511,8 +514,6 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options
FileSystem::DeleteFile(tmpPath);
}
// TODO: check model LODs sequence (eg. {LOD0, LOD2, LOD5} is invalid)
// Remove namespace prefixes from the nodes names
{
for (auto& node : data.Nodes)
@@ -523,9 +524,10 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options
{
RemoveNamespace(node.Name);
}
for (auto& channel : data.Animation.Channels)
for (auto& animation : data.Animations)
{
RemoveNamespace(channel.NodeName);
for (auto& channel : animation.Channels)
RemoveNamespace(channel.NodeName);
}
for (auto& lod : data.LODs)
{
@@ -533,18 +535,19 @@ bool ModelTool::ImportData(const String& path, ModelData& data, Options& options
{
RemoveNamespace(mesh->Name);
for (auto& blendShape : mesh->BlendShapes)
{
RemoveNamespace(blendShape.Name);
}
}
}
}
// Validate the animation channels
if (data.Animation.Channels.HasItems())
for (auto& animation : data.Animations)
{
auto& channels = animation.Channels;
if (channels.IsEmpty())
continue;
// Validate bone animations uniqueness
auto& channels = data.Animation.Channels;
for (int32 i = 0; i < channels.Count(); i++)
{
for (int32 j = i + 1; j < channels.Count(); j++)
@@ -742,7 +745,7 @@ void TrySetupMaterialParameter(MaterialInstance* instance, Span<const Char*> par
{
if (type == MaterialParameterType::Color)
{
if (paramType != MaterialParameterType::Vector3 ||
if (paramType != MaterialParameterType::Vector3 ||
paramType != MaterialParameterType::Vector4)
continue;
}
@@ -757,7 +760,7 @@ void TrySetupMaterialParameter(MaterialInstance* instance, Span<const Char*> par
}
}
bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& options, String& errorMsg, const String& autoImportOutput)
bool ModelTool::ImportModel(const String& path, ModelData& data, Options& options, String& errorMsg, const String& autoImportOutput)
{
LOG(Info, "Importing model from \'{0}\'", path);
const auto startTime = DateTime::NowUTC();
@@ -785,7 +788,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
default:
return true;
}
ModelData data;
if (ImportData(path, data, options, errorMsg))
return true;
@@ -926,13 +928,20 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
case ModelType::Animation:
{
// Validate
if (data.Animation.Channels.IsEmpty())
if (data.Animations.IsEmpty())
{
errorMsg = TEXT("Imported file has no valid animations.");
return true;
}
LOG(Info, "Imported animation has {0} channels, duration: {1} frames, frames per second: {2}", data.Animation.Channels.Count(), data.Animation.Duration, data.Animation.FramesPerSecond);
for (auto& animation : data.Animations)
{
LOG(Info, "Imported animation '{}' has {} channels, duration: {} frames, frames per second: {}", animation.Name, animation.Channels.Count(), animation.Duration, animation.FramesPerSecond);
if (animation.Duration <= ZeroTolerance || animation.FramesPerSecond <= ZeroTolerance)
{
errorMsg = TEXT("Invalid animation duration.");
return true;
}
}
break;
}
}
@@ -976,7 +985,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
case TextureEntry::TypeHint::Normals:
textureOptions.Type = TextureFormatType::NormalMap;
break;
default: ;
}
AssetsImportingManager::ImportIfEdited(texture.FilePath, assetPath, texture.AssetID, &textureOptions);
#endif
@@ -1461,65 +1469,67 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
}
else if (options.Type == ModelType::Animation)
{
// Trim the animation keyframes range if need to
if (options.Duration == AnimationDuration::Custom)
for (auto& animation : data.Animations)
{
// Custom animation import, frame index start and end
const float start = options.FramesRange.X;
const float end = options.FramesRange.Y;
for (int32 i = 0; i < data.Animation.Channels.Count(); i++)
// Trim the animation keyframes range if need to
if (options.Duration == AnimationDuration::Custom)
{
auto& anim = data.Animation.Channels[i];
anim.Position.Trim(start, end);
anim.Rotation.Trim(start, end);
anim.Scale.Trim(start, end);
}
data.Animation.Duration = end - start;
}
// Change the sampling rate if need to
if (!Math::IsZero(options.SamplingRate))
{
const float timeScale = (float)(data.Animation.FramesPerSecond / options.SamplingRate);
if (!Math::IsOne(timeScale))
{
data.Animation.FramesPerSecond = options.SamplingRate;
for (int32 i = 0; i < data.Animation.Channels.Count(); i++)
// Custom animation import, frame index start and end
const float start = options.FramesRange.X;
const float end = options.FramesRange.Y;
for (int32 i = 0; i < animation.Channels.Count(); i++)
{
auto& anim = data.Animation.Channels[i];
auto& anim = animation.Channels[i];
anim.Position.Trim(start, end);
anim.Rotation.Trim(start, end);
anim.Scale.Trim(start, end);
}
animation.Duration = end - start;
}
anim.Position.TransformTime(timeScale, 0.0f);
anim.Rotation.TransformTime(timeScale, 0.0f);
anim.Scale.TransformTime(timeScale, 0.0f);
// Change the sampling rate if need to
if (!Math::IsZero(options.SamplingRate))
{
const float timeScale = (float)(animation.FramesPerSecond / options.SamplingRate);
if (!Math::IsOne(timeScale))
{
animation.FramesPerSecond = options.SamplingRate;
for (int32 i = 0; i < animation.Channels.Count(); i++)
{
auto& anim = animation.Channels[i];
anim.Position.TransformTime(timeScale, 0.0f);
anim.Rotation.TransformTime(timeScale, 0.0f);
anim.Scale.TransformTime(timeScale, 0.0f);
}
}
}
}
// Optimize the keyframes
if (options.OptimizeKeyframes)
{
const int32 before = data.Animation.GetKeyframesCount();
for (int32 i = 0; i < data.Animation.Channels.Count(); i++)
// Optimize the keyframes
if (options.OptimizeKeyframes)
{
auto& anim = data.Animation.Channels[i];
// Optimize keyframes
OptimizeCurve(anim.Position);
OptimizeCurve(anim.Rotation);
OptimizeCurve(anim.Scale);
// Remove empty channels
if (anim.GetKeyframesCount() == 0)
const int32 before = animation.GetKeyframesCount();
for (int32 i = 0; i < animation.Channels.Count(); i++)
{
data.Animation.Channels.RemoveAt(i--);
}
}
const int32 after = data.Animation.GetKeyframesCount();
LOG(Info, "Optimized {0} animation keyframe(s). Before: {1}, after: {2}, Ratio: {3}%", before - after, before, after, Utilities::RoundTo2DecimalPlaces((float)after / before));
}
auto& anim = animation.Channels[i];
data.Animation.EnableRootMotion = options.EnableRootMotion;
data.Animation.RootNodeName = options.RootNodeName;
// Optimize keyframes
OptimizeCurve(anim.Position);
OptimizeCurve(anim.Rotation);
OptimizeCurve(anim.Scale);
// Remove empty channels
if (anim.GetKeyframesCount() == 0)
{
animation.Channels.RemoveAt(i--);
}
}
const int32 after = animation.GetKeyframesCount();
LOG(Info, "Optimized {0} animation keyframe(s). Before: {1}, after: {2}, Ratio: {3}%", before - after, before, after, Utilities::RoundTo2DecimalPlaces((float)after / before));
}
animation.EnableRootMotion = options.EnableRootMotion;
animation.RootNodeName = options.RootNodeName;
}
}
// Merge meshes with the same parent nodes, material and skinning
@@ -1696,27 +1706,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
}
}
// Export imported data to the output container (we reduce vertex data copy operations to minimum)
{
meshData.Textures.Swap(data.Textures);
meshData.Materials.Swap(data.Materials);
meshData.LODs.Resize(data.LODs.Count(), false);
for (int32 i = 0; i < data.LODs.Count(); i++)
{
auto& dst = meshData.LODs[i];
auto& src = data.LODs[i];
dst.Meshes = src.Meshes;
}
meshData.Skeleton.Swap(data.Skeleton);
meshData.Animation.Swap(data.Animation);
// Clear meshes from imported data (we link them to result model data). This reduces amount of allocations.
data.LODs.Resize(0);
}
// Calculate blend shapes vertices ranges
for (auto& lod : meshData.LODs)
for (auto& lod : data.LODs)
{
for (auto& mesh : lod.Meshes)
{
@@ -1737,6 +1728,9 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op
}
}
// Auto calculate LODs transition settings
data.CalculateLODsScreenSizes();
const auto endTime = DateTime::NowUTC();
LOG(Info, "Model file imported in {0} ms", static_cast<int32>((endTime - startTime).GetTotalMilliseconds()));

View File

@@ -296,13 +296,17 @@ public:
public: // Internals
// Runtime data for objects splitting during import (used internally)
void* SplitContext = nullptr;
Function<bool(Options& splitOptions, const String& objectName)> OnSplitImport;
// Internal flags for objects to import.
ImportDataTypes ImportTypes = ImportDataTypes::None;
struct CachedData
{
ModelData* Data = nullptr;
void* MeshesByName = nullptr;
};
// Cached model data - used when performing nested importing (eg. via objects splitting). Allows to read and process source file only once and use those results for creation of multiple assets (permutation via ObjectIndex).
CachedData* Cached = nullptr;
public:
// [ISerializable]
void Serialize(SerializeStream& stream, const void* otherObj) override;
@@ -324,12 +328,12 @@ public:
/// Imports the model.
/// </summary>
/// <param name="path">The file path.</param>
/// <param name="meshData">The output data.</param>
/// <param name="data">The output data.</param>
/// <param name="options">The import options.</param>
/// <param name="errorMsg">The error message container.</param>
/// <param name="autoImportOutput">The output folder for the additional imported data - optional. Used to auto-import textures and material assets.</param>
/// <returns>True if fails, otherwise false.</returns>
static bool ImportModel(const String& path, ModelData& meshData, Options& options, String& errorMsg, const String& autoImportOutput = String::Empty);
static bool ImportModel(const String& path, ModelData& data, Options& options, String& errorMsg, const String& autoImportOutput = String::Empty);
public:
static int32 DetectLodIndex(const String& nodeName);