Skip to content

Mesh tangent generation is very slow #17834

Open
@aloucks

Description

@aloucks

What problem does this solve or what need does it fill?

As the title states, generating tangents is quite slow. It's less of an issue when it's a one time operation but becomes more noticeable for frequent mesh updates or when generating many meshes.

What solution would you like?

It would be nice if the API allowed for the user to select which algorithm is used for tangent computation.

impl Mesh {
  #[deprecated(note = "use compute_tangents")]
  pub fn generate_tangents(&mut self) -> Result<(), GenerateTangentsError> { .. }

  pub fn compute_tangents(&mut self, tangent_strategy: TangentStrategy) -> Result<(), GenerateTangentsError> { .. }
  
  pub fn compute_normals(&mut self) { .. }
}

pub enum TangentStrategy {
  /// Uses the Gram-Schmidt fast approximation algorithm. Produces potentially lower quality tangents, but very fast.
  FastApproximation,
  /// Uses the `mikktspace` algorithm. Produces potentially higher quality tangents, but much slower.
  HighQuality,
}

I'm current using the following to compute the Gram-Schmidt tangents:

EDIT: This example is incorrect and producing a tangent per face instead of per vertex.

fn compute_tangents_fast(mesh: &mut Mesh) -> Vec<[f32; 4]> {
    let trinagle_count = mesh.indices().expect("indices").len() / 3;
    let mut tangents = Vec::with_capacity(trinagle_count);
    for triangle in mesh.triangles().expect("triangles") {
        let normal = Vec3::from(triangle.normal().expect("normal")).into();
        let verts = triangle.vertices;
        let verts: [Vec3A; 3] = [verts[0].into(), verts[1].into(), verts[2].into()];
        let edge1 = verts[1] - verts[0];
        let mut tangent = edge1 - normal * edge1.dot(normal);
        if tangent.length_squared() < f32::EPSILON {
            let edge2 = verts[2] - verts[1];
            tangent = edge2 - normal * edge2.dot(normal)
        }
        tangent = tangent.normalize();
        let bitangent_approximation = normal.cross(tangent).normalize();
        let w = if normal.cross(tangent).dot(bitangent_approximation) < 0.0 {
            -1.0
        } else {
            1.0
        };
        tangents.push(tangent.extend(w).to_array());
    }
    tangents
}

The method above is more than 20x faster for the terrain mesh chunks that I'm working on and I can't visually tell any difference between them.

generate_tangents:      2.4916 ms  (mikktspace)
compute_tangents_fast:  0.1065 ms  (Gram-Schmidt)

What alternative(s) have you considered?

I have a working alternative but it would be nice if this was in the bevy API.

Additional context

I can clean up the example code and start a PR if there's interest in this.

mikktspace

note: the thin green line is an axis poking up through the mesh. I think the camera was oriented very slightly differently.

Image

gram-schmidt

Image

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-MathFundamental domain-agnostic mathematical operationsA-RenderingDrawing game state to the screenC-PerformanceA change motivated by improving speed, memory usage or compile timesC-UsabilityA targeted quality-of-life change that makes Bevy easier to useS-Ready-For-ImplementationThis issue is ready for an implementation PR. Go for it!

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions