-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Add gram-schmidt fast approximation tangent generation algorithm #17989
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
c41e77e
to
e409343
Compare
crates/bevy_gltf/src/lib.rs
Outdated
@@ -124,6 +124,8 @@ pub mod prelude { | |||
#[derive(Default)] | |||
pub struct GltfPlugin { | |||
custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>, | |||
/// The strategy to use when computing mesh tangents. | |||
pub computed_tangent_strategy: TangentStrategy, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub computed_tangent_strategy: TangentStrategy, | |
pub tangent_calculation_strategy: TangentCalculationStrategy, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
renamed
ec40cb6
to
3495615
Compare
the |
i was making tangent generation for the primitives, but with a very naive algorithm #17691, if your method works without causing seams then my PR becames redundant |
The sphere looks much better with the gram-schmidt tangents. I actually left mikktspacesphere_mikktspace.mp4gram-schmidtsphere_gramschmidt.mp4 |
Can the strategy names be changed to just the name of the algorithm? Mikktspace is very dependant on the UVs, so just calling it |
Bikeshedding time! 🎨 @JMS55 had suggested to inject @JMS55 / @hukasu what are your thoughts? pub enum TangentAlgorithm {
Mikktspace,
GramSchmidt,
} |
04b5581
to
a65fe59
Compare
@JMS55 Anymore feedback on the enum and variant naming? Do we want to leave mikktspace the default or change to gram-schmidt? |
|
||
for i in 0..vertex_count { | ||
let normal = Vec3A::from_array(normals[i]); | ||
let tangent = tangents[i].normalize(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a numerical reason to normalize here?
The next line already involves a normalization and is linear in the tangent
variable.
On the other hand, I don't see the normal getting normalised, and that would cause problems in the G-S algo.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a numerical reason to normalize here?
The next line already involves a normalization and is linear in the tangent variable.
The examples I've seen either normalize the accumulated tangent it or average it by dividing by the number of vertices that were used to accumulate it. I don't think the direction would be correct if we only normalized the final result and omitted the normalizing here.
On the other hand, I don't see the normal getting normalised, and that would cause problems in the G-S algo.
This assumes that the vertex normals are normalized. Is that assumption false? If so, I'll need to normalize the normal here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not familiar with the code enough to determine whether the normals are normalised, although I would expect so.
Regarding the pre-normalisation of the tangent, that part is mathematically unnecessary as the projection is linear.
tangent = P(t).normalise()
where P
is the projection operator x -> x - x.dot(normal) * normal
, which is linear.
This tells us that if t
is not normalised by a positive factor k
we have:
P(k*t).normalise() = (k * P(t)).normalise() = P(t).normalise()
Where the first equality comes from the linearity of P, and the second from normalisation destroying these scalar factors.
So from a purely theoretical standpoint the normalisation has no effect.
The only reason I can think of for the normalisation would be to avoid some sort of numerical error, although I cannot really think of the reasons it would be relevant here given how floating point values work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about this?
let normal = Vec3A::from_array(normals[i]);
// Assume that the normal has not been pre-normalized
let normal = normal.normalize_or_zero();
// Although it is not mathematically necessary to normalize the accumulated
// tangent, doing so may provide better numerical stability in extreme cases.
let tangent = tangents[i].normalize();
// Gram-Schmidt orthogonalization
let tangent = (tangent - normal * normal.dot(tangent)).normalize();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can save a few operations (a square root and some divides) by doing:
let tangent = (tangent - normal * (normal.dot(tangent) / normal.magnitude_squared())).normalize();
Instead of normalising the normal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can save a few operations (a square root and some divides) by doing:
let tangent = (tangent - normal * (normal.dot(tangent) / normal.magnitude_squared())).normalize();Instead of normalising the normal.
Unfortunately that causes other issues when the normal is broken and has length of zero. The cone
unit test fails. Using normalize_or_zero
instead of normalize
when normalizing the normal guards against that case.
Using your suggestion above (or using normalize
instead of normalize_or_zero
) results in a tangent that has all NaN
values.
thread 'gramschmidt::tests::cone' panicked at crates\bevy_mesh\src\gramschmidt.rs:172:13:
tangents differ significantly: hq = Vec3A(1.0, 2.0480125e-5, -1.684416e-8), fa = Vec3A(NaN, NaN, NaN), angle.to_degrees() = NaN
There's probably an argument to be made that the tangent is likely wrong in either case, but I guess this way it at least aligns with what mikktspace would have produced and doesn't contain NaN
values.
0db39c9
to
d750eef
Compare
d750eef
to
4490ead
Compare
@hukasu I renamed the enum to Any chance this can land before |
i don't have authority to say when it can be merged |
4d50d9f
to
fa910e0
Compare
@alice-i-cecile Is this too late for 0.16? |
fa910e0
to
ae5d3aa
Compare
Yeah, we're down to "essential bug fixes and polish" at this stage, sorry! |
Also added a note about why the accumulated tangent is normalized prior to applying Gram-Schmidt orthogonalization. Normalizing the accumulated tangent may not be mathematically necessary, but it may prevent numerical stability issues in some cases.
The variants have also been renamed to match their algorithm names.
ae5d3aa
to
362735b
Compare
@alice-i-cecile Now that 0.16 is out, could this be considered for 0.17? |
Provide a fast alternative algorithm for tangent generation
Fixes: #17834
Related (maybe): #17877
Solution
Add a new enum
TangentStrategy
that gives the user the option to select how tangents are generated.The GLTF loader now has a property to set which algorithm is used for models that are missing tangents.
The
FastApproximation
strategy is implemented with the Gram-Schmidt fast approximation technique. TheHighQuality
variant uses MikkTSpace.Testing
Unit tests have been added to assert that both tangent computation strategies produce similar tangents for simple shapes. I also tried to check several examples to verify that the visual results are acceptable with both algorithms. I wasn't able to tell any visible difference.
I checked the
deferred_rendering
,parallax_mapping
, andanisotropy
examples. Suspiciously, when I produced intentionally invalid tangents, the deferred_rendering and parallax_mapping examples seemed to render fine. I didn't try the anisotropy example with intentionally invalid values. I'm curious if there is an unrelated bug in the shader where tangents are ignored (or the examples I tested somehow didn't leverage tangents).Migration Guide
GenerateTangentsError::MikktspaceError
has been renamedGenerateTangentsError::AlgorithmError
Mesh::generate_tangents
andMesh::with_generated_tangents
methods are now deprecated. They are replaced bycompute_tangents
andwith_computed_tangents
, both of which accept aTangentStrategy
parameter. The naming conventino of these methods now matchescompute_normals
.