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:
parent
831ecf030c
commit
11db71727d
315
crates/bevy_gltf/src/assets.rs
Normal file
315
crates/bevy_gltf/src/assets.rs
Normal 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);
|
||||||
127
crates/bevy_gltf/src/label.rs
Normal file
127
crates/bevy_gltf/src/label.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -90,36 +90,30 @@
|
|||||||
//!
|
//!
|
||||||
//! You can use [`GltfAssetLabel`] to ensure you are using the correct label.
|
//! You can use [`GltfAssetLabel`] to ensure you are using the correct label.
|
||||||
|
|
||||||
extern crate alloc;
|
mod assets;
|
||||||
|
mod label;
|
||||||
#[cfg(feature = "bevy_animation")]
|
|
||||||
use bevy_animation::AnimationClip;
|
|
||||||
use bevy_platform_support::collections::HashMap;
|
|
||||||
|
|
||||||
mod loader;
|
mod loader;
|
||||||
mod vertex_attributes;
|
mod vertex_attributes;
|
||||||
pub use loader::*;
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
|
use bevy_platform_support::collections::HashMap;
|
||||||
|
|
||||||
use bevy_app::prelude::*;
|
use bevy_app::prelude::*;
|
||||||
use bevy_asset::{Asset, AssetApp, AssetPath, Handle};
|
use bevy_asset::AssetApp;
|
||||||
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
|
|
||||||
use bevy_image::CompressedImageFormats;
|
use bevy_image::CompressedImageFormats;
|
||||||
use bevy_pbr::StandardMaterial;
|
use bevy_render::{mesh::MeshVertexAttribute, renderer::RenderDevice};
|
||||||
use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath};
|
|
||||||
use bevy_render::{
|
|
||||||
mesh::{skinning::SkinnedMeshInverseBindposes, Mesh, MeshVertexAttribute},
|
|
||||||
renderer::RenderDevice,
|
|
||||||
};
|
|
||||||
use bevy_scene::Scene;
|
|
||||||
|
|
||||||
/// The glTF prelude.
|
/// The glTF prelude.
|
||||||
///
|
///
|
||||||
/// This includes the most common types in this crate, re-exported for your convenience.
|
/// This includes the most common types in this crate, re-exported for your convenience.
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
#[doc(hidden)]
|
#[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.
|
/// Adds support for glTF file loading to the app.
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GltfPlugin {
|
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
100
crates/bevy_gltf/src/loader/extensions/khr_materials_specular.rs
Normal file
100
crates/bevy_gltf/src/loader/extensions/khr_materials_specular.rs
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
10
crates/bevy_gltf/src/loader/extensions/mod.rs
Normal file
10
crates/bevy_gltf/src/loader/extensions/mod.rs
Normal 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,
|
||||||
|
};
|
||||||
165
crates/bevy_gltf/src/loader/gltf_ext/material.rs
Normal file
165
crates/bevy_gltf/src/loader/gltf_ext/material.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
30
crates/bevy_gltf/src/loader/gltf_ext/mesh.rs
Normal file
30
crates/bevy_gltf/src/loader/gltf_ext/mesh.rs
Normal 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 }),
|
||||||
|
}
|
||||||
|
}
|
||||||
79
crates/bevy_gltf/src/loader/gltf_ext/mod.rs
Normal file
79
crates/bevy_gltf/src/loader/gltf_ext/mod.rs
Normal 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
|
||||||
|
}
|
||||||
91
crates/bevy_gltf/src/loader/gltf_ext/scene.rs
Normal file
91
crates/bevy_gltf/src/loader/gltf_ext/scene.rs
Normal 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));
|
||||||
|
}
|
||||||
126
crates/bevy_gltf/src/loader/gltf_ext/texture.rs
Normal file
126
crates/bevy_gltf/src/loader/gltf_ext/texture.rs
Normal 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)
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user