Skip to content

Commit a0e0df0

Browse files
committed
Add bevy_fbx, an fbx loader based on ufbx
1 parent a16adc7 commit a0e0df0

File tree

13 files changed

+399
-1
lines changed

13 files changed

+399
-1
lines changed

Cargo.toml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ default = [
138138
"bevy_gilrs",
139139
"bevy_gizmos",
140140
"bevy_gltf",
141+
"bevy_fbx",
141142
"bevy_input_focus",
142143
"bevy_log",
143144
"bevy_mesh_picking_backend",
@@ -227,6 +228,8 @@ bevy_gilrs = ["bevy_internal/bevy_gilrs"]
227228

228229
# [glTF](https://www.khronos.org/gltf/) support
229230
bevy_gltf = ["bevy_internal/bevy_gltf", "bevy_asset", "bevy_scene", "bevy_pbr"]
231+
# [FBX](https://www.autodesk.com/products/fbx)
232+
bevy_fbx = ["bevy_internal/bevy_fbx", "bevy_asset", "bevy_scene", "bevy_pbr"]
230233

231234
# Adds PBR rendering
232235
bevy_pbr = [
@@ -1125,6 +1128,17 @@ description = "Loads and renders a glTF file as a scene, including the gltf extr
11251128
category = "3D Rendering"
11261129
wasm = true
11271130

1131+
[[example]]
1132+
name = "load_fbx"
1133+
path = "examples/3d/load_fbx.rs"
1134+
doc-scrape-examples = true
1135+
1136+
[package.metadata.example.load_fbx]
1137+
name = "Load FBX"
1138+
description = "Loads and renders an FBX file as a scene"
1139+
category = "3D Rendering"
1140+
wasm = false
1141+
11281142
[[example]]
11291143
name = "query_gltf_primitives"
11301144
path = "examples/3d/query_gltf_primitives.rs"

assets/models/cube/cube.fbx

25.8 KB
Binary file not shown.

crates/bevy_fbx/Cargo.toml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
[package]
2+
name = "bevy_fbx"
3+
version = "0.16.0-dev"
4+
edition = "2024"
5+
description = "Bevy Engine FBX loading"
6+
homepage = "https://bevyengine.org"
7+
repository = "https://github.com/bevyengine/bevy"
8+
license = "MIT OR Apache-2.0"
9+
keywords = ["bevy"]
10+
11+
[dependencies]
12+
bevy_app = { path = "../bevy_app", version = "0.16.0-dev" }
13+
bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" }
14+
bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" }
15+
bevy_scene = { path = "../bevy_scene", version = "0.16.0-dev", features = ["bevy_render"] }
16+
bevy_render = { path = "../bevy_render", version = "0.16.0-dev" }
17+
bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev" }
18+
bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" }
19+
bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" }
20+
bevy_math = { path = "../bevy_math", version = "0.16.0-dev" }
21+
bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" }
22+
bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" }
23+
bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = ["std"] }
24+
thiserror = "1"
25+
tracing = { version = "0.1", default-features = false, features = ["std"] }
26+
ufbx = "0.8"
27+
28+
[dev-dependencies]
29+
bevy_log = { path = "../bevy_log", version = "0.16.0-dev" }
30+
31+
[lints]
32+
workspace = true
33+
34+
[package.metadata.docs.rs]
35+
rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"]
36+
all-features = true

crates/bevy_fbx/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Bevy FBX Loader
2+
3+
This crate provides basic support for importing FBX files into Bevy using the [`ufbx`](https://github.com/ufbx/ufbx-rust) library.
4+
5+
The loader converts meshes contained in an FBX scene into Bevy [`Mesh`] assets and groups them into a [`Scene`].

crates/bevy_fbx/src/label.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
//! Labels that can be used to load part of an FBX asset
2+
3+
use bevy_asset::AssetPath;
4+
5+
/// Labels that can be used to load part of an FBX
6+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7+
pub enum FbxAssetLabel {
8+
/// `Scene{}`: FBX Scene as a Bevy [`Scene`](bevy_scene::Scene)
9+
Scene(usize),
10+
/// `Mesh{}`: FBX Mesh as a Bevy [`Mesh`](bevy_mesh::Mesh)
11+
Mesh(usize),
12+
/// `Material{}`: FBX material as a Bevy [`StandardMaterial`](bevy_pbr::StandardMaterial)
13+
Material(usize),
14+
}
15+
16+
impl core::fmt::Display for FbxAssetLabel {
17+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
18+
match self {
19+
FbxAssetLabel::Scene(index) => f.write_str(&format!("Scene{index}")),
20+
FbxAssetLabel::Mesh(index) => f.write_str(&format!("Mesh{index}")),
21+
FbxAssetLabel::Material(index) => f.write_str(&format!("Material{index}")),
22+
}
23+
}
24+
}
25+
26+
impl FbxAssetLabel {
27+
/// Add this label to an asset path
28+
pub fn from_asset(&self, path: impl Into<AssetPath<'static>>) -> AssetPath<'static> {
29+
path.into().with_label(self.to_string())
30+
}
31+
}
32+

crates/bevy_fbx/src/lib.rs

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
2+
#![forbid(unsafe_code)]
3+
#![doc(
4+
html_logo_url = "https://bevyengine.org/assets/icon.png",
5+
html_favicon_url = "https://bevyengine.org/assets/icon.png"
6+
)]
7+
8+
//!
9+
//! Loader for FBX scenes using [`ufbx`](https://github.com/ufbx/ufbx-rust).
10+
//! The implementation is intentionally minimal and focuses on importing
11+
//! mesh geometry into Bevy.
12+
13+
use bevy_app::prelude::*;
14+
use bevy_asset::{
15+
io::Reader, Asset, AssetApp, AssetLoader, Handle, LoadContext, RenderAssetUsages,
16+
};
17+
use bevy_ecs::prelude::*;
18+
use bevy_mesh::{Indices, Mesh, PrimitiveTopology};
19+
use bevy_pbr::{MeshMaterial3d, StandardMaterial};
20+
use bevy_platform::collections::HashMap;
21+
use bevy_reflect::TypePath;
22+
use bevy_render::mesh::Mesh3d;
23+
use bevy_render::prelude::Visibility;
24+
use bevy_scene::Scene;
25+
use bevy_transform::prelude::*;
26+
use bevy_math::Mat4;
27+
28+
mod label;
29+
pub use label::FbxAssetLabel;
30+
31+
pub mod prelude {
32+
//! Commonly used items.
33+
pub use crate::{Fbx, FbxAssetLabel, FbxPlugin};
34+
}
35+
36+
/// Resulting asset for an FBX file.
37+
#[derive(Asset, Debug, TypePath)]
38+
pub struct Fbx {
39+
/// All scenes created from the FBX file.
40+
pub scenes: Vec<Handle<Scene>>,
41+
/// Meshes extracted from the FBX file.
42+
pub meshes: Vec<Handle<Mesh>>,
43+
/// All materials loaded from the FBX file.
44+
pub materials: Vec<Handle<StandardMaterial>>,
45+
/// Named materials loaded from the FBX file.
46+
pub named_materials: HashMap<Box<str>, Handle<StandardMaterial>>,
47+
/// Default scene to display.
48+
pub default_scene: Option<Handle<Scene>>,
49+
}
50+
51+
/// Errors that may occur while loading an FBX asset.
52+
#[derive(Debug)]
53+
pub enum FbxError {
54+
/// IO error while reading the file.
55+
Io(std::io::Error),
56+
/// Error reported by the `ufbx` parser.
57+
Parse(String),
58+
}
59+
60+
impl core::fmt::Display for FbxError {
61+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
62+
match self {
63+
FbxError::Io(err) => write!(f, "{}", err),
64+
FbxError::Parse(err) => write!(f, "{}", err),
65+
}
66+
}
67+
}
68+
69+
impl std::error::Error for FbxError {}
70+
71+
impl From<std::io::Error> for FbxError {
72+
fn from(err: std::io::Error) -> Self {
73+
FbxError::Io(err)
74+
}
75+
}
76+
77+
/// Loader implementation for FBX files.
78+
#[derive(Default)]
79+
pub struct FbxLoader;
80+
81+
impl AssetLoader for FbxLoader {
82+
type Asset = Fbx;
83+
type Settings = ();
84+
type Error = FbxError;
85+
86+
async fn load(
87+
&self,
88+
reader: &mut dyn Reader,
89+
_settings: &Self::Settings,
90+
load_context: &mut LoadContext<'_>,
91+
) -> Result<Fbx, FbxError> {
92+
// Read the complete file.
93+
let mut bytes = Vec::new();
94+
reader.read_to_end(&mut bytes).await?;
95+
96+
// Parse using `ufbx` and normalize the units/axes so that `1.0` equals
97+
// one meter and the coordinate system matches Bevy's.
98+
let root = ufbx::load_memory(
99+
&bytes,
100+
ufbx::LoadOpts {
101+
target_unit_meters: 1.0,
102+
target_axes: ufbx::CoordinateAxes::right_handed_y_up(),
103+
..Default::default()
104+
},
105+
)
106+
.map_err(|e| FbxError::Parse(format!("{:?}", e)))?;
107+
let scene: &ufbx::Scene = &*root;
108+
109+
let mut meshes = Vec::new();
110+
let mut transforms = Vec::new();
111+
let mut scratch = Vec::new();
112+
113+
for (index, node) in scene.nodes.as_ref().iter().enumerate() {
114+
let Some(mesh_ref) = node.mesh.as_ref() else { continue };
115+
let mesh = mesh_ref.as_ref();
116+
117+
// Each mesh becomes a Bevy `Mesh` asset.
118+
let handle =
119+
load_context.labeled_asset_scope::<_, FbxError>(FbxAssetLabel::Mesh(index).to_string(), |_lc| {
120+
let positions: Vec<[f32; 3]> = mesh
121+
.vertex_position
122+
.values
123+
.as_ref()
124+
.iter()
125+
.map(|v| [v.x as f32, v.y as f32, v.z as f32])
126+
.collect();
127+
128+
let mut bevy_mesh = Mesh::new(
129+
PrimitiveTopology::TriangleList,
130+
RenderAssetUsages::default(),
131+
);
132+
bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
133+
134+
if mesh.vertex_normal.exists {
135+
let normals: Vec<[f32; 3]> = (0..mesh.num_vertices)
136+
.map(|i| {
137+
let n = mesh.vertex_normal[i];
138+
[n.x as f32, n.y as f32, n.z as f32]
139+
})
140+
.collect();
141+
bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals);
142+
}
143+
144+
if mesh.vertex_uv.exists {
145+
let uvs: Vec<[f32; 2]> = (0..mesh.num_vertices)
146+
.map(|i| {
147+
let uv = mesh.vertex_uv[i];
148+
[uv.x as f32, uv.y as f32]
149+
})
150+
.collect();
151+
bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
152+
}
153+
154+
let mut indices = Vec::new();
155+
for &face in mesh.faces.as_ref() {
156+
scratch.clear();
157+
ufbx::triangulate_face_vec(&mut scratch, mesh, face);
158+
for idx in &scratch {
159+
let v = mesh.vertex_indices[*idx as usize];
160+
indices.push(v);
161+
}
162+
}
163+
bevy_mesh.insert_indices(Indices::U32(indices));
164+
165+
Ok(bevy_mesh)
166+
})?;
167+
meshes.push(handle);
168+
transforms.push(node.geometry_to_world);
169+
}
170+
171+
// Build a simple scene with all meshes at the origin.
172+
let mut world = World::new();
173+
let material: Handle<StandardMaterial> =
174+
load_context.add_labeled_asset(FbxAssetLabel::Material(0).to_string(), StandardMaterial::default());
175+
let materials = vec![material.clone()];
176+
let mut named_materials = HashMap::new();
177+
named_materials.insert(Box::from("Material0"), material.clone());
178+
179+
for (mesh_handle, matrix) in meshes.iter().zip(transforms.iter()) {
180+
let mat = Mat4::from_cols_array(&[
181+
matrix.m00 as f32,
182+
matrix.m10 as f32,
183+
matrix.m20 as f32,
184+
0.0,
185+
matrix.m01 as f32,
186+
matrix.m11 as f32,
187+
matrix.m21 as f32,
188+
0.0,
189+
matrix.m02 as f32,
190+
matrix.m12 as f32,
191+
matrix.m22 as f32,
192+
0.0,
193+
matrix.m03 as f32,
194+
matrix.m13 as f32,
195+
matrix.m23 as f32,
196+
1.0,
197+
]);
198+
let transform = Transform::from_matrix(mat);
199+
world.spawn((
200+
Mesh3d(mesh_handle.clone()),
201+
MeshMaterial3d(material.clone()),
202+
transform,
203+
GlobalTransform::default(),
204+
Visibility::default(),
205+
));
206+
}
207+
208+
let scene_handle = load_context.add_labeled_asset(FbxAssetLabel::Scene(0).to_string(), Scene::new(world));
209+
210+
Ok(Fbx {
211+
scenes: vec![scene_handle.clone()],
212+
meshes,
213+
materials,
214+
named_materials,
215+
default_scene: Some(scene_handle),
216+
})
217+
}
218+
219+
fn extensions(&self) -> &[&str] {
220+
&["fbx"]
221+
}
222+
}
223+
224+
/// Plugin adding the FBX loader to an [`App`].
225+
#[derive(Default)]
226+
pub struct FbxPlugin;
227+
228+
impl Plugin for FbxPlugin {
229+
fn build(&self, app: &mut App) {
230+
app.init_asset::<Fbx>()
231+
.register_asset_loader(FbxLoader::default());
232+
}
233+
}

crates/bevy_internal/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ bevy_core_pipeline = ["dep:bevy_core_pipeline", "bevy_image"]
177177
bevy_anti_aliasing = ["dep:bevy_anti_aliasing", "bevy_image"]
178178
bevy_gizmos = ["dep:bevy_gizmos", "bevy_image"]
179179
bevy_gltf = ["dep:bevy_gltf", "bevy_image"]
180+
bevy_fbx = ["dep:bevy_fbx", "bevy_image"]
180181
bevy_ui = ["dep:bevy_ui", "bevy_image"]
181182
bevy_image = ["dep:bevy_image"]
182183

@@ -400,6 +401,7 @@ bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.16.
400401
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.16.0-dev" }
401402
bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.16.0-dev", default-features = false }
402403
bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.16.0-dev" }
404+
bevy_fbx = { path = "../bevy_fbx", optional = true, version = "0.16.0-dev" }
403405
bevy_image = { path = "../bevy_image", optional = true, version = "0.16.0-dev" }
404406
bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0.16.0-dev", default-features = false, features = [
405407
"bevy_reflect",

crates/bevy_internal/src/default_plugins.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ plugin_group! {
5252
// compressed texture formats.
5353
#[cfg(feature = "bevy_gltf")]
5454
bevy_gltf:::GltfPlugin,
55+
#[cfg(feature = "bevy_fbx")]
56+
bevy_fbx:::FbxPlugin,
5557
#[cfg(feature = "bevy_audio")]
5658
bevy_audio:::AudioPlugin,
5759
#[cfg(feature = "bevy_gilrs")]

crates/bevy_internal/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ pub use bevy_gilrs as gilrs;
3939
pub use bevy_gizmos as gizmos;
4040
#[cfg(feature = "bevy_gltf")]
4141
pub use bevy_gltf as gltf;
42+
#[cfg(feature = "bevy_fbx")]
43+
pub use bevy_fbx as fbx;
4244
#[cfg(feature = "bevy_image")]
4345
pub use bevy_image as image;
4446
pub use bevy_input as input;

crates/bevy_internal/src/prelude.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ pub use crate::state::prelude::*;
7979
#[cfg(feature = "bevy_gltf")]
8080
pub use crate::gltf::prelude::*;
8181

82+
#[doc(hidden)]
83+
#[cfg(feature = "bevy_fbx")]
84+
pub use crate::fbx::prelude::*;
85+
8286
#[doc(hidden)]
8387
#[cfg(feature = "bevy_picking")]
8488
pub use crate::picking::prelude::*;

0 commit comments

Comments
 (0)