use std::collections::HashMap;
use bevy::asset::RenderAssetUsages;
use bevy::log::warn;
use bevy::math::{Mat3, Mat4, Vec2, Vec3};
use bevy::mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues};
use crate::CutSettings;
use crate::bond::BondGraph;
use crate::proxy::ProxyCell;
use crate::soup::{LatticeHash, LatticeMap, MIN_CROSS2, Soup, Vtx, WELD, fracture};
use crate::tree::{FragmentId, FragmentTree};
fn triangle_indices(mesh: &Mesh, vertex_count: usize) -> Vec<[u32; 3]> {
let mut tris: Vec<[u32; 3]> = Vec::new();
match mesh.indices() {
Some(Indices::U16(v)) => {
for c in v.chunks_exact(3) {
tris.push([c[0] as u32, c[1] as u32, c[2] as u32]);
}
}
Some(Indices::U32(v)) => {
for c in v.chunks_exact(3) {
tris.push([c[0], c[1], c[2]]);
}
}
None => {
let n = vertex_count as u32;
let mut i = 0;
while i + 3 <= n {
tris.push([i, i + 1, i + 2]);
i += 3;
}
}
}
tris
}
pub(crate) fn append_mesh(soup: &mut Soup, mesh: &Mesh, xform: Mat4, interior: bool) -> bool {
let Some(VertexAttributeValues::Float32x3(positions)) = mesh.attribute(Mesh::ATTRIBUTE_POSITION) else {
warn!("carnage: sub-mesh has no Float32x3 POSITION; skipping it");
return false;
};
if mesh.primitive_topology() != PrimitiveTopology::TriangleList {
warn!("carnage: sub-mesh is not a TriangleList; skipping it");
return false;
}
let tp: Vec<Vec3> = positions.iter().map(|p| xform.transform_point3(Vec3::from_array(*p))).collect();
let normal_mat = Mat3::from_mat4(xform).inverse().transpose();
let have_normals = matches!(
mesh.attribute(Mesh::ATTRIBUTE_NORMAL),
Some(VertexAttributeValues::Float32x3(n)) if n.len() == positions.len()
);
let mut tn: Vec<Vec3> = match mesh.attribute(Mesh::ATTRIBUTE_NORMAL) {
Some(VertexAttributeValues::Float32x3(n)) if have_normals => {
n.iter().map(|v| (normal_mat * Vec3::from_array(*v)).normalize_or_zero()).collect()
}
_ => vec![Vec3::ZERO; tp.len()],
};
let tuv: Vec<Vec2> = match mesh.attribute(Mesh::ATTRIBUTE_UV_0) {
Some(VertexAttributeValues::Float32x2(u)) if u.len() == positions.len() => {
u.iter().map(|v| Vec2::from_array(*v)).collect()
}
_ => vec![Vec2::ZERO; tp.len()],
};
let tris = triangle_indices(mesh, tp.len());
if !have_normals {
for t in &tris {
let (a, b, c) = (t[0] as usize, t[1] as usize, t[2] as usize);
if a >= tp.len() || b >= tp.len() || c >= tp.len() {
continue;
}
let fnrm = (tp[b] - tp[a]).cross(tp[c] - tp[a]);
tn[a] += fnrm;
tn[b] += fnrm;
tn[c] += fnrm;
}
for n in &mut tn {
*n = n.normalize_or_zero();
}
}
let vbase = soup.pos.len() as u32;
soup.pos.extend_from_slice(&tp);
soup.nrm.extend_from_slice(&tn);
soup.uv.extend_from_slice(&tuv);
for t in &tris {
if (t[0] as usize) < tp.len() && (t[1] as usize) < tp.len() && (t[2] as usize) < tp.len() {
soup.idx.push([t[0] + vbase, t[1] + vbase, t[2] + vbase]);
soup.tri_interior.push(interior);
}
}
true
}
fn soup_to_mesh(soup: &Soup, want_interior: bool, recenter: Vec3) -> Option<Mesh> {
let wanted = soup.tri_interior.iter().filter(|&&i| i == want_interior).count();
if wanted == 0 {
return None;
}
let verts = wanted * 3;
let mut pos: Vec<[f32; 3]> = Vec::with_capacity(verts);
let mut nrm: Vec<[f32; 3]> = Vec::with_capacity(verts);
let mut uv: Vec<[f32; 2]> = Vec::with_capacity(verts);
let mut idx: Vec<u32> = Vec::with_capacity(verts);
let mut weld: AttributeWeld = AttributeWeld::with_capacity(verts);
for (t, tri) in soup.idx.iter().enumerate() {
if soup.tri_interior[t] != want_interior {
continue;
}
let (pa, pb, pc) = (
soup.pos[tri[0] as usize],
soup.pos[tri[1] as usize],
soup.pos[tri[2] as usize],
);
if (pb - pa).cross(pc - pa).length_squared() < 1.0e-12 {
continue; }
for &old in tri {
let v = soup.vtx(old);
idx.push(weld.insert(v, recenter, &mut pos, &mut nrm, &mut uv));
}
}
if idx.is_empty() {
return None;
}
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, pos);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, nrm);
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uv);
mesh.insert_indices(Indices::U32(idx));
Some(mesh)
}
pub struct FragmentGeometry {
pub id: FragmentId,
pub outer: Option<Mesh>,
pub cap: Option<Mesh>,
pub cell: ProxyCell,
pub center_local: Vec3,
pub half_extents: Vec3,
}
pub struct FragmentSolid {
pub id: FragmentId,
pub cell: ProxyCell,
}
pub(crate) struct IntactGeometry {
pub(crate) outer: Option<Mesh>,
pub(crate) cap: Option<Mesh>,
pub(crate) center_local: Vec3,
pub(crate) half_extents: Vec3,
}
pub(crate) fn geometry_from_soup(soup: &Soup) -> Option<IntactGeometry> {
if soup.is_empty() {
return None;
}
let (mn, mx) = soup.bbox();
let center = (mn + mx) * 0.5;
let half_extents = ((mx - mn) * 0.5).max(Vec3::splat(0.01));
let outer = soup_to_mesh(soup, false, center);
let cap = soup_to_mesh(soup, true, center);
if outer.is_none() && cap.is_none() {
return None;
}
Some(IntactGeometry { outer, cap, center_local: center, half_extents })
}
#[derive(Clone, Copy)]
struct Candidate {
id: u32,
pos: [f32; 3],
nrm: (i64, i64, i64),
uv: (i64, i64),
}
struct AttributeWeld {
cells: LatticeMap<(i64, i64, i64), Vec<Candidate>>,
}
const NRM_STEP: f32 = 1.0e-2;
const UV_STEP: f32 = 1.0e-4;
impl AttributeWeld {
fn with_capacity(verts: usize) -> Self {
Self {
cells: HashMap::with_capacity_and_hasher(verts, LatticeHash),
}
}
fn insert(
&mut self,
v: crate::soup::Vtx,
recenter: Vec3,
pos: &mut Vec<[f32; 3]>,
nrm: &mut Vec<[f32; 3]>,
uv: &mut Vec<[f32; 2]>,
) -> u32 {
let p = v.pos - recenter;
let q = |x: f32| (x / crate::soup::WELD).round() as i64;
let key = (q(p.x), q(p.y), q(p.z));
let nk = |x: f32| (x / NRM_STEP).round() as i64;
let uk = |x: f32| (x / UV_STEP).round() as i64;
let want_n = (nk(v.nrm.x), nk(v.nrm.y), nk(v.nrm.z));
let want_uv = (uk(v.uv.x), uk(v.uv.y));
for dx in -1..=1 {
for dy in -1..=1 {
for dz in -1..=1 {
let Some(found) = self.cells.get(&(key.0 + dx, key.1 + dy, key.2 + dz)) else {
continue;
};
for c in found {
let same_point = (c.pos[0] - p.x).abs() <= crate::soup::WELD
&& (c.pos[1] - p.y).abs() <= crate::soup::WELD
&& (c.pos[2] - p.z).abs() <= crate::soup::WELD;
if !same_point {
continue;
}
if c.nrm == want_n && c.uv == want_uv {
return c.id;
}
}
}
}
}
let id = pos.len() as u32;
let e = [p.x, p.y, p.z];
pos.push(e);
nrm.push([v.nrm.x, v.nrm.y, v.nrm.z]);
uv.push([v.uv.x, v.uv.y]);
self.cells
.entry(key)
.or_default()
.push(Candidate { id, pos: e, nrm: want_n, uv: want_uv });
id
}
}
pub(crate) fn soup_to_mesh_all_faces(soup: &Soup) -> Result<Mesh, String> {
let (mn, mx) = soup.bbox();
soup_to_mesh_all(soup, (mn + mx) * 0.5).ok_or_else(|| "soup has no drawable triangles".to_string())
}
fn soup_to_mesh_all(soup: &Soup, recenter: Vec3) -> Option<Mesh> {
let mut pos = Vec::new();
let mut nrm = Vec::new();
let mut uv = Vec::new();
let mut idx = Vec::new();
for tri in &soup.idx {
let base = pos.len() as u32;
for &v in tri {
let v = v as usize;
let p = soup.pos[v] - recenter;
pos.push([p.x, p.y, p.z]);
nrm.push([soup.nrm[v].x, soup.nrm[v].y, soup.nrm[v].z]);
uv.push([soup.uv[v].x, soup.uv[v].y]);
}
idx.extend([base, base + 1, base + 2]);
}
if idx.is_empty() {
return None;
}
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, pos);
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, nrm);
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uv);
mesh.insert_indices(Indices::U32(idx));
Some(mesh)
}
pub(crate) fn proxy_soup(cell: &ProxyCell) -> Soup {
let mut s = Soup::default();
cell.append_all_faces(&mut s);
s
}
pub(crate) fn geometry_from_piece(id: FragmentId, piece: crate::soup::Piece) -> FragmentGeometry {
let Drawn { outer, cap, center, half_extents, cell } = draw_piece(piece);
FragmentGeometry { id, outer, cap, cell, center_local: center, half_extents }
}
pub(crate) struct Drawn {
pub(crate) outer: Option<Mesh>,
pub(crate) cap: Option<Mesh>,
pub(crate) center: Vec3,
pub(crate) half_extents: Vec3,
pub(crate) cell: ProxyCell,
}
fn draw_piece(piece: crate::soup::Piece) -> Drawn {
let crate::soup::Piece { cell, render, sheets, relief, soften: round } = piece;
let seam: Vec<Vec3> = render.pos.clone();
let mut drawn = render;
cell.append_cut_faces(&mut drawn, &seam, relief);
let mut drawn = soften(&drawn, round);
for sheet in &sheets {
for (t, tri) in sheet.idx.iter().enumerate() {
drawn.push_tri(
sheet.vtx(tri[0]),
sheet.vtx(tri[1]),
sheet.vtx(tri[2]),
sheet.tri_interior[t],
);
}
}
let (mn, mx) = if drawn.is_empty() { cell_bbox(&cell) } else { drawn.bbox() };
let center = (mn + mx) * 0.5;
let half_extents = ((mx - mn) * 0.5).max(Vec3::splat(0.01));
let (outer, cap) = if drawn.is_empty() {
(None, None)
} else {
(soup_to_mesh(&drawn, false, center), soup_to_mesh(&drawn, true, center))
};
Drawn { outer, cap, center, half_extents, cell }
}
pub struct Ejecta {
pub outer: Option<Mesh>,
pub cap: Option<Mesh>,
pub cell: ProxyCell,
pub center_local: Vec3,
pub half_extents: Vec3,
pub exit: Vec3,
pub direction: Vec3,
}
pub(crate) fn ejecta_from_piece(e: crate::soup::Ejected) -> Ejecta {
let crate::soup::Ejected { piece, exit, direction } = e;
let Drawn { outer, cap, center, half_extents, cell } = draw_piece(piece);
Ejecta { outer, cap, cell, center_local: center, half_extents, exit, direction }
}
fn cell_bbox(cell: &ProxyCell) -> (Vec3, Vec3) {
let mut mn = Vec3::splat(f32::INFINITY);
let mut mx = Vec3::splat(f32::NEG_INFINITY);
for p in cell.points() {
mn = mn.min(*p);
mx = mx.max(*p);
}
if cell.points().is_empty() { (Vec3::ZERO, Vec3::ZERO) } else { (mn, mx) }
}
pub fn fracture_mesh(parts: &[(&Mesh, Mat4)], proxy: &[ProxyCell], cut: &CutSettings) -> Fracture {
let mut soup = Soup::default();
for (mesh, xform) in parts {
append_mesh(&mut soup, mesh, *xform, false);
}
if proxy.is_empty() {
warn!("carnage: refusing to fracture — the caller supplied no proxy cells");
return Fracture::default();
}
if soup.is_empty() {
return Fracture::default();
}
let (pieces, tree, ejected) = fracture(soup, proxy, cut);
let bonds = bond_graph(&pieces, &tree);
let ejecta = ejected.into_iter().map(ejecta_from_piece).collect();
Fracture::new(pieces, tree, bonds, ejecta, crate::soup::residual_bend(cut))
}
pub(crate) fn bond_graph(pieces: &[crate::soup::Piece], tree: &FragmentTree) -> BondGraph {
let members: Vec<(FragmentId, &ProxyCell)> = tree
.leaves()
.into_iter()
.filter_map(|id| pieces.get(id.index()).map(|p| (id, &p.cell)))
.collect();
BondGraph::of(&members, tree.len())
}
#[derive(Default)]
pub struct Fracture {
solids: Vec<FragmentSolid>,
pending: Vec<Option<crate::soup::Piece>>,
built: Vec<Option<FragmentGeometry>>,
pub tree: FragmentTree,
pub bonds: BondGraph,
pub ejecta: Vec<Ejecta>,
pub bent: Vec3,
}
impl Fracture {
pub(crate) fn new(
pieces: Vec<crate::soup::Piece>,
tree: FragmentTree,
bonds: BondGraph,
ejecta: Vec<Ejecta>,
bent: Vec3,
) -> Self {
let solids = pieces
.iter()
.enumerate()
.map(|(i, p)| FragmentSolid { id: FragmentId(i as u32), cell: p.cell.clone() })
.collect();
let built = pieces.iter().map(|_| None).collect();
Fracture {
solids,
pending: pieces.into_iter().map(Some).collect(),
built,
tree,
bonds,
ejecta,
bent,
}
}
pub fn solids(&self) -> &[FragmentSolid] {
&self.solids
}
pub fn len(&self) -> usize {
self.solids.len()
}
pub fn is_empty(&self) -> bool {
self.solids.is_empty()
}
fn materialise(&mut self, ids: &[FragmentId]) {
for id in ids {
let i = id.index();
if self.built.get(i).is_none_or(Option::is_some) {
continue; }
let Some(piece) = self.pending.get_mut(i).and_then(Option::take) else { continue };
self.built[i] = Some(geometry_from_piece(*id, piece));
}
}
pub fn leaves(&mut self) -> Vec<&FragmentGeometry> {
let ids = self.tree.leaves();
self.pick(&ids)
}
pub fn frontier_of(&mut self, count: usize) -> Vec<&FragmentGeometry> {
let ids = self.tree.frontier_of(count);
self.pick(&ids)
}
pub fn at_depth(&mut self, depth: u16) -> Vec<&FragmentGeometry> {
let ids = self.tree.at_depth(depth);
self.pick(&ids)
}
pub fn pick(&mut self, ids: &[FragmentId]) -> Vec<&FragmentGeometry> {
self.materialise(ids);
ids.iter().filter_map(|id| self.built.get(id.index()).and_then(Option::as_ref)).collect()
}
pub fn into_leaves(self) -> Vec<FragmentGeometry> {
let ids = self.tree.leaves();
self.into_pick(&ids)
}
pub fn into_frontier_of(self, count: usize) -> Vec<FragmentGeometry> {
let ids = self.tree.frontier_of(count);
self.into_pick(&ids)
}
pub fn into_at_depth(self, depth: u16) -> Vec<FragmentGeometry> {
let ids = self.tree.at_depth(depth);
self.into_pick(&ids)
}
pub fn into_pick(mut self, ids: &[FragmentId]) -> Vec<FragmentGeometry> {
self.materialise(ids);
let mut keep = vec![false; self.built.len()];
for id in ids {
if let Some(slot) = keep.get_mut(id.index()) {
*slot = true;
}
}
self.built.into_iter().zip(keep).filter_map(|(f, k)| k.then_some(f).flatten()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::proxy::ProxyCell;
use bevy::math::primitives::Cuboid;
fn cube_soup() -> Soup {
let mut s = Soup::default();
assert!(append_mesh(&mut s, &Mesh::from(Cuboid::new(1.0, 1.0, 1.0)), Mat4::IDENTITY, false));
s
}
fn all_finite(s: &Soup) -> bool {
s.pos.iter().all(|p| p.is_finite()) && s.nrm.iter().all(|n| n.is_finite()) && s.uv.iter().all(|u| u.is_finite())
}
fn interior_area(s: &Soup) -> f32 {
s.idx
.iter()
.enumerate()
.filter(|(t, _)| s.tri_interior[*t])
.map(|(_, tri)| {
let (a, b, c) = (s.pos[tri[0] as usize], s.pos[tri[1] as usize], s.pos[tri[2] as usize]);
0.5 * (b - a).cross(c - a).length()
})
.sum()
}
fn cube_proxy() -> Vec<ProxyCell> {
vec![ProxyCell::from_box(Vec3::ZERO, Vec3::splat(0.5))]
}
#[test]
fn slice_cube_axis_plane() {
let (cube, _) = (Mesh::from(Cuboid::new(1.0, 1.0, 1.0)), ());
let pieces =
fracture_mesh(&[(&cube, Mat4::IDENTITY)], &cube_proxy(), &CutSettings::new(2, 0.05, 7)).into_leaves();
assert_eq!(pieces.len(), 2, "one cut should give two pieces");
for p in &pieces {
assert!(p.cap.is_some(), "every piece of a cut carries a cap face");
assert!(p.half_extents.is_finite(), "half extents went non-finite");
assert!(p.center_local.is_finite(), "centre went non-finite");
}
}
#[test]
fn cap_is_unit_square_area() {
let cell = ProxyCell::from_box(Vec3::ZERO, Vec3::splat(0.5));
let (above, _) = cell
.clip(&crate::soup::Plane { point: Vec3::ZERO, normal: Vec3::Y }, crate::proxy::FaceKind::Cut);
let mut cap = Soup::default();
above.expect("the cube cuts").append_cut_faces(&mut cap, &[], 0.0);
assert!(
(interior_area(&cap) - 1.0).abs() < 1.0e-4,
"cap area should be exactly 1.0, got {}",
interior_area(&cap)
);
}
#[test]
fn fracture_reaches_target_and_is_deterministic() {
let proxy = cube_proxy();
let (a, ta, _) = fracture(cube_soup(), &proxy, &CutSettings::new(8, 0.05, 0xABCD_1234));
let (b, tb, _) = fracture(cube_soup(), &proxy, &CutSettings::new(8, 0.05, 0xABCD_1234));
assert_eq!(a.len(), b.len());
assert_eq!(ta, tb, "the hierarchy is reproducible, not just the geometry");
let leaves = ta.leaves();
assert!(leaves.len() >= 2 && leaves.len() <= 8, "sane fragment count: {}", leaves.len());
assert!(
leaves.iter().all(|id| a.get(id.index()).is_some_and(|p| !p.render.is_empty())),
"every leaf kept some render surface"
);
assert!(
a[0].cell.centroid().distance(b[0].cell.centroid()) < 1.0e-6,
"deterministic per seed"
);
assert!(all_finite(&a[0].render), "render payload went non-finite");
}
#[test]
fn every_frontier_of_one_bake_conserves_the_whole_volume() {
let proxy = cube_proxy();
let (pieces, tree, _) = fracture(cube_soup(), &proxy, &CutSettings::new(8, 0.05, 0xABCD_1234));
let whole: f32 = proxy.iter().map(|c| c.volume()).sum();
for cuts in 0..=tree.cuts() {
let v: f32 = tree
.frontier_after(cuts)
.iter()
.filter_map(|id| pieces.get(id.index()))
.map(|p| p.cell.volume())
.sum();
assert!(
(v - whole).abs() < 1.0e-3,
"frontier after {cuts} cuts has volume {v}, expected {whole}"
);
}
}
#[test]
fn one_bake_answers_every_piece_count() {
let cube = Mesh::from(Cuboid::new(1.0, 1.0, 1.0));
let mut baked =
fracture_mesh(&[(&cube, Mat4::IDENTITY)], &cube_proxy(), &CutSettings::new(8, 0.05, 11));
let finest = baked.leaves().len();
assert!(finest >= 4, "expected a usable spread of granularities, got {finest}");
for want in 1..=finest {
let got = baked.frontier_of(want).len();
let expect = want.max(baked.tree.roots().len());
assert_eq!(got, expect, "asked for {want} pieces");
}
assert_eq!(baked.frontier_of(9_999).len(), finest, "past the finest clamps to the leaves");
}
#[test]
fn missing_uv_is_zero_filled() {
let mut m = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
m.insert_attribute(Mesh::ATTRIBUTE_POSITION, vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
m.insert_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 3]);
m.insert_indices(Indices::U32(vec![0, 1, 2]));
let mut s = Soup::default();
assert!(append_mesh(&mut s, &m, Mat4::IDENTITY, false));
assert_eq!(s.uv.len(), s.pos.len());
assert!(s.uv.iter().all(|u| *u == Vec2::ZERO));
}
#[test]
fn missing_normals_are_synthesized() {
let mut m = Mesh::new(PrimitiveTopology::TriangleList, RenderAssetUsages::default());
m.insert_attribute(Mesh::ATTRIBUTE_POSITION, vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
m.insert_indices(Indices::U32(vec![0, 1, 2]));
let mut s = Soup::default();
assert!(append_mesh(&mut s, &m, Mat4::IDENTITY, false));
assert!(s.nrm.iter().all(|n| n.z.abs() > 0.99));
}
#[test]
fn degenerate_plane_leaves_piece_whole() {
let cell = ProxyCell::from_box(Vec3::ZERO, Vec3::splat(0.5));
let (above, below) = cell.clip(
&crate::soup::Plane { point: Vec3::splat(5.0), normal: Vec3::X },
crate::proxy::FaceKind::Cut,
);
assert!(above.is_none(), "nothing above a plane past the cube");
assert!(below.is_some(), "the whole cell lies below it");
let (out, tree, _) = fracture(cube_soup(), &cube_proxy(), &CutSettings::new(4, 0.6, 42));
assert!(!out.is_empty());
assert!(tree.cuts() <= 4, "the volume floor bounded the cuts, got {}", tree.cuts());
assert!(tree.leaves().len() <= 4, "and so bounded the finest frontier");
}
#[test]
fn max_depth_bounds_the_hierarchy_without_looping() {
let (_, tree, _) = fracture(cube_soup(), &cube_proxy(), &CutSettings { max_depth: 2, ..CutSettings::new(32, 0.001, 0x5EED_1234) });
assert!(tree.cuts() > 0, "a depth of 2 still permits cuts");
assert!(
tree.iter().all(|(_, n)| n.depth <= 2),
"no node may sit deeper than the bound asked for"
);
assert!(tree.leaves().len() <= 4, "one cell cut at most twice deep is at most four leaves");
}
#[test]
fn a_render_fragment_carries_no_cap_of_its_own() {
let cube = Mesh::from(Cuboid::new(1.0, 1.0, 1.0));
let pieces =
fracture_mesh(&[(&cube, Mat4::IDENTITY)], &cube_proxy(), &CutSettings::new(4, 0.05, 3)).into_leaves();
assert!(!pieces.is_empty());
for p in &pieces {
assert!(p.cap.is_some(), "the cell supplies a cap for every cut piece");
assert!(p.cell.volume() > 0.0, "the cell is a positively oriented solid");
}
}
#[test]
fn fracture_mesh_is_deterministic_and_recentered() {
let cube = Mesh::from(Cuboid::new(1.0, 2.0, 1.0));
let parts = [(&cube, Mat4::IDENTITY)];
let proxy = vec![ProxyCell::from_box(Vec3::ZERO, Vec3::new(0.5, 1.0, 0.5))];
let a = fracture_mesh(&parts, &proxy, &CutSettings::new(6, 0.05, 0xFEED_BEEF)).into_leaves();
let b = fracture_mesh(&parts, &proxy, &CutSettings::new(6, 0.05, 0xFEED_BEEF)).into_leaves();
assert!(a.len() >= 2, "a 1x2x1 box should break into at least two pieces, got {}", a.len());
assert_eq!(a.len(), b.len(), "same seed, same fragment count");
for (x, y) in a.iter().zip(b.iter()) {
assert_eq!(x.center_local.to_array().map(f32::to_bits), y.center_local.to_array().map(f32::to_bits));
assert_eq!(x.half_extents.to_array().map(f32::to_bits), y.half_extents.to_array().map(f32::to_bits));
assert_eq!(x.cell, y.cell, "the proxy cell itself must be reproducible");
}
assert!(a.iter().all(|f| f.outer.is_some() || f.cap.is_some()), "every fragment draws something");
assert!(a.iter().any(|f| f.cap.is_some()), "cutting a solid must produce cut faces");
}
#[test]
fn two_cells_sharing_a_face_each_keep_their_own_surface() {
let left = Cuboid::new(1.0, 1.0, 1.0);
let right = Cuboid::new(0.4, 0.4, 0.4);
let (lm, rm) = (Mesh::from(left), Mesh::from(right));
let parts = [
(&lm, Mat4::IDENTITY),
(&rm, Mat4::from_translation(Vec3::new(0.7, 0.0, 0.0))),
];
let proxy = vec![
ProxyCell::from_box(Vec3::ZERO, Vec3::splat(0.5)),
ProxyCell::from_box(Vec3::new(0.7, 0.0, 0.0), Vec3::splat(0.2)),
];
let cut = CutSettings { soften: 0.0, cap_relief: 0.0, ..CutSettings::new(2, 0.9, 4) };
let mut baked = fracture_mesh(&parts, &proxy, &cut);
let ids = baked.tree.frontier_of(2);
assert_eq!(ids.len(), 2, "expected the two proxy cells, uncut");
let drawn = baked.pick(&ids);
assert_eq!(drawn.len(), 2, "a fragment per cell");
for (f, want) in drawn.iter().zip([6.0f32, 6.0 * 0.4 * 0.4]) {
let got = mesh_area(f.outer.as_ref()) + mesh_area(f.cap.as_ref());
assert!(
(got - want).abs() < 1.0e-3,
"{:?} drew {got} of its own {want} surface — the shared face went to the other cell",
f.id
);
}
}
fn mesh_area(mesh: Option<&Mesh>) -> f32 {
let Some(mesh) = mesh else { return 0.0 };
let Some(VertexAttributeValues::Float32x3(p)) = mesh.attribute(Mesh::ATTRIBUTE_POSITION)
else {
return 0.0;
};
let Some(idx) = mesh.indices() else { return 0.0 };
let v: Vec<Vec3> = p.iter().map(|q| Vec3::from_array(*q)).collect();
idx.iter()
.collect::<Vec<_>>()
.chunks_exact(3)
.filter_map(|t| {
let (a, b, c) = (*v.get(t[0])?, *v.get(t[1])?, *v.get(t[2])?);
Some((b - a).cross(c - a).length() * 0.5)
})
.sum()
}
#[test]
fn fracture_mesh_of_nothing_is_empty() {
assert!(fracture_mesh(&[], &cube_proxy(), &CutSettings::new(8, 0.1, 1)).tree.is_empty());
let cube = Mesh::from(Cuboid::new(1.0, 1.0, 1.0));
assert!(fracture_mesh(&[(&cube, Mat4::IDENTITY)], &[], &CutSettings::new(8, 0.1, 1)).tree.is_empty());
}
}
pub(crate) fn soften(soup: &Soup, strength: f32) -> Soup {
if strength <= 0.0 || soup.is_empty() {
return soup.clone();
}
let mut fine = Soup::with_capacity(soup.idx.len() * 4);
for (t, tri) in soup.idx.iter().enumerate() {
let (a, b, c) = (soup.vtx(tri[0]), soup.vtx(tri[1]), soup.vtx(tri[2]));
let mid = |x: Vtx, y: Vtx| Vtx {
pos: (x.pos + y.pos) * 0.5,
nrm: (x.nrm + y.nrm).normalize_or_zero(),
uv: (x.uv + y.uv) * 0.5,
};
let (ab, bc, ca) = (mid(a, b), mid(b, c), mid(c, a));
let inside = soup.tri_interior[t];
for (p, q, r) in [(a, ab, ca), (ab, b, bc), (ca, bc, c), (ab, bc, ca)] {
fine.push_tri(p, q, r, inside);
}
}
let key = |p: Vec3| {
let q = |x: f32| (x / WELD).round() as i64;
(q(p.x), q(p.y), q(p.z))
};
let mut canon: LatticeMap<(i64, i64, i64), u32> =
LatticeMap::with_capacity_and_hasher(fine.pos.len(), LatticeHash);
let mut unique: Vec<Vec3> = Vec::new();
let of: Vec<u32> = fine
.pos
.iter()
.map(|p| {
*canon.entry(key(*p)).or_insert_with(|| {
unique.push(*p);
unique.len() as u32 - 1
})
})
.collect();
let n_unique = unique.len();
let mut offs: Vec<u32> = vec![0; n_unique + 1];
for tri in &fine.idx {
for i in 0..3 {
let (u, v) = (of[tri[i] as usize], of[tri[(i + 1) % 3] as usize]);
if u != v {
offs[u as usize + 1] += 1;
offs[v as usize + 1] += 1;
}
}
}
for i in 0..n_unique {
offs[i + 1] += offs[i];
}
let total = offs[n_unique] as usize;
let mut nbr: Vec<u32> = vec![0; total];
let mut at: Vec<u32> = offs[..n_unique].to_vec();
for tri in &fine.idx {
for i in 0..3 {
let (u, v) = (
of[tri[i] as usize] as usize,
of[tri[(i + 1) % 3] as usize] as usize,
);
if u != v {
nbr[at[u] as usize] = v as u32;
at[u] += 1;
nbr[at[v] as usize] = u as u32;
at[v] += 1;
}
}
}
let mut moved = unique.clone();
let mut previous = moved.clone();
for _ in 0..2 {
previous.copy_from_slice(&moved);
for i in 0..n_unique {
let (lo, hi) = (offs[i] as usize, offs[i + 1] as usize);
if lo == hi {
continue;
}
let list = &nbr[lo..hi];
let mean: Vec3 =
list.iter().map(|&n| previous[n as usize]).sum::<Vec3>() / list.len() as f32;
moved[i] = previous[i].lerp(mean, strength.clamp(0.0, 1.0) * 0.5);
}
}
let mut smooth = vec![Vec3::ZERO; unique.len()];
for tri in &fine.idx {
let (i, j, k) = (
of[tri[0] as usize] as usize,
of[tri[1] as usize] as usize,
of[tri[2] as usize] as usize,
);
let face = (moved[j] - moved[i]).cross(moved[k] - moved[i]);
smooth[i] += face;
smooth[j] += face;
smooth[k] += face;
}
let unit: Vec<Vec3> = smooth.iter().map(|n| n.normalize_or_zero()).collect();
let n_fine = fine.pos.len();
let mut out_pos: Vec<Vec3> = Vec::with_capacity(n_fine);
let mut out_nrm: Vec<Vec3> = Vec::with_capacity(n_fine);
for i in 0..n_fine {
let u = of[i] as usize;
out_pos.push(moved[u]);
let n = unit[u];
out_nrm.push(if n == Vec3::ZERO { fine.nrm[i] } else { n });
}
let mut out = Soup {
pos: out_pos,
nrm: out_nrm,
uv: std::mem::take(&mut fine.uv),
idx: std::mem::take(&mut fine.idx),
tri_interior: std::mem::take(&mut fine.tri_interior),
};
let keep: Vec<bool> = out
.idx
.iter()
.map(|t| {
let (a, b, c) = (out.pos[t[0] as usize], out.pos[t[1] as usize], out.pos[t[2] as usize]);
(b - a).cross(c - a).length_squared() >= MIN_CROSS2
})
.collect();
let mut i = 0;
out.idx.retain(|_| {
i += 1;
keep[i - 1]
});
let mut j = 0;
out.tri_interior.retain(|_| {
j += 1;
keep[j - 1]
});
out
}