From b7000074c594a528b9a3070fd916a19c95bef585 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Wed, 26 Mar 2025 17:52:09 +0100 Subject: [PATCH 1/4] example to show multiple configurations --- examples3d/src/banana3.rs | 2 +- examples3d/src/configurations.rs | 224 +++++++++++++++++++++ examples3d/src/glb_to_point_cloud_color.rs | 4 +- examples3d/src/main.rs | 5 + examples3d/src/sand3.rs | 2 +- examples3d/src/utils/default_scene.rs | 24 +-- 6 files changed, 245 insertions(+), 16 deletions(-) create mode 100644 examples3d/src/configurations.rs diff --git a/examples3d/src/banana3.rs b/examples3d/src/banana3.rs index feb0223..52a655c 100644 --- a/examples3d/src/banana3.rs +++ b/examples3d/src/banana3.rs @@ -55,7 +55,7 @@ pub fn demo( app_state.num_substeps = 30; app_state.gravity_factor = 1.0; }; - default_scene::spawn_ground_and_walls(&mut rapier_data); + default_scene::spawn_ground_and_walls(&mut rapier_data, 1f32); let mut particles = vec![]; for (pos, color) in pc_grid { diff --git a/examples3d/src/configurations.rs b/examples3d/src/configurations.rs new file mode 100644 index 0000000..e6575da --- /dev/null +++ b/examples3d/src/configurations.rs @@ -0,0 +1,224 @@ +use wgsparkl3d::solver::ParticlePhase; +use wgsparkl3d::wgrapier::dynamics::body::{BodyCoupling, BodyCouplingEntry}; +use wgsparkl_testbed3d::{wgsparkl, Callbacks, RapierData}; + +use bevy::render::renderer::RenderDevice; +use nalgebra::{vector, Vector2, Vector3}; +use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder}; +use wgsparkl::models::DruckerPrager; +use wgsparkl::{ + models::ElasticCoefficients, + pipeline::MpmData, + solver::{Particle, ParticleDynamics, SimulationParams}, +}; +use wgsparkl_testbed3d::{AppState, PhysicsContext}; + +use crate::utils::default_scene; + +#[derive(Debug)] +pub struct ParticlesConfiguration { + pub coords: Vector2, + pub density: f32, + pub model: ElasticCoefficients, + pub plasticity: Option, + pub phase: Option, + pub description: String, +} + +pub fn configurations_demo( + device: RenderDevice, + app_state: &mut AppState, + _callbacks: &mut Callbacks, +) -> PhysicsContext { + let mut rapier_data = RapierData::default(); + let device = device.wgpu_device(); + + let grid_size_x = 10; + let grid_size_y = 10; + let grid_size_z = 10; + let num_particles = grid_size_x * grid_size_y * grid_size_z; + + let particle_positions = (0..num_particles) + .map(|i| { + let x = i % grid_size_x; + let y = (i / grid_size_x) % grid_size_y; + let z = (i / (grid_size_x * grid_size_y)) % grid_size_z; + Vector3::::new(x as f32, y as f32 + 1f32, z as f32) + }) + .collect::>(); + + if !app_state.restarting { + app_state.num_substeps = 16; + app_state.gravity_factor = 1.0; + }; + + let cell_width = 0.5f32; + let mut particles = vec![]; + let mut configurations = vec![]; + + { + let young_modulus = 1_000_000_000.0; + // line with plasticity, varying poisson + for x in -1..2 { + let poisson_ratio = match x { + -1 => 0.0, + 0 => 0.2, + 1 => 0.4, + _ => unreachable!(), + }; + let model = ElasticCoefficients::from_young_modulus(young_modulus, poisson_ratio); + let plasticity = Some(DruckerPrager { + h0: 35.0f32.to_radians(), + h1: 9.0f32.to_radians(), + h2: 0.2, + h3: 10.0f32.to_radians(), + ..DruckerPrager::new(model.lambda, model.mu) + }); + configurations.push(ParticlesConfiguration { + coords: Vector2::::new(x, -1), + density: 3700f32, + model, + plasticity, + phase: None, + description: format!("With plasticity.\n poisson: {}", poisson_ratio), + }); + } + } + + { + let poisson_ratio = 0f32; + // line with plasticity, varying young modulus + for x in -1..2 { + let young_modulus = match x { + -1 => 1_000_000.0, + 0 => 10_000_000.0, + 1 => 100_000_000.0, + _ => unreachable!(), + }; + let model = ElasticCoefficients::from_young_modulus(young_modulus, poisson_ratio); + let plasticity = Some(DruckerPrager { + h0: 35.0f32.to_radians(), + h1: 9.0f32.to_radians(), + h2: 0.2, + h3: 10.0f32.to_radians(), + ..DruckerPrager::new(model.lambda, model.mu) + }); + configurations.push(ParticlesConfiguration { + coords: Vector2::::new(x, 0), + density: 3700f32, + model, + plasticity, + phase: None, + description: format!( + "With plasticity.\nmodulus: {}M", + young_modulus / 1_000_000f32 + ), + }); + } + } + + { + let poisson_ratio = 0f32; + // line without plasticity, varying young modulus + for x in -1..2 { + let young_modulus = match x { + -1 => 1_000_000.0, + 0 => 50_000_000.0, + 1 => 200_000_000.0, + _ => unreachable!(), + }; + let model = ElasticCoefficients::from_young_modulus(young_modulus, poisson_ratio); + configurations.push(ParticlesConfiguration { + coords: Vector2::::new(x, 1), + density: 3700f32, + model, + plasticity: None, + phase: Some(ParticlePhase { + phase: 1.0, + max_stretch: f32::MAX, + }), + description: format!( + "Without plasticity.\nmodulus: {}M", + young_modulus / 1_000_000.0 + ), + }); + } + } + { + let young_modulus = 1_000_000.0; + // line without plasticity, varying poisson_ratio + for x in -1..2 { + let poisson_ratio = match x { + -1 => -0.2f32, + 0 => 0.3, + 1 => 0.48, + _ => unreachable!(), + }; + let model = ElasticCoefficients::from_young_modulus(young_modulus, poisson_ratio); + configurations.push(ParticlesConfiguration { + coords: Vector2::::new(x, 2), + density: 3700f32, + model, + plasticity: None, + phase: Some(ParticlePhase { + phase: 1.0, + max_stretch: f32::MAX, + }), + description: format!("Without plasticity.\npoisson: {}", poisson_ratio), + }); + } + } + + for c in configurations.iter() { + let x = c.coords.x as f32 * 3f32; + let z = c.coords.y as f32 * 3f32; + let offset = vector![ + x * grid_size_x as f32, + 3f32, + z * grid_size_z as f32 * 0.7f32 + ] * 2f32; + for particle in &particle_positions { + let position = vector![particle.x, particle.y, particle.z]; + let density = c.density; + particles.push(Particle { + position: nalgebra::Rotation::from_axis_angle( + &Vector3::z_axis(), + 1f32.to_radians(), + ) * position + + offset, + dynamics: ParticleDynamics::with_density(1.0, density), + model: c.model, + plasticity: c.plasticity, + phase: c.phase, + color: None, + }); + } + } + + if !app_state.restarting { + app_state.num_substeps = 20; + app_state.gravity_factor = 1.0; + }; + + let params = SimulationParams { + gravity: vector![0.0, -9.81, 0.0] * app_state.gravity_factor, + dt: (1.0 / 60.0) / (app_state.num_substeps as f32), + }; + + default_scene::spawn_ground_and_walls(&mut rapier_data, 3f32); + + let data = MpmData::new( + device, + params, + &particles, + &rapier_data.bodies, + &rapier_data.colliders, + cell_width, + 60_000, + ); + PhysicsContext { + data, + rapier_data, + particles, + } +} diff --git a/examples3d/src/glb_to_point_cloud_color.rs b/examples3d/src/glb_to_point_cloud_color.rs index 5e03b8a..1755e55 100644 --- a/examples3d/src/glb_to_point_cloud_color.rs +++ b/examples3d/src/glb_to_point_cloud_color.rs @@ -37,7 +37,7 @@ pub fn elastic_color_model_demo( app_state.num_substeps = 20; app_state.gravity_factor = 1.0; }; - default_scene::spawn_ground_and_walls(&mut rapier_data); + default_scene::spawn_ground_and_walls(&mut rapier_data, 1f32); let mut particles = vec![]; for (pos, color) in pc_grid { @@ -48,7 +48,7 @@ pub fn elastic_color_model_demo( ); particles.push(particle); } - default_scene::spawn_ground_and_walls(&mut rapier_data); + default_scene::spawn_ground_and_walls(&mut rapier_data, 1f32); let data = MpmData::new( device.wgpu_device(), params, diff --git a/examples3d/src/main.rs b/examples3d/src/main.rs index 09158a5..b2db757 100644 --- a/examples3d/src/main.rs +++ b/examples3d/src/main.rs @@ -4,6 +4,7 @@ use wgsparkl_testbed3d::{init_testbed, SceneInitFn, SceneInits}; pub mod utils; mod banana3; +mod configurations; mod elastic_cut3; mod glb_to_point_cloud_color; mod heightfield3; @@ -23,6 +24,10 @@ pub fn main() { fn register_scenes(world: &mut World) { let scenes: Vec<(String, SceneInitFn)> = vec![ + ( + "configurations".to_string(), + Box::new(configurations::configurations_demo), + ), ("sand".to_string(), Box::new(sand3::sand_demo)), ( "heightfield".to_string(), diff --git a/examples3d/src/sand3.rs b/examples3d/src/sand3.rs index 2c7570a..941cdf7 100644 --- a/examples3d/src/sand3.rs +++ b/examples3d/src/sand3.rs @@ -35,7 +35,7 @@ pub fn sand_demo( let radius = cell_width / 4.0; particles.push(Particle { position, - dynamics: ParticleDynamics::with_density(radius, density), + dynamics: ParticleDynamics::with_density(radius * 4f32, density), model: ElasticCoefficients::from_young_modulus(2_000_000_000.0, 0.2), plasticity: Some(DruckerPrager::new(2_000_000_000.0, 0.2)), phase: None, diff --git a/examples3d/src/utils/default_scene.rs b/examples3d/src/utils/default_scene.rs index 7add5de..ad752a8 100644 --- a/examples3d/src/utils/default_scene.rs +++ b/examples3d/src/utils/default_scene.rs @@ -1,7 +1,7 @@ #![allow(unused)] use bevy::prelude::*; -use nalgebra::vector; +use nalgebra::{vector, Vector3}; use rapier3d::prelude::{ColliderBuilder, RigidBodyBuilder}; use wgsparkl3d::{ models::ElasticCoefficients, @@ -12,27 +12,27 @@ use wgsparkl_testbed3d::{AppState, RapierData}; pub const SAMPLE_PER_UNIT: f32 = 10.0; /// Spawns a ground and 4 walls. -pub fn spawn_ground_and_walls(rapier_data: &mut RapierData) { +pub fn spawn_ground_and_walls(rapier_data: &mut RapierData, scale: f32) { rapier_data.insert_body_and_collider( - RigidBodyBuilder::fixed().translation(vector![0.0, -4.0, 0.0]), - ColliderBuilder::cuboid(100.0, 4.0, 100.0), + RigidBodyBuilder::fixed().translation(vector![0.0, -4.0, 0.0] * scale), + ColliderBuilder::cuboid(100.0 * scale, 4.0 * scale, 100.0 * scale), ); rapier_data.insert_body_and_collider( - RigidBodyBuilder::fixed().translation(vector![0.0, 5.0, -35.0]), - ColliderBuilder::cuboid(35.0, 5.0, 0.5), + RigidBodyBuilder::fixed().translation(vector![0.0, 5.0, -35.0] * scale), + ColliderBuilder::cuboid(35.0 * scale, 5.0 * scale, 0.5 * scale), ); rapier_data.insert_body_and_collider( - RigidBodyBuilder::fixed().translation(vector![0.0, 5.0, 35.0]), - ColliderBuilder::cuboid(35.0, 5.0, 0.5), + RigidBodyBuilder::fixed().translation(vector![0.0, 5.0, 35.0] * scale), + ColliderBuilder::cuboid(35.0 * scale, 5.0 * scale, 0.5 * scale), ); rapier_data.insert_body_and_collider( - RigidBodyBuilder::fixed().translation(vector![-35.0, 5.0, 0.0]), - ColliderBuilder::cuboid(0.5, 5.0, 35.0), + RigidBodyBuilder::fixed().translation(vector![-35.0, 5.0, 0.0] * scale), + ColliderBuilder::cuboid(0.5 * scale, 5.0 * scale, 35.0 * scale), ); rapier_data.insert_body_and_collider( - RigidBodyBuilder::fixed().translation(vector![35.0, 5.0, 0.0]), - ColliderBuilder::cuboid(0.5, 5.0, 35.0), + RigidBodyBuilder::fixed().translation(vector![35.0, 5.0, 0.0] * scale), + ColliderBuilder::cuboid(0.5 * scale, 5.0 * scale, 35.0 * scale), ); } From 70d263f8d5e112b8f01ba678e7087a4b849564ac Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Thu, 27 Mar 2025 11:46:50 +0100 Subject: [PATCH 2/4] add text gizmos for testbed + description text for configurations example --- examples3d/src/banana3.rs | 102 +++++++++++++++++-------------- examples3d/src/configurations.rs | 87 ++++++++++++++++++++------ src_testbed/egui_text_gizmo.rs | 97 +++++++++++++++++++++++++++++ src_testbed/gizmos.rs | 17 ++++++ src_testbed/lib.rs | 32 +++++++--- src_testbed/startup.rs | 3 + src_testbed/step.rs | 19 +++++- 7 files changed, 281 insertions(+), 76 deletions(-) create mode 100644 src_testbed/egui_text_gizmo.rs create mode 100644 src_testbed/gizmos.rs diff --git a/examples3d/src/banana3.rs b/examples3d/src/banana3.rs index 52a655c..61aeb50 100644 --- a/examples3d/src/banana3.rs +++ b/examples3d/src/banana3.rs @@ -132,51 +132,59 @@ pub fn demo( } fn move_knife_function(body_handle: RigidBodyHandle) -> Callback { - Box::new( - move |_render, physics: &mut PhysicsContext, _timestamps, app_state: &AppState| { - let t = app_state.physics_time_seconds as f32; - - let body = physics.rapier_data.bodies.get_mut(body_handle).unwrap(); - let length = 1.31; - let width = 0.35; - let x_pos = 2.8; - let y_pos = 1.3; - let z_pos = -1.5; - let velocity = 0.9; - let extended_width = 0.15; // extended width for the horizontal move to the right - let period = - (2f32 * length + width + extended_width + 2f32 * width + extended_width) / velocity; - - let i = (t / period).floor(); - let dis = velocity * (t - period * i); - - // Determine the new position of the knife based on the current displacement - let new_pos = if dis < length { - // Moving downwards - vector![x_pos - width * i, y_pos - dis, z_pos] - } else if length <= dis && dis < length + width + extended_width { - // Moving horizontally to the right with extended width - vector![x_pos - width * i + (dis - length), y_pos - length, z_pos] - } else if length + width + extended_width <= dis - && dis < 2f32 * length + width + extended_width - { - // Moving upwards - vector![ - x_pos - width * i + width + extended_width, - y_pos - length + dis - (length + width + extended_width), - z_pos - ] - } else { - // Moving horizontally to the left, back to the starting x position - vector![ - x_pos - width * i + width + extended_width - - (dis - 2.0 * length - width - extended_width), - y_pos, - z_pos - ] - }; - - body.set_translation(new_pos, true); - }, - ) + Callback { + callback: Box::new( + move |_render, + physics: &mut PhysicsContext, + _timestamps, + app_state: &AppState, + _text_gizmos| { + let t = app_state.physics_time_seconds as f32; + + let body = physics.rapier_data.bodies.get_mut(body_handle).unwrap(); + let length = 1.31; + let width = 0.35; + let x_pos = 2.8; + let y_pos = 1.3; + let z_pos = -1.5; + let velocity = 0.9; + let extended_width = 0.15; // extended width for the horizontal move to the right + let period = + (2f32 * length + width + extended_width + 2f32 * width + extended_width) + / velocity; + + let i = (t / period).floor(); + let dis = velocity * (t - period * i); + + // Determine the new position of the knife based on the current displacement + let new_pos = if dis < length { + // Moving downwards + vector![x_pos - width * i, y_pos - dis, z_pos] + } else if length <= dis && dis < length + width + extended_width { + // Moving horizontally to the right with extended width + vector![x_pos - width * i + (dis - length), y_pos - length, z_pos] + } else if length + width + extended_width <= dis + && dis < 2f32 * length + width + extended_width + { + // Moving upwards + vector![ + x_pos - width * i + width + extended_width, + y_pos - length + dis - (length + width + extended_width), + z_pos + ] + } else { + // Moving horizontally to the left, back to the starting x position + vector![ + x_pos - width * i + width + extended_width + - (dis - 2.0 * length - width - extended_width), + y_pos, + z_pos + ] + }; + + body.set_translation(new_pos, true); + }, + ), + run_when_paused: false, + } } diff --git a/examples3d/src/configurations.rs b/examples3d/src/configurations.rs index e6575da..fd23b97 100644 --- a/examples3d/src/configurations.rs +++ b/examples3d/src/configurations.rs @@ -1,6 +1,8 @@ +use bevy::color::palettes; use wgsparkl3d::solver::ParticlePhase; use wgsparkl3d::wgrapier::dynamics::body::{BodyCoupling, BodyCouplingEntry}; -use wgsparkl_testbed3d::{wgsparkl, Callbacks, RapierData}; +use wgsparkl_testbed3d::gizmos::TestbedGizmos; +use wgsparkl_testbed3d::{wgsparkl, Callback, Callbacks, RapierData}; use bevy::render::renderer::RenderDevice; use nalgebra::{vector, Vector2, Vector3}; @@ -28,7 +30,7 @@ pub struct ParticlesConfiguration { pub fn configurations_demo( device: RenderDevice, app_state: &mut AppState, - _callbacks: &mut Callbacks, + callbacks: &mut Callbacks, ) -> PhysicsContext { let mut rapier_data = RapierData::default(); let device = device.wgpu_device(); @@ -55,9 +57,37 @@ pub fn configurations_demo( let cell_width = 0.5f32; let mut particles = vec![]; let mut configurations = vec![]; - + let get_position_for_line = |z: f32| -> bevy::math::Vec3 { + bevy::math::Vec3::new( + -2f32 * grid_size_x as f32 * 3f32 * 2f32, + 5f32, + z * grid_size_z as f32 * 0.7f32 * 3f32 * 2f32 + grid_size_z as f32 / 2f32, + ) + }; + let mut display_text_at_world_pos = |world_pos: bevy::math::Vec3, text: String| { + callbacks.0.push(Callback { + callback: Box::new( + move |_render, + _physics, + _timestamps, + _app_state, + text_gizmos: &mut TestbedGizmos| { + text_gizmos.add_text(&text, world_pos, 42f32, palettes::css::CORAL); + }, + ), + run_when_paused: true, + }); + }; { let young_modulus = 1_000_000_000.0; + let z = -1f32; + display_text_at_world_pos( + get_position_for_line(z), + format!( + "With plasticity\nmodulus = {}M", + young_modulus / 1_000_000.0 + ), + ); // line with plasticity, varying poisson for x in -1..2 { let poisson_ratio = match x { @@ -75,18 +105,23 @@ pub fn configurations_demo( ..DruckerPrager::new(model.lambda, model.mu) }); configurations.push(ParticlesConfiguration { - coords: Vector2::::new(x, -1), + coords: Vector2::::new(x, z as i32), density: 3700f32, model, plasticity, phase: None, - description: format!("With plasticity.\n poisson: {}", poisson_ratio), + description: format!("poisson: {}", poisson_ratio), }); } } { let poisson_ratio = 0f32; + let z = 0f32; + display_text_at_world_pos( + get_position_for_line(z), + format!("With plasticity\npoisson = {}", poisson_ratio), + ); // line with plasticity, varying young modulus for x in -1..2 { let young_modulus = match x { @@ -104,21 +139,24 @@ pub fn configurations_demo( ..DruckerPrager::new(model.lambda, model.mu) }); configurations.push(ParticlesConfiguration { - coords: Vector2::::new(x, 0), + coords: Vector2::::new(x, z as i32), density: 3700f32, model, plasticity, phase: None, - description: format!( - "With plasticity.\nmodulus: {}M", - young_modulus / 1_000_000f32 - ), + description: format!("modulus: {}M", young_modulus / 1_000_000f32), }); } } { let poisson_ratio = 0f32; + let z = 1f32; + display_text_at_world_pos( + get_position_for_line(z), + format!("Without plasticity.\npoisson = {}", poisson_ratio), + ); + // line without plasticity, varying young modulus for x in -1..2 { let young_modulus = match x { @@ -129,7 +167,7 @@ pub fn configurations_demo( }; let model = ElasticCoefficients::from_young_modulus(young_modulus, poisson_ratio); configurations.push(ParticlesConfiguration { - coords: Vector2::::new(x, 1), + coords: Vector2::::new(x, z as i32), density: 3700f32, model, plasticity: None, @@ -137,15 +175,20 @@ pub fn configurations_demo( phase: 1.0, max_stretch: f32::MAX, }), - description: format!( - "Without plasticity.\nmodulus: {}M", - young_modulus / 1_000_000.0 - ), + description: format!("modulus: {}M", young_modulus / 1_000_000.0), }); } } { let young_modulus = 1_000_000.0; + let z = 2f32; + display_text_at_world_pos( + get_position_for_line(z), + format!( + "Without plasticity.\nmodulus = {}M", + young_modulus / 1_000_000.0 + ), + ); // line without plasticity, varying poisson_ratio for x in -1..2 { let poisson_ratio = match x { @@ -156,7 +199,7 @@ pub fn configurations_demo( }; let model = ElasticCoefficients::from_young_modulus(young_modulus, poisson_ratio); configurations.push(ParticlesConfiguration { - coords: Vector2::::new(x, 2), + coords: Vector2::::new(x, z as i32), density: 3700f32, model, plasticity: None, @@ -164,7 +207,7 @@ pub fn configurations_demo( phase: 1.0, max_stretch: f32::MAX, }), - description: format!("Without plasticity.\npoisson: {}", poisson_ratio), + description: format!("poisson: {}", poisson_ratio), }); } } @@ -192,6 +235,10 @@ pub fn configurations_demo( phase: c.phase, color: None, }); + display_text_at_world_pos( + bevy::math::Vec3::new(offset.x, 5f32, offset.z + 10f32 + grid_size_z as f32), + c.description.clone(), + ); } } @@ -205,7 +252,11 @@ pub fn configurations_demo( dt: (1.0 / 60.0) / (app_state.num_substeps as f32), }; - default_scene::spawn_ground_and_walls(&mut rapier_data, 3f32); + let scale = 3f32; + rapier_data.insert_body_and_collider( + RigidBodyBuilder::fixed().translation(vector![0.0, -4.0, 0.0] * scale), + ColliderBuilder::cuboid(100.0 * scale, 4.0 * scale, 100.0 * scale), + ); let data = MpmData::new( device, diff --git a/src_testbed/egui_text_gizmo.rs b/src_testbed/egui_text_gizmo.rs new file mode 100644 index 0000000..52851b3 --- /dev/null +++ b/src_testbed/egui_text_gizmo.rs @@ -0,0 +1,97 @@ +//! This work is dual licensed under the Apache License v2.0 and the MIT license (SPDX: Apache-2.0, MIT). +//! usage example: +//! fn example_system(mut text_gizmos: ResMut) { +//! text_gizmos.add("this is a text gizmo!", Vec3::ZERO, 10.0, Color::WHITE); +//! } +//! remember to include [`TextGizmosPlugin`]: + +pub struct TextGizmosPlugin; + +impl Plugin for TextGizmosPlugin { + fn build(&self, app: &mut bevy::prelude::App) { + app.init_resource::() + .add_systems(Last, draw_text_gizmos); + } +} + +use bevy::{prelude::*, window::PrimaryWindow}; +use bevy_egui::{egui, EguiContext}; +use std::collections::VecDeque; + +#[derive(Resource, Default)] +pub struct TextGizmos(VecDeque); + +impl TextGizmos { + pub fn add(&mut self, text: &str, pos: Vec3, size: f32, color: impl Into) { + self.0.push_back(TextGizmo { + text: text.to_string(), + pos, + size, + color: color.into(), + }); + } +} + +pub struct TextGizmo { + text: String, + pos: Vec3, + size: f32, + color: Color, +} + +#[derive(Component)] +pub struct TextGizmoCam; + +pub fn draw_text_gizmos( + window: Single<(&Window, &mut EguiContext), With>, + cam: Single<(&Camera, &GlobalTransform), With>, + mut text_gizmos: ResMut, +) { + let (window, mut egui_ctx) = window.into_inner(); + let (cam, cam_tf) = cam.into_inner(); + let res = window.resolution.size(); + let painter = egui_ctx.get_mut().layer_painter(egui::LayerId { + order: egui::Order::Background, + id: egui::Id::new("text gizmos"), + }); + + for text_gizmo in text_gizmos.0.drain(..) { + let world_dist = (text_gizmo.pos - cam_tf.translation()).length(); + let font_size = text_gizmo.size * 100.0 / world_dist; // treat font size as at 100 units away + if font_size < 5.0 { + continue; // if text too small, don't draw + } + let Some(ndc) = cam.world_to_ndc(cam_tf, text_gizmo.pos) else { + continue; + }; + if ndc.x.abs() > 1. || ndc.y.abs() > 1. || ndc.z < 0. { + continue; // if center of text is off screen, don't draw + } + let screen_pos = egui::pos2(((ndc.x + 1.) / 2.) * res.x, ((-ndc.y + 1.) / 2.) * res.y); + painter.text( + screen_pos, + egui::Align2::CENTER_CENTER, + text_gizmo.text, + egui::FontId::monospace(font_size), + bevy_color_to_egui(text_gizmo.color), + ); + } + text_gizmos.0.clear(); +} + +/// This is not an accurate color conversion, but i don't need it to be +fn bevy_color_to_egui(color: impl Into) -> egui::Color32 { + let color: Color = color.into(); + let color = color.to_srgba(); + fn f32_to_u8(f: f32) -> u8 { + let f = f.clamp(0.0, 1.0); + let u = (f * 256.0).round() as u8; + u + } + egui::Color32::from_rgba_unmultiplied( + f32_to_u8(color.red), + f32_to_u8(color.green), + f32_to_u8(color.blue), + f32_to_u8(color.alpha), + ) +} diff --git a/src_testbed/gizmos.rs b/src_testbed/gizmos.rs new file mode 100644 index 0000000..11a336e --- /dev/null +++ b/src_testbed/gizmos.rs @@ -0,0 +1,17 @@ +use bevy::prelude::*; + +use crate::egui_text_gizmo::TextGizmos; + +pub struct TestbedGizmos<'a> { + text_gizmos: &'a mut TextGizmos, +} + +impl<'a> TestbedGizmos<'a> { + pub fn new(text_gizmos: &'a mut TextGizmos) -> Self { + Self { text_gizmos } + } + + pub fn add_text(&mut self, text: &str, world_pos: Vec3, size: f32, color: impl Into) { + self.text_gizmos.add(text, world_pos, size, color); + } +} diff --git a/src_testbed/lib.rs b/src_testbed/lib.rs index c975794..2329e7e 100644 --- a/src_testbed/lib.rs +++ b/src_testbed/lib.rs @@ -7,6 +7,8 @@ pub extern crate wgsparkl2d as wgsparkl; pub extern crate wgsparkl3d as wgsparkl; use bevy::render::renderer::RenderDevice; +use egui_text_gizmo::TextGizmosPlugin; +use gizmos::TestbedGizmos; #[cfg(feature = "dim2")] pub use instancing2d as instancing; #[cfg(feature = "dim3")] @@ -18,6 +20,8 @@ pub mod instancing2d; #[cfg(feature = "dim3")] pub mod instancing3d; +mod egui_text_gizmo; +pub mod gizmos; mod hot_reload; pub mod prep_vertex_buffer; mod rigid_graphics; @@ -56,6 +60,7 @@ pub fn init_testbed(app: &mut App) { )) .add_plugins(instancing::ParticlesMaterialPlugin) .add_plugins(bevy_egui::EguiPlugin) + .add_plugins(TextGizmosPlugin) .init_resource::() .init_resource::() .add_systems(Startup, startup::setup_app) @@ -63,9 +68,12 @@ pub fn init_testbed(app: &mut App) { Update, ( ui::update_ui, - (step::step_simulation, step::callbacks) - .chain() - .run_if(|state: Res| state.run_state != RunState::Paused), + ( + step::step_simulation + .run_if(|state: Res| state.run_state != RunState::Paused), + step::callbacks, + ) + .chain(), rigid_graphics::update_rigid_graphics, hot_reload::handle_hot_reloading, ) @@ -119,11 +127,19 @@ pub struct PhysicsContext { #[derive(Resource, Default)] pub struct Callbacks(pub Vec); -pub type Callback = Box< - dyn FnMut(Option<&mut RenderContext>, &mut PhysicsContext, &Timestamps, &AppState) - + Send - + Sync, ->; +pub struct Callback { + pub callback: Box< + dyn FnMut( + Option<&mut RenderContext>, + &mut PhysicsContext, + &Timestamps, + &AppState, + &mut TestbedGizmos, + ) + Send + + Sync, + >, + pub run_when_paused: bool, +} #[derive(Resource, Default)] pub struct RenderContext { diff --git a/src_testbed/startup.rs b/src_testbed/startup.rs index 0182acf..d9330ef 100644 --- a/src_testbed/startup.rs +++ b/src_testbed/startup.rs @@ -1,3 +1,4 @@ +use crate::egui_text_gizmo::TextGizmoCam; use crate::instancing::{InstanceBuffer, InstanceData, InstanceMaterialData}; use crate::prep_vertex_buffer::{GpuRenderConfig, RenderConfig, RenderMode, WgPrepVertexBuffer}; use crate::rigid_graphics::{BevyMaterial, EntityWithGraphics}; @@ -82,6 +83,7 @@ pub fn setup_app(mut commands: Commands, device: Res) { last_anchor_depth: -99.0, ..Default::default() }, + TextGizmoCam, )); } @@ -91,6 +93,7 @@ pub fn setup_app(mut commands: Commands, device: Res) { Camera3d::default(), Transform::from_translation(Vec3::new(0.0, 1.5, 5.0)), EditorCam::default(), + TextGizmoCam, )); } } diff --git a/src_testbed/step.rs b/src_testbed/step.rs index 2a02c57..4378817 100644 --- a/src_testbed/step.rs +++ b/src_testbed/step.rs @@ -1,3 +1,5 @@ +use crate::egui_text_gizmo::TextGizmos; +use crate::gizmos::TestbedGizmos; use crate::instancing::InstanceMaterialData; use crate::startup::RigidParticlesTag; use crate::{AppState, Callbacks, PhysicsContext, RenderContext, RunState, Timestamps}; @@ -25,13 +27,19 @@ pub fn callbacks( app_state: ResMut, timings: Res, mut callbacks: ResMut, + mut text_gizmos: ResMut, ) { - for to_call in callbacks.0.iter_mut() { - to_call( + for callback in callbacks.0.iter_mut() { + let mut testbed_gizmos = TestbedGizmos::new(&mut text_gizmos); + if callback.run_when_paused == false && app_state.run_state == RunState::Paused { + continue; + } + (callback.callback)( Some(&mut render), &mut physics, timings.as_ref(), &app_state, + &mut testbed_gizmos, ); } } @@ -243,7 +251,12 @@ pub fn step_simulation_legacy( timestamps: Some(timestamps), ..Default::default() }; - + // `GpuTimestamps` uses a buffer of 2 `Timestamps`, one for the start and one for the end of the operation, + // it's holding 9 floats (see `timings` below). + assert!( + timestamps_ms.len() >= num_substeps * 2 * 10, + "GpuTimestamps should be initialized with a bigger size" + ); for i in 0..num_substeps { let mut timings = [ &mut new_timings.update_rigid_particles, From 2520cbcc1b1264cddd870840c1b1126d91e1f3db Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Mon, 31 Mar 2025 09:35:12 +0200 Subject: [PATCH 3/4] aggregate gizmos and render context --- examples3d/src/banana3.rs | 6 +----- examples3d/src/configurations.rs | 15 ++++++++------- src_testbed/egui_text_gizmo.rs | 1 + src_testbed/lib.rs | 17 +++++++++-------- src_testbed/step.rs | 9 +++++---- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/examples3d/src/banana3.rs b/examples3d/src/banana3.rs index 61aeb50..e2af7c5 100644 --- a/examples3d/src/banana3.rs +++ b/examples3d/src/banana3.rs @@ -134,11 +134,7 @@ pub fn demo( fn move_knife_function(body_handle: RigidBodyHandle) -> Callback { Callback { callback: Box::new( - move |_render, - physics: &mut PhysicsContext, - _timestamps, - app_state: &AppState, - _text_gizmos| { + move |_render, physics: &mut PhysicsContext, _timestamps, app_state: &AppState| { let t = app_state.physics_time_seconds as f32; let body = physics.rapier_data.bodies.get_mut(body_handle).unwrap(); diff --git a/examples3d/src/configurations.rs b/examples3d/src/configurations.rs index fd23b97..9b08825 100644 --- a/examples3d/src/configurations.rs +++ b/examples3d/src/configurations.rs @@ -2,7 +2,7 @@ use bevy::color::palettes; use wgsparkl3d::solver::ParticlePhase; use wgsparkl3d::wgrapier::dynamics::body::{BodyCoupling, BodyCouplingEntry}; use wgsparkl_testbed3d::gizmos::TestbedGizmos; -use wgsparkl_testbed3d::{wgsparkl, Callback, Callbacks, RapierData}; +use wgsparkl_testbed3d::{wgsparkl, Callback, Callbacks, RapierData, Rendering}; use bevy::render::renderer::RenderDevice; use nalgebra::{vector, Vector2, Vector3}; @@ -67,12 +67,13 @@ pub fn configurations_demo( let mut display_text_at_world_pos = |world_pos: bevy::math::Vec3, text: String| { callbacks.0.push(Callback { callback: Box::new( - move |_render, - _physics, - _timestamps, - _app_state, - text_gizmos: &mut TestbedGizmos| { - text_gizmos.add_text(&text, world_pos, 42f32, palettes::css::CORAL); + move |render: Option, _physics, _timestamps, _app_state| { + render.unwrap().text_gizmos.add_text( + &text, + world_pos, + 42f32, + palettes::css::CORAL, + ); }, ), run_when_paused: true, diff --git a/src_testbed/egui_text_gizmo.rs b/src_testbed/egui_text_gizmo.rs index 52851b3..55ad5ed 100644 --- a/src_testbed/egui_text_gizmo.rs +++ b/src_testbed/egui_text_gizmo.rs @@ -18,6 +18,7 @@ use bevy::{prelude::*, window::PrimaryWindow}; use bevy_egui::{egui, EguiContext}; use std::collections::VecDeque; +/// Displays text in immediate mode. #[derive(Resource, Default)] pub struct TextGizmos(VecDeque); diff --git a/src_testbed/lib.rs b/src_testbed/lib.rs index 2329e7e..bc9d846 100644 --- a/src_testbed/lib.rs +++ b/src_testbed/lib.rs @@ -127,20 +127,21 @@ pub struct PhysicsContext { #[derive(Resource, Default)] pub struct Callbacks(pub Vec); +/// Wrapper for the different rendering capabilities of the testbed. +pub struct Rendering<'a> { + pub render: &'a mut RenderContext, + pub text_gizmos: TestbedGizmos<'a>, +} + pub struct Callback { pub callback: Box< - dyn FnMut( - Option<&mut RenderContext>, - &mut PhysicsContext, - &Timestamps, - &AppState, - &mut TestbedGizmos, - ) + Send - + Sync, + dyn FnMut(Option, &mut PhysicsContext, &Timestamps, &AppState) + Send + Sync, >, pub run_when_paused: bool, } +/// Stores materials and meshes for physics colliders handled by the testbed, +/// and their mappings to Bevy entities. #[derive(Resource, Default)] pub struct RenderContext { pub instanced_materials: InstancedMaterials, diff --git a/src_testbed/step.rs b/src_testbed/step.rs index 4378817..9204fcf 100644 --- a/src_testbed/step.rs +++ b/src_testbed/step.rs @@ -2,7 +2,7 @@ use crate::egui_text_gizmo::TextGizmos; use crate::gizmos::TestbedGizmos; use crate::instancing::InstanceMaterialData; use crate::startup::RigidParticlesTag; -use crate::{AppState, Callbacks, PhysicsContext, RenderContext, RunState, Timestamps}; +use crate::{AppState, Callbacks, PhysicsContext, RenderContext, Rendering, RunState, Timestamps}; use async_channel::{Receiver, Sender}; use bevy::prelude::*; use bevy::render::renderer::{RenderDevice, RenderQueue}; @@ -30,16 +30,17 @@ pub fn callbacks( mut text_gizmos: ResMut, ) { for callback in callbacks.0.iter_mut() { - let mut testbed_gizmos = TestbedGizmos::new(&mut text_gizmos); if callback.run_when_paused == false && app_state.run_state == RunState::Paused { continue; } (callback.callback)( - Some(&mut render), + Some(Rendering { + render: &mut render, + text_gizmos: TestbedGizmos::new(&mut text_gizmos), + }), &mut physics, timings.as_ref(), &app_state, - &mut testbed_gizmos, ); } } From 4cd0e81fd9c14f87ea7d4ee79e4383e040e0afb7 Mon Sep 17 00:00:00 2001 From: Thierry Berger Date: Mon, 31 Mar 2025 09:38:39 +0200 Subject: [PATCH 4/4] better licence compliance --- src_testbed/egui_text_gizmo.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src_testbed/egui_text_gizmo.rs b/src_testbed/egui_text_gizmo.rs index 55ad5ed..64ea794 100644 --- a/src_testbed/egui_text_gizmo.rs +++ b/src_testbed/egui_text_gizmo.rs @@ -1,9 +1,16 @@ //! This work is dual licensed under the Apache License v2.0 and the MIT license (SPDX: Apache-2.0, MIT). +//! +//! Original source: https://gist.github.com/jakkos-net/7f1d2806fae0288a11f3eb0840a11b04 +//! +//! Changes: +//! - plugin addition. +//! //! usage example: //! fn example_system(mut text_gizmos: ResMut) { //! text_gizmos.add("this is a text gizmo!", Vec3::ZERO, 10.0, Color::WHITE); //! } -//! remember to include [`TextGizmosPlugin`]: +//! +//! Remember to include [`TextGizmosPlugin`]: pub struct TextGizmosPlugin;