using System.Collections.Generic;
using System.Text.Json;
namespace Cthangover.Live2D.Cubism.Framework
{
///
/// Blend mode for expression parameters.
/// Determines how the expression value is combined with the existing
/// parameter value during animation.
///
public enum ExpressionBlendType { Additive = 0, Multiply = 1, Overwrite = 2 }
///
/// A single parameter entry in an expression motion.
/// Targets a model parameter by ID with a specific blend type and value.
///
public struct ExpressionParameter
{
/// Target model parameter ID.
public string ParameterId;
/// How to combine this value with existing parameter data.
public ExpressionBlendType BlendType;
/// The target value (interpretation depends on ).
public float Value;
}
///
/// Expression motion, parsed from a Cubism .exp3.json file.
/// Inherits fade and weight management from .
///
/// Формулы применения — точный порт CubismExpressionMotion::DoUpdateParameters
/// через Set/Add/MultiplyParameterValue из CubismModel (SDK 5-r.5), weight — fade-вес:
///
/// - — value + expValue * weight
/// - — value * (1 + (expValue - 1) * weight)
/// - — value * (1 - weight) + expValue * weight
///
///
/// Как и в SDK, результат зажимается в [min, max] параметра (порт
/// CubismModel::SetParameterValue), а параметры применяются в порядке их
/// перечисления в exp3.json: при нескольких записях с одним Id побеждает последняя.
/// Fade-время по умолчанию — 1 c (DefaultFadeTime), если в файле не задано.
///
/// Загружается и запускается через .
///
public class CubismExpressionMotion : ACubismMotion
{
/// Порт DefaultFadeTime из CubismExpressionMotion.cpp.
private const float DefaultFadeTime = 1f;
private readonly List _parameters = new();
///
/// Parses an .exp3.json byte buffer into an expression motion.
/// Reads fade-in/out times and the list of target parameters with
/// their blend types and values.
///
public CubismExpressionMotion(byte[] jsonBytes)
{
var json = JsonDocument.Parse(jsonBytes);
var root = json.RootElement;
// Порт CubismExpressionMotion::Parse: root[FadeInTime].ToFloat(DefaultFadeTime).
_fadeInSeconds = root.TryGetProperty("FadeInTime", out var fi) && fi.ValueKind == JsonValueKind.Number ? fi.GetSingle() : DefaultFadeTime;
_fadeOutSeconds = root.TryGetProperty("FadeOutTime", out var fo) && fo.ValueKind == JsonValueKind.Number ? fo.GetSingle() : DefaultFadeTime;
if (root.TryGetProperty("Parameters", out var paramsArr))
{
foreach (var p in paramsArr.EnumerateArray())
{
// "Blend": null or absent -> Additive (SDK ToBlendFunction default).
var blend = p.TryGetProperty("Blend", out var b) && b.ValueKind == JsonValueKind.String
? ParseBlendType(b.GetString())
: ExpressionBlendType.Additive;
_parameters.Add(new ExpressionParameter
{
ParameterId = p.GetProperty("Id").GetString(),
Value = p.TryGetProperty("Value", out var val) && val.ValueKind == JsonValueKind.Number ? val.GetSingle() : 0f,
BlendType = blend
});
}
}
}
private static ExpressionBlendType ParseBlendType(string value) => value switch
{
"Multiply" => ExpressionBlendType.Multiply,
"Overwrite" => ExpressionBlendType.Overwrite,
_ => ExpressionBlendType.Additive
};
///
/// Applies all expression parameters to the model using their
/// respective blend modes. Called every frame by the queue manager.
///
public override unsafe void DoUpdateParameters(
CubismNativeModel model, float userTimeSeconds, float weight, CubismMotionQueueEntry entry)
{
var idsPtr = model.GetParameterIds();
var values = model.GetParameterValues();
var minValues = model.GetParameterMinimumValues();
var maxValues = model.GetParameterMaximumValues();
var indexById = new Dictionary(model.ParameterCount);
for (int i = 0; i < model.ParameterCount; i++)
indexById[CubismNativeModel.ReadStringFromPtrArray(idsPtr, i)] = i;
// Порт: цикл по параметрам выражения (в порядке exp3.json).
foreach (var ep in _parameters)
{
if (!indexById.TryGetValue(ep.ParameterId, out int pi)) continue;
switch (ep.BlendType)
{
case ExpressionBlendType.Additive:
// AddParameterValue: value + (expValue * weight)
values[pi] = ClampParameterValue(values[pi] + ep.Value * weight, minValues[pi], maxValues[pi]);
break;
case ExpressionBlendType.Multiply:
// MultiplyParameterValue: value * (1 + (expValue - 1) * weight)
values[pi] = ClampParameterValue(values[pi] * (1f + (ep.Value - 1f) * weight), minValues[pi], maxValues[pi]);
break;
case ExpressionBlendType.Overwrite:
// SetParameterValue(id, value, weight): expValue зажимается, затем лерп
values[pi] = values[pi] * (1f - weight)
+ ClampParameterValue(ep.Value, minValues[pi], maxValues[pi]) * weight;
break;
}
}
}
/// Порт GetParameterClampValue — зажим значения параметра в [min, max].
private static float ClampParameterValue(float value, float min, float max)
{
if (value < min) return min;
if (value > max) return max;
return value;
}
}
}