Skip to content

Commit 681c9c6

Browse files
committed
bevy_pbr: Fix tangent and normal normalization (#5666)
# Objective - Morten Mikkelsen clarified that the world normal and tangent must be normalized in the vertex stage and the interpolated values must not be normalized in the fragment stage. This is in order to match the mikktspace approach exactly. - Fixes #5514 by ensuring the tangent basis matrix (TBN) is orthonormal ## Solution - Normalize the world normal in the vertex stage and not the fragment stage - Normalize the world tangent xyz in the vertex stage - Take into account the sign of the determinant of the local to world matrix when calculating the bitangent --- ## Changelog - Fixed - scaling a model that uses normal mapping now has correct lighting again
1 parent 1c6be94 commit 681c9c6

File tree

4 files changed

+59
-18
lines changed

4 files changed

+59
-18
lines changed

crates/bevy_pbr/src/render/mesh.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use bevy_ecs::{
99
prelude::*,
1010
system::{lifetimeless::*, SystemParamItem, SystemState},
1111
};
12-
use bevy_math::{Mat4, Vec2};
12+
use bevy_math::{Mat3A, Mat4, Vec2};
1313
use bevy_reflect::TypeUuid;
1414
use bevy_render::{
1515
extract_component::{ComponentUniforms, DynamicUniformIndex, UniformComponentPlugin},
@@ -117,6 +117,9 @@ bitflags::bitflags! {
117117
#[repr(transparent)]
118118
struct MeshFlags: u32 {
119119
const SHADOW_RECEIVER = (1 << 0);
120+
// Indicates the sign of the determinant of the 3x3 model matrix. If the sign is positive,
121+
// then the flag should be set, else it should not be set.
122+
const SIGN_DETERMINANT_MODEL_3X3 = (1 << 31);
120123
const NONE = 0;
121124
const UNINITIALIZED = 0xFFFF;
122125
}
@@ -143,13 +146,16 @@ pub fn extract_meshes(
143146

144147
for (entity, _, transform, handle, not_receiver, not_caster) in visible_meshes {
145148
let transform = transform.compute_matrix();
146-
let shadow_receiver_flags = if not_receiver.is_some() {
147-
MeshFlags::empty().bits
149+
let mut flags = if not_receiver.is_some() {
150+
MeshFlags::empty()
148151
} else {
149-
MeshFlags::SHADOW_RECEIVER.bits
152+
MeshFlags::SHADOW_RECEIVER
150153
};
154+
if Mat3A::from_mat4(transform).determinant().is_sign_positive() {
155+
flags |= MeshFlags::SIGN_DETERMINANT_MODEL_3X3;
156+
}
151157
let uniform = MeshUniform {
152-
flags: shadow_receiver_flags,
158+
flags: flags.bits,
153159
transform,
154160
inverse_transpose_model: transform.inverse().transpose(),
155161
};

crates/bevy_pbr/src/render/mesh_functions.wgsl

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,47 @@ fn mesh_position_local_to_clip(model: mat4x4<f32>, vertex_position: vec4<f32>) -
1717
}
1818

1919
fn mesh_normal_local_to_world(vertex_normal: vec3<f32>) -> vec3<f32> {
20-
return mat3x3<f32>(
21-
mesh.inverse_transpose_model[0].xyz,
22-
mesh.inverse_transpose_model[1].xyz,
23-
mesh.inverse_transpose_model[2].xyz
24-
) * vertex_normal;
20+
// NOTE: The mikktspace method of normal mapping requires that the world normal is
21+
// re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents
22+
// and normal maps so that the exact inverse process is applied when shading. Blender, Unity,
23+
// Unreal Engine, Godot, and more all use the mikktspace method. Do not change this code
24+
// unless you really know what you are doing.
25+
// http://www.mikktspace.com/
26+
return normalize(
27+
mat3x3<f32>(
28+
mesh.inverse_transpose_model[0].xyz,
29+
mesh.inverse_transpose_model[1].xyz,
30+
mesh.inverse_transpose_model[2].xyz
31+
) * vertex_normal
32+
);
33+
}
34+
35+
// Calculates the sign of the determinant of the 3x3 model matrix based on a
36+
// mesh flag
37+
fn sign_determinant_model_3x3() -> f32 {
38+
// bool(u32) is false if 0u else true
39+
// f32(bool) is 1.0 if true else 0.0
40+
// * 2.0 - 1.0 remaps 0.0 or 1.0 to -1.0 or 1.0 respectively
41+
return f32(bool(mesh.flags & MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT)) * 2.0 - 1.0;
2542
}
2643

2744
fn mesh_tangent_local_to_world(model: mat4x4<f32>, vertex_tangent: vec4<f32>) -> vec4<f32> {
45+
// NOTE: The mikktspace method of normal mapping requires that the world tangent is
46+
// re-normalized in the vertex shader to match the way mikktspace bakes vertex tangents
47+
// and normal maps so that the exact inverse process is applied when shading. Blender, Unity,
48+
// Unreal Engine, Godot, and more all use the mikktspace method. Do not change this code
49+
// unless you really know what you are doing.
50+
// http://www.mikktspace.com/
2851
return vec4<f32>(
29-
mat3x3<f32>(
30-
model[0].xyz,
31-
model[1].xyz,
32-
model[2].xyz
33-
) * vertex_tangent.xyz,
34-
vertex_tangent.w
52+
normalize(
53+
mat3x3<f32>(
54+
model[0].xyz,
55+
model[1].xyz,
56+
model[2].xyz
57+
) * vertex_tangent.xyz
58+
),
59+
// NOTE: Multiplying by the sign of the determinant of the 3x3 model matrix accounts for
60+
// situations such as negative scaling.
61+
vertex_tangent.w * sign_determinant_model_3x3()
3562
);
3663
}

crates/bevy_pbr/src/render/mesh_types.wgsl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,5 @@ struct SkinnedMesh {
1414
#endif
1515

1616
let MESH_FLAGS_SHADOW_RECEIVER_BIT: u32 = 1u;
17+
// 2^31 - if the flag is set, the sign is positive, else it is negative
18+
let MESH_FLAGS_SIGN_DETERMINANT_MODEL_3X3_BIT: u32 = 2147483648u;

crates/bevy_pbr/src/render/pbr_functions.wgsl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@ fn prepare_normal(
1515
#endif
1616
is_front: bool,
1717
) -> vec3<f32> {
18-
var N: vec3<f32> = normalize(world_normal);
18+
// NOTE: The mikktspace method of normal mapping explicitly requires that the world normal NOT
19+
// be re-normalized in the fragment shader. This is primarily to match the way mikktspace
20+
// bakes vertex tangents and normal maps so that this is the exact inverse. Blender, Unity,
21+
// Unreal Engine, Godot, and more all use the mikktspace method. Do not change this code
22+
// unless you really know what you are doing.
23+
// http://www.mikktspace.com/
24+
var N: vec3<f32> = world_normal;
1925

2026
#ifdef VERTEX_TANGENTS
2127
#ifdef STANDARDMATERIAL_NORMAL_MAP
@@ -236,7 +242,7 @@ fn pbr(
236242
fn tone_mapping(in: vec4<f32>) -> vec4<f32> {
237243
// tone_mapping
238244
return vec4<f32>(reinhard_luminance(in.rgb), in.a);
239-
245+
240246
// Gamma correction.
241247
// Not needed with sRGB buffer
242248
// output_color.rgb = pow(output_color.rgb, vec3(1.0 / 2.2));

0 commit comments

Comments
 (0)