Files
mod_live2d/source_code/Cubism/Framework/CubismPose.cs
T
2026-09-10 10:59:37 +03:00

219 lines
7.8 KiB
C#

using System;
using System.Collections.Generic;
using System.Text.Json;
namespace Cthangover.Live2D.Cubism.Framework
{
/// <summary>
/// Manages pose-based part visibility from a Cubism .pose3.json file.
/// Poses organize model parts into mutual-exclusion groups — only one
/// part in each group is visible at a time. This is commonly used for
/// eye states (open/closed/surprised) and other binary/multi-state
/// part switches.
///
/// How it works:
/// <list type="number">
/// <item>On model change (detected via ModelPtr comparison), all
/// part groups are reset: the first part in each group gets
/// opacity 1, others get opacity 0</item>
/// <item>Each frame, the currently visible part in each group
/// fades toward opacity 1 while all others fade toward 0</item>
/// <item>Linked parts (defined by the "Link" array in pose data)
/// copy their parent's opacity, cascading visibility changes</item>
/// </list>
///
/// The fade uses a constant rate (1 / fadeTimeSeconds per second).
/// A piecewise-linear alpha calculation with a crossing threshold (Phi=0.5)
/// prevents parts from being partially visible during transitions.
/// </summary>
public class CubismPose
{
private struct PartData
{
public string PartId;
public int PartIndex;
public int ParameterIndex;
public List<PartData> Link;
}
private readonly List<List<PartData>> _partGroups = new();
private readonly float _fadeTimeSeconds;
private IntPtr _lastModel = IntPtr.Zero;
/// <summary>
/// Parses a .pose3.json byte buffer.
/// Extracts the global fade time and all part groups with their
/// linked-part chains.
/// </summary>
public CubismPose(byte[] pose3JsonBytes)
{
if (pose3JsonBytes == null || pose3JsonBytes.Length == 0) return;
var json = JsonDocument.Parse(pose3JsonBytes);
var root = json.RootElement;
_fadeTimeSeconds = root.TryGetProperty("FadeInTime", out var ft) ? ft.GetSingle() : 0.5f;
if (!root.TryGetProperty("Groups", out var groups)) return;
foreach (var group in groups.EnumerateArray())
{
var partList = new List<PartData>();
foreach (var part in group.EnumerateArray())
{
var pd = new PartData
{
PartId = part.GetProperty("Id").GetString(),
PartIndex = -1,
ParameterIndex = -1,
Link = new List<PartData>()
};
if (part.TryGetProperty("Link", out var links))
{
foreach (var link in links.EnumerateArray())
pd.Link.Add(new PartData { PartId = link.GetString(), PartIndex = -1, ParameterIndex = -1 });
}
partList.Add(pd);
}
_partGroups.Add(partList);
}
}
/// <summary>
/// Per-frame pose update. Detects model pointer changes to trigger
/// a full reset, then fades parts within each group and propagates
/// opacities to linked parts.
/// Called from <see cref="CubismModelNode.OnUpdate"/> after expressions.
/// </summary>
public unsafe void UpdateParameters(CubismNativeModel model, float deltaTimeSeconds)
{
if (model == null || model.ModelPtr == IntPtr.Zero) return;
if (_lastModel != model.ModelPtr)
{
_lastModel = model.ModelPtr;
Reset(model);
}
foreach (var group in _partGroups)
DoFade(model, group, deltaTimeSeconds);
CopyPartOpacities(model);
}
private unsafe void Reset(CubismNativeModel model)
{
var idsPtr = model.GetPartIds();
var opacities = model.GetPartOpacities();
int partCount = model.GetPartCount();
foreach (var group in _partGroups)
{
for (int i = 0; i < group.Count; i++)
{
var pd = group[i];
pd.PartIndex = -1;
pd.ParameterIndex = -1;
for (int pi = 0; pi < partCount; pi++)
{
if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi) == pd.PartId)
{
pd.PartIndex = pi;
opacities[pi] = i == 0 ? 1f : 0f;
break;
}
}
group[i] = pd;
}
}
}
private unsafe void DoFade(CubismNativeModel model, List<PartData> group, float deltaTimeSeconds)
{
const float Phi = 0.5f;
const float BackOpacityThreshold = 0.15f;
var idsPtr = model.GetPartIds();
var opacities = model.GetPartOpacities();
int partCount = model.GetPartCount();
float newOpacity = 1f;
int visibleIndex = 0;
for (int i = 0; i < group.Count; i++)
{
var pd = group[i];
if (pd.PartIndex < 0) continue;
if (opacities[pd.PartIndex] > 0.001f)
{
visibleIndex = i;
break;
}
}
for (int i = 0; i < group.Count; i++)
{
var pd = group[i];
if (pd.PartIndex < 0) continue;
if (i == visibleIndex)
{
var fadeRate = deltaTimeSeconds / _fadeTimeSeconds;
newOpacity = MathF.Min(opacities[pd.PartIndex] + fadeRate, 1f);
opacities[pd.PartIndex] = newOpacity;
}
else
{
float a1;
if (newOpacity < Phi)
a1 = newOpacity * (Phi - 1f) / Phi + 1f;
else
a1 = (1f - newOpacity) * Phi / (1f - Phi);
var backOpacity = (1f - a1) * (1f - newOpacity);
if (backOpacity > BackOpacityThreshold)
a1 = 1f - BackOpacityThreshold / (1f - newOpacity);
opacities[pd.PartIndex] = MathF.Min(opacities[pd.PartIndex], a1);
}
}
}
private unsafe void CopyPartOpacities(CubismNativeModel model)
{
var idsPtr = model.GetPartIds();
var opacities = model.GetPartOpacities();
int partCount = model.GetPartCount();
foreach (var group in _partGroups)
{
for (int gi = 0; gi < group.Count; gi++)
{
var pd = group[gi];
if (pd.PartIndex < 0 || pd.Link == null) continue;
var parentOpacity = opacities[pd.PartIndex];
for (int li = 0; li < pd.Link.Count; li++)
{
var link = pd.Link[li];
for (int pi = 0; pi < partCount; pi++)
{
if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi) == link.PartId)
{
link.PartIndex = pi;
opacities[pi] = parentOpacity;
pd.Link[li] = link;
break;
}
}
}
}
}
}
}
}