1mod geometry;
4mod motion;
5use ferravex_math::{Quat, Vec2, Vec3};
6pub use motion::{
7 Character, CharacterInput, CharacterStep, JumpState, Motion, Support, move_and_slide,
8};
9
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub enum CharacterShape {
12 Capsule {
13 radius: f32,
14 height: f32,
15 },
16 Box2 {
18 half: Vec2,
19 },
20}
21impl CharacterShape {
22 pub fn valid(self) -> bool {
23 match self {
24 Self::Capsule { radius, height } => {
25 radius.is_finite()
26 && height.is_finite()
27 && radius >= 0.001
28 && height >= radius * 2.
29 && height <= 1000.
30 }
31 Self::Box2 { half } => {
32 half.is_finite() && half.min_element() >= 0.001 && half.max_element() <= 1000.
33 }
34 }
35 }
36 pub fn half_extents(self) -> Vec3 {
37 match self {
38 Self::Capsule { radius, height } => Vec3::new(radius, height * 0.5, radius),
39 Self::Box2 { half } => Vec3::new(half.x, half.y, 0.),
40 }
41 }
42 fn planar(self) -> bool {
43 matches!(self, Self::Box2 { .. })
44 }
45 fn center(self, feet: Vec3) -> Vec3 {
46 feet + Vec3::Y * self.half_extents().y
47 }
48 pub fn bounds(self, feet: Vec3) -> QueryBounds {
49 let center = self.center(feet);
50 let half = self.half_extents();
51 QueryBounds {
52 min: center - half,
53 max: center + half,
54 }
55 }
56}
57
58#[derive(Clone, Copy, Debug)]
59pub struct QueryBounds {
60 pub min: Vec3,
61 pub max: Vec3,
62}
63impl QueryBounds {
64 pub fn intersects(self, other: Self) -> bool {
65 (0..3).all(|i| self.min[i] <= other.max[i] && self.max[i] >= other.min[i])
66 }
67 pub fn expanded(self, amount: f32) -> Self {
68 Self {
69 min: self.min - Vec3::splat(amount),
70 max: self.max + Vec3::splat(amount),
71 }
72 }
73 fn union(self, other: Self) -> Self {
74 Self {
75 min: self.min.min(other.min),
76 max: self.max.max(other.max),
77 }
78 }
79}
80#[derive(Clone, Copy, Debug)]
81pub enum ObstacleShape {
82 Box {
83 half: Vec3,
84 },
85 Ball {
86 radius: f32,
87 },
88 Capsule {
89 a: Vec3,
90 b: Vec3,
91 radius: f32,
92 },
93 Cylinder {
94 half_height: f32,
95 radius: f32,
96 },
97 Plane {
99 normal: Vec3,
100 },
101}
102#[derive(Clone, Copy, Debug)]
103pub struct Obstacle {
104 pub id: u64,
106 pub shape: ObstacleShape,
107 pub position: Vec3,
108 pub rotation: Quat,
109 pub membership: u32,
110 pub sensor: bool,
111}
112impl Obstacle {
113 pub fn cuboid(id: u64, position: Vec3, half: Vec3) -> Self {
114 Self {
115 id,
116 position,
117 shape: ObstacleShape::Box { half },
118 rotation: Quat::IDENTITY,
119 membership: u32::MAX,
120 sensor: false,
121 }
122 }
123 pub fn valid(self) -> bool {
124 if !bounded(self.position)
125 || !self.rotation.is_finite()
126 || (self.rotation.length_squared() - 1.).abs() > 0.001
127 {
128 return false;
129 }
130 let radius_valid = |r: f32| r.is_finite() && (0.001..=1000.).contains(&r);
131 match self.shape {
132 ObstacleShape::Box { half } => {
133 half.is_finite() && half.min_element() > 0. && half.max_element() <= 10000.
134 }
135 ObstacleShape::Ball { radius } => radius_valid(radius),
136 ObstacleShape::Capsule { a, b, radius } => {
137 bounded(a) && bounded(b) && (a - b).length() <= 2000. && radius_valid(radius)
138 }
139 ObstacleShape::Cylinder {
140 half_height,
141 radius,
142 } => radius_valid(half_height) && radius_valid(radius),
143 ObstacleShape::Plane { normal } => {
144 normal.is_finite() && (normal.length_squared() - 1.).abs() < 0.001
145 }
146 }
147 }
148 pub fn bounds(self) -> QueryBounds {
149 let (center, half) = match self.shape {
150 ObstacleShape::Box { half } => (
151 self.position,
152 (self.rotation * Vec3::X).abs() * half.x
153 + (self.rotation * Vec3::Y).abs() * half.y
154 + (self.rotation * Vec3::Z).abs() * half.z,
155 ),
156 ObstacleShape::Ball { radius } => (self.position, Vec3::splat(radius)),
157 ObstacleShape::Capsule { a, b, radius } => {
158 let a = self.position + self.rotation * a;
159 let b = self.position + self.rotation * b;
160 ((a + b) * 0.5, (a - b).abs() * 0.5 + Vec3::splat(radius))
161 }
162 ObstacleShape::Cylinder {
163 half_height,
164 radius,
165 } => {
166 let y = self.rotation * Vec3::Y;
167 (
168 self.position,
169 Vec3::new(
170 (1. - y.x * y.x).max(0.).sqrt(),
171 (1. - y.y * y.y).max(0.).sqrt(),
172 (1. - y.z * y.z).max(0.).sqrt(),
173 ) * radius
174 + y.abs() * half_height,
175 )
176 }
177 ObstacleShape::Plane { .. } => (Vec3::ZERO, Vec3::splat(2e6)),
178 };
179 QueryBounds {
180 min: center - half,
181 max: center + half,
182 }
183 }
184}
185pub trait Terrain {
189 fn visit(&self, bounds: QueryBounds, visitor: &mut dyn FnMut(Obstacle));
190 fn obstacle(&self, id: u64) -> Option<Obstacle>;
192}
193impl Terrain for [Obstacle] {
194 fn visit(&self, bounds: QueryBounds, visitor: &mut dyn FnMut(Obstacle)) {
195 for &o in self {
196 if !o.valid() || bounds.intersects(o.bounds()) {
197 visitor(o);
198 }
199 }
200 }
201 fn obstacle(&self, id: u64) -> Option<Obstacle> {
202 self.iter().find(|o| o.id == id).copied()
203 }
204}
205#[derive(Clone, Copy, Debug)]
206pub struct CharacterSettings {
207 pub skin: f32,
208 pub ground_snap: f32,
209 pub step_height: f32,
210 pub min_ground_normal: f32,
211 pub max_iterations: u32,
212 pub max_candidates: usize,
213 pub max_recovery: f32,
214 pub membership_mask: u32,
215 pub speed: f32,
216 pub ground_acceleration: f32,
217 pub air_acceleration: f32,
218 pub gravity: f32,
219 pub terminal_speed: f32,
220 pub jump_speed: f32,
221 pub coyote_time: f32,
222 pub jump_buffer: f32,
223}
224impl Default for CharacterSettings {
225 fn default() -> Self {
226 Self {
227 skin: 0.002,
228 ground_snap: 0.08,
229 step_height: 0.25,
230 min_ground_normal: 0.7,
231 max_iterations: 8,
232 max_candidates: 16384,
233 max_recovery: 1.,
234 membership_mask: u32::MAX,
235 speed: 7.,
236 ground_acceleration: 55.,
237 air_acceleration: 14.,
238 gravity: 24.,
239 terminal_speed: 50.,
240 jump_speed: 8.6,
241 coyote_time: 0.1,
242 jump_buffer: 0.12,
243 }
244 }
245}
246impl CharacterSettings {
247 pub fn valid(self) -> bool {
248 let nonnegative = |v: f32, max: f32| v.is_finite() && (0. ..=max).contains(&v);
249 self.skin.is_finite()
250 && (1e-5..=1.).contains(&self.skin)
251 && nonnegative(self.ground_snap, 100.)
252 && nonnegative(self.step_height, 100.)
253 && self.min_ground_normal.is_finite()
254 && (0.01..=1.).contains(&self.min_ground_normal)
255 && (1..=16).contains(&self.max_iterations)
256 && (1..=1_000_000).contains(&self.max_candidates)
257 && nonnegative(self.max_recovery, 1000.)
258 && nonnegative(self.speed, 100000.)
259 && nonnegative(self.ground_acceleration, 1e6)
260 && nonnegative(self.air_acceleration, 1e6)
261 && nonnegative(self.gravity, 1e6)
262 && nonnegative(self.terminal_speed, 100000.)
263 && nonnegative(self.jump_speed, 100000.)
264 && nonnegative(self.coyote_time, 0.5)
265 && nonnegative(self.jump_buffer, 0.5)
266 }
267}
268#[derive(Clone, Copy, Debug, PartialEq, Eq)]
269pub enum CharacterError {
270 InvalidInput,
271 InvalidObstacle,
272 QueryBudget,
273 SweepBudget,
274 UnresolvedPenetration,
275}
276impl std::fmt::Display for CharacterError {
277 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278 write!(f, "character controller: {self:?}")
279 }
280}
281impl std::error::Error for CharacterError {}
282fn bounded(p: Vec3) -> bool {
283 p.is_finite() && p.abs().max_element() <= 1e6
284}
285
286impl<B: crate::body::Access> Terrain for crate::world::World<B> {
289 fn visit(&self, bounds: QueryBounds, visitor: &mut dyn FnMut(Obstacle)) {
290 for (handle, collider) in self.colliders.iter() {
291 if let Some(o) = self.character_obstacle(handle, collider)
292 && bounds.intersects(o.bounds())
293 {
294 visitor(o);
295 }
296 }
297 }
298 fn obstacle(&self, id: u64) -> Option<Obstacle> {
299 let handle = crate::ColliderHandle {
300 index: (id & 0xffff_ffff) as usize,
301 generation: (id >> 32) as u32,
302 };
303 self.character_obstacle(handle, self.colliders.get(handle)?)
304 }
305}
306impl<B: crate::body::Access> crate::world::World<B> {
307 fn character_obstacle(
308 &self,
309 handle: crate::ColliderHandle,
310 collider: &crate::Collider,
311 ) -> Option<Obstacle> {
312 let core = self.bodies.get(collider.parent?)?.core();
313 let shape = match collider.shape {
314 crate::body::Shape::Ball(radius) => ObstacleShape::Ball { radius },
315 crate::body::Shape::Box(half) => ObstacleShape::Box { half },
316 #[cfg(feature = "dim2")]
317 crate::body::Shape::Capsule { a, b, radius } => ObstacleShape::Capsule { a, b, radius },
318 #[cfg(feature = "dim3")]
319 crate::body::Shape::Cylinder {
320 half_height,
321 radius,
322 } => ObstacleShape::Cylinder {
323 half_height,
324 radius,
325 },
326 };
327 let pose = collider.pose(core);
328 Some(Obstacle {
329 id: (u64::from(handle.generation) << 32) | handle.index as u64,
330 shape,
331 position: pose.position,
332 rotation: pose.rotation,
333 membership: collider.membership,
334 sensor: collider.sensor,
335 })
336 }
337}
338
339#[cfg(test)]
340mod tests;