pub(crate) mod anim;
pub(crate) mod body;
mod preset;
pub(crate) mod rig;
use glam::Vec3;
use crate::gltf::{Asset, Material, Part};
use crate::mesh::Mesh;
use crate::palette::Palette;
use crate::recipe::MonsterParams;
use rig::MonsterRig;
pub fn generate(p: &MonsterParams, pal: &Palette) -> Asset {
let mut owned = p.clone();
preset::apply_preset(&mut owned);
let p = &owned;
let r = rig::build_rig(p);
let mut mesh = body::build_body(&r, p.size, p.detail, p.seed, p.emissive, pal);
skin_body(&mut mesh, &r);
let phys = body::fit_collider(&r, p.size, p.body);
let animations = if p.animate {
anim::build_clips(&r, p)
} else {
Vec::new()
};
mesh.validate().expect("monster mesh invalid");
let eye_glow = r.prims.iter().any(|d| d.tint == rig::PrimTint::Eye);
let emissive_amt = p
.emissive
.clamp(0.0, 1.0)
.max(if eye_glow { 0.3 } else { 0.0 });
let emissive = pal.accent * emissive_amt * 0.6;
Asset {
name: "monster".into(),
parts: vec![Part {
mesh,
material: Material {
roughness: 0.75,
emissive,
..Default::default()
},
}],
skeleton: Some(r.skeleton),
animations,
physics: Some(phys),
lods: Vec::new(),
instanced: Vec::new(),
boss: None,
}
}
type Pairs = Vec<(usize, usize)>;
fn seg_w(p: Vec3, world: &[Vec3], pairs: &[(usize, usize)]) -> ([u16; 4], [f32; 4]) {
let mut dists: Vec<(f32, u16)> = pairs
.iter()
.map(|&(ja, jb)| {
let (a, b) = (world[ja], world[jb]);
let ab = b - a;
let t = if ab.length_squared() < 1e-12 {
0.0
} else {
((p - a).dot(ab) / ab.length_squared()).clamp(0.0, 1.0)
};
(p.distance(a + ab * t), ja as u16)
})
.collect();
dists.sort_by(|x, y| x.0.partial_cmp(&y.0).unwrap().then(x.1.cmp(&y.1)));
let mut joints = [0u16; 4];
let mut weights = [0f32; 4];
let mut sum = 0.0;
for (i, &(d, ja)) in dists.iter().take(4).enumerate() {
let wgt = 1.0 / (d + 1e-4).powi(4);
joints[i] = ja;
weights[i] = wgt;
sum += wgt;
}
if sum > 0.0 {
for w in &mut weights {
*w /= sum;
}
} else {
weights[0] = 1.0;
}
(joints, weights)
}
fn top2(j: [u16; 4], w: [f32; 4]) -> [(u16, f32); 2] {
let mut v: Vec<(u16, f32)> = j.iter().copied().zip(w.iter().copied()).collect();
v.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap().then(a.0.cmp(&b.0)));
[v[0], v[1]]
}
fn uf_find(uf: &mut [usize], x: usize) -> usize {
let mut r = x;
while uf[r] != r {
r = uf[r];
}
let mut c = x;
while uf[c] != r {
let nx = uf[c];
uf[c] = r;
c = nx;
}
r
}
fn trunk_weight(
p: Vec3,
world: &[Vec3],
pairs: &[(usize, usize)],
root: usize,
) -> ([u16; 4], [f32; 4]) {
if pairs.is_empty() {
([root as u16, 0, 0, 0], [1.0, 0.0, 0.0, 0.0])
} else {
seg_w(p, world, pairs)
}
}
pub(crate) fn skin_body(mesh: &mut Mesh, rig: &MonsterRig) {
use std::collections::{HashMap, HashSet};
let world = rig.world();
let g = &rig.gait;
let skel = &rig.skeleton;
let n = skel.joints.len();
let scale = (rig.bounds.1 - rig.bounds.0).length().max(1.0);
let mut is_trunk = vec![false; n];
for &j in &g.spine {
is_trunk[j] = true;
}
for d in &rig.prims {
if d.fold_rank <= 1 {
is_trunk[d.joint_a] = true;
is_trunk[d.joint_b] = true;
}
}
let mut uf: Vec<usize> = (0..n).collect();
for d in &rig.prims {
if d.fold_rank >= 2 && !is_trunk[d.joint_a] && !is_trunk[d.joint_b] {
let (ra, rb) = (uf_find(&mut uf, d.joint_a), uf_find(&mut uf, d.joint_b));
if ra != rb {
uf[ra] = rb;
}
}
}
let mut fam_of_root: HashMap<usize, usize> = HashMap::new();
let mut fam_joints: Vec<Vec<usize>> = Vec::new();
let mut prim_fam: Vec<usize> = vec![0; rig.prims.len()];
for (pi, d) in rig.prims.iter().enumerate() {
if d.fold_rank <= 1 {
continue;
}
let limb_j = if !is_trunk[d.joint_a] {
Some(d.joint_a)
} else if !is_trunk[d.joint_b] {
Some(d.joint_b)
} else {
None
};
if let Some(j) = limb_j {
let root = uf_find(&mut uf, j);
let next = fam_joints.len() + 1;
let fam = *fam_of_root.entry(root).or_insert(next);
if fam > fam_joints.len() {
fam_joints.push(Vec::new());
}
prim_fam[pi] = fam;
}
}
for (j, &trunk) in is_trunk.iter().enumerate() {
if trunk {
continue;
}
let root = uf_find(&mut uf, j);
if let Some(&fam) = fam_of_root.get(&root) {
fam_joints[fam - 1].push(j);
}
}
let n_fam = fam_joints.len() + 1;
let trunk_pairs: Pairs = g.spine.windows(2).map(|w| (w[0], w[1])).collect();
let trunk_root = *g.spine.first().unwrap_or(&0);
let limb_pairs: Vec<Pairs> = fam_joints
.iter()
.map(|joints| {
let set: HashSet<usize> = joints.iter().copied().collect();
let mut pairs = Pairs::new();
for &j in joints {
if let Some(pj) = skel.joints[j].parent {
if set.contains(&pj) {
pairs.push((pj, j));
}
}
}
pairs
})
.collect();
mesh.joints = Vec::with_capacity(mesh.positions.len());
mesh.weights = Vec::with_capacity(mesh.positions.len());
let near = 0.02 * scale;
for i in 0..mesh.positions.len() {
let p = mesh.positions[i];
let mut fam_d = vec![f32::INFINITY; n_fam];
for (pi, d) in rig.prims.iter().enumerate() {
let e = body::eval_prim(d, &world, p);
let f = prim_fam[pi];
if e < fam_d[f] {
fam_d[f] = e;
}
}
let trunk_d = fam_d[0];
let mut limb_d = f32::INFINITY;
let mut limb_fam = 1usize;
for (f, &d) in fam_d.iter().enumerate().skip(1) {
if d < limb_d {
limb_d = d;
limb_fam = f;
}
}
let has_limb = n_fam > 1 && limb_d.is_finite();
let trunk = trunk_weight(p, &world, &trunk_pairs, trunk_root);
let (j, wt) = if !has_limb || trunk_d + near < limb_d {
trunk
} else if limb_d + near < trunk_d {
seg_w(p, &world, &limb_pairs[limb_fam - 1])
} else {
let limb_pairs = &limb_pairs[limb_fam - 1];
let s = ((trunk_d - limb_d) / near * 0.5 + 0.5).clamp(0.0, 1.0);
let s = s * s * (3.0 - 2.0 * s);
if s <= 0.001 {
trunk
} else if s >= 0.999 {
seg_w(p, &world, limb_pairs)
} else {
let t2 = top2(trunk.0, trunk.1);
let l = seg_w(p, &world, limb_pairs);
let l2 = top2(l.0, l.1);
let joints = [t2[0].0, t2[1].0, l2[0].0, l2[1].0];
let mut weights = [
t2[0].1 * (1.0 - s),
t2[1].1 * (1.0 - s),
l2[0].1 * s,
l2[1].1 * s,
];
let sum: f32 = weights.iter().sum();
for w in &mut weights {
*w /= sum;
}
(joints, weights)
}
};
mesh.joints.push(j);
mesh.weights.push(wt);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn max_edge_stretch(bind: &Mesh, posed: &Mesh) -> f32 {
let mut m = 0.0f32;
for tri in bind.indices.chunks(3) {
for (a, b) in [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] {
let bl = bind.positions[a as usize].distance(bind.positions[b as usize]);
let pl = posed.positions[a as usize].distance(posed.positions[b as usize]);
if bl > 1e-5 {
m = m.max(pl / bl);
}
}
}
m
}
#[test]
fn locomotion_does_not_shatter_mesh() {
let p = MonsterParams::default();
let pal = crate::palette::by_name("verdant");
let asset = generate(&p, &pal);
let bind = &asset.parts[0].mesh;
let walk = asset
.animations
.iter()
.find(|c| c.name == "walk")
.expect("walk clip exists");
let dur = crate::anim::clip_duration(walk);
let posed_asset = crate::anim::pose_asset(&asset, "walk", dur * 0.5).unwrap();
let posed = &posed_asset.parts[0].mesh;
let moved = bind
.positions
.iter()
.zip(&posed.positions)
.any(|(a, b)| a.distance(*b) > 0.01);
assert!(moved, "walk pose should deform the mesh");
let stretch = max_edge_stretch(bind, posed);
assert!(
stretch < 2.5,
"edge stretch {stretch} indicates skinning web"
);
}
}