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; } }