Skip to content

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

aloucks
Copy link
Contributor

@aloucks aloucks commented Feb 22, 2025

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. The HighQuality 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.

How can other people (reviewers) test your changes? Is there anything specific they need to know?

I checked the deferred_rendering, parallax_mapping, and anisotropy 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 renamed GenerateTangentsError::AlgorithmError
  • The Mesh::generate_tangents and Mesh::with_generated_tangents methods are now deprecated. They are replaced by compute_tangents and with_computed_tangents, both of which accept a TangentStrategy parameter. The naming conventino of these methods now matches compute_normals.

@aloucks aloucks force-pushed the issue-17834-compute-tangents branch 5 times, most recently from c41e77e to e409343 Compare February 22, 2025 19:39
@@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub computed_tangent_strategy: TangentStrategy,
pub tangent_calculation_strategy: TangentCalculationStrategy,

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed

@aloucks aloucks force-pushed the issue-17834-compute-tangents branch 3 times, most recently from ec40cb6 to 3495615 Compare February 22, 2025 20:05
@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-Assets Load files from disk to use for things like images, models, and sounds S-Needs-Review Needs reviewer attention (from anyone!) to move forward A-glTF Related to the glTF 3D scene/model format X-Uncontroversial This work is generally agreed upon labels Feb 23, 2025
@hukasu
Copy link
Contributor

hukasu commented Feb 23, 2025

the anisotropy example does use tangents, this issue shows some issues caused by not optimal tangents #16179

@hukasu
Copy link
Contributor

hukasu commented Feb 23, 2025

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

@aloucks
Copy link
Contributor Author

aloucks commented Feb 23, 2025

the anisotropy example does use tangents, this issue shows some issues caused by not optimal tangents #16179

The sphere looks much better with the gram-schmidt tangents. I actually left Sphere out of the tests that I added because there was at least one vertex where the mikktspace generated tangent differed from the gram-schmidt tangent enough that it was outside of the test threshold.

mikktspace

sphere_mikktspace.mp4

gram-schmidt

sphere_gramschmidt.mp4

@hukasu
Copy link
Contributor

hukasu commented Feb 24, 2025

Can the strategy names be changed to just the name of the algorithm? Mikktspace is very dependant on the UVs, so just calling it HighQuality might be misleading

@aloucks
Copy link
Contributor Author

aloucks commented Feb 24, 2025

Can the strategy names be changed to just the name of the algorithm? Mikktspace is very dependant on the UVs, so just calling it HighQuality might be misleading

Bikeshedding time! 🎨

@JMS55 had suggested to inject Calculation into the name, but I think it's less necessary if we change Strategy to Algorithm. The word "algorithm" implies the "calculation" part, imo. Also, do we want to keep HighQuality/Mikktspace the default?

@JMS55 / @hukasu what are your thoughts?

pub enum TangentAlgorithm {
  Mikktspace,
  GramSchmidt,
}

@aloucks aloucks force-pushed the issue-17834-compute-tangents branch from 04b5581 to a65fe59 Compare February 26, 2025 19:50
@aloucks
Copy link
Contributor Author

aloucks commented Feb 26, 2025

@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();
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

@IQuick143 IQuick143 Feb 27, 2025

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.

Copy link
Contributor Author

@aloucks aloucks Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IQuick143

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();

Copy link
Contributor

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.

Copy link
Contributor Author

@aloucks aloucks Mar 2, 2025

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.

@aloucks aloucks force-pushed the issue-17834-compute-tangents branch from 0db39c9 to d750eef Compare March 1, 2025 16:21
github-merge-queue bot pushed a commit that referenced this pull request Mar 7, 2025
)

Fixes #17170

# Objective

Tangents are not currently transformed as described in #17170. I came
across this while working on #17989 and it seemed like low hanging
fruit.
@aloucks aloucks force-pushed the issue-17834-compute-tangents branch from d750eef to 4490ead Compare March 21, 2025 16:06
@aloucks
Copy link
Contributor Author

aloucks commented Mar 21, 2025

@hukasu I renamed the enum to TangentAlgorithm and the associated variants.

Any chance this can land before 0.16 is released?

@hukasu
Copy link
Contributor

hukasu commented Mar 21, 2025

i don't have authority to say when it can be merged

@aloucks aloucks force-pushed the issue-17834-compute-tangents branch 2 times, most recently from 4d50d9f to fa910e0 Compare March 25, 2025 02:35
@aloucks
Copy link
Contributor Author

aloucks commented Mar 25, 2025

@alice-i-cecile Is this too late for 0.16?

@aloucks aloucks force-pushed the issue-17834-compute-tangents branch from fa910e0 to ae5d3aa Compare March 25, 2025 23:50
@alice-i-cecile
Copy link
Member

Yeah, we're down to "essential bug fixes and polish" at this stage, sorry!

aloucks added 6 commits March 30, 2025 01:27
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.
@aloucks aloucks force-pushed the issue-17834-compute-tangents branch from ae5d3aa to 362735b Compare March 30, 2025 05:27
@aloucks
Copy link
Contributor Author

aloucks commented May 3, 2025

@alice-i-cecile Now that 0.16 is out, could this be considered for 0.17?

@alice-i-cecile alice-i-cecile added this to the 0.17 milestone May 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Assets Load files from disk to use for things like images, models, and sounds A-glTF Related to the glTF 3D scene/model format C-Feature A new feature, making something new possible S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Uncontroversial This work is generally agreed upon
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Mesh tangent generation is very slow
5 participants