Add bevy_fbx, an fbx loader based on ufbx
This commit is contained in:
parent
f1eace62f0
commit
4dc363c10b
14
Cargo.toml
14
Cargo.toml
@ -138,6 +138,7 @@ default = [
|
|||||||
"bevy_gilrs",
|
"bevy_gilrs",
|
||||||
"bevy_gizmos",
|
"bevy_gizmos",
|
||||||
"bevy_gltf",
|
"bevy_gltf",
|
||||||
|
"bevy_fbx",
|
||||||
"bevy_input_focus",
|
"bevy_input_focus",
|
||||||
"bevy_log",
|
"bevy_log",
|
||||||
"bevy_mesh_picking_backend",
|
"bevy_mesh_picking_backend",
|
||||||
@ -230,6 +231,8 @@ bevy_gilrs = ["bevy_internal/bevy_gilrs"]
|
|||||||
|
|
||||||
# [glTF](https://www.khronos.org/gltf/) support
|
# [glTF](https://www.khronos.org/gltf/) support
|
||||||
bevy_gltf = ["bevy_internal/bevy_gltf", "bevy_asset", "bevy_scene", "bevy_pbr"]
|
bevy_gltf = ["bevy_internal/bevy_gltf", "bevy_asset", "bevy_scene", "bevy_pbr"]
|
||||||
|
# [FBX](https://www.autodesk.com/products/fbx)
|
||||||
|
bevy_fbx = ["bevy_internal/bevy_fbx", "bevy_asset", "bevy_scene", "bevy_pbr", "bevy_animation"]
|
||||||
|
|
||||||
# Adds PBR rendering
|
# Adds PBR rendering
|
||||||
bevy_pbr = [
|
bevy_pbr = [
|
||||||
@ -1196,6 +1199,17 @@ description = "Loads and renders a glTF file as a scene, including the gltf extr
|
|||||||
category = "3D Rendering"
|
category = "3D Rendering"
|
||||||
wasm = true
|
wasm = true
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "load_fbx"
|
||||||
|
path = "examples/3d/load_fbx.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[package.metadata.example.load_fbx]
|
||||||
|
name = "Load FBX"
|
||||||
|
description = "Loads and renders an FBX file as a scene"
|
||||||
|
category = "3D Rendering"
|
||||||
|
wasm = false
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "query_gltf_primitives"
|
name = "query_gltf_primitives"
|
||||||
path = "examples/3d/query_gltf_primitives.rs"
|
path = "examples/3d/query_gltf_primitives.rs"
|
||||||
|
BIN
assets/models/cube/cube.fbx
Normal file
BIN
assets/models/cube/cube.fbx
Normal file
Binary file not shown.
37
crates/bevy_fbx/Cargo.toml
Normal file
37
crates/bevy_fbx/Cargo.toml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
[package]
|
||||||
|
name = "bevy_fbx"
|
||||||
|
version = "0.16.0-dev"
|
||||||
|
edition = "2024"
|
||||||
|
description = "Bevy Engine FBX loading"
|
||||||
|
homepage = "https://bevyengine.org"
|
||||||
|
repository = "https://github.com/bevyengine/bevy"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
keywords = ["bevy"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy_app = { path = "../bevy_app", version = "0.16.0-dev" }
|
||||||
|
bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" }
|
||||||
|
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }
|
||||||
|
bevy_scene = { path = "../bevy_scene", version = "0.16.0-dev", features = ["bevy_render"] }
|
||||||
|
bevy_render = { path = "../bevy_render", version = "0.16.0-dev" }
|
||||||
|
bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev" }
|
||||||
|
bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" }
|
||||||
|
bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" }
|
||||||
|
bevy_math = { path = "../bevy_math", version = "0.16.0-dev" }
|
||||||
|
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" }
|
||||||
|
bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" }
|
||||||
|
bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = ["std"] }
|
||||||
|
bevy_animation = { path = "../bevy_animation", version = "0.16.0-dev" }
|
||||||
|
thiserror = "1"
|
||||||
|
tracing = { version = "0.1", default-features = false, features = ["std"] }
|
||||||
|
ufbx = "0.8"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
bevy_log = { path = "../bevy_log", version = "0.16.0-dev" }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[package.metadata.docs.rs]
|
||||||
|
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
|
||||||
|
all-features = true
|
5
crates/bevy_fbx/README.md
Normal file
5
crates/bevy_fbx/README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Bevy FBX Loader
|
||||||
|
|
||||||
|
This crate provides basic support for importing FBX files into Bevy using the [`ufbx`](https://github.com/ufbx/ufbx-rust) library.
|
||||||
|
|
||||||
|
The loader converts meshes contained in an FBX scene into Bevy [`Mesh`] assets and groups them into a [`Scene`].
|
41
crates/bevy_fbx/src/label.rs
Normal file
41
crates/bevy_fbx/src/label.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
//! Labels that can be used to load part of an FBX asset
|
||||||
|
|
||||||
|
use bevy_asset::AssetPath;
|
||||||
|
|
||||||
|
/// Labels that can be used to load part of an FBX
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum FbxAssetLabel {
|
||||||
|
/// `Scene{}`: FBX Scene as a Bevy [`Scene`](bevy_scene::Scene)
|
||||||
|
Scene(usize),
|
||||||
|
/// `Mesh{}`: FBX Mesh as a Bevy [`Mesh`](bevy_mesh::Mesh)
|
||||||
|
Mesh(usize),
|
||||||
|
/// `Material{}`: FBX material as a Bevy [`StandardMaterial`](bevy_pbr::StandardMaterial)
|
||||||
|
Material(usize),
|
||||||
|
/// `Animation{}`: FBX animation as a Bevy [`AnimationClip`](bevy_animation::AnimationClip)
|
||||||
|
Animation(usize),
|
||||||
|
/// `Skeleton{}`: FBX skeleton as a Bevy [`Skeleton`](crate::Skeleton)
|
||||||
|
Skeleton(usize),
|
||||||
|
/// `DefaultMaterial`: fallback material used when no material is present
|
||||||
|
DefaultMaterial,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for FbxAssetLabel {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
FbxAssetLabel::Scene(index) => f.write_str(&format!("Scene{index}")),
|
||||||
|
FbxAssetLabel::Mesh(index) => f.write_str(&format!("Mesh{index}")),
|
||||||
|
FbxAssetLabel::Material(index) => f.write_str(&format!("Material{index}")),
|
||||||
|
FbxAssetLabel::Animation(index) => f.write_str(&format!("Animation{index}")),
|
||||||
|
FbxAssetLabel::Skeleton(index) => f.write_str(&format!("Skeleton{index}")),
|
||||||
|
FbxAssetLabel::DefaultMaterial => f.write_str("DefaultMaterial"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FbxAssetLabel {
|
||||||
|
/// Add this label to an asset path
|
||||||
|
pub fn from_asset(&self, path: impl Into<AssetPath<'static>>) -> AssetPath<'static> {
|
||||||
|
path.into().with_label(self.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
350
crates/bevy_fbx/src/lib.rs
Normal file
350
crates/bevy_fbx/src/lib.rs
Normal file
@ -0,0 +1,350 @@
|
|||||||
|
#![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"
|
||||||
|
)]
|
||||||
|
|
||||||
|
//!
|
||||||
|
//! Loader for FBX scenes using [`ufbx`](https://github.com/ufbx/ufbx-rust).
|
||||||
|
//! The implementation is intentionally minimal and focuses on importing
|
||||||
|
//! mesh geometry into Bevy.
|
||||||
|
|
||||||
|
use bevy_app::prelude::*;
|
||||||
|
use bevy_asset::{
|
||||||
|
io::Reader, Asset, AssetApp, AssetLoader, Handle, LoadContext, RenderAssetUsages,
|
||||||
|
};
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use bevy_mesh::{Indices, Mesh, PrimitiveTopology};
|
||||||
|
use bevy_pbr::{MeshMaterial3d, StandardMaterial};
|
||||||
|
use bevy_platform::collections::HashMap;
|
||||||
|
use bevy_reflect::TypePath;
|
||||||
|
use bevy_render::mesh::Mesh3d;
|
||||||
|
use bevy_render::prelude::Visibility;
|
||||||
|
use bevy_scene::Scene;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use bevy_animation::AnimationClip;
|
||||||
|
use bevy_transform::prelude::*;
|
||||||
|
use bevy_math::{Mat4, Vec3};
|
||||||
|
|
||||||
|
mod label;
|
||||||
|
pub use label::FbxAssetLabel;
|
||||||
|
|
||||||
|
pub mod prelude {
|
||||||
|
//! Commonly used items.
|
||||||
|
pub use crate::{Fbx, FbxAssetLabel, FbxPlugin};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Type of relationship between two objects in the FBX hierarchy.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum FbxConnKind {
|
||||||
|
/// Standard parent-child connection.
|
||||||
|
Parent,
|
||||||
|
/// Connection from an object to one of its properties.
|
||||||
|
ObjectProperty,
|
||||||
|
/// Constraint relationship.
|
||||||
|
Constraint,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simplified connection entry extracted from the FBX file.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FbxConnection {
|
||||||
|
/// Source object identifier.
|
||||||
|
pub src: String,
|
||||||
|
/// Destination object identifier.
|
||||||
|
pub dst: String,
|
||||||
|
/// The type of this connection.
|
||||||
|
pub kind: FbxConnKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handedness of a coordinate system.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum Handedness {
|
||||||
|
/// Right handed coordinate system.
|
||||||
|
Right,
|
||||||
|
/// Left handed coordinate system.
|
||||||
|
Left,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coordinate axes definition stored in an FBX file.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct FbxAxisSystem {
|
||||||
|
/// Up axis.
|
||||||
|
pub up: Vec3,
|
||||||
|
/// Forward axis.
|
||||||
|
pub front: Vec3,
|
||||||
|
/// Coordinate system handedness.
|
||||||
|
pub handedness: Handedness,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metadata found in the FBX header.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FbxMeta {
|
||||||
|
/// Creator string.
|
||||||
|
pub creator: Option<String>,
|
||||||
|
/// Timestamp when the file was created.
|
||||||
|
pub creation_time: Option<String>,
|
||||||
|
/// Original application that generated the file.
|
||||||
|
pub original_application: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Placeholder type for skeleton data.
|
||||||
|
#[derive(Asset, Debug, Clone, TypePath)]
|
||||||
|
pub struct Skeleton;
|
||||||
|
|
||||||
|
/// Resulting asset for an FBX file.
|
||||||
|
#[derive(Asset, Debug, TypePath)]
|
||||||
|
pub struct Fbx {
|
||||||
|
/* ===== Core sub-asset handles ===== */
|
||||||
|
/// Split Bevy scenes. A single FBX may contain many scenes.
|
||||||
|
pub scenes: Vec<Handle<Scene>>,
|
||||||
|
/// Triangulated meshes extracted from the FBX.
|
||||||
|
pub meshes: Vec<Handle<Mesh>>,
|
||||||
|
/// PBR materials or fallbacks converted from FBX materials.
|
||||||
|
pub materials: Vec<Handle<StandardMaterial>>,
|
||||||
|
/// Flattened animation takes.
|
||||||
|
pub animations: Vec<Handle<AnimationClip>>,
|
||||||
|
/// Skinning skeletons.
|
||||||
|
pub skeletons: Vec<Handle<Skeleton>>,
|
||||||
|
|
||||||
|
/* ===== Quick name lookups ===== */
|
||||||
|
pub named_meshes: HashMap<Box<str>, Handle<Mesh>>,
|
||||||
|
pub named_materials: HashMap<Box<str>, Handle<StandardMaterial>>,
|
||||||
|
pub named_animations: HashMap<Box<str>, Handle<AnimationClip>>,
|
||||||
|
pub named_skeletons: HashMap<Box<str>, Handle<Skeleton>>,
|
||||||
|
|
||||||
|
/* ===== FBX specific info ===== */
|
||||||
|
/// Flattened parent/child/constraint relations.
|
||||||
|
pub connections: Vec<FbxConnection>,
|
||||||
|
/// Original axis system of the file.
|
||||||
|
pub axis_system: FbxAxisSystem,
|
||||||
|
/// Conversion factor from the original unit to meters.
|
||||||
|
pub unit_scale: f32,
|
||||||
|
/// Copyright, creator and tool information.
|
||||||
|
pub metadata: FbxMeta,
|
||||||
|
|
||||||
|
/* ===== Optional original scene bytes ===== */
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub raw_scene_bytes: Option<Arc<[u8]>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors that may occur while loading an FBX asset.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FbxError {
|
||||||
|
/// IO error while reading the file.
|
||||||
|
Io(std::io::Error),
|
||||||
|
/// Error reported by the `ufbx` parser.
|
||||||
|
Parse(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl core::fmt::Display for FbxError {
|
||||||
|
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||||
|
match self {
|
||||||
|
FbxError::Io(err) => write!(f, "{}", err),
|
||||||
|
FbxError::Parse(err) => write!(f, "{}", err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for FbxError {}
|
||||||
|
|
||||||
|
impl From<std::io::Error> for FbxError {
|
||||||
|
fn from(err: std::io::Error) -> Self {
|
||||||
|
FbxError::Io(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loader implementation for FBX files.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FbxLoader;
|
||||||
|
|
||||||
|
impl AssetLoader for FbxLoader {
|
||||||
|
type Asset = Fbx;
|
||||||
|
type Settings = ();
|
||||||
|
type Error = FbxError;
|
||||||
|
|
||||||
|
async fn load(
|
||||||
|
&self,
|
||||||
|
reader: &mut dyn Reader,
|
||||||
|
_settings: &Self::Settings,
|
||||||
|
load_context: &mut LoadContext<'_>,
|
||||||
|
) -> Result<Fbx, FbxError> {
|
||||||
|
// Read the complete file.
|
||||||
|
let mut bytes = Vec::new();
|
||||||
|
reader.read_to_end(&mut bytes).await?;
|
||||||
|
|
||||||
|
// Parse using `ufbx` and normalize the units/axes so that `1.0` equals
|
||||||
|
// one meter and the coordinate system matches Bevy's.
|
||||||
|
let root = ufbx::load_memory(
|
||||||
|
&bytes,
|
||||||
|
ufbx::LoadOpts {
|
||||||
|
target_unit_meters: 1.0,
|
||||||
|
target_axes: ufbx::CoordinateAxes::right_handed_y_up(),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| FbxError::Parse(format!("{:?}", e)))?;
|
||||||
|
let scene: &ufbx::Scene = &*root;
|
||||||
|
|
||||||
|
let mut meshes = Vec::new();
|
||||||
|
let mut named_meshes = HashMap::new();
|
||||||
|
let mut transforms = Vec::new();
|
||||||
|
let mut scratch = Vec::new();
|
||||||
|
|
||||||
|
for (index, node) in scene.nodes.as_ref().iter().enumerate() {
|
||||||
|
let Some(mesh_ref) = node.mesh.as_ref() else { continue };
|
||||||
|
let mesh = mesh_ref.as_ref();
|
||||||
|
|
||||||
|
// Each mesh becomes a Bevy `Mesh` asset.
|
||||||
|
let handle =
|
||||||
|
load_context.labeled_asset_scope::<_, FbxError>(FbxAssetLabel::Mesh(index).to_string(), |_lc| {
|
||||||
|
let positions: Vec<[f32; 3]> = mesh
|
||||||
|
.vertex_position
|
||||||
|
.values
|
||||||
|
.as_ref()
|
||||||
|
.iter()
|
||||||
|
.map(|v| [v.x as f32, v.y as f32, v.z as f32])
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut bevy_mesh = Mesh::new(
|
||||||
|
PrimitiveTopology::TriangleList,
|
||||||
|
RenderAssetUsages::default(),
|
||||||
|
);
|
||||||
|
bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
|
||||||
|
|
||||||
|
if mesh.vertex_normal.exists {
|
||||||
|
let normals: Vec<[f32; 3]> = (0..mesh.num_vertices)
|
||||||
|
.map(|i| {
|
||||||
|
let n = mesh.vertex_normal[i];
|
||||||
|
[n.x as f32, n.y as f32, n.z as f32]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
|
||||||
|
}
|
||||||
|
|
||||||
|
if mesh.vertex_uv.exists {
|
||||||
|
let uvs: Vec<[f32; 2]> = (0..mesh.num_vertices)
|
||||||
|
.map(|i| {
|
||||||
|
let uv = mesh.vertex_uv[i];
|
||||||
|
[uv.x as f32, uv.y as f32]
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut indices = Vec::new();
|
||||||
|
for &face in mesh.faces.as_ref() {
|
||||||
|
scratch.clear();
|
||||||
|
ufbx::triangulate_face_vec(&mut scratch, mesh, face);
|
||||||
|
for idx in &scratch {
|
||||||
|
let v = mesh.vertex_indices[*idx as usize];
|
||||||
|
indices.push(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bevy_mesh.insert_indices(Indices::U32(indices));
|
||||||
|
|
||||||
|
Ok(bevy_mesh)
|
||||||
|
})?;
|
||||||
|
if !node.element.name.is_empty() {
|
||||||
|
named_meshes.insert(Box::from(node.element.name.as_ref()), handle.clone());
|
||||||
|
}
|
||||||
|
meshes.push(handle);
|
||||||
|
transforms.push(node.geometry_to_world);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert materials. Currently these are simple placeholders.
|
||||||
|
let mut materials = Vec::new();
|
||||||
|
let mut named_materials = HashMap::new();
|
||||||
|
for (index, mat) in scene.materials.as_ref().iter().enumerate() {
|
||||||
|
let handle = load_context.add_labeled_asset(
|
||||||
|
FbxAssetLabel::Material(index).to_string(),
|
||||||
|
StandardMaterial::default(),
|
||||||
|
);
|
||||||
|
if !mat.element.name.is_empty() {
|
||||||
|
named_materials.insert(Box::from(mat.element.name.as_ref()), handle.clone());
|
||||||
|
}
|
||||||
|
materials.push(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a simple scene with all meshes at the origin.
|
||||||
|
let mut world = World::new();
|
||||||
|
let default_material = materials.get(0).cloned().unwrap_or_else(|| {
|
||||||
|
load_context.add_labeled_asset(
|
||||||
|
FbxAssetLabel::DefaultMaterial.to_string(),
|
||||||
|
StandardMaterial::default(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
for (mesh_handle, matrix) in meshes.iter().zip(transforms.iter()) {
|
||||||
|
let mat = Mat4::from_cols_array(&[
|
||||||
|
matrix.m00 as f32,
|
||||||
|
matrix.m10 as f32,
|
||||||
|
matrix.m20 as f32,
|
||||||
|
0.0,
|
||||||
|
matrix.m01 as f32,
|
||||||
|
matrix.m11 as f32,
|
||||||
|
matrix.m21 as f32,
|
||||||
|
0.0,
|
||||||
|
matrix.m02 as f32,
|
||||||
|
matrix.m12 as f32,
|
||||||
|
matrix.m22 as f32,
|
||||||
|
0.0,
|
||||||
|
matrix.m03 as f32,
|
||||||
|
matrix.m13 as f32,
|
||||||
|
matrix.m23 as f32,
|
||||||
|
1.0,
|
||||||
|
]);
|
||||||
|
let transform = Transform::from_matrix(mat);
|
||||||
|
world.spawn((
|
||||||
|
Mesh3d(mesh_handle.clone()),
|
||||||
|
MeshMaterial3d(default_material.clone()),
|
||||||
|
transform,
|
||||||
|
GlobalTransform::default(),
|
||||||
|
Visibility::default(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let scene_handle = load_context.add_labeled_asset(FbxAssetLabel::Scene(0).to_string(), Scene::new(world));
|
||||||
|
|
||||||
|
Ok(Fbx {
|
||||||
|
scenes: vec![scene_handle.clone()],
|
||||||
|
meshes,
|
||||||
|
materials,
|
||||||
|
animations: Vec::new(),
|
||||||
|
skeletons: Vec::new(),
|
||||||
|
named_meshes,
|
||||||
|
named_materials,
|
||||||
|
named_animations: HashMap::new(),
|
||||||
|
named_skeletons: HashMap::new(),
|
||||||
|
connections: Vec::new(),
|
||||||
|
axis_system: FbxAxisSystem {
|
||||||
|
up: Vec3::Y,
|
||||||
|
front: Vec3::Z,
|
||||||
|
handedness: Handedness::Right,
|
||||||
|
},
|
||||||
|
unit_scale: 1.0,
|
||||||
|
metadata: FbxMeta {
|
||||||
|
creator: None,
|
||||||
|
creation_time: None,
|
||||||
|
original_application: None,
|
||||||
|
},
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
raw_scene_bytes: Some(bytes.into()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extensions(&self) -> &[&str] {
|
||||||
|
&["fbx"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plugin adding the FBX loader to an [`App`].
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FbxPlugin;
|
||||||
|
|
||||||
|
impl Plugin for FbxPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_asset::<Fbx>()
|
||||||
|
.register_asset_loader(FbxLoader::default());
|
||||||
|
}
|
||||||
|
}
|
@ -195,6 +195,7 @@ bevy_core_pipeline = ["dep:bevy_core_pipeline", "bevy_image"]
|
|||||||
bevy_anti_aliasing = ["dep:bevy_anti_aliasing", "bevy_image"]
|
bevy_anti_aliasing = ["dep:bevy_anti_aliasing", "bevy_image"]
|
||||||
bevy_gizmos = ["dep:bevy_gizmos", "bevy_image"]
|
bevy_gizmos = ["dep:bevy_gizmos", "bevy_image"]
|
||||||
bevy_gltf = ["dep:bevy_gltf", "bevy_image"]
|
bevy_gltf = ["dep:bevy_gltf", "bevy_image"]
|
||||||
|
bevy_fbx = ["dep:bevy_fbx", "bevy_image", "bevy_animation"]
|
||||||
bevy_ui = ["dep:bevy_ui", "bevy_image"]
|
bevy_ui = ["dep:bevy_ui", "bevy_image"]
|
||||||
bevy_ui_render = ["dep:bevy_ui_render"]
|
bevy_ui_render = ["dep:bevy_ui_render"]
|
||||||
bevy_image = ["dep:bevy_image"]
|
bevy_image = ["dep:bevy_image"]
|
||||||
@ -430,6 +431,7 @@ bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.17.
|
|||||||
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.17.0-dev" }
|
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.17.0-dev" }
|
||||||
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.17.0-dev", default-features = false }
|
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.17.0-dev", default-features = false }
|
||||||
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.17.0-dev" }
|
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.17.0-dev" }
|
||||||
|
bevy_fbx = { path = "../bevy_fbx", optional = true, version = "0.17.0-dev" }
|
||||||
bevy_feathers = { path = "../bevy_feathers", optional = true, version = "0.17.0-dev" }
|
bevy_feathers = { path = "../bevy_feathers", optional = true, version = "0.17.0-dev" }
|
||||||
bevy_image = { path = "../bevy_image", optional = true, version = "0.17.0-dev" }
|
bevy_image = { path = "../bevy_image", optional = true, version = "0.17.0-dev" }
|
||||||
bevy_mesh = { path = "../bevy_mesh", optional = true, version = "0.17.0-dev" }
|
bevy_mesh = { path = "../bevy_mesh", optional = true, version = "0.17.0-dev" }
|
||||||
|
@ -54,6 +54,8 @@ plugin_group! {
|
|||||||
// compressed texture formats.
|
// compressed texture formats.
|
||||||
#[cfg(feature = "bevy_gltf")]
|
#[cfg(feature = "bevy_gltf")]
|
||||||
bevy_gltf:::GltfPlugin,
|
bevy_gltf:::GltfPlugin,
|
||||||
|
#[cfg(feature = "bevy_fbx")]
|
||||||
|
bevy_fbx:::FbxPlugin,
|
||||||
#[cfg(feature = "bevy_audio")]
|
#[cfg(feature = "bevy_audio")]
|
||||||
bevy_audio:::AudioPlugin,
|
bevy_audio:::AudioPlugin,
|
||||||
#[cfg(feature = "bevy_gilrs")]
|
#[cfg(feature = "bevy_gilrs")]
|
||||||
|
@ -45,6 +45,8 @@ pub use bevy_gilrs as gilrs;
|
|||||||
pub use bevy_gizmos as gizmos;
|
pub use bevy_gizmos as gizmos;
|
||||||
#[cfg(feature = "bevy_gltf")]
|
#[cfg(feature = "bevy_gltf")]
|
||||||
pub use bevy_gltf as gltf;
|
pub use bevy_gltf as gltf;
|
||||||
|
#[cfg(feature = "bevy_fbx")]
|
||||||
|
pub use bevy_fbx as fbx;
|
||||||
#[cfg(feature = "bevy_image")]
|
#[cfg(feature = "bevy_image")]
|
||||||
pub use bevy_image as image;
|
pub use bevy_image as image;
|
||||||
pub use bevy_input as input;
|
pub use bevy_input as input;
|
||||||
|
@ -83,6 +83,10 @@ pub use crate::state::prelude::*;
|
|||||||
#[cfg(feature = "bevy_gltf")]
|
#[cfg(feature = "bevy_gltf")]
|
||||||
pub use crate::gltf::prelude::*;
|
pub use crate::gltf::prelude::*;
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[cfg(feature = "bevy_fbx")]
|
||||||
|
pub use crate::fbx::prelude::*;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
#[cfg(feature = "bevy_picking")]
|
#[cfg(feature = "bevy_picking")]
|
||||||
pub use crate::picking::prelude::*;
|
pub use crate::picking::prelude::*;
|
||||||
|
62
examples/3d/load_fbx.rs
Normal file
62
examples/3d/load_fbx.rs
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
use bevy::{
|
||||||
|
pbr::{CascadeShadowConfigBuilder, DirectionalLightShadowMap},
|
||||||
|
prelude::*,
|
||||||
|
};
|
||||||
|
use std::f32::consts::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.insert_resource(DirectionalLightShadowMap { size: 4096 })
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.add_systems(Update, animate_light_direction)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
|
commands.spawn((
|
||||||
|
Camera3d::default(),
|
||||||
|
Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y),
|
||||||
|
EnvironmentMapLight {
|
||||||
|
diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"),
|
||||||
|
specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"),
|
||||||
|
intensity: 250.0,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
DirectionalLight {
|
||||||
|
shadows_enabled: true,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
// This is a relatively small scene, so use tighter shadow
|
||||||
|
// cascade bounds than the default for better quality.
|
||||||
|
// We also adjusted the shadow map to be larger since we're
|
||||||
|
// only using a single cascade.
|
||||||
|
CascadeShadowConfigBuilder {
|
||||||
|
num_cascades: 1,
|
||||||
|
maximum_distance: 1.6,
|
||||||
|
..default()
|
||||||
|
}
|
||||||
|
.build(),
|
||||||
|
));
|
||||||
|
// FBX_TODO: the cube doesn't show up
|
||||||
|
commands.spawn(SceneRoot(
|
||||||
|
asset_server.load(FbxAssetLabel::Scene(0).from_asset("models/cube/cube.fbx")),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn animate_light_direction(
|
||||||
|
time: Res<Time>,
|
||||||
|
mut query: Query<&mut Transform, With<DirectionalLight>>,
|
||||||
|
) {
|
||||||
|
for mut transform in &mut query {
|
||||||
|
transform.rotation = Quat::from_euler(
|
||||||
|
EulerRot::ZYX,
|
||||||
|
0.0,
|
||||||
|
time.elapsed_secs() * PI / 5.0,
|
||||||
|
-FRAC_PI_4,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -167,6 +167,7 @@ Example | Description
|
|||||||
[Load glTF](../examples/3d/load_gltf.rs) | Loads and renders a glTF file as a scene
|
[Load glTF](../examples/3d/load_gltf.rs) | Loads and renders a glTF file as a scene
|
||||||
[Load glTF extras](../examples/3d/load_gltf_extras.rs) | Loads and renders a glTF file as a scene, including the gltf extras
|
[Load glTF extras](../examples/3d/load_gltf_extras.rs) | Loads and renders a glTF file as a scene, including the gltf extras
|
||||||
[Manual Material Implementation](../examples/3d/manual_material.rs) | Demonstrates how to implement a material manually using the mid-level render APIs
|
[Manual Material Implementation](../examples/3d/manual_material.rs) | Demonstrates how to implement a material manually using the mid-level render APIs
|
||||||
|
[Load FBX](../examples/3d/load_fbx.rs) | Loads and renders an FBX file as a scene
|
||||||
[Mesh Ray Cast](../examples/3d/mesh_ray_cast.rs) | Demonstrates ray casting with the `MeshRayCast` system parameter
|
[Mesh Ray Cast](../examples/3d/mesh_ray_cast.rs) | Demonstrates ray casting with the `MeshRayCast` system parameter
|
||||||
[Meshlet](../examples/3d/meshlet.rs) | Meshlet rendering for dense high-poly scenes (experimental)
|
[Meshlet](../examples/3d/meshlet.rs) | Meshlet rendering for dense high-poly scenes (experimental)
|
||||||
[Mixed lighting](../examples/3d/mixed_lighting.rs) | Demonstrates how to combine baked and dynamic lighting
|
[Mixed lighting](../examples/3d/mixed_lighting.rs) | Demonstrates how to combine baked and dynamic lighting
|
||||||
|
@ -82,7 +82,7 @@ fn setup(
|
|||||||
commands.spawn((
|
commands.spawn((
|
||||||
Mesh3d(cube_handle),
|
Mesh3d(cube_handle),
|
||||||
MeshMaterial3d(material_handle.clone()),
|
MeshMaterial3d(material_handle.clone()),
|
||||||
Transform::from_xyz(0.0, 0.0, 0.0),
|
Transform::from_xyz(0.0, 3.0, 0.0),
|
||||||
));
|
));
|
||||||
// sphere
|
// sphere
|
||||||
commands.spawn((
|
commands.spawn((
|
||||||
@ -90,6 +90,11 @@ fn setup(
|
|||||||
MeshMaterial3d(material_handle),
|
MeshMaterial3d(material_handle),
|
||||||
Transform::from_xyz(3.0, 0.0, 0.0),
|
Transform::from_xyz(3.0, 0.0, 0.0),
|
||||||
));
|
));
|
||||||
|
// TODO: don't touch this example
|
||||||
|
commands.spawn(SceneRoot(
|
||||||
|
asset_server.load(FbxAssetLabel::Scene(0).from_asset("models/cube/cube.fbx")),
|
||||||
|
));
|
||||||
|
|
||||||
// light
|
// light
|
||||||
commands.spawn((PointLight::default(), Transform::from_xyz(4.0, 5.0, 4.0)));
|
commands.spawn((PointLight::default(), Transform::from_xyz(4.0, 5.0, 4.0)));
|
||||||
// camera
|
// camera
|
||||||
|
Loading…
Reference in New Issue
Block a user