Skip to content

Commit f375422

Browse files
authored
Compute better smooth normals for cheaper, maybe (#16050)
# Objective Avoid a premature normalize operation and get better smooth normals for it. ## Inspiration @IceSentry suggested `face_normal()` could have its normalize removed based on [this article](https://iquilezles.org/articles/normals/) in PR #16039. ## Solution I did not want to change `face_normal()` to return a vector that's not normalized. The name "normal" implies it'll be normalized. Instead I added the `face_area_normal()` function, whose result is not normalized. Its magnitude is equal two times the triangle's area. I've noted why this is the case in its doc comment. I changed `compute_smooth_normals()` from computing normals from adjacent faces with equal weight to use the area of the faces as a weight. This has the benefit of being cheaper computationally and hopefully produces better normals. The `compute_flat_normals()` is unchanged and still uses `face_normal()`. ## Testing One test was added which shows the bigger triangle having an effect on the normal, but the previous test that uses the same size triangles is unchanged. **WARNING:** No visual test has been done yet. No example exists that demonstrates the compute_smooth_normals(). Perhaps there's a good model to demonstrate what the differences are. I would love to have some input on this. I'd suggest @IceSentry and @stepancheg to review this PR. ## Further Considerations It's possible weighting normals by their area is not definitely better than unweighted. It's possible there may be aesthetic reasons to prefer one over the other. In such a case, we could offer two variants: weighted or unweighted. Or we could offer another function perhaps like this: `compute_smooth_normals_with_weights(|normal, area| 1.0)` which would restore the original unweighted sum of normals. --- ## Showcase Smooth normal calculation now weights adjacent face normals by their area. ## Migration Guide
1 parent b9cc6e1 commit f375422

File tree

2 files changed

+64
-6
lines changed

2 files changed

+64
-6
lines changed

crates/bevy_mesh/src/mesh.rs

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ use bevy_transform::components::Transform;
22
pub use wgpu::PrimitiveTopology;
33

44
use super::{
5-
face_normal, generate_tangents_for_mesh, scale_normal, FourIterators, GenerateTangentsError,
6-
Indices, MeshAttributeData, MeshTrianglesError, MeshVertexAttribute, MeshVertexAttributeId,
7-
MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts,
8-
MeshWindingInvertError, VertexAttributeValues, VertexBufferLayout, VertexFormatSize,
5+
face_area_normal, face_normal, generate_tangents_for_mesh, scale_normal, FourIterators,
6+
GenerateTangentsError, Indices, MeshAttributeData, MeshTrianglesError, MeshVertexAttribute,
7+
MeshVertexAttributeId, MeshVertexBufferLayout, MeshVertexBufferLayoutRef,
8+
MeshVertexBufferLayouts, MeshWindingInvertError, VertexAttributeValues, VertexBufferLayout,
9+
VertexFormatSize,
910
};
1011
use alloc::collections::BTreeMap;
1112
use bevy_asset::{Asset, Handle, RenderAssetUsages};
@@ -698,7 +699,7 @@ impl Mesh {
698699
.chunks_exact(3)
699700
.for_each(|face| {
700701
let [a, b, c] = [face[0], face[1], face[2]];
701-
let normal = Vec3::from(face_normal(positions[a], positions[b], positions[c]));
702+
let normal = Vec3::from(face_area_normal(positions[a], positions[b], positions[c]));
702703
[a, b, c].iter().for_each(|pos| {
703704
normals[*pos] += normal;
704705
});
@@ -1402,6 +1403,41 @@ mod tests {
14021403
assert_eq!([1., 0., 0.], normals[3]);
14031404
}
14041405

1406+
#[test]
1407+
fn compute_smooth_normals_proportionate() {
1408+
let mut mesh = Mesh::new(
1409+
PrimitiveTopology::TriangleList,
1410+
RenderAssetUsages::default(),
1411+
);
1412+
1413+
// z y
1414+
// | /
1415+
// 3---2..
1416+
// | / \
1417+
// 0-------1---x
1418+
1419+
mesh.insert_attribute(
1420+
Mesh::ATTRIBUTE_POSITION,
1421+
vec![[0., 0., 0.], [2., 0., 0.], [0., 1., 0.], [0., 0., 1.]],
1422+
);
1423+
mesh.insert_indices(Indices::U16(vec![0, 1, 2, 0, 2, 3]));
1424+
mesh.compute_smooth_normals();
1425+
let normals = mesh
1426+
.attribute(Mesh::ATTRIBUTE_NORMAL)
1427+
.unwrap()
1428+
.as_float3()
1429+
.unwrap();
1430+
assert_eq!(4, normals.len());
1431+
// 0
1432+
assert_eq!(Vec3::new(1., 0., 2.).normalize().to_array(), normals[0]);
1433+
// 1
1434+
assert_eq!([0., 0., 1.], normals[1]);
1435+
// 2
1436+
assert_eq!(Vec3::new(1., 0., 2.).normalize().to_array(), normals[2]);
1437+
// 3
1438+
assert_eq!([1., 0., 0.], normals[3]);
1439+
}
1440+
14051441
#[test]
14061442
fn triangles_from_triangle_list() {
14071443
let mut mesh = Mesh::new(

crates/bevy_mesh/src/vertex.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,29 @@ pub(crate) struct MeshAttributeData {
138138
pub(crate) values: VertexAttributeValues,
139139
}
140140

141-
pub(crate) fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
141+
/// Compute a vector whose direction is the normal of the triangle formed by
142+
/// points a, b, c, and whose magnitude is double the area of the triangle. This
143+
/// is useful for computing smooth normals where the contributing normals are
144+
/// proportionate to the areas of the triangles as [discussed
145+
/// here](https://iquilezles.org/articles/normals/).
146+
///
147+
/// Question: Why double the area? Because the area of a triangle _A_ is
148+
/// determined by this equation:
149+
///
150+
/// _A = |(b - a) x (c - a)| / 2_
151+
///
152+
/// By computing _2 A_ we avoid a division operation, and when calculating the
153+
/// the sum of these vectors which are then normalized, a constant multiple has
154+
/// no effect.
155+
#[inline]
156+
pub fn face_area_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
157+
let (a, b, c) = (Vec3::from(a), Vec3::from(b), Vec3::from(c));
158+
(b - a).cross(c - a).into()
159+
}
160+
161+
/// Compute the normal of a face made of three points: a, b, and c.
162+
#[inline]
163+
pub fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
142164
let (a, b, c) = (Vec3::from(a), Vec3::from(b), Vec3::from(c));
143165
(b - a).cross(c - a).normalize().into()
144166
}

0 commit comments

Comments
 (0)