git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

src/scatter.rs (5.6K)

  1 //! Points on a surface, and pushing points apart: the Scatter node's
  2 //! Surface mode and the Relax node's Repel mode, which together are what
  3 //! hou-control's Scatter SOP (with Relax Points on) and Relax SOP do.
  4 //!
  5 //! Both came in with the Embryo, which needed them as pipeline steps; they
  6 //! live here as node modes so the Embryo can be a template of nodes rather
  7 //! than a pipeline of its own.
  8 
  9 use crate::detail::Detail;
 10 use crate::spatial::{PointGrid, TriGrid};
 11 use glam::Vec3;
 12 
 13 /// A xorshift32, seeded from the float the parameter carries.
 14 ///
 15 /// The Seed is a float because Houdini's is; two seeds that differ in any
 16 /// digit give different bit patterns, and that is all a seed needs.
 17 struct Rng(u32);
 18 
 19 impl Rng {
 20     fn from_seed(seed: f32) -> Rng {
 21         let bits = seed.to_bits() ^ 0x9e37_79b9;
 22         Rng(if bits == 0 { 1 } else { bits })
 23     }
 24 
 25     fn next_u32(&mut self) -> u32 {
 26         let mut x = self.0;
 27         x ^= x << 13;
 28         x ^= x >> 17;
 29         x ^= x << 5;
 30         self.0 = x;
 31         x
 32     }
 33 
 34     /// Uniform in [0, 1).
 35     fn next_f32(&mut self) -> f32 {
 36         (self.next_u32() >> 8) as f32 / (1u32 << 24) as f32
 37     }
 38 }
 39 
 40 /// The surface's triangles, fanned from its primitives.
 41 fn triangles(d: &Detail) -> Vec<[Vec3; 3]> {
 42     d.triangulate(|pos, _| Vec3::from(pos))
 43         .chunks_exact(3)
 44         .map(|t| [t[0], t[1], t[2]])
 45         .collect()
 46 }
 47 
 48 fn tri_area(t: &[Vec3; 3]) -> f32 {
 49     0.5 * (t[1] - t[0]).cross(t[2] - t[0]).length()
 50 }
 51 
 52 pub fn surface_area(d: &Detail) -> f32 {
 53     triangles(d).iter().map(tri_area).sum()
 54 }
 55 
 56 /// `count` points scattered uniformly by area over the surface.
 57 ///
 58 /// Uniform by AREA, not by primitive: a triangle is chosen with probability
 59 /// proportional to its area, then a point inside it by the square-root
 60 /// barycentric draw, so a big face gets its share and a sliver gets almost
 61 /// none. Deterministic for a seed, so a scrub or a reload gives the same
 62 /// points.
 63 pub fn scatter_on_surface(surface: &Detail, count: usize, seed: f32) -> Vec<Vec3> {
 64     let tris = triangles(surface);
 65     if tris.is_empty() || count == 0 {
 66         return Vec::new();
 67     }
 68     let mut cumulative = Vec::with_capacity(tris.len());
 69     let mut total = 0.0;
 70     for t in &tris {
 71         total += tri_area(t);
 72         cumulative.push(total);
 73     }
 74     if total <= 0.0 {
 75         return Vec::new();
 76     }
 77     let mut rng = Rng::from_seed(seed);
 78     let mut out = Vec::with_capacity(count);
 79     for _ in 0..count {
 80         let r = rng.next_f32() * total;
 81         let i = cumulative.partition_point(|&c| c < r).min(tris.len() - 1);
 82         let [a, b, c] = tris[i];
 83         let u = rng.next_f32().sqrt();
 84         let v = rng.next_f32();
 85         out.push(a * (1.0 - u) + b * (u * (1.0 - v)) + c * (u * v));
 86     }
 87     out
 88 }
 89 
 90 /// The radius each scattered point pushes with when relaxing: derived from
 91 /// the area it has to itself, so spheres of that radius roughly tile the
 92 /// surface, scaled by `scale` (the Scatter SOP's Scale Radii By; 1.248 is
 93 /// what hou-control's author settled on) and capped at `max` when given.
 94 pub fn relax_radius(surface: &Detail, count: usize, scale: f32, max: Option<f32>) -> f32 {
 95     let per_point = surface_area(surface) / count.max(1) as f32;
 96     let r = scale * (per_point / std::f32::consts::PI).sqrt();
 97     match max {
 98         Some(m) => r.min(m),
 99         None => r,
100     }
101 }
102 
103 /// One pass of pushing overlapping spheres apart. Returns the displacements
104 /// rather than applying them, so a caller can constrain them first.
105 fn repulsion(pts: &[Vec3], radius: f32) -> Vec<Vec3> {
106     let mut moves = vec![Vec3::ZERO; pts.len()];
107     if radius <= 0.0 || pts.len() < 2 {
108         return moves;
109     }
110     let reach = 2.0 * radius;
111     let grid = PointGrid::build(pts, reach.max(1e-6));
112     let mut near = Vec::new();
113     for (i, &p) in pts.iter().enumerate() {
114         grid.within(p, reach, &mut near);
115         for &j in &near {
116             let j = j as usize;
117             if j == i {
118                 continue;
119             }
120             let d = p - pts[j];
121             let len = d.length();
122             if len >= reach {
123                 continue;
124             }
125             // Each of the pair moves half the overlap; a coincident pair has
126             // no direction to move in, so it is nudged along an axis and the
127             // next pass separates it properly.
128             let dir = if len > 1e-9 { d / len } else { Vec3::X };
129             moves[i] += dir * ((reach - len) * 0.5);
130         }
131     }
132     moves
133 }
134 
135 /// Push points apart across a surface: repel, then put every point back on
136 /// the nearest surface point, `iterations` times.
137 pub fn relax_on_surface(pts: &mut [Vec3], surface: &TriGrid, radius: f32, iterations: usize) {
138     if surface.is_empty() {
139         return;
140     }
141     for _ in 0..iterations {
142         let moves = repulsion(pts, radius);
143         for (p, m) in pts.iter_mut().zip(moves) {
144             let moved = *p + m;
145             *p = surface.closest(moved).map_or(moved, |h| h.point);
146         }
147     }
148 }
149 
150 /// The Relax SOP: push spheres of `radius` apart. With `normals`, each
151 /// point's move is flattened into its tangent plane, so a relaxed mesh keeps
152 /// its shape and only its points slide; without, points move freely.
153 pub fn relax_points(pts: &mut [Vec3], normals: Option<&[Vec3]>, radius: f32, iterations: usize) {
154     for _ in 0..iterations {
155         let moves = repulsion(pts, radius);
156         for (i, (p, mut m)) in pts.iter_mut().zip(moves).enumerate() {
157             if let Some(n) = normals.and_then(|ns| ns.get(i)) {
158                 if n.length_squared() > 0.0 {
159                     m -= *n * m.dot(*n);
160                 }
161             }
162             *p += m;
163         }
164     }
165 }