upd: sync local changes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b8ddf1f0df
commit
a9d4417fce
27 files changed
+3446
-1148
No files matched your search
@@ -6,44 +6,75 @@ 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:
|
||||
/// Pose groups organize model parts into mutual-exclusion groups — only one
|
||||
/// part in each group is visible at a time. Commonly used for eye states
|
||||
/// (open/closed/surprised) and other multi-state part switches.
|
||||
///
|
||||
/// This is a faithful port of CubismPose.cpp (Cubism SDK for Native 5-r.5):
|
||||
/// <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>
|
||||
/// <item>Part groups are stored as a flattened list with per-group counts
|
||||
/// (matching the SDK layout, so index arithmetic in Reset/DoFade is identical).</item>
|
||||
/// <item>Each group part references both a part index (for opacity) and a
|
||||
/// parameter index (for visibility). The visible part of a group is
|
||||
/// decided by the PARAMETER VALUE (> Epsilon), not by the part opacity.
|
||||
/// If a pose parameter/part does not exist in the model, the SDK's
|
||||
/// "not exist" maps are emulated, and the group falls back to its
|
||||
/// first part with opacity 1.</item>
|
||||
/// <item>Reset (on model change): the first part of each group gets
|
||||
/// parameter value 1 / part opacity 1, all others get 0; linked parts
|
||||
/// resolve their indices too.</item>
|
||||
/// <item>DoFade: the visible part fades toward 1 at rate
|
||||
/// deltaTime / fadeTimeSeconds while all other parts are pushed toward
|
||||
/// 0 with the SDK's piecewise-linear crossing formula (Phi = 0.5) and
|
||||
/// back-opacity threshold (0.15). If no part is visible, the group
|
||||
/// falls back to its first part with opacity 1. If fadeTimeSeconds is
|
||||
/// 0, the visible part becomes fully opaque immediately.</item>
|
||||
/// <item>CopyPartOpacities: parts with a "Link" list copy their current
|
||||
/// part opacity to every linked part.</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
|
||||
private const float Epsilon = 0.001f;
|
||||
private const float DefaultFadeInSeconds = 0.5f;
|
||||
private const float Phi = 0.5f;
|
||||
private const float BackOpacityThreshold = 0.15f;
|
||||
|
||||
private sealed class PartData
|
||||
{
|
||||
public string PartId;
|
||||
public int PartIndex;
|
||||
public int ParameterIndex;
|
||||
public List<PartData> Link;
|
||||
public int PartIndex = -1;
|
||||
public int ParameterIndex = -1;
|
||||
public readonly List<PartData> Link = new();
|
||||
|
||||
/// <summary>
|
||||
/// SDK CubismPose::PartData::Initialize — resolves both indices for
|
||||
/// this part id and initializes its pose parameter to 1.
|
||||
/// </summary>
|
||||
public void Initialize(CubismNativeModel model, CubismPose owner)
|
||||
{
|
||||
ParameterIndex = owner.FindParameterIndex(model, PartId);
|
||||
PartIndex = owner.FindPartIndex(model, PartId);
|
||||
owner.SetParameterValue(model, ParameterIndex, PartId, 1f);
|
||||
}
|
||||
}
|
||||
|
||||
private readonly List<List<PartData>> _partGroups = new();
|
||||
private readonly float _fadeTimeSeconds;
|
||||
private readonly List<PartData> _partGroups = new();
|
||||
private readonly List<int> _partGroupCounts = new();
|
||||
private float _fadeTimeSeconds = DefaultFadeInSeconds;
|
||||
private IntPtr _lastModel = IntPtr.Zero;
|
||||
|
||||
// Emulation of the SDK's _notExistParameterValues / _notExistPartOpacities maps:
|
||||
// pose3.json may reference parameters/parts that do not exist in the model.
|
||||
// They are keyed by id here (the SDK keys parameter entries by synthesized
|
||||
// indices; per-id keys are equivalent for this emulation).
|
||||
private readonly Dictionary<string, float> _notExistParameterValues = new();
|
||||
private readonly Dictionary<string, float> _notExistPartOpacities = new();
|
||||
|
||||
/// <summary>
|
||||
/// Parses a .pose3.json byte buffer.
|
||||
/// Extracts the global fade time and all part groups with their
|
||||
/// linked-part chains.
|
||||
/// Extracts the fade time and all part groups (flattened, with counts per
|
||||
/// group) including their linked-part chains.
|
||||
/// </summary>
|
||||
public CubismPose(byte[] pose3JsonBytes)
|
||||
{
|
||||
@@ -51,168 +82,273 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
|
||||
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;
|
||||
if (root.TryGetProperty("FadeInTime", out var ft) && ft.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
_fadeTimeSeconds = ft.GetSingle();
|
||||
if (_fadeTimeSeconds < 0f)
|
||||
_fadeTimeSeconds = DefaultFadeInSeconds;
|
||||
}
|
||||
|
||||
if (!root.TryGetProperty("Groups", out var groups) || groups.ValueKind != JsonValueKind.Array) return;
|
||||
|
||||
foreach (var group in groups.EnumerateArray())
|
||||
{
|
||||
var partList = new List<PartData>();
|
||||
var groupCount = 0;
|
||||
foreach (var part in group.EnumerateArray())
|
||||
{
|
||||
var pd = new PartData
|
||||
{
|
||||
PartId = part.GetProperty("Id").GetString(),
|
||||
PartIndex = -1,
|
||||
ParameterIndex = -1,
|
||||
Link = new List<PartData>()
|
||||
PartId = part.GetProperty("Id").GetString()
|
||||
};
|
||||
|
||||
if (part.TryGetProperty("Link", out var links))
|
||||
if (part.TryGetProperty("Link", out var links) && links.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var link in links.EnumerateArray())
|
||||
pd.Link.Add(new PartData { PartId = link.GetString(), PartIndex = -1, ParameterIndex = -1 });
|
||||
pd.Link.Add(new PartData { PartId = link.GetString() });
|
||||
}
|
||||
|
||||
partList.Add(pd);
|
||||
_partGroups.Add(pd);
|
||||
++groupCount;
|
||||
}
|
||||
_partGroups.Add(partList);
|
||||
_partGroupCounts.Add(groupCount);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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.
|
||||
/// Per-frame pose update, mirroring CubismPose::UpdateParameters.
|
||||
/// Detects model changes to trigger a full reset, then fades 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)
|
||||
public 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);
|
||||
_lastModel = model.ModelPtr;
|
||||
|
||||
// 設定から時間を変更すると、経過時間がマイナスになることがあるので、経過時間0として対応。
|
||||
if (deltaTimeSeconds < 0f)
|
||||
deltaTimeSeconds = 0f;
|
||||
|
||||
var beginIndex = 0;
|
||||
for (int i = 0; i < _partGroupCounts.Count; i++)
|
||||
{
|
||||
var partGroupCount = _partGroupCounts[i];
|
||||
DoFade(model, deltaTimeSeconds, beginIndex, partGroupCount);
|
||||
beginIndex += partGroupCount;
|
||||
}
|
||||
|
||||
CopyPartOpacities(model);
|
||||
}
|
||||
|
||||
/// <summary>SDK CubismPose::Reset — re-resolves indices and restores the
|
||||
/// default state: the first part of each group is visible (parameter 1,
|
||||
/// opacity 1), all others hidden, links re-initialized.</summary>
|
||||
private unsafe void Reset(CubismNativeModel model)
|
||||
{
|
||||
var idsPtr = model.GetPartIds();
|
||||
var opacities = model.GetPartOpacities();
|
||||
int partCount = model.GetPartCount();
|
||||
var beginIndex = 0;
|
||||
|
||||
foreach (var group in _partGroups)
|
||||
for (int gi = 0; gi < _partGroupCounts.Count; gi++)
|
||||
{
|
||||
for (int i = 0; i < group.Count; i++)
|
||||
var groupCount = _partGroupCounts[gi];
|
||||
|
||||
for (int j = beginIndex; j < beginIndex + groupCount; j++)
|
||||
{
|
||||
var pd = group[i];
|
||||
pd.PartIndex = -1;
|
||||
pd.ParameterIndex = -1;
|
||||
var pd = _partGroups[j];
|
||||
pd.Initialize(model, this);
|
||||
|
||||
for (int pi = 0; pi < partCount; pi++)
|
||||
{
|
||||
if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi) == pd.PartId)
|
||||
{
|
||||
pd.PartIndex = pi;
|
||||
opacities[pi] = i == 0 ? 1f : 0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pd.PartIndex < 0)
|
||||
continue;
|
||||
|
||||
group[i] = pd;
|
||||
SetPartOpacity(model, pd.PartIndex, pd.PartId, j == beginIndex ? 1f : 0f);
|
||||
SetParameterValue(model, pd.ParameterIndex, pd.PartId, j == beginIndex ? 1f : 0f);
|
||||
|
||||
for (int k = 0; k < pd.Link.Count; k++)
|
||||
pd.Link[k].Initialize(model, this);
|
||||
}
|
||||
|
||||
beginIndex += groupCount;
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void DoFade(CubismNativeModel model, List<PartData> group, float deltaTimeSeconds)
|
||||
/// <summary>SDK CubismPose::DoFade — visibility is driven by the group
|
||||
/// part PARAMETER values; the visible part fades in while the rest are
|
||||
/// faded out with the crossing-threshold formula.</summary>
|
||||
private unsafe void DoFade(CubismNativeModel model, float deltaTimeSeconds, int beginIndex, int partGroupCount)
|
||||
{
|
||||
const float Phi = 0.5f;
|
||||
const float BackOpacityThreshold = 0.15f;
|
||||
var visiblePartIndex = -1;
|
||||
var newOpacity = 1f;
|
||||
|
||||
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++)
|
||||
// 現在、表示状態になっているパーツを取得
|
||||
for (int i = beginIndex; i < beginIndex + partGroupCount; i++)
|
||||
{
|
||||
var pd = group[i];
|
||||
if (pd.PartIndex < 0) continue;
|
||||
var pd = _partGroups[i];
|
||||
|
||||
if (opacities[pd.PartIndex] > 0.001f)
|
||||
if (GetParameterValue(model, pd.ParameterIndex, pd.PartId) > Epsilon)
|
||||
{
|
||||
visibleIndex = i;
|
||||
break;
|
||||
if (visiblePartIndex >= 0)
|
||||
break;
|
||||
|
||||
visiblePartIndex = i;
|
||||
|
||||
if (_fadeTimeSeconds == 0f)
|
||||
{
|
||||
newOpacity = 1f;
|
||||
continue;
|
||||
}
|
||||
|
||||
newOpacity = ComputeVisibleOpacity(
|
||||
GetPartOpacity(model, pd.PartIndex, pd.PartId),
|
||||
deltaTimeSeconds,
|
||||
_fadeTimeSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < group.Count; i++)
|
||||
if (visiblePartIndex < 0)
|
||||
{
|
||||
var pd = group[i];
|
||||
if (pd.PartIndex < 0) continue;
|
||||
visiblePartIndex = 0;
|
||||
newOpacity = 1f;
|
||||
}
|
||||
|
||||
if (i == visibleIndex)
|
||||
// 表示パーツ、非表示パーツの不透明度を設定する
|
||||
for (int i = beginIndex; i < beginIndex + partGroupCount; i++)
|
||||
{
|
||||
var pd = _partGroups[i];
|
||||
|
||||
if (visiblePartIndex == i)
|
||||
{
|
||||
var fadeRate = deltaTimeSeconds / _fadeTimeSeconds;
|
||||
newOpacity = MathF.Min(opacities[pd.PartIndex] + fadeRate, 1f);
|
||||
opacities[pd.PartIndex] = newOpacity;
|
||||
SetPartOpacity(model, pd.PartIndex, pd.PartId, newOpacity); // 先に設定
|
||||
}
|
||||
else
|
||||
{
|
||||
float a1;
|
||||
if (newOpacity < Phi)
|
||||
a1 = newOpacity * (Phi - 1f) / Phi + 1f;
|
||||
else
|
||||
a1 = (1f - newOpacity) * Phi / (1f - Phi);
|
||||
var opacity = GetPartOpacity(model, pd.PartIndex, pd.PartId);
|
||||
var a1 = ComputeHiddenOpacity(newOpacity);
|
||||
|
||||
var backOpacity = (1f - a1) * (1f - newOpacity);
|
||||
if (backOpacity > BackOpacityThreshold)
|
||||
a1 = 1f - BackOpacityThreshold / (1f - newOpacity);
|
||||
if (opacity > a1)
|
||||
opacity = a1;
|
||||
|
||||
opacities[pd.PartIndex] = MathF.Min(opacities[pd.PartIndex], a1);
|
||||
SetPartOpacity(model, pd.PartIndex, pd.PartId, opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>SDK CubismPose::CopyPartOpacities — every group part with a
|
||||
/// Link list copies its current part opacity to all linked parts.</summary>
|
||||
private unsafe void CopyPartOpacities(CubismNativeModel model)
|
||||
{
|
||||
var idsPtr = model.GetPartIds();
|
||||
var opacities = model.GetPartOpacities();
|
||||
int partCount = model.GetPartCount();
|
||||
|
||||
foreach (var group in _partGroups)
|
||||
foreach (var partData in _partGroups)
|
||||
{
|
||||
for (int gi = 0; gi < group.Count; gi++)
|
||||
{
|
||||
var pd = group[gi];
|
||||
if (pd.PartIndex < 0 || pd.Link == null) continue;
|
||||
if (partData.Link.Count == 0)
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
var opacity = GetPartOpacity(model, partData.PartIndex, partData.PartId);
|
||||
|
||||
for (int linkIndex = 0; linkIndex < partData.Link.Count; linkIndex++)
|
||||
{
|
||||
var linkPart = partData.Link[linkIndex];
|
||||
|
||||
if (linkPart.PartIndex < 0)
|
||||
continue;
|
||||
|
||||
SetPartOpacity(model, linkPart.PartIndex, linkPart.PartId, opacity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Resolves a part id to a part index, or -1 when absent.</summary>
|
||||
private unsafe int FindPartIndex(CubismNativeModel model, string partId)
|
||||
{
|
||||
var idsPtr = model.GetPartIds();
|
||||
var partCount = model.GetPartCount();
|
||||
for (int i = 0; i < partCount; i++)
|
||||
{
|
||||
if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, i) == partId)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>Resolves a parameter id to a parameter index, or -1 when absent.</summary>
|
||||
private unsafe int FindParameterIndex(CubismNativeModel model, string parameterId)
|
||||
{
|
||||
var idsPtr = model.GetParameterIds();
|
||||
var parameterCount = model.ParameterCount;
|
||||
for (int i = 0; i < parameterCount; i++)
|
||||
{
|
||||
if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, i) == parameterId)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private unsafe float GetParameterValue(CubismNativeModel model, int parameterIndex, string parameterId)
|
||||
{
|
||||
if (parameterIndex < 0)
|
||||
return _notExistParameterValues.TryGetValue(parameterId, out var v) ? v : 0f;
|
||||
return model.GetParameterValues()[parameterIndex];
|
||||
}
|
||||
|
||||
private unsafe void SetParameterValue(CubismNativeModel model, int parameterIndex, string parameterId, float value)
|
||||
{
|
||||
if (parameterIndex < 0)
|
||||
{
|
||||
_notExistParameterValues[parameterId] = value;
|
||||
return;
|
||||
}
|
||||
model.GetParameterValues()[parameterIndex] = value;
|
||||
}
|
||||
|
||||
private unsafe float GetPartOpacity(CubismNativeModel model, int partIndex, string partId)
|
||||
{
|
||||
if (partIndex < 0)
|
||||
return _notExistPartOpacities.TryGetValue(partId, out var v) ? v : 0f;
|
||||
return model.GetPartOpacities()[partIndex];
|
||||
}
|
||||
|
||||
private unsafe void SetPartOpacity(CubismNativeModel model, int partIndex, string partId, float opacity)
|
||||
{
|
||||
if (partIndex < 0)
|
||||
{
|
||||
_notExistPartOpacities[partId] = opacity;
|
||||
return;
|
||||
}
|
||||
model.GetPartOpacities()[partIndex] = opacity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure helper (SDK DoFade, visible branch): the visible part's opacity
|
||||
/// grows by deltaTimeSeconds / fadeTimeSeconds, clamped to 1.
|
||||
/// Internal for unit tests.
|
||||
/// </summary>
|
||||
internal static float ComputeVisibleOpacity(float currentOpacity, float deltaTimeSeconds, float fadeTimeSeconds)
|
||||
{
|
||||
var newOpacity = currentOpacity + (deltaTimeSeconds / fadeTimeSeconds);
|
||||
return newOpacity > 1f ? 1f : newOpacity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pure helper (SDK DoFade, hidden branch): piecewise-linear alpha with
|
||||
/// the Phi = 0.5 crossing and the 0.15 back-opacity threshold.
|
||||
/// Internal for unit tests.
|
||||
/// </summary>
|
||||
internal static float ComputeHiddenOpacity(float newOpacity)
|
||||
{
|
||||
float a1;
|
||||
if (newOpacity < Phi)
|
||||
a1 = newOpacity * (Phi - 1f) / Phi + 1f; // (0,1),(phi,phi)を通る直線式
|
||||
else
|
||||
a1 = (1f - newOpacity) * Phi / (1f - Phi); // (1,0),(phi,phi)を通る直線式
|
||||
|
||||
var backOpacity = (1f - a1) * (1f - newOpacity);
|
||||
if (backOpacity > BackOpacityThreshold)
|
||||
a1 = 1f - BackOpacityThreshold / (1f - newOpacity);
|
||||
|
||||
return a1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user