Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Populate polygon face fields in FBX I/O (#1067)
Summary:

Wire up the polyFaces, polyFaceSizes, and polyTexcoordFaces fields added
in the previous diff so they are populated during FBX load and written
during FBX save.

Load: copy raw polygon topology from PolygonData into the new mesh fields
alongside the existing triangulation.

Save: when polygon data is available, write actual polygons (quads, n-gons)
instead of only triangles; fall back to triangulated faces otherwise.

Reviewed By: nickyhe-gemini

Differential Revision: D94577295
  • Loading branch information
cdtwigg authored and facebook-github-bot committed Mar 4, 2026
commit a3a892254baaa15c63792786adcbe3be82d3e6fc
69 changes: 53 additions & 16 deletions momentum/io/fbx/fbx_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,56 @@ void addMetaData(::fbxsdk::FbxNode* skeletonRootNode, const Character& character
}
}

void writePolygonsToFbxMesh(const Mesh& mesh, ::fbxsdk::FbxMesh* lMesh) {
if (!mesh.polyFaces.empty() && !mesh.polyFaceSizes.empty()) {
// Write original polygon topology
uint32_t offset = 0;
for (const auto polySize : mesh.polyFaceSizes) {
lMesh->BeginPolygon();
for (uint32_t i = 0; i < polySize; ++i) {
lMesh->AddPolygon(mesh.polyFaces[offset + i]);
}
lMesh->EndPolygon();
offset += polySize;
}
} else {
// Fall back to triangulated faces
for (const auto& face : mesh.faces) {
lMesh->BeginPolygon();
for (int i = 0; i < 3; i++) {
lMesh->AddPolygon(face[i]);
}
lMesh->EndPolygon();
}
}
}

void writeTextureUVIndicesToFbxMesh(
const Mesh& mesh,
::fbxsdk::FbxMesh* lMesh,
fbxsdk::FbxLayerElement::EType uvType) {
if (!mesh.polyFaces.empty() && !mesh.polyFaceSizes.empty()) {
uint32_t offset = 0;
int polyIdx = 0;
for (const auto polySize : mesh.polyFaceSizes) {
for (uint32_t i = 0; i < polySize; ++i) {
const uint32_t texIdx = mesh.polyTexcoordFaces.empty() ? mesh.polyFaces[offset + i]
: mesh.polyTexcoordFaces[offset + i];
lMesh->SetTextureUVIndex(polyIdx, i, texIdx, uvType);
}
offset += polySize;
polyIdx++;
}
} else {
for (int faceIdx = 0; faceIdx < static_cast<int>(mesh.texcoord_faces.size()); ++faceIdx) {
const auto& texcoords = mesh.texcoord_faces[faceIdx];
lMesh->SetTextureUVIndex(faceIdx, 0, texcoords[0], uvType);
lMesh->SetTextureUVIndex(faceIdx, 1, texcoords[1], uvType);
lMesh->SetTextureUVIndex(faceIdx, 2, texcoords[2], uvType);
}
}
}

void saveFbxCommon(
const filesystem::path& filename,
const Character& character,
Expand Down Expand Up @@ -687,7 +737,6 @@ void saveFbxCommon(
if (saveMesh && character.mesh != nullptr) {
// Add the mesh
const int numVertices = character.mesh.get()->vertices.size();
const int numFaces = character.mesh.get()->faces.size();
::fbxsdk::FbxNode* meshNode = ::fbxsdk::FbxNode::Create(scene, "body_mesh");
::fbxsdk::FbxMesh* lMesh = ::fbxsdk::FbxMesh::Create(scene, "mesh");
lMesh->SetControlPointCount(numVertices);
Expand All @@ -704,14 +753,7 @@ void saveFbxCommon(
lMesh->SetControlPointAt(point, normal, i);
}
// Add polygons to lMesh
for (int iFace = 0; iFace < numFaces; iFace++) {
lMesh->BeginPolygon();
for (int i = 0; i < 3; i++) { // We have tris for models. This could be extended for
// supporting Quads or npoly if needed.
lMesh->AddPolygon(character.mesh.get()->faces[iFace][i]);
}
lMesh->EndPolygon();
}
writePolygonsToFbxMesh(*character.mesh, lMesh);
lMesh->BuildMeshEdgeArray();
meshNode->SetNodeAttribute(lMesh);

Expand All @@ -733,13 +775,8 @@ void saveFbxCommon(
lMesh->AddTextureUV(::fbxsdk::FbxVector2(texcoords[0], 1.0f - texcoords[1]), uvType);
}

// Set UV indices for each face. We only have triangles.
int faceCount = 0;
for (const auto& texcoords : character.mesh->texcoord_faces) {
lMesh->SetTextureUVIndex(faceCount, 0, texcoords[0], uvType);
lMesh->SetTextureUVIndex(faceCount, 1, texcoords[1], uvType);
lMesh->SetTextureUVIndex(faceCount++, 2, texcoords[2], uvType);
}
// Set UV indices for each face.
writeTextureUVIndicesToFbxMesh(*character.mesh, lMesh, uvType);
}

// ---------------------------------------------
Expand Down
24 changes: 24 additions & 0 deletions momentum/io/fbx/openfbx_loader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,27 @@ parseSkeleton(const ofbx::Object* sceneRoot, const std::string& skelRoot, Permis
return {skeleton, jointFbxNodes, locators, collision};
}

// Note: we intentionally omit reserve() here. This function is called once per sub-mesh in a loop,
// and reserve(size() + batchSize) defeats std::vector's geometric growth, causing O(K*N) copies
// instead of O(N) amortized. Letting the default 2x growth strategy handle it is more efficient.
void populatePolyFaceData(
Mesh& mesh,
const PolygonData& vertices,
size_t vertexOffset,
size_t textureCoordOffset) {
for (const auto& idx : vertices.indices) {
mesh.polyFaces.push_back(idx + static_cast<uint32_t>(vertexOffset));
}
for (size_t i = 0; i + 1 < vertices.offsets.size(); ++i) {
mesh.polyFaceSizes.push_back(vertices.offsets[i + 1] - vertices.offsets[i]);
}
if (!vertices.textureIndices.empty()) {
for (const auto& idx : vertices.textureIndices) {
mesh.polyTexcoordFaces.push_back(idx + static_cast<uint32_t>(textureCoordOffset));
}
}
}

void parseSkinnedModel(
const ofbx::Mesh* meshRoot,
const std::vector<const ofbx::Object*>& boneFbxNodes,
Expand Down Expand Up @@ -659,6 +680,9 @@ void parseSkinnedModel(
mesh.texcoord_faces.emplace_back(t + Eigen::Vector3i::Constant(textureCoordOffset));
}

// Populate polygon face data (preserves original topology)
populatePolyFaceData(mesh, vertices, vertexOffset, textureCoordOffset);

const auto* blendshapes = geometry->getBlendShape();
if (loadBlendShapes == LoadBlendShapes::Yes && blendshapes) {
if (!blendShape) {
Expand Down
Loading