//! Mesh processing functionality for FBX files. use crate::error::FbxError; use crate::label::FbxAssetLabel; use crate::loader::FbxLoaderSettings; use bevy::asset::{Handle, LoadContext}; use bevy::prelude::*; use bevy::mesh::{Indices, PrimitiveTopology, VertexAttributeValues}; use std::collections::HashMap; /// Process all meshes from the FBX scene. pub fn process_meshes( scene: &ufbx::Scene, settings: &FbxLoaderSettings, load_context: &mut LoadContext, ) -> Result< ( Vec>, HashMap, Handle>, Vec, Vec>, ), FbxError, > { let mut meshes = Vec::new(); let mut named_meshes = HashMap::new(); let mut transforms = Vec::new(); let mut mesh_material_info = Vec::new(); for (index, node) in scene.nodes.as_ref().iter().enumerate() { let Some(mesh_ref) = node.mesh.as_ref() else { continue; }; let mesh = mesh_ref.as_ref(); if mesh.num_vertices == 0 || mesh.faces.as_ref().is_empty() { continue; } // Group faces by material let material_groups = group_faces_by_material(mesh); // Create mesh for each material group for (material_idx, indices) in material_groups.iter() { let mesh_handle = create_mesh_from_group( mesh, indices, index, *material_idx, settings, load_context, )?; if *material_idx == 0 && !node.element.name.is_empty() { named_meshes.insert(Box::from(node.element.name.as_ref()), mesh_handle.clone()); } meshes.push(mesh_handle); transforms.push(node.geometry_to_world); let material_name = if *material_idx < mesh.materials.len() { mesh.materials[*material_idx].element.name.to_string() } else { "default".to_string() }; mesh_material_info.push(vec![material_name]); } } Ok((meshes, named_meshes, transforms, mesh_material_info)) } /// Group mesh faces by material index. pub fn group_faces_by_material(mesh: &ufbx::Mesh) -> HashMap> { let mut material_groups: HashMap> = HashMap::new(); let mut scratch = Vec::new(); if mesh.materials.is_empty() { // No materials - create single group let mut all_indices = Vec::new(); for &face in mesh.faces.as_ref().iter() { scratch.clear(); ufbx::triangulate_face_vec(&mut scratch, mesh, face); for idx in &scratch { if (*idx as usize) < mesh.vertex_indices.len() { all_indices.push(mesh.vertex_indices[*idx as usize]); } } } material_groups.insert(0, all_indices); } else { // Group by material for (face_idx, &face) in mesh.faces.as_ref().iter().enumerate() { let material_idx = if !mesh.face_material.is_empty() && face_idx < mesh.face_material.len() { mesh.face_material[face_idx] as usize } else { 0 }; scratch.clear(); ufbx::triangulate_face_vec(&mut scratch, mesh, face); let indices = material_groups.entry(material_idx).or_insert_with(Vec::new); for idx in &scratch { if (*idx as usize) < mesh.vertex_indices.len() { indices.push(mesh.vertex_indices[*idx as usize]); } } } } material_groups } /// Create a Bevy mesh from a material group. pub fn create_mesh_from_group( ufbx_mesh: &ufbx::Mesh, indices: &[u32], mesh_index: usize, material_index: usize, settings: &FbxLoaderSettings, load_context: &mut LoadContext, ) -> Result, FbxError> { let label = FbxAssetLabel::Mesh(mesh_index * 1000 + material_index).to_string(); let handle = load_context.labeled_asset_scope(label, |_| { let mut bevy_mesh = Mesh::new(PrimitiveTopology::TriangleList, settings.load_meshes); // Positions let positions: Vec<[f32; 3]> = ufbx_mesh .vertex_position .values .as_ref() .iter() .map(|v| [v.x as f32, v.y as f32, v.z as f32]) .collect(); bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); // Normals if ufbx_mesh.vertex_normal.exists { let normals: Vec<[f32; 3]> = (0..ufbx_mesh.num_vertices) .map(|i| { let n = ufbx_mesh.vertex_normal[i]; [n.x as f32, n.y as f32, n.z as f32] }) .collect(); bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, normals); } // UVs if ufbx_mesh.vertex_uv.exists { let uvs: Vec<[f32; 2]> = (0..ufbx_mesh.num_vertices) .map(|i| { let uv = ufbx_mesh.vertex_uv[i]; [uv.x as f32, uv.y as f32] }) .collect(); bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); } // Skinning if !ufbx_mesh.skin_deformers.is_empty() { process_skinning_data(ufbx_mesh, &mut bevy_mesh); } // Indices bevy_mesh.insert_indices(Indices::U32(indices.to_vec())); Ok::<_, FbxError>(bevy_mesh) })?; Ok(handle) } /// Process skinning data for a mesh. pub fn process_skinning_data(ufbx_mesh: &ufbx::Mesh, bevy_mesh: &mut Mesh) { let skin_deformer = &ufbx_mesh.skin_deformers[0]; let mut joint_indices = vec![[0u16; 4]; ufbx_mesh.num_vertices]; let mut joint_weights = vec![[0.0f32; 4]; ufbx_mesh.num_vertices]; for vertex_index in 0..ufbx_mesh.num_vertices { let mut weight_count = 0; let mut total_weight = 0.0f32; for (cluster_index, cluster) in skin_deformer.clusters.iter().enumerate() { if weight_count >= 4 { break; } for (i, &vert_idx) in cluster.vertices.iter().enumerate() { if vert_idx as usize == vertex_index && i < cluster.weights.len() { let weight = cluster.weights[i] as f32; if weight > 0.0 { joint_indices[vertex_index][weight_count] = cluster_index as u16; joint_weights[vertex_index][weight_count] = weight; total_weight += weight; weight_count += 1; break; } } } } // Normalize weights if total_weight > 0.0 { for i in 0..weight_count { joint_weights[vertex_index][i] /= total_weight; } } } bevy_mesh.insert_attribute( Mesh::ATTRIBUTE_JOINT_INDEX, VertexAttributeValues::Uint16x4(joint_indices), ); bevy_mesh.insert_attribute(Mesh::ATTRIBUTE_JOINT_WEIGHT, joint_weights); }