mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-14 23:53:14 +00:00
POC: Rapier picking for posable characters
This commit is contained in:
Generated
+1
@@ -5392,6 +5392,7 @@ dependencies = [
|
||||
"parry3d",
|
||||
"rustc-hash",
|
||||
"simba",
|
||||
"vec_map",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
+1
-1
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user