Skip to main content

dreamwell_engine/content/
import_scene.rs

1//! Imported scene types — GPU-neutral scene graph from external formats.
2
3use serde::{Deserialize, Serialize};
4
5use super::animation::ImportedAnimation;
6use super::mesh::ImportedMesh;
7use crate::game_object::Transform;
8use crate::material::LitMaterialDesc;
9
10/// A node in the imported scene hierarchy.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct SceneNode {
13    /// Node name (from source file).
14    pub name: String,
15    /// Local transform relative to parent.
16    pub transform: Transform,
17    /// Index of the mesh in the scene's mesh list, if any.
18    pub mesh_index: Option<usize>,
19    /// Index of the material in the scene's material list, if any.
20    pub material_index: Option<usize>,
21    /// Indices of child nodes.
22    pub children: Vec<usize>,
23}
24
25/// An imported material descriptor.
26#[derive(Debug, Clone, Default, Serialize, Deserialize)]
27pub struct ImportedMaterial {
28    /// Material name (from source file).
29    pub name: String,
30    /// PBR material properties.
31    pub desc: LitMaterialDesc,
32}
33
34/// A complete imported scene — nodes, meshes, materials, and animations.
35/// Produced by glTF/FBX importers, consumed by the GPU upload pipeline.
36#[derive(Debug, Clone, Default, Serialize, Deserialize)]
37pub struct ImportedScene {
38    /// Scene name.
39    pub name: String,
40    /// Flat list of scene nodes (index 0 is typically the root).
41    pub nodes: Vec<SceneNode>,
42    /// Root node indices (scenes may have multiple roots).
43    pub roots: Vec<usize>,
44    /// Imported meshes referenced by node `mesh_index`.
45    pub meshes: Vec<ImportedMesh>,
46    /// Imported materials referenced by node `material_index`.
47    pub materials: Vec<ImportedMaterial>,
48    /// Imported animations.
49    pub animations: Vec<ImportedAnimation>,
50}
51
52impl ImportedScene {
53    /// Total vertex count across all meshes.
54    pub fn total_vertices(&self) -> usize {
55        self.meshes.iter().map(|m| m.vertex_count()).sum()
56    }
57
58    /// Total triangle count across all meshes.
59    pub fn total_triangles(&self) -> usize {
60        self.meshes.iter().map(|m| m.triangle_count()).sum()
61    }
62
63    /// Validate scene integrity.
64    pub fn validate(&self) -> Result<(), String> {
65        let mesh_count = self.meshes.len();
66        let material_count = self.materials.len();
67        let node_count = self.nodes.len();
68
69        for (i, node) in self.nodes.iter().enumerate() {
70            if let Some(mi) = node.mesh_index {
71                if mi >= mesh_count {
72                    return Err(format!(
73                        "content_scene_mesh_oob:node[{i}] references mesh {mi}, only {mesh_count} meshes"
74                    ));
75                }
76            }
77            if let Some(mi) = node.material_index {
78                if mi >= material_count {
79                    return Err(format!(
80                        "content_scene_material_oob:node[{i}] references material {mi}, only {material_count} materials"
81                    ));
82                }
83            }
84            for &child in &node.children {
85                if child >= node_count {
86                    return Err(format!(
87                        "content_scene_child_oob:node[{i}] references child {child}, only {node_count} nodes"
88                    ));
89                }
90            }
91        }
92
93        for (i, mesh) in self.meshes.iter().enumerate() {
94            mesh.validate().map_err(|e| format!("mesh[{i}] '{}': {e}", mesh.name))?;
95        }
96
97        for anim in &self.animations {
98            anim.validate()?;
99        }
100
101        Ok(())
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::content::mesh::ImportedMesh;
109
110    fn simple_scene() -> ImportedScene {
111        let mesh = ImportedMesh {
112            name: "Cube".into(),
113            positions: vec![[0.0; 3]; 3],
114            normals: vec![[0.0, 0.0, 1.0]; 3],
115            uvs: vec![[0.0; 2]; 3],
116            indices: vec![0, 1, 2],
117        };
118        let mat = ImportedMaterial {
119            name: "Default".into(),
120            desc: LitMaterialDesc::default(),
121        };
122        let node = SceneNode {
123            name: "Root".into(),
124            transform: Transform::default(),
125            mesh_index: Some(0),
126            material_index: Some(0),
127            children: vec![],
128        };
129        ImportedScene {
130            name: "TestScene".into(),
131            nodes: vec![node],
132            roots: vec![0],
133            meshes: vec![mesh],
134            materials: vec![mat],
135            animations: vec![],
136        }
137    }
138
139    #[test]
140    fn valid_scene() {
141        let scene = simple_scene();
142        assert!(scene.validate().is_ok());
143        assert_eq!(scene.total_vertices(), 3);
144        assert_eq!(scene.total_triangles(), 1);
145    }
146
147    #[test]
148    fn mesh_oob() {
149        let mut scene = simple_scene();
150        scene.nodes[0].mesh_index = Some(99);
151        assert!(scene.validate().unwrap_err().contains("mesh_oob"));
152    }
153
154    #[test]
155    fn material_oob() {
156        let mut scene = simple_scene();
157        scene.nodes[0].material_index = Some(99);
158        assert!(scene.validate().unwrap_err().contains("material_oob"));
159    }
160
161    #[test]
162    fn child_oob() {
163        let mut scene = simple_scene();
164        scene.nodes[0].children.push(99);
165        assert!(scene.validate().unwrap_err().contains("child_oob"));
166    }
167
168    #[test]
169    fn empty_scene() {
170        let scene = ImportedScene::default();
171        assert!(scene.validate().is_ok());
172        assert_eq!(scene.total_vertices(), 0);
173    }
174
175    #[test]
176    fn imported_material_default() {
177        let mat = ImportedMaterial::default();
178        assert!(mat.name.is_empty());
179        assert_eq!(mat.desc.roughness, 0.5);
180    }
181}