bevyengine / bevyengine/bevy

Mesh tangent generation is very slow

Open
#17,834 3 comments 0 reactions 0 assignees View on GitHub
A-Math A-Rendering C-Performance C-Usability S-Ready-For-Implementation
Dominant language
Rust
Stars
48.2k
Forks
4.8k
Avg merge
3d 22h
Merged PRs (30d)
161

Description

## 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.

```rust
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.

```rust
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](https://github.com/user-attachments/assets/5c1faa7f-bfe6-498b-8637-09e30f3ffc39)

### gram-schmidt

![Image](https://github.com/user-attachments/assets/8b7e2565-197e-41cb-9e68-e51255c79f11)

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.