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,
};
}