Export glTF skins as a Gltf struct (#14343)

# Objective

- Make skin data of glTF meshes available for users, so it would be
possible to create skinned meshes without spawning a scene.
- I believe it contributes to
https://github.com/bevyengine/bevy/issues/13681 ?

## Solution

- Add a new `GltfSkin`, representing skin data from a glTF file, new
member `skin` to `GltfNode` and both `skins` + `named_skins` to `Gltf`
(a la meshes/nodes).
- Rewrite glTF nodes resolution as an iterator which sorts nodes by
their dependencies (nodes without dependencies first). So when we create
`GltfNodes` with their associated `GltfSkin` while iterating, their
dependencies already have been loaded.
- Make a distinction between `GltfSkin` and
`SkinnedMeshInverseBindposes` in assets: prior to this PR,
`GltfAssetLabel::Skin(n)` was responsible not for a skin, but for one of
skin's components. Now `GltfAssetLabel::InverseBindMatrices(n)` will map
to `SkinnedMeshInverseBindposes`, and `GltfAssetLabel::Skin(n)` will map
to `GltfSkin`.

## Testing

- New test `skin_node` does just that; it tests whether or not
`GltfSkin` was loaded properly.

## Migration Guide

- Change `GltfAssetLabel::Skin(..)` to
`GltfAssetLabel::InverseBindMatrices(..)`.
This commit is contained in:
barsoosayque 2024-08-06 03:14:42 +02:00 committed by GitHub
parent df61117850
commit 5f2570eb4c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 335 additions and 103 deletions

View File

@ -109,7 +109,7 @@ use bevy_ecs::{prelude::Component, reflect::ReflectComponent};
use bevy_pbr::StandardMaterial;
use bevy_reflect::{Reflect, TypePath};
use bevy_render::{
mesh::{Mesh, MeshVertexAttribute},
mesh::{skinning::SkinnedMeshInverseBindposes, Mesh, MeshVertexAttribute},
renderer::RenderDevice,
texture::CompressedImageFormats,
};
@ -153,6 +153,7 @@ impl Plugin for GltfPlugin {
.init_asset::<GltfNode>()
.init_asset::<GltfPrimitive>()
.init_asset::<GltfMesh>()
.init_asset::<GltfSkin>()
.preregister_asset_loader::<GltfLoader>(&["gltf", "glb"]);
}
@ -187,6 +188,10 @@ pub struct Gltf {
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.
@ -200,7 +205,8 @@ pub struct Gltf {
}
/// A glTF node with all of its child nodes, its [`GltfMesh`],
/// [`Transform`](bevy_transform::prelude::Transform) and an optional [`GltfExtras`].
/// [`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)]
@ -213,8 +219,13 @@ pub struct GltfNode {
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>,
}
@ -226,6 +237,7 @@ impl GltfNode {
children: Vec<Handle<GltfNode>>,
mesh: Option<Handle<GltfMesh>>,
transform: bevy_transform::prelude::Transform,
skin: Option<Handle<GltfSkin>>,
extras: Option<GltfExtras>,
) -> Self {
Self {
@ -238,16 +250,73 @@ impl GltfNode {
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 matricy 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`].
///
@ -449,8 +518,10 @@ pub enum GltfAssetLabel {
DefaultMaterial,
/// `Animation{}`: glTF Animation as Bevy `AnimationClip`
Animation(usize),
/// `Skin{}`: glTF mesh skin as Bevy `SkinnedMeshInverseBindposes`
/// `Skin{}`: glTF mesh skin as `GltfSkin`
Skin(usize),
/// `Skin{}/InverseBindMatrices`: glTF mesh skin matrices as Bevy `SkinnedMeshInverseBindposes`
InverseBindMatrices(usize),
}
impl std::fmt::Display for GltfAssetLabel {
@ -480,6 +551,9 @@ impl std::fmt::Display for GltfAssetLabel {
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"))
}
}
}
}

View File

@ -1,6 +1,6 @@
use crate::{
vertex_attributes::convert_attribute, Gltf, GltfAssetLabel, GltfExtras, GltfMaterialExtras,
GltfMeshExtras, GltfNode, GltfSceneExtras,
GltfMeshExtras, GltfNode, GltfSceneExtras, GltfSkin,
};
#[cfg(feature = "bevy_animation")]
@ -586,42 +586,6 @@ async fn load_gltf<'a, 'b, 'c>(
meshes.push(handle);
}
let mut nodes_intermediate = vec![];
let mut named_nodes_intermediate = HashMap::default();
for node in gltf.nodes() {
nodes_intermediate.push((
GltfNode::new(
&node,
vec![],
node.mesh()
.map(|mesh| mesh.index())
.and_then(|i: usize| meshes.get(i).cloned()),
node_transform(&node),
get_gltf_extras(node.extras()),
),
node.children()
.map(|child| {
(
child.index(),
load_context
.get_label_handle(format!("{}", GltfAssetLabel::Node(child.index()))),
)
})
.collect::<Vec<_>>(),
));
if let Some(name) = node.name() {
named_nodes_intermediate.insert(name, node.index());
}
}
let nodes = resolve_node_hierarchy(nodes_intermediate)?
.into_iter()
.map(|node| load_context.add_labeled_asset(node.asset_label().to_string(), node))
.collect::<Vec<Handle<GltfNode>>>();
let named_nodes = named_nodes_intermediate
.into_iter()
.filter_map(|(name, index)| nodes.get(index).map(|handle| (name.into(), handle.clone())))
.collect();
let skinned_mesh_inverse_bindposes: Vec<_> = gltf
.skins()
.map(|gltf_skin| {
@ -633,12 +597,76 @@ async fn load_gltf<'a, 'b, 'c>(
.collect();
load_context.add_labeled_asset(
skin_label(&gltf_skin),
inverse_bind_matrices_label(&gltf_skin),
SkinnedMeshInverseBindposes::from(local_to_bone_bind_matrices),
)
})
.collect();
let mut nodes = HashMap::<usize, Handle<GltfNode>>::new();
let mut named_nodes = HashMap::new();
let mut skins = vec![];
let mut named_skins = HashMap::default();
for node in GltfTreeIterator::try_new(&gltf)? {
let skin = node.skin().map(|skin| {
let joints = skin
.joints()
.map(|joint| nodes.get(&joint.index()).unwrap().clone())
.collect();
let gltf_skin = GltfSkin::new(
&skin,
joints,
skinned_mesh_inverse_bindposes[skin.index()].clone(),
get_gltf_extras(skin.extras()),
);
let handle = load_context.add_labeled_asset(skin_label(&skin), gltf_skin);
skins.push(handle.clone());
if let Some(name) = skin.name() {
named_skins.insert(name.into(), handle.clone());
}
handle
});
let children = node
.children()
.map(|child| nodes.get(&child.index()).unwrap().clone())
.collect();
let mesh = node
.mesh()
.map(|mesh| mesh.index())
.and_then(|i| meshes.get(i).cloned());
let gltf_node = GltfNode::new(
&node,
children,
mesh,
node_transform(&node),
skin,
get_gltf_extras(node.extras()),
);
#[cfg(feature = "bevy_animation")]
let gltf_node = gltf_node.with_animation_root(animation_roots.contains(&node.index()));
let handle = load_context.add_labeled_asset(gltf_node.asset_label().to_string(), gltf_node);
nodes.insert(node.index(), handle.clone());
if let Some(name) = node.name() {
named_nodes.insert(name.into(), handle);
}
}
let mut nodes_to_sort = nodes.into_iter().collect::<Vec<_>>();
nodes_to_sort.sort_by_key(|(i, _)| *i);
let nodes = nodes_to_sort
.into_iter()
.map(|(_, resolved)| resolved)
.collect();
let mut scenes = vec![];
let mut named_scenes = HashMap::default();
let mut active_camera_found = false;
@ -700,7 +728,6 @@ async fn load_gltf<'a, 'b, 'c>(
}
}
let mut warned_about_max_joints = HashSet::new();
for (&entity, &skin_index) in &entity_to_skin_index_map {
let mut entity = world.entity_mut(entity);
let skin = gltf.skins().nth(skin_index).unwrap();
@ -709,16 +736,6 @@ async fn load_gltf<'a, 'b, 'c>(
.map(|node| node_index_to_entity_map[&node.index()])
.collect();
if joint_entities.len() > MAX_JOINTS && warned_about_max_joints.insert(skin_index) {
warn!(
"The glTF skin {:?} has {} joints, but the maximum supported is {}",
skin.name()
.map(ToString::to_string)
.unwrap_or_else(|| skin.index().to_string()),
joint_entities.len(),
MAX_JOINTS
);
}
entity.insert(SkinnedMesh {
inverse_bindposes: skinned_mesh_inverse_bindposes[skin_index].clone(),
joints: joint_entities,
@ -742,6 +759,8 @@ async fn load_gltf<'a, 'b, 'c>(
named_scenes,
meshes,
named_meshes,
skins,
named_skins,
materials,
named_materials,
nodes,
@ -1547,10 +1566,16 @@ fn scene_label(scene: &gltf::Scene) -> String {
GltfAssetLabel::Scene(scene.index()).to_string()
}
/// Return the label for the `skin`.
fn skin_label(skin: &gltf::Skin) -> String {
GltfAssetLabel::Skin(skin.index()).to_string()
}
/// Return the label for the `inverseBindMatrices` of the node.
fn inverse_bind_matrices_label(skin: &gltf::Skin) -> String {
GltfAssetLabel::InverseBindMatrices(skin.index()).to_string()
}
/// Extracts the texture sampler data from the glTF texture.
fn texture_sampler(texture: &gltf::Texture) -> ImageSamplerDescriptor {
let gltf_sampler = texture.sampler();
@ -1667,57 +1692,109 @@ async fn load_buffers(
Ok(buffer_data)
}
#[allow(clippy::result_large_err)]
fn resolve_node_hierarchy(
nodes_intermediate: Vec<(GltfNode, Vec<(usize, Handle<GltfNode>)>)>,
) -> Result<Vec<GltfNode>, GltfError> {
let mut empty_children = VecDeque::new();
let mut parents = vec![None; nodes_intermediate.len()];
let mut unprocessed_nodes = nodes_intermediate
.into_iter()
.enumerate()
.map(|(i, (node, children))| {
for (child_index, _child_handle) in &children {
let parent = parents.get_mut(*child_index).unwrap();
*parent = Some(i);
}
let children = children.into_iter().collect::<HashMap<_, _>>();
if children.is_empty() {
empty_children.push_back(i);
}
(i, (node, children))
})
.collect::<HashMap<_, _>>();
let mut nodes = std::collections::HashMap::<usize, GltfNode>::new();
while let Some(index) = empty_children.pop_front() {
let (node, children) = unprocessed_nodes.remove(&index).unwrap();
assert!(children.is_empty());
nodes.insert(index, node);
if let Some(parent_index) = parents[index] {
let (parent_node, parent_children) = unprocessed_nodes.get_mut(&parent_index).unwrap();
/// Iterator for a Gltf tree.
///
/// It resolves a Gltf tree and allows for a safe Gltf nodes iteration,
/// putting dependant nodes before dependencies.
struct GltfTreeIterator<'a> {
nodes: Vec<gltf::Node<'a>>,
}
let handle = parent_children.remove(&index).unwrap();
parent_node.children.push(handle);
if parent_children.is_empty() {
empty_children.push_back(parent_index);
impl<'a> GltfTreeIterator<'a> {
#[allow(clippy::result_large_err)]
fn try_new(gltf: &'a gltf::Gltf) -> Result<Self, GltfError> {
let nodes = gltf.nodes().collect::<Vec<_>>();
let mut empty_children = VecDeque::new();
let mut parents = vec![None; nodes.len()];
let mut unprocessed_nodes = nodes
.into_iter()
.enumerate()
.map(|(i, node)| {
let children = node
.children()
.map(|child| child.index())
.collect::<HashSet<_>>();
for &child in &children {
let parent = parents.get_mut(child).unwrap();
*parent = Some(i);
}
if children.is_empty() {
empty_children.push_back(i);
}
(i, (node, children))
})
.collect::<HashMap<_, _>>();
let mut nodes = Vec::new();
let mut warned_about_max_joints = HashSet::new();
while let Some(index) = empty_children.pop_front() {
if let Some(skin) = unprocessed_nodes.get(&index).unwrap().0.skin() {
if skin.joints().len() > MAX_JOINTS && warned_about_max_joints.insert(skin.index())
{
warn!(
"The glTF skin {:?} has {} joints, but the maximum supported is {}",
skin.name()
.map(ToString::to_string)
.unwrap_or_else(|| skin.index().to_string()),
skin.joints().len(),
MAX_JOINTS
);
}
let skin_has_dependencies = skin
.joints()
.any(|joint| unprocessed_nodes.contains_key(&joint.index()));
if skin_has_dependencies && unprocessed_nodes.len() != 1 {
empty_children.push_back(index);
continue;
}
}
let (node, children) = unprocessed_nodes.remove(&index).unwrap();
assert!(children.is_empty());
nodes.push(node);
if let Some(parent_index) = parents[index] {
let (_, parent_children) = unprocessed_nodes.get_mut(&parent_index).unwrap();
assert!(parent_children.remove(&index));
if parent_children.is_empty() {
empty_children.push_back(parent_index);
}
}
}
if !unprocessed_nodes.is_empty() {
return Err(GltfError::CircularChildren(format!(
"{:?}",
unprocessed_nodes
.iter()
.map(|(k, _v)| *k)
.collect::<Vec<_>>(),
)));
}
nodes.reverse();
Ok(Self {
nodes: nodes.into_iter().collect(),
})
}
if !unprocessed_nodes.is_empty() {
return Err(GltfError::CircularChildren(format!(
"{:?}",
unprocessed_nodes
.iter()
.map(|(k, _v)| *k)
.collect::<Vec<_>>(),
)));
}
impl<'a> Iterator for GltfTreeIterator<'a> {
type Item = gltf::Node<'a>;
fn next(&mut self) -> Option<Self::Item> {
self.nodes.pop()
}
}
impl<'a> ExactSizeIterator for GltfTreeIterator<'a> {
fn len(&self) -> usize {
self.nodes.len()
}
let mut nodes_to_sort = nodes.into_iter().collect::<Vec<_>>();
nodes_to_sort.sort_by_key(|(i, _)| *i);
Ok(nodes_to_sort
.into_iter()
.map(|(_, resolved)| resolved)
.collect())
}
enum ImageOrPath {
@ -2003,7 +2080,7 @@ fn material_needs_tangents(material: &Material) -> bool {
mod test {
use std::path::Path;
use crate::{Gltf, GltfAssetLabel, GltfNode};
use crate::{Gltf, GltfAssetLabel, GltfNode, GltfSkin};
use bevy_app::App;
use bevy_asset::{
io::{
@ -2015,6 +2092,7 @@ mod test {
use bevy_core::TaskPoolPlugin;
use bevy_ecs::world::World;
use bevy_log::LogPlugin;
use bevy_render::mesh::{skinning::SkinnedMeshInverseBindposes, MeshPlugin};
use bevy_scene::ScenePlugin;
fn test_app(dir: Dir) -> App {
@ -2029,6 +2107,7 @@ mod test {
TaskPoolPlugin::default(),
AssetPlugin::default(),
ScenePlugin,
MeshPlugin,
crate::GltfPlugin::default(),
));
@ -2063,10 +2142,10 @@ mod test {
app.update();
run_app_until(&mut app, |_world| {
let load_state = asset_server.get_load_state(handle_id).unwrap();
if load_state == LoadState::Loaded {
Some(())
} else {
None
match load_state {
LoadState::Loaded => Some(()),
LoadState::Failed(err) => panic!("{err}"),
_ => None,
}
});
app
@ -2347,4 +2426,83 @@ mod test {
let load_state = asset_server.get_load_state(handle_id).unwrap();
assert!(matches!(load_state, LoadState::Failed(_)));
}
#[test]
fn skin_node() {
let gltf_path = "test.gltf";
let app = load_gltf_into_app(
gltf_path,
r#"
{
"asset": {
"version": "2.0"
},
"nodes": [
{
"name": "skinned",
"skin": 0,
"children": [1, 2]
},
{
"name": "joint1"
},
{
"name": "joint2"
}
],
"skins": [
{
"inverseBindMatrices": 0,
"joints": [1, 2]
}
],
"buffers": [
{
"uri" : "data:application/gltf-buffer;base64,AACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAAAAAAAAAAAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAgD8AAAAAAAAAAAAAAAAAAAAAAACAPwAAAAAAAAAAAAAAAAAAAAAAAIA/AAAAAAAAAAAAAIC/AAAAAAAAgD8=",
"byteLength" : 128
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": 128
}
],
"accessors": [
{
"bufferView" : 0,
"componentType" : 5126,
"count" : 2,
"type" : "MAT4"
}
],
"scene": 0,
"scenes": [{ "nodes": [0] }]
}
"#,
);
let asset_server = app.world().resource::<AssetServer>();
let handle = asset_server.load(gltf_path);
let gltf_root_assets = app.world().resource::<Assets<Gltf>>();
let gltf_node_assets = app.world().resource::<Assets<GltfNode>>();
let gltf_skin_assets = app.world().resource::<Assets<GltfSkin>>();
let gltf_inverse_bind_matrices = app
.world()
.resource::<Assets<SkinnedMeshInverseBindposes>>();
let gltf_root = gltf_root_assets.get(&handle).unwrap();
assert_eq!(gltf_root.skins.len(), 1);
assert_eq!(gltf_root.nodes.len(), 3);
let skin = gltf_skin_assets.get(&gltf_root.skins[0]).unwrap();
assert_eq!(skin.joints.len(), 2);
assert_eq!(skin.joints[0], gltf_root.nodes[1]);
assert_eq!(skin.joints[1], gltf_root.nodes[2]);
assert!(gltf_inverse_bind_matrices.contains(&skin.inverse_bind_matrices));
let skinned_node = gltf_node_assets.get(&gltf_root.nodes[0]).unwrap();
assert_eq!(skinned_node.name, "skinned");
assert_eq!(skinned_node.children.len(), 2);
assert_eq!(skinned_node.skin.as_ref(), Some(&gltf_root.skins[0]));
}
}