Skip to main content

primase_core/
morphs.rs

1//! A mesh's morph targets (`core.morphs`, blend shapes): per-vertex deltas of its primitives, on the entity holding the
2//! mesh, so every edit of the geometry carries them.
3//!
4//! A target's identity is its [`MorphId`], scoped to the asset's morph set: the prototype and each of its level-of-detail
5//! forms carry their own `core.morphs` for their own vertices, under the same ids, so a clip or an occurrence's weights
6//! name a target whichever form is drawn. Its name is the source's, provenance only.
7//!
8//! Meaning (core's evaluation rule; a source whose engine evaluates otherwise keeps its data in its namespace): a
9//! target at weight `w` adds to each vertex its delta at `w`. With one frame at weight 1 (glTF's targets) that is
10//! `w` times the frame's deltas. With several frames (in-between shapes) the delta is piecewise linear in `w` through
11//! `(0, 0)` and each frame's `(weight, deltas)` in increasing weight, extrapolated along the last segment above the
12//! last frame and along the first below 0. Targets add; they apply to the mesh as stored, before any skin. Normals
13//! and tangents with deltas are renormalised after the deltas are added.
14
15use std::collections::BTreeSet;
16
17use serde::Serialize;
18use serde_json::Value;
19
20use crate::{Component, Entity, JsonError, Mesh, World};
21
22/// A morph target's identity within its asset's morph set.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
24#[serde(transparent)]
25pub struct MorphId(pub u32);
26
27/// One primitive's deltas in one frame: dense (one per vertex, `indices` `None`) or sparse (one per listed vertex,
28/// `indices` ascending and unique). Each attribute is optional; a present one has one delta per entry.
29#[derive(Clone, Debug, Default, PartialEq, Serialize)]
30pub struct Deltas {
31    pub indices: Option<Vec<u32>>,
32    pub positions: Option<Vec<[f32; 3]>>,
33    pub normals: Option<Vec<[f32; 3]>>,
34    pub tangents: Option<Vec<[f32; 3]>>,
35}
36
37/// One frame of a target: its weight and, per primitive of the mesh, its deltas (`None`: none for that primitive).
38#[derive(Clone, Debug, PartialEq, Serialize)]
39pub struct Frame {
40    pub weight: f32,
41    pub primitives: Vec<Option<Deltas>>,
42}
43
44/// One morph target.
45#[derive(Clone, Debug, PartialEq, Serialize)]
46pub struct Target {
47    pub id: MorphId,
48    /// The source's name: provenance, not identity.
49    pub name: String,
50    /// The weight the source gives it when nothing else sets one.
51    pub default_weight: f32,
52    /// In increasing weight; at least one.
53    pub frames: Vec<Frame>,
54}
55
56/// `core.morphs` (see the module documentation).
57#[derive(Clone, Debug, Default, PartialEq, Serialize)]
58pub struct Morphs {
59    pub targets: Vec<Target>,
60}
61
62impl Deltas {
63    fn entries(&self) -> Option<usize> {
64        [&self.positions, &self.normals, &self.tangents]
65            .into_iter()
66            .flatten()
67            .map(Vec::len)
68            .next()
69    }
70
71    fn attributes_mut(&mut self) -> impl Iterator<Item = &mut Vec<[f32; 3]>> {
72        [&mut self.positions, &mut self.normals, &mut self.tangents]
73            .into_iter()
74            .flatten()
75    }
76
77    /// The deltas for a primitive whose vertices were compacted or reordered: new vertex `i` is old vertex `from[i]`.
78    /// Dense deltas are gathered; sparse ones keep each entry for every new vertex made from its old one.
79    fn remapped(&self, from: &[u32]) -> Result<Deltas, String> {
80        let mut out = self.clone();
81        match &self.indices {
82            None => {
83                for (dst, src) in out.attributes_mut().zip(
84                    [&self.positions, &self.normals, &self.tangents]
85                        .into_iter()
86                        .flatten(),
87                ) {
88                    *dst = from
89                        .iter()
90                        .map(|&i| {
91                            src.get(i as usize)
92                                .copied()
93                                .ok_or_else(|| format!("vertex {i} of {}", src.len()))
94                        })
95                        .collect::<Result<_, _>>()?;
96                }
97            }
98            Some(indices) => {
99                // for each new vertex, the entry of its old one, if it has one
100                let picked: Vec<(u32, usize)> = (0u32..)
101                    .zip(from)
102                    .filter_map(|(new, old)| indices.binary_search(old).ok().map(|k| (new, k)))
103                    .collect();
104                out.indices = Some(picked.iter().map(|&(new, _)| new).collect());
105                for (dst, src) in out.attributes_mut().zip(
106                    [&self.positions, &self.normals, &self.tangents]
107                        .into_iter()
108                        .flatten(),
109                ) {
110                    *dst = picked.iter().map(|&(_, k)| src[k]).collect();
111                }
112            }
113        }
114        Ok(out)
115    }
116}
117
118impl Deltas {
119    /// The deltas for a primitive whose vertices were rebuilt as blends of the old ones: a new vertex's delta is the
120    /// blend of its old vertices' deltas, so the morphed surface stays the surface the blended vertex lies on. A sparse
121    /// vertex without an entry has delta 0; a new vertex is listed when any old vertex it blends is.
122    fn blended(&self, from: &[crate::VertexBlend]) -> Result<Deltas, String> {
123        let mut out = self.clone();
124        /// A new vertex and, for each old vertex it blends, its entry in the deltas (if it has one) and weight.
125        type Picked = (u32, Vec<(Option<usize>, f32)>);
126        let mut picked: Vec<Picked> = Vec::new();
127        for (new, b) in (0u32..).zip(from) {
128            let terms: Vec<(Option<usize>, f32)> = b
129                .terms()
130                .map(|(old, w)| match &self.indices {
131                    None => (Some(old as usize), w),
132                    Some(indices) => (indices.binary_search(&old).ok(), w),
133                })
134                .collect();
135            if self.indices.is_none() || terms.iter().any(|(k, _)| k.is_some()) {
136                picked.push((new, terms));
137            }
138        }
139        out.indices = self
140            .indices
141            .as_ref()
142            .map(|_| picked.iter().map(|&(new, _)| new).collect());
143        for (dst, src) in out.attributes_mut().zip(
144            [&self.positions, &self.normals, &self.tangents]
145                .into_iter()
146                .flatten(),
147        ) {
148            *dst = picked
149                .iter()
150                .map(|(_, terms)| {
151                    let mut sum = [0.0f64; 3];
152                    for &(k, w) in terms {
153                        let Some(k) = k else { continue };
154                        let d = src.get(k).ok_or_else(|| format!("vertex {k} of {}", src.len()))?;
155                        for (s, x) in sum.iter_mut().zip(d) {
156                            *s += f64::from(w) * f64::from(*x);
157                        }
158                    }
159                    Ok(sum.map(|x| x as f32))
160                })
161                .collect::<Result<_, String>>()?;
162        }
163        Ok(out)
164    }
165}
166
167impl Morphs {
168    /// Target `id`.
169    pub fn target(&self, id: MorphId) -> Option<&Target> {
170        self.targets.iter().find(|t| t.id == id)
171    }
172}
173
174impl Component for Morphs {
175    const NAME: &'static str = "core.morphs";
176    const GEOMETRY: crate::Geometry = crate::Geometry::PerVertex;
177    /// Ids, names and weights, not the deltas.
178    fn to_json(&self) -> Result<Option<Value>, JsonError> {
179        let mut targets = Vec::new();
180        for t in &self.targets {
181            let frames: Vec<f32> = t.frames.iter().map(|f| f.weight).collect();
182            targets.push(serde_json::json!({
183                "id": t.id,
184                "name": t.name,
185                "default_weight": crate::json::to_value(&t.default_weight)?,
186                "frames": crate::json::to_value(&frames)?,
187            }));
188        }
189        Ok(Some(serde_json::json!({ "targets": targets })))
190    }
191    fn payload_bytes(&self) -> u64 {
192        let deltas = self
193            .targets
194            .iter()
195            .flat_map(|t| &t.frames)
196            .flat_map(|f| f.primitives.iter().flatten());
197        deltas
198            .map(|d| {
199                let values: usize = [&d.positions, &d.normals, &d.tangents]
200                    .into_iter()
201                    .flatten()
202                    .map(Vec::len)
203                    .sum();
204                values as u64 * 12 + d.indices.as_ref().map_or(0, |i| i.len() as u64 * 4)
205            })
206            .sum()
207    }
208    fn remap_primitives(&self, from: &[usize]) -> Option<Result<Self, String>> {
209        let remapped = (|| {
210            let mut out = self.clone();
211            for f in out.targets.iter_mut().flat_map(|t| &mut t.frames) {
212                f.primitives = crate::remap_list(&f.primitives, from)?;
213            }
214            Ok(out)
215        })();
216        Some(remapped)
217    }
218    fn remap_vertices(&self, primitive: usize, from: &[u32]) -> Option<Result<Self, String>> {
219        let remapped = (|| {
220            let mut out = self.clone();
221            for f in out.targets.iter_mut().flat_map(|t| &mut t.frames) {
222                let n = f.primitives.len();
223                let slot = f
224                    .primitives
225                    .get_mut(primitive)
226                    .ok_or_else(|| format!("primitive {primitive} of {n}"))?;
227                if let Some(d) = slot {
228                    *d = d.remapped(from)?;
229                }
230            }
231            Ok(out)
232        })();
233        Some(remapped)
234    }
235    fn blend_vertices(&self, primitive: usize, from: &[crate::VertexBlend]) -> Option<Result<Self, String>> {
236        let blended = (|| {
237            let mut out = self.clone();
238            for f in out.targets.iter_mut().flat_map(|t| &mut t.frames) {
239                let n = f.primitives.len();
240                let slot = f
241                    .primitives
242                    .get_mut(primitive)
243                    .ok_or_else(|| format!("primitive {primitive} of {n}"))?;
244                if let Some(d) = slot {
245                    *d = d.blended(from)?;
246                }
247            }
248            Ok(out)
249        })();
250        Some(blended)
251    }
252    /// Position deltas map by the linear part exactly. Normal and tangent deltas have an exact image only under a
253    /// similarity (a rotation or mirror with one scale), where they turn with the rotation; under any other map a
254    /// target with them is unsupported, so the World refuses the mapping.
255    fn apply_linear(&self, l: &crate::Linear) -> crate::Linearity<Self> {
256        let directions = self
257            .targets
258            .iter()
259            .flat_map(|t| &t.frames)
260            .flat_map(|f| f.primitives.iter().flatten())
261            .any(|d| d.normals.is_some() || d.tangents.is_some());
262        let scale = l.similarity();
263        if directions && scale.is_none() {
264            return crate::Linearity::Unsupported;
265        }
266        let mut out = self.clone();
267        for d in out
268            .targets
269            .iter_mut()
270            .flat_map(|t| &mut t.frames)
271            .flat_map(|f| f.primitives.iter_mut().flatten())
272        {
273            for p in d.positions.iter_mut().flatten() {
274                *p = l.point(*p);
275            }
276            if let Some(s) = scale {
277                // the rotation (or mirror) alone: the map over its scale
278                let turn = |v: [f32; 3]| l.point(v).map(|x| (f64::from(x) / s) as f32);
279                for v in d.normals.iter_mut().chain(d.tangents.iter_mut()).flatten() {
280                    *v = turn(*v);
281                }
282            }
283        }
284        crate::Linearity::Applied(out)
285    }
286    /// Ids unique; frames present, their weights finite and increasing, defaults finite; per frame one entry per
287    /// primitive of the entity's mesh (when it has one loaded); dense deltas one per vertex, sparse ones ascending,
288    /// unique and within the primitive; every attribute one delta per entry; every delta finite.
289    fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
290        let mut ids = BTreeSet::new();
291        let mesh = w.get::<Mesh>(e);
292        for t in &self.targets {
293            let at = |m: String| format!("target {:?}: {m}", t.id);
294            if !ids.insert(t.id) {
295                return Err(at("listed twice".into()));
296            }
297            if !t.default_weight.is_finite() || t.frames.is_empty() {
298                return Err(at("no frames, or a default weight not finite".into()));
299            }
300            if !t.frames.iter().all(|f| f.weight.is_finite())
301                || !t.frames.windows(2).all(|p| p[0].weight < p[1].weight)
302            {
303                return Err(at("frame weights not finite and increasing".into()));
304            }
305            // the zero point is no deltas: a frame there would contradict it
306            if t.frames.iter().any(|f| f.weight == 0.0) {
307                return Err(at("a frame at weight 0, where the delta is zero".into()));
308            }
309            for (k, f) in t.frames.iter().enumerate() {
310                if let Some(m) = mesh
311                    && m.primitives().len() != f.primitives.len()
312                {
313                    return Err(at(format!(
314                        "frame {k}: {} entries for {} primitives",
315                        f.primitives.len(),
316                        m.primitives().len()
317                    )));
318                }
319                for (i, d) in f.primitives.iter().enumerate() {
320                    let Some(d) = d else { continue };
321                    let n = mesh.and_then(|m| m.primitives().get(i)).map(|p| p.vertex_count());
322                    let entries = match &d.indices {
323                        None => n.unwrap_or_else(|| d.entries().unwrap_or(0)),
324                        Some(idx) => {
325                            if !idx.windows(2).all(|p| p[0] < p[1]) {
326                                return Err(at(format!(
327                                    "frame {k} primitive {i}: sparse indices not ascending and unique"
328                                )));
329                            }
330                            if let (Some(n), Some(&last)) = (n, idx.last())
331                                && last as usize >= n
332                            {
333                                return Err(at(format!("frame {k} primitive {i}: vertex {last} of {n}")));
334                            }
335                            idx.len()
336                        }
337                    };
338                    for a in [&d.positions, &d.normals, &d.tangents].into_iter().flatten() {
339                        if a.len() != entries {
340                            return Err(at(format!(
341                                "frame {k} primitive {i}: {} deltas for {entries} entries",
342                                a.len()
343                            )));
344                        }
345                        if !a.iter().flatten().all(|x| x.is_finite()) {
346                            return Err(at(format!("frame {k} primitive {i}: a delta not finite")));
347                        }
348                    }
349                }
350            }
351        }
352        Ok(())
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359    use crate::{Linear, Primitive, Semantic, Stream, Topology};
360
361    fn points(n: usize) -> Primitive {
362        Primitive {
363            topology: Topology::Points,
364            streams: [(Semantic::Position, Stream::F32x3(vec![[0.0; 3]; n]))].into(),
365            indices: None,
366            material: None,
367        }
368    }
369
370    /// Two primitives of 3 and 4 vertices; a target with one dense frame on the first, and a target with two frames
371    /// (in-betweens at 0.5 and 1) sparse on the second.
372    fn morphs() -> Morphs {
373        Morphs {
374            targets: vec![
375                Target {
376                    id: MorphId(1),
377                    name: "smile".into(),
378                    default_weight: 0.0,
379                    frames: vec![Frame {
380                        weight: 1.0,
381                        primitives: vec![
382                            Some(Deltas {
383                                positions: Some(vec![[1.0, 0.0, 0.0], [2.0, 0.0, 0.0], [3.0, 0.0, 0.0]]),
384                                normals: Some(vec![[0.0, 1.0, 0.0]; 3]),
385                                ..Deltas::default()
386                            }),
387                            None,
388                        ],
389                    }],
390                },
391                Target {
392                    id: MorphId(2),
393                    name: "blink".into(),
394                    default_weight: 0.25,
395                    frames: [0.5, 1.0]
396                        .map(|weight| Frame {
397                            weight,
398                            primitives: vec![
399                                None,
400                                Some(Deltas {
401                                    indices: Some(vec![1, 2]),
402                                    positions: Some(vec![[0.0, weight, 0.0], [0.0, 2.0 * weight, 0.0]]),
403                                    ..Deltas::default()
404                                }),
405                            ],
406                        })
407                        .into(),
408                },
409            ],
410        }
411    }
412
413    fn errors(m: Morphs) -> Vec<String> {
414        let mut w = World::new();
415        let e = w.spawn();
416        w.insert(e, Mesh::new(vec![points(3), points(4)]));
417        w.insert(e, m);
418        w.verify_components().into_iter().map(|(_, _, m)| m).collect()
419    }
420
421    #[test]
422    fn morphs_verify_and_refuse_malformed_targets() {
423        assert_eq!(errors(morphs()), Vec::<String>::new());
424        let fails = |edit: &dyn Fn(&mut Morphs), what: &str| {
425            let mut m = morphs();
426            edit(&mut m);
427            let e = errors(m);
428            assert!(e.iter().any(|x| x.contains(what)), "{what}: {e:?}");
429        };
430        fails(&|m| m.targets[1].id = MorphId(1), "listed twice");
431        fails(&|m| m.targets[1].frames.reverse(), "increasing");
432        fails(&|m| m.targets[0].frames.clear(), "no frames");
433        fails(
434            &|m| {
435                m.targets[0].frames[0].primitives[0]
436                    .as_mut()
437                    .unwrap()
438                    .positions
439                    .as_mut()
440                    .unwrap()
441                    .pop()
442                    .pipe(drop)
443            },
444            "2 deltas for 3 entries",
445        );
446        fails(
447            &|m| m.targets[1].frames[0].primitives[1].as_mut().unwrap().indices = Some(vec![2, 1]),
448            "not ascending",
449        );
450        fails(
451            &|m| m.targets[1].frames[0].primitives[1].as_mut().unwrap().indices = Some(vec![1, 4]),
452            "vertex 4 of 4",
453        );
454        fails(
455            &|m| m.targets[0].frames[0].primitives.pop().pipe(drop),
456            "1 entries for 2 primitives",
457        );
458        fails(
459            &|m| {
460                m.targets[0].frames[0].primitives[0]
461                    .as_mut()
462                    .unwrap()
463                    .normals
464                    .as_mut()
465                    .unwrap()[0][0] = f32::NAN
466            },
467            "not finite",
468        );
469    }
470
471    /// Hand-worked vertex remaps: dense deltas gathered (new 0 from old 2, new 1 from old 0); sparse entries kept for
472    /// every new vertex made from a listed old one (old 1 and 2 listed; new 0 and 2 from old 2, new 1 from old 0, not
473    /// listed): indices [0, 2], both the old 2's delta.
474    #[test]
475    fn deltas_follow_compacted_vertices_dense_and_sparse() {
476        let m = morphs();
477        let dense = m.remap_vertices(0, &[2, 0]).unwrap().unwrap();
478        let d = dense.targets[0].frames[0].primitives[0].as_ref().unwrap();
479        assert_eq!(d.positions, Some(vec![[3.0, 0.0, 0.0], [1.0, 0.0, 0.0]]));
480        assert_eq!(d.normals.as_ref().map(Vec::len), Some(2));
481        let sparse = m.remap_vertices(1, &[2, 0, 2]).unwrap().unwrap();
482        let s = sparse.targets[1].frames[1].primitives[1].as_ref().unwrap();
483        assert_eq!(s.indices, Some(vec![0, 2]));
484        assert_eq!(s.positions, Some(vec![[0.0, 2.0, 0.0], [0.0, 2.0, 0.0]]));
485        // a dense delta the list lacks is an error
486        assert!(m.remap_vertices(0, &[5]).unwrap().is_err());
487        // a rebuild of the primitive list: the second primitive alone
488        let rebuilt = m.remap_primitives(&[1]).unwrap().unwrap();
489        assert!(rebuilt.targets[0].frames[0].primitives == [None]);
490        assert!(rebuilt.targets[1].frames[0].primitives[0].is_some());
491    }
492
493    /// Hand-worked blends: dense deltas interpolated (new 0 the middle of old 0 and 2; new 1 a copy of old 1), sparse
494    /// ones listed for every new vertex blending a listed one, with 0 for an unlisted old vertex.
495    #[test]
496    fn deltas_follow_blended_vertices_dense_and_sparse() {
497        use crate::VertexBlend;
498        let half = |a, b| VertexBlend {
499            sources: [a, b, b],
500            weights: [0.5, 0.5, 0.0],
501        };
502        let m = morphs();
503        let from = [half(0, 2), VertexBlend::copy(1)];
504        let dense = m.blend_vertices(0, &from).unwrap().unwrap();
505        let d = dense.targets[0].frames[0].primitives[0].as_ref().unwrap();
506        let old = m.targets[0].frames[0].primitives[0].as_ref().unwrap();
507        let (p0, p1, p2) = (
508            old.positions.as_ref().unwrap()[0],
509            old.positions.as_ref().unwrap()[1],
510            old.positions.as_ref().unwrap()[2],
511        );
512        assert_eq!(
513            d.positions,
514            Some(vec![[0, 1, 2].map(|k| (p0[k] + p2[k]) / 2.0), p1])
515        );
516        // sparse: the old 1 and 2 are listed, so new 0 (old 0 and 2) is listed with half of old 2's delta, new 1 (old 1)
517        // with its own; a new vertex of unlisted old vertices only is not listed
518        let from = [half(0, 2), VertexBlend::copy(1), VertexBlend::copy(0)];
519        let sparse = m.blend_vertices(1, &from).unwrap().unwrap();
520        let s = sparse.targets[1].frames[1].primitives[1].as_ref().unwrap();
521        let old = m.targets[1].frames[1].primitives[1].as_ref().unwrap();
522        assert_eq!(s.indices, Some(vec![0, 1]));
523        let p = old.positions.as_ref().unwrap();
524        assert_eq!(s.positions, Some(vec![p[1].map(|x| x / 2.0), p[0]]));
525        // a dense delta the old primitive lacks is an error
526        assert!(m.blend_vertices(0, &[VertexBlend::copy(9)]).unwrap().is_err());
527    }
528
529    /// Positions map by the linear part; normals turn under a similarity (90 degrees about z: (0, 1, 0) to (-1, 0, 0),
530    /// also with a scale of 2) and make any other map unsupported.
531    #[test]
532    fn deltas_follow_a_linear_map() {
533        let m = morphs();
534        let turn2 = Linear::new([[0.0, 2.0, 0.0], [-2.0, 0.0, 0.0], [0.0, 0.0, 2.0]]);
535        let crate::Linearity::Applied(t) = m.apply_linear(&turn2) else {
536            panic!("a similarity applies")
537        };
538        let d = t.targets[0].frames[0].primitives[0].as_ref().unwrap();
539        assert_eq!(d.positions.as_ref().unwrap()[0], [0.0, 2.0, 0.0]);
540        assert_eq!(d.normals.as_ref().unwrap()[0], [-1.0, 0.0, 0.0]);
541        let stretch = Linear::new([[2.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
542        assert_eq!(m.apply_linear(&stretch), crate::Linearity::Unsupported);
543        // without normal or tangent deltas, any map applies to the positions
544        let mut positions_only = morphs();
545        positions_only.targets[0].frames[0].primitives[0]
546            .as_mut()
547            .unwrap()
548            .normals = None;
549        let crate::Linearity::Applied(s) = positions_only.apply_linear(&stretch) else {
550            panic!("positions alone follow any map")
551        };
552        assert_eq!(
553            s.targets[0].frames[0].primitives[0]
554                .as_ref()
555                .unwrap()
556                .positions
557                .as_ref()
558                .unwrap()[2],
559            [6.0, 0.0, 0.0]
560        );
561    }
562
563    trait Pipe: Sized {
564        fn pipe<R>(self, f: impl FnOnce(Self) -> R) -> R {
565            f(self)
566        }
567    }
568    impl<T> Pipe for T {}
569}