Smooth remote proxy rotation and conditional snapping

Add interpolation for remote proxy rotation and conditional snapping logic. Introduces kProxyRotationLerpAlpha and a ShouldSnapRemoteMovement declaration, and uses LerpRotation to smoothly apply headings for non-slot proxies (and slot-backed proxies in frame smoothing) unless movement type or large drift forces a snap. Adds a kLargeDriftSnapDistance threshold and threads a computed headingToApply through proxy locomotion/animation calls. Also updates the informational log to mention rotation lerp and removes an unused void cast.
This commit is contained in:
2026-06-07 16:34:21 +12:00
parent 8dfec0a4b3
commit 21c8bf9593
+42 -11
View File
@@ -84,6 +84,7 @@ namespace
constexpr auto kDebugRuntimeProxyOffsetZ = 0.0F; constexpr auto kDebugRuntimeProxyOffsetZ = 0.0F;
constexpr auto kDebugRuntimeProxyScale = 1.50F; constexpr auto kDebugRuntimeProxyScale = 1.50F;
constexpr float kProxyPositionLerpAlpha = 0.15F; constexpr float kProxyPositionLerpAlpha = 0.15F;
constexpr float kProxyRotationLerpAlpha = 0.25F;
constexpr std::size_t kMaxRuntimeProxyActors = 4; constexpr std::size_t kMaxRuntimeProxyActors = 4;
constexpr float kDisconnectedProxyReuseDelaySeconds = 30.0F; constexpr float kDisconnectedProxyReuseDelaySeconds = 30.0F;
// Keep inactive proxies inside the loaded test cell instead of disabling actor // Keep inactive proxies inside the loaded test cell instead of disabling actor
@@ -514,6 +515,8 @@ namespace
// proxy's active AIProcess and causes forward/back glitching. Proxy locomotion // proxy's active AIProcess and causes forward/back glitching. Proxy locomotion
// animation is driven by graph variables (Speed/speedSampled) instead of real velocity. // animation is driven by graph variables (Speed/speedSampled) instead of real velocity.
bool ShouldSnapRemoteMovement(std::string_view a_movementType);
// Hybrid movement strategy for AI-driven animations // Hybrid movement strategy for AI-driven animations
// Phase 1: Let the proxy's natural AI/character controller drive movement // Phase 1: Let the proxy's natural AI/character controller drive movement
// This allows Move() to be called, which updates velocity and plays animations // This allows Move() to be called, which updates velocity and plays animations
@@ -563,8 +566,6 @@ namespace
bool a_shouldSnap, bool a_shouldSnap,
ProxyActorSlot* a_optSlot = nullptr) ProxyActorSlot* a_optSlot = nullptr)
{ {
(void)a_remotePlayer;
// DEBUG: Log AI state once per proxy // DEBUG: Log AI state once per proxy
if (g_loggedProxies.find(&a_proxy) == g_loggedProxies.end()) { if (g_loggedProxies.find(&a_proxy) == g_loggedProxies.end()) {
g_loggedProxies.insert(&a_proxy); g_loggedProxies.insert(&a_proxy);
@@ -605,8 +606,27 @@ namespace
a_optSlot->interpolationComponent.Clear(); a_optSlot->interpolationComponent.Clear();
} }
// Always apply authoritative heading directly. // Slot-backed proxies get per-frame rotation smoothing in ApplySmoothFrameMovement.
a_proxy.SetHeading(a_heading); if (!a_optSlot) {
const bool shouldSnapHeading =
ShouldSnapRemoteMovement(a_remotePlayer.movementType) || distanceDrift >= kLargeDriftSnapDistance;
float headingToApply = a_heading;
if (!shouldSnapHeading) {
const float rotationAlpha = a_shouldSnap ?
kProxyRotationLerpAlpha :
std::clamp(
deltaSeconds * kInterpolationRatePerSecond,
kMinInterpolationAlpha,
kMaxInterpolationAlpha);
headingToApply = F4T::InterpolationSystem::LerpRotation(
a_proxy.GetHeading(),
a_heading,
rotationAlpha);
}
a_proxy.SetHeading(headingToApply);
}
a_proxy.SetPosition(positionToApply, true); a_proxy.SetPosition(positionToApply, true);
// Character controller velocity is injected once per frame in ApplySmoothFrameMovement // Character controller velocity is injected once per frame in ApplySmoothFrameMovement
@@ -4307,8 +4327,9 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
} }
if (!g_proxySmoothingEnabledLogged) { if (!g_proxySmoothingEnabledLogged) {
LogInfoWithLocalPlayerPrefix(std::format( LogInfoWithLocalPlayerPrefix(std::format(
"Proxy actor smoothing enabled for normal movement with position lerp alpha {:.2f}. Rotation still snaps; TODO: add wrapped angle smoothing later.", "Proxy actor smoothing enabled for normal movement with position lerp alpha {:.2f} and wrapped rotation lerp alpha {:.2f}.",
kProxyPositionLerpAlpha)); kProxyPositionLerpAlpha,
kProxyRotationLerpAlpha));
g_proxySmoothingEnabledLogged = true; g_proxySmoothingEnabledLogged = true;
} }
} }
@@ -5391,8 +5412,19 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
RE::NiPoint3 currentPosition = proxy->GetPosition(); RE::NiPoint3 currentPosition = proxy->GetPosition();
RE::NiPoint3 deltaToTarget = slot.targetPosition - currentPosition; RE::NiPoint3 deltaToTarget = slot.targetPosition - currentPosition;
float distanceToTarget = deltaToTarget.Length(); float distanceToTarget = deltaToTarget.Length();
constexpr float kLargeDriftSnapDistance = 1500.0F;
proxy->SetHeading(slot.targetHeading); const bool shouldSnapHeading =
slot.lastRemotePlayerSnapshot &&
ShouldSnapRemoteMovement(slot.lastRemotePlayerSnapshot->movementType);
float headingToApply = slot.targetHeading;
if (!shouldSnapHeading && distanceToTarget < kLargeDriftSnapDistance) {
headingToApply = F4T::InterpolationSystem::LerpRotation(
proxy->GetHeading(),
slot.targetHeading,
kProxyRotationLerpAlpha);
}
proxy->SetHeading(headingToApply);
const auto frameNow = std::chrono::steady_clock::now(); const auto frameNow = std::chrono::steady_clock::now();
float frameDeltaSeconds = 1.0F / 60.0F; float frameDeltaSeconds = 1.0F / 60.0F;
@@ -5416,18 +5448,17 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
F4T::ProxyPuppet::SetProxyLocomotionState( F4T::ProxyPuppet::SetProxyLocomotionState(
proxy->GetFormID(), proxy->GetFormID(),
-1.0F, -1.0F,
slot.puppetDirection); headingToApply);
F4T::ProxyPuppet::ApplyProxyLocomotionFrame( F4T::ProxyPuppet::ApplyProxyLocomotionFrame(
*proxy, *proxy,
RE::NiPoint3{}, RE::NiPoint3{},
frameDeltaSeconds, frameDeltaSeconds,
-1.0F, -1.0F,
slot.puppetDirection); headingToApply);
SyncProxyAnimationFromMotion(slot, *proxy, 0.0F); SyncProxyAnimationFromMotion(slot, *proxy, 0.0F);
continue; continue;
} }
constexpr float kLargeDriftSnapDistance = 1500.0F;
RE::NiPoint3 nextPosition; RE::NiPoint3 nextPosition;
if (distanceToTarget > kLargeDriftSnapDistance) { if (distanceToTarget > kLargeDriftSnapDistance) {
nextPosition = slot.targetPosition; nextPosition = slot.targetPosition;
@@ -5452,7 +5483,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
frameDelta, frameDelta,
frameDeltaSeconds, frameDeltaSeconds,
slot.puppetGraphSpeed, slot.puppetGraphSpeed,
slot.puppetDirection); headingToApply);
const float horizontalDelta = const float horizontalDelta =
std::sqrt((frameDelta.x * frameDelta.x) + (frameDelta.y * frameDelta.y)); std::sqrt((frameDelta.x * frameDelta.x) + (frameDelta.y * frameDelta.y));