Skip to content

Incorrect tangents on meshes with non-uniform scale #120980

Description

@zeux

Tested versions

  • Reproducible in: 4.7, master

System information

Vulkan (Forward+), Ubuntu 26.04

Issue description

All three shaders Godot uses for scene rendering (scene.glsl, scene_forward_clustered.glsl, scene_forward_mobile.glsl) use normal matrix to transform normal, tangent and bitangent (called "binormal" in the code) vectors.

This is incorrect. A basic intuition as to why the normal needs to use the normal matrix is that normal is a cross product of vectors in the tangent plane (tangent & bitangent), and tangent & bitangent are position gradients along U and V directions. Gradients are linear under matrix transformation; grad(P * M) = grad(P) * M. So any tangent vector needs to be transformed by M. Normal is the cross product of T & B; cross(grad_u(P) * M, grad_v(P) * M) = det(M) * cross(grad_u(P), grad_v(P)) * inverse(transpose(M)) = N * (det(M) * inverse(transpose(M)) = N * transpose(adj(M)). Godot uses the "inverse transpose" formulation to compute the normal matrix which is correct up to the scalar det(M) factor which is normalized away anyhow. Unless det(M) = 0 in which case Godot computes a degenerate inverse but that's a separate bug ;)

Steps to reproduce

Open the attached MRP; it has two spheres. One is a regular sphere that is transformed using non-uniform scale. The other one is a pre-scaled sphere, where the vertex transform is baked into positions/normals in the OBJ file. Both spheres then have the same normal map applied. You would expect that the lighting looks similar on both spheres, but it's visibly different:

Image

Note that because of how MikkTSpace weighting works, there's no expectation that these look exactly identical; the weighting of resulting tangents depends on the angle between vertex and triangle normals, and these angles change under non-uniform scaling. Also tangent frames are orthogonalized under MikkTSpace rules, and transforming both tangent vectors instead of re-deriving bitangent breaks that too. But the lighting is visibly off here and it should be much much closer.

Minimal reproduction project (MRP)

tangent_scale_mrp.zip

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions