Normals - Sphere
Here we’ll inflate a cube into a sphere, assign per-vertex normals, and export an OBJ.
Full example: cube → sphere with normals
Reuse the shared write_polygon_mesh helper and hexahedron() shape from lib.rs to keep the sample lean:
Create file examples/normals_sphere.rs
use truck_meshalgo::prelude::*; use truck_meshes::{hexahedron, write_polygon_mesh}; fn main() { //STEPS 1-6 GO HERE }
Step 1: create hexahedron
#![allow(unused)] fn main() { let hexa = hexahedron(); // Center the unit cube from the library so projection covers all octants. let center = Vector3::new(0.5, 0.5, 0.5); }
What it does
Pulls in the unit cube mesh from hexahedron() and recenters it so projecting to a sphere is symmetric in all directions.
Step 2: subdivide each face
#![allow(unused)] fn main() { const DIVISION: usize = 8; // the positions of vertices let positions: Vec<Point3> = hexa .face_iter() .flat_map(|face| { // convert face vertex positions into Vec<Vector3> let v: Vec<Vector3> = face .iter() .map(|vertex| (hexa.positions()[vertex.pos] - center).to_vec()) .collect(); // create (i,j) grid 0..DIVISION (0..=DIVISION) .flat_map(move |i| (0..=DIVISION).map(move |j| (i, j))) .map(move |(i, j)| { let s = i as f64 / DIVISION as f64; let t = j as f64 / DIVISION as f64; // bilinear interpolation inside the quad v[0] * (1.0 - s) * (1.0 - t) + v[1] * s * (1.0 - t) + v[3] * (1.0 - s) * t + v[2] * s * t }) }) // project onto the unit sphere .map(|vec| Point3::from_vec(vec.normalize())) .collect(); }
What it does
Samples each cube face on an (i,j) grid, bilinearly interpolates positions inside each quad, and normalizes every point to sit on the unit sphere.
Step 3: compute normals (just position → vector)
#![allow(unused)] fn main() { let normals: Vec<Vector3> = positions.iter().copied().map(Point3::to_vec).collect(); }
What it does
Converts every sphere position into its outward unit vector; for a unit sphere, position and normal share the same direction.
Step 4: attributes
#![allow(unused)] fn main() { let attrs = StandardAttributes { positions, normals, ..Default::default() }; }
What it does
Packs the generated positions and normals into StandardAttributes, leaving other attributes empty.
Step 5: face construction
#![allow(unused)] fn main() { let faces: Faces = (0..6) .flat_map(|face_idx| { let base = face_idx * (DIVISION + 1) * (DIVISION + 1); // closure to map (i,j) → attribute indices let to_index = move |i: usize, j: usize| { let idx = base + (DIVISION + 1) * i + j; // (pos index, texcoord, normal index) (idx, None, Some(idx)) }; (0..DIVISION) .flat_map(move |i| (0..DIVISION).map(move |j| (i, j))) .map(move |(i, j)| { [ to_index(i, j), to_index(i + 1, j), to_index(i + 1, j + 1), to_index(i, j + 1), ] }) }) .collect(); }
What it does
Builds quad faces for each subdivided patch, reusing the same index for both position and normal so the OBJ stays compact.
Step 6: build mesh and export
#![allow(unused)] fn main() { let sphere = PolygonMesh::new(attrs, faces); write_polygon_mesh(&sphere, "output/sphere.obj"); println!("Wrote output/sphere.obj"); } }
What it does
Creates the final PolygonMesh from attributes and faces, writes it to output/sphere.obj, and logs the output path.
Final result

What to look for
- The OBJ contains per-vertex normals, so most viewers will render it smoothly.
- Because positions and normals share indices, the mesh stays compact; if you ever vary normals independently, keep the separate indices pattern shown above.
- Increase
DIVISIONfor a denser sphere; shading stays smooth because normals are normalized unit vectors.
Updated directory layout
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
│ ├─ normals_icosahedron.rs
│ └─ normals_sphere.rs
└─ output/ # exported OBJ files from examples