fix(rendering): use separate timer for global sequence bones

Global sequence bones (hair, cape, physics) need time values spanning
their full duration (up to ~968733ms), but animationTime wraps at the
current animation's sequence duration (~2000ms for walk). This caused
vertex spikes projecting from fingers/neck/ponytail as bones got stuck
in the first ~2s of their loop. Add a separate globalSequenceTime
accumulator that is not wrapped at the animation duration.
This commit is contained in:
Kelsi Davis 2026-04-04 01:16:28 -07:00
parent f520511139
commit bde9bd20d8
2 changed files with 18 additions and 11 deletions

View file

@ -158,6 +158,7 @@ private:
uint32_t currentAnimationId = 0;
int currentSequenceIndex = -1; // Index into M2Model::sequences
float animationTime = 0.0f;
float globalSequenceTime = 0.0f; // Separate timer for global sequences (accumulates without wrapping at sequence duration)
bool animationLoop = true;
bool isDead = false; // Prevents movement while in death state
std::vector<glm::mat4> boneMatrices; // Current bone transforms
@ -206,8 +207,8 @@ private:
void calculateBindPose(M2ModelGPU& gpuModel);
void updateAnimation(CharacterInstance& instance, float deltaTime);
void calculateBoneMatrices(CharacterInstance& instance);
glm::mat4 getBoneTransform(const pipeline::M2Bone& bone, float time, int sequenceIndex,
const std::vector<uint32_t>& globalSeqDurations);
glm::mat4 getBoneTransform(const pipeline::M2Bone& bone, float animTime, float globalSeqTime,
int sequenceIndex, const std::vector<uint32_t>& globalSeqDurations);
glm::mat4 getModelMatrix(const CharacterInstance& instance) const;
void destroyModelGPU(M2ModelGPU& gpuModel, bool defer = false);
void destroyInstanceBones(CharacterInstance& inst, bool defer = false);

View file

@ -1690,6 +1690,9 @@ void CharacterRenderer::update(float deltaTime, const glm::vec3& cameraPos) {
float distSq = glm::distance2(inst.position, cameraPos);
if (distSq >= animUpdateRadiusSq) continue;
// Advance global sequence timer (accumulates independently of animation wrapping)
inst.globalSequenceTime += deltaTime * 1000.0f;
// Always advance animation time (cheap)
if (inst.cachedModel && !inst.cachedModel->data.sequences.empty()) {
if (inst.currentSequenceIndex < 0) {
@ -1852,8 +1855,10 @@ int CharacterRenderer::findKeyframeIndex(const std::vector<uint32_t>& timestamps
}
// Resolve sequence index and time for a track, handling global sequences.
// globalSeqTime is a separate accumulating timer that is NOT wrapped at the
// current animation's sequence duration, so global sequences get full range.
static void resolveTrackTime(const pipeline::M2AnimationTrack& track,
int seqIdx, float time,
int seqIdx, float animTime, float globalSeqTime,
const std::vector<uint32_t>& globalSeqDurations,
int& outSeqIdx, float& outTime) {
if (track.globalSequence >= 0 &&
@ -1861,14 +1866,14 @@ static void resolveTrackTime(const pipeline::M2AnimationTrack& track,
outSeqIdx = 0;
float dur = static_cast<float>(globalSeqDurations[track.globalSequence]);
if (dur > 0.0f) {
outTime = std::fmod(time, dur);
outTime = std::fmod(globalSeqTime, dur);
if (outTime < 0.0f) outTime += dur;
} else {
outTime = 0.0f;
}
} else {
outSeqIdx = seqIdx;
outTime = time;
outTime = animTime;
}
}
@ -1959,7 +1964,8 @@ void CharacterRenderer::calculateBoneMatrices(CharacterInstance& instance) {
// Local transform includes pivot bracket: T(pivot)*T*R*S*T(-pivot)
// At rest this is identity, so no separate bind pose is needed
glm::mat4 localTransform = getBoneTransform(bone, instance.animationTime, instance.currentSequenceIndex, gsd);
glm::mat4 localTransform = getBoneTransform(bone, instance.animationTime, instance.globalSequenceTime,
instance.currentSequenceIndex, gsd);
// Compose with parent
if (bone.parentBone >= 0 && static_cast<size_t>(bone.parentBone) < numBones) {
@ -1970,16 +1976,16 @@ void CharacterRenderer::calculateBoneMatrices(CharacterInstance& instance) {
}
}
glm::mat4 CharacterRenderer::getBoneTransform(const pipeline::M2Bone& bone, float time, int sequenceIndex,
const std::vector<uint32_t>& globalSeqDurations) {
glm::mat4 CharacterRenderer::getBoneTransform(const pipeline::M2Bone& bone, float animTime, float globalSeqTime,
int sequenceIndex, const std::vector<uint32_t>& globalSeqDurations) {
// Resolve global sequences: bones with globalSequence >= 0 use sequence 0
// with time wrapped at the global sequence duration, independent of the
// character's current animation.
int tSeq, rSeq, sSeq;
float tTime, rTime, sTime;
resolveTrackTime(bone.translation, sequenceIndex, time, globalSeqDurations, tSeq, tTime);
resolveTrackTime(bone.rotation, sequenceIndex, time, globalSeqDurations, rSeq, rTime);
resolveTrackTime(bone.scale, sequenceIndex, time, globalSeqDurations, sSeq, sTime);
resolveTrackTime(bone.translation, sequenceIndex, animTime, globalSeqTime, globalSeqDurations, tSeq, tTime);
resolveTrackTime(bone.rotation, sequenceIndex, animTime, globalSeqTime, globalSeqDurations, rSeq, rTime);
resolveTrackTime(bone.scale, sequenceIndex, animTime, globalSeqTime, globalSeqDurations, sSeq, sTime);
glm::vec3 translation = interpolateVec3(bone.translation, tSeq, tTime, glm::vec3(0.0f));
glm::quat rotation = interpolateQuat(bone.rotation, rSeq, rTime);