use crate::physics::{BodyHandle, CharacterMove, CharacterMoveInput, ColliderShape, LayerMask};
use crate::physics::sim::math::{Vec3, vec3};
use crate::physics::sim::query::{self, RayQuery, ShapeCast, ShapeCastHit};
use crate::physics::sim::scene::Scene;
use super::capsule::CharacterCapsule;
use super::config::CharacterConfig;
use super::slide;
const MAX_DEFLECTIONS: usize = 5;
const CONTACT_OFFSET: f32 = 5.0e-4;
const GROUND_PROBE: f32 = 0.02;
const MIN_MOTION: f32 = 1.0e-6;
const STEP_PROGRESS: f32 = 4.0 * CONTACT_OFFSET;
pub(crate) fn resolve(
scene: Scene<'_>,
config: &CharacterConfig,
capsule: &CharacterCapsule,
input: &CharacterMoveInput,
) -> CharacterMove {
let center = Vec3::from_array(input.center);
let desired = Vec3::from_array(input.desired);
if !is_finite(center) || !is_finite(desired) {
return CharacterMove {
translation: [0.0; 3],
grounded: false,
};
}
Mover {
scene,
config: *config,
shape: capsule.shape(),
radius: capsule.radius().abs(),
foot: capsule.half_height().abs() + capsule.radius().abs(),
exclude: Some(input.exclude),
mask: input.mask,
}
.run(center, desired)
}
struct Mover<'a> {
scene: Scene<'a>,
config: CharacterConfig,
shape: ColliderShape,
radius: f32,
foot: f32,
exclude: Option<BodyHandle>,
mask: LayerMask,
}
struct Step {
position: Vec3,
remaining: Vec3,
}
#[derive(Clone, Copy)]
struct Ground {
at: Vec3,
drop: f32,
}
impl Mover<'_> {
fn run(&self, center: Vec3, desired: Vec3) -> CharacterMove {
let footed = self.config.grounded && self.ground_below(center).is_some();
let mut position = center;
let mut remaining = desired;
let mut met: Option<Vec3> = None;
let mut stepped = false;
for _ in 0..MAX_DEFLECTIONS {
if remaining.length_squared() <= MIN_MOTION * MIN_MOTION {
break;
}
let Some(hit) = self.cast(position, remaining) else {
position += remaining;
break;
};
let normal = Vec3::from_array(hit.normal);
position += remaining * hit.toi + normal * clearance(hit.gap);
remaining = remaining * (1.0 - hit.toi);
let walkable = self.config.is_walkable(normal);
if !walkable
&& footed
&& !stepped
&& let Some(step) = self.step_over(position, remaining)
{
stepped = true;
position = step.position;
remaining = step.remaining;
met = None;
continue;
}
let slid = slide::deflect(remaining, normal, walkable);
remaining = match met {
Some(previous) if slide::re_entrant(slid, previous) => {
slide::crease(remaining, previous, normal)
}
_ => slid,
};
met = Some(normal);
}
let ground = self.ground_below(position);
let mut grounded = ground.is_some_and(|found| found.drop <= GROUND_PROBE);
if !grounded
&& footed
&& desired.y <= 0.0
&& let Some(found) = ground
{
position = found.at;
grounded = true;
}
CharacterMove {
translation: (position - center).to_array(),
grounded,
}
}
fn step_over(&self, position: Vec3, remaining: Vec3) -> Option<Step> {
let height = self.config.step_height;
let ahead = slide::horizontal(remaining);
let asked = ahead.length();
if height <= 0.0 || asked <= MIN_MOTION {
return None;
}
let room = height + CONTACT_OFFSET;
let lift = match self.cast(position, vec3(0.0, room, 0.0)) {
Some(hit) => room * hit.toi - CONTACT_OFFSET,
None => room,
};
if lift <= CONTACT_OFFSET {
return None;
}
let lifted = position + vec3(0.0, lift, 0.0);
let probe = ahead * ((asked + self.radius + CONTACT_OFFSET) / asked);
let advance = match self.cast(lifted, probe) {
Some(hit) => probe * hit.toi,
None => probe,
};
if advance.length_squared() <= STEP_PROGRESS * STEP_PROGRESS {
return None;
}
let landing = lifted + advance;
let stepped = self.set_down(landing, lift + CONTACT_OFFSET)?.at;
if stepped.y - position.y <= STEP_PROGRESS {
return None;
}
Some(Step {
position: stepped,
remaining: leftover(ahead, advance),
})
}
fn cast(&self, from: Vec3, motion: Vec3) -> Option<ShapeCastHit> {
query::shape_cast(
self.scene,
&ShapeCast {
shape: self.shape,
origin: from.to_array(),
euler_deg: [0.0; 3],
motion: motion.to_array(),
exclude: self.exclude,
mask: self.mask,
},
)
}
fn ground_below(&self, from: Vec3) -> Option<Ground> {
self.set_down(from, self.config.step_height.max(GROUND_PROBE))
}
fn set_down(&self, from: Vec3, reach: f32) -> Option<Ground> {
let hit = self.cast(from, vec3(0.0, -reach, 0.0))?;
let standing = self.config.is_walkable(Vec3::from_array(hit.normal))
|| self.ground_under_foot(from, reach);
standing.then(|| Ground {
at: settled(from, reach, &hit),
drop: reach * hit.toi,
})
}
fn ground_under_foot(&self, from: Vec3, reach: f32) -> bool {
query::raycast(
self.scene,
&RayQuery {
origin: from.to_array(),
dir: [0.0, -1.0, 0.0],
max_dist: self.foot + reach,
exclude: self.exclude,
mask: self.mask,
},
)
.is_some_and(|hit| self.config.is_walkable(Vec3::from_array(hit.normal)))
}
}
fn clearance(gap: f32) -> f32 {
if gap < 0.0 {
CONTACT_OFFSET - gap
} else {
CONTACT_OFFSET
}
}
fn settled(from: Vec3, reach: f32, hit: &ShapeCastHit) -> Vec3 {
let normal = Vec3::from_array(hit.normal);
from + vec3(0.0, -reach * hit.toi, 0.0) + normal * clearance(hit.gap)
}
fn leftover(ahead: Vec3, advance: Vec3) -> Vec3 {
let left = ahead.length() - advance.length();
if left <= MIN_MOTION {
return Vec3::ZERO;
}
ahead.normalize_or_zero() * left
}
fn is_finite(v: Vec3) -> bool {
v.x.is_finite() && v.y.is_finite() && v.z.is_finite()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_sweep_that_stopped_short_is_only_held_off_the_surface() {
assert_eq!(clearance(0.0), CONTACT_OFFSET);
assert_eq!(clearance(1.0e-5), CONTACT_OFFSET);
}
#[test]
fn a_move_that_began_inside_is_separated_by_what_it_overlaps() {
assert!((clearance(-0.1) - (0.1 + CONTACT_OFFSET)).abs() < 1.0e-6);
}
#[test]
fn a_step_that_reached_past_the_move_leaves_nothing_of_it() {
let ahead = vec3(0.0, 0.0, 0.05);
assert_eq!(leftover(ahead, vec3(0.0, 0.0, 0.35)), Vec3::ZERO);
assert_eq!(leftover(ahead, ahead), Vec3::ZERO);
let left = leftover(ahead, vec3(0.0, 0.0, 0.02));
assert!((left - vec3(0.0, 0.0, 0.03)).length() < 1.0e-6, "{left:?}");
}
#[test]
fn a_probe_leaves_the_mover_on_what_it_found() {
let hit = ShapeCastHit {
body: BodyHandle::from_parts(0, 0),
toi: 0.5,
point: [0.0; 3],
normal: [0.0, 1.0, 0.0],
gap: 0.0,
started_touching: false,
};
let landed = settled(vec3(0.0, 2.0, 0.0), 1.0, &hit);
assert!(
(landed.y - (1.5 + CONTACT_OFFSET)).abs() < 1.0e-6,
"{landed:?}"
);
}
}