POC: Rapier picking for posable characters

This commit is contained in:
Danny McGee
2024-04-11 03:47:21 -04:00
parent 58b02a7100
commit 6ed88d062c
5 changed files with 204 additions and 9 deletions
Generated
+1
View File
@@ -5392,6 +5392,7 @@ dependencies = [
"parry3d", "parry3d",
"rustc-hash", "rustc-hash",
"simba", "simba",
"vec_map",
] ]
[[package]] [[package]]
+1 -1
View File
@@ -20,7 +20,7 @@ path = "src/main-headless.rs"
[dependencies] [dependencies]
bevy-inspector-egui = "0.23.0" bevy-inspector-egui = "0.23.0"
bevy_mod_raycast = "0.17.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_mod_outline = "0.7.0"
bevy_web_asset = "0.8.0" bevy_web_asset = "0.8.0"
bitflags = "2.4.0" bitflags = "2.4.0"
+160 -2
View File
@@ -3,10 +3,15 @@ use bevy::{
math::Affine3A, math::Affine3A,
prelude::*, prelude::*,
render::{mesh::skinning::SkinnedMesh, view::NoFrustumCulling}, 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::*; pub use retargeting::*;
mod retargeting; mod retargeting;
@@ -34,6 +39,69 @@ impl Plugin for AnimPlugin {
retargeting::RetargetingPlugin, retargeting::RetargetingPlugin,
timeline_animation::TimelinePlugin, timeline_animation::TimelinePlugin,
)); ));
{
// TODO
app.register_type::<TempCharacterCollidersProcessed>();
app.editor_registry::<TempCharacterCollidersProcessed>();
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<TempCharacterColliderConfig>>);
#[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<TempCharacterCollidersConfig>,
q_named_prefabs: Query<
(Entity, &Name),
(With<PrefabMarker>, Without<TempCharacterCollidersProcessed>),
>,
q_named_joints: Query<(Entity, &Name), With<SkeletalJoint>>,
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);
}
}
+32 -5
View File
@@ -6,6 +6,7 @@ use bevy_mod_outline::{
}; };
use bevy_mod_picking::PickableBundle; use bevy_mod_picking::PickableBundle;
use bevy_mod_raycast::prelude::{Raycast, RaycastSettings}; use bevy_mod_raycast::prelude::{Raycast, RaycastSettings};
use bevy_rapier3d::{pipeline::QueryFilter, plugin::RapierContext};
use space_editor::prelude::space_undo::{self, NewChange, RemovedEntity}; use space_editor::prelude::space_undo::{self, NewChange, RemovedEntity};
use crate::{ use crate::{
@@ -130,6 +131,7 @@ fn update_selection(
q_selectables: Query<&Selectable>, q_selectables: Query<&Selectable>,
q_pickables: Query<&bevy_mod_picking::prelude::Pickable>, q_pickables: Query<&bevy_mod_picking::prelude::Pickable>,
q_camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>, q_camera: Query<(&Camera, &GlobalTransform), With<MainCamera>>,
r_rapier_context: Option<Res<RapierContext>>,
) { ) {
for SelectionAction(lifecycle, action) in er_selection.read() { for SelectionAction(lifecycle, action) in er_selection.read() {
match (lifecycle, action) { match (lifecycle, action) {
@@ -139,26 +141,51 @@ fn update_selection(
(_, input::Selection::Single(mouse_pos)) | (_, input::Selection::Multi(mouse_pos)) => { (_, input::Selection::Single(mouse_pos)) | (_, input::Selection::Multi(mouse_pos)) => {
let is_multi = matches!(action, input::Selection::Multi(_)); 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 (cam, cam_xform) = q_camera.single();
let ray = cam.viewport_to_world(cam_xform, *mouse_pos).unwrap(); 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, ray,
&RaycastSettings { &RaycastSettings {
filter: &|ent: Entity| q_pickables.contains(ent), filter: &|ent: Entity| q_pickables.contains(ent),
..default() ..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::<Vec<_>>();
if combined_hits.is_empty() {
if !is_multi { if !is_multi {
cmd.remove_resource::<Selection>(); cmd.remove_resource::<Selection>();
} }
continue; continue;
} }
let (ent, _) = &hits[0]; // Sort by distance from the camera and look for a `Selectable`
if let Some(ent) = nearest_selectable_parent(*ent, &q_parents, &q_selectables) { // 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)); cmd.insert_resource(Selection(ent));
} else { } else {
cmd.remove_resource::<Selection>(); cmd.remove_resource::<Selection>();
+10 -1
View File
@@ -2,6 +2,10 @@
use bevy::{app::PluginGroupBuilder, asset::AssetMetaCheck, prelude::*}; use bevy::{app::PluginGroupBuilder, asset::AssetMetaCheck, prelude::*};
use bevy_mod_picking::DefaultPickingPlugins; use bevy_mod_picking::DefaultPickingPlugins;
use bevy_rapier3d::{
plugin::{NoUserData, RapierPhysicsPlugin},
render::RapierDebugRenderPlugin,
};
use bevy_web_asset::WebAssetPlugin; use bevy_web_asset::WebAssetPlugin;
use scene::ImportSceneEvent; use scene::ImportSceneEvent;
@@ -171,7 +175,12 @@ pub fn start(init: InitParams, mode: StudioMode, window_config: Option<Window>)
)); ));
if matches!(mode, StudioMode::Editor) { if matches!(mode, StudioMode::Editor) {
app.add_plugins((DefaultPickingPlugins, EditorPlugins)); app.add_plugins((
DefaultPickingPlugins,
RapierPhysicsPlugin::<NoUserData>::default(),
RapierDebugRenderPlugin::default(),
EditorPlugins,
));
// Only for registering components for undo // Only for registering components for undo
use space_editor::prelude::space_undo::AppAutoUndo; use space_editor::prelude::space_undo::AppAutoUndo;