Files
mod_live2d/source_code/Cubism/Framework/CubismEyeBlink.cs
T
2026-09-10 10:59:37 +03:00

137 lines
5.2 KiB
C#

using System;
using System.Collections.Generic;
namespace Cthangover.Live2D.Cubism.Framework
{
/// <summary>
/// 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 <c>(0, 2 * BlinkIntervalSeconds)</c>. The actual blink consists of
/// configurable closing, closed, and opening phase durations.
///
/// Used by <see cref="CubismModelNode"/> when <see cref="CubismModelNode.EnableEyeBlink"/>
/// is true and the model3.json defines EyeBlink parameter group.
/// </summary>
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<string> _parameterIds = new();
/// <summary>
/// Average interval between successive blinks, in seconds.
/// Actual interval is randomized in range (0, 2 * this value).
/// Default is 4 seconds.
/// </summary>
public float BlinkIntervalSeconds { get; set; } = 4f;
/// <summary>
/// Duration of the eyelid closing transition, in seconds. Default 0.1.
/// </summary>
public float ClosingSeconds { get; set; } = 0.1f;
/// <summary>
/// Duration the eyes stay fully closed, in seconds. Default 0.05.
/// </summary>
public float ClosedSeconds { get; set; } = 0.05f;
/// <summary>
/// Duration of the eyelid opening transition, in seconds. Default 0.15.
/// </summary>
public float OpeningSeconds { get; set; } = 0.15f;
/// <summary>
/// Sets the parameter IDs that represent eye open/close.
/// Typically sourced from the model3.json EyeBlink group.
/// Clears any previously set IDs.
/// </summary>
public void SetParameterIds(IEnumerable<string> ids)
{
_parameterIds.Clear();
_parameterIds.AddRange(ids);
}
/// <summary>
/// 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 <see cref="CubismModelNode.OnUpdate"/>.
/// </summary>
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;
}
}
}
}