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