Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions crates/bevy_gltf/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand All @@ -214,6 +277,11 @@ pub struct GltfPlugin {
///
/// To specify, use [`GltfPlugin::add_custom_vertex_attribute`].
pub custom_vertex_attributes: HashMap<Box<str>, 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 {
Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -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,
});
}
}
131 changes: 76 additions & 55 deletions crates/bevy_gltf/src/loader/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,15 @@ use gltf::{
};

use serde::{Deserialize, Serialize};
#[cfg(feature = "bevy_animation")]
use smallvec::SmallVec;

use thiserror::Error;
use tracing::{error, info_span, warn};

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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<bool>,

/// 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<GltfAnimationSettings>,
}

impl Default for GltfLoaderSettings {
Expand All @@ -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(),
}
}
}
Expand Down Expand Up @@ -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,
);
Expand All @@ -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);
Expand Down Expand Up @@ -1409,8 +1414,6 @@ fn load_node(
entity_to_skin_index_map: &mut EntityHashMap<usize>,
active_camera_found: &mut bool,
parent_transform: &Transform,
#[cfg(feature = "bevy_animation")] animation_roots: &HashSet<usize>,
#[cfg(feature = "bevy_animation")] mut animation_context: Option<AnimationContext>,
document: &Document,
convert_coordinates: bool,
) -> Result<(), GltfError> {
Expand All @@ -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(),
Expand Down Expand Up @@ -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,
) {
Expand Down Expand Up @@ -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<String>,
}

// Create `AnimationPlayer`, `AnimatedBy`, and `AnimationTargetId` components.
#[cfg(feature = "bevy_animation")]
fn create_animation_components(
world: &mut World,
loader: &GltfLoader,
settings: &GltfLoaderSettings,
paths: &HashMap<usize, (usize, Vec<Name>)>,
animation_roots: &HashSet<usize>,
scene: &gltf::Scene<'_>,
node_index_to_entity_map: &HashMap<usize, Entity>,
) {
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;
Expand Down