From 3f887d3f3e315d73315169f2e15a9621d86ad39d Mon Sep 17 00:00:00 2001 From: Danny McGee Date: Wed, 28 Feb 2024 18:49:16 -0500 Subject: [PATCH] Attach IK rig to selected skeleton --- studio-frontend/src/app/app.element.ts | 9 +- studio/src/anim/ik.rs | 227 ++++++++++++++++++++++++ studio/src/anim/mod.rs | 7 +- studio/src/interaction/gizmos/camera.rs | 52 ++++-- studio/src/interaction/gizmos/mod.rs | 7 +- 5 files changed, 282 insertions(+), 20 deletions(-) create mode 100644 studio/src/anim/ik.rs diff --git a/studio-frontend/src/app/app.element.ts b/studio-frontend/src/app/app.element.ts index 1c8c3ec..12ccb2b 100644 --- a/studio-frontend/src/app/app.element.ts +++ b/studio-frontend/src/app/app.element.ts @@ -90,6 +90,11 @@ export class AppElement extends LitElement { } } + attachIkRig(): void { + if (this.selection) + studio.attachIkRig(this.selection.id); + } + toggleHelp(): void { this.showHelp = !this.showHelp; } @@ -123,9 +128,7 @@ export class AppElement extends LitElement { { - console.log("Attach IK rig"); - }} + @click=${this.attachIkRig} > Attach IK Rig diff --git a/studio/src/anim/ik.rs b/studio/src/anim/ik.rs new file mode 100644 index 0000000..d17c7c4 --- /dev/null +++ b/studio/src/anim/ik.rs @@ -0,0 +1,227 @@ +use bevy::{pbr::NotShadowCaster, prelude::*, render::view::RenderLayers}; + +use crate::{design_system, interaction::gizmos::GizmoMaterial, math::geo}; + +pub const IK_RIG_RENDER_LAYER: u8 = 1; + +pub struct IkPlugin; + +impl Plugin for IkPlugin { + fn build(&self, app: &mut App) { + app.add_systems(Update, setup_ik_rig.map(bevy::utils::error)); + + #[cfg(feature = "wasm")] + app.add_systems(Update, wasm::insert_ik_component); + } +} + +#[derive(Component)] +pub struct IkRig; + +#[derive(Component)] +pub struct IkRigCamera; + +#[derive(Component, Clone, Copy)] +struct IkBoneChain { + root: Entity, + twist_1: Option, + mid: Entity, + twist_2: Option, + end: Entity, + len_upper: f32, + len_lower: f32, +} + +#[derive(Default)] +struct IkBoneChainBuilder { + root: Option, + twist_1: Option, + mid: Option, + twist_2: Option, + end: Option, +} + +impl IkBoneChain { + fn build() -> IkBoneChainBuilder { + IkBoneChainBuilder::default() + } +} + +impl IkBoneChainBuilder { + fn set_root(&mut self, ent: Entity) { + self.root = Some(ent); + } + fn set_twist_1(&mut self, ent: Entity) { + self.twist_1 = Some(ent); + } + fn set_mid(&mut self, ent: Entity) { + self.mid = Some(ent); + } + fn set_twist_2(&mut self, ent: Entity) { + self.twist_2 = Some(ent); + } + fn set_end(&mut self, ent: Entity) { + self.end = Some(ent); + } + fn try_finish(self, q_xforms: &Query<&GlobalTransform>) -> Result { + let root = self.root.ok_or_else(|| "Root bone missing!".to_string())?; + let mid = self.mid.ok_or_else(|| "Mid bone missing!".to_string())?; + let end = self.end.ok_or_else(|| "End bone missing!".to_string())?; + + let root_xform = q_xforms.get(root).map_err(|err| format!("{err}"))?; + let mid_xform = q_xforms.get(mid).map_err(|err| format!("{err}"))?; + let end_xform = q_xforms.get(end).map_err(|err| format!("{err}"))?; + + let len_upper = Vec3::distance(root_xform.translation(), mid_xform.translation()); + let len_lower = Vec3::distance(mid_xform.translation(), end_xform.translation()); + + Ok(IkBoneChain { + root, + twist_1: self.twist_1, + mid, + twist_2: self.twist_2, + end, + len_upper, + len_lower, + }) + } +} + +fn setup_ik_rig( + mut cmd: Commands, + mut ra_meshes: ResMut>, + mut ra_mats: ResMut>, + q_target: Query>, + q_children: Query<&Children>, + q_names: Query<&Name>, + q_world_xforms: Query<&GlobalTransform>, +) -> Result<(), String> { + let Ok(target) = q_target.get_single() else { + return Ok(()); + }; + + let root_xform = q_world_xforms.get(target).map_err(|err| format!("{err}"))?; + + let mut leg_chain_l = IkBoneChain::build(); + let mut leg_chain_r = IkBoneChain::build(); + + let mut arm_chain_l = IkBoneChain::build(); + let mut arm_chain_r = IkBoneChain::build(); + + for ent in q_children.iter_descendants(target) { + let Ok(name) = q_names.get(ent) else { + continue; + }; + + match name.as_str() { + // Left Leg + "DEF-thigh.L" => leg_chain_l.set_root(ent), + "DEF-thigh.L.001" => leg_chain_l.set_twist_1(ent), + "DEF-shin.L" => leg_chain_l.set_mid(ent), + "DEF-shin.L.001" => leg_chain_l.set_twist_2(ent), + "DEF-foot.L" => leg_chain_l.set_end(ent), + // Right Leg + "DEF-thigh.R" => leg_chain_r.set_root(ent), + "DEF-thigh.R.001" => leg_chain_r.set_twist_1(ent), + "DEF-shin.R" => leg_chain_r.set_mid(ent), + "DEF-shin.R.001" => leg_chain_r.set_twist_2(ent), + "DEF-foot.R" => leg_chain_r.set_end(ent), + // Left Arm + "DEF-upper_arm.L" => arm_chain_l.set_root(ent), + "DEF-upper_arm.L.001" => arm_chain_l.set_twist_1(ent), + "DEF-forearm.L" => arm_chain_l.set_mid(ent), + "DEF-forearm.L.001" => arm_chain_l.set_twist_2(ent), + "DEF-hand.L" => arm_chain_l.set_end(ent), + // Right Arm + "DEF-upper_arm.R" => arm_chain_r.set_root(ent), + "DEF-upper_arm.R.001" => arm_chain_r.set_twist_1(ent), + "DEF-forearm.R" => arm_chain_r.set_mid(ent), + "DEF-forearm.R.001" => arm_chain_r.set_twist_2(ent), + "DEF-hand.R" => arm_chain_r.set_end(ent), + _ => {} + } + } + + for (color, bone_chain) in [ + (design_system::Color::RED, leg_chain_l), + (design_system::Color::BLUE, leg_chain_r), + (design_system::Color::RED, arm_chain_l), + (design_system::Color::BLUE, arm_chain_r), + ] { + let bone_chain = bone_chain.try_finish(&q_world_xforms)?; + let end_xform = q_world_xforms.get(bone_chain.end).unwrap(); + let mid_xform = q_world_xforms.get(bone_chain.mid).unwrap(); + + cmd.entity(target).with_children(|builder| { + builder.spawn(( + bone_chain, + NotShadowCaster, + RenderLayers::layer(IK_RIG_RENDER_LAYER), + MaterialMeshBundle { + transform: end_xform.reparented_to(root_xform), + mesh: ra_meshes.add(Mesh::from(shape::Cube { size: 0.1 })), + material: ra_mats.add(GizmoMaterial { + color: color.with_a(0.4), + blend_mode: AlphaMode::Blend, + }), + ..default() + }, + )); + + builder + .spawn(SpatialBundle { + transform: mid_xform.reparented_to(root_xform), + ..default() + }) + .with_children(|builder| { + let transform = Transform::from_xyz(0., 0., -0.1).with_rotation( + Quat::from_axis_angle(Vec3::NEG_X, std::f32::consts::FRAC_PI_2), + ); + + builder.spawn(( + NotShadowCaster, + RenderLayers::layer(IK_RIG_RENDER_LAYER), + MaterialMeshBundle { + transform, + mesh: ra_meshes.add(Mesh::from(geo::Cone { + radius: 0.05, + length: 0.15, + resolution: 12, + })), + material: ra_mats.add(GizmoMaterial { + color: color.with_a(0.4), + blend_mode: AlphaMode::Blend, + }), + ..default() + }, + )); + }); + }); + } + + Ok(()) +} + +#[cfg(feature = "wasm")] +mod wasm { + use bevy::prelude::*; + use wasm_bindgen::prelude::*; + + use crate::wasm::{FromStr, StaticBuffer, WasmMessageBuffer}; + + use super::IkRig; + + static ATTACH_IK_RIG: WasmMessageBuffer = WasmMessageBuffer::new(); + + #[wasm_bindgen(js_name = attachIkRig)] + pub fn attach_ik_rig(entity: &str) { + let entity = Entity::from_str(entity).unwrap(); + ATTACH_IK_RIG.write_message(entity); + } + + pub(super) fn insert_ik_component(mut cmd: Commands) { + if let Some(entity) = ATTACH_IK_RIG.take_message() { + cmd.entity(entity).insert(IkRig); + } + } +} diff --git a/studio/src/anim/mod.rs b/studio/src/anim/mod.rs index ce98564..ecf5253 100644 --- a/studio/src/anim/mod.rs +++ b/studio/src/anim/mod.rs @@ -10,6 +10,7 @@ use crate::{hierarchy, scene::SceneElement}; pub use retargeting::*; mod camera; +pub mod ik; mod retargeting; pub struct AnimPlugin; @@ -31,7 +32,11 @@ impl Plugin for AnimPlugin { app.add_systems(Update, process_skinned_meshes); app.insert_resource(GlobalSeekTime(0.)); - app.add_plugins((retargeting::RetargetingPlugin, camera::CameraAnimPlugin)); + app.add_plugins(( + retargeting::RetargetingPlugin, + camera::CameraAnimPlugin, + ik::IkPlugin, + )); #[cfg(feature = "wasm")] app.add_systems(Update, wasm::update_global_seek_time); diff --git a/studio/src/interaction/gizmos/camera.rs b/studio/src/interaction/gizmos/camera.rs index 4b6fde6..e7027e6 100644 --- a/studio/src/interaction/gizmos/camera.rs +++ b/studio/src/interaction/gizmos/camera.rs @@ -5,7 +5,10 @@ use bevy::{ }; use space_editor::space_editor_ui::NotShowCamera; -use crate::MainCamera; +use crate::{ + anim::ik::{IkRigCamera, IK_RIG_RENDER_LAYER}, + MainCamera, +}; use super::GIZMO_RENDER_LAYER; @@ -19,9 +22,13 @@ pub(super) fn spawn_gizmo_camera( let ent = q_main_camera.single(); cmd.entity(ent).with_children(|cmd| { + // TODO: This doesn't really belong here, but the render pipeline seems + // to break if we don't spawn these cameras in the correct order. + // We should probably add a dedicated module/plugin for setting up + // the editor cameras. cmd.spawn(( - Name::new("Gizmo Camera"), - GizmoCamera, + Name::new("IK Rig Camera"), + IkRigCamera, Camera3dBundle { camera_3d: Camera3d { clear_color: ClearColorConfig::None, @@ -34,6 +41,26 @@ pub(super) fn spawn_gizmo_camera( }, ..default() }, + RenderLayers::layer(IK_RIG_RENDER_LAYER), + Fxaa::default(), + NotShowCamera, + )); + + cmd.spawn(( + Name::new("Gizmo Camera"), + GizmoCamera, + Camera3dBundle { + camera_3d: Camera3d { + clear_color: ClearColorConfig::None, + ..default() + }, + camera: Camera { + hdr: true, + order: 2, + ..default() + }, + ..default() + }, RenderLayers::layer(GIZMO_RENDER_LAYER), Fxaa::default(), NotShowCamera, @@ -41,17 +68,18 @@ pub(super) fn spawn_gizmo_camera( }); } -pub(super) fn sync_gizmo_and_main_cameras( - q_main_camera: Query<&Camera, (With, Without)>, - mut q_gizmo_camera: Query<&mut Camera, (With, Without)>, +pub(super) fn sync_main_and_aux_cameras( + q_main_camera: Query<(Entity, &Camera), With>, + mut q_other_cameras: Query<&mut Camera, Without>, + q_children: Query<&Children>, ) { - let Ok(main_cam) = q_main_camera.get_single() else { + let Ok((main_cam_ent, main_cam)) = q_main_camera.get_single() else { return; }; - let Ok(mut gizmo_cam) = q_gizmo_camera.get_single_mut() else { - return; - }; - - gizmo_cam.viewport = main_cam.viewport.clone(); + for child in q_children.iter_descendants(main_cam_ent) { + if let Ok(mut cam) = q_other_cameras.get_mut(child) { + cam.viewport = main_cam.viewport.clone(); + } + } } diff --git a/studio/src/interaction/gizmos/mod.rs b/studio/src/interaction/gizmos/mod.rs index 5ed674d..9abedf6 100644 --- a/studio/src/interaction/gizmos/mod.rs +++ b/studio/src/interaction/gizmos/mod.rs @@ -6,12 +6,11 @@ use self::{ camera::{spawn_gizmo_camera, GizmoCamera}, combo::spawn_combo_translation_rotation_gizmo, common::*, - material::GizmoMaterial, resources::{init_resources, GizmoMats, GizmoMeshes}, rotation::{spawn_rotation_gizmo, update_ss_ring_rotation}, translation::spawn_translation_gizmo, }; -pub use common::TransformGizmo; +pub use self::{common::TransformGizmo, material::GizmoMaterial}; use super::{Selection, TransformMode, TransformType}; @@ -23,7 +22,7 @@ mod resources; mod rotation; mod translation; -const GIZMO_RENDER_LAYER: u8 = 1; +pub const GIZMO_RENDER_LAYER: u8 = 2; /// (sin|cos)(pi / 4) const CIRC_45: f32 = 0.70710677; @@ -67,7 +66,7 @@ impl Plugin for GizmoPlugin { ); //sync camera viewports - app.add_systems(Update, camera::sync_gizmo_and_main_cameras); + app.add_systems(Update, camera::sync_main_and_aux_cameras); app.add_systems( PostUpdate,