use std::collections::HashMap;
use std::f32::consts::TAU;
use std::hash::{BuildHasher, Hasher};
use bevy::log::{info, warn};
use bevy::math::{Vec2, Vec3};
use crate::CutSettings;
use crate::proxy::ProxyCell;
use crate::tree::{FragmentId, FragmentTree, TreeNode};
const LATTICE_MIX: u64 = 0x517c_c1b7_2722_0a95;
#[derive(Default, Clone, Copy)]
pub(crate) struct LatticeHasher {
h: u64,
}
impl LatticeHasher {
#[inline]
fn mix(&mut self, v: u64) {
self.h = (self.h.rotate_left(5) ^ v).wrapping_mul(LATTICE_MIX);
}
}
impl Hasher for LatticeHasher {
fn write(&mut self, bytes: &[u8]) {
for &b in bytes {
self.mix(u64::from(b));
}
}
#[inline]
fn write_i64(&mut self, i: i64) {
self.mix(i as u64);
}
#[inline]
fn write_u64(&mut self, i: u64) {
self.mix(i);
}
#[inline]
fn write_usize(&mut self, i: usize) {
self.mix(i as u64);
}
#[inline]
fn finish(&self) -> u64 {
let mut h = self.h;
h ^= h >> 32;
h = h.wrapping_mul(LATTICE_MIX);
h ^= h >> 29;
h
}
}
#[derive(Default, Clone, Copy)]
pub(crate) struct LatticeHash;
impl BuildHasher for LatticeHash {
type Hasher = LatticeHasher;
#[inline]
fn build_hasher(&self) -> LatticeHasher {
LatticeHasher::default()
}
}
pub(crate) type LatticeMap<K, V> = HashMap<K, V, LatticeHash>;
pub(crate) const EPS: f32 = 1.0e-5;
pub(crate) const WELD: f32 = 1.0e-4;
pub(crate) const MIN_CROSS2: f32 = 1.0e-12;
pub(crate) const INWARD_NUDGE: f32 = 1.0e-3;
fn face_normal(a: Vec3, b: Vec3, c: Vec3) -> Vec3 {
(b - a).cross(c - a).normalize_or_zero()
}
pub fn hash_f32(x: u32) -> f32 {
let mut h = x.wrapping_mul(747_796_405).wrapping_add(2_891_336_453);
h = ((h >> ((h >> 28).wrapping_add(4))) ^ h).wrapping_mul(277_803_737);
h = (h >> 22) ^ h;
(h as f32) / (u32::MAX as f32)
}
#[derive(Clone, Copy)]
pub(crate) struct Vtx {
pub(crate) pos: Vec3,
pub(crate) nrm: Vec3,
pub(crate) uv: Vec2,
}
pub(crate) struct Plane {
pub(crate) point: Vec3,
pub(crate) normal: Vec3,
}
#[derive(Default, Clone)]
pub(crate) struct Soup {
pub(crate) pos: Vec<Vec3>,
pub(crate) nrm: Vec<Vec3>,
pub(crate) uv: Vec<Vec2>,
pub(crate) idx: Vec<[u32; 3]>,
pub(crate) tri_interior: Vec<bool>,
}
impl Soup {
pub(crate) fn is_empty(&self) -> bool {
self.idx.is_empty()
}
pub(crate) fn with_capacity(tris: usize) -> Self {
Self {
pos: Vec::with_capacity(tris * 3),
nrm: Vec::with_capacity(tris * 3),
uv: Vec::with_capacity(tris * 3),
idx: Vec::with_capacity(tris),
tri_interior: Vec::with_capacity(tris),
}
}
pub(crate) fn vtx(&self, i: u32) -> Vtx {
let i = i as usize;
Vtx { pos: self.pos[i], nrm: self.nrm[i], uv: self.uv[i] }
}
pub(crate) fn push_tri(&mut self, a: Vtx, b: Vtx, c: Vtx, interior: bool) {
let base = self.pos.len() as u32;
self.pos.extend_from_slice(&[a.pos, b.pos, c.pos]);
self.nrm.extend_from_slice(&[a.nrm, b.nrm, c.nrm]);
self.uv.extend_from_slice(&[a.uv, b.uv, c.uv]);
self.idx.push([base, base + 1, base + 2]);
self.tri_interior.push(interior);
}
pub(crate) fn bbox(&self) -> (Vec3, Vec3) {
let mut mn = Vec3::splat(f32::INFINITY);
let mut mx = Vec3::splat(f32::NEG_INFINITY);
for p in &self.pos {
mn = mn.min(*p);
mx = mx.max(*p);
}
if self.pos.is_empty() {
(Vec3::ZERO, Vec3::ZERO)
} else {
(mn, mx)
}
}
pub(crate) fn extent(&self) -> f32 {
let (mn, mx) = self.bbox();
((mx - mn) * 0.5).max_element()
}
}
pub(crate) fn signed_dist(p: Vec3, plane: &Plane) -> f32 {
(p - plane.point).dot(plane.normal)
}
pub(crate) fn classify(s: f32) -> i32 {
if s > EPS {
1
} else if s < -EPS {
-1
} else {
0
}
}
fn lerp_vtx(a: Vtx, b: Vtx, t: f32) -> Vtx {
Vtx {
pos: a.pos.lerp(b.pos, t),
nrm: a.nrm.lerp(b.nrm, t).normalize_or_zero(),
uv: a.uv.lerp(b.uv, t),
}
}
fn clip_half(v: [Vtx; 3], s: [f32; 3], keep_above: bool, interior: bool, out: &mut Soup) {
let mut poly: Vec<Vtx> = Vec::with_capacity(4);
for i in 0..3 {
let j = (i + 1) % 3;
let (ci, cj) = (classify(s[i]), classify(s[j]));
let keep_i = if keep_above { ci >= 0 } else { ci <= 0 };
if keep_i {
poly.push(v[i]);
}
if ci != 0 && cj != 0 && ci != cj {
let t = s[i] / (s[i] - s[j]);
poly.push(lerp_vtx(v[i], v[j], t));
}
}
if poly.len() >= 3 {
for i in 1..poly.len() - 1 {
out.push_tri(poly[0], poly[i], poly[i + 1], interior);
}
}
}
pub(crate) fn plane_basis(n: Vec3) -> (Vec3, Vec3) {
let a = if n.x.abs() < 0.9 { Vec3::X } else { Vec3::Y };
let u = n.cross(a).normalize_or_zero();
let v = n.cross(u);
(u, v)
}
fn random_dir(seed: u32) -> Vec3 {
let h1 = hash_f32(seed.wrapping_add(0x1234_5678));
let h2 = hash_f32(seed.wrapping_add(0x9E37_79B9));
let z = 2.0 * h1 - 1.0;
let r = (1.0 - z * z).max(0.0).sqrt();
let phi = h2 * TAU;
Vec3::new(r * phi.cos(), z, r * phi.sin())
}
pub(crate) struct Piece {
pub(crate) cell: ProxyCell,
pub(crate) render: Soup,
pub(crate) sheets: Vec<Soup>,
pub(crate) relief: f32,
pub(crate) soften: f32,
}
pub(crate) struct Ejected {
pub(crate) piece: Piece,
pub(crate) exit: Vec3,
pub(crate) direction: Vec3,
}
struct Shell {
tris: Vec<usize>,
open: bool,
centroid: Vec3,
}
fn shells(soup: &Soup) -> Vec<Shell> {
let q = |x: f32| (x / WELD).round() as i64;
let mut vid: LatticeMap<(i64, i64, i64), usize> =
LatticeMap::with_capacity_and_hasher(soup.pos.len(), LatticeHash);
let mut canon: Vec<usize> = Vec::with_capacity(soup.pos.len());
for p in &soup.pos {
let key = (q(p.x), q(p.y), q(p.z));
let next = vid.len();
canon.push(*vid.entry(key).or_insert(next));
}
let mut parent: Vec<usize> = (0..vid.len()).collect();
fn find(parent: &mut [usize], mut i: usize) -> usize {
while parent[i] != i {
parent[i] = parent[parent[i]];
i = parent[i];
}
i
}
for tri in &soup.idx {
let (a, b, c) = (canon[tri[0] as usize], canon[tri[1] as usize], canon[tri[2] as usize]);
for (x, y) in [(a, b), (b, c)] {
let (rx, ry) = (find(&mut parent, x), find(&mut parent, y));
if rx != ry {
parent[rx] = ry;
}
}
}
let mut order: Vec<usize> = Vec::new();
let mut slot: HashMap<usize, usize> = HashMap::new();
let mut groups: Vec<Vec<usize>> = Vec::new();
for (t, tri) in soup.idx.iter().enumerate() {
let root = find(&mut parent, canon[tri[0] as usize]);
let idx = *slot.entry(root).or_insert_with(|| {
order.push(root);
groups.push(Vec::new());
groups.len() - 1
});
groups[idx].push(t);
}
groups
.into_iter()
.map(|tris| {
let mut edges: HashMap<(usize, usize), u32> = HashMap::new();
let mut sum = Vec3::ZERO;
let mut n = 0.0f32;
for &t in &tris {
let tri = soup.idx[t];
let v = [canon[tri[0] as usize], canon[tri[1] as usize], canon[tri[2] as usize]];
for i in 0..3 {
let (a, b) = (v[i], v[(i + 1) % 3]);
*edges.entry((a.min(b), a.max(b))).or_insert(0) += 1;
}
for &i in &tri {
sum += soup.pos[i as usize];
n += 1.0;
}
}
Shell {
open: edges.values().any(|&c| c == 1),
centroid: if n > 0.0 { sum / n } else { Vec3::ZERO },
tris,
}
})
.collect()
}
pub(crate) fn fracture(
render: Soup,
proxy: &[ProxyCell],
cut: &CutSettings,
) -> (Vec<Piece>, FragmentTree, Vec<Ejected>) {
let CutSettings {
target,
min_fraction,
max_depth,
plane_jitter,
size_spread,
weak_axis,
cap_relief,
soften,
ejecta_soften,
seed,
ref bores,
} = *cut;
let (bored, prisms, plugs) = crate::bore::apply(proxy, bores);
let proxy: &[ProxyCell] = &bored;
let mut torn: Vec<Soup> = (0..prisms.len()).map(|_| Soup::default()).collect();
let mut pieces: Vec<Piece> = proxy
.iter()
.map(|c| Piece { cell: c.clone(), render: Soup::default(), sheets: Vec::new(), relief: cap_relief, soften })
.collect();
let mut homeless = 0usize;
let mut carried = 0usize;
let mut considered = 0usize;
for shell in shells(&render) {
if shell.open {
let mut whole = Soup::default();
for &t in &shell.tris {
let tri = render.idx[t];
whole.push_tri(
render.vtx(tri[0]),
render.vtx(tri[1]),
render.vtx(tri[2]),
render.tri_interior[t],
);
}
match pieces.iter().position(|p| p.cell.contains(shell.centroid)) {
Some(i) => {
pieces[i].sheets.push(whole);
carried += 1;
}
None => homeless += shell.tris.len(),
}
considered += shell.tris.len();
continue;
}
let mut solid = Soup::default();
for &t in &shell.tris {
let tri = render.idx[t];
solid.push_tri(
render.vtx(tri[0]),
render.vtx(tri[1]),
render.vtx(tri[2]),
render.tri_interior[t],
);
}
let solid = crate::bore::carve(solid, &prisms, &mut torn);
for (t, tri) in solid.idx.iter().enumerate() {
let (a, b, c) = (solid.vtx(tri[0]), solid.vtx(tri[1]), solid.vtx(tri[2]));
let mid = (a.pos + b.pos + c.pos) / 3.0;
let mid = mid - face_normal(a.pos, b.pos, c.pos) * INWARD_NUDGE;
match pieces.iter().position(|p| p.cell.contains(mid)) {
Some(i) => pieces[i].render.push_tri(a, b, c, solid.tri_interior[t]),
None => homeless += 1,
}
considered += 1;
}
}
if carried > 0 {
info!("carnage: carrying {carried} open shell(s) whole rather than cutting them");
}
if homeless > 0 {
warn!(
"carnage: {homeless} of {} triangles lie outside every proxy cell and were dropped — the \
proxy does not cover the mesh",
considered
);
}
let whole: f32 = pieces.iter().map(|p| p.cell.volume()).sum();
let f = min_fraction.max(0.0);
let floor = whole * f * f * f;
let mut nodes: Vec<TreeNode> =
(0..pieces.len()).map(|_| TreeNode { parent: None, children: None, depth: 0, split_at: None }).collect();
let mut live: Vec<usize> = (0..pieces.len()).collect();
let mut unsplittable = vec![false; live.len()];
let mut cuts: u32 = 0;
let hard_cap = target * 16 + 32;
for cut_index in 0..hard_cap {
if live.len() >= target.max(1) {
break;
}
let ranked = |node: usize| -> f32 {
let v = pieces[node].cell.volume();
if size_spread <= 0.0 {
return v;
}
let h = hash_f32(seed ^ (node as u32).wrapping_mul(0x9E37_79B9));
v * (1.0 - size_spread * 0.5 + size_spread * h)
};
let Some(slot) = (0..live.len())
.filter(|&s| !unsplittable[s])
.max_by(|&a, &b| ranked(live[a]).total_cmp(&ranked(live[b])).then(b.cmp(&a)))
else {
break;
};
let parent = live[slot];
if pieces[parent].cell.volume() < floor {
unsplittable[slot] = true;
continue;
}
if nodes[parent].depth >= max_depth {
unsplittable[slot] = true;
continue;
}
let s = seed
.wrapping_add((cut_index as u32).wrapping_mul(2_654_435_761))
.wrapping_add(live.len() as u32);
let plane = choose_plane(&pieces[parent].cell, s, weak_axis, plane_jitter);
let (Some(above), Some(below)) = pieces[parent].cell.clip(&plane, crate::proxy::FaceKind::Cut)
else {
unsplittable[slot] = true;
continue;
};
let reserve = pieces[parent].render.idx.len();
let (mut ra, mut rb) = (Soup::with_capacity(reserve), Soup::with_capacity(reserve));
split_render(&pieces[parent].render, &plane, &mut ra, &mut rb);
let (mut sa, mut sb): (Vec<Soup>, Vec<Soup>) = (Vec::new(), Vec::new());
for sheet in &pieces[parent].sheets {
let c = sheet.pos.iter().copied().sum::<Vec3>() / sheet.pos.len().max(1) as f32;
if signed_dist(c, &plane) >= 0.0 { sa.push(sheet.clone()) } else { sb.push(sheet.clone()) }
}
let (above_id, below_id) = (nodes.len(), nodes.len() + 1);
let depth = nodes[parent].depth.saturating_add(1);
let kid = |parent: usize| TreeNode {
parent: Some(FragmentId(parent as u32)),
children: None,
depth,
split_at: None,
};
nodes.push(kid(parent));
nodes.push(kid(parent));
nodes[parent].children = Some([FragmentId(above_id as u32), FragmentId(below_id as u32)]);
nodes[parent].split_at = Some(cuts);
pieces.push(Piece { cell: above, render: ra, sheets: sa, relief: cap_relief, soften });
pieces.push(Piece { cell: below, render: rb, sheets: sb, relief: cap_relief, soften });
live[slot] = above_id;
live.push(below_id);
unsplittable.push(false);
cuts += 1;
}
let ejecta: Vec<Ejected> = plugs
.into_iter()
.enumerate()
.flat_map(|(n, plug)| {
let mut render = Soup::default();
if let Some(skin) = torn.get(plug.prism) {
for (t, tri) in skin.idx.iter().enumerate() {
let (a, b, c) = (skin.vtx(tri[0]), skin.vtx(tri[1]), skin.vtx(tri[2]));
let mid = (a.pos + b.pos + c.pos) / 3.0
- face_normal(a.pos, b.pos, c.pos) * INWARD_NUDGE;
if plug.cell.contains(mid) {
render.push_tri(a, b, c, skin.tri_interior[t]);
}
}
}
let (exit, direction) = (plug.exit, plug.direction);
crate::bore::shatter(
plug.cell,
render,
plug.shatter,
seed.wrapping_add((n as u32).wrapping_mul(0x27D4_EB2F)),
weak_axis,
plane_jitter,
size_spread,
)
.into_iter()
.map(move |(cell, render)| Ejected {
piece: Piece {
cell,
render,
sheets: Vec::new(),
relief: cap_relief,
soften: ejecta_soften,
},
exit,
direction,
})
.collect::<Vec<_>>()
})
.collect();
(pieces, FragmentTree::from_nodes(nodes, cuts), ejecta)
}
pub(crate) fn choose_plane(cell: &ProxyCell, s: u32, weak_axis: f32, plane_jitter: f32) -> Plane {
let centroid = cell.centroid();
let candidates = 1 + (weak_axis.clamp(0.0, 1.0) * 7.0).round() as u32;
let mut normal = random_dir(s);
if candidates > 1 {
let span_of = |n: Vec3| {
let (lo, hi) = cell.span_along(n, centroid);
hi - lo
};
let mut best = span_of(normal);
for k in 1..candidates {
let d = random_dir(s ^ k.wrapping_mul(0x9E37_79B9));
let span = span_of(d);
if span > best {
best = span;
normal = d;
}
}
}
let offset = if plane_jitter > 0.0 {
let (lo, hi) = cell.span_along(normal, centroid);
(lo + (hi - lo) * hash_f32(s ^ 0x5BD1_E995)) * plane_jitter
} else {
0.0
};
Plane { point: centroid + normal * offset, normal }
}
pub(crate) fn split_render(src: &Soup, plane: &Plane, above: &mut Soup, below: &mut Soup) {
for (t, tri) in src.idx.iter().enumerate() {
let v = [src.vtx(tri[0]), src.vtx(tri[1]), src.vtx(tri[2])];
let d = [
signed_dist(v[0].pos, plane),
signed_dist(v[1].pos, plane),
signed_dist(v[2].pos, plane),
];
let interior = src.tri_interior[t];
clip_half(v, d, true, interior, above);
clip_half(v, d, false, interior, below);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_f32_is_frozen() {
let got: Vec<u32> = (0..8u32).map(|i| hash_f32(i).to_bits()).collect();
assert_eq!(
got,
[1022846460, 1059634922, 1056243097, 1056841197, 1042407458, 1057018071, 1064390834, 1056755236],
"the fracture RNG moved. Every cut plane's direction comes from these bits, so a change \
here re-partitions every mesh this crate has ever fractured."
);
for i in 0..1024u32 {
let v = hash_f32(i);
assert!((0.0..1.0).contains(&v), "hash_f32({i}) = {v} escaped [0, 1)");
}
}
#[test]
fn random_dir_is_unit_length_and_never_zero() {
for i in 0..512u32 {
let d = random_dir(i.wrapping_mul(2_654_435_761));
assert!((d.length() - 1.0).abs() < 1.0e-5, "random_dir({i}) length {}", d.length());
}
}
}