#![allow(dead_code)]
#[derive(Debug, Clone)]
pub struct FluidSurfaceParams {
pub iso_level: f32,
pub cell_size: f32,
pub smooth_iters: u32,
}
impl Default for FluidSurfaceParams {
fn default() -> Self {
Self {
iso_level: 0.5,
cell_size: 0.05,
smooth_iters: 2,
}
}
}
#[derive(Debug, Clone)]
pub struct FluidSurface {
pub positions: Vec<[f32; 3]>,
pub indices: Vec<u32>,
pub normals: Vec<[f32; 3]>,
}
impl FluidSurface {
pub fn triangle_count(&self) -> usize {
self.indices.len() / 3
}
pub fn vertex_count(&self) -> usize {
self.positions.len()
}
}
pub fn extract_fluid_surface(
density: &[f32],
dims: [usize; 3],
params: &FluidSurfaceParams,
) -> FluidSurface {
let expected = dims[0] * dims[1] * dims[2];
if density.is_empty() || density.len() != expected {
return FluidSurface {
positions: Vec::new(),
indices: Vec::new(),
normals: Vec::new(),
};
}
use crate::marching_cubes::{marching_cubes, ScalarField};
let origin = [0.0f32; 3];
let spacing = [params.cell_size; 3];
let mut field = ScalarField::new(dims, origin, spacing);
field.data.copy_from_slice(density);
let mesh = marching_cubes(&field, params.iso_level);
if mesh.positions.is_empty() {
return FluidSurface {
positions: Vec::new(),
indices: Vec::new(),
normals: Vec::new(),
};
}
let mut positions = mesh.positions;
let indices = mesh.indices;
for _ in 0..params.smooth_iters {
positions = laplacian_smooth(&positions, &indices);
}
let normals = compute_vertex_normals(&positions, &indices);
FluidSurface {
positions,
indices,
normals,
}
}
fn laplacian_smooth(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
let n = positions.len();
if n == 0 || indices.is_empty() {
return positions.to_vec();
}
let mut sum = vec![[0.0f32; 3]; n];
let mut count = vec![0u32; n];
let tri_count = indices.len() / 3;
for t in 0..tri_count {
let a = indices[t * 3] as usize;
let b = indices[t * 3 + 1] as usize;
let c = indices[t * 3 + 2] as usize;
for &(u, v) in &[(a, b), (a, c), (b, a), (b, c), (c, a), (c, b)] {
if u < n && v < n {
sum[u][0] += positions[v][0];
sum[u][1] += positions[v][1];
sum[u][2] += positions[v][2];
count[u] += 1;
}
}
}
let mut out = positions.to_vec();
for i in 0..n {
if count[i] > 0 {
let inv = 1.0 / count[i] as f32;
out[i] = [sum[i][0] * inv, sum[i][1] * inv, sum[i][2] * inv];
}
}
out
}
fn compute_vertex_normals(positions: &[[f32; 3]], indices: &[u32]) -> Vec<[f32; 3]> {
let n = positions.len();
let mut normals = vec![[0.0f32; 3]; n];
let tri_count = indices.len() / 3;
for t in 0..tri_count {
let ia = indices[t * 3] as usize;
let ib = indices[t * 3 + 1] as usize;
let ic = indices[t * 3 + 2] as usize;
if ia >= n || ib >= n || ic >= n {
continue;
}
let pa = positions[ia];
let pb = positions[ib];
let pc = positions[ic];
let ab = [pb[0] - pa[0], pb[1] - pa[1], pb[2] - pa[2]];
let ac = [pc[0] - pa[0], pc[1] - pa[1], pc[2] - pa[2]];
let nx = ab[1] * ac[2] - ab[2] * ac[1];
let ny = ab[2] * ac[0] - ab[0] * ac[2];
let nz = ab[0] * ac[1] - ab[1] * ac[0];
for &idx in &[ia, ib, ic] {
normals[idx][0] += nx;
normals[idx][1] += ny;
normals[idx][2] += nz;
}
}
for norm in &mut normals {
let len = (norm[0] * norm[0] + norm[1] * norm[1] + norm[2] * norm[2]).sqrt();
if len > 1e-10 {
norm[0] /= len;
norm[1] /= len;
norm[2] /= len;
}
}
normals
}
pub fn grid_memory_bytes(dims: [usize; 3]) -> usize {
dims[0] * dims[1] * dims[2] * std::mem::size_of::<f32>()
}
pub fn validate_dims(dims: [usize; 3]) -> bool {
dims.iter().all(|&d| d > 0)
}
pub fn grid_extent(dims: [usize; 3], cell_size: f32) -> [f32; 3] {
[
dims[0] as f32 * cell_size,
dims[1] as f32 * cell_size,
dims[2] as f32 * cell_size,
]
}
pub fn clamp_iso_level(level: f32) -> f32 {
level.clamp(0.0, 1.0)
}
pub fn estimate_surface_triangles(active_voxels: usize) -> usize {
active_voxels * 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_params_iso_level() {
let p = FluidSurfaceParams::default();
assert!((p.iso_level - 0.5).abs() < 1e-6);
}
#[test]
fn extract_stub_empty() {
let s = extract_fluid_surface(&[], [4, 4, 4], &FluidSurfaceParams::default());
assert_eq!(s.triangle_count(), 0);
}
#[test]
fn grid_memory_nonzero() {
assert!(grid_memory_bytes([4, 4, 4]) > 0);
}
#[test]
fn validate_dims_ok() {
assert!(validate_dims([8, 8, 8]));
}
#[test]
fn validate_dims_zero() {
assert!(!validate_dims([0, 8, 8]));
}
#[test]
fn grid_extent_correct() {
let e = grid_extent([4, 4, 4], 0.1);
assert!((e[0] - 0.4).abs() < 1e-5);
}
#[test]
fn clamp_iso_above_one() {
assert!((clamp_iso_level(2.5) - 1.0).abs() < 1e-6);
}
#[test]
fn clamp_iso_below_zero() {
assert!((clamp_iso_level(-0.3)).abs() < 1e-6);
}
#[test]
fn estimate_triangles() {
assert_eq!(estimate_surface_triangles(100), 200);
}
#[test]
fn fluid_surface_vertex_count() {
let s = FluidSurface {
positions: vec![[0.0; 3]; 6],
indices: vec![0, 1, 2, 3, 4, 5],
normals: vec![[0.0, 1.0, 0.0]; 6],
};
assert_eq!(s.vertex_count(), 6);
}
#[test]
fn sphere_density_produces_non_empty_surface() {
let n: usize = 20;
let total = n * n * n;
let mut density = vec![0.0f32; total];
let cx = 10.0f32;
let cy = 10.0f32;
let cz = 10.0f32;
let r = 7.0f32;
for z in 0..n {
for y in 0..n {
for x in 0..n {
let dx = x as f32 - cx;
let dy = y as f32 - cy;
let dz = z as f32 - cz;
let dist = (dx * dx + dy * dy + dz * dz).sqrt();
let idx = x + n * (y + n * z);
density[idx] = if dist < r { 1.0 } else { 0.0 };
}
}
}
let params = FluidSurfaceParams {
iso_level: 0.5,
cell_size: 0.1,
smooth_iters: 0,
};
let surface = extract_fluid_surface(&density, [n, n, n], ¶ms);
assert!(
!surface.positions.is_empty(),
"sphere density must produce non-empty surface"
);
assert!(
!surface.indices.is_empty(),
"sphere density must produce non-empty indices"
);
}
}