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};
#[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 {
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,
}
}
#[must_use]
pub fn up(mut self, value: Vec3) -> Self {
self.frame.up = value;
self
}
#[must_use]
pub fn forward(mut self, value: Vec3) -> Self {
self.frame.forward = value;
self
}
#[must_use]
pub fn max_pitch_roll_angle(mut self, radians: f32) -> Self {
self.frame.max_pitch_roll_angle = radians;
self
}
#[must_use]
pub fn anti_roll_bars(mut self, value: Vec<VehicleAntiRollBar>) -> Self {
self.anti_roll_bars = value;
self
}
#[must_use]
pub fn engine(mut self, value: VehicleEngineSettings) -> Self {
self.engine = value;
self
}
#[must_use]
pub fn transmission(mut self, value: VehicleTransmissionSettings) -> Self {
self.transmission = value;
self
}
#[must_use]
pub fn differential_limited_slip_ratio(mut self, value: f32) -> Self {
self.differential_limited_slip_ratio = value;
self
}
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;
}
if (torque_ratio_sum - 1.0).abs() >= SUM_TOLERANCE {
return invalid("differential engine torque ratios must add up to 1");
}
Ok(())
}
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;
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(())
}
pub(crate) fn build(&self) -> BuiltSettings {
self.build_with(ControllerGuard::Wheeled(self.create_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,
}
}
pub(crate) fn create_controller(&self) -> Owned<JPH_WheeledVehicleControllerSettings> {
let controller = unsafe { Owned::from_raw(JPH_WheeledVehicleControllerSettings_Create()) }
.unwrap_or_else(|| unreachable!("joltc `new`s the settings"));
unsafe { self.fill_wheeled_controller(controller.as_ptr()) };
controller
}
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| {
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,
);
}
});
}
}
#[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 {
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(())
}
}
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(())
}
pub(crate) enum WheelGuard {
Wheeled(Owned<JPH_WheelSettingsWV>),
Tracked(Owned<JPH_WheelSettingsTV>),
}
impl WheelGuard {
pub(crate) fn as_base(&self) -> *mut JPH_WheelSettings {
match self {
Self::Wheeled(wheel) => wheel.as_ptr().cast(),
Self::Tracked(wheel) => wheel.as_ptr().cast(),
}
}
}
pub(crate) enum ControllerGuard {
Wheeled(Owned<JPH_WheeledVehicleControllerSettings>),
Tracked(Owned<JPH_TrackedVehicleControllerSettings>),
Motorcycle(Owned<JPH_MotorcycleControllerSettings>),
}
impl ControllerGuard {
pub(crate) fn as_base(&self) -> *mut JPH_VehicleControllerSettings {
match self {
Self::Wheeled(controller) => controller.as_ptr().cast(),
Self::Tracked(controller) => controller.as_ptr().cast(),
Self::Motorcycle(controller) => controller.as_ptr().cast(),
}
}
pub(crate) fn kind(&self) -> VehicleType {
match self {
Self::Wheeled(_) => VehicleType::Wheeled,
Self::Tracked(_) => VehicleType::Tracked,
Self::Motorcycle(_) => VehicleType::Motorcycle,
}
}
}
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>,
pub(crate) tracks: Option<[Range<u32>; 2]>,
}
const ANGULAR_VELOCITY_TO_RPM: f32 = 60.0 / (2.0 * PI);
const PERPENDICULAR_TOLERANCE: f32 = 1.0e-3;
const SUM_TOLERANCE: f32 = 5.0e-7;
pub(super) const MAX_PITCH_ROLL_RULE: &str = "max pitch roll angle must be between 0 and pi";
const LIMITED_SLIP_RULE: &str = "differential limited slip ratio must be finite and above 1";
fn is_limited_slip_ratio(value: f32) -> bool {
value.is_finite() && value > 1.0
}
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)
}
fn create_curve(points: &[(f32, f32)]) -> Owned<JPH_LinearCurve> {
let curve = unsafe { Owned::from_raw(JPH_LinearCurve_Create()) }
.unwrap_or_else(|| unreachable!("joltc `new`s the curve"));
for &(x, y) in points {
unsafe { JPH_LinearCurve_AddPoint(curve.as_ptr(), x, y) };
}
curve
}
impl JoltObject for JPH_WheeledVehicleControllerSettings {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_VehicleControllerSettings_Destroy(ptr.cast()) };
}
}
impl JoltObject for JPH_LinearCurve {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_LinearCurve_Destroy(ptr) };
}
}
#[cfg(test)]
mod tests;