using System; namespace EyeTracking { /// /// Handles smooth blink transitions with configurable lerp duration /// to create natural-looking eye opening and closing animations /// public class BlinkLerp { private float _blinkLerpDuration; private float _blinkStartTime; private float _blinkEndTime; private bool _isBlinkLerping; private bool _isBlinkOpening; // true when opening, false when closing private bool _wasBlinking; // Per-eye state for winking (each eye lerps independently) private bool _wasLeftBlinking; private bool _wasRightBlinking; private float _leftBlinkStartTime; private float _rightBlinkStartTime; private bool _leftBlinkLerping; private bool _rightBlinkLerping; private bool _leftBlinkOpening; private bool _rightBlinkOpening; /// /// Initializes a new instance of the BlinkLerp class /// /// Duration in seconds for blink lerping (default: 0.05f) public BlinkLerp(float blinkLerpDuration = 0.05f) { _blinkLerpDuration = blinkLerpDuration; _wasBlinking = false; _isBlinkLerping = false; _isBlinkOpening = false; _wasLeftBlinking = false; _wasRightBlinking = false; _leftBlinkLerping = false; _rightBlinkLerping = false; _leftBlinkOpening = false; _rightBlinkOpening = false; } /// /// Updates the blink lerp state and returns the current eye closed amount /// /// Whether the current frame is detected as blinking /// Current time in seconds /// Base closed amount from eye tracking data (0-1) /// Lerped eye closed amount (0-1) public float UpdateBlinkLerp(bool isCurrentFrameBlinking, float currentTime, float baseClosedAmount = 0.0f) { return UpdateOneEye( isCurrentFrameBlinking, currentTime, baseClosedAmount, ref _wasBlinking, ref _blinkStartTime, ref _isBlinkLerping, ref _isBlinkOpening); } /// /// Updates the blink lerp state and returns separate left and right eye closed amounts. /// Use this overload when both eyes share the same blink state (e.g. full blink only). /// public (float leftClosedAmount, float rightClosedAmount) UpdateBlinkLerp( bool isCurrentFrameBlinking, float currentTime, float leftBaseClosedAmount, float rightBaseClosedAmount) { float closedAmount = UpdateBlinkLerp( isCurrentFrameBlinking, currentTime, (leftBaseClosedAmount + rightBaseClosedAmount) * 0.5f); return (closedAmount, closedAmount); } /// /// Updates the blink lerp state per eye and returns separate left and right eye closed amounts. /// Use this overload when eyes can blink or wink independently (e.g. VRCFT with winking). /// When an eye is animating from closed to open, the other eye (if not fully open) is made to match it so full blinks stay in sync. /// public (float leftClosedAmount, float rightClosedAmount) UpdateBlinkLerpPerEye( bool isLeftBlinking, bool isRightBlinking, float currentTime, float leftBaseClosedAmount = 0.0f, float rightBaseClosedAmount = 0.0f) { float leftClosedAmount = UpdateOneEye( isLeftBlinking, currentTime, leftBaseClosedAmount, ref _wasLeftBlinking, ref _leftBlinkStartTime, ref _leftBlinkLerping, ref _leftBlinkOpening); float rightClosedAmount = UpdateOneEye( isRightBlinking, currentTime, rightBaseClosedAmount, ref _wasRightBlinking, ref _rightBlinkStartTime, ref _rightBlinkLerping, ref _rightBlinkOpening); // When an eye is animating from closed to open, keep the other eye in sync if it's not fully open (fixes full-blink desync) const float fullyOpenEpsilon = 0.0001f; bool leftOpening = _leftBlinkOpening && _leftBlinkLerping; bool rightOpening = _rightBlinkOpening && _rightBlinkLerping; if (leftOpening && rightOpening) { // Both opening: use the more-open value so both match and we don't slow the faster one float syncAmount = Math.Min(leftClosedAmount, rightClosedAmount); leftClosedAmount = rightClosedAmount = syncAmount; } else if (leftOpening && rightClosedAmount > fullyOpenEpsilon) { rightClosedAmount = leftClosedAmount; } else if (rightOpening && leftClosedAmount > fullyOpenEpsilon) { leftClosedAmount = rightClosedAmount; } return (leftClosedAmount, rightClosedAmount); } private float UpdateOneEye( bool isCurrentFrameBlinking, float currentTime, float baseClosedAmount, ref bool wasBlinking, ref float blinkStartTime, ref bool isLerping, ref bool isOpening) { bool transitioningOutOfBlink = wasBlinking && !isCurrentFrameBlinking; bool transitioningIntoBlink = !wasBlinking && isCurrentFrameBlinking; if (transitioningIntoBlink) { blinkStartTime = currentTime; isLerping = true; isOpening = false; } else if (transitioningOutOfBlink) { blinkStartTime = currentTime; isLerping = true; isOpening = true; } float closedAmount = baseClosedAmount; if (isLerping) { float elapsedTime = currentTime - blinkStartTime; float lerpProgress = Math.Min(elapsedTime / _blinkLerpDuration, 1.0f); if (isOpening) closedAmount = 1.0f - lerpProgress; else closedAmount = lerpProgress; if (lerpProgress >= 1.0f) isLerping = false; } else if (isCurrentFrameBlinking) { closedAmount = 1.0f; } wasBlinking = isCurrentFrameBlinking; return Math.Clamp(closedAmount, 0.0f, 1.0f); } /// /// Resets the blink lerp state /// public void Reset() { _wasBlinking = false; _isBlinkLerping = false; _isBlinkOpening = false; _wasLeftBlinking = false; _wasRightBlinking = false; _leftBlinkLerping = false; _rightBlinkLerping = false; _leftBlinkOpening = false; _rightBlinkOpening = false; } /// /// Gets or sets the blink lerp duration in seconds /// public float BlinkLerpDuration { get => _blinkLerpDuration; set => _blinkLerpDuration = Math.Max(0.001f, value); // Ensure positive duration } /// /// Gets whether the blink lerp is currently active (either combined or per-eye) /// public bool IsLerping => _isBlinkLerping || _leftBlinkLerping || _rightBlinkLerping; /// /// Gets whether the eyes are currently opening (true) or closing (false). /// True if either combined or per-eye opening is in progress. /// public bool IsOpening => _isBlinkOpening || _leftBlinkOpening || _rightBlinkOpening; } }