using System;
using System.Collections.Generic;
namespace Cthangover.Live2D.Cubism.Framework
{
public delegate float MotionSegmentFunction(float[] segments, int baseIndex, float time);
///
/// Версия обработки зацикленного времени — порт CubismMotion::MotionBehavior (SDK 5-r.5).
/// V2 (по умолчанию): длительность цикла расширяется на один кадр (1/Fps), а переход
/// между циклами интерполируется через коррекцию конечной точки (CorrectEndPoint).
/// V1: время цикла жёстко перезаписывается без кадра-хвоста (поведение старых SDK).
///
public enum MotionBehavior
{
V1 = 0,
V2 = 1,
}
///
/// Parses and plays a Cubism .motion3.json animation.
/// Manages three categories of curves:
///
/// - Model curves — eye blink, lip sync, and opacity
/// - Parameter curves — arbitrary model parameter animations
/// - Part opacity curves — visibility/opacity of model parts
///
///
/// Данные хранятся как в нативном CubismMotionData: один общий плоский массив точек
/// () и общий массив сегментов ();
/// каждая кривая ссылается на свой диапазон сегментов через BaseSegmentIndex/SegmentCount.
/// Оценка кривой сначала ищет сегмент по времени ПЕРВОЙ точки следующего сегмента,
/// а за пределами последнего сегмента возвращает значение последней точки
/// (при зацикливании V2 — интерполирует переход конца в начало).
///
/// Обновление параметров () — четыре прохода, порт SDK:
///
/// - Model-кривые: EyeBlink/LipSync значения и ModelOpacity
/// - Parameter-кривые: модуляция на EyeBlink (* значение) и LipSync (+ значение)
/// - Не анимированные параметры групп EyeBlink/LipSync: лерп с fade-весом
/// - PartOpacity-кривые
///
///
/// Фейд применяется по кривой: если у кривой заданы собственные FadeInTime/FadeOutTime —
/// используется они (база — фейд самого моушена), иначе общий fade-вес моушена.
///
public class CubismMotion : ACubismMotion
{
private const string ModelCurveIdEyeBlink = "EyeBlink";
private const string ModelCurveIdLipSync = "LipSync";
private const string ModelCurveIdOpacity = "Opacity";
/// Порт SDK-константы MaxTargetSize: лимит целей EyeBlink/LipSync (ширина битовой маски).
private const int MaxTargetSize = 64;
/// Порт CubismMath::Epsilon — порог сравнения в алгоритме Кардано.
private const float MathEpsilon = 0.00001f;
private readonly float _sourceFrameRate;
private float _lastWeight;
private float _loopDurationSeconds = -1f;
private MotionBehavior _motionBehavior = MotionBehavior.V2;
private readonly CubismMotionJson _motionJson;
private readonly bool _areBeziersRestricted;
private readonly List _modelCurves = new();
private readonly List _parameterCurves = new();
private readonly List _partOpacityCurves = new();
// Общие плоские хранилища точек и сегментов (порт CubismMotionData.Points/Segments).
private CubismMotionPoint[] _points = Array.Empty();
private MotionSegmentData[] _segments = Array.Empty();
private readonly List _eyeBlinkParameterIds = new();
private readonly List _lipSyncParameterIds = new();
private readonly List _events = new();
private float _modelOpacity = 1f;
///
/// Parses a .motion3.json byte buffer: метаданные (длительность, loop, fps, фейды,
/// признак restricted-безье) и кривые/события через /.
/// Отсутствующее или отрицательное время фейда заменяется на 1 c — порт CubismMotion::Parse.
///
public CubismMotion(byte[] motion3JsonBytes)
{
_motionJson = new CubismMotionJson(motion3JsonBytes);
_sourceFrameRate = _motionJson.GetFps();
_areBeziersRestricted = _motionJson.AreBeziersRestricted();
_isLoop = _motionJson.IsLoop();
_loopDurationSeconds = _motionJson.GetDuration();
var fadeIn = _motionJson.GetFadeInTime();
var fadeOut = _motionJson.GetFadeOutTime();
_fadeInSeconds = fadeIn < 0f ? 1f : fadeIn;
_fadeOutSeconds = fadeOut < 0f ? 1f : fadeOut;
ParseCurves();
ParseEvents();
}
///
/// Configures which parameters are affected by eye blink and lip sync
/// modulation (обычно из Groups модели в model3.json). Кривая параметра из
/// этих групп умножается на значение EyeBlink-кривой / складывается с LipSync
/// в проходе 2; параметры групп без собственной кривой получают лерп в проходе 3.
///
public void SetEffectIds(List eyeBlinkIds, List lipSyncIds)
{
_eyeBlinkParameterIds.Clear();
_lipSyncParameterIds.Clear();
if (eyeBlinkIds != null) _eyeBlinkParameterIds.AddRange(eyeBlinkIds);
if (lipSyncIds != null) _lipSyncParameterIds.AddRange(lipSyncIds);
}
/// Returns -1 for looping motions (they never end), иначе полную длительность.
public override float GetDuration() => _isLoop ? -1f : _loopDurationSeconds;
/// Полная длительность одного цикла, даже если моушен зациклен.
public override float GetLoopDuration() => _loopDurationSeconds;
/// Порт CubismMotion::SetMotionBehavior — версия обработки зацикленного времени.
public void SetMotionBehavior(MotionBehavior behavior) => _motionBehavior = behavior;
/// Порт CubismMotion::GetMotionBehavior.
public MotionBehavior GetMotionBehavior() => _motionBehavior;
///
/// true, если среди Model-кривых есть кривая "Opacity" — порт CubismMotion::IsExistModelOpacity:
/// наличие кривой, а не текущее значение прозрачности.
///
public bool IsExistModelOpacity()
{
foreach (var curve in _modelCurves)
if (curve.Id == ModelCurveIdOpacity) return true;
return false;
}
///
/// Текущая ModelOpacity из Model-кривой "Opacity" (1, если кривой нет).
/// Рендерер должен умножать на неё прозрачность drawable'ов — SDK применяет
/// её в CubismModel::SetModelOpacity, здесь значение отдаётся наружу.
///
public float GetModelOpacity() => _modelOpacity;
///
/// События (UserData), сработавшие в интервале (beforeCheckTime, motionTime].
/// Список очищается при каждом вызове; очередь вызывает метод раз в кадр
/// с lastCheckTime..currentTime, поэтому события не теряются между кадрами.
///
public override List GetFiredEvent(float beforeCheckTimeSeconds, float motionTimeSeconds)
{
_firedEventValues.Clear();
foreach (var evt in _events)
if (evt.FireTime > beforeCheckTimeSeconds && evt.FireTime <= motionTimeSeconds)
_firedEventValues.Add(evt.Value);
return _firedEventValues;
}
/// Совместимая обёртка над с тем же списком результатов.
public List GetFiredEventValue(float beforeTime, float motionTime) =>
GetFiredEvent(beforeTime, motionTime);
///
/// Four-pass parameter update. Called by
/// every frame (через ).
///
public override unsafe void DoUpdateParameters(
CubismNativeModel model, float userTimeSeconds, float fadeWeight, CubismMotionQueueEntry entry)
{
if (model == null || entry == null) return;
// Порт CubismMotion::DoUpdateParameters: пересчёт EndTime при смене loop-состояния (V2).
if (_motionBehavior == MotionBehavior.V2 && _previousLoopState != _isLoop)
{
AdjustEndTime(entry);
_previousLoopState = _isLoop;
}
float timeOffsetSeconds = userTimeSeconds - entry.StartTime;
if (timeOffsetSeconds < 0f) timeOffsetSeconds = 0f; // защита от старта в "будущем"
float lipSyncValue = float.MaxValue; // FLT_MAX — сигнатура "кривой нет"
float eyeBlinkValue = float.MaxValue;
ulong lipSyncFlags = 0UL;
ulong eyeBlinkFlags = 0UL;
// Базовые (моушен-уровень) фейды для кривых без собственных fade-настроек.
float tmpFadeIn = _fadeInSeconds <= 0f
? 1f
: CubismMath.GetEasingSine((userTimeSeconds - entry.FadeInStartTime) / _fadeInSeconds);
float tmpFadeOut = (_fadeOutSeconds <= 0f || entry.EndTime < 0f)
? 1f
: CubismMath.GetEasingSine((entry.EndTime - userTimeSeconds) / _fadeOutSeconds);
// 'Repeat' time as necessary.
float time = timeOffsetSeconds;
float duration = _loopDurationSeconds;
bool isCorrection = _motionBehavior == MotionBehavior.V2 && _isLoop;
if (_isLoop)
{
if (_motionBehavior == MotionBehavior.V2)
duration += 1f / _sourceFrameRate; // кадр-хвост для интерполяции цикла
while (time > duration)
time -= duration;
}
var idsPtr = model.GetParameterIds();
var values = model.GetParameterValues();
var minValues = model.GetParameterMinimumValues();
var maxValues = model.GetParameterMaximumValues();
// Индексы параметров модели: порт model->GetParameterIndex(curveId).
var paramIndexById = new Dictionary(model.ParameterCount);
for (int i = 0; i < model.ParameterCount; i++)
paramIndexById[CubismNativeModel.ReadStringFromPtrArray(idsPtr, i)] = i;
// Pass 1 — model curves (EyeBlink / LipSync / Opacity).
foreach (var curve in _modelCurves)
{
float value = EvaluateCurveInternal(curve, _points, _segments, time, isCorrection, duration, _areBeziersRestricted);
if (curve.Id == ModelCurveIdEyeBlink) eyeBlinkValue = value;
else if (curve.Id == ModelCurveIdLipSync) lipSyncValue = value;
else if (curve.Id == ModelCurveIdOpacity)
{
// Порт: _modelOpacity = value; model->SetModelOpacity(...).
// Значение отдаётся наружу через GetModelOpacity().
_modelOpacity = value;
}
}
// Pass 2 — parameter curves с модуляцией EyeBlink/LipSync и пер-кривым фейдом.
foreach (var curve in _parameterCurves)
{
if (!paramIndexById.TryGetValue(curve.Id, out int parameterIndex)) continue;
float sourceValue = values[parameterIndex];
float value = EvaluateCurveInternal(curve, _points, _segments, time, isCorrection, duration, _areBeziersRestricted);
if (eyeBlinkValue != float.MaxValue)
{
for (int i = 0; i < _eyeBlinkParameterIds.Count && i < MaxTargetSize; i++)
{
if (_eyeBlinkParameterIds[i] == curve.Id)
{
value *= eyeBlinkValue;
eyeBlinkFlags |= 1UL << i;
break;
}
}
}
if (lipSyncValue != float.MaxValue)
{
for (int i = 0; i < _lipSyncParameterIds.Count && i < MaxTargetSize; i++)
{
if (_lipSyncParameterIds[i] == curve.Id)
{
value += lipSyncValue;
lipSyncFlags |= 1UL << i;
break;
}
}
}
float v;
if (curve.FadeInTime < 0f && curve.FadeOutTime < 0f)
{
// Собственных фейдов у кривой нет — применяется общий fade-вес моушена.
v = sourceValue + (value - sourceValue) * fadeWeight;
}
else
{
// Порт: база — фейд самого моушена (tmpFadeIn/tmpFadeOut), не 1.
float fin = curve.FadeInTime < 0f
? tmpFadeIn
: curve.FadeInTime == 0f
? 1f
: CubismMath.GetEasingSine((userTimeSeconds - entry.FadeInStartTime) / curve.FadeInTime);
float fout = curve.FadeOutTime < 0f
? tmpFadeOut
: (curve.FadeOutTime == 0f || entry.EndTime < 0f)
? 1f
: CubismMath.GetEasingSine((entry.EndTime - userTimeSeconds) / curve.FadeOutTime);
float paramWeight = _weight * fin * fout;
v = sourceValue + (value - sourceValue) * paramWeight;
}
// Порт model->SetParameterValue: значение зажимается в [min, max] параметра.
values[parameterIndex] = ClampParameterValue(v, minValues[parameterIndex], maxValues[parameterIndex]);
}
// Pass 3 — EyeBlink/LipSync для параметров, не анимированных кривыми: лерп с fade-весом.
if (eyeBlinkValue != float.MaxValue)
{
for (int i = 0; i < _eyeBlinkParameterIds.Count && i < MaxTargetSize; i++)
{
// Моушен уже записал этот параметр в проходе 2 — моргание не применяем.
if ((eyeBlinkFlags >> i & 1UL) != 0UL) continue;
if (!paramIndexById.TryGetValue(_eyeBlinkParameterIds[i], out int pi)) continue;
float sourceValue = values[pi];
float v = sourceValue + (eyeBlinkValue - sourceValue) * fadeWeight;
values[pi] = ClampParameterValue(v, minValues[pi], maxValues[pi]);
}
}
if (lipSyncValue != float.MaxValue)
{
for (int i = 0; i < _lipSyncParameterIds.Count && i < MaxTargetSize; i++)
{
if ((lipSyncFlags >> i & 1UL) != 0UL) continue;
if (!paramIndexById.TryGetValue(_lipSyncParameterIds[i], out int pi)) continue;
float sourceValue = values[pi];
float v = sourceValue + (lipSyncValue - sourceValue) * fadeWeight;
values[pi] = ClampParameterValue(v, minValues[pi], maxValues[pi]);
}
}
// Pass 4 — part opacity curves (пишутся напрямую в массив прозрачностей частей).
var partOpacities = model.GetPartOpacities();
var partIdsPtr = model.GetPartIds();
int partCount = model.GetPartCount();
var partIndexById = new Dictionary(partCount);
for (int i = 0; i < partCount; i++)
partIndexById[CubismNativeModel.ReadStringFromPtrArray(partIdsPtr, i)] = i;
foreach (var curve in _partOpacityCurves)
{
if (!partIndexById.TryGetValue(curve.Id, out int pi)) continue;
partOpacities[pi] = EvaluateCurveInternal(curve, _points, _segments, time, isCorrection, duration, _areBeziersRestricted);
}
// Завершение/перезапуск: моушен сам выставляет IsFinished и зовёт колбэк
// (очередь только удаляет finished-записи — порт CubismMotionQueueManager).
if (timeOffsetSeconds >= duration)
{
if (_isLoop)
UpdateForNextLoop(entry, userTimeSeconds, time);
else
{
OnFinishedMotion?.Invoke(this);
entry.IsFinished = true;
}
}
_lastWeight = fadeWeight;
}
///
/// Порт CubismMotion::UpdateForNextLoop: сдвиг StartTime назад на время внутри цикла;
/// фейд-ин перезапускается только при включённом _isLoopFadeIn (поведение V2).
///
private void UpdateForNextLoop(CubismMotionQueueEntry entry, float userTimeSeconds, float time)
{
switch (_motionBehavior)
{
case MotionBehavior.V2:
default:
entry.StartTime = userTimeSeconds - time;
if (_isLoopFadeIn)
entry.FadeInStartTime = userTimeSeconds - time;
OnFinishedMotion?.Invoke(this);
break;
case MotionBehavior.V1:
entry.StartTime = userTimeSeconds;
if (_isLoopFadeIn)
entry.FadeInStartTime = userTimeSeconds;
break;
}
}
///
/// Разбор кривых: порт CubismMotion::Parse. Все точки и сегменты складываются
/// в общие плоские списки; кривая хранит BaseSegmentIndex и количество сегментов.
///
private void ParseCurves()
{
var points = new List();
var segments = new List();
for (int ci = 0; ci < _motionJson.GetCurveCount(); ci++)
{
var target = ParseTarget(_motionJson.GetCurveTarget(ci));
var curveData = new MotionCurveData
{
Type = target,
Id = _motionJson.GetCurveId(ci),
BaseSegmentIndex = segments.Count,
// -1 = время фейда кривой не задано в json (как в SDK).
FadeInTime = _motionJson.GetCurveFadeInTime(ci),
FadeOutTime = _motionJson.GetCurveFadeOutTime(ci),
};
_motionJson.GetCurveSegments(ci, out var rawSegments);
curveData.SegmentCount = ParseSegmentsInternal(rawSegments, points, segments);
switch (target)
{
case CubismMotionCurveTarget.Model: _modelCurves.Add(curveData); break;
case CubismMotionCurveTarget.Parameter: _parameterCurves.Add(curveData); break;
case CubismMotionCurveTarget.PartOpacity: _partOpacityCurves.Add(curveData); break;
}
}
_points = points.ToArray();
_segments = segments.ToArray();
}
///
/// Разбор массива Segments одной кривой — порт 1:1 цикла CubismMotion::Parse.
/// Формат motion3.json: [t0, v0, type, t, v, type, t, v, v, v(bezier)...]:
/// первые два элемента — стартовая точка кривой, дальше для каждого сегмента
/// маркер типа и его точки (Linear/Stepped/InverseStepped — 1 точка, Bezier — 3).
/// Возвращает число сегментов кривой; точки и сегменты дописываются в общие списки.
///
internal static int ParseSegmentsInternal(float[] raw, List points, List segments)
{
int count = 0;
int segmentPosition = 0;
while (segmentPosition < raw.Length)
{
int basePointIndex;
if (segmentPosition == 0)
{
// Первая точка кривой: время и значение — не маркер типа.
basePointIndex = points.Count;
points.Add(new CubismMotionPoint { Time = raw[segmentPosition], Value = raw[segmentPosition + 1] });
segmentPosition += 2;
}
else
{
// Сегменты продолжаются от последней точки предыдущего сегмента.
basePointIndex = points.Count - 1;
}
var segmentType = (CubismMotionSegmentType)(int)raw[segmentPosition];
switch (segmentType)
{
case CubismMotionSegmentType.Linear:
case CubismMotionSegmentType.Stepped:
case CubismMotionSegmentType.InverseStepped:
points.Add(new CubismMotionPoint { Time = raw[segmentPosition + 1], Value = raw[segmentPosition + 2] });
segmentPosition += 3;
break;
case CubismMotionSegmentType.Bezier:
points.Add(new CubismMotionPoint { Time = raw[segmentPosition + 1], Value = raw[segmentPosition + 2] });
points.Add(new CubismMotionPoint { Time = raw[segmentPosition + 3], Value = raw[segmentPosition + 4] });
points.Add(new CubismMotionPoint { Time = raw[segmentPosition + 5], Value = raw[segmentPosition + 6] });
segmentPosition += 7;
break;
default:
// SDK: CSM_ASSERT(0). Неизвестный тип — останавливаем разбор кривой,
// чтобы не зациклиться на некорректных данных.
return count;
}
segments.Add(new MotionSegmentData { BasePointIndex = basePointIndex, SegmentType = segmentType });
count++;
}
return count;
}
///
/// Порт CubismMotion::EvaluateCurve (анонимный namespace CubismMotion.cpp).
/// Сегмент ищется по времени ПЕРВОЙ точки следующего сегмента; если время за
/// пределами всех сегментов — значение последней точки, а при зацикленном V2
/// (isCorrection) — интерполяция конца кривой к её началу (CorrectEndPoint).
///
internal static float EvaluateCurveInternal(
MotionCurveData curve,
CubismMotionPoint[] points,
MotionSegmentData[] segments,
float time,
bool isCorrection,
float endTime,
bool areBeziersRestricted)
{
int target = -1;
int totalSegmentCount = curve.BaseSegmentIndex + curve.SegmentCount;
int pointPosition = 0;
for (int i = curve.BaseSegmentIndex; i < totalSegmentCount; i++)
{
// Первая точка следующего сегмента = конечная точка текущего.
pointPosition = segments[i].BasePointIndex
+ (segments[i].SegmentType == CubismMotionSegmentType.Bezier ? 3 : 1);
if (points[pointPosition].Time > time)
{
target = i;
break;
}
}
if (target == -1)
{
if (isCorrection && time < endTime)
{
// Переход конец -> начало при зацикливании (поведение V2).
return CorrectEndPointInternal(
segments[totalSegmentCount - 1].SegmentType,
points,
segments[curve.BaseSegmentIndex].BasePointIndex,
pointPosition,
time,
endTime);
}
return points[pointPosition].Value;
}
return EvaluateSegmentInternal(segments[target].SegmentType, points, segments[target].BasePointIndex, time, areBeziersRestricted);
}
///
/// Порт CorrectEndPoint: лерп от конечной точки кривой к начальной,
/// чьё время искусственно продлевается до endTime (границы цикла).
///
internal static float CorrectEndPointInternal(
CubismMotionSegmentType lastSegmentType,
CubismMotionPoint[] points,
int beginIndex,
int endIndex,
float time,
float endTime)
{
CubismMotionPoint end = points[endIndex];
CubismMotionPoint begin = points[beginIndex];
begin.Time = endTime;
switch (lastSegmentType)
{
case CubismMotionSegmentType.Stepped:
return end.Value;
case CubismMotionSegmentType.InverseStepped:
return begin.Value;
default: // Linear / Bezier / прочее — линейная интерполяция
return LinearEvaluatePoints(end, begin, time);
}
}
/// Диспетчер оценки одного сегмента — порт таблички SegmentType -> Evaluate.
internal static float EvaluateSegmentInternal(
CubismMotionSegmentType segmentType,
CubismMotionPoint[] points,
int basePointIndex,
float time,
bool areBeziersRestricted)
{
switch (segmentType)
{
case CubismMotionSegmentType.Linear:
return LinearEvaluateInternal(points, basePointIndex, time);
case CubismMotionSegmentType.Bezier:
return areBeziersRestricted
? BezierEvaluateBinarySearchInternal(points, basePointIndex, time)
: BezierEvaluateCardanoInternal(points, basePointIndex, time);
case CubismMotionSegmentType.Stepped:
return points[basePointIndex].Value;
case CubismMotionSegmentType.InverseStepped:
return points[basePointIndex + 1].Value;
default:
return LinearEvaluateInternal(points, basePointIndex, time);
}
}
/// Порт LerpPoints: лерп времени и значения пары контрольных точек.
private static CubismMotionPoint LerpPoints(CubismMotionPoint a, CubismMotionPoint b, float t)
{
return new CubismMotionPoint
{
Time = a.Time + (b.Time - a.Time) * t,
Value = a.Value + (b.Value - a.Value) * t,
};
}
/// Порт LinearEvaluate: лерп пары точек, t не опускается ниже 0.
internal static float LinearEvaluatePoints(CubismMotionPoint p0, CubismMotionPoint p1, float time)
{
float t = (time - p0.Time) / (p1.Time - p0.Time);
if (t < 0f) t = 0f;
return p0.Value + (p1.Value - p0.Value) * t;
}
internal static float LinearEvaluateInternal(CubismMotionPoint[] points, int baseIndex, float time) =>
LinearEvaluatePoints(points[baseIndex], points[baseIndex + 1], time);
/// Порт BezierEvaluate (restricted/старые безье): де Кастельо по параметру t из времени.
internal static float BezierEvaluateInternal(CubismMotionPoint[] points, int baseIndex, float time)
{
float t = (time - points[baseIndex].Time) / (points[baseIndex + 3].Time - points[baseIndex].Time);
if (t < 0f) t = 0f;
CubismMotionPoint p01 = LerpPoints(points[baseIndex], points[baseIndex + 1], t);
CubismMotionPoint p12 = LerpPoints(points[baseIndex + 1], points[baseIndex + 2], t);
CubismMotionPoint p23 = LerpPoints(points[baseIndex + 2], points[baseIndex + 3], t);
CubismMotionPoint p012 = LerpPoints(p01, p12, t);
CubismMotionPoint p123 = LerpPoints(p12, p23, t);
return LerpPoints(p012, p123, t).Value;
}
/// Порт BezierEvaluateBinarySearch: деление отрезка параметра пополам (x_error = 0.01, 20 итераций).
internal static float BezierEvaluateBinarySearchInternal(CubismMotionPoint[] points, int baseIndex, float time)
{
const float xError = 0.01f;
float x = time;
float x1 = points[baseIndex].Time;
float x2 = points[baseIndex + 3].Time;
float cx1 = points[baseIndex + 1].Time;
float cx2 = points[baseIndex + 2].Time;
float ta = 0f;
float tb = 1f;
float t = 0f;
int i = 0;
for (; i < 20; i++)
{
if (x < x1 + xError)
{
t = ta;
break;
}
if (x2 - xError < x)
{
t = tb;
break;
}
float centerx = (cx1 + cx2) * 0.5f;
cx1 = (x1 + cx1) * 0.5f;
cx2 = (x2 + cx2) * 0.5f;
float ctrlx12 = (cx1 + centerx) * 0.5f;
float ctrlx21 = (cx2 + centerx) * 0.5f;
centerx = (ctrlx12 + ctrlx21) * 0.5f;
if (x < centerx)
{
tb = (ta + tb) * 0.5f;
if (centerx - xError < x)
{
t = tb;
break;
}
x2 = centerx;
cx2 = ctrlx12;
}
else
{
ta = (ta + tb) * 0.5f;
if (x < centerx + xError)
{
t = ta;
break;
}
x1 = centerx;
cx1 = ctrlx21;
}
}
if (i == 20)
t = (ta + tb) * 0.5f;
if (t < 0f) t = 0f;
if (t > 1f) t = 1f;
CubismMotionPoint p01 = LerpPoints(points[baseIndex], points[baseIndex + 1], t);
CubismMotionPoint p12 = LerpPoints(points[baseIndex + 1], points[baseIndex + 2], t);
CubismMotionPoint p23 = LerpPoints(points[baseIndex + 2], points[baseIndex + 3], t);
CubismMotionPoint p012 = LerpPoints(p01, p12, t);
CubismMotionPoint p123 = LerpPoints(p12, p23, t);
return LerpPoints(p012, p123, t).Value;
}
///
/// Порт BezierEvaluateCardanoInterpretation: параметр t ищется решением
/// кубического уравнения по времени (алгоритм Кардано), затем де Кастельо.
///
internal static float BezierEvaluateCardanoInternal(CubismMotionPoint[] points, int baseIndex, float time)
{
float x1 = points[baseIndex].Time;
float x2 = points[baseIndex + 3].Time;
float cx1 = points[baseIndex + 1].Time;
float cx2 = points[baseIndex + 2].Time;
float a = x2 - 3f * cx2 + 3f * cx1 - x1;
float b = 3f * cx2 - 6f * cx1 + 3f * x1;
float c = 3f * cx1 - 3f * x1;
float d = x1 - time;
float t = CardanoAlgorithmForBezierInternal(a, b, c, d);
CubismMotionPoint p01 = LerpPoints(points[baseIndex], points[baseIndex + 1], t);
CubismMotionPoint p12 = LerpPoints(points[baseIndex + 1], points[baseIndex + 2], t);
CubismMotionPoint p23 = LerpPoints(points[baseIndex + 2], points[baseIndex + 3], t);
CubismMotionPoint p012 = LerpPoints(p01, p12, t);
CubismMotionPoint p123 = LerpPoints(p12, p23, t);
return LerpPoints(p012, p123, t).Value;
}
///
/// Порт CubismMath::CardanoAlgorithmForBezier — решение кубического уравнения
/// по формулам Кардано с выбором корня, ближайшего к 0.5, и зажимом в [0,1].
///
internal static float CardanoAlgorithmForBezierInternal(float a, float b, float c, float d)
{
if (MathF.Abs(a) < MathEpsilon)
return RangeFInternal(QuadraticEquationInternal(b, c, d), 0f, 1f);
float ba = b / a;
float ca = c / a;
float da = d / a;
float p = (3f * ca - ba * ba) / 3f;
float p3 = p / 3f;
float q = (2f * ba * ba * ba - 9f * ba * ca + 27f * da) / 27f;
float q2 = q / 2f;
float discriminant = q2 * q2 + p3 * p3 * p3;
const float center = 0.5f;
const float threshold = center + 0.01f;
if (discriminant < 0f)
{
float mp3 = -p / 3f;
float mp33 = mp3 * mp3 * mp3;
float r = MathF.Sqrt(mp33);
float t = -q / (2f * r);
float cosphi = RangeFInternal(t, -1f, 1f);
float phi = MathF.Acos(cosphi);
float crtr = MathF.Cbrt(r);
float t1 = 2f * crtr;
float root1 = t1 * MathF.Cos(phi / 3f) - ba / 3f;
if (MathF.Abs(root1 - center) < threshold)
return RangeFInternal(root1, 0f, 1f);
float root2 = t1 * MathF.Cos((phi + 2f * MathF.PI) / 3f) - ba / 3f;
if (MathF.Abs(root2 - center) < threshold)
return RangeFInternal(root2, 0f, 1f);
float root3 = t1 * MathF.Cos((phi + 4f * MathF.PI) / 3f) - ba / 3f;
return RangeFInternal(root3, 0f, 1f);
}
if (discriminant == 0f)
{
float u1 = q2 < 0f ? MathF.Cbrt(-q2) : -MathF.Cbrt(q2);
float root1 = 2f * u1 - ba / 3f;
if (MathF.Abs(root1 - center) < threshold)
return RangeFInternal(root1, 0f, 1f);
float root2 = -u1 - ba / 3f;
return RangeFInternal(root2, 0f, 1f);
}
float sd = MathF.Sqrt(discriminant);
float uc = MathF.Cbrt(sd - q2);
float vc = MathF.Cbrt(sd + q2);
return RangeFInternal(uc - vc - ba / 3f, 0f, 1f);
}
/// Порт CubismMath::QuadraticEquation — решение вырожденного (a ≈ 0) случая.
private static float QuadraticEquationInternal(float a, float b, float c)
{
if (MathF.Abs(a) < MathEpsilon)
{
if (MathF.Abs(b) < MathEpsilon)
return -c;
return -c / b;
}
return -(b + MathF.Sqrt(b * b - 4f * a * c)) / (2f * a);
}
/// Порт CubismMath::RangeF — зажим значения в [min, max].
private static float RangeFInternal(float value, float min, float max)
{
if (value < min) value = min;
else if (value > max) value = max;
return value;
}
/// Порт зажима model->SetParameterValue: значение параметра в границах [min, max].
private static float ClampParameterValue(float value, float min, float max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
private static CubismMotionCurveTarget ParseTarget(string target) => target switch
{
"Model" => CubismMotionCurveTarget.Model,
"Parameter" => CubismMotionCurveTarget.Parameter,
"PartOpacity" => CubismMotionCurveTarget.PartOpacity,
_ => CubismMotionCurveTarget.Parameter
};
private void ParseEvents()
{
for (int i = 0; i < _motionJson.GetEventCount(); i++)
{
_events.Add(new CubismMotionEvent
{
FireTime = _motionJson.GetEventTime(i),
Value = _motionJson.GetEventValue(i)
});
}
}
}
///
/// Internal data for a single parsed motion curve. Хранит тип цели, ID,
/// диапазон сегментов в общем плоском массиве (BaseSegmentIndex/SegmentCount)
/// и пер-кривые времена фейда; значения < 0 означают «не задано» (как в SDK).
///
internal class MotionCurveData
{
public CubismMotionCurveTarget Type;
public string Id;
public int SegmentCount;
public int BaseSegmentIndex;
public float FadeInTime;
public float FadeOutTime;
}
}