Skip to main content

subdiv_kernels/
patch.rs

1//! Bicubic B-spline patches over regular Catmull-Clark quads.
2//!
3//! After at least one uniform refinement every Catmull-Clark face is a
4//! quad. [`RefinementResult::patch_table`] classifies each refined quad
5//! ([`QuadClass`]) and extracts one uniform bicubic B-spline patch per
6//! *regular* quad: all four corner vertices interior, valence 4, with
7//! no vertex sharpness and no sharp incident edge at the refined level
8//! (sharp per the rule selection of the [`LimitStencils`] docs: stored
9//! crease value `> 0.0` or boundary). Everything else --
10//! extraordinary vertices, creases, boundaries -- is
11//! [`QuadClass::Feature`] and left to the feature-patch machinery
12//! (limit-surface SDF design s2).
13//!
14//! # Exactness contract
15//!
16//! Over a regular quad the patch evaluates the *exact* limit surface:
17//! its 16 control points are the quad's 4x4 refined-vertex neighborhood
18//! (the corners plus their one-rings), and under the regularity
19//! conditions every subdivision rule that influences the quad's nested
20//! neighborhoods -- the corner vertex rules, the edge rules of the
21//! corners' incident edges, and the (sharpness-free) face rules -- is
22//! the regular B-spline rule, so Catmull-Clark refinement under the
23//! quad coincides with B-spline knot insertion and converges to the
24//! B-spline surface. Ring vertices may themselves be boundary, crease,
25//! or extraordinary vertices; only their *positions* enter the patch.
26//!
27//! # Parameterization and control-point layout
28//!
29//! A patch covers its quad's `[0, 1]^2`: CSR corner `k` of the quad
30//! (the order of [`Mesh::face_vertex_indices`]) sits at
31//! `(u, v) = (0, 0), (1, 0), (1, 1), (0, 1)` for `k = 0, 1, 2, 3` --
32//! `u` runs along the corner-0 -> corner-1 edge and `v` along
33//! corner-0 -> corner-3. Control points are row-major in `v` then `u`:
34//! entry `4 * j + i` sits at grid position `(i, j)` with `i` along `u`
35//! and `j` along `v`, the quad's own corners occupying the interior
36//! positions `(1, 1)`, `(2, 1)`, `(2, 2)`, `(1, 2)`.
37//!
38//! Derivatives from [`PatchTable::eval_with_derivatives`] are with
39//! respect to this in-quad parameterization, so against a parent
40//! (ptex-style) unit parameterization of the root face they carry an
41//! extra factor of `2^level`.
42//!
43//! Control points are *indices* into the refined vertex order of
44//! [`RefinementResult::topology`]; evaluation gathers from whatever
45//! positions buffer the caller supplies (CPU-interpolated or read back
46//! from the GPU stencil path), so a patch table is built once per
47//! topology and re-evaluated across edits.
48//!
49//! [`LimitStencils`]: crate::LimitStencils
50
51use crate::limit::{validate_refined_quads, vertex_ring};
52use crate::{Adjacency, KernelError, Mesh, RefinementResult};
53
54/// Classification of one refined quad at the evaluated level.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
56pub enum QuadClass {
57    /// All four corners interior, valence-4, sharpness-free: covered
58    /// exactly by a bicubic B-spline patch.
59    Regular,
60    /// Touches an extraordinary vertex, crease, corner, or boundary;
61    /// no patch in the table (feature evaluation is s2's job).
62    Feature,
63}
64
65/// Bicubic B-spline patches over the regular quads of a refined level.
66///
67/// Built by [`RefinementResult::patch_table`]; see the module docs for
68/// the exactness contract and the control-point layout.
69#[derive(Debug, Clone, PartialEq)]
70pub struct PatchTable {
71    /// 16 refined-vertex indices per patch, row-major (v then u), one
72    /// entry per regular refined quad; feature quads get no patch here.
73    pub control_points: Vec<[u32; 16]>,
74    /// The refined face each patch covers (index into face CSR).
75    pub faces: Vec<u32>,
76    /// Per refined face: its patch index, `u32::MAX` for feature quads.
77    face_patches: Vec<u32>,
78}
79
80impl RefinementResult {
81    /// Classify every refined quad and extract the bicubic B-spline
82    /// patches of the regular ones.
83    ///
84    /// Catmull-Clark only, and the result must come from at least one
85    /// full (unselected) refinement -- the same gating as
86    /// [`limit_stencils`](Self::limit_stencils), except that open
87    /// meshes are accepted under every boundary rule (boundary quads
88    /// are always [`QuadClass::Feature`]).
89    pub fn patch_table(&self) -> Result<PatchTable, KernelError> {
90        build_patch_table(self)
91    }
92}
93
94impl PatchTable {
95    /// Number of patches (regular quads).
96    pub fn len(&self) -> usize {
97        self.control_points.len()
98    }
99
100    /// Whether the refined level has no regular quad at all.
101    pub fn is_empty(&self) -> bool {
102        self.control_points.is_empty()
103    }
104
105    /// Classification of refined face `face`.
106    pub fn quad_class(&self, face: u32) -> QuadClass {
107        if self.face_patches[face as usize] == u32::MAX {
108            QuadClass::Feature
109        } else {
110            QuadClass::Regular
111        }
112    }
113
114    /// The patch covering refined face `face`
115    /// (`faces[patch] == face`), `None` for feature quads.
116    pub fn face_patch(&self, face: u32) -> Option<u32> {
117        let patch = self.face_patches[face as usize];
118        (patch != u32::MAX).then_some(patch)
119    }
120
121    /// Limit position of patch `patch` at in-quad `(u, v)`.
122    ///
123    /// `control_positions` is indexed by the patch's control-point
124    /// entries, i.e. one position per refined vertex. The basis is
125    /// accumulated in f64 and rounded once on return.
126    pub fn eval(&self, patch: usize, uv: [f32; 2], control_positions: &[[f32; 3]]) -> [f32; 3] {
127        let wu = basis(uv[0] as f64);
128        let wv = basis(uv[1] as f64);
129        tensor(&self.control_points[patch], &wu, &wv, control_positions)
130    }
131
132    /// Limit position and first derivatives `(p, dp/du, dp/dv)` of
133    /// patch `patch` at in-quad `(u, v)`.
134    ///
135    /// Derivatives are with respect to the quad's own `[0, 1]^2`
136    /// parameterization (see the module docs for the scale);
137    /// `dp/du x dp/dv` is the winding-oriented surface normal.
138    pub fn eval_with_derivatives(
139        &self,
140        patch: usize,
141        uv: [f32; 2],
142        control_positions: &[[f32; 3]],
143    ) -> ([f32; 3], [f32; 3], [f32; 3]) {
144        let points = &self.control_points[patch];
145        let (u, v) = (uv[0] as f64, uv[1] as f64);
146        let (wu, wv) = (basis(u), basis(v));
147        let (du, dv) = (derivative(u), derivative(v));
148        (
149            tensor(points, &wu, &wv, control_positions),
150            tensor(points, &du, &wv, control_positions),
151            tensor(points, &wu, &dv, control_positions),
152        )
153    }
154
155    /// The 16 `(control-point index, basis weight)` pairs behind
156    /// [`eval`](Self::eval) at in-quad `(u, v)` -- the sparse position
157    /// row of patch `patch`, f64 weights for downstream stencil
158    /// composition (`LimitEvaluator::weights_at`).
159    pub(crate) fn position_weights(&self, patch: usize, uv: [f32; 2]) -> [(u32, f64); 16] {
160        let points = &self.control_points[patch];
161        let wu = basis(uv[0] as f64);
162        let wv = basis(uv[1] as f64);
163        core::array::from_fn(|slot| (points[slot], wu[slot % 4] * wv[slot / 4]))
164    }
165}
166
167/// Uniform cubic B-spline basis on the patch's knot interval.
168fn basis(t: f64) -> [f64; 4] {
169    let s = 1.0 - t;
170    [
171        s * s * s / 6.0,
172        (3.0 * t * t * t - 6.0 * t * t + 4.0) / 6.0,
173        (-3.0 * t * t * t + 3.0 * t * t + 3.0 * t + 1.0) / 6.0,
174        t * t * t / 6.0,
175    ]
176}
177
178/// First derivative of [`basis`].
179fn derivative(t: f64) -> [f64; 4] {
180    let s = 1.0 - t;
181    [
182        -0.5 * s * s,
183        1.5 * t * t - 2.0 * t,
184        -1.5 * t * t + t + 0.5,
185        0.5 * t * t,
186    ]
187}
188
189/// Tensor-product accumulation of one patch over a positions buffer.
190fn tensor(
191    points: &[u32; 16],
192    wu: &[f64; 4],
193    wv: &[f64; 4],
194    control_positions: &[[f32; 3]],
195) -> [f32; 3] {
196    let mut acc = [0.0f64; 3];
197    for (j, &row_weight) in wv.iter().enumerate() {
198        for (i, &column_weight) in wu.iter().enumerate() {
199            let p = control_positions[points[4 * j + i] as usize];
200            let w = column_weight * row_weight;
201            acc[0] += w * p[0] as f64;
202            acc[1] += w * p[1] as f64;
203            acc[2] += w * p[2] as f64;
204        }
205    }
206    [acc[0] as f32, acc[1] as f32, acc[2] as f32]
207}
208
209/// Control-grid slot `(i, j)` (`i` along `u`, `j` along `v`) -> index.
210const fn grid(i: usize, j: usize) -> usize {
211    4 * j + i
212}
213
214/// Grid slots filled from each quad corner's rotated ring (slot 0 = the
215/// corner's out-edge within the quad): ring neighbors 2 and 3 and ring
216/// diagonals 1, 2, 3, in that order. Neighbors 0/1 and diagonal 0 are
217/// quad corners and already placed; the overlap between consecutive
218/// corners' rings is debug-asserted consistent.
219const RING_GRID: [[usize; 5]; 4] = [
220    [grid(0, 1), grid(1, 0), grid(0, 2), grid(0, 0), grid(2, 0)],
221    [grid(2, 0), grid(3, 1), grid(1, 0), grid(3, 0), grid(3, 2)],
222    [grid(3, 2), grid(2, 3), grid(3, 1), grid(3, 3), grid(1, 3)],
223    [grid(1, 3), grid(0, 2), grid(2, 3), grid(0, 3), grid(0, 1)],
224];
225
226fn build_patch_table(result: &RefinementResult) -> Result<PatchTable, KernelError> {
227    validate_refined_quads(result)?;
228    let mesh = &result.topology;
229    let adjacency = &result.adjacency;
230
231    // Per-vertex regularity: interior, valence 4, no vertex sharpness,
232    // and no sharp incident edge at the refined level (the sharpness
233    // convention of the limit module docs; boundary edges are sharp).
234    let regular_corner: Vec<bool> = (0..mesh.vertex_count as usize)
235        .map(|vi| {
236            let start = adjacency.vertex_edge_offsets[vi] as usize;
237            let end = adjacency.vertex_edge_offsets[vi + 1] as usize;
238            !adjacency.vertex_is_boundary[vi]
239                && end - start == 4
240                && mesh.vertex_corners[vi] <= 0.0
241                && adjacency.vertex_edges[start..end].iter().all(|&ei| {
242                    mesh.edge_creases[ei as usize] <= 0.0
243                        && !adjacency.edge_is_boundary[ei as usize]
244                })
245        })
246        .collect();
247
248    let face_count = mesh.face_vertex_counts.len();
249    let mut control_points = Vec::new();
250    let mut faces = Vec::new();
251    let mut face_patches = vec![u32::MAX; face_count];
252    for (face, corners) in mesh.face_vertex_indices.chunks_exact(4).enumerate() {
253        if corners.iter().all(|&c| regular_corner[c as usize]) {
254            face_patches[face] = control_points.len() as u32;
255            control_points.push(extract_control_points(
256                face as u32,
257                corners,
258                mesh,
259                adjacency,
260            )?);
261            faces.push(face as u32);
262        }
263    }
264
265    Ok(PatchTable {
266        control_points,
267        faces,
268        face_patches,
269    })
270}
271
272/// The 4x4 control-point neighborhood of one regular quad, from its
273/// corners' one-rings (see [`RING_GRID`] for the slot correspondence).
274fn extract_control_points(
275    face: u32,
276    corners: &[u32],
277    mesh: &Mesh,
278    adjacency: &Adjacency,
279) -> Result<[u32; 16], KernelError> {
280    let mut points = [u32::MAX; 16];
281    let mut place = |slot: usize, vertex: u32| {
282        debug_assert!(
283            points[slot] == u32::MAX || points[slot] == vertex,
284            "control-point slot {slot} of face {face} disagrees between corner rings",
285        );
286        points[slot] = vertex;
287    };
288    place(grid(1, 1), corners[0]);
289    place(grid(2, 1), corners[1]);
290    place(grid(2, 2), corners[2]);
291    place(grid(1, 2), corners[3]);
292
293    for (k, &corner) in corners.iter().enumerate() {
294        // Regular corners are interior valence-4 vertices, so the ring
295        // is a 4-slot cycle and rotation is well defined.
296        let ring = vertex_ring(corner as usize, mesh, adjacency)?;
297        let slot =
298            ring.faces
299                .iter()
300                .position(|&f| f == face)
301                .ok_or(KernelError::InvalidTopology(
302                    "quad corner ring does not contain the quad",
303                ))?;
304        let ring = ring.rotated(slot);
305        debug_assert_eq!(
306            ring.neighbors[0],
307            corners[(k + 1) % 4],
308            "rotated ring of corner {k} does not lead with the quad's out-edge",
309        );
310        debug_assert_eq!(
311            ring.diagonals[0],
312            corners[(k + 2) % 4],
313            "rotated ring of corner {k} does not see the quad's diagonal",
314        );
315        debug_assert_eq!(
316            ring.neighbors[1],
317            corners[(k + 3) % 4],
318            "rotated ring of corner {k} does not trail into the quad's in-edge",
319        );
320        let [n2, n3, d1, d2, d3] = RING_GRID[k];
321        place(n2, ring.neighbors[2]);
322        place(n3, ring.neighbors[3]);
323        place(d1, ring.diagonals[1]);
324        place(d2, ring.diagonals[2]);
325        place(d3, ring.diagonals[3]);
326    }
327    debug_assert!(
328        points.iter().all(|&p| p != u32::MAX),
329        "regular quad {face} did not fill its 4x4 neighborhood",
330    );
331    Ok(points)
332}
333
334#[cfg(test)]
335mod tests {
336    use core::num::NonZeroU8;
337
338    use crate::{KernelError, Mesh, Refiner, Scheme, SchemeOptions, UniformRefine};
339
340    /// A 2x2 quad grid (the geometry gates live in
341    /// `tests/patch_table.rs` and `tests/patch_osd_oracle.rs`; these
342    /// unit tests cover the error paths only).
343    fn grid() -> Mesh {
344        Mesh {
345            vertex_count: 9,
346            face_vertex_counts: vec![4; 4],
347            face_vertex_indices: vec![0, 3, 4, 1, 1, 4, 5, 2, 3, 6, 7, 4, 4, 7, 8, 5],
348            edge_vertices: Vec::new(),
349            edge_creases: Vec::new(),
350            vertex_corners: vec![0.0; 9],
351        }
352    }
353
354    #[test]
355    fn non_catmull_clark_scheme_is_rejected() {
356        let refiner =
357            Refiner::new(grid(), Scheme::DooSabin, SchemeOptions::default()).expect("refiner");
358        let result = refiner
359            .refine_uniform(&UniformRefine::default())
360            .expect("refinement");
361        assert!(matches!(
362            result.patch_table(),
363            Err(KernelError::NotImplemented(_)),
364        ));
365    }
366
367    #[test]
368    fn partial_face_selection_is_rejected() {
369        let req = UniformRefine {
370            // SAFETY: 1 is non-zero.
371            levels: NonZeroU8::new(1).unwrap(),
372            selected_faces: Some(vec![true, true, true, false]),
373            ..Default::default()
374        };
375        let refiner =
376            Refiner::new(grid(), Scheme::CatmullClark, SchemeOptions::default()).expect("refiner");
377        let result = refiner.refine_uniform(&req).expect("refinement");
378        assert!(matches!(
379            result.patch_table(),
380            Err(KernelError::NotImplemented(_)),
381        ));
382    }
383}