diff --git a/Cargo.lock b/Cargo.lock index 1d77086..f2d9740 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5392,6 +5392,7 @@ dependencies = [ "parry3d", "rustc-hash", "simba", + "vec_map", ] [[package]] diff --git a/studio/Cargo.toml b/studio/Cargo.toml index 9bad3e7..585255e 100644 --- a/studio/Cargo.toml +++ b/studio/Cargo.toml @@ -20,7 +20,7 @@ path = "src/main-headless.rs" [dependencies] bevy-inspector-egui = "0.23.0" bevy_mod_raycast = "0.17.0" -bevy_rapier3d = "0.25.0" +bevy_rapier3d = { version = "0.25.0", features = ["simd-stable", "debug-render-3d"] } bevy_mod_outline = "0.7.0" bevy_web_asset = "0.8.0" bitflags = "2.4.0" diff --git a/studio/src/anim/mod.rs b/studio/src/anim/mod.rs index 2c5d4f9..c2496a6 100644 --- a/studio/src/anim/mod.rs +++ b/studio/src/anim/mod.rs @@ -3,10 +3,15 @@ use bevy::{ math::Affine3A, prelude::*, render::{mesh::skinning::SkinnedMesh, view::NoFrustumCulling}, + utils::HashMap, }; -use space_editor::space_prefab::editor_registry::EditorRegistryExt; +use bevy_rapier3d::geometry::Collider; +use space_editor::{prelude::PrefabMarker, space_prefab::editor_registry::EditorRegistryExt}; -use crate::{hierarchy, scene::SceneElement}; +use crate::{ + hierarchy, + scene::{SceneElement, SceneState}, +}; pub use retargeting::*; mod retargeting; @@ -34,6 +39,69 @@ impl Plugin for AnimPlugin { retargeting::RetargetingPlugin, timeline_animation::TimelinePlugin, )); + + { + // TODO + app.register_type::(); + app.editor_registry::(); + + use ColliderShape::*; + + app.insert_resource(TempCharacterCollidersConfig( + [ + ( + "DEF-spine", + vec![TempCharacterColliderConfig { + shape: Capsule { + radius: 0.108191, + length: 0.135549, + alignment: BoneAlignment::X, + }, + transform: Transform::from_xyz(0., 0.117, -0.001), + }], + ), + ( + "DEF-spine.001", + vec![TempCharacterColliderConfig { + shape: Capsule { + radius: 0.091, + length: 0.095, + alignment: BoneAlignment::X, + }, + transform: Transform::from_xyz(0., 0.068, 0.), + }], + ), + ( + "DEF-spine.002", + vec![ + TempCharacterColliderConfig { + shape: Capsule { + radius: 0.082, + length: 0.062, + alignment: BoneAlignment::X, + }, + transform: Transform::from_xyz(0., 0.006, 0.), + }, + TempCharacterColliderConfig { + shape: Capsule { + radius: 0.0823, + length: 0.03661, + alignment: BoneAlignment::X, + }, + transform: Transform::from_xyz(0., 0.088, 0.010), + }, + ], + ), + ] + .into_iter() + .collect(), + )); + + app.add_systems( + Update, + temp_attach_colliders_to_character.run_if(in_state(SceneState::Active)), + ); + } } } @@ -144,3 +212,93 @@ fn process_skinned_meshes( } } } + +#[derive(Resource, Clone, Default, Deref)] +struct TempCharacterCollidersConfig(HashMap<&'static str, Vec>); + +#[derive(Debug, Clone, Copy)] +enum BoneAlignment { + X, + Y, + Z, +} + +#[derive(Debug, Clone, Copy)] +enum ColliderShape { + Capsule { + radius: f32, + length: f32, + alignment: BoneAlignment, + }, + Box { + x: f32, + y: f32, + z: f32, + }, +} + +#[derive(Debug, Clone, Copy)] +struct TempCharacterColliderConfig { + shape: ColliderShape, + transform: Transform, +} + +#[derive(Component, Reflect, Default, Clone, Copy)] +#[reflect(Component)] +struct TempCharacterCollidersProcessed; + +fn temp_attach_colliders_to_character( + mut cmd: Commands, + r_colliders_map: Res, + q_named_prefabs: Query< + (Entity, &Name), + (With, Without), + >, + q_named_joints: Query<(Entity, &Name), With>, + q_children: Query<&Children>, +) { + for target in q_named_prefabs.iter().filter_map(|(ent, name)| { + if name.as_str() == "base-human-female.glb" { + Some(ent) + } else { + None + } + }) { + for (joint, name) in q_children + .iter_descendants(target) + .filter_map(|ent| q_named_joints.get(ent).ok()) + { + if let Some(colliders) = r_colliders_map.get(&name.as_str()) { + info!("Processing colliders for bone: '{name}'"); + + for TempCharacterColliderConfig { shape, transform } in colliders.iter().copied() { + cmd.entity(joint).with_children(|cmd| { + cmd.spawn(( + TransformBundle::from_transform(transform), + match shape { + ColliderShape::Capsule { + radius, + length, + alignment, + } => match alignment { + BoneAlignment::X => Collider::capsule_x(length / 2., radius), + BoneAlignment::Y => Collider::capsule_x(length / 2., radius), + BoneAlignment::Z => Collider::capsule_x(length / 2., radius), + }, + ColliderShape::Box { x, y, z } => { + Collider::cuboid(x / 2., y / 2., z / 2.) + } + }, + // SceneElement::Collider, + bevy_mod_picking::PickableBundle::default(), + )); + }); + } + } else { + warn!("No collider config found for bone: '{name}'"); + } + } + + cmd.entity(target).insert(TempCharacterCollidersProcessed); + } +} diff --git a/studio/src/interaction/selection.rs b/studio/src/interaction/selection.rs index b520e74..d4a7dbd 100644 --- a/studio/src/interaction/selection.rs +++ b/studio/src/interaction/selection.rs @@ -6,6 +6,7 @@ use bevy_mod_outline::{ }; use bevy_mod_picking::PickableBundle; use bevy_mod_raycast::prelude::{Raycast, RaycastSettings}; +use bevy_rapier3d::{pipeline::QueryFilter, plugin::RapierContext}; use space_editor::prelude::space_undo::{self, NewChange, RemovedEntity}; use crate::{ @@ -130,6 +131,7 @@ fn update_selection( q_selectables: Query<&Selectable>, q_pickables: Query<&bevy_mod_picking::prelude::Pickable>, q_camera: Query<(&Camera, &GlobalTransform), With>, + r_rapier_context: Option>, ) { for SelectionAction(lifecycle, action) in er_selection.read() { match (lifecycle, action) { @@ -139,26 +141,51 @@ fn update_selection( (_, input::Selection::Single(mouse_pos)) | (_, input::Selection::Multi(mouse_pos)) => { let is_multi = matches!(action, input::Selection::Multi(_)); - // Raycast to find the pickable entity under the cursor + // Cast a camera-to-world ray... let (cam, cam_xform) = q_camera.single(); let ray = cam.viewport_to_world(cam_xform, *mouse_pos).unwrap(); - let hits = raycast.cast_ray( + + // Hit test against standard meshes... + let raycast_hits = raycast.cast_ray( ray, &RaycastSettings { filter: &|ent: Entity| q_pickables.contains(ent), ..default() }, ); + // And also against Rapier colliders + let rapier_hit = r_rapier_context.as_ref().and_then(|ctx| { + ctx.cast_ray( + ray.origin, + *ray.direction, + f32::MAX, + false, + QueryFilter::default(), + ) + }); - if hits.is_empty() { + // Combine both sets of hit results and bail early if it's empty + let mut combined_hits = raycast_hits + .iter() + .map(|(ent, data)| (*ent, data.distance())) + .chain(rapier_hit) + .collect::>(); + + if combined_hits.is_empty() { if !is_multi { cmd.remove_resource::(); } continue; } - let (ent, _) = &hits[0]; - if let Some(ent) = nearest_selectable_parent(*ent, &q_parents, &q_selectables) { + // Sort by distance from the camera and look for a `Selectable` + // parent among the results + combined_hits.sort_unstable_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap()); + + if let Some(ent) = combined_hits + .into_iter() + .find_map(|(ent, _)| nearest_selectable_parent(ent, &q_parents, &q_selectables)) + { cmd.insert_resource(Selection(ent)); } else { cmd.remove_resource::(); diff --git a/studio/src/lib.rs b/studio/src/lib.rs index 1d6ea7f..ddd7b5e 100644 --- a/studio/src/lib.rs +++ b/studio/src/lib.rs @@ -2,6 +2,10 @@ use bevy::{app::PluginGroupBuilder, asset::AssetMetaCheck, prelude::*}; use bevy_mod_picking::DefaultPickingPlugins; +use bevy_rapier3d::{ + plugin::{NoUserData, RapierPhysicsPlugin}, + render::RapierDebugRenderPlugin, +}; use bevy_web_asset::WebAssetPlugin; use scene::ImportSceneEvent; @@ -171,7 +175,12 @@ pub fn start(init: InitParams, mode: StudioMode, window_config: Option) )); if matches!(mode, StudioMode::Editor) { - app.add_plugins((DefaultPickingPlugins, EditorPlugins)); + app.add_plugins(( + DefaultPickingPlugins, + RapierPhysicsPlugin::::default(), + RapierDebugRenderPlugin::default(), + EditorPlugins, + )); // Only for registering components for undo use space_editor::prelude::space_undo::AppAutoUndo;