Skip to main content

geop_ops_subd/
subdivide.rs

1//! One Catmull–Clark step, with sharp creases: what every face of the
2//! cage becomes before its limit surface is built (see [`crate::limit`]).
3//!
4//! A face of `n` sides becomes `n` quads around its *face point*, each
5//! running from one of its corners — that corner's *vertex point* — to the
6//! *edge points* on the two sides there. So after one step every face is a
7//! quad, whatever the cage was made of, and the limit surface is the same
8//! as the cage's.
9//!
10//! The rules, with the mesh's boundary counted as creased:
11//!
12//! - face point: the average of the face's corners;
13//! - edge point: on a creased edge its midpoint, else the average of its
14//!   two ends and its two faces' face points;
15//! - vertex point, by how many creased edges meet at the vertex: none or
16//!   one (a *dart*) — `(ΣF + ΣE + n(n - 2) V) / n²` over its `n` faces' face
17//!   points `F` and the far ends `E` of its edges; two — a *crease* —
18//!   `(A + 6 V + B) / 8` with `A`, `B` the creased edges' far ends; three or
19//!   more, or a boundary vertex of one face — a *corner* — stays where it
20//!   is.
21//!
22//! A *soft* crease, of sharpness `s` below one (see [`crate::cage::Crease`]),
23//! is semi-sharp: its edge point is `(1 - s)` of the smooth rule's and `s`
24//! of the creased one's, and a vertex with soft creases is moved
25//! `(1 - s̄)` of the way by the rule counting its sharp creases alone and `s̄`
26//! by the rule counting the soft ones too, `s̄` the mean sharpness of its
27//! creases. Its halves are smooth after that: the limit surface rounds
28//! off tighter there, without a corner.
29
30use std::collections::{BTreeMap, BTreeSet};
31
32use geop_core_math::{geop_error::GeopResult, scalars::Scalar, vector::Vector3};
33
34use crate::cage::{Mesh, Topology, next, prev, undirected};
35
36/// How the surface behaves at a vertex.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum VertexKind {
39    /// Smooth all round: no creased edge, or one.
40    Smooth,
41    /// Along a crease: exactly two creased edges meet, to these vertices.
42    Crease(usize, usize),
43    /// A sharp corner: three creased edges or more, or a boundary vertex
44    /// of a single face.
45    Corner,
46}
47
48/// Whether the edge between `a` and `b` is sharp: creased, or on the
49/// mesh's boundary.
50pub fn is_sharp(sharp: &BTreeSet<[usize; 2]>, topology: &Topology, a: usize, b: usize) -> bool {
51    sharp.contains(&undirected(a, b)) || topology.is_boundary(a, b)
52}
53
54/// What kind of vertex `v` is (see [`VertexKind`]).
55pub fn vertex_kind(
56    faces: &[Vec<usize>],
57    sharp: &BTreeSet<[usize; 2]>,
58    topology: &Topology,
59    v: usize,
60) -> VertexKind {
61    let (fan, closed) = &topology.fans[v];
62    let mut neighbours: Vec<usize> = fan.iter().map(|&c| next(faces, c)).collect();
63    if !closed {
64        neighbours.push(prev(faces, fan[fan.len() - 1]));
65    }
66    let sharp_ends: Vec<usize> = neighbours
67        .into_iter()
68        .filter(|&w| is_sharp(sharp, topology, v, w))
69        .collect();
70    match sharp_ends[..] {
71        _ if !closed && fan.len() == 1 => VertexKind::Corner,
72        [a, b] => VertexKind::Crease(a, b),
73        [] | [_] => VertexKind::Smooth,
74        _ => VertexKind::Corner,
75    }
76}
77
78/// `sum / count`, one division per coordinate.
79pub fn divide(sum: Vector3, count: i64) -> GeopResult<Vector3> {
80    let d = Scalar::from_i64(count);
81    Ok(Vector3::from_array([
82        sum[0].div(d)?,
83        sum[1].div(d)?,
84        sum[2].div(d)?,
85    ]))
86}
87
88/// `(1 - s) a + s b`: `a` pulled the fraction `s` of the way to `b`.
89fn blend(a: Vector3, b: Vector3, s: f64) -> Vector3 {
90    let s = Scalar::from_f64(s);
91    a.add(&b.sub(&a).prod_scalar(s))
92}
93
94/// `Σ wᵢ pᵢ` for integer weights.
95pub fn weighted(terms: &[(i64, Vector3)]) -> Vector3 {
96    terms.iter().fold(Vector3::zero(), |sum, (w, p)| {
97        if *w == 1 {
98            sum.add(p)
99        } else {
100            sum.add(&p.prod_scalar(Scalar::from_i64(*w)))
101        }
102    })
103}
104
105/// The mesh after one step: quads only, with where each of its vertices
106/// and faces came from.
107#[derive(Clone, Debug)]
108pub struct Subdivided {
109    pub positions: Vec<Vector3>,
110    /// Face `face_start[f] + k` is the quad at corner `k` of the cage's
111    /// face `f`: its vertex point, the edge point after it, the face point,
112    /// the edge point before it.
113    pub faces: Vec<Vec<usize>>,
114    pub face_start: Vec<usize>,
115    /// The cage's vertex `v` is vertex `v` here too; its face `f`'s face
116    /// point is `face_point[f]`, and its edge `(a, b)`'s edge point
117    /// `edge_point[&[a, b]]`, the smaller index first.
118    pub face_point: Vec<usize>,
119    pub edge_point: BTreeMap<[usize; 2], usize>,
120    /// The halves of every creased edge.
121    pub sharp: BTreeSet<[usize; 2]>,
122    pub topology: Topology,
123}
124
125/// `mesh`, of topology `topology`, after one Catmull–Clark step.
126pub fn subdivide(mesh: &Mesh, topology: &Topology) -> GeopResult<Subdivided> {
127    let p: Vec<Vector3> = mesh
128        .positions
129        .iter()
130        .map(|a| Vector3::from_array(a.map(Scalar::from_f64)))
131        .collect();
132    let faces = &mesh.faces;
133    let face_points = faces
134        .iter()
135        .map(|face| {
136            divide(
137                weighted(&face.iter().map(|&w| (1, p[w])).collect::<Vec<_>>()),
138                face.len() as i64,
139            )
140        })
141        .collect::<GeopResult<Vec<_>>>()?;
142    // The vertex point of `v` with the edges `sharp` counted as creased.
143    let vertex_point = |v: usize, sharp: &BTreeSet<[usize; 2]>| -> GeopResult<Vector3> {
144        Ok(match vertex_kind(faces, sharp, topology, v) {
145            VertexKind::Corner => p[v],
146            VertexKind::Crease(a, b) => divide(weighted(&[(1, p[a]), (6, p[v]), (1, p[b])]), 8)?,
147            VertexKind::Smooth => {
148                let fan = &topology.fans[v].0;
149                let n = fan.len() as i64;
150                let mut terms = vec![(n * (n - 2), p[v])];
151                for &c in fan {
152                    terms.push((1, face_points[c.0]));
153                    terms.push((1, p[next(faces, c)]));
154                }
155                divide(weighted(&terms), n * n)?
156            }
157        })
158    };
159    // Every crease, soft ones too, for the rule a soft vertex is pulled
160    // towards.
161    let all_creased: BTreeSet<[usize; 2]> =
162        mesh.sharp.iter().chain(mesh.soft.keys()).copied().collect();
163    let mut positions = Vec::with_capacity(p.len() + 2 * faces.len());
164    for v in 0..p.len() {
165        let point = vertex_point(v, &mesh.sharp)?;
166        // The creases at `v`, by sharpness: the mean decides how far it is
167        // pulled towards the rule counting them all.
168        let creases: Vec<f64> = all_creased
169            .iter()
170            .filter(|&&[a, b]| a == v || b == v)
171            .map(|e| mesh.soft.get(e).copied().unwrap_or(1.0))
172            .collect();
173        let soft = creases.iter().any(|&s| s < 1.0);
174        let point = if soft {
175            let mean = creases.iter().sum::<f64>() / creases.len() as f64;
176            blend(point, vertex_point(v, &all_creased)?, mean)
177        } else {
178            point
179        };
180        positions.push(point);
181    }
182    let face_point: Vec<usize> = (0..faces.len()).map(|f| positions.len() + f).collect();
183    positions.extend(face_points.iter().copied());
184    let mut edge_point = BTreeMap::new();
185    for ([a, b], adjacent) in topology.edges() {
186        let middle = || divide(p[a].add(&p[b]), 2);
187        let point = if is_sharp(&mesh.sharp, topology, a, b) {
188            middle()?
189        } else {
190            let terms = [
191                (1, p[a]),
192                (1, p[b]),
193                (1, face_points[adjacent[0]]),
194                (1, face_points[adjacent[1]]),
195            ];
196            let smooth = divide(weighted(&terms), 4)?;
197            match mesh.soft.get(&[a, b]) {
198                Some(&s) => blend(smooth, middle()?, s),
199                None => smooth,
200            }
201        };
202        edge_point.insert([a, b], positions.len());
203        positions.push(point);
204    }
205    let edge = |a: usize, b: usize| edge_point[&undirected(a, b)];
206    let mut quads = Vec::new();
207    let mut face_start = Vec::with_capacity(faces.len());
208    for (f, face) in faces.iter().enumerate() {
209        face_start.push(quads.len());
210        let n = face.len();
211        for k in 0..n {
212            let (v, after, before) = (face[k], face[(k + 1) % n], face[(k + n - 1) % n]);
213            quads.push(vec![v, edge(v, after), face_point[f], edge(before, v)]);
214        }
215    }
216    let mut sharp = BTreeSet::new();
217    for &[a, b] in &mesh.sharp {
218        let m = edge(a, b);
219        sharp.insert(undirected(a, m));
220        sharp.insert(undirected(b, m));
221    }
222    let names: Vec<String> = (0..positions.len())
223        .map(|i| format!("subdivided vertex {i}"))
224        .collect();
225    let face_names: Vec<String> = (0..quads.len())
226        .map(|i| format!("subdivided face {i}"))
227        .collect();
228    let topology = Topology::new(positions.len(), &quads, &names, &face_names)?;
229    Ok(Subdivided {
230        positions,
231        faces: quads,
232        face_start,
233        face_point,
234        edge_point,
235        sharp,
236        topology,
237    })
238}