mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 04:33:37 +00:00
Port bevy_gltf to pipelined-rendering (#2537)
# Objective Port bevy_gltf to the pipelined-rendering branch. ## Solution crates/bevy_gltf has been copied and pasted into pipelined/bevy_gltf2 and modifications were made to work with the pipelined-rendering branch. Notably vertex tangents and vertex colours are not supported. Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
parent
7b336fd779
commit
ae4f809a52
9 changed files with 1043 additions and 1 deletions
|
@ -25,7 +25,7 @@ default = [
|
|||
"bevy_core_pipeline",
|
||||
"bevy_dynamic_plugin",
|
||||
"bevy_gilrs",
|
||||
"bevy_gltf",
|
||||
"bevy_gltf2",
|
||||
"bevy_wgpu",
|
||||
"bevy_sprite2",
|
||||
"bevy_render2",
|
||||
|
@ -56,6 +56,7 @@ bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline"]
|
|||
bevy_render2 = ["bevy_internal/bevy_render2"]
|
||||
bevy_sprite2 = ["bevy_internal/bevy_sprite2"]
|
||||
bevy_pbr2 = ["bevy_internal/bevy_pbr2"]
|
||||
bevy_gltf2 = ["bevy_internal/bevy_gltf2"]
|
||||
|
||||
trace_chrome = ["bevy_internal/trace_chrome"]
|
||||
trace = ["bevy_internal/trace"]
|
||||
|
@ -156,6 +157,10 @@ path = "examples/3d/cornell_box_pipelined.rs"
|
|||
name = "load_gltf"
|
||||
path = "examples/3d/load_gltf.rs"
|
||||
|
||||
[[example]]
|
||||
name = "load_gltf_pipelined"
|
||||
path = "examples/3d/load_gltf_pipelined.rs"
|
||||
|
||||
[[example]]
|
||||
name = "msaa"
|
||||
path = "examples/3d/msaa.rs"
|
||||
|
|
|
@ -68,6 +68,7 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.5.0" }
|
|||
bevy_audio = { path = "../bevy_audio", optional = true, version = "0.5.0" }
|
||||
bevy_core_pipeline = { path = "../../pipelined/bevy_core_pipeline", optional = true, version = "0.5.0" }
|
||||
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.5.0" }
|
||||
bevy_gltf2 = { path = "../../pipelined/bevy_gltf2", optional = true, version = "0.5.0" }
|
||||
bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.5.0" }
|
||||
bevy_pbr2 = { path = "../../pipelined/bevy_pbr2", optional = true, version = "0.5.0" }
|
||||
bevy_render = { path = "../bevy_render", optional = true, version = "0.5.0" }
|
||||
|
|
|
@ -117,6 +117,7 @@ impl PluginGroup for PipelinedDefaultPlugins {
|
|||
group.add(bevy_input::InputPlugin::default());
|
||||
group.add(bevy_window::WindowPlugin::default());
|
||||
group.add(bevy_asset::AssetPlugin::default());
|
||||
group.add(bevy_scene::ScenePlugin::default());
|
||||
|
||||
#[cfg(feature = "bevy_render2")]
|
||||
{
|
||||
|
@ -132,6 +133,9 @@ impl PluginGroup for PipelinedDefaultPlugins {
|
|||
|
||||
#[cfg(feature = "bevy_pbr2")]
|
||||
group.add(bevy_pbr2::PbrPlugin::default());
|
||||
|
||||
#[cfg(feature = "bevy_gltf2")]
|
||||
group.add(bevy_gltf2::GltfPlugin::default());
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_winit")]
|
||||
|
|
|
@ -99,6 +99,12 @@ pub mod gltf {
|
|||
pub use bevy_gltf::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_gltf2")]
|
||||
pub mod gltf2 {
|
||||
//! Support for GLTF file loading.
|
||||
pub use bevy_gltf2::*;
|
||||
}
|
||||
|
||||
#[cfg(feature = "bevy_pbr")]
|
||||
pub mod pbr {
|
||||
//! Physically based rendering.
|
||||
|
|
62
examples/3d/load_gltf_pipelined.rs
Normal file
62
examples/3d/load_gltf_pipelined.rs
Normal file
|
@ -0,0 +1,62 @@
|
|||
use bevy::{
|
||||
core::Time,
|
||||
ecs::prelude::*,
|
||||
math::{EulerRot, Quat, Vec3},
|
||||
pbr2::{AmbientLight, DirectionalLight, DirectionalLightBundle},
|
||||
prelude::{App, AssetServer, SpawnSceneCommands, Transform},
|
||||
render2::{
|
||||
camera::{OrthographicProjection, PerspectiveCameraBundle},
|
||||
color::Color,
|
||||
},
|
||||
PipelinedDefaultPlugins,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.insert_resource(AmbientLight {
|
||||
color: Color::WHITE,
|
||||
brightness: 1.0 / 5.0f32,
|
||||
})
|
||||
.add_plugins(PipelinedDefaultPlugins)
|
||||
.add_startup_system(setup.system())
|
||||
.add_system(animate_light_direction.system())
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
commands.spawn_scene(asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0"));
|
||||
commands.spawn_bundle(PerspectiveCameraBundle {
|
||||
transform: Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
|
||||
..Default::default()
|
||||
});
|
||||
const HALF_SIZE: f32 = 1.0;
|
||||
commands.spawn_bundle(DirectionalLightBundle {
|
||||
directional_light: DirectionalLight {
|
||||
shadow_projection: OrthographicProjection {
|
||||
left: -HALF_SIZE,
|
||||
right: HALF_SIZE,
|
||||
bottom: -HALF_SIZE,
|
||||
top: HALF_SIZE,
|
||||
near: -10.0 * HALF_SIZE,
|
||||
far: 10.0 * HALF_SIZE,
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
|
||||
fn animate_light_direction(
|
||||
time: Res<Time>,
|
||||
mut query: Query<&mut Transform, With<DirectionalLight>>,
|
||||
) {
|
||||
for mut transform in query.iter_mut() {
|
||||
transform.rotation = Quat::from_euler(
|
||||
EulerRot::ZYX,
|
||||
0.0,
|
||||
time.seconds_since_startup() as f32 * std::f32::consts::TAU / 10.0,
|
||||
-std::f32::consts::FRAC_PI_4,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -99,6 +99,7 @@ Example | File | Description
|
|||
`3d_scene_pipelined` | [`3d/3d_scene_pipelined.rs`](./3d/3d_scene_pipelined.rs) | Simple 3D scene with basic shapes and lighting
|
||||
`cornell_box_pipelined` | [`3d/cornell_box_pipelined.rs`](./3d/cornell_box_pipelined.rs) | Re-production of the cornell box
|
||||
`load_gltf` | [`3d/load_gltf.rs`](./3d/load_gltf.rs) | Loads and renders a gltf file as a scene
|
||||
`load_gltf_pipelined` | [`3d/load_gltf_pipelined.rs`](./3d/load_gltf_pipelined.rs) | Loads and renders a gltf file as a scene
|
||||
`msaa` | [`3d/msaa.rs`](./3d/msaa.rs) | Configures MSAA (Multi-Sample Anti-Aliasing) for smoother edges
|
||||
`orthographic` | [`3d/orthographic.rs`](./3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look games or CAD applications)
|
||||
`parenting` | [`3d/parenting.rs`](./3d/parenting.rs) | Demonstrates parent->child relationships and relative transformations
|
||||
|
|
35
pipelined/bevy_gltf2/Cargo.toml
Normal file
35
pipelined/bevy_gltf2/Cargo.toml
Normal file
|
@ -0,0 +1,35 @@
|
|||
[package]
|
||||
name = "bevy_gltf2"
|
||||
version = "0.5.0"
|
||||
edition = "2018"
|
||||
authors = [
|
||||
"Bevy Contributors <bevyengine@gmail.com>",
|
||||
"Carter Anderson <mcanders1@gmail.com>",
|
||||
]
|
||||
description = "Bevy Engine GLTF loading"
|
||||
homepage = "https://bevyengine.org"
|
||||
repository = "https://github.com/bevyengine/bevy"
|
||||
license = "MIT OR Apache-2.0"
|
||||
keywords = ["bevy"]
|
||||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_app = { path = "../../crates/bevy_app", version = "0.5.0" }
|
||||
bevy_asset = { path = "../../crates/bevy_asset", version = "0.5.0" }
|
||||
bevy_core = { path = "../../crates/bevy_core", version = "0.5.0" }
|
||||
bevy_ecs = { path = "../../crates/bevy_ecs", version = "0.5.0" }
|
||||
bevy_pbr2 = { path = "../bevy_pbr2", version = "0.5.0" }
|
||||
bevy_reflect = { path = "../../crates/bevy_reflect", version = "0.5.0", features = ["bevy"] }
|
||||
bevy_render2 = { path = "../bevy_render2", version = "0.5.0" }
|
||||
bevy_transform = { path = "../../crates/bevy_transform", version = "0.5.0" }
|
||||
bevy_math = { path = "../../crates/bevy_math", version = "0.5.0" }
|
||||
bevy_scene = { path = "../../crates/bevy_scene", version = "0.5.0" }
|
||||
bevy_log = { path = "../../crates/bevy_log", version = "0.5.0" }
|
||||
|
||||
# other
|
||||
gltf = { version = "0.16.0", default-features = false, features = ["utils", "names", "KHR_materials_unlit"] }
|
||||
thiserror = "1.0"
|
||||
anyhow = "1.0.4"
|
||||
base64 = "0.13.0"
|
||||
percent-encoding = "2.1"
|
||||
wgpu = "0.9"
|
60
pipelined/bevy_gltf2/src/lib.rs
Normal file
60
pipelined/bevy_gltf2/src/lib.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
mod loader;
|
||||
pub use loader::*;
|
||||
|
||||
use bevy_app::prelude::*;
|
||||
use bevy_asset::{AddAsset, Handle};
|
||||
use bevy_pbr2::StandardMaterial;
|
||||
use bevy_reflect::TypeUuid;
|
||||
use bevy_render2::mesh::Mesh;
|
||||
use bevy_scene::Scene;
|
||||
|
||||
/// Adds support for GLTF file loading to Apps
|
||||
#[derive(Default)]
|
||||
pub struct GltfPlugin;
|
||||
|
||||
impl Plugin for GltfPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_asset_loader::<GltfLoader>()
|
||||
.add_asset::<Gltf>()
|
||||
.add_asset::<GltfNode>()
|
||||
.add_asset::<GltfPrimitive>()
|
||||
.add_asset::<GltfMesh>();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, TypeUuid)]
|
||||
#[uuid = "5c7d5f8a-f7b0-4e45-a09e-406c0372fea2"]
|
||||
pub struct Gltf {
|
||||
pub scenes: Vec<Handle<Scene>>,
|
||||
pub named_scenes: HashMap<String, Handle<Scene>>,
|
||||
pub meshes: Vec<Handle<GltfMesh>>,
|
||||
pub named_meshes: HashMap<String, Handle<GltfMesh>>,
|
||||
pub materials: Vec<Handle<StandardMaterial>>,
|
||||
pub named_materials: HashMap<String, Handle<StandardMaterial>>,
|
||||
pub nodes: Vec<Handle<GltfNode>>,
|
||||
pub named_nodes: HashMap<String, Handle<GltfNode>>,
|
||||
pub default_scene: Option<Handle<Scene>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypeUuid)]
|
||||
#[uuid = "dad74750-1fd6-460f-ac51-0a7937563865"]
|
||||
pub struct GltfNode {
|
||||
pub children: Vec<GltfNode>,
|
||||
pub mesh: Option<Handle<GltfMesh>>,
|
||||
pub transform: bevy_transform::prelude::Transform,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypeUuid)]
|
||||
#[uuid = "8ceaec9a-926a-4f29-8ee3-578a69f42315"]
|
||||
pub struct GltfMesh {
|
||||
pub primitives: Vec<GltfPrimitive>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, TypeUuid)]
|
||||
#[uuid = "cbfca302-82fd-41cb-af77-cab6b3d50af1"]
|
||||
pub struct GltfPrimitive {
|
||||
pub mesh: Handle<Mesh>,
|
||||
pub material: Option<Handle<StandardMaterial>>,
|
||||
}
|
868
pipelined/bevy_gltf2/src/loader.rs
Normal file
868
pipelined/bevy_gltf2/src/loader.rs
Normal file
|
@ -0,0 +1,868 @@
|
|||
use anyhow::Result;
|
||||
use bevy_asset::{
|
||||
AssetIoError, AssetLoader, AssetPath, BoxedFuture, Handle, LoadContext, LoadedAsset,
|
||||
};
|
||||
use bevy_core::Name;
|
||||
use bevy_ecs::world::World;
|
||||
use bevy_log::warn;
|
||||
use bevy_math::Mat4;
|
||||
use bevy_pbr2::{PbrBundle, StandardMaterial};
|
||||
use bevy_render2::{
|
||||
camera::{
|
||||
Camera, CameraPlugin, CameraProjection, OrthographicProjection, PerspectiveProjection,
|
||||
},
|
||||
color::Color,
|
||||
mesh::{Indices, Mesh, VertexAttributeValues},
|
||||
texture::{Image, ImageType, TextureError},
|
||||
};
|
||||
use bevy_scene::Scene;
|
||||
use bevy_transform::{
|
||||
hierarchy::{BuildWorldChildren, WorldChildBuilder},
|
||||
prelude::{GlobalTransform, Transform},
|
||||
};
|
||||
use gltf::{
|
||||
mesh::Mode,
|
||||
texture::{MagFilter, MinFilter, WrappingMode},
|
||||
Material, Primitive,
|
||||
};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::Path,
|
||||
};
|
||||
use thiserror::Error;
|
||||
use wgpu::{AddressMode, FilterMode, PrimitiveTopology, SamplerDescriptor, TextureFormat};
|
||||
|
||||
use crate::{Gltf, GltfNode};
|
||||
|
||||
/// An error that occurs when loading a GLTF file
|
||||
#[derive(Error, Debug)]
|
||||
pub enum GltfError {
|
||||
#[error("unsupported primitive mode")]
|
||||
UnsupportedPrimitive { mode: Mode },
|
||||
#[error("invalid GLTF file: {0}")]
|
||||
Gltf(#[from] gltf::Error),
|
||||
#[error("binary blob is missing")]
|
||||
MissingBlob,
|
||||
#[error("failed to decode base64 mesh data")]
|
||||
Base64Decode(#[from] base64::DecodeError),
|
||||
#[error("unsupported buffer format")]
|
||||
BufferFormatUnsupported,
|
||||
#[error("invalid image mime type: {0}")]
|
||||
InvalidImageMimeType(String),
|
||||
#[error("You may need to add the feature for the file format: {0}")]
|
||||
ImageError(#[from] TextureError),
|
||||
#[error("failed to load an asset path: {0}")]
|
||||
AssetIoError(#[from] AssetIoError),
|
||||
}
|
||||
|
||||
/// Loads meshes from GLTF files into Mesh assets
|
||||
#[derive(Default)]
|
||||
pub struct GltfLoader;
|
||||
|
||||
impl AssetLoader for GltfLoader {
|
||||
fn load<'a>(
|
||||
&'a self,
|
||||
bytes: &'a [u8],
|
||||
load_context: &'a mut LoadContext,
|
||||
) -> BoxedFuture<'a, Result<()>> {
|
||||
Box::pin(async move { Ok(load_gltf(bytes, load_context).await?) })
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &[&str] {
|
||||
&["gltf", "glb"]
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_gltf<'a, 'b>(
|
||||
bytes: &'a [u8],
|
||||
load_context: &'a mut LoadContext<'b>,
|
||||
) -> Result<(), GltfError> {
|
||||
let gltf = gltf::Gltf::from_slice(bytes)?;
|
||||
let buffer_data = load_buffers(&gltf, load_context, load_context.path()).await?;
|
||||
|
||||
let mut materials = vec![];
|
||||
let mut named_materials = HashMap::new();
|
||||
let mut linear_textures = HashSet::new();
|
||||
for material in gltf.materials() {
|
||||
let handle = load_material(&material, load_context);
|
||||
if let Some(name) = material.name() {
|
||||
named_materials.insert(name.to_string(), handle.clone());
|
||||
}
|
||||
materials.push(handle);
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
let mut meshes = vec![];
|
||||
let mut named_meshes = HashMap::new();
|
||||
for mesh in gltf.meshes() {
|
||||
let mut primitives = vec![];
|
||||
for primitive in mesh.primitives() {
|
||||
let primitive_label = primitive_label(&mesh, &primitive);
|
||||
let reader = primitive.reader(|buffer| Some(&buffer_data[buffer.index()]));
|
||||
let primitive_topology = get_primitive_topology(primitive.mode())?;
|
||||
|
||||
let mut mesh = Mesh::new(primitive_topology);
|
||||
|
||||
if let Some(vertex_attribute) = reader
|
||||
.read_positions()
|
||||
.map(|v| VertexAttributeValues::Float32x3(v.collect()))
|
||||
{
|
||||
mesh.set_attribute(Mesh::ATTRIBUTE_POSITION, vertex_attribute);
|
||||
}
|
||||
|
||||
if let Some(vertex_attribute) = reader
|
||||
.read_normals()
|
||||
.map(|v| VertexAttributeValues::Float32x3(v.collect()))
|
||||
{
|
||||
mesh.set_attribute(Mesh::ATTRIBUTE_NORMAL, vertex_attribute);
|
||||
}
|
||||
|
||||
// if let Some(vertex_attribute) = reader
|
||||
// .read_tangents()
|
||||
// .map(|v| VertexAttributeValues::Float32x4(v.collect()))
|
||||
// {
|
||||
// mesh.set_attribute(Mesh::ATTRIBUTE_TANGENT, vertex_attribute);
|
||||
// }
|
||||
|
||||
if let Some(vertex_attribute) = reader
|
||||
.read_tex_coords(0)
|
||||
.map(|v| VertexAttributeValues::Float32x2(v.into_f32().collect()))
|
||||
{
|
||||
mesh.set_attribute(Mesh::ATTRIBUTE_UV_0, vertex_attribute);
|
||||
} else {
|
||||
let len = mesh.count_vertices();
|
||||
let uvs = vec![[0.0, 0.0]; len];
|
||||
bevy_log::debug!("missing `TEXCOORD_0` vertex attribute, loading zeroed out UVs");
|
||||
mesh.set_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
|
||||
}
|
||||
|
||||
// if let Some(vertex_attribute) = reader
|
||||
// .read_colors(0)
|
||||
// .map(|v| VertexAttributeValues::Float32x4(v.into_rgba_f32().collect()))
|
||||
// {
|
||||
// mesh.set_attribute(Mesh::ATTRIBUTE_COLOR, vertex_attribute);
|
||||
// }
|
||||
|
||||
if let Some(indices) = reader.read_indices() {
|
||||
mesh.set_indices(Some(Indices::U32(indices.into_u32().collect())));
|
||||
};
|
||||
|
||||
if mesh.attribute(Mesh::ATTRIBUTE_NORMAL).is_none() {
|
||||
let vertex_count_before = mesh.count_vertices();
|
||||
mesh.duplicate_vertices();
|
||||
mesh.compute_flat_normals();
|
||||
let vertex_count_after = mesh.count_vertices();
|
||||
|
||||
if vertex_count_before != vertex_count_after {
|
||||
bevy_log::debug!("Missing vertex normals in indexed geometry, computing them as flat. Vertex count increased from {} to {}", vertex_count_before, vertex_count_after);
|
||||
} else {
|
||||
bevy_log::debug!(
|
||||
"Missing vertex normals in indexed geometry, computing them as flat."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mesh = load_context.set_labeled_asset(&primitive_label, LoadedAsset::new(mesh));
|
||||
primitives.push(super::GltfPrimitive {
|
||||
mesh,
|
||||
material: primitive
|
||||
.material()
|
||||
.index()
|
||||
.and_then(|i| materials.get(i).cloned()),
|
||||
});
|
||||
}
|
||||
let handle = load_context.set_labeled_asset(
|
||||
&mesh_label(&mesh),
|
||||
LoadedAsset::new(super::GltfMesh { primitives }),
|
||||
);
|
||||
if let Some(name) = mesh.name() {
|
||||
named_meshes.insert(name.to_string(), handle.clone());
|
||||
}
|
||||
meshes.push(handle);
|
||||
}
|
||||
|
||||
let mut nodes_intermediate = vec![];
|
||||
let mut named_nodes_intermediate = HashMap::new();
|
||||
for node in gltf.nodes() {
|
||||
let node_label = node_label(&node);
|
||||
nodes_intermediate.push((
|
||||
node_label,
|
||||
GltfNode {
|
||||
children: vec![],
|
||||
mesh: node
|
||||
.mesh()
|
||||
.map(|mesh| mesh.index())
|
||||
.and_then(|i| meshes.get(i).cloned()),
|
||||
transform: match node.transform() {
|
||||
gltf::scene::Transform::Matrix { matrix } => {
|
||||
Transform::from_matrix(bevy_math::Mat4::from_cols_array_2d(&matrix))
|
||||
}
|
||||
gltf::scene::Transform::Decomposed {
|
||||
translation,
|
||||
rotation,
|
||||
scale,
|
||||
} => Transform {
|
||||
translation: bevy_math::Vec3::from(translation),
|
||||
rotation: bevy_math::Quat::from_vec4(rotation.into()),
|
||||
scale: bevy_math::Vec3::from(scale),
|
||||
},
|
||||
},
|
||||
},
|
||||
node.children()
|
||||
.map(|child| 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(|(label, node)| load_context.set_labeled_asset(&label, LoadedAsset::new(node)))
|
||||
.collect::<Vec<bevy_asset::Handle<GltfNode>>>();
|
||||
let named_nodes = named_nodes_intermediate
|
||||
.into_iter()
|
||||
.filter_map(|(name, index)| {
|
||||
nodes
|
||||
.get(index)
|
||||
.map(|handle| (name.to_string(), handle.clone()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// TODO: use the threaded impl on wasm once wasm thread pool doesn't deadlock on it
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
for gltf_texture in gltf.textures() {
|
||||
let (texture, label) =
|
||||
load_texture(gltf_texture, &buffer_data, &linear_textures, &load_context).await?;
|
||||
load_context.set_labeled_asset(&label, LoadedAsset::new(texture));
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
load_context
|
||||
.task_pool()
|
||||
.scope(|scope| {
|
||||
gltf.textures().for_each(|gltf_texture| {
|
||||
let linear_textures = &linear_textures;
|
||||
let load_context: &LoadContext = load_context;
|
||||
let buffer_data = &buffer_data;
|
||||
scope.spawn(async move {
|
||||
load_texture(gltf_texture, buffer_data, linear_textures, load_context).await
|
||||
});
|
||||
});
|
||||
})
|
||||
.into_iter()
|
||||
.filter_map(|res| {
|
||||
if let Err(err) = res.as_ref() {
|
||||
warn!("Error loading GLTF texture: {}", err);
|
||||
}
|
||||
res.ok()
|
||||
})
|
||||
.for_each(|(texture, label)| {
|
||||
load_context.set_labeled_asset(&label, LoadedAsset::new(texture));
|
||||
});
|
||||
|
||||
let mut scenes = vec![];
|
||||
let mut named_scenes = HashMap::new();
|
||||
for scene in gltf.scenes() {
|
||||
let mut err = None;
|
||||
let mut world = World::default();
|
||||
world
|
||||
.spawn()
|
||||
.insert_bundle((Transform::identity(), GlobalTransform::identity()))
|
||||
.with_children(|parent| {
|
||||
for node in scene.nodes() {
|
||||
let result = load_node(&node, parent, load_context, &buffer_data);
|
||||
if result.is_err() {
|
||||
err = Some(result);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some(Err(err)) = err {
|
||||
return Err(err);
|
||||
}
|
||||
let scene_handle = load_context
|
||||
.set_labeled_asset(&scene_label(&scene), LoadedAsset::new(Scene::new(world)));
|
||||
|
||||
if let Some(name) = scene.name() {
|
||||
named_scenes.insert(name.to_string(), scene_handle.clone());
|
||||
}
|
||||
scenes.push(scene_handle);
|
||||
}
|
||||
|
||||
load_context.set_default_asset(LoadedAsset::new(Gltf {
|
||||
default_scene: gltf
|
||||
.default_scene()
|
||||
.and_then(|scene| scenes.get(scene.index()))
|
||||
.cloned(),
|
||||
scenes,
|
||||
named_scenes,
|
||||
meshes,
|
||||
named_meshes,
|
||||
materials,
|
||||
named_materials,
|
||||
nodes,
|
||||
named_nodes,
|
||||
}));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_texture<'a>(
|
||||
gltf_texture: gltf::Texture<'a>,
|
||||
buffer_data: &[Vec<u8>],
|
||||
linear_textures: &HashSet<usize>,
|
||||
load_context: &LoadContext<'a>,
|
||||
) -> Result<(Image, String), GltfError> {
|
||||
let mut texture = match gltf_texture.source().source() {
|
||||
gltf::image::Source::View { view, mime_type } => {
|
||||
let start = view.offset() as usize;
|
||||
let end = (view.offset() + view.length()) as usize;
|
||||
let buffer = &buffer_data[view.buffer().index()][start..end];
|
||||
Image::from_buffer(buffer, ImageType::MimeType(mime_type))?
|
||||
}
|
||||
gltf::image::Source::Uri { uri, mime_type } => {
|
||||
let uri = percent_encoding::percent_decode_str(uri)
|
||||
.decode_utf8()
|
||||
.unwrap();
|
||||
let uri = uri.as_ref();
|
||||
let (bytes, image_type) = match DataUri::parse(uri) {
|
||||
Ok(data_uri) => (data_uri.decode()?, ImageType::MimeType(data_uri.mime_type)),
|
||||
Err(()) => {
|
||||
let parent = load_context.path().parent().unwrap();
|
||||
let image_path = parent.join(uri);
|
||||
let bytes = load_context.read_asset_bytes(image_path.clone()).await?;
|
||||
|
||||
let extension = Path::new(uri).extension().unwrap().to_str().unwrap();
|
||||
let image_type = ImageType::Extension(extension);
|
||||
|
||||
(bytes, image_type)
|
||||
}
|
||||
};
|
||||
|
||||
Image::from_buffer(
|
||||
&bytes,
|
||||
mime_type
|
||||
.map(|mt| ImageType::MimeType(mt))
|
||||
.unwrap_or(image_type),
|
||||
)?
|
||||
}
|
||||
};
|
||||
texture.sampler_descriptor = texture_sampler(&gltf_texture);
|
||||
if (linear_textures).contains(&gltf_texture.index()) {
|
||||
texture.texture_descriptor.format = TextureFormat::Rgba8Unorm;
|
||||
}
|
||||
|
||||
Ok((texture, texture_label(&gltf_texture)))
|
||||
}
|
||||
|
||||
fn load_material(material: &Material, load_context: &mut LoadContext) -> Handle<StandardMaterial> {
|
||||
let material_label = material_label(material);
|
||||
|
||||
let pbr = material.pbr_metallic_roughness();
|
||||
|
||||
let color = pbr.base_color_factor();
|
||||
let base_color_texture = if let Some(info) = pbr.base_color_texture() {
|
||||
// TODO: handle info.tex_coord() (the *set* index for the right texcoords)
|
||||
let label = texture_label(&info.texture());
|
||||
let path = AssetPath::new_ref(load_context.path(), Some(&label));
|
||||
Some(load_context.get_handle(path))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// let normal_map: Option<Handle<Texture>> = if let Some(normal_texture) = material.normal_texture() {
|
||||
// // TODO: handle normal_texture.scale
|
||||
// // TODO: handle normal_texture.tex_coord() (the *set* index for the right texcoords)
|
||||
// let label = texture_label(&normal_texture.texture());
|
||||
// let path = AssetPath::new_ref(load_context.path(), Some(&label));
|
||||
// Some(load_context.get_handle(path))
|
||||
// } else {
|
||||
// None
|
||||
// };
|
||||
|
||||
let metallic_roughness_texture = if let Some(info) = pbr.metallic_roughness_texture() {
|
||||
// TODO: handle info.tex_coord() (the *set* index for the right texcoords)
|
||||
let label = texture_label(&info.texture());
|
||||
let path = AssetPath::new_ref(load_context.path(), Some(&label));
|
||||
Some(load_context.get_handle(path))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let occlusion_texture = if let Some(occlusion_texture) = material.occlusion_texture() {
|
||||
// TODO: handle occlusion_texture.tex_coord() (the *set* index for the right texcoords)
|
||||
// TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength)
|
||||
let label = texture_label(&occlusion_texture.texture());
|
||||
let path = AssetPath::new_ref(load_context.path(), Some(&label));
|
||||
Some(load_context.get_handle(path))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let emissive = material.emissive_factor();
|
||||
let emissive_texture = if let Some(info) = material.emissive_texture() {
|
||||
// TODO: handle occlusion_texture.tex_coord() (the *set* index for the right texcoords)
|
||||
// TODO: handle occlusion_texture.strength() (a scalar multiplier for occlusion strength)
|
||||
let label = texture_label(&info.texture());
|
||||
let path = AssetPath::new_ref(load_context.path(), Some(&label));
|
||||
Some(load_context.get_handle(path))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
load_context.set_labeled_asset(
|
||||
&material_label,
|
||||
LoadedAsset::new(StandardMaterial {
|
||||
base_color: Color::rgba(color[0], color[1], color[2], color[3]),
|
||||
base_color_texture,
|
||||
perceptual_roughness: pbr.roughness_factor(),
|
||||
metallic: pbr.metallic_factor(),
|
||||
metallic_roughness_texture,
|
||||
// normal_map,
|
||||
double_sided: material.double_sided(),
|
||||
occlusion_texture,
|
||||
emissive: Color::rgba(emissive[0], emissive[1], emissive[2], 1.0),
|
||||
emissive_texture,
|
||||
unlit: material.unlit(),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn load_node(
|
||||
gltf_node: &gltf::Node,
|
||||
world_builder: &mut WorldChildBuilder,
|
||||
load_context: &mut LoadContext,
|
||||
buffer_data: &[Vec<u8>],
|
||||
) -> Result<(), GltfError> {
|
||||
let transform = gltf_node.transform();
|
||||
let mut gltf_error = None;
|
||||
let mut node = world_builder.spawn_bundle((
|
||||
Transform::from_matrix(Mat4::from_cols_array_2d(&transform.matrix())),
|
||||
GlobalTransform::identity(),
|
||||
));
|
||||
|
||||
if let Some(name) = gltf_node.name() {
|
||||
node.insert(Name::new(name.to_string()));
|
||||
}
|
||||
|
||||
// create camera node
|
||||
if let Some(camera) = gltf_node.camera() {
|
||||
// node.insert(VisibleEntities {
|
||||
// ..Default::default()
|
||||
// });
|
||||
|
||||
match camera.projection() {
|
||||
gltf::camera::Projection::Orthographic(orthographic) => {
|
||||
let xmag = orthographic.xmag();
|
||||
let ymag = orthographic.ymag();
|
||||
let orthographic_projection: OrthographicProjection = OrthographicProjection {
|
||||
left: -xmag,
|
||||
right: xmag,
|
||||
top: ymag,
|
||||
bottom: -ymag,
|
||||
far: orthographic.zfar(),
|
||||
near: orthographic.znear(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
node.insert(Camera {
|
||||
name: Some(CameraPlugin::CAMERA_2D.to_owned()),
|
||||
projection_matrix: orthographic_projection.get_projection_matrix(),
|
||||
..Default::default()
|
||||
});
|
||||
node.insert(orthographic_projection);
|
||||
}
|
||||
gltf::camera::Projection::Perspective(perspective) => {
|
||||
let mut perspective_projection: PerspectiveProjection = PerspectiveProjection {
|
||||
fov: perspective.yfov(),
|
||||
near: perspective.znear(),
|
||||
..Default::default()
|
||||
};
|
||||
if let Some(zfar) = perspective.zfar() {
|
||||
perspective_projection.far = zfar;
|
||||
}
|
||||
if let Some(aspect_ratio) = perspective.aspect_ratio() {
|
||||
perspective_projection.aspect_ratio = aspect_ratio;
|
||||
}
|
||||
node.insert(Camera {
|
||||
name: Some(CameraPlugin::CAMERA_3D.to_owned()),
|
||||
projection_matrix: perspective_projection.get_projection_matrix(),
|
||||
..Default::default()
|
||||
});
|
||||
node.insert(perspective_projection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
node.with_children(|parent| {
|
||||
if let Some(mesh) = gltf_node.mesh() {
|
||||
// append primitives
|
||||
for primitive in mesh.primitives() {
|
||||
let material = primitive.material();
|
||||
let material_label = material_label(&material);
|
||||
|
||||
// This will make sure we load the default material now since it would not have been
|
||||
// added when iterating over all the gltf materials (since the default material is
|
||||
// not explicitly listed in the gltf).
|
||||
if !load_context.has_labeled_asset(&material_label) {
|
||||
load_material(&material, load_context);
|
||||
}
|
||||
|
||||
let primitive_label = primitive_label(&mesh, &primitive);
|
||||
let mesh_asset_path =
|
||||
AssetPath::new_ref(load_context.path(), Some(&primitive_label));
|
||||
let material_asset_path =
|
||||
AssetPath::new_ref(load_context.path(), Some(&material_label));
|
||||
|
||||
parent.spawn_bundle(PbrBundle {
|
||||
mesh: load_context.get_handle(mesh_asset_path),
|
||||
material: load_context.get_handle(material_asset_path),
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// append other nodes
|
||||
for child in gltf_node.children() {
|
||||
if let Err(err) = load_node(&child, parent, load_context, buffer_data) {
|
||||
gltf_error = Some(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
if let Some(err) = gltf_error {
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn mesh_label(mesh: &gltf::Mesh) -> String {
|
||||
format!("Mesh{}", mesh.index())
|
||||
}
|
||||
|
||||
fn primitive_label(mesh: &gltf::Mesh, primitive: &Primitive) -> String {
|
||||
format!("Mesh{}/Primitive{}", mesh.index(), primitive.index())
|
||||
}
|
||||
|
||||
fn material_label(material: &gltf::Material) -> String {
|
||||
if let Some(index) = material.index() {
|
||||
format!("Material{}", index)
|
||||
} else {
|
||||
"MaterialDefault".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn texture_label(texture: &gltf::Texture) -> String {
|
||||
format!("Texture{}", texture.index())
|
||||
}
|
||||
|
||||
fn node_label(node: &gltf::Node) -> String {
|
||||
format!("Node{}", node.index())
|
||||
}
|
||||
|
||||
fn scene_label(scene: &gltf::Scene) -> String {
|
||||
format!("Scene{}", scene.index())
|
||||
}
|
||||
|
||||
fn texture_sampler<'a>(texture: &gltf::Texture) -> SamplerDescriptor<'a> {
|
||||
let gltf_sampler = texture.sampler();
|
||||
|
||||
SamplerDescriptor {
|
||||
address_mode_u: texture_address_mode(&gltf_sampler.wrap_s()),
|
||||
address_mode_v: texture_address_mode(&gltf_sampler.wrap_t()),
|
||||
|
||||
mag_filter: gltf_sampler
|
||||
.mag_filter()
|
||||
.map(|mf| match mf {
|
||||
MagFilter::Nearest => FilterMode::Nearest,
|
||||
MagFilter::Linear => FilterMode::Linear,
|
||||
})
|
||||
.unwrap_or(SamplerDescriptor::default().mag_filter),
|
||||
|
||||
min_filter: gltf_sampler
|
||||
.min_filter()
|
||||
.map(|mf| match mf {
|
||||
MinFilter::Nearest
|
||||
| MinFilter::NearestMipmapNearest
|
||||
| MinFilter::NearestMipmapLinear => FilterMode::Nearest,
|
||||
MinFilter::Linear
|
||||
| MinFilter::LinearMipmapNearest
|
||||
| MinFilter::LinearMipmapLinear => FilterMode::Linear,
|
||||
})
|
||||
.unwrap_or(SamplerDescriptor::default().min_filter),
|
||||
|
||||
mipmap_filter: gltf_sampler
|
||||
.min_filter()
|
||||
.map(|mf| match mf {
|
||||
MinFilter::Nearest
|
||||
| MinFilter::Linear
|
||||
| MinFilter::NearestMipmapNearest
|
||||
| MinFilter::LinearMipmapNearest => FilterMode::Nearest,
|
||||
MinFilter::NearestMipmapLinear | MinFilter::LinearMipmapLinear => {
|
||||
FilterMode::Linear
|
||||
}
|
||||
})
|
||||
.unwrap_or(SamplerDescriptor::default().mipmap_filter),
|
||||
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn texture_address_mode(gltf_address_mode: &gltf::texture::WrappingMode) -> AddressMode {
|
||||
match gltf_address_mode {
|
||||
WrappingMode::ClampToEdge => AddressMode::ClampToEdge,
|
||||
WrappingMode::Repeat => AddressMode::Repeat,
|
||||
WrappingMode::MirroredRepeat => AddressMode::MirrorRepeat,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_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 }),
|
||||
}
|
||||
}
|
||||
|
||||
async fn load_buffers(
|
||||
gltf: &gltf::Gltf,
|
||||
load_context: &LoadContext<'_>,
|
||||
asset_path: &Path,
|
||||
) -> Result<Vec<Vec<u8>>, GltfError> {
|
||||
const OCTET_STREAM_URI: &str = "application/octet-stream";
|
||||
|
||||
let mut buffer_data = Vec::new();
|
||||
for buffer in gltf.buffers() {
|
||||
match buffer.source() {
|
||||
gltf::buffer::Source::Uri(uri) => {
|
||||
let uri = percent_encoding::percent_decode_str(uri)
|
||||
.decode_utf8()
|
||||
.unwrap();
|
||||
let uri = uri.as_ref();
|
||||
let buffer_bytes = match DataUri::parse(uri) {
|
||||
Ok(data_uri) if data_uri.mime_type == OCTET_STREAM_URI => data_uri.decode()?,
|
||||
Ok(_) => return Err(GltfError::BufferFormatUnsupported),
|
||||
Err(()) => {
|
||||
// TODO: Remove this and add dep
|
||||
let buffer_path = asset_path.parent().unwrap().join(uri);
|
||||
let buffer_bytes = load_context.read_asset_bytes(buffer_path).await?;
|
||||
buffer_bytes
|
||||
}
|
||||
};
|
||||
buffer_data.push(buffer_bytes);
|
||||
}
|
||||
gltf::buffer::Source::Bin => {
|
||||
if let Some(blob) = gltf.blob.as_deref() {
|
||||
buffer_data.push(blob.into());
|
||||
} else {
|
||||
return Err(GltfError::MissingBlob);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(buffer_data)
|
||||
}
|
||||
|
||||
fn resolve_node_hierarchy(
|
||||
nodes_intermediate: Vec<(String, GltfNode, Vec<usize>)>,
|
||||
) -> Vec<(String, GltfNode)> {
|
||||
let mut max_steps = nodes_intermediate.len();
|
||||
let mut nodes_step = nodes_intermediate
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (label, node, children))| (i, label, node, children))
|
||||
.collect::<Vec<_>>();
|
||||
let mut nodes = std::collections::HashMap::<usize, (String, GltfNode)>::new();
|
||||
while max_steps > 0 && !nodes_step.is_empty() {
|
||||
if let Some((index, label, node, _)) = nodes_step
|
||||
.iter()
|
||||
.find(|(_, _, _, children)| children.is_empty())
|
||||
.cloned()
|
||||
{
|
||||
nodes.insert(index, (label, node));
|
||||
for (_, _, node, children) in nodes_step.iter_mut() {
|
||||
if let Some((i, _)) = children
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find(|(_, child_index)| **child_index == index)
|
||||
{
|
||||
children.remove(i);
|
||||
|
||||
if let Some((_, child_node)) = nodes.get(&index) {
|
||||
node.children.push(child_node.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
nodes_step = nodes_step
|
||||
.into_iter()
|
||||
.filter(|(i, _, _, _)| *i != index)
|
||||
.collect()
|
||||
}
|
||||
max_steps -= 1;
|
||||
}
|
||||
|
||||
let mut nodes_to_sort = nodes.into_iter().collect::<Vec<_>>();
|
||||
nodes_to_sort.sort_by_key(|(i, _)| *i);
|
||||
nodes_to_sort
|
||||
.into_iter()
|
||||
.map(|(_, resolved)| resolved)
|
||||
.collect()
|
||||
}
|
||||
|
||||
struct DataUri<'a> {
|
||||
mime_type: &'a str,
|
||||
base64: bool,
|
||||
data: &'a str,
|
||||
}
|
||||
|
||||
fn split_once(input: &str, delimiter: char) -> Option<(&str, &str)> {
|
||||
let mut iter = input.splitn(2, delimiter);
|
||||
Some((iter.next()?, iter.next()?))
|
||||
}
|
||||
|
||||
impl<'a> DataUri<'a> {
|
||||
fn parse(uri: &'a str) -> Result<DataUri<'a>, ()> {
|
||||
let uri = uri.strip_prefix("data:").ok_or(())?;
|
||||
let (mime_type, data) = split_once(uri, ',').ok_or(())?;
|
||||
|
||||
let (mime_type, base64) = match mime_type.strip_suffix(";base64") {
|
||||
Some(mime_type) => (mime_type, true),
|
||||
None => (mime_type, false),
|
||||
};
|
||||
|
||||
Ok(DataUri {
|
||||
mime_type,
|
||||
base64,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
fn decode(&self) -> Result<Vec<u8>, base64::DecodeError> {
|
||||
if self.base64 {
|
||||
base64::decode(self.data)
|
||||
} else {
|
||||
Ok(self.data.as_bytes().to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::resolve_node_hierarchy;
|
||||
use crate::GltfNode;
|
||||
|
||||
impl GltfNode {
|
||||
fn empty() -> Self {
|
||||
GltfNode {
|
||||
children: vec![],
|
||||
mesh: None,
|
||||
transform: bevy_transform::prelude::Transform::identity(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn node_hierarchy_single_node() {
|
||||
let result = resolve_node_hierarchy(vec![("l1".to_string(), GltfNode::empty(), vec![])]);
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].0, "l1");
|
||||
assert_eq!(result[0].1.children.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_hierarchy_no_hierarchy() {
|
||||
let result = resolve_node_hierarchy(vec![
|
||||
("l1".to_string(), GltfNode::empty(), vec![]),
|
||||
("l2".to_string(), GltfNode::empty(), vec![]),
|
||||
]);
|
||||
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result[0].0, "l1");
|
||||
assert_eq!(result[0].1.children.len(), 0);
|
||||
assert_eq!(result[1].0, "l2");
|
||||
assert_eq!(result[1].1.children.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_hierarchy_simple_hierarchy() {
|
||||
let result = resolve_node_hierarchy(vec![
|
||||
("l1".to_string(), GltfNode::empty(), vec![1]),
|
||||
("l2".to_string(), GltfNode::empty(), vec![]),
|
||||
]);
|
||||
|
||||
assert_eq!(result.len(), 2);
|
||||
assert_eq!(result[0].0, "l1");
|
||||
assert_eq!(result[0].1.children.len(), 1);
|
||||
assert_eq!(result[1].0, "l2");
|
||||
assert_eq!(result[1].1.children.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_hierarchy_hierarchy() {
|
||||
let result = resolve_node_hierarchy(vec![
|
||||
("l1".to_string(), GltfNode::empty(), vec![1]),
|
||||
("l2".to_string(), GltfNode::empty(), vec![2]),
|
||||
("l3".to_string(), GltfNode::empty(), vec![3, 4, 5]),
|
||||
("l4".to_string(), GltfNode::empty(), vec![6]),
|
||||
("l5".to_string(), GltfNode::empty(), vec![]),
|
||||
("l6".to_string(), GltfNode::empty(), vec![]),
|
||||
("l7".to_string(), GltfNode::empty(), vec![]),
|
||||
]);
|
||||
|
||||
assert_eq!(result.len(), 7);
|
||||
assert_eq!(result[0].0, "l1");
|
||||
assert_eq!(result[0].1.children.len(), 1);
|
||||
assert_eq!(result[1].0, "l2");
|
||||
assert_eq!(result[1].1.children.len(), 1);
|
||||
assert_eq!(result[2].0, "l3");
|
||||
assert_eq!(result[2].1.children.len(), 3);
|
||||
assert_eq!(result[3].0, "l4");
|
||||
assert_eq!(result[3].1.children.len(), 1);
|
||||
assert_eq!(result[4].0, "l5");
|
||||
assert_eq!(result[4].1.children.len(), 0);
|
||||
assert_eq!(result[5].0, "l6");
|
||||
assert_eq!(result[5].1.children.len(), 0);
|
||||
assert_eq!(result[6].0, "l7");
|
||||
assert_eq!(result[6].1.children.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_hierarchy_cyclic() {
|
||||
let result = resolve_node_hierarchy(vec![
|
||||
("l1".to_string(), GltfNode::empty(), vec![1]),
|
||||
("l2".to_string(), GltfNode::empty(), vec![0]),
|
||||
]);
|
||||
|
||||
assert_eq!(result.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn node_hierarchy_missing_node() {
|
||||
let result = resolve_node_hierarchy(vec![
|
||||
("l1".to_string(), GltfNode::empty(), vec![2]),
|
||||
("l2".to_string(), GltfNode::empty(), vec![]),
|
||||
]);
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
assert_eq!(result[0].0, "l2");
|
||||
assert_eq!(result[0].1.children.len(), 0);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue