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 }