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(
|
fn present(
|
||||||
skater: Res<SkaterRuntime>,
|
history: Res<crate::presentation::Presentation>,
|
||||||
|
time: Res<Time<Fixed>>,
|
||||||
animation: Res<AnimationStatus>,
|
animation: Res<AnimationStatus>,
|
||||||
mut nodes: Query<&mut Transform>,
|
mut nodes: Query<&mut Transform>,
|
||||||
) {
|
) {
|
||||||
@@ -116,15 +117,19 @@ fn present(
|
|||||||
// rotated -90 degrees about X relative to the native frames. Both files'
|
// 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.
|
// world positions are Y-up. This is a skin basis change, not a physics turn.
|
||||||
let basis = render_basis();
|
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 {
|
for binding in &animation.bindings {
|
||||||
let global = native_matrix(skater.render_pose[binding.bone]) * basis;
|
// Blend bone-local rotations, not matrix entries or independent world
|
||||||
let local = if let Some(parent) = binding.parent_bone {
|
// positions: joints retain their hierarchy while limbs turn.
|
||||||
(native_matrix(skater.render_pose[parent]) * basis).inverse() * global
|
let local = |snapshot: &crate::presentation::Snapshot| {
|
||||||
} else {
|
let global = snapshot.bones[binding.bone] * basis;
|
||||||
global
|
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) {
|
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((
|
.add_plugins((
|
||||||
input::InputPlugin,
|
input::InputPlugin,
|
||||||
PhysicsPlugin,
|
PhysicsPlugin,
|
||||||
|
crate::presentation::PresentationPlugin,
|
||||||
assets::GameAssetsPlugin,
|
assets::GameAssetsPlugin,
|
||||||
animation::AnimationPlugin,
|
animation::AnimationPlugin,
|
||||||
world::WorldPlugin,
|
world::WorldPlugin,
|
||||||
|
|||||||
@@ -48,19 +48,17 @@ fn spawn(mut commands: Commands, config: Res<Config>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn present(mut runtime: ResMut<CameraRuntime>, windows: Query<&Window>,
|
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>>) {
|
mut cameras: Query<(&mut Camera, &mut Transform, &mut Projection), With<GameplayCamera>>) {
|
||||||
if let Ok(window) = windows.single() {
|
if let Ok(window) = windows.single() {
|
||||||
runtime.set_aspect_ratio(window.width() / window.height());
|
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 {
|
for (mut camera, mut transform, mut projection) in &mut cameras {
|
||||||
let [right, up, at] = frame.basis.columns.map(Vec3::from_array);
|
*transform = crate::presentation::blend(previous.camera, current.camera, alpha);
|
||||||
// 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]);
|
|
||||||
if let Projection::Perspective(p) = &mut *projection {
|
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;
|
camera.is_active = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ mod verification;
|
|||||||
mod performance;
|
mod performance;
|
||||||
mod graphics_menu;
|
mod graphics_menu;
|
||||||
mod render_capacity;
|
mod render_capacity;
|
||||||
|
mod presentation;
|
||||||
mod world;
|
mod world;
|
||||||
mod skate_world;
|
mod skate_world;
|
||||||
|
|
||||||
|
|||||||
@@ -336,15 +336,16 @@ mod difficulty_tests;
|
|||||||
|
|
||||||
fn present(
|
fn present(
|
||||||
physics: Res<GamePhysics>,
|
physics: Res<GamePhysics>,
|
||||||
skater: Res<SkaterRuntime>,
|
history: Res<crate::presentation::Presentation>,
|
||||||
|
time: Res<Time<Fixed>>,
|
||||||
mut roots: Query<&mut Transform, With<PlayerRoot>>,
|
mut roots: Query<&mut Transform, With<PlayerRoot>>,
|
||||||
) {
|
) {
|
||||||
if physics.failed {
|
if physics.failed {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let Some((previous, current)) = history.pair() else { return; };
|
||||||
for mut root in &mut roots {
|
for mut root in &mut roots {
|
||||||
*root = Transform::from_matrix(crate::animation::native_matrix(
|
*root = crate::presentation::blend(previous.root, current.root,
|
||||||
skater.animated_skeleton.roots.animation_to_world,
|
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