use crate::compiled::real::law_max_f32;
use crate::io::IoError;
use crate::material::MaterialLibrary;
use crate::mesh::Mesh;
use std::collections::HashMap;
use std::io::Write;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FbxFormat {
Ascii,
Binary,
}
#[derive(Debug, Clone)]
pub struct FbxConfig {
pub export_normals: bool,
pub export_uvs: bool,
pub export_materials: bool,
pub up_axis: FbxUpAxis,
pub format: FbxFormat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FbxUpAxis {
Y,
Z,
}
impl Default for FbxConfig {
fn default() -> Self {
Self {
export_normals: true,
export_uvs: true,
export_materials: true,
up_axis: FbxUpAxis::Y,
format: FbxFormat::Ascii,
}
}
}
impl FbxConfig {
pub fn binary() -> Self {
Self {
format: FbxFormat::Binary,
..Default::default()
}
}
}
#[derive(Debug, Clone)]
pub struct FbxBone {
pub name: String,
pub parent: Option<usize>,
pub bind_pose: [f32; 16],
pub vertex_indices: Vec<u32>,
pub weights: Vec<f32>,
}
#[derive(Debug, Clone)]
pub struct FbxAnimCurve {
pub property: String,
pub channel: usize,
pub times: Vec<f32>,
pub values: Vec<f32>,
}
#[derive(Debug, Clone)]
pub struct FbxAnimClip {
pub name: String,
pub bone_curves: HashMap<String, Vec<FbxAnimCurve>>,
pub duration: f32,
pub fps: f32,
}
#[derive(Debug, Clone)]
pub struct ImportedFbx {
pub mesh: Mesh,
pub skeleton: Option<Vec<FbxBone>>,
pub animations: Vec<FbxAnimClip>,
}
pub fn export_fbx(
mesh: &Mesh,
path: impl AsRef<Path>,
config: &FbxConfig,
materials: Option<&MaterialLibrary>,
) -> Result<(), IoError> {
match config.format {
FbxFormat::Ascii => export_fbx_ascii(mesh, path, config, materials),
FbxFormat::Binary => export_fbx_binary(mesh, path, config, materials),
}
}
fn export_fbx_ascii(
mesh: &Mesh,
path: impl AsRef<Path>,
config: &FbxConfig,
materials: Option<&MaterialLibrary>,
) -> Result<(), IoError> {
let file = std::fs::File::create(path)?;
let mut w = std::io::BufWriter::new(file);
let vert_count = mesh.vertices.len();
let tri_count = mesh.indices.len() / 3;
writeln!(w, "; FBX 7.4.0 project file")?;
writeln!(w, "; Generated by ALICE-SDF")?;
writeln!(w, ";")?;
writeln!(w, "FBXHeaderExtension: {{")?;
writeln!(w, "\tFBXHeaderVersion: 1003")?;
writeln!(w, "\tFBXVersion: 7400")?;
writeln!(w, "\tCreator: \"ALICE-SDF\"")?;
writeln!(w, "}}")?;
writeln!(w)?;
let up_axis_val = match config.up_axis {
FbxUpAxis::Y => 1,
FbxUpAxis::Z => 2,
};
writeln!(w, "GlobalSettings: {{")?;
writeln!(w, "\tVersion: 1000")?;
writeln!(w, "\tProperties70: {{")?;
writeln!(
w,
"\t\tP: \"UpAxis\", \"int\", \"Integer\", \"\",{}",
up_axis_val
)?;
writeln!(w, "\t\tP: \"UpAxisSign\", \"int\", \"Integer\", \"\",1")?;
writeln!(w, "\t\tP: \"FrontAxis\", \"int\", \"Integer\", \"\",2")?;
writeln!(w, "\t\tP: \"FrontAxisSign\", \"int\", \"Integer\", \"\",1")?;
writeln!(w, "\t\tP: \"CoordAxis\", \"int\", \"Integer\", \"\",0")?;
writeln!(w, "\t\tP: \"CoordAxisSign\", \"int\", \"Integer\", \"\",1")?;
writeln!(
w,
"\t\tP: \"UnitScaleFactor\", \"double\", \"Number\", \"\",1.0"
)?;
writeln!(w, "\t}}")?;
writeln!(w, "}}")?;
writeln!(w)?;
let mat_count = materials.map_or(0, MaterialLibrary::len);
let total_objects = 2 + mat_count; writeln!(w, "Definitions: {{")?;
writeln!(w, "\tVersion: 100")?;
writeln!(w, "\tCount: {}", total_objects)?;
writeln!(w, "\tObjectType: \"Model\" {{")?;
writeln!(w, "\t\tCount: 1")?;
writeln!(w, "\t}}")?;
writeln!(w, "\tObjectType: \"Geometry\" {{")?;
writeln!(w, "\t\tCount: 1")?;
writeln!(w, "\t}}")?;
if mat_count > 0 && config.export_materials {
writeln!(w, "\tObjectType: \"Material\" {{")?;
writeln!(w, "\t\tCount: {}", mat_count)?;
writeln!(w, "\t}}")?;
}
writeln!(w, "}}")?;
writeln!(w)?;
writeln!(w, "Objects: {{")?;
let geom_id: i64 = 100000;
writeln!(
w,
"\tGeometry: {}, \"Geometry::Mesh\", \"Mesh\" {{",
geom_id
)?;
writeln!(w, "\t\tVertices: *{} {{", vert_count * 3)?;
write!(w, "\t\t\ta: ")?;
for (i, v) in mesh.vertices.iter().enumerate() {
if i > 0 {
write!(w, ",")?;
}
write!(w, "{},{},{}", v.position.x, v.position.y, v.position.z)?;
}
writeln!(w)?;
writeln!(w, "\t\t}}")?;
writeln!(w, "\t\tPolygonVertexIndex: *{} {{", tri_count * 3)?;
write!(w, "\t\t\ta: ")?;
for t in 0..tri_count {
if t > 0 {
write!(w, ",")?;
}
let a = mesh.indices[t * 3] as i32;
let b = mesh.indices[t * 3 + 1] as i32;
let c = mesh.indices[t * 3 + 2] as i32;
write!(w, "{},{},{}", a, b, -(c + 1))?;
}
writeln!(w)?;
writeln!(w, "\t\t}}")?;
if config.export_normals {
writeln!(w, "\t\tLayerElementNormal: 0 {{")?;
writeln!(w, "\t\t\tVersion: 101")?;
writeln!(w, "\t\t\tName: \"\"")?;
writeln!(w, "\t\t\tMappingInformationType: \"ByVertice\"")?;
writeln!(w, "\t\t\tReferenceInformationType: \"Direct\"")?;
writeln!(w, "\t\t\tNormals: *{} {{", vert_count * 3)?;
write!(w, "\t\t\t\ta: ")?;
for (i, v) in mesh.vertices.iter().enumerate() {
if i > 0 {
write!(w, ",")?;
}
write!(w, "{},{},{}", v.normal.x, v.normal.y, v.normal.z)?;
}
writeln!(w)?;
writeln!(w, "\t\t\t}}")?;
writeln!(w, "\t\t}}")?;
}
if config.export_uvs {
writeln!(w, "\t\tLayerElementUV: 0 {{")?;
writeln!(w, "\t\t\tVersion: 101")?;
writeln!(w, "\t\t\tName: \"UVMap\"")?;
writeln!(w, "\t\t\tMappingInformationType: \"ByVertice\"")?;
writeln!(w, "\t\t\tReferenceInformationType: \"Direct\"")?;
writeln!(w, "\t\t\tUV: *{} {{", vert_count * 2)?;
write!(w, "\t\t\t\ta: ")?;
for (i, v) in mesh.vertices.iter().enumerate() {
if i > 0 {
write!(w, ",")?;
}
write!(w, "{},{}", v.uv.x, v.uv.y)?;
}
writeln!(w)?;
writeln!(w, "\t\t\t}}")?;
writeln!(w, "\t\t}}")?;
}
if config.export_materials && mat_count > 0 {
writeln!(w, "\t\tLayerElementMaterial: 0 {{")?;
writeln!(w, "\t\t\tVersion: 101")?;
writeln!(w, "\t\t\tName: \"\"")?;
writeln!(w, "\t\t\tMappingInformationType: \"ByPolygon\"")?;
writeln!(w, "\t\t\tReferenceInformationType: \"IndexToDirect\"")?;
writeln!(w, "\t\t\tMaterials: *{} {{", tri_count)?;
write!(w, "\t\t\t\ta: ")?;
for t in 0..tri_count {
if t > 0 {
write!(w, ",")?;
}
let v0 = mesh.indices[t * 3] as usize;
let mat_id = if v0 < mesh.vertices.len() {
mesh.vertices[v0].material_id
} else {
0
};
write!(w, "{}", mat_id)?;
}
writeln!(w)?;
writeln!(w, "\t\t\t}}")?;
writeln!(w, "\t\t}}")?;
}
writeln!(w, "\t\tLayer: 0 {{")?;
writeln!(w, "\t\t\tVersion: 100")?;
if config.export_normals {
writeln!(w, "\t\t\tLayerElement: {{")?;
writeln!(w, "\t\t\t\tType: \"LayerElementNormal\"")?;
writeln!(w, "\t\t\t\tTypedIndex: 0")?;
writeln!(w, "\t\t\t}}")?;
}
if config.export_uvs {
writeln!(w, "\t\t\tLayerElement: {{")?;
writeln!(w, "\t\t\t\tType: \"LayerElementUV\"")?;
writeln!(w, "\t\t\t\tTypedIndex: 0")?;
writeln!(w, "\t\t\t}}")?;
}
if config.export_materials && mat_count > 0 {
writeln!(w, "\t\t\tLayerElement: {{")?;
writeln!(w, "\t\t\t\tType: \"LayerElementMaterial\"")?;
writeln!(w, "\t\t\t\tTypedIndex: 0")?;
writeln!(w, "\t\t\t}}")?;
}
writeln!(w, "\t\t}}")?;
writeln!(w, "\t}}")?;
let model_id: i64 = 200000;
writeln!(w, "\tModel: {}, \"Model::Mesh\", \"Mesh\" {{", model_id)?;
writeln!(w, "\t\tVersion: 232")?;
writeln!(w, "\t\tProperties70: {{")?;
writeln!(
w,
"\t\t\tP: \"ScalingMax\", \"Vector3D\", \"Vector\", \"\",0,0,0"
)?;
writeln!(w, "\t\t}}")?;
writeln!(w, "\t\tShading: T")?;
writeln!(w, "\t\tCulling: \"CullingOff\"")?;
writeln!(w, "\t}}")?;
if config.export_materials {
if let Some(mat_lib) = materials {
for (i, mat) in mat_lib.iter() {
let mat_id: i64 = 300000 + i as i64;
writeln!(
w,
"\tMaterial: {}, \"Material::{}\", \"\" {{",
mat_id, mat.name
)?;
writeln!(w, "\t\tVersion: 102")?;
writeln!(w, "\t\tShadingModel: \"phong\"")?;
writeln!(w, "\t\tProperties70: {{")?;
writeln!(
w,
"\t\t\tP: \"DiffuseColor\", \"Color\", \"\", \"A\",{},{},{}",
mat.base_color[0], mat.base_color[1], mat.base_color[2]
)?;
writeln!(
w,
"\t\t\tP: \"SpecularColor\", \"Color\", \"\", \"A\",{},{},{}",
mat.metallic, mat.metallic, mat.metallic
)?;
writeln!(
w,
"\t\t\tP: \"Shininess\", \"double\", \"Number\", \"\",{}",
(1.0 - mat.roughness) * 100.0
)?;
writeln!(
w,
"\t\t\tP: \"Opacity\", \"double\", \"Number\", \"\",{}",
mat.opacity
)?;
if mat.emission_strength > 0.0 {
writeln!(
w,
"\t\t\tP: \"EmissiveColor\", \"Color\", \"\", \"A\",{},{},{}",
mat.emission[0], mat.emission[1], mat.emission[2]
)?;
writeln!(
w,
"\t\t\tP: \"EmissiveFactor\", \"double\", \"Number\", \"\",{}",
mat.emission_strength
)?;
}
writeln!(w, "\t\t}}")?;
writeln!(w, "\t}}")?;
}
}
}
writeln!(w, "}}")?; writeln!(w)?;
writeln!(w, "Connections: {{")?;
writeln!(w, "\tC: \"OO\",{},0", model_id)?;
writeln!(w, "\tC: \"OO\",{},{}", geom_id, model_id)?;
if config.export_materials {
if let Some(mat_lib) = materials {
for i in 0..mat_lib.len() {
let mat_id: i64 = 300000 + i as i64;
writeln!(w, "\tC: \"OO\",{},{}", mat_id, model_id)?;
}
}
}
writeln!(w, "}}")?;
w.flush()?;
Ok(())
}
const FBX_BINARY_MAGIC: &[u8; 23] = b"Kaydara FBX Binary \x00\x1a\x00";
const FBX_BINARY_VERSION: u32 = 7400;
fn write_fbx_prop_i32(w: &mut impl Write, val: i32) -> Result<(), IoError> {
w.write_all(b"I")?;
w.write_all(&val.to_le_bytes())?;
Ok(())
}
fn write_fbx_prop_i64(w: &mut impl Write, val: i64) -> Result<(), IoError> {
w.write_all(b"L")?;
w.write_all(&val.to_le_bytes())?;
Ok(())
}
fn write_fbx_prop_f64(w: &mut impl Write, val: f64) -> Result<(), IoError> {
w.write_all(b"D")?;
w.write_all(&val.to_le_bytes())?;
Ok(())
}
fn write_fbx_prop_str(w: &mut impl Write, s: &str) -> Result<(), IoError> {
w.write_all(b"S")?;
w.write_all(&(s.len() as u32).to_le_bytes())?;
w.write_all(s.as_bytes())?;
Ok(())
}
fn write_fbx_prop_f64_array(w: &mut impl Write, data: &[f64]) -> Result<(), IoError> {
w.write_all(b"d")?;
w.write_all(&(data.len() as u32).to_le_bytes())?;
w.write_all(&0u32.to_le_bytes())?; w.write_all(&((data.len() * 8) as u32).to_le_bytes())?; for &v in data {
w.write_all(&v.to_le_bytes())?;
}
Ok(())
}
fn write_fbx_prop_i32_array(w: &mut impl Write, data: &[i32]) -> Result<(), IoError> {
w.write_all(b"i")?;
w.write_all(&(data.len() as u32).to_le_bytes())?;
w.write_all(&0u32.to_le_bytes())?; w.write_all(&((data.len() * 4) as u32).to_le_bytes())?; for &v in data {
w.write_all(&v.to_le_bytes())?;
}
Ok(())
}
fn serialize_fbx_node(name: &str, props: &[FbxProp], children: &[Vec<u8>]) -> Vec<u8> {
let mut body = Vec::new();
let mut prop_data = Vec::new();
for prop in props {
match prop {
FbxProp::I32(v) => {
write_fbx_prop_i32(&mut prop_data, *v).ok();
}
FbxProp::I64(v) => {
write_fbx_prop_i64(&mut prop_data, *v).ok();
}
FbxProp::F64(v) => {
write_fbx_prop_f64(&mut prop_data, *v).ok();
}
FbxProp::Str(s) => {
write_fbx_prop_str(&mut prop_data, s).ok();
}
FbxProp::F64Array(a) => {
write_fbx_prop_f64_array(&mut prop_data, a).ok();
}
FbxProp::I32Array(a) => {
write_fbx_prop_i32_array(&mut prop_data, a).ok();
}
}
}
let mut children_data = Vec::new();
for child in children {
children_data.extend_from_slice(child);
}
if !children.is_empty() {
children_data.extend_from_slice(&[0u8; 13]);
}
let header_size = 4 + 4 + 4 + 1 + name.len();
let end_offset = header_size + prop_data.len() + children_data.len();
body.extend_from_slice(&(end_offset as u32).to_le_bytes());
body.extend_from_slice(&(props.len() as u32).to_le_bytes());
body.extend_from_slice(&(prop_data.len() as u32).to_le_bytes());
body.push(name.len() as u8);
body.extend_from_slice(name.as_bytes());
body.extend_from_slice(&prop_data);
body.extend_from_slice(&children_data);
body
}
enum FbxProp {
I32(i32),
I64(i64),
F64(f64),
Str(String),
F64Array(Vec<f64>),
I32Array(Vec<i32>),
}
fn export_fbx_binary(
mesh: &Mesh,
path: impl AsRef<Path>,
config: &FbxConfig,
materials: Option<&MaterialLibrary>,
) -> Result<(), IoError> {
let file = std::fs::File::create(path)?;
let mut w = std::io::BufWriter::new(file);
let vert_count = mesh.vertices.len();
let tri_count = mesh.indices.len() / 3;
w.write_all(FBX_BINARY_MAGIC)?;
w.write_all(&FBX_BINARY_VERSION.to_le_bytes())?;
let mut top_nodes: Vec<Vec<u8>> = Vec::new();
{
let version_node = serialize_fbx_node("FBXHeaderVersion", &[FbxProp::I32(1003)], &[]);
let fbx_version = serialize_fbx_node("FBXVersion", &[FbxProp::I32(7400)], &[]);
let creator = serialize_fbx_node("Creator", &[FbxProp::Str("ALICE-SDF".to_string())], &[]);
top_nodes.push(serialize_fbx_node(
"FBXHeaderExtension",
&[],
&[version_node, fbx_version, creator],
));
}
{
let up_axis_val = match config.up_axis {
FbxUpAxis::Y => 1,
FbxUpAxis::Z => 2,
};
let p_nodes = vec![
serialize_fbx_node(
"P",
&[
FbxProp::Str("UpAxis".to_string()),
FbxProp::Str("int".to_string()),
FbxProp::Str("Integer".to_string()),
FbxProp::Str(String::new()),
FbxProp::I32(up_axis_val),
],
&[],
),
serialize_fbx_node(
"P",
&[
FbxProp::Str("UpAxisSign".to_string()),
FbxProp::Str("int".to_string()),
FbxProp::Str("Integer".to_string()),
FbxProp::Str(String::new()),
FbxProp::I32(1),
],
&[],
),
serialize_fbx_node(
"P",
&[
FbxProp::Str("UnitScaleFactor".to_string()),
FbxProp::Str("double".to_string()),
FbxProp::Str("Number".to_string()),
FbxProp::Str(String::new()),
FbxProp::F64(1.0),
],
&[],
),
];
let props70 = serialize_fbx_node("Properties70", &[], &p_nodes);
let version = serialize_fbx_node("Version", &[FbxProp::I32(1000)], &[]);
top_nodes.push(serialize_fbx_node(
"GlobalSettings",
&[],
&[version, props70],
));
}
{
let mut object_children = Vec::new();
let geom_id: i64 = 100000;
let model_id: i64 = 200000;
{
let mut geom_children = Vec::new();
let verts_f64: Vec<f64> = mesh
.vertices
.iter()
.flat_map(|v| {
vec![
v.position.x as f64,
v.position.y as f64,
v.position.z as f64,
]
})
.collect();
geom_children.push(serialize_fbx_node(
"Vertices",
&[FbxProp::F64Array(verts_f64)],
&[],
));
let indices_i32: Vec<i32> = (0..tri_count)
.flat_map(|t| {
let a = mesh.indices[t * 3] as i32;
let b = mesh.indices[t * 3 + 1] as i32;
let c = mesh.indices[t * 3 + 2] as i32;
vec![a, b, -(c + 1)]
})
.collect();
geom_children.push(serialize_fbx_node(
"PolygonVertexIndex",
&[FbxProp::I32Array(indices_i32)],
&[],
));
if config.export_normals {
let normals_f64: Vec<f64> = mesh
.vertices
.iter()
.flat_map(|v| vec![v.normal.x as f64, v.normal.y as f64, v.normal.z as f64])
.collect();
let normals_node =
serialize_fbx_node("Normals", &[FbxProp::F64Array(normals_f64)], &[]);
let mapping = serialize_fbx_node(
"MappingInformationType",
&[FbxProp::Str("ByVertice".to_string())],
&[],
);
let reference = serialize_fbx_node(
"ReferenceInformationType",
&[FbxProp::Str("Direct".to_string())],
&[],
);
let version = serialize_fbx_node("Version", &[FbxProp::I32(101)], &[]);
geom_children.push(serialize_fbx_node(
"LayerElementNormal",
&[FbxProp::I32(0)],
&[version, mapping, reference, normals_node],
));
}
if config.export_uvs {
let uvs_f64: Vec<f64> = mesh
.vertices
.iter()
.flat_map(|v| vec![v.uv.x as f64, v.uv.y as f64])
.collect();
let uv_node = serialize_fbx_node("UV", &[FbxProp::F64Array(uvs_f64)], &[]);
let name = serialize_fbx_node("Name", &[FbxProp::Str("UVMap".to_string())], &[]);
let mapping = serialize_fbx_node(
"MappingInformationType",
&[FbxProp::Str("ByVertice".to_string())],
&[],
);
let reference = serialize_fbx_node(
"ReferenceInformationType",
&[FbxProp::Str("Direct".to_string())],
&[],
);
let version = serialize_fbx_node("Version", &[FbxProp::I32(101)], &[]);
geom_children.push(serialize_fbx_node(
"LayerElementUV",
&[FbxProp::I32(0)],
&[version, name, mapping, reference, uv_node],
));
}
let mat_count = materials.map_or(0, MaterialLibrary::len);
if config.export_materials && mat_count > 0 {
let mat_ids: Vec<i32> = (0..tri_count)
.map(|t| {
let v0 = mesh.indices[t * 3] as usize;
if v0 < vert_count {
mesh.vertices[v0].material_id as i32
} else {
0
}
})
.collect();
let mat_data = serialize_fbx_node("Materials", &[FbxProp::I32Array(mat_ids)], &[]);
let mapping = serialize_fbx_node(
"MappingInformationType",
&[FbxProp::Str("ByPolygon".to_string())],
&[],
);
let reference = serialize_fbx_node(
"ReferenceInformationType",
&[FbxProp::Str("IndexToDirect".to_string())],
&[],
);
let version = serialize_fbx_node("Version", &[FbxProp::I32(101)], &[]);
geom_children.push(serialize_fbx_node(
"LayerElementMaterial",
&[FbxProp::I32(0)],
&[version, mapping, reference, mat_data],
));
}
{
let mut layer_children =
vec![serialize_fbx_node("Version", &[FbxProp::I32(100)], &[])];
if config.export_normals {
let le = serialize_fbx_node(
"LayerElement",
&[],
&[
serialize_fbx_node(
"Type",
&[FbxProp::Str("LayerElementNormal".to_string())],
&[],
),
serialize_fbx_node("TypedIndex", &[FbxProp::I32(0)], &[]),
],
);
layer_children.push(le);
}
if config.export_uvs {
let le = serialize_fbx_node(
"LayerElement",
&[],
&[
serialize_fbx_node(
"Type",
&[FbxProp::Str("LayerElementUV".to_string())],
&[],
),
serialize_fbx_node("TypedIndex", &[FbxProp::I32(0)], &[]),
],
);
layer_children.push(le);
}
if config.export_materials && mat_count > 0 {
let le = serialize_fbx_node(
"LayerElement",
&[],
&[
serialize_fbx_node(
"Type",
&[FbxProp::Str("LayerElementMaterial".to_string())],
&[],
),
serialize_fbx_node("TypedIndex", &[FbxProp::I32(0)], &[]),
],
);
layer_children.push(le);
}
geom_children.push(serialize_fbx_node(
"Layer",
&[FbxProp::I32(0)],
&layer_children,
));
}
object_children.push(serialize_fbx_node(
"Geometry",
&[
FbxProp::I64(geom_id),
FbxProp::Str("Geometry::Mesh\x00\x01Mesh".to_string()),
FbxProp::Str("Mesh".to_string()),
],
&geom_children,
));
}
{
let props70 = serialize_fbx_node(
"Properties70",
&[],
&[serialize_fbx_node(
"P",
&[
FbxProp::Str("ScalingMax".to_string()),
FbxProp::Str("Vector3D".to_string()),
FbxProp::Str("Vector".to_string()),
FbxProp::Str(String::new()),
FbxProp::F64(0.0),
FbxProp::F64(0.0),
FbxProp::F64(0.0),
],
&[],
)],
);
let version = serialize_fbx_node("Version", &[FbxProp::I32(232)], &[]);
object_children.push(serialize_fbx_node(
"Model",
&[
FbxProp::I64(model_id),
FbxProp::Str("Model::Mesh\x00\x01Mesh".to_string()),
FbxProp::Str("Mesh".to_string()),
],
&[version, props70],
));
}
if config.export_materials {
if let Some(mat_lib) = materials {
for (i, mat) in mat_lib.iter() {
let mat_id: i64 = 300000 + i as i64;
let p_nodes = vec![
serialize_fbx_node(
"P",
&[
FbxProp::Str("DiffuseColor".to_string()),
FbxProp::Str("Color".to_string()),
FbxProp::Str(String::new()),
FbxProp::Str("A".to_string()),
FbxProp::F64(mat.base_color[0] as f64),
FbxProp::F64(mat.base_color[1] as f64),
FbxProp::F64(mat.base_color[2] as f64),
],
&[],
),
serialize_fbx_node(
"P",
&[
FbxProp::Str("Shininess".to_string()),
FbxProp::Str("double".to_string()),
FbxProp::Str("Number".to_string()),
FbxProp::Str(String::new()),
FbxProp::F64(((1.0 - mat.roughness) * 100.0) as f64),
],
&[],
),
serialize_fbx_node(
"P",
&[
FbxProp::Str("Opacity".to_string()),
FbxProp::Str("double".to_string()),
FbxProp::Str("Number".to_string()),
FbxProp::Str(String::new()),
FbxProp::F64(mat.opacity as f64),
],
&[],
),
];
let props70 = serialize_fbx_node("Properties70", &[], &p_nodes);
let version = serialize_fbx_node("Version", &[FbxProp::I32(102)], &[]);
let shading = serialize_fbx_node(
"ShadingModel",
&[FbxProp::Str("phong".to_string())],
&[],
);
let mat_name = format!("Material::{}\x00\x01", mat.name);
object_children.push(serialize_fbx_node(
"Material",
&[
FbxProp::I64(mat_id),
FbxProp::Str(mat_name),
FbxProp::Str(String::new()),
],
&[version, shading, props70],
));
}
}
}
top_nodes.push(serialize_fbx_node("Objects", &[], &object_children));
}
{
let model_id: i64 = 200000;
let geom_id: i64 = 100000;
let mut conn_children = Vec::new();
conn_children.push(serialize_fbx_node(
"C",
&[
FbxProp::Str("OO".to_string()),
FbxProp::I64(model_id),
FbxProp::I64(0),
],
&[],
));
conn_children.push(serialize_fbx_node(
"C",
&[
FbxProp::Str("OO".to_string()),
FbxProp::I64(geom_id),
FbxProp::I64(model_id),
],
&[],
));
if config.export_materials {
if let Some(mat_lib) = materials {
for i in 0..mat_lib.len() {
let mat_id: i64 = 300000 + i as i64;
conn_children.push(serialize_fbx_node(
"C",
&[
FbxProp::Str("OO".to_string()),
FbxProp::I64(mat_id),
FbxProp::I64(model_id),
],
&[],
));
}
}
}
top_nodes.push(serialize_fbx_node("Connections", &[], &conn_children));
}
let header_size = 27u32; let mut current_offset = header_size;
for node_data in &top_nodes {
let absolute_end = current_offset + node_data.len() as u32;
let mut patched = node_data.clone();
patched[0..4].copy_from_slice(&absolute_end.to_le_bytes());
w.write_all(&patched)?;
current_offset = absolute_end;
}
w.write_all(&[0u8; 13])?;
w.flush()?;
Ok(())
}
pub fn import_fbx(path: impl AsRef<Path>) -> Result<Mesh, IoError> {
use crate::mesh::Vertex;
use glam::{Vec2, Vec3};
let bytes = std::fs::read(path.as_ref())?;
if bytes.len() >= 23 && &bytes[..18] == b"Kaydara FBX Binary" {
return Err(IoError::InvalidFormat(
"Binary FBX import not supported. Use ASCII FBX or convert to glTF.".into(),
));
}
let content = String::from_utf8(bytes)
.map_err(|e| IoError::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?;
let mut raw_vertices: Vec<f32> = Vec::new();
let mut raw_polygon_indices: Vec<i32> = Vec::new();
let mut raw_normals: Vec<f32> = Vec::new();
let mut raw_uvs: Vec<f32> = Vec::new();
let mut in_vertices = false;
let mut in_polygon_indices = false;
let mut in_normals = false;
let mut in_uvs = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("Vertices: *") {
if let Some(n_str) = trimmed.strip_prefix("Vertices: *") {
if let Ok(n) = n_str.parse::<usize>() {
raw_vertices.reserve(n);
}
}
in_vertices = true;
continue;
}
if trimmed.starts_with("PolygonVertexIndex: *") {
if let Some(n_str) = trimmed.strip_prefix("PolygonVertexIndex: *") {
if let Ok(n) = n_str.parse::<usize>() {
raw_polygon_indices.reserve(n);
}
}
in_polygon_indices = true;
continue;
}
if trimmed.starts_with("Normals: *") {
if let Some(n_str) = trimmed.strip_prefix("Normals: *") {
if let Ok(n) = n_str.parse::<usize>() {
raw_normals.reserve(n);
}
}
in_normals = true;
continue;
}
if trimmed.starts_with("UV: *") && !trimmed.starts_with("UVIndex:") {
if let Some(n_str) = trimmed.strip_prefix("UV: *") {
if let Ok(n) = n_str.parse::<usize>() {
raw_uvs.reserve(n);
}
}
in_uvs = true;
continue;
}
if let Some(data_str) = trimmed.strip_prefix("a: ") {
if in_vertices {
for tok in data_str.split(',') {
if let Ok(v) = tok.trim().parse::<f32>() {
raw_vertices.push(v);
}
}
in_vertices = false;
} else if in_polygon_indices {
for tok in data_str.split(',') {
if let Ok(v) = tok.trim().parse::<i32>() {
raw_polygon_indices.push(v);
}
}
in_polygon_indices = false;
} else if in_normals {
for tok in data_str.split(',') {
if let Ok(v) = tok.trim().parse::<f32>() {
raw_normals.push(v);
}
}
in_normals = false;
} else if in_uvs {
for tok in data_str.split(',') {
if let Ok(v) = tok.trim().parse::<f32>() {
raw_uvs.push(v);
}
}
in_uvs = false;
}
}
if trimmed == "}" {
in_vertices = false;
in_polygon_indices = false;
in_normals = false;
in_uvs = false;
}
}
if raw_vertices.is_empty() || raw_polygon_indices.is_empty() {
return Err(IoError::InvalidFormat(
"No geometry found in FBX file".into(),
));
}
let vert_count = raw_vertices.len() / 3;
let has_normals = raw_normals.len() / 3 == vert_count;
let has_uvs = raw_uvs.len() / 2 == vert_count;
let mut vertices: Vec<Vertex> = Vec::with_capacity(vert_count);
for i in 0..vert_count {
let i3 = i * 3;
let pos = Vec3::new(raw_vertices[i3], raw_vertices[i3 + 1], raw_vertices[i3 + 2]);
let norm = if has_normals {
Vec3::new(raw_normals[i3], raw_normals[i3 + 1], raw_normals[i3 + 2]).normalize()
} else {
Vec3::Y
};
let mut vert = Vertex::new(pos, norm);
if has_uvs {
let i2 = i * 2;
vert.uv = Vec2::new(raw_uvs[i2], raw_uvs[i2 + 1]);
}
vertices.push(vert);
}
let mut indices: Vec<u32> = Vec::new();
let mut polygon: Vec<u32> = Vec::new();
for &idx in &raw_polygon_indices {
if idx < 0 {
let real_idx = (-(idx + 1)) as u32;
polygon.push(real_idx);
for i in 1..polygon.len().saturating_sub(1) {
indices.push(polygon[0]);
indices.push(polygon[i]);
indices.push(polygon[i + 1]);
}
polygon.clear();
} else {
polygon.push(idx as u32);
}
}
Ok(Mesh { vertices, indices })
}
pub fn import_fbx_full(path: impl AsRef<Path>) -> Result<ImportedFbx, IoError> {
let mesh = import_fbx(path.as_ref())?;
let content = std::fs::read_to_string(path.as_ref())?;
let skeleton = parse_skeleton(&content);
let animations = parse_animations(&content);
Ok(ImportedFbx {
mesh,
skeleton,
animations,
})
}
fn parse_skeleton(content: &str) -> Option<Vec<FbxBone>> {
let mut bones = Vec::new();
let mut bone_id_map: HashMap<i64, usize> = HashMap::new();
let mut connections: Vec<(i64, i64)> = Vec::new();
let mut current_bone: Option<(i64, FbxBone)> = None;
let mut in_indices = false;
let mut in_weights = false;
let mut in_transform_link = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("Deformer:") && trimmed.contains("\"Cluster\"") {
let parts: Vec<&str> = trimmed.split(',').collect();
if parts.len() >= 2 {
if let Some(id_str) = parts[0].strip_prefix("Deformer:") {
if let Ok(id) = id_str.trim().parse::<i64>() {
let name = parts[1]
.trim()
.trim_matches('"')
.strip_prefix("SubDeformer::")
.unwrap_or("UnnamedBone")
.to_string();
current_bone = Some((
id,
FbxBone {
name,
parent: None,
bind_pose: [
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,
],
vertex_indices: Vec::new(),
weights: Vec::new(),
},
));
}
}
}
}
if trimmed.starts_with("Indexes: *") {
in_indices = true;
continue;
}
if trimmed.starts_with("Weights: *") {
in_weights = true;
continue;
}
if trimmed.starts_with("TransformLink: *") {
in_transform_link = true;
continue;
}
if let Some(data_str) = trimmed.strip_prefix("a: ") {
if in_indices {
if let Some((_, ref mut bone)) = current_bone {
for tok in data_str.split(',') {
if let Ok(idx) = tok.trim().parse::<u32>() {
bone.vertex_indices.push(idx);
}
}
}
in_indices = false;
} else if in_weights {
if let Some((_, ref mut bone)) = current_bone {
for tok in data_str.split(',') {
if let Ok(w) = tok.trim().parse::<f32>() {
bone.weights.push(w);
}
}
}
in_weights = false;
} else if in_transform_link {
if let Some((_, ref mut bone)) = current_bone {
let mut values: Vec<f32> = Vec::new();
for tok in data_str.split(',') {
if let Ok(v) = tok.trim().parse::<f32>() {
values.push(v);
}
}
if values.len() == 16 {
bone.bind_pose.copy_from_slice(&values);
}
}
in_transform_link = false;
}
}
if trimmed == "}" {
if let Some((id, bone)) = current_bone.take() {
bone_id_map.insert(id, bones.len());
bones.push(bone);
}
in_indices = false;
in_weights = false;
in_transform_link = false;
}
}
let mut in_connections = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("Connections:") {
in_connections = true;
continue;
}
if in_connections {
if trimmed == "}" {
break;
}
if trimmed.starts_with("C: \"OO\"") {
let parts: Vec<&str> = trimmed.split(',').collect();
if parts.len() >= 3 {
if let (Ok(child_id), Ok(parent_id)) = (
parts[1].trim().parse::<i64>(),
parts[2].trim().parse::<i64>(),
) {
connections.push((child_id, parent_id));
}
}
}
}
}
for (child_id, parent_id) in connections {
if let (Some(&child_idx), Some(&parent_idx)) =
(bone_id_map.get(&child_id), bone_id_map.get(&parent_id))
{
bones[child_idx].parent = Some(parent_idx);
}
}
if bones.is_empty() {
None
} else {
Some(bones)
}
}
fn parse_animations(content: &str) -> Vec<FbxAnimClip> {
let mut clips = Vec::new();
let mut bone_curves: HashMap<String, Vec<FbxAnimCurve>> = HashMap::new();
const FBX_TIME_UNIT: f64 = 46186158000.0;
let mut current_curve: Option<FbxAnimCurve> = None;
let mut in_key_time = false;
let mut in_key_value = false;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with("AnimationCurve:") {
current_curve = Some(FbxAnimCurve {
property: "Unknown".to_string(),
channel: 0,
times: Vec::new(),
values: Vec::new(),
});
}
if trimmed.starts_with("KeyTime: *") {
in_key_time = true;
continue;
}
if trimmed.starts_with("KeyValueFloat: *") {
in_key_value = true;
continue;
}
if let Some(data_str) = trimmed.strip_prefix("a: ") {
if in_key_time {
if let Some(ref mut curve) = current_curve {
for tok in data_str.split(',') {
if let Ok(fbx_time) = tok.trim().parse::<i64>() {
let time_sec = (fbx_time as f64 / FBX_TIME_UNIT) as f32;
curve.times.push(time_sec);
}
}
}
in_key_time = false;
} else if in_key_value {
if let Some(ref mut curve) = current_curve {
for tok in data_str.split(',') {
if let Ok(val) = tok.trim().parse::<f32>() {
curve.values.push(val);
}
}
}
in_key_value = false;
}
}
if trimmed == "}" {
if let Some(curve) = current_curve.take() {
bone_curves
.entry("Root".to_string())
.or_default()
.push(curve);
}
in_key_time = false;
in_key_value = false;
}
}
if !bone_curves.is_empty() {
let max_time = bone_curves
.values()
.flat_map(|curves| curves.iter().flat_map(|c| c.times.iter()))
.cloned()
.fold(0.0f32, law_max_f32);
clips.push(FbxAnimClip {
name: "DefaultClip".to_string(),
bone_curves,
duration: max_time,
fps: 30.0,
});
}
clips
}
pub fn fbx_animation_to_timeline(
clip: &FbxAnimClip,
root_bone: &str,
) -> crate::animation::Timeline {
use crate::animation::{Keyframe, Timeline, Track};
let mut timeline = Timeline::new(&clip.name);
if let Some(curves) = clip.bone_curves.get(root_bone) {
for curve in curves {
let track_name = match (curve.property.as_str(), curve.channel) {
("Lcl Translation", 0) => "translate.x",
("Lcl Translation", 1) => "translate.y",
("Lcl Translation", 2) => "translate.z",
("Lcl Rotation", 0) => "rotate.x",
("Lcl Rotation", 1) => "rotate.y",
("Lcl Rotation", 2) => "rotate.z",
_ => continue,
};
let mut track = Track::new(track_name);
for (t, v) in curve.times.iter().zip(curve.values.iter()) {
let value = if curve.property == "Lcl Rotation" {
v.to_radians()
} else {
*v
};
track.add_keyframe(Keyframe::new(*t, value));
}
timeline.add_track(track);
}
}
timeline
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mesh::{sdf_to_mesh, MarchingCubesConfig};
use crate::types::SdfNode;
use glam::Vec3;
#[test]
fn test_fbx_export() {
let sphere = SdfNode::sphere(1.0);
let config = MarchingCubesConfig {
resolution: 8,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
let path = std::env::temp_dir().join("alice_test.fbx");
export_fbx(&mesh, &path, &FbxConfig::default(), None).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("FBXVersion: 7400"));
assert!(content.contains("Vertices:"));
assert!(content.contains("PolygonVertexIndex:"));
assert!(content.contains("LayerElementNormal:"));
assert!(content.contains("LayerElementUV:"));
std::fs::remove_file(&path).ok();
}
#[test]
fn test_fbx_with_materials() {
let sphere = SdfNode::sphere(1.0);
let config = MarchingCubesConfig {
resolution: 8,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
let mut mat_lib = crate::material::MaterialLibrary::new();
mat_lib.add(crate::material::Material::metal(
"Steel", 0.7, 0.7, 0.7, 0.4,
));
let path = std::env::temp_dir().join("alice_test_mat.fbx");
export_fbx(&mesh, &path, &FbxConfig::default(), Some(&mat_lib)).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("Material::Default"));
assert!(content.contains("Material::Steel"));
assert!(content.contains("DiffuseColor"));
std::fs::remove_file(&path).ok();
}
#[test]
fn test_fbx_z_up() {
let sphere = SdfNode::sphere(1.0);
let config = MarchingCubesConfig {
resolution: 4,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
let fbx_config = FbxConfig {
up_axis: FbxUpAxis::Z,
..Default::default()
};
let path = std::env::temp_dir().join("alice_test_zup.fbx");
export_fbx(&mesh, &path, &fbx_config, None).unwrap();
let content = std::fs::read_to_string(&path).unwrap();
assert!(content.contains("\"UpAxis\", \"int\", \"Integer\", \"\",2"));
std::fs::remove_file(&path).ok();
}
#[test]
fn test_fbx_binary_export() {
let sphere = SdfNode::sphere(1.0);
let config = MarchingCubesConfig {
resolution: 8,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
let path = std::env::temp_dir().join("alice_test_binary.fbx");
export_fbx(&mesh, &path, &FbxConfig::binary(), None).unwrap();
let bytes = std::fs::read(&path).unwrap();
assert_eq!(
&bytes[0..21],
&FBX_BINARY_MAGIC[0..21],
"FBX binary magic mismatch"
);
let version = u32::from_le_bytes([bytes[23], bytes[24], bytes[25], bytes[26]]);
assert_eq!(version, 7400);
assert!(
bytes.len() > 100,
"Binary FBX too small: {} bytes",
bytes.len()
);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_fbx_binary_with_materials() {
let sphere = SdfNode::sphere(1.0);
let config = MarchingCubesConfig {
resolution: 4,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
let mut mat_lib = crate::material::MaterialLibrary::new();
mat_lib.add(crate::material::Material::metal(
"Steel", 0.7, 0.7, 0.7, 0.4,
));
let path = std::env::temp_dir().join("alice_test_bin_mat.fbx");
export_fbx(&mesh, &path, &FbxConfig::binary(), Some(&mat_lib)).unwrap();
let bytes = std::fs::read(&path).unwrap();
assert_eq!(&bytes[0..21], &FBX_BINARY_MAGIC[0..21]);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_fbx_import_roundtrip() {
let sphere = SdfNode::sphere(1.0);
let config = MarchingCubesConfig {
resolution: 8,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
let orig_verts = mesh.vertex_count();
let orig_tris = mesh.indices.len() / 3;
let path = std::env::temp_dir().join("alice_import_test.fbx");
export_fbx(&mesh, &path, &FbxConfig::default(), None).unwrap();
let imported = import_fbx(&path).unwrap();
assert_eq!(imported.vertex_count(), orig_verts, "Vertex count mismatch");
assert_eq!(
imported.indices.len() / 3,
orig_tris,
"Triangle count mismatch"
);
let orig_pos = mesh.vertices[0].position;
let imp_pos = imported.vertices[0].position;
assert!(
(orig_pos - imp_pos).length() < 0.001,
"Position mismatch: {:?} vs {:?}",
orig_pos,
imp_pos
);
std::fs::remove_file(&path).ok();
}
#[test]
fn test_fbx_import_empty() {
let result = import_fbx("/nonexistent/file.fbx");
assert!(result.is_err());
}
#[test]
fn test_fbx_skeletal_structs() {
let bone = FbxBone {
name: "TestBone".to_string(),
parent: None,
bind_pose: [
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,
],
vertex_indices: vec![0, 1, 2],
weights: vec![1.0, 0.8, 0.5],
};
assert_eq!(bone.name, "TestBone");
assert_eq!(bone.vertex_indices.len(), 3);
assert_eq!(bone.weights.len(), 3);
let sphere = SdfNode::sphere(1.0);
let config = MarchingCubesConfig {
resolution: 4,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
let imported = ImportedFbx {
mesh,
skeleton: Some(vec![bone]),
animations: Vec::new(),
};
assert!(imported.skeleton.is_some());
assert_eq!(imported.skeleton.unwrap().len(), 1);
assert_eq!(imported.animations.len(), 0);
}
#[test]
fn test_fbx_animation_to_timeline() {
let mut bone_curves = HashMap::new();
let curves = vec![
FbxAnimCurve {
property: "Lcl Translation".to_string(),
channel: 0,
times: vec![0.0, 1.0, 2.0],
values: vec![0.0, 1.0, 2.0],
},
FbxAnimCurve {
property: "Lcl Rotation".to_string(),
channel: 0,
times: vec![0.0, 1.0],
values: vec![0.0, 90.0], },
];
bone_curves.insert("Root".to_string(), curves);
let clip = FbxAnimClip {
name: "TestClip".to_string(),
bone_curves,
duration: 2.0,
fps: 30.0,
};
let timeline = fbx_animation_to_timeline(&clip, "Root");
assert_eq!(timeline.name, "TestClip");
assert!(!timeline.tracks.is_empty());
let translate_x = timeline.tracks.iter().find(|t| t.name == "translate.x");
assert!(translate_x.is_some());
let track = translate_x.unwrap();
assert_eq!(track.keyframes.len(), 3);
assert_eq!(track.keyframes[0].value, 0.0);
assert_eq!(track.keyframes[1].value, 1.0);
assert_eq!(track.keyframes[2].value, 2.0);
let rotate_x = timeline.tracks.iter().find(|t| t.name == "rotate.x");
assert!(rotate_x.is_some());
let track = rotate_x.unwrap();
assert_eq!(track.keyframes.len(), 2);
assert_eq!(track.keyframes[0].value, 0.0);
assert!((track.keyframes[1].value - (90.0f32.to_radians())).abs() < 0.001);
}
#[test]
fn test_import_fbx_full_geometry_only() {
let sphere = SdfNode::sphere(1.0);
let config = MarchingCubesConfig {
resolution: 8,
iso_level: 0.0,
compute_normals: true,
..Default::default()
};
let mesh = sdf_to_mesh(&sphere, Vec3::splat(-2.0), Vec3::splat(2.0), &config);
let path = std::env::temp_dir().join("alice_fbx_full_test.fbx");
export_fbx(&mesh, &path, &FbxConfig::default(), None).unwrap();
let imported = import_fbx_full(&path).unwrap();
assert_eq!(imported.mesh.vertex_count(), mesh.vertex_count());
assert!(imported.skeleton.is_none());
assert_eq!(imported.animations.len(), 0);
std::fs::remove_file(&path).ok();
}
}