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
}