oxijolt 1.1.0+jolt-5.6.0

Safe Rust API for Jolt Physics
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
//! Creation and update settings of a character.

use std::fmt;
use std::ptr::null;
use std::sync::Arc;

use oxijolt_sys::*;

use super::{InnerBody, INNER_BODY_INERTIA_RULE, INVALID_ID};
use crate::body::{has_finite_inverse, mass_properties};
use crate::limits;
use crate::math::{is_finite_non_negative, is_finite_positive};
use crate::{CharacterError, Shape, ShapeError, Vec3};

/// How to create a character, for [`PhysicsWorld::create_character`]. Build it with
/// [`new`](Self::new) and the setters.
///
/// The defaults are Jolt's `CharacterVirtualSettings` defaults. Lengths are in metres, angles in
/// radians, masses in kg and forces in newtons. The world gives every character its own id; it
/// is not a setting.
///
/// [`PhysicsWorld::create_character`]: crate::PhysicsWorld::create_character
#[derive(Clone)]
pub struct CharacterSettings<'a> {
    shape: CharacterShape<'a>,
    up: Vec3,
    supporting_volume_normal: Vec3,
    supporting_volume_constant: f32,
    max_slope_angle: f32,
    enhanced_internal_edge_removal: bool,
    pub(super) mass: f32,
    max_strength: f32,
    shape_offset: Vec3,
    back_face_collision: bool,
    predictive_contact_distance: f32,
    max_collision_iterations: u32,
    max_constraint_iterations: u32,
    min_time_remaining: f32,
    collision_tolerance: f32,
    character_padding: f32,
    max_num_hits: u32,
    hit_reduction_cos_max_angle: f32,
    penetration_recovery_speed: f32,
    pub(super) inner_body: Option<InnerBody<'a>>,
    pub(super) collide_with_characters: bool,
    pub(super) user_data: u64,
}

impl fmt::Debug for CharacterSettings<'_> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.debug_struct("CharacterSettings")
            .field("up", &self.up)
            .field("max_slope_angle", &self.max_slope_angle)
            .field("shape_offset", &self.shape_offset)
            .field("character_padding", &self.character_padding)
            .field("inner_body", &self.inner_body)
            .field("collide_with_characters", &self.collide_with_characters)
            .finish_non_exhaustive()
    }
}

/// The shape of a character's settings: borrowed from the caller, or owned by the settings and
/// shared by their clones.
#[derive(Clone)]
enum CharacterShape<'a> {
    Borrowed(&'a Shape),
    Owned(Arc<Shape>),
}

impl CharacterShape<'_> {
    fn get(&self) -> &Shape {
        match self {
            Self::Borrowed(shape) => shape,
            Self::Owned(shape) => shape,
        }
    }
}

/// Jolt's `DegreesToRadians(50.0f)`, the default maximum slope angle.
const DEFAULT_MAX_SLOPE_ANGLE: f32 = 50.0 * (std::f32::consts::PI / 180.0);

impl<'a> CharacterSettings<'a> {
    /// A character of `shape`, with Jolt's defaults for everything else.
    ///
    /// The shape must be convex (a capsule, sphere, box or cylinder):
    /// [`PhysicsWorld::create_character`] rejects other shapes, because the character moves by
    /// casting its shape, and Jolt's casts of compound or heightfield shapes find nothing.
    ///
    /// [`PhysicsWorld::create_character`]: crate::PhysicsWorld::create_character
    pub fn new(shape: &'a Shape) -> Self {
        Self::with_shape(CharacterShape::Borrowed(shape))
    }

    fn with_shape(shape: CharacterShape<'a>) -> Self {
        Self {
            shape,
            up: Vec3::new(0.0, 1.0, 0.0),
            supporting_volume_normal: Vec3::new(0.0, 1.0, 0.0),
            supporting_volume_constant: -1.0e10,
            max_slope_angle: DEFAULT_MAX_SLOPE_ANGLE,
            enhanced_internal_edge_removal: false,
            mass: 70.0,
            max_strength: 100.0,
            shape_offset: Vec3::ZERO,
            back_face_collision: true,
            predictive_contact_distance: 0.1,
            max_collision_iterations: 5,
            max_constraint_iterations: 15,
            min_time_remaining: 1.0e-4,
            collision_tolerance: 1.0e-3,
            character_padding: 0.02,
            max_num_hits: 256,
            hit_reduction_cos_max_angle: 0.999,
            penetration_recovery_speed: 1.0,
            inner_body: None,
            collide_with_characters: false,
            user_data: 0,
        }
    }

    /// The initial up direction, a unit vector. Default `+Y`. Change it per update with
    /// [`CharacterMut::set_up`](crate::CharacterMut::set_up).
    #[must_use]
    pub fn up(mut self, value: Vec3) -> Self {
        self.up = value;
        self
    }

    /// The plane, in the character's local space, that splits contacts that can support the
    /// character (below it) from the rest: points `p` with `normal · p + constant < 0` count.
    /// `normal` must be a unit vector. Default `+Y` and `-1e10`, so every contact counts.
    #[must_use]
    pub fn supporting_volume(mut self, normal: Vec3, constant: f32) -> Self {
        self.supporting_volume_normal = normal;
        self.supporting_volume_constant = constant;
        self
    }

    /// The steepest ground the character can stand on, radians in `[0, π/2]`. Default 50°.
    ///
    /// Jolt turns the slope limit off when its cosine is 0.9999 or above, that is for angles below
    /// about 0.81° (0.0141 rad): then no ground is too steep, so `0.0` means "no limit", not
    /// "flat ground only".
    #[must_use]
    pub fn max_slope_angle(mut self, radians: f32) -> Self {
        self.max_slope_angle = radians;
        self
    }

    /// Whether Jolt removes ghost contacts with internal edges of triangle shapes (heightfields,
    /// meshes). Costs extra CPU per contact. Default false.
    #[must_use]
    pub fn enhanced_internal_edge_removal(mut self, value: bool) -> Self {
        self.enhanced_internal_edge_removal = value;
        self
    }

    /// Mass in kg, between 0 and [`limits::MAX_MASS`], with which the character presses on what
    /// it stands on. Default 70. Each update also bounds the weight impulse it gives, see
    /// [`PhysicsWorld::update_character`](crate::PhysicsWorld::update_character).
    #[must_use]
    pub fn mass(mut self, value: f32) -> Self {
        self.mass = value;
        self
    }

    /// Largest force in newtons, at least 0, with which the character pushes bodies.
    /// Default 100.
    #[must_use]
    pub fn max_strength(mut self, value: f32) -> Self {
        self.max_strength = value;
        self
    }

    /// Offset of the shape from the character position, in the character's local space, every
    /// component at most [`limits::MAX_SHAPE_EXTENT`] in absolute value. Default zero.
    #[must_use]
    pub fn shape_offset(mut self, value: Vec3) -> Self {
        self.shape_offset = value;
        self
    }

    /// Whether the character collides with back-facing triangles, so it cannot pass through
    /// triangles from behind. Default true.
    #[must_use]
    pub fn back_face_collision(mut self, value: bool) -> Self {
        self.back_face_collision = value;
        self
    }

    /// How far beyond the shape to look for contacts, metres, between 0 and
    /// [`limits::MAX_SHAPE_EXTENT`]. Default 0.1.
    #[must_use]
    pub fn predictive_contact_distance(mut self, value: f32) -> Self {
        self.predictive_contact_distance = value;
        self
    }

    /// Most collision passes per update, at least 1. Default 5.
    #[must_use]
    pub fn max_collision_iterations(mut self, value: u32) -> Self {
        self.max_collision_iterations = value;
        self
    }

    /// Most constraint-solving passes per collision pass, at least 1. Default 15.
    #[must_use]
    pub fn max_constraint_iterations(mut self, value: u32) -> Self {
        self.max_constraint_iterations = value;
        self
    }

    /// Time in seconds, positive, below which an update stops moving. Default 1e-4.
    #[must_use]
    pub fn min_time_remaining(mut self, value: f32) -> Self {
        self.min_time_remaining = value;
        self
    }

    /// How far the character may penetrate geometry, metres, positive and at most
    /// [`limits::MAX_SHAPE_EXTENT`]. Default 1e-3.
    #[must_use]
    pub fn collision_tolerance(mut self, value: f32) -> Self {
        self.collision_tolerance = value;
        self
    }

    /// How far the character keeps away from geometry, metres, between 0 and
    /// [`limits::MAX_SHAPE_EXTENT`]. The shape sits this far above the character position along
    /// up. Default 0.02.
    #[must_use]
    pub fn character_padding(mut self, value: f32) -> Self {
        self.character_padding = value;
        self
    }

    /// Most contacts collected per query, at least 1. Default 256. Jolt sorts contacts
    /// deterministically only while fewer than this many are found; see
    /// [`CharacterRef::max_hits_exceeded`](crate::CharacterRef::max_hits_exceeded).
    #[must_use]
    pub fn max_num_hits(mut self, value: u32) -> Self {
        self.max_num_hits = value;
        self
    }

    /// Cosine of the largest angle between two contact normals that Jolt merges into one
    /// contact; -1 turns merging off. Default 0.999 (about 2.5°).
    #[must_use]
    pub fn hit_reduction_cos_max_angle(mut self, value: f32) -> Self {
        self.hit_reduction_cos_max_angle = value;
        self
    }

    /// Fraction in `[0, 1]` of a penetration resolved per update: 0 resolves nothing, 1 all of
    /// it at once. Default 1.
    #[must_use]
    pub fn penetration_recovery_speed(mut self, value: f32) -> Self {
        self.penetration_recovery_speed = value;
        self
    }

    /// The kinematic inner body, or none. Default none.
    ///
    /// Jolt creates the body with the character, moves it with the character and removes it
    /// with the character; it never collides with its own character. While the character
    /// exists, [`PhysicsWorld::remove_body`] refuses to remove it.
    ///
    /// [`PhysicsWorld::remove_body`]: crate::PhysicsWorld::remove_body
    #[must_use]
    pub fn inner_body(mut self, value: Option<InnerBody<'a>>) -> Self {
        self.inner_body = value;
        self
    }

    /// Whether the character collides with the other characters of its world that have this
    /// set. Default false.
    #[must_use]
    pub fn collide_with_characters(mut self, value: bool) -> Self {
        self.collide_with_characters = value;
        self
    }

    /// The caller's value for this character. Jolt also gives it to the inner body as its body
    /// user data. Default 0.
    #[must_use]
    pub fn user_data(mut self, value: u64) -> Self {
        self.user_data = value;
        self
    }

    pub(crate) fn validate(&self, object_layer_count: u32) -> Result<(), CharacterError> {
        let invalid = |what| Err(CharacterError::InvalidValue(what));
        if !is_unit(self.up) {
            return invalid(UP_RULE);
        }
        if !is_unit(self.supporting_volume_normal) {
            return invalid("supporting volume normal must be a finite unit vector");
        }
        if !self.supporting_volume_constant.is_finite() {
            return invalid("supporting volume constant must be finite");
        }
        if !(is_finite_non_negative(self.max_slope_angle)
            && self.max_slope_angle <= std::f32::consts::FRAC_PI_2)
        {
            return invalid("max slope angle must be between 0 and pi/2");
        }
        if !(0.0..=limits::MAX_MASS).contains(&self.mass) {
            return invalid("mass must be between 0 and limits::MAX_MASS");
        }
        if !is_finite_non_negative(self.max_strength) {
            return invalid("max strength must be finite and not negative");
        }
        if !limits::is_local_offset(self.shape_offset) {
            return invalid("shape offset must be finite and within limits::MAX_SHAPE_EXTENT");
        }
        if !limits::is_local_distance(self.predictive_contact_distance) {
            return invalid(
                "predictive contact distance must be between 0 and limits::MAX_SHAPE_EXTENT",
            );
        }
        if self.max_collision_iterations == 0 || self.max_constraint_iterations == 0 {
            return invalid("iteration counts must be at least 1");
        }
        if !is_finite_positive(self.min_time_remaining) {
            return invalid("min time remaining must be finite and positive");
        }
        if !(limits::is_local_distance(self.collision_tolerance) && self.collision_tolerance > 0.0)
        {
            return invalid(
                "collision tolerance must be positive and at most limits::MAX_SHAPE_EXTENT",
            );
        }
        if !limits::is_local_distance(self.character_padding) {
            return invalid("character padding must be between 0 and limits::MAX_SHAPE_EXTENT");
        }
        if self.max_num_hits == 0 {
            return invalid("max num hits must be at least 1");
        }
        if !self.hit_reduction_cos_max_angle.is_finite() {
            return invalid("hit reduction cos max angle must be finite");
        }
        if !is_recovery_speed(self.penetration_recovery_speed) {
            return invalid(RECOVERY_SPEED_RULE);
        }
        // SAFETY: the shape is live for the call; the getter only reads it.
        if unsafe { JPH_Shape_GetType(self.shape.get().as_ptr()) } != JPH_ShapeType_Convex {
            return invalid("the character shape must be convex");
        }
        if let Some(inner) = &self.inner_body {
            if inner.object_layer.get() >= object_layer_count {
                return invalid("inner body object layer does not exist in this world");
            }
            if inner.shape.must_be_static() {
                return invalid("inner body shape can only be used by static bodies");
            }
            // Jolt creates the inner body kinematic, which computes mass properties.
            if !has_finite_inverse(&mass_properties(inner.shape, None)) {
                return invalid(INNER_BODY_INERTIA_RULE);
            }
        }
        Ok(())
    }

    /// The joltc settings for a character with Jolt id `id`. The pointers borrow `self`.
    pub(super) fn to_jph(&self, id: u32) -> JPH_CharacterVirtualSettings {
        JPH_CharacterVirtualSettings {
            base: JPH_CharacterBaseSettings {
                up: self.up.to_jph(),
                supportingVolume: JPH_Plane {
                    normal: self.supporting_volume_normal.to_jph(),
                    distance: self.supporting_volume_constant,
                },
                maxSlopeAngle: self.max_slope_angle,
                enhancedInternalEdgeRemoval: self.enhanced_internal_edge_removal,
                shape: self.shape.get().as_ptr(),
            },
            ID: id,
            mass: self.mass,
            maxStrength: self.max_strength,
            shapeOffset: self.shape_offset.to_jph(),
            backFaceMode: if self.back_face_collision {
                JPH_BackFaceMode_CollideWithBackFaces
            } else {
                JPH_BackFaceMode_IgnoreBackFaces
            },
            predictiveContactDistance: self.predictive_contact_distance,
            maxCollisionIterations: self.max_collision_iterations,
            maxConstraintIterations: self.max_constraint_iterations,
            minTimeRemaining: self.min_time_remaining,
            collisionTolerance: self.collision_tolerance,
            characterPadding: self.character_padding,
            maxNumHits: self.max_num_hits,
            hitReductionCosMaxAngle: self.hit_reduction_cos_max_angle,
            penetrationRecoverySpeed: self.penetration_recovery_speed,
            innerBodyShape: self.inner_body.map_or(null(), |inner| inner.shape.as_ptr()),
            innerBodyIDOverride: INVALID_ID,
            innerBodyLayer: self.inner_body.map_or(0, |inner| inner.object_layer.get()),
        }
    }
}

impl CharacterSettings<'static> {
    /// A standing humanoid: a capsule `height` metres tall of `radius` whose bottom is at the
    /// character position, with Jolt's defaults for everything else. The settings own the
    /// capsule, and their clones share it.
    ///
    /// The capsule sits on the position through a [`shape_offset`](Self::shape_offset) of
    /// `(0, height / 2, 0)`, in the character's local space, so it follows the character's
    /// rotation. Jolt keeps the [`character_padding`](Self::character_padding) (0.02 m by
    /// default) between the shape and the ground, so a character on flat ground has its position
    /// that far above the ground.
    ///
    /// # Errors
    /// [`ShapeError::InvalidValue`] when `radius` is not finite and positive, when `height`
    /// is not finite and more than twice `radius`, or when the capsule is larger than
    /// [`Shape::new_capsule`] accepts.
    ///
    /// # Example
    /// ```
    /// use oxijolt::*;
    ///
    /// # fn main() -> Result<(), Box<dyn std::error::Error>> {
    /// let mut world = PhysicsWorld::new(WorldSettings::default())?;
    /// let floor = Shape::new_box(Vec3::new(10.0, 0.5, 10.0))?;
    /// let below = BodySettings::new_static().position(RVec3::new(0.0, -0.5, 0.0));
    /// world.create_body(&floor, &below)?;
    ///
    /// let settings = CharacterSettings::humanoid(1.8, 0.3)?;
    /// let id = world.create_character(&settings, RVec3::ZERO, Quat::IDENTITY)?;
    /// // A new character reports `InAir` until its contacts are first found.
    /// world.refresh_character_contacts(id, &QueryFilter::new())?;
    /// assert_eq!(world.character(id)?.ground_state(), GroundState::OnGround);
    /// # Ok(())
    /// # }
    /// ```
    pub fn humanoid(height: f32, radius: f32) -> Result<Self, ShapeError> {
        if !(radius.is_finite() && radius > 0.0) {
            return Err(ShapeError::InvalidValue(
                "humanoid radius must be finite and positive",
            ));
        }
        if !(height.is_finite() && height > 2.0 * radius) {
            return Err(ShapeError::InvalidValue(
                "humanoid height must be finite and more than twice its radius",
            ));
        }
        let capsule = Shape::new_capsule(height / 2.0 - radius, radius)?;
        let foot_offset = Vec3::new(0.0, height / 2.0, 0.0);
        Ok(Self::with_shape(CharacterShape::Owned(Arc::new(capsule))).shape_offset(foot_offset))
    }
}

/// What a character's up must satisfy ([`is_unit`]).
pub(super) const UP_RULE: &str = "up must be a finite unit vector";

/// What a penetration recovery speed must satisfy ([`is_recovery_speed`]).
pub(super) const RECOVERY_SPEED_RULE: &str = "penetration recovery speed must be between 0 and 1";

/// Whether `value` is a penetration recovery speed: finite and in `[0, 1]`.
pub(super) fn is_recovery_speed(value: f32) -> bool {
    is_finite_non_negative(value) && value <= 1.0
}

/// Whether `v` is finite and of unit length within Jolt's `Vec3::IsNormalized` tolerance
/// (`|length² − 1| <= 1e-5`).
pub(super) fn is_unit(v: Vec3) -> bool {
    v.is_finite() && (v.dot(v) - 1.0).abs() <= 1.0e-5
}

/// What an update does besides moving the character, for [`PhysicsWorld::update_character`]
/// (Jolt `CharacterVirtual::ExtendedUpdateSettings`). The defaults are Jolt's.
///
/// The vectors are in world space and assume `+Y` up by default; with another up the caller
/// supplies them along its up. A zero stick-to-floor or step-up vector turns that feature off
/// (Jolt's `IsNearZero`). Lengths are in metres.
///
/// [`PhysicsWorld::update_character`]: crate::PhysicsWorld::update_character
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ExtendedUpdateSettings {
    stick_to_floor_step_down: Vec3,
    walk_stairs_step_up: Vec3,
    walk_stairs_min_step_forward: f32,
    walk_stairs_step_forward_test: f32,
    walk_stairs_cos_angle_forward_contact: f32,
    walk_stairs_step_down_extra: Vec3,
}

impl Default for ExtendedUpdateSettings {
    fn default() -> Self {
        Self {
            stick_to_floor_step_down: Vec3::new(0.0, -0.5, 0.0),
            walk_stairs_step_up: Vec3::new(0.0, 0.4, 0.0),
            walk_stairs_min_step_forward: 0.02,
            walk_stairs_step_forward_test: 0.15,
            walk_stairs_cos_angle_forward_contact: (75.0 * (std::f32::consts::PI / 180.0_f32))
                .cos(),
            walk_stairs_step_down_extra: Vec3::ZERO,
        }
    }
}

impl ExtendedUpdateSettings {
    /// How far down the character looks for floor to stick to when it was supported before the
    /// update and is not after it, without moving up. Every component at most
    /// [`limits::MAX_SHAPE_EXTENT`] in absolute value. Default `(0, -0.5, 0)`.
    ///
    /// The [character study] measures where this holds a character on the floor, and where it
    /// does not (over crests, after a contact refresh).
    ///
    /// [character study]: https://github.com/pockerhead/oxijolt/blob/main/docs/character-study.md
    #[must_use]
    pub fn stick_to_floor_step_down(mut self, value: Vec3) -> Self {
        self.stick_to_floor_step_down = value;
        self
    }

    /// How high the character steps up when a step blocks it. Every component at most
    /// [`limits::MAX_SHAPE_EXTENT`] in absolute value. Default `(0, 0.4, 0)`.
    ///
    /// The highest step climbed is not this length: a capsule of radius `r` climbs about
    /// step-up + padding + `r (1 - cos max_slope_angle)`, and the step's own rounding changes it
    /// too, so measure it. Jolt judges a step by the surface normal at the contact; on a box with
    /// sharp edges (convex radius 0) the contact sits on the top edge, where float rounding
    /// decides between the top face's normal and the side's, so walk stairs climbs such a step
    /// unreliably. The [character study] measured step-up + 0.137 m for a capsule of radius 0.4
    /// with padding 0.02 on rounded steps.
    ///
    /// [character study]: https://github.com/pockerhead/oxijolt/blob/main/docs/character-study.md
    #[must_use]
    pub fn walk_stairs_step_up(mut self, value: Vec3) -> Self {
        self.walk_stairs_step_up = value;
        self
    }

    /// Least distance, between 0 and [`limits::MAX_SHAPE_EXTENT`], the character moves forward
    /// after stepping up. Default 0.02.
    #[must_use]
    pub fn walk_stairs_min_step_forward(mut self, value: f32) -> Self {
        self.walk_stairs_min_step_forward = value;
        self
    }

    /// How far ahead, between 0 and [`limits::MAX_SHAPE_EXTENT`], the character tests for floor
    /// after stepping up, so it does not step onto a slope it would slide off. Default 0.15.
    #[must_use]
    pub fn walk_stairs_step_forward_test(mut self, value: f32) -> Self {
        self.walk_stairs_step_forward_test = value;
        self
    }

    /// Cosine of the largest angle between the movement and a step's normal at which the
    /// character steps toward the step's normal instead. Default cos 75°.
    #[must_use]
    pub fn walk_stairs_cos_angle_forward_contact(mut self, value: f32) -> Self {
        self.walk_stairs_cos_angle_forward_contact = value;
        self
    }

    /// Extra distance the character moves down after stepping up, to stay on stairs that go
    /// down. Every component at most [`limits::MAX_SHAPE_EXTENT`] in absolute value. Default
    /// zero.
    #[must_use]
    pub fn walk_stairs_step_down_extra(mut self, value: Vec3) -> Self {
        self.walk_stairs_step_down_extra = value;
        self
    }

    pub(super) fn validate(&self) -> Result<(), CharacterError> {
        let offsets = limits::is_local_offset(self.stick_to_floor_step_down)
            && limits::is_local_offset(self.walk_stairs_step_up)
            && limits::is_local_offset(self.walk_stairs_step_down_extra);
        if !offsets {
            return Err(CharacterError::InvalidValue(
                "extended update steps must be finite and within limits::MAX_SHAPE_EXTENT",
            ));
        }
        if !self.walk_stairs_cos_angle_forward_contact.is_finite() {
            return Err(CharacterError::InvalidValue(
                "walk stairs cos angle forward contact must be finite",
            ));
        }
        if !(limits::is_local_distance(self.walk_stairs_min_step_forward)
            && limits::is_local_distance(self.walk_stairs_step_forward_test))
        {
            return Err(CharacterError::InvalidValue(
                "walk stairs forward distances must be between 0 and limits::MAX_SHAPE_EXTENT",
            ));
        }
        Ok(())
    }

    pub(super) fn to_jph(self) -> JPH_ExtendedUpdateSettings {
        JPH_ExtendedUpdateSettings {
            stickToFloorStepDown: self.stick_to_floor_step_down.to_jph(),
            walkStairsStepUp: self.walk_stairs_step_up.to_jph(),
            walkStairsMinStepForward: self.walk_stairs_min_step_forward,
            walkStairsStepForwardTest: self.walk_stairs_step_forward_test,
            walkStairsCosAngleForwardContact: self.walk_stairs_cos_angle_forward_contact,
            walkStairsStepDownExtra: self.walk_stairs_step_down_extra.to_jph(),
        }
    }
}