volume.rs
9.1 kB · rust · 270 lines
1use super::sample::{membership, pack};2use super::stack::merge;3use super::{Combine, Spec};4use mrlycore::errors::{value_error, Result};5use mrlycore::tensor::Tensor;67/// A cubic grid of f32 samples, x-major.8#[derive(Clone, Debug, PartialEq)]9pub struct Volume {10 /// The samples, x-major, then y, then z.11 pub data: Vec<f32>,12 /// The side in samples.13 pub size: usize,14}1516/// A plane through the unit box, framed for sampling: its centre, its two in-plane axes and the width of the square window that holds the whole section, all in the box `[-1, 1]^3`.17#[derive(Clone, Debug, PartialEq)]18pub struct Frame {19 /// The point of the plane the window is centred on.20 pub centre: [f64; 3],21 /// The unit axis the window's columns run along.22 pub u: [f64; 3],23 /// The unit axis the window's rows run along.24 pub v: [f64; 3],25 /// The unit normal.26 pub normal: [f64; 3],27 /// The side of the square window.28 pub width: f64,29}3031fn dot(a: [f64; 3], b: [f64; 3]) -> f64 {32 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]33}3435fn cross(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {36 [37 a[1] * b[2] - a[2] * b[1],38 a[2] * b[0] - a[0] * b[2],39 a[0] * b[1] - a[1] * b[0],40 ]41}4243fn unit(a: [f64; 3]) -> Option<[f64; 3]> {44 let n = dot(a, a).sqrt();45 (n > 1e-12).then(|| [a[0] / n, a[1] / n, a[2] / n])46}4748impl Volume {49 /// Builds a zeroed volume of the side.50 pub fn new(size: usize) -> Volume {51 Volume {52 data: vec![0.0; size * size * size],53 size,54 }55 }56 /// Wraps x-major samples of the side.57 pub fn from_data(data: Vec<f32>, size: usize) -> Result<Volume> {58 if data.len() != size * size * size {59 return value_error("data must be size*size*size.");60 }61 Ok(Volume { data, size })62 }63 /// Returns the smallest sample.64 pub fn min(&self) -> f32 {65 self.data.iter().cloned().fold(f32::INFINITY, f32::min)66 }67 /// Returns the largest sample.68 pub fn max(&self) -> f32 {69 self.data.iter().cloned().fold(f32::NEG_INFINITY, f32::max)70 }71 /// Reads the sample at a voxel.72 pub fn at(&self, x: usize, y: usize, z: usize) -> f32 {73 self.data[(x * self.size + y) * self.size + z]74 }75 /// Reads the voxel a point of the unit cube falls in, or zero outside it.76 pub fn sample(&self, p: [f64; 3]) -> Option<f32> {77 if p.iter().any(|&c| !(0.0..1.0).contains(&c)) {78 return None;79 }80 let s = self.size as f64;81 Some(self.at(82 (p[0] * s) as usize,83 (p[1] * s) as usize,84 (p[2] * s) as usize,85 ))86 }87 /// Thresholds into a byte tensor: one where a sample reaches the level, zero below.88 pub fn solid(&self, level: f32) -> Tensor {89 let mut grid = Tensor::new(vec![self.size; 3]);90 for (site, &v) in grid.bytes_mut().iter_mut().zip(self.data.iter()) {91 *site = (v >= level) as u8;92 }93 grid94 }95 /// Counts the samples at or above the level.96 pub fn count(&self, level: f32) -> usize {97 self.data.iter().filter(|&&v| v >= level).count()98 }99 /// Samples the plane of the frame on an out by out window: the values row by row, and one byte per pixel saying whether it lies inside the cube.100 pub fn plane(&self, frame: &Frame, out: usize) -> (Vec<f32>, Vec<u8>) {101 let mut values = Vec::with_capacity(out * out);102 let mut inside = Vec::with_capacity(out * out);103 for i in 0..out {104 for j in 0..out {105 let a = ((j as f64 + 0.5) / out as f64 - 0.5) * frame.width;106 let b = ((i as f64 + 0.5) / out as f64 - 0.5) * frame.width;107 let p: Vec<f64> = (0..3)108 .map(|k| (frame.centre[k] + a * frame.u[k] + b * frame.v[k] + 1.0) / 2.0)109 .collect();110 match self.sample([p[0], p[1], p[2]]) {111 Some(v) => {112 values.push(v);113 inside.push(1);114 }115 None => {116 values.push(0.0);117 inside.push(0);118 }119 }120 }121 }122 (values, inside)123 }124}125126/// Frames the plane normal to the direction, at the offset from zero to one across the box along it; the window is the smallest square holding every section on that normal.127pub fn frame(normal: [f64; 3], offset: f64) -> Result<Frame> {128 let Some(n) = unit(normal) else {129 return value_error("the normal must not be zero.");130 };131 let seed = if n[0].abs() < 0.9 {132 [1.0, 0.0, 0.0]133 } else {134 [0.0, 1.0, 0.0]135 };136 let u = unit(cross(seed, n)).unwrap();137 let v = cross(n, u);138 let corners = (0..8).map(|c| {139 [140 if c & 1 == 0 { -1.0 } else { 1.0 },141 if c & 2 == 0 { -1.0 } else { 1.0 },142 if c & 4 == 0 { -1.0 } else { 1.0 },143 ]144 });145 let mut span = [[f64::INFINITY, f64::NEG_INFINITY]; 3];146 for c in corners {147 for (k, axis) in [u, v, n].iter().enumerate() {148 let t = dot(c, *axis);149 span[k][0] = span[k][0].min(t);150 span[k][1] = span[k][1].max(t);151 }152 }153 let mid = |k: usize| (span[k][0] + span[k][1]) / 2.0;154 let depth = span[2][0] + offset.clamp(0.0, 1.0) * (span[2][1] - span[2][0]);155 let centre = [156 mid(0) * u[0] + mid(1) * v[0] + depth * n[0],157 mid(0) * u[1] + mid(1) * v[1] + depth * n[1],158 mid(0) * u[2] + mid(1) * v[2] + depth * n[2],159 ];160 Ok(Frame {161 centre,162 u,163 v,164 normal: n,165 width: (span[0][1] - span[0][0]).max(span[1][1] - span[1][0]),166 })167}168169fn layer(spec: Spec, number: usize, level: usize, size: usize) -> Result<Vec<bool>> {170 let Spec {171 code,172 base: q,173 dimension: d,174 } = spec;175 if d != 3 {176 return value_error("a volume needs dimension 3.");177 }178 let table = membership(code, q, 3)?;179 let mut mask = vec![true; size * size * size];180 let inv = 1.0 / size as f64;181 for k in 0..level.max(1) {182 let s = (number * q.pow(k as u32)) as f64;183 let residue =184 |i: usize| ((s * (i as f64 + 0.5) * inv).floor() as i64).rem_euclid(q as i64) as usize;185 let digits: Vec<usize> = (0..size).map(residue).collect();186 for x in 0..size {187 for y in 0..size {188 for z in 0..size {189 let cell = (x * size + y) * size + z;190 if mask[cell] && !table[pack(&[digits[x], digits[y], digits[z]], q)] {191 mask[cell] = false;192 }193 }194 }195 }196 }197 Ok(mask)198}199200/// Layers one cube design at several side numbers into a volume under the chosen combine.201pub fn volume(202 spec: Spec,203 numbers: &[usize],204 combine: Combine,205 level: usize,206 size: usize,207) -> Result<Volume> {208 if size == 0 {209 return value_error("size must be at least 1.");210 }211 let mut acc = vec![0.0f32; size * size * size];212 let mut first = true;213 for &n in numbers {214 let mask = layer(spec, n, level, size)?;215 merge(&mut acc, &mask, combine, first);216 first = false;217 }218 Volume::from_data(acc, size)219}220221#[cfg(test)]222mod tests {223 use super::*;224 use mrlymath::bang::corners_to_code;225226 fn low() -> Spec {227 Spec::new(corners_to_code(&[vec![0, 0, 0]], 3, 2), 2, 3)228 }229230 #[test]231 fn the_low_corner_at_scale_one_fills_the_first_octant() {232 let v = volume(low(), &[1], Combine::Sum, 1, 4).unwrap();233 assert_eq!(v.count(1.0), 64);234 let v = volume(low(), &[2], Combine::Sum, 1, 4).unwrap();235 assert_eq!(v.count(1.0), 8);236 assert_eq!(v.at(0, 0, 0), 1.0);237 assert_eq!(v.at(3, 0, 0), 0.0);238 }239240 #[test]241 fn the_sponge_stack_counts_its_layers() {242 let sponge = Spec::new(23, 2, 3);243 let v = volume(sponge, &[1, 3], Combine::Sum, 1, 9).unwrap();244 assert_eq!((v.min(), v.max()), (1.0, 2.0));245 assert_eq!(v.count(2.0), 20 * 27);246 let x = volume(sponge, &[1, 3], Combine::Xor, 1, 9).unwrap();247 assert_eq!(x.count(1.0), 7 * 27);248 let a = volume(sponge, &[3, 9], Combine::And, 1, 9).unwrap();249 assert_eq!(a.count(1.0), 64 + 240 + 48);250 assert!(volume(Spec::new(1, 2, 2), &[1], Combine::Sum, 1, 4).is_err());251 assert!(volume(sponge, &[1], Combine::Sum, 1, 0).is_err());252 }253254 #[test]255 fn the_diagonal_frame_holds_the_hexagon() {256 let f = frame([1.0, 1.0, 1.0], 0.5).unwrap();257 assert!((dot(f.centre, f.normal)).abs() < 1e-12);258 assert!((dot(f.u, f.v)).abs() < 1e-12);259 assert!((f.width - 2.0 * (8.0f64 / 3.0).sqrt()).abs() < 1e-9);260 let solid = Volume::from_data(vec![1.0; 27], 3).unwrap();261 let (_, inside) = solid.plane(&f, 200);262 let share = inside.iter().map(|&b| b as f64).sum::<f64>() / 40000.0;263 let hexagon = 3.0 * 3f64.sqrt() / (f.width * f.width);264 assert!((share - hexagon).abs() < 0.01, "{share} {hexagon}");265 let x = frame([1.0, 0.0, 0.0], 0.25).unwrap();266 assert!((x.centre[0] + 0.5).abs() < 1e-12);267 assert_eq!(x.width, 2.0);268 assert!(frame([0.0, 0.0, 0.0], 0.5).is_err());269 }270}