Skip to main content

wde_gltf/
lib.rs

1//! GLTF Loader for WaterDropEngine
2//!
3//! This crate provides functionality to load and parse glTF files, converting them into assets that can be used within the WaterDropEngine. It supports loading meshes, materials, and textures defined in glTF files and integrates them with the engine's rendering system.
4//!
5//! # Example
6//! To load a glTF model, you can use the [`GltfLoader`](crate::GltfLoader) in your `setup` system as follows:
7//! ```rust
8//! let gltf_asset = asset_server.load("models/model.gltf");
9//! GltfLoader::spawn(&mut gltf_spawn_queue, gltf_asset);
10//! ```
11//! Note that the `gltf_asset` handle will be enqueued for spawning once it is loaded. The actual spawning of the model into the world will happen automatically in the background, and you can check for its presence using the asset handle.
12//! The rendering of the loaded model is then managed by the [`wde_pbr`](wde_pbr) crate, which handles the materials and shaders for the meshes.
13use std::collections::HashMap;
14
15use serde::{Deserialize, Serialize};
16use wde_logger::prelude::*;
17
18use bevy::{
19    asset::{AssetLoader, LoadContext, io::Reader},
20    prelude::*
21};
22use wde_pbr::prelude::*;
23use wde_renderer::prelude::*;
24
25#[doc(hidden)]
26pub mod prelude {
27    pub use crate::GltfAsset;
28    pub use crate::GltfError;
29    pub use crate::GltfLoader;
30    pub use crate::GltfLoaderSettings;
31    pub use crate::GltfSpawnQueue;
32}
33
34mod accessor;
35mod error;
36mod loader;
37mod material;
38mod model;
39mod parser;
40
41pub use error::GltfError;
42
43pub struct GltfPlugin;
44impl Plugin for GltfPlugin {
45    fn build(&self, app: &mut App) {
46        app.init_asset::<GltfAsset>()
47            .init_asset_loader::<GltfAssetLoader>()
48            .init_resource::<GltfSpawnQueue>()
49            .add_systems(Update, process_gltf_spawn_queue);
50    }
51}
52
53/// Queue of glTF assets to spawn once they are loaded.
54#[derive(Resource, Default)]
55pub struct GltfSpawnQueue {
56    pending: Vec<(Entity, Handle<GltfAsset>)>
57}
58
59/// Representation of a 3D GLTF model asset.
60/// This will spawn the model and return the parent entity ID.
61/// See the [crate] documentation for usage examples.
62#[derive(Asset, TypePath, Clone)]
63pub struct GltfAsset {
64    path: String,
65
66    /// The list of parsed glTF models.
67    /// Each model is represented by a mesh and its associated material.
68    pub models: Vec<(Handle<Mesh>, Handle<PbrMaterial>)>,
69    /// The englobing bounding box of the entire model, computed from the bounding boxes of all meshes.
70    pub bbox: MeshBbox,
71    /// Named properties (e.g. spawn points) extracted from meshes whose name starts with `%P%`.
72    /// These meshes are not added to [`Self::models`]; only their name, position and scale are kept here.
73    pub properties: HashMap<String, (Vec3, Vec3)>
74}
75
76/// Options for loading a glTF model.
77#[derive(Serialize, Deserialize, Default)]
78pub struct GltfLoaderSettings {
79    /// The optional stencil value to set the stencil buffer to when rendering the model. By default, no stencil value is set.
80    pub stencil_value: Option<u32>
81}
82
83#[derive(Default, TypePath)]
84pub(crate) struct GltfAssetLoader;
85impl AssetLoader for GltfAssetLoader {
86    type Asset = GltfAsset;
87    type Settings = GltfLoaderSettings;
88    type Error = GltfError;
89
90    async fn load(
91        &self,
92        reader: &mut dyn Reader,
93        _settings: &Self::Settings,
94        load_context: &mut LoadContext<'_>
95    ) -> Result<Self::Asset, Self::Error> {
96        let path = load_context.path().clone();
97        debug!("Loading glTF file {}.", &path);
98
99        // Parse the glTF file
100        let mut bytes = Vec::new();
101        reader.read_to_end(&mut bytes).await?;
102        let model = parser::parse_gltf(bytes, &path.to_string())?;
103
104        // Form and load the model into the Bevy world
105        let (raw_materials, raw_meshes, bounding_boxes) = loader::form_models(&model)?;
106
107        // Construct materials
108        let materials_handles: Vec<Handle<PbrMaterial>> = raw_materials
109            .iter()
110            .map(|material| material.to_pbr(load_context))
111            .collect();
112
113        // Add meshes to the asset server
114        let mut models = Vec::new();
115        let mut bbox_min = Vec3::splat(f32::INFINITY);
116        let mut bbox_max = Vec3::splat(f32::NEG_INFINITY);
117        for (i, (indices_data, vertices, material_id)) in raw_meshes.iter().enumerate() {
118            let label = format!("gltf_mesh_{}", i);
119            let (bb_min, bb_max) = bounding_boxes[i];
120            let mesh_asset = Mesh {
121                label: label.clone(),
122                vertices: vertices.clone(),
123                indices: indices_data.clone(),
124                bbox: MeshBbox {
125                    min: bb_min,
126                    max: bb_max
127                },
128                use_ssbo: true
129            };
130
131            models.push((
132                load_context.add_labeled_asset(label.clone(), mesh_asset),
133                materials_handles[*material_id].clone()
134            ));
135
136            // Update the overall bounding box of the model
137            for j in 0..3 {
138                if bb_min[j] < bbox_min[j] {
139                    bbox_min[j] = bb_min[j];
140                }
141                if bb_max[j] > bbox_max[j] {
142                    bbox_max[j] = bb_max[j];
143                }
144            }
145        }
146
147        Ok(GltfAsset {
148            path: path.to_string(),
149            models,
150            bbox: MeshBbox {
151                min: bbox_min,
152                max: bbox_max
153            },
154            properties: model.properties
155        })
156    }
157
158    fn extensions(&self) -> &[&str] {
159        &["gltf", "glb"]
160    }
161}
162
163/// Manager to load glTF models into the Bevy world.
164/// See the [crate] documentation for usage examples.
165pub struct GltfLoader;
166impl GltfLoader {
167    /// Enqueue a glTF asset handle to be spawned automatically when loaded.
168    pub fn spawn(queue: &mut GltfSpawnQueue, gltf_asset: Handle<GltfAsset>, parent: Entity) {
169        queue.pending.push((parent, gltf_asset));
170    }
171
172    /// Try to spawn the loaded glTF model into the Bevy world.
173    /// Returns `None` if the asset handle is not loaded yet.
174    pub fn try_spawn(
175        commands: &mut Commands,
176        gltf_asset: &Handle<GltfAsset>,
177        gltf_assets: &Assets<GltfAsset>,
178        parent: Entity
179    ) -> Option<()> {
180        let gltf_asset = gltf_assets.get(gltf_asset)?;
181        for (i, (mesh_handle, material_handle)) in gltf_asset.models.iter().enumerate() {
182            commands.spawn((
183                Name::new(format!(
184                    "Mesh Entity {} for GLTF Model {}",
185                    i, gltf_asset.path
186                )),
187                Transform::default(),
188                Mesh3d(mesh_handle.clone()),
189                PbrMaterial3d(material_handle.clone()),
190                ChildOf(parent)
191            ));
192        }
193        Some(())
194    }
195}
196
197fn process_gltf_spawn_queue(
198    mut commands: Commands,
199    mut queue: ResMut<GltfSpawnQueue>,
200    gltf_assets: Res<Assets<GltfAsset>>
201) {
202    let mut i = 0;
203    while i < queue.pending.len() {
204        let (parent, gltf_asset) = &queue.pending[i];
205        if GltfLoader::try_spawn(&mut commands, gltf_asset, &gltf_assets, *parent).is_some() {
206            queue.pending.swap_remove(i);
207        } else {
208            i += 1;
209        }
210    }
211}