veloren_common_systems/phys/
mod.rs

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