@@ -2,7 +2,7 @@ use bevy_transform::components::Transform;
22pub use wgpu_types:: PrimitiveTopology ;
33
44use 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 (
0 commit comments