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
//! Settings of the vehicle kinds, in Jolt's vocabulary, with Jolt's defaults.
//!
//! The types here are plain Rust values. The world's vehicle creators validate them and build
//! the joltc settings objects from them, which they release again once the vehicle exists.

use std::f32::consts::PI;
use std::ops::Range;

use oxijolt_sys::*;

use crate::math::{is_finite_non_negative, is_unit};
use crate::owned::{JoltObject, Owned};
use crate::{PhysicsWorld, Vec3, VehicleError, VehicleType};

mod collision_tester;
mod drivetrain;
mod motorcycle;
mod presets;
mod tracked;
mod wheel;

pub use collision_tester::VehicleCollisionTester;
pub use drivetrain::{
    VehicleDifferentialSettings, VehicleEngineSettings, VehicleTransmissionSettings,
};
pub use motorcycle::MotorcycleSettings;
pub(crate) use tracked::ChassisMass;
pub use tracked::{TrackedVehicleSettings, TrackedWheelSettings, VehicleTrackSettings};
pub(crate) use wheel::WheelGeometry;
pub use wheel::{VehicleAntiRollBar, WheelSettings};

/// The settings of a wheeled vehicle (Jolt `VehicleConstraintSettings` with a
/// `WheeledVehicleControllerSettings`).
///
/// Directions are in the chassis body's local space. Wheels are indexed in the order given;
/// differentials and anti-roll bars refer to them by index. Jolt's samples, with up +Y and
/// forward +Z, put the left wheels at +X and the right wheels at −X; a differential's
/// `left_wheel` and an anti-roll bar's `left_wheel` name the wheel on the left in that sense.
#[derive(Clone, Debug, PartialEq)]
pub struct WheeledVehicleSettings {
    pub(crate) wheels: Vec<WheelSettings>,
    pub(crate) differentials: Vec<VehicleDifferentialSettings>,
    pub(crate) collision_tester: VehicleCollisionTester,
    pub(super) frame: VehicleFrame,
    anti_roll_bars: Vec<VehicleAntiRollBar>,
    engine: VehicleEngineSettings,
    transmission: VehicleTransmissionSettings,
    differential_limited_slip_ratio: f32,
}

impl WheeledVehicleSettings {
    /// A vehicle with `wheels`, the engine driving them through `differentials` (at least one
    /// is needed for the vehicle to move), and wheels that find the ground with
    /// `collision_tester`.
    pub fn new(
        wheels: Vec<WheelSettings>,
        differentials: Vec<VehicleDifferentialSettings>,
        collision_tester: VehicleCollisionTester,
    ) -> Self {
        Self {
            wheels,
            differentials,
            collision_tester,
            frame: VehicleFrame::default(),
            anti_roll_bars: Vec::new(),
            engine: VehicleEngineSettings::default(),
            transmission: VehicleTransmissionSettings::default(),
            differential_limited_slip_ratio: 1.4,
        }
    }

    /// Up of the vehicle in body space, a unit vector. Default +Y.
    #[must_use]
    pub fn up(mut self, value: Vec3) -> Self {
        self.frame.up = value;
        self
    }

    /// Forward of the vehicle in body space, a unit vector perpendicular to
    /// [`up`](Self::up). Default +Z.
    #[must_use]
    pub fn forward(mut self, value: Vec3) -> Self {
        self.frame.forward = value;
        self
    }

    /// Largest angle in radians, in `[0, π]`, between the vehicle's up and the world up (the
    /// opposite of gravity, or the last world up while gravity is zero; see
    /// [`VehicleRef::world_up`](crate::VehicleRef::world_up)) before a constraint keeps it from
    /// tilting further. π, the default, turns the limit off.
    #[must_use]
    pub fn max_pitch_roll_angle(mut self, radians: f32) -> Self {
        self.frame.max_pitch_roll_angle = radians;
        self
    }

    /// Anti-roll bars between pairs of wheels. Default none.
    #[must_use]
    pub fn anti_roll_bars(mut self, value: Vec<VehicleAntiRollBar>) -> Self {
        self.anti_roll_bars = value;
        self
    }

    /// The engine. Default [`VehicleEngineSettings::default`].
    #[must_use]
    pub fn engine(mut self, value: VehicleEngineSettings) -> Self {
        self.engine = value;
        self
    }

    /// The transmission. Default [`VehicleTransmissionSettings::default`].
    #[must_use]
    pub fn transmission(mut self, value: VehicleTransmissionSettings) -> Self {
        self.transmission = value;
        self
    }

    /// Ratio of the fastest to the slowest differential, measured at the clutch, above which
    /// all torque goes to the slowest one: a limited slip between differentials. Above 1;
    /// `f32::MAX` makes it open. Default 1.4.
    #[must_use]
    pub fn differential_limited_slip_ratio(mut self, value: f32) -> Self {
        self.differential_limited_slip_ratio = value;
        self
    }

    /// Checks every value Jolt asserts on or divides by, so none of Jolt's assertions can fire
    /// and no division by a setting can give a non-finite result, and the coefficients Jolt
    /// derives from the settings ([`validate_step_coefficients`](Self::validate_step_coefficients)).
    pub(crate) fn validate(&self, object_layer_count: u32) -> Result<(), VehicleError> {
        let invalid = |what| Err(VehicleError::InvalidValue(what));
        validate_wheel_count(self.wheels.len())?;
        self.frame.validate()?;
        if !is_limited_slip_ratio(self.differential_limited_slip_ratio) {
            return invalid(LIMITED_SLIP_RULE);
        }
        for wheel in &self.wheels {
            wheel.validate()?;
        }
        self.engine.validate()?;
        self.transmission.validate(&self.engine)?;
        self.validate_differentials()?;
        for bar in &self.anti_roll_bars {
            bar.validate(self.wheels.len())?;
        }
        self.validate_step_coefficients()?;
        self.collision_tester.validate(
            object_layer_count,
            self.wheels.iter().map(WheelGeometry::of),
        )
    }

    fn validate_differentials(&self) -> Result<(), VehicleError> {
        let invalid = |what| Err(VehicleError::InvalidValue(what));
        if self.differentials.is_empty() {
            return invalid("a vehicle needs at least one differential");
        }
        let wheel_count = self.wheels.len();
        let mut torque_ratio_sum = 0.0_f32;
        for differential in &self.differentials {
            differential.validate(wheel_count)?;
            torque_ratio_sum += differential.engine_torque_ratio;
        }
        // Jolt asserts that the ratios, summed in list order, are within 1e-6 of 1 on every
        // step (`WheeledVehicleController::PostCollide`).
        if (torque_ratio_sum - 1.0).abs() >= SUM_TOLERANCE {
            return invalid("differential engine torque ratios must add up to 1");
        }
        Ok(())
    }

    /// Checks that the coefficients the wheeled controller forms from the settings alone
    /// (`WheeledVehicleController::PostCollide`, `VehicleEngine::ApplyTorque`) are finite at
    /// every step [`PhysicsWorld::step`] accepts. Most grow with the step, so the largest step
    /// is their worst case; the brake-lock torque per rad/s of wheel speed, `inertia / step`,
    /// grows as the step shrinks, so the smallest step is its worst case. Values that are valid
    /// one by one can overflow together: a subnormal inertia makes `delta_time / inertia`
    /// infinite, huge gear and differential ratios make their product infinite. What the step
    /// computes from the vehicle's state afterwards is not bounded here.
    fn validate_step_coefficients(&self) -> Result<(), VehicleError> {
        let invalid = |what| Err(VehicleError::InvalidValue(what));
        let dt = PhysicsWorld::MAX_DELTA_TIME;
        let all_finite = |values: &[f32]| values.iter().all(|value| value.is_finite());
        for wheel in &self.wheels {
            let brake_impulse = dt * (wheel.max_brake_torque + wheel.max_hand_brake_torque);
            if !all_finite(&[
                dt / wheel.inertia,
                wheel.inertia / PhysicsWorld::MIN_DELTA_TIME,
                brake_impulse,
                brake_impulse / wheel.inertia,
                brake_impulse / wheel.base.radius,
                wheel.base.radius / wheel.inertia,
                wheel.inertia / wheel.base.radius,
            ]) {
                return invalid(
                    "wheel inertia, radius and brake torques give a non-finite step coefficient",
                );
            }
        }

        self.engine.validate_step_coefficients()?;
        let dt_div_ie = dt / self.engine.inertia;

        // The clutch couples the engine to every driven wheel through the gear ratio times the
        // differential ratio; each wheel gets at most all of the engine torque.
        let transmission = &self.transmission;
        let clutch = transmission.clutch_strength;
        let largest_gear_ratio = transmission
            .gear_ratios
            .iter()
            .chain(&transmission.reverse_gear_ratios)
            .map(|ratio| ratio.abs())
            .fold(0.0, f32::max);
        let clutch_to_wheel_ratio = |differential: &VehicleDifferentialSettings| {
            largest_gear_ratio * differential.differential_ratio
        };
        let largest_clutch_to_wheel_ratio = self
            .differentials
            .iter()
            .map(clutch_to_wheel_ratio)
            .fold(0.0, f32::max);
        if !all_finite(&[1.0 + dt_div_ie * clutch]) {
            return invalid(
                "engine inertia and clutch strength give a non-finite step coefficient",
            );
        }
        for differential in &self.differentials {
            let s_r = clutch * clutch_to_wheel_ratio(differential);
            let driven = [differential.left_wheel, differential.right_wheel];
            for wheel in driven.into_iter().flatten() {
                let dt_s_r_div_iw = dt * s_r / self.wheels[wheel as usize].inertia;
                if !all_finite(&[
                    s_r,
                    dt_div_ie * s_r,
                    dt_s_r_div_iw,
                    dt_s_r_div_iw * largest_clutch_to_wheel_ratio,
                ]) {
                    return invalid(
                        "clutch strength, ratios and wheel inertia give a non-finite step coefficient",
                    );
                }
            }
        }
        Ok(())
    }

    /// The joltc objects of these validated settings, for a wheeled vehicle.
    pub(crate) fn build(&self) -> BuiltSettings {
        self.build_with(ControllerGuard::Wheeled(self.create_controller()))
    }

    /// The joltc objects of these validated settings, with `controller` as the controller.
    pub(super) fn build_with(&self, controller: ControllerGuard) -> BuiltSettings {
        BuiltSettings {
            wheels: self
                .wheels
                .iter()
                .map(|wheel| WheelGuard::Wheeled(wheel.create()))
                .collect(),
            controller,
            anti_roll_bars: self
                .anti_roll_bars
                .iter()
                .map(|bar| JPH_VehicleAntiRollBar {
                    leftWheel: bar.left_wheel as i32,
                    rightWheel: bar.right_wheel as i32,
                    stiffness: bar.stiffness,
                })
                .collect(),
            frame: self.frame,
            collision_tester: self.collision_tester,
            geometry: self.wheels.iter().map(WheelGeometry::of).collect(),
            tracks: None,
        }
    }

    /// The joltc controller settings: engine, transmission and differentials.
    pub(crate) fn create_controller(&self) -> Owned<JPH_WheeledVehicleControllerSettings> {
        // SAFETY: Jolt is initialised (a world exists before any vehicle settings are built).
        // The settings are returned holding one reference, which the guard takes over.
        let controller = unsafe { Owned::from_raw(JPH_WheeledVehicleControllerSettings_Create()) }
            .unwrap_or_else(|| unreachable!("joltc `new`s the settings"));
        // SAFETY: the guard owns the new settings, which nothing else uses yet.
        unsafe { self.fill_wheeled_controller(controller.as_ptr()) };
        controller
    }

    /// Writes the engine, transmission and differentials of these validated settings into
    /// wheeled controller settings.
    ///
    /// # Safety
    /// `controller` points to live joltc wheeled controller settings (or settings derived from
    /// them with single inheritance) that nothing else uses during the call.
    pub(super) unsafe fn fill_wheeled_controller(
        &self,
        controller: *mut JPH_WheeledVehicleControllerSettings,
    ) {
        let transmission = self.transmission.create();
        let differentials: Vec<JPH_VehicleDifferentialSettings> = self
            .differentials
            .iter()
            .map(|differential| differential.to_jph())
            .collect();
        self.engine.with_jph(|engine| {
            // SAFETY: `controller` is live and unshared (contract); the transmission settings are
            // owned by their guard and the engine's curve lives for this closure. joltc copies
            // the engine (with its curve), the transmission and the differentials into the
            // controller settings. `differentials` holds as many entries as passed and lives for
            // the call. All values were validated.
            unsafe {
                JPH_WheeledVehicleControllerSettings_SetEngine(controller, engine);
                JPH_WheeledVehicleControllerSettings_SetTransmission(
                    controller,
                    transmission.as_ptr(),
                );
                JPH_WheeledVehicleControllerSettings_SetDifferentials(
                    controller,
                    differentials.as_ptr(),
                    differentials.len() as u32,
                );
                JPH_WheeledVehicleControllerSettings_SetDifferentialLimitedSlipRatio(
                    controller,
                    self.differential_limited_slip_ratio,
                );
            }
        });
    }
}

/// A vehicle's frame in its chassis' local space and its pitch and roll limit, shared by every
/// vehicle kind (Jolt `VehicleConstraintSettings::mUp`, `mForward`, `mMaxPitchRollAngle`).
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct VehicleFrame {
    pub(crate) up: Vec3,
    pub(crate) forward: Vec3,
    pub(crate) max_pitch_roll_angle: f32,
}

impl Default for VehicleFrame {
    /// Jolt's defaults: up +Y, forward +Z, no pitch and roll limit.
    fn default() -> Self {
        Self {
            up: Vec3::new(0.0, 1.0, 0.0),
            forward: Vec3::new(0.0, 0.0, 1.0),
            max_pitch_roll_angle: PI,
        }
    }
}

impl VehicleFrame {
    fn validate(&self) -> Result<(), VehicleError> {
        let invalid = |what| Err(VehicleError::InvalidValue(what));
        if !(is_unit(self.up) && is_unit(self.forward)) {
            return invalid("vehicle up and forward must be finite unit vectors");
        }
        if self.up.dot(self.forward).abs() > PERPENDICULAR_TOLERANCE {
            return invalid("vehicle up and forward must be perpendicular");
        }
        if !(is_finite_non_negative(self.max_pitch_roll_angle) && self.max_pitch_roll_angle <= PI) {
            return invalid(MAX_PITCH_ROLL_RULE);
        }
        Ok(())
    }
}

/// Checks that a vehicle with `count` wheels can be built: at least one wheel, and indices that
/// fit Jolt's `uint` and joltc's `int` wheel indices.
fn validate_wheel_count(count: usize) -> Result<(), VehicleError> {
    if count == 0 {
        return Err(VehicleError::InvalidValue(
            "a vehicle needs at least one wheel",
        ));
    }
    if u32::try_from(count).is_err() || i32::try_from(count).is_err() {
        return Err(VehicleError::InvalidValue("too many wheels"));
    }
    Ok(())
}

/// One wheel's joltc settings, of the kind its vehicle's controller expects.
pub(crate) enum WheelGuard {
    /// The wheel of a wheeled vehicle or a motorcycle.
    Wheeled(Owned<JPH_WheelSettingsWV>),
    /// The wheel of a tracked vehicle.
    Tracked(Owned<JPH_WheelSettingsTV>),
}

impl WheelGuard {
    /// The settings as joltc's base wheel settings, still owned by the guard.
    pub(crate) fn as_base(&self) -> *mut JPH_WheelSettings {
        match self {
            // Both kinds are `WheelSettings` with single inheritance.
            Self::Wheeled(wheel) => wheel.as_ptr().cast(),
            Self::Tracked(wheel) => wheel.as_ptr().cast(),
        }
    }
}

/// A vehicle controller's joltc settings.
pub(crate) enum ControllerGuard {
    /// A wheeled vehicle's controller.
    Wheeled(Owned<JPH_WheeledVehicleControllerSettings>),
    /// A tracked vehicle's controller.
    Tracked(Owned<JPH_TrackedVehicleControllerSettings>),
    /// A motorcycle's controller.
    Motorcycle(Owned<JPH_MotorcycleControllerSettings>),
}

impl ControllerGuard {
    /// The settings as joltc's base controller settings, still owned by the guard.
    pub(crate) fn as_base(&self) -> *mut JPH_VehicleControllerSettings {
        match self {
            // Every controller's settings derive from `VehicleControllerSettings` with single
            // inheritance.
            Self::Wheeled(controller) => controller.as_ptr().cast(),
            Self::Tracked(controller) => controller.as_ptr().cast(),
            Self::Motorcycle(controller) => controller.as_ptr().cast(),
        }
    }

    /// The kind of vehicle these settings make.
    pub(crate) fn kind(&self) -> VehicleType {
        match self {
            Self::Wheeled(_) => VehicleType::Wheeled,
            Self::Tracked(_) => VehicleType::Tracked,
            Self::Motorcycle(_) => VehicleType::Motorcycle,
        }
    }
}

/// The joltc objects a vehicle is created from, with what the world keeps of the settings. The
/// guards release the world's references once `JPH_VehicleConstraint_Create` has taken its own.
pub(crate) struct BuiltSettings {
    pub(crate) wheels: Vec<WheelGuard>,
    pub(crate) controller: ControllerGuard,
    pub(crate) anti_roll_bars: Vec<JPH_VehicleAntiRollBar>,
    pub(crate) frame: VehicleFrame,
    pub(crate) collision_tester: VehicleCollisionTester,
    pub(crate) geometry: Vec<WheelGeometry>,
    /// The vehicle wheel indices of a tracked vehicle's left and right track.
    pub(crate) tracks: Option<[Range<u32>; 2]>,
}

/// Jolt's `VehicleEngine::cAngularVelocityToRPM`.
const ANGULAR_VELOCITY_TO_RPM: f32 = 60.0 / (2.0 * PI);

/// Tolerance of the perpendicularity checks on unit vectors, `|a·b|` at most this.
const PERPENDICULAR_TOLERANCE: f32 = 1.0e-3;

/// Tolerance of the engine torque ratio sum, `|sum − 1|` below this, half of Jolt's 1e-6.
const SUM_TOLERANCE: f32 = 5.0e-7;

/// What a vehicle's max pitch/roll angle must satisfy: finite, `0..=pi`.
pub(super) const MAX_PITCH_ROLL_RULE: &str = "max pitch roll angle must be between 0 and pi";

/// What a differential's limited slip ratio must satisfy ([`is_limited_slip_ratio`]).
const LIMITED_SLIP_RULE: &str = "differential limited slip ratio must be finite and above 1";

/// Whether `value` is a valid limited slip ratio: finite and above 1 (`f32::MAX` is open).
fn is_limited_slip_ratio(value: f32) -> bool {
    value.is_finite() && value > 1.0
}

/// Whether `points` make a valid Jolt `LinearCurve` for a wheel's friction: at least one point,
/// finite, with strictly increasing x. Engine torque curves have their own domain
/// ([`limits::is_torque_curve`](crate::limits::is_torque_curve)).
fn is_curve(points: &[(f32, f32)]) -> bool {
    !points.is_empty()
        && points.iter().all(|(x, y)| x.is_finite() && y.is_finite())
        && points.windows(2).all(|pair| pair[0].0 < pair[1].0)
}

/// A joltc linear curve holding `points`, which [`is_curve`] or
/// [`limits::is_torque_curve`](crate::limits::is_torque_curve) accepted.
fn create_curve(points: &[(f32, f32)]) -> Owned<JPH_LinearCurve> {
    // SAFETY: Jolt is initialised. joltc `new`s the curve, which the guard owns whole.
    let curve = unsafe { Owned::from_raw(JPH_LinearCurve_Create()) }
        .unwrap_or_else(|| unreachable!("joltc `new`s the curve"));
    for &(x, y) in points {
        // SAFETY: the curve is live and owned by the guard. The points are added in increasing
        // x, the order Jolt's `GetValue` expects.
        unsafe { JPH_LinearCurve_AddPoint(curve.as_ptr(), x, y) };
    }
    curve
}

/// Controller settings, of which the owner holds the one reference
/// `JPH_WheeledVehicleControllerSettings_Create` returns. A vehicle copies them into its own
/// controller when it is created.
impl JoltObject for JPH_WheeledVehicleControllerSettings {
    unsafe fn destroy(ptr: *mut Self) {
        // SAFETY: the owner holds one reference (trait contract), released here once; the
        // wheeled settings derive from `VehicleControllerSettings` with single inheritance.
        unsafe { JPH_VehicleControllerSettings_Destroy(ptr.cast()) };
    }
}

/// A linear curve, owned whole: joltc `new`s it, and wheel and engine settings copy it.
impl JoltObject for JPH_LinearCurve {
    unsafe fn destroy(ptr: *mut Self) {
        // SAFETY: the owner owns the curve (trait contract), which joltc deletes.
        unsafe { JPH_LinearCurve_Destroy(ptr) };
    }
}

#[cfg(test)]
mod tests;