|
| 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 | +} |
0 commit comments