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 2026-04-03 22:49:33 -07:00
parent b54458fe6c
commit aeb295e0bb
2 changed files with 18 additions and 11 deletions

View file

@ -158,6 +158,7 @@ private:
uint32_t currentAnimationId = 0; uint32_t currentAnimationId = 0;
int currentSequenceIndex = -1; // Index into M2Model::sequences int currentSequenceIndex = -1; // Index into M2Model::sequences
float animationTime = 0.0f; float animationTime = 0.0f;
float globalSequenceTime = 0.0f; // Separate timer for global sequences (accumulates without wrapping at sequence duration)
bool animationLoop = true; bool animationLoop = true;
bool isDead = false; // Prevents movement while in death state bool isDead = false; // Prevents movement while in death state
std::vector<glm::mat4> boneMatrices; // Current bone transforms std::vector<glm::mat4> boneMatrices; // Current bone transforms
@ -206,8 +207,8 @@ private:
void calculateBindPose(M2ModelGPU& gpuModel); void calculateBindPose(M2ModelGPU& gpuModel);
void updateAnimation(CharacterInstance& instance, float deltaTime); void updateAnimation(CharacterInstance& instance, float deltaTime);
void calculateBoneMatrices(CharacterInstance& instance); void calculateBoneMatrices(CharacterInstance& instance);
glm::mat4 getBoneTransform(const pipeline::M2Bone& bone, float time, int sequenceIndex, glm::mat4 getBoneTransform(const pipeline::M2Bone& bone, float animTime, float globalSeqTime,
const std::vector<uint32_t>& globalSeqDurations); int sequenceIndex, const std::vector<uint32_t>& globalSeqDurations);
glm::mat4 getModelMatrix(const CharacterInstance& instance) const; glm::mat4 getModelMatrix(const CharacterInstance& instance) const;
void destroyModelGPU(M2ModelGPU& gpuModel, bool defer = false); void destroyModelGPU(M2ModelGPU& gpuModel, bool defer = false);
void destroyInstanceBones(CharacterInstance& inst, 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); float distSq = glm::distance2(inst.position, cameraPos);
if (distSq >= animUpdateRadiusSq) continue; if (distSq >= animUpdateRadiusSq) continue;
// Advance global sequence timer (accumulates independently of animation wrapping)
inst.globalSequenceTime += deltaTime * 1000.0f;
// Always advance animation time (cheap) // Always advance animation time (cheap)
if (inst.cachedModel && !inst.cachedModel->data.sequences.empty()) { if (inst.cachedModel && !inst.cachedModel->data.sequences.empty()) {
if (inst.currentSequenceIndex < 0) { 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. // 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, static void resolveTrackTime(const pipeline::M2AnimationTrack& track,
int seqIdx, float time, int seqIdx, float animTime, float globalSeqTime,
const std::vector<uint32_t>& globalSeqDurations, const std::vector<uint32_t>& globalSeqDurations,
int& outSeqIdx, float& outTime) { int& outSeqIdx, float& outTime) {
if (track.globalSequence >= 0 && if (track.globalSequence >= 0 &&
@ -1861,14 +1866,14 @@ static void resolveTrackTime(const pipeline::M2AnimationTrack& track,
outSeqIdx = 0; outSeqIdx = 0;
float dur = static_cast<float>(globalSeqDurations[track.globalSequence]); float dur = static_cast<float>(globalSeqDurations[track.globalSequence]);
if (dur > 0.0f) { if (dur > 0.0f) {
outTime = std::fmod(time, dur); outTime = std::fmod(globalSeqTime, dur);
if (outTime < 0.0f) outTime += dur; if (outTime < 0.0f) outTime += dur;
} else { } else {
outTime = 0.0f; outTime = 0.0f;
} }
} else { } else {
outSeqIdx = seqIdx; 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) // Local transform includes pivot bracket: T(pivot)*T*R*S*T(-pivot)
// At rest this is identity, so no separate bind pose is needed // 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 // Compose with parent
if (bone.parentBone >= 0 && static_cast<size_t>(bone.parentBone) < numBones) { 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, glm::mat4 CharacterRenderer::getBoneTransform(const pipeline::M2Bone& bone, float animTime, float globalSeqTime,
const std::vector<uint32_t>& globalSeqDurations) { int sequenceIndex, const std::vector<uint32_t>& globalSeqDurations) {
// Resolve global sequences: bones with globalSequence >= 0 use sequence 0 // Resolve global sequences: bones with globalSequence >= 0 use sequence 0
// with time wrapped at the global sequence duration, independent of the // with time wrapped at the global sequence duration, independent of the
// character's current animation. // character's current animation.
int tSeq, rSeq, sSeq; int tSeq, rSeq, sSeq;
float tTime, rTime, sTime; float tTime, rTime, sTime;
resolveTrackTime(bone.translation, sequenceIndex, time, globalSeqDurations, tSeq, tTime); resolveTrackTime(bone.translation, sequenceIndex, animTime, globalSeqTime, globalSeqDurations, tSeq, tTime);
resolveTrackTime(bone.rotation, sequenceIndex, time, globalSeqDurations, rSeq, rTime); resolveTrackTime(bone.rotation, sequenceIndex, animTime, globalSeqTime, globalSeqDurations, rSeq, rTime);
resolveTrackTime(bone.scale, sequenceIndex, time, globalSeqDurations, sSeq, sTime); resolveTrackTime(bone.scale, sequenceIndex, animTime, globalSeqTime, globalSeqDurations, sSeq, sTime);
glm::vec3 translation = interpolateVec3(bone.translation, tSeq, tTime, glm::vec3(0.0f)); glm::vec3 translation = interpolateVec3(bone.translation, tSeq, tTime, glm::vec3(0.0f));
glm::quat rotation = interpolateQuat(bone.rotation, rSeq, rTime); glm::quat rotation = interpolateQuat(bone.rotation, rSeq, rTime);