use std::ops::Range;
use oxijolt_sys::*;
use super::wheel::WheelBase;
use super::{
validate_wheel_count, BuiltSettings, ControllerGuard, VehicleCollisionTester,
VehicleEngineSettings, VehicleFrame, VehicleTransmissionSettings, WheelGeometry, WheelGuard,
ANGULAR_VELOCITY_TO_RPM,
};
use crate::limits::{self, PrincipalMass};
use crate::math::{is_finite_non_negative, is_finite_positive};
use crate::owned::{JoltObject, Owned};
use crate::{PhysicsWorld, Quat, SpringSettings, Vec3, VehicleError};
#[derive(Clone, Debug, PartialEq)]
pub struct TrackedWheelSettings {
pub(super) base: WheelBase,
pub(super) longitudinal_friction: f32,
pub(super) lateral_friction: f32,
}
impl TrackedWheelSettings {
pub fn new(position: Vec3) -> Self {
Self {
base: WheelBase::new(position),
longitudinal_friction: 4.0,
lateral_friction: 2.0,
}
}
#[must_use]
pub fn suspension_force_point(mut self, value: Option<Vec3>) -> Self {
self.base.suspension_force_point = value;
self
}
#[must_use]
pub fn suspension_direction(mut self, value: Vec3) -> Self {
self.base.suspension_direction = value;
self
}
#[must_use]
pub fn wheel_up(mut self, value: Vec3) -> Self {
self.base.wheel_up = value;
self
}
#[must_use]
pub fn wheel_forward(mut self, value: Vec3) -> Self {
self.base.wheel_forward = value;
self
}
#[must_use]
pub fn suspension_min_length(mut self, value: f32) -> Self {
self.base.suspension_min_length = value;
self
}
#[must_use]
pub fn suspension_max_length(mut self, value: f32) -> Self {
self.base.suspension_max_length = value;
self
}
#[must_use]
pub fn suspension_preload_length(mut self, value: f32) -> Self {
self.base.suspension_preload_length = value;
self
}
#[must_use]
pub fn suspension_spring(mut self, value: SpringSettings) -> Self {
self.base.suspension_spring = value;
self
}
#[must_use]
pub fn radius(mut self, value: f32) -> Self {
self.base.radius = value;
self
}
#[must_use]
pub fn width(mut self, value: f32) -> Self {
self.base.width = value;
self
}
#[must_use]
pub fn longitudinal_friction(mut self, value: f32) -> Self {
self.longitudinal_friction = value;
self
}
#[must_use]
pub fn lateral_friction(mut self, value: f32) -> Self {
self.lateral_friction = value;
self
}
fn validate(&self) -> Result<(), VehicleError> {
self.base.validate()?;
if !(limits::is_friction(self.longitudinal_friction)
&& limits::is_friction(self.lateral_friction))
{
return Err(VehicleError::InvalidValue(
"track wheel friction must be finite and between 0 and limits::MAX_FRICTION",
));
}
Ok(())
}
pub(super) fn create(&self) -> Owned<JPH_WheelSettingsTV> {
let wheel = unsafe { Owned::from_raw(JPH_WheelSettingsTV_Create()) }
.unwrap_or_else(|| unreachable!("joltc `new`s the settings"));
let ptr = wheel.as_ptr();
unsafe {
self.base.apply(ptr.cast());
JPH_WheelSettingsTV_SetLongitudinalFriction(ptr, self.longitudinal_friction);
JPH_WheelSettingsTV_SetLateralFriction(ptr, self.lateral_friction);
}
wheel
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct VehicleTrackSettings {
pub(super) wheels: Vec<TrackedWheelSettings>,
pub(super) driven_wheel: u32,
pub(super) inertia: f32,
pub(super) angular_damping: f32,
pub(super) max_brake_torque: f32,
pub(super) differential_ratio: f32,
}
impl VehicleTrackSettings {
pub fn new(wheels: Vec<TrackedWheelSettings>, driven_wheel: u32) -> Self {
Self {
wheels,
driven_wheel,
inertia: 10.0,
angular_damping: 0.5,
max_brake_torque: 15000.0,
differential_ratio: 6.0,
}
}
#[must_use]
pub fn inertia(mut self, value: f32) -> Self {
self.inertia = value;
self
}
#[must_use]
pub fn angular_damping(mut self, value: f32) -> Self {
self.angular_damping = value;
self
}
#[must_use]
pub fn max_brake_torque(mut self, value: f32) -> Self {
self.max_brake_torque = value;
self
}
#[must_use]
pub fn differential_ratio(mut self, value: f32) -> Self {
self.differential_ratio = value;
self
}
fn validate(&self) -> Result<(), VehicleError> {
let invalid = |what| Err(VehicleError::InvalidValue(what));
if self.wheels.is_empty() {
return invalid("a track needs at least one wheel");
}
if (self.driven_wheel as usize) >= self.wheels.len() {
return invalid("a track's driven wheel must be one of its wheels");
}
if !limits::is_track_inertia(self.inertia) {
return invalid(
"track inertia must be within limits::MIN_TRACK_INERTIA..=limits::MAX_TRACK_INERTIA",
);
}
if !(is_finite_non_negative(self.angular_damping)
&& is_finite_non_negative(self.max_brake_torque))
{
return invalid(
"track angular damping and max brake torque must be finite and not negative",
);
}
if !is_finite_positive(self.differential_ratio) {
return invalid("track differential ratio must be finite and positive");
}
for wheel in &self.wheels {
wheel.validate()?;
}
let driven_radius = self.driven_radius();
if !self
.wheels
.iter()
.all(|wheel| limits::is_ratio(driven_radius / wheel.base.radius))
{
return invalid(
"a track's wheel radii must be within a factor limits::MAX_RATIO of its driven wheel's",
);
}
Ok(())
}
fn smallest_radius(&self) -> f32 {
self.wheels
.iter()
.map(|wheel| wheel.base.radius)
.fold(f32::INFINITY, f32::min)
}
fn driven_radius(&self) -> f32 {
self.wheels[self.driven_wheel as usize].base.radius
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct TrackedVehicleSettings {
pub(super) left: VehicleTrackSettings,
pub(super) right: VehicleTrackSettings,
pub(super) collision_tester: VehicleCollisionTester,
frame: VehicleFrame,
engine: VehicleEngineSettings,
transmission: VehicleTransmissionSettings,
}
impl TrackedVehicleSettings {
pub fn new(
left: VehicleTrackSettings,
right: VehicleTrackSettings,
collision_tester: VehicleCollisionTester,
) -> Self {
Self {
left,
right,
collision_tester,
frame: VehicleFrame::default(),
engine: Self::default_engine(),
transmission: Self::default_transmission(),
}
}
pub fn default_engine() -> VehicleEngineSettings {
VehicleEngineSettings::default()
.min_rpm(500.0)
.max_rpm(4000.0)
.max_torque(500.0)
}
pub fn default_transmission() -> VehicleTransmissionSettings {
VehicleTransmissionSettings::default()
.gear_ratios(vec![4.0, 3.0, 2.0, 1.0])
.reverse_gear_ratios(vec![-4.0, -3.0])
.shift_down_rpm(1000.0)
.shift_up_rpm(3500.0)
}
#[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 engine(mut self, value: VehicleEngineSettings) -> Self {
self.engine = value;
self
}
#[must_use]
pub fn transmission(mut self, value: VehicleTransmissionSettings) -> Self {
self.transmission = value;
self
}
fn tracks(&self) -> [&VehicleTrackSettings; 2] {
[&self.left, &self.right]
}
fn track_wheels(&self) -> [Range<u32>; 2] {
let left = self.left.wheels.len() as u32;
let right = self.right.wheels.len() as u32;
[0..left, left..left + right]
}
pub(crate) fn validate(&self, object_layer_count: u32) -> Result<(), VehicleError> {
self.frame.validate()?;
for track in self.tracks() {
track.validate()?;
}
if !limits::is_track_inertia_ratio(self.left.inertia, self.right.inertia) {
return Err(VehicleError::InvalidValue(
"track inertia ratio must be at most limits::MAX_TRACK_INERTIA_RATIO",
));
}
validate_wheel_count(
self.left
.wheels
.len()
.saturating_add(self.right.wheels.len()),
)?;
self.engine.validate()?;
self.transmission.validate(&self.engine)?;
self.engine.validate_step_coefficients()?;
self.validate_drive_envelope()?;
self.collision_tester
.validate(object_layer_count, self.wheel_geometry().into_iter())
}
fn validate_drive_envelope(&self) -> Result<(), VehicleError> {
let invalid = |what| Err(VehicleError::InvalidValue(what));
let (smallest_gear, _) = self.gear_range();
for track in self.tracks() {
let divisor = smallest_gear as f32
* track.differential_ratio
* (1.0 / limits::MAX_RATIO)
* ANGULAR_VELOCITY_TO_RPM;
if divisor < f32::MIN_POSITIVE {
return invalid(
"gear and track differential ratios give an engine speed limit that underflows",
);
}
}
if !self
.drive_envelope()
.into_iter()
.all(|term| term <= ENVELOPE_CEILING)
{
return invalid("tracked drivetrain exceeds the track drive envelope");
}
Ok(())
}
fn gear_range(&self) -> (f64, f64) {
let transmission = &self.transmission;
let gears = || {
transmission
.gear_ratios
.iter()
.chain(&transmission.reverse_gear_ratios)
.map(|ratio| f64::from(ratio.abs()))
};
(
gears().fold(f64::INFINITY, f64::min),
gears().fold(0.0, f64::max),
)
}
fn drive_envelope(&self) -> Vec<f64> {
let smallest_ratio = 1.0 / f64::from(limits::MAX_RATIO);
let dt = f64::from(PhysicsWorld::MAX_DELTA_TIME);
let rpm_per_rad = f64::from(ANGULAR_VELOCITY_TO_RPM);
let (smallest_gear, largest_gear) = self.gear_range();
let transmission_torque = largest_gear * f64::from(self.engine.largest_torque());
let tracks = self.tracks();
let inertia = tracks.map(|track| f64::from(track.inertia));
let target = tracks.map(|track| {
let differential = f64::from(track.differential_ratio);
let limit = 1.001 * f64::from(self.engine.max_rpm)
/ (smallest_gear * differential * smallest_ratio * rpm_per_rad);
limit + differential * transmission_torque * dt / f64::from(track.inertia)
});
let weighted = (target[0] / inertia[0] + target[1] / inertia[1])
.max(target[0] * (1.0 / inertia[0] + 1.0 / (smallest_ratio * inertia[1])))
.max(target[1] * (1.0 / inertia[1] + 1.0 / (smallest_ratio * inertia[0])));
let speed = [
weighted * inertia[0].min(inertia[1] / smallest_ratio),
weighted * inertia[1].min(inertia[0] / smallest_ratio),
];
let sync = (speed[0] + speed[1]) / (smallest_ratio * (inertia[0] + inertia[1]));
let mut terms = vec![transmission_torque, sync, sync * inertia[0].max(inertia[1])];
for (side, track) in tracks.into_iter().enumerate() {
let differential_torque = f64::from(track.differential_ratio) * transmission_torque;
let brake = f64::from(track.max_brake_torque);
let brake_per_radius = brake / f64::from(track.smallest_radius());
terms.extend([
speed[side],
speed[side] * inertia[side] / f64::from(PhysicsWorld::MIN_DELTA_TIME),
differential_torque,
differential_torque * dt,
brake * dt / inertia[side],
brake_per_radius,
brake_per_radius * dt,
]);
let driven_radius = f64::from(track.driven_radius());
for wheel in &track.wheels {
let radius = f64::from(wheel.base.radius);
let wheel_speed = speed[side] * driven_radius / radius;
terms.extend([
wheel_speed,
wheel_speed * dt,
inertia[side] / radius,
radius / inertia[side],
]);
}
}
terms
}
pub(crate) fn largest_track_mass_ratio(&self, chassis: &ChassisMass) -> f64 {
self.tracks()
.into_iter()
.flat_map(|track| track.wheels.iter().map(move |wheel| (track.inertia, wheel)))
.map(|(inertia, wheel)| {
let radius = f64::from(wheel.base.radius);
f64::from(inertia) / (radius * radius) * chassis.inverse_effective_mass(&wheel.base)
})
.fold(0.0, f64::max)
}
fn wheel_geometry(&self) -> Vec<WheelGeometry> {
self.tracks()
.into_iter()
.flat_map(|track| &track.wheels)
.map(|wheel| wheel.base.geometry())
.collect()
}
pub(crate) fn build(&self) -> BuiltSettings {
BuiltSettings {
wheels: self
.tracks()
.into_iter()
.flat_map(|track| &track.wheels)
.map(|wheel| WheelGuard::Tracked(wheel.create()))
.collect(),
controller: ControllerGuard::Tracked(self.create_controller()),
anti_roll_bars: Vec::new(),
frame: self.frame,
collision_tester: self.collision_tester,
geometry: self.wheel_geometry(),
tracks: Some(self.track_wheels()),
}
}
pub(super) fn create_controller(&self) -> Owned<JPH_TrackedVehicleControllerSettings> {
let controller = unsafe { Owned::from_raw(JPH_TrackedVehicleControllerSettings_Create()) }
.unwrap_or_else(|| unreachable!("joltc `new`s the settings"));
let ptr = controller.as_ptr();
let transmission = self.transmission.create();
self.engine.with_jph(|engine| {
unsafe {
JPH_TrackedVehicleControllerSettings_SetEngine(ptr, engine);
JPH_TrackedVehicleControllerSettings_SetTransmission(ptr, transmission.as_ptr());
}
});
for (side, (track, wheels)) in self
.tracks()
.into_iter()
.zip(self.track_wheels())
.enumerate()
{
let indices: Vec<u32> = wheels.clone().collect();
let jolt_track = JPH_VehicleTrackSettings {
drivenWheel: wheels.start + track.driven_wheel,
wheels: indices.as_ptr(),
wheelsCount: indices.len() as u32,
inertia: track.inertia,
angularDamping: track.angular_damping,
maxBrakeTorque: track.max_brake_torque,
differentialRatio: track.differential_ratio,
};
unsafe { JPH_TrackedVehicleControllerSettings_SetTrack(ptr, side as u32, &jolt_track) };
}
controller
}
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct ChassisMass {
pub(crate) mass: PrincipalMass,
pub(crate) body_to_principal: Quat,
pub(crate) center_of_mass: Vec3,
}
impl ChassisMass {
fn inverse_effective_mass(&self, wheel: &WheelBase) -> f64 {
let principal = |v: Vec3| {
let v = self.body_to_principal.rotate(v);
[v.x, v.y, v.z].map(f64::from)
};
let center = self.center_of_mass;
let from_center =
|p: Vec3| principal(Vec3::new(p.x - center.x, p.y - center.y, p.z - center.z));
let up = limits::normalized(principal(wheel.wheel_up));
let right = limits::normalized(limits::cross(principal(wheel.wheel_forward), up));
let forward = limits::cross(up, right);
let reach = f64::from(wheel.suspension_max_length)
+ f64::from(wheel.radius).hypot(0.5 * f64::from(wheel.width));
let lever_at = |point: Vec3| self.mass.lever_in_plane(from_center(point), forward, up);
let mut lever = lever_at(wheel.position) + reach * self.mass.lever_per_metre();
if let Some(point) = wheel.suspension_force_point {
lever = lever.max(lever_at(point));
}
self.mass.inverse_mass + lever * lever
}
}
const ENVELOPE_CEILING: f64 = 1.0e30;
impl JoltObject for JPH_WheelSettingsTV {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_WheelSettings_Destroy(ptr.cast()) };
}
}
impl JoltObject for JPH_TrackedVehicleControllerSettings {
unsafe fn destroy(ptr: *mut Self) {
unsafe { JPH_VehicleControllerSettings_Destroy(ptr.cast()) };
}
}
#[cfg(test)]
#[path = "tracked_tests.rs"]
mod tests;