Skip to content

Commit 9e1e8bc

Browse files
authored
Add angle-weighted smooth normals implementation (#18383) (#18552)
# Objective Closes #18383 ## Solution Given the 2 votes (me and @komadori ) for making angle-weighted normals the default, I went ahead and did so, moving the face-weighted implementation to the new `Mesh::compute_face_weighted_normals` method. I factored out the common code between both into `Mesh::compute_custom_smooth_normals`, which I went ahead and made public to make it easier for users to add any other weighting methods they might come up with. If any of these decisions are undesirable for any reason, please let me know and I will gladly make modifications. ## Testing & Showcase I made a demo that exaggerates the difference at [Waridley/bevy_smooth_normals_comparison](https://github.com/Waridley/bevy_smooth_normals_comparison). Screenshots included in the readme. Another way it could be demonstrated is via scaling a mesh along its normals, like for generating outline meshes with inverted faces. I'd be willing to make a demo for that as well. I also edited and renamed the `compute_smooth_normals` tests to use face-weighted normals, and added a new test for angle-weighted ones which validates that all normals of a unit cube have each component equal to `(±1 / √3) ± f32::EPSILON`. ## Migration Guide #16050 already did not mention a migration guide, and it is not even in a stable release yet. If this lands in a 0.16 RC, updating from RC1 would probably not require any changes in the vast majority of cases, anyway. If someone really needs face-weighted normals, they can switch to `.compute_face_weighted_normals()` or `.with_computed_face_weighted_normals()`. And if for some reason they really liked the old count-weighted implementation from 0.15, there is an example in the docs for `compute_custom_smooth_normals`.
1 parent f964ee1 commit 9e1e8bc

File tree

4 files changed

+277
-35
lines changed

4 files changed

+277
-35
lines changed

benches/benches/bevy_render/compute_normals.rs

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ fn compute_normals(c: &mut Criterion) {
5454
});
5555
});
5656

57-
c.bench_function("face_weighted_normals", |b| {
57+
c.bench_function("angle_weighted_normals", |b| {
5858
b.iter_custom(|iters| {
5959
let mut total = Duration::default();
6060
for _ in 0..iters {
@@ -70,11 +70,23 @@ fn compute_normals(c: &mut Criterion) {
7070
});
7171
});
7272

73-
let new_mesh = || {
74-
new_mesh()
75-
.with_duplicated_vertices()
76-
.with_computed_flat_normals()
77-
};
73+
c.bench_function("face_weighted_normals", |b| {
74+
b.iter_custom(|iters| {
75+
let mut total = Duration::default();
76+
for _ in 0..iters {
77+
let mut mesh = new_mesh();
78+
black_box(mesh.attribute(Mesh::ATTRIBUTE_NORMAL));
79+
let start = Instant::now();
80+
mesh.compute_area_weighted_normals();
81+
let end = Instant::now();
82+
black_box(mesh.attribute(Mesh::ATTRIBUTE_NORMAL));
83+
total += end.duration_since(start);
84+
}
85+
total
86+
});
87+
});
88+
89+
let new_mesh = || new_mesh().with_duplicated_vertices();
7890

7991
c.bench_function("flat_normals", |b| {
8092
b.iter_custom(|iters| {

crates/bevy_mesh/src/mesh.rs

Lines changed: 209 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use bevy_transform::components::Transform;
22
pub use wgpu_types::PrimitiveTopology;
33

44
use super::{
5-
face_area_normal, face_normal, generate_tangents_for_mesh, scale_normal, FourIterators,
5+
generate_tangents_for_mesh, scale_normal, triangle_area_normal, triangle_normal, FourIterators,
66
GenerateTangentsError, Indices, MeshAttributeData, MeshTrianglesError, MeshVertexAttribute,
77
MeshVertexAttributeId, MeshVertexBufferLayout, MeshVertexBufferLayoutRef,
88
MeshVertexBufferLayouts, MeshWindingInvertError, VertexAttributeValues, VertexBufferLayout,
@@ -649,11 +649,7 @@ impl Mesh {
649649
///
650650
/// # Panics
651651
/// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`.
652-
/// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`].
653-
///
654-
/// FIXME: This should handle more cases since this is called as a part of gltf
655-
/// mesh loading where we can't really blame users for loading meshes that might
656-
/// not conform to the limitations here!
652+
/// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`].=
657653
pub fn compute_normals(&mut self) {
658654
assert!(
659655
matches!(self.primitive_topology, PrimitiveTopology::TriangleList),
@@ -695,7 +691,7 @@ impl Mesh {
695691

696692
let normals: Vec<_> = positions
697693
.chunks_exact(3)
698-
.map(|p| face_normal(p[0], p[1], p[2]))
694+
.map(|p| triangle_normal(p[0], p[1], p[2]))
699695
.flat_map(|normal| [normal; 3])
700696
.collect();
701697

@@ -705,22 +701,141 @@ impl Mesh {
705701
/// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared
706702
/// vertices.
707703
///
704+
/// This method weights normals by the angles of the corners of connected triangles, thus
705+
/// eliminating triangle area and count as factors in the final normal. This does make it
706+
/// somewhat slower than [`Mesh::compute_area_weighted_normals`] which does not need to
707+
/// greedily normalize each triangle's normal or calculate corner angles.
708+
///
709+
/// If you would rather have the computed normals be weighted by triangle area, see
710+
/// [`Mesh::compute_area_weighted_normals`] instead. If you need to weight them in some other
711+
/// way, see [`Mesh::compute_custom_smooth_normals`].
712+
///
708713
/// # Panics
709714
/// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`.
710715
/// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`].
711716
/// Panics if the mesh does not have indices defined.
712-
///
713-
/// FIXME: This should handle more cases since this is called as a part of gltf
714-
/// mesh loading where we can't really blame users for loading meshes that might
715-
/// not conform to the limitations here!
716717
pub fn compute_smooth_normals(&mut self) {
718+
self.compute_custom_smooth_normals(|[a, b, c], positions, normals| {
719+
let pa = Vec3::from(positions[a]);
720+
let pb = Vec3::from(positions[b]);
721+
let pc = Vec3::from(positions[c]);
722+
723+
let ab = pb - pa;
724+
let ba = pa - pb;
725+
let bc = pc - pb;
726+
let cb = pb - pc;
727+
let ca = pa - pc;
728+
let ac = pc - pa;
729+
730+
const EPS: f32 = f32::EPSILON;
731+
let weight_a = if ab.length_squared() * ac.length_squared() > EPS {
732+
ab.angle_between(ac)
733+
} else {
734+
0.0
735+
};
736+
let weight_b = if ba.length_squared() * bc.length_squared() > EPS {
737+
ba.angle_between(bc)
738+
} else {
739+
0.0
740+
};
741+
let weight_c = if ca.length_squared() * cb.length_squared() > EPS {
742+
ca.angle_between(cb)
743+
} else {
744+
0.0
745+
};
746+
747+
let normal = Vec3::from(triangle_normal(positions[a], positions[b], positions[c]));
748+
749+
normals[a] += normal * weight_a;
750+
normals[b] += normal * weight_b;
751+
normals[c] += normal * weight_c;
752+
});
753+
}
754+
755+
/// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared
756+
/// vertices.
757+
///
758+
/// This method weights normals by the area of each triangle containing the vertex. Thus,
759+
/// larger triangles will skew the normals of their vertices towards their own normal more
760+
/// than smaller triangles will.
761+
///
762+
/// This method is actually somewhat faster than [`Mesh::compute_smooth_normals`] because an
763+
/// intermediate result of triangle normal calculation is already scaled by the triangle's area.
764+
///
765+
/// If you would rather have the computed normals be influenced only by the angles of connected
766+
/// edges, see [`Mesh::compute_smooth_normals`] instead. If you need to weight them in some
767+
/// other way, see [`Mesh::compute_custom_smooth_normals`].
768+
///
769+
/// # Panics
770+
/// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`.
771+
/// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`].
772+
/// Panics if the mesh does not have indices defined.
773+
pub fn compute_area_weighted_normals(&mut self) {
774+
self.compute_custom_smooth_normals(|[a, b, c], positions, normals| {
775+
let normal = Vec3::from(triangle_area_normal(
776+
positions[a],
777+
positions[b],
778+
positions[c],
779+
));
780+
[a, b, c].into_iter().for_each(|pos| {
781+
normals[pos] += normal;
782+
});
783+
});
784+
}
785+
786+
/// Calculates the [`Mesh::ATTRIBUTE_NORMAL`] of an indexed mesh, smoothing normals for shared
787+
/// vertices.
788+
///
789+
/// This method allows you to customize how normals are weighted via the `per_triangle` parameter,
790+
/// which must be a function or closure that accepts 3 parameters:
791+
/// - The indices of the three vertices of the triangle as a `[usize; 3]`.
792+
/// - A reference to the values of the [`Mesh::ATTRIBUTE_POSITION`] of the mesh (`&[[f32; 3]]`).
793+
/// - A mutable reference to the sums of all normals so far.
794+
///
795+
/// See also the standard methods included in Bevy for calculating smooth normals:
796+
/// - [`Mesh::compute_smooth_normals`]
797+
/// - [`Mesh::compute_area_weighted_normals`]
798+
///
799+
/// An example that would weight each connected triangle's normal equally, thus skewing normals
800+
/// towards the planes divided into the most triangles:
801+
/// ```
802+
/// # use bevy_asset::RenderAssetUsages;
803+
/// # use bevy_mesh::{Mesh, PrimitiveTopology, Meshable, MeshBuilder};
804+
/// # use bevy_math::{Vec3, primitives::Cuboid};
805+
/// # let mut mesh = Cuboid::default().mesh().build();
806+
/// mesh.compute_custom_smooth_normals(|[a, b, c], positions, normals| {
807+
/// let normal = Vec3::from(bevy_mesh::triangle_normal(positions[a], positions[b], positions[c]));
808+
/// for idx in [a, b, c] {
809+
/// normals[idx] += normal;
810+
/// }
811+
/// });
812+
/// ```
813+
///
814+
/// # Panics
815+
/// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`.
816+
/// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`].
817+
/// Panics if the mesh does not have indices defined.
818+
//
819+
// FIXME: This should handle more cases since this is called as a part of gltf
820+
// mesh loading where we can't really blame users for loading meshes that might
821+
// not conform to the limitations here!
822+
//
823+
// When fixed, also update "Panics" sections of
824+
// - [Mesh::compute_smooth_normals]
825+
// - [Mesh::with_computed_smooth_normals]
826+
// - [Mesh::compute_area_weighted_normals]
827+
// - [Mesh::with_computed_area_weighted_normals]
828+
pub fn compute_custom_smooth_normals(
829+
&mut self,
830+
mut per_triangle: impl FnMut([usize; 3], &[[f32; 3]], &mut [Vec3]),
831+
) {
717832
assert!(
718833
matches!(self.primitive_topology, PrimitiveTopology::TriangleList),
719-
"`compute_smooth_normals` can only work on `TriangleList`s"
834+
"smooth normals can only be computed on `TriangleList`s"
720835
);
721836
assert!(
722837
self.indices().is_some(),
723-
"`compute_smooth_normals` can only work on indexed meshes"
838+
"smooth normals can only be computed on indexed meshes"
724839
);
725840

726841
let positions = self
@@ -736,16 +851,8 @@ impl Mesh {
736851
.iter()
737852
.collect::<Vec<usize>>()
738853
.chunks_exact(3)
739-
.for_each(|face| {
740-
let [a, b, c] = [face[0], face[1], face[2]];
741-
let normal = Vec3::from(face_area_normal(positions[a], positions[b], positions[c]));
742-
[a, b, c].iter().for_each(|pos| {
743-
normals[*pos] += normal;
744-
});
745-
});
854+
.for_each(|face| per_triangle([face[0], face[1], face[2]], positions, &mut normals));
746855

747-
// average (smooth) normals for shared vertices...
748-
// TODO: support different methods of weighting the average
749856
for normal in &mut normals {
750857
*normal = normal.try_normalize().unwrap_or(Vec3::ZERO);
751858
}
@@ -786,6 +893,10 @@ impl Mesh {
786893
///
787894
/// (Alternatively, you can use [`Mesh::compute_smooth_normals`] to mutate an existing mesh in-place)
788895
///
896+
/// This method weights normals by the angles of triangle corners connected to each vertex. If
897+
/// you would rather have the computed normals be weighted by triangle area, see
898+
/// [`Mesh::with_computed_area_weighted_normals`] instead.
899+
///
789900
/// # Panics
790901
/// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`.
791902
/// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`].
@@ -796,6 +907,25 @@ impl Mesh {
796907
self
797908
}
798909

910+
/// Consumes the mesh and returns a mesh with calculated [`Mesh::ATTRIBUTE_NORMAL`].
911+
///
912+
/// (Alternatively, you can use [`Mesh::compute_area_weighted_normals`] to mutate an existing mesh in-place)
913+
///
914+
/// This method weights normals by the area of each triangle containing the vertex. Thus,
915+
/// larger triangles will skew the normals of their vertices towards their own normal more
916+
/// than smaller triangles will. If you would rather have the computed normals be influenced
917+
/// only by the angles of connected edges, see [`Mesh::with_computed_smooth_normals`] instead.
918+
///
919+
/// # Panics
920+
/// Panics if [`Mesh::ATTRIBUTE_POSITION`] is not of type `float3`.
921+
/// Panics if the mesh has any other topology than [`PrimitiveTopology::TriangleList`].
922+
/// Panics if the mesh does not have indices defined.
923+
#[must_use]
924+
pub fn with_computed_area_weighted_normals(mut self) -> Self {
925+
self.compute_area_weighted_normals();
926+
self
927+
}
928+
799929
/// Generate tangents for the mesh using the `mikktspace` algorithm.
800930
///
801931
/// Sets the [`Mesh::ATTRIBUTE_TANGENT`] attribute if successful.
@@ -1587,7 +1717,7 @@ mod tests {
15871717
}
15881718

15891719
#[test]
1590-
fn compute_smooth_normals() {
1720+
fn compute_area_weighted_normals() {
15911721
let mut mesh = Mesh::new(
15921722
PrimitiveTopology::TriangleList,
15931723
RenderAssetUsages::default(),
@@ -1604,7 +1734,7 @@ mod tests {
16041734
vec![[0., 0., 0.], [1., 0., 0.], [0., 1., 0.], [0., 0., 1.]],
16051735
);
16061736
mesh.insert_indices(Indices::U16(vec![0, 1, 2, 0, 2, 3]));
1607-
mesh.compute_smooth_normals();
1737+
mesh.compute_area_weighted_normals();
16081738
let normals = mesh
16091739
.attribute(Mesh::ATTRIBUTE_NORMAL)
16101740
.unwrap()
@@ -1622,7 +1752,7 @@ mod tests {
16221752
}
16231753

16241754
#[test]
1625-
fn compute_smooth_normals_proportionate() {
1755+
fn compute_area_weighted_normals_proportionate() {
16261756
let mut mesh = Mesh::new(
16271757
PrimitiveTopology::TriangleList,
16281758
RenderAssetUsages::default(),
@@ -1639,7 +1769,7 @@ mod tests {
16391769
vec![[0., 0., 0.], [2., 0., 0.], [0., 1., 0.], [0., 0., 1.]],
16401770
);
16411771
mesh.insert_indices(Indices::U16(vec![0, 1, 2, 0, 2, 3]));
1642-
mesh.compute_smooth_normals();
1772+
mesh.compute_area_weighted_normals();
16431773
let normals = mesh
16441774
.attribute(Mesh::ATTRIBUTE_NORMAL)
16451775
.unwrap()
@@ -1656,6 +1786,59 @@ mod tests {
16561786
assert_eq!([1., 0., 0.], normals[3]);
16571787
}
16581788

1789+
#[test]
1790+
fn compute_angle_weighted_normals() {
1791+
// CuboidMeshBuilder duplicates vertices (even though it is indexed)
1792+
1793+
// 5---------4
1794+
// /| /|
1795+
// 1-+-------0 |
1796+
// | 6-------|-7
1797+
// |/ |/
1798+
// 2---------3
1799+
let verts = vec![
1800+
[1.0, 1.0, 1.0],
1801+
[-1.0, 1.0, 1.0],
1802+
[-1.0, -1.0, 1.0],
1803+
[1.0, -1.0, 1.0],
1804+
[1.0, 1.0, -1.0],
1805+
[-1.0, 1.0, -1.0],
1806+
[-1.0, -1.0, -1.0],
1807+
[1.0, -1.0, -1.0],
1808+
];
1809+
1810+
let indices = Indices::U16(vec![
1811+
0, 1, 2, 2, 3, 0, // front
1812+
5, 4, 7, 7, 6, 5, // back
1813+
1, 5, 6, 6, 2, 1, // left
1814+
4, 0, 3, 3, 7, 4, // right
1815+
4, 5, 1, 1, 0, 4, // top
1816+
3, 2, 6, 6, 7, 3, // bottom
1817+
]);
1818+
let mut mesh = Mesh::new(
1819+
PrimitiveTopology::TriangleList,
1820+
RenderAssetUsages::default(),
1821+
);
1822+
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, verts);
1823+
mesh.insert_indices(indices);
1824+
mesh.compute_smooth_normals();
1825+
1826+
let normals = mesh
1827+
.attribute(Mesh::ATTRIBUTE_NORMAL)
1828+
.unwrap()
1829+
.as_float3()
1830+
.unwrap();
1831+
1832+
for new in normals.iter().copied().flatten() {
1833+
// std impl is unstable
1834+
const FRAC_1_SQRT_3: f32 = 0.57735026;
1835+
const MIN: f32 = FRAC_1_SQRT_3 - f32::EPSILON;
1836+
const MAX: f32 = FRAC_1_SQRT_3 + f32::EPSILON;
1837+
assert!(new.abs() >= MIN, "{new} < {MIN}");
1838+
assert!(new.abs() <= MAX, "{new} > {MAX}");
1839+
}
1840+
}
1841+
16591842
#[test]
16601843
fn triangles_from_triangle_list() {
16611844
let mut mesh = Mesh::new(

crates/bevy_mesh/src/vertex.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,16 +219,16 @@ impl SerializedMeshAttributeData {
219219
/// the sum of these vectors which are then normalized, a constant multiple has
220220
/// no effect.
221221
#[inline]
222-
pub fn face_area_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
222+
pub fn triangle_area_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
223223
let (a, b, c) = (Vec3::from(a), Vec3::from(b), Vec3::from(c));
224224
(b - a).cross(c - a).into()
225225
}
226226

227227
/// Compute the normal of a face made of three points: a, b, and c.
228228
#[inline]
229-
pub fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
229+
pub fn triangle_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] {
230230
let (a, b, c) = (Vec3::from(a), Vec3::from(b), Vec3::from(c));
231-
(b - a).cross(c - a).normalize().into()
231+
(b - a).cross(c - a).normalize_or_zero().into()
232232
}
233233

234234
/// Contains an array where each entry describes a property of a single vertex.

0 commit comments

Comments
 (0)