mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-26 19:47:44 +00:00
Interpolate camera and skater presentation between fixed ticks
This commit is contained in:
1 parent
12ab6baa68
commit
1c8acbb010
7 files changed
+130
-18
No files matched your search
@@ -105,7 +105,8 @@ fn bind(
|
||||
}
|
||||
}
|
||||
fn present(
|
||||
skater: Res<SkaterRuntime>,
|
||||
history: Res<crate::presentation::Presentation>,
|
||||
time: Res<Time<Fixed>>,
|
||||
animation: Res<AnimationStatus>,
|
||||
mut nodes: Query<&mut Transform>,
|
||||
) {
|
||||
@@ -116,15 +117,19 @@ fn present(
|
||||
// rotated -90 degrees about X relative to the native frames. Both files'
|
||||
// world positions are Y-up. This is a skin basis change, not a physics turn.
|
||||
let basis = render_basis();
|
||||
let Some((previous, current)) = history.pair() else { return; };
|
||||
let alpha = time.overstep_fraction().clamp(0.0, 1.0);
|
||||
for binding in &animation.bindings {
|
||||
let global = native_matrix(skater.render_pose[binding.bone]) * basis;
|
||||
let local = if let Some(parent) = binding.parent_bone {
|
||||
(native_matrix(skater.render_pose[parent]) * basis).inverse() * global
|
||||
} else {
|
||||
global
|
||||
// Blend bone-local rotations, not matrix entries or independent world
|
||||
// positions: joints retain their hierarchy while limbs turn.
|
||||
let local = |snapshot: &crate::presentation::Snapshot| {
|
||||
let global = snapshot.bones[binding.bone] * basis;
|
||||
Transform::from_matrix(if let Some(parent) = binding.parent_bone {
|
||||
(snapshot.bones[parent] * basis).inverse() * global
|
||||
} else { global })
|
||||
};
|
||||
if let Ok(mut transform) = nodes.get_mut(binding.entity) {
|
||||
*transform = Transform::from_matrix(local);
|
||||
*transform = crate::presentation::blend(local(previous), local(current), alpha);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +90,7 @@ pub(crate) fn build(
|
||||
.add_plugins((
|
||||
input::InputPlugin,
|
||||
PhysicsPlugin,
|
||||
crate::presentation::PresentationPlugin,
|
||||
assets::GameAssetsPlugin,
|
||||
animation::AnimationPlugin,
|
||||
world::WorldPlugin,
|
||||
|
||||
@@ -48,19 +48,17 @@ fn spawn(mut commands: Commands, config: Res<Config>) {
|
||||
}
|
||||
|
||||
fn present(mut runtime: ResMut<CameraRuntime>, windows: Query<&Window>,
|
||||
history: Res<crate::presentation::Presentation>, time: Res<Time<Fixed>>,
|
||||
mut cameras: Query<(&mut Camera, &mut Transform, &mut Projection), With<GameplayCamera>>) {
|
||||
if let Ok(window) = windows.single() {
|
||||
runtime.set_aspect_ratio(window.width() / window.height());
|
||||
}
|
||||
let Some(frame) = runtime.frame else { return; };
|
||||
let Some((previous, current)) = history.pair() else { return; };
|
||||
let alpha = time.overstep_fraction().clamp(0.0, 1.0);
|
||||
for (mut camera, mut transform, mut projection) in &mut cameras {
|
||||
let [right, up, at] = frame.basis.columns.map(Vec3::from_array);
|
||||
// World rendering retains nativeXYZ. Native camera At points forward;
|
||||
// Bevy camera looks along local-Z, hence the two-axis basis conversion.
|
||||
transform.rotation = Quat::from_mat3(&Mat3::from_cols(-right, up, -at));
|
||||
transform.translation = Vec3::new(frame.position[0], frame.position[1], frame.position[2]);
|
||||
*transform = crate::presentation::blend(previous.camera, current.camera, alpha);
|
||||
if let Projection::Perspective(p) = &mut *projection {
|
||||
p.fov = frame.field_of_view_degrees.to_radians();
|
||||
p.fov = previous.fov + (current.fov - previous.fov) * alpha;
|
||||
}
|
||||
camera.is_active = true;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ mod verification;
|
||||
mod performance;
|
||||
mod graphics_menu;
|
||||
mod render_capacity;
|
||||
mod presentation;
|
||||
mod world;
|
||||
mod skate_world;
|
||||
|
||||
|
||||
@@ -336,15 +336,16 @@ mod difficulty_tests;
|
||||
|
||||
fn present(
|
||||
physics: Res<GamePhysics>,
|
||||
skater: Res<SkaterRuntime>,
|
||||
history: Res<crate::presentation::Presentation>,
|
||||
time: Res<Time<Fixed>>,
|
||||
mut roots: Query<&mut Transform, With<PlayerRoot>>,
|
||||
) {
|
||||
if physics.failed {
|
||||
return;
|
||||
}
|
||||
let Some((previous, current)) = history.pair() else { return; };
|
||||
for mut root in &mut roots {
|
||||
*root = Transform::from_matrix(crate::animation::native_matrix(
|
||||
skater.animated_skeleton.roots.animation_to_world,
|
||||
));
|
||||
*root = crate::presentation::blend(previous.root, current.root,
|
||||
time.overstep_fraction().clamp(0.0, 1.0));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
//! Render-only snapshots of completed fixed ticks. Camera, root and skin use
|
||||
//! the same interval; interpolated values never feed back into simulation.
|
||||
use bevy::prelude::*;
|
||||
use crate::{app::SimulationSet, camera::CameraRuntime, physics::SkaterRuntime};
|
||||
use skate_core::camera::CameraFrame;
|
||||
|
||||
pub(crate) struct PresentationPlugin;
|
||||
impl Plugin for PresentationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<Presentation>()
|
||||
.add_systems(FixedUpdate, capture.after(SimulationSet::Physics));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Snapshot {
|
||||
pub root: Transform,
|
||||
pub bones: Vec<Mat4>,
|
||||
pub camera: Transform,
|
||||
pub fov: f32,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub(crate) struct Presentation {
|
||||
pair: Option<(Snapshot, Snapshot)>,
|
||||
generation: u64,
|
||||
period: std::time::Duration,
|
||||
}
|
||||
impl Presentation {
|
||||
pub fn pair(&self) -> Option<(&Snapshot, &Snapshot)> {
|
||||
self.pair.as_ref().map(|(a, b)| (a, b))
|
||||
}
|
||||
}
|
||||
|
||||
fn capture(skater: Res<SkaterRuntime>, camera: Res<CameraRuntime>,
|
||||
time: Res<Time<Fixed>>, mut history: ResMut<Presentation>) {
|
||||
if skater.pose_generation == history.generation { return; }
|
||||
let Some(frame) = camera.frame else { return; };
|
||||
let next = Snapshot {
|
||||
root: Transform::from_matrix(crate::animation::native_matrix(
|
||||
skater.animated_skeleton.roots.animation_to_world)),
|
||||
bones: skater.render_pose.iter().copied()
|
||||
.map(crate::animation::native_matrix).collect(),
|
||||
camera: camera_transform(frame),
|
||||
fov: frame.field_of_view_degrees.to_radians(),
|
||||
};
|
||||
// Snap teleports/cuts and cadence changes instead of sweeping through the
|
||||
// world or blending snapshots across different simulation-clock periods.
|
||||
let reset = frame.discontinuity
|
||||
|| skater.player_input.processed.flags_2472 & (1 << 10) != 0
|
||||
|| history.period != time.timestep();
|
||||
if let Some((previous, current)) = history.pair.as_mut() {
|
||||
if reset || current.bones.len() != next.bones.len() {
|
||||
*previous = next.clone();
|
||||
} else {
|
||||
std::mem::swap(previous, current);
|
||||
}
|
||||
*current = next;
|
||||
} else {
|
||||
history.pair = Some((next.clone(), next));
|
||||
}
|
||||
history.generation = skater.pose_generation;
|
||||
history.period = time.timestep();
|
||||
}
|
||||
|
||||
fn camera_transform(frame: CameraFrame) -> Transform {
|
||||
let [right, up, at] = frame.basis.columns.map(Vec3::from_array);
|
||||
Transform {
|
||||
translation: Vec3::new(frame.position[0], frame.position[1], frame.position[2]),
|
||||
// Native At is forward; Bevy cameras look along local -Z.
|
||||
rotation: Quat::from_mat3(&Mat3::from_cols(-right, up, -at)).normalize(),
|
||||
..default()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) 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),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Render interpolation
|
||||
|
||||
The Bevy presentation layer retains two completed simulation snapshots and
|
||||
interpolates them with `Time<Fixed>::overstep_fraction()` in `Update`. Capture
|
||||
runs after the physics system on every fixed tick, including catch-up ticks.
|
||||
Frames without a new simulation tick still advance the interpolation fraction.
|
||||
|
||||
Snapshots contain the animation root, final physical skin hierarchy (including
|
||||
the board/trucks/wheels), and camera transform/FOV. Bone transforms are converted
|
||||
to skin-local transforms at each endpoint before translation/scale lerp and
|
||||
quaternion slerp. The root and camera use that same fraction. These rendered
|
||||
transforms are not read back by the native simulation or camera controllers.
|
||||
|
||||
Initialization, camera discontinuities, the existing camera-reset flag and
|
||||
simulation-period changes collapse the history to the newest snapshot. There
|
||||
is no boxcar camera filter, additional smoothing window, or extrapolation.
|
||||
Interpolation introduces one fixed interval of presentation delay (normally
|
||||
about 16.7 ms). This is a renderer feature, not a claim about retail behavior.
|
||||
|
||||
Reference: Bevy's official fixed-timestep presentation example:
|
||||
https://bevy.org/examples-webgpu/movement/physics-in-fixed-timestep/
|
||||
|
||||
Validation: `Build.ps1` succeeded; no automated or gameplay tests were run,
|
||||
as requested. Gameplay smoothness remains for the user to assess.
|
||||
Reference in new issue
Block a user