Also interpolate taxi and interactive objects

This commit is contained in:
Vladislav Khorev committed 2026-11-06 15:33:15 +06:00
1 parent 28846b9f25
commit 1ab9f300b6
5 files changed
+213 -28

No files matched your search

+9
View File
@@ -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
+72 -16
View File
@@ -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<std::chrono::milliseconds>(
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<float>(
std::chrono::duration_cast<std::chrono::milliseconds>(
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)
+16
View File
@@ -19,6 +19,7 @@
#include "LocationGameState.h"
#include <functional>
#include <cstdint>
#include <chrono>
#include <unordered_map>
#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
+98 -12
View File
@@ -5,6 +5,8 @@
#include <iostream>
#include <string>
#include <algorithm>
#include <chrono>
#include <cmath>
#include <Eigen/Geometry>
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<std::chrono::milliseconds>(
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<float>(
std::chrono::duration_cast<std::chrono::milliseconds>(
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
+18
View File
@@ -4,6 +4,7 @@
#include <Eigen/Core>
#include "render/Renderer.h"
#include "InteractiveObjectState.h"
#include <chrono>
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