diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 991d0776b8795..b7bca04797a35 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -59,6 +59,8 @@ gltf = { version = "1.4.0", default-features = false, features = [ ] } thiserror = { version = "2", default-features = false } base64 = "0.22.0" +fixedbitset = "0.5" +itertools = "0.13" percent-encoding = "2.1" serde = { version = "1.0", features = ["derive"] } serde_json = "1" diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index b4453ab24fe1a..1a8e170ee793f 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -3,7 +3,6 @@ use crate::{ GltfMaterialName, GltfMeshExtras, GltfNode, GltfSceneExtras, GltfSkin, }; -use alloc::collections::VecDeque; use bevy_asset::{ io::Reader, AssetLoadError, AssetLoader, Handle, LoadContext, ReadAssetBytesError, }; @@ -42,6 +41,7 @@ use bevy_scene::Scene; #[cfg(not(target_arch = "wasm32"))] use bevy_tasks::IoTaskPool; use bevy_transform::components::Transform; +use fixedbitset::FixedBitSet; use gltf::{ accessor::Iter, image::Source, @@ -50,6 +50,7 @@ use gltf::{ texture::{Info, MagFilter, MinFilter, TextureTransform, WrappingMode}, Document, Material, Node, Primitive, Semantic, }; +use itertools::Itertools; use serde::{Deserialize, Serialize}; #[cfg(any( feature = "pbr_specular_textures", @@ -793,13 +794,36 @@ async fn load_gltf<'a, 'b, 'c>( let mut named_nodes = >::default(); let mut skins = vec![]; let mut named_skins = >::default(); - for node in GltfTreeIterator::try_new(&gltf)? { + + // First, create the node handles. + for node in gltf.nodes() { + let label = GltfAssetLabel::Node(node.index()); + let label_handle = load_context.get_label_handle(label.to_string()); + nodes.insert(node.index(), label_handle); + } + + // Then check for cycles. + check_gltf_for_cycles(&gltf)?; + + // Now populate the nodes. + for node in gltf.nodes() { let skin = node.skin().map(|skin| { - let joints = skin + let joints: Vec<_> = skin .joints() .map(|joint| nodes.get(&joint.index()).unwrap().clone()) .collect(); + if joints.len() > MAX_JOINTS { + warn!( + "The glTF skin {} has {} joints, but the maximum supported is {}", + skin.name() + .map(ToString::to_string) + .unwrap_or_else(|| skin.index().to_string()), + joints.len(), + MAX_JOINTS + ); + } + let gltf_skin = GltfSkin::new( &skin, joints, @@ -1904,114 +1928,6 @@ async fn load_buffers( Ok(buffer_data) } -/// Iterator for a Gltf tree. -/// -/// It resolves a Gltf tree and allows for a safe Gltf nodes iteration, -/// putting dependent nodes before dependencies. -struct GltfTreeIterator<'a> { - nodes: Vec>, -} - -impl<'a> GltfTreeIterator<'a> { - #[expect( - clippy::result_large_err, - reason = "`GltfError` is only barely past the threshold for large errors." - )] - fn try_new(gltf: &'a gltf::Gltf) -> Result { - let nodes = gltf.nodes().collect::>(); - - let mut empty_children = VecDeque::new(); - let mut parents = vec![None; nodes.len()]; - let mut unprocessed_nodes = nodes - .into_iter() - .enumerate() - .map(|(i, node)| { - let children = node - .children() - .map(|child| child.index()) - .collect::>(); - for &child in &children { - let parent = parents.get_mut(child).unwrap(); - *parent = Some(i); - } - if children.is_empty() { - empty_children.push_back(i); - } - (i, (node, children)) - }) - .collect::>(); - - let mut nodes = Vec::new(); - let mut warned_about_max_joints = >::default(); - while let Some(index) = empty_children.pop_front() { - if let Some(skin) = unprocessed_nodes.get(&index).unwrap().0.skin() { - if skin.joints().len() > MAX_JOINTS && warned_about_max_joints.insert(skin.index()) - { - warn!( - "The glTF skin {} has {} joints, but the maximum supported is {}", - skin.name() - .map(ToString::to_string) - .unwrap_or_else(|| skin.index().to_string()), - skin.joints().len(), - MAX_JOINTS - ); - } - - let skin_has_dependencies = skin - .joints() - .any(|joint| unprocessed_nodes.contains_key(&joint.index())); - - if skin_has_dependencies && unprocessed_nodes.len() != 1 { - empty_children.push_back(index); - continue; - } - } - - let (node, children) = unprocessed_nodes.remove(&index).unwrap(); - assert!(children.is_empty()); - nodes.push(node); - - if let Some(parent_index) = parents[index] { - let (_, parent_children) = unprocessed_nodes.get_mut(&parent_index).unwrap(); - - assert!(parent_children.remove(&index)); - if parent_children.is_empty() { - empty_children.push_back(parent_index); - } - } - } - - if !unprocessed_nodes.is_empty() { - return Err(GltfError::CircularChildren(format!( - "{:?}", - unprocessed_nodes - .iter() - .map(|(k, _v)| *k) - .collect::>(), - ))); - } - - nodes.reverse(); - Ok(Self { - nodes: nodes.into_iter().collect(), - }) - } -} - -impl<'a> Iterator for GltfTreeIterator<'a> { - type Item = Node<'a>; - - fn next(&mut self) -> Option { - self.nodes.pop() - } -} - -impl<'a> ExactSizeIterator for GltfTreeIterator<'a> { - fn len(&self) -> usize { - self.nodes.len() - } -} - enum ImageOrPath { Image { image: Image, @@ -2415,6 +2331,51 @@ fn material_needs_tangents(material: &Material) -> bool { false } +/// Checks all glTF nodes for cycles, starting at the scene root. +#[expect( + clippy::result_large_err, + reason = "need to be signature compatible with `load_gltf`" +)] +fn check_gltf_for_cycles(gltf: &gltf::Gltf) -> Result<(), GltfError> { + // Initialize with the scene roots. + let mut roots = FixedBitSet::with_capacity(gltf.nodes().len()); + for root in gltf.scenes().flat_map(|scene| scene.nodes()) { + roots.insert(root.index()); + } + + // Check each one. + let mut visited = FixedBitSet::with_capacity(gltf.nodes().len()); + for root in roots.ones() { + check(gltf.nodes().nth(root).unwrap(), &mut visited)?; + } + return Ok(()); + + // Depth first search. + #[expect( + clippy::result_large_err, + reason = "need to be signature compatible with `load_gltf`" + )] + fn check(node: Node, visited: &mut FixedBitSet) -> Result<(), GltfError> { + // Do we have a cycle? + if visited.contains(node.index()) { + return Err(GltfError::CircularChildren(format!( + "glTF nodes form a cycle: {} -> {}", + visited.ones().map(|bit| bit.to_string()).join(" -> "), + node.index() + ))); + } + + // Recurse. + visited.insert(node.index()); + for kid in node.children() { + check(kid, visited)?; + } + visited.remove(node.index()); + + Ok(()) + } +} + #[cfg(test)] mod test { use std::path::Path;