diff --git a/crates/bevy_gltf/src/assets.rs b/crates/bevy_gltf/src/assets.rs new file mode 100644 index 0000000000000..e8a7788168009 --- /dev/null +++ b/crates/bevy_gltf/src/assets.rs @@ -0,0 +1,315 @@ +//! Representation of assets present in a glTF file + +#[cfg(feature = "bevy_animation")] +use bevy_animation::AnimationClip; +use bevy_asset::{Asset, Handle}; +use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_pbr::StandardMaterial; +use bevy_platform_support::collections::HashMap; +use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath}; +use bevy_render::mesh::{skinning::SkinnedMeshInverseBindposes, Mesh}; +use bevy_scene::Scene; + +use crate::GltfAssetLabel; + +/// Representation of a loaded glTF file. +#[derive(Asset, Debug, TypePath)] +pub struct Gltf { + /// All scenes loaded from the glTF file. + pub scenes: Vec>, + /// Named scenes loaded from the glTF file. + pub named_scenes: HashMap, Handle>, + /// All meshes loaded from the glTF file. + pub meshes: Vec>, + /// Named meshes loaded from the glTF file. + pub named_meshes: HashMap, Handle>, + /// All materials loaded from the glTF file. + pub materials: Vec>, + /// Named materials loaded from the glTF file. + pub named_materials: HashMap, Handle>, + /// All nodes loaded from the glTF file. + pub nodes: Vec>, + /// Named nodes loaded from the glTF file. + pub named_nodes: HashMap, Handle>, + /// All skins loaded from the glTF file. + pub skins: Vec>, + /// Named skins loaded from the glTF file. + pub named_skins: HashMap, Handle>, + /// Default scene to be displayed. + pub default_scene: Option>, + /// All animations loaded from the glTF file. + #[cfg(feature = "bevy_animation")] + pub animations: Vec>, + /// Named animations loaded from the glTF file. + #[cfg(feature = "bevy_animation")] + pub named_animations: HashMap, Handle>, + /// The gltf root of the gltf asset, see . Only has a value when `GltfLoaderSettings::include_source` is true. + pub source: Option, +} + +/// A glTF mesh, which may consist of multiple [`GltfPrimitives`](GltfPrimitive) +/// and an optional [`GltfExtras`]. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-mesh). +#[derive(Asset, Debug, Clone, TypePath)] +pub struct GltfMesh { + /// Index of the mesh inside the scene + pub index: usize, + /// Computed name for a mesh - either a user defined mesh name from gLTF or a generated name from index + pub name: String, + /// Primitives of the glTF mesh. + pub primitives: Vec, + /// Additional data. + pub extras: Option, +} + +impl GltfMesh { + /// Create a mesh extracting name and index from glTF def + pub fn new( + mesh: &gltf::Mesh, + primitives: Vec, + extras: Option, + ) -> Self { + Self { + index: mesh.index(), + name: if let Some(name) = mesh.name() { + name.to_string() + } else { + format!("GltfMesh{}", mesh.index()) + }, + primitives, + extras, + } + } + + /// Subasset label for this mesh within the gLTF parent asset. + pub fn asset_label(&self) -> GltfAssetLabel { + GltfAssetLabel::Mesh(self.index) + } +} + +/// A glTF node with all of its child nodes, its [`GltfMesh`], +/// [`Transform`](bevy_transform::prelude::Transform), its optional [`GltfSkin`] +/// and an optional [`GltfExtras`]. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-node). +#[derive(Asset, Debug, Clone, TypePath)] +pub struct GltfNode { + /// Index of the node inside the scene + pub index: usize, + /// Computed name for a node - either a user defined node name from gLTF or a generated name from index + pub name: String, + /// Direct children of the node. + pub children: Vec>, + /// Mesh of the node. + pub mesh: Option>, + /// Skin of the node. + pub skin: Option>, + /// Local transform. + pub transform: bevy_transform::prelude::Transform, + /// Is this node used as an animation root + #[cfg(feature = "bevy_animation")] + pub is_animation_root: bool, + /// Additional data. + pub extras: Option, +} + +impl GltfNode { + /// Create a node extracting name and index from glTF def + pub fn new( + node: &gltf::Node, + children: Vec>, + mesh: Option>, + transform: bevy_transform::prelude::Transform, + skin: Option>, + extras: Option, + ) -> Self { + Self { + index: node.index(), + name: if let Some(name) = node.name() { + name.to_string() + } else { + format!("GltfNode{}", node.index()) + }, + children, + mesh, + transform, + skin, + #[cfg(feature = "bevy_animation")] + is_animation_root: false, + extras, + } + } + + /// Create a node with animation root mark + #[cfg(feature = "bevy_animation")] + pub fn with_animation_root(self, is_animation_root: bool) -> Self { + Self { + is_animation_root, + ..self + } + } + + /// Subasset label for this node within the gLTF parent asset. + pub fn asset_label(&self) -> GltfAssetLabel { + GltfAssetLabel::Node(self.index) + } +} + +/// Part of a [`GltfMesh`] that consists of a [`Mesh`], an optional [`StandardMaterial`] and [`GltfExtras`]. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-mesh-primitive). +#[derive(Asset, Debug, Clone, TypePath)] +pub struct GltfPrimitive { + /// Index of the primitive inside the mesh + pub index: usize, + /// Index of the parent [`GltfMesh`] of this primitive + pub parent_mesh_index: usize, + /// Computed name for a primitive - either a user defined primitive name from gLTF or a generated name from index + pub name: String, + /// Topology to be rendered. + pub mesh: Handle, + /// Material to apply to the `mesh`. + pub material: Option>, + /// Additional data. + pub extras: Option, + /// Additional data of the `material`. + pub material_extras: Option, +} + +impl GltfPrimitive { + /// Create a primitive extracting name and index from glTF def + pub fn new( + gltf_mesh: &gltf::Mesh, + gltf_primitive: &gltf::Primitive, + mesh: Handle, + material: Option>, + extras: Option, + material_extras: Option, + ) -> Self { + GltfPrimitive { + index: gltf_primitive.index(), + parent_mesh_index: gltf_mesh.index(), + name: { + let mesh_name = gltf_mesh.name().unwrap_or("Mesh"); + if gltf_mesh.primitives().len() > 1 { + format!("{}.{}", mesh_name, gltf_primitive.index()) + } else { + mesh_name.to_string() + } + }, + mesh, + material, + extras, + material_extras, + } + } + + /// Subasset label for this primitive within its parent [`GltfMesh`] within the gLTF parent asset. + pub fn asset_label(&self) -> GltfAssetLabel { + GltfAssetLabel::Primitive { + mesh: self.parent_mesh_index, + primitive: self.index, + } + } +} + +/// A glTF skin with all of its joint nodes, [`SkinnedMeshInversiveBindposes`](bevy_render::mesh::skinning::SkinnedMeshInverseBindposes) +/// and an optional [`GltfExtras`]. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-skin). +#[derive(Asset, Debug, Clone, TypePath)] +pub struct GltfSkin { + /// Index of the skin inside the scene + pub index: usize, + /// Computed name for a skin - either a user defined skin name from gLTF or a generated name from index + pub name: String, + /// All the nodes that form this skin. + pub joints: Vec>, + /// Inverse-bind matrices of this skin. + pub inverse_bind_matrices: Handle, + /// Additional data. + pub extras: Option, +} + +impl GltfSkin { + /// Create a skin extracting name and index from glTF def + pub fn new( + skin: &gltf::Skin, + joints: Vec>, + inverse_bind_matrices: Handle, + extras: Option, + ) -> Self { + Self { + index: skin.index(), + name: if let Some(name) = skin.name() { + name.to_string() + } else { + format!("GltfSkin{}", skin.index()) + }, + joints, + inverse_bind_matrices, + extras, + } + } + + /// Subasset label for this skin within the gLTF parent asset. + pub fn asset_label(&self) -> GltfAssetLabel { + GltfAssetLabel::Skin(self.index) + } +} + +/// Additional untyped data that can be present on most glTF types at the primitive level. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras). +#[derive(Clone, Debug, Reflect, Default, Component)] +#[reflect(Component, Default, Debug)] +pub struct GltfExtras { + /// Content of the extra data. + pub value: String, +} + +impl From<&serde_json::value::RawValue> for GltfExtras { + fn from(value: &serde_json::value::RawValue) -> Self { + GltfExtras { + value: value.get().to_string(), + } + } +} + +/// Additional untyped data that can be present on most glTF types at the scene level. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras). +#[derive(Clone, Debug, Reflect, Default, Component)] +#[reflect(Component, Default, Debug)] +pub struct GltfSceneExtras { + /// Content of the extra data. + pub value: String, +} + +/// Additional untyped data that can be present on most glTF types at the mesh level. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras). +#[derive(Clone, Debug, Reflect, Default, Component)] +#[reflect(Component, Default, Debug)] +pub struct GltfMeshExtras { + /// Content of the extra data. + pub value: String, +} + +/// Additional untyped data that can be present on most glTF types at the material level. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras). +#[derive(Clone, Debug, Reflect, Default, Component)] +#[reflect(Component, Default, Debug)] +pub struct GltfMaterialExtras { + /// Content of the extra data. + pub value: String, +} + +/// The material name of a glTF primitive. +/// +/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-material). +#[derive(Clone, Debug, Reflect, Default, Component)] +#[reflect(Component)] +pub struct GltfMaterialName(pub String); diff --git a/crates/bevy_gltf/src/label.rs b/crates/bevy_gltf/src/label.rs new file mode 100644 index 0000000000000..3a1ddfdf10f16 --- /dev/null +++ b/crates/bevy_gltf/src/label.rs @@ -0,0 +1,127 @@ +//! Labels that can be used to load part of a glTF + +use bevy_asset::AssetPath; + +/// Labels that can be used to load part of a glTF +/// +/// You can use [`GltfAssetLabel::from_asset`] to add it to an asset path +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_asset::prelude::*; +/// # use bevy_scene::prelude::*; +/// # use bevy_gltf::prelude::*; +/// +/// fn load_gltf_scene(asset_server: Res) { +/// let gltf_scene: Handle = asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")); +/// } +/// ``` +/// +/// Or when formatting a string for the path +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # use bevy_asset::prelude::*; +/// # use bevy_scene::prelude::*; +/// # use bevy_gltf::prelude::*; +/// +/// fn load_gltf_scene(asset_server: Res) { +/// let gltf_scene: Handle = asset_server.load(format!("models/FlightHelmet/FlightHelmet.gltf#{}", GltfAssetLabel::Scene(0))); +/// } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum GltfAssetLabel { + /// `Scene{}`: glTF Scene as a Bevy [`Scene`](bevy_scene::Scene) + Scene(usize), + /// `Node{}`: glTF Node as a [`GltfNode`](crate::GltfNode) + Node(usize), + /// `Mesh{}`: glTF Mesh as a [`GltfMesh`](crate::GltfMesh) + Mesh(usize), + /// `Mesh{}/Primitive{}`: glTF Primitive as a Bevy [`Mesh`](bevy_render::mesh::Mesh) + Primitive { + /// Index of the mesh for this primitive + mesh: usize, + /// Index of this primitive in its parent mesh + primitive: usize, + }, + /// `Mesh{}/Primitive{}/MorphTargets`: Morph target animation data for a glTF Primitive + /// as a Bevy [`Image`](bevy_image::prelude::Image) + MorphTarget { + /// Index of the mesh for this primitive + mesh: usize, + /// Index of this primitive in its parent mesh + primitive: usize, + }, + /// `Texture{}`: glTF Texture as a Bevy [`Image`](bevy_image::prelude::Image) + Texture(usize), + /// `Material{}`: glTF Material as a Bevy [`StandardMaterial`](bevy_pbr::StandardMaterial) + Material { + /// Index of this material + index: usize, + /// Used to set the [`Face`](bevy_render::render_resource::Face) of the material, + /// useful if it is used with negative scale + is_scale_inverted: bool, + }, + /// `DefaultMaterial`: glTF's default Material as a + /// Bevy [`StandardMaterial`](bevy_pbr::StandardMaterial) + DefaultMaterial, + /// `Animation{}`: glTF Animation as Bevy [`AnimationClip`](bevy_animation::AnimationClip) + Animation(usize), + /// `Skin{}`: glTF mesh skin as [`GltfSkin`](crate::GltfSkin) + Skin(usize), + /// `Skin{}/InverseBindMatrices`: glTF mesh skin matrices as Bevy + /// [`SkinnedMeshInverseBindposes`](bevy_render::mesh::skinning::SkinnedMeshInverseBindposes) + InverseBindMatrices(usize), +} + +impl core::fmt::Display for GltfAssetLabel { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + GltfAssetLabel::Scene(index) => f.write_str(&format!("Scene{index}")), + GltfAssetLabel::Node(index) => f.write_str(&format!("Node{index}")), + GltfAssetLabel::Mesh(index) => f.write_str(&format!("Mesh{index}")), + GltfAssetLabel::Primitive { mesh, primitive } => { + f.write_str(&format!("Mesh{mesh}/Primitive{primitive}")) + } + GltfAssetLabel::MorphTarget { mesh, primitive } => { + f.write_str(&format!("Mesh{mesh}/Primitive{primitive}/MorphTargets")) + } + GltfAssetLabel::Texture(index) => f.write_str(&format!("Texture{index}")), + GltfAssetLabel::Material { + index, + is_scale_inverted, + } => f.write_str(&format!( + "Material{index}{}", + if *is_scale_inverted { + " (inverted)" + } else { + "" + } + )), + GltfAssetLabel::DefaultMaterial => f.write_str("DefaultMaterial"), + GltfAssetLabel::Animation(index) => f.write_str(&format!("Animation{index}")), + GltfAssetLabel::Skin(index) => f.write_str(&format!("Skin{index}")), + GltfAssetLabel::InverseBindMatrices(index) => { + f.write_str(&format!("Skin{index}/InverseBindMatrices")) + } + } + } +} + +impl GltfAssetLabel { + /// Add this label to an asset path + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # use bevy_asset::prelude::*; + /// # use bevy_scene::prelude::*; + /// # use bevy_gltf::prelude::*; + /// + /// fn load_gltf_scene(asset_server: Res) { + /// let gltf_scene: Handle = asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")); + /// } + /// ``` + pub fn from_asset(&self, path: impl Into>) -> AssetPath<'static> { + path.into().with_label(self.to_string()) + } +} diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 27f0ad00bbc47..159cdf4c67b5b 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -90,36 +90,30 @@ //! //! You can use [`GltfAssetLabel`] to ensure you are using the correct label. +mod assets; +mod label; +mod loader; +mod vertex_attributes; + extern crate alloc; -#[cfg(feature = "bevy_animation")] -use bevy_animation::AnimationClip; use bevy_platform_support::collections::HashMap; -mod loader; -mod vertex_attributes; -pub use loader::*; - use bevy_app::prelude::*; -use bevy_asset::{Asset, AssetApp, AssetPath, Handle}; -use bevy_ecs::{prelude::Component, reflect::ReflectComponent}; +use bevy_asset::AssetApp; use bevy_image::CompressedImageFormats; -use bevy_pbr::StandardMaterial; -use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; -use bevy_render::{ - mesh::{skinning::SkinnedMeshInverseBindposes, Mesh, MeshVertexAttribute}, - renderer::RenderDevice, -}; -use bevy_scene::Scene; +use bevy_render::{mesh::MeshVertexAttribute, renderer::RenderDevice}; /// The glTF prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] - pub use crate::{Gltf, GltfAssetLabel, GltfExtras}; + pub use crate::{assets::Gltf, assets::GltfExtras, label::GltfAssetLabel}; } +pub use {assets::*, label::GltfAssetLabel, loader::*}; + /// Adds support for glTF file loading to the app. #[derive(Default)] pub struct GltfPlugin { @@ -168,418 +162,3 @@ impl Plugin for GltfPlugin { }); } } - -/// Representation of a loaded glTF file. -#[derive(Asset, Debug, TypePath)] -pub struct Gltf { - /// All scenes loaded from the glTF file. - pub scenes: Vec>, - /// Named scenes loaded from the glTF file. - pub named_scenes: HashMap, Handle>, - /// All meshes loaded from the glTF file. - pub meshes: Vec>, - /// Named meshes loaded from the glTF file. - pub named_meshes: HashMap, Handle>, - /// All materials loaded from the glTF file. - pub materials: Vec>, - /// Named materials loaded from the glTF file. - pub named_materials: HashMap, Handle>, - /// All nodes loaded from the glTF file. - pub nodes: Vec>, - /// Named nodes loaded from the glTF file. - pub named_nodes: HashMap, Handle>, - /// All skins loaded from the glTF file. - pub skins: Vec>, - /// Named skins loaded from the glTF file. - pub named_skins: HashMap, Handle>, - /// Default scene to be displayed. - pub default_scene: Option>, - /// All animations loaded from the glTF file. - #[cfg(feature = "bevy_animation")] - pub animations: Vec>, - /// Named animations loaded from the glTF file. - #[cfg(feature = "bevy_animation")] - pub named_animations: HashMap, Handle>, - /// The gltf root of the gltf asset, see . Only has a value when `GltfLoaderSettings::include_source` is true. - pub source: Option, -} - -/// A glTF node with all of its child nodes, its [`GltfMesh`], -/// [`Transform`](bevy_transform::prelude::Transform), its optional [`GltfSkin`] -/// and an optional [`GltfExtras`]. -/// -/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-node). -#[derive(Asset, Debug, Clone, TypePath)] -pub struct GltfNode { - /// Index of the node inside the scene - pub index: usize, - /// Computed name for a node - either a user defined node name from gLTF or a generated name from index - pub name: String, - /// Direct children of the node. - pub children: Vec>, - /// Mesh of the node. - pub mesh: Option>, - /// Skin of the node. - pub skin: Option>, - /// Local transform. - pub transform: bevy_transform::prelude::Transform, - /// Is this node used as an animation root - #[cfg(feature = "bevy_animation")] - pub is_animation_root: bool, - /// Additional data. - pub extras: Option, -} - -impl GltfNode { - /// Create a node extracting name and index from glTF def - pub fn new( - node: &gltf::Node, - children: Vec>, - mesh: Option>, - transform: bevy_transform::prelude::Transform, - skin: Option>, - extras: Option, - ) -> Self { - Self { - index: node.index(), - name: if let Some(name) = node.name() { - name.to_string() - } else { - format!("GltfNode{}", node.index()) - }, - children, - mesh, - transform, - skin, - #[cfg(feature = "bevy_animation")] - is_animation_root: false, - extras, - } - } - - /// Create a node with animation root mark - #[cfg(feature = "bevy_animation")] - pub fn with_animation_root(self, is_animation_root: bool) -> Self { - Self { - is_animation_root, - ..self - } - } - - /// Subasset label for this node within the gLTF parent asset. - pub fn asset_label(&self) -> GltfAssetLabel { - GltfAssetLabel::Node(self.index) - } -} - -/// A glTF skin with all of its joint nodes, [`SkinnedMeshInversiveBindposes`](bevy_render::mesh::skinning::SkinnedMeshInverseBindposes) -/// and an optional [`GltfExtras`]. -/// -/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-skin). -#[derive(Asset, Debug, Clone, TypePath)] -pub struct GltfSkin { - /// Index of the skin inside the scene - pub index: usize, - /// Computed name for a skin - either a user defined skin name from gLTF or a generated name from index - pub name: String, - /// All the nodes that form this skin. - pub joints: Vec>, - /// Inverse-bind matrices of this skin. - pub inverse_bind_matrices: Handle, - /// Additional data. - pub extras: Option, -} - -impl GltfSkin { - /// Create a skin extracting name and index from glTF def - pub fn new( - skin: &gltf::Skin, - joints: Vec>, - inverse_bind_matrices: Handle, - extras: Option, - ) -> Self { - Self { - index: skin.index(), - name: if let Some(name) = skin.name() { - name.to_string() - } else { - format!("GltfSkin{}", skin.index()) - }, - joints, - inverse_bind_matrices, - extras, - } - } - - /// Subasset label for this skin within the gLTF parent asset. - pub fn asset_label(&self) -> GltfAssetLabel { - GltfAssetLabel::Skin(self.index) - } -} - -/// A glTF mesh, which may consist of multiple [`GltfPrimitives`](GltfPrimitive) -/// and an optional [`GltfExtras`]. -/// -/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-mesh). -#[derive(Asset, Debug, Clone, TypePath)] -pub struct GltfMesh { - /// Index of the mesh inside the scene - pub index: usize, - /// Computed name for a mesh - either a user defined mesh name from gLTF or a generated name from index - pub name: String, - /// Primitives of the glTF mesh. - pub primitives: Vec, - /// Additional data. - pub extras: Option, -} - -impl GltfMesh { - /// Create a mesh extracting name and index from glTF def - pub fn new( - mesh: &gltf::Mesh, - primitives: Vec, - extras: Option, - ) -> Self { - Self { - index: mesh.index(), - name: if let Some(name) = mesh.name() { - name.to_string() - } else { - format!("GltfMesh{}", mesh.index()) - }, - primitives, - extras, - } - } - - /// Subasset label for this mesh within the gLTF parent asset. - pub fn asset_label(&self) -> GltfAssetLabel { - GltfAssetLabel::Mesh(self.index) - } -} - -/// Part of a [`GltfMesh`] that consists of a [`Mesh`], an optional [`StandardMaterial`] and [`GltfExtras`]. -/// -/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-mesh-primitive). -#[derive(Asset, Debug, Clone, TypePath)] -pub struct GltfPrimitive { - /// Index of the primitive inside the mesh - pub index: usize, - /// Index of the parent [`GltfMesh`] of this primitive - pub parent_mesh_index: usize, - /// Computed name for a primitive - either a user defined primitive name from gLTF or a generated name from index - pub name: String, - /// Topology to be rendered. - pub mesh: Handle, - /// Material to apply to the `mesh`. - pub material: Option>, - /// Additional data. - pub extras: Option, - /// Additional data of the `material`. - pub material_extras: Option, -} - -impl GltfPrimitive { - /// Create a primitive extracting name and index from glTF def - pub fn new( - gltf_mesh: &gltf::Mesh, - gltf_primitive: &gltf::Primitive, - mesh: Handle, - material: Option>, - extras: Option, - material_extras: Option, - ) -> Self { - GltfPrimitive { - index: gltf_primitive.index(), - parent_mesh_index: gltf_mesh.index(), - name: { - let mesh_name = gltf_mesh.name().unwrap_or("Mesh"); - if gltf_mesh.primitives().len() > 1 { - format!("{}.{}", mesh_name, gltf_primitive.index()) - } else { - mesh_name.to_string() - } - }, - mesh, - material, - extras, - material_extras, - } - } - - /// Subasset label for this primitive within its parent [`GltfMesh`] within the gLTF parent asset. - pub fn asset_label(&self) -> GltfAssetLabel { - GltfAssetLabel::Primitive { - mesh: self.parent_mesh_index, - primitive: self.index, - } - } -} - -/// Additional untyped data that can be present on most glTF types at the primitive level. -/// -/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras). -#[derive(Clone, Debug, Reflect, Default, Component)] -#[reflect(Component, Default, Debug)] -pub struct GltfExtras { - /// Content of the extra data. - pub value: String, -} - -/// Additional untyped data that can be present on most glTF types at the scene level. -/// -/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras). -#[derive(Clone, Debug, Reflect, Default, Component)] -#[reflect(Component, Default, Debug)] -pub struct GltfSceneExtras { - /// Content of the extra data. - pub value: String, -} - -/// Additional untyped data that can be present on most glTF types at the mesh level. -/// -/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras). -#[derive(Clone, Debug, Reflect, Default, Component)] -#[reflect(Component, Default, Debug)] -pub struct GltfMeshExtras { - /// Content of the extra data. - pub value: String, -} - -/// Additional untyped data that can be present on most glTF types at the material level. -/// -/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-extras). -#[derive(Clone, Debug, Reflect, Default, Component)] -#[reflect(Component, Default, Debug)] -pub struct GltfMaterialExtras { - /// Content of the extra data. - pub value: String, -} - -/// The material name of a glTF primitive. -/// -/// See [the relevant glTF specification section](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#reference-material). -#[derive(Clone, Debug, Reflect, Default, Component)] -#[reflect(Component)] -pub struct GltfMaterialName(pub String); - -/// Labels that can be used to load part of a glTF -/// -/// You can use [`GltfAssetLabel::from_asset`] to add it to an asset path -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # use bevy_asset::prelude::*; -/// # use bevy_scene::prelude::*; -/// # use bevy_gltf::prelude::*; -/// -/// fn load_gltf_scene(asset_server: Res) { -/// let gltf_scene: Handle = asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")); -/// } -/// ``` -/// -/// Or when formatting a string for the path -/// -/// ``` -/// # use bevy_ecs::prelude::*; -/// # use bevy_asset::prelude::*; -/// # use bevy_scene::prelude::*; -/// # use bevy_gltf::prelude::*; -/// -/// fn load_gltf_scene(asset_server: Res) { -/// let gltf_scene: Handle = asset_server.load(format!("models/FlightHelmet/FlightHelmet.gltf#{}", GltfAssetLabel::Scene(0))); -/// } -/// ``` -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum GltfAssetLabel { - /// `Scene{}`: glTF Scene as a Bevy [`Scene`] - Scene(usize), - /// `Node{}`: glTF Node as a [`GltfNode`] - Node(usize), - /// `Mesh{}`: glTF Mesh as a [`GltfMesh`] - Mesh(usize), - /// `Mesh{}/Primitive{}`: glTF Primitive as a Bevy [`Mesh`] - Primitive { - /// Index of the mesh for this primitive - mesh: usize, - /// Index of this primitive in its parent mesh - primitive: usize, - }, - /// `Mesh{}/Primitive{}/MorphTargets`: Morph target animation data for a glTF Primitive - /// as a Bevy [`Image`](bevy_image::prelude::Image) - MorphTarget { - /// Index of the mesh for this primitive - mesh: usize, - /// Index of this primitive in its parent mesh - primitive: usize, - }, - /// `Texture{}`: glTF Texture as a Bevy [`Image`](bevy_image::prelude::Image) - Texture(usize), - /// `Material{}`: glTF Material as a Bevy [`StandardMaterial`] - Material { - /// Index of this material - index: usize, - /// Used to set the [`Face`](bevy_render::render_resource::Face) of the material, useful if it is used with negative scale - is_scale_inverted: bool, - }, - /// `DefaultMaterial`: glTF's default Material as a Bevy [`StandardMaterial`] - DefaultMaterial, - /// `Animation{}`: glTF Animation as Bevy [`AnimationClip`] - Animation(usize), - /// `Skin{}`: glTF mesh skin as [`GltfSkin`] - Skin(usize), - /// `Skin{}/InverseBindMatrices`: glTF mesh skin matrices as Bevy [`SkinnedMeshInverseBindposes`] - InverseBindMatrices(usize), -} - -impl core::fmt::Display for GltfAssetLabel { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - match self { - GltfAssetLabel::Scene(index) => f.write_str(&format!("Scene{index}")), - GltfAssetLabel::Node(index) => f.write_str(&format!("Node{index}")), - GltfAssetLabel::Mesh(index) => f.write_str(&format!("Mesh{index}")), - GltfAssetLabel::Primitive { mesh, primitive } => { - f.write_str(&format!("Mesh{mesh}/Primitive{primitive}")) - } - GltfAssetLabel::MorphTarget { mesh, primitive } => { - f.write_str(&format!("Mesh{mesh}/Primitive{primitive}/MorphTargets")) - } - GltfAssetLabel::Texture(index) => f.write_str(&format!("Texture{index}")), - GltfAssetLabel::Material { - index, - is_scale_inverted, - } => f.write_str(&format!( - "Material{index}{}", - if *is_scale_inverted { - " (inverted)" - } else { - "" - } - )), - GltfAssetLabel::DefaultMaterial => f.write_str("DefaultMaterial"), - GltfAssetLabel::Animation(index) => f.write_str(&format!("Animation{index}")), - GltfAssetLabel::Skin(index) => f.write_str(&format!("Skin{index}")), - GltfAssetLabel::InverseBindMatrices(index) => { - f.write_str(&format!("Skin{index}/InverseBindMatrices")) - } - } - } -} - -impl GltfAssetLabel { - /// Add this label to an asset path - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # use bevy_asset::prelude::*; - /// # use bevy_scene::prelude::*; - /// # use bevy_gltf::prelude::*; - /// - /// fn load_gltf_scene(asset_server: Res) { - /// let gltf_scene: Handle = asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")); - /// } - /// ``` - pub fn from_asset(&self, path: impl Into>) -> AssetPath<'static> { - path.into().with_label(self.to_string()) - } -} diff --git a/crates/bevy_gltf/src/loader/extensions/khr_materials_anisotropy.rs b/crates/bevy_gltf/src/loader/extensions/khr_materials_anisotropy.rs new file mode 100644 index 0000000000000..f859cfba84052 --- /dev/null +++ b/crates/bevy_gltf/src/loader/extensions/khr_materials_anisotropy.rs @@ -0,0 +1,71 @@ +use bevy_asset::LoadContext; + +use gltf::{Document, Material}; + +use serde_json::Value; + +#[cfg(feature = "pbr_anisotropy_texture")] +use { + crate::loader::gltf_ext::{material::uv_channel, texture::texture_handle_from_info}, + bevy_asset::Handle, + bevy_image::Image, + bevy_pbr::UvChannel, + gltf::json::texture::Info, + serde_json::value, +}; + +/// Parsed data from the `KHR_materials_anisotropy` extension. +/// +/// See the specification: +/// +#[derive(Default)] +pub(crate) struct AnisotropyExtension { + pub(crate) anisotropy_strength: Option, + pub(crate) anisotropy_rotation: Option, + #[cfg(feature = "pbr_anisotropy_texture")] + pub(crate) anisotropy_channel: UvChannel, + #[cfg(feature = "pbr_anisotropy_texture")] + pub(crate) anisotropy_texture: Option>, +} + +impl AnisotropyExtension { + #[expect( + clippy::allow_attributes, + reason = "`unused_variables` is not always linted" + )] + #[allow( + unused_variables, + reason = "Depending on what features are used to compile this crate, certain parameters may end up unused." + )] + pub(crate) fn parse( + load_context: &mut LoadContext, + document: &Document, + material: &Material, + ) -> Option { + let extension = material + .extensions()? + .get("KHR_materials_anisotropy")? + .as_object()?; + + #[cfg(feature = "pbr_anisotropy_texture")] + let (anisotropy_channel, anisotropy_texture) = extension + .get("anisotropyTexture") + .and_then(|value| value::from_value::(value.clone()).ok()) + .map(|json_info| { + ( + uv_channel(material, "anisotropy", json_info.tex_coord), + texture_handle_from_info(&json_info, document, load_context), + ) + }) + .unzip(); + + Some(AnisotropyExtension { + anisotropy_strength: extension.get("anisotropyStrength").and_then(Value::as_f64), + anisotropy_rotation: extension.get("anisotropyRotation").and_then(Value::as_f64), + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_channel: anisotropy_channel.unwrap_or_default(), + #[cfg(feature = "pbr_anisotropy_texture")] + anisotropy_texture, + }) + } +} diff --git a/crates/bevy_gltf/src/loader/extensions/khr_materials_clearcoat.rs b/crates/bevy_gltf/src/loader/extensions/khr_materials_clearcoat.rs new file mode 100644 index 0000000000000..5128487ca4445 --- /dev/null +++ b/crates/bevy_gltf/src/loader/extensions/khr_materials_clearcoat.rs @@ -0,0 +1,104 @@ +use bevy_asset::LoadContext; + +use gltf::{Document, Material}; + +use serde_json::Value; + +#[cfg(feature = "pbr_multi_layer_material_textures")] +use { + crate::loader::gltf_ext::material::parse_material_extension_texture, bevy_asset::Handle, + bevy_image::Image, bevy_pbr::UvChannel, +}; + +/// Parsed data from the `KHR_materials_clearcoat` extension. +/// +/// See the specification: +/// +#[derive(Default)] +pub(crate) struct ClearcoatExtension { + pub(crate) clearcoat_factor: Option, + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub(crate) clearcoat_channel: UvChannel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub(crate) clearcoat_texture: Option>, + pub(crate) clearcoat_roughness_factor: Option, + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub(crate) clearcoat_roughness_channel: UvChannel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub(crate) clearcoat_roughness_texture: Option>, + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub(crate) clearcoat_normal_channel: UvChannel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + pub(crate) clearcoat_normal_texture: Option>, +} + +impl ClearcoatExtension { + #[expect( + clippy::allow_attributes, + reason = "`unused_variables` is not always linted" + )] + #[allow( + unused_variables, + reason = "Depending on what features are used to compile this crate, certain parameters may end up unused." + )] + pub(crate) fn parse( + load_context: &mut LoadContext, + document: &Document, + material: &Material, + ) -> Option { + let extension = material + .extensions()? + .get("KHR_materials_clearcoat")? + .as_object()?; + + #[cfg(feature = "pbr_multi_layer_material_textures")] + let (clearcoat_channel, clearcoat_texture) = parse_material_extension_texture( + material, + load_context, + document, + extension, + "clearcoatTexture", + "clearcoat", + ); + + #[cfg(feature = "pbr_multi_layer_material_textures")] + let (clearcoat_roughness_channel, clearcoat_roughness_texture) = + parse_material_extension_texture( + material, + load_context, + document, + extension, + "clearcoatRoughnessTexture", + "clearcoat roughness", + ); + + #[cfg(feature = "pbr_multi_layer_material_textures")] + let (clearcoat_normal_channel, clearcoat_normal_texture) = parse_material_extension_texture( + material, + load_context, + document, + extension, + "clearcoatNormalTexture", + "clearcoat normal", + ); + + Some(ClearcoatExtension { + clearcoat_factor: extension.get("clearcoatFactor").and_then(Value::as_f64), + clearcoat_roughness_factor: extension + .get("clearcoatRoughnessFactor") + .and_then(Value::as_f64), + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_roughness_texture, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_channel, + #[cfg(feature = "pbr_multi_layer_material_textures")] + clearcoat_normal_texture, + }) + } +} diff --git a/crates/bevy_gltf/src/loader/extensions/khr_materials_specular.rs b/crates/bevy_gltf/src/loader/extensions/khr_materials_specular.rs new file mode 100644 index 0000000000000..f0adcc4940b10 --- /dev/null +++ b/crates/bevy_gltf/src/loader/extensions/khr_materials_specular.rs @@ -0,0 +1,100 @@ +use bevy_asset::LoadContext; + +use gltf::{Document, Material}; + +use serde_json::Value; + +#[cfg(feature = "pbr_specular_textures")] +use { + crate::loader::gltf_ext::material::parse_material_extension_texture, bevy_asset::Handle, + bevy_image::Image, bevy_pbr::UvChannel, +}; + +/// Parsed data from the `KHR_materials_specular` extension. +/// +/// We currently don't parse `specularFactor` and `specularTexture`, since +/// they're incompatible with Filament. +/// +/// Note that the map is a *specular map*, not a *reflectance map*. In Bevy and +/// Filament terms, the reflectance values in the specular map range from [0.0, +/// 0.5], rather than [0.0, 1.0]. This is an unfortunate +/// `KHR_materials_specular` specification requirement that stems from the fact +/// that glTF is specified in terms of a specular strength model, not the +/// reflectance model that Filament and Bevy use. A workaround, which is noted +/// in the [`StandardMaterial`](bevy_pbr::StandardMaterial) documentation, is to set the reflectance value +/// to 2.0, which spreads the specular map range from [0.0, 1.0] as normal. +/// +/// See the specification: +/// +#[derive(Default)] +pub(crate) struct SpecularExtension { + pub(crate) specular_factor: Option, + #[cfg(feature = "pbr_specular_textures")] + pub(crate) specular_channel: UvChannel, + #[cfg(feature = "pbr_specular_textures")] + pub(crate) specular_texture: Option>, + pub(crate) specular_color_factor: Option<[f64; 3]>, + #[cfg(feature = "pbr_specular_textures")] + pub(crate) specular_color_channel: UvChannel, + #[cfg(feature = "pbr_specular_textures")] + pub(crate) specular_color_texture: Option>, +} + +impl SpecularExtension { + pub(crate) fn parse( + _load_context: &mut LoadContext, + _document: &Document, + material: &Material, + ) -> Option { + let extension = material + .extensions()? + .get("KHR_materials_specular")? + .as_object()?; + + #[cfg(feature = "pbr_specular_textures")] + let (_specular_channel, _specular_texture) = parse_material_extension_texture( + material, + _load_context, + _document, + extension, + "specularTexture", + "specular", + ); + + #[cfg(feature = "pbr_specular_textures")] + let (_specular_color_channel, _specular_color_texture) = parse_material_extension_texture( + material, + _load_context, + _document, + extension, + "specularColorTexture", + "specular color", + ); + + Some(SpecularExtension { + specular_factor: extension.get("specularFactor").and_then(Value::as_f64), + #[cfg(feature = "pbr_specular_textures")] + specular_channel: _specular_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_texture: _specular_texture, + specular_color_factor: extension + .get("specularColorFactor") + .and_then(Value::as_array) + .and_then(|json_array| { + if json_array.len() < 3 { + None + } else { + Some([ + json_array[0].as_f64()?, + json_array[1].as_f64()?, + json_array[2].as_f64()?, + ]) + } + }), + #[cfg(feature = "pbr_specular_textures")] + specular_color_channel: _specular_color_channel, + #[cfg(feature = "pbr_specular_textures")] + specular_color_texture: _specular_color_texture, + }) + } +} diff --git a/crates/bevy_gltf/src/loader/extensions/mod.rs b/crates/bevy_gltf/src/loader/extensions/mod.rs new file mode 100644 index 0000000000000..14863fa4538c5 --- /dev/null +++ b/crates/bevy_gltf/src/loader/extensions/mod.rs @@ -0,0 +1,10 @@ +//! glTF extensions defined by the Khronos Group and other vendors + +mod khr_materials_anisotropy; +mod khr_materials_clearcoat; +mod khr_materials_specular; + +pub(crate) use self::{ + khr_materials_anisotropy::AnisotropyExtension, khr_materials_clearcoat::ClearcoatExtension, + khr_materials_specular::SpecularExtension, +}; diff --git a/crates/bevy_gltf/src/loader/gltf_ext/material.rs b/crates/bevy_gltf/src/loader/gltf_ext/material.rs new file mode 100644 index 0000000000000..9d8b7c5745910 --- /dev/null +++ b/crates/bevy_gltf/src/loader/gltf_ext/material.rs @@ -0,0 +1,165 @@ +use bevy_math::Affine2; +use bevy_pbr::UvChannel; +use bevy_render::alpha::AlphaMode; + +use gltf::{json::texture::Info, Material}; + +use serde_json::value; + +use crate::GltfAssetLabel; + +use super::texture::texture_transform_to_affine2; + +#[cfg(any( + feature = "pbr_specular_textures", + feature = "pbr_multi_layer_material_textures" +))] +use { + super::texture::texture_handle_from_info, + bevy_asset::{Handle, LoadContext}, + bevy_image::Image, + gltf::Document, + serde_json::{Map, Value}, +}; + +/// Parses a texture that's part of a material extension block and returns its +/// UV channel and image reference. +#[cfg(any( + feature = "pbr_specular_textures", + feature = "pbr_multi_layer_material_textures" +))] +pub(crate) fn parse_material_extension_texture( + material: &Material, + load_context: &mut LoadContext, + document: &Document, + extension: &Map, + texture_name: &str, + texture_kind: &str, +) -> (UvChannel, Option>) { + match extension + .get(texture_name) + .and_then(|value| value::from_value::(value.clone()).ok()) + { + Some(json_info) => ( + uv_channel(material, texture_kind, json_info.tex_coord), + Some(texture_handle_from_info(&json_info, document, load_context)), + ), + None => (UvChannel::default(), None), + } +} + +pub(crate) fn uv_channel(material: &Material, texture_kind: &str, tex_coord: u32) -> UvChannel { + match tex_coord { + 0 => UvChannel::Uv0, + 1 => UvChannel::Uv1, + _ => { + let material_name = material + .name() + .map(|n| format!("the material \"{n}\"")) + .unwrap_or_else(|| "an unnamed material".to_string()); + let material_index = material + .index() + .map(|i| format!("index {i}")) + .unwrap_or_else(|| "default".to_string()); + tracing::warn!( + "Only 2 UV Channels are supported, but {material_name} ({material_index}) \ + has the TEXCOORD attribute {} on texture kind {texture_kind}, which will fallback to 0.", + tex_coord, + ); + UvChannel::Uv0 + } + } +} + +pub(crate) fn alpha_mode(material: &Material) -> AlphaMode { + match material.alpha_mode() { + gltf::material::AlphaMode::Opaque => AlphaMode::Opaque, + gltf::material::AlphaMode::Mask => AlphaMode::Mask(material.alpha_cutoff().unwrap_or(0.5)), + gltf::material::AlphaMode::Blend => AlphaMode::Blend, + } +} + +/// Returns the index (within the `textures` array) of the texture with the +/// given field name in the data for the material extension with the given name, +/// if there is one. +pub(crate) fn extension_texture_index( + material: &Material, + extension_name: &str, + texture_field_name: &str, +) -> Option { + Some( + value::from_value::( + material + .extensions()? + .get(extension_name)? + .as_object()? + .get(texture_field_name)? + .clone(), + ) + .ok()? + .index + .value(), + ) +} + +/// Returns true if the material needs mesh tangents in order to be successfully +/// rendered. +/// +/// We generate them if this function returns true. +pub(crate) fn needs_tangents(material: &Material) -> bool { + [ + material.normal_texture().is_some(), + #[cfg(feature = "pbr_multi_layer_material_textures")] + extension_texture_index( + material, + "KHR_materials_clearcoat", + "clearcoatNormalTexture", + ) + .is_some(), + ] + .into_iter() + .reduce(|a, b| a || b) + .unwrap_or(false) +} + +pub(crate) fn warn_on_differing_texture_transforms( + material: &Material, + info: &gltf::texture::Info, + texture_transform: Affine2, + texture_kind: &str, +) { + let has_differing_texture_transform = info + .texture_transform() + .map(texture_transform_to_affine2) + .is_some_and(|t| t != texture_transform); + if has_differing_texture_transform { + let material_name = material + .name() + .map(|n| format!("the material \"{n}\"")) + .unwrap_or_else(|| "an unnamed material".to_string()); + let texture_name = info + .texture() + .name() + .map(|n| format!("its {texture_kind} texture \"{n}\"")) + .unwrap_or_else(|| format!("its unnamed {texture_kind} texture")); + let material_index = material + .index() + .map(|i| format!("index {i}")) + .unwrap_or_else(|| "default".to_string()); + tracing::warn!( + "Only texture transforms on base color textures are supported, but {material_name} ({material_index}) \ + has a texture transform on {texture_name} (index {}), which will be ignored.", info.texture().index() + ); + } +} + +pub(crate) fn material_label(material: &Material, is_scale_inverted: bool) -> GltfAssetLabel { + if let Some(index) = material.index() { + GltfAssetLabel::Material { + index, + is_scale_inverted, + } + } else { + GltfAssetLabel::DefaultMaterial + } +} diff --git a/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs b/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs new file mode 100644 index 0000000000000..90c838b4682b1 --- /dev/null +++ b/crates/bevy_gltf/src/loader/gltf_ext/mesh.rs @@ -0,0 +1,30 @@ +use bevy_render::mesh::PrimitiveTopology; + +use gltf::mesh::{Mesh, Mode, Primitive}; + +use crate::GltfError; + +pub(crate) fn primitive_name(mesh: &Mesh<'_>, primitive: &Primitive) -> String { + let mesh_name = mesh.name().unwrap_or("Mesh"); + if mesh.primitives().len() > 1 { + format!("{}.{}", mesh_name, primitive.index()) + } else { + mesh_name.to_string() + } +} + +/// Maps the `primitive_topology` from glTF to `wgpu`. +#[expect( + clippy::result_large_err, + reason = "`GltfError` is only barely past the threshold for large errors." +)] +pub(crate) fn primitive_topology(mode: Mode) -> Result { + match mode { + Mode::Points => Ok(PrimitiveTopology::PointList), + Mode::Lines => Ok(PrimitiveTopology::LineList), + Mode::LineStrip => Ok(PrimitiveTopology::LineStrip), + Mode::Triangles => Ok(PrimitiveTopology::TriangleList), + Mode::TriangleStrip => Ok(PrimitiveTopology::TriangleStrip), + mode => Err(GltfError::UnsupportedPrimitive { mode }), + } +} diff --git a/crates/bevy_gltf/src/loader/gltf_ext/mod.rs b/crates/bevy_gltf/src/loader/gltf_ext/mod.rs new file mode 100644 index 0000000000000..558ed645dd9f2 --- /dev/null +++ b/crates/bevy_gltf/src/loader/gltf_ext/mod.rs @@ -0,0 +1,79 @@ +//! Methods to access information from [`gltf`] types + +pub mod material; +pub mod mesh; +pub mod scene; +pub mod texture; + +use bevy_platform_support::collections::HashSet; + +use fixedbitset::FixedBitSet; +use gltf::{Document, Gltf}; + +use super::GltfError; + +use self::{material::extension_texture_index, scene::check_is_part_of_cycle}; + +#[expect( + clippy::result_large_err, + reason = "need to be signature compatible with `load_gltf`" +)] +/// Checks all glTF nodes for cycles, starting at the scene root. +pub(crate) fn check_for_cycles(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() { + let Some(node) = gltf.nodes().nth(root) else { + unreachable!("Index of a root node should always exist."); + }; + check_is_part_of_cycle(&node, &mut visited)?; + } + + Ok(()) +} + +pub(crate) fn get_linear_textures(document: &Document) -> HashSet { + let mut linear_textures = HashSet::default(); + + for material in document.materials() { + if let Some(texture) = material.normal_texture() { + linear_textures.insert(texture.texture().index()); + } + if let Some(texture) = material.occlusion_texture() { + linear_textures.insert(texture.texture().index()); + } + if let Some(texture) = material + .pbr_metallic_roughness() + .metallic_roughness_texture() + { + linear_textures.insert(texture.texture().index()); + } + if let Some(texture_index) = + extension_texture_index(&material, "KHR_materials_anisotropy", "anisotropyTexture") + { + linear_textures.insert(texture_index); + } + + // None of the clearcoat maps should be loaded as sRGB. + #[cfg(feature = "pbr_multi_layer_material_textures")] + for texture_field_name in [ + "clearcoatTexture", + "clearcoatRoughnessTexture", + "clearcoatNormalTexture", + ] { + if let Some(texture_index) = + extension_texture_index(&material, "KHR_materials_clearcoat", texture_field_name) + { + linear_textures.insert(texture_index); + } + } + } + + linear_textures +} diff --git a/crates/bevy_gltf/src/loader/gltf_ext/scene.rs b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs new file mode 100644 index 0000000000000..7845280b18ff6 --- /dev/null +++ b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs @@ -0,0 +1,91 @@ +use bevy_ecs::name::Name; +use bevy_math::{Mat4, Vec3}; +use bevy_transform::components::Transform; + +use gltf::scene::Node; + +use fixedbitset::FixedBitSet; +use itertools::Itertools; + +#[cfg(feature = "bevy_animation")] +use bevy_platform_support::collections::{HashMap, HashSet}; + +use crate::GltfError; + +pub(crate) fn node_name(node: &Node) -> Name { + let name = node + .name() + .map(ToString::to_string) + .unwrap_or_else(|| format!("GltfNode{}", node.index())); + Name::new(name) +} + +/// Calculate the transform of gLTF [`Node`]. +/// +/// This should be used instead of calling [`gltf::scene::Transform::matrix()`] +/// on [`Node::transform()`](gltf::Node::transform) directly because it uses optimized glam types and +/// if `libm` feature of `bevy_math` crate is enabled also handles cross +/// platform determinism properly. +pub(crate) fn node_transform(node: &Node) -> Transform { + match node.transform() { + gltf::scene::Transform::Matrix { matrix } => { + Transform::from_matrix(Mat4::from_cols_array_2d(&matrix)) + } + gltf::scene::Transform::Decomposed { + translation, + rotation, + scale, + } => Transform { + translation: Vec3::from(translation), + rotation: bevy_math::Quat::from_array(rotation), + scale: Vec3::from(scale), + }, + } +} + +#[expect( + clippy::result_large_err, + reason = "need to be signature compatible with `load_gltf`" +)] +/// Check if [`Node`] is part of cycle +pub(crate) fn check_is_part_of_cycle( + 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_is_part_of_cycle(&kid, visited)?; + } + visited.remove(node.index()); + + Ok(()) +} + +#[cfg(feature = "bevy_animation")] +pub(crate) fn collect_path( + node: &Node, + current_path: &[Name], + paths: &mut HashMap)>, + root_index: usize, + visited: &mut HashSet, +) { + let mut path = current_path.to_owned(); + path.push(node_name(node)); + visited.insert(node.index()); + for child in node.children() { + if !visited.contains(&child.index()) { + collect_path(&child, &path, paths, root_index, visited); + } + } + paths.insert(node.index(), (root_index, path)); +} diff --git a/crates/bevy_gltf/src/loader/gltf_ext/texture.rs b/crates/bevy_gltf/src/loader/gltf_ext/texture.rs new file mode 100644 index 0000000000000..5fb5bcce0d4c0 --- /dev/null +++ b/crates/bevy_gltf/src/loader/gltf_ext/texture.rs @@ -0,0 +1,126 @@ +use bevy_asset::{Handle, LoadContext}; +use bevy_image::{Image, ImageAddressMode, ImageFilterMode, ImageSamplerDescriptor}; +use bevy_math::Affine2; + +use gltf::{ + image::Source, + texture::{MagFilter, MinFilter, Texture, TextureTransform, WrappingMode}, +}; + +#[cfg(any( + feature = "pbr_anisotropy_texture", + feature = "pbr_multi_layer_material_textures", + feature = "pbr_specular_textures" +))] +use gltf::{json::texture::Info, Document}; + +use crate::{loader::DataUri, GltfAssetLabel}; + +pub(crate) fn texture_handle( + texture: &Texture<'_>, + load_context: &mut LoadContext, +) -> Handle { + match texture.source().source() { + Source::View { .. } => load_context.get_label_handle(texture_label(texture).to_string()), + Source::Uri { uri, .. } => { + let uri = percent_encoding::percent_decode_str(uri) + .decode_utf8() + .unwrap(); + let uri = uri.as_ref(); + if let Ok(_data_uri) = DataUri::parse(uri) { + load_context.get_label_handle(texture_label(texture).to_string()) + } else { + let parent = load_context.path().parent().unwrap(); + let image_path = parent.join(uri); + load_context.load(image_path) + } + } + } +} + +/// Extracts the texture sampler data from the glTF [`Texture`]. +pub(crate) fn texture_sampler(texture: &Texture<'_>) -> ImageSamplerDescriptor { + let gltf_sampler = texture.sampler(); + + ImageSamplerDescriptor { + address_mode_u: address_mode(&gltf_sampler.wrap_s()), + address_mode_v: address_mode(&gltf_sampler.wrap_t()), + + mag_filter: gltf_sampler + .mag_filter() + .map(|mf| match mf { + MagFilter::Nearest => ImageFilterMode::Nearest, + MagFilter::Linear => ImageFilterMode::Linear, + }) + .unwrap_or(ImageSamplerDescriptor::default().mag_filter), + + min_filter: gltf_sampler + .min_filter() + .map(|mf| match mf { + MinFilter::Nearest + | MinFilter::NearestMipmapNearest + | MinFilter::NearestMipmapLinear => ImageFilterMode::Nearest, + MinFilter::Linear + | MinFilter::LinearMipmapNearest + | MinFilter::LinearMipmapLinear => ImageFilterMode::Linear, + }) + .unwrap_or(ImageSamplerDescriptor::default().min_filter), + + mipmap_filter: gltf_sampler + .min_filter() + .map(|mf| match mf { + MinFilter::Nearest + | MinFilter::Linear + | MinFilter::NearestMipmapNearest + | MinFilter::LinearMipmapNearest => ImageFilterMode::Nearest, + MinFilter::NearestMipmapLinear | MinFilter::LinearMipmapLinear => { + ImageFilterMode::Linear + } + }) + .unwrap_or(ImageSamplerDescriptor::default().mipmap_filter), + + ..Default::default() + } +} + +pub(crate) fn texture_label(texture: &Texture<'_>) -> GltfAssetLabel { + GltfAssetLabel::Texture(texture.index()) +} + +pub(crate) fn address_mode(wrapping_mode: &WrappingMode) -> ImageAddressMode { + match wrapping_mode { + WrappingMode::ClampToEdge => ImageAddressMode::ClampToEdge, + WrappingMode::Repeat => ImageAddressMode::Repeat, + WrappingMode::MirroredRepeat => ImageAddressMode::MirrorRepeat, + } +} + +pub(crate) fn texture_transform_to_affine2(texture_transform: TextureTransform) -> Affine2 { + Affine2::from_scale_angle_translation( + texture_transform.scale().into(), + -texture_transform.rotation(), + texture_transform.offset().into(), + ) +} + +#[cfg(any( + feature = "pbr_anisotropy_texture", + feature = "pbr_multi_layer_material_textures", + feature = "pbr_specular_textures" +))] +/// Given a [`Info`], returns the handle of the texture that this +/// refers to. +/// +/// This is a low-level function only used when the [`gltf`] crate has no support +/// for an extension, forcing us to parse its texture references manually. +pub(crate) fn texture_handle_from_info( + info: &Info, + document: &Document, + load_context: &mut LoadContext, +) -> Handle { + let texture = document + .textures() + .nth(info.index.value()) + .expect("Texture info references a nonexistent texture"); + texture_handle(&texture, load_context) +} diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader/mod.rs similarity index 75% rename from crates/bevy_gltf/src/loader.rs rename to crates/bevy_gltf/src/loader/mod.rs index 6700d04000749..9d400e44bc0cb 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -1,8 +1,13 @@ -use crate::{ - vertex_attributes::convert_attribute, Gltf, GltfAssetLabel, GltfExtras, GltfMaterialExtras, - GltfMaterialName, GltfMeshExtras, GltfNode, GltfSceneExtras, GltfSkin, +mod extensions; +mod gltf_ext; + +use std::{ + io::Error, + path::{Path, PathBuf}, }; +#[cfg(feature = "bevy_animation")] +use bevy_animation::{prelude::*, AnimationTarget, AnimationTargetId}; use bevy_asset::{ io::Reader, AssetLoadError, AssetLoader, Handle, LoadContext, ReadAssetBytesError, }; @@ -15,17 +20,17 @@ use bevy_ecs::{ world::World, }; use bevy_image::{ - CompressedImageFormats, Image, ImageAddressMode, ImageFilterMode, ImageLoaderSettings, - ImageSampler, ImageSamplerDescriptor, ImageType, TextureError, + CompressedImageFormats, Image, ImageLoaderSettings, ImageSampler, ImageSamplerDescriptor, + ImageType, TextureError, }; -use bevy_math::{Affine2, Mat4, Vec3}; +use bevy_math::{Mat4, Vec3}; +#[cfg(feature = "pbr_transmission_textures")] +use bevy_pbr::UvChannel; use bevy_pbr::{ - DirectionalLight, MeshMaterial3d, PointLight, SpotLight, StandardMaterial, UvChannel, - MAX_JOINTS, + DirectionalLight, MeshMaterial3d, PointLight, SpotLight, StandardMaterial, MAX_JOINTS, }; use bevy_platform_support::collections::{HashMap, HashSet}; use bevy_render::{ - alpha::AlphaMode, camera::{Camera, OrthographicProjection, PerspectiveProjection, Projection, ScalingMode}, mesh::{ morph::{MeshMorphWeights, MorphAttributes, MorphTargetImage, MorphWeights}, @@ -41,33 +46,38 @@ 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, - json, mesh::{util::ReadIndices, Mode}, - texture::{Info, MagFilter, MinFilter, TextureTransform, WrappingMode}, - Document, Material, Node, Primitive, Semantic, + Document, Material, Node, Semantic, }; -use itertools::Itertools; + use serde::{Deserialize, Serialize}; -#[cfg(any( - feature = "pbr_specular_textures", - feature = "pbr_multi_layer_material_textures" -))] -use serde_json::Map; -use serde_json::{value, Value}; -use std::{ - io::Error, - path::{Path, PathBuf}, -}; +#[cfg(feature = "bevy_animation")] +use smallvec::SmallVec; + use thiserror::Error; use tracing::{error, info_span, warn}; -#[cfg(feature = "bevy_animation")] -use { - bevy_animation::{prelude::*, AnimationTarget, AnimationTargetId}, - smallvec::SmallVec, + +use crate::{ + vertex_attributes::convert_attribute, Gltf, GltfAssetLabel, GltfExtras, GltfMaterialExtras, + GltfMaterialName, GltfMeshExtras, GltfNode, GltfSceneExtras, GltfSkin, +}; + +use self::{ + extensions::{AnisotropyExtension, ClearcoatExtension, SpecularExtension}, + gltf_ext::{ + check_for_cycles, get_linear_textures, + material::{ + alpha_mode, material_label, needs_tangents, uv_channel, + warn_on_differing_texture_transforms, + }, + mesh::{primitive_name, primitive_topology}, + scene::{collect_path, node_name, node_transform}, + texture::{texture_handle, texture_sampler, texture_transform_to_affine2}, + }, }; /// An error that occurs when loading a glTF file. @@ -194,6 +204,7 @@ impl AssetLoader for GltfLoader { ) -> Result { let mut bytes = Vec::new(); reader.read_to_end(&mut bytes).await?; + load_gltf(self, &bytes, load_context, settings).await } @@ -210,6 +221,7 @@ async fn load_gltf<'a, 'b, 'c>( settings: &'b GltfLoaderSettings, ) -> Result { let gltf = gltf::Gltf::from_slice(bytes)?; + let file_name = load_context .asset_path() .path() @@ -221,45 +233,7 @@ async fn load_gltf<'a, 'b, 'c>( .to_string(); let buffer_data = load_buffers(&gltf, load_context).await?; - let mut linear_textures = >::default(); - - for material in gltf.materials() { - if let Some(texture) = material.normal_texture() { - linear_textures.insert(texture.texture().index()); - } - if let Some(texture) = material.occlusion_texture() { - linear_textures.insert(texture.texture().index()); - } - if let Some(texture) = material - .pbr_metallic_roughness() - .metallic_roughness_texture() - { - linear_textures.insert(texture.texture().index()); - } - if let Some(texture_index) = material_extension_texture_index( - &material, - "KHR_materials_anisotropy", - "anisotropyTexture", - ) { - linear_textures.insert(texture_index); - } - - // None of the clearcoat maps should be loaded as sRGB. - #[cfg(feature = "pbr_multi_layer_material_textures")] - for texture_field_name in [ - "clearcoatTexture", - "clearcoatRoughnessTexture", - "clearcoatNormalTexture", - ] { - if let Some(texture_index) = material_extension_texture_index( - &material, - "KHR_materials_clearcoat", - texture_field_name, - ) { - linear_textures.insert(texture_index); - } - } - } + let linear_textures = get_linear_textures(&gltf.document); #[cfg(feature = "bevy_animation")] let paths = { @@ -267,7 +241,7 @@ async fn load_gltf<'a, 'b, 'c>( for scene in gltf.scenes() { for node in scene.nodes() { let root_index = node.index(); - paths_recur(node, &[], &mut paths, root_index, &mut HashSet::default()); + collect_path(&node, &[], &mut paths, root_index, &mut HashSet::default()); } } paths @@ -531,35 +505,6 @@ async fn load_gltf<'a, 'b, 'c>( (animations, named_animations, animation_roots) }; - // TODO: use the threaded impl on wasm once wasm thread pool doesn't deadlock on it - // See https://github.com/bevyengine/bevy/issues/1924 for more details - // The taskpool use is also avoided when there is only one texture for performance reasons and - // to avoid https://github.com/bevyengine/bevy/pull/2725 - // PERF: could this be a Vec instead? Are gltf texture indices dense? - fn process_loaded_texture( - load_context: &mut LoadContext, - handles: &mut Vec>, - texture: ImageOrPath, - ) { - let handle = match texture { - ImageOrPath::Image { label, image } => load_context - .add_labeled_asset(label.to_string(), image) - .expect("texture indices are unique, so the label is unique"), - ImageOrPath::Path { - path, - is_srgb, - sampler_descriptor, - } => load_context - .loader() - .with_settings(move |settings: &mut ImageLoaderSettings| { - settings.is_srgb = is_srgb; - settings.sampler = ImageSampler::Descriptor(sampler_descriptor.clone()); - }) - .load(path), - }; - handles.push(handle); - } - // We collect handles to ensure loaded images from paths are not unloaded before they are used elsewhere // in the loader. This prevents "reloads", but it also prevents dropping the is_srgb context on reload. // @@ -579,7 +524,7 @@ async fn load_gltf<'a, 'b, 'c>( settings.load_materials, ) .await?; - process_loaded_texture(load_context, &mut _texture_handles, image); + image.process_loaded_texture(load_context, &mut _texture_handles); } } else { #[cfg(not(target_arch = "wasm32"))] @@ -605,7 +550,7 @@ async fn load_gltf<'a, 'b, 'c>( .into_iter() .for_each(|result| match result { Ok(image) => { - process_loaded_texture(load_context, &mut _texture_handles, image); + image.process_loaded_texture(load_context, &mut _texture_handles); } Err(err) => { warn!("Error loading glTF texture: {}", err); @@ -646,7 +591,7 @@ async fn load_gltf<'a, 'b, 'c>( mesh: gltf_mesh.index(), primitive: primitive.index(), }; - let primitive_topology = get_primitive_topology(primitive.mode())?; + let primitive_topology = primitive_topology(primitive.mode())?; let mut mesh = Mesh::new(primitive_topology, settings.load_meshes); @@ -734,7 +679,7 @@ async fn load_gltf<'a, 'b, 'c>( { mesh.insert_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute); } else if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_some() - && material_needs_tangents(&primitive.material()) + && needs_tangents(&primitive.material()) { tracing::debug!( "Missing vertex tangents for {}, computing them using the mikktspace algorithm. Consider using a tool such as Blender to pre-compute the tangents.", file_name @@ -763,13 +708,20 @@ async fn load_gltf<'a, 'b, 'c>( .material() .index() .and_then(|i| materials.get(i).cloned()), - get_gltf_extras(primitive.extras()), - get_gltf_extras(primitive.material().extras()), + primitive.extras().as_deref().map(GltfExtras::from), + primitive + .material() + .extras() + .as_deref() + .map(GltfExtras::from), )); } - let mesh = - super::GltfMesh::new(&gltf_mesh, primitives, get_gltf_extras(gltf_mesh.extras())); + let mesh = super::GltfMesh::new( + &gltf_mesh, + primitives, + gltf_mesh.extras().as_deref().map(GltfExtras::from), + ); let handle = load_context .add_labeled_asset(mesh.asset_label().to_string(), mesh) @@ -792,7 +744,7 @@ async fn load_gltf<'a, 'b, 'c>( load_context .add_labeled_asset( - inverse_bind_matrices_label(&gltf_skin), + GltfAssetLabel::InverseBindMatrices(gltf_skin.index()).to_string(), SkinnedMeshInverseBindposes::from(local_to_bone_bind_matrices), ) .expect("inverse bind matrix indices are unique, so the label is unique") @@ -812,7 +764,7 @@ async fn load_gltf<'a, 'b, 'c>( } // Then check for cycles. - check_gltf_for_cycles(&gltf)?; + check_for_cycles(&gltf)?; // Now populate the nodes. for node in gltf.nodes() { @@ -840,11 +792,11 @@ async fn load_gltf<'a, 'b, 'c>( &skin, joints, skinned_mesh_inverse_bindposes[skin.index()].clone(), - get_gltf_extras(skin.extras()), + skin.extras().as_deref().map(GltfExtras::from), ); let handle = load_context - .add_labeled_asset(skin_label(&skin), gltf_skin) + .add_labeled_asset(gltf_skin.asset_label().to_string(), gltf_skin) .expect("skin indices are unique, so the label is unique"); if let Some(name) = skin.name() { @@ -872,7 +824,7 @@ async fn load_gltf<'a, 'b, 'c>( mesh, node_transform(&node), skin, - get_gltf_extras(node.extras()), + node.extras().as_deref().map(GltfExtras::from), ); #[cfg(feature = "bevy_animation")] @@ -970,7 +922,10 @@ async fn load_gltf<'a, 'b, 'c>( } let loaded_scene = scene_load_context.finish(Scene::new(world)); let scene_handle = load_context - .add_loaded_labeled_asset(scene_label(&scene), loaded_scene) + .add_loaded_labeled_asset( + GltfAssetLabel::Scene(scene.index()).to_string(), + loaded_scene, + ) .expect("scene indices are unique, so the label is unique"); if let Some(name) = scene.name() { @@ -1006,62 +961,6 @@ async fn load_gltf<'a, 'b, 'c>( }) } -fn get_gltf_extras(extras: &json::Extras) -> Option { - extras.as_ref().map(|extras| GltfExtras { - value: extras.get().to_string(), - }) -} - -/// Calculate the transform of gLTF node. -/// -/// This should be used instead of calling [`gltf::scene::Transform::matrix()`] -/// on [`Node::transform()`] directly because it uses optimized glam types and -/// if `libm` feature of `bevy_math` crate is enabled also handles cross -/// platform determinism properly. -fn node_transform(node: &Node) -> Transform { - match node.transform() { - gltf::scene::Transform::Matrix { matrix } => { - Transform::from_matrix(Mat4::from_cols_array_2d(&matrix)) - } - gltf::scene::Transform::Decomposed { - translation, - rotation, - scale, - } => Transform { - translation: Vec3::from(translation), - rotation: bevy_math::Quat::from_array(rotation), - scale: Vec3::from(scale), - }, - } -} - -fn node_name(node: &Node) -> Name { - let name = node - .name() - .map(ToString::to_string) - .unwrap_or_else(|| format!("GltfNode{}", node.index())); - Name::new(name) -} - -#[cfg(feature = "bevy_animation")] -fn paths_recur( - node: Node, - current_path: &[Name], - paths: &mut HashMap)>, - root_index: usize, - visited: &mut HashSet, -) { - let mut path = current_path.to_owned(); - path.push(node_name(&node)); - visited.insert(node.index()); - for child in node.children() { - if !visited.contains(&child.index()) { - paths_recur(child, &path, paths, root_index, visited); - } - } - paths.insert(node.index(), (root_index, path)); -} - /// Loads a glTF texture as a bevy [`Image`] and returns it together with its label. async fn load_image<'a, 'b>( gltf_texture: gltf::Texture<'a>, @@ -1139,40 +1038,37 @@ fn load_material( ) -> Handle { let material_label = material_label(material, is_scale_inverted); load_context - .labeled_asset_scope(material_label, |load_context| { + .labeled_asset_scope(material_label.to_string(), |load_context| { let pbr = material.pbr_metallic_roughness(); // TODO: handle missing label handle errors here? let color = pbr.base_color_factor(); let base_color_channel = pbr .base_color_texture() - .map(|info| get_uv_channel(material, "base color", info.tex_coord())) + .map(|info| uv_channel(material, "base color", info.tex_coord())) .unwrap_or_default(); let base_color_texture = pbr .base_color_texture() - .map(|info| texture_handle(load_context, &info.texture())); + .map(|info| texture_handle(&info.texture(), load_context)); let uv_transform = pbr .base_color_texture() - .and_then(|info| { - info.texture_transform() - .map(convert_texture_transform_to_affine2) - }) + .and_then(|info| info.texture_transform().map(texture_transform_to_affine2)) .unwrap_or_default(); let normal_map_channel = material .normal_texture() - .map(|info| get_uv_channel(material, "normal map", info.tex_coord())) + .map(|info| uv_channel(material, "normal map", info.tex_coord())) .unwrap_or_default(); let normal_map_texture: Option> = material.normal_texture().map(|normal_texture| { // TODO: handle normal_texture.scale - texture_handle(load_context, &normal_texture.texture()) + texture_handle(&normal_texture.texture(), load_context) }); let metallic_roughness_channel = pbr .metallic_roughness_texture() - .map(|info| get_uv_channel(material, "metallic/roughness", info.tex_coord())) + .map(|info| uv_channel(material, "metallic/roughness", info.tex_coord())) .unwrap_or_default(); let metallic_roughness_texture = pbr.metallic_roughness_texture().map(|info| { warn_on_differing_texture_transforms( @@ -1181,27 +1077,27 @@ fn load_material( uv_transform, "metallic/roughness", ); - texture_handle(load_context, &info.texture()) + texture_handle(&info.texture(), load_context) }); let occlusion_channel = material .occlusion_texture() - .map(|info| get_uv_channel(material, "occlusion", info.tex_coord())) + .map(|info| uv_channel(material, "occlusion", info.tex_coord())) .unwrap_or_default(); let occlusion_texture = material.occlusion_texture().map(|occlusion_texture| { // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) - texture_handle(load_context, &occlusion_texture.texture()) + texture_handle(&occlusion_texture.texture(), load_context) }); let emissive = material.emissive_factor(); let emissive_channel = material .emissive_texture() - .map(|info| get_uv_channel(material, "emissive", info.tex_coord())) + .map(|info| uv_channel(material, "emissive", info.tex_coord())) .unwrap_or_default(); let emissive_texture = material.emissive_texture().map(|info| { // TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength) warn_on_differing_texture_transforms(material, &info, uv_transform, "emissive"); - texture_handle(load_context, &info.texture()) + texture_handle(&info.texture(), load_context) }); #[cfg(feature = "pbr_transmission_textures")] @@ -1214,14 +1110,12 @@ fn load_material( .map_or((0.0, UvChannel::Uv0, None), |transmission| { let specular_transmission_channel = transmission .transmission_texture() - .map(|info| { - get_uv_channel(material, "specular/transmission", info.tex_coord()) - }) + .map(|info| uv_channel(material, "specular/transmission", info.tex_coord())) .unwrap_or_default(); let transmission_texture: Option> = transmission .transmission_texture() .map(|transmission_texture| { - texture_handle(load_context, &transmission_texture.texture()) + texture_handle(&transmission_texture.texture(), load_context) }); ( @@ -1248,11 +1142,11 @@ fn load_material( |volume| { let thickness_channel = volume .thickness_texture() - .map(|info| get_uv_channel(material, "thickness", info.tex_coord())) + .map(|info| uv_channel(material, "thickness", info.tex_coord())) .unwrap_or_default(); let thickness_texture: Option> = volume.thickness_texture().map(|thickness_texture| { - texture_handle(load_context, &thickness_texture.texture()) + texture_handle(&thickness_texture.texture(), load_context) }); ( @@ -1383,68 +1277,6 @@ fn load_material( .expect("material indices are unique, so the label is unique") } -fn get_uv_channel(material: &Material, texture_kind: &str, tex_coord: u32) -> UvChannel { - match tex_coord { - 0 => UvChannel::Uv0, - 1 => UvChannel::Uv1, - _ => { - let material_name = material - .name() - .map(|n| format!("the material \"{n}\"")) - .unwrap_or_else(|| "an unnamed material".to_string()); - let material_index = material - .index() - .map(|i| format!("index {i}")) - .unwrap_or_else(|| "default".to_string()); - warn!( - "Only 2 UV Channels are supported, but {material_name} ({material_index}) \ - has the TEXCOORD attribute {} on texture kind {texture_kind}, which will fallback to 0.", - tex_coord, - ); - UvChannel::Uv0 - } - } -} - -fn convert_texture_transform_to_affine2(texture_transform: TextureTransform) -> Affine2 { - Affine2::from_scale_angle_translation( - texture_transform.scale().into(), - -texture_transform.rotation(), - texture_transform.offset().into(), - ) -} - -fn warn_on_differing_texture_transforms( - material: &Material, - info: &Info, - texture_transform: Affine2, - texture_kind: &str, -) { - let has_differing_texture_transform = info - .texture_transform() - .map(convert_texture_transform_to_affine2) - .is_some_and(|t| t != texture_transform); - if has_differing_texture_transform { - let material_name = material - .name() - .map(|n| format!("the material \"{n}\"")) - .unwrap_or_else(|| "an unnamed material".to_string()); - let texture_name = info - .texture() - .name() - .map(|n| format!("its {texture_kind} texture \"{n}\"")) - .unwrap_or_else(|| format!("its unnamed {texture_kind} texture")); - let material_index = material - .index() - .map(|i| format!("index {i}")) - .unwrap_or_else(|| "default".to_string()); - warn!( - "Only texture transforms on base color textures are supported, but {material_name} ({material_index}) \ - has a texture transform on {texture_name} (index {}), which will be ignored.", info.texture().index() - ); - } -} - /// Loads a glTF node. #[expect( clippy::result_large_err, @@ -1562,7 +1394,7 @@ fn load_node( // append primitives for primitive in mesh.primitives() { let material = primitive.material(); - let material_label = material_label(&material, is_scale_inverted); + let material_label = material_label(&material, is_scale_inverted).to_string(); // This will make sure we load the default material now since it would not have been // added when iterating over all the gltf materials (since the default material is @@ -1756,164 +1588,6 @@ fn load_node( } } -fn primitive_name(mesh: &gltf::Mesh, primitive: &Primitive) -> String { - let mesh_name = mesh.name().unwrap_or("Mesh"); - if mesh.primitives().len() > 1 { - format!("{}.{}", mesh_name, primitive.index()) - } else { - mesh_name.to_string() - } -} - -/// Returns the label for the `material`. -fn material_label(material: &Material, is_scale_inverted: bool) -> String { - if let Some(index) = material.index() { - GltfAssetLabel::Material { - index, - is_scale_inverted, - } - .to_string() - } else { - GltfAssetLabel::DefaultMaterial.to_string() - } -} - -fn texture_handle(load_context: &mut LoadContext, texture: &gltf::Texture) -> Handle { - match texture.source().source() { - Source::View { .. } => { - load_context.get_label_handle(GltfAssetLabel::Texture(texture.index()).to_string()) - } - Source::Uri { uri, .. } => { - let uri = percent_encoding::percent_decode_str(uri) - .decode_utf8() - .unwrap(); - let uri = uri.as_ref(); - if let Ok(_data_uri) = DataUri::parse(uri) { - load_context.get_label_handle(GltfAssetLabel::Texture(texture.index()).to_string()) - } else { - let parent = load_context.path().parent().unwrap(); - let image_path = parent.join(uri); - load_context.load(image_path) - } - } - } -} - -/// Given a [`json::texture::Info`], returns the handle of the texture that this -/// refers to. -/// -/// This is a low-level function only used when the `gltf` crate has no support -/// for an extension, forcing us to parse its texture references manually. -#[cfg(any( - feature = "pbr_anisotropy_texture", - feature = "pbr_multi_layer_material_textures", - feature = "pbr_specular_textures" -))] -fn texture_handle_from_info( - load_context: &mut LoadContext, - document: &Document, - texture_info: &json::texture::Info, -) -> Handle { - let texture = document - .textures() - .nth(texture_info.index.value()) - .expect("Texture info references a nonexistent texture"); - texture_handle(load_context, &texture) -} - -/// Returns the label for the `scene`. -fn scene_label(scene: &gltf::Scene) -> String { - GltfAssetLabel::Scene(scene.index()).to_string() -} - -/// Return the label for the `skin`. -fn skin_label(skin: &gltf::Skin) -> String { - GltfAssetLabel::Skin(skin.index()).to_string() -} - -/// Return the label for the `inverseBindMatrices` of the node. -fn inverse_bind_matrices_label(skin: &gltf::Skin) -> String { - GltfAssetLabel::InverseBindMatrices(skin.index()).to_string() -} - -/// Extracts the texture sampler data from the glTF texture. -fn texture_sampler(texture: &gltf::Texture) -> ImageSamplerDescriptor { - let gltf_sampler = texture.sampler(); - - ImageSamplerDescriptor { - address_mode_u: texture_address_mode(&gltf_sampler.wrap_s()), - address_mode_v: texture_address_mode(&gltf_sampler.wrap_t()), - - mag_filter: gltf_sampler - .mag_filter() - .map(|mf| match mf { - MagFilter::Nearest => ImageFilterMode::Nearest, - MagFilter::Linear => ImageFilterMode::Linear, - }) - .unwrap_or(ImageSamplerDescriptor::default().mag_filter), - - min_filter: gltf_sampler - .min_filter() - .map(|mf| match mf { - MinFilter::Nearest - | MinFilter::NearestMipmapNearest - | MinFilter::NearestMipmapLinear => ImageFilterMode::Nearest, - MinFilter::Linear - | MinFilter::LinearMipmapNearest - | MinFilter::LinearMipmapLinear => ImageFilterMode::Linear, - }) - .unwrap_or(ImageSamplerDescriptor::default().min_filter), - - mipmap_filter: gltf_sampler - .min_filter() - .map(|mf| match mf { - MinFilter::Nearest - | MinFilter::Linear - | MinFilter::NearestMipmapNearest - | MinFilter::LinearMipmapNearest => ImageFilterMode::Nearest, - MinFilter::NearestMipmapLinear | MinFilter::LinearMipmapLinear => { - ImageFilterMode::Linear - } - }) - .unwrap_or(ImageSamplerDescriptor::default().mipmap_filter), - - ..Default::default() - } -} - -/// Maps the texture address mode from glTF to wgpu. -fn texture_address_mode(gltf_address_mode: &WrappingMode) -> ImageAddressMode { - match gltf_address_mode { - WrappingMode::ClampToEdge => ImageAddressMode::ClampToEdge, - WrappingMode::Repeat => ImageAddressMode::Repeat, - WrappingMode::MirroredRepeat => ImageAddressMode::MirrorRepeat, - } -} - -/// Maps the `primitive_topology` from glTF to `wgpu`. -#[expect( - clippy::result_large_err, - reason = "`GltfError` is only barely past the threshold for large errors." -)] -fn get_primitive_topology(mode: Mode) -> Result { - match mode { - Mode::Points => Ok(PrimitiveTopology::PointList), - Mode::Lines => Ok(PrimitiveTopology::LineList), - Mode::LineStrip => Ok(PrimitiveTopology::LineStrip), - Mode::Triangles => Ok(PrimitiveTopology::TriangleList), - Mode::TriangleStrip => Ok(PrimitiveTopology::TriangleStrip), - mode => Err(GltfError::UnsupportedPrimitive { mode }), - } -} - -fn alpha_mode(material: &Material) -> AlphaMode { - match material.alpha_mode() { - gltf::material::AlphaMode::Opaque => AlphaMode::Opaque, - gltf::material::AlphaMode::Mask => AlphaMode::Mask(material.alpha_cutoff().unwrap_or(0.5)), - gltf::material::AlphaMode::Blend => AlphaMode::Blend, - } -} - /// Loads the raw glTF buffer data for a specific glTF file. async fn load_buffers( gltf: &gltf::Gltf, @@ -1955,33 +1629,16 @@ async fn load_buffers( Ok(buffer_data) } -enum ImageOrPath { - Image { - image: Image, - label: GltfAssetLabel, - }, - Path { - path: PathBuf, - is_srgb: bool, - sampler_descriptor: ImageSamplerDescriptor, - }, -} - struct DataUri<'a> { - mime_type: &'a str, - base64: bool, - data: &'a str, -} - -fn split_once(input: &str, delimiter: char) -> Option<(&str, &str)> { - let mut iter = input.splitn(2, delimiter); - Some((iter.next()?, iter.next()?)) + pub mime_type: &'a str, + pub base64: bool, + pub data: &'a str, } impl<'a> DataUri<'a> { fn parse(uri: &'a str) -> Result, ()> { let uri = uri.strip_prefix("data:").ok_or(())?; - let (mime_type, data) = split_once(uri, ',').ok_or(())?; + let (mime_type, data) = Self::split_once(uri, ',').ok_or(())?; let (mime_type, base64) = match mime_type.strip_suffix(";base64") { Some(mime_type) => (mime_type, true), @@ -2002,15 +1659,64 @@ impl<'a> DataUri<'a> { Ok(self.data.as_bytes().to_owned()) } } + + fn split_once(input: &str, delimiter: char) -> Option<(&str, &str)> { + let mut iter = input.splitn(2, delimiter); + Some((iter.next()?, iter.next()?)) + } +} + +enum ImageOrPath { + Image { + image: Image, + label: GltfAssetLabel, + }, + Path { + path: PathBuf, + is_srgb: bool, + sampler_descriptor: ImageSamplerDescriptor, + }, +} + +impl ImageOrPath { + // TODO: use the threaded impl on wasm once wasm thread pool doesn't deadlock on it + // See https://github.com/bevyengine/bevy/issues/1924 for more details + // The taskpool use is also avoided when there is only one texture for performance reasons and + // to avoid https://github.com/bevyengine/bevy/pull/2725 + // PERF: could this be a Vec instead? Are gltf texture indices dense? + fn process_loaded_texture( + self, + load_context: &mut LoadContext, + handles: &mut Vec>, + ) { + let handle = match self { + ImageOrPath::Image { label, image } => load_context + .add_labeled_asset(label.to_string(), image) + .expect("texture indices are unique, so the label is unique"), + ImageOrPath::Path { + path, + is_srgb, + sampler_descriptor, + } => load_context + .loader() + .with_settings(move |settings: &mut ImageLoaderSettings| { + settings.is_srgb = is_srgb; + settings.sampler = ImageSampler::Descriptor(sampler_descriptor.clone()); + }) + .load(path), + }; + handles.push(handle); + } } -pub(super) struct PrimitiveMorphAttributesIter<'s>( +struct PrimitiveMorphAttributesIter<'s>( pub ( Option>, Option>, Option>, ), ); + impl<'s> Iterator for PrimitiveMorphAttributesIter<'s> { type Item = MorphAttributes; @@ -2030,377 +1736,22 @@ impl<'s> Iterator for PrimitiveMorphAttributesIter<'s> { } } -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -struct MorphTargetNames { - pub target_names: Vec, -} - -// A helper structure for `load_node` that contains information about the -// nearest ancestor animation root. +/// 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. - root: Entity, - // The path to the animation root. This is used for constructing the - // animation target UUIDs. - path: SmallVec<[Name; 8]>, -} - -/// Parsed data from the `KHR_materials_clearcoat` extension. -/// -/// See the specification: -/// -#[derive(Default)] -struct ClearcoatExtension { - clearcoat_factor: Option, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_channel: UvChannel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_texture: Option>, - clearcoat_roughness_factor: Option, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_roughness_channel: UvChannel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_roughness_texture: Option>, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_normal_channel: UvChannel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_normal_texture: Option>, -} - -impl ClearcoatExtension { - #[expect( - clippy::allow_attributes, - reason = "`unused_variables` is not always linted" - )] - #[allow( - unused_variables, - reason = "Depending on what features are used to compile this crate, certain parameters may end up unused." - )] - fn parse( - load_context: &mut LoadContext, - document: &Document, - material: &Material, - ) -> Option { - let extension = material - .extensions()? - .get("KHR_materials_clearcoat")? - .as_object()?; - - #[cfg(feature = "pbr_multi_layer_material_textures")] - let (clearcoat_channel, clearcoat_texture) = parse_material_extension_texture( - load_context, - document, - material, - extension, - "clearcoatTexture", - "clearcoat", - ); - - #[cfg(feature = "pbr_multi_layer_material_textures")] - let (clearcoat_roughness_channel, clearcoat_roughness_texture) = - parse_material_extension_texture( - load_context, - document, - material, - extension, - "clearcoatRoughnessTexture", - "clearcoat roughness", - ); - - #[cfg(feature = "pbr_multi_layer_material_textures")] - let (clearcoat_normal_channel, clearcoat_normal_texture) = parse_material_extension_texture( - load_context, - document, - material, - extension, - "clearcoatNormalTexture", - "clearcoat normal", - ); - - Some(ClearcoatExtension { - clearcoat_factor: extension.get("clearcoatFactor").and_then(Value::as_f64), - clearcoat_roughness_factor: extension - .get("clearcoatRoughnessFactor") - .and_then(Value::as_f64), - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_texture, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_roughness_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_roughness_texture, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_normal_channel, - #[cfg(feature = "pbr_multi_layer_material_textures")] - clearcoat_normal_texture, - }) - } -} - -/// Parsed data from the `KHR_materials_anisotropy` extension. -/// -/// See the specification: -/// -#[derive(Default)] -struct AnisotropyExtension { - anisotropy_strength: Option, - anisotropy_rotation: Option, - #[cfg(feature = "pbr_anisotropy_texture")] - anisotropy_channel: UvChannel, - #[cfg(feature = "pbr_anisotropy_texture")] - anisotropy_texture: Option>, -} - -impl AnisotropyExtension { - #[expect( - clippy::allow_attributes, - reason = "`unused_variables` is not always linted" - )] - #[allow( - unused_variables, - reason = "Depending on what features are used to compile this crate, certain parameters may end up unused." - )] - fn parse( - load_context: &mut LoadContext, - document: &Document, - material: &Material, - ) -> Option { - let extension = material - .extensions()? - .get("KHR_materials_anisotropy")? - .as_object()?; - - #[cfg(feature = "pbr_anisotropy_texture")] - let (anisotropy_channel, anisotropy_texture) = extension - .get("anisotropyTexture") - .and_then(|value| value::from_value::(value.clone()).ok()) - .map(|json_info| { - ( - get_uv_channel(material, "anisotropy", json_info.tex_coord), - texture_handle_from_info(load_context, document, &json_info), - ) - }) - .unzip(); - - Some(AnisotropyExtension { - anisotropy_strength: extension.get("anisotropyStrength").and_then(Value::as_f64), - anisotropy_rotation: extension.get("anisotropyRotation").and_then(Value::as_f64), - #[cfg(feature = "pbr_anisotropy_texture")] - anisotropy_channel: anisotropy_channel.unwrap_or_default(), - #[cfg(feature = "pbr_anisotropy_texture")] - anisotropy_texture, - }) - } -} - -/// Parsed data from the `KHR_materials_specular` extension. -/// -/// We currently don't parse `specularFactor` and `specularTexture`, since -/// they're incompatible with Filament. -/// -/// Note that the map is a *specular map*, not a *reflectance map*. In Bevy and -/// Filament terms, the reflectance values in the specular map range from [0.0, -/// 0.5], rather than [0.0, 1.0]. This is an unfortunate -/// `KHR_materials_specular` specification requirement that stems from the fact -/// that glTF is specified in terms of a specular strength model, not the -/// reflectance model that Filament and Bevy use. A workaround, which is noted -/// in the [`StandardMaterial`] documentation, is to set the reflectance value -/// to 2.0, which spreads the specular map range from [0.0, 1.0] as normal. -/// -/// See the specification: -/// -#[derive(Default)] -struct SpecularExtension { - specular_factor: Option, - #[cfg(feature = "pbr_specular_textures")] - specular_channel: UvChannel, - #[cfg(feature = "pbr_specular_textures")] - specular_texture: Option>, - specular_color_factor: Option<[f64; 3]>, - #[cfg(feature = "pbr_specular_textures")] - specular_color_channel: UvChannel, - #[cfg(feature = "pbr_specular_textures")] - specular_color_texture: Option>, -} - -impl SpecularExtension { - fn parse( - _load_context: &mut LoadContext, - _document: &Document, - material: &Material, - ) -> Option { - let extension = material - .extensions()? - .get("KHR_materials_specular")? - .as_object()?; - - #[cfg(feature = "pbr_specular_textures")] - let (_specular_channel, _specular_texture) = parse_material_extension_texture( - _load_context, - _document, - material, - extension, - "specularTexture", - "specular", - ); - - #[cfg(feature = "pbr_specular_textures")] - let (_specular_color_channel, _specular_color_texture) = parse_material_extension_texture( - _load_context, - _document, - material, - extension, - "specularColorTexture", - "specular color", - ); - - Some(SpecularExtension { - specular_factor: extension.get("specularFactor").and_then(Value::as_f64), - #[cfg(feature = "pbr_specular_textures")] - specular_channel: _specular_channel, - #[cfg(feature = "pbr_specular_textures")] - specular_texture: _specular_texture, - specular_color_factor: extension - .get("specularColorFactor") - .and_then(Value::as_array) - .and_then(|json_array| { - if json_array.len() < 3 { - None - } else { - Some([ - json_array[0].as_f64()?, - json_array[1].as_f64()?, - json_array[2].as_f64()?, - ]) - } - }), - #[cfg(feature = "pbr_specular_textures")] - specular_color_channel: _specular_color_channel, - #[cfg(feature = "pbr_specular_textures")] - specular_color_texture: _specular_color_texture, - }) - } -} - -/// Parses a texture that's part of a material extension block and returns its -/// UV channel and image reference. -#[cfg(any( - feature = "pbr_specular_textures", - feature = "pbr_multi_layer_material_textures" -))] -fn parse_material_extension_texture( - load_context: &mut LoadContext, - document: &Document, - material: &Material, - extension: &Map, - texture_name: &str, - texture_kind: &str, -) -> (UvChannel, Option>) { - match extension - .get(texture_name) - .and_then(|value| value::from_value::(value.clone()).ok()) - { - Some(json_info) => ( - get_uv_channel(material, texture_kind, json_info.tex_coord), - Some(texture_handle_from_info(load_context, document, &json_info)), - ), - None => (UvChannel::default(), None), - } -} - -/// Returns the index (within the `textures` array) of the texture with the -/// given field name in the data for the material extension with the given name, -/// if there is one. -fn material_extension_texture_index( - material: &Material, - extension_name: &str, - texture_field_name: &str, -) -> Option { - Some( - value::from_value::( - material - .extensions()? - .get(extension_name)? - .as_object()? - .get(texture_field_name)? - .clone(), - ) - .ok()? - .index - .value(), - ) -} - -/// Returns true if the material needs mesh tangents in order to be successfully -/// rendered. -/// -/// We generate them if this function returns true. -fn material_needs_tangents(material: &Material) -> bool { - if material.normal_texture().is_some() { - return true; - } - - #[cfg(feature = "pbr_multi_layer_material_textures")] - if material_extension_texture_index( - material, - "KHR_materials_clearcoat", - "clearcoatNormalTexture", - ) - .is_some() - { - return true; - } - - false + /// 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]>, } -/// 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(()) - } +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct MorphTargetNames { + pub target_names: Vec, } #[cfg(test)]