using System; using System.Collections.Generic; namespace Cthangover.Live2D.Cubism.Framework { /// /// Drives a state-machine based eye blink animation on target model parameters. /// Cycles through five states: First -> Interval -> Closing -> Closed -> Opening, /// modulating the specified parameter values from 1 (open) to 0 (closed) and back. /// /// Blink timing is randomized per cycle: each interval between blinks is a random /// value in (0, 2 * BlinkIntervalSeconds). The actual blink consists of /// configurable closing, closed, and opening phase durations. /// /// Used by when /// is true and the model3.json defines EyeBlink parameter group. /// public class CubismEyeBlink { private enum EyeState { First, Interval, Closing, Closed, Opening } private EyeState _state = EyeState.First; private float _nextBlinkTime; private float _stateStartTime; private float _userTimeSeconds; private readonly List _parameterIds = new(); /// /// Average interval between successive blinks, in seconds. /// Actual interval is randomized in range (0, 2 * this value). /// Default is 4 seconds. /// public float BlinkIntervalSeconds { get; set; } = 4f; /// /// Duration of the eyelid closing transition, in seconds. Default 0.1. /// public float ClosingSeconds { get; set; } = 0.1f; /// /// Duration the eyes stay fully closed, in seconds. Default 0.05. /// public float ClosedSeconds { get; set; } = 0.05f; /// /// Duration of the eyelid opening transition, in seconds. Default 0.15. /// public float OpeningSeconds { get; set; } = 0.15f; /// /// Sets the parameter IDs that represent eye open/close. /// Typically sourced from the model3.json EyeBlink group. /// Clears any previously set IDs. /// public void SetParameterIds(IEnumerable ids) { _parameterIds.Clear(); _parameterIds.AddRange(ids); } /// /// Advances the blink state machine and writes eye openness to model parameters. /// 1.0 = fully open, 0.0 = fully closed. Linear interpolation between phases. /// Call every frame from . /// public void UpdateParameters(CubismNativeModel model, float deltaTimeSeconds) { _userTimeSeconds += deltaTimeSeconds; switch (_state) { case EyeState.First: _state = EyeState.Interval; _nextBlinkTime = _userTimeSeconds + Random.Shared.NextSingle() * (2f * BlinkIntervalSeconds - 1f); break; case EyeState.Interval: SetParameterValues(model, 1f); if (_userTimeSeconds >= _nextBlinkTime) { _state = EyeState.Closing; _stateStartTime = _userTimeSeconds; } break; case EyeState.Closing: { var elapsed = _userTimeSeconds - _stateStartTime; var value = 1f - Math.Clamp(elapsed / ClosingSeconds, 0f, 1f); SetParameterValues(model, value); if (elapsed >= ClosingSeconds) { _state = EyeState.Closed; _stateStartTime = _userTimeSeconds; } break; } case EyeState.Closed: SetParameterValues(model, 0f); if (_userTimeSeconds - _stateStartTime >= ClosedSeconds) { _state = EyeState.Opening; _stateStartTime = _userTimeSeconds; } break; case EyeState.Opening: { var elapsed = _userTimeSeconds - _stateStartTime; var value = Math.Clamp(elapsed / OpeningSeconds, 0f, 1f); SetParameterValues(model, value); if (elapsed >= OpeningSeconds) { _state = EyeState.Interval; _nextBlinkTime = _userTimeSeconds + Random.Shared.NextSingle() * (2f * BlinkIntervalSeconds - 1f); } break; } } } private unsafe void SetParameterValues(CubismNativeModel model, float value) { var idsPtr = model.GetParameterIds(); var values = model.GetParameterValues(); for (int i = 0; i < model.ParameterCount; i++) { var id = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i); if (_parameterIds.Contains(id)) values[i] = value; } } } }