using System;
using System.Collections.Generic;
using System.Text.Json;
namespace Cthangover.Live2D.Cubism.Framework
{
public delegate float MotionSegmentFunction(float[] segments, int baseIndex, float time);
///
/// 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
///
///
/// Each curve is a sequence of segments (linear, stepped, bezier) stored in
/// flat arrays for performance. Evaluation uses a segment-finding pass
/// followed by per-segment interpolation — linear, stepped, or bezier
/// (analytic via Cardano formula or binary search for restricted beziers).
///
/// The four-pass update cycle ():
///
/// - Evaluate model curves (eye blink, lip sync, opacity)
/// - Evaluate parameter curves, applying eye blink/lip sync modulation
/// - Apply eye blink/lip sync to remaining (non-animated) parameters
/// - Evaluate part opacity curves
///
///
/// Parameter fade-in/out is handled per-curve (overriding the motion-level
/// fade settings). Curve targets and IDs come from the motion3.json metadata.
///
public class CubismMotion : ACubismMotion
{
private const string ModelCurveIdEyeBlink = "EyeBlink";
private const string ModelCurveIdLipSync = "LipSync";
private const string ModelCurveIdOpacity = "Opacity";
private readonly float _sourceFrameRate;
private float _lastWeight;
private CubismMotionJson _motionJson;
private readonly List _modelCurves = new();
private readonly List _parameterCurves = new();
private readonly List _partOpacityCurves = new();
private readonly List _eyeBlinkParameterIds = new();
private readonly List _lipSyncParameterIds = new();
private readonly List _events = new();
private readonly List _firedEventValues = new();
private float _modelOpacity = 1f;
private bool _areBeziersRestricted;
///
/// Parses a .motion3.json byte buffer.
/// Reads the meta section (duration, loop, fps, fade times, bezier flag)
/// and delegates curve/event parsing to private helpers.
///
public CubismMotion(byte[] motion3JsonBytes)
{
_motionJson = new CubismMotionJson(motion3JsonBytes);
_sourceFrameRate = _motionJson.GetFps();
_areBeziersRestricted = _motionJson.AreBeziersRestricted();
_isLoop = _motionJson.IsLoop();
if (_motionJson.GetFadeInTime() >= 0f) _fadeInSeconds = _motionJson.GetFadeInTime();
if (_motionJson.GetFadeOutTime() >= 0f) _fadeOutSeconds = _motionJson.GetFadeOutTime();
ParseCurves();
ParseEvents();
}
///
/// Configures which parameters are affected by eye blink and lip sync
/// modulation. These lists typically come from the model3.json Groups.
/// When a parameter curve targets one of these IDs, its value is
/// multiplied by the eye blink curve value (for blink) or added to
/// the lip sync curve value (for lip sync) in Pass 2.
///
public void SetEffectIds(List eyeBlinkIds, List lipSyncIds)
{
_eyeBlinkParameterIds.Clear();
_lipSyncParameterIds.Clear();
if (eyeBlinkIds != null) _eyeBlinkParameterIds.AddRange(eyeBlinkIds);
if (lipSyncIds != null) _lipSyncParameterIds.AddRange(lipSyncIds);
}
///
/// Returns the total duration from the motion3.json, or -1 for looping motions.
///
public override float GetDuration() => _isLoop ? -1f : _motionJson.GetDuration();
///
/// Returns the full duration even when looping is active — used for computing
/// loop cycle length.
///
public override float GetLoopDuration() => _motionJson.GetDuration();
/// Whether the model's overall opacity is actively controlled by this motion.
public bool IsExistModelOpacity() => _modelOpacity > 0f;
/// Current model opacity value from the Opacity model curve, clamped [0,1].
public float GetModelOpacity() => _modelOpacity;
///
/// Collects event values that fired between two time points.
/// Clears the internal fired-event list on each call. (Currently returns 0f;
/// the fired values list is populated as a side effect.)
///
public float GetFiredEventValue(float beforeTime, float motionTime)
{
_firedEventValues.Clear();
foreach (var evt in _events)
if (evt.FireTime > beforeTime && evt.FireTime <= motionTime)
_firedEventValues.Add(evt.Value);
return 0f;
}
///
/// Four-pass parameter update. Called by
/// every frame.
///
public override unsafe void DoUpdateParameters(
CubismNativeModel model, float userTimeSeconds, float weight, CubismMotionQueueEntry entry)
{
if (model == null || entry == null) return;
AdjustEndTime(entry, _motionJson.GetDuration());
var timeOffset = userTimeSeconds - entry.StartTime;
var duration = _motionJson.GetDuration();
if (_isLoop && duration > 0f)
{
while (timeOffset > duration)
timeOffset -= duration;
}
var fadeWeight = weight;
int paramCount = model.ParameterCount;
var idsPtr = model.GetParameterIds();
var values = model.GetParameterValues();
var minValues = model.GetParameterMinimumValues();
var maxValues = model.GetParameterMaximumValues();
var defaultValues = model.GetParameterDefaultValues();
float eyeBlinkValue = 0f;
float lipSyncValue = 0f;
_modelOpacity = 1f;
var modifiedFlags = new bool[paramCount];
// Pass 1 — model curves
foreach (var curve in _modelCurves)
{
var val = EvaluateCurve(curve, timeOffset, fadeWeight);
if (curve.Id == ModelCurveIdEyeBlink) eyeBlinkValue = val;
else if (curve.Id == ModelCurveIdLipSync) lipSyncValue = val;
else if (curve.Id == ModelCurveIdOpacity) _modelOpacity = Math.Clamp(val, 0f, 1f);
}
// Pass 2 — parameter curves
foreach (var curve in _parameterCurves)
{
var curveValue = EvaluateCurve(curve, timeOffset, fadeWeight);
for (int pi = 0; pi < paramCount; pi++)
{
var pid = CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi);
if (pid != curve.Id) continue;
var modulatedValue = curveValue;
if (_eyeBlinkParameterIds.Contains(pid))
modulatedValue *= eyeBlinkValue;
if (_lipSyncParameterIds.Contains(pid))
modulatedValue += lipSyncValue;
var paramFadeWeight = fadeWeight;
if (curve.ParamFadeIn >= 0f || curve.ParamFadeOut >= 0f)
{
var fi = curve.ParamFadeIn >= 0f ? curve.ParamFadeIn : 1f;
var fo = curve.ParamFadeOut >= 0f ? curve.ParamFadeOut : 1f;
var paramFadeIn = CubismMath.GetEasingSine(
(userTimeSeconds - entry.FadeInStartTime) / fi);
var paramFadeOut = CubismMath.GetEasingSine(
(entry.EndTime > 0f ? entry.EndTime - userTimeSeconds : 1f) / fo);
paramFadeWeight = _weight * paramFadeIn * paramFadeOut;
}
values[pi] = values[pi] + (modulatedValue - values[pi]) * paramFadeWeight;
ClampParameter(pi, values, minValues, maxValues);
modifiedFlags[pi] = true;
break;
}
}
// Pass 3 — remaining eye blink / lip sync (non-modulated params)
for (int pi = 0; pi < paramCount; pi++)
{
if (modifiedFlags[pi]) continue;
var pid = CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi);
if (_eyeBlinkParameterIds.Contains(pid))
values[pi] = eyeBlinkValue > 0f ? values[pi] * eyeBlinkValue : values[pi];
if (_lipSyncParameterIds.Contains(pid))
values[pi] += lipSyncValue;
}
// Pass 4 — part opacity curves
var partOpacities = model.GetPartOpacities();
var partIdsPtr = model.GetPartIds();
int partCount = model.GetPartCount();
foreach (var curve in _partOpacityCurves)
{
var val = EvaluateCurve(curve, timeOffset, fadeWeight);
for (int pi = 0; pi < partCount; pi++)
{
var pid = CubismNativeModel.ReadStringFromPtrArray(partIdsPtr, pi);
if (pid == curve.Id)
{
partOpacities[pi] = val;
break;
}
}
}
// End-of-motion
if (!_isLoop && timeOffset >= duration)
{
entry.IsFinished = true;
OnFinishedMotion?.Invoke(this);
}
else if (_isLoop && timeOffset >= duration)
{
entry.StartTime = userTimeSeconds - (timeOffset - duration);
if (!_isLoopFadeIn)
entry.FadeInStartTime = userTimeSeconds;
}
}
private float EvaluateCurve(MotionCurveData curve, float time, float weight)
{
var segIdx = FindSegmentIndex(curve, time);
if (segIdx < 0 || segIdx >= curve.SegmentCount) return 0f;
return EvaluateSegment(curve, segIdx, time);
}
private int FindSegmentIndex(MotionCurveData curve, float time)
{
for (int i = 0; i < curve.SegmentCount; i++)
{
var nextIdx = curve.BaseSegmentIndex + i + 1;
if (nextIdx < curve.Points.Length)
{
var nextPoint = curve.Points[nextIdx];
if (time < nextPoint.Time)
return i;
}
}
var lastPt = curve.Points[curve.BaseSegmentIndex + curve.SegmentCount];
if (time <= lastPt.Time)
return curve.SegmentCount - 1;
return -1;
}
private float EvaluateSegment(MotionCurveData curve, int segIndex, float time)
{
var basePtIdx = curve.BaseSegmentIndex + segIndex;
var p0 = curve.Points[basePtIdx];
var p1 = curve.Points[basePtIdx + 1];
var segType = curve.SegmentTypes != null && segIndex < curve.SegmentTypes.Length
? curve.SegmentTypes[segIndex] : CubismMotionSegmentType.Linear;
switch (segType)
{
case CubismMotionSegmentType.Linear:
return CubismMath.LinearEvaluation(ref p0, ref p1, time);
case CubismMotionSegmentType.Stepped:
return p0.Value;
case CubismMotionSegmentType.InverseStepped:
return p1.Value;
case CubismMotionSegmentType.Bezier:
var cp1 = curve.Points[basePtIdx + 1];
var cp2 = curve.Points[basePtIdx + 2];
var cp3 = curve.Points[basePtIdx + 3];
if (_areBeziersRestricted)
return CubismMath.BezierEvaluateBinarySearch(
p0.Time, cp1.Time, cp2.Time, cp3.Time,
p0.Value, cp1.Value, cp2.Value, cp3.Value, time);
else
{
var t = CubismMath.CardanoAlgorithmForBezier(
p0.Time, cp1.Time, cp2.Time, cp3.Time, time);
t = Math.Clamp(t, 0f, 1f);
var t1 = 1f - t;
var t2 = t * t;
var t12 = t1 * t1;
return t12 * t1 * p0.Value + 3f * t12 * t * cp1.Value +
3f * t1 * t2 * cp2.Value + t2 * t * cp3.Value;
}
default:
return CubismMath.LinearEvaluation(ref p0, ref p1, time);
}
}
private static unsafe void ClampParameter(int index, float* values, float* min, float* max)
{
if (values[index] < min[index]) values[index] = min[index];
if (values[index] > max[index]) values[index] = max[index];
}
private void ParseCurves()
{
var segsFlat = new float[_motionJson.GetTotalSegmentCount() * 2];
var segTypesFlat = new CubismMotionSegmentType[_motionJson.GetTotalSegmentCount()];
var allPoints = new List();
int segOffset = 0;
for (int ci = 0; ci < _motionJson.GetCurveCount(); ci++)
{
var target = _motionJson.GetCurveTarget(ci);
var id = _motionJson.GetCurveId(ci);
var fadeIn = _motionJson.GetCurveFadeInTime(ci);
var fadeOut = _motionJson.GetCurveFadeOutTime(ci);
_motionJson.GetCurveSegments(ci, out var rawSegments);
var segmentCount = ParseSegments(rawSegments, segsFlat, segTypesFlat, segOffset, allPoints);
var curveTarget = ParseTarget(target);
var curveData = new MotionCurveData
{
Type = curveTarget,
Id = id,
SegmentCount = segmentCount,
BaseSegmentIndex = segOffset,
ParamFadeIn = fadeIn,
ParamFadeOut = fadeOut,
Points = allPoints.ToArray(),
SegmentTypes = segTypesFlat
};
segOffset += segmentCount;
switch (curveTarget)
{
case CubismMotionCurveTarget.Model:
_modelCurves.Add(curveData); break;
case CubismMotionCurveTarget.Parameter:
_parameterCurves.Add(curveData); break;
case CubismMotionCurveTarget.PartOpacity:
_partOpacityCurves.Add(curveData); break;
}
}
}
private int ParseSegments(float[] raw, float[] segsFlat, CubismMotionSegmentType[] segTypesFlat,
int segOffset, List points)
{
int count = 0;
int i = 0;
while (i < raw.Length)
{
var segType = (CubismMotionSegmentType)(int)raw[i++];
if (count == 0)
{
// First "segment" is actually the starting point
points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] });
continue;
}
segTypesFlat[segOffset + count - 1] = segType;
switch (segType)
{
case CubismMotionSegmentType.Linear:
case CubismMotionSegmentType.Stepped:
case CubismMotionSegmentType.InverseStepped:
points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] });
break;
case CubismMotionSegmentType.Bezier:
points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] });
points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] });
points.Add(new CubismMotionPoint { Time = raw[i++], Value = raw[i++] });
break;
}
count++;
}
return count - 1;
}
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.
/// Holds the curve type, target ID, segment layout, points array,
/// and optional per-curve fade-in/fade-out durations.
///
internal class MotionCurveData
{
public CubismMotionCurveTarget Type;
public string Id;
public int SegmentCount;
public int BaseSegmentIndex;
public float ParamFadeIn;
public float ParamFadeOut;
public CubismMotionPoint[] Points;
public CubismMotionSegmentType[] SegmentTypes;
}
}