Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

facett-model3d/src/skull.rs

//! **A procedural low-poly skull**, authored here so the crate ships a
//! license-clean, self-contained 3D fixture (no external asset to source/verify).
//! It builds a [`Mesh3d`](crate::mesh::Mesh3d) with two named groups — `"skull"`
//! (cranium + jaw) and `"eyes"` (two orbs in the sockets) — so the `"eyes"` group
//! is the status-tinted target. [`crate::glb`] serialises this to the embedded
//! `assets/skull.glb`, which the component then loads back through the real glTF
//! path. Pure + deterministic (no RNG) → byte-stable geometry.

use crate::mesh::{Mesh3d, MeshGroup};

/// A low-poly ellipsoid (UV sphere scaled per-axis) as `(positions, normals,
/// indices)`. `lon`×`lat` segments keep it chunky. Proper ellipsoid normals
/// (gradient `[x/rx², y/ry², z/rz²]`, normalised).
fn ellipsoid(center: [f32; 3], radii: [f32; 3], lon: usize, lat: usize) -> (Vec<[f32; 3]>, Vec<[f32; 3]>, Vec<u32>) {
    use std::f32::consts::PI;
    let mut positions = Vec::new();
    let mut normals = Vec::new();
    for i in 0..=lat {
        let v = i as f32 / lat as f32;
        let theta = v * PI; // 0..π  (north→south)
        let (st, ct) = theta.sin_cos();
        for j in 0..=lon {
            let u = j as f32 / lon as f32;
            let phi = u * 2.0 * PI;
            let (sp, cp) = phi.sin_cos();
            let unit = [st * cp, ct, st * sp];
            let p = [center[0] + unit[0] * radii[0], center[1] + unit[1] * radii[1], center[2] + unit[2] * radii[2]];
            let mut n = [unit[0] / radii[0], unit[1] / radii[1], unit[2] / radii[2]];
            let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt().max(1e-6);
            n = [n[0] / len, n[1] / len, n[2] / len];
            positions.push(p);
            normals.push(n);
        }
    }
    let mut indices = Vec::new();
    let row = lon + 1;
    for i in 0..lat {
        for j in 0..lon {
            let a = (i * row + j) as u32;
            let b = (i * row + j + 1) as u32;
            let c = ((i + 1) * row + j) as u32;
            let d = ((i + 1) * row + j + 1) as u32;
            indices.extend_from_slice(&[a, c, b, b, c, d]);
        }
    }
    (positions, normals, indices)
}

/// Append `(p, n, i)` onto a group, fixing up index offsets.
fn append(into: &mut MeshGroup, part: (Vec<[f32; 3]>, Vec<[f32; 3]>, Vec<u32>)) {
    let base = into.positions.len() as u32;
    into.positions.extend(part.0);
    into.normals.extend(part.1);
    into.indices.extend(part.2.into_iter().map(|i| i + base));
}

/// Build the procedural skull. y is **up**, +z is **forward** (toward the viewer).
pub fn skull_mesh() -> Mesh3d {
    // ── bone group: cranium + jaw ────────────────────────────────────────────
    let mut bone = MeshGroup {
        name: "skull".into(),
        positions: Vec::new(),
        normals: Vec::new(),
        indices: Vec::new(),
        base_color: [0.90, 0.88, 0.82, 1.0], // bone white
    };
    // Cranium: a slightly forward-flattened, tall ellipsoid.
    append(&mut bone, ellipsoid([0.0, 0.18, 0.0], [0.72, 0.82, 0.74], 16, 12));
    // Cheek/upper-jaw mass, narrower and pushed forward+down.
    append(&mut bone, ellipsoid([0.0, -0.30, 0.18], [0.56, 0.40, 0.58], 14, 10));
    // Lower jaw, smaller still.
    append(&mut bone, ellipsoid([0.0, -0.62, 0.16], [0.42, 0.26, 0.44], 12, 8));

    // ── eyes group: two orbs sunk into the sockets (the status-tinted target) ─
    let mut eyes = MeshGroup {
        name: "eyes".into(),
        positions: Vec::new(),
        normals: Vec::new(),
        indices: Vec::new(),
        base_color: [0.10, 0.95, 0.45, 1.0], // healthy green default (overridden by status)
    };
    let eye_r = [0.20, 0.20, 0.20];
    append(&mut eyes, ellipsoid([-0.34, 0.06, 0.52], eye_r, 12, 9));
    append(&mut eyes, ellipsoid([0.34, 0.06, 0.52], eye_r, 12, 9));

    let mut mesh = Mesh3d { groups: vec![bone, eyes] };
    mesh.normalise_unit_box();
    mesh
}