Skip to content

Commit

Permalink
Support FBX mesh instancing (#104)
Browse files Browse the repository at this point in the history
  • Loading branch information
shrinktofit authored Aug 24, 2023
1 parent 1b9ba0b commit 331edd2
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 15 deletions.
18 changes: 18 additions & 0 deletions Cli/ReadCliArgs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ getCommandLineArgsU8(int argc_, const char *argv_[]) {

template <auto MemberPtr> 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> {
Expand Down Expand Up @@ -178,6 +188,10 @@ std::optional<CliArgs> readCliArgs(std::span<std::string_view> args_) {
"Prefer local time spans recorded in FBX file for animation "
"exporting.",
cxxopts::value<bool>()->default_value("true"));

add_cxx_option
.template operator()<&bee::ConvertOptions::no_mesh_instancing>();

options.add_options()("match-mesh-names",
"Prefer mesh names "
"exporting.",
Expand Down Expand Up @@ -292,6 +306,10 @@ std::optional<CliArgs> readCliArgs(std::span<std::string_view> args_) {
cliArgs.convertOptions.prefer_local_time_span =
cliParseResult["prefer-local-time-span"].as<bool>();
}

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<bool>();
Expand Down
6 changes: 6 additions & 0 deletions Cli/Test/ReadCliArgs.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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");
}
Expand Down
56 changes: 46 additions & 10 deletions Core/Source/bee/Convert/SceneConverter.Mesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <bee/Convert/ConvertError.h>
#include <bee/Convert/SceneConverter.h>
#include <bee/Convert/fbxsdk/Spreader.h>
#include <bee/Convert/fbxsdk/String.h>
#include <bee/UntypedVertex.h>
#include <fmt/format.h>

Expand Down Expand Up @@ -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<decltype(_meshInstanceMap)::key_type> 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;
nodeMeshesSkinData = _extractNodeMeshesSkinData(fbx_meshes_);

Expand All @@ -66,13 +93,18 @@ SceneConverter::_convertNodeMeshes(
std::optional<std::uint32_t> 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];
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -143,7 +180,7 @@ std::string SceneConverter::_getName(fbxsdk::FbxMesh &fbx_mesh_,
}

std::tuple<fbxsdk::FbxMatrix, fbxsdk::FbxMatrix>
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 =
Expand Down Expand Up @@ -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;
Expand Down
3 changes: 1 addition & 2 deletions Core/Source/bee/Convert/SceneConverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
6 changes: 3 additions & 3 deletions Core/Source/bee/Convert/SceneConverter.h
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,7 @@ class SceneConverter {
_textureMap;
std::unordered_map<const fbxsdk::FbxNode *, FbxNodeDumpMeta> _nodeDumpMetaMap;
std::optional<fbxsdk::FbxDouble> _unitScaleFactor = 1.0;
std::unordered_map<fbxsdk::FbxMesh*, ConvertMeshResult> _meshInstanceMap;

inline fbxsdk::FbxVector4
_applyUnitScaleFactorV3(const fbxsdk::FbxVector4 &v_) const {
Expand Down Expand Up @@ -330,7 +331,7 @@ class SceneConverter {
std::string _getName(fbxsdk::FbxMesh &fbx_mesh_, fbxsdk::FbxNode &fbx_node_);

std::tuple<fbxsdk::FbxMatrix, fbxsdk::FbxMatrix>
_getGeometrixTransform(fbxsdk::FbxNode &fbx_node_);
_getGeometrixTransform(const fbxsdk::FbxNode &fbx_node_);

fx::gltf::Primitive _convertMeshAsPrimitive(
fbxsdk::FbxMesh &fbx_mesh_,
Expand All @@ -357,8 +358,7 @@ class SceneConverter {
std::list<VertexBulk>
_typeVertices(const FbxMeshVertexLayout &vertex_layout_);

fbxsdk::FbxSurfaceMaterial *_getTheUniqueMaterial(fbxsdk::FbxMesh &fbx_mesh_,
fbxsdk::FbxNode &fbx_node_);
fbxsdk::FbxSurfaceMaterial *_getTheUniqueMaterial(fbxsdk::FbxMesh &fbx_mesh_);

/// <summary>
/// Things get even more complicated if there are more than one mesh attached to a node.
Expand Down
8 changes: 8 additions & 0 deletions Core/Source/bee/Converter.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ struct ConvertOptions {

bool match_mesh_names = true;

/// <summary>
/// Whether to disable mesh instancing.
/// </summary>
/// <default>
/// false
/// </default>
bool no_mesh_instancing = false;

float animation_position_error_multiplier = 1e-5f;

float animation_scale_error_multiplier = 1e-5f;
Expand Down
116 changes: 116 additions & 0 deletions Core/Test/Mesh.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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, "");
}
}
}

0 comments on commit 331edd2

Please sign in to comment.