diff --git a/CHANGELOG.md b/CHANGELOG.md index 81acb91..db648bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Changelog +### 3.2.0 +- Added ability to load scenes with property stores (see `load_scene_with_props` example) + ### 3.1.1 - Fixing windows compilation. Thanks @Snowiiii for this. diff --git a/Cargo.toml b/Cargo.toml index 1a4762a..5e89cd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "russimp" -version = "3.1.1" +version = "3.2.0" authors = ["Jhonny Knaak de Vargas"] edition = "2021" license-file = "LICENSE" diff --git a/examples/load_scene_with_props.rs b/examples/load_scene_with_props.rs new file mode 100644 index 0000000..9d651f5 --- /dev/null +++ b/examples/load_scene_with_props.rs @@ -0,0 +1,46 @@ +use russimp::node::Node; +use russimp::property::Property; +use russimp::scene::PostProcess; +use russimp::sys::AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS; +use russimp::{property::PropertyStore, scene::Scene}; + +fn traverse_nodes(node: &Node, indent: String) { + println!("{}{}", indent, node.name); + for child in node.children.borrow().iter() { + traverse_nodes(&*child, format!(" {}", indent)); + } +} + +fn main() { + // NOTE: You can construct this from a HashMap + // or any iterator if you want. + // + // The cast here is only necessary because + // the array only contains one entry and rust + // tries to make an iterator returning a reference + // to a sized array instead of a slice. + let props: PropertyStore = [( + AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS as &[u8], + Property::Integer(0), + )] + .into_iter() + .into(); + + let scene = Scene::from_file_with_props( + "models/FBX/y_bot_run.fbx", + vec![ + PostProcess::Triangulate, + PostProcess::GenerateSmoothNormals, + PostProcess::FlipUVs, + PostProcess::FlipWindingOrder, + PostProcess::JoinIdenticalVertices, + PostProcess::OptimizeGraph, + ], + &props, + ) + .unwrap(); + + if let Some(root) = &scene.root { + traverse_nodes(&*root, String::from("")); + } +} diff --git a/models/COLLADA/blender_cube.dae b/models/COLLADA/blender_cube.dae new file mode 100644 index 0000000..b3f0ce1 --- /dev/null +++ b/models/COLLADA/blender_cube.dae @@ -0,0 +1,69 @@ + + + + + Blender User + Blender 4.0.2 commit date:2023-12-05, commit time:07:41, hash:9be62e85b727 + + 2024-02-02T16:02:18 + 2024-02-02T16:02:18 + + Z_UP + + + + + + + -1 -1 -1 -1 -1 1 -1 1 -1 -1 1 1 1 -1 -1 1 -1 1 1 1 -1 1 1 1 + + + + + + + + + + -1 0 0 0 1 0 1 0 0 0 -1 0 0 0 -1 0 0 1 + + + + + + + + + + 0.625 0 0.375 0.25 0.375 0 0.625 0.25 0.375 0.5 0.375 0.25 0.625 0.5 0.375 0.75 0.375 0.5 0.625 0.75 0.375 1 0.375 0.75 0.375 0.5 0.125 0.75 0.125 0.5 0.875 0.5 0.625 0.75 0.625 0.5 0.625 0 0.625 0.25 0.375 0.25 0.625 0.25 0.625 0.5 0.375 0.5 0.625 0.5 0.625 0.75 0.375 0.75 0.625 0.75 0.625 1 0.375 1 0.375 0.5 0.375 0.75 0.125 0.75 0.875 0.5 0.875 0.75 0.625 0.75 + + + + + + + + + + + + + + +

1 0 0 2 0 1 0 0 2 3 1 3 6 1 4 2 1 5 7 2 6 4 2 7 6 2 8 5 3 9 0 3 10 4 3 11 6 4 12 0 4 13 2 4 14 3 5 15 5 5 16 7 5 17 1 0 18 3 0 19 2 0 20 3 1 21 7 1 22 6 1 23 7 2 24 5 2 25 4 2 26 5 3 27 1 3 28 0 3 29 6 4 30 4 4 31 0 4 32 3 5 33 1 5 34 5 5 35

+
+
+
+
+ + + + 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 + + + + + + + +
\ No newline at end of file diff --git a/models/COLLADA/blender_plane.dae b/models/COLLADA/blender_plane.dae new file mode 100644 index 0000000..adafceb --- /dev/null +++ b/models/COLLADA/blender_plane.dae @@ -0,0 +1,69 @@ + + + + + Blender User + Blender 4.0.2 commit date:2023-12-05, commit time:07:41, hash:9be62e85b727 + + 2024-02-02T16:28:47 + 2024-02-02T16:28:47 + + Z_UP + + + + + + + -1 -1 0 1 -1 0 -1 1 0 1 1 0 + + + + + + + + + + 0 0 1 + + + + + + + + + + 1 0 0 1 0 0 1 0 1 1 0 1 + + + + + + + + + + + + + + +

1 0 0 2 0 1 0 0 2 1 0 3 3 0 4 2 0 5

+
+
+
+
+ + + + 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 + + + + + + + +
\ No newline at end of file diff --git a/models/FBX/cube_armature.fbx b/models/FBX/cube_armature.fbx new file mode 100644 index 0000000..d2c770e Binary files /dev/null and b/models/FBX/cube_armature.fbx differ diff --git a/models/FBX/y_bot_run.fbx b/models/FBX/y_bot_run.fbx new file mode 100644 index 0000000..53cc873 Binary files /dev/null and b/models/FBX/y_bot_run.fbx differ diff --git a/src/lib.rs b/src/lib.rs index 2cb27f7..c802eee 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ pub mod material; pub mod mesh; pub mod metadata; pub mod node; +pub mod property; pub mod scene; #[derive(Derivative)] diff --git a/src/property.rs b/src/property.rs new file mode 100644 index 0000000..1a0865b --- /dev/null +++ b/src/property.rs @@ -0,0 +1,327 @@ +use std::ffi::CStr; + +use russimp_sys::{ + aiCreatePropertyStore, aiMatrix4x4, aiPropertyStore, aiReleasePropertyStore, + aiSetImportPropertyFloat, aiSetImportPropertyInteger, aiSetImportPropertyMatrix, + aiSetImportPropertyString, aiString, +}; + +pub enum Property { + String(&'static str), + Float(f32), + Integer(i32), + Matrix([[f32; 4]; 4]), +} + +pub struct PropertyStore { + ptr: *mut aiPropertyStore, +} + +impl Drop for PropertyStore { + #[inline] + fn drop(&mut self) { + unsafe { aiReleasePropertyStore(self.ptr) }; + } +} + +impl Default for PropertyStore { + fn default() -> Self { + let ptr = unsafe { aiCreatePropertyStore() }; + Self { ptr } + } +} + +impl PropertyStore { + pub fn set_integer(&mut self, name: &[u8], value: i32) { + let c_name = CStr::from_bytes_until_nul(name).unwrap(); + unsafe { aiSetImportPropertyInteger(self.ptr, c_name.as_ptr(), value) }; + } + + pub fn set_float(&mut self, name: &[u8], value: f32) { + let c_name = CStr::from_bytes_until_nul(name).unwrap(); + unsafe { aiSetImportPropertyFloat(self.ptr, c_name.as_ptr(), value) }; + } + + pub fn set_string(&mut self, name: &[u8], value: &str) { + let c_name = CStr::from_bytes_until_nul(name).unwrap(); + let bytes: &[::std::os::raw::c_char] = unsafe { std::mem::transmute(value.as_bytes()) }; + let mut string = aiString { + length: bytes.len() as u32, + data: [0; 1024], + }; + let n = std::cmp::min(string.data.len(), bytes.len()); + string.data[0..n].copy_from_slice(&bytes[0..n]); + unsafe { aiSetImportPropertyString(self.ptr, c_name.as_ptr(), &string as *const aiString) }; + } + + pub fn set_matrix(&mut self, name: &[u8], value: [[f32; 4]; 4]) { + let c_name = CStr::from_bytes_until_nul(name).unwrap(); + // NOTE: Assuming column-major matrix + let matrix = aiMatrix4x4 { + a1: value[0][0], + a2: value[1][0], + a3: value[2][0], + a4: value[3][0], + b1: value[0][1], + b2: value[1][1], + b3: value[2][1], + b4: value[3][1], + c1: value[0][2], + c2: value[1][2], + c3: value[2][2], + c4: value[3][2], + d1: value[0][3], + d2: value[1][3], + d3: value[2][3], + d4: value[3][3], + }; + unsafe { + aiSetImportPropertyMatrix(self.ptr, c_name.as_ptr(), &matrix as *const aiMatrix4x4) + }; + } + + pub(crate) fn as_ptr(&self) -> *mut aiPropertyStore { + self.ptr + } +} + +impl> From for PropertyStore { + fn from(value: T) -> Self { + let mut props = Self::default(); + for (name, prop) in value { + match prop { + Property::String(v) => props.set_string(name, v), + Property::Float(v) => props.set_float(name, v), + Property::Integer(v) => props.set_integer(name, v), + Property::Matrix(v) => props.set_matrix(name, v), + } + } + props + } +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use crate::node::Node; + use crate::scene::{PostProcess, PostProcessSteps, Scene}; + use crate::sys::{ + AI_CONFIG_IMPORT_COLLADA_IGNORE_UP_DIRECTION, AI_CONFIG_IMPORT_COLLADA_USE_COLLADA_NAMES, + AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS, AI_CONFIG_IMPORT_FBX_READ_WEIGHTS, + AI_CONFIG_PP_OG_EXCLUDE_LIST, AI_CONFIG_PP_PTV_ADD_ROOT_TRANSFORMATION, + AI_CONFIG_PP_PTV_ROOT_TRANSFORMATION, + }; + use crate::{utils, RussimpError, Russult}; + + use super::{Property, PropertyStore}; + + fn load_scene_with_props( + model: &str, + flags: Option, + props: &PropertyStore, + from_buffer: bool, + ) -> Russult { + let model = utils::get_model(model); + let flags = flags.unwrap_or(vec![]); + if from_buffer { + let model_path = Path::new(model.as_str()); + let buffer = std::fs::read(model.as_str()) + .map_err(|_| RussimpError::Import(String::from("Failed to read file")))?; + let file_name = model_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or(""); + Scene::from_buffer_with_props(buffer.as_slice(), flags, file_name, props) + } else { + Scene::from_file_with_props(model.as_str(), flags, props) + } + } + + #[test] + fn import_fbx_without_preserving_pivots() { + fn traverse_check_fbx_node(node: &Node) -> bool { + if node.name.ends_with("_$AssimpFbx$_RotationPivot") + || node.name.ends_with("_$AssimpFbx$_RotationOffset") + || node.name.ends_with("_$AssimpFbx$_PreRotation") + || node.name.ends_with("_$AssimpFbx$_PostRotation") + || node.name.ends_with("_$AssimpFbx$_ScalingPivot") + || node.name.ends_with("_$AssimpFbx$_ScalingOffset") + || node.name.ends_with("_$AssimpFbx$_Translation") + || node.name.ends_with("_$AssimpFbx$_Scaling") + || node.name.ends_with("_$AssimpFbx$_Rotation") + { + return false; + } + for child in node.children.borrow().iter() { + if !traverse_check_fbx_node(&*child) { + return false; + } + } + true + } + + let props: PropertyStore = [( + AI_CONFIG_IMPORT_FBX_PRESERVE_PIVOTS as &[u8], + Property::Integer(0), + )] + .into_iter() + .into(); + let scene = load_scene_with_props("models/FBX/y_bot_run.fbx", None, &props, false).unwrap(); + + // NOTE: A scene with collapsed FBX transforms should not contain + // nodes with names like "_$AssimpFbx$_" + // https://github.com/assimp/assimp/blob/master/code/AssetLib/FBX/FBXImportSettings.h#L141 + if let Some(root) = scene.root { + assert!(traverse_check_fbx_node(&root)); + } + } + + #[test] + fn import_fbx_without_weights() { + let props: PropertyStore = [( + AI_CONFIG_IMPORT_FBX_READ_WEIGHTS as &[u8], + Property::Integer(0), + )] + .into_iter() + .into(); + let scene = load_scene_with_props("models/FBX/y_bot_run.fbx", None, &props, true).unwrap(); + assert_eq!(scene.meshes.len(), 2); + for mesh in &scene.meshes { + for bone in &mesh.bones { + assert_eq!(bone.weights.len(), 0); + } + } + } + + #[test] + fn import_collada_with_ignore_up_direction() { + let props: PropertyStore = [( + AI_CONFIG_IMPORT_COLLADA_IGNORE_UP_DIRECTION as &[u8], + Property::Integer(1), + )] + .into_iter() + .into(); + let scene = + load_scene_with_props("models/COLLADA/blender_cube.dae", None, &props, false).unwrap(); + + // NOTE: Ignoring the COLLADA file's UP direction should yield + // an identity matrix as the root node transformation, meaning + // we are now using blender coordinate system (+Z up instead of +Y up) + if let Some(root) = scene.root { + let is_identity = root.transformation.a1 == 1.0 + && root.transformation.a2 == 0.0 + && root.transformation.a3 == 0.0 + && root.transformation.a4 == 0.0 + && root.transformation.b1 == 0.0 + && root.transformation.b2 == 1.0 + && root.transformation.b3 == 0.0 + && root.transformation.b4 == 0.0 + && root.transformation.c1 == 0.0 + && root.transformation.c2 == 0.0 + && root.transformation.c3 == 1.0 + && root.transformation.c4 == 0.0 + && root.transformation.d1 == 0.0 + && root.transformation.d2 == 0.0 + && root.transformation.d3 == 0.0 + && root.transformation.d4 == 1.0; + assert!(is_identity); + } + } + + #[test] + fn import_collada_with_use_collada_names() { + let props: PropertyStore = [( + AI_CONFIG_IMPORT_COLLADA_USE_COLLADA_NAMES as &[u8], + Property::Integer(1), + )] + .into_iter() + .into(); + let scene = + load_scene_with_props("models/COLLADA/blender_cube.dae", None, &props, true).unwrap(); + + // NOTE: Importing a COLLADA file with this option enabled + // should yield the real mesh names like: "Cube.001" + // instead of "Cube_001-mesh" as the importer should use + // the geometry's `name` property instead of `id` + assert_eq!(scene.meshes.len(), 1); + assert_eq!(scene.meshes[0].name, "Cube.001"); + } + + #[test] + fn import_pp_ptv_root_transformation() { + let props: PropertyStore = [ + ( + AI_CONFIG_IMPORT_COLLADA_IGNORE_UP_DIRECTION, + Property::Integer(1), + ), + ( + AI_CONFIG_PP_PTV_ADD_ROOT_TRANSFORMATION, + Property::Integer(1), + ), + ( + AI_CONFIG_PP_PTV_ROOT_TRANSFORMATION, + Property::Matrix([ + [1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, -1.0, 0.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0], + ]), + ), + ] + .into_iter() + .into(); + let scene = load_scene_with_props( + "models/COLLADA/blender_plane.dae", + Some(vec![PostProcess::PreTransformVertices]), + &props, + false, + ) + .unwrap(); + + // NOTE: The exported blender plane's normal is facing +Z (ignoring COLLADA up direction) + // If we pre-transform its vertices with the above matrix, + // its normal should be aligned with the Y axis, + // i.e. all the Y coordinates of its vertices should be equal to 0 + assert_eq!(scene.meshes.len(), 1); + for vertex in &scene.meshes[0].vertices { + assert_eq!(vertex.y, 0.0); + } + } + + #[test] + fn import_pp_og_exclude_list() { + fn traverse_find_bone_end(node: &Node) -> bool { + if node.name.as_str() == "Bone_end" { + return true; + } + for child in node.children.borrow().iter() { + if traverse_find_bone_end(&*child) { + return true; + } + } + return false; + } + + let props: PropertyStore = [( + AI_CONFIG_PP_OG_EXCLUDE_LIST as &[u8], + Property::String("Bone_end"), + )] + .into_iter() + .into(); + let scene = load_scene_with_props( + "models/FBX/cube_armature.fbx", + Some(vec![PostProcess::OptimizeGraph]), + &props, + true, + ) + .unwrap(); + + // NOTE: Exported FBX file contains a cube with a single bone. + // The bone's end is also exported and is technically unused, + // but setting this option should preserve it in the hierarchy. + if let Some(root) = &scene.root { + assert!(traverse_find_bone_end(&*root)); + } + } +} diff --git a/src/scene.rs b/src/scene.rs index 8a7ae8d..8091fab 100644 --- a/src/scene.rs +++ b/src/scene.rs @@ -16,6 +16,8 @@ use std::{ rc::Rc, }; +use self::property::PropertyStore; + #[derive(Derivative)] #[derivative(Debug)] pub struct Scene { @@ -426,16 +428,31 @@ impl Scene { pub fn from_file(file_path: &str, flags: PostProcessSteps) -> Russult { let bitwise_flag = flags.into_iter().fold(0, |acc, x| acc | (x as u32)); let file_path = CString::new(file_path).unwrap(); - - let raw_scene = Scene::get_scene_from_file(file_path, bitwise_flag); - if raw_scene.is_none() { - return Err(Scene::get_error()); + match Scene::get_scene_from_file(file_path, bitwise_flag) { + Some(raw_scene) => { + let result = Scene::new(raw_scene); + Scene::drop_scene(raw_scene); + result + } + None => Err(Scene::get_error()), } + } - let result = Scene::new(raw_scene.unwrap()); - Scene::drop_scene(raw_scene); - - result + pub fn from_file_with_props( + file_path: &str, + flags: PostProcessSteps, + props: &PropertyStore, + ) -> Russult { + let bitwise_flag = flags.into_iter().fold(0, |acc, x| acc | (x as u32)); + let file_path = CString::new(file_path).unwrap(); + match Scene::get_scene_from_file_with_props(file_path, bitwise_flag, Some(props)) { + Some(raw_scene) => { + let result = Scene::new(raw_scene); + Scene::drop_scene(raw_scene); + result + } + None => Err(Scene::get_error()), + } } pub fn from_file_system( @@ -445,44 +462,102 @@ impl Scene { ) -> Russult { let bitwise_flag = flags.into_iter().fold(0, |acc, x| acc | (x as u32)); let file_path = CString::new(file_path).unwrap(); - - let raw_scene = Scene::get_scene_from_filesystem(file_path, bitwise_flag, file_io); - if raw_scene.is_none() { - return Err(Scene::get_error()); + match Scene::get_scene_from_filesystem(file_path, bitwise_flag, file_io) { + Some(raw_scene) => { + let result = Scene::new(raw_scene); + Scene::drop_scene(raw_scene); + result + } + None => Err(Scene::get_error()), } + } - let result = Scene::new(raw_scene.unwrap()); - Scene::drop_scene(raw_scene); - - result + pub fn from_file_system_with_props( + file_path: &str, + flags: PostProcessSteps, + file_io: &mut T, + props: &PropertyStore, + ) -> Russult { + let bitwise_flag = flags.into_iter().fold(0, |acc, x| acc | (x as u32)); + let file_path = CString::new(file_path).unwrap(); + match Scene::get_scene_from_filesystem_with_props( + file_path, + bitwise_flag, + file_io, + Some(props), + ) { + Some(raw_scene) => { + let result = Scene::new(raw_scene); + Scene::drop_scene(raw_scene); + result + } + None => Err(Scene::get_error()), + } } + pub fn from_buffer(buffer: &[u8], flags: PostProcessSteps, hint: &str) -> Russult { let bitwise_flag = flags.into_iter().fold(0, |acc, x| acc | (x as u32)); let hint = CString::new(hint).unwrap(); - - let raw_scene = Scene::get_scene_from_file_from_memory(buffer, bitwise_flag, hint); - if raw_scene.is_none() { - return Err(Scene::get_error()); + match Scene::get_scene_from_file_from_memory(buffer, bitwise_flag, hint) { + Some(raw_scene) => { + let result = Scene::new(raw_scene); + Scene::drop_scene(raw_scene); + result + } + None => Err(Scene::get_error()), } + } - let result = Scene::new(raw_scene.unwrap()); - Scene::drop_scene(raw_scene); - - result + pub fn from_buffer_with_props( + buffer: &[u8], + flags: PostProcessSteps, + hint: &str, + props: &PropertyStore, + ) -> Russult { + let bitwise_flag = flags.into_iter().fold(0, |acc, x| acc | (x as u32)); + let hint = CString::new(hint).unwrap(); + match Scene::get_scene_from_file_from_memory_with_props( + buffer, + bitwise_flag, + hint, + Some(props), + ) { + Some(raw_scene) => { + let result = Scene::new(raw_scene); + Scene::drop_scene(raw_scene); + result + } + None => Err(Scene::get_error()), + } } #[inline] - fn drop_scene(scene: Option<&aiScene>) { - if let Some(content) = scene { - unsafe { - aiReleaseImport(content); - } + fn drop_scene(scene: &aiScene) { + unsafe { + aiReleaseImport(scene); } } #[inline] fn get_scene_from_file<'a>(string: CString, flags: u32) -> Option<&'a aiScene> { - unsafe { aiImportFile(string.as_ptr(), flags).as_ref() } + Self::get_scene_from_file_with_props(string, flags, None) + } + + #[inline] + fn get_scene_from_file_with_props<'a>( + string: CString, + flags: u32, + props: Option<&PropertyStore>, + ) -> Option<&'a aiScene> { + unsafe { + aiImportFileExWithProperties( + string.as_ptr(), + flags, + std::ptr::null_mut(), + props.map(|p| p.as_ptr()).unwrap_or(std::ptr::null_mut()), + ) + .as_ref() + } } #[inline] @@ -490,9 +565,27 @@ impl Scene { string: CString, flags: u32, fs: &mut T, + ) -> Option<&'a aiScene> { + Self::get_scene_from_filesystem_with_props(string, flags, fs, None) + } + + #[inline] + fn get_scene_from_filesystem_with_props<'a, T: FileSystem>( + string: CString, + flags: u32, + fs: &mut T, + props: Option<&PropertyStore>, ) -> Option<&'a aiScene> { let mut file_io = FileOperationsWrapper::new(fs); - unsafe { aiImportFileEx(string.as_ptr(), flags, file_io.ai_file()).as_ref() } + unsafe { + aiImportFileExWithProperties( + string.as_ptr(), + flags, + file_io.ai_file(), + props.map(|p| p.as_ptr()).unwrap_or(std::ptr::null_mut()), + ) + .as_ref() + } } #[inline] @@ -500,13 +593,24 @@ impl Scene { buffer: &[u8], flags: u32, hint: CString, + ) -> Option<&'a aiScene> { + Self::get_scene_from_file_from_memory_with_props(buffer, flags, hint, None) + } + + #[inline] + fn get_scene_from_file_from_memory_with_props<'a>( + buffer: &[u8], + flags: u32, + hint: CString, + props: Option<&PropertyStore>, ) -> Option<&'a aiScene> { unsafe { - aiImportFileFromMemory( + aiImportFileFromMemoryWithProperties( buffer.as_ptr() as *const _, buffer.len() as _, flags, hint.as_ptr(), + props.map(|p| p.as_ptr()).unwrap_or(std::ptr::null_mut()), ) .as_ref() }