Normal Helper Modules
Truck has normal filters, but no helper functions for manually computing face or vertex normals, so we provide our own utilities here.
Implement Functions in src/utils/normal_helpers.rs
Import dependencies
#![allow(unused)] fn main() { use truck_meshalgo::prelude::*; }
compute_face_normals
#![allow(unused)] fn main() { /// Returns one normalized face normal per polygon. pub fn compute_face_normals(mesh: &PolygonMesh) -> Vec<Vector3> { let positions = mesh.positions(); mesh.face_iter() .map(|face| { // Need at least three vertices to form a plane if face.len() < 3 { return Vector3::zero(); } // First three positions define the face plane let p0 = positions[face[0].pos]; let p1 = positions[face[1].pos]; let p2 = positions[face[2].pos]; let n = (p1 - p0).cross(p2 - p0); // Skip degenerate/collinear faces if n.magnitude2() < 1e-12 { return Vector3::zero(); } n.normalize() }) .collect() } }
What it does + example
Derives a normalized face normal from the first three vertices of each polygon. Degenerate (cannot form a valid surface) or collinear (vertices all lie on the same straight line.) faces yield Vector3::zero(), enabling later routines to detect and correct them.
Example
#![allow(unused)] fn main() { let mut mesh = build_mesh(); let face_normals = compute_face_normals(&mesh); mesh.attributes_mut().add_face_normals(face_normals); }
compute_vertex_normals
#![allow(unused)] fn main() { /// Returns one normalized vertex normal per vertex (averaged from faces). pub fn compute_vertex_normals(mesh: &PolygonMesh) -> Vec<Vector3> { let positions = mesh.positions(); let face_normals = compute_face_normals(mesh); let mut vertex_normals = vec![Vector3::zero(); positions.len()]; for (face_idx, face) in mesh.face_iter().enumerate() { let n = face_normals[face_idx]; // Accumulate adjacent face normals onto each vertex for v in face { vertex_normals[v.pos] += n; } } // Normalize summed vectors so each vertex normal is unit length for n in vertex_normals.iter_mut() { *n = n.normalize(); } vertex_normals } }
What it does + example
Accumulates the face normals that touch each vertex and normalizes the sum so shared vertices shade smoothly across adjacent polygons.
Example
#![allow(unused)] fn main() { let mut mesh = load_mesh(); let vertex_normals = compute_vertex_normals(&mesh); mesh.attributes_mut().add_vertex_normals(vertex_normals); }
add_face_normals
#![allow(unused)] fn main() { /// Computes and attaches a normal per face, wiring each vertex in the face to that normal. pub fn add_face_normals(mesh: &mut PolygonMesh) { let normals = compute_face_normals(mesh); // one normal per polygon let editor = mesh.editor(); editor.attributes.normals = normals; // store normals into attributes for (idx, face) in editor.faces.face_iter_mut().enumerate() { for v in face.iter_mut() { v.nor = Some(idx); // point each vertex's normal index to its face normal } } } }
What it does + example
Computes per-face normals with compute_face_normals and wires each vertex in a face to that face's normal index.
Example
#![allow(unused)] fn main() { let mut mesh = icosahedron(); add_face_normals(&mut mesh); write_polygon_mesh(&mesh, "output/icosahedron_flat.obj"); }
add_vertex_normals
#![allow(unused)] fn main() { /// Computes and attaches a normal per vertex, wiring indices to match position indices. pub fn add_vertex_normals(mesh: &mut PolygonMesh) { let normals = compute_vertex_normals(mesh); // smooth normals per vertex let editor = mesh.editor(); editor.attributes.normals = normals; // store normals into attributes for face in editor.faces.face_iter_mut() { for v in face.iter_mut() { v.nor = Some(v.pos); // normal index aligns with vertex position index } } } }
What it does + example
Computes smooth vertex normals using compute_vertex_normals and stores them so each vertex reuses its own normal index.
Example
#![allow(unused)] fn main() { let mut mesh = icosahedron(); add_vertex_normals(&mut mesh); write_polygon_mesh(&mesh, "output/icosahedron_smooth.obj"); }
normalize_vertex_normals
#![allow(unused)] fn main() { /// Normalizes any normals currently stored on the mesh in-place. pub fn normalize_vertex_normals(mesh: &mut PolygonMesh) { let editor = mesh.editor(); for n in editor.attributes.normals.iter_mut() { *n = n.normalize(); // keep direction, ensure unit length } } }
What it does + example
Renormalizes all stored normals in place, leaving their directions unchanged; if no normals exist, nothing happens.
Example
#![allow(unused)] fn main() { let mut mesh = mesh_with_normals(); // ... edit or mix normals ... normalize_vertex_normals(&mut mesh); }
Add the following re-exports to src/utils/mod.rs
#![allow(unused)] fn main() { pub mod normal_helpers; pub use normal_helpers::{ compute_face_normals, compute_vertex_normals, add_face_normals, add_vertex_normals, normalize_vertex_normals, }; }
Add the normal helpers re-exports to src/lib.rs
#![allow(unused)] fn main() { pub mod utils; pub use utils::normal_helpers::{ compute_face_normals, compute_vertex_normals, add_face_normals, add_vertex_normals, normalize_vertex_normals, }; }
Verify everything works now with cargo check
Run
cargo check
Next page, we will apply these functions to an Icosahedron
Directory layout (modular)
truck_meshes/
├─ Cargo.toml
├─ src/
│ ├─ lib.rs
│ ├─ shapes/
│ │ ├─ mod.rs
│ │ ├─ triangle.rs
│ │ ├─ square.rs
│ │ ├─ tetrahedron.rs
│ │ ├─ hexahedron.rs
│ │ ├─ octahedron.rs
│ │ ├─ dodecahedron.rs
│ │ └─ icosahedron.rs
│ └─ utils/
│ ├─ mod.rs
│ └─ normal_helpers.rs
├─ examples/
│ ├─ triangle.rs
│ ├─ square.rs
│ ├─ tetrahedron.rs
│ ├─ hexahedron.rs
│ ├─ octahedron.rs
│ ├─ dodecahedron.rs
│ └─ icosahedron.rs
│
└─ output/ # exported OBJ files from examples
Full code:
src/utils/normal_helpers.rs
#![allow(unused)] fn main() { use truck_meshalgo::prelude::*; /// Returns one normalized face normal per polygon. pub fn compute_face_normals(mesh: &PolygonMesh) -> Vec<Vector3> { let positions = mesh.positions(); mesh.face_iter() .map(|face| { // Need at least three vertices to form a plane if face.len() < 3 { return Vector3::zero(); } // First three positions define the face plane let p0 = positions[face[0].pos]; let p1 = positions[face[1].pos]; let p2 = positions[face[2].pos]; let n = (p1 - p0).cross(p2 - p0); // Skip degenerate/collinear faces if n.magnitude2() < 1e-12 { return Vector3::zero(); } n.normalize() }) .collect() } /// Returns one normalized vertex normal per vertex (averaged from faces). pub fn compute_vertex_normals(mesh: &PolygonMesh) -> Vec<Vector3> { let positions = mesh.positions(); let face_normals = compute_face_normals(mesh); let mut vertex_normals = vec![Vector3::zero(); positions.len()]; for (face_idx, face) in mesh.face_iter().enumerate() { let n = face_normals[face_idx]; // Accumulate adjacent face normals onto each vertex for v in face { vertex_normals[v.pos] += n; } } // Normalize summed vectors so each vertex normal is unit length for n in vertex_normals.iter_mut() { *n = n.normalize(); } vertex_normals } /// Computes and attaches a normal per face, wiring each vertex in the face to that normal. pub fn add_face_normals(mesh: &mut PolygonMesh) { let normals = compute_face_normals(mesh); let editor = mesh.editor(); editor.attributes.normals = normals; for (idx, face) in editor.faces.face_iter_mut().enumerate() { for v in face.iter_mut() { v.nor = Some(idx); } } } /// Computes and attaches a normal per vertex, wiring indices to match position indices. pub fn add_vertex_normals(mesh: &mut PolygonMesh) { let normals = compute_vertex_normals(mesh); let editor = mesh.editor(); editor.attributes.normals = normals; for face in editor.faces.face_iter_mut() { for v in face.iter_mut() { v.nor = Some(v.pos); } } } /// Normalizes any normals currently stored on the mesh in-place. pub fn normalize_vertex_normals(mesh: &mut PolygonMesh) { let editor = mesh.editor(); for n in editor.attributes.normals.iter_mut() { *n = n.normalize(); } } }
src/utils/mod.rs
#![allow(unused)] fn main() { pub mod normal_helpers; pub use normal_helpers::{ compute_face_normals, compute_vertex_normals, add_face_normals, add_vertex_normals, normalize_vertex_normals, }; }
src/lib.rs
#![allow(unused)] fn main() { use truck_meshalgo::prelude::*; pub fn write_polygon_mesh(mesh: &PolygonMesh, path: &str) { let mut obj = std::fs::File::create(path).unwrap(); obj::write(mesh, &mut obj).unwrap(); } pub mod shapes; pub use shapes::{ triangle, square, tetrahedron, hexahedron, octahedron, dodecahedron, icosahedron, }; pub mod utils; pub use utils::normal_helpers::{ compute_face_normals, compute_vertex_normals, add_face_normals, add_vertex_normals, normalize_vertex_normals, }; }