Skip to main content

bevy_gltf/loader/extensions/
mod.rs

1//! glTF extensions defined by the Khronos Group and other vendors
2
3mod khr_materials_anisotropy;
4mod khr_materials_clearcoat;
5mod khr_materials_specular;
6
7use alloc::sync::Arc;
8use async_lock::RwLock;
9
10use bevy_asset::{Handle, LoadContext};
11use bevy_ecs::{
12    entity::Entity,
13    resource::Resource,
14    world::{EntityWorldMut, World},
15};
16use bevy_mesh::{Mesh, MeshVertexAttribute};
17use bevy_tasks::{BoxedFuture, ConditionalSendFuture};
18use gltf::Node;
19
20use bevy_platform::collections::HashMap;
21#[cfg(feature = "bevy_animation")]
22use {bevy_animation::AnimationClip, bevy_platform::collections::HashSet};
23
24use crate::{GltfLoaderSettings, GltfMaterial, GltfMesh};
25
26pub(crate) use self::{
27    khr_materials_anisotropy::AnisotropyExtension, khr_materials_clearcoat::ClearcoatExtension,
28    khr_materials_specular::SpecularExtension,
29};
30
31/// Stores the `ErasedGltfExtensionHandler` implementations so that they
32/// can be added by users and also passed to the glTF loader
33#[derive(impl bevy_ecs::component::Component for GltfExtensionHandlers where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {
    const STORAGE_TYPE: bevy_ecs::component::StorageType =
        bevy_ecs::component::StorageType::SparseSet;
    type Mutability = bevy_ecs::component::Mutable;
    fn register_required_components(_requiree:
            bevy_ecs::component::ComponentId,
        required_components:
            &mut bevy_ecs::component::RequiredComponentsRegistrator) {
        let resource_component_id =
            if let ::core::option::Option::Some(id) =
                    required_components.components_registrator().component_id::<GltfExtensionHandlers>()
                {
                id
            } else {
                required_components.components_registrator().register_component::<GltfExtensionHandlers>()
            };
        required_components.register_required::<bevy_ecs::resource::IsResource>(move
                ||
                bevy_ecs::resource::IsResource::new(resource_component_id));
    }
    fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
        use bevy_ecs::component::{
            DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
        };
        (&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
    }
    fn relationship_accessor()
        ->
            ::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
        ::core::option::Option::None
    }
}
impl bevy_ecs::resource::Resource for GltfExtensionHandlers where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {}Resource, #[automatically_derived]impl ::core::default::Default for
    GltfExtensionHandlers {
    #[inline]
    fn default() -> Self {
        GltfExtensionHandlers(::core::default::Default::default())
    }
}Default)]
34pub struct GltfExtensionHandlers(pub Arc<RwLock<Vec<Box<dyn ErasedGltfExtensionHandler>>>>);
35
36/// glTF Extensions can attach data to any objects in a glTF file.
37/// This is done by inserting data in the `extensions` sub-object, and
38/// data in the extensions sub-object is keyed by the id of the extension.
39/// For example: `KHR_materials_variants`, `EXT_meshopt_compression`, or `BEVY_my_tool`
40///
41/// A list of publicly known extensions and their ids can be found
42/// in the [KhronosGroup/glTF](https://github.com/KhronosGroup/glTF/blob/main/extensions/README.md)
43/// git repo. Vendors reserve prefixes, such as the `BEVY` prefix,
44/// which is also listed in the [KhronosGroup repo](https://github.com/KhronosGroup/glTF/blob/main/extensions/Prefixes.md).
45///
46/// The `GltfExtensionHandler` trait should be implemented to participate in
47/// processing glTF files as they load, and exposes glTF extension data via
48/// a series of hook callbacks.
49///
50/// The type a `GltfExtensionHandler` is implemented for can define data
51/// which will be cloned for each new glTF load. This enables stateful
52/// handling of glTF extension data during a single load.
53///
54/// When loading a glTF file, a glTF object that could contain extension
55/// data will cause the relevant hook to execute once per object.
56/// Each invocation will receive all extension data, which is required because
57/// many extensions require accessing data defined by other extensions.
58///
59/// The hooks are always called once, even if there is no extension data
60/// This is useful for scenarios where additional extension data isn't
61/// required, but processing should still happen.
62pub trait GltfExtensionHandler: Send + Sync + 'static {
63    /// Required for dyn cloning
64    fn dyn_clone(&self) -> Box<dyn ErasedGltfExtensionHandler>;
65
66    /// Called when the "global" data for an extension
67    /// at the root of a glTF file is encountered.
68    #[expect(
69        unused,
70        reason = "default trait implementations do not use the arguments because they are no-ops"
71    )]
72    fn on_root(
73        &mut self,
74        load_context: &mut LoadContext<'_>,
75        gltf: &gltf::Gltf,
76        settings: &GltfLoaderSettings,
77    ) {
78    }
79
80    #[cfg(feature = "bevy_animation")]
81    #[expect(
82        unused,
83        reason = "default trait implementations do not use the arguments because they are no-ops"
84    )]
85    /// Called when an individual animation is processed
86    fn on_animation(
87        &mut self,
88        load_context: &mut LoadContext<'_>,
89        gltf_animation: &gltf::Animation,
90        animation_clip: &mut AnimationClip,
91    ) {
92    }
93
94    #[cfg(feature = "bevy_animation")]
95    #[expect(
96        unused,
97        reason = "default trait implementations do not use the arguments because they are no-ops"
98    )]
99    /// Called when all animations have been collected.
100    /// `animations` is the glTF ordered list of `Handle<AnimationClip>`s
101    /// `named_animations` is a `HashMap` from animation name to `Handle<AnimationClip>`
102    /// `animation_roots` is the glTF index of the animation root object
103    fn on_animations_collected(
104        &mut self,
105        load_context: &mut LoadContext<'_>,
106        animations: &[Handle<AnimationClip>],
107        named_animations: &HashMap<Box<str>, Handle<AnimationClip>>,
108        animation_roots: &HashSet<usize>,
109    ) {
110    }
111
112    /// Called when an individual texture is processed
113    #[expect(
114        unused,
115        reason = "default trait implementations do not use the arguments because they are no-ops"
116    )]
117    fn on_texture(&mut self, gltf_texture: &gltf::Texture, texture: Handle<bevy_image::Image>) {}
118
119    /// Called when an individual material is processed
120    #[expect(
121        unused,
122        reason = "default trait implementations do not use the arguments because they are no-ops"
123    )]
124    fn on_material(
125        &mut self,
126        load_context: &mut LoadContext<'_>,
127        gltf_material: &gltf::Material,
128        material: Handle<GltfMaterial>,
129        material_asset: &GltfMaterial,
130        material_label: &str,
131    ) {
132    }
133
134    /// Called when an individual glTF primitive is processed
135    /// glTF primitives are what become a Bevy `Mesh`
136    /// This hook is useful for extensions that need to
137    /// decompress or transform primitives and their associated
138    /// glTF data.
139    ///
140    /// `buffer_data` is a reference to all of the buffers from the
141    /// glTF document, in order, after it has been loaded by Bevy. Extensions
142    /// in glTF are allowed to add arbitrary buffers, so while this
143    /// data is often vertex data, it can not be assumed to be
144    /// vertex data.
145    ///
146    /// `out_doc` is an optional `gltf::Document` which, if set,
147    /// must contain a single `gltf::Mesh` with a single
148    /// `gltf::Primitive`. This document is only used by Bevy for
149    /// the processing of the relevant primitive and can not affect
150    /// other processing.
151    ///
152    /// `out_data` is a single buffer wrapped in a `Vec`, which mirrors
153    /// the buffer structure of a loaded `gltf::Document`'s buffers, which
154    /// is the same structure as `buffer_data`. The outer `Vec` must
155    /// contain a single `Vec<u8>` of data, as only the first generated
156    /// buffer is used. If set, the loader will use this modified buffer
157    /// data instead of the original `buffer_data` to construct the Mesh.
158    #[expect(
159        unused,
160        reason = "default trait implementations do not use the arguments because they are no-ops"
161    )]
162    fn on_gltf_primitive(
163        &mut self,
164        load_context: &mut LoadContext<'_>,
165        gltf_document: &gltf::Gltf,
166        gltf_mesh: &gltf::Mesh,
167        gltf_primitive: &gltf::Primitive,
168        buffer_data: &[Vec<u8>],
169        custom_vertex_attributes: &HashMap<Box<str>, MeshVertexAttribute>,
170        gltf_mesh_on_skinned_nodes: bool,
171        gltf_mesh_on_non_skinned_nodes: bool,
172        user_mesh: &mut Option<Mesh>,
173    ) -> impl ConditionalSendFuture<Output = ()> {
174        async {}
175    }
176
177    /// Called when an individual glTF Mesh is processed
178    #[expect(
179        unused,
180        reason = "default trait implementations do not use the arguments because they are no-ops"
181    )]
182    fn on_gltf_mesh(
183        &mut self,
184        load_context: &mut LoadContext<'_>,
185        gltf_mesh: &gltf::Mesh,
186        mesh: Handle<GltfMesh>,
187    ) {
188    }
189
190    /// mesh and material are spawned as a single Entity,
191    /// which means an extension would have to decide for
192    /// itself how to merge the extension data.
193    #[expect(
194        unused,
195        reason = "default trait implementations do not use the arguments because they are no-ops"
196    )]
197    fn on_spawn_mesh_and_material(
198        &mut self,
199        load_context: &mut LoadContext<'_>,
200        primitive: &gltf::Primitive,
201        mesh: &gltf::Mesh,
202        material: &gltf::Material,
203        entity: &mut EntityWorldMut,
204        material_label: &str,
205    ) {
206    }
207
208    /// Called when an individual Scene is done processing
209    #[expect(
210        unused,
211        reason = "default trait implementations do not use the arguments because they are no-ops"
212    )]
213    fn on_scene_completed(
214        &mut self,
215        load_context: &mut LoadContext<'_>,
216        scene: &gltf::Scene,
217        world_root_id: Entity,
218        scene_world: &mut World,
219    ) {
220    }
221
222    /// Called when a node is processed
223    #[expect(
224        unused,
225        reason = "default trait implementations do not use the arguments because they are no-ops"
226    )]
227    fn on_gltf_node(
228        &mut self,
229        load_context: &mut LoadContext<'_>,
230        gltf_node: &Node,
231        entity: &mut EntityWorldMut,
232    ) {
233    }
234
235    /// Called with a `DirectionalLight` node is spawned
236    /// which is typically created as a result of
237    /// `KHR_lights_punctual`
238    #[expect(
239        unused,
240        reason = "default trait implementations do not use the arguments because they are no-ops"
241    )]
242    fn on_spawn_light_directional(
243        &mut self,
244        load_context: &mut LoadContext<'_>,
245        gltf_node: &Node,
246        entity: &mut EntityWorldMut,
247    ) {
248    }
249    /// Called with a `PointLight` node is spawned
250    /// which is typically created as a result of
251    /// `KHR_lights_punctual`
252    #[expect(
253        unused,
254        reason = "default trait implementations do not use the arguments because they are no-ops"
255    )]
256    fn on_spawn_light_point(
257        &mut self,
258        load_context: &mut LoadContext<'_>,
259        gltf_node: &Node,
260        entity: &mut EntityWorldMut,
261    ) {
262    }
263    /// Called with a `SpotLight` node is spawned
264    /// which is typically created as a result of
265    /// `KHR_lights_punctual`
266    #[expect(
267        unused,
268        reason = "default trait implementations do not use the arguments because they are no-ops"
269    )]
270    fn on_spawn_light_spot(
271        &mut self,
272        load_context: &mut LoadContext<'_>,
273        gltf_node: &Node,
274        entity: &mut EntityWorldMut,
275    ) {
276    }
277}
278
279/// Type-erased version of [`GltfExtensionHandler`].
280/// This is used to store heterogeneous handlers in a collection.
281pub trait ErasedGltfExtensionHandler: Send + Sync + 'static {
282    /// Required for dyn cloning
283    fn dyn_clone(&self) -> Box<dyn ErasedGltfExtensionHandler>;
284
285    /// Called when the "global" data for an extension
286    /// at the root of a glTF file is encountered.
287    fn on_root(
288        &mut self,
289        load_context: &mut LoadContext<'_>,
290        gltf: &gltf::Gltf,
291        settings: &GltfLoaderSettings,
292    );
293
294    #[cfg(feature = "bevy_animation")]
295    /// Called when an individual animation is processed
296    fn on_animation(
297        &mut self,
298        load_context: &mut LoadContext<'_>,
299        gltf_animation: &gltf::Animation,
300        animation_clip: &mut AnimationClip,
301    );
302
303    #[cfg(feature = "bevy_animation")]
304    /// Called when all animations have been collected.
305    fn on_animations_collected(
306        &mut self,
307        load_context: &mut LoadContext<'_>,
308        animations: &[Handle<AnimationClip>],
309        named_animations: &HashMap<Box<str>, Handle<AnimationClip>>,
310        animation_roots: &HashSet<usize>,
311    );
312
313    /// Called when an individual texture is processed
314    fn on_texture(&mut self, gltf_texture: &gltf::Texture, texture: Handle<bevy_image::Image>);
315
316    /// Called when an individual material is processed
317    fn on_material(
318        &mut self,
319        load_context: &mut LoadContext<'_>,
320        gltf_material: &gltf::Material,
321        material: Handle<GltfMaterial>,
322        material_asset: &GltfMaterial,
323        material_label: &str,
324    );
325
326    /// Called when an individual glTF primitive is processed
327    fn on_gltf_primitive<'a>(
328        &'a mut self,
329        load_context: &'a mut LoadContext<'_>,
330        gltf_document: &'a gltf::Gltf,
331        gltf_mesh: &'a gltf::Mesh,
332        gltf_primitive: &'a gltf::Primitive,
333        buffer_data: &'a [Vec<u8>],
334        custom_vertex_attributes: &'a HashMap<Box<str>, MeshVertexAttribute>,
335        gltf_mesh_on_skinned_nodes: bool,
336        gltf_mesh_on_non_skinned_nodes: bool,
337        user_mesh: &'a mut Option<Mesh>,
338    ) -> BoxedFuture<'a, ()>;
339
340    /// Called when an individual glTF Mesh is processed
341    fn on_gltf_mesh(
342        &mut self,
343        load_context: &mut LoadContext<'_>,
344        gltf_mesh: &gltf::Mesh,
345        mesh: Handle<GltfMesh>,
346    );
347
348    /// Called when mesh and material are spawned as a single Entity
349    fn on_spawn_mesh_and_material(
350        &mut self,
351        load_context: &mut LoadContext<'_>,
352        primitive: &gltf::Primitive,
353        mesh: &gltf::Mesh,
354        material: &gltf::Material,
355        entity: &mut EntityWorldMut,
356        material_label: &str,
357    );
358
359    /// Called when an individual Scene is done processing
360    fn on_scene_completed(
361        &mut self,
362        load_context: &mut LoadContext<'_>,
363        scene: &gltf::Scene,
364        world_root_id: Entity,
365        scene_world: &mut World,
366    );
367
368    /// Called when a node is processed
369    fn on_gltf_node(
370        &mut self,
371        load_context: &mut LoadContext<'_>,
372        gltf_node: &Node,
373        entity: &mut EntityWorldMut,
374    );
375
376    /// Called when a `DirectionalLight` node is spawned
377    fn on_spawn_light_directional(
378        &mut self,
379        load_context: &mut LoadContext<'_>,
380        gltf_node: &Node,
381        entity: &mut EntityWorldMut,
382    );
383
384    /// Called when a `PointLight` node is spawned
385    fn on_spawn_light_point(
386        &mut self,
387        load_context: &mut LoadContext<'_>,
388        gltf_node: &Node,
389        entity: &mut EntityWorldMut,
390    );
391
392    /// Called when a `SpotLight` node is spawned
393    fn on_spawn_light_spot(
394        &mut self,
395        load_context: &mut LoadContext<'_>,
396        gltf_node: &Node,
397        entity: &mut EntityWorldMut,
398    );
399}
400
401impl<H: GltfExtensionHandler> ErasedGltfExtensionHandler for H {
402    fn dyn_clone(&self) -> Box<dyn ErasedGltfExtensionHandler> {
403        Self::dyn_clone(self)
404    }
405
406    fn on_root(
407        &mut self,
408        load_context: &mut LoadContext<'_>,
409        gltf: &gltf::Gltf,
410        settings: &GltfLoaderSettings,
411    ) {
412        Self::on_root(self, load_context, gltf, settings);
413    }
414
415    #[cfg(feature = "bevy_animation")]
416    fn on_animation(
417        &mut self,
418        load_context: &mut LoadContext<'_>,
419        gltf_animation: &gltf::Animation,
420        animation_clip: &mut AnimationClip,
421    ) {
422        Self::on_animation(self, load_context, gltf_animation, animation_clip);
423    }
424
425    #[cfg(feature = "bevy_animation")]
426    fn on_animations_collected(
427        &mut self,
428        load_context: &mut LoadContext<'_>,
429        animations: &[Handle<AnimationClip>],
430        named_animations: &HashMap<Box<str>, Handle<AnimationClip>>,
431        animation_roots: &HashSet<usize>,
432    ) {
433        Self::on_animations_collected(
434            self,
435            load_context,
436            animations,
437            named_animations,
438            animation_roots,
439        );
440    }
441
442    fn on_texture(&mut self, gltf_texture: &gltf::Texture, texture: Handle<bevy_image::Image>) {
443        Self::on_texture(self, gltf_texture, texture);
444    }
445
446    fn on_material(
447        &mut self,
448        load_context: &mut LoadContext<'_>,
449        gltf_material: &gltf::Material,
450        material: Handle<GltfMaterial>,
451        material_asset: &GltfMaterial,
452        material_label: &str,
453    ) {
454        Self::on_material(
455            self,
456            load_context,
457            gltf_material,
458            material,
459            material_asset,
460            material_label,
461        );
462    }
463
464    fn on_gltf_primitive<'a>(
465        &'a mut self,
466        load_context: &'a mut LoadContext<'_>,
467        gltf_document: &'a gltf::Gltf,
468        gltf_mesh: &'a gltf::Mesh,
469        gltf_primitive: &'a gltf::Primitive,
470        buffer_data: &'a [Vec<u8>],
471        custom_vertex_attributes: &'a HashMap<Box<str>, MeshVertexAttribute>,
472        gltf_mesh_on_skinned_nodes: bool,
473        gltf_mesh_on_non_skinned_nodes: bool,
474        user_mesh: &'a mut Option<Mesh>,
475    ) -> BoxedFuture<'a, ()> {
476        Box::pin(async move {
477            Self::on_gltf_primitive(
478                self,
479                load_context,
480                gltf_document,
481                gltf_mesh,
482                gltf_primitive,
483                buffer_data,
484                custom_vertex_attributes,
485                gltf_mesh_on_skinned_nodes,
486                gltf_mesh_on_non_skinned_nodes,
487                user_mesh,
488            )
489            .await;
490        })
491    }
492
493    fn on_gltf_mesh(
494        &mut self,
495        load_context: &mut LoadContext<'_>,
496        gltf_mesh: &gltf::Mesh,
497        mesh: Handle<GltfMesh>,
498    ) {
499        Self::on_gltf_mesh(self, load_context, gltf_mesh, mesh);
500    }
501
502    fn on_spawn_mesh_and_material(
503        &mut self,
504        load_context: &mut LoadContext<'_>,
505        primitive: &gltf::Primitive,
506        mesh: &gltf::Mesh,
507        material: &gltf::Material,
508        entity: &mut EntityWorldMut,
509        material_label: &str,
510    ) {
511        Self::on_spawn_mesh_and_material(
512            self,
513            load_context,
514            primitive,
515            mesh,
516            material,
517            entity,
518            material_label,
519        );
520    }
521
522    fn on_scene_completed(
523        &mut self,
524        load_context: &mut LoadContext<'_>,
525        scene: &gltf::Scene,
526        world_root_id: Entity,
527        scene_world: &mut World,
528    ) {
529        Self::on_scene_completed(self, load_context, scene, world_root_id, scene_world);
530    }
531
532    fn on_gltf_node(
533        &mut self,
534        load_context: &mut LoadContext<'_>,
535        gltf_node: &Node,
536        entity: &mut EntityWorldMut,
537    ) {
538        Self::on_gltf_node(self, load_context, gltf_node, entity);
539    }
540
541    fn on_spawn_light_directional(
542        &mut self,
543        load_context: &mut LoadContext<'_>,
544        gltf_node: &Node,
545        entity: &mut EntityWorldMut,
546    ) {
547        Self::on_spawn_light_directional(self, load_context, gltf_node, entity);
548    }
549
550    fn on_spawn_light_point(
551        &mut self,
552        load_context: &mut LoadContext<'_>,
553        gltf_node: &Node,
554        entity: &mut EntityWorldMut,
555    ) {
556        Self::on_spawn_light_point(self, load_context, gltf_node, entity);
557    }
558
559    fn on_spawn_light_spot(
560        &mut self,
561        load_context: &mut LoadContext<'_>,
562        gltf_node: &Node,
563        entity: &mut EntityWorldMut,
564    ) {
565        Self::on_spawn_light_spot(self, load_context, gltf_node, entity);
566    }
567}
568
569impl Clone for Box<dyn ErasedGltfExtensionHandler> {
570    fn clone(&self) -> Self {
571        self.dyn_clone()
572    }
573}