use crate::{Actor, ActorBehavior, Message, Port};
use anyhow::{Error, Result};
use fbxcel::low::v7400::AttributeValue;
use fbxcel::pull_parser::v7400::attribute::loaders::DirectLoader;
use reflow_actor::{message::EncodableValue, ActorContext};
use reflow_actor_macro::actor;
use serde_json::{json, Value};
use std::collections::HashMap;
use std::io::{BufReader, Cursor};
#[actor(
FbxImportActor,
inports::<10>(file_data),
outports::<1>(mesh, skeleton, inverse_bind_matrices, clip, skin, skin_descriptor, metadata, error),
state(MemoryState)
)]
pub async fn fbx_import_actor(ctx: ActorContext) -> Result<HashMap<String, Message>, Error> {
let payload = ctx.get_payload();
let _config = ctx.get_config_hashmap();
let data = match payload.get("file_data") {
Some(Message::Bytes(b)) => b.clone(),
_ => return Ok(error_output("Expected Bytes on file_data port")),
};
match import_fbx(&data) {
Ok(out) => Ok(out),
Err(e) => Ok(error_output(&format!("FBX import failed: {}", e))),
}
}
fn error_output(msg: &str) -> HashMap<String, Message> {
let mut out = HashMap::new();
out.insert("error".to_string(), Message::Error(msg.to_string().into()));
out
}
fn import_fbx(data: &[u8]) -> Result<HashMap<String, Message>> {
use fbxcel::pull_parser::any::AnyParser;
let reader = BufReader::new(Cursor::new(data));
let parser = AnyParser::from_seekable_reader(reader)
.map_err(|e| anyhow::anyhow!("FBX parse error: {:?}", e))?;
let mut parser = match parser {
AnyParser::V7400(p) => p,
_ => return Err(anyhow::anyhow!("Unsupported FBX version (need v7400+)")),
};
let tree = collect_fbx_tree(&mut parser)?;
let mut out = HashMap::new();
let geom = extract_geometry(&tree)?;
let textures = extract_embedded_textures(&tree);
let diffuse_data = textures
.iter()
.find(|(name, _)| name.contains("diffuse") || name.contains("Diffuse"))
.or_else(|| textures.first())
.map(|(_, data)| data.clone());
let has_uvs = !geom.uvs.is_empty();
let has_texture = diffuse_data.is_some() && has_uvs;
let (mesh_bytes, tri_to_control) = build_mesh_bytes_colored(&geom, has_texture);
let mesh_stride = if has_texture { 32 } else { 24 };
out.insert("mesh".to_string(), Message::bytes(mesh_bytes));
if let Some(tex_data) = &diffuse_data {
out.insert("texture".to_string(), Message::bytes(tex_data.clone()));
}
let (skeleton, bone_names, pre_rotations) = extract_skeleton(&tree)?;
out.insert(
"skeleton".to_string(),
Message::object(EncodableValue::from(skeleton.clone())),
);
let clip = extract_animation(&tree, &bone_names, &pre_rotations)?;
out.insert(
"clip".to_string(),
Message::object(EncodableValue::from(clip)),
);
let control_vert_count = geom.vertices.len() / 3;
let (cp_skin_bytes, skin_desc) =
extract_skin_weights(&tree, control_vert_count, bone_names.len())?;
let max_influences = 4;
let entry_size = 6; let weights_per_vert = max_influences * entry_size;
let tri_vert_count = tri_to_control.len();
let mut skin_bytes = Vec::with_capacity(tri_vert_count * weights_per_vert);
for &cp_idx in &tri_to_control {
let src_off = cp_idx * weights_per_vert;
if src_off + weights_per_vert <= cp_skin_bytes.len() {
skin_bytes.extend_from_slice(&cp_skin_bytes[src_off..src_off + weights_per_vert]);
} else {
for j in 0..max_influences {
skin_bytes.extend_from_slice(&0u16.to_le_bytes());
let w: f32 = if j == 0 { 1.0 } else { 0.0 };
skin_bytes.extend_from_slice(&w.to_le_bytes());
}
}
}
let skin_desc = json!({
"vertexCount": tri_vert_count,
"maxInfluences": max_influences,
"boneCount": bone_names.len(),
});
out.insert("skin".to_string(), Message::bytes(skin_bytes));
out.insert(
"skin_descriptor".to_string(),
Message::object(EncodableValue::from(skin_desc)),
);
let ibm_bytes = extract_inverse_bind_matrices(&tree, bone_names.len())?;
out.insert(
"inverse_bind_matrices".to_string(),
Message::bytes(ibm_bytes),
);
let tri_vert_count = tri_to_control.len();
out.insert(
"metadata".to_string(),
Message::object(EncodableValue::from(json!({
"format": "fbx",
"vertices": geom.vertices.len() / 3,
"triangleVertices": tri_vert_count,
"bones": bone_names.len(),
"boneNames": bone_names,
"stride": mesh_stride,
"hasTexture": has_texture,
}))),
);
Ok(out)
}
#[derive(Debug, Default)]
struct FbxNode {
name: String,
attributes: Vec<AttributeValue>,
children: Vec<FbxNode>,
}
impl FbxNode {
fn child(&self, name: &str) -> Option<&FbxNode> {
self.children.iter().find(|c| c.name == name)
}
fn children_named(&self, name: &str) -> Vec<&FbxNode> {
self.children.iter().filter(|c| c.name == name).collect()
}
fn attr_str(&self, idx: usize) -> Option<&str> {
match self.attributes.get(idx) {
Some(AttributeValue::String(s)) => Some(s.as_str()),
_ => None,
}
}
fn attr_i64(&self, idx: usize) -> Option<i64> {
match self.attributes.get(idx) {
Some(AttributeValue::I64(v)) => Some(*v),
Some(AttributeValue::I32(v)) => Some(*v as i64),
_ => None,
}
}
fn attr_f64_arr(&self, idx: usize) -> Option<&[f64]> {
match self.attributes.get(idx) {
Some(AttributeValue::ArrF64(v)) => Some(v),
_ => None,
}
}
fn attr_i32_arr(&self, idx: usize) -> Option<&[i32]> {
match self.attributes.get(idx) {
Some(AttributeValue::ArrI32(v)) => Some(v),
_ => None,
}
}
fn attr_i64_arr(&self, idx: usize) -> Option<&[i64]> {
match self.attributes.get(idx) {
Some(AttributeValue::ArrI64(v)) => Some(v),
_ => None,
}
}
}
fn collect_fbx_tree<R: std::io::Read + std::io::Seek>(
parser: &mut fbxcel::pull_parser::v7400::Parser<R>,
) -> Result<FbxNode> {
use fbxcel::pull_parser::v7400::Event;
let mut root = FbxNode {
name: "root".into(),
..Default::default()
};
let mut stack: Vec<FbxNode> = vec![];
loop {
match parser
.next_event()
.map_err(|e| anyhow::anyhow!("FBX parse: {:?}", e))?
{
Event::StartNode(start) => {
let name = start.name().to_string();
let mut attrs_reader = start.attributes();
let mut attrs = Vec::new();
while let Some(attr) = attrs_reader
.load_next(DirectLoader)
.map_err(|e| anyhow::anyhow!("FBX attr: {:?}", e))?
{
attrs.push(attr);
}
stack.push(FbxNode {
name,
attributes: attrs,
children: vec![],
});
}
Event::EndNode => {
if let Some(node) = stack.pop() {
if let Some(parent) = stack.last_mut() {
parent.children.push(node);
} else {
root.children.push(node);
}
}
}
Event::EndFbx(_) => break,
}
}
Ok(root)
}
struct GeometryData {
vertices: Vec<f64>,
normals: Vec<f64>,
indices: Vec<i32>,
uvs: Vec<f64>, uv_indices: Vec<i32>, mat_indices: Vec<i32>, }
fn extract_geometry(tree: &FbxNode) -> Result<GeometryData> {
let objects = tree
.child("Objects")
.ok_or_else(|| anyhow::anyhow!("No Objects node"))?;
for child in &objects.children {
if child.name == "Geometry" {
let class = child.attr_str(2).unwrap_or("");
if class == "Mesh" {
let verts = child
.child("Vertices")
.and_then(|n| n.attr_f64_arr(0))
.unwrap_or(&[]);
let indices = child
.child("PolygonVertexIndex")
.and_then(|n| n.attr_i32_arr(0))
.unwrap_or(&[]);
let normals = child
.child("LayerElementNormal")
.and_then(|n| n.child("Normals"))
.and_then(|n| n.attr_f64_arr(0))
.unwrap_or(&[]);
let uv_node = child.child("LayerElementUV");
let uvs = uv_node
.and_then(|n| n.child("UV"))
.and_then(|n| n.attr_f64_arr(0))
.unwrap_or(&[]);
let uv_indices = uv_node
.and_then(|n| n.child("UVIndex"))
.and_then(|n| n.attr_i32_arr(0))
.unwrap_or(&[]);
let mat_indices = child
.child("LayerElementMaterial")
.and_then(|n| n.child("Materials"))
.and_then(|n| n.attr_i32_arr(0))
.unwrap_or(&[]);
return Ok(GeometryData {
vertices: verts.to_vec(),
normals: normals.to_vec(),
indices: indices.to_vec(),
uvs: uvs.to_vec(),
uv_indices: uv_indices.to_vec(),
mat_indices: mat_indices.to_vec(),
});
}
}
}
Err(anyhow::anyhow!("No Geometry/Mesh found in FBX"))
}
fn build_mesh_bytes_colored(geom: &GeometryData, has_uv: bool) -> (Vec<u8>, Vec<usize>) {
let vertices = &geom.vertices;
let normals = &geom.normals;
let indices = &geom.indices;
let stride = if has_uv { 32 } else { 24 };
let mut poly = Vec::new();
let normals_by_polygon_vertex = normals.len() != vertices.len() && !normals.is_empty();
let mut poly_vert_idx = 0usize;
struct TriVert {
pos_idx: usize,
normal_idx: usize,
pv_idx: usize, }
let mut tri_verts: Vec<TriVert> = Vec::new();
for &idx in indices {
let actual_idx = if idx < 0 { -(idx + 1) } else { idx };
poly.push((actual_idx as usize, poly_vert_idx));
poly_vert_idx += 1;
if idx < 0 {
for i in 1..poly.len() - 1 {
tri_verts.push(TriVert {
pos_idx: poly[0].0,
normal_idx: poly[0].1,
pv_idx: poly[0].1,
});
tri_verts.push(TriVert {
pos_idx: poly[i].0,
normal_idx: poly[i].1,
pv_idx: poly[i].1,
});
tri_verts.push(TriVert {
pos_idx: poly[i + 1].0,
normal_idx: poly[i + 1].1,
pv_idx: poly[i + 1].1,
});
}
poly.clear();
}
}
let vert_count = vertices.len() / 3;
let normal_count = normals.len() / 3;
let uv_count = geom.uvs.len() / 2;
let mut bytes = Vec::with_capacity(tri_verts.len() * stride);
let mut tri_to_control: Vec<usize> = Vec::with_capacity(tri_verts.len());
for tv in &tri_verts {
if tv.pos_idx < vert_count {
tri_to_control.push(tv.pos_idx);
let px = vertices[tv.pos_idx * 3] as f32;
let py = vertices[tv.pos_idx * 3 + 1] as f32;
let pz = vertices[tv.pos_idx * 3 + 2] as f32;
bytes.extend_from_slice(&px.to_le_bytes());
bytes.extend_from_slice(&py.to_le_bytes());
bytes.extend_from_slice(&pz.to_le_bytes());
let ni = if normals_by_polygon_vertex {
tv.normal_idx
} else {
tv.pos_idx
};
if ni < normal_count {
let nx = normals[ni * 3] as f32;
let ny = normals[ni * 3 + 1] as f32;
let nz = normals[ni * 3 + 2] as f32;
bytes.extend_from_slice(&nx.to_le_bytes());
bytes.extend_from_slice(&ny.to_le_bytes());
bytes.extend_from_slice(&nz.to_le_bytes());
} else {
bytes.extend_from_slice(&[0u8; 12]);
}
if has_uv {
let uv_idx = if !geom.uv_indices.is_empty() {
geom.uv_indices.get(tv.pv_idx).copied().unwrap_or(0) as usize
} else {
tv.pv_idx
};
if uv_idx < uv_count {
let u = geom.uvs[uv_idx * 2] as f32;
let v = geom.uvs[uv_idx * 2 + 1] as f32;
bytes.extend_from_slice(&u.to_le_bytes());
bytes.extend_from_slice(&v.to_le_bytes());
} else {
bytes.extend_from_slice(&[0u8; 8]);
}
}
}
}
(bytes, tri_to_control)
}
fn extract_embedded_textures(tree: &FbxNode) -> Vec<(String, Vec<u8>)> {
let mut textures = Vec::new();
let objects = match tree.child("Objects") {
Some(o) => o,
None => return textures,
};
for child in &objects.children {
if child.name == "Video" {
let name = child
.attr_str(1)
.unwrap_or("")
.split('\0')
.next()
.unwrap_or("")
.to_string();
if let Some(content) = child.child("Content") {
if let Some(AttributeValue::Binary(data)) = content.attributes.first() {
if !data.is_empty() {
textures.push((name, data.clone()));
}
}
}
}
}
textures
}
fn sample_texture_at_uv(img: &image::RgbaImage, u: f32, v: f32) -> [f32; 3] {
let w = img.width() as f32;
let h = img.height() as f32;
let px = ((u * w) as u32).min(img.width().saturating_sub(1));
let py = (((1.0 - v) * h) as u32).min(img.height().saturating_sub(1));
let pixel = img.get_pixel(px, py);
[
pixel[0] as f32 / 255.0,
pixel[1] as f32 / 255.0,
pixel[2] as f32 / 255.0,
]
}
fn extract_skeleton(tree: &FbxNode) -> Result<(Value, Vec<String>, Vec<[f64; 3]>)> {
let objects = tree
.child("Objects")
.ok_or_else(|| anyhow::anyhow!("No Objects node"))?;
let connections = tree
.child("Connections")
.ok_or_else(|| anyhow::anyhow!("No Connections node"))?;
struct BoneInfo {
id: i64,
name: String,
lcl_translation: [f64; 3],
pre_rotation: [f64; 3], lcl_rotation: [f64; 3], lcl_scaling: [f64; 3],
}
let mut bones: Vec<BoneInfo> = Vec::new();
let mut bone_ids: HashMap<i64, usize> = HashMap::new();
for child in &objects.children {
if child.name == "Model" {
let class = child.attr_str(2).unwrap_or("");
if class == "LimbNode" || class == "Null" || class == "Root" {
let id = child.attr_i64(0).unwrap_or(0);
let name = child
.attr_str(1)
.unwrap_or("bone")
.split('\0')
.next()
.unwrap_or("bone")
.to_string();
let name = name.strip_prefix("Model::").unwrap_or(&name).to_string();
let mut lcl_t = [0.0f64; 3];
let mut pre_r = [0.0f64; 3];
let mut lcl_r = [0.0f64; 3];
let mut lcl_s = [1.0f64, 1.0, 1.0];
if let Some(props) = child.child("Properties70") {
for p in &props.children {
if p.name == "P" {
let prop_name = p.attr_str(0).unwrap_or("");
match prop_name {
"Lcl Translation" => lcl_t = extract_p_xyz(p),
"PreRotation" => pre_r = extract_p_xyz(p),
"Lcl Rotation" => lcl_r = extract_p_xyz(p),
"Lcl Scaling" => lcl_s = extract_p_xyz(p),
_ => {}
}
}
}
}
bone_ids.insert(id, bones.len());
bones.push(BoneInfo {
id,
name,
lcl_translation: lcl_t,
pre_rotation: pre_r,
lcl_rotation: lcl_r,
lcl_scaling: lcl_s,
});
}
}
}
let mut parent_map: HashMap<usize, usize> = HashMap::new();
for conn in &connections.children {
if conn.name == "C" {
let conn_type = conn.attr_str(0).unwrap_or("");
if conn_type == "OO" {
let child_id = conn.attr_i64(1).unwrap_or(0);
let parent_id = conn.attr_i64(2).unwrap_or(0);
if let (Some(&ci), Some(&pi)) = (bone_ids.get(&child_id), bone_ids.get(&parent_id))
{
parent_map.insert(ci, pi);
}
}
}
}
let bone_names: Vec<String> = bones.iter().map(|b| b.name.clone()).collect();
let pre_rotations: Vec<[f64; 3]> = bones.iter().map(|b| b.pre_rotation).collect();
let mut bone_array = Vec::new();
for (i, bone) in bones.iter().enumerate() {
let parent = parent_map.get(&i).map(|&p| p as i64).unwrap_or(-1);
let local_mat = trs_pre_rot_mat4(
bone.lcl_translation,
bone.pre_rotation,
bone.lcl_rotation,
bone.lcl_scaling,
);
let mat_json: Vec<Value> = local_mat.iter().map(|&v| json!(v)).collect();
bone_array.push(json!({
"name": bone.name,
"parent": parent,
"index": i,
"localBindTransform": mat_json,
}));
}
let skeleton = json!({
"bones": bone_array,
"boneCount": bones.len(),
});
Ok((skeleton, bone_names, pre_rotations))
}
fn extract_p_xyz(p: &FbxNode) -> [f64; 3] {
let x = match p.attributes.get(4) {
Some(AttributeValue::F64(v)) => *v,
Some(AttributeValue::F32(v)) => *v as f64,
Some(AttributeValue::I32(v)) => *v as f64,
_ => 0.0,
};
let y = match p.attributes.get(5) {
Some(AttributeValue::F64(v)) => *v,
Some(AttributeValue::F32(v)) => *v as f64,
Some(AttributeValue::I32(v)) => *v as f64,
_ => 0.0,
};
let z = match p.attributes.get(6) {
Some(AttributeValue::F64(v)) => *v,
Some(AttributeValue::F32(v)) => *v as f64,
Some(AttributeValue::I32(v)) => *v as f64,
_ => 0.0,
};
[x, y, z]
}
fn trs_pre_rot_mat4(t: [f64; 3], pre_deg: [f64; 3], lcl_deg: [f64; 3], s: [f64; 3]) -> [f64; 16] {
let pre_q = euler_xyz_to_quat(
pre_deg[0].to_radians(),
pre_deg[1].to_radians(),
pre_deg[2].to_radians(),
);
let lcl_q = euler_xyz_to_quat(
lcl_deg[0].to_radians(),
lcl_deg[1].to_radians(),
lcl_deg[2].to_radians(),
);
let combined_q = quat_mul_f64(pre_q, lcl_q);
let [qx, qy, qz, qw] = combined_q;
let xx = qx * qx;
let yy = qy * qy;
let zz = qz * qz;
let xy = qx * qy;
let xz = qx * qz;
let yz = qy * qz;
let wx = qw * qx;
let wy = qw * qy;
let wz = qw * qz;
let r00 = 1.0 - 2.0 * (yy + zz);
let r01 = 2.0 * (xy - wz);
let r02 = 2.0 * (xz + wy);
let r10 = 2.0 * (xy + wz);
let r11 = 1.0 - 2.0 * (xx + zz);
let r12 = 2.0 * (yz - wx);
let r20 = 2.0 * (xz - wy);
let r21 = 2.0 * (yz + wx);
let r22 = 1.0 - 2.0 * (xx + yy);
[
r00 * s[0],
r10 * s[0],
r20 * s[0],
0.0,
r01 * s[1],
r11 * s[1],
r21 * s[1],
0.0,
r02 * s[2],
r12 * s[2],
r22 * s[2],
0.0,
t[0],
t[1],
t[2],
1.0,
]
}
fn trs_to_column_major_mat4(t: [f64; 3], r_deg: [f64; 3], s: [f64; 3]) -> [f64; 16] {
let rx = r_deg[0].to_radians();
let ry = r_deg[1].to_radians();
let rz = r_deg[2].to_radians();
let (sx, cx) = (rx.sin(), rx.cos());
let (sy, cy) = (ry.sin(), ry.cos());
let (sz, cz) = (rz.sin(), rz.cos());
let r00 = cy * cz;
let r01 = cz * sx * sy - cx * sz;
let r02 = cx * cz * sy + sx * sz;
let r10 = cy * sz;
let r11 = cx * cz + sx * sy * sz;
let r12 = cx * sy * sz - cz * sx;
let r20 = -sy;
let r21 = cy * sx;
let r22 = cx * cy;
[
r00 * s[0],
r10 * s[0],
r20 * s[0],
0.0, r01 * s[1],
r11 * s[1],
r21 * s[1],
0.0, r02 * s[2],
r12 * s[2],
r22 * s[2],
0.0, t[0],
t[1],
t[2],
1.0, ]
}
const FBX_TIME_UNIT: f64 = 46186158000.0;
fn extract_animation(
tree: &FbxNode,
bone_names: &[String],
pre_rotations: &[[f64; 3]],
) -> Result<Value> {
let objects = tree
.child("Objects")
.ok_or_else(|| anyhow::anyhow!("No Objects node"))?;
let connections = tree
.child("Connections")
.ok_or_else(|| anyhow::anyhow!("No Connections node"))?;
let mut conn_oo: Vec<(i64, i64)> = Vec::new(); let mut conn_op: Vec<(i64, i64, String)> = Vec::new(); for conn in &connections.children {
if conn.name == "C" {
let ct = conn.attr_str(0).unwrap_or("");
let child_id = conn.attr_i64(1).unwrap_or(0);
let parent_id = conn.attr_i64(2).unwrap_or(0);
if ct == "OO" {
conn_oo.push((child_id, parent_id));
} else if ct == "OP" {
let prop = conn.attr_str(3).unwrap_or("").to_string();
conn_op.push((child_id, parent_id, prop));
}
}
}
let mut curve_node_prop: HashMap<i64, String> = HashMap::new();
let mut curves: HashMap<i64, (Vec<f64>, Vec<f32>)> = HashMap::new();
let mut model_id_to_bone: HashMap<i64, String> = HashMap::new();
for child in &objects.children {
match child.name.as_str() {
"AnimationCurveNode" => {
let id = child.attr_i64(0).unwrap_or(0);
let name = child
.attr_str(1)
.unwrap_or("")
.split('\0')
.next()
.unwrap_or("")
.to_string();
let prop = name
.strip_prefix("AnimCurveNode::")
.unwrap_or(&name)
.to_string();
curve_node_prop.insert(id, prop);
}
"AnimationCurve" => {
let id = child.attr_i64(0).unwrap_or(0);
let times: Vec<f64> = child
.child("KeyTime")
.and_then(|n| n.attr_i64_arr(0))
.map(|arr| arr.iter().map(|&t| t as f64 / FBX_TIME_UNIT).collect())
.unwrap_or_default();
let values: Vec<f32> = child
.child("KeyValueFloat")
.and_then(|n| match n.attributes.first() {
Some(AttributeValue::ArrF32(arr)) => Some(arr.clone()),
Some(AttributeValue::ArrF64(arr)) => {
Some(arr.iter().map(|&v| v as f32).collect())
}
_ => None,
})
.unwrap_or_default();
if !times.is_empty() && !values.is_empty() {
curves.insert(id, (times, values));
}
}
"Model" => {
let id = child.attr_i64(0).unwrap_or(0);
let name = child
.attr_str(1)
.unwrap_or("")
.split('\0')
.next()
.unwrap_or("")
.to_string();
let name = name.strip_prefix("Model::").unwrap_or(&name).to_string();
model_id_to_bone.insert(id, name);
}
_ => {}
}
}
let mut curve_to_curvenode: HashMap<i64, i64> = HashMap::new();
let mut curvenode_to_model: HashMap<i64, i64> = HashMap::new();
let mut curve_channel: HashMap<i64, String> = HashMap::new();
for &(child_id, parent_id) in &conn_oo {
if curves.contains_key(&child_id) && curve_node_prop.contains_key(&parent_id) {
curve_to_curvenode.insert(child_id, parent_id);
}
if curve_node_prop.contains_key(&child_id) && model_id_to_bone.contains_key(&parent_id) {
curvenode_to_model.insert(child_id, parent_id);
}
}
for (child_id, parent_id, prop) in &conn_op {
if curves.contains_key(child_id) && curve_node_prop.contains_key(parent_id) {
curve_to_curvenode.insert(*child_id, *parent_id);
curve_channel.insert(*child_id, prop.clone());
}
if curve_node_prop.contains_key(child_id) && model_id_to_bone.contains_key(parent_id) {
curvenode_to_model.insert(*child_id, *parent_id);
}
}
struct ScalarCurve {
bone_idx: usize,
property: String, component: String, times: Vec<f64>,
values: Vec<f32>,
}
let mut scalar_curves: Vec<ScalarCurve> = Vec::new();
let mut max_time: f64 = 0.0;
for (&curve_id, (times, values)) in &curves {
let cn_id = match curve_to_curvenode.get(&curve_id) {
Some(id) => *id,
None => continue,
};
let model_id = match curvenode_to_model.get(&cn_id) {
Some(id) => *id,
None => continue,
};
let bone_name = match model_id_to_bone.get(&model_id) {
Some(n) => n,
None => continue,
};
let bone_idx = match bone_names.iter().position(|n| n == bone_name) {
Some(i) => i,
None => continue,
};
let property = curve_node_prop.get(&cn_id).cloned().unwrap_or_default();
let component = curve_channel.get(&curve_id).cloned().unwrap_or_default();
if let Some(&t) = times.last() {
if t > max_time {
max_time = t;
}
}
scalar_curves.push(ScalarCurve {
bone_idx,
property,
component,
times: times.clone(),
values: values.clone(),
});
}
let mut grouped: HashMap<(usize, String), HashMap<String, (Vec<f64>, Vec<f32>)>> =
HashMap::new();
for sc in &scalar_curves {
grouped
.entry((sc.bone_idx, sc.property.clone()))
.or_default()
.insert(sc.component.clone(), (sc.times.clone(), sc.values.clone()));
}
let mut channels: Vec<Value> = Vec::new();
for ((bone_idx, prop), components) in &grouped {
let property = match prop.as_str() {
"T" => "position",
"R" => "rotation",
"S" => "scale",
_ => continue,
};
let x_curve = components.get("d|X");
let y_curve = components.get("d|Y");
let z_curve = components.get("d|Z");
let ref_times = [x_curve, y_curve, z_curve]
.iter()
.filter_map(|c| c.map(|(t, _)| t))
.max_by_key(|t| t.len())
.cloned()
.unwrap_or_default();
if ref_times.is_empty() {
continue;
}
let times_json: Vec<Value> = ref_times.iter().map(|&t| json!(t)).collect();
let values_json: Vec<Value> = if property == "rotation" {
let pre_r = if *bone_idx < pre_rotations.len() {
pre_rotations[*bone_idx]
} else {
[0.0, 0.0, 0.0]
};
let pre_q = euler_xyz_to_quat(
pre_r[0].to_radians(),
pre_r[1].to_radians(),
pre_r[2].to_radians(),
);
ref_times
.iter()
.map(|&t| {
let rx = sample_curve(x_curve, t).to_radians() as f64;
let ry = sample_curve(y_curve, t).to_radians() as f64;
let rz = sample_curve(z_curve, t).to_radians() as f64;
let anim_q = euler_xyz_to_quat(rx, ry, rz);
let q = quat_mul_f64(pre_q, anim_q);
json!([q[0], q[1], q[2], q[3]])
})
.collect()
} else {
ref_times
.iter()
.map(|&t| {
let x = sample_curve(x_curve, t);
let y = sample_curve(y_curve, t);
let z = sample_curve(z_curve, t);
json!([x, y, z])
})
.collect()
};
channels.push(json!({
"boneIndex": bone_idx,
"property": property,
"interpolation": "linear",
"times": times_json,
"values": values_json,
}));
}
let clip = json!({
"name": "mixamo_clip",
"duration": max_time,
"channels": channels,
"boneCount": bone_names.len(),
});
Ok(clip)
}
fn sample_curve(curve: Option<&(Vec<f64>, Vec<f32>)>, t: f64) -> f32 {
let (times, values) = match curve {
Some(c) if !c.0.is_empty() => (&c.0, &c.1),
_ => return 0.0,
};
if times.len() == 1 || t <= times[0] {
return values[0];
}
if t >= *times.last().unwrap() {
return *values.last().unwrap();
}
let mut i = 0;
while i + 1 < times.len() && times[i + 1] < t {
i += 1;
}
let dt = times[i + 1] - times[i];
if dt <= 0.0 {
return values[i];
}
let frac = ((t - times[i]) / dt) as f32;
values[i] * (1.0 - frac) + values[i + 1] * frac
}
fn quat_mul_f64(a: [f64; 4], b: [f64; 4]) -> [f64; 4] {
[
a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2],
]
}
fn euler_xyz_to_quat(rx: f64, ry: f64, rz: f64) -> [f64; 4] {
let (sx, cx) = (rx * 0.5).sin_cos();
let (sy, cy) = (ry * 0.5).sin_cos();
let (sz, cz) = (rz * 0.5).sin_cos();
let w = cx * cy * cz + sx * sy * sz;
let x = sx * cy * cz - cx * sy * sz;
let y = cx * sy * cz + sx * cy * sz;
let z = cx * cy * sz - sx * sy * cz;
[x, y, z, w]
}
fn extract_skin_weights(
tree: &FbxNode,
vertex_count: usize,
bone_count: usize,
) -> Result<(Vec<u8>, Value)> {
let max_influences = 4;
let objects = tree.child("Objects");
let connections = tree.child("Connections");
let mut vert_weights: Vec<Vec<(u16, f32)>> = vec![Vec::new(); vertex_count];
if let (Some(objects), Some(connections)) = (objects, connections) {
let mut conn_oo: Vec<(i64, i64)> = Vec::new();
for conn in &connections.children {
if conn.name == "C" && conn.attr_str(0) == Some("OO") {
let child_id = conn.attr_i64(1).unwrap_or(0);
let parent_id = conn.attr_i64(2).unwrap_or(0);
conn_oo.push((child_id, parent_id));
}
}
let mut cluster_to_bone: HashMap<i64, usize> = HashMap::new();
let mut model_name_to_idx: HashMap<String, usize> = HashMap::new();
let mut model_ids: HashMap<i64, String> = HashMap::new();
let mut bone_idx = 0usize;
for child in &objects.children {
if child.name == "Model" {
let class = child.attr_str(2).unwrap_or("");
if class == "LimbNode" || class == "Null" || class == "Root" {
let id = child.attr_i64(0).unwrap_or(0);
let name = child
.attr_str(1)
.unwrap_or("bone")
.split('\0')
.next()
.unwrap_or("bone")
.to_string();
let name = name.strip_prefix("Model::").unwrap_or(&name).to_string();
model_ids.insert(id, name.clone());
model_name_to_idx.insert(name, bone_idx);
bone_idx += 1;
}
}
}
let cluster_ids: Vec<i64> = objects
.children
.iter()
.filter(|c| c.name == "Deformer" && c.attr_str(2) == Some("Cluster"))
.filter_map(|c| c.attr_i64(0))
.collect();
let cluster_id_set: std::collections::HashSet<i64> = cluster_ids.iter().copied().collect();
for &(child_id, parent_id) in &conn_oo {
if let Some(bone_name) = model_ids.get(&child_id) {
if cluster_id_set.contains(&parent_id) {
if let Some(&bi) = model_name_to_idx.get(bone_name) {
cluster_to_bone.insert(parent_id, bi);
}
}
}
}
for child in &objects.children {
if child.name == "Deformer" && child.attr_str(2) == Some("Cluster") {
let cluster_id = child.attr_i64(0).unwrap_or(0);
let bi = match cluster_to_bone.get(&cluster_id) {
Some(&b) => b,
None => continue,
};
let vert_indices = child
.child("Indexes")
.and_then(|n| n.attr_i32_arr(0))
.unwrap_or(&[]);
let weights = child
.child("Weights")
.and_then(|n| n.attr_f64_arr(0))
.unwrap_or(&[]);
for (i, &vi) in vert_indices.iter().enumerate() {
let vi = vi as usize;
if vi < vertex_count {
let w = weights.get(i).copied().unwrap_or(0.0) as f32;
if w > 0.0 {
vert_weights[vi].push((bi as u16, w));
}
}
}
}
}
}
let mut skin_bytes = Vec::with_capacity(vertex_count * max_influences * 6);
for vw in &mut vert_weights {
let mut merged: HashMap<u16, f32> = HashMap::new();
for &(bi, w) in vw.iter() {
*merged.entry(bi).or_insert(0.0) += w;
}
*vw = merged.into_iter().collect();
vw.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
vw.truncate(max_influences);
let total: f32 = vw.iter().map(|(_, w)| w).sum();
if total > 0.0 {
for (_, w) in vw.iter_mut() {
*w /= total;
}
}
for j in 0..max_influences {
let (bone_idx, weight) = vw.get(j).copied().unwrap_or((0, 0.0));
skin_bytes.extend_from_slice(&bone_idx.to_le_bytes());
skin_bytes.extend_from_slice(&weight.to_le_bytes());
}
}
let desc = json!({
"vertexCount": vertex_count,
"maxInfluences": max_influences,
"boneCount": bone_count,
});
Ok((skin_bytes, desc))
}
fn extract_inverse_bind_matrices(tree: &FbxNode, bone_count: usize) -> Result<Vec<u8>> {
let objects = tree.child("Objects");
let connections = tree.child("Connections");
let identity: [f32; 16] = [
1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0,
];
let mut matrices: Vec<[f32; 16]> = vec![identity; bone_count];
if let (Some(objects), Some(connections)) = (objects, connections) {
let mut conn_oo: Vec<(i64, i64)> = Vec::new();
for conn in &connections.children {
if conn.name == "C" && conn.attr_str(0) == Some("OO") {
conn_oo.push((conn.attr_i64(1).unwrap_or(0), conn.attr_i64(2).unwrap_or(0)));
}
}
let mut model_ids: HashMap<i64, usize> = HashMap::new();
let mut bone_idx = 0usize;
for child in &objects.children {
if child.name == "Model" {
let class = child.attr_str(2).unwrap_or("");
if class == "LimbNode" || class == "Null" || class == "Root" {
let id = child.attr_i64(0).unwrap_or(0);
model_ids.insert(id, bone_idx);
bone_idx += 1;
}
}
}
let cluster_id_set: std::collections::HashSet<i64> = objects
.children
.iter()
.filter(|c| c.name == "Deformer" && c.attr_str(2) == Some("Cluster"))
.filter_map(|c| c.attr_i64(0))
.collect();
let mut cluster_to_bone: HashMap<i64, usize> = HashMap::new();
for &(child_id, parent_id) in &conn_oo {
if let Some(&bi) = model_ids.get(&child_id) {
if cluster_id_set.contains(&parent_id) {
cluster_to_bone.insert(parent_id, bi);
}
}
}
for child in &objects.children {
if child.name == "Deformer" && child.attr_str(2) == Some("Cluster") {
let cluster_id = child.attr_i64(0).unwrap_or(0);
let bi = match cluster_to_bone.get(&cluster_id) {
Some(&b) => b,
None => continue,
};
if let Some(tl) = child.child("TransformLink") {
if let Some(arr) = tl.attr_f64_arr(0) {
if arr.len() >= 16 {
let mut m = [0f32; 16];
for i in 0..16 {
m[i] = arr[i] as f32;
}
if let Some(inv) = invert_4x4(&m) {
matrices[bi] = inv;
}
}
}
}
}
}
}
let mut bytes = Vec::with_capacity(bone_count * 64);
for mat in &matrices {
for &v in mat {
bytes.extend_from_slice(&v.to_le_bytes());
}
}
Ok(bytes)
}
fn invert_4x4(m: &[f32; 16]) -> Option<[f32; 16]> {
let mut inv = [0f32; 16];
inv[0] = m[5] * m[10] * m[15] - m[5] * m[11] * m[14] - m[9] * m[6] * m[15]
+ m[9] * m[7] * m[14]
+ m[13] * m[6] * m[11]
- m[13] * m[7] * m[10];
inv[4] = -m[4] * m[10] * m[15] + m[4] * m[11] * m[14] + m[8] * m[6] * m[15]
- m[8] * m[7] * m[14]
- m[12] * m[6] * m[11]
+ m[12] * m[7] * m[10];
inv[8] = m[4] * m[9] * m[15] - m[4] * m[11] * m[13] - m[8] * m[5] * m[15]
+ m[8] * m[7] * m[13]
+ m[12] * m[5] * m[11]
- m[12] * m[7] * m[9];
inv[12] = -m[4] * m[9] * m[14] + m[4] * m[10] * m[13] + m[8] * m[5] * m[14]
- m[8] * m[6] * m[13]
- m[12] * m[5] * m[10]
+ m[12] * m[6] * m[9];
inv[1] = -m[1] * m[10] * m[15] + m[1] * m[11] * m[14] + m[9] * m[2] * m[15]
- m[9] * m[3] * m[14]
- m[13] * m[2] * m[11]
+ m[13] * m[3] * m[10];
inv[5] = m[0] * m[10] * m[15] - m[0] * m[11] * m[14] - m[8] * m[2] * m[15]
+ m[8] * m[3] * m[14]
+ m[12] * m[2] * m[11]
- m[12] * m[3] * m[10];
inv[9] = -m[0] * m[9] * m[15] + m[0] * m[11] * m[13] + m[8] * m[1] * m[15]
- m[8] * m[3] * m[13]
- m[12] * m[1] * m[11]
+ m[12] * m[3] * m[9];
inv[13] = m[0] * m[9] * m[14] - m[0] * m[10] * m[13] - m[8] * m[1] * m[14]
+ m[8] * m[2] * m[13]
+ m[12] * m[1] * m[10]
- m[12] * m[2] * m[9];
inv[2] = m[1] * m[6] * m[15] - m[1] * m[7] * m[14] - m[5] * m[2] * m[15]
+ m[5] * m[3] * m[14]
+ m[13] * m[2] * m[7]
- m[13] * m[3] * m[6];
inv[6] = -m[0] * m[6] * m[15] + m[0] * m[7] * m[14] + m[4] * m[2] * m[15]
- m[4] * m[3] * m[14]
- m[12] * m[2] * m[7]
+ m[12] * m[3] * m[6];
inv[10] = m[0] * m[5] * m[15] - m[0] * m[7] * m[13] - m[4] * m[1] * m[15]
+ m[4] * m[3] * m[13]
+ m[12] * m[1] * m[7]
- m[12] * m[3] * m[5];
inv[14] = -m[0] * m[5] * m[14] + m[0] * m[6] * m[13] + m[4] * m[1] * m[14]
- m[4] * m[2] * m[13]
- m[12] * m[1] * m[6]
+ m[12] * m[2] * m[5];
inv[3] = -m[1] * m[6] * m[11] + m[1] * m[7] * m[10] + m[5] * m[2] * m[11]
- m[5] * m[3] * m[10]
- m[9] * m[2] * m[7]
+ m[9] * m[3] * m[6];
inv[7] = m[0] * m[6] * m[11] - m[0] * m[7] * m[10] - m[4] * m[2] * m[11]
+ m[4] * m[3] * m[10]
+ m[8] * m[2] * m[7]
- m[8] * m[3] * m[6];
inv[11] = -m[0] * m[5] * m[11] + m[0] * m[7] * m[9] + m[4] * m[1] * m[11]
- m[4] * m[3] * m[9]
- m[8] * m[1] * m[7]
+ m[8] * m[3] * m[5];
inv[15] = m[0] * m[5] * m[10] - m[0] * m[6] * m[9] - m[4] * m[1] * m[10]
+ m[4] * m[2] * m[9]
+ m[8] * m[1] * m[6]
- m[8] * m[2] * m[5];
let det = m[0] * inv[0] + m[1] * inv[4] + m[2] * inv[8] + m[3] * inv[12];
if det.abs() < 1e-10 {
return None;
}
let inv_det = 1.0 / det;
for v in &mut inv {
*v *= inv_det;
}
Some(inv)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_mixamo_fbx() {
let data = std::fs::read("../../assets/leg_sweep.fbx")
.expect("Failed to read leg_sweep.fbx — run from crate root");
println!("FBX file size: {} bytes", data.len());
let result = import_fbx(&data);
match &result {
Ok(out) => {
assert!(out.contains_key("mesh"), "Should have mesh");
assert!(out.contains_key("skeleton"), "Should have skeleton");
assert!(out.contains_key("clip"), "Should have clip");
assert!(out.contains_key("metadata"), "Should have metadata");
if let Some(Message::Object(meta)) = out.get("metadata") {
let v: Value = meta.as_ref().clone().into();
println!("Metadata: {}", serde_json::to_string_pretty(&v).unwrap());
let verts = v["vertices"].as_u64().unwrap_or(0);
let bones = v["bones"].as_u64().unwrap_or(0);
assert!(verts > 0, "Should have vertices");
assert!(bones > 0, "Should have bones");
println!("Vertices: {}, Bones: {}", verts, bones);
if let Some(names) = v["boneNames"].as_array() {
println!("Bone hierarchy ({} bones):", names.len());
for n in names {
println!(" {}", n);
}
}
}
if let Some(Message::Bytes(mesh)) = out.get("mesh") {
let tri_verts = mesh.len() / 24;
println!(
"Mesh: {} bytes, {} triangle-vertices, {} triangles",
mesh.len(),
tri_verts,
tri_verts / 3
);
let mut min = [f32::MAX; 3];
let mut max = [f32::MIN; 3];
for i in 0..tri_verts {
let off = i * 24;
for j in 0..3 {
let v = f32::from_le_bytes([
mesh[off + j * 4],
mesh[off + j * 4 + 1],
mesh[off + j * 4 + 2],
mesh[off + j * 4 + 3],
]);
if v < min[j] {
min[j] = v;
}
if v > max[j] {
max[j] = v;
}
}
}
println!(
"Bounds: x=[{:.1}, {:.1}] y=[{:.1}, {:.1}] z=[{:.1}, {:.1}]",
min[0], max[0], min[1], max[1], min[2], max[2]
);
}
if let Some(Message::Object(clip)) = out.get("clip") {
let v: Value = clip.as_ref().clone().into();
let duration = v["duration"].as_f64().unwrap_or(0.0);
let channels = v["channels"].as_array().map(|a| a.len()).unwrap_or(0);
println!(
"Animation: duration={:.3}s, {} channels",
duration, channels
);
if let Some(chs) = v["channels"].as_array() {
for ch in chs.iter().take(8) {
let bi = ch["boneIndex"].as_u64().unwrap_or(0);
let prop = ch["property"].as_str().unwrap_or("?");
let kf_count = ch["times"].as_array().map(|a| a.len()).unwrap_or(0);
println!(" bone[{}] {}: {} keyframes", bi, prop, kf_count);
if let Some(vals) = ch["values"].as_array() {
if let Some(v0) = vals.first() {
println!(" first value: {}", v0);
}
}
}
if chs.len() > 8 {
println!(" ... and {} more channels", chs.len() - 8);
}
}
}
if let Some(Message::Object(sd)) = out.get("skin_descriptor") {
let v: Value = sd.as_ref().clone().into();
println!("Skin: {}", serde_json::to_string(&v).unwrap());
}
if let Some(Message::Bytes(skin)) = out.get("skin") {
let stride = 24; let total = skin.len() / stride;
println!("Skin weight samples ({} total):", total);
for &vi in &[0, 100, total / 4, total / 2, total * 3 / 4, total - 1] {
if vi >= total {
continue;
}
let off = vi * stride;
print!(" v[{}]: ", vi);
for j in 0..4 {
let w_off = off + j * 6;
let bi = u16::from_le_bytes([skin[w_off], skin[w_off + 1]]);
let w =
f32::from_le_bytes(skin[w_off + 2..w_off + 6].try_into().unwrap());
if w > 0.001 {
print!("b{}={:.3} ", bi, w);
}
}
println!();
}
}
}
Err(e) => {
panic!("FBX import failed: {}", e);
}
}
}
}