diff --git a/cursebreaker-parser/src/main.rs b/cursebreaker-parser/src/main.rs index 9207648..c9df9e2 100644 --- a/cursebreaker-parser/src/main.rs +++ b/cursebreaker-parser/src/main.rs @@ -40,13 +40,19 @@ fn main() -> Result<(), Box> { // Parse the scene using the project match project.parse_scene(scene_path) { - Ok(scene) => { + Ok(mut scene) => { info!("✅ Scene parsed successfully!"); info!(" Total entities: {}", scene.entity_map.len()); + // Post-processing: Compute world transforms + info!("🔄 Computing world transforms..."); + unity_parser::compute_world_transforms(&mut scene.world, &scene.entity_map); + info!(" ✓ World transforms computed"); + // Get views for component types we need let resource_view = scene.world.borrow::(); let transform_view = scene.world.borrow::(); + let world_transform_view = scene.world.borrow::(); let gameobject_view = scene.world.borrow::(); info!("DEBUG: entity_map size: {}", scene.entity_map.len()); @@ -72,17 +78,24 @@ fn main() -> Result<(), Box> { for entity in scene.entity_map.values() { if let Some(resource) = resource_view.get(*entity) { let transform = transform_view.get(*entity); + let world_transform = world_transform_view.get(*entity); let game_object = gameobject_view.get(*entity); let name = game_object .and_then(|go| go.name()) .unwrap_or("(unnamed)"); - let position = transform + let local_position = transform .and_then(|t| t.local_position()) .map(|p| (p.x, p.y, p.z)); - found_resources.push((name.to_string(), resource.clone(), position)); + let world_position = world_transform + .map(|wt| { + let pos = wt.position(); + (pos.x, pos.y, pos.z) + }); + + found_resources.push((name.to_string(), resource.clone(), local_position, world_position)); } } @@ -91,14 +104,19 @@ fn main() -> Result<(), Box> { if !found_resources.is_empty() { // Display resources in console - for (name, resource, position) in &found_resources { + for (name, resource, local_pos, world_pos) in &found_resources { info!(" 📦 Resource: \"{}\"", name); info!(" • typeId: {}", resource.type_id); info!(" • maxHealth: {}", resource.max_health); - if let Some((x, y, z)) = position { - info!(" • Position: ({:.2}, {:.2}, {:.2})", x, y, z); + if let Some((x, y, z)) = local_pos { + info!(" • Local Position: ({:.2}, {:.2}, {:.2})", x, y, z); } else { - info!(" • Position: (no transform)"); + info!(" • Local Position: (no transform)"); + } + if let Some((x, y, z)) = world_pos { + info!(" • World Position: ({:.2}, {:.2}, {:.2})", x, y, z); + } else { + info!(" • World Position: (not computed)"); } } @@ -114,14 +132,19 @@ fn main() -> Result<(), Box> { writeln!(output_file, "{}", "-".repeat(70))?; writeln!(output_file)?; - for (name, resource, position) in &found_resources { + for (name, resource, local_pos, world_pos) in &found_resources { writeln!(output_file, "Resource: {}", name)?; writeln!(output_file, " TypeID: {}", resource.type_id)?; writeln!(output_file, " MaxHealth: {}", resource.max_health)?; - if let Some((x, y, z)) = position { - writeln!(output_file, " Position: ({:.6}, {:.6}, {:.6})", x, y, z)?; + if let Some((x, y, z)) = local_pos { + writeln!(output_file, " Local Position: ({:.6}, {:.6}, {:.6})", x, y, z)?; } else { - writeln!(output_file, " Position: N/A")?; + writeln!(output_file, " Local Position: N/A")?; + } + if let Some((x, y, z)) = world_pos { + writeln!(output_file, " World Position: ({:.6}, {:.6}, {:.6})", x, y, z)?; + } else { + writeln!(output_file, " World Position: N/A")?; } writeln!(output_file)?; } diff --git a/unity-parser/src/ecs/builder.rs b/unity-parser/src/ecs/builder.rs index 097286f..438fa0a 100644 --- a/unity-parser/src/ecs/builder.rs +++ b/unity-parser/src/ecs/builder.rs @@ -12,6 +12,7 @@ use crate::{Error, Result}; use sparsey::{Entity, World}; use std::cell::RefCell; use std::collections::HashMap; +use crate::post_processing::WorldTransform; /// Build a Sparsey ECS World from raw Unity documents /// @@ -40,7 +41,8 @@ pub fn build_world_from_documents( .register::() .register::() .register::() - .register::(); + .register::() + .register::(); // Register all custom components from inventory for reg in inventory::iter:: { @@ -161,8 +163,6 @@ pub fn build_world_from_documents( // PASS 3: Execute all deferred linking callbacks let entity_map = linking_ctx.into_inner().execute_callbacks(&mut world); - info!("DEBUG (build_world_from_documents): Final scene entity_map size: {}", entity_map.len()); - Ok((world, entity_map)) } @@ -314,14 +314,9 @@ pub fn build_world_from_documents_into( // PASS 3: Execute all deferred linking callbacks let final_entity_map = linking_ctx.into_inner().execute_callbacks(world); - info!("DEBUG (build_world_from_documents_into): Spawned {} entities", spawned_entities.len()); - info!("DEBUG (build_world_from_documents_into): final_entity_map size: {}, entity_map size before extend: {}", final_entity_map.len(), entity_map.len()); - // Update caller's entity_map with new mappings entity_map.extend(final_entity_map); - info!("DEBUG (build_world_from_documents_into): entity_map size after extend: {}", entity_map.len()); - Ok(spawned_entities) } @@ -374,17 +369,7 @@ fn attach_component( .copied(); match entity_opt { - Some(e) => { - // Debug logging for Interactable_Resource MonoBehaviours - if doc.class_name == "MonoBehaviour" { - if let Some(script_ref) = yaml_helpers::get_external_ref(yaml, "m_Script") { - if script_ref.guid.as_str() == "d39ddbf1c2c3d1a4baa070e5e76548bd" { - info!("DEBUG: Found GameObject entity {:?} for Interactable_Resource MonoBehaviour (FileID: {})", e, doc.file_id); - } - } - } - e - } + Some(e) => e, None => { return Err(Error::reference_error(format!("Unknown GameObject: {}", r.file_id))); } @@ -447,11 +432,6 @@ fn attach_component( if let Some(script_ref) = yaml_helpers::get_external_ref(yaml, "m_Script") { // Resolve GUID to class name if let Some(class_name) = resolver.resolve_class_name(script_ref.guid.as_str()) { - // Debug logging for specific GUID - if script_ref.guid.as_str() == "d39ddbf1c2c3d1a4baa070e5e76548bd" { - info!("DEBUG: Found Interactable_Resource GUID! Resolved to class name: {}", class_name); - } - // Try to find a registered custom component with this class name let mut found_custom = false; for reg in inventory::iter:: { @@ -461,7 +441,6 @@ fn attach_component( if (reg.parse_and_insert)(yaml, &ctx, world, entity) { // Successfully parsed and inserted linking_ctx.borrow_mut().entity_map_mut().insert(doc.file_id, entity); - info!("DEBUG: Successfully inserted component: {}", class_name); } break; } diff --git a/unity-parser/src/lib.rs b/unity-parser/src/lib.rs index 8be164f..38b398e 100644 --- a/unity-parser/src/lib.rs +++ b/unity-parser/src/lib.rs @@ -31,6 +31,7 @@ pub mod log; pub mod macros; pub mod model; pub mod parser; +pub mod post_processing; // TODO: Update project module to work with new UnityFile enum architecture // pub mod project; pub mod property; @@ -43,6 +44,7 @@ pub use parser::{ find_project_root, meta::MetaFile, parse_unity_file, parse_unity_file_filtered, GuidResolver, PrefabGuidResolver, }; +pub use post_processing::{compute_world_transforms, WorldTransform}; pub use property::PropertyValue; pub use types::{ get_class_name, get_type_id, Color, ComponentContext, ComponentRegistration, EcsInsertable, diff --git a/unity-parser/src/post_processing/mod.rs b/unity-parser/src/post_processing/mod.rs new file mode 100644 index 0000000..7138070 --- /dev/null +++ b/unity-parser/src/post_processing/mod.rs @@ -0,0 +1,8 @@ +//! Post-processing features for Unity scenes +//! +//! This module provides various post-processing operations that can be applied +//! to parsed Unity scenes to enhance or compute additional data. + +mod world_transform; + +pub use world_transform::{compute_world_transforms, WorldTransform}; diff --git a/unity-parser/src/post_processing/world_transform.rs b/unity-parser/src/post_processing/world_transform.rs new file mode 100644 index 0000000..225d4f2 --- /dev/null +++ b/unity-parser/src/post_processing/world_transform.rs @@ -0,0 +1,252 @@ +//! World Transform Computation +//! +//! This module provides functionality to compute world-space (global) transforms +//! from local transforms in a Unity scene hierarchy. + +use glam::Mat4; +use sparsey::{Entity, World}; +use std::collections::HashMap; + +use crate::types::{Transform, RectTransform}; +use crate::FileID; + +/// A component that stores the world-space transform matrix +/// +/// This is computed from the local transform hierarchy and represents +/// the final global position, rotation, and scale of an entity. +/// +/// Note: This is a computed component added during post-processing, +/// not a Unity component parsed from YAML. +#[derive(Debug, Clone)] +pub struct WorldTransform { + /// 4x4 transformation matrix in world space + pub matrix: Mat4, +} + +impl WorldTransform { + /// Create a new WorldTransform from a matrix + pub fn new(matrix: Mat4) -> Self { + Self { matrix } + } + + /// Create an identity WorldTransform + pub fn identity() -> Self { + Self { + matrix: Mat4::IDENTITY, + } + } + + /// Get the world position from the matrix + pub fn position(&self) -> glam::Vec3 { + self.matrix.col(3).truncate() + } + + /// Get the world rotation (as a quaternion) from the matrix + pub fn rotation(&self) -> glam::Quat { + glam::Quat::from_mat4(&self.matrix) + } + + /// Get the world scale from the matrix + pub fn scale(&self) -> glam::Vec3 { + glam::Vec3::new( + self.matrix.col(0).truncate().length(), + self.matrix.col(1).truncate().length(), + self.matrix.col(2).truncate().length(), + ) + } +} + +/// Compute world transforms for all entities in the scene +/// +/// This function traverses the transform hierarchy and computes the world-space +/// transform matrix for each entity by combining local transforms with their +/// parent's world transform. +/// +/// # Arguments +/// +/// * `world` - The ECS world containing Transform components +/// * `entity_map` - Mapping from Unity FileID to ECS Entity +/// +/// # Example +/// +/// ```no_run +/// use unity_parser::UnityProject; +/// use unity_parser::post_processing::compute_world_transforms; +/// use std::path::Path; +/// +/// let project = UnityProject::from_path("/path/to/project")?; +/// let mut scene = project.parse_scene("scene.unity")?; +/// +/// // Compute world transforms as a post-processing step +/// compute_world_transforms(&mut scene.world, &scene.entity_map); +/// +/// // Now you can query WorldTransform components +/// let world_transforms = scene.world.borrow::(); +/// # Ok::<(), unity_parser::Error>(()) +/// ``` +pub fn compute_world_transforms(world: &mut World, entity_map: &HashMap) { + // Find all root transforms (transforms with no parent) + let mut root_entities = Vec::new(); + + { + let transform_view = world.borrow::(); + for &entity in entity_map.values() { + if let Some(transform) = transform_view.get(entity) { + if transform.parent().is_none() { + root_entities.push(entity); + } + } + } + } + + // Recursively compute world transforms starting from roots + for root_entity in root_entities { + compute_world_transform_recursive(world, root_entity, Mat4::IDENTITY); + } + + // Handle RectTransforms separately (they might have their own hierarchy) + let mut rect_root_entities = Vec::new(); + + { + let rect_transform_view = world.borrow::(); + for &entity in entity_map.values() { + if let Some(rect_transform) = rect_transform_view.get(entity) { + if rect_transform.transform().parent().is_none() { + rect_root_entities.push(entity); + } + } + } + } + + for root_entity in rect_root_entities { + compute_rect_world_transform_recursive(world, root_entity, Mat4::IDENTITY); + } +} + +/// Recursively compute world transform for a Transform entity and its children +fn compute_world_transform_recursive(world: &mut World, entity: Entity, parent_matrix: Mat4) { + // Get the local transform + let local_matrix = { + let transform_view = world.borrow::(); + if let Some(transform) = transform_view.get(entity) { + compute_local_matrix( + transform.local_position(), + transform.local_rotation(), + transform.local_scale(), + ) + } else { + // No transform component, skip this entity + return; + } + }; + + // Compute world matrix = parent * local + let world_matrix = parent_matrix * local_matrix; + + // Get children before inserting component + let children = { + let transform_view = world.borrow::(); + transform_view + .get(entity) + .map(|t| t.children().to_vec()) + .unwrap_or_default() + }; + + // Insert the WorldTransform component (will auto-register on first insert) + world.insert(entity, (WorldTransform::new(world_matrix),)); + + // Recursively process children + for child in children { + compute_world_transform_recursive(world, child, world_matrix); + } +} + +/// Recursively compute world transform for a RectTransform entity and its children +fn compute_rect_world_transform_recursive(world: &mut World, entity: Entity, parent_matrix: Mat4) { + // Get the local transform from RectTransform + let (local_matrix, children) = { + let rect_transform_view = world.borrow::(); + if let Some(rect_transform) = rect_transform_view.get(entity) { + let transform = rect_transform.transform(); + let local = compute_local_matrix( + transform.local_position(), + transform.local_rotation(), + transform.local_scale(), + ); + let children = transform.children().to_vec(); + (local, children) + } else { + // No rect transform component, skip this entity + return; + } + }; + + // Compute world matrix = parent * local + let world_matrix = parent_matrix * local_matrix; + + // Insert the WorldTransform component (will auto-register on first insert) + world.insert(entity, (WorldTransform::new(world_matrix),)); + + // Recursively process children + for child in children { + // Children might be either Transform or RectTransform + // Try RectTransform first, then Transform + let has_rect_transform = world.borrow::().contains(child); + if has_rect_transform { + compute_rect_world_transform_recursive(world, child, world_matrix); + } else { + compute_world_transform_recursive(world, child, world_matrix); + } + } +} + +/// Compute a local transformation matrix from position, rotation, and scale +fn compute_local_matrix( + position: Option<&crate::Vector3>, + rotation: Option<&crate::Quaternion>, + scale: Option<&crate::Vector3>, +) -> Mat4 { + // Default values if not present + let pos = position + .map(|p| glam::Vec3::new(p.x, p.y, p.z)) + .unwrap_or(glam::Vec3::ZERO); + + let rot = rotation + .map(|q| glam::Quat::from_xyzw(q.x, q.y, q.z, q.w)) + .unwrap_or(glam::Quat::IDENTITY); + + let scl = scale + .map(|s| glam::Vec3::new(s.x, s.y, s.z)) + .unwrap_or(glam::Vec3::ONE); + + // Compute TRS matrix + Mat4::from_scale_rotation_translation(scl, rot, pos) +} + +#[cfg(test)] +mod tests { + use super::*; + use glam::Vec3; + + #[test] + fn test_local_matrix_identity() { + let matrix = compute_local_matrix(None, None, None); + assert_eq!(matrix, Mat4::IDENTITY); + } + + #[test] + fn test_local_matrix_translation() { + let pos = crate::Vector3::new(1.0, 2.0, 3.0); + let matrix = compute_local_matrix(Some(&pos), None, None); + let expected = Mat4::from_translation(Vec3::new(1.0, 2.0, 3.0)); + assert_eq!(matrix, expected); + } + + #[test] + fn test_world_transform_position() { + let matrix = Mat4::from_translation(Vec3::new(5.0, 10.0, 15.0)); + let wt = WorldTransform::new(matrix); + let pos = wt.position(); + assert_eq!(pos, Vec3::new(5.0, 10.0, 15.0)); + } +}