diff --git a/README.md b/README.md index ac719ea..ae8599c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# FBX2glTF +# FBX2glTF [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause) @@ -56,6 +56,7 @@ Options: --flip-v Flip all V texture coordinates. --no-flip-v Don't flip V texture coordinates. --no-khr-lights-punctual Don't use KHR_lights_punctual extension to export FBX lights. + --animation-files Read multiple fbx animation files. --user-properties Transcribe FBX User Properties into glTF node and material 'extras'. --blend-shape-normals Include blend shape normals, if reported present by the FBX SDK. --blend-shape-tangents Include blend shape tangents, if reported present by the FBX SDK. @@ -97,6 +98,10 @@ Some of these switches are not obvious: Your FBX is likely constructed with the assumption that `(0, 0)` is bottom left, whereas glTF has `(0, 0)` as top left. To produce spec-compliant glTF, we must flip the texcoords. To request unflipped coordinates: +- '--animation-files' will try to read in additional animations from multiple + fbx files with numbers added to the base name (eg. test1.fbx, test2.fbx, ...). + It will apply animations to geometry in the base file by matching the node + names, so make sure all nodes have a unique name. - `--long-indices` lets you force the use of either 16-bit or 32-bit indices. The default option is auto, which make the choice on a per-mesh-size basis. - `--compute-normals` controls when automatic vertex normals should be computed diff --git a/src/FBX2glTF.cpp b/src/FBX2glTF.cpp index 12c8dcc..3d7c3fe 100644 --- a/src/FBX2glTF.cpp +++ b/src/FBX2glTF.cpp @@ -137,6 +137,11 @@ int main(int argc, char* argv[]) { [&](size_t count) { gltfOptions.useKHRLightsPunctual = (count == 0); }, "Don't use KHR_lights_punctual extension to export FBX lights."); + app.add_flag( + "--animation-files", + gltfOptions.readAnimationFiles, + "Read multiple fbx animation files."); + app.add_flag( "--user-properties", gltfOptions.enableUserProperties, @@ -314,6 +319,7 @@ int main(int argc, char* argv[]) { if (verboseOutput) { fmt::printf("Loading FBX File: %s\n", inputPath); } + if (!LoadFBXFile(raw, inputPath, {"png", "jpg", "jpeg"}, gltfOptions)) { fmt::fprintf(stderr, "ERROR:: Failed to parse FBX: %s\n", inputPath); return 1; diff --git a/src/FBX2glTF.h b/src/FBX2glTF.h index e075bbd..b6b3c5f 100644 --- a/src/FBX2glTF.h +++ b/src/FBX2glTF.h @@ -112,6 +112,9 @@ struct GltfOptions { /** Whether to include lights through the KHR_punctual_lights extension. */ bool useKHRLightsPunctual{true}; + /** Whether to read multiple fbx animation files. */ + bool readAnimationFiles{false}; + /** Whether to include blend shape normals, if present according to the SDK. */ bool useBlendShapeNormals{false}; /** Whether to include blend shape tangents, if present according to the SDK. */ diff --git a/src/fbx/Fbx2Raw.cpp b/src/fbx/Fbx2Raw.cpp index 2bc8e80..5c463d2 100644 --- a/src/fbx/Fbx2Raw.cpp +++ b/src/fbx/Fbx2Raw.cpp @@ -720,7 +720,7 @@ static void ReadNodeHierarchy( } } -static void ReadAnimations(RawModel& raw, FbxScene* pScene, const GltfOptions& options) { +static void ReadAnimations(RawModel& raw, FbxScene* pScene, const GltfOptions& options, bool nodeNameLookup) { FbxTime::EMode eMode = FbxTime::eFrames24; switch (options.animationFramerate) { case AnimationFramerateOptions::BAKE24: @@ -816,7 +816,14 @@ static void ReadAnimations(RawModel& raw, FbxScene* pScene, const GltfOptions& o bool hasMorphs = false; RawChannel channel; - channel.nodeIndex = raw.GetNodeById(pNode->GetUniqueID()); + if (nodeNameLookup) { + channel.nodeIndex = raw.GetNodeByName(pNode->GetName()); + } else { + channel.nodeIndex = raw.GetNodeById(pNode->GetUniqueID()); + } + if (channel.nodeIndex == -1) { + continue; + } for (FbxLongLong frameIndex = firstFrameIndex; frameIndex <= lastFrameIndex; frameIndex++) { FbxTime pTime; @@ -1068,15 +1075,7 @@ static void FindFbxTextures( } } -bool LoadFBXFile( - RawModel& raw, - const std::string fbxFileName, - const std::set& textureExtensions, - const GltfOptions& options) { - FbxManager* pManager = FbxManager::Create(); - FbxIOSettings* pIoSettings = FbxIOSettings::Create(pManager, IOSROOT); - pManager->SetIOSettings(pIoSettings); - +static FbxScene* LoadScene(FbxManager* pManager, const std::string fbxFileName) { FbxImporter* pImporter = FbxImporter::Create(pManager, ""); if (!pImporter->Initialize(fbxFileName.c_str(), -1, pManager->GetIOSettings())) { @@ -1084,8 +1083,7 @@ bool LoadFBXFile( fmt::printf("%s\n", pImporter->GetStatus().GetErrorString()); } pImporter->Destroy(); - pManager->Destroy(); - return false; + return nullptr; } FbxScene* pScene = FbxScene::Create(pManager, "fbxScene"); @@ -1093,14 +1091,9 @@ bool LoadFBXFile( pImporter->Destroy(); if (pScene == nullptr) { - pImporter->Destroy(); - pManager->Destroy(); - return false; + return nullptr; } - std::map textureLocations; - FindFbxTextures(pScene, fbxFileName, textureExtensions, textureLocations); - // Use Y up for glTF FbxAxisSystem::MayaYUp.ConvertScene(pScene); @@ -1113,14 +1106,54 @@ bool LoadFBXFile( if (sceneSystemUnit != FbxSystemUnit::cm) { FbxSystemUnit::cm.ConvertScene(pScene); } + + return pScene; +} + +bool LoadFBXFile( + RawModel& raw, + const std::string fbxFileName, + const std::set& textureExtensions, + const GltfOptions& options) { + FbxManager* pManager = FbxManager::Create(); + FbxIOSettings* pIoSettings = FbxIOSettings::Create(pManager, IOSROOT); + pManager->SetIOSettings(pIoSettings); + + FbxScene* pScene = LoadScene(pManager, fbxFileName); + if (pScene == nullptr) { + pManager->Destroy(); + return false; + } + // this is always 0.01, but let's opt for clarity. scaleFactor = FbxSystemUnit::m.GetConversionFactorFrom(FbxSystemUnit::cm); + std::map textureLocations; + FindFbxTextures(pScene, fbxFileName, textureExtensions, textureLocations); + ReadNodeHierarchy(raw, pScene, pScene->GetRootNode(), 0, ""); ReadNodeAttributes(raw, pScene, pScene->GetRootNode(), textureLocations); - ReadAnimations(raw, pScene, options); + ReadAnimations(raw, pScene, options, false); pScene->Destroy(); + + // read additional animation files + bool readAnimationFiles = options.readAnimationFiles; + int animationFileNumber = 1; + while (readAnimationFiles) { + std::string animationFileName = StringUtils::GetFolderString(fbxFileName) + + StringUtils::GetFileBaseString(fbxFileName) + + std::to_string(animationFileNumber) + ".fbx"; + FbxScene* pAnimationScene = LoadScene(pManager, animationFileName.c_str()); + if (pAnimationScene != nullptr) { + ReadAnimations(raw, pAnimationScene, options, true); + pAnimationScene->Destroy(); + animationFileNumber++; + } else { + readAnimationFiles = false; + } + } + pManager->Destroy(); return true; diff --git a/src/raw/RawModel.cpp b/src/raw/RawModel.cpp index f5c40d9..26490bb 100644 --- a/src/raw/RawModel.cpp +++ b/src/raw/RawModel.cpp @@ -640,6 +640,15 @@ int RawModel::GetNodeById(const long nodeId) const { return -1; } +int RawModel::GetNodeByName(const char* name) const { + for (size_t i = 0; i < nodes.size(); i++) { + if (nodes[i].name == name) { + return (int)i; + } + } + return -1; +} + int RawModel::GetSurfaceById(const long surfaceId) const { for (size_t i = 0; i < surfaces.size(); i++) { if (surfaces[i].id == surfaceId) { diff --git a/src/raw/RawModel.hpp b/src/raw/RawModel.hpp index 1da65bb..e75dd32 100644 --- a/src/raw/RawModel.hpp +++ b/src/raw/RawModel.hpp @@ -508,6 +508,7 @@ class RawModel { return nodes[index]; } int GetNodeById(const long nodeId) const; + int GetNodeByName(const char* name) const; // Create individual attribute arrays. // Returns true if the vertices store the particular attribute.