veloren_common_systems/phys/mod.rs
1use common::{
2 comp::{
3 Body, CharacterState, Collider, Density, Immovable, Mass, Ori, PhysicsState, Pos,
4 PosVelOriDefer, PreviousPhysCache, Projectile, Scale, Stats, Sticky, Vel,
5 body::ship::figuredata::VOXEL_COLLIDER_MANIFEST,
6 fluid_dynamics::{Fluid, Wings},
7 inventory::item::armor::Friction,
8 },
9 consts::{AIR_DENSITY, GRAVITY},
10 event::{EmitExt, EventBus, LandOnGroundEvent},
11 event_emitters,
12 link::Is,
13 mounting::{Rider, VolumeRider},
14 outcome::Outcome,
15 resources::{DeltaTime, GameMode, TimeOfDay},
16 states,
17 terrain::{CoordinateConversions, TerrainGrid},
18 uid::Uid,
19 util::{Projection, SpatialGrid},
20 weather::WeatherGrid,
21};
22use common_base::{prof_span, span};
23use common_ecs::{Job, Origin, ParMode, Phase, PhysicsMetrics, System};
24use rayon::iter::ParallelIterator;
25use specs::{
26 Entities, Join, LendJoin, ParJoin, Read, ReadExpect, ReadStorage, SystemData, Write,
27 WriteExpect, WriteStorage, shred,
28};
29use vek::*;
30
31mod collision;
32mod weather;
33use collision::ColliderData;
34
35/// The density of the fluid as a function of submersion ratio in given fluid
36/// where it is assumed that any unsubmersed part is is air.
37// TODO: Better suited partial submersion curve?
38fn fluid_density(height: f32, fluid: &Fluid) -> Density {
39 // If depth is less than our height (partial submersion), remove
40 // fluid density based on the ratio of displacement to full volume.
41 let immersion = fluid
42 .depth()
43 .map_or(1.0, |depth| (depth / height).clamp(0.0, 1.0));
44
45 Density(fluid.density().0 * immersion + AIR_DENSITY * (1.0 - immersion))
46}
47
48fn integrate_forces(
49 dt: &DeltaTime,
50 mut vel: Vel,
51 (body, wings): (&Body, Option<&Wings>),
52 density: &Density,
53 mass: &Mass,
54 fluid: &Fluid,
55 gravity: f32,
56 scale: Option<Scale>,
57) -> Vel {
58 let dim = body.dimensions();
59 let height = dim.z;
60 let rel_flow = fluid.relative_flow(&vel);
61 let fluid_density = fluid_density(height, fluid);
62 debug_assert!(mass.0 > 0.0);
63 debug_assert!(density.0 > 0.0);
64
65 // Aerodynamic/hydrodynamic forces
66 if !rel_flow.0.is_approx_zero() {
67 debug_assert!(!rel_flow.0.map(|a| a.is_nan()).reduce_or());
68 // HACK: We should really use the latter logic (i.e: `aerodynamic_forces`) for
69 // liquids, but it results in pretty serious dt-dependent problems that
70 // are extremely difficult to resolve. This is a compromise: for liquids
71 // only, we calculate drag using an incorrect (but still visually plausible)
72 // model that is much more resistant to differences in dt. Because it's
73 // physically incorrect anyway, there are magic coefficients that
74 // exist simply to get us closer to what water 'should' feel like.
75 if fluid.is_liquid() {
76 let fric = body
77 .drag_coefficient_liquid(fluid_density.0, scale.map_or(1.0, |s| s.0))
78 .powf(0.75)
79 * 0.02;
80
81 let fvel = fluid.flow_vel();
82
83 // Drag is relative to fluid velocity, so compensate before applying drag
84 vel.0 = (vel.0 - fvel.0) * (1.0 / (1.0 + fric)).powf(dt.0 * 10.0) + fvel.0;
85 } else {
86 let impulse = dt.0
87 * body.aerodynamic_forces(
88 &rel_flow,
89 fluid_density.0,
90 wings,
91 scale.map_or(1.0, |s| s.0),
92 );
93 debug_assert!(!impulse.map(|a| a.is_nan()).reduce_or());
94 if !impulse.is_approx_zero() {
95 let new_v = vel.0 + impulse / mass.0;
96 // If the new velocity is in the opposite direction, it's because the forces
97 // involved are too high for the current tick to handle. We deal with this by
98 // removing the component of our velocity vector along the direction of force.
99 // This way we can only ever lose velocity and will never experience a reverse
100 // in direction from events such as falling into water at high velocities.
101 if new_v.dot(vel.0) < 0.0 {
102 // Multiply by a factor to prevent full stop,
103 // as this can cause things to get stuck in high-density medium
104 vel.0 -= vel.0.projected(&impulse) * 0.9;
105 } else {
106 vel.0 = new_v;
107 }
108 }
109 }
110 debug_assert!(!vel.0.map(|a| a.is_nan()).reduce_or());
111 };
112
113 // Hydrostatic/aerostatic forces
114 // modify gravity to account for the effective density as a result of buoyancy
115 let down_force = dt.0 * gravity * (density.0 - fluid_density.0) / density.0;
116 vel.0.z -= down_force;
117
118 vel
119}
120
121fn calc_z_limit(char_state_maybe: Option<&CharacterState>, collider: &Collider) -> (f32, f32) {
122 let modifier = if char_state_maybe.is_some_and(|c_s| c_s.is_dodge() || c_s.is_glide()) {
123 0.5
124 } else {
125 1.0
126 };
127 collider.get_z_limits(modifier)
128}
129
130event_emitters! {
131 struct Events[Emitters] {
132 land_on_ground: LandOnGroundEvent,
133 }
134}
135
136/// This system applies forces and calculates new positions and velocities.
137#[derive(Default)]
138pub struct Sys;
139
140#[derive(SystemData)]
141pub struct PhysicsRead<'a> {
142 entities: Entities<'a>,
143 events: Events<'a>,
144 uids: ReadStorage<'a, Uid>,
145 terrain: ReadExpect<'a, TerrainGrid>,
146 dt: Read<'a, DeltaTime>,
147 game_mode: ReadExpect<'a, GameMode>,
148 scales: ReadStorage<'a, Scale>,
149 stickies: ReadStorage<'a, Sticky>,
150 immovables: ReadStorage<'a, Immovable>,
151 masses: ReadStorage<'a, Mass>,
152 colliders: ReadStorage<'a, Collider>,
153 is_riders: ReadStorage<'a, Is<Rider>>,
154 is_volume_riders: ReadStorage<'a, Is<VolumeRider>>,
155 projectiles: ReadStorage<'a, Projectile>,
156 character_states: ReadStorage<'a, CharacterState>,
157 bodies: ReadStorage<'a, Body>,
158 densities: ReadStorage<'a, Density>,
159 stats: ReadStorage<'a, Stats>,
160 weather: Option<Read<'a, WeatherGrid>>,
161 time_of_day: Read<'a, TimeOfDay>,
162}
163
164#[derive(SystemData)]
165pub struct PhysicsWrite<'a> {
166 physics_metrics: WriteExpect<'a, PhysicsMetrics>,
167 cached_spatial_grid: Write<'a, common::CachedSpatialGrid>,
168 physics_states: WriteStorage<'a, PhysicsState>,
169 positions: WriteStorage<'a, Pos>,
170 velocities: WriteStorage<'a, Vel>,
171 pos_vel_ori_defers: WriteStorage<'a, PosVelOriDefer>,
172 orientations: WriteStorage<'a, Ori>,
173 previous_phys_cache: WriteStorage<'a, PreviousPhysCache>,
174 outcomes: Read<'a, EventBus<Outcome>>,
175}
176
177#[derive(SystemData)]
178pub struct PhysicsData<'a> {
179 read: PhysicsRead<'a>,
180 write: PhysicsWrite<'a>,
181}
182
183impl PhysicsData<'_> {
184 /// Add/reset physics state components
185 fn reset(&mut self) {
186 span!(_guard, "Add/reset physics state components");
187 for (entity, _, _, _, _) in (
188 &self.read.entities,
189 &self.read.colliders,
190 &self.write.positions,
191 &self.write.velocities,
192 &self.write.orientations,
193 )
194 .join()
195 {
196 let _ = self
197 .write
198 .physics_states
199 .entry(entity)
200 .map(|e| e.or_insert_with(Default::default));
201 }
202 }
203
204 fn maintain_pushback_cache(&mut self) {
205 span!(_guard, "Maintain pushback cache");
206 // Add PreviousPhysCache for all relevant entities
207 for entity in (
208 &self.read.entities,
209 &self.read.colliders,
210 &self.write.velocities,
211 &self.write.positions,
212 !&self.write.previous_phys_cache,
213 )
214 .join()
215 .map(|(e, _, _, _, _)| e)
216 .collect::<Vec<_>>()
217 {
218 let _ = self
219 .write
220 .previous_phys_cache
221 .insert(entity, PreviousPhysCache {
222 velocity: Vec3::zero(),
223 velocity_dt: Vec3::zero(),
224 in_fluid: None,
225 center: Vec3::zero(),
226 collision_boundary: 0.0,
227 scale: 0.0,
228 scaled_radius: 0.0,
229 neighborhood_radius: 0.0,
230 origins: None,
231 pos: None,
232 ori: Quaternion::identity(),
233 });
234 }
235
236 // Update PreviousPhysCache
237 for (_, vel, position, ori, phys_state, phys_cache, collider, scale, cs) in (
238 &self.read.entities,
239 &self.write.velocities,
240 &self.write.positions,
241 &self.write.orientations,
242 &self.write.physics_states,
243 &mut self.write.previous_phys_cache,
244 &self.read.colliders,
245 self.read.scales.maybe(),
246 self.read.character_states.maybe(),
247 )
248 .join()
249 {
250 let scale = scale.map(|s| s.0).unwrap_or(1.0);
251 let z_limits = calc_z_limit(cs, collider);
252 let (z_min, z_max) = z_limits;
253 let (z_min, z_max) = (z_min * scale, z_max * scale);
254 let half_height = (z_max - z_min) / 2.0;
255
256 phys_cache.velocity_dt = vel.0 * self.read.dt.0;
257 phys_cache.velocity = vel.0;
258 phys_cache.in_fluid = phys_state.in_fluid;
259 let entity_center = position.0 + Vec3::new(0.0, 0.0, z_min + half_height);
260 let flat_radius = collider.bounding_radius() * scale;
261 let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt();
262
263 // Move center to the middle between OLD and OLD+VEL_DT
264 // so that we can reduce the collision_boundary.
265 phys_cache.center = entity_center + phys_cache.velocity_dt / 2.0;
266 phys_cache.collision_boundary = radius + (phys_cache.velocity_dt / 2.0).magnitude();
267 phys_cache.scale = scale;
268 phys_cache.scaled_radius = flat_radius;
269
270 let neighborhood_radius = match collider {
271 Collider::CapsulePrism { radius, .. } => radius * scale,
272 Collider::Voxel { .. } | Collider::Volume(_) | Collider::Point => flat_radius,
273 };
274 phys_cache.neighborhood_radius = neighborhood_radius;
275
276 let ori = ori.to_quat();
277 let origins = match collider {
278 Collider::CapsulePrism { p0, p1, .. } => {
279 let a = p1 - p0;
280 let len = a.magnitude();
281 // If origins are close enough, our capsule prism is cylinder
282 // with one origin which we don't even need to rotate.
283 //
284 // Other advantage of early-return is that we don't
285 // later divide by zero and return NaN
286 if len < f32::EPSILON * 10.0 {
287 Some((*p0, *p0))
288 } else {
289 // Apply orientation to origins of prism.
290 //
291 // We do this by building line between them,
292 // rotate it and then split back to origins.
293 // (Otherwise we will need to do the same with each
294 // origin).
295 //
296 // Cast it to 3d and then convert it back to 2d
297 // to apply quaternion.
298 let a = a.with_z(0.0);
299 let a = ori * a;
300 let a = a.xy();
301 // Previous operation could shrink x and y coordinates
302 // if orientation had Z parameter.
303 // Make sure we have the same length as before
304 // (and scale it, while we on it).
305 let a = a.normalized() * scale * len;
306 let p0 = -a / 2.0;
307 let p1 = a / 2.0;
308
309 Some((p0, p1))
310 }
311 },
312 Collider::Voxel { .. } | Collider::Volume(_) | Collider::Point => None,
313 };
314 phys_cache.origins = origins;
315 }
316 }
317
318 fn construct_spatial_grid(&mut self) -> SpatialGrid {
319 span!(_guard, "Construct spatial grid");
320 let &mut PhysicsData {
321 ref read,
322 ref write,
323 } = self;
324 // NOTE: i32 places certain constraints on how far out collision works
325 // NOTE: uses the radius of the entity and their current position rather than
326 // the radius of their bounding sphere for the current frame of movement
327 // because the nonmoving entity is what is collided against in the inner
328 // loop of the pushback collision code
329 // TODO: maintain frame to frame? (requires handling deletion)
330 // TODO: if not maintaining frame to frame consider counting entities to
331 // preallocate?
332 // TODO: assess parallelizing (overhead might dominate here? would need to merge
333 // the vecs in each hashmap)
334 let lg2_cell_size = 5;
335 let lg2_large_cell_size = 6;
336 let radius_cutoff = 8;
337 let mut spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff);
338 for (entity, pos, phys_cache, _, _) in (
339 &read.entities,
340 &write.positions,
341 &write.previous_phys_cache,
342 write.velocities.mask(),
343 !&read.projectiles, // Not needed because they are skipped in the inner loop below
344 )
345 .join()
346 {
347 // Note: to not get too fine grained we use a 2D grid for now
348 let radius_2d = phys_cache.scaled_radius.ceil() as u32;
349 let pos_2d = pos.0.xy().map(|e| e as i32);
350 const POS_TRUNCATION_ERROR: u32 = 1;
351 spatial_grid.insert(pos_2d, radius_2d + POS_TRUNCATION_ERROR, entity);
352 }
353
354 spatial_grid
355 }
356
357 fn apply_pushback(&mut self, job: &mut Job<Sys>, spatial_grid: &SpatialGrid) {
358 span!(_guard, "Apply pushback");
359 job.cpu_stats.measure(ParMode::Rayon);
360 let &mut PhysicsData {
361 ref read,
362 ref mut write,
363 } = self;
364 let (positions, previous_phys_cache) = (&write.positions, &write.previous_phys_cache);
365 let metrics = (
366 &read.entities,
367 positions,
368 &mut write.velocities,
369 previous_phys_cache,
370 &read.masses,
371 &read.colliders,
372 read.is_riders.maybe(),
373 read.is_volume_riders.maybe(),
374 read.stickies.maybe(),
375 read.immovables.maybe(),
376 &mut write.physics_states,
377 // TODO: if we need to avoid collisions for other things consider
378 // moving whether it should interact into the collider component
379 // or into a separate component.
380 read.projectiles.maybe(),
381 read.character_states.maybe(),
382 )
383 .par_join()
384 .map_init(
385 || {
386 prof_span!(guard, "physics e<>e rayon job");
387 guard
388 },
389 |_guard,
390 (
391 entity,
392 pos,
393 vel,
394 previous_cache,
395 mass,
396 collider,
397 is_rider,
398 is_volume_rider,
399 sticky,
400 immovable,
401 physics,
402 projectile,
403 char_state_maybe,
404 )| {
405 let is_sticky = sticky.is_some();
406 let is_immovable = immovable.is_some();
407 let is_mid_air = physics.on_surface().is_none();
408 let mut entity_entity_collision_checks = 0;
409 let mut entity_entity_collisions = 0;
410
411 // TODO: quick fix for bad performance. At extrememly high
412 // velocities use oriented rectangles at some threshold of
413 // displacement/radius to query the spatial grid and limit
414 // max displacement per tick somehow.
415 if previous_cache.collision_boundary > 128.0 {
416 return PhysicsMetrics {
417 entity_entity_collision_checks,
418 entity_entity_collisions,
419 };
420 }
421
422 let z_limits = calc_z_limit(char_state_maybe, collider);
423
424 // Resets touch_entities in physics
425 physics.touch_entities.clear();
426
427 let is_projectile = projectile.is_some();
428
429 let mut vel_delta = Vec3::zero();
430
431 let query_center = previous_cache.center.xy();
432 let query_radius = previous_cache.collision_boundary;
433
434 spatial_grid
435 .in_circle_aabr(query_center, query_radius)
436 .filter_map(|entity| {
437 let uid = read.uids.get(entity)?;
438 let pos = positions.get(entity)?;
439 let previous_cache = previous_phys_cache.get(entity)?;
440 let mass = read.masses.get(entity)?;
441 let collider = read.colliders.get(entity)?;
442
443 Some((
444 entity,
445 uid,
446 pos,
447 previous_cache,
448 mass,
449 collider,
450 read.character_states.get(entity),
451 read.is_riders.get(entity),
452 ))
453 })
454 .for_each(
455 |(
456 entity_other,
457 other,
458 pos_other,
459 previous_cache_other,
460 mass_other,
461 collider_other,
462 char_state_other_maybe,
463 other_is_rider_maybe,
464 )| {
465 let collision_boundary = previous_cache.collision_boundary
466 + previous_cache_other.collision_boundary;
467 if previous_cache
468 .center
469 .distance_squared(previous_cache_other.center)
470 > collision_boundary.powi(2)
471 || entity == entity_other
472 {
473 return;
474 }
475
476 let z_limits_other =
477 calc_z_limit(char_state_other_maybe, collider_other);
478
479 entity_entity_collision_checks += 1;
480
481 const MIN_COLLISION_DIST: f32 = 0.3;
482
483 let increments = ((previous_cache.velocity_dt
484 - previous_cache_other.velocity_dt)
485 .magnitude()
486 / MIN_COLLISION_DIST)
487 .max(1.0)
488 .ceil()
489 as usize;
490 let step_delta = 1.0 / increments as f32;
491
492 let mut collision_registered = false;
493
494 for i in 0..increments {
495 let factor = i as f32 * step_delta;
496 // We are not interested if collision succeed
497 // or no as of now.
498 // Collision reaction is done inside.
499 let _ = collision::resolve_e2e_collision(
500 // utility variables for our entity
501 &mut collision_registered,
502 &mut entity_entity_collisions,
503 factor,
504 physics,
505 char_state_maybe,
506 &mut vel_delta,
507 step_delta,
508 // physics flags
509 is_mid_air,
510 is_sticky,
511 is_immovable,
512 is_projectile,
513 // entity we colliding with
514 *other,
515 // symetrical collider context
516 ColliderData {
517 pos,
518 previous_cache,
519 z_limits,
520 collider,
521 mass: *mass,
522 },
523 ColliderData {
524 pos: pos_other,
525 previous_cache: previous_cache_other,
526 z_limits: z_limits_other,
527 collider: collider_other,
528 mass: *mass_other,
529 },
530 vel,
531 is_rider.is_some()
532 || is_volume_rider.is_some()
533 || other_is_rider_maybe.is_some(),
534 );
535 }
536 },
537 );
538
539 // Change velocity
540 vel.0 += vel_delta * read.dt.0;
541
542 // Metrics
543 PhysicsMetrics {
544 entity_entity_collision_checks,
545 entity_entity_collisions,
546 }
547 },
548 )
549 .reduce(PhysicsMetrics::default, |old, new| PhysicsMetrics {
550 entity_entity_collision_checks: old.entity_entity_collision_checks
551 + new.entity_entity_collision_checks,
552 entity_entity_collisions: old.entity_entity_collisions
553 + new.entity_entity_collisions,
554 });
555 write.physics_metrics.entity_entity_collision_checks =
556 metrics.entity_entity_collision_checks;
557 write.physics_metrics.entity_entity_collisions = metrics.entity_entity_collisions;
558 }
559
560 fn construct_voxel_collider_spatial_grid(&mut self) -> SpatialGrid {
561 span!(_guard, "Construct voxel collider spatial grid");
562 let &mut PhysicsData {
563 ref read,
564 ref write,
565 } = self;
566
567 let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
568
569 // NOTE: i32 places certain constraints on how far out collision works
570 // NOTE: uses the radius of the entity and their current position rather than
571 // the radius of their bounding sphere for the current frame of movement
572 // because the nonmoving entity is what is collided against in the inner
573 // loop of the pushback collision code
574 // TODO: optimize these parameters (especially radius cutoff)
575 let lg2_cell_size = 7; // 128
576 let lg2_large_cell_size = 8; // 256
577 let radius_cutoff = 64;
578 let mut spatial_grid = SpatialGrid::new(lg2_cell_size, lg2_large_cell_size, radius_cutoff);
579 // TODO: give voxel colliders their own component type
580 for (entity, pos, collider, scale, ori) in (
581 &read.entities,
582 &write.positions,
583 &read.colliders,
584 read.scales.maybe(),
585 &write.orientations,
586 )
587 .join()
588 {
589 let vol = collider.get_vol(&voxel_colliders_manifest);
590
591 if let Some(vol) = vol {
592 let sphere = collision::voxel_collider_bounding_sphere(vol, pos, ori, scale);
593 let radius = sphere.radius.ceil() as u32;
594 let pos_2d = sphere.center.xy().map(|e| e as i32);
595 const POS_TRUNCATION_ERROR: u32 = 1;
596 spatial_grid.insert(pos_2d, radius + POS_TRUNCATION_ERROR, entity);
597 }
598 }
599
600 spatial_grid
601 }
602
603 fn handle_movement_and_terrain(
604 &mut self,
605 job: &mut Job<Sys>,
606 voxel_collider_spatial_grid: &SpatialGrid,
607 ) {
608 let &mut PhysicsData {
609 ref read,
610 ref mut write,
611 } = self;
612
613 prof_span!(guard, "Apply Weather");
614 if let Some(weather) = &read.weather {
615 for (_, state, pos, phys) in (
616 &read.entities,
617 &read.character_states,
618 &write.positions,
619 &mut write.physics_states,
620 )
621 .join()
622 {
623 // Always reset air_vel to zero
624 let mut air_vel = Vec3::zero();
625
626 'simulation: {
627 // Don't simulate for non-gliding, for now
628 if !state.is_glide() {
629 break 'simulation;
630 }
631
632 let pos_2d = pos.0.as_().xy();
633 let chunk_pos: Vec2<i32> = pos_2d.wpos_to_cpos();
634 let Some(current_chunk) = &read.terrain.get_key(chunk_pos) else {
635 // oopsie
636 break 'simulation;
637 };
638
639 let meta = current_chunk.meta();
640
641 // Skip simulating for entites deeply under the ground
642 if pos.0.z < meta.alt() - 25.0 {
643 break 'simulation;
644 }
645
646 // If couldn't simulate wind for some reason, skip
647 if let Ok(simulated_vel) =
648 weather::simulated_wind_vel(pos, weather, &read.terrain, &read.time_of_day)
649 {
650 air_vel = simulated_vel
651 };
652 }
653
654 phys.in_fluid = phys.in_fluid.map(|f| match f {
655 Fluid::Air { elevation, .. } => Fluid::Air {
656 vel: Vel(air_vel),
657 elevation,
658 },
659 fluid => fluid,
660 });
661 }
662 }
663
664 drop(guard);
665
666 prof_span!(guard, "insert PosVelOriDefer");
667 // NOTE: keep in sync with join below
668 (
669 &read.entities,
670 read.colliders.mask(),
671 &write.positions,
672 &write.velocities,
673 &write.orientations,
674 write.orientations.mask(),
675 write.physics_states.mask(),
676 !&write.pos_vel_ori_defers, // This is the one we are adding
677 write.previous_phys_cache.mask(),
678 !&read.is_riders,
679 !&read.is_volume_riders,
680 )
681 .join()
682 .map(|t| (t.0, *t.2, *t.3, *t.4))
683 .collect::<Vec<_>>()
684 .into_iter()
685 .for_each(|(entity, pos, vel, ori)| {
686 let _ = write.pos_vel_ori_defers.insert(entity, PosVelOriDefer {
687 pos: Some(pos),
688 vel: Some(vel),
689 ori: Some(ori),
690 });
691 });
692 drop(guard);
693
694 // Apply movement inputs
695 span!(guard, "Apply movement");
696 let (positions, velocities) = (&write.positions, &mut write.velocities);
697
698 // First pass: update velocity using air resistance and gravity for each entity.
699 // We do this in a first pass because it helps keep things more stable for
700 // entities that are anchored to other entities (such as airships).
701 (
702 positions,
703 velocities,
704 read.stickies.maybe(),
705 &read.bodies,
706 read.character_states.maybe(),
707 &write.physics_states,
708 &read.masses,
709 &read.densities,
710 read.scales.maybe(),
711 !&read.is_riders,
712 !&read.is_volume_riders,
713 )
714 .par_join()
715 .for_each_init(
716 || {
717 prof_span!(guard, "velocity update rayon job");
718 guard
719 },
720 |_guard,
721 (
722 pos,
723 vel,
724 sticky,
725 body,
726 character_state,
727 physics_state,
728 mass,
729 density,
730 scale,
731 _,
732 _,
733 )| {
734 let in_loaded_chunk = read
735 .terrain
736 .contains_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32)));
737
738 // Apply physics only if in a loaded chunk
739 if in_loaded_chunk
740 // And not already stuck on a block (e.g., for arrows)
741 && !(physics_state.on_surface().is_some() && sticky.is_some())
742 // HACK: Special-case boats. Experimentally, clients are *bad* at making guesses about movement,
743 // and this is a particular problem for volume entities since careful control of velocity is
744 // required for nice movement of entities on top of the volume. Special-case volume entities here
745 // to prevent weird drag/gravity guesses messing with movement, relying on the client's hermite
746 // interpolation instead.
747 && !(matches!(body, Body::Ship(_)) && matches!(&*read.game_mode, GameMode::Client))
748 {
749 // Clamp dt to an effective 10 TPS, to prevent gravity
750 // from slamming the players into the floor when
751 // stationary if other systems cause the server
752 // to lag (as observed in the 0.9 release party).
753 let dt = DeltaTime(read.dt.0.min(0.1));
754
755 match physics_state.in_fluid {
756 None => {
757 vel.0.z -= dt.0 * GRAVITY;
758 },
759 Some(fluid) => {
760 let wings = match character_state {
761 Some(&CharacterState::Glide(states::glide::Data {
762 aspect_ratio,
763 planform_area,
764 ori,
765 ..
766 })) => Some(Wings {
767 aspect_ratio,
768 planform_area,
769 ori,
770 }),
771
772 _ => None,
773 };
774 vel.0 = integrate_forces(
775 &dt,
776 *vel,
777 (body, wings.as_ref()),
778 density,
779 mass,
780 &fluid,
781 GRAVITY,
782 scale.copied(),
783 )
784 .0
785 },
786 }
787 }
788 },
789 );
790 drop(guard);
791 job.cpu_stats.measure(ParMode::Single);
792
793 // Second pass: resolve collisions for terrain-like entities, this is required
794 // in order to update their positions before resolving collisions for
795 // non-terrain-like entities, since otherwise, collision is resolved
796 // based on where the terrain-like entity was in the previous frame.
797 Self::resolve_et_collision(job, read, write, voxel_collider_spatial_grid, true);
798
799 // Third pass: resolve collisions for non-terrain-like entities
800 Self::resolve_et_collision(job, read, write, voxel_collider_spatial_grid, false);
801
802 // Update cached 'old' physics values to the current values ready for the next
803 // tick
804 prof_span!(guard, "record ori into phys_cache");
805 for (pos, ori, previous_phys_cache) in (
806 &write.positions,
807 &write.orientations,
808 &mut write.previous_phys_cache,
809 )
810 .join()
811 {
812 // Note: updating ori with the rest of the cache values above was attempted but
813 // it did not work (investigate root cause?)
814 previous_phys_cache.pos = Some(*pos);
815 previous_phys_cache.ori = ori.to_quat();
816 }
817 drop(guard);
818 }
819
820 fn resolve_et_collision(
821 job: &mut Job<Sys>,
822 read: &PhysicsRead,
823 write: &mut PhysicsWrite,
824 voxel_collider_spatial_grid: &SpatialGrid,
825 terrain_like_entities: bool,
826 ) {
827 let (positions, velocities, previous_phys_cache, orientations) = (
828 &write.positions,
829 &write.velocities,
830 &write.previous_phys_cache,
831 &write.orientations,
832 );
833 span!(guard, "Apply terrain collision");
834 job.cpu_stats.measure(ParMode::Rayon);
835 let (land_on_grounds, outcomes) = (
836 &read.entities,
837 read.scales.maybe(),
838 read.stickies.maybe(),
839 &read.colliders,
840 positions,
841 velocities,
842 orientations,
843 read.bodies.maybe(),
844 read.character_states.maybe(),
845 &mut write.physics_states,
846 &mut write.pos_vel_ori_defers,
847 previous_phys_cache,
848 !&read.is_riders,
849 !&read.is_volume_riders,
850 )
851 .par_join()
852 .filter(|tuple| tuple.3.is_voxel() == terrain_like_entities)
853 .map_init(
854 || {
855 prof_span!(guard, "physics e<>t rayon job");
856 guard
857 },
858 |_guard,
859 (
860 entity,
861 scale,
862 sticky,
863 collider,
864 pos,
865 vel,
866 ori,
867 body,
868 character_state,
869 physics_state,
870 pos_vel_ori_defer,
871 previous_cache,
872 _,
873 _,
874 )| {
875 let mut land_on_ground = None;
876 let mut outcomes = Vec::new();
877 // Defer the writes of positions, velocities and orientations
878 // to allow an inner loop over terrain-like entities.
879 let old_vel = *vel;
880 let mut vel = *vel;
881 let old_ori = *ori;
882 let mut ori = *ori;
883
884 let scale = if collider.is_voxel() {
885 scale.map(|s| s.0).unwrap_or(1.0)
886 } else {
887 // TODO: Use scale & actual proportions when pathfinding is good
888 // enough to manage irregular entity sizes
889 1.0
890 };
891
892 if let Some(state) = character_state {
893 let footwear = state.footwear().unwrap_or(Friction::Normal);
894 if footwear != physics_state.footwear {
895 physics_state.footwear = footwear;
896 }
897 }
898
899 let in_loaded_chunk = read
900 .terrain
901 .contains_key(read.terrain.pos_key(pos.0.map(|e| e.floor() as i32)));
902
903 // Don't move if we're not in a loaded chunk
904 let pos_delta = if in_loaded_chunk {
905 vel.0 * read.dt.0
906 } else {
907 Vec3::zero()
908 };
909
910 // What's going on here?
911 // Because collisions need to be resolved against multiple
912 // colliders, this code takes the current position and
913 // propagates it forward according to velocity to find a
914 // 'target' position.
915 //
916 // This is where we'd ideally end up at the end of the tick,
917 // assuming no collisions. Then, we refine this target by
918 // stepping from the original position to the target for
919 // every obstacle, refining the target position as we go.
920 //
921 // It's not perfect, but it works pretty well in practice.
922 // Oddities can occur on the intersection between multiple
923 // colliders, but it's not like any game physics system
924 // resolves these sort of things well anyway.
925 // At the very least, we don't do things that result in glitchy
926 // velocities or entirely broken position snapping.
927 let mut tgt_pos = pos.0 + pos_delta;
928
929 let was_on_ground = physics_state.on_ground.is_some();
930 let block_snap =
931 body.is_some_and(|b| !matches!(b, Body::Object(_) | Body::Ship(_)));
932 let climbing =
933 character_state.is_some_and(|cs| matches!(cs, CharacterState::Climb(_)));
934
935 let friction_factor = |vel: Vec3<f32>| {
936 if let Some(Body::Ship(ship)) = body
937 && ship.has_wheels()
938 {
939 vel.try_normalized()
940 .and_then(|dir| {
941 Some(orientations.get(entity)?.right().dot(dir).abs())
942 })
943 .unwrap_or(1.0)
944 .max(0.2)
945 } else {
946 1.0
947 }
948 };
949
950 match &collider {
951 Collider::Voxel { .. } | Collider::Volume(_) => {
952 // For now, treat entities with voxel colliders
953 // as their bounding cylinders for the purposes of
954 // colliding them with terrain.
955 //
956 // Additionally, multiply radius by 0.1 to make
957 // the cylinder smaller to avoid lag.
958 let radius = collider.bounding_radius() * scale * 0.1;
959 let (_, z_max) = collider.get_z_limits(scale);
960 let z_min = 0.0;
961
962 let mut cpos = *pos;
963 let cylinder = (radius, z_min, z_max);
964 collision::box_voxel_collision(
965 cylinder,
966 &*read.terrain,
967 entity,
968 &mut cpos,
969 tgt_pos,
970 &mut vel,
971 physics_state,
972 &read.dt,
973 was_on_ground,
974 block_snap,
975 climbing,
976 |entity, vel, surface_normal| {
977 land_on_ground = Some((entity, vel, surface_normal))
978 },
979 read,
980 &ori,
981 friction_factor,
982 );
983 tgt_pos = cpos.0;
984 },
985 Collider::CapsulePrism {
986 z_min: _,
987 z_max,
988 p0: _,
989 p1: _,
990 radius: _,
991 } => {
992 // Scale collider
993 let radius = collider.bounding_radius().min(0.45) * scale;
994 let z_min = 0.0;
995 let z_max = z_max.clamped(1.2, 1.95) * scale;
996
997 let cylinder = (radius, z_min, z_max);
998 let mut cpos = *pos;
999 collision::box_voxel_collision(
1000 cylinder,
1001 &*read.terrain,
1002 entity,
1003 &mut cpos,
1004 tgt_pos,
1005 &mut vel,
1006 physics_state,
1007 &read.dt,
1008 was_on_ground,
1009 block_snap,
1010 climbing,
1011 |entity, vel, surface_normal| {
1012 land_on_ground = Some((entity, vel, surface_normal))
1013 },
1014 read,
1015 &ori,
1016 friction_factor,
1017 );
1018
1019 // Sticky things shouldn't move when on a surface
1020 if physics_state.on_surface().is_some() && sticky.is_some() {
1021 vel.0 = physics_state.ground_vel;
1022 }
1023
1024 tgt_pos = cpos.0;
1025 },
1026 Collider::Point => {
1027 let mut pos = *pos;
1028
1029 collision::point_voxel_collision(
1030 entity,
1031 &mut pos,
1032 pos_delta,
1033 &mut vel,
1034 physics_state,
1035 sticky.is_some(),
1036 &mut outcomes,
1037 read,
1038 );
1039
1040 tgt_pos = pos.0;
1041 },
1042 }
1043
1044 // Compute center and radius of tick path bounding sphere
1045 // for the entity for broad checks of whether it will
1046 // collide with a voxel collider
1047 let path_sphere = {
1048 // TODO: duplicated with maintain_pushback_cache,
1049 // make a common function to call to compute all this info?
1050 let z_limits = calc_z_limit(character_state, collider);
1051 let z_limits = (z_limits.0 * scale, z_limits.1 * scale);
1052 let half_height = (z_limits.1 - z_limits.0) / 2.0;
1053
1054 let entity_center = pos.0 + (z_limits.0 + half_height) * Vec3::unit_z();
1055 let path_center = entity_center + pos_delta / 2.0;
1056
1057 let flat_radius = collider.bounding_radius() * scale;
1058 let radius = (flat_radius.powi(2) + half_height.powi(2)).sqrt();
1059 let path_bounding_radius = radius + (pos_delta / 2.0).magnitude();
1060
1061 Sphere {
1062 center: path_center,
1063 radius: path_bounding_radius,
1064 }
1065 };
1066 // Collide with terrain-like entities
1067 let query_center = path_sphere.center.xy();
1068 let query_radius = path_sphere.radius;
1069
1070 let voxel_colliders_manifest = VOXEL_COLLIDER_MANIFEST.read();
1071
1072 voxel_collider_spatial_grid
1073 .in_circle_aabr(query_center, query_radius)
1074 .filter_map(|entity| {
1075 positions.get(entity).and_then(|pos| {
1076 Some((
1077 entity,
1078 pos,
1079 velocities.get(entity)?,
1080 previous_phys_cache.get(entity)?,
1081 read.colliders.get(entity)?,
1082 read.scales.get(entity),
1083 orientations.get(entity)?,
1084 ))
1085 })
1086 })
1087 .for_each(
1088 |(
1089 entity_other,
1090 pos_other,
1091 vel_other,
1092 previous_cache_other,
1093 collider_other,
1094 scale_other,
1095 ori_other,
1096 )| {
1097 if entity == entity_other {
1098 return;
1099 }
1100
1101 let voxel_collider =
1102 collider_other.get_vol(&voxel_colliders_manifest);
1103
1104 // use bounding cylinder regardless of our collider
1105 // TODO: extract point-terrain collision above to its own
1106 // function
1107 let radius = collider.bounding_radius();
1108 let (_, z_max) = collider.get_z_limits(1.0);
1109
1110 let radius = radius.min(0.45) * scale;
1111 let z_min = 0.0;
1112 let z_max = z_max.clamped(1.2, 1.95) * scale;
1113
1114 if let Some(voxel_collider) = voxel_collider {
1115 // TODO: cache/precompute sphere?
1116 let voxel_sphere = collision::voxel_collider_bounding_sphere(
1117 voxel_collider,
1118 pos_other,
1119 ori_other,
1120 scale_other,
1121 );
1122 // Early check
1123 if voxel_sphere.center.distance_squared(path_sphere.center)
1124 > (voxel_sphere.radius + path_sphere.radius).powi(2)
1125 {
1126 return;
1127 }
1128
1129 let mut physics_state_delta = physics_state.clone();
1130
1131 // Helper function for computing a transformation matrix and its
1132 // inverse. Should
1133 // be much cheaper than using `Mat4::inverted`.
1134 let from_to_matricies =
1135 |entity_rpos: Vec3<f32>, collider_ori: Quaternion<f32>| {
1136 (
1137 Mat4::<f32>::translation_3d(entity_rpos)
1138 * Mat4::from(collider_ori)
1139 * Mat4::scaling_3d(previous_cache_other.scale)
1140 * Mat4::translation_3d(
1141 voxel_collider.translation,
1142 ),
1143 Mat4::<f32>::translation_3d(
1144 -voxel_collider.translation,
1145 ) * Mat4::scaling_3d(
1146 1.0 / previous_cache_other.scale,
1147 ) * Mat4::from(collider_ori.inverse())
1148 * Mat4::translation_3d(-entity_rpos),
1149 )
1150 };
1151
1152 // Compute matrices that allow us to transform to and from the
1153 // coordinate space of
1154 // the collider. We have two variants of each, one for the
1155 // current state and one for
1156 // the previous state. This allows us to 'perfectly' track
1157 // change in position
1158 // between ticks, which prevents entities falling through voxel
1159 // colliders due to spurious
1160 // issues like differences in ping/variable dt.
1161 // TODO: Cache the matrices here to avoid recomputing for each
1162 // entity on them
1163 let (_transform_last_from, transform_last_to) =
1164 from_to_matricies(
1165 previous_cache_other.pos.unwrap_or(*pos_other).0
1166 - previous_cache.pos.unwrap_or(*pos).0,
1167 previous_cache_other.ori,
1168 );
1169 let (transform_from, transform_to) =
1170 from_to_matricies(pos_other.0 - pos.0, ori_other.to_quat());
1171
1172 // Compute the velocity of the collider, accounting for changes
1173 // in orientation
1174 // from the last tick. We then model this change as a change in
1175 // surface velocity
1176 // for the collider.
1177 let vel_other = {
1178 let pos_rel =
1179 (Mat4::<f32>::translation_3d(
1180 -voxel_collider.translation,
1181 ) * Mat4::from(ori_other.to_quat().inverse()))
1182 .mul_point(pos.0 - pos_other.0);
1183 let rpos_last =
1184 (Mat4::<f32>::from(previous_cache_other.ori)
1185 * Mat4::translation_3d(voxel_collider.translation))
1186 .mul_point(pos_rel);
1187 vel_other.0
1188 + (pos.0 - (pos_other.0 + rpos_last)) / read.dt.0
1189 };
1190
1191 {
1192 // Transform the entity attributes into the coordinate space
1193 // of the collider ready
1194 // for collision resolution
1195 let mut rpos =
1196 Pos(transform_last_to.mul_point(Vec3::zero()));
1197 vel.0 = previous_cache_other.ori.inverse()
1198 * (vel.0 - vel_other);
1199
1200 // Perform collision resolution
1201 collision::box_voxel_collision(
1202 (radius, z_min, z_max),
1203 &voxel_collider.volume(),
1204 entity,
1205 &mut rpos,
1206 transform_to.mul_point(tgt_pos - pos.0),
1207 &mut vel,
1208 &mut physics_state_delta,
1209 &read.dt,
1210 was_on_ground,
1211 block_snap,
1212 climbing,
1213 |entity, vel, surface_normal| {
1214 land_on_ground = Some((
1215 entity,
1216 Vel(previous_cache_other.ori * vel.0
1217 + vel_other),
1218 previous_cache_other.ori * surface_normal,
1219 ));
1220 },
1221 read,
1222 &ori,
1223 |vel| friction_factor(previous_cache_other.ori * vel),
1224 );
1225
1226 // Transform entity attributes back into world space now
1227 // that we've performed
1228 // collision resolution with them
1229 tgt_pos = transform_from.mul_point(rpos.0) + pos.0;
1230 vel.0 = previous_cache_other.ori * vel.0 + vel_other;
1231 }
1232
1233 // Collision resolution may also change the physics state. Since
1234 // we may be interacting
1235 // with multiple colliders at once (along with the regular
1236 // terrain!) we keep track
1237 // of a physics state 'delta' and try to sensibly resolve them
1238 // against one-another at each step.
1239 if physics_state_delta.on_ground.is_some() {
1240 // TODO: Do we need to do this? Perhaps just take the
1241 // ground_vel regardless?
1242 physics_state.ground_vel = previous_cache_other.ori
1243 * physics_state_delta.ground_vel
1244 + vel_other;
1245 }
1246 if physics_state_delta.on_surface().is_some() {
1247 // If the collision resulted in us being on a surface,
1248 // rotate us with the
1249 // collider. Really this should be modelled via friction or
1250 // something, but
1251 // our physics model doesn't really take orientation into
1252 // consideration.
1253 ori = ori.rotated(
1254 ori_other.to_quat()
1255 * previous_cache_other.ori.inverse(),
1256 );
1257 }
1258 physics_state.on_ground =
1259 physics_state.on_ground.or(physics_state_delta.on_ground);
1260 physics_state.on_ceiling |= physics_state_delta.on_ceiling;
1261 physics_state.on_wall = physics_state.on_wall.or_else(|| {
1262 physics_state_delta
1263 .on_wall
1264 .map(|dir| previous_cache_other.ori * dir)
1265 });
1266 physics_state.in_fluid = match (
1267 physics_state.in_fluid,
1268 physics_state_delta.in_fluid,
1269 ) {
1270 (Some(x), Some(y)) => x
1271 .depth()
1272 .and_then(|xh| {
1273 y.depth()
1274 .map(|yh| xh > yh)
1275 .unwrap_or(true)
1276 .then_some(x)
1277 })
1278 .or(Some(y)),
1279 (x @ Some(_), _) => x,
1280 (_, y @ Some(_)) => y,
1281 _ => None,
1282 };
1283 }
1284 },
1285 );
1286
1287 if tgt_pos != pos.0 {
1288 pos_vel_ori_defer.pos = Some(Pos(tgt_pos));
1289 } else {
1290 pos_vel_ori_defer.pos = None;
1291 }
1292 if vel != old_vel {
1293 pos_vel_ori_defer.vel = Some(vel);
1294 } else {
1295 pos_vel_ori_defer.vel = None;
1296 }
1297 if ori != old_ori {
1298 pos_vel_ori_defer.ori = Some(ori);
1299 } else {
1300 pos_vel_ori_defer.ori = None;
1301 }
1302
1303 (land_on_ground, outcomes)
1304 },
1305 )
1306 .fold(
1307 || (Vec::new(), Vec::new()),
1308 |(mut land_on_grounds, mut all_outcomes), (land_on_ground, mut outcomes)| {
1309 land_on_ground.map(|log| land_on_grounds.push(log));
1310 all_outcomes.append(&mut outcomes);
1311 (land_on_grounds, all_outcomes)
1312 },
1313 )
1314 .reduce(
1315 || (Vec::new(), Vec::new()),
1316 |(mut land_on_grounds_a, mut outcomes_a),
1317 (mut land_on_grounds_b, mut outcomes_b)| {
1318 land_on_grounds_a.append(&mut land_on_grounds_b);
1319 outcomes_a.append(&mut outcomes_b);
1320 (land_on_grounds_a, outcomes_a)
1321 },
1322 );
1323 drop(guard);
1324 job.cpu_stats.measure(ParMode::Single);
1325
1326 write.outcomes.emitter().emit_many(outcomes);
1327
1328 prof_span!(guard, "write deferred pos and vel");
1329 for (_, pos, vel, ori, pos_vel_ori_defer, _) in (
1330 &read.entities,
1331 &mut write.positions,
1332 &mut write.velocities,
1333 &mut write.orientations,
1334 &mut write.pos_vel_ori_defers,
1335 &read.colliders,
1336 )
1337 .join()
1338 .filter(|tuple| tuple.5.is_voxel() == terrain_like_entities)
1339 {
1340 if let Some(new_pos) = pos_vel_ori_defer.pos.take() {
1341 *pos = new_pos;
1342 }
1343 if let Some(new_vel) = pos_vel_ori_defer.vel.take() {
1344 *vel = new_vel;
1345 }
1346 if let Some(new_ori) = pos_vel_ori_defer.ori.take() {
1347 *ori = new_ori;
1348 }
1349 }
1350 drop(guard);
1351
1352 let mut emitters = read.events.get_emitters();
1353 emitters.emit_many(
1354 land_on_grounds
1355 .into_iter()
1356 .map(|(entity, vel, surface_normal)| LandOnGroundEvent {
1357 entity,
1358 vel: vel.0,
1359 surface_normal,
1360 }),
1361 );
1362 }
1363
1364 fn update_cached_spatial_grid(&mut self) {
1365 span!(_guard, "Update cached spatial grid");
1366 let &mut PhysicsData {
1367 ref read,
1368 ref mut write,
1369 } = self;
1370
1371 let spatial_grid = &mut write.cached_spatial_grid.0;
1372 spatial_grid.clear();
1373 (
1374 &read.entities,
1375 &write.positions,
1376 read.scales.maybe(),
1377 read.colliders.maybe(),
1378 )
1379 .join()
1380 .for_each(|(entity, pos, scale, collider)| {
1381 let scale = scale.map(|s| s.0).unwrap_or(1.0);
1382 let radius_2d =
1383 (collider.map(|c| c.bounding_radius()).unwrap_or(0.5) * scale).ceil() as u32;
1384 let pos_2d = pos.0.xy().map(|e| e as i32);
1385 const POS_TRUNCATION_ERROR: u32 = 1;
1386 spatial_grid.insert(pos_2d, radius_2d + POS_TRUNCATION_ERROR, entity);
1387 });
1388 }
1389}
1390
1391impl<'a> System<'a> for Sys {
1392 type SystemData = PhysicsData<'a>;
1393
1394 const NAME: &'static str = "phys";
1395 const ORIGIN: Origin = Origin::Common;
1396 const PHASE: Phase = Phase::Create;
1397
1398 fn run(job: &mut Job<Self>, mut physics_data: Self::SystemData) {
1399 physics_data.reset();
1400
1401 // Apply pushback
1402 //
1403 // Note: We now do this first because we project velocity ahead. This is slighty
1404 // imperfect and implies that we might get edge-cases where entities
1405 // standing right next to the edge of a wall may get hit by projectiles
1406 // fired into the wall very close to them. However, this sort of thing is
1407 // already possible with poorly-defined hitboxes anyway so it's not too
1408 // much of a concern.
1409 //
1410 // If this situation becomes a problem, this code should be integrated with the
1411 // terrain collision code below, although that's not trivial to do since
1412 // it means the step needs to take into account the speeds of both
1413 // entities.
1414 physics_data.maintain_pushback_cache();
1415
1416 let spatial_grid = physics_data.construct_spatial_grid();
1417 physics_data.apply_pushback(job, &spatial_grid);
1418
1419 let voxel_collider_spatial_grid = physics_data.construct_voxel_collider_spatial_grid();
1420 physics_data.handle_movement_and_terrain(job, &voxel_collider_spatial_grid);
1421
1422 // Spatial grid used by other systems
1423 physics_data.update_cached_spatial_grid();
1424 }
1425}