From 331edd21231983725f595d859ea095f58ef6d1f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Leslie=20Leigh=20=28=E6=9D=8E=E7=9A=84=E5=BA=8F=29?= Date: Thu, 24 Aug 2023 17:11:32 +0800 Subject: [PATCH] Support FBX mesh instancing (#104) --- Cli/ReadCliArgs.cpp | 18 +++ Cli/Test/ReadCliArgs.cpp | 6 + .../bee/Convert/SceneConverter.Mesh.cpp | 56 +++++++-- Core/Source/bee/Convert/SceneConverter.cpp | 3 +- Core/Source/bee/Convert/SceneConverter.h | 6 +- Core/Source/bee/Converter.h | 8 ++ Core/Test/Mesh.cpp | 116 ++++++++++++++++++ 7 files changed, 198 insertions(+), 15 deletions(-) diff --git a/Cli/ReadCliArgs.cpp b/Cli/ReadCliArgs.cpp index e62416e..dcd6ab1 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::animation_position_error_multiplier> { @@ -178,6 +188,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.", @@ -292,6 +306,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 c38ee77..3ffb8e2 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, @@ -164,6 +165,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/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 256b070..2d2a7f0 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 ed4f9ea..11a100a 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 ffea2fe..8290188 100644 --- a/Core/Test/Mesh.cpp +++ b/Core/Test/Mesh.cpp @@ -207,4 +207,120 @@ TEST_CASE("Mesh") { 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, ""); + } + } }