diff --git a/perro_source/core/perro_nodes/src/nodes/node_3d/bone_attachment_3d.rs b/perro_source/core/perro_nodes/src/nodes/node_3d/bone_attachment_3d.rs new file mode 100644 index 000000000..e86e7de8c --- /dev/null +++ b/perro_source/core/perro_nodes/src/nodes/node_3d/bone_attachment_3d.rs @@ -0,0 +1,68 @@ +use crate::node_3d::Node3D; +use perro_ids::NodeID; +use std::ops::{Deref, DerefMut}; + +pub type BoneIndex = i32; + +#[derive(Clone, Debug)] +pub struct BoneAttachment3D { + pub base: Node3D, + pub skeleton: Option, + pub bone_index: BoneIndex, + pub enabled: bool, +} + +impl BoneAttachment3D { + pub const fn new() -> Self { + Self { + base: Node3D::new(), + skeleton: None, + bone_index: -1, + enabled: true, + } + } + + pub fn set_skeleton(&mut self, skeleton: Option) { + self.skeleton = skeleton; + } + + pub fn skeleton(&self) -> Option { + self.skeleton + } + + pub fn set_bone_index(&mut self, bone_index: BoneIndex) { + self.bone_index = bone_index; + } + + pub fn bone_index(&self) -> BoneIndex { + self.bone_index + } + + pub fn set_enabled(&mut self, enabled: bool) { + self.enabled = enabled; + } + + pub fn enabled(&self) -> bool { + self.enabled + } +} + +impl Default for BoneAttachment3D { + fn default() -> Self { + Self::new() + } +} + +impl Deref for BoneAttachment3D { + type Target = Node3D; + + fn deref(&self) -> &Self::Target { + &self.base + } +} + +impl DerefMut for BoneAttachment3D { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.base + } +} diff --git a/perro_source/core/perro_nodes/src/nodes/node_3d/mod.rs b/perro_source/core/perro_nodes/src/nodes/node_3d/mod.rs index 93206fa62..36dff0cdb 100644 --- a/perro_source/core/perro_nodes/src/nodes/node_3d/mod.rs +++ b/perro_source/core/perro_nodes/src/nodes/node_3d/mod.rs @@ -1,5 +1,6 @@ #[path = "lights/ambient_light_3d.rs"] pub mod ambient_light_3d; +pub mod bone_attachment_3d; pub mod camera_3d; pub mod mesh_instance_3d; pub mod multi_mesh_instance_3d; @@ -17,6 +18,7 @@ pub mod sky_3d; pub mod spot_light_3d; pub use ambient_light_3d::*; +pub use bone_attachment_3d::*; pub use camera_3d::*; pub use mesh_instance_3d::*; pub use multi_mesh_instance_3d::*; diff --git a/perro_source/core/perro_nodes/src/nodes/node_registry.rs b/perro_source/core/perro_nodes/src/nodes/node_registry.rs index f18d0cc3e..ceea2364c 100644 --- a/perro_source/core/perro_nodes/src/nodes/node_registry.rs +++ b/perro_source/core/perro_nodes/src/nodes/node_registry.rs @@ -1,5 +1,6 @@ use crate::ambient_light_3d::AmbientLight3D; use crate::animation_player::AnimationPlayer; +use crate::bone_attachment_3d::BoneAttachment3D; use crate::camera_2d::Camera2D; use crate::camera_3d::Camera3D; use crate::mesh_instance_3d::MeshInstance3D; @@ -822,6 +823,7 @@ define_scene_nodes! { Area3D => (Node3D, Area3D, Renderable::False, InternalUpdate::False, InternalFixedUpdate::True), RigidBody3D => (Node3D, RigidBody3D, Renderable::False, InternalUpdate::False, InternalFixedUpdate::True), Skeleton3D => (Node3D, Skeleton3D, Renderable::False, InternalUpdate::False, InternalFixedUpdate::False), + BoneAttachment3D => (Node3D, BoneAttachment3D, Renderable::False, InternalUpdate::False, InternalFixedUpdate::False), ParticleEmitter3D => (Node3D, ParticleEmitter3D, Renderable::True, InternalUpdate::True, InternalFixedUpdate::False), //Lights AmbientLight3D => (None, AmbientLight3D, Renderable::True, InternalUpdate::False, InternalFixedUpdate::False), diff --git a/perro_source/runtime_project/perro_runtime/src/runtime.rs b/perro_source/runtime_project/perro_runtime/src/runtime.rs index 44a687d43..15c898aa2 100644 --- a/perro_source/runtime_project/perro_runtime/src/runtime.rs +++ b/perro_source/runtime_project/perro_runtime/src/runtime.rs @@ -236,6 +236,7 @@ impl Runtime { self.schedules.snapshot_update(&self.scripts); self.run_update_schedule(); self.run_internal_update_schedule(); + self.update_bone_attachments(); } #[inline] @@ -255,6 +256,7 @@ impl Runtime { let internal_start = std::time::Instant::now(); self.run_internal_update_schedule(); + self.update_bone_attachments(); let internal_update = internal_start.elapsed(); RuntimeUpdateTiming { diff --git a/perro_source/runtime_project/perro_runtime/src/runtime/internal_updates.rs b/perro_source/runtime_project/perro_runtime/src/runtime/internal_updates.rs index 1a34048b9..7f1810442 100644 --- a/perro_source/runtime_project/perro_runtime/src/runtime/internal_updates.rs +++ b/perro_source/runtime_project/perro_runtime/src/runtime/internal_updates.rs @@ -1,7 +1,7 @@ use super::Runtime; use perro_ids::NodeID; use perro_input::InputWindow; -use perro_nodes::{InternalFixedUpdate, InternalUpdate, NodeType}; +use perro_nodes::{InternalFixedUpdate, InternalUpdate, NodeType, SceneNodeData}; use perro_resource_context::ResourceWindow; use perro_runtime_context::RuntimeWindow; @@ -290,4 +290,40 @@ impl Runtime { let mut ctx = RuntimeWindow::new(self); perro_internal_updates::internal_fixed_update_node(&mut ctx, res, ipt, id); } + + pub(crate) fn update_bone_attachments(&mut self) { + let mut updates = Vec::new(); + for (id, node) in self.nodes.iter() { + let SceneNodeData::BoneAttachment3D(attachment) = &node.data else { + continue; + }; + if !attachment.enabled { + continue; + } + let Some(skeleton_id) = attachment.skeleton else { + continue; + }; + let Some(skeleton_node) = self.nodes.get(skeleton_id) else { + continue; + }; + let SceneNodeData::Skeleton3D(skeleton) = &skeleton_node.data else { + continue; + }; + let Ok(bone_index) = usize::try_from(attachment.bone_index) else { + continue; + }; + let Some(bone) = skeleton.bones.get(bone_index) else { + continue; + }; + // TODO: apply local/user offsets once attachment offset support lands. + updates.push((id, bone.rest)); + } + + for (id, transform) in updates { + let _ = self.with_base_node_mut::(id, |node| { + node.transform = transform; + }); + self.mark_global_transform_dirty(id); + } + } } diff --git a/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/core.rs b/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/core.rs index d50044dfe..72340a75f 100644 --- a/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/core.rs +++ b/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/core.rs @@ -5,6 +5,7 @@ use perro_nodes::{ ambient_light_3d::AmbientLight3D, animation_player::AnimationPlayer, camera_2d::Camera2D, + bone_attachment_3d::BoneAttachment3D, camera_3d::{Camera3D, CameraProjection}, mesh_instance_3d::{MaterialParamOverride, MaterialParamOverrideValue, MeshInstance3D, MeshSurfaceBinding}, multi_mesh_instance_3d::MultiMeshInstance3D, @@ -29,6 +30,7 @@ use perro_scene::{ RayLight3DField, RigidBody2DField, RigidBody3DField, Scene, SceneFieldIterRef, SceneKey, SceneNodeData as SceneDefNodeData, SceneNodeEntry as SceneDefNodeEntry, SceneObjectField, SceneValue, Skeleton3DField, Sky3DField, SpotLight3DField, Sprite2DField, + BoneAttachment3DField, StaticBody2DField, StaticBody3DField, resolve_node_field, }; use perro_structs::{ @@ -611,6 +613,7 @@ fn scene_node_data_from(data: &SceneDefNodeData) -> Result Ok(SceneNodeData::Area3D(build_area_3d(data))), "RigidBody3D" => Ok(SceneNodeData::RigidBody3D(build_rigid_body_3d(data))), "Skeleton3D" => Ok(SceneNodeData::Skeleton3D(build_skeleton_3d(data))), + "BoneAttachment3D" => Ok(SceneNodeData::BoneAttachment3D(build_bone_attachment_3d(data))), "Camera3D" => Ok(SceneNodeData::Camera3D(build_camera_3d(data))), "ParticleEmitter3D" => Ok(SceneNodeData::ParticleEmitter3D(build_particle_emitter_3d( data, diff --git a/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/nodes/three_d/base.rs b/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/nodes/three_d/base.rs index e988990e3..1c51a6bf8 100644 --- a/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/nodes/three_d/base.rs +++ b/perro_source/runtime_project/perro_runtime/src/runtime/scene_loader/prepare/nodes/three_d/base.rs @@ -24,6 +24,17 @@ fn build_multi_mesh_instance_3d(data: &SceneDefNodeData) -> MultiMeshInstance3D node } + +fn build_bone_attachment_3d(data: &SceneDefNodeData) -> BoneAttachment3D { + let mut node = BoneAttachment3D::new(); + if let Some(base) = data.base_ref() { + apply_node_3d_data(&mut node, base); + } + apply_node_3d_fields(&mut node, &data.fields); + apply_bone_attachment_3d_fields(&mut node, &data.fields); + node +} + fn build_skeleton_3d(data: &SceneDefNodeData) -> Skeleton3D { let mut node = Skeleton3D::new(); if let Some(base) = data.base_ref() { @@ -120,6 +131,21 @@ fn apply_multi_mesh_instance_3d_fields( fn apply_skeleton_3d_fields(_node: &mut Skeleton3D, _fields: &[SceneObjectField]) {} +fn apply_bone_attachment_3d_fields(node: &mut BoneAttachment3D, fields: &[SceneObjectField]) { + SceneFieldIterRef::new(fields).for_each(|name, value| match resolve_node_field("BoneAttachment3D", name) { + Some(NodeField::BoneAttachment3D(BoneAttachment3DField::Skeleton)) => { + node.skeleton = as_node_id(value); + } + Some(NodeField::BoneAttachment3D(BoneAttachment3DField::BoneIndex)) => { + if let Some(v) = as_i32(value) { node.bone_index = v; } + } + Some(NodeField::BoneAttachment3D(BoneAttachment3DField::Enabled)) => { + if let Some(v) = as_bool(value) { node.enabled = v; } + } + _ => {} + }); +} + fn extract_mesh_source(data: &SceneDefNodeData) -> Option { if data.ty != "MeshInstance3D" && data.ty != "MultiMeshInstance3D" { return None; diff --git a/perro_source/runtime_project/perro_runtime/tests/unit/bone_attachment_3d_tests.rs b/perro_source/runtime_project/perro_runtime/tests/unit/bone_attachment_3d_tests.rs new file mode 100644 index 000000000..6fa742012 --- /dev/null +++ b/perro_source/runtime_project/perro_runtime/tests/unit/bone_attachment_3d_tests.rs @@ -0,0 +1,53 @@ +use perro_ids::NodeID; +use perro_nodes::{Bone3D, BoneAttachment3D, Node3D, SceneNode, SceneNodeData, Skeleton3D}; +use perro_runtime::runtime::Runtime; +use perro_structs::{Transform3D, Vector3}; + +#[test] +fn bone_attachment_create_and_setters() { + let mut node = BoneAttachment3D::new(); + let skeleton = NodeID::from_u64(99); + node.set_skeleton(Some(skeleton)); + node.set_bone_index(2); + node.set_enabled(false); + assert_eq!(node.skeleton(), Some(skeleton)); + assert_eq!(node.bone_index(), 2); + assert!(!node.enabled()); +} + +#[test] +fn bone_attachment_runtime_updates_transform() { + let mut runtime = Runtime::new(); + + let mut skeleton = Skeleton3D::new(); + skeleton.bones.push(Bone3D { rest: Transform3D { position: Vector3::new(3.0, 4.0, 5.0), ..Transform3D::IDENTITY }, ..Bone3D::new()}); + let skeleton_id = runtime.nodes.insert(SceneNode::new(SceneNodeData::Skeleton3D(skeleton))); + + let mut attachment = BoneAttachment3D::new(); + attachment.set_skeleton(Some(skeleton_id)); + attachment.set_bone_index(0); + let attachment_id = runtime.nodes.insert(SceneNode::new(SceneNodeData::BoneAttachment3D(attachment))); + + runtime.update_bone_attachments(); + + let t = runtime.with_base_node::(attachment_id, |node| node.transform).unwrap(); + assert_eq!(t.position, Vector3::new(3.0, 4.0, 5.0)); +} + +#[test] +fn bone_attachment_invalid_index_or_disabled_safe() { + let mut runtime = Runtime::new(); + let skeleton_id = runtime.nodes.insert(SceneNode::new(SceneNodeData::Skeleton3D(Skeleton3D::new()))); + let mut attachment = BoneAttachment3D::new(); + attachment.set_skeleton(Some(skeleton_id)); + attachment.set_bone_index(100); + let attachment_id = runtime.nodes.insert(SceneNode::new(SceneNodeData::BoneAttachment3D(attachment.clone()))); + runtime.update_bone_attachments(); + + let mut disabled = attachment; + disabled.set_enabled(false); + let _disabled_id = runtime.nodes.insert(SceneNode::new(SceneNodeData::BoneAttachment3D(disabled))); + runtime.update_bone_attachments(); + + assert!(runtime.nodes.get(attachment_id).is_some()); +} diff --git a/perro_source/runtime_project/perro_scene/src/node_fields.rs b/perro_source/runtime_project/perro_scene/src/node_fields.rs index a071690ab..e55510603 100644 --- a/perro_source/runtime_project/perro_scene/src/node_fields.rs +++ b/perro_source/runtime_project/perro_scene/src/node_fields.rs @@ -13,6 +13,7 @@ pub enum NodeField { Area2D(Area2DField), MeshInstance3D(MeshInstance3DField), Skeleton3D(Skeleton3DField), + BoneAttachment3D(BoneAttachment3DField), Camera3D(Camera3DField), ParticleEmitter3D(ParticleEmitter3DField), AnimationPlayer(AnimationPlayerField), @@ -105,6 +106,13 @@ pub enum Skeleton3DField { Skeleton, } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BoneAttachment3DField { + Skeleton, + BoneIndex, + Enabled, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Camera3DField { Zoom, @@ -309,6 +317,14 @@ pub fn resolve_node_field(node_type_name: &str, field: &str) -> Option Some(NodeField::Skeleton3D(Skeleton3DField::Skeleton)), _ => None, }, + NodeType::BoneAttachment3D => match field { + "skeleton" => Some(NodeField::BoneAttachment3D(BoneAttachment3DField::Skeleton)), + "bone_index" | "bone" => Some(NodeField::BoneAttachment3D( + BoneAttachment3DField::BoneIndex, + )), + "enabled" => Some(NodeField::BoneAttachment3D(BoneAttachment3DField::Enabled)), + _ => None, + }, NodeType::Camera3D => match field { "zoom" => Some(NodeField::Camera3D(Camera3DField::Zoom)), "projection" => Some(NodeField::Camera3D(Camera3DField::Projection)),