using System; namespace Cthangover.Live2D.Cubism.Framework { /// /// Smooth target-point tracking for face/eye movement parameters. /// Drives the ParamAngleX/Y and ParamEyeBallX/Y model parameters in /// . /// /// Bit-exact port of Cubism SDK for Native 5-r.5 Math/CubismTargetPoint.cpp: /// a velocity-based follower that accelerates toward the target with a /// capped acceleration (reaching top speed in 0.15 s), then smoothly /// decelerates so the configured acceleration can still brake before the /// target. All time is expressed in internal 30 fps frame units. /// public class CubismTargetPoint { /// Internal reference frame rate (SDK FrameRate). private const int FrameRate = 30; /// Threshold below which no movement is applied (SDK Epsilon). private const float Epsilon = 0.01f; private float _faceTargetX, _faceTargetY; private float _faceX, _faceY; private float _faceVX, _faceVY; private float _lastTimeSeconds; private float _userTimeSeconds; /// Current smoothed X position. public float X => _faceX; /// Current smoothed Y position. public float Y => _faceY; /// /// Sets the target point immediately. The actual position /// will smoothly approach this target over subsequent calls to . /// public void Set(float x, float y) { _faceTargetX = x; _faceTargetY = y; } /// /// Advances the smoothing simulation by one frame (SDK CubismTargetPoint::Update). /// The very first call only anchors the internal clock and performs no /// integration, matching the native implementation. /// public void Update(float deltaTimeSeconds) { // Add the delta time. _userTimeSeconds += deltaTimeSeconds; // The average speed of swinging the head from center to side is about 1 second. // Considering acceleration/deceleration, twice that is the maximum speed. // The face direction is 0.0 at center and +-1.0 at the sides. const float FaceParamMaxV = 40f / 10f; // Moves 40 in 7.5 seconds (5.3/sc) float MaxV = FaceParamMaxV * 1f / FrameRate; // Upper speed limit per frame // First call only anchors the clock; no integration (SDK behavior). if (_lastTimeSeconds == 0f) { _lastTimeSeconds = _userTimeSeconds; return; } float deltaTimeWeight = (_userTimeSeconds - _lastTimeSeconds) * FrameRate; _lastTimeSeconds = _userTimeSeconds; // Time until maximum speed, expressed in internal frames. const float TimeToMaxSpeed = 0.15f; const float FrameToMaxSpeed = TimeToMaxSpeed * FrameRate; // sec * frame/sec float MaxA = deltaTimeWeight * MaxV / FrameToMaxSpeed; // Acceleration per frame // The target direction is the vector (dx, dy). float dx = _faceTargetX - _faceX; float dy = _faceTargetY - _faceY; if (MathF.Abs(dx) <= Epsilon && MathF.Abs(dy) <= Epsilon) { return; // No change. } // Slow down when the distance requires more than the maximum speed. float d = MathF.Sqrt((dx * dx) + (dy * dy)); // Maximum speed vector toward the target. float vx = MaxV * dx / d; float vy = MaxV * dy / d; // Acceleration needed to change from the current velocity to the new one. float ax = vx - _faceVX; float ay = vy - _faceVY; float a = MathF.Sqrt((ax * ax) + (ay * ay)); // While accelerating. if (a < -MaxA || a > MaxA) { ax *= MaxA / a; ay *= MaxA / a; } // Add the acceleration to the original velocity to get the new velocity. _faceVX += ax; _faceVY += ay; // When approaching the target direction, decelerate smoothly: // from the relation between acceleration, speed and distance, compute // the maximum speed that can still be stopped with the configured // acceleration, and clamp the current speed to it. { // Relation between acceleration, velocity and distance. // 2 6 2 3 // sqrt(a t + 16 a h t - 8 a h) - a t // v = -------------------------------------- // 2 // 4 t - 2 // (t=1) float maxV = 0.5f * (MathF.Sqrt((MaxA * MaxA) + 16f * MaxA * d - 8f * MaxA * d) - MaxA); float curV = MathF.Sqrt((_faceVX * _faceVX) + (_faceVY * _faceVY)); if (curV > maxV) { // Current speed > maximum speed: decelerate to the maximum. _faceVX *= maxV / curV; _faceVY *= maxV / curV; } } _faceX += _faceVX; _faceY += _faceVY; } } }