Skip to main content

draco_io/
fbx_render_mesh.rs

1//! Polygon-corner-domain expansion of an [`FbxMeshInstance`].
2//!
3//! FBX stores geometry as control points plus layer elements that may be
4//! addressed per control point, per polygon, or per polygon *corner*. Only the
5//! corner domain can express a seam: two triangles meeting at one control
6//! point but carrying different UVs or normals across a hard edge.
7//!
8//! Resolving layers onto control points therefore loses data, silently. This
9//! module resolves them onto corners instead, which is what Blender's importer
10//! and every renderer do.
11
12use draco_core::draco_types::DataType;
13use draco_core::geometry_attribute::{GeometryAttributeType, PointAttribute};
14use draco_core::geometry_indices::{FaceIndex, PointIndex};
15use draco_core::mesh::Mesh;
16
17use crate::fbx_scene::{
18    FbxBinormalSet, FbxColorSet, FbxCreaseLayer, FbxLayerSet, FbxMeshInstance, FbxMeshLayers,
19    FbxNormalSet, FbxSmoothingLayer, FbxTangentSet, FbxUvSet,
20};
21
22/// One layer element resolved onto the polygon-corner domain.
23#[derive(Debug, Clone, Default, PartialEq)]
24pub struct FbxRenderLayer<T> {
25    /// Layer name from the source file, when it had one.
26    pub name: Option<String>,
27    /// One value per entry in [`FbxRenderMesh::positions`].
28    pub values: Vec<T>,
29}
30
31/// An [`FbxMeshInstance`] expanded onto the polygon-corner domain.
32///
33/// Every list indexed "per corner" has exactly [`Self::corner_count`] entries,
34/// so a renderer can upload them as parallel vertex buffers directly.
35#[derive(Debug, Clone, Default, PartialEq)]
36pub struct FbxRenderMesh {
37    /// Corner positions, duplicated wherever a control point is shared.
38    pub positions: Vec<[f32; 3]>,
39    /// Every `LayerElementNormal`, in source order.
40    pub normals: Vec<FbxRenderLayer<[f32; 3]>>,
41    /// Every `LayerElementUV`, in source order.
42    pub uvs: Vec<FbxRenderLayer<[f32; 2]>>,
43    /// Every `LayerElementColor`, in source order. Linear RGBA.
44    pub colors: Vec<FbxRenderLayer<[f32; 4]>>,
45    /// Every `LayerElementTangent`, in source order, with the handedness sign
46    /// in `w` -- the layout glTF's `TANGENT` expects.
47    pub tangents: Vec<FbxRenderLayer<[f32; 4]>>,
48    /// Every `LayerElementBinormal`, in source order.
49    pub binormals: Vec<FbxRenderLayer<[f32; 4]>>,
50    /// Triangle-fan indices into the corner arrays.
51    pub indices: Vec<u32>,
52    /// Corner count of each source polygon, in order.
53    ///
54    /// Lets a consumer that preserves n-gons -- Blender's importer does --
55    /// rebuild the original faces without re-reading the file.
56    pub polygon_sizes: Vec<u32>,
57    /// Source control point of each corner.
58    ///
59    /// Skin weights and blend-shape deltas are indexed by control point, so
60    /// this is how they are re-indexed onto the expanded mesh.
61    pub corner_to_control_point: Vec<u32>,
62    /// Source polygon of each corner.
63    pub corner_to_polygon: Vec<u32>,
64}
65
66impl FbxRenderMesh {
67    /// Number of polygon corners, and therefore of per-corner values.
68    pub fn corner_count(&self) -> usize {
69        self.positions.len()
70    }
71}
72
73/// Borrowed FBX geometry, as [`expand_to_render_mesh`] and the writer consume
74/// it.
75///
76/// The borrowed counterpart of [`FbxMeshLayers`] plus the positions and
77/// indices those layers index into. Flat slices rather than a borrow of the
78/// owning struct so the reader can assemble one from decode locals before an
79/// [`FbxMeshInstance`] exists, and so it stays `Copy`.
80///
81/// A struct rather than a positional argument list because the list grows with
82/// every layer family the crate learns to read, and nine adjacent slices of
83/// nearly interchangeable types are easy to transpose silently.
84#[derive(Debug, Clone, Copy, Default)]
85pub struct FbxGeometryLayers<'a> {
86    /// FBX control-point positions.
87    pub control_points: &'a [[f32; 3]],
88    /// Polygon-corner indices; a negative value terminates a polygon.
89    pub polygon_vertex_indices: &'a [i32],
90    /// UV layer elements.
91    pub uv_sets: &'a [FbxUvSet],
92    /// Normal layer elements.
93    pub normal_sets: &'a [FbxNormalSet],
94    /// Colour layer elements.
95    pub color_sets: &'a [FbxColorSet],
96    /// Tangent layer elements.
97    pub tangent_sets: &'a [FbxTangentSet],
98    /// Binormal layer elements.
99    pub binormal_sets: &'a [FbxBinormalSet],
100    /// Smoothing layers.
101    ///
102    /// Scalars on a non-corner domain, so unlike the families above these do
103    /// not reach [`FbxRenderMesh`]; they are carried for the writer.
104    pub smoothing_layers: &'a [FbxSmoothingLayer],
105    /// Edge and vertex crease layers, carried for the writer as above.
106    pub crease_layers: &'a [FbxCreaseLayer],
107}
108
109impl<'a> FbxGeometryLayers<'a> {
110    /// Borrows every layer family from a decoded mesh instance.
111    pub fn from_instance(instance: &'a FbxMeshInstance) -> Self {
112        Self::new(
113            &instance.control_points,
114            &instance.polygon_vertex_indices,
115            &instance.layers,
116        )
117    }
118
119    /// Borrows layers that are not (yet) attached to an instance.
120    pub fn new(
121        control_points: &'a [[f32; 3]],
122        polygon_vertex_indices: &'a [i32],
123        layers: &'a FbxMeshLayers,
124    ) -> Self {
125        Self {
126            control_points,
127            polygon_vertex_indices,
128            uv_sets: &layers.uv_sets,
129            normal_sets: &layers.normal_sets,
130            color_sets: &layers.color_sets,
131            tangent_sets: &layers.tangent_sets,
132            binormal_sets: &layers.binormal_sets,
133            smoothing_layers: &layers.smoothing_layers,
134            crease_layers: &layers.crease_layers,
135        }
136    }
137}
138
139/// One emitted polygon corner, carrying every domain a layer element can be
140/// addressed in.
141#[derive(Debug, Clone, Copy)]
142struct EmittedCorner {
143    /// Control point this corner references.
144    control_point: u32,
145    /// Position in the source `PolygonVertexIndex` stream.
146    source_corner: usize,
147    /// Polygon this corner belongs to.
148    polygon: u32,
149}
150
151/// Resolves one layer-element value for a corner.
152///
153/// `mapping` selects the domain the layer is addressed in and `reference`
154/// whether values are direct or indirected through `indices`.
155fn resolve_layer_value<const N: usize>(
156    mapping: Option<&str>,
157    reference: Option<&str>,
158    indices: &[i32],
159    values: &[[f32; N]],
160    corner: EmittedCorner,
161) -> [f32; N] {
162    let logical = match mapping {
163        Some("ByPolygonVertex") => corner.source_corner,
164        // One value per polygon -- flat shading, typically. Reading this on
165        // the control-point domain returned an unrelated value.
166        Some("ByPolygon") => corner.polygon as usize,
167        Some("AllSame") | Some("AllSameOrPolygon") => 0,
168        // `ByVertice`, `ByVertex`, `ByControlPoint` and anything unrecognized
169        // address the control point.
170        _ => corner.control_point as usize,
171    };
172    let value_index = if reference == Some("IndexToDirect") {
173        match indices.get(logical).copied() {
174            // A negative index is corrupt data, not a back-reference.
175            Some(index) if index < 0 => return [0.0; N],
176            Some(index) => index as usize,
177            None => logical,
178        }
179    } else {
180        logical
181    };
182    values.get(value_index).copied().unwrap_or([0.0; N])
183}
184
185/// Resolves one preserved layer element onto every polygon corner.
186///
187/// Component count is the only thing that differs between UV, normal, colour
188/// and tangent layers, and it is a const parameter, so one function covers all
189/// of them.
190fn resolve_layer<const N: usize>(
191    set: &FbxLayerSet<N>,
192    corners: &[EmittedCorner],
193) -> FbxRenderLayer<[f32; N]> {
194    FbxRenderLayer {
195        name: set.name.clone(),
196        values: corners
197            .iter()
198            .map(|&corner| {
199                resolve_layer_value(
200                    set.mapping.as_deref(),
201                    set.reference.as_deref(),
202                    &set.indices,
203                    &set.values,
204                    corner,
205                )
206            })
207            .collect(),
208    }
209}
210
211/// Expands raw FBX geometry onto the polygon-corner domain.
212///
213/// Takes the parts rather than an [`FbxMeshInstance`] so the reader can call
214/// it while decoding, before an instance exists.
215///
216/// Returns an empty mesh when there is no raw geometry, which is the case for
217/// scenes synthesized in memory rather than read from a file.
218pub fn expand_to_render_mesh(source: FbxGeometryLayers<'_>) -> FbxRenderMesh {
219    let FbxGeometryLayers {
220        control_points,
221        polygon_vertex_indices,
222        uv_sets,
223        normal_sets,
224        color_sets,
225        tangent_sets,
226        binormal_sets,
227        // Scalars on the edge, polygon, or control-point domain. This mesh is
228        // indexed by polygon corner, which cannot represent them.
229        smoothing_layers: _,
230        crease_layers: _,
231    } = source;
232    let mut render = FbxRenderMesh::default();
233    if control_points.is_empty() || polygon_vertex_indices.is_empty() {
234        return render;
235    }
236
237    // Walk the polygon-corner stream once, fan-triangulating as we go and
238    // recording which source corner each emitted vertex came from. Layer
239    // resolution then happens per emitted corner.
240    let mut emitted: Vec<EmittedCorner> = Vec::new();
241    let mut polygon: Vec<EmittedCorner> = Vec::new();
242    let mut polygon_index = 0u32;
243
244    for (corner, encoded) in polygon_vertex_indices.iter().enumerate() {
245        let control_point = if *encoded < 0 {
246            !*encoded as u32
247        } else {
248            *encoded as u32
249        };
250        polygon.push(EmittedCorner {
251            control_point,
252            source_corner: corner,
253            polygon: polygon_index,
254        });
255
256        // A negative index terminates the polygon.
257        if *encoded < 0 {
258            render.polygon_sizes.push(polygon.len() as u32);
259            for offset in 1..polygon.len().saturating_sub(1) {
260                for &vertex in &[polygon[0], polygon[offset], polygon[offset + 1]] {
261                    emitted.push(vertex);
262                    render.corner_to_polygon.push(polygon_index);
263                }
264            }
265            polygon.clear();
266            polygon_index += 1;
267        }
268    }
269
270    render.positions = emitted
271        .iter()
272        .map(|corner| {
273            control_points
274                .get(corner.control_point as usize)
275                .copied()
276                .unwrap_or([0.0; 3])
277        })
278        .collect();
279    render.corner_to_control_point = emitted.iter().map(|c| c.control_point).collect();
280    render.indices = (0..emitted.len() as u32).collect();
281    render.uvs = uv_sets.iter().map(|s| resolve_layer(s, &emitted)).collect();
282    render.normals = normal_sets
283        .iter()
284        .map(|s| resolve_layer(s, &emitted))
285        .collect();
286    render.colors = color_sets
287        .iter()
288        .map(|s| resolve_layer(s, &emitted))
289        .collect();
290    render.tangents = tangent_sets
291        .iter()
292        .map(|s| resolve_layer(&s.layer, &emitted))
293        .collect();
294    render.binormals = binormal_sets
295        .iter()
296        .map(|s| resolve_layer(&s.layer, &emitted))
297        .collect();
298    render
299}
300
301/// [`build_draco_mesh_with_corner_map`]'s output, for a caller that also
302/// needs to carry data addressed by the original render corner -- a skin
303/// weight or a morph delta, both defined per FBX control point -- onto the
304/// welded points that now stand in for them.
305pub struct DracoMeshWithCornerMap {
306    /// The welded Draco mesh -- identical to what [`build_draco_mesh`] returns.
307    pub mesh: Mesh,
308    /// The welded point each original render corner collapsed into.
309    /// Indexed by corner, length [`FbxRenderMesh::corner_count`].
310    pub corner_to_point: Vec<u32>,
311    /// One representative render corner per welded point -- the same one
312    /// [`build_draco_mesh_with_corner_map`] read the point's attributes from.
313    /// Indexed by point, length `mesh.num_points()`.
314    pub point_to_corner: Vec<u32>,
315}
316
317/// Builds a Draco mesh from corner-domain data, welding corners that agree on
318/// every attribute.
319///
320/// Welding keeps seams -- corners that differ in UV or normal stay separate --
321/// while collapsing the interior duplicates that plain corner expansion would
322/// triple. This is the same trade-off a glTF exporter makes.
323///
324/// Only the first UV, normal and colour set reach the Draco mesh; Draco has no
325/// concept of multiple sets. The rest stay on [`FbxRenderMesh`].
326pub fn build_draco_mesh(render: &FbxRenderMesh) -> Mesh {
327    build_draco_mesh_with_corner_map(render).mesh
328}
329
330/// [`build_draco_mesh`], additionally returning the corner/point
331/// correspondence the weld produced.
332///
333/// Built one point per corner -- an explicit-mapping attribute would only
334/// reproduce the identity this default already is -- and welded through
335/// [`draco_core::mesh::Mesh::finalize_returning_corner_map`], the same
336/// merge-bit-identical-values-then-merge-points pass the OBJ, PLY and glTF
337/// readers all end construction with. One fewer bespoke weld in the crate.
338pub fn build_draco_mesh_with_corner_map(render: &FbxRenderMesh) -> DracoMeshWithCornerMap {
339    let normals = render.normals.first();
340    let uvs = render.uvs.first();
341    let colors = render.colors.first();
342    let corner_count = render.corner_count();
343
344    let mut mesh = Mesh::new();
345    mesh.set_num_points(corner_count);
346
347    let mut position = PointAttribute::new();
348    position.init(
349        GeometryAttributeType::Position,
350        3,
351        DataType::Float32,
352        false,
353        corner_count,
354    );
355    for (corner, value) in render.positions.iter().enumerate() {
356        let bytes: Vec<u8> = value.iter().flat_map(|v| v.to_le_bytes()).collect();
357        position.buffer_mut().write(corner * 12, &bytes);
358    }
359    mesh.add_attribute(position);
360
361    if let Some(layer) = normals {
362        let mut normal = PointAttribute::new();
363        normal.init(
364            GeometryAttributeType::Normal,
365            3,
366            DataType::Float32,
367            false,
368            corner_count,
369        );
370        for (corner, value) in layer.values.iter().enumerate() {
371            let bytes: Vec<u8> = value.iter().flat_map(|v| v.to_le_bytes()).collect();
372            normal.buffer_mut().write(corner * 12, &bytes);
373        }
374        mesh.add_attribute(normal);
375    }
376
377    if let Some(layer) = uvs {
378        let mut tex_coord = PointAttribute::new();
379        tex_coord.init(
380            GeometryAttributeType::TexCoord,
381            2,
382            DataType::Float32,
383            false,
384            corner_count,
385        );
386        for (corner, value) in layer.values.iter().enumerate() {
387            let bytes: Vec<u8> = value.iter().flat_map(|v| v.to_le_bytes()).collect();
388            tex_coord.buffer_mut().write(corner * 8, &bytes);
389        }
390        mesh.add_attribute(tex_coord);
391    }
392
393    if let Some(layer) = colors {
394        let mut color = PointAttribute::new();
395        color.init(
396            GeometryAttributeType::Color,
397            4,
398            DataType::Float32,
399            false,
400            corner_count,
401        );
402        for (corner, value) in layer.values.iter().enumerate() {
403            let bytes: Vec<u8> = value.iter().flat_map(|v| v.to_le_bytes()).collect();
404            color.buffer_mut().write(corner * 16, &bytes);
405        }
406        mesh.add_attribute(color);
407    }
408
409    mesh.set_num_faces(corner_count / 3);
410    for face in 0..corner_count / 3 {
411        let base = (face * 3) as u32;
412        mesh.set_face(
413            FaceIndex(face as u32),
414            [PointIndex(base), PointIndex(base + 1), PointIndex(base + 2)],
415        );
416    }
417
418    let corner_to_point = mesh
419        .finalize_returning_corner_map()
420        .expect("deduplicating an in-memory mesh cannot fail on I/O");
421
422    // The first corner to reach each point, matching what the point's
423    // attribute values were themselves read from -- one representative per
424    // point, for a caller that needs data (a tangent, a second UV set) this
425    // mesh has no attribute for.
426    let mut point_to_corner = vec![u32::MAX; mesh.num_points()];
427    for (corner, &point) in corner_to_point.iter().enumerate() {
428        if point_to_corner[point as usize] == u32::MAX {
429            point_to_corner[point as usize] = corner as u32;
430        }
431    }
432
433    DracoMeshWithCornerMap {
434        mesh,
435        corner_to_point,
436        point_to_corner,
437    }
438}
439
440impl FbxMeshInstance {
441    /// Expands this instance onto the polygon-corner domain.
442    pub fn to_render_mesh(&self) -> FbxRenderMesh {
443        expand_to_render_mesh(FbxGeometryLayers::from_instance(self))
444    }
445
446    /// Corner-domain Draco mesh for this instance, welded by attribute tuple.
447    ///
448    /// Falls back to the stored mesh when the instance carries no raw FBX
449    /// geometry to expand.
450    pub fn to_draco_mesh(&self) -> Mesh {
451        let render = self.to_render_mesh();
452        if render.positions.is_empty() {
453            return self.mesh.clone();
454        }
455        build_draco_mesh(&render)
456    }
457
458    /// [`Self::to_draco_mesh`], additionally returning the corner/point
459    /// correspondence the weld produced -- what a caller needs to carry a
460    /// per-control-point skin weight or morph delta onto the welded points
461    /// that now stand in for the corners it used to be duplicated across.
462    ///
463    /// The stored-mesh fallback has no corners to map from, so both maps are
464    /// the identity over its own points -- matching the WASM binding's own
465    /// prior assumption that a point *is* its control point when there is no
466    /// raw geometry to re-derive one from.
467    pub fn to_draco_mesh_with_corner_map(&self) -> DracoMeshWithCornerMap {
468        let render = self.to_render_mesh();
469        if render.positions.is_empty() {
470            let identity: Vec<u32> = (0..self.mesh.num_points() as u32).collect();
471            return DracoMeshWithCornerMap {
472                mesh: self.mesh.clone(),
473                corner_to_point: identity.clone(),
474                point_to_corner: identity,
475            };
476        }
477        build_draco_mesh_with_corner_map(&render)
478    }
479}
480
481#[cfg(test)]
482mod tests {
483    use super::*;
484    use crate::fbx_scene::FbxMeshInstance;
485
486    /// Two triangles sharing an edge, with per-corner UVs that disagree across
487    /// it -- the seam case that control-point resolution destroys.
488    fn seamed_quad() -> FbxMeshInstance {
489        FbxMeshInstance {
490            control_points: vec![
491                [0.0, 0.0, 0.0],
492                [1.0, 0.0, 0.0],
493                [1.0, 1.0, 0.0],
494                [0.0, 1.0, 0.0],
495            ],
496            // One quad: 0, 1, 2, ~3
497            polygon_vertex_indices: vec![0, 1, 2, !3],
498            layers: FbxMeshLayers {
499                uv_sets: vec![FbxUvSet {
500                    name: Some("map1".to_string()),
501                    mapping: Some("ByPolygonVertex".to_string()),
502                    reference: Some("Direct".to_string()),
503                    values: vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.5, 0.5]],
504                    indices: Vec::new(),
505                }],
506                ..Default::default()
507            },
508            ..Default::default()
509        }
510    }
511
512    #[test]
513    fn a_quad_fans_into_two_triangles_over_four_corners() {
514        let render = seamed_quad().to_render_mesh();
515        assert_eq!(render.polygon_sizes, vec![4]);
516        assert_eq!(render.corner_count(), 6, "two fan triangles");
517        assert_eq!(render.indices, vec![0, 1, 2, 3, 4, 5]);
518        assert_eq!(render.corner_to_control_point, vec![0, 1, 2, 0, 2, 3]);
519        assert!(render.corner_to_polygon.iter().all(|&p| p == 0));
520    }
521
522    #[test]
523    fn per_corner_uvs_follow_the_corner_not_the_control_point() {
524        let render = seamed_quad().to_render_mesh();
525        let uv = &render.uvs[0].values;
526        // Corner 3 of the fan reuses control point 0 but is source corner 0.
527        assert_eq!(uv[0], [0.0, 0.0]);
528        assert_eq!(uv[1], [1.0, 0.0]);
529        assert_eq!(uv[2], [1.0, 1.0]);
530        assert_eq!(render.uvs[0].name.as_deref(), Some("map1"));
531    }
532
533    #[test]
534    fn welding_keeps_seams_but_drops_exact_duplicates() {
535        let mesh = seamed_quad().to_draco_mesh();
536        // Corners 0/3 and 2/4 agree on position and UV, so they weld; the
537        // quad's four distinct corners survive.
538        assert_eq!(mesh.num_points(), 4);
539        assert_eq!(mesh.num_faces(), 2);
540    }
541
542    /// The weld map's hasher seeds itself per instance, so two builds of the
543    /// same geometry hash their keys differently. Nothing about the result may
544    /// follow from that: the ids are handed out in corner order and the map is
545    /// only ever asked whether it has seen a key, never iterated.
546    #[test]
547    fn welding_gives_the_same_mesh_whatever_the_hasher_seed_is() {
548        let first = seamed_quad().to_draco_mesh();
549        let second = seamed_quad().to_draco_mesh();
550
551        assert_eq!(first.num_points(), second.num_points());
552        assert_eq!(first.num_faces(), second.num_faces());
553        for face in 0..first.num_faces() {
554            assert_eq!(
555                first.face(FaceIndex(face as u32)),
556                second.face(FaceIndex(face as u32)),
557                "face {face} came out differently"
558            );
559        }
560    }
561
562    /// Corner 5 of polygon 2, sitting on control point 1.
563    fn probe_corner() -> EmittedCorner {
564        EmittedCorner {
565            control_point: 1,
566            source_corner: 5,
567            polygon: 2,
568        }
569    }
570
571    #[test]
572    fn a_negative_index_to_direct_entry_yields_a_default_rather_than_panicking() {
573        let value: [f32; 2] = resolve_layer_value(
574            Some("ByPolygonVertex"),
575            Some("IndexToDirect"),
576            &[-31],
577            &[[1.0, 2.0]],
578            EmittedCorner {
579                control_point: 0,
580                source_corner: 0,
581                polygon: 0,
582            },
583        );
584        assert_eq!(value, [0.0, 0.0]);
585    }
586
587    #[test]
588    fn each_mapping_addresses_its_own_domain() {
589        // Distinct per index, so a wrong domain cannot coincidentally match.
590        let values: Vec<[f32; 1]> = (0..8).map(|i| [i as f32]).collect();
591        let resolve = |mapping| {
592            resolve_layer_value(Some(mapping), Some("Direct"), &[], &values, probe_corner())
593        };
594        assert_eq!(resolve("ByPolygonVertex"), [5.0]);
595        assert_eq!(resolve("ByVertice"), [1.0]);
596        assert_eq!(resolve("AllSame"), [0.0]);
597        // Regression: `ByPolygon` used to fall through to the control point,
598        // silently returning the value of an unrelated polygon.
599        assert_eq!(resolve("ByPolygon"), [2.0]);
600    }
601
602    #[test]
603    fn an_instance_without_raw_geometry_expands_to_nothing() {
604        let mut instance = seamed_quad();
605        instance.control_points.clear();
606        instance.polygon_vertex_indices.clear();
607        assert_eq!(instance.to_render_mesh(), FbxRenderMesh::default());
608    }
609}