From 1ab9f300b616bf5cf67364f95e5cf131b93ee0b2 Mon Sep 17 00:00:00 2001 From: Vladislav Khorev Date: Fri, 6 Nov 2026 15:33:15 +0600 Subject: [PATCH] Also interpolate taxi and interactive objects --- CHARACTER_INTERPOLATION.md | 9 +++ src/Location.cpp | 88 ++++++++++++++++++++----- src/Location.h | 16 +++++ src/items/InteractiveObject.cpp | 110 ++++++++++++++++++++++++++++---- src/items/InteractiveObject.h | 18 ++++++ 5 files changed, 213 insertions(+), 28 deletions(-) diff --git a/CHARACTER_INTERPOLATION.md b/CHARACTER_INTERPOLATION.md index cd4713e..8eefa74 100644 --- a/CHARACTER_INTERPOLATION.md +++ b/CHARACTER_INTERPOLATION.md @@ -44,6 +44,15 @@ Dialogue typewriter text and cutscene presentation timers use the same per-render-frame path. This keeps character reveals, subtitle timing, fades, and cutscene image poses smooth instead of stepping in 100 ms increments. +Interactive objects retain authoritative move, rotate, scale, and fade tasks on +the simulation tick. Their previous and current visual transforms are sampled +at tick boundaries and interpolated only while drawing, so doors and similar +objects remain logically deterministic while their presentation is smooth. + +The same render-only sampling is used for `NpcCar` taxi visuals. Its position, +body rotation, wheel rotation, and steering angle are interpolated for drawing; +the authoritative taxi movement and waypoint logic remain fixed-step. + ## Animation skinning GPU and CPU bone interpolation receive the fractional render frame instead of an diff --git a/src/Location.cpp b/src/Location.cpp index a6b2cb9..f43416d 100644 --- a/src/Location.cpp +++ b/src/Location.cpp @@ -1134,12 +1134,8 @@ namespace FRG drawInstancedStatics("shadow_depth_inst", InstancedTexture::None); for (auto& intObj : interactiveObjects) { - if (intObj.state.castShadow && intObj.loadedObject.texture) { - renderer.PushMatrix(); - renderer.TranslateMatrix(intObj.state.position); - renderer.DrawVertexRenderStruct(intObj.loadedObject.mesh); - renderer.PopMatrix(); - } + if (intObj.state.castShadow) + intObj.drawShadowDepth(renderer); } // Draw characters (they handle their own skinning shader switch internally) @@ -1431,12 +1427,8 @@ namespace FRG } drawInstancedStatics("shadow_depth_inst", InstancedTexture::None); for (auto& intObj : interactiveObjects) { - if (intObj.state.castShadowNight && intObj.loadedObject.texture) { - renderer.PushMatrix(); - renderer.TranslateMatrix(intObj.state.position); - renderer.DrawVertexRenderStruct(intObj.loadedObject.mesh); - renderer.PopMatrix(); - } + if (intObj.state.castShadowNight) + intObj.drawShadowDepth(renderer); } if (player) player->drawShadowDepth(renderer); for (auto& [id, remote] : remotePlayers_) @@ -1453,14 +1445,75 @@ namespace FRG shadowMap->unbind(); } + void Location::captureNpcCarPresentation(const NpcCar& car) + { + NpcCarPresentationSample sample; + sample.position = car.position; + sample.rotation = car.rotation; + sample.wheelAngle = car.wheelAngle; + sample.steeringAngle = car.steeringAngle; + + const auto now = std::chrono::steady_clock::now(); + const bool largeGap = hasNpcCarPresentationSample_ && + std::chrono::duration_cast( + now - lastNpcCarPresentationUpdate_).count() > 250; + if (!hasNpcCarPresentationSample_ || largeGap) { + previousNpcCarPresentation_ = sample; + } else { + previousNpcCarPresentation_ = currentNpcCarPresentation_; + } + currentNpcCarPresentation_ = sample; + lastNpcCarPresentationUpdate_ = now; + hasNpcCarPresentationSample_ = true; + } + + Location::NpcCarPresentationSample + Location::interpolatedNpcCarPresentation(const NpcCar& car) const + { + NpcCarPresentationSample result; + result.position = car.position; + result.rotation = car.rotation; + result.wheelAngle = car.wheelAngle; + result.steeringAngle = car.steeringAngle; + if (!hasNpcCarPresentationSample_) return result; + + constexpr float kSimulationTickMs = 100.0f; + const float elapsedMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now() - lastNpcCarPresentationUpdate_).count()); + const float alpha = (std::max)(0.0f, + (std::min)(1.0f, elapsedMs / kSimulationTickMs)); + + result.position = previousNpcCarPresentation_.position + + alpha * (currentNpcCarPresentation_.position - + previousNpcCarPresentation_.position); + result.wheelAngle = previousNpcCarPresentation_.wheelAngle + + alpha * (currentNpcCarPresentation_.wheelAngle - + previousNpcCarPresentation_.wheelAngle); + result.steeringAngle = previousNpcCarPresentation_.steeringAngle + + alpha * (currentNpcCarPresentation_.steeringAngle - + previousNpcCarPresentation_.steeringAngle); + + constexpr float kPi = 3.14159265358979323846f; + constexpr float kTwoPi = 2.0f * kPi; + float rotationDelta = currentNpcCarPresentation_.rotation - + previousNpcCarPresentation_.rotation; + while (rotationDelta > kPi) rotationDelta -= kTwoPi; + while (rotationDelta < -kPi) rotationDelta += kTwoPi; + result.rotation = previousNpcCarPresentation_.rotation + alpha * rotationDelta; + return result; + } + void Location::drawNpcCar() { auto locationState = stateSnapshot(); if (!locationState) return; + const NpcCarPresentationSample car = + interpolatedNpcCarPresentation(locationState->taxiCar); renderer.PushMatrix(); - renderer.TranslateMatrix(locationState->taxiCar.position); + renderer.TranslateMatrix(car.position); renderer.TranslateMatrix({ 0, 0.7f - 0.25f +x, 0 }); - renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->taxiCar.rotation, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(car.rotation, Eigen::Vector3f::UnitY())).toRotationMatrix()); glBindTexture(GL_TEXTURE_2D, carTexture->getTexID()); renderer.DrawVertexRenderStruct(carMesh); @@ -1480,10 +1533,10 @@ namespace FRG for (int i = 0; i < 4; ++i) { renderer.PushMatrix(); renderer.TranslateMatrix(wheelPositions[i]); - renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->taxiCar.wheelAngle, -Eigen::Vector3f::UnitX())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(car.wheelAngle, -Eigen::Vector3f::UnitX())).toRotationMatrix()); if (isFront[i]) { - renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(locationState->taxiCar.steeringAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); + renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(car.steeringAngle, Eigen::Vector3f::UnitY())).toRotationMatrix()); } renderer.DrawVertexRenderStruct(carWheelMesh); renderer.PopMatrix(); @@ -1800,6 +1853,7 @@ namespace FRG if (!locationState) return; auto gameSnapshot = gameServer_.snapshot(); if (!gameSnapshot) return; + captureNpcCarPresentation(locationState->taxiCar); if (events.taxiArrived) scriptEngine.callCallTaxiCallback(); @@ -1936,6 +1990,8 @@ namespace FRG } } + for (auto& intObj : interactiveObjects) + intObj.capturePresentationState(); } void Location::handleDown(int64_t fingerId, int eventX, int eventY, int mx, int my) diff --git a/src/Location.h b/src/Location.h index 7905e62..1215c05 100644 --- a/src/Location.h +++ b/src/Location.h @@ -19,6 +19,7 @@ #include "LocationGameState.h" #include #include +#include #include #include "Character.h" @@ -265,6 +266,21 @@ namespace FRG PathVisualization pathVisualization; + struct NpcCarPresentationSample { + Eigen::Vector3f position = Eigen::Vector3f::Zero(); + float rotation = 0.f; + float wheelAngle = 0.f; + float steeringAngle = 0.f; + }; + + NpcCarPresentationSample previousNpcCarPresentation_; + NpcCarPresentationSample currentNpcCarPresentation_; + std::chrono::steady_clock::time_point lastNpcCarPresentationUpdate_; + bool hasNpcCarPresentationSample_ = false; + + void captureNpcCarPresentation(const NpcCar& car); + NpcCarPresentationSample interpolatedNpcCarPresentation(const NpcCar& car) const; + }; } // namespace FRG diff --git a/src/items/InteractiveObject.cpp b/src/items/InteractiveObject.cpp index 0fb7cf6..c1a7b60 100644 --- a/src/items/InteractiveObject.cpp +++ b/src/items/InteractiveObject.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include namespace FRG { @@ -68,8 +70,72 @@ namespace FRG { } + void InteractiveObject::capturePresentationState() + { + PresentationSample sample; + sample.position = state.position; + sample.rotationY = state.rotationY; + sample.scale = state.scale; + sample.alpha = state.alpha; + sample.isActive = state.isActive; + + const auto now = std::chrono::steady_clock::now(); + const bool largeGap = hasPresentationSample_ && + std::chrono::duration_cast( + now - lastPresentationUpdate_).count() > 250; + if (!hasPresentationSample_ || largeGap || + currentPresentationSample_.isActive != sample.isActive) { + previousPresentationSample_ = sample; + } else { + previousPresentationSample_ = currentPresentationSample_; + } + currentPresentationSample_ = sample; + lastPresentationUpdate_ = now; + hasPresentationSample_ = true; + } + + InteractiveObject::PresentationSample + InteractiveObject::interpolatedPresentationState() const + { + PresentationSample result; + result.position = state.position; + result.rotationY = state.rotationY; + result.scale = state.scale; + result.alpha = state.alpha; + result.isActive = state.isActive; + if (!hasPresentationSample_) return result; + + constexpr float kSimulationTickMs = 100.0f; + const float elapsedMs = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now() - lastPresentationUpdate_).count()); + const float alpha = (std::max)(0.0f, + (std::min)(1.0f, elapsedMs / kSimulationTickMs)); + + result.position = previousPresentationSample_.position + + alpha * (currentPresentationSample_.position - + previousPresentationSample_.position); + result.scale = previousPresentationSample_.scale + + alpha * (currentPresentationSample_.scale - + previousPresentationSample_.scale); + result.alpha = previousPresentationSample_.alpha + + alpha * (currentPresentationSample_.alpha - + previousPresentationSample_.alpha); + + constexpr float kPi = 3.14159265358979323846f; + constexpr float kTwoPi = 2.0f * kPi; + float rotationDelta = currentPresentationSample_.rotationY - + previousPresentationSample_.rotationY; + while (rotationDelta > kPi) rotationDelta -= kTwoPi; + while (rotationDelta < -kPi) rotationDelta += kTwoPi; + result.rotationY = previousPresentationSample_.rotationY + + alpha * rotationDelta; + return result; + } + void InteractiveObject::drawDarklands(Renderer& renderer) const { if (!state.isActive || !loadedObject.textureDarklands) return; + const PresentationSample presentation = interpolatedPresentationState(); if (state.alpha < 0.999f) { glEnable(GL_BLEND); @@ -77,16 +143,16 @@ namespace FRG { } renderer.PushMatrix(); - renderer.TranslateMatrix(state.position); - if (state.rotationY != 0.f) { + renderer.TranslateMatrix(presentation.position); + if (presentation.rotationY != 0.f) { renderer.TranslateMatrix(state.pivot); - renderer.RotateMatrix(Eigen::AngleAxisf(state.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix()); + renderer.RotateMatrix(Eigen::AngleAxisf(presentation.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix()); renderer.TranslateMatrix(-state.pivot); } - if (state.scale != 1.f) - renderer.ScaleMatrix(state.scale); + if (presentation.scale != 1.f) + renderer.ScaleMatrix(presentation.scale); renderer.RenderUniform1i(textureUniformName, 0); - renderer.RenderUniform1f("uAlpha", state.alpha); + renderer.RenderUniform1f("uAlpha", presentation.alpha); glBindTexture(GL_TEXTURE_2D, loadedObject.textureDarklands->getTexID()); renderer.DrawVertexRenderStruct(loadedObject.mesh); renderer.PopMatrix(); @@ -96,6 +162,7 @@ namespace FRG { void InteractiveObject::draw(Renderer& renderer) const { if (!state.isActive || !loadedObject.texture) return; + const PresentationSample presentation = interpolatedPresentationState(); if (state.alpha < 0.999f) { glEnable(GL_BLEND); @@ -103,16 +170,16 @@ namespace FRG { } renderer.PushMatrix(); - renderer.TranslateMatrix(state.position); - if (state.rotationY != 0.f) { + renderer.TranslateMatrix(presentation.position); + if (presentation.rotationY != 0.f) { renderer.TranslateMatrix(state.pivot); - renderer.RotateMatrix(Eigen::AngleAxisf(state.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix()); + renderer.RotateMatrix(Eigen::AngleAxisf(presentation.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix()); renderer.TranslateMatrix(-state.pivot); } - if (state.scale != 1.f) - renderer.ScaleMatrix(state.scale); + if (presentation.scale != 1.f) + renderer.ScaleMatrix(presentation.scale); renderer.RenderUniform1i(textureUniformName, 0); - renderer.RenderUniform1f("uAlpha", state.alpha); + renderer.RenderUniform1f("uAlpha", presentation.alpha); glBindTexture(GL_TEXTURE_2D, loadedObject.texture->getTexID()); renderer.DrawVertexRenderStruct(loadedObject.mesh); renderer.PopMatrix(); @@ -120,4 +187,23 @@ namespace FRG { renderer.RenderUniform1f("uAlpha", 1.0f); } + void InteractiveObject::drawShadowDepth(Renderer& renderer) const + { + if (!state.isActive) return; + const PresentationSample presentation = interpolatedPresentationState(); + + renderer.PushMatrix(); + renderer.TranslateMatrix(presentation.position); + if (presentation.rotationY != 0.f) { + renderer.TranslateMatrix(state.pivot); + renderer.RotateMatrix(Eigen::AngleAxisf( + presentation.rotationY, Eigen::Vector3f::UnitY()).toRotationMatrix()); + renderer.TranslateMatrix(-state.pivot); + } + if (presentation.scale != 1.f) + renderer.ScaleMatrix(presentation.scale); + renderer.DrawVertexRenderStruct(loadedObject.mesh); + renderer.PopMatrix(); + } + } // namespace FRG diff --git a/src/items/InteractiveObject.h b/src/items/InteractiveObject.h index acc4f58..88f5b49 100644 --- a/src/items/InteractiveObject.h +++ b/src/items/InteractiveObject.h @@ -4,6 +4,7 @@ #include #include "render/Renderer.h" #include "InteractiveObjectState.h" +#include namespace FRG { @@ -30,6 +31,8 @@ namespace FRG { void draw(Renderer& renderer) const; void drawDarklands(Renderer& renderer) const; + void drawShadowDepth(Renderer& renderer) const; + void capturePresentationState(); const std::string& entityId() const { return id; } private: @@ -38,6 +41,21 @@ namespace FRG { friend class LocationEditor; friend class ScriptEngine; InteractiveObjectState& state; // non-owning binding to LocationGameState + + struct PresentationSample { + Eigen::Vector3f position = Eigen::Vector3f::Zero(); + float rotationY = 0.f; + float scale = 1.f; + float alpha = 1.f; + bool isActive = true; + }; + + PresentationSample previousPresentationSample_; + PresentationSample currentPresentationSample_; + std::chrono::steady_clock::time_point lastPresentationUpdate_; + bool hasPresentationSample_ = false; + + PresentationSample interpolatedPresentationState() const; }; } // namespace FRG