Skip to main content

oxijolt/vehicle/settings/
drivetrain.rs

1//! The drivetrain: engine, transmission and differentials.
2
3use oxijolt_sys::*;
4
5use super::{create_curve, is_limited_slip_ratio, ANGULAR_VELOCITY_TO_RPM, LIMITED_SLIP_RULE};
6use crate::limits;
7use crate::math::{is_finite_non_negative, is_finite_positive};
8use crate::owned::{JoltObject, Owned};
9use crate::{PhysicsWorld, VehicleError};
10
11/// How far above its largest point Jolt's `LinearCurve::GetValue` may round when it
12/// interpolates a torque curve: `1 + 8·2⁻²⁴`.
13const CURVE_ROUNDING: f32 = 1.0 + 4.0 * f32::EPSILON;
14
15/// The engine of a vehicle (Jolt `VehicleEngineSettings`). The defaults are Jolt's wheeled
16/// vehicle defaults; [`TrackedVehicleSettings::default_engine`](crate::TrackedVehicleSettings::default_engine)
17/// gives the tracked ones.
18#[derive(Clone, Debug, PartialEq)]
19pub struct VehicleEngineSettings {
20    pub(super) max_torque: f32,
21    pub(super) min_rpm: f32,
22    pub(super) max_rpm: f32,
23    pub(super) normalized_torque: Vec<(f32, f32)>,
24    pub(super) inertia: f32,
25    pub(super) angular_damping: f32,
26}
27
28impl Default for VehicleEngineSettings {
29    fn default() -> Self {
30        Self {
31            max_torque: 500.0,
32            min_rpm: 1000.0,
33            max_rpm: 6000.0,
34            normalized_torque: Self::DEFAULT_NORMALIZED_TORQUE.to_vec(),
35            inertia: 0.5,
36            angular_damping: 0.2,
37        }
38    }
39}
40
41impl VehicleEngineSettings {
42    /// Jolt's default normalized torque curve of an engine (`VehicleEngineSettings`): fraction
43    /// of the maximum torque over fraction of the maximum rpm.
44    pub const DEFAULT_NORMALIZED_TORQUE: [(f32, f32); 3] = [(0.0, 0.8), (0.66, 1.0), (1.0, 0.8)];
45
46    /// Largest torque the engine delivers, N·m, not negative. Default 500.
47    #[must_use]
48    pub fn max_torque(mut self, value: f32) -> Self {
49        self.max_torque = value;
50        self
51    }
52
53    /// Lowest rpm, at which the engine idles, not negative. Default 1000.
54    #[must_use]
55    pub fn min_rpm(mut self, value: f32) -> Self {
56        self.min_rpm = value;
57        self
58    }
59
60    /// Highest rpm, positive and at least the lowest. Default 6000.
61    #[must_use]
62    pub fn max_rpm(mut self, value: f32) -> Self {
63        self.max_rpm = value;
64        self
65    }
66
67    /// Fraction of the maximum torque over fraction of the maximum rpm, as `(rpm fraction,
68    /// torque fraction)` points: x within `0..=1`, increasing by at least
69    /// [`limits::MIN_TORQUE_CURVE_SPACING`]; y within `0..=`[`limits::MAX_NORMALIZED_TORQUE`].
70    /// Jolt reads the curve at the current rpm over the max rpm, between `min_rpm / max_rpm` and
71    /// 1. Default [`DEFAULT_NORMALIZED_TORQUE`](Self::DEFAULT_NORMALIZED_TORQUE).
72    #[must_use]
73    pub fn normalized_torque(mut self, points: Vec<(f32, f32)>) -> Self {
74        self.normalized_torque = points;
75        self
76    }
77
78    /// Moment of inertia of the engine, kg·m², positive. Default 0.5.
79    #[must_use]
80    pub fn inertia(mut self, value: f32) -> Self {
81        self.inertia = value;
82        self
83    }
84
85    /// Angular damping of the engine, `dω/dt = −c·ω`, not negative. Default 0.2.
86    #[must_use]
87    pub fn angular_damping(mut self, value: f32) -> Self {
88        self.angular_damping = value;
89        self
90    }
91
92    pub(super) fn validate(&self) -> Result<(), VehicleError> {
93        let invalid = |what| Err(VehicleError::InvalidValue(what));
94        if !is_finite_non_negative(self.max_torque) {
95            return invalid("engine max torque must be finite and not negative");
96        }
97        if !is_finite_non_negative(self.min_rpm) {
98            return invalid("engine min rpm must be finite and not negative");
99        }
100        if !(is_finite_positive(self.max_rpm) && self.max_rpm >= self.min_rpm) {
101            return invalid("engine max rpm must be finite, positive and at least the min rpm");
102        }
103        // Jolt divides by the engine inertia (`VehicleEngine::ApplyTorque`,
104        // `WheeledVehicleController::PostCollide`).
105        if !is_finite_positive(self.inertia) {
106            return invalid("engine inertia must be finite and positive");
107        }
108        if !is_finite_non_negative(self.angular_damping) {
109            return invalid("engine angular damping must be finite and not negative");
110        }
111        if !limits::is_torque_curve(&self.normalized_torque) {
112            return invalid(limits::TORQUE_CURVE_RULE);
113        }
114        Ok(())
115    }
116
117    /// The largest torque these validated settings let the engine deliver, N·m: the max torque
118    /// times the largest torque fraction of the curve, with the rounding of Jolt's
119    /// interpolation; see [docs/limits.md#torque-curves].
120    ///
121    /// [docs/limits.md#torque-curves]: https://github.com/pockerhead/oxijolt/blob/main/docs/limits.md#torque-curves
122    pub(super) fn largest_torque(&self) -> f32 {
123        let largest_torque_fraction = self
124            .normalized_torque
125            .iter()
126            .map(|&(_, fraction)| fraction)
127            .fold(0.0, f32::max);
128        self.max_torque * largest_torque_fraction * CURVE_ROUNDING
129    }
130
131    /// Checks that the coefficients `VehicleEngine::ApplyTorque` forms from these validated
132    /// settings are finite at the largest step.
133    pub(super) fn validate_step_coefficients(&self) -> Result<(), VehicleError> {
134        let dt = PhysicsWorld::MAX_DELTA_TIME;
135        let dt_div_ie = dt / self.inertia;
136        let torque = self.largest_torque();
137        let coefficients = [
138            dt_div_ie,
139            torque,
140            dt_div_ie * torque,
141            ANGULAR_VELOCITY_TO_RPM * torque * dt / self.inertia,
142        ];
143        if coefficients.iter().all(|value| value.is_finite()) {
144            Ok(())
145        } else {
146            Err(VehicleError::InvalidValue(
147                "engine inertia and torque give a non-finite step coefficient",
148            ))
149        }
150    }
151
152    /// Calls `f` with the joltc engine settings of these validated settings; their torque curve
153    /// lives for the call.
154    pub(super) fn with_jph<R>(&self, f: impl FnOnce(&JPH_VehicleEngineSettings) -> R) -> R {
155        let torque_curve = create_curve(&self.normalized_torque);
156        f(&JPH_VehicleEngineSettings {
157            maxTorque: self.max_torque,
158            minRPM: self.min_rpm,
159            maxRPM: self.max_rpm,
160            normalizedTorque: torque_curve.as_ptr(),
161            inertia: self.inertia,
162            angularDamping: self.angular_damping,
163        })
164    }
165}
166
167/// The automatic transmission of a vehicle (Jolt `VehicleTransmissionSettings` in
168/// `ETransmissionMode::Auto`; the manual mode is not offered). The defaults are Jolt's wheeled
169/// vehicle defaults;
170/// [`TrackedVehicleSettings::default_transmission`](crate::TrackedVehicleSettings::default_transmission)
171/// gives the tracked ones.
172#[derive(Clone, Debug, PartialEq)]
173pub struct VehicleTransmissionSettings {
174    pub(super) gear_ratios: Vec<f32>,
175    pub(super) reverse_gear_ratios: Vec<f32>,
176    pub(super) switch_time: f32,
177    pub(super) clutch_release_time: f32,
178    pub(super) switch_latency: f32,
179    pub(super) shift_up_rpm: f32,
180    pub(super) shift_down_rpm: f32,
181    pub(super) clutch_strength: f32,
182}
183
184impl Default for VehicleTransmissionSettings {
185    fn default() -> Self {
186        Self {
187            gear_ratios: vec![2.66, 1.78, 1.3, 1.0, 0.74],
188            reverse_gear_ratios: vec![-2.9],
189            switch_time: 0.5,
190            clutch_release_time: 0.3,
191            switch_latency: 0.5,
192            shift_up_rpm: 4000.0,
193            shift_down_rpm: 2000.0,
194            clutch_strength: 10.0,
195        }
196    }
197}
198
199impl VehicleTransmissionSettings {
200    /// Engine to gearbox rotation ratios of the forward gears, first gear first; at least one,
201    /// each positive. Default `[2.66, 1.78, 1.3, 1.0, 0.74]`.
202    #[must_use]
203    pub fn gear_ratios(mut self, value: Vec<f32>) -> Self {
204        self.gear_ratios = value;
205        self
206    }
207
208    /// Ratios of the reverse gears; at least one, each negative. Default `[-2.9]`.
209    #[must_use]
210    pub fn reverse_gear_ratios(mut self, value: Vec<f32>) -> Self {
211        self.reverse_gear_ratios = value;
212        self
213    }
214
215    /// Seconds a gear switch takes, not negative. Default 0.5.
216    #[must_use]
217    pub fn switch_time(mut self, value: f32) -> Self {
218        self.switch_time = value;
219        self
220    }
221
222    /// Seconds the clutch takes to engage fully after a switch, not negative. Default 0.3.
223    #[must_use]
224    pub fn clutch_release_time(mut self, value: f32) -> Self {
225        self.clutch_release_time = value;
226        self
227    }
228
229    /// Seconds to wait after the clutch engaged before another switch, not negative. Default
230    /// 0.5.
231    #[must_use]
232    pub fn switch_latency(mut self, value: f32) -> Self {
233        self.switch_latency = value;
234        self
235    }
236
237    /// Engine rpm above which the transmission shifts up; above the shift down rpm and below
238    /// the engine's max rpm. Default 4000.
239    #[must_use]
240    pub fn shift_up_rpm(mut self, value: f32) -> Self {
241        self.shift_up_rpm = value;
242        self
243    }
244
245    /// Engine rpm below which the transmission shifts down, positive. Default 2000.
246    #[must_use]
247    pub fn shift_down_rpm(mut self, value: f32) -> Self {
248        self.shift_down_rpm = value;
249        self
250    }
251
252    /// Strength of the fully engaged clutch, kg·m²/s, positive. Default 10.
253    #[must_use]
254    pub fn clutch_strength(mut self, value: f32) -> Self {
255        self.clutch_strength = value;
256        self
257    }
258
259    pub(super) fn validate(&self, engine: &VehicleEngineSettings) -> Result<(), VehicleError> {
260        let invalid = |what| Err(VehicleError::InvalidValue(what));
261        // Jolt indexes gear 1 and gear -1 without a check
262        // (`VehicleTransmission::GetCurrentRatio`).
263        if self.gear_ratios.is_empty() || !self.gear_ratios.iter().all(|&r| is_finite_positive(r)) {
264            return invalid("gear ratios need at least one gear, each finite and positive");
265        }
266        if self.reverse_gear_ratios.is_empty()
267            || !self
268                .reverse_gear_ratios
269                .iter()
270                .all(|&r| r.is_finite() && r < 0.0)
271        {
272            return invalid("reverse gear ratios need at least one gear, each finite and negative");
273        }
274        if u32::try_from(self.gear_ratios.len()).is_err()
275            || u32::try_from(self.reverse_gear_ratios.len()).is_err()
276        {
277            return invalid("too many gears");
278        }
279        let times = [
280            self.switch_time,
281            self.clutch_release_time,
282            self.switch_latency,
283        ];
284        if !times.into_iter().all(is_finite_non_negative) {
285            return invalid("transmission times must be finite and not negative");
286        }
287        if !is_finite_positive(self.shift_down_rpm) {
288            return invalid("shift down rpm must be finite and positive");
289        }
290        if !(self.shift_up_rpm.is_finite() && self.shift_up_rpm > self.shift_down_rpm) {
291            return invalid("shift up rpm must be finite and above the shift down rpm");
292        }
293        if self.shift_up_rpm >= engine.max_rpm {
294            return invalid("shift up rpm must be below the engine's max rpm");
295        }
296        if !is_finite_positive(self.clutch_strength) {
297            return invalid("clutch strength must be finite and positive");
298        }
299        Ok(())
300    }
301
302    /// The joltc settings of this validated transmission.
303    pub(super) fn create(&self) -> Owned<JPH_VehicleTransmissionSettings> {
304        // SAFETY: Jolt is initialised (a world exists). joltc `new`s the settings, which the
305        // guard owns whole.
306        let settings = unsafe { Owned::from_raw(JPH_VehicleTransmissionSettings_Create()) }
307            .unwrap_or_else(|| unreachable!("joltc `new`s the settings"));
308        let ptr = settings.as_ptr();
309        // SAFETY: the settings are live and owned by the guard; the gear slices live for the
310        // calls and hold as many ratios as passed (`validate` bounds the counts), which joltc
311        // copies. All values were validated.
312        unsafe {
313            JPH_VehicleTransmissionSettings_SetMode(ptr, JPH_TransmissionMode_Auto);
314            JPH_VehicleTransmissionSettings_SetGearRatios(
315                ptr,
316                self.gear_ratios.as_ptr(),
317                self.gear_ratios.len() as u32,
318            );
319            JPH_VehicleTransmissionSettings_SetReverseGearRatios(
320                ptr,
321                self.reverse_gear_ratios.as_ptr(),
322                self.reverse_gear_ratios.len() as u32,
323            );
324            JPH_VehicleTransmissionSettings_SetSwitchTime(ptr, self.switch_time);
325            JPH_VehicleTransmissionSettings_SetClutchReleaseTime(ptr, self.clutch_release_time);
326            JPH_VehicleTransmissionSettings_SetSwitchLatency(ptr, self.switch_latency);
327            JPH_VehicleTransmissionSettings_SetShiftUpRPM(ptr, self.shift_up_rpm);
328            JPH_VehicleTransmissionSettings_SetShiftDownRPM(ptr, self.shift_down_rpm);
329            JPH_VehicleTransmissionSettings_SetClutchStrength(ptr, self.clutch_strength);
330        }
331        settings
332    }
333}
334
335/// A differential: how engine torque reaches a pair of wheels (Jolt
336/// `VehicleDifferentialSettings`). The defaults are Jolt's.
337#[derive(Clone, Copy, Debug, PartialEq)]
338pub struct VehicleDifferentialSettings {
339    pub(super) left_wheel: Option<u32>,
340    pub(super) right_wheel: Option<u32>,
341    pub(super) differential_ratio: f32,
342    left_right_split: f32,
343    limited_slip_ratio: f32,
344    pub(super) engine_torque_ratio: f32,
345}
346
347impl VehicleDifferentialSettings {
348    /// A differential driving the wheels with these indices; `None` for a side without a wheel,
349    /// but at least one side must have one.
350    pub fn new(left_wheel: Option<u32>, right_wheel: Option<u32>) -> Self {
351        Self {
352            left_wheel,
353            right_wheel,
354            differential_ratio: 3.42,
355            left_right_split: 0.5,
356            limited_slip_ratio: 1.4,
357            engine_torque_ratio: 1.0,
358        }
359    }
360
361    /// Ratio between the gearbox and wheel rotation rates, positive. Default 3.42.
362    #[must_use]
363    pub fn differential_ratio(mut self, value: f32) -> Self {
364        self.differential_ratio = value;
365        self
366    }
367
368    /// How torque is split between the wheels: 0 all left, 1 all right, in `[0, 1]`. Default
369    /// 0.5.
370    #[must_use]
371    pub fn left_right_split(mut self, value: f32) -> Self {
372        self.left_right_split = value;
373        self
374    }
375
376    /// Ratio of the faster to the slower wheel above which all torque goes to the slower one;
377    /// above 1, `f32::MAX` for an open differential. Default 1.4.
378    #[must_use]
379    pub fn limited_slip_ratio(mut self, value: f32) -> Self {
380        self.limited_slip_ratio = value;
381        self
382    }
383
384    /// Fraction of the engine torque this differential gets, not negative. The fractions of
385    /// all differentials must add up to 1. Default 1.
386    #[must_use]
387    pub fn engine_torque_ratio(mut self, value: f32) -> Self {
388        self.engine_torque_ratio = value;
389        self
390    }
391
392    pub(super) fn validate(&self, wheel_count: usize) -> Result<(), VehicleError> {
393        let invalid = |what| Err(VehicleError::InvalidValue(what));
394        let wheel_exists = |index: Option<u32>| index.is_none_or(|i| (i as usize) < wheel_count);
395        if !(wheel_exists(self.left_wheel) && wheel_exists(self.right_wheel)) {
396            return invalid("differential wheel index out of range");
397        }
398        if self.left_wheel.is_none() && self.right_wheel.is_none() {
399            return invalid("a differential needs at least one wheel");
400        }
401        if !is_finite_positive(self.differential_ratio) {
402            return invalid("differential ratio must be finite and positive");
403        }
404        if !(self.left_right_split.is_finite() && (0.0..=1.0).contains(&self.left_right_split)) {
405            return invalid("differential left right split must be between 0 and 1");
406        }
407        if !is_limited_slip_ratio(self.limited_slip_ratio) {
408            return invalid(LIMITED_SLIP_RULE);
409        }
410        if !is_finite_non_negative(self.engine_torque_ratio) {
411            return invalid("differential engine torque ratio must be finite and not negative");
412        }
413        Ok(())
414    }
415
416    /// The joltc value of this validated differential; the wheel indices fit in an `i32`
417    /// because the wheel count does.
418    pub(super) fn to_jph(self) -> JPH_VehicleDifferentialSettings {
419        let index = |wheel: Option<u32>| wheel.map_or(-1, |i| i as i32);
420        JPH_VehicleDifferentialSettings {
421            leftWheel: index(self.left_wheel),
422            rightWheel: index(self.right_wheel),
423            differentialRatio: self.differential_ratio,
424            leftRightSplit: self.left_right_split,
425            limitedSlipRatio: self.limited_slip_ratio,
426            engineTorqueRatio: self.engine_torque_ratio,
427        }
428    }
429}
430
431/// Transmission settings, owned whole: joltc `new`s them, and controller settings copy them.
432impl JoltObject for JPH_VehicleTransmissionSettings {
433    unsafe fn destroy(ptr: *mut Self) {
434        // SAFETY: the owner owns the settings (trait contract), which joltc deletes.
435        unsafe { JPH_VehicleTransmissionSettings_Destroy(ptr) };
436    }
437}