mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Synchronize vehicle rider wheels and camera with fixed-step interpolation
This commit is contained in:
1 parent
2ef93b2739
commit
fb0dd98e83
7 files changed
+109
-24
No files matched your search
@@ -3,7 +3,7 @@ $ErrorActionPreference = 'Stop'
|
||||
$privateDirectory = Join-Path $PSScriptRoot '.local/vehicle-sdk'
|
||||
$buildDirectory = Join-Path $privateDirectory 'build'
|
||||
$artifact = Join-Path $buildDirectory 'skate3rust-vehicle-sdk.exe'
|
||||
$destination = Join-Path $privateDirectory 'skate3-vehicle-animations.exe'
|
||||
$destination = Join-Path $privateDirectory 'skate3-vehicle-smooth.exe'
|
||||
Push-Location $PSScriptRoot
|
||||
$previousFlags = $env:CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS
|
||||
try {
|
||||
|
||||
+2
-2
@@ -9,7 +9,7 @@ if not defined SKATE3_ASSETS (
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
if not exist "%~dp0.local\vehicle-sdk\skate3-vehicle-animations.exe" (
|
||||
if not exist "%~dp0.local\vehicle-sdk\skate3-vehicle-smooth.exe" (
|
||||
echo Build missing. Run Build-VehicleSDK.ps1 first.
|
||||
pause
|
||||
exit /b 1
|
||||
@@ -20,5 +20,5 @@ echo Open Mods and ENABLE Mario Kart. F10 spawns the kart; E enters/exits. Escap
|
||||
echo WASD drive, Space brake, Shift handbrake, R reset. Slow down before exiting.
|
||||
echo Mods folder: %~dp0.local\vehicle-sdk\mods
|
||||
echo Trainer log: %~dp0.local\vehicle-sdk\session.log
|
||||
"%~dp0.local\vehicle-sdk\skate3-vehicle-animations.exe" --assets "%SKATE3_ASSETS%" --start-paused %* > "%~dp0.local\vehicle-sdk\session.log" 2>&1
|
||||
"%~dp0.local\vehicle-sdk\skate3-vehicle-smooth.exe" --assets "%SKATE3_ASSETS%" --start-paused %* > "%~dp0.local\vehicle-sdk\session.log" 2>&1
|
||||
if errorlevel 1 pause
|
||||
@@ -90,9 +90,9 @@ pub(crate) fn present(vehicles: Res<crate::modding::vehicles::Vehicles>, mut run
|
||||
if vehicle_blend.elapsed<0.5 && from.translation.distance(transform.translation)<50. {
|
||||
let t=(vehicle_blend.elapsed/0.5).clamp(0.,1.);*transform=crate::presentation::blend(from,*transform,t*t*(3.-2.*t));
|
||||
} else {vehicle_blend.from=None;}
|
||||
} else if active {
|
||||
if let Some(previous)=vehicle_blend.previous {*transform=crate::presentation::blend(previous,*transform,1.-(-18.*virtual_time.delta_secs()).exp());}
|
||||
}
|
||||
// Vehicle motion is already interpolated with the rider/chassis. A separate
|
||||
// follow filter introduces relative motion and makes fixed ticks visible.
|
||||
vehicle_blend.previous=Some(*transform);
|
||||
camera.is_active = true;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
//! One render-time sample for every visible part of a fixed-step vehicle.
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(super) struct Motion {
|
||||
pub body: Transform,
|
||||
pub wheels: Vec<Transform>,
|
||||
}
|
||||
|
||||
fn blend(a: Transform, b: Transform, alpha: f32) -> Transform {
|
||||
Transform {
|
||||
translation: a.translation.lerp(b.translation, alpha),
|
||||
rotation: a.rotation.normalize().slerp(b.rotation.normalize(), alpha),
|
||||
scale: a.scale.lerp(b.scale, alpha),
|
||||
}
|
||||
}
|
||||
|
||||
impl Motion {
|
||||
pub fn capture(sim: &skate_vehicles::Simulation, id: u64) -> Option<Self> {
|
||||
let (p, q) = sim.pose(id)?;
|
||||
let car = sim.vehicles.get(&id)?;
|
||||
Some(Self {
|
||||
body: Transform::from_translation(Vec3::from_array(p))
|
||||
.with_rotation(Quat::from_array(q)),
|
||||
wheels: car.controller.wheels().iter().map(|wheel| {
|
||||
Transform::from_xyz(0., (car.definition.suspension_length
|
||||
- wheel.raycast_info().suspension_length) / car.definition.model_scale, 0.)
|
||||
.with_rotation(Quat::from_rotation_y(wheel.steering)
|
||||
* Quat::from_rotation_x(-wheel.rotation))
|
||||
}).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn sample(&self, current: &Self, alpha: f32) -> Self {
|
||||
let alpha = alpha.clamp(0., 1.);
|
||||
Self {
|
||||
body: blend(self.body, current.body, alpha),
|
||||
wheels: current.wheels.iter().enumerate().map(|(i, &now)| {
|
||||
self.wheels.get(i).map_or(now, |&before| blend(before, now, alpha))
|
||||
}).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn subframes_move_continuously_and_keep_seat_attached() {
|
||||
let before = Motion { body: Transform::IDENTITY, wheels: vec![Transform::IDENTITY] };
|
||||
let after = Motion {
|
||||
body: Transform::from_xyz(1., 0., 0.).with_rotation(Quat::from_rotation_y(0.2)),
|
||||
wheels: vec![Transform::from_xyz(0., 0.1, 0.)],
|
||||
};
|
||||
let seat = Vec3::new(0., 0.18, -0.2);
|
||||
for index in 0..=4 {
|
||||
let alpha = index as f32 / 4.;
|
||||
let sample = before.sample(&after, alpha);
|
||||
assert!((sample.body.translation.x - alpha).abs() < 1e-6);
|
||||
assert!((sample.wheels[0].translation.y - alpha * 0.1).abs() < 1e-6);
|
||||
let rider = sample.body.transform_point(seat);
|
||||
assert!((rider.distance(sample.body.translation) - seat.length()).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Native vehicle host: isolated Rapier world, mod ownership and driver lifecycle.
|
||||
mod animations;
|
||||
mod interpolation;
|
||||
use bevy::prelude::*;
|
||||
use serde_json::{Value, json};
|
||||
use skate_mods::Command;
|
||||
@@ -40,6 +41,8 @@ pub(crate) struct Vehicles {
|
||||
visual_phase: String,
|
||||
blend_time: f32,
|
||||
steering_visual: f32,
|
||||
previous_motion: BTreeMap<u64, interpolation::Motion>,
|
||||
rendered_motion: BTreeMap<u64, interpolation::Motion>,
|
||||
}
|
||||
impl Vehicles {
|
||||
pub(crate) fn occupied(&self) -> bool {
|
||||
@@ -60,10 +63,10 @@ impl Vehicles {
|
||||
pub(crate) fn camera(&self) -> Option<Transform> {
|
||||
let d = self.driver.as_ref()?;
|
||||
let i = self.owned.get(&(d.owner.clone(), d.key.clone()))?;
|
||||
let (p, q) = self.simulation.pose(i.id)?;
|
||||
let q = Quat::from_array(q);
|
||||
let pose = self.rendered_motion.get(&i.id)?.body;
|
||||
let q = pose.rotation;
|
||||
let def = &self.simulation.vehicles[&i.id].definition;
|
||||
let center = Vec3::from_array(p) + Vec3::Y * 0.5;
|
||||
let center = pose.translation + Vec3::Y * 0.5;
|
||||
let forward = (q * Vec3::Z).with_y(0.).normalize_or_zero();
|
||||
Some(
|
||||
Transform::from_translation(
|
||||
@@ -165,6 +168,8 @@ pub(super) fn retire(world: &mut World, owner: &str) {
|
||||
for key in keys {
|
||||
if let Some(i) = v.owned.remove(&key) {
|
||||
v.simulation.remove(i.id);
|
||||
v.previous_motion.remove(&i.id);
|
||||
v.rendered_motion.remove(&i.id);
|
||||
world.despawn(i.entity);
|
||||
}
|
||||
}
|
||||
@@ -184,6 +189,8 @@ pub(super) fn clear(world: &mut World) {
|
||||
v.driver = None;
|
||||
v.pose = None;
|
||||
v.last_visual.clear();v.blend_from.clear();v.visual_phase.clear();v.steering_visual=0.;
|
||||
v.previous_motion.clear();
|
||||
v.rendered_motion.clear();
|
||||
v.simulation = Simulation::default();
|
||||
v.events.clear();
|
||||
});
|
||||
@@ -309,6 +316,8 @@ pub(super) fn command(
|
||||
}
|
||||
if let Some(i) = v.owned.remove(&owned_key) {
|
||||
v.simulation.remove(i.id);
|
||||
v.previous_motion.remove(&i.id);
|
||||
v.rendered_motion.remove(&i.id);
|
||||
world.despawn(i.entity);
|
||||
event(&mut v, owner, &key, "vehicle_removed");
|
||||
}
|
||||
@@ -398,6 +407,8 @@ pub(super) fn command(
|
||||
let i = v.owned.get(&owned_key).ok_or("Unknown vehicle")?;
|
||||
let id = i.id;
|
||||
v.simulation.reset(id, position, heading)?;
|
||||
v.previous_motion.remove(&id);
|
||||
v.rendered_motion.remove(&id);
|
||||
event(&mut v, owner, &key, "vehicle_reset");
|
||||
}
|
||||
_ => unreachable!(),
|
||||
@@ -460,6 +471,8 @@ fn tick(world: &mut World) {
|
||||
}
|
||||
}
|
||||
if !v.owned.is_empty() {
|
||||
v.previous_motion = v.simulation.vehicles.keys().filter_map(|&id|
|
||||
interpolation::Motion::capture(&v.simulation, id).map(|m| (id, m))).collect();
|
||||
v.simulation.step(dt);
|
||||
}
|
||||
if let Some((owner, key, phase)) = driver_info {
|
||||
@@ -491,6 +504,7 @@ fn tick(world: &mut World) {
|
||||
}
|
||||
pub(crate) fn present(world: &mut World) {
|
||||
let dt=world.resource::<Time<Virtual>>().delta_secs();
|
||||
let alpha=world.resource::<Time<Fixed>>().overstep_fraction();
|
||||
let phase=world.resource::<Vehicles>().driver.as_ref().map_or("vanilla",|d|d.phase).to_owned();
|
||||
let current=crate::animation::capture_vehicle_visual(world);
|
||||
{
|
||||
@@ -524,11 +538,17 @@ pub(crate) fn present(world: &mut World) {
|
||||
.manager
|
||||
.fail(&owner, error);
|
||||
}
|
||||
// Chassis, wheels, rider and camera share one fixed-step render sample.
|
||||
v.rendered_motion = v.simulation.vehicles.keys().filter_map(|&id| {
|
||||
let current = interpolation::Motion::capture(&v.simulation, id)?;
|
||||
let sample = v.previous_motion.get(&id).map_or_else(|| current.clone(),
|
||||
|previous| previous.sample(¤t, alpha));
|
||||
Some((id, sample))
|
||||
}).collect();
|
||||
for i in v.owned.values() {
|
||||
if let Some((p, q)) = v.simulation.pose(i.id) {
|
||||
if let Some(sample) = v.rendered_motion.get(&i.id) {
|
||||
if let Some(mut t) = world.get_mut::<Transform>(i.entity) {
|
||||
*t = Transform::from_translation(Vec3::from_array(p))
|
||||
.with_rotation(Quat::from_array(q));
|
||||
*t = sample.body;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -582,15 +602,12 @@ pub(crate) fn present(world: &mut World) {
|
||||
.query::<(&WheelVisual, &mut Transform)>()
|
||||
.iter_mut(world)
|
||||
{
|
||||
if let Some(car) = v.simulation.vehicles.get(&wheel.vehicle) {
|
||||
let physical = &car.controller.wheels()[wheel.index];
|
||||
*transform = wheel.rest;
|
||||
transform.translation.y += (car.definition.suspension_length
|
||||
- physical.raycast_info().suspension_length)
|
||||
/ car.definition.model_scale;
|
||||
transform.rotation = wheel.rest.rotation
|
||||
* Quat::from_rotation_y(physical.steering)
|
||||
* Quat::from_rotation_x(-physical.rotation);
|
||||
if let Some(sample) = v.rendered_motion.get(&wheel.vehicle) {
|
||||
if let Some(offset) = sample.wheels.get(wheel.index) {
|
||||
*transform = wheel.rest;
|
||||
transform.translation += offset.translation;
|
||||
transform.rotation = wheel.rest.rotation * offset.rotation;
|
||||
}
|
||||
}
|
||||
}
|
||||
v.pose = None;
|
||||
@@ -622,9 +639,9 @@ pub(crate) fn present(world: &mut World) {
|
||||
let steering=v.steering_visual+(target_steering-v.steering_visual)*(1.-(-12.*dt).exp());
|
||||
let pose = i.clips.pose(name, d.time, d.phase == "driving");
|
||||
let turn=if d.phase=="driving" {i.clips.pose(if steering>=0. {a.steer_left.as_ref()} else {a.steer_right.as_ref()},d.time,true)} else {None};
|
||||
let (p, q) = v.simulation.pose(i.id).unwrap();
|
||||
let q = Quat::from_array(q);
|
||||
let seat = Vec3::from_array(p) + q * Vec3::from_array(car.definition.seat);
|
||||
let body = v.rendered_motion[&i.id].body;
|
||||
let q = body.rotation;
|
||||
let seat = body.translation + q * Vec3::from_array(car.definition.seat);
|
||||
v.steering_visual=steering;
|
||||
let roots: Vec<_> = world
|
||||
.query_filtered::<Entity, With<crate::world::PlayerRoot>>()
|
||||
|
||||
@@ -63,6 +63,7 @@ fn definitions_and_controls_reject_invalid_inputs() {
|
||||
d.model = "../outside.glb".into();
|
||||
assert!(d.validate().is_err());
|
||||
let mut d = definition();
|
||||
d.animations.file = None;
|
||||
d.animations.drive = Some("drive".into());
|
||||
assert!(d.validate().is_err());
|
||||
let mut d = definition();
|
||||
|
||||
+3
-1
@@ -166,7 +166,9 @@ The host blends vehicle phase changes over 0.4 seconds and the return to vanilla
|
||||
0.5 seconds, using the game's bone-local translation/scale lerp and quaternion slerp.
|
||||
Steering input is smoothed and blends the current driving pose towards steer_left or
|
||||
steer_right continuously, so these slots should contain compatible seated poses.
|
||||
Vehicle camera hand-offs ease over 0.5 seconds; the follow camera is damped while driving.
|
||||
Vehicle camera hand-offs ease over 0.5 seconds. While driving, chassis, wheels, rider
|
||||
and camera share the same interpolated fixed-step motion sample, avoiding relative
|
||||
jitter from separate camera damping. Resets discard the old motion sample.
|
||||
The board root is scaled away during vehicle playback and restored by the vanilla blend.
|
||||
The local Mario Kart package supplies fitted entry/exit, seated and steering clips;
|
||||
its proprietary rider.json is generated locally and is not included in source control.
|
||||
Reference in new issue
Block a user