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",
"rustc-hash",
"simba",
"vec_map",
]
[[package]]
+1 -1
View File
@@ -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"
+160 -2
View File
@@ -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::<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_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<MainCamera>>,
r_rapier_context: Option<Res<RapierContext>>,
) {
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::<Vec<_>>();
if combined_hits.is_empty() {
if !is_multi {
cmd.remove_resource::<Selection>();
}
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::<Selection>();
+10 -1
View File
@@ -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<Window>)
));
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
use space_editor::prelude::space_undo::AppAutoUndo;