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, sync::Arc};
14
15use serde::{Deserialize, Serialize};
16use wde_logger::prelude::*;
17
18use bevy::{
19    asset::{AssetLoader, LoadContext, io::Reader},
20    prelude::*
21};
22use wde_animation::prelude::*;
23use wde_pbr::prelude::*;
24use wde_renderer::prelude::*;
25
26#[doc(hidden)]
27pub mod prelude {
28    pub use crate::GltfAsset;
29    pub use crate::GltfError;
30    pub use crate::GltfLight;
31    pub use crate::GltfLightKind;
32    pub use crate::GltfLoader;
33    pub use crate::GltfLoaderSettings;
34    pub use crate::GltfSpawnQueue;
35    pub use crate::MeshGeometry;
36    pub use crate::ModelCondition;
37}
38
39mod accessor;
40mod error;
41mod loader;
42mod material;
43mod model;
44mod parser;
45
46pub use error::GltfError;
47pub use model::{GltfBone, GltfLight, GltfLightKind, ModelCondition};
48
49pub struct GltfPlugin;
50impl Plugin for GltfPlugin {
51    fn build(&self, app: &mut App) {
52        app.init_asset::<GltfAsset>()
53            .init_asset_loader::<GltfAssetLoader>()
54            .init_resource::<GltfSpawnQueue>()
55            .add_systems(Update, process_gltf_spawn_queue);
56    }
57}
58
59/// Queue of glTF assets to spawn once they are loaded.
60#[derive(Resource, Default)]
61pub struct GltfSpawnQueue {
62    pending: Vec<(Entity, Handle<GltfAsset>)>
63}
64
65/// Representation of a 3D GLTF model asset.
66/// This will spawn the model and return the parent entity ID.
67/// See the [crate] documentation for usage examples.
68#[derive(Asset, TypePath, Clone)]
69pub struct GltfAsset {
70    path: String,
71
72    /// The list of parsed glTF models.
73    /// Each model is represented by a mesh and its associated material.
74    pub models: Vec<(Handle<Mesh>, Handle<PbrMaterial>)>,
75    /// Connection condition of each entry of [`Self::models`] (`None` = always shown).
76    pub conditions: Vec<Option<ModelCondition>>,
77    /// Triangles of each entry of [`Self::models`], kept on the CPU (e.g. for navigation): the
78    /// meshes themselves leave the main world once uploaded to the GPU.
79    pub geometry: Vec<Arc<MeshGeometry>>,
80    /// The englobing bounding box of the entire model, computed from the bounding boxes of all meshes.
81    pub bbox: MeshBbox,
82    /// Named properties (e.g. spawn points) extracted from meshes whose name starts with `%P%`.
83    /// These meshes are not added to [`Self::models`]; only their name, position and scale are kept here.
84    pub properties: HashMap<String, (Vec3, Vec3)>,
85    /// Bones of a rigged model (nodes named `%B%<bone>`), parents first.
86    pub bones: Vec<GltfBone>,
87    /// Bone each entry of [`Self::models`] moves with (its vertices are then relative to the bone).
88    pub model_bones: Vec<Option<usize>>,
89    /// Animation clips of the bones, by name.
90    pub animations: HashMap<String, Handle<AnimationClip>>,
91    /// Lights of the model, spawned with it (see [`Self::spawn_lights`]).
92    pub lights: Vec<GltfLight>
93}
94
95impl GltfAsset {
96    pub fn animation(&self, name: &str) -> Option<&Handle<AnimationClip>> {
97        self.animations.get(name)
98    }
99
100    /// Spawns the lights of the model under `parent`, following its transform.
101    pub fn spawn_lights(&self, commands: &mut Commands, parent: Entity) {
102        for light in &self.lights {
103            let color = wde_renderer::prelude::Color::LinearRgba(
104                light.color.x,
105                light.color.y,
106                light.color.z,
107                1.0
108            );
109            let mut entity = commands.spawn((
110                Name::new(format!("Light {}", light.name)),
111                light.transform,
112                ChildOf(parent)
113            ));
114            match light.kind {
115                GltfLightKind::Point => entity.insert(PointLight {
116                    position: Vec3::ZERO,
117                    color,
118                    intensity: light.intensity,
119                    range: light.range
120                }),
121                GltfLightKind::Spot {
122                    inner_cone,
123                    outer_cone
124                } => entity.insert(SpotLight {
125                    position: Vec3::ZERO,
126                    direction: Vec3::NEG_Z,
127                    color,
128                    intensity: light.intensity,
129                    range: light.range,
130                    inner_cone,
131                    outer_cone
132                })
133            };
134        }
135    }
136
137    /// Models whose connection condition holds, `connected` telling whether an anchor is connected.
138    pub fn models_where(
139        &self,
140        connected: impl Fn(&str) -> bool
141    ) -> impl Iterator<Item = &(Handle<Mesh>, Handle<PbrMaterial>)> {
142        self.models
143            .iter()
144            .zip(self.conditions.iter())
145            .filter(move |(_, condition): &(_, &Option<ModelCondition>)| {
146                condition
147                    .as_ref()
148                    .is_none_or(|c| connected(&c.anchor) == c.connected)
149            })
150            .map(|(model, _)| model)
151    }
152}
153
154/// Triangles of a mesh, in model space (or bone space for meshes moving with a bone).
155#[derive(Debug, Default)]
156pub struct MeshGeometry {
157    pub positions: Vec<Vec3>,
158    pub indices: Vec<u32>
159}
160
161/// Options for loading a glTF model.
162#[derive(Serialize, Deserialize, Default)]
163pub struct GltfLoaderSettings {
164    /// The optional stencil value to set the stencil buffer to when rendering the model. By default, no stencil value is set.
165    pub stencil_value: Option<u32>
166}
167
168#[derive(Default, TypePath)]
169pub(crate) struct GltfAssetLoader;
170impl AssetLoader for GltfAssetLoader {
171    type Asset = GltfAsset;
172    type Settings = GltfLoaderSettings;
173    type Error = GltfError;
174
175    async fn load(
176        &self,
177        reader: &mut dyn Reader,
178        _settings: &Self::Settings,
179        load_context: &mut LoadContext<'_>
180    ) -> Result<Self::Asset, Self::Error> {
181        let path = load_context.path().clone();
182        debug!("Loading glTF file {}.", &path);
183
184        // Parse the glTF file
185        let mut bytes = Vec::new();
186        reader.read_to_end(&mut bytes).await?;
187        let model = parser::parse_gltf(bytes, &path.to_string())?;
188
189        // Form and load the model into the Bevy world
190        let (raw_materials, raw_meshes, bounding_boxes) = loader::form_models(&model)?;
191
192        // Construct materials
193        let materials_handles: Vec<Handle<PbrMaterial>> = raw_materials
194            .iter()
195            .map(|material| material.to_pbr(load_context))
196            .collect();
197
198        // Model space transform of each bone in its rest pose
199        let mut bone_matrices: Vec<Mat4> = Vec::with_capacity(model.bones.len());
200        for bone in &model.bones {
201            let local = Mat4::from_scale_rotation_translation(
202                bone.rest.scale,
203                bone.rest.rotation,
204                bone.rest.translation
205            );
206            bone_matrices.push(match bone.parent {
207                Some(parent) => bone_matrices[parent] * local,
208                None => local
209            });
210        }
211
212        // Add meshes to the asset server
213        let mut models = Vec::new();
214        let mut conditions = Vec::new();
215        let mut geometry = Vec::new();
216        let mut model_bones = Vec::new();
217        let mut bbox_min = Vec3::splat(f32::INFINITY);
218        let mut bbox_max = Vec3::splat(f32::NEG_INFINITY);
219        for (i, (indices_data, vertices, material_id, condition, bone)) in
220            raw_meshes.iter().enumerate()
221        {
222            let label = format!("gltf_mesh_{}", i);
223            let (bb_min, bb_max) = bounding_boxes[i];
224            let mesh_asset = Mesh {
225                label: label.clone(),
226                vertices: vertices.clone(),
227                indices: indices_data.clone(),
228                bbox: MeshBbox {
229                    min: bb_min,
230                    max: bb_max
231                },
232                use_ssbo: true
233            };
234
235            models.push((
236                load_context.add_labeled_asset(label.clone(), mesh_asset),
237                materials_handles[*material_id].clone()
238            ));
239            conditions.push(condition.clone());
240            geometry.push(Arc::new(MeshGeometry {
241                positions: vertices.iter().map(|v| Vec3::from(v.position)).collect(),
242                indices: match indices_data.is_empty() {
243                    true => (0..vertices.len() as u32).collect(),
244                    false => indices_data.clone()
245                }
246            }));
247            model_bones.push(*bone);
248
249            // Update the overall bounding box of the model, with bone meshes in their rest pose
250            let corners = match bone {
251                Some(bone) => (0..8)
252                    .map(|c| {
253                        let corner = Vec3::select(
254                            bevy::math::BVec3::new(c & 1 != 0, c & 2 != 0, c & 4 != 0),
255                            bb_max,
256                            bb_min
257                        );
258                        bone_matrices[*bone].transform_point3(corner)
259                    })
260                    .collect(),
261                None => vec![bb_min, bb_max]
262            };
263            for corner in corners {
264                bbox_min = bbox_min.min(corner);
265                bbox_max = bbox_max.max(corner);
266            }
267        }
268
269        let animations = model
270            .animations
271            .into_iter()
272            .map(|clip| {
273                let name = clip.name.clone();
274                let handle = load_context.add_labeled_asset(format!("animation_{name}"), clip);
275                (name, handle)
276            })
277            .collect();
278
279        Ok(GltfAsset {
280            path: path.to_string(),
281            models,
282            conditions,
283            geometry,
284            bbox: MeshBbox {
285                min: bbox_min,
286                max: bbox_max
287            },
288            properties: model.properties,
289            bones: model.bones,
290            model_bones,
291            animations,
292            lights: model.lights
293        })
294    }
295
296    fn extensions(&self) -> &[&str] {
297        &["gltf", "glb"]
298    }
299}
300
301/// Manager to load glTF models into the Bevy world.
302/// See the [crate] documentation for usage examples.
303pub struct GltfLoader;
304impl GltfLoader {
305    /// Enqueue a glTF asset handle to be spawned automatically when loaded.
306    pub fn spawn(queue: &mut GltfSpawnQueue, gltf_asset: Handle<GltfAsset>, parent: Entity) {
307        queue.pending.push((parent, gltf_asset));
308    }
309
310    /// Try to spawn the loaded glTF model into the Bevy world.
311    /// Returns `None` if the asset handle is not loaded yet.
312    pub fn try_spawn(
313        commands: &mut Commands,
314        gltf_asset: &Handle<GltfAsset>,
315        gltf_assets: &Assets<GltfAsset>,
316        parent: Entity
317    ) -> Option<()> {
318        let gltf_asset = gltf_assets.get(gltf_asset)?;
319
320        // Bones, animated by the `AnimationPlayer` of an ancestor
321        let mut bones: Vec<Entity> = Vec::with_capacity(gltf_asset.bones.len());
322        for bone in &gltf_asset.bones {
323            let bone_entity = commands
324                .spawn((
325                    Name::new(format!("Bone {}", bone.name)),
326                    bone.rest,
327                    AnimationBone {
328                        name: bone.name.clone(),
329                        rest: bone.rest
330                    },
331                    ChildOf(bone.parent.map_or(parent, |p| bones[p]))
332                ))
333                .id();
334            bones.push(bone_entity);
335        }
336
337        for (i, (mesh_handle, material_handle)) in gltf_asset.models.iter().enumerate() {
338            let mesh_parent = gltf_asset.model_bones[i].map_or(parent, |bone| bones[bone]);
339            commands.spawn((
340                Name::new(format!(
341                    "Mesh Entity {} for GLTF Model {}",
342                    i, gltf_asset.path
343                )),
344                Transform::default(),
345                Mesh3d(mesh_handle.clone()),
346                PbrMaterial3d(material_handle.clone()),
347                ChildOf(mesh_parent)
348            ));
349        }
350        gltf_asset.spawn_lights(commands, parent);
351        Some(())
352    }
353}
354
355fn process_gltf_spawn_queue(
356    mut commands: Commands,
357    mut queue: ResMut<GltfSpawnQueue>,
358    gltf_assets: Res<Assets<GltfAsset>>
359) {
360    let mut i = 0;
361    while i < queue.pending.len() {
362        let (parent, gltf_asset) = &queue.pending[i];
363        if GltfLoader::try_spawn(&mut commands, gltf_asset, &gltf_assets, *parent).is_some() {
364            queue.pending.swap_remove(i);
365        } else {
366            i += 1;
367        }
368    }
369}