Skip to content

Importer: fbx: join identical vertices again - #3533

Open
icauro wants to merge 2 commits into
stride3d:masterfrom
icauro:fix/importer-join-identical-vertices
Open

icauro wants to merge 2 commits into
stride3d:masterfrom
icauro:fix/importer-join-identical-vertices

Conversation

@icauro

@icauro icauro commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Restores aiProcess_JoinIdenticalVertices, dropped in the Assimp importer rewrite (#2163). Since then, FBX, OBJ and Collada meshes import with one vertex per face corner, so no vertex is shared between triangles. Also bumps the ImportModelCommand version so existing projects re-import their models once.

Timeline

  • 2018: the C++ importer used aiProcess_JoinIdenticalVertices.
  • 2023-08, #1719: the Silk.NET port kept it.
  • 2024-04, #2163: the unified importer's flag list leaves it out, apparently by accident; it isn't discussed in the PR.
  • 2024-06, #2291: the flags were rewritten, still without it.

It went unnoticed because rendering looks the same, and glTF exporters already write shared vertices.

Results

Model Before After
Head, FBX (17,662 quads) 70,648 vertices 18,437 (matches its glTF export)
Skinned character, FBX 38,436 vertices (= index count) 7,380
Same head, glTF 18,437 18,437

Smaller vertex buffers and content files, and the GPU can reuse already-processed shared vertices again.

Position, normal, UVs, colours and skin weights are byte-identical per triangle before and after. Tangents are now averaged across faces that share a vertex, as for glTF imports and the pre-2024 importer.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)

The Assimp importer rewrite dropped aiProcess_JoinIdenticalVertices, so FBX
meshes import with one vertex per face corner. Bump the import version to
regenerate models.
@xen2

xen2 commented Oct 8, 2026

Copy link
Copy Markdown
Member

@Eideren Any memory as to why it was removed in #2163?

@Eideren

Eideren commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

@xen2 I can't remember the exact reason why that was the case, not even sure if that was mine or Noa's call. But if it was mine, it's likely that I interpreted this flag as forcing split vertices (unique vertex attribute values, like normals, uvs or colors for each faces connected to it) to be fused back together. Those vertex attributes are split on purpose, but evidently, given the official documentation on this property, it's just spitting back out a standard index+vertex buffer mesh instead of a triangle soup.
I'm sure this change is fine as is, it would have been quite obvious if my assumption about this property would have been correct as texture coordinates would have been completely broken otherwise.

@icauro

icauro commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

@xen2 what about this: i can setup importer path equivalency test, with simple object with different format like sphere.fbx sphere.gltf etc, and also in binary save of it. Even maybe held blender file in the repo for future edits or refence, all of size not passing 4 MiB for example. That would catch these kind of regressions in the future.

@xen2

xen2 commented Oct 9, 2026

Copy link
Copy Markdown
Member

Tangents are now averaged across faces that share a vertex, as for glTF imports and the pre-2024 importer.

I don't think this should change tangents (except within epsilon).
In Assimp, JoinIdenticalVertices runs after normal/tangent calculation, and it only merges vertices when all attributes (including tangents) are equal within epsilon. How did you compare them?
This also confirms @Eideren's point: split vertices (UV seams, hard edges) stay split.

So all good on my side.

@icauro sure, it would be good to have some tests, at least for FBX and glTF (note: I don't think Assimp supports .blend anymore).
If you do a binary dump, be careful: the whole Stride data has headers that we might bump, it needs a regeneration script, float results can differ slightly between OS/CPU (so it needs an epsilon), and it is hard to inspect in a PR diff.
Maybe it's enough to check the VB/IB counts and the attribute list, and to compare a few selected vertices in code?

Better in a separate PR, so I can merge this one first?

@xen2

xen2 commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Correction to my previous comment: I checked again after the CI failure, and tangents do change.

Stride ships Assimp 6.0.2, where JoinIdenticalVertices ignores tangents (regression from assimp/assimp#5895).
Vertices with the same position/normal/UV are merged, and they keep the first vertex's tangent.

This is fixed upstream in Assimp 6.0.3 (assimp/assimp#6278), but the Ultz.Native.Assimp package used by Silk.NET is still on 6.0.2.
Maybe we need to ask them if a release with latest assimp can be done?
It might take time. Another alternative is to roll our own dedup (probably in MeshConverter.cs line 1045).

vbPointer += vertexStride;
}
}
// Build the indices data buffer
var nbIndices = (int)(3 * mesh->MNumFaces);
var is32BitIndex = mesh->MNumVertices > 65535;

@icauro

icauro commented Oct 9, 2026

Copy link
Copy Markdown
Contributor Author

ok, i will focus on possible regression test improvements later, possibly after my morph target / compute skinner pr.

@sasvdw

sasvdw commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

@xen2 it's weird to me that so many downstream dependencies are pinned. If they're following semver, then surely it isn't necessary to pin all dependencies like this? Same goes for Stride components itself.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants