feat: migrate to glam whenever relevant + migrate testbed to kiss3d instead of bevy + release v0.32.0 (#909)
* feat: migrate to glam whenever relevant + migrate testbed to kiss3d instead of bevy * chore: update changelog * Fix warnings and tests * Release v0.32.0
This commit is contained in:
@@ -1,98 +0,0 @@
|
||||
// NOTE: this is inspired from the `bevy-orbit-controls` projects but
|
||||
// with some modifications like Panning, and 2D support.
|
||||
// Most of these modifications have been contributed upstream.
|
||||
use bevy::input::mouse::MouseMotion;
|
||||
use bevy::input::mouse::MouseScrollUnit::{Line, Pixel};
|
||||
use bevy::input::mouse::MouseWheel;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::camera::Camera;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const LINE_TO_PIXEL_RATIO: f32 = 0.0005;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
const LINE_TO_PIXEL_RATIO: f32 = 0.1;
|
||||
|
||||
#[derive(Component, PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OrbitCamera {
|
||||
pub zoom: f32,
|
||||
pub center: Vec3,
|
||||
pub pan_sensitivity: f32,
|
||||
pub zoom_sensitivity: f32,
|
||||
pub pan_button: MouseButton,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for OrbitCamera {
|
||||
fn default() -> Self {
|
||||
OrbitCamera {
|
||||
zoom: 100.0,
|
||||
center: Vec3::ZERO,
|
||||
pan_sensitivity: 1.0,
|
||||
zoom_sensitivity: 0.8,
|
||||
pan_button: MouseButton::Right,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adapted from the 3D orbit camera from bevy-orbit-controls
|
||||
pub struct OrbitCameraPlugin;
|
||||
impl OrbitCameraPlugin {
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn update_transform_system(
|
||||
mut query: Query<(&OrbitCamera, &mut Transform), (Changed<OrbitCamera>, With<Camera>)>,
|
||||
) {
|
||||
for (camera, mut transform) in query.iter_mut() {
|
||||
transform.translation = camera.center;
|
||||
transform.scale = Vec3::new(1.0 / camera.zoom, 1.0 / camera.zoom, 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_motion_system(
|
||||
_time: Res<Time>,
|
||||
mut mouse_motion_events: EventReader<MouseMotion>,
|
||||
mouse_button_input: Res<ButtonInput<MouseButton>>,
|
||||
mut query: Query<(&mut OrbitCamera, &mut Transform, &mut Camera)>,
|
||||
) {
|
||||
let mut delta = Vec2::ZERO;
|
||||
for event in mouse_motion_events.read() {
|
||||
delta += event.delta;
|
||||
}
|
||||
for (mut camera, _, _) in query.iter_mut() {
|
||||
if !camera.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
if mouse_button_input.pressed(camera.pan_button) {
|
||||
let delta = delta * camera.pan_sensitivity;
|
||||
camera.center += Vec3::new(-delta.x, delta.y, 0.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn zoom_system(
|
||||
mut mouse_wheel_events: EventReader<MouseWheel>,
|
||||
mut query: Query<&mut OrbitCamera, With<Camera>>,
|
||||
) {
|
||||
let mut total = 0.0;
|
||||
for event in mouse_wheel_events.read() {
|
||||
total -= event.y
|
||||
* match event.unit {
|
||||
Line => 1.0,
|
||||
Pixel => LINE_TO_PIXEL_RATIO,
|
||||
};
|
||||
}
|
||||
for mut camera in query.iter_mut() {
|
||||
if camera.enabled {
|
||||
camera.zoom *= camera.zoom_sensitivity.powf(total);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Plugin for OrbitCameraPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Update, Self::mouse_motion_system)
|
||||
.add_systems(Update, Self::zoom_system)
|
||||
.add_systems(Update, Self::update_transform_system);
|
||||
}
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
// NOTE: this is mostly taken from the `iMplode-nZ/bevy-orbit-controls` projects but
|
||||
// with some modifications like Panning, and 2D support.
|
||||
// Most of these modifications have been contributed upstream.
|
||||
|
||||
use bevy::input::mouse::MouseMotion;
|
||||
use bevy::input::mouse::MouseScrollUnit::{Line, Pixel};
|
||||
use bevy::input::mouse::MouseWheel;
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::camera::Camera;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
const LINE_TO_PIXEL_RATIO: f32 = 0.0005;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
const LINE_TO_PIXEL_RATIO: f32 = 0.1;
|
||||
|
||||
#[derive(Component, PartialEq, Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct OrbitCamera {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub pitch_range: RangeInclusive<f32>,
|
||||
pub distance: f32,
|
||||
pub center: Vec3,
|
||||
pub rotate_sensitivity: f32,
|
||||
pub pan_sensitivity: f32,
|
||||
pub zoom_sensitivity: f32,
|
||||
pub rotate_button: MouseButton,
|
||||
pub pan_button: MouseButton,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl Default for OrbitCamera {
|
||||
fn default() -> Self {
|
||||
OrbitCamera {
|
||||
x: 0.0,
|
||||
y: std::f32::consts::FRAC_PI_2,
|
||||
pitch_range: 0.01..=3.13,
|
||||
distance: 5.0,
|
||||
center: Vec3::ZERO,
|
||||
rotate_sensitivity: 1.0,
|
||||
pan_sensitivity: 1.0,
|
||||
zoom_sensitivity: 0.8,
|
||||
rotate_button: MouseButton::Left,
|
||||
pan_button: MouseButton::Right,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OrbitCameraPlugin;
|
||||
impl OrbitCameraPlugin {
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn update_transform_system(
|
||||
mut query: Query<(&OrbitCamera, &mut Transform), (Changed<OrbitCamera>, With<Camera>)>,
|
||||
) {
|
||||
for (camera, mut transform) in query.iter_mut() {
|
||||
let rot = Quat::from_axis_angle(Vec3::Y, camera.x)
|
||||
* Quat::from_axis_angle(-Vec3::X, camera.y);
|
||||
transform.translation = (rot * Vec3::Y) * camera.distance + camera.center;
|
||||
transform.look_at(camera.center, Vec3::Y);
|
||||
}
|
||||
}
|
||||
|
||||
fn mouse_motion_system(
|
||||
time: Res<Time>,
|
||||
mut mouse_motion_events: EventReader<MouseMotion>,
|
||||
mouse_button_input: Res<ButtonInput<MouseButton>>,
|
||||
mut query: Query<(&mut OrbitCamera, &mut Transform, &mut Camera)>,
|
||||
) {
|
||||
let mut delta = Vec2::ZERO;
|
||||
for event in mouse_motion_events.read() {
|
||||
delta += event.delta;
|
||||
}
|
||||
for (mut camera, transform, _) in query.iter_mut() {
|
||||
if !camera.enabled {
|
||||
continue;
|
||||
}
|
||||
|
||||
if mouse_button_input.pressed(camera.rotate_button) {
|
||||
camera.x -= delta.x * camera.rotate_sensitivity * time.delta_secs();
|
||||
camera.y -= delta.y * camera.rotate_sensitivity * time.delta_secs();
|
||||
camera.y = camera
|
||||
.y
|
||||
.max(*camera.pitch_range.start())
|
||||
.min(*camera.pitch_range.end());
|
||||
}
|
||||
|
||||
if mouse_button_input.pressed(camera.pan_button) {
|
||||
let right_dir = transform.rotation * -Vec3::X;
|
||||
let up_dir = transform.rotation * Vec3::Y;
|
||||
let pan_vector = (delta.x * right_dir + delta.y * up_dir)
|
||||
* camera.pan_sensitivity
|
||||
* time.delta_secs();
|
||||
camera.center += pan_vector;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn zoom_system(
|
||||
mut mouse_wheel_events: EventReader<MouseWheel>,
|
||||
mut query: Query<&mut OrbitCamera, With<Camera>>,
|
||||
) {
|
||||
let mut total = 0.0;
|
||||
for event in mouse_wheel_events.read() {
|
||||
total += event.y
|
||||
* match event.unit {
|
||||
Line => 1.0,
|
||||
Pixel => LINE_TO_PIXEL_RATIO,
|
||||
};
|
||||
}
|
||||
for mut camera in query.iter_mut() {
|
||||
if camera.enabled {
|
||||
camera.distance *= camera.zoom_sensitivity.powf(total);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Plugin for OrbitCameraPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Update, Self::mouse_motion_system)
|
||||
.add_systems(Update, Self::zoom_system)
|
||||
.add_systems(Update, Self::update_transform_system);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +1,69 @@
|
||||
#![allow(clippy::unnecessary_cast)] // Casts are needed for switching between f32/f64.
|
||||
|
||||
use crate::harness::Harness;
|
||||
use bevy::gizmos::gizmos::Gizmos;
|
||||
use bevy::prelude::*;
|
||||
use rapier::math::{Point, Real};
|
||||
use kiss3d::window::Window;
|
||||
use rapier::math::Vector;
|
||||
use rapier::pipeline::{
|
||||
DebugColor, DebugRenderBackend, DebugRenderMode, DebugRenderObject, DebugRenderPipeline,
|
||||
};
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct DebugRenderPipelineResource {
|
||||
pub pipeline: DebugRenderPipeline,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RapierDebugRenderPlugin {}
|
||||
|
||||
impl Plugin for RapierDebugRenderPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.insert_resource(DebugRenderPipelineResource {
|
||||
impl Default for DebugRenderPipelineResource {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pipeline: DebugRenderPipeline::new(
|
||||
Default::default(),
|
||||
!DebugRenderMode::RIGID_BODY_AXES & !DebugRenderMode::COLLIDER_AABBS,
|
||||
),
|
||||
enabled: false,
|
||||
})
|
||||
.add_systems(Update, debug_render_scene);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BevyLinesRenderBackend<'w, 's> {
|
||||
gizmos: Gizmos<'w, 's>,
|
||||
/// Kiss3d-based debug render backend
|
||||
pub struct Kiss3dLinesRenderBackend<'a> {
|
||||
pub window: &'a mut Window,
|
||||
}
|
||||
|
||||
impl<'w, 's> DebugRenderBackend for BevyLinesRenderBackend<'w, 's> {
|
||||
impl<'a> DebugRenderBackend for Kiss3dLinesRenderBackend<'a> {
|
||||
#[cfg(feature = "dim2")]
|
||||
fn draw_line(
|
||||
&mut self,
|
||||
_: DebugRenderObject,
|
||||
a: Point<Real>,
|
||||
b: Point<Real>,
|
||||
color: DebugColor,
|
||||
) {
|
||||
self.gizmos.line(
|
||||
[a.x as f32, a.y as f32, 1.0e-8].into(),
|
||||
[b.x as f32, b.y as f32, 1.0e-8].into(),
|
||||
Color::hsla(color[0], color[1], color[2], color[3]),
|
||||
)
|
||||
fn draw_line(&mut self, _: DebugRenderObject, a: Vector, b: Vector, color: DebugColor) {
|
||||
// Convert HSLA to RGB
|
||||
let rgb = hsla_to_rgb(color[0], color[1], color[2], color[3]);
|
||||
self.window.draw_line_2d(
|
||||
glamx::Vec2::new(a.x as f32, a.y as f32),
|
||||
glamx::Vec2::new(b.x as f32, b.y as f32),
|
||||
rgb.into(),
|
||||
4.0,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
fn draw_line(
|
||||
&mut self,
|
||||
_: DebugRenderObject,
|
||||
a: Point<Real>,
|
||||
b: Point<Real>,
|
||||
color: DebugColor,
|
||||
) {
|
||||
self.gizmos.line(
|
||||
[a.x as f32, a.y as f32, a.z as f32].into(),
|
||||
[b.x as f32, b.y as f32, b.z as f32].into(),
|
||||
Color::hsla(color[0], color[1], color[2], color[3]),
|
||||
)
|
||||
fn draw_line(&mut self, _: DebugRenderObject, a: Vector, b: Vector, color: DebugColor) {
|
||||
// Convert HSLA to RGB
|
||||
let rgb = hsla_to_rgb(color[0], color[1], color[2], color[3]);
|
||||
self.window.draw_line(
|
||||
glamx::Vec3::new(a.x as f32, a.y as f32, a.z as f32),
|
||||
glamx::Vec3::new(b.x as f32, b.y as f32, b.z as f32),
|
||||
rgb.into(),
|
||||
4.0,
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_render_scene(
|
||||
mut debug_render: ResMut<DebugRenderPipelineResource>,
|
||||
harness: NonSend<Harness>,
|
||||
gizmos: Gizmos,
|
||||
/// Render debug visualization using kiss3d
|
||||
pub fn debug_render_scene(
|
||||
window: &mut Window,
|
||||
debug_render: &mut DebugRenderPipelineResource,
|
||||
harness: &Harness,
|
||||
) {
|
||||
if debug_render.enabled {
|
||||
let mut backend = BevyLinesRenderBackend { gizmos };
|
||||
let mut backend = Kiss3dLinesRenderBackend { window };
|
||||
debug_render.pipeline.render(
|
||||
&mut backend,
|
||||
&harness.physics.bodies,
|
||||
@@ -82,3 +74,43 @@ fn debug_render_scene(
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert HSLA color to RGB
|
||||
fn hsla_to_rgb(h: f32, s: f32, l: f32, a: f32) -> [f32; 4] {
|
||||
if s == 0.0 {
|
||||
return [l, l, l, a];
|
||||
}
|
||||
|
||||
let q = if l < 0.5 {
|
||||
l * (1.0 + s)
|
||||
} else {
|
||||
l + s - l * s
|
||||
};
|
||||
let p = 2.0 * l - q;
|
||||
|
||||
let r = hue_to_rgb(p, q, h / 360.0 + 1.0 / 3.0);
|
||||
let g = hue_to_rgb(p, q, h / 360.0);
|
||||
let b = hue_to_rgb(p, q, h / 360.0 - 1.0 / 3.0);
|
||||
|
||||
[r, g, b, a]
|
||||
}
|
||||
|
||||
fn hue_to_rgb(p: f32, q: f32, t: f32) -> f32 {
|
||||
let t = if t < 0.0 {
|
||||
t + 1.0
|
||||
} else if t > 1.0 {
|
||||
t - 1.0
|
||||
} else {
|
||||
t
|
||||
};
|
||||
|
||||
if t < 1.0 / 6.0 {
|
||||
p + (q - p) * 6.0 * t
|
||||
} else if t < 1.0 / 2.0 {
|
||||
q
|
||||
} else if t < 2.0 / 3.0 {
|
||||
p + (q - p) * (2.0 / 3.0 - t) * 6.0
|
||||
} else {
|
||||
p
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,7 +10,6 @@ use rapier::dynamics::{
|
||||
RigidBodySet,
|
||||
};
|
||||
use rapier::geometry::{BroadPhaseBvh, BvhOptimizationStrategy, ColliderSet, NarrowPhase};
|
||||
use rapier::math::{Real, Vector};
|
||||
use rapier::pipeline::{ChannelEventCollector, PhysicsHooks, PhysicsPipeline};
|
||||
|
||||
pub mod plugin;
|
||||
@@ -184,7 +183,7 @@ impl Harness {
|
||||
impulse_joints,
|
||||
multibody_joints,
|
||||
broad_phase_type,
|
||||
Vector::y() * -9.81,
|
||||
rapier::math::Vector::Y * -9.81,
|
||||
(),
|
||||
)
|
||||
}
|
||||
@@ -196,11 +195,9 @@ impl Harness {
|
||||
impulse_joints: ImpulseJointSet,
|
||||
multibody_joints: MultibodyJointSet,
|
||||
broad_phase_type: RapierBroadPhaseType,
|
||||
gravity: Vector<Real>,
|
||||
gravity: rapier::math::Vector,
|
||||
hooks: impl PhysicsHooks + 'static,
|
||||
) {
|
||||
// println!("Num bodies: {}", bodies.len());
|
||||
// println!("Num impulse_joints: {}", impulse_joints.len());
|
||||
self.physics.gravity = gravity;
|
||||
self.physics.bodies = bodies;
|
||||
self.physics.colliders = colliders;
|
||||
@@ -243,7 +240,7 @@ impl Harness {
|
||||
let event_handler = &self.event_handler;
|
||||
self.state.thread_pool.install(|| {
|
||||
physics.pipeline.step(
|
||||
&physics.gravity,
|
||||
physics.gravity,
|
||||
&physics.integration_parameters,
|
||||
&mut physics.islands,
|
||||
&mut physics.broad_phase,
|
||||
@@ -261,7 +258,7 @@ impl Harness {
|
||||
|
||||
#[cfg(not(feature = "parallel"))]
|
||||
self.physics.pipeline.step(
|
||||
&self.physics.gravity,
|
||||
self.physics.gravity,
|
||||
&self.physics.integration_parameters,
|
||||
&mut self.physics.islands,
|
||||
&mut self.physics.broad_phase,
|
||||
|
||||
@@ -2,22 +2,29 @@
|
||||
|
||||
extern crate nalgebra as na;
|
||||
|
||||
pub use crate::graphics::{BevyMaterial, GraphicsManager};
|
||||
pub use crate::graphics::GraphicsManager;
|
||||
pub use crate::harness::plugin::HarnessPlugin;
|
||||
pub use crate::physics::PhysicsState;
|
||||
pub use crate::plugin::TestbedPlugin;
|
||||
pub use crate::testbed::{Testbed, TestbedApp, TestbedGraphics, TestbedState};
|
||||
pub use bevy::prelude::{Color, KeyCode};
|
||||
pub use crate::testbed::{
|
||||
Example, KeysState, RunMode, Testbed, TestbedActionFlags, TestbedApp, TestbedGraphics,
|
||||
TestbedState, TestbedStateFlags,
|
||||
};
|
||||
|
||||
// Re-export kiss3d types that users might need
|
||||
pub use kiss3d::event::{Action, Key, MouseButton, WindowEvent};
|
||||
|
||||
// KeyCode alias for backwards compatibility with examples
|
||||
// Maps to kiss3d Key variants
|
||||
pub use kiss3d::event::Key as KeyCode;
|
||||
|
||||
// Re-export egui for UI
|
||||
pub use egui;
|
||||
|
||||
#[cfg(feature = "dim2")]
|
||||
mod camera2d;
|
||||
#[cfg(feature = "dim3")]
|
||||
mod camera3d;
|
||||
mod debug_render;
|
||||
mod graphics;
|
||||
pub mod harness;
|
||||
mod mouse;
|
||||
pub mod objects;
|
||||
pub mod physics;
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
mod physx_backend;
|
||||
@@ -27,18 +34,23 @@ mod settings;
|
||||
mod testbed;
|
||||
pub mod ui;
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
pub use kiss3d::camera::OrbitCamera3d as Camera;
|
||||
#[cfg(feature = "dim2")]
|
||||
pub use kiss3d::camera::PanZoomCamera2d as Camera;
|
||||
|
||||
#[cfg(feature = "dim2")]
|
||||
pub mod math {
|
||||
pub type Isometry<N> = na::Isometry2<N>;
|
||||
pub type Vector<N> = na::Vector2<N>;
|
||||
pub type SimdPose<N> = na::Isometry2<N>;
|
||||
pub type SimdVector<N> = na::Vector2<N>;
|
||||
pub type Point<N> = na::Point2<N>;
|
||||
pub type Translation<N> = na::Translation2<N>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
pub mod math {
|
||||
pub type Isometry<N> = na::Isometry3<N>;
|
||||
pub type Vector<N> = na::Vector3<N>;
|
||||
pub type SimdPose<N> = na::Isometry3<N>;
|
||||
pub type SimdVector<N> = na::Vector3<N>;
|
||||
pub type Point<N> = na::Point3<N>;
|
||||
pub type Translation<N> = na::Translation3<N>;
|
||||
}
|
||||
|
||||
@@ -1,46 +1,54 @@
|
||||
use crate::math::Point;
|
||||
use bevy::prelude::*;
|
||||
use bevy::window::PrimaryWindow;
|
||||
use glamx::Vec2;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct MainCamera;
|
||||
|
||||
#[derive(Default, Copy, Clone, Debug, Resource)]
|
||||
#[derive(Default, Copy, Clone, Debug)]
|
||||
pub struct SceneMouse {
|
||||
#[cfg(feature = "dim2")]
|
||||
pub point: Option<Point<f32>>,
|
||||
pub point: Option<Vec2>,
|
||||
#[cfg(feature = "dim3")]
|
||||
pub ray: Option<(Point<f32>, crate::math::Vector<f32>)>,
|
||||
pub ray: Option<(glamx::Vec3, glamx::Vec3)>,
|
||||
}
|
||||
|
||||
pub fn track_mouse_state(
|
||||
mut scene_mouse: ResMut<SceneMouse>,
|
||||
windows: Query<&Window, With<PrimaryWindow>>,
|
||||
camera: Query<(&GlobalTransform, &Camera), With<MainCamera>>,
|
||||
) {
|
||||
if let Ok(window) = windows.get_single() {
|
||||
for (camera_transform, camera) in camera.iter() {
|
||||
if let Some(cursor) = window.cursor_position() {
|
||||
let ndc_cursor = ((cursor / Vec2::new(window.width(), window.height()) * 2.0)
|
||||
- Vec2::ONE)
|
||||
* Vec2::new(1.0, -1.0);
|
||||
let ndc_to_world =
|
||||
camera_transform.compute_matrix() * camera.clip_from_view().inverse();
|
||||
let ray_pt1 =
|
||||
ndc_to_world.project_point3(Vec3::new(ndc_cursor.x, ndc_cursor.y, -1.0));
|
||||
impl SceneMouse {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim2")]
|
||||
{
|
||||
scene_mouse.point = Some(Point::new(ray_pt1.x, ray_pt1.y));
|
||||
}
|
||||
#[cfg(feature = "dim3")]
|
||||
{
|
||||
let ray_pt2 =
|
||||
ndc_to_world.project_point3(Vec3::new(ndc_cursor.x, ndc_cursor.y, 1.0));
|
||||
let ray_dir = ray_pt2 - ray_pt1;
|
||||
scene_mouse.ray = Some((na::Vector3::from(ray_pt1).into(), ray_dir.into()));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "dim2")]
|
||||
pub fn update_from_window(
|
||||
&mut self,
|
||||
cursor_pos: Option<(f64, f64)>,
|
||||
window_size: (u32, u32),
|
||||
camera: &kiss3d::camera::PanZoomCamera2d,
|
||||
) {
|
||||
use kiss3d::camera::Camera2d;
|
||||
|
||||
if let Some((x, y)) = cursor_pos {
|
||||
// Convert cursor position to world coordinates using the camera
|
||||
let cursor = Vec2::new(x as f32, y as f32);
|
||||
let window_size = Vec2::new(window_size.0 as f32, window_size.1 as f32);
|
||||
let world_pos = camera.unproject(cursor, window_size);
|
||||
self.point = Some(world_pos);
|
||||
} else {
|
||||
self.point = None;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
pub fn update_from_window(
|
||||
&mut self,
|
||||
cursor_pos: Option<(f64, f64)>,
|
||||
window_size: (u32, u32),
|
||||
camera: &crate::Camera,
|
||||
) {
|
||||
use kiss3d::camera::Camera3d;
|
||||
|
||||
if let Some((x, y)) = cursor_pos {
|
||||
// Convert cursor position to world coordinates using the camera
|
||||
let cursor = Vec2::new(x as f32, y as f32);
|
||||
let window_size = Vec2::new(window_size.0 as f32, window_size.1 as f32);
|
||||
self.ray = Some(camera.unproject(cursor, window_size));
|
||||
} else {
|
||||
self.ray = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub mod node;
|
||||
@@ -1,483 +0,0 @@
|
||||
#![allow(clippy::unnecessary_cast)] // Casts are needed for switching between f32/f64.
|
||||
|
||||
use bevy::prelude::*;
|
||||
use bevy::render::mesh::{Indices, VertexAttributeValues};
|
||||
|
||||
//use crate::objects::plane::Plane;
|
||||
use na::{Point3, Vector3, point};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bevy::render::render_resource::PrimitiveTopology;
|
||||
use bevy_pbr::wireframe::Wireframe;
|
||||
use rapier::geometry::{ColliderHandle, ColliderSet, Shape, ShapeType};
|
||||
#[cfg(feature = "dim3")]
|
||||
use rapier::geometry::{Cone, Cylinder};
|
||||
use rapier::math::{Isometry, Real, Vector};
|
||||
|
||||
use crate::graphics::{BevyMaterial, InstancedMaterials, SELECTED_OBJECT_COLOR};
|
||||
#[cfg(feature = "dim2")]
|
||||
use {
|
||||
na::{Point2, Vector2, vector},
|
||||
rapier::geometry::{Ball, Cuboid},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EntityWithGraphics {
|
||||
pub entity: Entity,
|
||||
pub color: Point3<f32>,
|
||||
pub base_color: Point3<f32>,
|
||||
pub collider: Option<ColliderHandle>,
|
||||
pub delta: Isometry<Real>,
|
||||
pub opacity: f32,
|
||||
pub material: Handle<BevyMaterial>,
|
||||
}
|
||||
|
||||
impl EntityWithGraphics {
|
||||
pub fn register_selected_object_material(
|
||||
materials: &mut Assets<BevyMaterial>,
|
||||
instanced_materials: &mut InstancedMaterials,
|
||||
) {
|
||||
instanced_materials.insert(materials, SELECTED_OBJECT_COLOR, 1.0);
|
||||
}
|
||||
|
||||
pub fn spawn(
|
||||
commands: &mut Commands,
|
||||
meshes: &mut Assets<Mesh>,
|
||||
materials: &mut Assets<BevyMaterial>,
|
||||
prefab_meshs: &HashMap<ShapeType, Handle<Mesh>>,
|
||||
instanced_materials: &mut InstancedMaterials,
|
||||
shape: &dyn Shape,
|
||||
collider: Option<ColliderHandle>,
|
||||
collider_pos: Isometry<Real>,
|
||||
delta: Isometry<Real>,
|
||||
color: Point3<f32>,
|
||||
sensor: bool,
|
||||
) -> Self {
|
||||
Self::register_selected_object_material(materials, instanced_materials);
|
||||
|
||||
let entity = commands.spawn_empty().id();
|
||||
|
||||
let scale = collider_mesh_scale(shape);
|
||||
let mesh = prefab_meshs
|
||||
.get(&shape.shape_type())
|
||||
.cloned()
|
||||
.or_else(|| generate_collider_mesh(shape).map(|m| meshes.add(m)));
|
||||
|
||||
let opacity = if sensor { 0.25 } else { 1.0 };
|
||||
let shape_pos = collider_pos * delta;
|
||||
let mut transform = Transform::from_scale(scale);
|
||||
transform.translation.x = shape_pos.translation.vector.x as f32;
|
||||
transform.translation.y = shape_pos.translation.vector.y as f32;
|
||||
#[cfg(feature = "dim3")]
|
||||
{
|
||||
transform.translation.z = shape_pos.translation.vector.z as f32;
|
||||
transform.rotation = Quat::from_xyzw(
|
||||
shape_pos.rotation.i as f32,
|
||||
shape_pos.rotation.j as f32,
|
||||
shape_pos.rotation.k as f32,
|
||||
shape_pos.rotation.w as f32,
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "dim2")]
|
||||
{
|
||||
if sensor {
|
||||
transform.translation.z = -10.0;
|
||||
}
|
||||
transform.rotation = Quat::from_rotation_z(shape_pos.rotation.angle() as f32);
|
||||
}
|
||||
|
||||
let material_handle = instanced_materials.insert(materials, color, opacity);
|
||||
|
||||
if let Some(mesh) = mesh {
|
||||
#[cfg(feature = "dim2")]
|
||||
let bundle = (
|
||||
Mesh2d(mesh),
|
||||
MeshMaterial2d(material_handle.clone_weak()),
|
||||
transform,
|
||||
);
|
||||
#[cfg(feature = "dim3")]
|
||||
let bundle = (
|
||||
Mesh3d(mesh),
|
||||
MeshMaterial3d(material_handle.clone_weak()),
|
||||
transform,
|
||||
);
|
||||
|
||||
let mut entity_commands = commands.entity(entity);
|
||||
entity_commands.insert(bundle);
|
||||
|
||||
if sensor {
|
||||
entity_commands.insert(Wireframe);
|
||||
}
|
||||
}
|
||||
|
||||
EntityWithGraphics {
|
||||
entity,
|
||||
color,
|
||||
base_color: color,
|
||||
collider,
|
||||
delta,
|
||||
material: material_handle,
|
||||
opacity,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn despawn(&mut self, commands: &mut Commands) {
|
||||
//FIXME: Should this be despawn_recursive?
|
||||
commands.entity(self.entity).despawn();
|
||||
}
|
||||
|
||||
pub fn set_color(
|
||||
&mut self,
|
||||
materials: &mut Assets<BevyMaterial>,
|
||||
instanced_materials: &mut InstancedMaterials,
|
||||
color: Point3<f32>,
|
||||
) {
|
||||
self.material = instanced_materials.insert(materials, color, self.opacity);
|
||||
self.color = color;
|
||||
self.base_color = color;
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
colliders: &ColliderSet,
|
||||
components: &mut Query<&mut Transform>,
|
||||
gfx_shift: &Vector<Real>,
|
||||
) {
|
||||
if let Some(Some(co)) = self.collider.map(|c| colliders.get(c))
|
||||
&& let Ok(mut pos) = components.get_mut(self.entity)
|
||||
{
|
||||
let co_pos = co.position() * self.delta;
|
||||
pos.translation.x = (co_pos.translation.vector.x + gfx_shift.x) as f32;
|
||||
pos.translation.y = (co_pos.translation.vector.y + gfx_shift.y) as f32;
|
||||
#[cfg(feature = "dim3")]
|
||||
{
|
||||
pos.translation.z = (co_pos.translation.vector.z + gfx_shift.z) as f32;
|
||||
pos.rotation = Quat::from_xyzw(
|
||||
co_pos.rotation.i as f32,
|
||||
co_pos.rotation.j as f32,
|
||||
co_pos.rotation.k as f32,
|
||||
co_pos.rotation.w as f32,
|
||||
);
|
||||
}
|
||||
#[cfg(feature = "dim2")]
|
||||
{
|
||||
pos.rotation = Quat::from_rotation_z(co_pos.rotation.angle() as f32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object(&self) -> Option<ColliderHandle> {
|
||||
self.collider
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim2")]
|
||||
pub fn gen_prefab_meshes(
|
||||
out: &mut HashMap<ShapeType, Handle<Mesh>>,
|
||||
meshes: &mut Assets<Mesh>,
|
||||
) {
|
||||
//
|
||||
// Cuboid mesh
|
||||
//
|
||||
let cuboid = bevy_mesh_from_polyline(Cuboid::new(Vector2::new(1.0, 1.0)).to_polyline());
|
||||
out.insert(ShapeType::Cuboid, meshes.add(cuboid.clone()));
|
||||
out.insert(ShapeType::RoundCuboid, meshes.add(cuboid));
|
||||
|
||||
//
|
||||
// Ball mesh
|
||||
//
|
||||
let ball = bevy_mesh_from_polyline(Ball::new(1.0).to_polyline(30));
|
||||
out.insert(ShapeType::Ball, meshes.add(ball));
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
pub fn gen_prefab_meshes(
|
||||
out: &mut HashMap<ShapeType, Handle<Mesh>>,
|
||||
meshes: &mut Assets<Mesh>,
|
||||
) {
|
||||
//
|
||||
// Cuboid mesh
|
||||
//
|
||||
let cuboid = Mesh::from(bevy::math::primitives::Cuboid::new(2.0, 2.0, 2.0));
|
||||
out.insert(ShapeType::Cuboid, meshes.add(cuboid.clone()));
|
||||
out.insert(ShapeType::RoundCuboid, meshes.add(cuboid));
|
||||
|
||||
//
|
||||
// Ball mesh
|
||||
//
|
||||
let ball = Mesh::from(bevy::math::primitives::Sphere::new(1.0));
|
||||
out.insert(ShapeType::Ball, meshes.add(ball));
|
||||
|
||||
//
|
||||
// Cylinder mesh
|
||||
//
|
||||
let cylinder = Cylinder::new(1.0, 1.0);
|
||||
let mesh = bevy_mesh(cylinder.to_trimesh(20));
|
||||
out.insert(ShapeType::Cylinder, meshes.add(mesh.clone()));
|
||||
out.insert(ShapeType::RoundCylinder, meshes.add(mesh));
|
||||
|
||||
//
|
||||
// Cone mesh
|
||||
//
|
||||
let cone = Cone::new(1.0, 1.0);
|
||||
let mesh = bevy_mesh(cone.to_trimesh(10));
|
||||
out.insert(ShapeType::Cone, meshes.add(mesh.clone()));
|
||||
out.insert(ShapeType::RoundCone, meshes.add(mesh));
|
||||
|
||||
//
|
||||
// Halfspace
|
||||
//
|
||||
let vertices = vec![
|
||||
point![-1000.0, 0.0, -1000.0],
|
||||
point![1000.0, 0.0, -1000.0],
|
||||
point![1000.0, 0.0, 1000.0],
|
||||
point![-1000.0, 0.0, 1000.0],
|
||||
];
|
||||
let indices = vec![[0, 1, 2], [0, 2, 3]];
|
||||
let mesh = bevy_mesh((vertices, indices));
|
||||
out.insert(ShapeType::HalfSpace, meshes.add(mesh));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim2")]
|
||||
fn bevy_mesh_from_polyline(vertices: Vec<Point2<Real>>) -> Mesh {
|
||||
let n = vertices.len();
|
||||
let idx = (1..n as u32 - 1).map(|i| [0, i, i + 1]).collect();
|
||||
let vtx = vertices
|
||||
.into_iter()
|
||||
.map(|v| Point3::new(v.x, v.y, 0.0))
|
||||
.collect();
|
||||
bevy_mesh((vtx, idx))
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim2")]
|
||||
fn bevy_polyline(buffers: (Vec<Point2<Real>>, Option<Vec<[u32; 2]>>)) -> Mesh {
|
||||
let (vtx, idx) = buffers;
|
||||
// let mut normals: Vec<[f32; 3]> = vec![];
|
||||
let mut vertices: Vec<[f32; 3]> = vec![];
|
||||
|
||||
if let Some(idx) = idx {
|
||||
for idx in idx {
|
||||
let a = vtx[idx[0] as usize];
|
||||
let b = vtx[idx[1] as usize];
|
||||
|
||||
vertices.push([a.x as f32, a.y as f32, 0.0]);
|
||||
vertices.push([b.x as f32, b.y as f32, 0.0]);
|
||||
}
|
||||
} else {
|
||||
vertices = vtx.iter().map(|v| [v.x as f32, v.y as f32, 0.0]).collect();
|
||||
}
|
||||
|
||||
let indices: Vec<_> = (0..vertices.len() as u32).collect();
|
||||
let uvs: Vec<_> = (0..vertices.len()).map(|_| [0.0, 0.0]).collect();
|
||||
let normals: Vec<_> = (0..vertices.len()).map(|_| [0.0, 0.0, 1.0]).collect();
|
||||
|
||||
// Generate the mesh
|
||||
let mut mesh = Mesh::new(PrimitiveTopology::LineStrip, Default::default());
|
||||
mesh.insert_attribute(
|
||||
Mesh::ATTRIBUTE_POSITION,
|
||||
VertexAttributeValues::from(vertices),
|
||||
);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, VertexAttributeValues::from(normals));
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, VertexAttributeValues::from(uvs));
|
||||
mesh.insert_indices(Indices::U32(indices));
|
||||
mesh
|
||||
}
|
||||
|
||||
fn bevy_mesh(buffers: (Vec<Point3<Real>>, Vec<[u32; 3]>)) -> Mesh {
|
||||
let (vtx, idx) = buffers;
|
||||
let mut normals: Vec<[f32; 3]> = vec![];
|
||||
let mut vertices: Vec<[f32; 3]> = vec![];
|
||||
|
||||
for idx in idx {
|
||||
let a = vtx[idx[0] as usize];
|
||||
let b = vtx[idx[1] as usize];
|
||||
let c = vtx[idx[2] as usize];
|
||||
|
||||
vertices.push(a.cast::<f32>().into());
|
||||
vertices.push(b.cast::<f32>().into());
|
||||
vertices.push(c.cast::<f32>().into());
|
||||
}
|
||||
|
||||
for vtx in vertices.chunks(3) {
|
||||
let a = Point3::from(vtx[0]);
|
||||
let b = Point3::from(vtx[1]);
|
||||
let c = Point3::from(vtx[2]);
|
||||
let n = (b - a).cross(&(c - a)).normalize();
|
||||
normals.push(n.cast::<f32>().into());
|
||||
normals.push(n.cast::<f32>().into());
|
||||
normals.push(n.cast::<f32>().into());
|
||||
}
|
||||
|
||||
normals
|
||||
.iter_mut()
|
||||
.for_each(|n| *n = Vector3::from(*n).normalize().into());
|
||||
let indices: Vec<_> = (0..vertices.len() as u32).collect();
|
||||
let uvs: Vec<_> = (0..vertices.len()).map(|_| [0.0, 0.0]).collect();
|
||||
|
||||
// Generate the mesh
|
||||
let mut mesh = Mesh::new(PrimitiveTopology::TriangleList, Default::default());
|
||||
mesh.insert_attribute(
|
||||
Mesh::ATTRIBUTE_POSITION,
|
||||
VertexAttributeValues::from(vertices),
|
||||
);
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, VertexAttributeValues::from(normals));
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, VertexAttributeValues::from(uvs));
|
||||
mesh.insert_indices(Indices::U32(indices));
|
||||
mesh
|
||||
}
|
||||
|
||||
fn collider_mesh_scale(co_shape: &dyn Shape) -> Vec3 {
|
||||
match co_shape.shape_type() {
|
||||
#[cfg(feature = "dim2")]
|
||||
ShapeType::Cuboid => {
|
||||
let c = co_shape.as_cuboid().unwrap();
|
||||
Vec3::new(c.half_extents.x as f32, c.half_extents.y as f32, 1.0)
|
||||
}
|
||||
#[cfg(feature = "dim2")]
|
||||
ShapeType::RoundCuboid => {
|
||||
let c = &co_shape.as_round_cuboid().unwrap().inner_shape;
|
||||
Vec3::new(c.half_extents.x as f32, c.half_extents.y as f32, 1.0)
|
||||
}
|
||||
ShapeType::Ball => {
|
||||
let b = co_shape.as_ball().unwrap();
|
||||
Vec3::new(b.radius as f32, b.radius as f32, b.radius as f32)
|
||||
}
|
||||
#[cfg(feature = "dim3")]
|
||||
ShapeType::Cuboid => {
|
||||
let c = co_shape.as_cuboid().unwrap();
|
||||
Vec3::from_slice(c.half_extents.cast::<f32>().as_slice())
|
||||
}
|
||||
#[cfg(feature = "dim3")]
|
||||
ShapeType::RoundCuboid => {
|
||||
let c = co_shape.as_round_cuboid().unwrap();
|
||||
Vec3::from_slice(c.inner_shape.half_extents.cast::<f32>().as_slice())
|
||||
}
|
||||
#[cfg(feature = "dim3")]
|
||||
ShapeType::Cylinder => {
|
||||
let c = co_shape.as_cylinder().unwrap();
|
||||
Vec3::new(c.radius as f32, c.half_height as f32, c.radius as f32)
|
||||
}
|
||||
#[cfg(feature = "dim3")]
|
||||
ShapeType::RoundCylinder => {
|
||||
let c = &co_shape.as_round_cylinder().unwrap().inner_shape;
|
||||
Vec3::new(c.radius as f32, c.half_height as f32, c.radius as f32)
|
||||
}
|
||||
#[cfg(feature = "dim3")]
|
||||
ShapeType::Cone => {
|
||||
let c = co_shape.as_cone().unwrap();
|
||||
Vec3::new(c.radius as f32, c.half_height as f32, c.radius as f32)
|
||||
}
|
||||
#[cfg(feature = "dim3")]
|
||||
ShapeType::RoundCone => {
|
||||
let c = &co_shape.as_round_cone().unwrap().inner_shape;
|
||||
Vec3::new(c.radius as f32, c.half_height as f32, c.radius as f32)
|
||||
}
|
||||
_ => Vec3::ONE,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim2")]
|
||||
fn generate_collider_mesh(co_shape: &dyn Shape) -> Option<Mesh> {
|
||||
let mesh = match co_shape.shape_type() {
|
||||
ShapeType::Capsule => {
|
||||
let capsule = co_shape.as_capsule().unwrap();
|
||||
bevy_mesh_from_polyline(capsule.to_polyline(10))
|
||||
}
|
||||
ShapeType::Triangle => {
|
||||
let tri = co_shape.as_triangle().unwrap();
|
||||
bevy_mesh_from_polyline(vec![tri.a, tri.b, tri.c])
|
||||
}
|
||||
ShapeType::TriMesh => {
|
||||
let trimesh = co_shape.as_trimesh().unwrap();
|
||||
let vertices = trimesh
|
||||
.vertices()
|
||||
.iter()
|
||||
.map(|p| point![p.x, p.y, 0.0])
|
||||
.collect();
|
||||
bevy_mesh((vertices, trimesh.indices().to_vec()))
|
||||
}
|
||||
ShapeType::Voxels => {
|
||||
let mut vtx = vec![];
|
||||
let mut idx = vec![];
|
||||
let voxels = co_shape.as_voxels().unwrap();
|
||||
let sz = voxels.voxel_size() / 2.0;
|
||||
for vox in voxels.voxels() {
|
||||
if !vox.state.is_empty() {
|
||||
let bid = vtx.len() as u32;
|
||||
let center = point![vox.center.x, vox.center.y, 0.0];
|
||||
vtx.push(center + vector![sz.x, sz.y, 0.0]);
|
||||
vtx.push(center + vector![-sz.x, sz.y, 0.0]);
|
||||
vtx.push(center + vector![-sz.x, -sz.y, 0.0]);
|
||||
vtx.push(center + vector![sz.x, -sz.y, 0.0]);
|
||||
idx.push([bid, bid + 1, bid + 2]);
|
||||
idx.push([bid + 2, bid + 3, bid]);
|
||||
}
|
||||
}
|
||||
|
||||
bevy_mesh((vtx, idx))
|
||||
}
|
||||
ShapeType::Polyline => {
|
||||
let polyline = co_shape.as_polyline().unwrap();
|
||||
bevy_polyline((
|
||||
polyline.vertices().to_vec(),
|
||||
Some(polyline.indices().to_vec()),
|
||||
))
|
||||
}
|
||||
ShapeType::HeightField => {
|
||||
let heightfield = co_shape.as_heightfield().unwrap();
|
||||
let vertices: Vec<_> = heightfield
|
||||
.segments()
|
||||
.flat_map(|s| vec![s.a, s.b])
|
||||
.collect();
|
||||
bevy_polyline((vertices, None))
|
||||
}
|
||||
ShapeType::ConvexPolygon => {
|
||||
let poly = co_shape.as_convex_polygon().unwrap();
|
||||
bevy_mesh_from_polyline(poly.points().to_vec())
|
||||
}
|
||||
ShapeType::RoundConvexPolygon => {
|
||||
let poly = co_shape.as_round_convex_polygon().unwrap();
|
||||
bevy_mesh_from_polyline(poly.inner_shape.points().to_vec())
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(mesh)
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
fn generate_collider_mesh(co_shape: &dyn Shape) -> Option<Mesh> {
|
||||
let mesh = match co_shape.shape_type() {
|
||||
ShapeType::Capsule => {
|
||||
let capsule = co_shape.as_capsule().unwrap();
|
||||
bevy_mesh(capsule.to_trimesh(20, 10))
|
||||
}
|
||||
ShapeType::Triangle => {
|
||||
let tri = co_shape.as_triangle().unwrap();
|
||||
bevy_mesh((vec![tri.a, tri.b, tri.c], vec![[0, 1, 2], [0, 2, 1]]))
|
||||
}
|
||||
ShapeType::TriMesh => {
|
||||
let trimesh = co_shape.as_trimesh().unwrap();
|
||||
bevy_mesh((trimesh.vertices().to_vec(), trimesh.indices().to_vec()))
|
||||
}
|
||||
ShapeType::HeightField => {
|
||||
let heightfield = co_shape.as_heightfield().unwrap();
|
||||
bevy_mesh(heightfield.to_trimesh())
|
||||
}
|
||||
ShapeType::ConvexPolyhedron => {
|
||||
let poly = co_shape.as_convex_polyhedron().unwrap();
|
||||
bevy_mesh(poly.to_trimesh())
|
||||
}
|
||||
ShapeType::RoundConvexPolyhedron => {
|
||||
let poly = co_shape.as_round_convex_polyhedron().unwrap();
|
||||
bevy_mesh(poly.inner_shape.to_trimesh())
|
||||
}
|
||||
ShapeType::Voxels => {
|
||||
let voxels = co_shape.as_voxels().unwrap();
|
||||
bevy_mesh(voxels.to_trimesh())
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(mesh)
|
||||
}
|
||||
@@ -5,10 +5,10 @@ use rapier::dynamics::{
|
||||
use rapier::geometry::{
|
||||
BroadPhaseBvh, ColliderSet, CollisionEvent, ContactForceEvent, DefaultBroadPhase, NarrowPhase,
|
||||
};
|
||||
use rapier::math::{Real, Vector};
|
||||
use rapier::pipeline::{PhysicsHooks, PhysicsPipeline};
|
||||
use std::sync::mpsc::Receiver;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PhysicsSnapshot {
|
||||
timestep_id: usize,
|
||||
broad_phase: Vec<u8>,
|
||||
@@ -98,7 +98,7 @@ pub struct PhysicsState {
|
||||
pub ccd_solver: CCDSolver,
|
||||
pub pipeline: PhysicsPipeline,
|
||||
pub integration_parameters: IntegrationParameters,
|
||||
pub gravity: Vector<Real>,
|
||||
pub gravity: rapier::math::Vector,
|
||||
pub hooks: Box<dyn PhysicsHooks>,
|
||||
}
|
||||
|
||||
@@ -121,10 +121,39 @@ impl PhysicsState {
|
||||
ccd_solver: CCDSolver::new(),
|
||||
pipeline: PhysicsPipeline::new(),
|
||||
integration_parameters: IntegrationParameters::default(),
|
||||
gravity: Vector::y() * -9.81,
|
||||
gravity: rapier::math::Vector::Y * -9.81,
|
||||
hooks: Box::new(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a snapshot of the current physics state.
|
||||
pub fn snapshot(&self) -> PhysicsSnapshot {
|
||||
PhysicsSnapshot::new(
|
||||
0, // timestep_id - could be tracked if needed
|
||||
&self.broad_phase,
|
||||
&self.narrow_phase,
|
||||
&self.islands,
|
||||
&self.bodies,
|
||||
&self.colliders,
|
||||
&self.impulse_joints,
|
||||
&self.multibody_joints,
|
||||
)
|
||||
.expect("Failed to create physics snapshot")
|
||||
}
|
||||
|
||||
/// Restore physics state from a snapshot.
|
||||
pub fn restore_snapshot(&mut self, snapshot: PhysicsSnapshot) {
|
||||
let restored = snapshot
|
||||
.restore()
|
||||
.expect("Failed to restore physics snapshot");
|
||||
self.broad_phase = restored.broad_phase;
|
||||
self.narrow_phase = restored.narrow_phase;
|
||||
self.islands = restored.island_manager;
|
||||
self.bodies = restored.bodies;
|
||||
self.colliders = restored.colliders;
|
||||
self.impulse_joints = restored.impulse_joints;
|
||||
self.multibody_joints = restored.multibody_joints;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PhysicsEvents {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::ui::egui::emath::OrderedFloat;
|
||||
use na::{Isometry3, Matrix4, Point3, Quaternion, Translation3, Unit, UnitQuaternion, Vector3};
|
||||
use glamx::{Pose3, Vec3};
|
||||
use na::{Isometry3, Matrix4, Point3, Quaternion, Unit, UnitQuaternion, Vector3};
|
||||
use physx::cooking::{
|
||||
ConvexMeshCookingResult, PxConvexMeshDesc, PxCookingParams, PxHeightFieldDesc,
|
||||
PxTriangleMeshDesc, TriangleMeshCookingResult,
|
||||
@@ -28,7 +29,7 @@ trait IntoNa {
|
||||
fn into_na(self) -> Self::Output;
|
||||
}
|
||||
|
||||
impl IntoNa for glam::Mat4 {
|
||||
impl IntoNa for glamx::Mat4 {
|
||||
type Output = Matrix4<f32>;
|
||||
fn into_na(self) -> Self::Output {
|
||||
self.to_cols_array_2d().into()
|
||||
@@ -85,6 +86,20 @@ impl IntoPhysx for UnitQuaternion<f32> {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPhysx for glamx::Vec3 {
|
||||
type Output = PxVec3;
|
||||
fn into_physx(self) -> Self::Output {
|
||||
PxVec3::new(self.x, self.y, self.z)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPhysx for glamx::Quat {
|
||||
type Output = PxQuat;
|
||||
fn into_physx(self) -> Self::Output {
|
||||
PxQuat::new(self.x, self.y, self.z, self.w)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPhysx for Isometry3<f32> {
|
||||
type Output = PxTransform;
|
||||
fn into_physx(self) -> Self::Output {
|
||||
@@ -95,36 +110,46 @@ impl IntoPhysx for Isometry3<f32> {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoPhysx for glamx::Pose3 {
|
||||
type Output = PxTransform;
|
||||
fn into_physx(self) -> Self::Output {
|
||||
PxTransform::from_translation_rotation(
|
||||
&self.translation.into_physx(),
|
||||
&self.rotation.into_physx(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
trait IntoGlam {
|
||||
type Output;
|
||||
fn into_glam(self) -> Self::Output;
|
||||
}
|
||||
|
||||
impl IntoGlam for Vector3<f32> {
|
||||
type Output = glam::Vec3;
|
||||
type Output = glamx::Vec3;
|
||||
fn into_glam(self) -> Self::Output {
|
||||
glam::vec3(self.x, self.y, self.z)
|
||||
glamx::vec3(self.x, self.y, self.z)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGlam for Point3<f32> {
|
||||
type Output = glam::Vec3;
|
||||
type Output = glamx::Vec3;
|
||||
fn into_glam(self) -> Self::Output {
|
||||
glam::vec3(self.x, self.y, self.z)
|
||||
glamx::vec3(self.x, self.y, self.z)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGlam for Matrix4<f32> {
|
||||
type Output = glam::Mat4;
|
||||
type Output = glamx::Mat4;
|
||||
fn into_glam(self) -> Self::Output {
|
||||
glam::Mat4::from_cols_array_2d(&self.into())
|
||||
glamx::Mat4::from_cols_array_2d(&self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoGlam for Isometry3<f32> {
|
||||
type Output = glam::Mat4;
|
||||
type Output = glamx::Mat4;
|
||||
fn into_glam(self) -> Self::Output {
|
||||
glam::Mat4::from_cols_array_2d(&self.to_homogeneous().into())
|
||||
glamx::Mat4::from_cols_array_2d(&self.to_homogeneous().into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +175,7 @@ impl Drop for PhysxWorld {
|
||||
impl PhysxWorld {
|
||||
#[profiling::function]
|
||||
pub fn from_rapier(
|
||||
gravity: Vector3<f32>,
|
||||
gravity: Vec3,
|
||||
integration_parameters: &IntegrationParameters,
|
||||
bodies: &RigidBodySet,
|
||||
colliders: &ColliderSet,
|
||||
@@ -522,14 +547,17 @@ impl PhysxWorld {
|
||||
}
|
||||
|
||||
pub fn sync(&mut self, bodies: &mut RigidBodySet, colliders: &mut ColliderSet) {
|
||||
let mut sync_pos = |handle: &RigidBodyHandle, pos: Isometry3<f32>| {
|
||||
let mut sync_pos = |handle: &RigidBodyHandle, pos: Pose3| {
|
||||
let rb = &mut bodies[*handle];
|
||||
rb.set_position(pos, false);
|
||||
|
||||
for coll_handle in rb.colliders() {
|
||||
let collider = &mut colliders[*coll_handle];
|
||||
collider.set_position(
|
||||
pos * collider.position_wrt_parent().copied().unwrap_or(na::one()),
|
||||
pos * collider
|
||||
.position_wrt_parent()
|
||||
.copied()
|
||||
.unwrap_or(Pose3::IDENTITY),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -537,7 +565,7 @@ impl PhysxWorld {
|
||||
for actor in self.scene.as_mut().unwrap().get_dynamic_actors() {
|
||||
let handle = actor.get_user_data();
|
||||
let pos = actor.get_global_pose().into_na();
|
||||
sync_pos(handle, pos);
|
||||
sync_pos(handle, pos.into());
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -559,8 +587,11 @@ fn physx_collider_from_rapier_collider(
|
||||
physics: &mut PxPhysicsFoundation,
|
||||
collider: &Collider,
|
||||
materials_cache: &mut HashMap<[OrderedFloat<f32>; 2], Owner<PxMaterial>>,
|
||||
) -> Option<(Owner<PxShape>, Isometry3<f32>)> {
|
||||
let mut local_pose = collider.position_wrt_parent().copied().unwrap_or(na::one());
|
||||
) -> Option<(Owner<PxShape>, Pose3)> {
|
||||
let mut local_pose = collider
|
||||
.position_wrt_parent()
|
||||
.copied()
|
||||
.unwrap_or(Pose3::IDENTITY);
|
||||
let cooking_params = PxCookingParams::new(physics).unwrap();
|
||||
let shape = collider.shape();
|
||||
let shape_flags = if collider.is_sensor() {
|
||||
@@ -604,20 +635,19 @@ fn physx_collider_from_rapier_collider(
|
||||
dir = -dir;
|
||||
}
|
||||
|
||||
let rot = UnitQuaternion::rotation_between(&Vector3::x(), &dir);
|
||||
local_pose = local_pose
|
||||
* Translation3::from(center.coords)
|
||||
* rot.unwrap_or(UnitQuaternion::identity());
|
||||
let rot = glamx::Quat::from_rotation_arc(Vec3::X, dir);
|
||||
local_pose = local_pose.prepend_translation(center) * rot;
|
||||
let geometry = PxCapsuleGeometry::new(capsule.radius, capsule.half_height());
|
||||
physics.create_shape(&geometry, materials, true, shape_flags, ())
|
||||
} else if let Some(heightfield) = shape.as_heightfield() {
|
||||
let heights = heightfield.heights();
|
||||
let scale = heightfield.scale();
|
||||
local_pose = local_pose * Translation3::new(-scale.x / 2.0, 0.0, -scale.z / 2.0);
|
||||
local_pose = local_pose.prepend_translation(Vec3::new(-scale.x / 2.0, 0.0, -scale.z / 2.0));
|
||||
const Y_FACTOR: f32 = 1_000f32;
|
||||
let mut heightfield_desc;
|
||||
unsafe {
|
||||
let samples: Vec<_> = heights
|
||||
.data()
|
||||
.iter()
|
||||
.map(|h| PxHeightFieldSample {
|
||||
height: (*h * Y_FACTOR) as i16,
|
||||
|
||||
@@ -1,43 +1,26 @@
|
||||
use crate::GraphicsManager;
|
||||
use crate::graphics::BevyMaterial;
|
||||
use crate::harness::Harness;
|
||||
use crate::physics::PhysicsState;
|
||||
use bevy::prelude::*;
|
||||
// use bevy::render::render_resource::RenderPipelineDescriptor;
|
||||
use bevy_egui::EguiContexts;
|
||||
use kiss3d::window::Window;
|
||||
|
||||
pub trait TestbedPlugin {
|
||||
fn init_plugin(&mut self);
|
||||
fn init_graphics(
|
||||
&mut self,
|
||||
graphics: &mut GraphicsManager,
|
||||
commands: &mut Commands,
|
||||
meshes: &mut Assets<Mesh>,
|
||||
materials: &mut Assets<BevyMaterial>,
|
||||
components: &mut Query<&mut Transform>,
|
||||
window: &mut Window,
|
||||
harness: &mut Harness,
|
||||
);
|
||||
fn clear_graphics(&mut self, graphics: &mut GraphicsManager, commands: &mut Commands);
|
||||
fn clear_graphics(&mut self, graphics: &mut GraphicsManager, window: &mut Window);
|
||||
fn run_callbacks(&mut self, harness: &mut Harness);
|
||||
fn step(&mut self, physics: &mut PhysicsState);
|
||||
fn draw(
|
||||
&mut self,
|
||||
graphics: &mut GraphicsManager,
|
||||
commands: &mut Commands,
|
||||
meshes: &mut Assets<Mesh>,
|
||||
materials: &mut Assets<BevyMaterial>,
|
||||
components: &mut Query<&mut Transform>,
|
||||
harness: &mut Harness,
|
||||
);
|
||||
fn draw(&mut self, graphics: &mut GraphicsManager, window: &mut Window, harness: &mut Harness);
|
||||
fn update_ui(
|
||||
&mut self,
|
||||
ui_context: &EguiContexts,
|
||||
ui_context: &egui::Context,
|
||||
harness: &mut Harness,
|
||||
graphics: &mut GraphicsManager,
|
||||
commands: &mut Commands,
|
||||
meshes: &mut Assets<Mesh>,
|
||||
materials: &mut Assets<BevyMaterial>,
|
||||
components: &mut Query<&mut Transform>,
|
||||
window: &mut Window,
|
||||
);
|
||||
fn profiling_string(&self) -> String;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
#[cfg(feature = "dim2")]
|
||||
use crate::camera2d::OrbitCamera;
|
||||
#[cfg(feature = "dim3")]
|
||||
use crate::camera3d::OrbitCamera;
|
||||
use crate::settings::ExampleSettings;
|
||||
use crate::testbed::{RunMode, TestbedStateFlags};
|
||||
use crate::{Camera, TestbedState};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, PartialEq, Debug, Default, Clone)]
|
||||
@@ -14,5 +11,30 @@ pub struct SerializableTestbedState {
|
||||
pub selected_backend: usize,
|
||||
pub example_settings: ExampleSettings,
|
||||
pub physx_use_two_friction_directions: bool,
|
||||
pub camera: OrbitCamera,
|
||||
pub camera: Camera,
|
||||
}
|
||||
|
||||
impl TestbedState {
|
||||
pub fn save_data(&self, camera: Camera) -> SerializableTestbedState {
|
||||
SerializableTestbedState {
|
||||
running: self.running,
|
||||
flags: self.flags,
|
||||
selected_example: self.selected_display_index,
|
||||
selected_backend: self.selected_backend,
|
||||
example_settings: self.example_settings.clone(),
|
||||
physx_use_two_friction_directions: self.physx_use_two_friction_directions,
|
||||
camera,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_saved_data(&mut self, state: SerializableTestbedState, camera: &mut Camera) {
|
||||
self.prev_save_data = state.clone();
|
||||
self.running = state.running;
|
||||
self.flags = state.flags;
|
||||
self.selected_display_index = state.selected_example;
|
||||
self.selected_backend = state.selected_backend;
|
||||
self.example_settings = state.example_settings;
|
||||
self.physx_use_two_friction_directions = state.physx_use_two_friction_directions;
|
||||
*camera = state.camera;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
658
src_testbed/testbed/app.rs
Normal file
658
src_testbed/testbed/app.rs
Normal file
@@ -0,0 +1,658 @@
|
||||
//! TestbedApp - the main application runner.
|
||||
|
||||
use crate::Camera;
|
||||
use crate::debug_render::{DebugRenderPipelineResource, debug_render_scene};
|
||||
use crate::graphics::GraphicsManager;
|
||||
use crate::harness::Harness;
|
||||
use crate::mouse::SceneMouse;
|
||||
use crate::save::SerializableTestbedState;
|
||||
use crate::testbed::hover::highlight_hovered_body;
|
||||
use crate::ui;
|
||||
use kiss3d::color::Color;
|
||||
use kiss3d::event::{Action, Key, WindowEvent};
|
||||
use kiss3d::window::Window;
|
||||
use rapier::dynamics::RigidBodyActivation;
|
||||
use std::mem;
|
||||
|
||||
use super::Plugins;
|
||||
use super::graphics_context::TestbedGraphics;
|
||||
use super::keys::KeysState;
|
||||
use super::state::{RAPIER_BACKEND, RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags};
|
||||
use super::testbed::{SimulationBuilders, Testbed};
|
||||
|
||||
#[cfg(feature = "other-backends")]
|
||||
use super::OtherBackends;
|
||||
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
use super::state::{PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR};
|
||||
|
||||
/// The main testbed application
|
||||
pub struct TestbedApp {
|
||||
builders: SimulationBuilders,
|
||||
graphics: GraphicsManager,
|
||||
state: TestbedState,
|
||||
harness: Harness,
|
||||
#[cfg(feature = "other-backends")]
|
||||
other_backends: OtherBackends,
|
||||
plugins: Plugins,
|
||||
}
|
||||
|
||||
impl TestbedApp {
|
||||
pub fn save_file_path() -> String {
|
||||
format!("testbed_state_{}.autosave.json", env!("CARGO_CRATE_NAME"))
|
||||
}
|
||||
|
||||
pub fn new_empty() -> Self {
|
||||
let graphics = GraphicsManager::new();
|
||||
let state = TestbedState::default();
|
||||
let harness = Harness::new_empty();
|
||||
#[cfg(feature = "other-backends")]
|
||||
let other_backends = OtherBackends {
|
||||
#[cfg(feature = "dim3")]
|
||||
physx: None,
|
||||
};
|
||||
|
||||
TestbedApp {
|
||||
builders: Vec::new(),
|
||||
plugins: Plugins(Vec::new()),
|
||||
graphics,
|
||||
state,
|
||||
harness,
|
||||
#[cfg(feature = "other-backends")]
|
||||
other_backends,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_builders(builders: SimulationBuilders) -> Self {
|
||||
let mut res = TestbedApp::new_empty();
|
||||
res.set_builders(builders);
|
||||
res
|
||||
}
|
||||
|
||||
pub fn set_builders(&mut self, builders: SimulationBuilders) {
|
||||
use super::state::ExampleEntry;
|
||||
use indexmap::IndexSet;
|
||||
|
||||
// Collect unique groups in order of first appearance
|
||||
let mut groups: IndexSet<&'static str> = IndexSet::new();
|
||||
for example in &builders {
|
||||
groups.insert(example.group);
|
||||
}
|
||||
|
||||
// Build the display order: group by group, preserving original order within each group
|
||||
let mut examples = Vec::new();
|
||||
for group in &groups {
|
||||
for (builder_index, example) in builders.iter().enumerate() {
|
||||
if example.group == *group {
|
||||
examples.push(ExampleEntry {
|
||||
name: example.name,
|
||||
group: example.group,
|
||||
builder_index,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.state.example_groups = groups.into_iter().collect();
|
||||
self.state.examples = examples;
|
||||
self.builders = builders;
|
||||
}
|
||||
|
||||
pub async fn run(self) {
|
||||
self.run_with_init(|_| {}).await
|
||||
}
|
||||
|
||||
pub async fn run_with_init(mut self, init: impl FnMut(&mut Testbed)) {
|
||||
#[cfg(feature = "profiler_ui")]
|
||||
profiling::puffin::set_scopes_on(true);
|
||||
|
||||
// Check for benchmark mode
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
if args.iter().any(|a| a == "--bench") {
|
||||
self.run_benchmark();
|
||||
return;
|
||||
}
|
||||
|
||||
self.run_async(init).await
|
||||
}
|
||||
|
||||
fn run_benchmark(&mut self) {
|
||||
use std::fs::File;
|
||||
use std::io::{BufWriter, Write};
|
||||
|
||||
let num_bench_iters = 1000u32;
|
||||
let builders = mem::take(&mut self.builders);
|
||||
let backend_names = self.state.backend_names.clone();
|
||||
|
||||
for builder in &builders {
|
||||
let mut results = Vec::new();
|
||||
println!("Running benchmark for {}", builder.name);
|
||||
|
||||
for (backend_id, backend) in backend_names.iter().enumerate() {
|
||||
println!("|_ using backend {backend}");
|
||||
self.state.selected_backend = backend_id;
|
||||
self.harness = Harness::new_empty();
|
||||
|
||||
let mut testbed = Testbed {
|
||||
graphics: None,
|
||||
state: &mut self.state,
|
||||
harness: &mut self.harness,
|
||||
#[cfg(feature = "other-backends")]
|
||||
other_backends: &mut self.other_backends,
|
||||
plugins: &mut self.plugins,
|
||||
};
|
||||
(builder.builder)(&mut testbed);
|
||||
|
||||
let mut timings = Vec::new();
|
||||
for k in 0..num_bench_iters {
|
||||
if self.state.selected_backend == RAPIER_BACKEND {
|
||||
self.harness.step();
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
{
|
||||
if self.state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
|
||||
|| self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR
|
||||
{
|
||||
self.other_backends.physx.as_mut().unwrap().step(
|
||||
&mut self.harness.physics.pipeline.counters,
|
||||
&self.harness.physics.integration_parameters,
|
||||
);
|
||||
self.other_backends.physx.as_mut().unwrap().sync(
|
||||
&mut self.harness.physics.bodies,
|
||||
&mut self.harness.physics.colliders,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if k > 0 {
|
||||
timings.push(self.harness.physics.pipeline.counters.step_time.time_ms());
|
||||
}
|
||||
}
|
||||
results.push(timings);
|
||||
}
|
||||
|
||||
use inflector::Inflector;
|
||||
let filename = format!("{}.csv", builder.name.to_camel_case());
|
||||
let mut file = BufWriter::new(File::create(filename).unwrap());
|
||||
|
||||
write!(file, "{}", backend_names[0]).unwrap();
|
||||
for backend in &backend_names[1..] {
|
||||
write!(file, ",{backend}").unwrap();
|
||||
}
|
||||
writeln!(file).unwrap();
|
||||
|
||||
for i in 0..results[0].len() {
|
||||
write!(file, "{}", results[0][i]).unwrap();
|
||||
for result in &results[1..] {
|
||||
write!(file, ",{}", result[i]).unwrap();
|
||||
}
|
||||
writeln!(file).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_async(mut self, mut init: impl FnMut(&mut Testbed)) {
|
||||
let title = if cfg!(feature = "dim2") {
|
||||
"Rapier: 2D demos"
|
||||
} else {
|
||||
"Rapier: 3D demos"
|
||||
};
|
||||
|
||||
let mut window = Window::new_with_size(title, 1280, 720).await;
|
||||
window.set_background_color(Color::new(245.0 / 255.0, 245.0 / 255.0, 236.0 / 255.0, 1.0));
|
||||
|
||||
let mut debug_render = DebugRenderPipelineResource::default();
|
||||
let mut camera = Camera::default();
|
||||
let mut scene_mouse = SceneMouse::new();
|
||||
let mut keys = KeysState::default();
|
||||
|
||||
// User init
|
||||
let testbed_gfx = TestbedGraphics {
|
||||
graphics: &mut self.graphics,
|
||||
window: &mut window,
|
||||
camera: &mut camera,
|
||||
mouse: &mut scene_mouse,
|
||||
keys: &mut keys,
|
||||
settings: None,
|
||||
};
|
||||
|
||||
let mut testbed = Testbed {
|
||||
graphics: Some(testbed_gfx),
|
||||
state: &mut self.state,
|
||||
harness: &mut self.harness,
|
||||
#[cfg(feature = "other-backends")]
|
||||
other_backends: &mut self.other_backends,
|
||||
plugins: &mut self.plugins,
|
||||
};
|
||||
|
||||
init(&mut testbed);
|
||||
|
||||
// Main render loop
|
||||
#[cfg(feature = "dim3")]
|
||||
while window
|
||||
.render_3d(self.graphics.scene_mut(), &mut camera)
|
||||
.await
|
||||
{
|
||||
self.run_frame(
|
||||
&mut window,
|
||||
&mut debug_render,
|
||||
&mut camera,
|
||||
&mut scene_mouse,
|
||||
&mut keys,
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim2")]
|
||||
while window
|
||||
.render_2d(self.graphics.scene_mut(), &mut camera)
|
||||
.await
|
||||
{
|
||||
self.run_frame(
|
||||
&mut window,
|
||||
&mut debug_render,
|
||||
&mut camera,
|
||||
&mut scene_mouse,
|
||||
&mut keys,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_frame(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
debug_render: &mut DebugRenderPipelineResource,
|
||||
camera: &mut Camera,
|
||||
scene_mouse: &mut SceneMouse,
|
||||
keys: &mut KeysState,
|
||||
) {
|
||||
profiling::finish_frame!();
|
||||
|
||||
// Handle input events
|
||||
self.handle_events(window, keys);
|
||||
|
||||
// Handle the vehicle controller if there is one.
|
||||
#[cfg(feature = "dim3")]
|
||||
{
|
||||
self.update_vehicle_controller(keys);
|
||||
}
|
||||
|
||||
// Update mouse state
|
||||
let cursor_pos = window.cursor_pos();
|
||||
scene_mouse.update_from_window(cursor_pos, window.size().into(), camera);
|
||||
|
||||
// Handle action flags
|
||||
self.handle_action_flags(window, camera, scene_mouse, keys);
|
||||
|
||||
// Handle sleep settings
|
||||
self.handle_sleep_settings();
|
||||
|
||||
// Run simulation
|
||||
if self.state.running != RunMode::Stop {
|
||||
for _ in 0..self.state.nsteps {
|
||||
if self.state.selected_backend == RAPIER_BACKEND {
|
||||
let mut testbed_gfx = TestbedGraphics {
|
||||
graphics: &mut self.graphics,
|
||||
window,
|
||||
camera,
|
||||
mouse: scene_mouse,
|
||||
keys,
|
||||
settings: Some(&mut self.state.example_settings),
|
||||
};
|
||||
self.harness.step_with_graphics(Some(&mut testbed_gfx));
|
||||
|
||||
for plugin in &mut self.plugins.0 {
|
||||
plugin.step(&mut self.harness.physics);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
{
|
||||
if self.state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
|
||||
|| self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR
|
||||
{
|
||||
self.other_backends.physx.as_mut().unwrap().step(
|
||||
&mut self.harness.physics.pipeline.counters,
|
||||
&self.harness.physics.integration_parameters,
|
||||
);
|
||||
self.other_backends.physx.as_mut().unwrap().sync(
|
||||
&mut self.harness.physics.bodies,
|
||||
&mut self.harness.physics.colliders,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for plugin in &mut self.plugins.0 {
|
||||
plugin.run_callbacks(&mut self.harness);
|
||||
}
|
||||
}
|
||||
|
||||
if self.state.running == RunMode::Step {
|
||||
self.state.running = RunMode::Stop;
|
||||
}
|
||||
}
|
||||
|
||||
// Autosave state.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let new_save_data = self.state.save_data(*camera);
|
||||
if self.state.prev_save_data != new_save_data {
|
||||
// Save the data in a file.
|
||||
let data = serde_json::to_string_pretty(&new_save_data).unwrap();
|
||||
if let Err(e) = std::fs::write(Self::save_file_path(), &data) {
|
||||
eprintln!("Failed to write autosave file: {}", e);
|
||||
}
|
||||
self.state.prev_save_data = new_save_data;
|
||||
}
|
||||
}
|
||||
|
||||
highlight_hovered_body(&mut self.graphics, scene_mouse, &self.harness.physics);
|
||||
|
||||
// Update graphics
|
||||
self.graphics.draw(
|
||||
self.state.flags,
|
||||
&self.harness.physics.bodies,
|
||||
&self.harness.physics.colliders,
|
||||
);
|
||||
|
||||
// Draw debug render
|
||||
debug_render_scene(window, debug_render, &self.harness);
|
||||
|
||||
// Draw UI
|
||||
window.draw_ui(|ctx| {
|
||||
ui::update_ui(ctx, &mut self.state, &mut self.harness, debug_render);
|
||||
});
|
||||
|
||||
self.state.prev_flags = self.state.flags;
|
||||
}
|
||||
|
||||
fn handle_events(&mut self, window: &mut Window, keys: &mut KeysState) {
|
||||
for event in window.events().iter() {
|
||||
match event.value {
|
||||
WindowEvent::Key(key, Action::Press, _) => {
|
||||
// Track pressed keys
|
||||
if !keys.pressed_keys.contains(&key) {
|
||||
keys.pressed_keys.push(key);
|
||||
}
|
||||
// Update modifier states
|
||||
match key {
|
||||
Key::LShift | Key::RShift => keys.shift = true,
|
||||
Key::LControl | Key::RControl => keys.ctrl = true,
|
||||
Key::LAlt | Key::RAlt => keys.alt = true,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
WindowEvent::Key(key, Action::Release, _) => {
|
||||
// Remove from pressed keys
|
||||
keys.pressed_keys.retain(|k| *k != key);
|
||||
// Handle special keys
|
||||
match key {
|
||||
Key::T => {
|
||||
if self.state.running == RunMode::Stop {
|
||||
self.state.running = RunMode::Running;
|
||||
} else {
|
||||
self.state.running = RunMode::Stop;
|
||||
}
|
||||
}
|
||||
Key::S => {
|
||||
self.state.running = RunMode::Step;
|
||||
}
|
||||
Key::R => {
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::EXAMPLE_CHANGED, true);
|
||||
}
|
||||
Key::LShift | Key::RShift => keys.shift = false,
|
||||
Key::LControl | Key::RControl => keys.ctrl = false,
|
||||
Key::LAlt | Key::RAlt => keys.alt = false,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
fn update_vehicle_controller(&mut self, keys: &mut KeysState) {
|
||||
use rapier::prelude::QueryFilter;
|
||||
|
||||
if self.state.running == RunMode::Stop {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(vehicle) = &mut self.state.vehicle_controller {
|
||||
let mut engine_force = 0.0;
|
||||
let mut steering_angle = 0.0;
|
||||
|
||||
println!("Pressed: {:?}", keys);
|
||||
if keys.pressed(Key::Right) {
|
||||
steering_angle += -0.7;
|
||||
}
|
||||
if keys.pressed(Key::Left) {
|
||||
steering_angle += 0.7;
|
||||
}
|
||||
if keys.pressed(Key::Up) {
|
||||
engine_force += 30.0;
|
||||
}
|
||||
if keys.pressed(Key::Down) {
|
||||
engine_force += -30.0;
|
||||
}
|
||||
|
||||
let wheels = vehicle.wheels_mut();
|
||||
wheels[0].engine_force = engine_force;
|
||||
wheels[0].steering = steering_angle;
|
||||
wheels[1].engine_force = engine_force;
|
||||
wheels[1].steering = steering_angle;
|
||||
|
||||
let query_pipeline = self.harness.physics.broad_phase.as_query_pipeline_mut(
|
||||
self.harness.physics.narrow_phase.query_dispatcher(),
|
||||
&mut self.harness.physics.bodies,
|
||||
&mut self.harness.physics.colliders,
|
||||
QueryFilter::exclude_dynamic().exclude_rigid_body(vehicle.chassis),
|
||||
);
|
||||
|
||||
vehicle.update_vehicle(
|
||||
self.harness.physics.integration_parameters.dt,
|
||||
query_pipeline,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_action_flags(
|
||||
&mut self,
|
||||
window: &mut Window,
|
||||
camera: &mut Camera,
|
||||
scene_mouse: &mut SceneMouse,
|
||||
keys: &mut KeysState,
|
||||
) {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let app_started = self
|
||||
.state
|
||||
.action_flags
|
||||
.contains(TestbedActionFlags::APP_STARTED);
|
||||
|
||||
if app_started {
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::APP_STARTED, false);
|
||||
if let Some(saved_state) = std::fs::read(Self::save_file_path())
|
||||
.ok()
|
||||
.and_then(|data| serde_json::from_slice::<SerializableTestbedState>(&data).ok())
|
||||
{
|
||||
self.state.apply_saved_data(saved_state, camera);
|
||||
self.state.camera_locked = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let backend_changed = self
|
||||
.state
|
||||
.action_flags
|
||||
.contains(TestbedActionFlags::BACKEND_CHANGED);
|
||||
if backend_changed {
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::BACKEND_CHANGED, false);
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::EXAMPLE_CHANGED, true);
|
||||
self.state.camera_locked = true;
|
||||
}
|
||||
|
||||
let restarted = self
|
||||
.state
|
||||
.action_flags
|
||||
.contains(TestbedActionFlags::RESTART);
|
||||
if restarted {
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::RESTART, false);
|
||||
self.state.camera_locked = true;
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::EXAMPLE_CHANGED, true);
|
||||
}
|
||||
|
||||
let example_changed = self
|
||||
.state
|
||||
.action_flags
|
||||
.contains(TestbedActionFlags::EXAMPLE_CHANGED);
|
||||
if example_changed {
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::EXAMPLE_CHANGED, false);
|
||||
self.clear(window);
|
||||
self.harness.clear_callbacks();
|
||||
|
||||
if !self.state.camera_locked {
|
||||
*camera = Camera::default();
|
||||
}
|
||||
|
||||
if !restarted {
|
||||
self.state.example_settings.clear();
|
||||
}
|
||||
|
||||
// Clamp selected_display_index to valid range
|
||||
let max_index = self.state.examples.len().saturating_sub(1);
|
||||
self.state.selected_display_index = self.state.selected_display_index.min(max_index);
|
||||
|
||||
if !self.builders.is_empty() {
|
||||
let builder_index = self.state.selected_builder_index();
|
||||
let builder = self.builders[builder_index].builder;
|
||||
let testbed_gfx = TestbedGraphics {
|
||||
graphics: &mut self.graphics,
|
||||
window,
|
||||
camera,
|
||||
mouse: scene_mouse,
|
||||
keys,
|
||||
settings: None,
|
||||
};
|
||||
|
||||
let mut testbed = Testbed {
|
||||
graphics: Some(testbed_gfx),
|
||||
state: &mut self.state,
|
||||
harness: &mut self.harness,
|
||||
#[cfg(feature = "other-backends")]
|
||||
other_backends: &mut self.other_backends,
|
||||
plugins: &mut self.plugins,
|
||||
};
|
||||
builder(&mut testbed);
|
||||
}
|
||||
|
||||
self.state.camera_locked = false;
|
||||
}
|
||||
|
||||
if self
|
||||
.state
|
||||
.action_flags
|
||||
.contains(TestbedActionFlags::RESET_WORLD_GRAPHICS)
|
||||
{
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::RESET_WORLD_GRAPHICS, false);
|
||||
for (handle, _) in self.harness.physics.bodies.iter() {
|
||||
self.graphics.add_body_colliders(
|
||||
window,
|
||||
handle,
|
||||
&self.harness.physics.bodies,
|
||||
&self.harness.physics.colliders,
|
||||
);
|
||||
}
|
||||
|
||||
for (handle, co) in self.harness.physics.colliders.iter() {
|
||||
if co.parent().is_none() {
|
||||
self.graphics
|
||||
.add_collider(window, handle, &self.harness.physics.colliders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self
|
||||
.state
|
||||
.action_flags
|
||||
.contains(TestbedActionFlags::TAKE_SNAPSHOT)
|
||||
{
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::TAKE_SNAPSHOT, false);
|
||||
self.state.snapshot = Some(self.harness.physics.snapshot());
|
||||
}
|
||||
|
||||
if self
|
||||
.state
|
||||
.action_flags
|
||||
.contains(TestbedActionFlags::RESTORE_SNAPSHOT)
|
||||
{
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::RESTORE_SNAPSHOT, false);
|
||||
if let Some(snapshot) = &self.state.snapshot {
|
||||
self.harness.physics.restore_snapshot(snapshot.clone());
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::RESET_WORLD_GRAPHICS, true);
|
||||
}
|
||||
}
|
||||
|
||||
if example_changed
|
||||
|| self.state.prev_flags.contains(TestbedStateFlags::WIREFRAME)
|
||||
!= self.state.flags.contains(TestbedStateFlags::WIREFRAME)
|
||||
{
|
||||
self.graphics.toggle_wireframe_mode(
|
||||
&self.harness.physics.colliders,
|
||||
self.state.flags.contains(TestbedStateFlags::WIREFRAME),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_sleep_settings(&mut self) {
|
||||
if self.state.prev_flags.contains(TestbedStateFlags::SLEEP)
|
||||
!= self.state.flags.contains(TestbedStateFlags::SLEEP)
|
||||
{
|
||||
if self.state.flags.contains(TestbedStateFlags::SLEEP) {
|
||||
for (_, body) in self.harness.physics.bodies.iter_mut() {
|
||||
body.activation_mut().normalized_linear_threshold =
|
||||
RigidBodyActivation::default_normalized_linear_threshold();
|
||||
body.activation_mut().angular_threshold =
|
||||
RigidBodyActivation::default_angular_threshold();
|
||||
}
|
||||
} else {
|
||||
for (_, body) in self.harness.physics.bodies.iter_mut() {
|
||||
body.wake_up(true);
|
||||
body.activation_mut().normalized_linear_threshold = -1.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn clear(&mut self, window: &mut Window) {
|
||||
self.state.can_grab_behind_ground = false;
|
||||
self.graphics.clear();
|
||||
|
||||
for mut plugin in self.plugins.0.drain(..) {
|
||||
plugin.clear_graphics(&mut self.graphics, window);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src_testbed/testbed/graphics_context.rs
Normal file
94
src_testbed/testbed/graphics_context.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
//! Graphics context for a frame.
|
||||
|
||||
use kiss3d::color::Color;
|
||||
use kiss3d::window::Window;
|
||||
use rapier::dynamics::RigidBodyHandle;
|
||||
use rapier::dynamics::RigidBodySet;
|
||||
use rapier::geometry::{ColliderHandle, ColliderSet};
|
||||
|
||||
use crate::Camera;
|
||||
use crate::graphics::GraphicsManager;
|
||||
use crate::mouse::SceneMouse;
|
||||
use crate::settings::ExampleSettings;
|
||||
|
||||
use super::keys::KeysState;
|
||||
|
||||
/// Context for graphics operations during a frame
|
||||
pub struct TestbedGraphics<'a> {
|
||||
pub graphics: &'a mut GraphicsManager,
|
||||
pub window: &'a mut Window,
|
||||
pub camera: &'a mut Camera,
|
||||
pub mouse: &'a SceneMouse,
|
||||
pub keys: &'a KeysState,
|
||||
pub settings: Option<&'a mut ExampleSettings>,
|
||||
}
|
||||
|
||||
impl<'a> TestbedGraphics<'a> {
|
||||
pub fn set_body_color(&mut self, body: RigidBodyHandle, color: Color, tmp_color: bool) {
|
||||
self.graphics.set_body_color(body, color, tmp_color);
|
||||
}
|
||||
|
||||
pub fn add_body(
|
||||
&mut self,
|
||||
handle: RigidBodyHandle,
|
||||
bodies: &RigidBodySet,
|
||||
colliders: &ColliderSet,
|
||||
) {
|
||||
self.graphics
|
||||
.add_body_colliders(self.window, handle, bodies, colliders);
|
||||
}
|
||||
|
||||
pub fn remove_body(&mut self, handle: RigidBodyHandle) {
|
||||
self.graphics.remove_body_nodes(handle);
|
||||
}
|
||||
|
||||
pub fn add_collider(&mut self, handle: ColliderHandle, colliders: &ColliderSet) {
|
||||
self.graphics.add_collider(self.window, handle, colliders);
|
||||
}
|
||||
|
||||
pub fn remove_collider(&mut self, handle: ColliderHandle) {
|
||||
self.graphics.remove_collider_nodes(handle);
|
||||
}
|
||||
|
||||
pub fn keys(&self) -> &KeysState {
|
||||
self.keys
|
||||
}
|
||||
|
||||
pub fn mouse(&self) -> &SceneMouse {
|
||||
self.mouse
|
||||
}
|
||||
|
||||
/// Update collider graphics after shape modification
|
||||
pub fn update_collider(&mut self, handle: ColliderHandle, colliders: &ColliderSet) {
|
||||
// Remove and re-add the collider to update its graphics
|
||||
self.graphics.remove_collider_nodes(handle);
|
||||
self.graphics.add_collider(self.window, handle, colliders);
|
||||
}
|
||||
|
||||
/// Get the camera rotation as a unit quaternion (3D only)
|
||||
#[cfg(feature = "dim3")]
|
||||
pub fn camera_rotation(&self) -> na::UnitQuaternion<f32> {
|
||||
// Calculate rotation from orbit camera angles
|
||||
let rot_x = na::UnitQuaternion::from_axis_angle(&na::Vector3::y_axis(), self.camera.at().x);
|
||||
let rot_y =
|
||||
na::UnitQuaternion::from_axis_angle(&(-na::Vector3::x_axis()), self.camera.at().y);
|
||||
rot_x * rot_y
|
||||
}
|
||||
|
||||
/// Get the camera forward direction (3D only)
|
||||
#[cfg(feature = "dim3")]
|
||||
pub fn camera_fwd_dir(&self) -> na::Vector3<f32> {
|
||||
let rot = self.camera_rotation();
|
||||
rot * na::Vector3::z()
|
||||
}
|
||||
|
||||
/// Get mutable access to the egui context for custom UI
|
||||
pub fn egui_context(&self) -> &egui::Context {
|
||||
self.window.egui_context()
|
||||
}
|
||||
|
||||
/// Get mutable access to the egui context for custom UI
|
||||
pub fn egui_context_mut(&mut self) -> &mut egui::Context {
|
||||
self.window.egui_context_mut()
|
||||
}
|
||||
}
|
||||
64
src_testbed/testbed/hover.rs
Normal file
64
src_testbed/testbed/hover.rs
Normal file
@@ -0,0 +1,64 @@
|
||||
#![allow(clippy::useless_conversion)] // Conversions are needed for switching between f32/f64.
|
||||
|
||||
use crate::mouse::SceneMouse;
|
||||
use crate::{GraphicsManager, PhysicsState};
|
||||
use kiss3d::prelude::*;
|
||||
use rapier::prelude::QueryFilter;
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
use rapier::prelude::{Ray, Real};
|
||||
|
||||
#[cfg(feature = "dim2")]
|
||||
pub fn highlight_hovered_body(
|
||||
graphics_manager: &mut GraphicsManager,
|
||||
mouse: &SceneMouse,
|
||||
physics: &PhysicsState,
|
||||
) {
|
||||
use rapier::math::Vector;
|
||||
|
||||
if let Some(pt) = mouse.point {
|
||||
// Convert from kiss3d Vec2 (f32) to rapier Vector (may be f64)
|
||||
let pt = Vector::new(pt.x as _, pt.y as _);
|
||||
|
||||
let query_pipeline = physics.broad_phase.as_query_pipeline(
|
||||
physics.narrow_phase.query_dispatcher(),
|
||||
&physics.bodies,
|
||||
&physics.colliders,
|
||||
QueryFilter::only_dynamic(),
|
||||
);
|
||||
|
||||
for (handle, _) in query_pipeline.intersect_point(pt) {
|
||||
let collider = &physics.colliders[handle];
|
||||
if let Some(parent_handle) = collider.parent() {
|
||||
graphics_manager.set_body_color(parent_handle, RED, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
pub fn highlight_hovered_body(
|
||||
graphics_manager: &mut GraphicsManager,
|
||||
mouse: &SceneMouse,
|
||||
physics: &PhysicsState,
|
||||
) {
|
||||
if let Some((ray_origin, ray_dir)) = mouse.ray {
|
||||
let ray = Ray::new(ray_origin.into(), ray_dir.into());
|
||||
let query_pipeline = physics.broad_phase.as_query_pipeline(
|
||||
physics.narrow_phase.query_dispatcher(),
|
||||
&physics.bodies,
|
||||
&physics.colliders,
|
||||
QueryFilter::only_dynamic(),
|
||||
);
|
||||
|
||||
let hit = query_pipeline.cast_ray(&ray, Real::MAX, true);
|
||||
|
||||
if let Some((handle, _)) = hit {
|
||||
let collider = &physics.colliders[handle];
|
||||
|
||||
if let Some(parent_handle) = collider.parent() {
|
||||
graphics_manager.set_body_color(parent_handle, RED, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src_testbed/testbed/keys.rs
Normal file
24
src_testbed/testbed/keys.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
//! Keyboard state tracking.
|
||||
|
||||
use kiss3d::event::Key;
|
||||
|
||||
/// Keyboard state
|
||||
#[derive(Default, Clone, Debug)]
|
||||
pub struct KeysState {
|
||||
pub shift: bool,
|
||||
pub ctrl: bool,
|
||||
pub alt: bool,
|
||||
pub pressed_keys: Vec<Key>,
|
||||
}
|
||||
|
||||
impl KeysState {
|
||||
/// Check if a specific key is currently pressed
|
||||
pub fn pressed(&self, key: Key) -> bool {
|
||||
self.pressed_keys.contains(&key)
|
||||
}
|
||||
|
||||
/// Get all currently pressed keys
|
||||
pub fn get_pressed(&self) -> &[Key] {
|
||||
&self.pressed_keys
|
||||
}
|
||||
}
|
||||
35
src_testbed/testbed/mod.rs
Normal file
35
src_testbed/testbed/mod.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
//! Testbed module - visual debugging and example runner for Rapier physics.
|
||||
|
||||
#![allow(clippy::bad_bit_mask)]
|
||||
#![allow(clippy::unnecessary_cast)]
|
||||
#![allow(clippy::module_inception)]
|
||||
|
||||
mod app;
|
||||
mod graphics_context;
|
||||
mod hover;
|
||||
mod keys;
|
||||
mod state;
|
||||
mod testbed;
|
||||
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
use crate::physx_backend::PhysxWorld;
|
||||
|
||||
// Re-export all public types
|
||||
pub use app::TestbedApp;
|
||||
pub use graphics_context::TestbedGraphics;
|
||||
pub use keys::KeysState;
|
||||
pub use state::{RunMode, TestbedActionFlags, TestbedState, TestbedStateFlags, UiTab};
|
||||
pub use testbed::{Example, Testbed};
|
||||
|
||||
// Internal re-exports for other modules
|
||||
pub(crate) use state::{PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR};
|
||||
|
||||
/// Backend implementations for other physics engines
|
||||
#[cfg(feature = "other-backends")]
|
||||
pub struct OtherBackends {
|
||||
#[cfg(feature = "dim3")]
|
||||
pub physx: Option<PhysxWorld>,
|
||||
}
|
||||
|
||||
/// Container for testbed plugins
|
||||
pub struct Plugins(pub Vec<Box<dyn crate::plugin::TestbedPlugin>>);
|
||||
159
src_testbed/testbed/state.rs
Normal file
159
src_testbed/testbed/state.rs
Normal file
@@ -0,0 +1,159 @@
|
||||
//! Testbed state types and flags.
|
||||
|
||||
use bitflags::bitflags;
|
||||
use na::Point3;
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
use rapier::control::DynamicRayCastVehicleController;
|
||||
|
||||
use crate::harness::RapierBroadPhaseType;
|
||||
use crate::physics::PhysicsSnapshot;
|
||||
use crate::save::SerializableTestbedState;
|
||||
use crate::settings::ExampleSettings;
|
||||
|
||||
/// Run mode for the simulation
|
||||
#[derive(Default, PartialEq, Copy, Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum RunMode {
|
||||
Running,
|
||||
#[default]
|
||||
Stop,
|
||||
Step,
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Flags for controlling what is displayed in the testbed
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct TestbedStateFlags: u32 {
|
||||
const SLEEP = 1 << 0;
|
||||
const SUB_STEPPING = 1 << 1;
|
||||
const SHAPES = 1 << 2;
|
||||
const JOINTS = 1 << 3;
|
||||
const AABBS = 1 << 4;
|
||||
const CONTACT_POINTS = 1 << 5;
|
||||
const CONTACT_NORMALS = 1 << 6;
|
||||
const CENTER_OF_MASSES = 1 << 7;
|
||||
const WIREFRAME = 1 << 8;
|
||||
const STATISTICS = 1 << 9;
|
||||
const DRAW_SURFACES = 1 << 10;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TestbedStateFlags {
|
||||
fn default() -> Self {
|
||||
TestbedStateFlags::DRAW_SURFACES | TestbedStateFlags::SLEEP
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
/// Flags for testbed actions that need to be processed
|
||||
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub struct TestbedActionFlags: u32 {
|
||||
const RESET_WORLD_GRAPHICS = 1 << 0;
|
||||
const EXAMPLE_CHANGED = 1 << 1;
|
||||
const RESTART = 1 << 2;
|
||||
const BACKEND_CHANGED = 1 << 3;
|
||||
const TAKE_SNAPSHOT = 1 << 4;
|
||||
const RESTORE_SNAPSHOT = 1 << 5;
|
||||
const APP_STARTED = 1 << 6;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const RAPIER_BACKEND: usize = 0;
|
||||
pub(crate) const PHYSX_BACKEND_PATCH_FRICTION: usize = 1;
|
||||
pub(crate) const PHYSX_BACKEND_TWO_FRICTION_DIR: usize = 2;
|
||||
|
||||
/// Which tab is currently selected in the UI
|
||||
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
|
||||
pub enum UiTab {
|
||||
#[default]
|
||||
Examples,
|
||||
Settings,
|
||||
Performance,
|
||||
}
|
||||
|
||||
/// Information about an example for UI display
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ExampleEntry {
|
||||
pub name: &'static str,
|
||||
pub group: &'static str,
|
||||
/// Index in the original builders array
|
||||
pub builder_index: usize,
|
||||
}
|
||||
|
||||
/// State for the testbed application
|
||||
pub struct TestbedState {
|
||||
pub running: RunMode,
|
||||
pub draw_colls: bool,
|
||||
#[cfg(feature = "dim3")]
|
||||
pub vehicle_controller: Option<DynamicRayCastVehicleController>,
|
||||
pub grabbed_object_plane: (Point3<f32>, na::Vector3<f32>),
|
||||
pub can_grab_behind_ground: bool,
|
||||
pub drawing_ray: Option<na::Point2<f32>>,
|
||||
pub prev_flags: TestbedStateFlags,
|
||||
pub flags: TestbedStateFlags,
|
||||
pub action_flags: TestbedActionFlags,
|
||||
pub backend_names: Vec<&'static str>,
|
||||
/// Examples in display order (grouped, then by original order within group)
|
||||
pub examples: Vec<ExampleEntry>,
|
||||
/// Unique group names in order of first appearance
|
||||
pub example_groups: Vec<&'static str>,
|
||||
/// Currently selected position in the display order
|
||||
pub selected_display_index: usize,
|
||||
pub selected_backend: usize,
|
||||
pub example_settings: ExampleSettings,
|
||||
pub broad_phase_type: RapierBroadPhaseType,
|
||||
pub physx_use_two_friction_directions: bool,
|
||||
pub snapshot: Option<PhysicsSnapshot>,
|
||||
pub nsteps: usize,
|
||||
pub camera_locked: bool,
|
||||
pub selected_tab: UiTab,
|
||||
pub prev_save_data: SerializableTestbedState,
|
||||
}
|
||||
|
||||
impl Default for TestbedState {
|
||||
fn default() -> Self {
|
||||
#[allow(unused_mut)]
|
||||
let mut backend_names = vec!["rapier"];
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
backend_names.push("physx (patch friction)");
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
backend_names.push("physx (two friction dir)");
|
||||
|
||||
let flags = TestbedStateFlags::default();
|
||||
Self {
|
||||
running: RunMode::Running,
|
||||
draw_colls: false,
|
||||
#[cfg(feature = "dim3")]
|
||||
vehicle_controller: None,
|
||||
grabbed_object_plane: (Point3::origin(), na::zero()),
|
||||
can_grab_behind_ground: false,
|
||||
drawing_ray: None,
|
||||
snapshot: None,
|
||||
prev_flags: flags,
|
||||
flags,
|
||||
action_flags: TestbedActionFlags::APP_STARTED | TestbedActionFlags::EXAMPLE_CHANGED,
|
||||
backend_names,
|
||||
examples: Vec::new(),
|
||||
example_groups: Vec::new(),
|
||||
example_settings: ExampleSettings::default(),
|
||||
selected_display_index: 0,
|
||||
selected_backend: RAPIER_BACKEND,
|
||||
broad_phase_type: RapierBroadPhaseType::default(),
|
||||
physx_use_two_friction_directions: true,
|
||||
nsteps: 1,
|
||||
camera_locked: false,
|
||||
selected_tab: UiTab::default(),
|
||||
prev_save_data: SerializableTestbedState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestbedState {
|
||||
/// Get the builder index for the currently selected example
|
||||
pub fn selected_builder_index(&self) -> usize {
|
||||
self.examples
|
||||
.get(self.selected_display_index)
|
||||
.map(|e| e.builder_index)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
243
src_testbed/testbed/testbed.rs
Normal file
243
src_testbed/testbed/testbed.rs
Normal file
@@ -0,0 +1,243 @@
|
||||
//! Testbed struct for building examples.
|
||||
|
||||
use kiss3d::color::Color;
|
||||
use rapier::dynamics::{
|
||||
ImpulseJointSet, IntegrationParameters, MultibodyJointSet, RigidBodyHandle, RigidBodySet,
|
||||
};
|
||||
use rapier::geometry::{ColliderHandle, ColliderSet};
|
||||
use rapier::pipeline::PhysicsHooks;
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
use {glamx::Vec3, rapier::control::DynamicRayCastVehicleController};
|
||||
|
||||
use crate::harness::Harness;
|
||||
use crate::physics::PhysicsState;
|
||||
use crate::settings::ExampleSettings;
|
||||
|
||||
use super::graphics_context::TestbedGraphics;
|
||||
use super::state::{TestbedActionFlags, TestbedState};
|
||||
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
use super::OtherBackends;
|
||||
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
use super::state::{PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR};
|
||||
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
use crate::physx_backend::PhysxWorld;
|
||||
|
||||
use super::Plugins;
|
||||
|
||||
/// An example/demo that can be run in the testbed
|
||||
#[derive(Clone)]
|
||||
pub struct Example {
|
||||
/// Display name of the example
|
||||
pub name: &'static str,
|
||||
/// Group/category for organizing in the UI (e.g., "Demos", "Joints", "Debug")
|
||||
pub group: &'static str,
|
||||
/// The builder function that initializes the example
|
||||
pub builder: fn(&mut Testbed),
|
||||
}
|
||||
|
||||
impl Example {
|
||||
/// Create a new example with a group
|
||||
pub fn new(group: &'static str, name: &'static str, builder: fn(&mut Testbed)) -> Self {
|
||||
Self {
|
||||
name,
|
||||
group,
|
||||
builder,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new example in the default "Demos" group
|
||||
pub fn demo(name: &'static str, builder: fn(&mut Testbed)) -> Self {
|
||||
Self::new("Demos", name, builder)
|
||||
}
|
||||
}
|
||||
|
||||
/// Allow constructing Example from a tuple (group, name, builder) for convenience
|
||||
impl From<(&'static str, &'static str, fn(&mut Testbed))> for Example {
|
||||
fn from((group, name, builder): (&'static str, &'static str, fn(&mut Testbed))) -> Self {
|
||||
Self::new(group, name, builder)
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias for simulation builder functions
|
||||
pub type SimulationBuilders = Vec<Example>;
|
||||
|
||||
/// The main testbed struct passed to example builders
|
||||
pub struct Testbed<'a> {
|
||||
pub graphics: Option<TestbedGraphics<'a>>,
|
||||
pub harness: &'a mut Harness,
|
||||
pub state: &'a mut TestbedState,
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
pub other_backends: &'a mut OtherBackends,
|
||||
pub plugins: &'a mut Plugins,
|
||||
}
|
||||
|
||||
impl Testbed<'_> {
|
||||
pub fn set_number_of_steps_per_frame(&mut self, nsteps: usize) {
|
||||
self.state.nsteps = nsteps;
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
pub fn set_vehicle_controller(&mut self, controller: DynamicRayCastVehicleController) {
|
||||
self.state.vehicle_controller = Some(controller);
|
||||
}
|
||||
|
||||
pub fn allow_grabbing_behind_ground(&mut self, allow: bool) {
|
||||
self.state.can_grab_behind_ground = allow;
|
||||
}
|
||||
|
||||
pub fn integration_parameters_mut(&mut self) -> &mut IntegrationParameters {
|
||||
&mut self.harness.physics.integration_parameters
|
||||
}
|
||||
|
||||
pub fn physics_state_mut(&mut self) -> &mut PhysicsState {
|
||||
&mut self.harness.physics
|
||||
}
|
||||
|
||||
pub fn harness(&self) -> &Harness {
|
||||
self.harness
|
||||
}
|
||||
|
||||
pub fn harness_mut(&mut self) -> &mut Harness {
|
||||
self.harness
|
||||
}
|
||||
|
||||
pub fn example_settings_mut(&mut self) -> &mut ExampleSettings {
|
||||
&mut self.state.example_settings
|
||||
}
|
||||
|
||||
pub fn set_world(
|
||||
&mut self,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
impulse_joints: ImpulseJointSet,
|
||||
multibody_joints: MultibodyJointSet,
|
||||
) {
|
||||
self.set_world_with_params(
|
||||
bodies,
|
||||
colliders,
|
||||
impulse_joints,
|
||||
multibody_joints,
|
||||
rapier::math::Vector::Y * -9.81,
|
||||
(),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn set_world_with_params(
|
||||
&mut self,
|
||||
bodies: RigidBodySet,
|
||||
colliders: ColliderSet,
|
||||
impulse_joints: ImpulseJointSet,
|
||||
multibody_joints: MultibodyJointSet,
|
||||
gravity: rapier::math::Vector,
|
||||
hooks: impl PhysicsHooks + 'static,
|
||||
) {
|
||||
self.harness.set_world_with_params(
|
||||
bodies,
|
||||
colliders,
|
||||
impulse_joints,
|
||||
multibody_joints,
|
||||
self.state.broad_phase_type,
|
||||
gravity,
|
||||
hooks,
|
||||
);
|
||||
|
||||
self.state
|
||||
.action_flags
|
||||
.set(TestbedActionFlags::RESET_WORLD_GRAPHICS, true);
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
{
|
||||
self.state.vehicle_controller = None;
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "dim3", feature = "other-backends"))]
|
||||
{
|
||||
if self.state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
|
||||
|| self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR
|
||||
{
|
||||
self.other_backends.physx = Some(PhysxWorld::from_rapier(
|
||||
self.harness.physics.gravity,
|
||||
&self.harness.physics.integration_parameters,
|
||||
&self.harness.physics.bodies,
|
||||
&self.harness.physics.colliders,
|
||||
&self.harness.physics.impulse_joints,
|
||||
&self.harness.physics.multibody_joints,
|
||||
self.state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR,
|
||||
self.harness.state.num_threads(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_graphics_shift(&mut self, shift: rapier::math::Vector) {
|
||||
if !self.state.camera_locked
|
||||
&& let Some(graphics) = &mut self.graphics
|
||||
{
|
||||
graphics.graphics.gfx_shift = shift;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim2")]
|
||||
pub fn look_at(&mut self, at: glamx::Vec2, zoom: f32) {
|
||||
if !self.state.camera_locked
|
||||
&& let Some(graphics) = &mut self.graphics
|
||||
{
|
||||
graphics.camera.set_at(at);
|
||||
graphics.camera.set_zoom(zoom);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dim3")]
|
||||
pub fn look_at(&mut self, eye: Vec3, at: Vec3) {
|
||||
if !self.state.camera_locked
|
||||
&& let Some(graphics) = &mut self.graphics
|
||||
{
|
||||
graphics.camera.look_at(eye, at);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_initial_body_color(&mut self, body: RigidBodyHandle, color: Color) {
|
||||
if let Some(graphics) = &mut self.graphics {
|
||||
graphics.graphics.set_initial_body_color(body, color);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_initial_collider_color(&mut self, collider: ColliderHandle, color: Color) {
|
||||
if let Some(graphics) = &mut self.graphics {
|
||||
graphics
|
||||
.graphics
|
||||
.set_initial_collider_color(collider, color);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_body_wireframe(&mut self, body: RigidBodyHandle, wireframe_enabled: bool) {
|
||||
if let Some(graphics) = &mut self.graphics {
|
||||
graphics
|
||||
.graphics
|
||||
.set_body_wireframe(body, wireframe_enabled);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_callback<
|
||||
F: FnMut(
|
||||
Option<&mut TestbedGraphics>,
|
||||
&mut PhysicsState,
|
||||
&crate::physics::PhysicsEvents,
|
||||
&crate::harness::RunState,
|
||||
) + 'static,
|
||||
>(
|
||||
&mut self,
|
||||
callback: F,
|
||||
) {
|
||||
self.harness.add_callback(callback);
|
||||
}
|
||||
|
||||
pub fn add_plugin(&mut self, mut plugin: impl crate::plugin::TestbedPlugin + 'static) {
|
||||
plugin.init_plugin();
|
||||
self.plugins.0.push(Box::new(plugin));
|
||||
}
|
||||
}
|
||||
1106
src_testbed/ui.rs
1106
src_testbed/ui.rs
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user