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