Refactor bevy_gltf (#15994)

# Objective

Refactor `bevy_gltf`, the criteria for the split is kind of arbitrary
but at least it is not a 2.6k line file.

## Solution

Move methods and structs found in `bevy_gltf/loader.rs` into multiple
new modules.

## Testing

`cargo run -p ci`
This commit is contained in:
Lucas Franca 2025-02-25 22:00:11 -03:00 committed by GitHub
parent 831ecf030c
commit 11db71727d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1378 additions and 1230 deletions

View File

@ -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<Handle<Scene>>,
/// Named scenes loaded from the glTF file.
pub named_scenes: HashMap<Box<str>, Handle<Scene>>,
/// All meshes loaded from the glTF file.
pub meshes: Vec<Handle<GltfMesh>>,
/// Named meshes loaded from the glTF file.
pub named_meshes: HashMap<Box<str>, Handle<GltfMesh>>,
/// All materials loaded from the glTF file.
pub materials: Vec<Handle<StandardMaterial>>,
/// Named materials loaded from the glTF file.
pub named_materials: HashMap<Box<str>, Handle<StandardMaterial>>,
/// All nodes loaded from the glTF file.
pub nodes: Vec<Handle<GltfNode>>,
/// Named nodes loaded from the glTF file.
pub named_nodes: HashMap<Box<str>, Handle<GltfNode>>,
/// All skins loaded from the glTF file.
pub skins: Vec<Handle<GltfSkin>>,
/// Named skins loaded from the glTF file.
pub named_skins: HashMap<Box<str>, Handle<GltfSkin>>,
/// Default scene to be displayed.
pub default_scene: Option<Handle<Scene>>,
/// All animations loaded from the glTF file.
#[cfg(feature = "bevy_animation")]
pub animations: Vec<Handle<AnimationClip>>,
/// Named animations loaded from the glTF file.
#[cfg(feature = "bevy_animation")]
pub named_animations: HashMap<Box<str>, Handle<AnimationClip>>,
/// The gltf root of the gltf asset, see <https://docs.rs/gltf/latest/gltf/struct.Gltf.html>. Only has a value when `GltfLoaderSettings::include_source` is true.
pub source: Option<gltf::Gltf>,
}
/// 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<GltfPrimitive>,
/// Additional data.
pub extras: Option<GltfExtras>,
}
impl GltfMesh {
/// Create a mesh extracting name and index from glTF def
pub fn new(
mesh: &gltf::Mesh,
primitives: Vec<GltfPrimitive>,
extras: Option<GltfExtras>,
) -> 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<Handle<GltfNode>>,
/// Mesh of the node.
pub mesh: Option<Handle<GltfMesh>>,
/// Skin of the node.
pub skin: Option<Handle<GltfSkin>>,
/// 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<GltfExtras>,
}
impl GltfNode {
/// Create a node extracting name and index from glTF def
pub fn new(
node: &gltf::Node,
children: Vec<Handle<GltfNode>>,
mesh: Option<Handle<GltfMesh>>,
transform: bevy_transform::prelude::Transform,
skin: Option<Handle<GltfSkin>>,
extras: Option<GltfExtras>,
) -> 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<Mesh>,
/// Material to apply to the `mesh`.
pub material: Option<Handle<StandardMaterial>>,
/// Additional data.
pub extras: Option<GltfExtras>,
/// Additional data of the `material`.
pub material_extras: Option<GltfExtras>,
}
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<Mesh>,
material: Option<Handle<StandardMaterial>>,
extras: Option<GltfExtras>,
material_extras: Option<GltfExtras>,
) -> 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<Handle<GltfNode>>,
/// Inverse-bind matrices of this skin.
pub inverse_bind_matrices: Handle<SkinnedMeshInverseBindposes>,
/// Additional data.
pub extras: Option<GltfExtras>,
}
impl GltfSkin {
/// Create a skin extracting name and index from glTF def
pub fn new(
skin: &gltf::Skin,
joints: Vec<Handle<GltfNode>>,
inverse_bind_matrices: Handle<SkinnedMeshInverseBindposes>,
extras: Option<GltfExtras>,
) -> 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);

View File

@ -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<AssetServer>) {
/// let gltf_scene: Handle<Scene> = 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<AssetServer>) {
/// let gltf_scene: Handle<Scene> = 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<AssetServer>) {
/// let gltf_scene: Handle<Scene> = asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf"));
/// }
/// ```
pub fn from_asset(&self, path: impl Into<AssetPath<'static>>) -> AssetPath<'static> {
path.into().with_label(self.to_string())
}
}

View File

@ -90,36 +90,30 @@
//!
//! You can use [`GltfAssetLabel`] to ensure you are using the correct label.
extern crate alloc;
#[cfg(feature = "bevy_animation")]
use bevy_animation::AnimationClip;
use bevy_platform_support::collections::HashMap;
mod assets;
mod label;
mod loader;
mod vertex_attributes;
pub use loader::*;
extern crate alloc;
use bevy_platform_support::collections::HashMap;
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<Handle<Scene>>,
/// Named scenes loaded from the glTF file.
pub named_scenes: HashMap<Box<str>, Handle<Scene>>,
/// All meshes loaded from the glTF file.
pub meshes: Vec<Handle<GltfMesh>>,
/// Named meshes loaded from the glTF file.
pub named_meshes: HashMap<Box<str>, Handle<GltfMesh>>,
/// All materials loaded from the glTF file.
pub materials: Vec<Handle<StandardMaterial>>,
/// Named materials loaded from the glTF file.
pub named_materials: HashMap<Box<str>, Handle<StandardMaterial>>,
/// All nodes loaded from the glTF file.
pub nodes: Vec<Handle<GltfNode>>,
/// Named nodes loaded from the glTF file.
pub named_nodes: HashMap<Box<str>, Handle<GltfNode>>,
/// All skins loaded from the glTF file.
pub skins: Vec<Handle<GltfSkin>>,
/// Named skins loaded from the glTF file.
pub named_skins: HashMap<Box<str>, Handle<GltfSkin>>,
/// Default scene to be displayed.
pub default_scene: Option<Handle<Scene>>,
/// All animations loaded from the glTF file.
#[cfg(feature = "bevy_animation")]
pub animations: Vec<Handle<AnimationClip>>,
/// Named animations loaded from the glTF file.
#[cfg(feature = "bevy_animation")]
pub named_animations: HashMap<Box<str>, Handle<AnimationClip>>,
/// The gltf root of the gltf asset, see <https://docs.rs/gltf/latest/gltf/struct.Gltf.html>. Only has a value when `GltfLoaderSettings::include_source` is true.
pub source: Option<gltf::Gltf>,
}
/// 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<Handle<GltfNode>>,
/// Mesh of the node.
pub mesh: Option<Handle<GltfMesh>>,
/// Skin of the node.
pub skin: Option<Handle<GltfSkin>>,
/// 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<GltfExtras>,
}
impl GltfNode {
/// Create a node extracting name and index from glTF def
pub fn new(
node: &gltf::Node,
children: Vec<Handle<GltfNode>>,
mesh: Option<Handle<GltfMesh>>,
transform: bevy_transform::prelude::Transform,
skin: Option<Handle<GltfSkin>>,
extras: Option<GltfExtras>,
) -> 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<Handle<GltfNode>>,
/// Inverse-bind matrices of this skin.
pub inverse_bind_matrices: Handle<SkinnedMeshInverseBindposes>,
/// Additional data.
pub extras: Option<GltfExtras>,
}
impl GltfSkin {
/// Create a skin extracting name and index from glTF def
pub fn new(
skin: &gltf::Skin,
joints: Vec<Handle<GltfNode>>,
inverse_bind_matrices: Handle<SkinnedMeshInverseBindposes>,
extras: Option<GltfExtras>,
) -> 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<GltfPrimitive>,
/// Additional data.
pub extras: Option<GltfExtras>,
}
impl GltfMesh {
/// Create a mesh extracting name and index from glTF def
pub fn new(
mesh: &gltf::Mesh,
primitives: Vec<GltfPrimitive>,
extras: Option<GltfExtras>,
) -> 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<Mesh>,
/// Material to apply to the `mesh`.
pub material: Option<Handle<StandardMaterial>>,
/// Additional data.
pub extras: Option<GltfExtras>,
/// Additional data of the `material`.
pub material_extras: Option<GltfExtras>,
}
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<Mesh>,
material: Option<Handle<StandardMaterial>>,
extras: Option<GltfExtras>,
material_extras: Option<GltfExtras>,
) -> 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<AssetServer>) {
/// let gltf_scene: Handle<Scene> = 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<AssetServer>) {
/// let gltf_scene: Handle<Scene> = 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<AssetServer>) {
/// let gltf_scene: Handle<Scene> = asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf"));
/// }
/// ```
pub fn from_asset(&self, path: impl Into<AssetPath<'static>>) -> AssetPath<'static> {
path.into().with_label(self.to_string())
}
}

View File

@ -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:
/// <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_anisotropy/README.md>
#[derive(Default)]
pub(crate) struct AnisotropyExtension {
pub(crate) anisotropy_strength: Option<f64>,
pub(crate) anisotropy_rotation: Option<f64>,
#[cfg(feature = "pbr_anisotropy_texture")]
pub(crate) anisotropy_channel: UvChannel,
#[cfg(feature = "pbr_anisotropy_texture")]
pub(crate) anisotropy_texture: Option<Handle<Image>>,
}
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<AnisotropyExtension> {
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::<Info>(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,
})
}
}

View File

@ -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:
/// <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_clearcoat/README.md>
#[derive(Default)]
pub(crate) struct ClearcoatExtension {
pub(crate) clearcoat_factor: Option<f64>,
#[cfg(feature = "pbr_multi_layer_material_textures")]
pub(crate) clearcoat_channel: UvChannel,
#[cfg(feature = "pbr_multi_layer_material_textures")]
pub(crate) clearcoat_texture: Option<Handle<Image>>,
pub(crate) clearcoat_roughness_factor: Option<f64>,
#[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<Handle<Image>>,
#[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<Handle<Image>>,
}
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<ClearcoatExtension> {
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,
})
}
}

View File

@ -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:
/// <https://github.com/KhronosGroup/glTF/blob/main/extensions/2.0/Khronos/KHR_materials_specular/README.md>
#[derive(Default)]
pub(crate) struct SpecularExtension {
pub(crate) specular_factor: Option<f64>,
#[cfg(feature = "pbr_specular_textures")]
pub(crate) specular_channel: UvChannel,
#[cfg(feature = "pbr_specular_textures")]
pub(crate) specular_texture: Option<Handle<Image>>,
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<Handle<Image>>,
}
impl SpecularExtension {
pub(crate) fn parse(
_load_context: &mut LoadContext,
_document: &Document,
material: &Material,
) -> Option<Self> {
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,
})
}
}

View File

@ -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,
};

View File

@ -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<String, Value>,
texture_name: &str,
texture_kind: &str,
) -> (UvChannel, Option<Handle<Image>>) {
match extension
.get(texture_name)
.and_then(|value| value::from_value::<Info>(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<usize> {
Some(
value::from_value::<Info>(
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
}
}

View File

@ -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<PrimitiveTopology, GltfError> {
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 }),
}
}

View File

@ -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<usize> {
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
}

View File

@ -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<usize, (usize, Vec<Name>)>,
root_index: usize,
visited: &mut HashSet<usize>,
) {
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));
}

View File

@ -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<Image> {
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<Image> {
let texture = document
.textures()
.nth(info.index.value())
.expect("Texture info references a nonexistent texture");
texture_handle(&texture, load_context)
}