use crate::ecs::animation::components::{
AnimationChannel, AnimationClip, AnimationInterpolation, AnimationProperty, AnimationSampler,
AnimationSamplerOutput,
};
use crate::ecs::material::components::Material;
use crate::ecs::mesh::components::{Mesh, SkinData, SkinnedVertex, Vertex};
use crate::ecs::world::components::{LocalTransform, Name, RenderMesh, Visibility};
use image::ImageReader;
use nalgebra_glm::{Mat4, Quat, Vec3, vec3};
use std::collections::HashMap;
use std::io::Cursor;
use super::super::components::{Prefab, PrefabComponents, PrefabNode};
use super::gltf_import::GltfSkin;
pub struct FbxLoadResult {
pub prefabs: Vec<Prefab>,
pub meshes: HashMap<String, Mesh>,
pub materials: Vec<Material>,
pub textures: HashMap<String, (Vec<u8>, u32, u32)>,
pub animations: Vec<AnimationClip>,
pub skins: Vec<GltfSkin>,
pub node_to_skin: HashMap<usize, usize>,
pub node_count: usize,
}
#[derive(Debug)]
pub struct FbxError(String);
impl std::fmt::Display for FbxError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "FBX error: {}", self.0)
}
}
impl std::error::Error for FbxError {}
fn fbx_load_opts() -> ufbx::LoadOpts<'static> {
ufbx::LoadOpts {
target_axes: ufbx::CoordinateAxes::right_handed_y_up(),
target_unit_meters: 1.0,
..Default::default()
}
}
pub fn import_fbx_from_path(
path: &std::path::Path,
) -> Result<FbxLoadResult, Box<dyn std::error::Error>> {
let path_str = path
.to_str()
.ok_or_else(|| FbxError("Invalid path".into()))?;
let scene = ufbx::load_file(path_str, fbx_load_opts())
.map_err(|error| FbxError(format!("{:?}", error)))?;
let canonical_path = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
let namespace = canonical_path
.to_str()
.unwrap_or("unnamed")
.replace('\\', "/")
.replace('/', "::");
process_fbx_scene(&scene, &namespace)
}
pub fn import_fbx_from_bytes(bytes: &[u8]) -> Result<FbxLoadResult, Box<dyn std::error::Error>> {
let scene = ufbx::load_memory(bytes, fbx_load_opts())
.map_err(|error| FbxError(format!("{:?}", error)))?;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher);
let namespace = format!("fbx_bytes_{:016x}", hasher.finish());
process_fbx_scene(&scene, &namespace)
}
pub fn import_fbx_animations_from_path(
path: &std::path::Path,
) -> Result<Vec<AnimationClip>, Box<dyn std::error::Error>> {
let path_str = path
.to_str()
.ok_or_else(|| FbxError("Invalid path".into()))?;
let scene = ufbx::load_file(path_str, fbx_load_opts())
.map_err(|error| FbxError(format!("{:?}", error)))?;
convert_animations(&scene)
}
pub fn import_fbx_animations_from_bytes(
bytes: &[u8],
) -> Result<Vec<AnimationClip>, Box<dyn std::error::Error>> {
let scene = ufbx::load_memory(bytes, fbx_load_opts())
.map_err(|error| FbxError(format!("{:?}", error)))?;
convert_animations(&scene)
}
fn process_fbx_scene(
scene: &ufbx::Scene,
namespace: &str,
) -> Result<FbxLoadResult, Box<dyn std::error::Error>> {
let mut meshes = HashMap::new();
let mut materials = Vec::new();
let mut material_id_to_index: HashMap<usize, usize> = HashMap::new();
let mut skins = Vec::new();
let mut node_to_skin: HashMap<usize, usize> = HashMap::new();
let mut node_index_to_mesh_name: HashMap<usize, String> = HashMap::new();
for (mesh_index, mesh) in scene.meshes.iter().enumerate() {
let mesh_name = if mesh.element.name.is_empty() {
format!("{}::Mesh_{}", namespace, mesh_index)
} else {
format!("{}::{}", namespace, mesh.element.name)
};
let skin_deformer = mesh.skin_deformers.first();
let converted_mesh = if let Some(skin) = skin_deformer {
convert_skinned_mesh(mesh, skin)?
} else {
convert_static_mesh(mesh)?
};
meshes.insert(mesh_name.clone(), converted_mesh);
for node in &scene.nodes {
if let Some(ref node_mesh) = node.mesh
&& node_mesh.element.typed_id == mesh.element.typed_id
{
let node_index = node.element.typed_id as usize;
node_index_to_mesh_name.insert(node_index, mesh_name.clone());
if let Some(skin) = skin_deformer {
let skin_index = skins.len();
let gltf_skin = convert_skin(skin);
skins.push(gltf_skin);
node_to_skin.insert(node_index, skin_index);
}
}
}
}
let mut textures: HashMap<String, (Vec<u8>, u32, u32)> = HashMap::new();
let mut texture_id_to_name: HashMap<usize, String> = HashMap::new();
for (texture_index, texture) in scene.textures.iter().enumerate() {
if !texture.content.is_empty() {
let texture_name = if texture.element.name.is_empty() {
format!("{}::Texture_{}", namespace, texture_index)
} else {
format!("{}::{}", namespace, texture.element.name)
};
if let Some((rgba_data, width, height)) = decode_embedded_texture(&texture.content) {
textures.insert(texture_name.clone(), (rgba_data, width, height));
texture_id_to_name.insert(texture.element.typed_id as usize, texture_name);
}
}
}
for (index, material) in scene.materials.iter().enumerate() {
let converted = convert_material(material, &texture_id_to_name);
material_id_to_index.insert(material.element.typed_id as usize, index);
materials.push(converted);
}
let animations = convert_animations(scene)?;
let prefabs = build_prefabs(
scene,
&node_index_to_mesh_name,
&node_to_skin,
&materials,
&material_id_to_index,
);
Ok(FbxLoadResult {
prefabs,
meshes,
materials,
textures,
animations,
skins,
node_to_skin,
node_count: scene.nodes.len(),
})
}
fn convert_static_mesh(mesh: &ufbx::Mesh) -> Result<Mesh, Box<dyn std::error::Error>> {
let mut vertices = Vec::new();
let mut indices = Vec::new();
let mut triangle_indices = Vec::new();
for face in &mesh.faces {
let triangle_count = ufbx::triangulate_face_vec(&mut triangle_indices, mesh, *face);
for triangle_index in 0..(triangle_count as usize) {
for corner in 0..3 {
let index = triangle_indices[triangle_index * 3 + corner] as usize;
let position = mesh.vertex_position[index];
let normal = if !mesh.vertex_normal.values.is_empty() {
mesh.vertex_normal[index]
} else {
ufbx::Vec3 {
x: 0.0,
y: 1.0,
z: 0.0,
}
};
let uv = if !mesh.vertex_uv.values.is_empty() {
mesh.vertex_uv[index]
} else {
ufbx::Vec2 { x: 0.0, y: 0.0 }
};
let tangent = if !mesh.vertex_tangent.values.is_empty() {
let value = mesh.vertex_tangent[index];
[value.x as f32, value.y as f32, value.z as f32, 1.0]
} else {
[1.0, 0.0, 0.0, 1.0]
};
vertices.push(Vertex {
position: [position.x as f32, position.y as f32, position.z as f32],
normal: [normal.x as f32, normal.y as f32, normal.z as f32],
tex_coords: [uv.x as f32, 1.0 - uv.y as f32],
tex_coords_1: [0.0, 0.0],
tangent,
color: [1.0, 1.0, 1.0, 1.0],
});
indices.push(indices.len() as u32);
}
}
}
Ok(Mesh {
vertices,
indices,
bounding_volume: None,
skin_data: None,
morph_targets: None,
})
}
fn convert_skinned_mesh(
mesh: &ufbx::Mesh,
skin: &ufbx::SkinDeformer,
) -> Result<Mesh, Box<dyn std::error::Error>> {
let mut skinned_vertices = Vec::new();
let mut indices = Vec::new();
let mut triangle_indices = Vec::new();
for face in &mesh.faces {
let triangle_count = ufbx::triangulate_face_vec(&mut triangle_indices, mesh, *face);
for triangle_index in 0..(triangle_count as usize) {
for corner in 0..3 {
let index = triangle_indices[triangle_index * 3 + corner] as usize;
let vertex_index = mesh.vertex_indices[index] as usize;
let position = mesh.vertex_position[index];
let normal = if !mesh.vertex_normal.values.is_empty() {
mesh.vertex_normal[index]
} else {
ufbx::Vec3 {
x: 0.0,
y: 1.0,
z: 0.0,
}
};
let uv = if !mesh.vertex_uv.values.is_empty() {
mesh.vertex_uv[index]
} else {
ufbx::Vec2 { x: 0.0, y: 0.0 }
};
let tangent = if !mesh.vertex_tangent.values.is_empty() {
let value = mesh.vertex_tangent[index];
[value.x as f32, value.y as f32, value.z as f32, 1.0]
} else {
[1.0, 0.0, 0.0, 1.0]
};
let skin_vertex = &skin.vertices[vertex_index];
let mut joint_indices = [0u32; 4];
let mut joint_weights = [0.0f32; 4];
let mut total_weight = 0.0f32;
let weight_count = (skin_vertex.num_weights as usize).min(4);
for weight_index in 0..weight_count {
let weight = &skin.weights[skin_vertex.weight_begin as usize + weight_index];
joint_indices[weight_index] = weight.cluster_index;
joint_weights[weight_index] = weight.weight as f32;
total_weight += weight.weight as f32;
}
if total_weight > 0.0 {
for weight in &mut joint_weights {
*weight /= total_weight;
}
}
skinned_vertices.push(SkinnedVertex {
position: [position.x as f32, position.y as f32, position.z as f32],
normal: [normal.x as f32, normal.y as f32, normal.z as f32],
tex_coords: [uv.x as f32, 1.0 - uv.y as f32],
tex_coords_1: [0.0, 0.0],
tangent,
color: [1.0, 1.0, 1.0, 1.0],
joint_indices,
joint_weights,
});
indices.push(indices.len() as u32);
}
}
}
Ok(Mesh {
vertices: Vec::new(),
indices,
bounding_volume: None,
skin_data: Some(SkinData {
skinned_vertices,
skin_index: None,
}),
morph_targets: None,
})
}
fn convert_skin(skin: &ufbx::SkinDeformer) -> GltfSkin {
let mut joints = Vec::new();
let mut inverse_bind_matrices = Vec::new();
for cluster in &skin.clusters {
if let Some(ref bone_node) = cluster.bone_node {
let node_index = bone_node.element.typed_id as usize;
joints.push(node_index);
let inverse_bind_matrix = &cluster.geometry_to_bone;
inverse_bind_matrices.push(ufbx_matrix_to_mat4(inverse_bind_matrix));
}
}
GltfSkin {
name: if skin.element.name.is_empty() {
None
} else {
Some(skin.element.name.to_string())
},
joints,
inverse_bind_matrices,
}
}
fn decode_embedded_texture(content: &ufbx::Blob) -> Option<(Vec<u8>, u32, u32)> {
let bytes: &[u8] = content.as_ref();
let cursor = Cursor::new(bytes);
let reader = ImageReader::new(cursor).with_guessed_format().ok()?;
let image = reader.decode().ok()?;
let rgba = image.to_rgba8();
let width = rgba.width();
let height = rgba.height();
Some((rgba.into_raw(), width, height))
}
fn get_texture_name(
map: &ufbx::MaterialMap,
texture_id_to_name: &HashMap<usize, String>,
) -> Option<String> {
map.texture.as_ref().and_then(|texture| {
texture_id_to_name
.get(&(texture.element.typed_id as usize))
.cloned()
})
}
fn convert_material(
material: &ufbx::Material,
texture_id_to_name: &HashMap<usize, String>,
) -> Material {
let pbr_color = material.pbr.base_color.value_vec4;
let diffuse_color = material.fbx.diffuse_color.value_vec4;
let base_color = if pbr_color.x > 0.0 || pbr_color.y > 0.0 || pbr_color.z > 0.0 {
[
pbr_color.x as f32,
pbr_color.y as f32,
pbr_color.z as f32,
pbr_color.w as f32,
]
} else if diffuse_color.x > 0.0 || diffuse_color.y > 0.0 || diffuse_color.z > 0.0 {
[
diffuse_color.x as f32,
diffuse_color.y as f32,
diffuse_color.z as f32,
1.0,
]
} else {
[1.0, 1.0, 1.0, 1.0]
};
let base_texture = get_texture_name(&material.pbr.base_color, texture_id_to_name)
.or_else(|| get_texture_name(&material.fbx.diffuse_color, texture_id_to_name));
let normal_texture = get_texture_name(&material.pbr.normal_map, texture_id_to_name)
.or_else(|| get_texture_name(&material.fbx.normal_map, texture_id_to_name))
.or_else(|| get_texture_name(&material.fbx.bump, texture_id_to_name));
let emissive_texture = get_texture_name(&material.pbr.emission_color, texture_id_to_name)
.or_else(|| get_texture_name(&material.pbr.emission_factor, texture_id_to_name))
.or_else(|| get_texture_name(&material.fbx.emission_color, texture_id_to_name));
let metallic_roughness_texture = get_texture_name(&material.pbr.roughness, texture_id_to_name)
.or_else(|| get_texture_name(&material.pbr.metalness, texture_id_to_name));
let occlusion_texture = get_texture_name(&material.pbr.ambient_occlusion, texture_id_to_name);
let pbr_emissive = material.pbr.emission_color.value_vec4;
let fbx_emissive = material.fbx.emission_color.value_vec4;
let emissive_factor = if pbr_emissive.x > 0.0 || pbr_emissive.y > 0.0 || pbr_emissive.z > 0.0 {
[
pbr_emissive.x as f32,
pbr_emissive.y as f32,
pbr_emissive.z as f32,
]
} else if fbx_emissive.x > 0.0 || fbx_emissive.y > 0.0 || fbx_emissive.z > 0.0 {
[
fbx_emissive.x as f32,
fbx_emissive.y as f32,
fbx_emissive.z as f32,
]
} else {
[0.0, 0.0, 0.0]
};
let metallic = material.pbr.metalness.value_vec4.x as f32;
let roughness = if material.pbr.roughness.value_vec4.x > 0.0 {
material.pbr.roughness.value_vec4.x as f32
} else {
let shininess = material.fbx.specular_exponent.value_vec4.x as f32;
if shininess > 0.0 {
1.0 - (shininess / 100.0).min(1.0)
} else {
0.5
}
};
Material {
base_color,
base_texture,
normal_texture,
emissive_texture,
emissive_factor,
metallic_roughness_texture,
occlusion_texture,
metallic,
roughness,
..Material::default()
}
}
fn convert_animations(
scene: &ufbx::Scene,
) -> Result<Vec<AnimationClip>, Box<dyn std::error::Error>> {
let mut clips = Vec::new();
for anim_stack in &scene.anim_stacks {
let stack_duration = (anim_stack.time_end - anim_stack.time_begin) as f32;
let bake_opts = ufbx::BakeOpts {
resample_rate: 30.0,
..Default::default()
};
let baked = ufbx::bake_anim(scene, &anim_stack.anim, bake_opts)
.map_err(|error| FbxError(format!("{:?}", error)))?;
let mut channels = Vec::new();
let mut max_time = 0.0f32;
for baked_node in &baked.nodes {
let node_index = baked_node.typed_id as usize;
let node_name = scene
.nodes
.iter()
.find(|node| node.element.typed_id == baked_node.typed_id)
.map(|node| node.element.name.to_string());
if !baked_node.translation_keys.is_empty() {
let times: Vec<f32> = baked_node
.translation_keys
.iter()
.map(|key| key.time as f32)
.collect();
let values: Vec<Vec3> = baked_node
.translation_keys
.iter()
.map(|key| vec3(key.value.x as f32, key.value.y as f32, key.value.z as f32))
.collect();
if let Some(&last_time) = times.last() {
max_time = max_time.max(last_time);
}
channels.push(AnimationChannel {
target_node: node_index,
target_bone_name: node_name.clone(),
target_property: AnimationProperty::Translation,
sampler: AnimationSampler {
input: times,
output: AnimationSamplerOutput::Vec3(values),
interpolation: AnimationInterpolation::Linear,
},
});
}
if !baked_node.rotation_keys.is_empty() {
let times: Vec<f32> = baked_node
.rotation_keys
.iter()
.map(|key| key.time as f32)
.collect();
let values: Vec<Quat> = baked_node
.rotation_keys
.iter()
.map(|key| {
Quat::new(
key.value.w as f32,
key.value.x as f32,
key.value.y as f32,
key.value.z as f32,
)
})
.collect();
if let Some(&last_time) = times.last() {
max_time = max_time.max(last_time);
}
channels.push(AnimationChannel {
target_node: node_index,
target_bone_name: node_name.clone(),
target_property: AnimationProperty::Rotation,
sampler: AnimationSampler {
input: times,
output: AnimationSamplerOutput::Quat(values),
interpolation: AnimationInterpolation::Linear,
},
});
}
if !baked_node.scale_keys.is_empty() {
let times: Vec<f32> = baked_node
.scale_keys
.iter()
.map(|key| key.time as f32)
.collect();
let values: Vec<Vec3> = baked_node
.scale_keys
.iter()
.map(|key| vec3(key.value.x as f32, key.value.y as f32, key.value.z as f32))
.collect();
if let Some(&last_time) = times.last() {
max_time = max_time.max(last_time);
}
channels.push(AnimationChannel {
target_node: node_index,
target_bone_name: node_name,
target_property: AnimationProperty::Scale,
sampler: AnimationSampler {
input: times,
output: AnimationSamplerOutput::Vec3(values),
interpolation: AnimationInterpolation::Linear,
},
});
}
}
let final_duration = if stack_duration > max_time {
stack_duration
} else {
max_time
};
if !channels.is_empty() {
let clip_name = if anim_stack.element.name.is_empty() {
format!("Animation_{}", clips.len())
} else {
anim_stack.element.name.to_string()
};
clips.push(AnimationClip {
name: clip_name,
duration: final_duration,
channels,
});
}
}
Ok(clips)
}
fn build_prefabs(
scene: &ufbx::Scene,
node_index_to_mesh_name: &HashMap<usize, String>,
node_to_skin: &HashMap<usize, usize>,
materials: &[Material],
material_id_to_index: &HashMap<usize, usize>,
) -> Vec<Prefab> {
let root_nodes: Vec<_> = scene
.nodes
.iter()
.filter(|node| {
node.parent.is_none()
|| node
.parent
.as_ref()
.map(|parent| parent.is_root)
.unwrap_or(false)
})
.filter(|node| !node.is_root)
.collect();
if root_nodes.is_empty() {
return Vec::new();
}
let mut prefab_root_nodes = Vec::new();
for root_node in &root_nodes {
let prefab_node = build_single_prefab_node(
root_node,
node_index_to_mesh_name,
node_to_skin,
materials,
material_id_to_index,
);
prefab_root_nodes.push(prefab_node);
}
let prefab_name = if root_nodes.len() == 1 {
let first = &root_nodes[0];
if first.element.name.is_empty() {
"FBX_Import".to_string()
} else {
first.element.name.to_string()
}
} else {
"FBX_Import".to_string()
};
vec![Prefab {
name: prefab_name,
root_nodes: prefab_root_nodes,
material_variants: Vec::new(),
}]
}
fn build_single_prefab_node(
node: &ufbx::Node,
node_index_to_mesh_name: &HashMap<usize, String>,
node_to_skin: &HashMap<usize, usize>,
materials: &[Material],
material_id_to_index: &HashMap<usize, usize>,
) -> PrefabNode {
let node_index = node.element.typed_id as usize;
let local_transform = LocalTransform {
translation: vec3(
node.local_transform.translation.x as f32,
node.local_transform.translation.y as f32,
node.local_transform.translation.z as f32,
),
rotation: Quat::new(
node.local_transform.rotation.w as f32,
node.local_transform.rotation.x as f32,
node.local_transform.rotation.y as f32,
node.local_transform.rotation.z as f32,
),
scale: vec3(
node.local_transform.scale.x as f32,
node.local_transform.scale.y as f32,
node.local_transform.scale.z as f32,
),
};
let mut components = PrefabComponents::default();
if !node.element.name.is_empty() {
components.name = Some(Name(node.element.name.to_string()));
}
if let Some(mesh_name) = node_index_to_mesh_name.get(&node_index) {
components.render_mesh = Some(RenderMesh::new(mesh_name.clone()));
components.visibility = Some(Visibility { visible: true });
if let Some(&skin_index) = node_to_skin.get(&node_index) {
components.skin_index = Some(skin_index);
}
if let Some(ref mesh) = node.mesh
&& let Some(material) = mesh.materials.first()
&& let Some(&material_index) =
material_id_to_index.get(&(material.element.typed_id as usize))
&& material_index < materials.len()
{
components.material = Some(materials[material_index].clone());
}
}
let mut children = Vec::new();
for child in &node.children {
let child_node = build_single_prefab_node(
child,
node_index_to_mesh_name,
node_to_skin,
materials,
material_id_to_index,
);
children.push(child_node);
}
PrefabNode {
local_transform,
components,
children,
node_index: Some(node_index),
}
}
fn ufbx_matrix_to_mat4(matrix: &ufbx::Matrix) -> Mat4 {
Mat4::new(
matrix.m00 as f32,
matrix.m01 as f32,
matrix.m02 as f32,
matrix.m03 as f32,
matrix.m10 as f32,
matrix.m11 as f32,
matrix.m12 as f32,
matrix.m13 as f32,
matrix.m20 as f32,
matrix.m21 as f32,
matrix.m22 as f32,
matrix.m23 as f32,
0.0,
0.0,
0.0,
1.0,
)
}