Conversation
Previously, we unified triangle corners into groups with consistent orientation, allowing degenerate triangles to be unified into separate groups independently. This could lead to inconsistent tangent orientations on triangle corners for degenerate triangles; this makes tangent space interpolation within triangles like nonsensical. This is similar to how MikkTSpace works too: while it assigns orientations to degenerate triangles based on non-degenerate neighbors, it still allows corners of these triangles that don't participate in other groups to fall back to -1 orientation. This change instead adopts triangle groups simlar to MikkTSpace but ensures full consistency at all corners by propagating the groups through vertices with degenerate-only fans. This results in a similar number of split vertices to MikkTS but consistent orientation and more consistent interpolation. For performance, we only do the unification if either triangle is degenerate; this results in degenerate islands being grouped together with their boundary, which might be useful in the future for better degenerate tangent propagation.
Instead of using incident angle as a weight, scale it by the product of the two adjacent edge lengths. This deviates from MikkTSpace but significantly improves the tangent quality around beveled areas which are very common in practice. This should be generally safe and produce superior quality but if the asset normal maps were baked with the exact tangents MikkTSpace computed, this could result in difference in shading; for users who need exact MikkTSpace compatible weights, a new option can be used to achieve that.
If all three points of a triangle have the same position, but have different UVs, the determinant is non-zero but rs is zero which is mostly reasonable. However, if the triangle is a degenerate line, we compute an actual tangent vector and treat the triangle as non-degenerate. This can lead to additional splits and disagreements with MikkTSpace, and is fundamentally flawed because there isn't a well defined tangent on a line triangles. We now enforce consistency here by setting rw to 0, similarly to when the UVs are degenerate but positions aren't.
By default, we use MikkTSpace fallbacks and for vertices where a valid tangent can not be computed, we set the vector to (1,0,0). These tangents are fundamentally invalid - they aren't orthogonal to the normal and using them in shading may result in skewed normals. For applications that can handle non-orthogonal frames, using zero vector might be a reasonable alternative; it flattens the normal map instead of rotating it arbitrarily. For applications that can give user feedback, it may be valuable to detect the invalid frames and warn, which is difficult for 1,0,0 fallback.
This is to avoid confusion because by default, our weighting diverges from MikkTSpace; generating MikkTSpace tangents requires using meshopt_TangentCompatible flag.
This is carefully constructed so that two triangles share a fan that subsumes the degenerate triangle, creating a split with the third edge along which the two vertices have duplicate tangents. This also validates averaging behavior.
|
For reference - Eric Lengyel mentions in FGED2 section 7.5 a maybe compatible quality addition -- to duplicate vertices on tangent frame discontinuities, set their tangents to their average if they share the same handedness and whose original tangents point in similar directions. |
|
This is described in the sample chapter https://foundationsofgameenginedev.com/FGED2-sample.pdf but I don't think it's very well motivated or really very compatible; it requires some sort of cutoff for directions which would need to be tuned. I have tested Lengyel's approach to implicit weighting (by averaging un-normalized tangents) last week and found it generally produces worse quality tangents, and the code doesn't handle tangent orientation correctly at all when UVs are mirrored so I think in general I am not keen on taking any FGED material about tangents into account. Note that when the vertices are split on different attributes, e.g. if you have a second UV set for lightmaps that introduces its own seams, MikkTSpace, as well as my code, handles this correctly because the normals & UVs are identical at these vertices; FGED code doesn't and the note you mention would fix that (which is indeed correct and matches the spirit of building tangent frames that are independent of the mesh indexing). |
Add a section about tangent space generation, for now with minimal example and a reference to demo/main.cpp to full splitting loop.
Add an inline example for splitting vertices based on tangents
We are now using a slightly cleaner style where data is first copied into vertices and then splits are fixed up in a separate pass, which matches the readme version.
This change builds on top of #1046 and improves the tangent generation behavior further. A couple fixes improve consistency with MikkTSpace while improving some situations that MikkTSpace mishandles; for example, with this change tangent frame orientation is now consistent across all three triangle corners. Additionally, by default this change switches the tangent weighting to be area-sensitive. This is a departure from MikkTSpace; on real world geometry it's often the case that MikkTSpace generates broken tangents because it over-weighs tangent on thin bevels which displaces tangents away from regular triangles.
While this is almost always an improvement, and is generally benign on other parts of the mesh that don't exhibit this problem, this technically produces different tangents which may distort the lighting if the normal maps were baked from higher resolution geometry using MikkTSpace weighting. Because of this, an option
meshopt_TangentCompatiblecan be used to use the original MikkTSpace weighting.I've also spent some time looking into how to improve tangents on degenerate UV triangles (that are zero-area in UV space), but so far all solutions have tradeoffs and aren't clearly beneficial. An experimental option
meshopt_TangentZeroFallbackcan be used to zero these out which can work better as it flattens the normal map but it requires renderers to handle non-orthogonal frames properly which is atypical.A few examples of improved weighting; the left side matches MikkTSpace perfectly whereas the right side is the new default:
This contribution is sponsored by Valve.