Skip to content

Keep motion EndTime absolute and rescale correctly on speed change#95

Open
FMsongX2 wants to merge 1 commit into
Live2D:developfrom
FMsongX2:fix/motion-speed-endtime
Open

Keep motion EndTime absolute and rescale correctly on speed change#95
FMsongX2 wants to merge 1 commit into
Live2D:developfrom
FMsongX2:fix/motion-speed-endtime

Conversation

@FMsongX2

@FMsongX2 FMsongX2 commented Jun 27, 2026

Copy link
Copy Markdown

Problem

CubismMotionLayer.SetStateSpeed (reached via the public CubismMotionController.SetAnimationSpeed) writes a relative value into EndTime, which is an absolute timestamp everywhere else — set as StartTime + length / speed on play and read via Time.time > EndTime. The relative value lands in the past, so Time.time > EndTime is immediately true: the motion fires its end event at once, and its fade-out weight collapses to 0 (the now-negative EndTime - time is clamped by GetEasingSine), cutting the motion instead of playing it out. It triggers on any speed change, including speed == 1.

Reproduction

Play a non-looping motion through CubismMotionController, then call SetAnimationSpeed(0, index, 1.0f) (speed unchanged — a no-op):

playing: EndTime = 142.4 (absolute), Time.time = 136.8, ~5.6s remaining
after SetAnimationSpeed(1.0):  EndTime = 142.4 -> 5.57
  Time.time(136.8) > EndTime(5.57)       -> end event fires immediately
  fade-out weight: (5.57 - 136.8) < 0    -> GetEasingSine clamps to 0 (motion cut)
next Update():  IsPlayingAnimation -> false   (a motion with 5.6s left is cut instantly)

The API is public but no sample calls it, which is why this went unnoticed.

Fix

Rebuild EndTime from the remaining content time, keeping it absolute:

EndTime = Time.time + (EndTime - Time.time) * previousSpeed / speed;

previousSpeed is the value being overwritten, so the rescale is correct for any prior speed, not only 1 — (EndTime - Time.time) is the wall-clock remaining under the old speed, * previousSpeed gives the remaining content time, / speed converts it to the new speed. The EndTime == -1 "no end" sentinel (MotionLength <= 0) is left untouched, and the rescale is skipped when the previous speed was 0 so speed == 0 (pause) paths stay unchanged instead of producing NaN.

SetStateSpeed stored remaining duration ((EndTime - Time.time) / speed) into
EndTime, which is an absolute timestamp everywhere else (set as StartTime +
length/speed, compared against Time.time). The relative value lands in the past,
so the motion ends immediately and the fade-out weight goes negative. Reachable
via the public CubismMotionController.SetAnimationSpeed; triggers on any speed
change, including speed == 1.

Rebuild EndTime as Time.time + (EndTime - Time.time) * previousSpeed / speed so
it stays absolute and rescales by remaining content time -- correct also when
the motion already ran at a non-default speed (the prior form was exact only
when the previous speed was 1).

Guards: skip the rescale when EndTime is the -1 "no end" sentinel
(MotionLength <= 0), and when the previous speed was 0 (paused) so the 0 *
Infinity term does not produce NaN. speed == 0 paths are left unchanged from
before.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant