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