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;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BoneSystemNew::Interpolate(int frame)
|
void BoneSystemNew::Interpolate(float frame)
|
||||||
{
|
{
|
||||||
int startingKeyFrame = -1;
|
int startingKeyFrame = -1;
|
||||||
|
int nextKeyFrame = -1;
|
||||||
for (int i = 0; i < static_cast<int>(animations[0].keyFrames.size()) - 1; i++)
|
for (int i = 0; i < static_cast<int>(animations[0].keyFrames.size()) - 1; i++)
|
||||||
{
|
{
|
||||||
int oldFrame = animations[0].keyFrames[i].frame;
|
int oldFrame = animations[0].keyFrames[i].frame;
|
||||||
@@ -732,22 +733,29 @@ namespace FRG
|
|||||||
if (frame >= oldFrame && frame < nextFrame)
|
if (frame >= oldFrame && frame < nextFrame)
|
||||||
{
|
{
|
||||||
startingKeyFrame = i;
|
startingKeyFrame = i;
|
||||||
|
nextKeyFrame = i + 1;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startingKeyFrame == -1)
|
if (startingKeyFrame == -1)
|
||||||
{
|
{
|
||||||
logger() << "Exception here: frame number is out of range of keyframes. Frame: " << frame << std::endl;
|
if (animations[0].keyFrames.size() < 2)
|
||||||
throw std::runtime_error("Exception here");
|
return;
|
||||||
|
startingKeyFrame = static_cast<int>(animations[0].keyFrames.size()) - 1;
|
||||||
|
nextKeyFrame = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int modifiedFrameNumber = frame - animations[0].keyFrames[startingKeyFrame].frame;
|
float modifiedFrameNumber = frame - animations[0].keyFrames[startingKeyFrame].frame;
|
||||||
int diffFrames = animations[0].keyFrames[startingKeyFrame + 1].frame - animations[0].keyFrames[startingKeyFrame].frame;
|
int diffFrames = nextKeyFrame == 0
|
||||||
float t = (modifiedFrameNumber + 0.f) / diffFrames;
|
? 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>& 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());
|
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 startingKeyFrame = -1;
|
||||||
|
int nextKeyFrame = -1;
|
||||||
for (size_t i = 0; i < keyFrames.size() - 1; i++)
|
for (size_t i = 0; i < keyFrames.size() - 1; i++)
|
||||||
{
|
{
|
||||||
int oldFrame = keyFrames[i].frame;
|
int oldFrame = keyFrames[i].frame;
|
||||||
@@ -916,6 +925,7 @@ namespace FRG
|
|||||||
if (frame >= oldFrame && frame < nextFrame)
|
if (frame >= oldFrame && frame < nextFrame)
|
||||||
{
|
{
|
||||||
startingKeyFrame = static_cast<int>(i);
|
startingKeyFrame = static_cast<int>(i);
|
||||||
|
nextKeyFrame = static_cast<int>(i + 1);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -924,16 +934,23 @@ namespace FRG
|
|||||||
|
|
||||||
if (startingKeyFrame == -1)
|
if (startingKeyFrame == -1)
|
||||||
{
|
{
|
||||||
for (auto& m : skinningMatrices) m = Matrix4f::Identity();
|
if (keyFrames.size() < 2)
|
||||||
return;
|
{
|
||||||
|
for (auto& m : skinningMatrices) m = Matrix4f::Identity();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startingKeyFrame = static_cast<int>(keyFrames.size()) - 1;
|
||||||
|
nextKeyFrame = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int modifiedFrameNumber = frame - keyFrames[startingKeyFrame].frame;
|
float modifiedFrameNumber = frame - keyFrames[startingKeyFrame].frame;
|
||||||
int diffFrames = keyFrames[startingKeyFrame + 1].frame - keyFrames[startingKeyFrame].frame;
|
int diffFrames = nextKeyFrame == 0
|
||||||
float t = (modifiedFrameNumber + 0.f) / diffFrames;
|
? 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>& 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++)
|
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 LoadFromFile(const std::string& fileName, const std::string& ZIPFileName = "");
|
||||||
void LoadFromBinaryFile(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;
|
int findBoneIndex(const std::string& name) const;
|
||||||
};
|
};
|
||||||
@@ -94,14 +94,14 @@ namespace FRG
|
|||||||
|
|
||||||
void prepareGpuSkinningVBOs(BoneSystemNew& model);
|
void prepareGpuSkinningVBOs(BoneSystemNew& model);
|
||||||
void RenderVBO(Renderer& renderer, const std::vector<std::string>& meshNamesOrdered);
|
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
|
struct BoneAnimationDataNew
|
||||||
{
|
{
|
||||||
BoneSystemNew model;
|
BoneSystemNew model;
|
||||||
float currentFrame = 0.f;
|
float currentFrame = 0.f;
|
||||||
int lastFrame = -1;
|
float lastFrame = -1.0f;
|
||||||
int totalFrames = 1;
|
int totalFrames = 1;
|
||||||
|
|
||||||
GpuSkinningShaderDataNew gpuSkinningShaderData;
|
GpuSkinningShaderDataNew gpuSkinningShaderData;
|
||||||
|
|||||||
+98
-26
@@ -52,6 +52,11 @@ Eigen::Vector3f Character::getCurrentNavigationTarget() const {
|
|||||||
return state ? state->getCurrentNavigationTarget() : Eigen::Vector3f::Zero();
|
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 {
|
float Character::getHp() const {
|
||||||
auto state = stateSnapshot();
|
auto state = stateSnapshot();
|
||||||
return state ? state->getHp() : 0.f;
|
return state ? state->getHp() : 0.f;
|
||||||
@@ -163,6 +168,62 @@ AnimationState Character::resolveActiveState(const CharacterState& state) const
|
|||||||
return AnimationState::STAND;
|
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) {
|
void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
|
||||||
if (hitSparkEmitter.isConfigured()) {
|
if (hitSparkEmitter.isConfigured()) {
|
||||||
hitSparkEmitter.update(static_cast<float>(deltaMs));
|
hitSparkEmitter.update(static_cast<float>(deltaMs));
|
||||||
@@ -172,6 +233,26 @@ void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
|
|||||||
if (!stateSnapshot) return;
|
if (!stateSnapshot) return;
|
||||||
const CharacterState& state = *stateSnapshot;
|
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) {
|
||||||
if (events->deathAnimationComplete && onDeathAnimComplete)
|
if (events->deathAnimationComplete && onDeathAnimComplete)
|
||||||
onDeathAnimComplete();
|
onDeathAnimComplete();
|
||||||
@@ -183,22 +264,7 @@ void Character::update(int64_t deltaMs, const CharacterUpdateEvents* events) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!state.enabled) return;
|
if (!state.enabled) return;
|
||||||
auto it = animations.find(state.currentState);
|
updateAnimationForRender(state);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Character::draw(Renderer& renderer) {
|
void Character::draw(Renderer& renderer) {
|
||||||
@@ -211,8 +277,9 @@ void Character::draw(Renderer& renderer) {
|
|||||||
|
|
||||||
auto stateSnapshot = this->stateSnapshot();
|
auto stateSnapshot = this->stateSnapshot();
|
||||||
if (!stateSnapshot) return;
|
if (!stateSnapshot) return;
|
||||||
const CharacterState& state = *stateSnapshot;
|
CharacterState state = interpolatedState(*stateSnapshot);
|
||||||
if (!state.enabled) return;
|
if (!state.enabled) return;
|
||||||
|
updateAnimationForRender(state);
|
||||||
|
|
||||||
if (state.useGpuSkinning) {
|
if (state.useGpuSkinning) {
|
||||||
drawGpuSkinning(renderer, state);
|
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;
|
if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty()) return;
|
||||||
anim.gpuSkinningShaderData.ComputeSkinningMatrices(
|
anim.gpuSkinningShaderData.ComputeSkinningMatrices(
|
||||||
anim.model.startBones, anim.model.animations[0].keyFrames,
|
anim.model.startBones, anim.model.animations[0].keyFrames,
|
||||||
static_cast<int>(anim.currentFrame));
|
anim.currentFrame);
|
||||||
if (anim.gpuSkinningShaderData.skinningMatrices.empty()) return;
|
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;
|
if (anim.model.animations.empty() || anim.model.animations[0].keyFrames.empty()) return false;
|
||||||
anim.gpuSkinningShaderData.ComputeSkinningMatrices(
|
anim.gpuSkinningShaderData.ComputeSkinningMatrices(
|
||||||
anim.model.startBones, anim.model.animations[0].keyFrames,
|
anim.model.startBones, anim.model.animations[0].keyFrames,
|
||||||
static_cast<int>(anim.currentFrame));
|
anim.currentFrame);
|
||||||
if (anim.gpuSkinningShaderData.skinningMatrices.empty()) return false;
|
if (anim.gpuSkinningShaderData.skinningMatrices.empty()) return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -370,8 +437,9 @@ void Character::drawAttachedWeapon(Renderer& renderer, const CharacterState& sta
|
|||||||
void Character::drawShadowDepth(Renderer& renderer) {
|
void Character::drawShadowDepth(Renderer& renderer) {
|
||||||
auto stateSnapshot = this->stateSnapshot();
|
auto stateSnapshot = this->stateSnapshot();
|
||||||
if (!stateSnapshot) return;
|
if (!stateSnapshot) return;
|
||||||
const CharacterState& state = *stateSnapshot;
|
CharacterState state = interpolatedState(*stateSnapshot);
|
||||||
if (!state.enabled) return;
|
if (!state.enabled) return;
|
||||||
|
updateAnimationForRender(state);
|
||||||
if (state.useGpuSkinning) {
|
if (state.useGpuSkinning) {
|
||||||
drawShadowDepthGpuSkinning(renderer, state);
|
drawShadowDepthGpuSkinning(renderer, state);
|
||||||
} else {
|
} 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) {
|
void Character::drawWithShadow(Renderer& renderer, const Eigen::Matrix4f& lightFromCamera, GLuint shadowMapTex, const Eigen::Vector3f& lightDirCamera) {
|
||||||
auto stateSnapshot = this->stateSnapshot();
|
auto stateSnapshot = this->stateSnapshot();
|
||||||
if (!stateSnapshot) return;
|
if (!stateSnapshot) return;
|
||||||
const CharacterState& state = *stateSnapshot;
|
CharacterState state = interpolatedState(*stateSnapshot);
|
||||||
if (!state.enabled) return;
|
if (!state.enabled) return;
|
||||||
|
updateAnimationForRender(state);
|
||||||
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
||||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||||
}
|
}
|
||||||
@@ -603,8 +672,9 @@ void Character::drawNight(Renderer& renderer,
|
|||||||
{
|
{
|
||||||
auto stateSnapshot = this->stateSnapshot();
|
auto stateSnapshot = this->stateSnapshot();
|
||||||
if (!stateSnapshot) return;
|
if (!stateSnapshot) return;
|
||||||
const CharacterState& state = *stateSnapshot;
|
CharacterState state = interpolatedState(*stateSnapshot);
|
||||||
if (!state.enabled) return;
|
if (!state.enabled) return;
|
||||||
|
updateAnimationForRender(state);
|
||||||
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
||||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||||
}
|
}
|
||||||
@@ -766,8 +836,9 @@ void Character::drawNightWithShadow(Renderer& renderer,
|
|||||||
{
|
{
|
||||||
auto stateSnapshot = this->stateSnapshot();
|
auto stateSnapshot = this->stateSnapshot();
|
||||||
if (!stateSnapshot) return;
|
if (!stateSnapshot) return;
|
||||||
const CharacterState& state = *stateSnapshot;
|
CharacterState state = interpolatedState(*stateSnapshot);
|
||||||
if (!state.enabled) return;
|
if (!state.enabled) return;
|
||||||
|
updateAnimationForRender(state);
|
||||||
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
if (hitSparkEmitter.isConfigured() && hitSparkEmitter.getActiveParticleCount() > 0) {
|
||||||
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
hitSparkEmitter.draw(renderer, 1.0f, Environment::width, Environment::height, false);
|
||||||
}
|
}
|
||||||
@@ -1014,7 +1085,7 @@ void Character::drawName(TextRenderer& textRenderer,
|
|||||||
{
|
{
|
||||||
auto stateSnapshot = this->stateSnapshot();
|
auto stateSnapshot = this->stateSnapshot();
|
||||||
if (!stateSnapshot) return;
|
if (!stateSnapshot) return;
|
||||||
const CharacterState& state = *stateSnapshot;
|
CharacterState state = interpolatedState(*stateSnapshot);
|
||||||
if (state.isPlayer) return;
|
if (state.isPlayer) return;
|
||||||
if (state.npcName.empty()) return;
|
if (state.npcName.empty()) return;
|
||||||
|
|
||||||
@@ -1037,10 +1108,11 @@ void Character::drawPlayerName(
|
|||||||
{
|
{
|
||||||
auto snapshot = stateSnapshot();
|
auto snapshot = stateSnapshot();
|
||||||
if (!snapshot) return;
|
if (!snapshot) return;
|
||||||
|
CharacterState state = interpolatedState(*snapshot);
|
||||||
static const std::array<float, 4> color = {0.f, 1.f, 0.f, 1.f};
|
static const std::array<float, 4> color = {0.f, 1.f, 0.f, 1.f};
|
||||||
drawCharacterNameLabel(
|
drawCharacterNameLabel(
|
||||||
textRenderer, cameraViewMatrix, projectionMatrix,
|
textRenderer, cameraViewMatrix, projectionMatrix,
|
||||||
*snapshot, displayName, color);
|
state, displayName, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Character::drawHealthBar(Renderer& renderer,
|
void Character::drawHealthBar(Renderer& renderer,
|
||||||
@@ -1049,7 +1121,7 @@ void Character::drawHealthBar(Renderer& renderer,
|
|||||||
{
|
{
|
||||||
auto stateSnapshot = this->stateSnapshot();
|
auto stateSnapshot = this->stateSnapshot();
|
||||||
if (!stateSnapshot) return;
|
if (!stateSnapshot) return;
|
||||||
const CharacterState& state = *stateSnapshot;
|
CharacterState state = interpolatedState(*stateSnapshot);
|
||||||
if (state.isPlayer || !state.canAttack) return;
|
if (state.isPlayer || !state.canAttack) return;
|
||||||
if (!state.enabled) return;
|
if (!state.enabled) return;
|
||||||
if (state.hp <= 0.f) return;
|
if (state.hp <= 0.f) return;
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <chrono>
|
||||||
#include "dialogue/TranslationDatabase.h"
|
#include "dialogue/TranslationDatabase.h"
|
||||||
#include "AudioPlayerAsync.h"
|
#include "AudioPlayerAsync.h"
|
||||||
|
|
||||||
@@ -94,6 +95,7 @@ public:
|
|||||||
bool isMoving() const;
|
bool isMoving() const;
|
||||||
Eigen::Vector3f getRequestedWalkTarget() const;
|
Eigen::Vector3f getRequestedWalkTarget() const;
|
||||||
Eigen::Vector3f getCurrentNavigationTarget() const;
|
Eigen::Vector3f getCurrentNavigationTarget() const;
|
||||||
|
Eigen::Vector3f renderPosition() const;
|
||||||
float getHp() const;
|
float getHp() const;
|
||||||
|
|
||||||
// ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ----
|
// ---- Weapon rendering data (initialized from state.creationInfo by createFromState) ----
|
||||||
@@ -124,7 +126,22 @@ private:
|
|||||||
VertexRenderStruct healthBarMesh;
|
VertexRenderStruct healthBarMesh;
|
||||||
std::unordered_map<std::string, std::shared_ptr<Texture>> meshTextures;
|
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;
|
AnimationState resolveActiveState(const CharacterState& state) const;
|
||||||
|
CharacterState interpolatedState(const CharacterState& state) const;
|
||||||
|
void updateAnimationForRender(const CharacterState& state);
|
||||||
bool prepareGpuSkinning(const CharacterState& state);
|
bool prepareGpuSkinning(const CharacterState& state);
|
||||||
void drawAttachedWeapon(Renderer& renderer, const CharacterState& state);
|
void drawAttachedWeapon(Renderer& renderer, const CharacterState& state);
|
||||||
|
|
||||||
|
|||||||
@@ -1194,6 +1194,8 @@ namespace FRG
|
|||||||
simulationAccumulatorMs -= kSimulationTickMs;
|
simulationAccumulatorMs -= kSimulationTickMs;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (Location* location = currentLocation())
|
||||||
|
location->updatePresentation(delta);
|
||||||
#if defined(NETWORK)
|
#if defined(NETWORK)
|
||||||
if (remoteCampaignActive && websocketGameClient &&
|
if (remoteCampaignActive && websocketGameClient &&
|
||||||
websocketGameClient->connectionFailed()) {
|
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_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||||
const Eigen::Vector3f camTarget =
|
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({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||||
|
|
||||||
@@ -1088,7 +1088,7 @@ namespace FRG
|
|||||||
|
|
||||||
auto playerState = player ? player->stateSnapshot() : nullptr;
|
auto playerState = player ? player->stateSnapshot() : nullptr;
|
||||||
const Eigen::Vector3f sceneCenter =
|
const Eigen::Vector3f sceneCenter =
|
||||||
playerState ? playerState->position : Eigen::Vector3f::Zero();
|
player ? player->renderPosition() : Eigen::Vector3f::Zero();
|
||||||
shadowMap->updateLightSpaceMatrix(sceneCenter);
|
shadowMap->updateLightSpaceMatrix(sceneCenter);
|
||||||
|
|
||||||
shadowMap->bind();
|
shadowMap->bind();
|
||||||
@@ -1206,7 +1206,7 @@ namespace FRG
|
|||||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||||
auto playerState = player ? player->stateSnapshot() : nullptr;
|
auto playerState = player ? player->stateSnapshot() : nullptr;
|
||||||
const Eigen::Vector3f camTarget =
|
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({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
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_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||||
const Eigen::Vector3f camTarget =
|
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({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
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_->cameraInclination, Eigen::Vector3f::UnitX())).toRotationMatrix());
|
||||||
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
renderer.RotateMatrix(Eigen::Quaternionf(Eigen::AngleAxisf(localState_->cameraAzimuth, Eigen::Vector3f::UnitY())).toRotationMatrix());
|
||||||
const Eigen::Vector3f camTarget =
|
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({ -camTarget.x(), -camTarget.y(), -camTarget.z() });
|
||||||
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
renderer.TranslateMatrix({ 0, -1.f, 0 });
|
||||||
|
|
||||||
@@ -1860,7 +1860,6 @@ namespace FRG
|
|||||||
|
|
||||||
if (player) {
|
if (player) {
|
||||||
player->update(delta, eventFor(gameServer_.localPlayerId()));
|
player->update(delta, eventFor(gameServer_.localPlayerId()));
|
||||||
dialogueSystem.update(static_cast<int>(delta));
|
|
||||||
auto playerState = player->stateSnapshot();
|
auto playerState = player->stateSnapshot();
|
||||||
if (playerState) updateTriggerZones(playerState->position);
|
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;
|
auto playerState = player ? player->stateSnapshot() : nullptr;
|
||||||
if (auto* tz = getTargetTeleportZone(*locationState); tz && playerState) {
|
if (auto* tz = getTargetTeleportZone(*locationState); tz && playerState) {
|
||||||
float dist = (playerState->position - tz->position).norm();
|
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)
|
bool Location::requestDialogueStart(const std::string& dialogueId)
|
||||||
{
|
{
|
||||||
return dialogueSystem.startDialogue(dialogueId);
|
return dialogueSystem.startDialogue(dialogueId);
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ namespace FRG
|
|||||||
bool setNavigationAreaAvailable(const std::string& areaName, bool available);
|
bool setNavigationAreaAvailable(const std::string& areaName, bool available);
|
||||||
|
|
||||||
void update(int64_t deltaMs, const LocationSimulationEvents& events);
|
void update(int64_t deltaMs, const LocationSimulationEvents& events);
|
||||||
|
void updatePresentation(int64_t deltaMs);
|
||||||
// Purely visual feedback for click-triggered player routes. Disabling it
|
// Purely visual feedback for click-triggered player routes. Disabling it
|
||||||
// also cancels an animation that is currently playing.
|
// also cancels an animation that is currently playing.
|
||||||
void setPathVisualizationEnabled(bool enabled);
|
void setPathVisualizationEnabled(bool enabled);
|
||||||
|
|||||||
Reference in new issue
Block a user