use glam::{Quat, Vec3};
use crate::gltf::{Joint, Skeleton};
use crate::recipe::{BodyPlan, MonsterParams};
use crate::generators::{range, rng};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PrimKind {
RoundCone,
Ellipsoid,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PrimTint {
Body,
Horn,
Eye,
}
#[derive(Clone, Copy, Debug)]
pub struct PrimitiveDesc {
pub kind: PrimKind,
pub joint_a: usize,
pub joint_b: usize,
pub r1: f32,
pub r2: f32,
pub fold_rank: u8,
pub k: f32,
pub radii: Option<Vec3>,
pub tint: PrimTint,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Gait {
Walk,
Slither,
Fly,
Crawl,
Pulse,
}
#[derive(Clone, Debug)]
pub struct GaitDesc {
pub legs: Vec<Vec<usize>>,
pub spine: Vec<usize>,
pub wings: Vec<usize>,
pub tail: Vec<usize>,
pub head: Option<usize>,
pub style: Gait,
}
#[derive(Clone, Debug)]
pub struct MonsterRig {
pub skeleton: Skeleton,
pub prims: Vec<PrimitiveDesc>,
pub gait: GaitDesc,
pub bounds: (Vec3, Vec3),
}
impl MonsterRig {
pub fn joint_world(&self, i: usize) -> Vec3 {
self.skeleton.global(i).transform_point3(Vec3::ZERO)
}
pub fn world(&self) -> Vec<Vec3> {
(0..self.skeleton.joints.len())
.map(|i| self.joint_world(i))
.collect()
}
}
pub fn build_rig(p: &MonsterParams) -> MonsterRig {
let mut rig = match p.body {
BodyPlan::QuadrupedBeast => plan_quadruped_beast(p),
BodyPlan::Ooze => plan_ooze(p),
BodyPlan::Serpent => plan_serpent(p),
BodyPlan::BipedBrute => plan_biped_brute(p),
BodyPlan::WingedFlyer => plan_winged_flyer(p),
BodyPlan::Arachnid => plan_arachnid(p),
BodyPlan::Insectoid => plan_insectoid(p),
BodyPlan::Aberration => plan_aberration(p),
};
apply_knobs(&mut rig, p);
rig
}
pub(crate) struct RigBuilder {
joints: Vec<(Option<usize>, String, Vec3)>,
world: Vec<Vec3>,
prims: Vec<PrimitiveDesc>,
}
impl RigBuilder {
pub(crate) fn new() -> Self {
Self {
joints: Vec::new(),
world: Vec::new(),
prims: Vec::new(),
}
}
pub(crate) fn joint(&mut self, parent: Option<usize>, name: &str, world_pos: Vec3) -> usize {
let local = match parent {
Some(p) => world_pos - self.world[p],
None => world_pos,
};
let i = self.joints.len();
self.joints.push((parent, name.into(), local));
self.world.push(world_pos);
i
}
pub(crate) fn wpos(&self, i: usize) -> Vec3 {
self.world[i]
}
pub(crate) fn cone(&mut self, a: usize, b: usize, r1: f32, r2: f32, fold_rank: u8, k: f32) {
self.prims.push(PrimitiveDesc {
kind: PrimKind::RoundCone,
joint_a: a,
joint_b: b,
r1,
r2,
fold_rank,
k,
radii: None,
tint: PrimTint::Body,
});
}
pub(crate) fn ellip(&mut self, a: usize, b: usize, r1: f32, r2: f32, fold_rank: u8, k: f32) {
self.prims.push(PrimitiveDesc {
kind: PrimKind::Ellipsoid,
joint_a: a,
joint_b: b,
r1,
r2,
fold_rank,
k,
radii: None,
tint: PrimTint::Body,
});
}
pub(crate) fn flat(&mut self, a: usize, b: usize, radii: Vec3, fold_rank: u8, k: f32) {
self.prims.push(PrimitiveDesc {
kind: PrimKind::Ellipsoid,
joint_a: a,
joint_b: b,
r1: 0.0,
r2: 0.0,
fold_rank,
k,
radii: Some(radii),
tint: PrimTint::Body,
});
}
pub(crate) fn finish(self, gait: GaitDesc) -> MonsterRig {
let skeleton = Skeleton {
joints: self
.joints
.iter()
.map(|(parent, name, t)| Joint {
name: name.clone(),
parent: *parent,
translation: *t,
rotation: Quat::IDENTITY,
})
.collect(),
};
let mut rig = MonsterRig {
skeleton,
prims: self.prims,
gait,
bounds: (Vec3::ZERO, Vec3::ZERO),
};
rig.bounds = compute_bounds(&rig);
rig
}
}
const HIPS: usize = 0;
const SPINE1: usize = 1;
const SPINE2: usize = 2;
const NECK: usize = 3;
const HEAD: usize = 4;
const TAIL1: usize = 5;
const TAIL2: usize = 6;
const FL_UP: usize = 7;
const FL_LO: usize = 8;
const FL_FT: usize = 9;
const FR_UP: usize = 10;
const FR_LO: usize = 11;
const FR_FT: usize = 12;
const RL_UP: usize = 13;
const RL_LO: usize = 14;
const RL_FT: usize = 15;
const RR_UP: usize = 16;
const RR_LO: usize = 17;
const RR_FT: usize = 18;
const SNOUT: usize = 19; const TAIL3: usize = 20;
pub fn plan_quadruped_beast(p: &MonsterParams) -> MonsterRig {
let s = p.size.clamp(0.2, 4.0);
let menace = p.menace.clamp(0.0, 1.0);
let bulk = 1.0 + 0.35 * menace;
let hy = 0.62 * s; let leg_seg = 0.21 * s; let fl_drop = -0.24 * s; let rl_drop = -0.20 * s;
let joints: Vec<(Option<usize>, &str, Vec3)> = vec![
(None, "hips", Vec3::new(0.0, hy, -0.35 * s)),
(Some(HIPS), "spine1", Vec3::new(0.0, 0.02 * s, 0.30 * s)),
(Some(SPINE1), "spine2", Vec3::new(0.0, 0.02 * s, 0.32 * s)),
(Some(SPINE2), "neck", Vec3::new(0.0, 0.06 * s, 0.22 * s)),
(Some(NECK), "head", Vec3::new(0.0, -0.06 * s, 0.20 * s)),
(Some(HIPS), "tail1", Vec3::new(0.0, 0.02 * s, -0.18 * s)),
(Some(TAIL1), "tail2", Vec3::new(0.0, -0.03 * s, -0.22 * s)),
(Some(SPINE2), "fl_upper", Vec3::new(0.18 * s, fl_drop, 0.0)),
(Some(FL_UP), "fl_lower", Vec3::new(0.0, -leg_seg, 0.0)),
(Some(FL_LO), "fl_foot", Vec3::new(0.0, -leg_seg, 0.04 * s)),
(Some(SPINE2), "fr_upper", Vec3::new(-0.18 * s, fl_drop, 0.0)),
(Some(FR_UP), "fr_lower", Vec3::new(0.0, -leg_seg, 0.0)),
(Some(FR_LO), "fr_foot", Vec3::new(0.0, -leg_seg, 0.04 * s)),
(Some(HIPS), "rl_upper", Vec3::new(0.18 * s, rl_drop, 0.0)),
(Some(RL_UP), "rl_lower", Vec3::new(0.0, -leg_seg, 0.0)),
(Some(RL_LO), "rl_foot", Vec3::new(0.0, -leg_seg, -0.02 * s)),
(Some(HIPS), "rr_upper", Vec3::new(-0.18 * s, rl_drop, 0.0)),
(Some(RR_UP), "rr_lower", Vec3::new(0.0, -leg_seg, 0.0)),
(Some(RR_LO), "rr_foot", Vec3::new(0.0, -leg_seg, -0.02 * s)),
(Some(HEAD), "snout", Vec3::new(0.0, -0.02 * s, 0.15 * s)),
(Some(TAIL2), "tail3", Vec3::new(0.0, -0.05 * s, -0.20 * s)),
];
let skeleton = Skeleton {
joints: joints
.iter()
.map(|(parent, name, t)| Joint {
name: (*name).into(),
parent: *parent,
translation: *t,
rotation: Quat::IDENTITY,
})
.collect(),
};
let torso_r = 0.28 * s * bulk;
let neck_r = 0.16 * s * bulk;
let head_r = 0.16 * s;
let leg_up_r = 0.14 * s * bulk; let leg_lo_r = 0.08 * s;
let foot_r = 0.055 * s;
let rc = |a: usize, b: usize, r1: f32, r2: f32, fold_rank: u8, k: f32| PrimitiveDesc {
kind: PrimKind::RoundCone,
joint_a: a,
joint_b: b,
r1,
r2,
fold_rank,
k,
radii: None,
tint: PrimTint::Body,
};
let el = |a: usize, b: usize, r1: f32, r2: f32, fold_rank: u8, k: f32| PrimitiveDesc {
kind: PrimKind::Ellipsoid,
joint_a: a,
joint_b: b,
r1,
r2,
fold_rank,
k,
radii: None,
tint: PrimTint::Body,
};
let mut prims = vec![
el(HIPS, SPINE2, torso_r, 0.05 * s, 0, 0.11 * s),
rc(HIPS, SPINE2, torso_r * 0.86, torso_r * 0.92, 0, 0.11 * s),
rc(SPINE2, NECK, torso_r * 0.6, neck_r * 1.05, 1, 0.06 * s),
rc(NECK, HEAD, neck_r, head_r * 0.85, 1, 0.05 * s),
el(HEAD, SNOUT, head_r, 0.015 * s, 1, 0.045 * s),
rc(HIPS, TAIL1, torso_r * 0.5, 0.12 * s, 3, 0.03 * s),
rc(TAIL1, TAIL2, 0.12 * s, 0.06 * s, 3, 0.028 * s),
rc(TAIL2, TAIL3, 0.06 * s, 0.018 * s, 3, 0.022 * s),
];
let legs_joints = [
(FL_UP, FL_LO, FL_FT),
(FR_UP, FR_LO, FR_FT),
(RL_UP, RL_LO, RL_FT),
(RR_UP, RR_LO, RR_FT),
];
for (up, lo, ft) in legs_joints {
prims.push(rc(up, lo, leg_up_r, leg_lo_r * 1.15, 2, 0.018 * s));
prims.push(rc(lo, ft, leg_lo_r, foot_r, 2, 0.026 * s));
}
let gait = GaitDesc {
legs: legs_joints
.iter()
.map(|(u, l, f)| vec![*u, *l, *f])
.collect(),
spine: vec![HIPS, SPINE1, SPINE2, NECK, HEAD],
wings: Vec::new(),
tail: vec![TAIL1, TAIL2, TAIL3],
head: Some(HEAD),
style: Gait::Walk,
};
let mut rig = MonsterRig {
skeleton,
prims,
gait,
bounds: (Vec3::ZERO, Vec3::ZERO),
};
rig.bounds = compute_bounds(&rig);
rig
}
pub(crate) fn compute_bounds(rig: &MonsterRig) -> (Vec3, Vec3) {
let world = rig.world();
let mut lo = Vec3::splat(f32::INFINITY);
let mut hi = Vec3::splat(f32::NEG_INFINITY);
for d in &rig.prims {
let r = match d.radii {
Some(rv) => rv.max_element(),
None => d.r1.max(d.r2),
} + d.k;
for j in [d.joint_a, d.joint_b] {
let w = world[j];
lo = lo.min(w - Vec3::splat(r));
hi = hi.max(w + Vec3::splat(r));
}
}
lo.y = lo.y.min(0.0);
let pad = Vec3::splat(0.06 * (hi - lo).length().max(1.0) * 0.1 + 0.05);
(lo - pad, hi + pad)
}
use core::f32::consts::{PI, TAU};
fn plan_ooze(p: &MonsterParams) -> MonsterRig {
let s = p.size.clamp(0.2, 4.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let core = r.joint(None, "core", v(0.0, 0.30 * s, 0.0));
let top = r.joint(Some(core), "top", v(0.0, 0.48 * s, 0.05 * s));
let left = r.joint(Some(core), "l", v(-0.32 * s, 0.30 * s, 0.0));
let right = r.joint(Some(core), "r", v(0.32 * s, 0.30 * s, 0.0));
let front = r.joint(Some(core), "f", v(0.0, 0.30 * s, 0.30 * s));
let back = r.joint(Some(core), "b", v(0.0, 0.30 * s, -0.30 * s));
let mut rr = rng(p.seed);
let lump = r.joint(
Some(core),
"lump",
v(
range(&mut rr, -0.2, 0.2) * s,
0.44 * s,
range(&mut rr, -0.15, 0.15) * s,
),
);
r.cone(left, right, 0.33 * s, 0.33 * s, 0, 0.14 * s);
r.cone(front, back, 0.30 * s, 0.30 * s, 0, 0.14 * s);
r.ellip(top, top, 0.24 * s, 0.0, 0, 0.10 * s);
r.ellip(lump, lump, 0.16 * s, 0.0, 0, 0.08 * s);
r.finish(GaitDesc {
legs: Vec::new(),
spine: vec![core, top],
wings: Vec::new(),
tail: Vec::new(),
head: None,
style: Gait::Pulse,
})
}
fn plan_serpent(p: &MonsterParams) -> MonsterRig {
let s = p.size.clamp(0.2, 4.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let n = 12usize;
let mut chain = Vec::new();
let mut prev = None;
for i in 0..n {
let t = i as f32 / (n - 1) as f32; let z = (t - 0.5) * 2.4 * s;
let x = 0.42 * s * (t * PI * 2.2).sin(); let rear = (((t - 0.55) / 0.45).clamp(0.0, 1.0)).powi(2); let y = 0.20 * s + 0.95 * s * rear;
let idx = r.joint(prev, &format!("spine{i}"), v(x, y, z));
chain.push(idx);
prev = Some(idx);
}
let rad = |t: f32| (0.28 * s * (1.0 - (2.0 * t - 1.0).powi(2) * 0.78)).max(0.04 * s);
for i in 0..n - 1 {
let ta = i as f32 / (n - 1) as f32;
let tb = (i + 1) as f32 / (n - 1) as f32;
r.cone(chain[i], chain[i + 1], rad(ta), rad(tb), 0, 0.05 * s);
}
let head = chain[n - 1];
let hp = r.wpos(head);
let snout = r.joint(Some(head), "snout", hp + v(0.0, -0.14 * s, 0.30 * s));
let jaw = r.joint(Some(head), "jaw", hp + v(0.0, -0.20 * s, 0.20 * s));
r.flat(head, snout, v(0.26 * s, 0.24 * s, 0.34 * s), 0, 0.05 * s);
r.cone(head, jaw, 0.16 * s, 0.09 * s, 0, 0.04 * s); r.finish(GaitDesc {
legs: Vec::new(),
spine: chain,
wings: Vec::new(),
tail: Vec::new(),
head: Some(head),
style: Gait::Slither,
})
}
fn plan_biped_brute(p: &MonsterParams) -> MonsterRig {
let s = p.size.clamp(0.2, 4.0);
let bulk = 1.0 + 0.4 * p.menace.clamp(0.0, 1.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let hips = r.joint(None, "hips", v(0.0, 0.88 * s, 0.0));
let spine = r.joint(Some(hips), "spine", v(0.0, 1.02 * s, 0.0));
let chest = r.joint(Some(spine), "chest", v(0.0, 1.20 * s, -0.05 * s));
let neck = r.joint(Some(chest), "neck", v(0.0, 1.30 * s, 0.06 * s));
let head = r.joint(Some(neck), "head", v(0.0, 1.40 * s, 0.12 * s));
r.ellip(hips, chest, 0.24 * s * bulk, 0.05 * s, 0, 0.10 * s); r.cone(hips, chest, 0.20 * s * bulk, 0.24 * s * bulk, 0, 0.10 * s);
r.flat(
chest,
chest,
v(0.36 * s * bulk, 0.22 * s, 0.24 * s),
0,
0.09 * s,
); r.cone(chest, neck, 0.16 * s * bulk, 0.12 * s, 1, 0.06 * s); r.cone(neck, head, 0.10 * s, 0.13 * s, 1, 0.05 * s);
r.ellip(head, head, 0.15 * s, 0.0, 1, 0.05 * s);
for side in [-1.0f32, 1.0] {
let sh = r.joint(Some(chest), "upperarm", v(side * 0.34 * s, 1.18 * s, 0.0));
let el = r.joint(Some(sh), "forearm", v(side * 0.46 * s, 0.90 * s, 0.06 * s));
let hn = r.joint(Some(el), "hand", v(side * 0.50 * s, 0.64 * s, 0.10 * s));
r.cone(sh, el, 0.16 * s * bulk, 0.12 * s * bulk, 2, 0.02 * s);
r.cone(el, hn, 0.12 * s, 0.09 * s, 2, 0.026 * s);
r.ellip(hn, hn, 0.11 * s, 0.0, 2, 0.02 * s); }
let mut legs = Vec::new();
for side in [-1.0f32, 1.0] {
let th = r.joint(Some(hips), "thigh", v(side * 0.16 * s, 0.70 * s, 0.0));
let sn = r.joint(Some(th), "shin", v(side * 0.17 * s, 0.38 * s, 0.02 * s));
let ft = r.joint(Some(sn), "foot", v(side * 0.18 * s, 0.05 * s, 0.14 * s));
r.cone(th, sn, 0.18 * s * bulk, 0.12 * s, 2, 0.018 * s);
r.cone(sn, ft, 0.12 * s, 0.07 * s, 2, 0.026 * s);
legs.push(vec![th, sn, ft]);
}
r.finish(GaitDesc {
legs,
spine: vec![hips, spine, chest, neck, head],
wings: Vec::new(),
tail: Vec::new(),
head: Some(head),
style: Gait::Walk,
})
}
fn plan_winged_flyer(p: &MonsterParams) -> MonsterRig {
let s = p.size.clamp(0.2, 4.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let hips = r.joint(None, "hips", v(0.0, 0.85 * s, 0.0));
let chest = r.joint(Some(hips), "chest", v(0.0, 1.02 * s, 0.08 * s));
let neck = r.joint(Some(chest), "neck", v(0.0, 1.10 * s, 0.20 * s));
let head = r.joint(Some(neck), "head", v(0.0, 1.12 * s, 0.36 * s));
let hp = r.wpos(head);
let snout = r.joint(Some(head), "snout", hp + v(0.0, -0.02 * s, 0.16 * s));
r.ellip(hips, chest, 0.17 * s, 0.04 * s, 0, 0.09 * s);
r.cone(hips, chest, 0.15 * s, 0.15 * s, 0, 0.09 * s);
r.cone(chest, neck, 0.11 * s, 0.09 * s, 1, 0.05 * s);
r.cone(neck, head, 0.08 * s, 0.09 * s, 1, 0.045 * s);
r.ellip(head, snout, 0.10 * s, 0.015 * s, 1, 0.04 * s);
let mut legs = Vec::new();
for side in [-1.0f32, 1.0] {
let th = r.joint(Some(hips), "thigh", v(side * 0.10 * s, 0.72 * s, -0.05 * s));
let sn = r.joint(Some(th), "shin", v(side * 0.11 * s, 0.50 * s, 0.02 * s));
let ft = r.joint(Some(sn), "foot", v(side * 0.12 * s, 0.16 * s, 0.14 * s));
r.cone(th, sn, 0.07 * s, 0.05 * s, 2, 0.018 * s);
r.cone(sn, ft, 0.05 * s, 0.035 * s, 2, 0.02 * s);
legs.push(vec![th, sn, ft]);
}
let mut wings = Vec::new();
for side in [-1.0f32, 1.0] {
let root = r.joint(Some(chest), "wing", v(side * 0.16 * s, 1.06 * s, 0.02 * s));
let tip = r.joint(
Some(root),
"wingtip",
v(side * 1.15 * s, 1.14 * s, 0.12 * s),
);
r.flat(root, tip, v(0.62 * s, 0.045 * s, 0.42 * s), 4, 0.012 * s);
r.cone(root, tip, 0.05 * s, 0.02 * s, 4, 0.02 * s);
wings.push(root);
}
r.finish(GaitDesc {
legs,
spine: vec![hips, chest, neck, head],
wings,
tail: Vec::new(),
head: Some(head),
style: Gait::Fly,
})
}
fn plan_arachnid(p: &MonsterParams) -> MonsterRig {
let s = p.size.clamp(0.2, 4.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let ceph = r.joint(None, "cephalothorax", v(0.0, 0.34 * s, 0.18 * s));
let abdo = r.joint(Some(ceph), "abdomen", v(0.0, 0.40 * s, -0.35 * s));
r.ellip(ceph, ceph, 0.20 * s, 0.0, 0, 0.07 * s);
r.ellip(abdo, abdo, 0.26 * s, 0.0, 0, 0.08 * s);
r.cone(ceph, abdo, 0.13 * s, 0.15 * s, 0, 0.05 * s);
let mut legs = Vec::new();
for side in [-1.0f32, 1.0] {
for kk in 0..4 {
let zc = 0.30 * s - kk as f32 * 0.20 * s;
let spread = 0.16 * s * (1.5 - kk as f32);
let root = r.joint(Some(ceph), "coxa", v(side * 0.20 * s, 0.33 * s, zc));
let knee = r.joint(
Some(root),
"knee",
v(side * 0.52 * s, 0.58 * s, zc + spread * 0.4),
);
let foot = r.joint(Some(knee), "foot", v(side * 0.80 * s, 0.0, zc + spread));
r.cone(root, knee, 0.06 * s, 0.05 * s, 2, 0.02 * s);
r.cone(knee, foot, 0.05 * s, 0.03 * s, 2, 0.02 * s);
legs.push(vec![root, knee, foot]);
}
}
r.finish(GaitDesc {
legs,
spine: vec![ceph, abdo],
wings: Vec::new(),
tail: Vec::new(),
head: None,
style: Gait::Crawl,
})
}
fn plan_insectoid(p: &MonsterParams) -> MonsterRig {
let s = p.size.clamp(0.2, 4.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let thorax = r.joint(None, "thorax", v(0.0, 0.42 * s, 0.10 * s));
let head = r.joint(Some(thorax), "head", v(0.0, 0.44 * s, 0.55 * s));
let abdo = r.joint(Some(thorax), "abdomen", v(0.0, 0.44 * s, -0.55 * s));
let abtip = r.joint(Some(abdo), "abtip", v(0.0, 0.40 * s, -1.05 * s));
r.ellip(thorax, thorax, 0.20 * s, 0.0, 0, 0.06 * s);
r.ellip(head, head, 0.15 * s, 0.0, 0, 0.05 * s);
r.ellip(abdo, abtip, 0.18 * s, 0.02 * s, 0, 0.06 * s);
r.cone(thorax, head, 0.10 * s, 0.12 * s, 0, 0.04 * s);
r.cone(thorax, abdo, 0.11 * s, 0.14 * s, 0, 0.04 * s);
let hp = r.wpos(head);
let al = r.joint(Some(head), "antL", hp + v(0.10 * s, 0.28 * s, 0.20 * s));
let ar = r.joint(Some(head), "antR", hp + v(-0.10 * s, 0.28 * s, 0.20 * s));
r.cone(head, al, 0.03 * s, 0.012 * s, 1, 0.02 * s);
r.cone(head, ar, 0.03 * s, 0.012 * s, 1, 0.02 * s);
let mut legs = Vec::new();
for side in [-1.0f32, 1.0] {
for kk in 0..3 {
let zc = 0.28 * s - kk as f32 * 0.22 * s;
let root = r.joint(Some(thorax), "coxa", v(side * 0.16 * s, 0.40 * s, zc));
let knee = r.joint(
Some(root),
"knee",
v(side * 0.42 * s, 0.55 * s, zc + 0.05 * s),
);
let foot = r.joint(
Some(knee),
"foot",
v(side * 0.58 * s, 0.0, zc + 0.12 * s * (1.0 - kk as f32)),
);
r.cone(root, knee, 0.05 * s, 0.04 * s, 2, 0.018 * s);
r.cone(knee, foot, 0.04 * s, 0.028 * s, 2, 0.02 * s);
legs.push(vec![root, knee, foot]);
}
}
r.finish(GaitDesc {
legs,
spine: vec![head, thorax, abdo, abtip],
wings: Vec::new(),
tail: Vec::new(),
head: Some(head),
style: Gait::Crawl,
})
}
fn plan_aberration(p: &MonsterParams) -> MonsterRig {
let s = p.size.clamp(0.2, 4.0);
let v = Vec3::new;
let mut r = RigBuilder::new();
let core = r.joint(None, "core", v(0.0, 0.60 * s, 0.0));
let lower = r.joint(Some(core), "lower", v(0.0, 0.42 * s, 0.0));
r.ellip(core, core, 0.34 * s, 0.0, 0, 0.10 * s);
r.ellip(lower, lower, 0.26 * s, 0.0, 0, 0.09 * s);
let mut rr = rng(p.seed);
let ntent = 6usize;
let mut legs = Vec::new();
for i in 0..ntent {
let ang = i as f32 / ntent as f32 * TAU + range(&mut rr, -0.3, 0.3);
let dir = v(ang.cos(), 0.0, ang.sin());
let h = 0.55 * s + range(&mut rr, -0.1, 0.1) * s;
let p0 = v(0.0, h, 0.0) + dir * 0.30 * s;
let j0 = r.joint(Some(core), "t0", p0);
let p1 = p0 + dir * 0.28 * s + v(0.0, -0.18 * s, 0.0);
let j1 = r.joint(Some(j0), "t1", p1);
let p2 = p1 + dir * 0.24 * s + v(0.0, -0.28 * s, 0.0);
let j2 = r.joint(Some(j1), "t2", p2);
let p3 = p2 + dir * 0.18 * s + v(0.0, -0.30 * s, 0.0);
let j3 = r.joint(Some(j2), "t3", p3);
r.cone(j0, j1, 0.09 * s, 0.07 * s, 2, 0.02 * s);
r.cone(j1, j2, 0.07 * s, 0.05 * s, 2, 0.02 * s);
r.cone(j2, j3, 0.05 * s, 0.02 * s, 2, 0.02 * s);
legs.push(vec![j0, j1, j2, j3]);
}
r.finish(GaitDesc {
legs,
spine: vec![core, lower],
wings: Vec::new(),
tail: Vec::new(),
head: None,
style: Gait::Pulse,
})
}
use std::collections::HashSet;
struct Knobs {
horns: f32,
spikes: f32,
plates: f32,
tail: f32,
wings: f32,
eyes: i32,
maw: f32,
}
fn resolve_knobs(rig: &MonsterRig, p: &MonsterParams) -> Knobs {
let has_head = rig.gait.head.is_some();
let tail = if p.tail < 0.0 {
if rig.gait.tail.is_empty() { 0.0 } else { 1.0 }
} else {
p.tail.clamp(0.0, 1.0)
};
let wings = if p.wings < 0.0 {
if rig.gait.wings.is_empty() { 0.0 } else { 1.0 }
} else {
p.wings.clamp(0.0, 1.0)
};
let eyes = if p.eyes < 0 {
if has_head { 2 } else { 0 }
} else {
p.eyes.clamp(0, 12)
};
let maw = if p.maw < 0.0 {
0.0
} else {
p.maw.clamp(0.0, 1.0)
};
Knobs {
horns: p.horns.clamp(0.0, 1.0),
spikes: p.spikes.clamp(0.0, 1.0),
plates: p.plates.clamp(0.0, 1.0),
tail,
wings,
eyes,
maw,
}
}
pub(crate) fn add_joint(rig: &mut MonsterRig, parent: usize, name: &str, world_pos: Vec3) -> usize {
let pw = rig.joint_world(parent);
let i = rig.skeleton.joints.len();
rig.skeleton.joints.push(Joint {
name: name.into(),
parent: Some(parent),
translation: world_pos - pw,
rotation: Quat::IDENTITY,
});
i
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn push_cone(
rig: &mut MonsterRig,
a: usize,
b: usize,
r1: f32,
r2: f32,
fold_rank: u8,
k: f32,
tint: PrimTint,
) {
rig.prims.push(PrimitiveDesc {
kind: PrimKind::RoundCone,
joint_a: a,
joint_b: b,
r1,
r2,
fold_rank,
k,
radii: None,
tint,
});
}
pub(crate) fn push_flat(
rig: &mut MonsterRig,
a: usize,
b: usize,
radii: Vec3,
fold_rank: u8,
k: f32,
tint: PrimTint,
) {
rig.prims.push(PrimitiveDesc {
kind: PrimKind::Ellipsoid,
joint_a: a,
joint_b: b,
r1: 0.0,
r2: 0.0,
fold_rank,
k,
radii: Some(radii),
tint,
});
}
pub(crate) fn spine_sample(rig: &MonsterRig, t: f32) -> (Vec3, usize) {
let spine = &rig.gait.spine;
if spine.len() < 2 {
let j = *spine.first().unwrap_or(&0);
return (rig.joint_world(j), j);
}
let segs = spine.len() - 1;
let ft = (t.clamp(0.0, 1.0) * segs as f32).min(segs as f32 - 1e-4);
let i = ft as usize;
let f = ft - i as f32;
let a = rig.joint_world(spine[i]);
let b = rig.joint_world(spine[i + 1]);
(a.lerp(b, f), spine[i])
}
fn apply_knobs(rig: &mut MonsterRig, p: &MonsterParams) {
let s = p.size.clamp(0.2, 4.0);
let k = resolve_knobs(rig, p);
let base = rig.prims.iter().map(|d| d.fold_rank).max().unwrap_or(0);
scale_tail(rig, k.tail);
scale_wings(rig, k.wings);
if k.horns > 0.0 {
add_horns(rig, s, k.horns, base.saturating_add(1));
}
if k.spikes > 0.0 {
add_spikes(rig, s, k.spikes, base.saturating_add(2));
}
if k.plates > 0.0 {
add_plates(rig, s, k.plates, base.saturating_add(3));
}
if k.eyes > 0 {
add_eyes(rig, s, k.eyes, base.saturating_add(4));
}
if k.maw > 0.0 {
add_maw(rig, s, k.maw, base.saturating_add(5));
}
rig.bounds = compute_bounds(rig);
}
fn scale_tail(rig: &mut MonsterRig, v: f32) {
if rig.gait.tail.is_empty() {
return;
}
let set: HashSet<usize> = rig.gait.tail.iter().copied().collect();
let touches = |d: &PrimitiveDesc| set.contains(&d.joint_a) || set.contains(&d.joint_b);
if v <= 0.0 {
rig.prims.retain(|d| !touches(d));
rig.gait.tail.clear();
} else if (v - 1.0).abs() > 1e-3 {
for d in rig.prims.iter_mut().filter(|d| touches(d)) {
d.r1 *= v;
d.r2 *= v;
if let Some(r) = d.radii.as_mut() {
*r *= v;
}
}
}
}
fn scale_wings(rig: &mut MonsterRig, v: f32) {
if rig.gait.wings.is_empty() {
return;
}
let set: HashSet<usize> = rig.gait.wings.iter().copied().collect();
let touches = |d: &PrimitiveDesc| set.contains(&d.joint_a) || set.contains(&d.joint_b);
if v <= 0.0 {
rig.prims.retain(|d| !touches(d));
rig.gait.wings.clear();
} else if (v - 1.0).abs() > 1e-3 {
for d in rig.prims.iter_mut().filter(|d| touches(d)) {
d.r1 *= v;
d.r2 *= v;
if let Some(r) = d.radii.as_mut() {
*r *= v;
}
}
}
}
fn add_horns(rig: &mut MonsterRig, s: f32, v: f32, rank: u8) {
let Some(head) = rig.gait.head else {
return; };
let hp = rig.joint_world(head);
for side in [-1.0f32, 1.0] {
let base_w = hp + Vec3::new(side * 0.07 * s, 0.09 * s, -0.02 * s);
let mid_w = base_w
+ Vec3::new(
side * 0.04 * s,
(0.10 + 0.10 * v) * s,
-(0.06 + 0.08 * v) * s,
);
let tip_w = mid_w
+ Vec3::new(
side * 0.05 * s,
(0.04 + 0.06 * v) * s,
-(0.08 + 0.12 * v) * s,
);
let b0 = add_joint(rig, head, "horn", base_w);
let b1 = add_joint(rig, b0, "horn_mid", mid_w);
let b2 = add_joint(rig, b1, "horn_tip", tip_w);
let r0 = (0.05 * s * (0.5 + 0.5 * v)).max(0.012);
let r1 = r0 * 0.6;
push_cone(rig, b0, b1, r0, r1, rank, 0.02 * s, PrimTint::Horn);
push_cone(rig, b1, b2, r1, 0.008 * s, rank, 0.015 * s, PrimTint::Horn);
}
}
fn add_spikes(rig: &mut MonsterRig, s: f32, v: f32, rank: u8) {
let count = (2.0 + 6.0 * v).round() as usize;
if count == 0 {
return;
}
let height = (0.08 + 0.16 * v) * s;
let rad = (0.028 + 0.03 * v) * s;
for i in 0..count {
let t = 0.18 + 0.62 * (i as f32 / (count.max(2) - 1) as f32);
let (pt, parent) = spine_sample(rig, t);
let scale = 1.0 - 0.4 * (i as f32 / count as f32);
let base_w = pt + Vec3::new(0.0, 0.16 * s, 0.0);
let tip_w = base_w + Vec3::new(0.0, height * scale, -0.03 * s);
let b0 = add_joint(rig, parent, "spike", base_w);
let b1 = add_joint(rig, b0, "spike_tip", tip_w);
push_cone(
rig,
b0,
b1,
rad * scale,
0.006 * s,
rank,
0.012 * s,
PrimTint::Horn,
);
}
}
fn add_plates(rig: &mut MonsterRig, s: f32, v: f32, rank: u8) {
let count = (1.0 + 2.0 * v).round() as usize;
if count == 0 {
return;
}
let w = (0.16 + 0.10 * v) * s;
for i in 0..count {
let t = 0.28 + 0.44 * (i as f32 / (count.max(2) - 1) as f32);
let (pt, parent) = spine_sample(rig, t);
let base_w = pt + Vec3::new(0.0, 0.14 * s, 0.0);
let tip_w = base_w + Vec3::new(0.0, 0.0, -0.02 * s);
let b0 = add_joint(rig, parent, "plate", base_w);
let b1 = add_joint(rig, b0, "plate_b", tip_w);
let radii = Vec3::new(w, 0.045 * s, w * 0.85);
push_flat(rig, b0, b1, radii, rank, 0.02 * s, PrimTint::Horn);
}
}
fn add_eyes(rig: &mut MonsterRig, s: f32, count: i32, rank: u8) {
let Some(head) = rig.gait.head else {
return; };
let hp = rig.joint_world(head);
let er = (0.05 * s).max(0.012);
let pairs = count / 2;
let center = count % 2 == 1;
let r = er * (1.0 - 0.03 * count as f32).clamp(0.5, 1.0);
for i in 0..pairs {
let row = i as f32;
let dy = 0.05 * s - row * 0.06 * s;
let dz = 0.10 * s;
let dx = (0.06 + 0.02 * row) * s;
for side in [-1.0f32, 1.0] {
let base_w = hp + Vec3::new(side * dx, dy, dz);
let out_w = base_w + Vec3::new(side * 0.01 * s, 0.0, 0.02 * s);
let b0 = add_joint(rig, head, "eye", base_w);
let b1 = add_joint(rig, b0, "eye_out", out_w);
push_cone(rig, b0, b1, r, r * 0.7, rank, 0.006 * s, PrimTint::Eye);
}
}
if center {
let base_w = hp + Vec3::new(0.0, 0.09 * s, 0.11 * s);
let out_w = base_w + Vec3::new(0.0, 0.0, 0.02 * s);
let b0 = add_joint(rig, head, "eye", base_w);
let b1 = add_joint(rig, b0, "eye_out", out_w);
push_cone(rig, b0, b1, r, r * 0.7, rank, 0.006 * s, PrimTint::Eye);
}
}
fn add_maw(rig: &mut MonsterRig, s: f32, v: f32, rank: u8) {
let Some(head) = rig.gait.head else {
return; };
let hp = rig.joint_world(head);
let jaw_base = hp + Vec3::new(0.0, -0.10 * s, 0.04 * s);
let jaw_tip = hp + Vec3::new(0.0, -(0.12 + 0.06 * v) * s, (0.14 + 0.10 * v) * s);
let j0 = add_joint(rig, head, "jaw", jaw_base);
let j1 = add_joint(rig, j0, "jaw_tip", jaw_tip);
let jr = (0.10 + 0.08 * v) * s;
push_cone(rig, j0, j1, jr, 0.05 * s, rank, 0.03 * s, PrimTint::Body);
if v > 0.5 {
let n = (2.0 + 4.0 * v).round() as usize;
for i in 0..n {
let f = if n > 1 {
i as f32 / (n - 1) as f32
} else {
0.5
};
let side = (f - 0.5) * 2.0;
let root = jaw_base.lerp(jaw_tip, 0.4) + Vec3::new(side * jr * 0.7, 0.02 * s, 0.0);
let tip = root + Vec3::new(0.0, 0.05 * s, 0.01 * s);
let t0 = add_joint(rig, head, "tooth", root);
let t1 = add_joint(rig, t0, "tooth_tip", tip);
push_cone(
rig,
t0,
t1,
0.018 * s,
0.003 * s,
rank,
0.006 * s,
PrimTint::Horn,
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn params_from(json: &str) -> MonsterParams {
serde_json::from_str(json).unwrap()
}
#[test]
fn horns_and_eyes_add_geometry() {
let base = build_rig(¶ms_from(
r#"{"kind":"monster","body":"quadruped_beast"}"#,
));
let horned = build_rig(¶ms_from(
r#"{"kind":"monster","body":"quadruped_beast","horns":1.0,"eyes":4}"#,
));
assert!(
horned.prims.len() > base.prims.len(),
"horns/eyes add prims"
);
}
#[test]
fn hostile_knob_input_cannot_panic() {
let rig = build_rig(¶ms_from(
r#"{"kind":"monster","body":"biped_brute","horns":-5.0,"spikes":1e30,
"plates":-1.0,"eyes":999999,"maw":1e30,"tail":-9.0,"wings":50.0}"#,
));
let n = rig.skeleton.joints.len();
assert!(rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(n < 400, "eye count clamp keeps joints bounded: {n}");
}
#[test]
fn tail_zero_disables_chain() {
let with = build_rig(¶ms_from(
r#"{"kind":"monster","body":"quadruped_beast"}"#,
));
let without = build_rig(¶ms_from(
r#"{"kind":"monster","body":"quadruped_beast","tail":0.0}"#,
));
assert!(without.gait.tail.is_empty());
assert!(without.prims.len() < with.prims.len(), "tail prims removed");
}
#[test]
fn quadruped_rig_is_wellformed() {
let p = MonsterParams::default(); let rig = build_rig(&p);
assert_eq!(rig.gait.legs.len(), 4, "quadruped has 4 legs");
assert!(matches!(rig.gait.style, Gait::Walk));
assert!(rig.prims.iter().any(|d| d.fold_rank == 0));
let n = rig.skeleton.joints.len();
assert!(rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
}
fn rig_for(body: &str) -> MonsterRig {
let p: MonsterParams = serde_json::from_str(&format!("{{\"body\":\"{body}\"}}")).unwrap();
build_rig(&p)
}
fn assert_wellformed(rig: &MonsterRig) {
let n = rig.skeleton.joints.len();
assert!(rig.prims.iter().all(|d| d.joint_a < n && d.joint_b < n));
assert!(rig.prims.iter().any(|d| d.fold_rank == 0));
for chain in &rig.gait.legs {
assert!(chain.iter().all(|&j| j < n));
}
assert!(rig.gait.spine.iter().all(|&j| j < n));
}
#[test]
fn ooze_plan() {
let rig = rig_for("ooze");
assert!(rig.gait.legs.is_empty());
assert!(matches!(rig.gait.style, Gait::Pulse));
assert_wellformed(&rig);
}
#[test]
fn serpent_plan() {
let rig = rig_for("serpent");
assert!(rig.gait.spine.len() >= 8, "spine {}", rig.gait.spine.len());
assert!(matches!(rig.gait.style, Gait::Slither));
assert_wellformed(&rig);
}
#[test]
fn biped_plan() {
let rig = rig_for("biped_brute");
assert_eq!(rig.gait.legs.len(), 2);
assert!(matches!(rig.gait.style, Gait::Walk));
assert_wellformed(&rig);
}
#[test]
fn flyer_plan() {
let rig = rig_for("winged_flyer");
assert!(!rig.gait.wings.is_empty());
assert!(matches!(rig.gait.style, Gait::Fly));
assert_wellformed(&rig);
}
#[test]
fn arachnid_plan() {
let rig = rig_for("arachnid");
assert!(rig.gait.legs.len() >= 6, "legs {}", rig.gait.legs.len());
assert!(matches!(rig.gait.style, Gait::Crawl));
assert_wellformed(&rig);
}
#[test]
fn insectoid_plan() {
let rig = rig_for("insectoid");
assert_eq!(rig.gait.legs.len(), 6);
assert!(matches!(rig.gait.style, Gait::Crawl));
assert_wellformed(&rig);
}
#[test]
fn aberration_plan() {
let rig = rig_for("aberration");
assert!(
rig.gait.legs.len() >= 4,
"tentacles {}",
rig.gait.legs.len()
);
assert!(matches!(rig.gait.style, Gait::Pulse));
assert_wellformed(&rig);
}
}