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

src/shapes.rs (18.4K)

  1 //! The four shapes that were kernel subnets — Sphere, Box, Plane, Extrude —
  2 //! as native generators. Phase 7 step 3 of `shapeshifter.md`.
  3 //!
  4 //! Each was a subnet of `input → opencl → output` whose kernel ran under
  5 //! `if (id == 0)`: one work item doing loops, then a weld by position on the
  6 //! way back that threw away every shared point the loop had known about.
  7 //! Native, each builds welded points and real primitives directly — a quad
  8 //! stays a quad — costs no JIT compile, and needs no OpenCL runtime at all.
  9 //! The parameter surfaces are the templates' own, so a saved instance keeps
 10 //! its values through `nativize_kernel_subnets` in `merge_template_defs`.
 11 //!
 12 //! The Sphere's three methods (UV, Icosphere, Cube) weld by a QUANTIZED
 13 //! position key rather than by trusting bit-identical arithmetic across
 14 //! faces: the kernel summed barycentric weights in one fixed expression so
 15 //! shared corners landed on the same bits, then welded at 1e-4 anyway. A
 16 //! quantized key is the same guarantee stated once.
 17 
 18 use crate::app::FsNode;
 19 use crate::detail::{AttribData, Detail, CD, DEFAULT_COLOR};
 20 use crate::geometry::{node_param_f32, node_param_str, node_param_vec3, point_normals, sphere_detail};
 21 use glam::Vec3;
 22 use std::collections::HashMap;
 23 
 24 /// The golden ratio: the icosahedron's corners sit on three orthogonal
 25 /// golden rectangles.
 26 const T: f32 = 1.618_034;
 27 
 28 /// Points welded by quantized position, so a corner two faces share is one
 29 /// point whichever face names it first.
 30 struct Welder {
 31     map: HashMap<[i64; 3], u32>,
 32     d: Detail,
 33 }
 34 
 35 impl Welder {
 36     fn new() -> Self {
 37         Welder { map: HashMap::new(), d: Detail::new() }
 38     }
 39 
 40     fn point(&mut self, p: Vec3) -> u32 {
 41         let key = [
 42             (p.x as f64 * 1e5).round() as i64,
 43             (p.y as f64 * 1e5).round() as i64,
 44             (p.z as f64 * 1e5).round() as i64,
 45         ];
 46         if let Some(&i) = self.map.get(&key) {
 47             return i;
 48         }
 49         let i = self.d.add_point(p);
 50         self.map.insert(key, i);
 51         i
 52     }
 53 
 54     /// A primitive turned OUTWARD about the origin: the shapes here are
 55     /// convex about their centre, so a face whose normal points toward its
 56     /// own centroid is inside out, whatever table it came from.
 57     fn outward_prim(&mut self, pts: &[u32]) {
 58         let a = self.d.pos(pts[0] as usize);
 59         let b = self.d.pos(pts[1] as usize);
 60         let c = self.d.pos(pts[2] as usize);
 61         let n = (b - a).cross(c - a);
 62         let centroid: Vec3 = pts.iter().map(|&p| self.d.pos(p as usize)).sum();
 63         if n.dot(centroid) < 0.0 {
 64             let rev: Vec<u32> = pts.iter().rev().copied().collect();
 65             self.d.add_prim(&rev);
 66         } else {
 67             self.d.add_prim(pts);
 68         }
 69     }
 70 }
 71 
 72 /// Whether a sphere node carries its own Center parameters. A bare `sphere`
 73 /// node built by hand (the tests' `ref_node`) has none and is placed by
 74 /// index, the way Line and Points still are; every node that came through a
 75 /// template or a load does.
 76 pub fn sphere_has_center(target: &FsNode) -> bool {
 77     target.params.iter().any(|p| p.name == "Center X")
 78 }
 79 
 80 /// The Sphere node, by Method: UV (Rows x Columns), Icosphere (20 faces
 81 /// each split into Frequency^2 triangles), Cube (a Resolution x Resolution
 82 /// grid on each face, pushed out through the spherified-cube map). Welded
 83 /// point counts are the closed forms: `2 + (rows - 1) * cols`,
 84 /// `10 f^2 + 2` and `6 r^2 + 2`. Cube builds QUADS — the kernel fanned them.
 85 pub fn sphere_node_detail(target: &FsNode, legacy_center: Option<Vec3>) -> Detail {
 86     let method = node_param_str(target, "Method", "UV").to_lowercase();
 87     let radius = node_param_f32(target, "Radius", 0.5).max(1e-4);
 88     let center = legacy_center.unwrap_or_else(|| {
 89         Vec3::new(
 90             node_param_f32(target, "Center X", 0.0),
 91             node_param_f32(target, "Center Y", 0.55),
 92             node_param_f32(target, "Center Z", 0.0),
 93         )
 94     });
 95     let colored = node_param_str(target, "Color", "true") != "false";
 96     let unit = match method.as_str() {
 97         "icosphere" => icosphere_unit(node_param_f32(target, "Frequency", 4.0).round().clamp(1.0, 16.0) as usize),
 98         "cube" => cube_sphere_unit(node_param_f32(target, "Resolution", 8.0).round().clamp(1.0, 64.0) as usize),
 99         _ => {
100             let rows = node_param_f32(target, "Rows", 16.0).round().clamp(2.0, 128.0) as usize;
101             let cols = node_param_f32(target, "Columns", 24.0).round().clamp(3.0, 128.0) as usize;
102             let mut d = sphere_detail(center, radius, rows, cols);
103             finish_sphere(&mut d, center, colored);
104             return d;
105         }
106     };
107     let mut d = unit;
108     for p in 0..d.num_points() {
109         let u = d.pos(p);
110         d.set_pos(p, center + u * radius);
111     }
112     finish_sphere(&mut d, center, colored);
113     d
114 }
115 
116 /// `Norm`, `UV` and `Cd` from the surface normal, as the sphere has always
117 /// carried them. Colour is the kernel's: the SIGNED normal folded into
118 /// 0..1, world-anchored so a point keeps its colour as the sphere turns.
119 fn finish_sphere(d: &mut Detail, center: Vec3, colored: bool) {
120     let n: Vec<Vec3> = (0..d.num_points()).map(|p| (d.pos(p) - center).normalize_or_zero()).collect();
121     let norms = n.iter().map(|n| n.to_array()).collect();
122     let uvs = n
123         .iter()
124         .map(|n| [0.5 + n.z.atan2(n.x) / std::f32::consts::TAU, 0.5 - n.y.clamp(-1.0, 1.0).asin() / std::f32::consts::PI])
125         .collect();
126     let cds = n
127         .iter()
128         .map(|n| if colored { [0.5 + n.x * 0.5, 0.5 + n.y * 0.5, 0.5 + n.z * 0.5] } else { DEFAULT_COLOR })
129         .collect();
130     let points = d.points_mut();
131     let _ = points.insert("Norm", AttribData::Float3(norms));
132     let _ = points.insert("UV", AttribData::Float2(uvs));
133     let _ = points.insert(CD, AttribData::Float3(cds));
134 }
135 
136 /// The unit icosphere: the icosahedron's 20 faces, each split into
137 /// `freq^2` triangles by integer barycentric weights, every corner pushed
138 /// onto the sphere. Row i of a face holds `freq - i` upright triangles and
139 /// `freq - i - 1` inverted ones.
140 fn icosphere_unit(freq: usize) -> Detail {
141     let v: [Vec3; 12] = [
142         Vec3::new(-1.0, T, 0.0),
143         Vec3::new(1.0, T, 0.0),
144         Vec3::new(-1.0, -T, 0.0),
145         Vec3::new(1.0, -T, 0.0),
146         Vec3::new(0.0, -1.0, T),
147         Vec3::new(0.0, 1.0, T),
148         Vec3::new(0.0, -1.0, -T),
149         Vec3::new(0.0, 1.0, -T),
150         Vec3::new(T, 0.0, -1.0),
151         Vec3::new(T, 0.0, 1.0),
152         Vec3::new(-T, 0.0, -1.0),
153         Vec3::new(-T, 0.0, 1.0),
154     ];
155     let faces: [[usize; 3]; 20] = [
156         [0, 11, 5], [0, 5, 1], [0, 1, 7], [0, 7, 10], [0, 10, 11],
157         [1, 5, 9], [5, 11, 4], [11, 10, 2], [10, 7, 6], [7, 1, 8],
158         [3, 9, 4], [3, 4, 2], [3, 2, 6], [3, 6, 8], [3, 8, 9],
159         [4, 9, 5], [2, 4, 11], [6, 2, 10], [8, 6, 7], [9, 8, 1],
160     ];
161     let f = freq.max(1);
162     let mut w = Welder::new();
163     for [a, b, c] in faces {
164         let corner = |w: &mut Welder, wb: usize, wc: usize| {
165             let wa = f - wb - wc;
166             let p = (v[a] * wa as f32 + v[b] * wb as f32 + v[c] * wc as f32) / f as f32;
167             w.point(p.normalize())
168         };
169         for i in 0..f {
170             for j in 0..(f - i) {
171                 let p0 = corner(&mut w, i, j);
172                 let p1 = corner(&mut w, i + 1, j);
173                 let p2 = corner(&mut w, i, j + 1);
174                 w.outward_prim(&[p0, p1, p2]);
175                 if j + 1 < f - i {
176                     let q0 = corner(&mut w, i + 1, j);
177                     let q1 = corner(&mut w, i + 1, j + 1);
178                     let q2 = corner(&mut w, i, j + 1);
179                     w.outward_prim(&[q0, q1, q2]);
180                 }
181             }
182         }
183     }
184     w.d
185 }
186 
187 /// The unit cube sphere: a `res x res` grid on each face of the cube
188 /// spanning -1..1, every point pushed onto the sphere by the spherified-cube
189 /// map — not a bare normalize, which crowds the corners and stretches the
190 /// face centres. One quad per cell.
191 fn cube_sphere_unit(res: usize) -> Detail {
192     let r = res.max(1);
193     let mut w = Welder::new();
194     for face in 0..6 {
195         let axis = face / 2;
196         let sign = if face % 2 == 0 { 1.0 } else { -1.0 };
197         let at = |w: &mut Welder, i: usize, j: usize| {
198             let u = -1.0 + 2.0 * i as f32 / r as f32;
199             let v = -1.0 + 2.0 * j as f32 / r as f32;
200             let (x, y, z) = match axis {
201                 0 => (sign, u, v),
202                 1 => (v, sign, u),
203                 _ => (u, v, sign),
204             };
205             let (x2, y2, z2) = (x * x, y * y, z * z);
206             w.point(Vec3::new(
207                 x * (1.0 - y2 * 0.5 - z2 * 0.5 + y2 * z2 / 3.0).sqrt(),
208                 y * (1.0 - z2 * 0.5 - x2 * 0.5 + z2 * x2 / 3.0).sqrt(),
209                 z * (1.0 - x2 * 0.5 - y2 * 0.5 + x2 * y2 / 3.0).sqrt(),
210             ))
211         };
212         for i in 0..r {
213             for j in 0..r {
214                 let q = [at(&mut w, i, j), at(&mut w, i + 1, j), at(&mut w, i + 1, j + 1), at(&mut w, i, j + 1)];
215                 w.outward_prim(&q);
216             }
217         }
218     }
219     w.d
220 }
221 
222 /// An axis-aligned box: eight shared corners, six quads wound
223 /// counter-clockwise seen from outside, normals on the VERTICES (three faces
224 /// meet at a corner with three different normals), one colour.
225 pub fn cuboid_detail(center: Vec3, half: Vec3, color: [f32; 3]) -> Detail {
226     let mut d = Detail::new();
227     let (x, y, z) = (half.x, half.y, half.z);
228     let corners = [
229         center + Vec3::new(-x, -y, -z),
230         center + Vec3::new(x, -y, -z),
231         center + Vec3::new(x, y, -z),
232         center + Vec3::new(-x, y, -z),
233         center + Vec3::new(-x, -y, z),
234         center + Vec3::new(x, -y, z),
235         center + Vec3::new(x, y, z),
236         center + Vec3::new(-x, y, z),
237     ];
238     for c in corners {
239         d.add_point(c);
240     }
241     let faces: [([u32; 4], Vec3); 6] = [
242         ([3, 2, 1, 0], Vec3::NEG_Z),
243         ([6, 7, 4, 5], Vec3::Z),
244         ([7, 3, 0, 4], Vec3::NEG_X),
245         ([2, 6, 5, 1], Vec3::X),
246         ([7, 6, 2, 3], Vec3::Y),
247         ([1, 5, 4, 0], Vec3::NEG_Y),
248     ];
249     let mut norms = Vec::with_capacity(24);
250     for (quad, normal) in &faces {
251         d.add_prim(quad);
252         norms.extend(std::iter::repeat(normal.to_array()).take(4));
253     }
254     let _ = d.verts_mut().insert("Norm", AttribData::Float3(norms));
255     let _ = d.verts_mut().insert("UV", AttribData::Float2(vec![[0.0, 0.0]; 24]));
256     let _ = d.points_mut().insert(CD, AttribData::Float3(vec![color; 8]));
257     d
258 }
259 
260 /// The Box node: a cube of Scale about Center — or, with Wireframe on, its
261 /// twelve edges as thin bars and its eight corners as small cubes, which is
262 /// what the kernel drew and what a reference frame wants.
263 pub fn box_node_detail(target: &FsNode) -> Detail {
264     let scale = node_param_f32(target, "Scale", 1.0).max(1e-4);
265     let wireframe = node_param_str(target, "Wireframe", "false") != "false";
266     let center = node_param_vec3(target, "Center", Vec3::new(0.0, 0.55, 0.0));
267     let color = [0.8, 0.2, 0.2];
268     let h = 0.5 * scale;
269     if !wireframe {
270         return cuboid_detail(center, Vec3::splat(h), color);
271     }
272     let (t_line, t_corner) = (0.008 * scale, 0.012 * scale);
273     let signs = [-1.0f32, 1.0];
274     let mut out = Detail::new();
275     for &sx in &signs {
276         for &sy in &signs {
277             for &sz in &signs {
278                 out.merge(&cuboid_detail(center + Vec3::new(sx * h, sy * h, sz * h), Vec3::splat(t_corner), color));
279             }
280         }
281     }
282     for &sa in &signs {
283         for &sb in &signs {
284             out.merge(&cuboid_detail(center + Vec3::new(0.0, sa * h, sb * h), Vec3::new(h, t_line, t_line), color));
285             out.merge(&cuboid_detail(center + Vec3::new(sa * h, 0.0, sb * h), Vec3::new(t_line, h, t_line), color));
286             out.merge(&cuboid_detail(center + Vec3::new(sa * h, sb * h, 0.0), Vec3::new(t_line, t_line, h), color));
287         }
288     }
289     out
290 }
291 
292 /// The Plane node: a Width x Length sheet of Columns x Rows quads in the XZ
293 /// plane about Center, wound counter-clockwise seen from +Y. Colour is the
294 /// kernel's gradient across the sheet, or the default grey with Color off.
295 /// Grid is the same sheet with a float3 Center and no gradient; two nodes
296 /// for history's sake, and this one is the older.
297 pub fn plane_node_detail(target: &FsNode) -> Detail {
298     let width = node_param_f32(target, "Width", 1.0).max(1e-4);
299     let length = node_param_f32(target, "Length", 1.0).max(1e-4);
300     let cols = node_param_f32(target, "Columns", 16.0).round().clamp(1.0, 500.0) as usize;
301     let rows = node_param_f32(target, "Rows", 16.0).round().clamp(1.0, 500.0) as usize;
302     let center = Vec3::new(
303         node_param_f32(target, "Center X", 0.0),
304         node_param_f32(target, "Center Y", 0.0),
305         node_param_f32(target, "Center Z", 0.0),
306     );
307     let colored = node_param_str(target, "Color", "true") != "false";
308 
309     let mut d = Detail::new();
310     let mut cds = Vec::with_capacity((rows + 1) * (cols + 1));
311     let mut uvs = Vec::with_capacity((rows + 1) * (cols + 1));
312     for r in 0..=rows {
313         for c in 0..=cols {
314             let fx = c as f32 / cols as f32;
315             let fz = r as f32 / rows as f32;
316             d.add_point(center + Vec3::new((fx - 0.5) * width, 0.0, (fz - 0.5) * length));
317             uvs.push([fx, fz]);
318             cds.push(if colored { [0.35 + fx * 0.35, 0.45 + fz * 0.35, 0.85] } else { DEFAULT_COLOR });
319         }
320     }
321     let at = |r: usize, c: usize| (r * (cols + 1) + c) as u32;
322     for r in 0..rows {
323         for c in 0..cols {
324             d.add_prim(&[at(r, c), at(r + 1, c), at(r + 1, c + 1), at(r, c + 1)]);
325         }
326     }
327     let n = d.num_points();
328     let points = d.points_mut();
329     let _ = points.insert("Norm", AttribData::Float3(vec![[0.0, 1.0, 0.0]; n]));
330     let _ = points.insert("UV", AttribData::Float2(uvs));
331     let _ = points.insert(CD, AttribData::Float3(cds));
332     d
333 }
334 
335 /// Extrude, as a WHOLE: every point moves along its point normal by
336 /// `distance`, the input's primitives become the top, and one quad wall
337 /// rises from each BOUNDARY edge — an edge one primitive uses. With Keep
338 /// Base the original primitives stay, wound the other way, so a sheet
339 /// becomes a closed slab and a closed surface a two-skinned shell.
340 ///
341 /// The kernel extruded each triangle on its own and welded the pieces back
342 /// together afterwards, which put a wall along every interior edge too; a
343 /// surface extruded that way was a bed of prisms. A point shared by two
344 /// faces has one top here, which is what "extrude the surface" means, and
345 /// what makes the result closed when the input was a closed sheet.
346 ///
347 /// Point attributes and groups carry to the top copies, primitive
348 /// attributes to the top and base copies; walls are fresh. Wall corners
349 /// share points with the top and base, so the kernel's 15% darker walls —
350 /// a per-corner colour a soup could hold — have no place to live and are
351 /// gone.
352 pub fn extrude_detail(d: &Detail, distance: f32, keep_base: bool) -> Detail {
353     let n = d.num_points();
354     if n == 0 || d.num_prims() == 0 {
355         return d.clone();
356     }
357     let normals = point_normals(d);
358     let mut out = Detail::new();
359     let base: Vec<[f32; 3]> = d.positions().to_vec();
360     let top: Vec<[f32; 3]> = (0..n).map(|p| (d.pos(p) + normals[p] * distance).to_array()).collect();
361     out.add_points(&base);
362     out.add_points(&top);
363     // Base points keep their identities; the tops are new.
364     let mut ids = d.ids().to_vec();
365     let next = ids.iter().max().map_or(0, |m| m + 1);
366     ids.extend((0..n as u64).map(|i| next + i));
367     let _ = out.set_ids(ids, next + n as u64);
368 
369     let n32 = n as u32;
370     let mut src: Vec<Option<usize>> = Vec::new();
371     let mut directed: Vec<(u32, u32)> = Vec::new();
372     let mut uses: HashMap<(u32, u32), usize> = HashMap::new();
373     for pr in 0..d.num_prims() {
374         let pts = d.prim_points(pr);
375         if pts.len() < 3 {
376             continue;
377         }
378         let lifted: Vec<u32> = pts.iter().map(|&p| p + n32).collect();
379         out.add_prim(&lifted);
380         src.push(Some(pr));
381         for k in 0..pts.len() {
382             let (a, b) = (pts[k], pts[(k + 1) % pts.len()]);
383             let key = (a.min(b), a.max(b));
384             let count = uses.entry(key).or_insert(0);
385             if *count == 0 {
386                 directed.push((a, b));
387             }
388             *count += 1;
389         }
390     }
391     // Walls on the boundary, wound so that for a counter-clockwise top and
392     // a positive distance the outside faces out: `(b - a) x N` is the
393     // outward direction of edge a→b on a counter-clockwise face.
394     for (a, b) in directed {
395         if uses[&(a.min(b), a.max(b))] == 1 {
396             out.add_prim(&[a, b, b + n32, a + n32]);
397             src.push(None);
398         }
399     }
400     if keep_base {
401         for pr in 0..d.num_prims() {
402             let pts = d.prim_points(pr);
403             if pts.len() < 3 {
404                 continue;
405             }
406             let rev: Vec<u32> = pts.iter().rev().copied().collect();
407             out.add_prim(&rev);
408             src.push(Some(pr));
409         }
410     }
411 
412     // Attributes: points doubled, primitives by source.
413     for name in d.points().names() {
414         let data = d.points().get(name).unwrap();
415         let mut nd = AttribData::zeroed(data.ty(), 2 * n);
416         for p in 0..n {
417             if let Some(v) = data.get(p) {
418                 let _ = nd.set(p, v);
419                 let _ = nd.set(p + n, v);
420             }
421         }
422         let kind = d.points().kind(name);
423         let _ = out.points_mut().insert(name, nd);
424         out.points_mut().set_kind(name, kind);
425     }
426     for g in d.points().group_names() {
427         out.points_mut().create_group(g);
428         for m in d.points().group_members(g) {
429             out.points_mut().add_to_group(g, m as usize);
430             out.points_mut().add_to_group(g, m as usize + n);
431         }
432     }
433     let np = out.num_prims();
434     for name in d.prims().names() {
435         let data = d.prims().get(name).unwrap();
436         let mut nd = AttribData::zeroed(data.ty(), np);
437         for (i, s) in src.iter().enumerate() {
438             if let Some(v) = s.and_then(|pr| data.get(pr)) {
439                 let _ = nd.set(i, v);
440             }
441         }
442         let kind = d.prims().kind(name);
443         let _ = out.prims_mut().insert(name, nd);
444         out.prims_mut().set_kind(name, kind);
445     }
446     for g in d.prims().group_names() {
447         out.prims_mut().create_group(g);
448         for (i, s) in src.iter().enumerate() {
449             if s.is_some_and(|pr| d.prims().in_group(g, pr)) {
450                 out.prims_mut().add_to_group(g, i);
451             }
452         }
453     }
454     for name in d.detail().names() {
455         let _ = out.detail_mut().insert(name, d.detail().get(name).unwrap().clone());
456     }
457     out
458 }