use glam::Vec3;
use super::MutableVoxelGrid;
use crate::mesh::{Mesh, Vertex};
#[derive(Debug, Clone)]
pub struct FractureConfig {
pub piece_count: u32,
pub seed: u64,
pub min_piece_size: f32,
pub noise_amplitude: f32,
}
impl Default for FractureConfig {
fn default() -> Self {
Self {
piece_count: 8,
seed: 42,
min_piece_size: 0.05,
noise_amplitude: 0.0,
}
}
}
#[derive(Debug)]
pub struct FracturePiece {
pub cell_index: u32,
pub center: Vec3,
pub mesh: Mesh,
pub voxel_count: u32,
}
pub fn voronoi_fracture(
grid: &MutableVoxelGrid,
center: Vec3,
radius: f32,
config: &FractureConfig,
) -> Vec<FracturePiece> {
let seeds = generate_seeds(center, radius, config.piece_count, config.seed);
let [rx, ry, rz] = grid.resolution;
let total = (rx * ry * rz) as usize;
let mut assignments = vec![u32::MAX; total];
let mut cell_counts = vec![0u32; config.piece_count as usize];
let vs = grid.voxel_size();
let radius_sq = radius * radius;
for z in 0..rz {
for y in 0..ry {
for x in 0..rx {
let idx = grid.voxel_index(x, y, z);
let d = grid.distances[idx];
if d >= 0.0 {
continue;
}
let world = grid.grid_to_world(x, y, z);
if (world - center).length_squared() > radius_sq {
continue;
}
let mut best_dist = f32::MAX;
let mut best_seed = 0u32;
for (si, seed_pos) in seeds.iter().enumerate() {
let mut d2 = (world - *seed_pos).length_squared();
if config.noise_amplitude > 0.0 {
let noise = simple_hash_noise(world, si as u32) * config.noise_amplitude;
d2 += noise;
}
if d2 < best_dist {
best_dist = d2;
best_seed = si as u32;
}
}
assignments[idx] = best_seed;
cell_counts[best_seed as usize] += 1;
}
}
}
let mut pieces = Vec::new();
let min_voxels = (config.min_piece_size / vs.x).max(1.0) as u32;
for cell in 0..config.piece_count {
if cell_counts[cell as usize] < min_voxels {
continue;
}
let (mesh, center_of_mass) = extract_piece_mesh(grid, &assignments, cell);
if mesh.vertices.is_empty() {
continue;
}
pieces.push(FracturePiece {
cell_index: cell,
center: center_of_mass,
mesh,
voxel_count: cell_counts[cell as usize],
});
}
pieces
}
fn generate_seeds(center: Vec3, radius: f32, count: u32, seed: u64) -> Vec<Vec3> {
let mut seeds = Vec::with_capacity(count as usize);
let mut rng = seed;
for _ in 0..count {
loop {
rng = lcg_next(rng);
let rx = lcg_float(rng).mul_add(2.0, -1.0);
rng = lcg_next(rng);
let ry = lcg_float(rng).mul_add(2.0, -1.0);
rng = lcg_next(rng);
let rz = lcg_float(rng).mul_add(2.0, -1.0);
let p = Vec3::new(rx, ry, rz);
if p.length_squared() <= 1.0 {
seeds.push(center + p * radius);
break;
}
}
}
seeds
}
fn extract_piece_mesh(grid: &MutableVoxelGrid, assignments: &[u32], cell: u32) -> (Mesh, Vec3) {
let [rx, ry, rz] = grid.resolution;
let vs = grid.voxel_size();
let mut vertices = Vec::new();
let mut indices = Vec::new();
let mut center_sum = Vec3::ZERO;
let mut center_count = 0u32;
for z in 1..rz.saturating_sub(1) {
for y in 1..ry.saturating_sub(1) {
for x in 1..rx.saturating_sub(1) {
let idx = grid.voxel_index(x, y, z);
if assignments[idx] != cell {
continue;
}
let world = grid.grid_to_world(x, y, z);
center_sum += world;
center_count += 1;
let neighbors = [
grid.voxel_index(x + 1, y, z),
grid.voxel_index(x.saturating_sub(1), y, z),
grid.voxel_index(x, y + 1, z),
grid.voxel_index(x, y.saturating_sub(1), z),
grid.voxel_index(x, y, z + 1),
grid.voxel_index(x, y, z.saturating_sub(1)),
];
let normals = [
Vec3::X,
Vec3::NEG_X,
Vec3::Y,
Vec3::NEG_Y,
Vec3::Z,
Vec3::NEG_Z,
];
for (ni, &nidx) in neighbors.iter().enumerate() {
if assignments[nidx] != cell {
let normal = normals[ni];
let face_center = world + normal * vs.x * 0.5;
let (u, v) = make_tangent_basis(normal);
let half = vs.x * 0.5;
let p0 = face_center + (-u - v) * half;
let p1 = face_center + (u - v) * half;
let p2 = face_center + (u + v) * half;
let p3 = face_center + (-u + v) * half;
let vi = vertices.len() as u32;
vertices.push(Vertex {
position: p0,
normal,
..Default::default()
});
vertices.push(Vertex {
position: p1,
normal,
..Default::default()
});
vertices.push(Vertex {
position: p2,
normal,
..Default::default()
});
vertices.push(Vertex {
position: p3,
normal,
..Default::default()
});
indices.push(vi);
indices.push(vi + 1);
indices.push(vi + 2);
indices.push(vi);
indices.push(vi + 2);
indices.push(vi + 3);
}
}
}
}
}
let center = if center_count > 0 {
center_sum / center_count as f32
} else {
Vec3::ZERO
};
(Mesh { vertices, indices }, center)
}
fn make_tangent_basis(normal: Vec3) -> (Vec3, Vec3) {
let up = if normal.y.abs() < 0.9 {
Vec3::Y
} else {
Vec3::X
};
let u = normal.cross(up).normalize();
let v = u.cross(normal).normalize();
(u, v)
}
fn simple_hash_noise(pos: Vec3, seed: u32) -> f32 {
let ix = (pos.x * 100.0) as u32;
let iy = (pos.y * 100.0) as u32;
let iz = (pos.z * 100.0) as u32;
let h = ix
.wrapping_mul(374761393)
.wrapping_add(iy.wrapping_mul(668265263))
.wrapping_add(iz.wrapping_mul(1274126177))
.wrapping_add(seed.wrapping_mul(1103515245));
let h = h ^ (h >> 13);
let h = h.wrapping_mul(1103515245);
(h as f32) / (u32::MAX as f32)
}
#[inline]
const fn lcg_next(state: u64) -> u64 {
state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407)
}
#[inline]
fn lcg_float(state: u64) -> f32 {
((state >> 16) as u32 as f32) / (u32::MAX as f32)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::SdfNode;
fn make_test_grid() -> MutableVoxelGrid {
let sphere = SdfNode::sphere(1.5);
MutableVoxelGrid::from_sdf(&sphere, [16, 16, 16], Vec3::splat(-2.0), Vec3::splat(2.0))
}
#[test]
fn test_voronoi_fracture() {
let grid = make_test_grid();
let config = FractureConfig {
piece_count: 4,
seed: 42,
min_piece_size: 0.01,
..Default::default()
};
let pieces = voronoi_fracture(&grid, Vec3::ZERO, 1.5, &config);
assert!(!pieces.is_empty(), "Should produce fracture pieces");
let total_voxels: u32 = pieces.iter().map(|p| p.voxel_count).sum();
assert!(total_voxels > 0);
for piece in &pieces {
assert!(!piece.mesh.vertices.is_empty());
}
}
#[test]
fn test_fracture_deterministic() {
let grid = make_test_grid();
let config = FractureConfig {
piece_count: 3,
seed: 999,
min_piece_size: 0.01,
..Default::default()
};
let a = voronoi_fracture(&grid, Vec3::ZERO, 1.0, &config);
let b = voronoi_fracture(&grid, Vec3::ZERO, 1.0, &config);
assert_eq!(a.len(), b.len());
for (pa, pb) in a.iter().zip(b.iter()) {
assert_eq!(pa.voxel_count, pb.voxel_count);
assert_eq!(pa.center, pb.center);
}
}
#[test]
fn test_fracture_small_radius() {
let grid = make_test_grid();
let config = FractureConfig {
piece_count: 4,
seed: 42,
min_piece_size: 0.01,
..Default::default()
};
let pieces = voronoi_fracture(&grid, Vec3::ZERO, 0.3, &config);
assert!(pieces.len() <= 4);
}
#[test]
fn test_generate_seeds() {
let seeds = generate_seeds(Vec3::ZERO, 1.0, 10, 42);
assert_eq!(seeds.len(), 10);
for seed in &seeds {
assert!(seed.length() <= 1.0 + 0.001);
}
}
#[test]
fn test_make_tangent_basis() {
let (u, v) = make_tangent_basis(Vec3::Z);
assert!((u.dot(Vec3::Z)).abs() < 0.001);
assert!((v.dot(Vec3::Z)).abs() < 0.001);
assert!((u.dot(v)).abs() < 0.001);
assert!((u.length() - 1.0).abs() < 0.001);
assert!((v.length() - 1.0).abs() < 0.001);
}
}