bevy/crates/bevy_gltf/src/lib.rs
Mikhail Novikov 52215ce072
Add labels to Gltf Node and Mesh assets (#13558)
# Objective

Add labels to GltfNode and GltfMesh - they are missing from the assets
even though they are need if one wants to write a custom Gltf spawning
logic.

Eg AnimationPlayer relies on Name component of the node entities to
control the animation. There is no way to actually get names of the gltf
nodes, thus you can't manually spawn subtree from the scene and animate
it.

## Solution

- Add label field and make use of existing label creation logic to store
it there.

## Testing

- Ran all tests
- Fixed tests for node_hierarchy to use lable now

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: François Mockers <francois.mockers@vleue.com>
2024-06-05 23:10:33 +00:00

495 lines
18 KiB
Rust

#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
)]
//! Plugin providing an [`AssetLoader`](bevy_asset::AssetLoader) and type definitions
//! for loading glTF 2.0 (a standard 3D scene definition format) files in Bevy.
//!
//! The [glTF 2.0 specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html) defines the format of the glTF files.
//!
//! # Quick Start
//!
//! Here's how to spawn a simple glTF scene
//!
//! ```
//! # use bevy_ecs::prelude::*;
//! # use bevy_asset::prelude::*;
//! # use bevy_scene::prelude::*;
//! # use bevy_transform::prelude::*;
//! # use bevy_gltf::prelude::*;
//!
//! fn spawn_gltf(mut commands: Commands, asset_server: Res<AssetServer>) {
//! commands.spawn(SceneBundle {
//! // This is equivalent to "models/FlightHelmet/FlightHelmet.gltf#Scene0"
//! // The `#Scene0` label here is very important because it tells bevy to load the first scene in the glTF file.
//! // If this isn't specified bevy doesn't know which part of the glTF file to load.
//! scene: asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")),
//! // You can use the transform to give it a position
//! transform: Transform::from_xyz(2.0, 0.0, -5.0),
//! ..Default::default()
//! });
//! }
//! ```
//! # Loading parts of a glTF asset
//!
//! ## Using `Gltf`
//!
//! If you want to access part of the asset, you can load the entire `Gltf` using the `AssetServer`.
//! Once the `Handle<Gltf>` is loaded you can then use it to access named parts of it.
//!
//! ```
//! # use bevy_ecs::prelude::*;
//! # use bevy_asset::prelude::*;
//! # use bevy_scene::prelude::*;
//! # use bevy_transform::prelude::*;
//! # use bevy_gltf::Gltf;
//!
//! // Holds the scene handle
//! #[derive(Resource)]
//! struct HelmetScene(Handle<Gltf>);
//!
//! fn load_gltf(mut commands: Commands, asset_server: Res<AssetServer>) {
//! let gltf = asset_server.load("models/FlightHelmet/FlightHelmet.gltf");
//! commands.insert_resource(HelmetScene(gltf));
//! }
//!
//! fn spawn_gltf_objects(
//! mut commands: Commands,
//! helmet_scene: Res<HelmetScene>,
//! gltf_assets: Res<Assets<Gltf>>,
//! mut loaded: Local<bool>,
//! ) {
//! // Only do this once
//! if *loaded {
//! return;
//! }
//! // Wait until the scene is loaded
//! let Some(gltf) = gltf_assets.get(&helmet_scene.0) else {
//! return;
//! };
//! *loaded = true;
//!
//! commands.spawn(SceneBundle {
//! // Gets the first scene in the file
//! scene: gltf.scenes[0].clone(),
//! ..Default::default()
//! });
//!
//! commands.spawn(SceneBundle {
//! // Gets the scene named "Lenses_low"
//! scene: gltf.named_scenes["Lenses_low"].clone(),
//! transform: Transform::from_xyz(1.0, 2.0, 3.0),
//! ..Default::default()
//! });
//! }
//! ```
//!
//! ## Asset Labels
//!
//! The glTF loader let's you specify labels that let you target specific parts of the glTF.
//!
//! Be careful when using this feature, if you misspell a label it will simply ignore it without warning.
//!
//! You can use [`GltfAssetLabel`] to ensure you are using the correct label.
#[cfg(feature = "bevy_animation")]
use bevy_animation::AnimationClip;
use bevy_utils::HashMap;
mod loader;
mod vertex_attributes;
pub use loader::*;
use bevy_app::prelude::*;
use bevy_asset::{Asset, AssetApp, AssetPath, Handle};
use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_pbr::StandardMaterial;
use bevy_reflect::{Reflect, TypePath};
use bevy_render::{
mesh::{Mesh, MeshVertexAttribute},
renderer::RenderDevice,
texture::CompressedImageFormats,
};
use bevy_scene::Scene;
/// The `bevy_gltf` prelude.
pub mod prelude {
#[doc(hidden)]
pub use crate::{Gltf, GltfAssetLabel, GltfExtras};
}
/// Adds support for glTF file loading to the app.
#[derive(Default)]
pub struct GltfPlugin {
custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,
}
impl GltfPlugin {
/// Register a custom vertex attribute so that it is recognized when loading a glTF file with the [`GltfLoader`].
///
/// `name` must be the attribute name as found in the glTF data, which must start with an underscore.
/// See [this section of the glTF specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview)
/// for additional details on custom attributes.
pub fn add_custom_vertex_attribute(
mut self,
name: &str,
attribute: MeshVertexAttribute,
) -> Self {
self.custom_vertex_attributes.insert(name.into(), attribute);
self
}
}
impl Plugin for GltfPlugin {
fn build(&self, app: &mut App) {
app.register_type::<GltfExtras>()
.register_type::<GltfSceneExtras>()
.register_type::<GltfMeshExtras>()
.register_type::<GltfMaterialExtras>()
.init_asset::<Gltf>()
.init_asset::<GltfNode>()
.init_asset::<GltfPrimitive>()
.init_asset::<GltfMesh>()
.preregister_asset_loader::<GltfLoader>(&["gltf", "glb"]);
}
fn finish(&self, app: &mut App) {
let supported_compressed_formats = match app.world().get_resource::<RenderDevice>() {
Some(render_device) => CompressedImageFormats::from_features(render_device.features()),
None => CompressedImageFormats::NONE,
};
app.register_asset_loader(GltfLoader {
supported_compressed_formats,
custom_vertex_attributes: self.custom_vertex_attributes.clone(),
});
}
}
/// 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>>,
/// 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) 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,
/// Subasset label for this node within the gLTF parent asset.
pub asset_label: GltfAssetLabel,
/// Direct children of the node.
pub children: Vec<GltfNode>,
/// Mesh of the node.
pub mesh: Option<Handle<GltfMesh>>,
/// Local transform.
pub transform: bevy_transform::prelude::Transform,
/// 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<GltfNode>,
mesh: Option<Handle<GltfMesh>>,
transform: bevy_transform::prelude::Transform,
extras: Option<GltfExtras>,
) -> Self {
Self {
index: node.index(),
asset_label: GltfAssetLabel::Node(node.index()),
name: if let Some(name) = node.name() {
name.to_string()
} else {
format!("GltfNode{}", node.index())
},
children,
mesh,
transform,
extras,
}
}
}
/// 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,
/// Subasset label for this mesh within the gLTF parent asset.
pub asset_label: GltfAssetLabel,
/// 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(),
asset_label: GltfAssetLabel::Mesh(mesh.index()),
name: if let Some(name) = mesh.name() {
name.to_string()
} else {
format!("GltfMesh{}", mesh.index())
},
primitives,
extras,
}
}
}
/// 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,
/// Computed name for a primitive - either a user defined primitive name from gLTF or a generated name from index
pub name: String,
/// Subasset label for this mesh within the gLTF parent asset.
pub asset_label: GltfAssetLabel,
/// 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(),
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()
}
},
asset_label: GltfAssetLabel::Primitive {
mesh: gltf_mesh.index(),
primitive: gltf_primitive.index(),
},
mesh,
material,
extras,
material_extras,
}
}
}
/// 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)]
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)]
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)]
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)]
pub struct GltfMaterialExtras {
/// Content of the extra data.
pub value: 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)]
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
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`
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`: as above, if the glTF file contains a default material with no index
DefaultMaterial,
/// `Animation{}`: glTF Animation as Bevy `AnimationClip`
Animation(usize),
/// `Skin{}`: glTF mesh skin as Bevy `SkinnedMeshInverseBindposes`
Skin(usize),
}
impl std::fmt::Display for GltfAssetLabel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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}")),
}
}
}
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())
}
}