Skip to main content

oxide_engine/scene/
gltf_hierarchy.rs

1//! glTF scene hierarchy spawning utilities
2
3use std::collections::HashMap;
4use std::path::PathBuf;
5use std::sync::Arc;
6
7use crate::asset::{load_gltf_async, AssetServerResource, GltfSceneAssets, Handle};
8use oxide_ecs::entity::Entity;
9use oxide_ecs::world::World;
10use oxide_ecs::{Component, Resource};
11use oxide_renderer::gltf::{GltfNode, GltfScene};
12use oxide_transform::{attach_child, GlobalTransform, TransformComponent};
13
14use oxide_math::transform::Transform;
15use wgpu::{Device, Queue};
16
17/// Component storing the source glTF mesh index for an entity.
18#[derive(Component, Clone, Copy, Debug, PartialEq, Eq)]
19pub struct GltfMeshRef {
20    pub mesh_index: usize,
21}
22
23/// Resource containing scene handles waiting to be spawned into ECS.
24#[derive(Resource, Default)]
25pub struct PendingGltfSceneSpawns {
26    pub handles: Vec<Handle<GltfScene>>,
27}
28
29impl PendingGltfSceneSpawns {
30    pub fn queue(&mut self, handle: Handle<GltfScene>) {
31        if !self.handles.contains(&handle) {
32            self.handles.push(handle);
33        }
34    }
35}
36
37/// Resource storing spawned root entities keyed by scene-handle ID.
38#[derive(Resource, Default)]
39pub struct SpawnedGltfScenes {
40    pub roots_by_scene: HashMap<u64, Vec<Entity>>,
41}
42
43/// Spawns glTF nodes into ECS while preserving the node hierarchy.
44///
45/// Returns the root entities created from the scene.
46pub fn spawn_gltf_scene_hierarchy(world: &mut World, scene: &GltfScene) -> Vec<Entity> {
47    scene
48        .nodes
49        .iter()
50        .map(|node| spawn_gltf_node(world, node, None))
51        .collect()
52}
53
54fn spawn_gltf_node(world: &mut World, node: &GltfNode, parent: Option<Entity>) -> Entity {
55    let mut entity_builder = world.spawn((
56        TransformComponent::new(Transform {
57            position: node.translation,
58            rotation: node.rotation,
59            scale: node.scale,
60        }),
61        GlobalTransform::default(),
62    ));
63
64    if let Some(mesh_index) = node.mesh_index {
65        entity_builder.insert(GltfMeshRef { mesh_index });
66    }
67
68    let entity = entity_builder.id();
69
70    if let Some(parent_entity) = parent {
71        attach_child(world, parent_entity, entity);
72    }
73
74    for child in &node.children {
75        spawn_gltf_node(world, child, Some(entity));
76    }
77
78    entity
79}
80
81/// Queues an already requested glTF scene handle for spawn-on-resolve.
82pub fn queue_gltf_scene_spawn(world: &mut World, handle: Handle<GltfScene>) {
83    if !world.contains_resource::<PendingGltfSceneSpawns>() {
84        world.insert_resource(PendingGltfSceneSpawns::default());
85    }
86    world.resource_mut::<PendingGltfSceneSpawns>().queue(handle);
87}
88
89/// Starts async glTF loading and queues the scene for automatic spawn when ready.
90pub fn request_gltf_scene_spawn(
91    world: &mut World,
92    device: Arc<Device>,
93    queue: Arc<Queue>,
94    path: impl Into<PathBuf>,
95) -> Handle<GltfScene> {
96    if !world.contains_resource::<AssetServerResource>() {
97        world.insert_resource(AssetServerResource::default());
98    }
99    if !world.contains_resource::<PendingGltfSceneSpawns>() {
100        world.insert_resource(PendingGltfSceneSpawns::default());
101    }
102    if !world.contains_resource::<GltfSceneAssets>() {
103        world.insert_resource(GltfSceneAssets::default());
104    }
105
106    let handle = {
107        let server = world.resource_mut::<AssetServerResource>();
108        load_gltf_async(&mut server.server, device, queue, path)
109    };
110    world.resource_mut::<PendingGltfSceneSpawns>().queue(handle);
111    handle
112}
113
114/// Returns and removes spawned roots for a resolved scene handle.
115pub fn take_spawned_scene_roots(
116    world: &mut World,
117    handle: Handle<GltfScene>,
118) -> Option<Vec<Entity>> {
119    if !world.contains_resource::<SpawnedGltfScenes>() {
120        return None;
121    }
122    world
123        .resource_mut::<SpawnedGltfScenes>()
124        .roots_by_scene
125        .remove(&handle.id())
126}
127
128/// Polls async glTF loads and spawns queued scenes once available.
129pub fn gltf_scene_spawn_system(world: &mut World) {
130    if !world.contains_resource::<AssetServerResource>()
131        || !world.contains_resource::<GltfSceneAssets>()
132        || !world.contains_resource::<PendingGltfSceneSpawns>()
133        || !world.contains_resource::<SpawnedGltfScenes>()
134    {
135        return;
136    }
137
138    let completed = {
139        let server = world.resource_mut::<AssetServerResource>();
140        server.server.poll_ready::<GltfScene>()
141    };
142
143    if !completed.is_empty() {
144        let mut ready_handles = Vec::new();
145        {
146            let scene_assets = world.resource_mut::<GltfSceneAssets>();
147            for result in completed {
148                match result {
149                    Ok((handle, scene)) => {
150                        scene_assets.assets.insert(handle, scene);
151                        ready_handles.push(handle);
152                    }
153                    Err(err) => tracing::warn!("Failed to load glTF scene: {err}"),
154                }
155            }
156        }
157
158        if !ready_handles.is_empty() {
159            let pending = world.resource_mut::<PendingGltfSceneSpawns>();
160            for handle in ready_handles {
161                pending.queue(handle);
162            }
163        }
164    }
165
166    let queued_handles = world.resource::<PendingGltfSceneSpawns>().handles.clone();
167    let mut spawned = Vec::new();
168    for handle in queued_handles {
169        let scene = {
170            let scene_assets = world.resource_mut::<GltfSceneAssets>();
171            scene_assets.assets.remove(&handle)
172        };
173
174        if let Some(scene) = scene {
175            let roots = spawn_gltf_scene_hierarchy(world, &scene);
176            spawned.push((handle, roots));
177        }
178    }
179
180    if spawned.is_empty() {
181        return;
182    }
183
184    {
185        let pending = world.resource_mut::<PendingGltfSceneSpawns>();
186        pending
187            .handles
188            .retain(|handle| !spawned.iter().any(|(done, _)| done == handle));
189    }
190
191    {
192        let results = world.resource_mut::<SpawnedGltfScenes>();
193        for (handle, roots) in spawned {
194            results.roots_by_scene.insert(handle.id(), roots);
195        }
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::*;
202    use crate::asset::AssetServerResource;
203    use glam::{Quat, Vec3};
204    use oxide_transform::{Children, Parent};
205
206    #[test]
207    fn gltf_hierarchy_spawns_parent_child_relationships() {
208        let mut world = World::new();
209
210        let scene = GltfScene {
211            meshes: Vec::new(),
212            nodes: vec![GltfNode {
213                name: Some("root".to_string()),
214                mesh_index: None,
215                translation: Vec3::new(1.0, 0.0, 0.0),
216                rotation: Quat::IDENTITY,
217                scale: Vec3::ONE,
218                children: vec![GltfNode {
219                    name: Some("child".to_string()),
220                    mesh_index: Some(0),
221                    translation: Vec3::new(0.0, 2.0, 0.0),
222                    rotation: Quat::IDENTITY,
223                    scale: Vec3::ONE,
224                    children: Vec::new(),
225                }],
226            }],
227        };
228
229        let roots = spawn_gltf_scene_hierarchy(&mut world, &scene);
230        assert_eq!(roots.len(), 1);
231
232        let root = roots[0];
233        let children = world.get::<Children>(root).unwrap();
234        assert_eq!(children.len(), 1);
235
236        let child = children.iter().next().unwrap();
237        let parent = world.get::<Parent>(child).unwrap();
238        assert_eq!(parent.0, root);
239        assert!(world.get::<GltfMeshRef>(child).is_some());
240    }
241
242    #[test]
243    fn queued_gltf_scene_spawns_when_asset_is_available() {
244        let mut world = World::new();
245        world.insert_resource(AssetServerResource::default());
246        world.insert_resource(GltfSceneAssets::default());
247        world.insert_resource(PendingGltfSceneSpawns::default());
248        world.insert_resource(SpawnedGltfScenes::default());
249
250        let handle = {
251            let server = world.resource_mut::<AssetServerResource>();
252            server.server.allocate_handle::<GltfScene>()
253        };
254
255        let scene = GltfScene {
256            meshes: Vec::new(),
257            nodes: vec![GltfNode {
258                name: Some("root".to_string()),
259                mesh_index: None,
260                translation: Vec3::ZERO,
261                rotation: Quat::IDENTITY,
262                scale: Vec3::ONE,
263                children: Vec::new(),
264            }],
265        };
266
267        world.resource_mut::<GltfSceneAssets>().assets.insert(handle, scene);
268        queue_gltf_scene_spawn(&mut world, handle);
269        gltf_scene_spawn_system(&mut world);
270
271        let spawned_roots =
272            take_spawned_scene_roots(&mut world, handle).expect("scene should have spawned");
273        assert_eq!(spawned_roots.len(), 1);
274        assert!(world.contains(spawned_roots[0]));
275    }
276}