blender_mesh/combine_indices/
weighted_normals.rs

1use crate::SingleIndexedVertexAttributes;
2use nalgebra::{Point3, Vector3};
3use std::collections::hash_map::Entry;
4use std::collections::HashMap;
5
6/// An error when blending normals
7#[derive(Debug, thiserror::Error)]
8pub enum WeightedNormalsError {
9    #[error("There were no normals to weight")]
10    NoNormals,
11}
12
13impl SingleIndexedVertexAttributes {
14    /// Alter normals to be both
15    ///   surface weighted (connected triangle size) and
16    ///   angle weighted (angle of connected triangle corner)
17    ///
18    /// @see http://www.bytehazard.com/articles/vertnorm.html
19    ///
20    /// TODO: We could also implement this for multi-indexed - but we should wait until we
21    /// refactor / replace the combine_indices function because, for example, if we weight normals
22    /// before we calculate face tangents our face tangents will be incorrect.
23    /// In general this entire crate needs to be heavily TDD"d and refactored into something clean..
24    ///
25    /// TODO: When we combine normals we'll end up with a lot of vertices that have the same data
26    ///  so we should dedupe the vertices / indices
27    pub fn face_weight_normals(&mut self) -> Result<(), WeightedNormalsError> {
28        let mut encountered_positions: HashMap<[u32; 3], SharedVertexPositionWeightedNormal> =
29            HashMap::new();
30
31        for (vertex_num, pos_norm_data_idx) in self.indices.iter().enumerate() {
32            let pos_norm_data_idx = *pos_norm_data_idx as usize;
33
34            let vertex = self.vertices()[pos_norm_data_idx];
35
36            let pos = vertex.position;
37            let pos_point = Point3::new(pos[0], pos[1], pos[2]);
38
39            let face_normal = vertex.normal.unwrap();
40            let face_normal = Vector3::new(face_normal[0], face_normal[1], face_normal[2]);
41
42            let (connected_vertex_1, connected_vertex_2) = match vertex_num % 3 {
43                0 => (vertex_num + 1, vertex_num + 2),
44                1 => (vertex_num - 1, vertex_num + 1),
45                2 => (vertex_num - 2, vertex_num - 1),
46                _ => unreachable!(),
47            };
48
49            let connected_vertex_1 =
50                self.vertices[self.indices[connected_vertex_1] as usize].position;
51            let connected_vertex_1 = Point3::new(
52                connected_vertex_1[0],
53                connected_vertex_1[1],
54                connected_vertex_1[2],
55            );
56
57            let connected_vertex_2 =
58                self.vertices[self.indices[connected_vertex_2] as usize].position;
59            let connected_vertex_2 = Point3::new(
60                connected_vertex_2[0],
61                connected_vertex_2[1],
62                connected_vertex_2[2],
63            );
64
65            let weighted_normal = weight_normal_using_surface_and_angle(
66                face_normal,
67                connected_vertex_1 - pos_point.clone(),
68                connected_vertex_2 - pos_point,
69            );
70
71            let pos_hash = [pos[0].to_bits(), pos[1].to_bits(), pos[2].to_bits()];
72            match encountered_positions.entry(pos_hash) {
73                Entry::Occupied(mut previous) => {
74                    previous
75                        .get_mut()
76                        .normals_to_overwrite
77                        .push(pos_norm_data_idx);
78                    previous.get_mut().weighted_normal += weighted_normal;
79                }
80                Entry::Vacant(vacant) => {
81                    vacant.insert(SharedVertexPositionWeightedNormal {
82                        normals_to_overwrite: vec![pos_norm_data_idx],
83                        weighted_normal,
84                    });
85                }
86            };
87        }
88
89        for (_pos_hash, overlapping_vertices) in encountered_positions.into_iter() {
90            let weighted_normal = overlapping_vertices.weighted_normal.normalize();
91            let weighted_normal = weighted_normal.as_slice();
92
93            for normal_data_idx in overlapping_vertices.normals_to_overwrite {
94                let mut normal = [0.; 3];
95                normal.copy_from_slice(weighted_normal);
96
97                self.vertices_mut()[normal_data_idx].normal = Some(normal);
98            }
99        }
100
101        Ok(())
102    }
103}
104
105// While we iterate through our positions we keep track of which normals corresponded to
106// duplicate positions - along with the weighted normal.
107// Then when we're done we go back to all of the corresponding normals for each shared
108// position and overwrite them with the new weighted normal.
109#[derive(Debug)]
110struct SharedVertexPositionWeightedNormal {
111    normals_to_overwrite: Vec<usize>,
112    weighted_normal: Vector3<f32>,
113}
114
115/// Alter normals to be both
116///
117///   surface weighted (area of the connected face) and
118///   angle weighted (angle of the current vertex corner triangle corner)
119///
120/// We assume that the connected face is a triangle - and as such we only need the two vectors
121/// on the face (triangle) that are connected to the vertex.
122///
123/// @see http://www.bytehazard.com/articles/vertnorm.html
124fn weight_normal_using_surface_and_angle(
125    face_normal: Vector3<f32>,
126    connected_face_edge_1: Vector3<f32>,
127    connected_face_edge_2: Vector3<f32>,
128) -> Vector3<f32> {
129    let face_normal = face_normal.normalize();
130
131    let angle = connected_face_edge_1.angle(&connected_face_edge_2);
132
133    let area =
134        0.5 * connected_face_edge_1.magnitude() * connected_face_edge_2.magnitude() * angle.sin();
135
136    face_normal * area * angle
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::Vertex;
143    use std::f32::consts::PI;
144
145    /// Calculate a weighted normal from a given normal and the connected edges
146    #[test]
147    fn calculate_weighted_normal() {
148        let input_normal: Vector3<f32> = [0., 1., 0.].into();
149
150        let connected_face_edge_1 = [1., 0., 0.].into();
151        let connected_face_edge_2 = [0., 0., 1.].into();
152
153        let expected_angle = PI / 2.;
154        let expected_area = 0.5;
155
156        let weighted_normal = weight_normal_using_surface_and_angle(
157            input_normal.clone(),
158            connected_face_edge_1,
159            connected_face_edge_2,
160        );
161
162        assert_eq!(
163            weighted_normal,
164            input_normal * expected_area * expected_angle
165        );
166    }
167
168    /// Given vertex indices, positions and normals - calculate the weighted normals.
169    ///
170    /// We provide a single triangle, so there are no overlapping vertices, so no normals are
171    /// weighted and we end with the data that we started with.
172    #[test]
173    fn weighted_normals_one_triangle() {
174        let indices = vec![0, 1, 2];
175
176        #[rustfmt::skip]
177        let positions = vec![
178            0., 0., 0.,
179            1., 0., 0.,
180            0., 1., 0.,
181        ];
182
183        #[rustfmt::skip]
184        let normals = vec![
185            0., 1., 0.,
186            1., 0., 0.,
187            0., 1., 0.,
188        ];
189
190        let mut vertices = vec![];
191        for idx in 0..positions.len() / 3 {
192            vertices.push(Vertex {
193                position: [
194                    positions[idx * 3],
195                    positions[idx * 3 + 1],
196                    positions[idx * 3 + 2],
197                ],
198                normal: Some([normals[idx * 3], normals[idx * 3 + 1], normals[idx * 3 + 2]]),
199                ..Vertex::default()
200            });
201        }
202
203        let mut single_indexed = SingleIndexedVertexAttributes {
204            indices,
205            vertices,
206            ..SingleIndexedVertexAttributes::default()
207        };
208        single_indexed.face_weight_normals().unwrap();
209
210        let face_weighted_normals: Vec<f32> = single_indexed
211            .vertices()
212            .iter()
213            .flat_map(|v| v.normal.unwrap().to_vec())
214            .collect();
215
216        assert_eq!(face_weighted_normals, normals);
217    }
218
219    /// We repeat position index 0 twice - meaning that there are two vertices that share
220    /// a position.
221    /// The corresponding normals should be blended
222    ///
223    /// We have a vertex [0., 0., 0.] 0 that is part of two triangles.
224    ///   The two connected vertices for the first triangle are [1., 0., 0.] and [0., 1., 0.]
225    ///   The two connected vertices for the second triangle are [10., 0., 0.] and [0., 10., 0.]
226    ///
227    /// Since the area of the second triangle is 100x the area of the first, the normal of the
228    /// second triangle should have 100x the influence.
229    /// (both triangles have the same angle, so only the surface area applies here)
230    #[test]
231    fn weights_two_normals() {
232        let mut single_indexed = create_single_indexed();
233        single_indexed.face_weight_normals().unwrap();
234
235        let expected_weighted_norm = get_expected_weighted_norm();
236
237        let normals: Vec<f32> = single_indexed
238            .vertices()
239            .iter()
240            .flat_map(|v| v.normal.unwrap().to_vec())
241            .collect();
242
243        let actual_normals = normals;
244        assert_eq!(&actual_normals[0..3], &expected_weighted_norm);
245        assert_eq!(&actual_normals[12..15], &expected_weighted_norm);
246    }
247
248    fn get_expected_weighted_norm() -> [f32; 3] {
249        let angle = PI / 2.;
250
251        let area = 1. * 1. / 2.;
252        let first_triangle_contrib = Vector3::new(0., 1., 0.) * area * angle;
253
254        let area = 10. * 10. / 2.;
255        let second_triangle_contrib = Vector3::new(1., 0., 0.) * area * angle;
256
257        let weighted_normal = (first_triangle_contrib + second_triangle_contrib).normalize();
258        let expected_weighted_norm = [weighted_normal[0], weighted_normal[1], weighted_normal[2]];
259
260        expected_weighted_norm
261    }
262
263    fn create_single_indexed() -> SingleIndexedVertexAttributes {
264        // index 0 and index 4 both correspond to position (0., 0., 0.)
265        let indices = vec![0, 1, 2, 3, 4, 5];
266
267        #[rustfmt::skip]
268        let positions = vec![
269            0., 0., 0.,
270            1., 0., 0.,
271            0., 1., 0.,
272            10., 0., 0.,
273            0., 0., 0.,
274            0., 10., 0.,
275        ];
276
277        #[rustfmt::skip]
278        let normals = vec![
279            0., 1., 0.,
280            0., 0., 0.,
281            0., 0., 0.,
282            0., 0., 0.,
283            1., 0., 0.,
284            0., 0., 0.,
285        ];
286
287        let mut vertices = vec![];
288        for idx in 0..positions.len() / 3 {
289            vertices.push(Vertex {
290                position: [
291                    positions[idx * 3],
292                    positions[idx * 3 + 1],
293                    positions[idx * 3 + 2],
294                ],
295                normal: Some([normals[idx * 3], normals[idx * 3 + 1], normals[idx * 3 + 2]]),
296                ..Vertex::default()
297            });
298        }
299
300        let single_indexed = SingleIndexedVertexAttributes {
301            indices,
302            vertices,
303            ..SingleIndexedVertexAttributes::default()
304        };
305        single_indexed
306    }
307}