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:
Sébastien Crozet
2026-01-09 17:26:36 +01:00
committed by GitHub
parent 48de83817e
commit 0b7c3b34ec
265 changed files with 8501 additions and 8575 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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>;
}

View File

@@ -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;
}
}
}

View File

@@ -1 +0,0 @@
pub mod node;

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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;
}

View File

@@ -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
View 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);
}
}
}

View 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()
}
}

View 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);
}
}
}
}

View 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
}
}

View 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>>);

View 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)
}
}

View 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));
}
}

File diff suppressed because it is too large Load Diff