use crate::anim::skeleton::{BoneId, Pose, Skeleton};
use glam::{Mat4, Vec3};
pub const MAX_INFLUENCES: usize = 2;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Bind {
pub bones: [u32; MAX_INFLUENCES],
pub weights: [f32; MAX_INFLUENCES],
}
impl Bind {
pub fn rigid(bone: BoneId) -> Bind {
Bind {
bones: [bone.index() as u32, 0],
weights: [1.0, 0.0],
}
}
pub fn dominant(&self) -> BoneId {
let i = if self.weights[1] > self.weights[0] { 1 } else { 0 };
BoneId(self.bones[i])
}
}
impl Default for Bind {
fn default() -> Self {
Bind {
bones: [0; MAX_INFLUENCES],
weights: [1.0, 0.0],
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ParticleSkin {
binds: Vec<Bind>,
}
#[derive(Debug, Clone, Copy)]
struct Segment {
bone: u32,
a: Vec3,
b: Vec3,
}
impl Segment {
fn distance(&self, p: Vec3) -> f32 {
let ab = self.b - self.a;
let len2 = ab.length_squared();
if len2 < 1e-9 {
return (p - self.a).length();
}
let t = ((p - self.a).dot(ab) / len2).clamp(0.0, 1.0);
(p - (self.a + ab * t)).length()
}
}
impl ParticleSkin {
pub fn new() -> ParticleSkin {
ParticleSkin { binds: Vec::new() }
}
pub fn len(&self) -> usize {
self.binds.len()
}
pub fn is_empty(&self) -> bool {
self.binds.is_empty()
}
pub fn bind_of(&self, i: usize) -> Option<Bind> {
self.binds.get(i).copied()
}
pub fn from_binds(binds: Vec<Bind>) -> ParticleSkin {
let mut skin = ParticleSkin { binds };
skin.normalise();
skin
}
pub fn bind_nearest(
points: &[Vec3],
skeleton: &Skeleton,
falloff: f32,
) -> ParticleSkin {
let segments = Self::segments(skeleton);
if segments.is_empty() || points.is_empty() {
return ParticleSkin {
binds: vec![Bind::default(); points.len()],
};
}
let falloff = falloff.max(0.0);
let binds = points
.iter()
.map(|p| Self::nearest_bind(*p, &segments, falloff))
.collect();
ParticleSkin { binds }
}
pub fn bind_grouped(
points: &[Vec3],
skeleton: &Skeleton,
falloff: f32,
groups: &[u16],
group_bones: &[Vec<BoneId>],
) -> ParticleSkin {
let all = Self::segments(skeleton);
if all.is_empty() || points.is_empty() {
return ParticleSkin {
binds: vec![Bind::default(); points.len()],
};
}
let falloff = falloff.max(0.0);
let per_group: Vec<Vec<Segment>> = group_bones
.iter()
.map(|bones| {
if bones.is_empty() {
all.clone()
} else {
all.iter()
.filter(|s| bones.iter().any(|b| b.index() as u32 == s.bone))
.copied()
.collect()
}
})
.collect();
let binds = points
.iter()
.enumerate()
.map(|(i, p)| {
let g = groups.get(i).copied().unwrap_or(u16::MAX) as usize;
let segs = per_group.get(g).filter(|s| !s.is_empty()).unwrap_or(&all);
Self::nearest_bind(*p, segs, falloff)
})
.collect();
ParticleSkin { binds }
}
fn nearest_bind(p: Vec3, segments: &[Segment], falloff: f32) -> Bind {
let (mut best, mut second) = ((u32::MAX, f32::MAX), (u32::MAX, f32::MAX));
for seg in segments {
let d = seg.distance(p);
if d < best.1 {
second = best;
best = (seg.bone, d);
} else if d < second.1 && seg.bone != best.0 {
second = (seg.bone, d);
}
}
if best.0 == u32::MAX {
return Bind::default();
}
let share = if second.0 == u32::MAX || falloff <= 0.0 {
0.0
} else {
let excess = (second.1 - best.1) / falloff;
let k = (1.0 - excess).clamp(0.0, 1.0);
0.5 * k * k
};
Bind {
bones: [best.0, if second.0 == u32::MAX { best.0 } else { second.0 }],
weights: [1.0 - share, share],
}
}
pub fn apply(&self, skeleton: &Skeleton, pose: &Pose, rest: &[Vec3], out: &mut Vec<Vec3>) {
let matrices = Self::skinning_matrices(skeleton, pose);
self.apply_with(&matrices, rest, out);
}
pub fn apply_with(&self, matrices: &[Mat4], rest: &[Vec3], out: &mut Vec<Vec3>) {
out.resize(rest.len(), Vec3::ZERO);
for (i, p) in rest.iter().enumerate() {
let Some(bind) = self.binds.get(i) else {
out[i] = *p;
continue;
};
out[i] = Self::skin_one(*p, bind, matrices);
}
}
pub fn skin_one(p: Vec3, bind: &Bind, matrices: &[Mat4]) -> Vec3 {
let mut out = Vec3::ZERO;
let mut total = 0.0;
for k in 0..MAX_INFLUENCES {
let w = bind.weights[k];
if w <= 0.0 {
continue;
}
let Some(m) = matrices.get(bind.bones[k] as usize) else {
continue;
};
out += m.transform_point3(p) * w;
total += w;
}
if total > 1e-6 {
out / total
} else {
p
}
}
pub fn apply_normals(
&self,
skeleton: &Skeleton,
pose: &Pose,
rest: &[Vec3],
out: &mut Vec<Vec3>,
) {
let matrices = Self::skinning_matrices(skeleton, pose);
self.apply_normals_with(&matrices, rest, out);
}
pub fn apply_normals_with(&self, matrices: &[Mat4], rest: &[Vec3], out: &mut Vec<Vec3>) {
out.resize(rest.len(), Vec3::Z);
for (i, n) in rest.iter().enumerate() {
let Some(bind) = self.binds.get(i) else {
out[i] = *n;
continue;
};
let Some(m) = matrices.get(bind.dominant().index()) else {
out[i] = *n;
continue;
};
let turned = m.transform_vector3(*n);
out[i] = if turned.length_squared() > 1e-12 {
turned.normalize()
} else {
*n
};
}
}
pub fn skinning_matrices(skeleton: &Skeleton, pose: &Pose) -> Vec<Mat4> {
let n = skeleton.len();
let mut world = vec![Mat4::IDENTITY; n];
for id in skeleton.topological_order() {
let i = id.index();
let Some(bone) = skeleton.bone(id) else { continue };
let local = pose
.get(id)
.unwrap_or(bone.local_bind_pose)
.to_mat4();
world[i] = match bone.parent {
Some(p) if p.index() < n => world[p.index()] * local,
_ => local,
};
}
let bind = skeleton.compute_bind_world_matrices();
(0..n)
.map(|i| world[i] * bind.get(i).copied().unwrap_or(Mat4::IDENTITY).inverse())
.collect()
}
fn segments(skeleton: &Skeleton) -> Vec<Segment> {
let world = skeleton.compute_bind_world_matrices();
let origin = |i: usize| -> Vec3 {
world
.get(i)
.map(|m| m.transform_point3(Vec3::ZERO))
.unwrap_or(Vec3::ZERO)
};
let mut out = Vec::with_capacity(skeleton.len());
for i in 0..skeleton.len() {
let id = BoneId(i as u32);
let a = origin(i);
let kids = skeleton.children_of(id);
if kids.is_empty() {
let back = skeleton
.bone(id)
.and_then(|b| b.parent)
.map(|p| a - origin(p.index()))
.unwrap_or(Vec3::Y);
let dir = if back.length_squared() > 1e-9 {
back.normalize()
} else {
Vec3::Y
};
let stub = back.length().max(1e-3) * 0.45;
out.push(Segment {
bone: i as u32,
a,
b: a + dir * stub,
});
} else {
for kid in kids {
out.push(Segment {
bone: i as u32,
a,
b: origin(kid.index()),
});
}
}
}
out
}
fn normalise(&mut self) {
for b in &mut self.binds {
let total: f32 = b.weights.iter().sum();
if total > 1e-6 {
for w in &mut b.weights {
*w /= total;
}
} else {
b.weights = [1.0, 0.0];
}
}
}
}