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

Sphere

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 DIVISION for 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