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,