Files
rapier/src_testbed/ui.rs
2025-11-21 17:12:05 +01:00

515 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use rapier::counters::Counters;
use rapier::math::Real;
use crate::debug_render::DebugRenderPipelineResource;
use crate::harness::{Harness, RapierBroadPhaseType};
use crate::testbed::{
PHYSX_BACKEND_PATCH_FRICTION, PHYSX_BACKEND_TWO_FRICTION_DIR, RunMode, TestbedActionFlags,
TestbedState, TestbedStateFlags,
};
pub use bevy_egui::egui;
use crate::PhysicsState;
use crate::settings::SettingValue;
use bevy_egui::EguiContexts;
use bevy_egui::egui::{ComboBox, Slider, Ui, Window};
use web_time::Instant;
#[cfg(feature = "dim3")]
use rapier::dynamics::FrictionModel;
pub(crate) fn update_ui(
ui_context: &mut EguiContexts,
state: &mut TestbedState,
harness: &mut Harness,
debug_render: &mut DebugRenderPipelineResource,
) {
#[cfg(feature = "profiler_ui")]
{
profiler_ui(ui_context);
}
example_settings_ui(ui_context, state);
Window::new("Parameters").show(ui_context.ctx_mut(), |ui| {
if state.backend_names.len() > 1 && !state.example_names.is_empty() {
let mut changed = false;
ComboBox::from_label("backend")
.width(150.0)
.selected_text(state.backend_names[state.selected_backend])
.show_ui(ui, |ui| {
for (id, name) in state.backend_names.iter().enumerate() {
changed = ui
.selectable_value(&mut state.selected_backend, id, *name)
.changed()
|| changed;
}
});
if changed {
state
.action_flags
.set(TestbedActionFlags::BACKEND_CHANGED, true);
}
ui.separator();
}
ui.horizontal(|ui| {
if ui.button("<").clicked() && state.selected_example > 0 {
state.selected_example -= 1;
state
.action_flags
.set(TestbedActionFlags::EXAMPLE_CHANGED, true)
}
if ui.button(">").clicked() && state.selected_example + 1 < state.example_names.len() {
state.selected_example += 1;
state
.action_flags
.set(TestbedActionFlags::EXAMPLE_CHANGED, true)
}
let mut changed = false;
egui::ComboBox::from_label("example")
.width(150.0)
.selected_text(state.example_names[state.selected_example])
.show_ui(ui, |ui| {
for (id, name) in state.example_names.iter().enumerate() {
changed = ui
.selectable_value(&mut state.selected_example, id, *name)
.changed()
|| changed;
}
});
if changed {
state
.action_flags
.set(TestbedActionFlags::EXAMPLE_CHANGED, true);
}
});
ui.separator();
ui.collapsing("Scene infos", |ui| {
scene_infos_ui(ui, &harness.physics);
});
ui.collapsing("Profile infos", |ui| {
ui.horizontal_wrapped(|ui| {
profiling_ui(ui, &harness.physics.pipeline.counters);
});
});
ui.collapsing("Serialization infos", |ui| {
ui.horizontal_wrapped(|ui| {
ui.label(serialization_string(
harness.state.timestep_id,
&harness.physics,
))
});
});
let integration_parameters = &mut harness.physics.integration_parameters;
if state.selected_backend == PHYSX_BACKEND_PATCH_FRICTION
|| state.selected_backend == PHYSX_BACKEND_TWO_FRICTION_DIR
{
ui.add(
Slider::new(&mut integration_parameters.num_solver_iterations, 0..=10)
.text("pos. iters."),
);
} else {
// Broad-phase.
let mut changed = false;
egui::ComboBox::from_label("broad-phase")
.width(150.0)
.selected_text(format!("{:?}", state.broad_phase_type))
.show_ui(ui, |ui| {
let broad_phase_type = [
RapierBroadPhaseType::BvhSubtreeOptimizer,
RapierBroadPhaseType::BvhWithoutOptimization,
];
for sty in broad_phase_type {
changed = ui
.selectable_value(&mut state.broad_phase_type, sty, format!("{sty:?}"))
.changed()
|| changed;
}
});
if changed {
harness.physics.broad_phase = state.broad_phase_type.init_broad_phase();
// Restart the simulation after a broad-phase changes since some
// broad-phase might not support hot-swapping.
state.action_flags.set(TestbedActionFlags::RESTART, true);
}
// Friction model.
#[cfg(feature = "dim3")]
egui::ComboBox::from_label("friction-model")
.width(150.0)
.selected_text(format!("{:?}", integration_parameters.friction_model))
.show_ui(ui, |ui| {
let friction_model = [FrictionModel::Simplified, FrictionModel::Coulomb];
for model in friction_model {
changed = ui
.selectable_value(
&mut integration_parameters.friction_model,
model,
format!("{model:?}"),
)
.changed()
|| changed;
}
});
// Solver iterations.
ui.add(
Slider::new(&mut integration_parameters.num_solver_iterations, 0..=10)
.text("num solver iters."),
);
ui.add(
Slider::new(
&mut integration_parameters.num_internal_pgs_iterations,
1..=40,
)
.text("num internal PGS iters."),
);
ui.add(
Slider::new(
&mut integration_parameters.num_internal_stabilization_iterations,
0..=100,
)
.text("max internal stabilization iters."),
);
ui.add(
Slider::new(&mut integration_parameters.warmstart_coefficient, 0.0..=1.0)
.text("warmstart coefficient"),
);
let mut substep_params = *integration_parameters;
substep_params.dt /= substep_params.num_solver_iterations as Real;
let curr_erp = substep_params.contact_softness.erp(substep_params.dt);
let curr_cfm_factor = substep_params
.contact_softness
.cfm_factor(substep_params.dt);
ui.add(
Slider::new(
&mut integration_parameters.contact_softness.natural_frequency,
0.01..=120.0,
)
.text(format!("contacts Hz (erp = {curr_erp:.3})")),
);
ui.add(
Slider::new(
&mut integration_parameters.contact_softness.damping_ratio,
0.01..=20.0,
)
.text(format!("damping ratio (cfm-factor = {curr_cfm_factor:.3})",)),
);
}
#[cfg(feature = "parallel")]
{
let mut num_threads = harness.state.num_threads();
ui.add(
Slider::new(&mut num_threads, 1..=num_cpus::get_physical()).text("num. threads"),
);
harness.state.set_num_threads(num_threads);
}
ui.add(
Slider::new(&mut integration_parameters.max_ccd_substeps, 0..=10).text("CCD substeps"),
);
ui.add(
Slider::new(&mut integration_parameters.min_island_size, 1..=10_000)
.text("min island size"),
);
ui.add(Slider::new(&mut state.nsteps, 1..=100).text("sims. per frame"));
let mut frequency = integration_parameters.inv_dt().round() as u32;
ui.add(Slider::new(&mut frequency, 0..=240).text("frequency (Hz)"));
let mut gravity_y = harness.physics.gravity.y;
if ui
.add(Slider::new(&mut gravity_y, 0.0..=-200.0).text("Gravity"))
.changed()
{
harness.physics.gravity.y = gravity_y;
}
integration_parameters.set_inv_dt(frequency as Real);
let mut sleep = state.flags.contains(TestbedStateFlags::SLEEP);
let mut draw_surfaces = state.flags.contains(TestbedStateFlags::DRAW_SURFACES);
// let mut contact_points = state.flags.contains(TestbedStateFlags::CONTACT_POINTS);
// let mut wireframe = state.flags.contains(TestbedStateFlags::WIREFRAME);
ui.checkbox(&mut sleep, "sleep enabled");
// ui.checkbox(&mut contact_points, "draw contacts");
// ui.checkbox(&mut wireframe, "draw wireframes");
ui.checkbox(&mut draw_surfaces, "surface render enabled");
ui.checkbox(&mut debug_render.enabled, "debug render enabled");
state.flags.set(TestbedStateFlags::SLEEP, sleep);
state
.flags
.set(TestbedStateFlags::DRAW_SURFACES, draw_surfaces);
// state
// .flags
// .set(TestbedStateFlags::CONTACT_POINTS, contact_points);
// state.flags.set(TestbedStateFlags::WIREFRAME, wireframe);
ui.separator();
let label = if state.running == RunMode::Stop {
"Start (T)"
} else {
"Pause (T)"
};
if ui.button(label).clicked() {
if state.running == RunMode::Stop {
state.running = RunMode::Running
} else {
state.running = RunMode::Stop
}
}
if ui.button("Single Step (S)").clicked() {
state.running = RunMode::Step;
}
if ui.button("Take snapshot").clicked() {
state
.action_flags
.set(TestbedActionFlags::TAKE_SNAPSHOT, true);
}
if ui.button("Restore snapshot").clicked() {
state
.action_flags
.set(TestbedActionFlags::RESTORE_SNAPSHOT, true);
}
if ui.button("Restart (R)").clicked() {
state.action_flags.set(TestbedActionFlags::RESTART, true);
}
});
}
fn scene_infos_ui(ui: &mut Ui, physics: &PhysicsState) {
ui.label(format!("# rigid-bodies: {}", physics.bodies.len()));
ui.label(format!("# colliders: {}", physics.colliders.len()));
ui.label(format!("# impulse joint: {}", physics.impulse_joints.len()));
// ui.label(format!(
// "# multibody joint: {}",
// physics.multibody_joints.len()
// ));
}
fn profiling_ui(ui: &mut Ui, counters: &Counters) {
egui::CollapsingHeader::new(format!(
"Total: {:.2}ms - {} fps",
counters.step_time_ms(),
(1000.0 / counters.step_time_ms()).round()
))
.id_salt("total")
.show(ui, |ui| {
egui::CollapsingHeader::new(format!(
"Collision detection: {:.2}ms",
counters.collision_detection_time_ms()
))
.id_salt("collision detection")
.show(ui, |ui| {
ui.label(format!(
"Broad-phase: {:.2}ms",
counters.broad_phase_time_ms()
));
ui.label(format!(
"Narrow-phase: {:.2}ms",
counters.narrow_phase_time_ms()
));
});
egui::CollapsingHeader::new(format!("Solver: {:.2}ms", counters.solver_time_ms()))
.id_salt("solver")
.show(ui, |ui| {
ui.label(format!(
"Velocity assembly: {:.2}ms",
counters.solver.velocity_assembly_time.time_ms()
));
ui.label(format!(
"Velocity resolution: {:.2}ms",
counters.velocity_resolution_time_ms()
));
ui.label(format!(
"Velocity integration: {:.2}ms",
counters.solver.velocity_update_time.time_ms()
));
ui.label(format!(
"Writeback: {:.2}ms",
counters.solver.velocity_writeback_time.time_ms()
));
});
egui::CollapsingHeader::new(format!("CCD: {:.2}ms", counters.ccd_time_ms()))
.id_salt("ccd")
.show(ui, |ui| {
ui.label(format!("# of substeps: {}", counters.ccd.num_substeps));
ui.label(format!(
"TOI computation: {:.2}ms",
counters.ccd.toi_computation_time.time_ms(),
));
ui.label(format!(
"Broad-phase: {:.2}ms",
counters.ccd.broad_phase_time.time_ms()
));
ui.label(format!(
"Narrow-phase: {:.2}ms",
counters.ccd.narrow_phase_time.time_ms(),
));
ui.label(format!(
"Solver: {:.2}ms",
counters.ccd.solver_time.time_ms()
));
});
ui.label(format!(
"Island computation: {:.2}ms",
counters.island_construction_time_ms()
));
ui.label(format!(
"User changes: {:.2}ms",
counters.stages.user_changes.time_ms()
));
});
}
fn serialization_string(timestep_id: usize, physics: &PhysicsState) -> String {
let t = Instant::now();
// let t = Instant::now();
let bf = bincode::serialize(&physics.broad_phase).unwrap();
// println!("bf: {}", Instant::now() - t);
// let t = Instant::now();
let nf = bincode::serialize(&physics.narrow_phase).unwrap();
// println!("nf: {}", Instant::now() - t);
// let t = Instant::now();
let bs = bincode::serialize(&physics.bodies).unwrap();
// println!("bs: {}", Instant::now() - t);
// let t = Instant::now();
let cs = bincode::serialize(&physics.colliders).unwrap();
// println!("cs: {}", Instant::now() - t);
// let t = Instant::now();
let js = bincode::serialize(&physics.impulse_joints).unwrap();
// println!("js: {}", Instant::now() - t);
let serialization_time = Instant::now() - t;
let hash_bf = md5::compute(&bf);
let hash_nf = md5::compute(&nf);
let hash_bodies = md5::compute(&bs);
let hash_colliders = md5::compute(&cs);
let hash_joints = md5::compute(&js);
format!(
r#"Serialization time: {:.2}ms
Hashes at frame: {}
|_ Broad phase [{:.1}KB]: {}
|_ Narrow phase [{:.1}KB]: {}
|_ &RigidBodySet [{:.1}KB]: {}
|_ Colliders [{:.1}KB]: {}
|_ Joints [{:.1}KB]: {}"#,
serialization_time.as_secs_f64() * 1000.0,
timestep_id,
bf.len() as f32 / 1000.0,
format!("{hash_bf:?}").split_at(10).0,
nf.len() as f32 / 1000.0,
format!("{hash_nf:?}").split_at(10).0,
bs.len() as f32 / 1000.0,
format!("{hash_bodies:?}").split_at(10).0,
cs.len() as f32 / 1000.0,
format!("{hash_colliders:?}").split_at(10).0,
js.len() as f32 / 1000.0,
format!("{hash_joints:?}").split_at(10).0,
)
}
fn example_settings_ui(ui_context: &mut EguiContexts, state: &mut TestbedState) {
if state.example_settings.is_empty() {
// Dont show any window if there is no settings for the
// example.
return;
}
Window::new("Example settings").show(ui_context.ctx_mut(), |ui| {
let mut any_changed = false;
for (name, value) in state.example_settings.iter_mut() {
let prev_value = value.clone();
match value {
SettingValue::Label(value) => {
ui.label(format!("{name}: {value}"));
}
SettingValue::F32 { value, range } => {
ui.add(Slider::new(value, range.clone()).text(name));
}
SettingValue::U32 { value, range } => {
ui.horizontal(|ui| {
if ui.button("<").clicked() && *value > *range.start() {
*value -= 1;
}
if ui.button(">").clicked() && *value <= *range.end() {
*value += 1;
}
ui.add(Slider::new(value, range.clone()).text(name));
});
}
SettingValue::Bool { value } => {
ui.checkbox(value, name);
}
SettingValue::String { value, range } => {
ComboBox::from_label(name)
.width(150.0)
.selected_text(&range[*value])
.show_ui(ui, |ui| {
for (id, name) in range.iter().enumerate() {
ui.selectable_value(value, id, name);
}
});
}
}
any_changed = any_changed || *value != prev_value;
}
if any_changed {
// The value changed, request a restart.
state.action_flags.set(TestbedActionFlags::RESTART, true);
}
});
}
#[cfg(feature = "profiler_ui")]
fn profiler_ui(ui_context: &mut EguiContexts) {
let window = egui::Window::new("Profiling");
let window = window.default_open(false);
#[cfg(feature = "unstable-puffin-pr-235")]
{
use std::sync::Once;
static START: Once = Once::new();
fn set_default_rapier_filter() {
let mut profile_ui = puffin_egui::PROFILE_UI.lock();
profile_ui
.profiler_ui
.flamegraph_options
.scope_name_filter
.set_filter("Harness::step_with_graphics".to_string());
}
START.call_once(|| {
set_default_rapier_filter();
});
window.show(ui_context.ctx_mut(), |ui| {
if ui.button("🔍 Rapier filter").clicked() {
set_default_rapier_filter();
}
puffin_egui::profiler_ui(ui);
});
}
#[cfg(not(feature = "unstable-puffin-pr-235"))]
window.show(ui_context.ctx_mut(), |ui| {
puffin_egui::profiler_ui(ui);
});
}