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 kDebugRuntimeProxyScale = 1.50F;
constexpr float kProxyPositionLerpAlpha = 0.15F;
constexpr float kProxyRotationLerpAlpha = 0.25F;
constexpr std::size_t kMaxRuntimeProxyActors = 4;
constexpr float kDisconnectedProxyReuseDelaySeconds = 30.0F;
// 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
// 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
// Phase 1: Let the proxy's natural AI/character controller drive movement
// This allows Move() to be called, which updates velocity and plays animations
@@ -563,8 +566,6 @@ namespace
bool a_shouldSnap,
ProxyActorSlot* a_optSlot = nullptr)
{
(void)a_remotePlayer;
// DEBUG: Log AI state once per proxy
if (g_loggedProxies.find(&a_proxy) == g_loggedProxies.end()) {
g_loggedProxies.insert(&a_proxy);
@@ -605,8 +606,27 @@ namespace
a_optSlot->interpolationComponent.Clear();
}
// Always apply authoritative heading directly.
a_proxy.SetHeading(a_heading);
// Slot-backed proxies get per-frame rotation smoothing in ApplySmoothFrameMovement.
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);
// 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) {
LogInfoWithLocalPlayerPrefix(std::format(
"Proxy actor smoothing enabled for normal movement with position lerp alpha {:.2f}. Rotation still snaps; TODO: add wrapped angle smoothing later.",
kProxyPositionLerpAlpha));
"Proxy actor smoothing enabled for normal movement with position lerp alpha {:.2f} and wrapped rotation lerp alpha {:.2f}.",
kProxyPositionLerpAlpha,
kProxyRotationLerpAlpha));
g_proxySmoothingEnabledLogged = true;
}
}
@@ -5391,8 +5412,19 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
RE::NiPoint3 currentPosition = proxy->GetPosition();
RE::NiPoint3 deltaToTarget = slot.targetPosition - currentPosition;
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();
float frameDeltaSeconds = 1.0F / 60.0F;
@@ -5416,18 +5448,17 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
F4T::ProxyPuppet::SetProxyLocomotionState(
proxy->GetFormID(),
-1.0F,
slot.puppetDirection);
headingToApply);
F4T::ProxyPuppet::ApplyProxyLocomotionFrame(
*proxy,
RE::NiPoint3{},
frameDeltaSeconds,
-1.0F,
slot.puppetDirection);
headingToApply);
SyncProxyAnimationFromMotion(slot, *proxy, 0.0F);
continue;
}
constexpr float kLargeDriftSnapDistance = 1500.0F;
RE::NiPoint3 nextPosition;
if (distanceToTarget > kLargeDriftSnapDistance) {
nextPosition = slot.targetPosition;
@@ -5452,7 +5483,7 @@ bool IsConsolePlaceAtMeCandidateActor(const RE::Actor& a_actor, const RE::Player
frameDelta,
frameDeltaSeconds,
slot.puppetGraphSpeed,
slot.puppetDirection);
headingToApply);
const float horizontalDelta =
std::sqrt((frameDelta.x * frameDelta.x) + (frameDelta.y * frameDelta.y));