diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index bb01fb034fc30..e9c9e8ad95591 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -136,6 +136,8 @@ mod vertex_attributes; extern crate alloc; use alloc::sync::Arc; +#[cfg(feature = "bevy_animation")] +use serde::{Deserialize, Serialize}; use std::sync::Mutex; use tracing::warn; @@ -189,6 +191,67 @@ impl DefaultGltfImageSampler { } } +/// Decides if the loader will create [`AnimationTargetId`] components. These +/// are used to identify which parts of an [`AnimationClip`] can be applied to +/// a node. +/// +/// [`AnimationTargetId`]: bevy_animation::AnimationTargetId +/// [`AnimationClip`]: bevy_animation::AnimationClip +#[cfg(feature = "bevy_animation")] +#[derive(Default, Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize)] +pub enum GltfCreateAnimationTargetIds { + /// Never create `AnimationTargetId`s. + Never, + /// Always create `AnimationTargetId`s. This is typically used when the glTF + /// does not contain animations itself, but might be bound to animations in + /// another glTF or some other source. + Always, + /// Only create `AnimationTargetId`s for a hierarchy if at least one node in + /// the hierarchy is affected by an animation within the glTF. + #[default] + Automatically, +} + +/// Decides if the loader will create [`AnimationPlayer`] and [`AnimatedBy`] +/// components. +/// +/// These components are only created if a hierarchy has [`AnimationTargetId`] +/// components (see [`GltfCreateAnimationTargetIds`]). `AnimationPlayer` components +/// are created on the root node of a hierarchy. `AnimatedBy` components are +/// created on all nodes in a hierarchy, alongside the `AnimationTargetId` +/// components. +/// +/// [`AnimationTargetId`]: bevy_animation::AnimationTargetId +/// [`AnimatedBy`]: bevy_animation::AnimatedBy +/// [`AnimationPlayer`]: bevy_animation::AnimationPlayer +#[cfg(feature = "bevy_animation")] +#[derive(Default, Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize)] +pub enum GltfCreateAnimationPlayers { + /// Never create `AnimationPlayer` and `AnimatedBy` components. + Never, + /// Only create `AnimationPlayer` and `AnimatedBy` components if + /// the hierarchy has `AnimationTargetId` components. + #[default] + Automatically, +} + +/// Animation specific settings. Used by [`GltfPlugin`] and +/// [`GltfLoaderSettings`]. +#[cfg(feature = "bevy_animation")] +#[derive(Default, Clone, Copy, Eq, PartialEq, Debug, Serialize, Deserialize)] +pub struct GltfAnimationSettings { + /// Decides if the loader will create [`AnimationTargetId`] components. + /// + /// [`AnimationTargetId`]: bevy_animation::AnimationTargetId + pub create_target_ids: GltfCreateAnimationTargetIds, + /// Decides if the loader will create [`AnimationPlayer`] and [`AnimatedBy`] + /// components. + /// + /// [`AnimatedBy`]: bevy_animation::AnimatedBy + /// [`AnimationPlayer`]: bevy_animation::AnimationPlayer + pub create_players: GltfCreateAnimationPlayers, +} + /// Adds support for glTF file loading to the app. pub struct GltfPlugin { /// The default image sampler to lay glTF sampler data on top of. @@ -214,6 +277,11 @@ pub struct GltfPlugin { /// /// To specify, use [`GltfPlugin::add_custom_vertex_attribute`]. pub custom_vertex_attributes: HashMap, MeshVertexAttribute>, + + /// The default animation settings. These can be overridden per-load by + /// [`GltfLoaderSettings::animation_settings`]. + #[cfg(feature = "bevy_animation")] + pub animation_settings: GltfAnimationSettings, } impl Default for GltfPlugin { @@ -222,6 +290,8 @@ impl Default for GltfPlugin { default_sampler: ImageSamplerDescriptor::linear(), custom_vertex_attributes: HashMap::default(), use_model_forward_direction: false, + #[cfg(feature = "bevy_animation")] + animation_settings: Default::default(), } } } @@ -272,6 +342,8 @@ impl Plugin for GltfPlugin { custom_vertex_attributes: self.custom_vertex_attributes.clone(), default_sampler, default_use_model_forward_direction: self.use_model_forward_direction, + #[cfg(feature = "bevy_animation")] + default_animation_settings: self.animation_settings, }); } } diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 7c6c0eaa56ca9..8d637345948a5 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -50,9 +50,6 @@ use gltf::{ }; use serde::{Deserialize, Serialize}; -#[cfg(feature = "bevy_animation")] -use smallvec::SmallVec; - use thiserror::Error; use tracing::{error, info_span, warn}; @@ -60,6 +57,8 @@ use crate::{ vertex_attributes::convert_attribute, Gltf, GltfAssetLabel, GltfExtras, GltfMaterialExtras, GltfMaterialName, GltfMeshExtras, GltfMeshName, GltfNode, GltfSceneExtras, GltfSkin, }; +#[cfg(feature = "bevy_animation")] +use crate::{GltfAnimationSettings, GltfCreateAnimationPlayers, GltfCreateAnimationTargetIds}; #[cfg(feature = "bevy_animation")] use self::gltf_ext::scene::collect_path; @@ -160,6 +159,10 @@ pub struct GltfLoader { /// /// The default is `false`. pub default_use_model_forward_direction: bool, + /// The default animation settings. These can be be overridden per-load by + /// [`GltfLoaderSettings::animation_settings`]. + #[cfg(feature = "bevy_animation")] + pub default_animation_settings: GltfAnimationSettings, } /// Specifies optional settings for processing gltfs at load time. By default, all recognized contents of @@ -218,6 +221,12 @@ pub struct GltfLoaderSettings { /// /// If `None`, uses the global default set by [`GltfPlugin::use_model_forward_direction`](crate::GltfPlugin::use_model_forward_direction). pub use_model_forward_direction: Option, + + /// Overrides the default animation settings. + /// + /// If `None`, uses the global default set by [`GltfPlugin::animation_settings`](crate::GltfPlugin::animation_settings). + #[cfg(feature = "bevy_animation")] + pub animation_settings: Option, } impl Default for GltfLoaderSettings { @@ -232,6 +241,8 @@ impl Default for GltfLoaderSettings { default_sampler: None, override_sampler: false, use_model_forward_direction: None, + #[cfg(feature = "bevy_animation")] + animation_settings: Default::default(), } } } @@ -963,10 +974,6 @@ impl GltfLoader { &mut entity_to_skin_index_map, &mut active_camera_found, &Transform::default(), - #[cfg(feature = "bevy_animation")] - &animation_roots, - #[cfg(feature = "bevy_animation")] - None, &gltf.document, convert_coordinates, ); @@ -989,17 +996,15 @@ impl GltfLoader { } #[cfg(feature = "bevy_animation")] - { - // for each node root in a scene, check if it's the root of an animation - // if it is, add the AnimationPlayer component - for node in scene.nodes() { - if animation_roots.contains(&node.index()) { - world - .entity_mut(*node_index_to_entity_map.get(&node.index()).unwrap()) - .insert(AnimationPlayer::default()); - } - } - } + create_animation_components( + &mut world, + loader, + settings, + &paths, + &animation_roots, + &scene, + &node_index_to_entity_map, + ); for (&entity, &skin_index) in &entity_to_skin_index_map { let mut entity = world.entity_mut(entity); @@ -1409,8 +1414,6 @@ fn load_node( entity_to_skin_index_map: &mut EntityHashMap, active_camera_found: &mut bool, parent_transform: &Transform, - #[cfg(feature = "bevy_animation")] animation_roots: &HashSet, - #[cfg(feature = "bevy_animation")] mut animation_context: Option, document: &Document, convert_coordinates: bool, ) -> Result<(), GltfError> { @@ -1429,25 +1432,6 @@ fn load_node( let name = node_name(gltf_node); node.insert(name.clone()); - #[cfg(feature = "bevy_animation")] - if animation_context.is_none() && animation_roots.contains(&gltf_node.index()) { - // This is an animation root. Make a new animation context. - animation_context = Some(AnimationContext { - root: node.id(), - path: SmallVec::new(), - }); - } - - #[cfg(feature = "bevy_animation")] - if let Some(ref mut animation_context) = animation_context { - animation_context.path.push(name); - - node.insert(( - AnimationTargetId::from_names(animation_context.path.iter()), - AnimatedBy(animation_context.root), - )); - } - if let Some(extras) = gltf_node.extras() { node.insert(GltfExtras { value: extras.get().to_string(), @@ -1688,10 +1672,6 @@ fn load_node( entity_to_skin_index_map, active_camera_found, &world_transform, - #[cfg(feature = "bevy_animation")] - animation_roots, - #[cfg(feature = "bevy_animation")] - animation_context.clone(), document, convert_coordinates, ) { @@ -1881,24 +1861,65 @@ impl<'s> Iterator for PrimitiveMorphAttributesIter<'s> { } } -/// A helper structure for `load_node` that contains information about the -/// nearest ancestor animation root. -#[cfg(feature = "bevy_animation")] -#[derive(Clone)] -struct AnimationContext { - /// The nearest ancestor animation root. - pub root: Entity, - /// The path to the animation root. This is used for constructing the - /// animation target UUIDs. - pub path: SmallVec<[Name; 8]>, -} - #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct MorphTargetNames { pub target_names: Vec, } +// Create `AnimationPlayer`, `AnimatedBy`, and `AnimationTargetId` components. +#[cfg(feature = "bevy_animation")] +fn create_animation_components( + world: &mut World, + loader: &GltfLoader, + settings: &GltfLoaderSettings, + paths: &HashMap)>, + animation_roots: &HashSet, + scene: &gltf::Scene<'_>, + node_index_to_entity_map: &HashMap, +) { + let animation_settings = settings + .animation_settings + .unwrap_or(loader.default_animation_settings); + + if animation_settings.create_target_ids != GltfCreateAnimationTargetIds::Never { + // Add `AnimationTargetId` and `AnimatedBy` components to + // nodes, following the rules in `animation_settings`. + for (node_index, &entity_id) in node_index_to_entity_map { + let (root_node_index, path) = paths.get(node_index).unwrap(); + + if (animation_settings.create_target_ids == GltfCreateAnimationTargetIds::Always) + || animation_roots.contains(root_node_index) + { + let mut entity = world.entity_mut(entity_id); + + entity.insert(AnimationTargetId::from_names(path.iter())); + + if animation_settings.create_players == GltfCreateAnimationPlayers::Automatically { + entity.insert(AnimatedBy( + *node_index_to_entity_map.get(root_node_index).unwrap(), + )); + } + } + } + + if animation_settings.create_players == GltfCreateAnimationPlayers::Automatically { + // Add `AnimationPlayer` components to the root node of + // hierarchies that we know contain `AnimationTargetId` + // and `AnimatedBy` components. + for node in scene.nodes() { + if (animation_settings.create_target_ids == GltfCreateAnimationTargetIds::Always) + || animation_roots.contains(&node.index()) + { + world + .entity_mut(*node_index_to_entity_map.get(&node.index()).unwrap()) + .insert(AnimationPlayer::default()); + } + } + } + } +} + #[cfg(test)] mod test { use std::path::Path;