diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml index 878bc3f..9885e04 100644 --- a/.github/workflows/Test.yml +++ b/.github/workflows/Test.yml @@ -19,4 +19,5 @@ jobs: shell: pwsh run: | out/build/Release/Release/FBX-glTF-conv-test.exe + out/build/Release/Release/FBX-glTF-conv-core-test.exe diff --git a/Cli/ReadCliArgs.cpp b/Cli/ReadCliArgs.cpp index 90e3dcc..b851ee8 100644 --- a/Cli/ReadCliArgs.cpp +++ b/Cli/ReadCliArgs.cpp @@ -69,6 +69,16 @@ getCommandLineArgsU8(int argc_, const char *argv_[]) { template struct ConvertOptionBindingTrait {}; +template <> +struct ConvertOptionBindingTrait<&bee::ConvertOptions::no_mesh_instancing> { + constexpr static auto name = "no-mesh-instancing"; + constexpr static auto description = + "Whether to disable mesh instancing. " + "By default, if a mesh is shared by multi nodes. They reference to the " + "same mesh."; + constexpr static auto default_value = "false"; +}; + template <> struct ConvertOptionBindingTrait<&bee::ConvertOptions::export_node_visibility> { constexpr static auto name = "export-node-visibility"; @@ -194,6 +204,10 @@ std::optional readCliArgs(std::span args_) { "Prefer local time spans recorded in FBX file for animation " "exporting.", cxxopts::value()->default_value("true")); + + add_cxx_option + .template operator()<&bee::ConvertOptions::no_mesh_instancing>(); + options.add_options()("match-mesh-names", "Prefer mesh names " "exporting.", @@ -316,6 +330,10 @@ std::optional readCliArgs(std::span args_) { cliArgs.convertOptions.prefer_local_time_span = cliParseResult["prefer-local-time-span"].as(); } + + fetch_convert_option + .template operator()<&bee::ConvertOptions::no_mesh_instancing>(); + if (cliParseResult.count("match-mesh-names")) { cliArgs.convertOptions.match_mesh_names = cliParseResult["match-mesh-names"].as(); diff --git a/Cli/Test/ReadCliArgs.cpp b/Cli/Test/ReadCliArgs.cpp index 91fed86..9a62fe9 100644 --- a/Cli/Test/ReadCliArgs.cpp +++ b/Cli/Test/ReadCliArgs.cpp @@ -65,6 +65,7 @@ TEST_CASE("Read CLI arguments") { CHECK_EQ(u8toexe(convertOptions->fbmDir), ""); CHECK_EQ(convertOptions->logFile, std::nullopt); CHECK_EQ(convertOptions->convertOptions.prefer_local_time_span, true); + CHECK_EQ(convertOptions->convertOptions.no_mesh_instancing, false); CHECK_EQ(convertOptions->convertOptions.match_mesh_names, true); CHECK_EQ(convertOptions->convertOptions.animationBakeRate, 0); CHECK_EQ(convertOptions->convertOptions.animation_position_error_multiplier, @@ -167,6 +168,11 @@ CHECK_EQ(u8toexe(args->convertOptions.textureResolution.locations[0]), "/a"s); "prefer-local-time-span"); } +{ // --no-mesh-instancing + test_boolean_arg<&bee::ConvertOptions::no_mesh_instancing>( + "no-mesh-instancing"); +} + { // --match-mesh-names test_boolean_arg<&bee::ConvertOptions::match_mesh_names>("match-mesh-names"); } diff --git a/Core/CMakeLists.txt b/Core/CMakeLists.txt index 565676d..42d3c6e 100644 --- a/Core/CMakeLists.txt +++ b/Core/CMakeLists.txt @@ -155,6 +155,7 @@ if (NOT APPLE) # FIXME: iconv is not found on GitHub macOS release flow target_include_directories (FBX-glTF-conv-core-test PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../Polyfills/nlohmann-json") target_include_directories (FBX-glTF-conv-core-test PRIVATE "${CMAKE_CURRENT_LIST_DIR}/../Core/fx/include") target_link_libraries (FBX-glTF-conv-core-test PUBLIC BeeCore) + target_link_libraries(FBX-glTF-conv-core-test PRIVATE fmt::fmt fmt::fmt-header-only) if (APPLE) find_library (CF_FRAMEWORK CoreFoundation) message("CoreFoundation Framework: ${CF_FRAMEWORK}") diff --git a/Core/Source/bee/Convert/SceneConverter.Mesh.cpp b/Core/Source/bee/Convert/SceneConverter.Mesh.cpp index 33fb3a3..6070361 100644 --- a/Core/Source/bee/Convert/SceneConverter.Mesh.cpp +++ b/Core/Source/bee/Convert/SceneConverter.Mesh.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include @@ -48,13 +49,39 @@ SceneConverter::_convertNodeMeshes( fbxsdk::FbxNode &fbx_node_) { assert(!fbx_meshes_.empty()); - auto meshName = _getName(*fbx_meshes_.front(), fbx_node_); auto [vertexTransform, normalTransform] = _getGeometrixTransform(fbx_node_); auto vertexTransformX = (vertexTransform == fbxsdk::FbxMatrix{}) ? nullptr : &vertexTransform; auto normalTransformX = (normalTransform == fbxsdk::FbxMatrix{}) ? nullptr : &normalTransform; + // FBX supports mesh instancing: + // See + // http://docs.autodesk.com/FBX/2014/ENU/FBX-SDK-Documentation/index.html?url=files/GUID-0D483705-23D9-476D-A567-09609396B190.htm,topicNumber=d30e10223 + // As the document pointed out: + // > a single instance of FbxMesh can be bound to multiple instances of + // > FbxNode... + // > ...This is called instancing. + // + // But we have more requirements on that. + // A node is consider being instancing a mesh if and only if: + // - it has only one mesh bount. + // - it does not has any geometrix transform on that. + std::optional meshInstancingKey; + if (!_options.no_mesh_instancing) { + if (fbx_meshes_.size() == 1 && (!vertexTransformX && !normalTransformX)) { + meshInstancingKey = fbx_meshes_.front(); + } + } + + // If the mesh could be instanced, try to use the instance previously created. + if (meshInstancingKey) { + const auto iter = _meshInstanceMap.find(*meshInstancingKey); + if (iter != _meshInstanceMap.end()) { + return iter->second; + } + } + std::optional nodeMeshesSkinData; nodeMeshesSkinData = _extractNodeMeshesSkinData(fbx_meshes_); @@ -66,13 +93,18 @@ SceneConverter::_convertNodeMeshes( std::optional glTFSkinIndex; fx::gltf::Mesh glTFMesh; - if (_options.match_mesh_names) { + if (meshInstancingKey) { + glTFMesh.name = fbx_string_to_utf8_checked(fbx_meshes_.front()->GetName()); + } else { + auto meshName = _getName(*fbx_meshes_.front(), fbx_node_); + if (_options.match_mesh_names) { auto it = nodeMeshMap.find(&fbx_node_); glTFMesh.name = it == nodeMeshMap.end() ? meshName : it->second; - } else { + } else { glTFMesh.name = meshName; + } } - + for (decltype(fbx_meshes_.size()) iFbxMesh = 0; iFbxMesh < fbx_meshes_.size(); ++iFbxMesh) { const auto fbxMesh = fbx_meshes_[iFbxMesh]; @@ -89,10 +121,10 @@ SceneConverter::_convertNodeMeshes( MaterialUsage materialUsage; auto glTFPrimitive = _convertMeshAsPrimitive( - *fbxMesh, meshName, vertexTransformX, normalTransformX, fbxShapes, + *fbxMesh, glTFMesh.name, vertexTransformX, normalTransformX, fbxShapes, skinInfluenceChannels, materialUsage); - if (auto fbxMaterial = _getTheUniqueMaterial(*fbxMesh, fbx_node_)) { + if (auto fbxMaterial = _getTheUniqueMaterial(*fbxMesh)) { if (auto glTFMaterialIndex = _convertMaterial(*fbxMaterial, materialUsage)) { glTFPrimitive.material = *glTFMaterialIndex; @@ -129,12 +161,17 @@ SceneConverter::_convertNodeMeshes( myMeta.meshes = fbx_meshes_; node_meta_.meshes = myMeta; + // If the mesh could be instanced, cache the convert result. + if (meshInstancingKey) { + _meshInstanceMap.emplace(*meshInstancingKey, convertMeshResult); + } + return convertMeshResult; } std::string SceneConverter::_getName(fbxsdk::FbxMesh &fbx_mesh_, fbxsdk::FbxNode &fbx_node_) { - auto meshName = std::string{fbx_mesh_.GetName()}; + auto meshName = fbx_string_to_utf8_checked(fbx_mesh_.GetName()); if (!meshName.empty()) { return meshName; } else { @@ -143,7 +180,7 @@ std::string SceneConverter::_getName(fbxsdk::FbxMesh &fbx_mesh_, } std::tuple -SceneConverter::_getGeometrixTransform(fbxsdk::FbxNode &fbx_node_) { +SceneConverter::_getGeometrixTransform(const fbxsdk::FbxNode &fbx_node_) { const auto meshTranslation = fbx_node_.GetGeometricTranslation( fbxsdk::FbxNode::EPivotSet::eSourcePivot); const auto meshRotation = @@ -699,8 +736,7 @@ SceneConverter::_typeVertices(const FbxMeshVertexLayout &vertex_layout_) { } fbxsdk::FbxSurfaceMaterial * -SceneConverter::_getTheUniqueMaterial(fbxsdk::FbxMesh &fbx_mesh_, - fbxsdk::FbxNode &fbx_node_) { +SceneConverter::_getTheUniqueMaterial(fbxsdk::FbxMesh &fbx_mesh_) { const auto nElementMaterialCount = fbx_mesh_.GetElementMaterialCount(); if (!nElementMaterialCount) { return nullptr; diff --git a/Core/Source/bee/Convert/SceneConverter.cpp b/Core/Source/bee/Convert/SceneConverter.cpp index 35bbd09..9e40914 100644 --- a/Core/Source/bee/Convert/SceneConverter.cpp +++ b/Core/Source/bee/Convert/SceneConverter.cpp @@ -149,7 +149,6 @@ void SceneConverter::_traverseNodes(FbxNode *node) { } } - void SceneConverter::_announceNodes(const fbxsdk::FbxScene &fbx_scene_) { auto rootNode = fbx_scene_.GetRootNode(); auto nChildren = rootNode->GetChildCount(); @@ -386,6 +385,6 @@ void SceneConverter::_convertNode(fbxsdk::FbxNode &fbx_node_) { } std::string SceneConverter::_getName(fbxsdk::FbxNode &fbx_node_) { - return fbx_node_.GetName(); + return fbx_string_to_utf8_checked(fbx_node_.GetName()); } } // namespace bee \ No newline at end of file diff --git a/Core/Source/bee/Convert/SceneConverter.h b/Core/Source/bee/Convert/SceneConverter.h index 40b88a0..5d49f88 100644 --- a/Core/Source/bee/Convert/SceneConverter.h +++ b/Core/Source/bee/Convert/SceneConverter.h @@ -268,6 +268,7 @@ class SceneConverter { _textureMap; std::unordered_map _nodeDumpMetaMap; std::optional _unitScaleFactor = 1.0; + std::unordered_map _meshInstanceMap; inline fbxsdk::FbxVector4 _applyUnitScaleFactorV3(const fbxsdk::FbxVector4 &v_) const { @@ -330,7 +331,7 @@ class SceneConverter { std::string _getName(fbxsdk::FbxMesh &fbx_mesh_, fbxsdk::FbxNode &fbx_node_); std::tuple - _getGeometrixTransform(fbxsdk::FbxNode &fbx_node_); + _getGeometrixTransform(const fbxsdk::FbxNode &fbx_node_); fx::gltf::Primitive _convertMeshAsPrimitive( fbxsdk::FbxMesh &fbx_mesh_, @@ -357,8 +358,7 @@ class SceneConverter { std::list _typeVertices(const FbxMeshVertexLayout &vertex_layout_); - fbxsdk::FbxSurfaceMaterial *_getTheUniqueMaterial(fbxsdk::FbxMesh &fbx_mesh_, - fbxsdk::FbxNode &fbx_node_); + fbxsdk::FbxSurfaceMaterial *_getTheUniqueMaterial(fbxsdk::FbxMesh &fbx_mesh_); /// /// Things get even more complicated if there are more than one mesh attached to a node. diff --git a/Core/Source/bee/Converter.h b/Core/Source/bee/Converter.h index 828d69e..1a345c2 100644 --- a/Core/Source/bee/Converter.h +++ b/Core/Source/bee/Converter.h @@ -80,6 +80,14 @@ struct ConvertOptions { bool match_mesh_names = true; + /// + /// Whether to disable mesh instancing. + /// + /// + /// false + /// + bool no_mesh_instancing = false; + float animation_position_error_multiplier = 1e-5f; float animation_scale_error_multiplier = 1e-5f; diff --git a/Core/Test/Mesh.cpp b/Core/Test/Mesh.cpp index 8153139..bc7f664 100644 --- a/Core/Test/Mesh.cpp +++ b/Core/Test/Mesh.cpp @@ -1,8 +1,80 @@ #include "bee/Converter.Test.h" +#include #include #include +#include +#include #include +#include #include +#include + +auto create_fbx_scene_fixture( + std::function callback_) { + + const auto manager = fbxsdk::FbxManager::Create(); + + struct Guard { + Guard(fbxsdk::FbxManager &manager_) : _manager(&manager_) { + } + + ~Guard() { + if (_manager) { + _manager->Destroy(); + _manager = nullptr; + } + } + + private: + fbxsdk::FbxManager *_manager; + }; + + Guard guard{*manager}; + + auto &fbxScene = callback_(*manager); + + static int counter = 0; + const auto fbxPath = std::filesystem::temp_directory_path() / + u8"FBX-glTF-conv-test" / + fmt::format("{}.fbx", counter++); + std::filesystem::create_directories(fbxPath.parent_path()); + + const auto exporter = + fbxsdk::FbxExporter::Create(fbxScene.GetFbxManager(), ""); + const auto exportStatus = exporter->Initialize(fbxPath.string().c_str(), -1); + CHECK_UNARY(exportStatus); + exporter->Export(&fbxScene); + + exporter->Destroy(); + + struct Fixture { + Fixture(std::filesystem::path path_) noexcept + : _path(path_), _deleted(false) { + } + + Fixture(Fixture &&other_) noexcept { + std::swap(this->_deleted, other_._deleted); + std::swap(this->_path, other_._path); + } + + ~Fixture() { + if (!_deleted) { + _deleted = true; + std::filesystem::remove(_path); + } + } + + const auto &path() const { + return this->_path; + } + + private: + std::filesystem::path _path; + bool _deleted = true; + }; + + return Fixture{fbxPath}; +} void createFbxGrid(const char *path_, std::uint32_t N) { // Initialize the FBX SDK @@ -15,107 +87,106 @@ void createFbxGrid(const char *path_, std::uint32_t N) { const auto gridNode = fbxsdk::FbxNode::Create(fbxManager, "Gird"); const auto grid = fbxsdk::FbxMesh::Create(fbxManager, "Grid"); - std::vector vertices; - vertices.reserve(N * N); - for (decltype(N) i = 0; i < N; i++) { - for (decltype(N) j = 0; j < N; j++) { - vertices.push_back(FbxVector4((float)i, 0.0, (float)j)); - } - } - // Set the vertices of the grid - grid->InitControlPoints(N * N); - for (decltype(N) i = 0; i < N * N; i++) { - grid->SetControlPointAt(vertices[i], i); - } - // Define the faces of the grid - std::vector indices; - indices.reserve((N - 1) * (N - 1) * 4); - for (decltype(N) i = 0; i < N - 1; i++) { - for (decltype(N) j = 0; j < N - 1; j++) { - indices.push_back(i * N + j); - indices.push_back(i * N + j + 1); - indices.push_back((i + 1) * N + j + 1); - indices.push_back((i + 1) * N + j); - } - } - for (decltype(N) i = 0; i < (N - 1) * (N - 1); i++) { - grid->BeginPolygon(-1, -1, false); - grid->AddPolygon(indices[i * 4 + 0]); - grid->AddPolygon(indices[i * 4 + 1]); - grid->AddPolygon(indices[i * 4 + 2]); - grid->AddPolygon(indices[i * 4 + 3]); - grid->EndPolygon(); - } - gridNode->SetNodeAttribute(grid); - // Add the grid node to the scene - fbxScene->GetRootNode()->AddChild(gridNode); - // Create and assign a material to the mesh - FbxSurfacePhong *pMaterial = - FbxSurfacePhong::Create(fbxManager, "Material"); - gridNode->AddMaterial(pMaterial); - const auto fbxFileName = path_; - const auto exporter = fbxsdk::FbxExporter::Create(fbxManager, ""); - const auto success = - exporter->Initialize(fbxFileName, -1, fbxManager->GetIOSettings()); - CHECK_EQ(success, true); - const auto exportSuccess = exporter->Export(fbxScene); - CHECK_EQ(exportSuccess, true); - exporter->Destroy(); - fbxManager->Destroy(); + std::vector vertices; + vertices.reserve(N * N); + for (decltype(N) i = 0; i < N; i++) { + for (decltype(N) j = 0; j < N; j++) { + vertices.push_back(FbxVector4((float)i, 0.0, (float)j)); + } + } + // Set the vertices of the grid + grid->InitControlPoints(N * N); + for (decltype(N) i = 0; i < N * N; i++) { + grid->SetControlPointAt(vertices[i], i); + } + // Define the faces of the grid + std::vector indices; + indices.reserve((N - 1) * (N - 1) * 4); + for (decltype(N) i = 0; i < N - 1; i++) { + for (decltype(N) j = 0; j < N - 1; j++) { + indices.push_back(i * N + j); + indices.push_back(i * N + j + 1); + indices.push_back((i + 1) * N + j + 1); + indices.push_back((i + 1) * N + j); + } + } + for (decltype(N) i = 0; i < (N - 1) * (N - 1); i++) { + grid->BeginPolygon(-1, -1, false); + grid->AddPolygon(indices[i * 4 + 0]); + grid->AddPolygon(indices[i * 4 + 1]); + grid->AddPolygon(indices[i * 4 + 2]); + grid->AddPolygon(indices[i * 4 + 3]); + grid->EndPolygon(); + } + gridNode->SetNodeAttribute(grid); + // Add the grid node to the scene + fbxScene->GetRootNode()->AddChild(gridNode); + // Create and assign a material to the mesh + FbxSurfacePhong *pMaterial = FbxSurfacePhong::Create(fbxManager, "Material"); + gridNode->AddMaterial(pMaterial); + const auto fbxFileName = path_; + const auto exporter = fbxsdk::FbxExporter::Create(fbxManager, ""); + const auto success = + exporter->Initialize(fbxFileName, -1, fbxManager->GetIOSettings()); + CHECK_EQ(success, true); + const auto exportSuccess = exporter->Export(fbxScene); + CHECK_EQ(exportSuccess, true); + exporter->Destroy(); + fbxManager->Destroy(); } // N : total triangles void createTriangles(const char *path_, int N, int M = 200) { - // Initialize the FBX SDK - FbxManager *manager = FbxManager::Create(); - - // Create an FBX scene - FbxScene *scene = FbxScene::Create(manager, "MyScene"); - - // Create a Node - FbxNode *trianglesNode = FbxNode::Create(scene, "MyTriangles"); - - // Create a mesh for the triangles - FbxMesh *triangleMesh = FbxMesh::Create(scene, "MyTrianglesMesh"); - trianglesNode->AddNodeAttribute(triangleMesh); - - // Create control points for the triangle mesh - int controlPointsCount = 3 * N; - triangleMesh->InitControlPoints(controlPointsCount); - - // Set the control point positions - FbxVector4 *controlPoints = triangleMesh->GetControlPoints(); - int count = 0; - for (decltype(N) i = 0; i < N; i++) { - double x = i % M * 1.1 ; - double y = i / M * 1.1; - double z = 0; - controlPoints[count++] = FbxVector4(x, y, z); - controlPoints[count++] = FbxVector4(x+1 ,y, z); - controlPoints[count++] = FbxVector4(x+1 ,y+1, z); - } + // Initialize the FBX SDK + FbxManager *manager = FbxManager::Create(); - // Create triangles - int triangleCount = N; - for (decltype(N) i = 0; i < triangleCount; i++) { - triangleMesh->BeginPolygon(); - triangleMesh->AddPolygon(i * 3); - triangleMesh->AddPolygon(i * 3 + 1); - triangleMesh->AddPolygon(i * 3 + 2); - triangleMesh->EndPolygon(); - } - scene->GetRootNode()->AddChild(trianglesNode); - FbxExporter *exporter = FbxExporter::Create(manager, "MyExporter"); - exporter->Initialize(path_, -1, manager->GetIOSettings()); - exporter->Export(scene); - exporter->Destroy(); - manager->Destroy(); + // Create an FBX scene + FbxScene *scene = FbxScene::Create(manager, "MyScene"); + + // Create a Node + FbxNode *trianglesNode = FbxNode::Create(scene, "MyTriangles"); + + // Create a mesh for the triangles + FbxMesh *triangleMesh = FbxMesh::Create(scene, "MyTrianglesMesh"); + trianglesNode->AddNodeAttribute(triangleMesh); + + // Create control points for the triangle mesh + int controlPointsCount = 3 * N; + triangleMesh->InitControlPoints(controlPointsCount); + + // Set the control point positions + FbxVector4 *controlPoints = triangleMesh->GetControlPoints(); + int count = 0; + for (decltype(N) i = 0; i < N; i++) { + double x = i % M * 1.1; + double y = i / M * 1.1; + double z = 0; + controlPoints[count++] = FbxVector4(x, y, z); + controlPoints[count++] = FbxVector4(x + 1, y, z); + controlPoints[count++] = FbxVector4(x + 1, y + 1, z); + } + + // Create triangles + int triangleCount = N; + for (decltype(N) i = 0; i < triangleCount; i++) { + triangleMesh->BeginPolygon(); + triangleMesh->AddPolygon(i * 3); + triangleMesh->AddPolygon(i * 3 + 1); + triangleMesh->AddPolygon(i * 3 + 2); + triangleMesh->EndPolygon(); + } + scene->GetRootNode()->AddChild(trianglesNode); + FbxExporter *exporter = FbxExporter::Create(manager, "MyExporter"); + exporter->Initialize(path_, -1, manager->GetIOSettings()); + exporter->Export(scene); + exporter->Destroy(); + manager->Destroy(); } void testIndexUnit(const char *path_, std::uint32_t vertex_count_, fx::gltf::Accessor::ComponentType component_type_) { const auto fbxFileName = path_; - createTriangles(fbxFileName, vertex_count_/3 ); + createTriangles(fbxFileName, vertex_count_ / 3); bee::ConvertOptions convertOptions; auto glTF = bee::_convert_test(reinterpret_cast(fbxFileName), convertOptions); @@ -130,11 +201,127 @@ void testIndexUnit(const char *path_, TEST_CASE("Mesh") { SUBCASE("Index unit") { - //test 65538 index - testIndexUnit("T_65538.fbx", 65538 , - fx::gltf::Accessor::ComponentType::UnsignedInt); + // test 65538 index + testIndexUnit("T_65538.fbx", 65538, + fx::gltf::Accessor::ComponentType::UnsignedInt); // test 65535 index - testIndexUnit("T_65535.fbx", 65535 , + testIndexUnit("T_65535.fbx", 65535, fx::gltf::Accessor::ComponentType::UnsignedShort); } + + SUBCASE("Mesh instancing") { + const auto fixture = create_fbx_scene_fixture( + [](fbxsdk::FbxManager &manager_) -> fbxsdk::FbxScene & { + const auto scene = fbxsdk::FbxScene::Create(&manager_, "myScene"); + + { + const auto mesh = + fbxsdk::FbxMesh::Create(scene, "some-shared-mesh"); + + { + const auto node1 = + fbxsdk::FbxNode::Create(scene, "node-ref-to-shared-mesh"); + CHECK_UNARY(scene->GetRootNode()->AddChild(node1)); + CHECK_UNARY(node1->AddNodeAttribute(mesh)); + + const auto node2 = + fbxsdk::FbxNode::Create(scene, "node2-ref-to-shared-mesh"); + CHECK_UNARY(scene->GetRootNode()->AddChild(node2)); + CHECK_UNARY(node2->AddNodeAttribute(mesh)); + } + + { + const auto node = fbxsdk::FbxNode::Create( + scene, + "node-ref-to-shared-mesh-but-have-geometrix-transform"); + CHECK_UNARY(scene->GetRootNode()->AddChild(node)); + CHECK_UNARY(node->AddNodeAttribute(mesh)); + node->SetGeometricTranslation( + fbxsdk::FbxNode::EPivotSet::eSourcePivot, + fbxsdk::FbxVector4{1., 2., 3.}); + } + + { + const auto node = fbxsdk::FbxNode::Create( + scene, "node-ref-to-shared-mesh-but-have-more-than-one-mesh"); + CHECK_UNARY(scene->GetRootNode()->AddChild(node)); + CHECK_UNARY(node->AddNodeAttribute(mesh)); + CHECK_UNARY(node->AddNodeAttribute( + fbxsdk::FbxMesh::Create(scene, "some-mesh-2"))); + } + } + + { + auto node1 = fbxsdk::FbxNode::Create( + scene, "node-ref-to-anonymous-mesh-instance"); + CHECK_UNARY(scene->GetRootNode()->AddChild(node1)); + auto node2 = fbxsdk::FbxNode::Create( + scene, "node2-ref-to-anonymous-mesh-instance"); + CHECK_UNARY(scene->GetRootNode()->AddChild(node2)); + auto mesh = fbxsdk::FbxMesh::Create(scene, ""); + CHECK_UNARY(node1->AddNodeAttribute(mesh)); + CHECK_UNARY(node2->AddNodeAttribute(mesh)); + } + + return *scene; + }); + + bee::ConvertOptions options; + auto result = bee::_convert_test(fixture.path().u8string(), options); + + CHECK_EQ(result.document().meshes.size(), 4); + + const auto getNodeByName = [](fx::gltf::Document &gltf_document_, + std::string_view name_) -> fx::gltf::Node & { + const auto rNode = std::ranges::find_if( + gltf_document_.nodes, + [name_](const auto &node_) { return node_.name == name_; }); + CHECK_NE(rNode, gltf_document_.nodes.end()); + return *rNode; + }; + + const auto countMeshReferences = [](fx::gltf::Document &gltf_document_, + int mesh_index_) { + return std::ranges::count_if(gltf_document_.nodes, + [mesh_index_](const auto &node_) { + return node_.mesh == mesh_index_; + }); + }; + + { + const auto &node1 = + getNodeByName(result.document(), "node-ref-to-shared-mesh"); + const auto &node2 = + getNodeByName(result.document(), "node2-ref-to-shared-mesh"); + CHECK_EQ(node1.mesh, node2.mesh); + CHECK_EQ(countMeshReferences(result.document(), node1.mesh), 2); + CHECK_EQ(result.document().meshes[node1.mesh].name, "some-shared-mesh"); + } + + { + const auto &node = + getNodeByName(result.document(), + "node-ref-to-shared-mesh-but-have-geometrix-transform"); + CHECK_EQ(countMeshReferences(result.document(), node.mesh), 1); + CHECK_EQ(result.document().meshes[node.mesh].name, "some-shared-mesh"); + } + + { + const auto &node = + getNodeByName(result.document(), + "node-ref-to-shared-mesh-but-have-more-than-one-mesh"); + CHECK_EQ(countMeshReferences(result.document(), node.mesh), 1); + CHECK_EQ(result.document().meshes[node.mesh].name, "some-shared-mesh"); + } + + { + const auto &node1 = getNodeByName(result.document(), + "node-ref-to-anonymous-mesh-instance"); + const auto &node2 = getNodeByName(result.document(), + "node2-ref-to-anonymous-mesh-instance"); + CHECK_EQ(node1.mesh, node2.mesh); + CHECK_EQ(countMeshReferences(result.document(), node1.mesh), 2); + CHECK_EQ(result.document().meshes[node1.mesh].name, ""); + } + } }