use crate::ecs::animation::components::{
AnimationChannel, AnimationClip, AnimationInterpolation, AnimationProperty, AnimationSampler,
AnimationSamplerOutput,
};
use crate::ecs::mesh::components::{Mesh, SkinData, SkinnedVertex, Vertex};
use crate::ecs::world::components::{LocalTransform, Material, 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, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "FBX error: {}", self.0)
}
}
impl std::error::Error for FbxError {}
pub fn import_fbx_from_path(
path: &std::path::Path,
) -> Result<FbxLoadResult, Box<dyn std::error::Error>> {
let opts = ufbx::LoadOpts {
target_axes: ufbx::CoordinateAxes::right_handed_y_up(),
target_unit_meters: 1.0,
..Default::default()
};
let path_str = path
.to_str()
.ok_or_else(|| FbxError("Invalid path".into()))?;
let scene = ufbx::load_file(path_str, opts).map_err(|e| FbxError(format!("{:?}", e)))?;
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_animations_from_path(
path: &std::path::Path,
) -> Result<Vec<AnimationClip>, Box<dyn std::error::Error>> {
let opts = ufbx::LoadOpts {
target_axes: ufbx::CoordinateAxes::right_handed_y_up(),
target_unit_meters: 1.0,
..Default::default()
};
let path_str = path
.to_str()
.ok_or_else(|| FbxError("Invalid path".into()))?;
let scene = ufbx::load_file(path_str, opts).map_err(|e| FbxError(format!("{:?}", e)))?;
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, mat) in scene.materials.iter().enumerate() {
let material = convert_material(mat, &texture_id_to_name);
material_id_to_index.insert(mat.element.typed_id as usize, index);
materials.push(material);
}
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 tri_indices_buf = Vec::new();
for face in &mesh.faces {
let num_tris = ufbx::triangulate_face_vec(&mut tri_indices_buf, mesh, *face);
for tri_idx in 0..(num_tris as usize) {
for corner in 0..3 {
let index = tri_indices_buf[tri_idx * 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 t = mesh.vertex_tangent[index];
[t.x as f32, t.y as f32, t.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 tri_indices_buf = Vec::new();
for face in &mesh.faces {
let num_tris = ufbx::triangulate_face_vec(&mut tri_indices_buf, mesh, *face);
for tri_idx in 0..(num_tris as usize) {
for corner in 0..3 {
let index = tri_indices_buf[tri_idx * 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 t = mesh.vertex_tangent[index];
[t.x as f32, t.y as f32, t.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 ibm = &cluster.geometry_to_bone;
inverse_bind_matrices.push(ufbx_matrix_to_mat4(ibm));
}
}
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(|tex| {
texture_id_to_name
.get(&(tex.element.typed_id as usize))
.cloned()
})
}
fn convert_material(mat: &ufbx::Material, texture_id_to_name: &HashMap<usize, String>) -> Material {
let pbr_color = mat.pbr.base_color.value_vec4;
let diffuse_color = mat.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(&mat.pbr.base_color, texture_id_to_name)
.or_else(|| get_texture_name(&mat.fbx.diffuse_color, texture_id_to_name));
let normal_texture = get_texture_name(&mat.pbr.normal_map, texture_id_to_name)
.or_else(|| get_texture_name(&mat.fbx.normal_map, texture_id_to_name))
.or_else(|| get_texture_name(&mat.fbx.bump, texture_id_to_name));
let emissive_texture = get_texture_name(&mat.pbr.emission_color, texture_id_to_name)
.or_else(|| get_texture_name(&mat.pbr.emission_factor, texture_id_to_name))
.or_else(|| get_texture_name(&mat.fbx.emission_color, texture_id_to_name));
let metallic_roughness_texture = get_texture_name(&mat.pbr.roughness, texture_id_to_name)
.or_else(|| get_texture_name(&mat.pbr.metalness, texture_id_to_name));
let occlusion_texture = get_texture_name(&mat.pbr.ambient_occlusion, texture_id_to_name);
let pbr_emissive = mat.pbr.emission_color.value_vec4;
let fbx_emissive = mat.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 = mat.pbr.metalness.value_vec4.x as f32;
let roughness = if mat.pbr.roughness.value_vec4.x > 0.0 {
mat.pbr.roughness.value_vec4.x as f32
} else {
let shininess = mat.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();
tracing::trace!(
"Converting {} animation stacks from FBX",
scene.anim_stacks.len()
);
for anim_stack in &scene.anim_stacks {
let stack_duration = (anim_stack.time_end - anim_stack.time_begin) as f32;
tracing::trace!(
" Animation stack '{}': time_begin={}, time_end={}, duration={}s",
anim_stack.element.name,
anim_stack.time_begin,
anim_stack.time_end,
stack_duration
);
let bake_opts = ufbx::BakeOpts {
resample_rate: 30.0,
..Default::default()
};
let baked = ufbx::bake_anim(scene, &anim_stack.anim, bake_opts)
.map_err(|e| FbxError(format!("{:?}", e)))?;
tracing::trace!(
" Baked animation '{}': {} nodes",
anim_stack.element.name,
baked.nodes.len()
);
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(|n| n.element.typed_id == baked_node.typed_id)
.map(|n| n.element.name.to_string());
if !baked_node.translation_keys.is_empty() {
let times: Vec<f32> = baked_node
.translation_keys
.iter()
.map(|k| k.time as f32)
.collect();
let values: Vec<Vec3> = baked_node
.translation_keys
.iter()
.map(|k| vec3(k.value.x as f32, k.value.y as f32, k.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(|k| k.time as f32)
.collect();
let values: Vec<Quat> = baked_node
.rotation_keys
.iter()
.map(|k| {
Quat::new(
k.value.w as f32,
k.value.x as f32,
k.value.y as f32,
k.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(|k| k.time as f32)
.collect();
let values: Vec<Vec3> = baked_node
.scale_keys
.iter()
.map(|k| vec3(k.value.x as f32, k.value.y as f32, k.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
};
tracing::info!(
" Created {} channels, baked_max_time: {}s, stack_duration: {}s, final: {}s",
channels.len(),
max_time,
stack_duration,
final_duration
);
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(|n| n.parent.is_none() || n.parent.as_ref().map(|p| p.is_root).unwrap_or(false))
.filter(|n| !n.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,
}]
}
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(mat) = mesh.materials.first()
&& let Some(&mat_index) = material_id_to_index.get(&(mat.element.typed_id as usize))
&& mat_index < materials.len()
{
components.material = Some(materials[mat_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(m: &ufbx::Matrix) -> Mat4 {
Mat4::new(
m.m00 as f32,
m.m01 as f32,
m.m02 as f32,
m.m03 as f32,
m.m10 as f32,
m.m11 as f32,
m.m12 as f32,
m.m13 as f32,
m.m20 as f32,
m.m21 as f32,
m.m22 as f32,
m.m23 as f32,
0.0,
0.0,
0.0,
1.0,
)
}