Working on interpolation
This commit is contained in:
1 parent
23fef6f349
commit
28846b9f25
8 files changed
+231
-51
No files matched your search
@@ -0,0 +1,67 @@
|
||||
# Character Presentation Interpolation
|
||||
|
||||
The authoritative game simulation still advances in exact 100 ms ticks. Character
|
||||
interpolation is a rendering-only feature and does not modify `CharacterState`,
|
||||
network commands, tick IDs, state hashes, or deterministic lockstep behavior.
|
||||
|
||||
## How it works
|
||||
|
||||
Each runtime `Character` keeps two presentation samples:
|
||||
|
||||
- `previous`: the visual values received at the preceding completed simulation tick;
|
||||
- `current`: the visual values received at the latest completed simulation tick.
|
||||
|
||||
Rendering calculates an alpha from the real elapsed time since the latest completed
|
||||
tick, clamped to the 100 ms simulation interval. It renders the delayed
|
||||
`previous -> current` transition. This introduces at most one simulation tick of
|
||||
presentation latency, but it avoids predicting a state that has not arrived from
|
||||
the authoritative simulation.
|
||||
|
||||
The following values are interpolated:
|
||||
|
||||
- Position uses linear interpolation.
|
||||
- Facing angle uses the shortest angular path, including wraparound at ±π.
|
||||
- Animation frame remains fractional and is interpolated when both samples use
|
||||
the same animation state. Looping clips treat the last frame to first frame
|
||||
transition as a cyclic interval instead of interpolating backward through the
|
||||
entire clip.
|
||||
|
||||
The interpolated values are used by mesh, weapon, shadow, night-lighting, name,
|
||||
and health-bar rendering. The local camera and shadow-map center follow the same
|
||||
interpolated player position; otherwise the camera would jump to the newest
|
||||
authoritative position while the player model was still in its presentation
|
||||
transition. Gameplay queries, commands, callbacks, collision, and presentation
|
||||
events continue to use the authoritative snapshot.
|
||||
|
||||
## Other presentation animations
|
||||
|
||||
Teleport-zone star particles are advanced by `Location::updatePresentation()`
|
||||
once per render frame using the real frame delta. They are no longer advanced
|
||||
only from the 100 ms simulation-tick callback, so their visual motion remains
|
||||
continuous without affecting teleport activation or other authoritative state.
|
||||
|
||||
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.
|
||||
|
||||
## Animation skinning
|
||||
|
||||
GPU and CPU bone interpolation receive the fractional render frame instead of an
|
||||
integer-truncated frame. Bone positions are linearly interpolated and bone
|
||||
rotations use spherical interpolation as before. Frames at the end of a clip are
|
||||
clamped to the final keyframe.
|
||||
|
||||
## Discontinuities
|
||||
|
||||
The presentation buffer is reset instead of blended when a character is first
|
||||
observed, changes animation state or enabled state, or has not received a tick
|
||||
for a sufficiently long gap. This prevents stale positions, animation clips, or
|
||||
disabled characters from producing visible interpolation artifacts.
|
||||
|
||||
## Local and networked games
|
||||
|
||||
Local games continue accumulating wall-clock remainder time and executing exact
|
||||
100 ms simulation updates. Networked games continue consuming only contiguous
|
||||
server tick messages. Both paths publish completed snapshots to the same runtime
|
||||
presentation objects; interpolation only changes how those snapshots are drawn
|
||||
between updates.
|
||||
@@ -722,9 +722,10 @@ namespace FRG
|
||||
return -1;
|
||||
}
|
||||
|
||||
void BoneSystemNew::Interpolate(int frame)
|
||||
void BoneSystemNew::Interpolate(float frame)
|
||||
{
|
||||
int startingKeyFrame = -1;
|
||||
int nextKeyFrame = -1;
|
||||
for (int i = 0; i < static_cast<int>(animations[0].keyFrames.size()) - 1; i++)
|
||||
{
|
||||
int oldFrame = animations[0].keyFrames[i].frame;
|
||||
@@ -732,22 +733,29 @@ namespace FRG
|
||||
if (frame >= oldFrame && frame < nextFrame)
|
||||
{
|
||||
startingKeyFrame = i;
|
||||
nextKeyFrame = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startingKeyFrame == -1)
|
||||
{
|
||||
logger() << "Exception here: frame number is out of range of keyframes. Frame: " << frame << std::endl;
|
||||
throw std::runtime_error("Exception here");
|
||||
if (animations[0].keyFrames.size() < 2)
|
||||
return;
|
||||
startingKeyFrame = static_cast<int>(animations[0].keyFrames.size()) - 1;
|
||||
nextKeyFrame = 0;
|
||||
}
|
||||
|
||||
int modifiedFrameNumber = frame - animations[0].keyFrames[startingKeyFrame].frame;
|
||||
int diffFrames = animations[0].keyFrames[startingKeyFrame + 1].frame - animations[0].keyFrames[startingKeyFrame].frame;
|
||||
float t = (modifiedFrameNumber + 0.f) / diffFrames;
|
||||
float modifiedFrameNumber = frame - animations[0].keyFrames[startingKeyFrame].frame;
|
||||
int diffFrames = nextKeyFrame == 0
|
||||
? animations[0].keyFrames.back().frame -
|
||||
animations[0].keyFrames.front().frame + 1
|
||||
: animations[0].keyFrames[nextKeyFrame].frame -
|
||||
animations[0].keyFrames[startingKeyFrame].frame;
|
||||
float t = diffFrames > 0 ? modifiedFrameNumber / diffFrames : 0.0f;
|
||||
|
||||
std::vector<Bone>& oneFrameBones = animations[0].keyFrames[startingKeyFrame].bones;
|
||||
std::vector<Bone>& nextFrameBones = animations[0].keyFrames[startingKeyFrame + 1].bones;
|
||||
std::vector<Bone>& nextFrameBones = animations[0].keyFrames[nextKeyFrame].bones;
|
||||
|
||||
std::vector<Matrix4f> skinningMatrixForEachBone(currentBones.size());
|
||||
|
||||
@@ -906,9 +914,10 @@ namespace FRG
|
||||
}
|
||||
}
|
||||
|
||||
void GpuSkinningShaderDataNew::ComputeSkinningMatrices(const std::vector<Bone>& startBones, const std::vector<AnimationKeyFrame>& keyFrames, int frame)
|
||||
void GpuSkinningShaderDataNew::ComputeSkinningMatrices(const std::vector<Bone>& startBones, const std::vector<AnimationKeyFrame>& keyFrames, float frame)
|
||||
{
|
||||
int startingKeyFrame = -1;
|
||||
int nextKeyFrame = -1;
|
||||
for (size_t i = 0; i < keyFrames.size() - 1; i++)
|
||||
{
|
||||
int oldFrame = keyFrames[i].frame;
|
||||
@@ -916,6 +925,7 @@ namespace FRG
|
||||
if (frame >= oldFrame && frame < nextFrame)
|
||||
{
|
||||
startingKeyFrame = static_cast<int>(i);
|
||||
nextKeyFrame = static_cast<int>(i + 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -924,16 +934,23 @@ namespace FRG
|
||||
|
||||
if (startingKeyFrame == -1)
|
||||
{
|
||||
for (auto& m : skinningMatrices) m = Matrix4f::Identity();
|
||||
return;
|
||||
if (keyFrames.size() < 2)
|
||||
{
|
||||
for (auto& m : skinningMatrices) m = Matrix4f::Identity();
|
||||
return;
|
||||
}
|
||||
startingKeyFrame = static_cast<int>(keyFrames.size()) - 1;
|
||||
nextKeyFrame = 0;
|
||||
}
|
||||
|
||||
int modifiedFrameNumber = frame - keyFrames[startingKeyFrame].frame;
|
||||
int diffFrames = keyFrames[startingKeyFrame + 1].frame - keyFrames[startingKeyFrame].frame;
|
||||
float t = (modifiedFrameNumber + 0.f) / diffFrames;
|
||||
float modifiedFrameNumber = frame - keyFrames[startingKeyFrame].frame;
|
||||
int diffFrames = nextKeyFrame == 0
|
||||
? keyFrames.back().frame - keyFrames.front().frame + 1
|
||||
: keyFrames[nextKeyFrame].frame - keyFrames[startingKeyFrame].frame;
|
||||
float t = diffFrames > 0 ? modifiedFrameNumber / diffFrames : 0.0f;
|
||||
|
||||
const std::vector<Bone>& oneFrameBones = keyFrames[startingKeyFrame].bones;
|
||||
const std::vector<Bone>& nextFrameBones = keyFrames[startingKeyFrame + 1].bones;
|
||||
const std::vector<Bone>& nextFrameBones = keyFrames[nextKeyFrame].bones;
|
||||
|
||||
for (size_t i = 0; i < startBones.size(); i++)
|
||||
{
|
||||
|
||||
@@ -67,7 +67,7 @@ namespace FRG
|
||||
void LoadFromFile(const std::string& fileName, const std::string& ZIPFileName = "");
|
||||
void LoadFromBinaryFile(const std::string& fileName, const std::string& ZIPFileName = "");
|
||||
|
||||
void Interpolate(int frame);
|
||||
void Interpolate(float frame);
|
||||
|
||||
int findBoneIndex(const std::string& name) const;
|
||||
};
|
||||
@@ -94,14 +94,14 @@ namespace FRG
|
||||
|
||||
void prepareGpuSkinningVBOs(BoneSystemNew& model);
|
||||
void RenderVBO(Renderer& renderer, const std::vector<std::string>& meshNamesOrdered);
|
||||
void ComputeSkinningMatrices(const std::vector<Bone>& startBones, const std::vector<AnimationKeyFrame>& keyFrames, int frame);
|
||||
void ComputeSkinningMatrices(const std::vector<Bone>& startBones, const std::vector<AnimationKeyFrame>& keyFrames, float frame);
|
||||
};
|
||||
|
||||
struct BoneAnimationDataNew
|
||||
{
|
||||
BoneSystemNew model;
|
||||
float currentFrame = 0.f;
|
||||
int lastFrame = -1;
|
||||
float lastFrame = -1.0f;
|
||||
int totalFrames = 1;
|
||||
|
||||
GpuSkinningShaderDataNew gpuSkinningShaderData;
|
||||
|
||||
+98
-26
@@ -52,6 +52,11 @@ Eigen::Vector3f Character::getCurrentNavigationTarget() const {
|
||||
return state ? state->getCurrentNavigationTarget() : Eigen::Vector3f::Zero();
|
||||
}
|
||||
|
||||
Eigen::Vector3f Character::renderPosition() const {
|
||||
auto state = stateSnapshot();
|
||||
return state ? interpolatedState(*state).position : Eigen::Vector3f::Zero();
|
||||
}
|
||||
|
||||
float Character::getHp() const {
|
||||
auto state = stateSnapshot();
|
||||
return state ? state->getHp() : 0.f;
|
||||
@@ -163,6 +168,62 @@ AnimationState Character::resolveActiveState(const CharacterState& state) const
|
||||
return AnimationState::STAND;
|
||||
}
|
||||
|
||||
CharacterState Character::interpolatedState(const CharacterState& state) const {
|
||||
if (!hasPresentationSample_ || !currentPresentationSample_.enabled || !state.enabled) {
|
||||
return state;
|
||||
}
|
||||
|
||||
constexpr float kSimulationTickMs = 100.0f;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
const float elapsedMs = static_cast<float>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - lastPresentationUpdate_).count());
|
||||
const float alpha = (std::max)(0.0f, (std::min)(1.0f, elapsedMs / kSimulationTickMs));
|
||||
|
||||
CharacterState result = state;
|
||||
result.position = previousPresentationSample_.position +
|
||||
alpha * (currentPresentationSample_.position - previousPresentationSample_.position);
|
||||
|
||||
constexpr float kPi = 3.14159265358979323846f;
|
||||
constexpr float kTwoPi = 2.0f * kPi;
|
||||
float angleDelta = currentPresentationSample_.facingAngle -
|
||||
previousPresentationSample_.facingAngle;
|
||||
while (angleDelta > kPi) angleDelta -= kTwoPi;
|
||||
while (angleDelta < -kPi) angleDelta += kTwoPi;
|
||||
result.facingAngle = previousPresentationSample_.facingAngle + alpha * angleDelta;
|
||||
if (previousPresentationSample_.currentState == state.currentState &&
|
||||
currentPresentationSample_.currentState == state.currentState) {
|
||||
float previousFrame = previousPresentationSample_.animationFrame;
|
||||
float currentFrame = currentPresentationSample_.animationFrame;
|
||||
const auto animationIndex = static_cast<std::size_t>(state.currentState);
|
||||
const int frameCount = animationIndex < state.animationFrameCounts.size()
|
||||
? state.animationFrameCounts[animationIndex] : 1;
|
||||
if (frameCount > 1 && currentFrame < previousFrame)
|
||||
currentFrame += static_cast<float>(frameCount);
|
||||
result.animationFrame = previousFrame + alpha * (currentFrame - previousFrame);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Character::updateAnimationForRender(const CharacterState& state) {
|
||||
auto it = animations.find(state.currentState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
auto& anim = it->second;
|
||||
anim.currentFrame = (std::min)(
|
||||
state.animationFrame, static_cast<float>((std::max)(0, anim.totalFrames)));
|
||||
if (std::abs(anim.currentFrame - anim.lastFrame) <= 0.0001f) return;
|
||||
|
||||
if (!anim.model.animations.empty() && !anim.model.animations[0].keyFrames.empty()) {
|
||||
anim.gpuSkinningShaderData.ComputeSkinningMatrices(
|
||||
anim.model.startBones, anim.model.animations[0].keyFrames,
|
||||
anim.currentFrame);
|
||||
if (!state.useGpuSkinning)
|
||||
anim.model.Interpolate(anim.currentFrame);
|
||||
}
|
||||
anim.lastFrame = anim.currentFrame;
|
||||
}
|
||||
|
||||
void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
|
||||
if (hitSparkEmitter.isConfigured()) {
|
||||
hitSparkEmitter.update(static_cast<float>(deltaMs));
|
||||
@@ -172,6 +233,26 @@ void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
PresentationSample sample;
|
||||
sample.position = state.position;
|
||||
sample.facingAngle = state.facingAngle;
|
||||
sample.animationFrame = state.animationFrame;
|
||||
sample.currentState = state.currentState;
|
||||
sample.enabled = state.enabled;
|
||||
const bool largeGap = hasPresentationSample_ &&
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
now - lastPresentationUpdate_).count() > 250;
|
||||
if (!hasPresentationSample_ || largeGap ||
|
||||
currentPresentationSample_.enabled != sample.enabled) {
|
||||
previousPresentationSample_ = sample;
|
||||
} else {
|
||||
previousPresentationSample_ = currentPresentationSample_;
|
||||
}
|
||||
currentPresentationSample_ = sample;
|
||||
lastPresentationUpdate_ = now;
|
||||
hasPresentationSample_ = true;
|
||||
|
||||
if (events) {
|
||||
if (events->deathAnimationComplete && onDeathAnimComplete)
|
||||
onDeathAnimComplete();
|
||||
@@ -183,22 +264,7 @@ void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
|
||||
}
|
||||
|
||||
if (!state.enabled) return;
|
||||
auto it = animations.find(state.currentState);
|
||||
if (it == animations.end()) return;
|
||||
|
||||
auto& anim = it->second;
|
||||
anim.currentFrame = (std::min)(
|
||||
state.animationFrame, static_cast<float>((std::max)(0, anim.totalFrames - 1)));
|
||||
|
||||
if (static_cast<int>(anim.currentFrame) != anim.lastFrame) {
|
||||
anim.gpuSkinningShaderData.ComputeSkinningMatrices(
|
||||
anim.model.startBones, anim.model.animations[0].keyFrames,
|
||||
static_cast<int>(anim.currentFrame));
|
||||
if (!state.useGpuSkinning) {
|
||||
anim.model.Interpolate(static_cast<int>(anim.currentFrame));
|
||||
}
|
||||
anim.lastFrame = static_cast<int>(anim.currentFrame);
|
||||
}
|
||||
updateAnimationForRender(state);
|
||||
}
|
||||
|
||||
void Character::draw(Renderer& renderer) {
|
||||
@@ -211,8 +277,9 @@ void Character::draw(Renderer& renderer) {
|
||||
|
||||
auto stateSnapshot = this->stateSnapshot();
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
CharacterState state = interpolatedState(*stateSnapshot);
|
||||
if (!state.enabled) return;
|
||||
updateAnimationForRender(state);
|
||||
|
||||
if (state.useGpuSkinning) {
|
||||
drawGpuSkinning(renderer, state);
|
||||
@@ -270,7 +337,7 @@ void Character::drawGpuSkinning(Renderer& renderer, const CharacterState& state)
|
||||
if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty()) return;
|
||||
anim.gpuSkinningShaderData.ComputeSkinningMatrices(
|
||||
anim.model.startBones, anim.model.animations[0].keyFrames,
|
||||
static_cast<int>(anim.currentFrame));
|
||||
anim.currentFrame);
|
||||
if (anim.gpuSkinningShaderData.skinningMatrices.empty()) return;
|
||||
}
|
||||
|
||||
@@ -328,7 +395,7 @@ bool Character::prepareGpuSkinning(const CharacterState& state) {
|
||||
if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty()) return false;
|
||||
anim.gpuSkinningShaderData.ComputeSkinningMatrices(
|
||||
anim.model.startBones, anim.model.animations[0].keyFrames,
|
||||
static_cast<int>(anim.currentFrame));
|
||||
anim.currentFrame);
|
||||
if (anim.gpuSkinningShaderData.skinningMatrices.empty()) return false;
|
||||
}
|
||||
return true;
|
||||
@@ -370,8 +437,9 @@ void Character::drawAttachedWeapon(Renderer& renderer, const CharacterState& sta
|
||||
void Character::drawShadowDepth(Renderer& renderer) {
|
||||
auto stateSnapshot = this->stateSnapshot();
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
CharacterState state = interpolatedState(*stateSnapshot);
|
||||
if (!state.enabled) return;
|
||||
updateAnimationForRender(state);
|
||||
if (state.useGpuSkinning) {
|
||||
drawShadowDepthGpuSkinning(renderer, state);
|
||||
} else {
|
||||
@@ -453,8 +521,9 @@ void Character::drawShadowDepthGpuSkinning(Renderer& renderer, const CharacterSt
|
||||
void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
|
||||
auto stateSnapshot = this->stateSnapshot();
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
CharacterState state = interpolatedState(*stateSnapshot);
|
||||
if (!state.enabled) return;
|
||||
updateAnimationForRender(state);
|
||||
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||
}
|
||||
@@ -603,8 +672,9 @@ void Character::drawNight(Renderer& renderer,
|
||||
{
|
||||
auto stateSnapshot = this->stateSnapshot();
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
CharacterState state = interpolatedState(*stateSnapshot);
|
||||
if (!state.enabled) return;
|
||||
updateAnimationForRender(state);
|
||||
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||
}
|
||||
@@ -766,8 +836,9 @@ void Character::drawNightWithShadow(Renderer& renderer,
|
||||
{
|
||||
auto stateSnapshot = this->stateSnapshot();
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
CharacterState state = interpolatedState(*stateSnapshot);
|
||||
if (!state.enabled) return;
|
||||
updateAnimationForRender(state);
|
||||
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||
}
|
||||
@@ -1014,7 +1085,7 @@ void Character::drawName(TextRenderer& textRenderer,
|
||||
{
|
||||
auto stateSnapshot = this->stateSnapshot();
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
CharacterState state = interpolatedState(*stateSnapshot);
|
||||
if (state.isPlayer) return;
|
||||
if (state.npcName.empty()) return;
|
||||
|
||||
@@ -1037,10 +1108,11 @@ void Character::drawPlayerName(
|
||||
{
|
||||
auto snapshot = stateSnapshot();
|
||||
if (!snapshot) return;
|
||||
CharacterState state = interpolatedState(*snapshot);
|
||||
static const std::array<float, 4> color = {0.f, 1.f, 0.f, 1.f};
|
||||
drawCharacterNameLabel(
|
||||
textRenderer, cameraViewMatrix, projectionMatrix,
|
||||
*snapshot, displayName, color);
|
||||
state, displayName, color);
|
||||
}
|
||||
|
||||
void Character::drawHealthBar(Renderer& renderer,
|
||||
@@ -1049,7 +1121,7 @@ void Character::drawHealthBar(Renderer& renderer,
|
||||
{
|
||||
auto stateSnapshot = this->stateSnapshot();
|
||||
if (!stateSnapshot) return;
|
||||
const CharacterState& state = *stateSnapshot;
|
||||
CharacterState state = interpolatedState(*stateSnapshot);
|
||||
if (state.isPlayer || !state.canAttack) return;
|
||||
if (!state.enabled) return;
|
||||
if (state.hp <= 0.f) return;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <chrono>
|
||||
#include "dialogue/TranslationDatabase.h"
|
||||
#include "AudioPlayerAsync.h"
|
||||
|
||||
@@ -94,6 +95,7 @@ public:
|
||||
bool isMoving() const;
|
||||
Eigen::Vector3f getRequestedWalkTarget() const;
|
||||
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||
Eigen::Vector3f renderPosition() const;
|
||||
float getHp() const;
|
||||
|
||||
// ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ----
|
||||
@@ -124,7 +126,22 @@ private:
|
||||
VertexRenderStruct healthBarMesh;
|
||||
std::unordered_map<std::string, std::shared_ptr<Texture>> meshTextures;
|
||||
|
||||
struct PresentationSample {
|
||||
Eigen::Vector3f position = Eigen::Vector3f::Zero();
|
||||
float facingAngle = 0.0f;
|
||||
float animationFrame = 0.0f;
|
||||
AnimationState currentState = AnimationState::STAND;
|
||||
bool enabled = true;
|
||||
};
|
||||
|
||||
PresentationSample previousPresentationSample_;
|
||||
PresentationSample currentPresentationSample_;
|
||||
std::chrono::steady_clock::time_point lastPresentationUpdate_;
|
||||
bool hasPresentationSample_ = false;
|
||||
|
||||
AnimationState resolveActiveState(const CharacterState& state) const;
|
||||
CharacterState interpolatedState(const CharacterState& state) const;
|
||||
void updateAnimationForRender(const CharacterState& state);
|
||||
bool prepareGpuSkinning(const CharacterState& state);
|
||||
void drawAttachedWeapon(Renderer& renderer, const CharacterState& state);
|
||||
|
||||
|
||||
@@ -1194,6 +1194,8 @@ namespace FRG
|
||||
simulationAccumulatorMs -= kSimulationTickMs;
|
||||
}
|
||||
}
|
||||
if (Location* location = currentLocation())
|
||||
location->updatePresentation(delta);
|
||||
#if defined(NETWORK)
|
||||
if (remoteCampaignActive && websocketGameClient &&
|
||||
websocketGameClient->connectionFailed()) {
|
||||
|
||||
+12
-8
@@ -1011,7 +1011,7 @@ namespace FRG
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
const Eigen::Vector3f camTarget =
|
||||
playerState ? playerState->position : Eigen::Vector3f::Zero();
|
||||
player ? player->renderPosition() : Eigen::Vector3f::Zero();
|
||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||
|
||||
@@ -1088,7 +1088,7 @@ namespace FRG
|
||||
|
||||
auto playerState = player ? player->stateSnapshot() : nullptr;
|
||||
const Eigen::Vector3f sceneCenter =
|
||||
playerState ? playerState->position : Eigen::Vector3f::Zero();
|
||||
player ? player->renderPosition() : Eigen::Vector3f::Zero();
|
||||
shadowMap->updateLightSpaceMatrix(sceneCenter);
|
||||
|
||||
shadowMap->bind();
|
||||
@@ -1206,7 +1206,7 @@ namespace FRG
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
auto playerState = player ? player->stateSnapshot() : nullptr;
|
||||
const Eigen::Vector3f camTarget =
|
||||
playerState ? playerState->position : Eigen::Vector3f::Zero();
|
||||
player ? player->renderPosition() : Eigen::Vector3f::Zero();
|
||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||
|
||||
@@ -1345,7 +1345,7 @@ namespace FRG
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
const Eigen::Vector3f camTarget =
|
||||
playerState ? playerState->position : Eigen::Vector3f::Zero();
|
||||
player ? player->renderPosition() : Eigen::Vector3f::Zero();
|
||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||
|
||||
@@ -1591,7 +1591,7 @@ namespace FRG
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||
const Eigen::Vector3f camTarget =
|
||||
playerState ? playerState->position : Eigen::Vector3f::Zero();
|
||||
player ? player->renderPosition() : Eigen::Vector3f::Zero();
|
||||
renderer.TranslateMatrix({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||
|
||||
@@ -1860,7 +1860,6 @@ namespace FRG
|
||||
|
||||
if (player) {
|
||||
player->update(delta, eventFor(gameServer_.localPlayerId()));
|
||||
dialogueSystem.update(static_cast<int>(delta));
|
||||
auto playerState = player->stateSnapshot();
|
||||
if (playerState) updateTriggerZones(playerState->position);
|
||||
}
|
||||
@@ -1896,8 +1895,6 @@ namespace FRG
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& tz : teleportZones) tz.update(static_cast<float>(delta));
|
||||
|
||||
auto playerState = player ? player->stateSnapshot() : nullptr;
|
||||
if (auto* tz = getTargetTeleportZone(*locationState); tz && playerState) {
|
||||
float dist = (playerState->position - tz->position).norm();
|
||||
@@ -2179,6 +2176,13 @@ namespace FRG
|
||||
}
|
||||
}
|
||||
|
||||
void Location::updatePresentation(int64_t delta)
|
||||
{
|
||||
dialogueSystem.update(static_cast<int>(delta));
|
||||
for (auto& tz : teleportZones)
|
||||
tz.update(static_cast<float>(delta));
|
||||
}
|
||||
|
||||
bool Location::requestDialogueStart(const std::string& dialogueId)
|
||||
{
|
||||
return dialogueSystem.startDialogue(dialogueId);
|
||||
|
||||
@@ -210,6 +210,7 @@ namespace FRG
|
||||
bool setNavigationAreaAvailable(const std::string& areaName, bool available);
|
||||
|
||||
void update(int64_t deltaMs, const LocationSimulationEvents& events);
|
||||
void updatePresentation(int64_t deltaMs);
|
||||
// Purely visual feedback for click-triggered player routes. Disabling it
|
||||
// also cancels an animation that is currently playing.
|
||||
void setPathVisualizationEnabled(bool enabled);
|
||||
|
||||
Reference in new issue
Block a user