#[derive(Debug, Clone, Copy)]
pub struct FbxVertex {
pub position: [f32; 3],
pub normal: [f32; 3],
pub uv: [f32; 2],
pub tangent: [f32; 4],
pub color: [f32; 4],
}
#[derive(Debug, Clone)]
pub struct FbxBone {
pub name: String,
pub parent_index: Option<usize>,
pub bind_pose: [f32; 16], }
#[derive(Debug, Clone, Copy)]
pub struct FbxKeyframe {
pub time_seconds: f32,
pub bone_index: usize,
pub translation: [f32; 3],
pub rotation: [f32; 4], pub scale: [f32; 3],
}
#[derive(Debug, Clone)]
pub struct FbxAnimation {
pub name: String,
pub duration_seconds: f32,
pub keyframes: Vec<FbxKeyframe>,
}
#[derive(Debug, Clone, Copy)]
pub struct FbxSkinWeight {
pub bone_indices: [u32; 4],
pub bone_weights: [f32; 4],
}
impl Default for FbxSkinWeight {
fn default() -> Self {
Self {
bone_indices: [0; 4],
bone_weights: [1.0, 0.0, 0.0, 0.0],
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FbxSkinData {
pub weights: Vec<FbxSkinWeight>,
pub deformer_count: usize,
}
#[derive(Debug, Clone)]
pub struct FbxMaterialData {
pub name: String,
pub base_color: [f32; 4],
pub roughness: f32,
pub metallic: f32,
pub emissive: [f32; 3],
}
impl Default for FbxMaterialData {
fn default() -> Self {
Self {
name: String::new(),
base_color: [0.8, 0.8, 0.8, 1.0],
roughness: 0.5,
metallic: 0.0,
emissive: [0.0; 3],
}
}
}
#[derive(Debug, Clone, Default)]
pub struct FbxImportResult {
pub vertices: Vec<FbxVertex>,
pub indices: Vec<u32>,
pub bones: Vec<FbxBone>,
pub animations: Vec<FbxAnimation>,
pub skin_data: Option<FbxSkinData>,
pub materials: Vec<FbxMaterialData>,
pub warnings: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct FbxValidation {
pub vertex_count: usize,
pub index_count: usize,
pub bone_count: usize,
pub animation_count: usize,
pub meshlet_compatible: bool,
pub dreamlet_compatible: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
pub const MAX_FBX_BONES: usize = 256;
pub const MAX_FBX_VERTICES: usize = 1_000_000;
pub fn parse_fbx_geometry(raw_vertices: &[f64], raw_polygon_indices: &[i32]) -> FbxImportResult {
let mut result = FbxImportResult::default();
for chunk in raw_vertices.chunks_exact(3) {
if result.vertices.len() >= MAX_FBX_VERTICES {
result
.warnings
.push(format!("fbx_vertex_limit:truncated_at_{MAX_FBX_VERTICES}"));
break;
}
result.vertices.push(FbxVertex {
position: [chunk[0] as f32, chunk[1] as f32, chunk[2] as f32],
normal: [0.0, 1.0, 0.0],
uv: [0.0, 0.0],
tangent: [1.0, 0.0, 0.0, 1.0],
color: [1.0, 1.0, 1.0, 1.0],
});
}
let mut polygon: Vec<u32> = Vec::new();
for &idx in raw_polygon_indices {
if idx < 0 {
let actual = (!idx) as u32; polygon.push(actual);
if polygon.len() >= 3 {
for i in 1..polygon.len() - 1 {
result.indices.push(polygon[0]);
result.indices.push(polygon[i]);
result.indices.push(polygon[i + 1]);
}
}
polygon.clear();
} else {
polygon.push(idx as u32);
}
}
result
}
pub fn apply_fbx_normals(result: &mut FbxImportResult, raw_normals: &[f64]) {
for (i, chunk) in raw_normals.chunks_exact(3).enumerate() {
if i < result.vertices.len() {
result.vertices[i].normal = [chunk[0] as f32, chunk[1] as f32, chunk[2] as f32];
}
}
}
pub fn validate_fbx_import(result: &FbxImportResult) -> FbxValidation {
let mut validation = FbxValidation {
vertex_count: result.vertices.len(),
index_count: result.indices.len(),
bone_count: result.bones.len(),
animation_count: result.animations.len(),
meshlet_compatible: true,
dreamlet_compatible: true,
errors: Vec::new(),
warnings: Vec::new(),
};
if result.vertices.is_empty() {
validation.errors.push("fbx_validate_no_vertices".into());
validation.meshlet_compatible = false;
}
if result.vertices.len() > MAX_FBX_VERTICES {
validation.errors.push(format!(
"fbx_validate_vertex_limit:{}>{MAX_FBX_VERTICES}",
result.vertices.len()
));
validation.meshlet_compatible = false;
}
if !result.indices.is_empty() && !result.indices.len().is_multiple_of(3) {
validation.errors.push("fbx_validate_indices_not_triangulated".into());
validation.meshlet_compatible = false;
}
for &idx in &result.indices {
if idx as usize >= result.vertices.len() {
validation.errors.push(format!("fbx_validate_index_oob:{idx}"));
validation.meshlet_compatible = false;
break;
}
}
if result.bones.len() > MAX_FBX_BONES {
validation.errors.push(format!(
"fbx_validate_bone_limit:{}>{MAX_FBX_BONES}",
result.bones.len()
));
validation.dreamlet_compatible = false;
}
for (i, v) in result.vertices.iter().enumerate() {
if !v.position.iter().all(|f| f.is_finite()) {
validation.errors.push(format!("fbx_validate_vertex_nan:{i}"));
validation.meshlet_compatible = false;
break;
}
}
validation.dreamlet_compatible = validation.meshlet_compatible && validation.errors.is_empty();
validation
}
pub fn fbx_vertices_to_positions(vertices: &[FbxVertex]) -> Vec<[f32; 3]> {
vertices.iter().map(|v| v.position).collect()
}
#[cfg(feature = "fbx-import")]
pub fn import_fbx_file(path: &std::path::Path) -> Result<FbxImportResult, String> {
let scene = dreamwell_ufbx::Scene::load_file(path).map_err(|e| format!("fbx_import_error:{e}"))?;
if !scene.is_valid() {
return Err("fbx_import_error:invalid_scene".into());
}
let mut result = FbxImportResult::default();
result.warnings.push(format!("fbx_import_loaded:{}", path.display()));
Ok(result)
}
pub fn fbx_vertex(position: [f32; 3], normal: [f32; 3]) -> FbxVertex {
FbxVertex {
position,
normal,
uv: [0.0, 0.0],
tangent: [1.0, 0.0, 0.0, 1.0],
color: [1.0, 1.0, 1.0, 1.0],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_import_validates_with_error() {
let result = FbxImportResult::default();
let v = validate_fbx_import(&result);
assert!(!v.meshlet_compatible);
assert!(v.errors.iter().any(|e| e.contains("no_vertices")));
}
#[test]
fn valid_triangle_validates() {
let result = FbxImportResult {
vertices: vec![
fbx_vertex([0.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
fbx_vertex([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
fbx_vertex([0.0, 1.0, 0.0], [0.0, 1.0, 0.0]),
],
indices: vec![0, 1, 2],
..Default::default()
};
let v = validate_fbx_import(&result);
assert!(v.meshlet_compatible);
assert!(v.dreamlet_compatible);
assert!(v.errors.is_empty());
}
#[test]
fn oob_index_fails() {
let result = FbxImportResult {
vertices: vec![fbx_vertex([0.0; 3], [0.0, 1.0, 0.0])],
indices: vec![0, 1, 2],
..Default::default()
};
let v = validate_fbx_import(&result);
assert!(!v.meshlet_compatible);
}
#[test]
fn bone_limit_check() {
let mut result = FbxImportResult {
vertices: vec![fbx_vertex([0.0; 3], [0.0, 1.0, 0.0]); 3],
indices: vec![0, 1, 2],
..Default::default()
};
for i in 0..300 {
result.bones.push(FbxBone {
name: format!("bone_{i}"),
parent_index: 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,
],
});
}
let v = validate_fbx_import(&result);
assert!(!v.dreamlet_compatible);
}
#[test]
fn nan_vertex_fails() {
let result = FbxImportResult {
vertices: vec![
fbx_vertex([f32::NAN, 0.0, 0.0], [0.0, 1.0, 0.0]),
fbx_vertex([1.0, 0.0, 0.0], [0.0, 1.0, 0.0]),
fbx_vertex([0.0, 1.0, 0.0], [0.0, 1.0, 0.0]),
],
indices: vec![0, 1, 2],
..Default::default()
};
let v = validate_fbx_import(&result);
assert!(!v.meshlet_compatible);
}
#[test]
fn parse_fbx_quad_geometry() {
let verts = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0];
let indices = [0i32, 1, 2, -4];
let result = parse_fbx_geometry(&verts, &indices);
assert_eq!(result.vertices.len(), 4);
assert_eq!(result.indices.len(), 6);
assert_eq!(result.indices, vec![0, 1, 2, 0, 2, 3]);
}
#[test]
fn parse_fbx_triangle_geometry() {
let verts = [0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0];
let indices = [0i32, 1, -3];
let result = parse_fbx_geometry(&verts, &indices);
assert_eq!(result.vertices.len(), 3);
assert_eq!(result.indices.len(), 3);
}
#[test]
fn apply_normals() {
let mut result = FbxImportResult {
vertices: vec![fbx_vertex([0.0; 3], [0.0; 3]), fbx_vertex([1.0; 3], [0.0; 3])],
..Default::default()
};
let normals = [0.0, 1.0, 0.0, 0.0, 0.0, 1.0];
apply_fbx_normals(&mut result, &normals);
assert_eq!(result.vertices[0].normal, [0.0, 1.0, 0.0]);
assert_eq!(result.vertices[1].normal, [0.0, 0.0, 1.0]);
}
#[test]
fn vertices_to_positions() {
let verts = vec![
fbx_vertex([1.0, 2.0, 3.0], [0.0; 3]),
fbx_vertex([4.0, 5.0, 6.0], [0.0; 3]),
];
let positions = fbx_vertices_to_positions(&verts);
assert_eq!(positions.len(), 2);
assert_eq!(positions[0], [1.0, 2.0, 3.0]);
}
#[test]
fn constants_valid() {
assert_eq!(MAX_FBX_BONES, 256);
assert_eq!(MAX_FBX_VERTICES, 1_000_000);
}
#[test]
fn skin_weight_default() {
let w = FbxSkinWeight::default();
assert_eq!(w.bone_indices, [0, 0, 0, 0]);
assert!((w.bone_weights[0] - 1.0).abs() < 0.001);
assert!((w.bone_weights[1]).abs() < 0.001);
}
#[test]
fn material_data_default() {
let m = FbxMaterialData::default();
assert!((m.roughness - 0.5).abs() < 0.001);
assert!((m.metallic).abs() < 0.001);
}
}