summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2026-08-17 19:31:31 -0700
committerKai Stevenson <kai@kaistevenson.com>2026-08-17 19:31:31 -0700
commit4ad69d966f0640daae34c677eead5b689cbfac2e (patch)
treee80de1972ac02660b27ef6f46434caa388f41761 /src
parent7f1b0b16ad51afd46b4f8fe2ba209c4bd94391ba (diff)
debug physics
Diffstat (limited to 'src')
-rw-r--r--src/main.rs6
-rw-r--r--src/renderer/mod.rs145
-rw-r--r--src/renderer/ui.rs24
-rw-r--r--src/shader/debug.wgsl28
-rw-r--r--src/sim/rb_sim/debug_render.rs71
-rw-r--r--src/sim/rb_sim/mod.rs37
6 files changed, 306 insertions, 5 deletions
diff --git a/src/main.rs b/src/main.rs
index 043d125..3452065 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -25,7 +25,7 @@ use crate::{
sim::{
cell::{cell::Cell, materials::MaterialId},
cell_sim::{sim::sim_tick, world::World},
- rb_sim::RbSimManager,
+ rb_sim::{DebugRenderMode, RbSimManager},
write_rb_entity_to_world,
},
};
@@ -38,6 +38,8 @@ struct Config {
brush_material: MaterialId,
dropper_material: MaterialId,
use_threading: bool,
+ debug_render: bool,
+ debug_render_mode: DebugRenderMode,
}
struct Input {
@@ -230,6 +232,8 @@ impl Default for App {
brush_radius: 10.0,
brush_material: MaterialId::Sand,
dropper_material: MaterialId::Sand,
+ debug_render: true,
+ debug_render_mode: DebugRenderMode::default(),
},
diagnostics: Diagnostics {
fps: 0.0,
diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs
index 9bdbe46..7dd1802 100644
--- a/src/renderer/mod.rs
+++ b/src/renderer/mod.rs
@@ -10,7 +10,11 @@ use crate::{
camera::Camera,
config::{CELLS_IN_CHUNK, CHUNK_SIZE},
renderer::ui::draw_egui,
- sim::{cell::materials::MaterialId, cell_sim::world::World, rb_sim::RbSimManager},
+ sim::{
+ cell::materials::MaterialId,
+ cell_sim::world::World,
+ rb_sim::{RbSimManager, debug_render::DebugVertex},
+ },
};
// TODO derive from shared chunk config
@@ -18,6 +22,8 @@ const CHUNK_SLOTS: usize = 11 * 200;
const RB_ENTITY_SLOTS: usize = 64;
const CELL_SLOTS: usize = CHUNK_SLOTS + RB_ENTITY_SLOTS;
+const INITIAL_DEBUG_VERTEX_CAPACITY: usize = 4096;
+
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Instance {
@@ -38,6 +44,18 @@ fn srgb_to_linear(channel: u8) -> f32 {
}
}
+const DEBUG_VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 2] =
+ wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x4];
+
+fn create_debug_vertex_buffer(device: &wgpu::Device, capacity: usize) -> wgpu::Buffer {
+ device.create_buffer(&wgpu::BufferDescriptor {
+ label: Some("Debug vertex buffer"),
+ size: (capacity * size_of::<DebugVertex>()) as u64,
+ usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
+ mapped_at_creation: false,
+ })
+}
+
pub struct RendererState {
window: Arc<Window>,
surface: wgpu::Surface<'static>,
@@ -59,6 +77,12 @@ pub struct RendererState {
cell_buffer: wgpu::Buffer,
pixels_bind_group: wgpu::BindGroup,
+ // physics debug lines
+ debug_pipeline: wgpu::RenderPipeline,
+ debug_bind_group: wgpu::BindGroup,
+ debug_vertex_buffer: wgpu::Buffer,
+ debug_vertex_capacity: usize,
+
renderer_rb_entities: FxHashMap<u32, usize>,
}
@@ -293,6 +317,86 @@ impl RendererState {
],
});
+ let debug_bind_group_layout =
+ device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
+ label: Some("Debug bind group layout"),
+ entries: &[
+ // camera uniform
+ wgpu::BindGroupLayoutEntry {
+ ty: wgpu::BindingType::Buffer {
+ ty: wgpu::BufferBindingType::Uniform,
+ has_dynamic_offset: false,
+ min_binding_size: None,
+ },
+ binding: 0,
+ count: None,
+ visibility: wgpu::ShaderStages::VERTEX,
+ },
+ ],
+ });
+
+ let debug_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
+ label: Some("Debug bind group"),
+ layout: &debug_bind_group_layout,
+ entries: &[wgpu::BindGroupEntry {
+ binding: 0,
+ resource: camera_uniform_buffer.as_entire_binding(),
+ }],
+ });
+
+ let debug_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
+ label: Some("Debug shader"),
+ source: wgpu::ShaderSource::Wgsl(include_str!("../shader/debug.wgsl").into()),
+ });
+
+ let debug_pipeline_layout =
+ device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
+ label: Some("Debug pipeline layout"),
+ immediate_size: 0,
+ bind_group_layouts: &[Some(&debug_bind_group_layout)],
+ });
+
+ let debug_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+ label: Some("Debug pipeline"),
+ layout: Some(&debug_pipeline_layout),
+ vertex: wgpu::VertexState {
+ module: &debug_shader,
+ entry_point: Some("vs_main"),
+ buffers: &[wgpu::VertexBufferLayout {
+ array_stride: size_of::<DebugVertex>() as wgpu::BufferAddress,
+ step_mode: wgpu::VertexStepMode::Vertex,
+ attributes: &DEBUG_VERTEX_ATTRIBUTES,
+ }],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ },
+ fragment: Some(wgpu::FragmentState {
+ module: &debug_shader,
+ entry_point: Some("fs_main"),
+ targets: &[Some(wgpu::ColorTargetState {
+ format: config.format,
+ blend: Some(wgpu::BlendState::ALPHA_BLENDING),
+ write_mask: wgpu::ColorWrites::ALL,
+ })],
+ compilation_options: wgpu::PipelineCompilationOptions::default(),
+ }),
+ primitive: wgpu::PrimitiveState {
+ topology: wgpu::PrimitiveTopology::LineList,
+ strip_index_format: None,
+ front_face: wgpu::FrontFace::Ccw,
+ cull_mode: None,
+ polygon_mode: wgpu::PolygonMode::Fill,
+ unclipped_depth: false,
+ conservative: false,
+ },
+ depth_stencil: None,
+ multisample: wgpu::MultisampleState::default(),
+ multiview_mask: None,
+ cache: None,
+ });
+
+ let debug_vertex_capacity = INITIAL_DEBUG_VERTEX_CAPACITY;
+ let debug_vertex_buffer = create_debug_vertex_buffer(&device, debug_vertex_capacity);
+
RendererState {
window,
surface,
@@ -311,6 +415,11 @@ impl RendererState {
cell_buffer,
pixels_bind_group,
+ debug_pipeline,
+ debug_bind_group,
+ debug_vertex_buffer,
+ debug_vertex_capacity,
+
renderer_rb_entities: FxHashMap::default(),
}
}
@@ -327,7 +436,7 @@ impl RendererState {
pub fn render(
&mut self,
world: &mut World,
- rb_sim_manager: &RbSimManager,
+ rb_sim_manager: &mut RbSimManager,
camera: &mut Camera,
config: &mut Config,
diagnostics: &Diagnostics,
@@ -510,6 +619,31 @@ impl RendererState {
.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances));
}
+ let debug_vertex_count = if config.debug_render {
+ puffin::profile_scope!("Build physics debug lines");
+ let vertices = rb_sim_manager.debug_render(config.debug_render_mode);
+
+ if vertices.is_empty() {
+ 0
+ } else {
+ if vertices.len() > self.debug_vertex_capacity {
+ self.debug_vertex_capacity = vertices.len().next_power_of_two();
+ self.debug_vertex_buffer =
+ create_debug_vertex_buffer(&self.device, self.debug_vertex_capacity);
+ }
+
+ self.queue.write_buffer(
+ &self.debug_vertex_buffer,
+ 0,
+ bytemuck::cast_slice(vertices),
+ );
+
+ vertices.len() as u32
+ }
+ } else {
+ 0
+ };
+
{
puffin::profile_scope!("Main render pass");
let mut render_pass: wgpu::RenderPass<'_> =
@@ -538,6 +672,13 @@ impl RendererState {
render_pass.set_pipeline(&self.pixels_pipeline);
render_pass.set_bind_group(0, &self.pixels_bind_group, &[]);
render_pass.draw(0..4, 0..instances.len() as u32);
+
+ if debug_vertex_count > 0 {
+ render_pass.set_pipeline(&self.debug_pipeline);
+ render_pass.set_bind_group(0, &self.debug_bind_group, &[]);
+ render_pass.set_vertex_buffer(0, self.debug_vertex_buffer.slice(..));
+ render_pass.draw(0..debug_vertex_count, 0..1);
+ }
}
{
diff --git a/src/renderer/ui.rs b/src/renderer/ui.rs
index bf51ab4..a1ac31f 100644
--- a/src/renderer/ui.rs
+++ b/src/renderer/ui.rs
@@ -2,9 +2,18 @@ use egui::{Color32, Stroke, Ui, epaint::CircleShape};
use crate::{
Camera, Config, Diagnostics, Input,
- sim::{cell::materials::MaterialId, cell_sim::world::World},
+ sim::{cell::materials::MaterialId, cell_sim::world::World, rb_sim::DebugRenderMode},
};
+const DEBUG_RENDER_MODES: [(&str, DebugRenderMode); 6] = [
+ ("Collider shapes", DebugRenderMode::COLLIDER_SHAPES),
+ ("Collider AABBs", DebugRenderMode::COLLIDER_AABBS),
+ ("Rigid body axes", DebugRenderMode::RIGID_BODY_AXES),
+ ("Joints", DebugRenderMode::JOINTS),
+ ("Contacts", DebugRenderMode::CONTACTS),
+ ("Solver contacts", DebugRenderMode::SOLVER_CONTACTS),
+];
+
pub fn draw_egui<'a>(
ui: &mut Ui,
config: &mut Config,
@@ -73,6 +82,19 @@ pub fn draw_egui<'a>(
});
ui.checkbox(&mut config.use_threading, "Use multithreading");
+ ui.checkbox(&mut config.debug_render, "Draw physics debug lines");
+
+ if config.debug_render {
+ ui.indent("debug_render_modes", |ui| {
+ for (name, flag) in DEBUG_RENDER_MODES {
+ let mut enabled = config.debug_render_mode.contains(flag);
+ if ui.checkbox(&mut enabled, name).changed() {
+ config.debug_render_mode.set(flag, enabled);
+ }
+ }
+ });
+ }
+
ui.label(format!("Real FPS: {}", diagnostics.fps));
ui.heading("Camera");
ui.add(egui::Slider::new(&mut camera.zoom, 0.0..=10.0).text("Zoom"));
diff --git a/src/shader/debug.wgsl b/src/shader/debug.wgsl
new file mode 100644
index 0000000..256b969
--- /dev/null
+++ b/src/shader/debug.wgsl
@@ -0,0 +1,28 @@
+struct Camera {
+ scale: vec2f,
+ centre: vec2f,
+};
+
+@group(0) @binding(0) var<uniform> camera: Camera;
+
+struct VertexOutput {
+ @builtin(position) clip_position: vec4f,
+ @location(0) color: vec4f,
+};
+
+@vertex
+fn vs_main(
+ @location(0) world: vec2f,
+ @location(1) color: vec4f,
+) -> VertexOutput {
+ var out: VertexOutput;
+ out.clip_position = vec4f((world - camera.centre) * camera.scale, 0.0, 1.0);
+ out.color = color;
+
+ return out;
+}
+
+@fragment
+fn fs_main(in: VertexOutput) -> @location(0) vec4f {
+ return in.color;
+}
diff --git a/src/sim/rb_sim/debug_render.rs b/src/sim/rb_sim/debug_render.rs
new file mode 100644
index 0000000..342fc5b
--- /dev/null
+++ b/src/sim/rb_sim/debug_render.rs
@@ -0,0 +1,71 @@
+use rapier2d::pipeline::{DebugColor, DebugRenderBackend, DebugRenderObject};
+
+use crate::config::PIXELS_TO_METRES;
+
+#[repr(C)]
+#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
+pub struct DebugVertex {
+ pub position: [f32; 2],
+ pub color: [f32; 4],
+}
+
+#[derive(Default)]
+pub struct DebugLineBuffer {
+ pub vertices: Vec<DebugVertex>,
+}
+
+impl DebugRenderBackend for DebugLineBuffer {
+ fn draw_line(
+ &mut self,
+ _object: DebugRenderObject,
+ a: rapier2d::math::Vector,
+ b: rapier2d::math::Vector,
+ color: DebugColor,
+ ) {
+ let color = hsla_to_linear_rgba(color);
+ self.vertices.push(DebugVertex {
+ position: [a.x * PIXELS_TO_METRES, a.y * PIXELS_TO_METRES],
+ color,
+ });
+ self.vertices.push(DebugVertex {
+ position: [b.x * PIXELS_TO_METRES, b.y * PIXELS_TO_METRES],
+ color,
+ });
+ }
+}
+
+fn hsla_to_linear_rgba(hsla: DebugColor) -> [f32; 4] {
+ let [hue, saturation, lightness, alpha] = hsla;
+
+ let hue = hue.rem_euclid(360.0) / 60.0;
+ let saturation = saturation.clamp(0.0, 1.0);
+ let lightness = lightness.clamp(0.0, 1.0);
+
+ let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation;
+ let second = chroma * (1.0 - (hue % 2.0 - 1.0).abs());
+ let (r, g, b) = match hue as u32 {
+ 0 => (chroma, second, 0.0),
+ 1 => (second, chroma, 0.0),
+ 2 => (0.0, chroma, second),
+ 3 => (0.0, second, chroma),
+ 4 => (second, 0.0, chroma),
+ _ => (chroma, 0.0, second),
+ };
+ let m = lightness - chroma / 2.0;
+
+ [
+ srgb_to_linear(r + m),
+ srgb_to_linear(g + m),
+ srgb_to_linear(b + m),
+ alpha.clamp(0.0, 1.0),
+ ]
+}
+
+fn srgb_to_linear(channel: f32) -> f32 {
+ let channel = channel.clamp(0.0, 1.0);
+ if channel <= 0.04045 {
+ channel / 12.92
+ } else {
+ ((channel + 0.055) / 1.055).powf(2.4)
+ }
+}
diff --git a/src/sim/rb_sim/mod.rs b/src/sim/rb_sim/mod.rs
index 8101afd..48e4282 100644
--- a/src/sim/rb_sim/mod.rs
+++ b/src/sim/rb_sim/mod.rs
@@ -1,3 +1,4 @@
+pub mod debug_render;
pub mod rb_entity;
use fxhash::FxHashMap;
@@ -12,10 +13,15 @@ use crate::{
config::{CELLS_IN_CHUNK, CHUNK_SIZE, PHYSICS_DELTA_TIME, PIXELS_TO_METRES},
sim::{
cell::{cell::Cell, materials::MaterialId},
- rb_sim::rb_entity::RbEntity,
+ rb_sim::{
+ debug_render::{DebugLineBuffer, DebugVertex},
+ rb_entity::RbEntity,
+ },
},
};
+pub use rapier2d::pipeline::DebugRenderMode;
+
pub struct PhysicsManager {
rigid_body_set: prelude::RigidBodySet,
collider_set: prelude::ColliderSet,
@@ -27,6 +33,7 @@ pub struct PhysicsManager {
impulse_joint_set: prelude::ImpulseJointSet,
multibody_joint_set: prelude::MultibodyJointSet,
ccd_solver: prelude::CCDSolver,
+ debug_render_pipeline: prelude::DebugRenderPipeline,
}
impl PhysicsManager {
@@ -47,6 +54,14 @@ impl PhysicsManager {
impulse_joint_set: prelude::ImpulseJointSet::new(),
multibody_joint_set: prelude::MultibodyJointSet::new(),
ccd_solver: prelude::CCDSolver::new(),
+ debug_render_pipeline: prelude::DebugRenderPipeline::new(
+ prelude::DebugRenderStyle {
+ sleep_color_multiplier: [1.0; 4],
+ sleep_eligible_color_multiplier: [1.0; 4],
+ ..prelude::DebugRenderStyle::default()
+ },
+ prelude::DebugRenderMode::default(),
+ ),
}
}
}
@@ -55,6 +70,7 @@ pub struct RbSimManager {
physics_manager: PhysicsManager,
pub rb_entities: FxHashMap<u32, RbEntity>,
next_id: u32,
+ debug_line_buffer: DebugLineBuffer,
}
impl RbSimManager {
@@ -77,6 +93,24 @@ impl RbSimManager {
);
}
+ pub fn debug_render(&mut self, mode: DebugRenderMode) -> &[DebugVertex] {
+ puffin::profile_function!();
+
+ let physics = &mut self.physics_manager;
+ self.debug_line_buffer.vertices.clear();
+ physics.debug_render_pipeline.mode = mode;
+ physics.debug_render_pipeline.render(
+ &mut self.debug_line_buffer,
+ &physics.rigid_body_set,
+ &physics.collider_set,
+ &physics.impulse_joint_set,
+ &physics.multibody_joint_set,
+ &physics.narrow_phase,
+ );
+
+ &self.debug_line_buffer.vertices
+ }
+
pub fn create_rb_entity(
&mut self,
cells: Box<[Cell; CELLS_IN_CHUNK]>,
@@ -175,6 +209,7 @@ impl RbSimManager {
physics_manager: PhysicsManager::new(),
rb_entities: FxHashMap::default(),
next_id: 0,
+ debug_line_buffer: DebugLineBuffer::default(),
}
}
}