1use ferravex_core::{FixedClock, FrameStats};
3use ferravex_physics::d3::{PhysicsWorld, RigidBodyHandle};
4use ferravex_render::{
5 mesh::Mesh3d,
6 prelude::*,
7 shaders::{LitMaterial, LitSettings},
8 ui,
9};
10
11const MAX_BODIES: usize = 180;
12const CONCRETE: Color = Color::from_rgba(106, 116, 116, 255);
13const CRATE: Color = Color::from_rgba(173, 125, 68, 255);
14const PAINT: Color = Color::from_rgba(102, 143, 141, 255);
15
16struct Body {
17 handle: RigidBodyHandle,
18 ball: bool,
19 previous_position: Vec3,
20 previous_rotation: Quat,
21}
22
23struct Sandbox {
24 physics: PhysicsWorld,
25 bodies: Vec<Body>,
26 sequence: u32,
27}
28
29impl Sandbox {
30 fn new() -> Self {
31 let mut sandbox = Self {
32 physics: PhysicsWorld::new([0.0, -9.81, 0.0]),
33 bodies: Vec::new(),
34 sequence: 0,
35 };
36 sandbox
37 .physics
38 .add_box([0.0, -0.4, 0.0], [10.0, 0.4, 8.0], false);
39 for (position, half) in [
41 ([0.0, 0.25, -8.0], [10.25, 0.25, 0.25]),
42 ([0.0, 0.25, 8.0], [10.25, 0.25, 0.25]),
43 ([-10.0, 0.25, 0.0], [0.25, 0.25, 8.0]),
44 ([10.0, 0.25, 0.0], [0.25, 0.25, 8.0]),
45 ] {
46 sandbox.physics.add_box(position, half, false);
47 }
48 for x in [-9.4, 9.4] {
49 for z in [-7.4, 7.4] {
50 sandbox
51 .physics
52 .add_box([x, 0.4, z], [0.29, 0.4, 0.29], false);
53 }
54 }
55 for y in 0..5 {
57 for x in 0..3 {
58 for z in 0..2 {
59 sandbox.spawn(
60 vec3(
61 -1.8 + x as f32 * 1.22,
62 0.6 + y as f32 * 1.21,
63 -0.7 + z as f32 * 1.22,
64 ),
65 false,
66 );
67 }
68 }
69 }
70 for index in 0..5 {
71 sandbox.spawn(
72 vec3(-5.0 + index as f32 * 0.9, 7.5 + index as f32 * 0.45, 2.5),
73 true,
74 );
75 }
76 sandbox
77 }
78
79 fn spawn(&mut self, position: Vec3, ball: bool) {
80 if self.bodies.len() >= MAX_BODIES {
81 return;
82 }
83 let handle = if ball {
84 self.physics.add_ball(position.to_array(), 0.58, true)
85 } else {
86 self.physics.add_box(position.to_array(), [0.58; 3], true)
87 };
88 let collider = self.physics.bodies[handle].colliders()[0];
89 self.physics.colliders[collider].set_restitution(if ball { 0.45 } else { 0.05 });
90 self.physics.bodies[handle].enable_ccd(true);
91 self.bodies.push(Body {
92 handle,
93 ball,
94 previous_position: position,
95 previous_rotation: Quat::IDENTITY,
96 });
97 }
98
99 fn drop_body(&mut self, ball: bool) {
100 self.sequence = self.sequence.wrapping_add(1);
101 let phase = self.sequence as f32 * 2.399_963;
102 self.spawn(vec3(phase.cos() * 2.3, 10.0, phase.sin() * 2.3), ball);
103 }
104
105 fn step(&mut self, dt: f32) {
106 for body in &mut self.bodies {
107 let rigid_body = &self.physics.bodies[body.handle];
108 body.previous_position = Vec3::from_array(rigid_body.translation().to_array());
109 body.previous_rotation = Quat::from_array(rigid_body.rotation().to_array());
110 }
111 self.physics.step(dt);
112 }
113}
114
115fn window_conf() -> Conf {
116 ferravex_render::window_conf("Ferravex / low-poly physics laboratory")
117}
118
119ferravex_render::game!(window_conf, game);
120async fn game() {
121 let mut demo = ferravex_render::demo::Demo::new();
122 let mut sandbox = Sandbox::new();
123 let mut clock = FixedClock::new(120.0, 8);
124 let mut stats = FrameStats::<90>::default();
125 let material = LitMaterial::new()
126 .expect("The built-in Ferravex material must compile on the selected GPU backend");
127 let crate_mesh = Mesh3d::cuboid(vec3(1.16, 1.16, 1.16), CRATE);
128 let ball_mesh = Mesh3d::sphere(0.58, PAINT, 10, 6);
129 let floor_mesh = Mesh3d::cuboid(vec3(20.0, 0.8, 16.0), Color::from_rgba(71, 81, 86, 255));
130 let curb_x = Mesh3d::cuboid(vec3(20.5, 0.5, 0.5), CONCRETE);
131 let curb_z = Mesh3d::cuboid(vec3(0.5, 0.5, 16.0), CONCRETE);
132 let floor_mark = Mesh3d::cuboid(
133 vec3(0.12, 0.012, 14.0),
134 Color::from_rgba(152, 146, 117, 255),
135 );
136 let cross_mark = Mesh3d::cuboid(
137 vec3(18.0, 0.012, 0.12),
138 Color::from_rgba(152, 146, 117, 255),
139 );
140 let pedestal = Mesh3d::cuboid(vec3(0.55, 0.8, 0.55), Color::from_rgba(82, 92, 98, 255));
141 let marker = Mesh3d::cuboid(vec3(0.58, 0.13, 0.58), Color::from_rgba(208, 163, 74, 255));
142 let mut paused = false;
143 let mut yaw: f32 = 0.68;
144 let mut pitch: f32 = 0.48;
145 let mut radius: f32 = 25.0;
146 let mut previous_mouse = Vec2::from(mouse_position());
147 loop {
148 ui::warm_up_font();
149 stats.record(get_frame_time());
150 let pointer = Vec2::from(mouse_position());
151 let footer_y = screen_height() - 98.0;
152 let in_scene = pointer.y > 126.0 && pointer.y < footer_y;
153 if is_mouse_button_down(MouseButton::Left)
154 && !is_mouse_button_pressed(MouseButton::Left)
155 && in_scene
156 {
157 let delta = pointer - previous_mouse;
158 yaw -= delta.x * 0.006;
159 pitch = (pitch + delta.y * 0.004).clamp(0.12, 1.35);
160 }
161 previous_mouse = pointer;
162 if in_scene {
163 radius = (radius - mouse_wheel().1 * 0.8).clamp(12.0, 42.0);
164 }
165 let rotate_speed = get_frame_time() * 1.2;
166 if is_key_down(KeyCode::Left) {
167 yaw -= rotate_speed;
168 }
169 if is_key_down(KeyCode::Right) {
170 yaw += rotate_speed;
171 }
172 if is_key_down(KeyCode::Up) {
173 pitch = (pitch + rotate_speed).min(1.35);
174 }
175 if is_key_down(KeyCode::Down) {
176 pitch = (pitch - rotate_speed).max(0.12);
177 }
178 if is_key_pressed(KeyCode::Space) {
179 paused = !paused;
180 }
181 if is_key_pressed(KeyCode::R) {
182 sandbox = Sandbox::new();
183 clock.reset();
184 }
185 if is_key_pressed(KeyCode::Key1) {
186 sandbox.drop_body(false);
187 }
188 if is_key_pressed(KeyCode::Key2) {
189 sandbox.drop_body(true);
190 }
191 let frame = clock.advance(if paused { 0.0 } else { get_frame_time() as f64 });
192 for _ in 0..frame.steps {
193 sandbox.step(frame.dt);
194 }
195 let alpha = if paused { 1.0 } else { frame.alpha };
196 clear_background(ui::BACKGROUND);
197 let target = vec3(0.0, 2.2, 0.0);
198 let eye = target
199 + vec3(
200 yaw.sin() * pitch.cos(),
201 pitch.sin(),
202 yaw.cos() * pitch.cos(),
203 ) * radius;
204 set_camera(&Camera3D {
205 position: eye,
206 target,
207 up: Vec3::Y,
208 fovy: 43.0_f32.to_radians(),
209 z_near: 0.1,
210 z_far: 100.0,
211 ..Default::default()
212 });
213 material.begin(LitSettings {
214 camera_position: eye,
215 fog_start: 22.0,
216 fog_end: 65.0,
217 ambient: 0.48,
218 ..Default::default()
219 });
220 floor_mesh.draw(vec3(0.0, -0.4, 0.0), Quat::IDENTITY);
221 for z in [-8.0, 8.0] {
222 curb_x.draw(vec3(0.0, 0.25, z), Quat::IDENTITY);
223 }
224 for x in [-10.0, 10.0] {
225 curb_z.draw(vec3(x, 0.25, 0.0), Quat::IDENTITY);
226 }
227 for x in [-5.8, 5.8] {
228 floor_mark.draw(vec3(x, 0.008, 0.0), Quat::IDENTITY);
229 }
230 for z in [-4.5, 4.5] {
231 cross_mark.draw(vec3(0.0, 0.008, z), Quat::IDENTITY);
232 }
233 for x in [-9.4, 9.4] {
234 for z in [-7.4, 7.4] {
235 pedestal.draw(vec3(x, 0.4, z), Quat::IDENTITY);
236 marker.draw(vec3(x, 0.65, z), Quat::IDENTITY);
237 }
238 }
239 for body in &sandbox.bodies {
240 let rigid_body = &sandbox.physics.bodies[body.handle];
241 let position = body
242 .previous_position
243 .lerp(Vec3::from_array(rigid_body.translation().to_array()), alpha);
244 let rotation = body
245 .previous_rotation
246 .slerp(Quat::from_array(rigid_body.rotation().to_array()), alpha);
247 if body.ball {
248 ball_mesh.draw(position, rotation);
249 } else {
250 crate_mesh.draw(position, rotation);
251 }
252 }
253 material.end();
254 for body in &sandbox.bodies {
257 if body.ball {
258 continue;
259 }
260 let rigid_body = &sandbox.physics.bodies[body.handle];
261 let position = body
262 .previous_position
263 .lerp(Vec3::from_array(rigid_body.translation().to_array()), alpha);
264 let rotation = body
265 .previous_rotation
266 .slerp(Quat::from_array(rigid_body.rotation().to_array()), alpha);
267 let points = [
268 vec3(-0.582, -0.582, -0.582),
269 vec3(0.582, -0.582, -0.582),
270 vec3(0.582, 0.582, -0.582),
271 vec3(-0.582, 0.582, -0.582),
272 vec3(-0.582, -0.582, 0.582),
273 vec3(0.582, -0.582, 0.582),
274 vec3(0.582, 0.582, 0.582),
275 vec3(-0.582, 0.582, 0.582),
276 ]
277 .map(|v| position + rotation * v);
278 for (a, b) in [
279 (0, 1),
280 (1, 2),
281 (2, 3),
282 (3, 0),
283 (4, 5),
284 (5, 6),
285 (6, 7),
286 (7, 4),
287 (0, 4),
288 (1, 5),
289 (2, 6),
290 (3, 7),
291 ] {
292 draw_line_3d(points[a], points[b], Color::from_rgba(84, 64, 43, 255));
293 }
294 }
295 set_default_camera();
296 ui::header(
297 "3D RIGID BODIES",
298 "The loading yard.",
299 if screen_width() > 660.0 {
300 "Drag or use arrow keys to orbit / Scroll: zoom / 1, 2: drop bodies / Space: pause"
301 } else {
302 "Drag to orbit. Drop bodies below."
303 },
304 );
305 ui::panel(Rect::new(20.0, footer_y, screen_width() - 40.0, 78.0));
306 let button_width = ((screen_width() - 92.0) / 4.0).clamp(38.0, 90.0);
307 let button_rect = |index: f32| {
308 Rect::new(
309 34.0 + index * (button_width + 8.0),
310 footer_y + 20.0,
311 button_width,
312 36.0,
313 )
314 };
315 if ui::button(button_rect(0.0), "+ CRATE", false) {
316 sandbox.drop_body(false);
317 }
318 if ui::button(button_rect(1.0), "+ BALL", false) {
319 sandbox.drop_body(true);
320 }
321 if ui::button(
322 button_rect(2.0),
323 if paused { "RESUME" } else { "PAUSE" },
324 paused,
325 ) {
326 paused = !paused;
327 }
328 if ui::button(button_rect(3.0), "RESET", false) {
329 sandbox = Sandbox::new();
330 clock.reset();
331 }
332 if screen_width() > 760.0 {
333 let awake = sandbox
334 .bodies
335 .iter()
336 .filter(|b| !sandbox.physics.bodies[b.handle].is_sleeping())
337 .count();
338 ui::stat(
339 450.0,
340 footer_y + 25.0,
341 "DYNAMIC BODIES",
342 &format!("{} / {MAX_BODIES}", sandbox.bodies.len()),
343 );
344 ui::stat(625.0, footer_y + 25.0, "AWAKE", &awake.to_string());
345 }
346 if screen_width() > 940.0 {
347 ui::stat(
348 765.0,
349 footer_y + 25.0,
350 "FRAME AVERAGE",
351 &format!("{:.1} ms", stats.mean_ms()),
352 );
353 }
354 if screen_width() > 1100.0 {
355 ui::stat(955.0, footer_y + 25.0, "SHADING", "Flat + fog");
356 }
357 if demo.finish_frame() {
358 break;
359 }
360 next_frame().await;
361 }
362}
363
364#[cfg(test)]
365mod tests {
366 use super::*;
367
368 #[test]
369 fn recovered_sandbox_settles_without_floor_tunneling() {
370 let mut sandbox = Sandbox::new();
371 for _ in 0..960 {
372 sandbox.step(1.0 / 120.0);
373 for body in &sandbox.bodies {
374 let rigid = &sandbox.physics.bodies[body.handle];
375 assert!(rigid.translation().is_finite());
376 assert!(rigid.linvel().is_finite());
377 assert!(rigid.rotation().is_finite());
378 assert!(rigid.translation().y > 0.3);
379 }
380 }
381 assert!(
382 sandbox
383 .bodies
384 .iter()
385 .any(|body| sandbox.physics.bodies[body.handle].is_sleeping())
386 );
387 }
388
389 #[test]
390 fn dropping_is_bounded_and_rebuild_replaces_the_world() {
391 let mut sandbox = Sandbox::new();
392 for i in 0..MAX_BODIES + 10 {
393 sandbox.drop_body(i % 2 == 0);
394 }
395 assert_eq!(sandbox.bodies.len(), MAX_BODIES);
396 assert_eq!(sandbox.physics.bodies.len(), MAX_BODIES + 9);
397 sandbox = Sandbox::new();
398 assert_eq!(sandbox.bodies.len(), 35);
399 assert_eq!(sandbox.physics.bodies.len(), 44);
400 }
401}