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
@@ -2,6 +2,7 @@ using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.UI.Event;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Cthangover.Live2D.Cubism.Framework
|
||||
{
|
||||
@@ -36,6 +37,11 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
private CubismRenderer2D _renderer;
|
||||
private CubismModelSettingJson _modelSetting;
|
||||
|
||||
// The autoload event controller this node registered its OnUpdate with.
|
||||
// Kept so _ExitTree can unregister: the controller outlives the scene,
|
||||
// and a stale registration would keep dispatching into a freed node.
|
||||
private SceneEventController _eventController;
|
||||
|
||||
private CubismEyeBlink _eyeBlink;
|
||||
private CubismBreath _breath;
|
||||
private CubismPose _pose;
|
||||
@@ -53,7 +59,29 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
private string _modelDir;
|
||||
private IModFileProvider _fileProvider;
|
||||
private bool _layoutLogged;
|
||||
private readonly System.Collections.Generic.HashSet<string> _manualParameters = new();
|
||||
// Parameters claimed by SetLiveParameter with their last written values.
|
||||
// Re-applied after the Load/Save snapshot bracket every frame, so manual
|
||||
// control survives the per-frame parameter restore.
|
||||
private readonly System.Collections.Generic.Dictionary<string, float> _manualValues = new();
|
||||
|
||||
// Cache of parameter index by id, built once after model load — hot loops
|
||||
// (face-track injection, SetLiveParameter) resolve by index without
|
||||
// per-frame Marshal.PtrToStringAnsi / string comparisons.
|
||||
private readonly Dictionary<string, int> _parameterIndexById = new();
|
||||
|
||||
private FaceTrackEntry[] _faceTrackCache = System.Array.Empty<FaceTrackEntry>();
|
||||
|
||||
private readonly List<string> _eyeBlinkIds = new();
|
||||
private readonly List<string> _lipSyncIds = new();
|
||||
|
||||
/// <summary>One precomputed face-track injection: values[index] = targetAxis * scale.</summary>
|
||||
private struct FaceTrackEntry
|
||||
{
|
||||
public int Index;
|
||||
public string Id;
|
||||
public bool FromTargetX;
|
||||
public float Scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// All model parameters with their IDs, bounds, and current values.
|
||||
@@ -178,6 +206,13 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
|
||||
_nativeModel = new CubismNativeModel();
|
||||
_nativeModel.LoadFromProvider(jsonBytes, _modelDir, _fileProvider);
|
||||
if (!_nativeModel.IsLoaded)
|
||||
{
|
||||
// LoadFromProvider logs failures (missing moc, invalid JSON) and returns
|
||||
// without a native model — FinishSetup would P/Invoke on IntPtr.Zero.
|
||||
GameLogger.Log("LIVE2D", $"LoadModelFromMod: native model load failed for '{relativePath}'", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
FinishSetup(jsonBytes);
|
||||
}
|
||||
|
||||
@@ -202,7 +237,10 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
|
||||
var ec = GetNodeOrNull<SceneEventController>("/root/EventController");
|
||||
if (ec != null)
|
||||
{
|
||||
_eventController = ec;
|
||||
ec.AddUpdateEventListener(this);
|
||||
}
|
||||
|
||||
GameLogger.Log("LIVE2D", $"Model loaded: {_nativeModel.ParameterCount} params, {_nativeModel.DrawableCount} drawables, canvas={_nativeModel.CanvasWidth}x{_nativeModel.CanvasHeight}");
|
||||
}
|
||||
@@ -211,15 +249,17 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame update called by the SceneEventController.
|
||||
/// Execution order:
|
||||
/// Execution order (SDK LAppModel::Update):
|
||||
/// <list type="number">
|
||||
/// <item>Apply deferred layout (first valid frame only)</item>
|
||||
/// <item>Update <see cref="_targetPoint"/> for face tracking</item>
|
||||
/// <item>Inject face-tracking parameters (ParamAngleX/Y, ParamEyeBallX/Y)
|
||||
/// into the native model</item>
|
||||
/// <item>Restore the parameter snapshot (<c>LoadParameters</c>)</item>
|
||||
/// <item>Evaluate motion queue (character animations)</item>
|
||||
/// <item>Evaluate expression queue (facial expressions)</item>
|
||||
/// <item>Run effect layers in order: breath, eye blink, pose, physics</item>
|
||||
/// <item>Save the parameter snapshot (<c>SaveParameters</c>) — additive layers
|
||||
/// (expressions, breath, face-track) start every frame from the same
|
||||
/// baseline and never compound frame-over-frame</item>
|
||||
/// <item>Run effect layers in SDK 5 scheduler order (ICubismUpdater.hpp):
|
||||
/// eye blink → expression → look/face-track → breath → physics → pose</item>
|
||||
/// <item>Call <c>csmUpdateModel</c> on the native model</item>
|
||||
/// <item>Update the renderer (visibility, opacity, vertex positions)</item>
|
||||
/// </list>
|
||||
@@ -227,6 +267,10 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
public void OnUpdate()
|
||||
{
|
||||
if (!_loaded) return;
|
||||
// If the node was freed while detached from the tree, _ExitTree never
|
||||
// ran and the stale registration survives: bail out before any Godot
|
||||
// API call on the disposed meshes (ObjectDisposedException).
|
||||
if (!GodotObject.IsInstanceValid(this) || !IsInsideTree()) return;
|
||||
|
||||
var now = Time.GetTicksMsec();
|
||||
var dt = _lastTickMsec == 0 ? 0.016f : (now - _lastTickMsec) / 1000f;
|
||||
@@ -237,45 +281,108 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
_userTimeSeconds += dt;
|
||||
|
||||
_targetPoint.Update(dt);
|
||||
var tx = _targetPoint.X;
|
||||
var ty = _targetPoint.Y;
|
||||
|
||||
unsafe
|
||||
{
|
||||
var values = _nativeModel.GetParameterValues();
|
||||
var idsPtr = _nativeModel.GetParameterIds();
|
||||
|
||||
for (int i = 0; i < _nativeModel.ParameterCount; i++)
|
||||
{
|
||||
var id = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i);
|
||||
if (_manualParameters.Contains(id)) continue;
|
||||
switch (id)
|
||||
{
|
||||
case "ParamAngleX": values[i] = tx * 30f; break;
|
||||
case "ParamAngleY": values[i] = ty * 30f; break;
|
||||
case "ParamEyeBallX": values[i] = tx; break;
|
||||
case "ParamEyeBallY": values[i] = ty; break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// LAppModel.cpp:388-398 — Load/SaveParameters bracket around the motion
|
||||
// phase. Without it, Additive/Multiply expressions and breath deltas
|
||||
// accumulate frame-over-frame and drive parameters into their clamps.
|
||||
_nativeModel.LoadParameters();
|
||||
|
||||
_motionManager.DoUpdateMotion(_nativeModel, _userTimeSeconds);
|
||||
_expressionManager.DoUpdateMotion(_nativeModel, _userTimeSeconds);
|
||||
|
||||
_breath?.UpdateParameters(_nativeModel, dt);
|
||||
_nativeModel.SaveParameters();
|
||||
|
||||
// Manual values survive the snapshot bracket: LoadParameters restores the
|
||||
// pre-motion baseline and would otherwise silently revert SetLiveParameter.
|
||||
ReapplyManualParameters();
|
||||
|
||||
// SDK 5 update-scheduler order (ICubismUpdater.hpp):
|
||||
// EyeBlink(200) → Expression(300) → Look(400) → Breath(500) → Physics(600) → Pose(800).
|
||||
_eyeBlink?.UpdateParameters(_nativeModel, dt);
|
||||
_pose?.UpdateParameters(_nativeModel, dt);
|
||||
_expressionManager.DoUpdateMotion(_nativeModel, _userTimeSeconds);
|
||||
InjectFaceTrackParameters();
|
||||
_breath?.UpdateParameters(_nativeModel, dt);
|
||||
_physics?.Evaluate(_nativeModel, dt);
|
||||
_pose?.UpdateParameters(_nativeModel, dt);
|
||||
|
||||
_nativeModel.UpdateModel();
|
||||
_renderer.UpdateModel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Frees the pinned native model memory when the node leaves the scene tree.
|
||||
/// Without this the pinned GCHandles (moc + model buffers) leak for the rest
|
||||
/// of the process — <see cref="CubismNativeModel.Dispose"/> is unreachable otherwise.
|
||||
/// </summary>
|
||||
public override void _ExitTree()
|
||||
{
|
||||
// Invalidate before anything else: the autoload controller may
|
||||
// dispatch OnUpdate between tree exit and the deferred delete pass,
|
||||
// and the meshes die together with this subtree.
|
||||
_loaded = false;
|
||||
if (_eventController != null)
|
||||
{
|
||||
_eventController.RemoveUpdateEventListener(this);
|
||||
_eventController = null;
|
||||
}
|
||||
_nativeModel?.Dispose();
|
||||
_nativeModel = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the face-tracking target to the native parameter values via the
|
||||
/// precomputed index cache — no string reads or comparisons in the per-frame
|
||||
/// path. Parameters claimed through <see cref="SetLiveParameter"/> are skipped.
|
||||
/// Mirrors SDK CubismLook::UpdateParameters (AddParameterValue): AngleX/Y =
|
||||
/// target × 30, EyeBallX/Y = target — layered on top of the motion result,
|
||||
/// never replacing it. The Load/Save bracket in <see cref="OnUpdate"/>
|
||||
/// keeps the addition from accumulating across frames.
|
||||
/// </summary>
|
||||
private unsafe void InjectFaceTrackParameters()
|
||||
{
|
||||
if (_faceTrackCache.Length == 0) return;
|
||||
|
||||
var values = _nativeModel.GetParameterValues();
|
||||
var tx = _targetPoint.X;
|
||||
var ty = _targetPoint.Y;
|
||||
|
||||
for (int i = 0; i < _faceTrackCache.Length; i++)
|
||||
{
|
||||
var entry = _faceTrackCache[i];
|
||||
if (_manualValues.ContainsKey(entry.Id)) continue;
|
||||
values[entry.Index] += (entry.FromTargetX ? tx : ty) * entry.Scale;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Restores values set through <see cref="SetLiveParameter"/> after
|
||||
/// <c>LoadParameters</c> resets the native parameter arrays to the snapshot.
|
||||
/// </summary>
|
||||
private unsafe void ReapplyManualParameters()
|
||||
{
|
||||
if (_manualValues.Count == 0) return;
|
||||
|
||||
var values = _nativeModel.GetParameterValues();
|
||||
foreach (var kv in _manualValues)
|
||||
{
|
||||
if (!_parameterIndexById.TryGetValue(kv.Key, out var index)) continue;
|
||||
values[index] = kv.Value;
|
||||
}
|
||||
}
|
||||
|
||||
private void DeferredApplyLayout()
|
||||
{
|
||||
if (_layoutLogged) return;
|
||||
if (!_contentBoundsReady) return;
|
||||
|
||||
// Layout from model3.json takes precedence over the Fit logic
|
||||
// (same as LAppModel::SetupModel -> modelMatrix->SetupFromLayout).
|
||||
var layout = _modelSetting?.GetLayoutMap();
|
||||
if (layout != null && layout.Count > 0)
|
||||
{
|
||||
ApplyLayoutFromModelSetting(layout);
|
||||
return;
|
||||
}
|
||||
|
||||
var parentSize = GetParentSize();
|
||||
if (parentSize.X <= 0f || parentSize.Y <= 0f) return;
|
||||
|
||||
@@ -300,6 +407,36 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
GameLogger.Log("LIVE2D", $"ApplyLayout: parentSize={parentSize} contentSize={cw}x{ch} contentCenter={_contentCenter} scale={scale} pos={Position}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the "Layout" section of model3.json through
|
||||
/// <see cref="CubismModelMatrix.SetupFromLayout"/> (SDK semantics:
|
||||
/// canvas logical units, model height defaults to 2 units via the matrix
|
||||
/// constructor). The logical view scale maps 2 logical units to the
|
||||
/// parent height (like the SDK samples' view matrix); Y is flipped
|
||||
/// because Godot's Y axis points down.
|
||||
/// </summary>
|
||||
private void ApplyLayoutFromModelSetting(Dictionary<string, float> layout)
|
||||
{
|
||||
var ppu = _nativeModel.PixelsPerUnit;
|
||||
if (ppu <= 0f) ppu = 1f;
|
||||
|
||||
var matrix = new CubismModelMatrix(CanvasWidth / ppu, CanvasHeight / ppu);
|
||||
matrix.SetupFromLayout(layout);
|
||||
|
||||
var parentSize = GetParentSize();
|
||||
var logicalPixelsPerUnit = parentSize.Y > 0f ? parentSize.Y / 2f : ppu;
|
||||
|
||||
Scale = new Vector2(
|
||||
matrix.ScaleX * logicalPixelsPerUnit / ppu,
|
||||
matrix.ScaleY * logicalPixelsPerUnit / ppu);
|
||||
Position = new Vector2(
|
||||
matrix.TranslateX * logicalPixelsPerUnit,
|
||||
-matrix.TranslateY * logicalPixelsPerUnit);
|
||||
|
||||
_layoutLogged = true;
|
||||
GameLogger.Log("LIVE2D", $"ApplyModelSettingLayout: matrixScale=({matrix.ScaleX:F3},{matrix.ScaleY:F3}) translate=({matrix.TranslateX:F3},{matrix.TranslateY:F3}) nodeScale={Scale} pos={Position}");
|
||||
}
|
||||
|
||||
private Vector2 GetParentSize()
|
||||
{
|
||||
var parent = GetParent();
|
||||
@@ -329,6 +466,44 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
Value = defaults[i]
|
||||
};
|
||||
}
|
||||
|
||||
// Parameter index cache is built once after load — hot loops
|
||||
// resolve by index only.
|
||||
_parameterIndexById.Clear();
|
||||
for (int i = 0; i < count; i++)
|
||||
_parameterIndexById[Parameters[i].Id] = i;
|
||||
|
||||
_faceTrackCache = BuildFaceTrackCache();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Precomputes the face-track injection list once after load:
|
||||
/// ParamAngleX/Y (×30) and ParamEyeBallX/Y (×1) — LAppModel semantics.
|
||||
/// Parameters absent from the model are skipped.
|
||||
/// </summary>
|
||||
private FaceTrackEntry[] BuildFaceTrackCache()
|
||||
{
|
||||
var specs = new (string Id, bool FromTargetX, float Scale)[]
|
||||
{
|
||||
("ParamAngleX", true, 30f),
|
||||
("ParamAngleY", false, 30f),
|
||||
("ParamEyeBallX", true, 1f),
|
||||
("ParamEyeBallY", false, 1f),
|
||||
};
|
||||
|
||||
var result = new List<FaceTrackEntry>(specs.Length);
|
||||
foreach (var spec in specs)
|
||||
{
|
||||
if (!_parameterIndexById.TryGetValue(spec.Item1, out var index)) continue;
|
||||
result.Add(new FaceTrackEntry
|
||||
{
|
||||
Index = index,
|
||||
Id = spec.Item1,
|
||||
FromTargetX = spec.Item2,
|
||||
Scale = spec.Item3
|
||||
});
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
private unsafe void SetupParts()
|
||||
@@ -351,13 +526,20 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
|
||||
private void SetupEffects()
|
||||
{
|
||||
if (EnableEyeBlink && _modelSetting.GetEyeBlinkParameterCount() > 0)
|
||||
// Collect the Groups (EyeBlink/LipSync) parameter ids once — they are
|
||||
// wired into every motion via SetEffectIds (see StartMotion).
|
||||
_eyeBlinkIds.Clear();
|
||||
for (int i = 0; i < _modelSetting.GetEyeBlinkParameterCount(); i++)
|
||||
_eyeBlinkIds.Add(_modelSetting.GetEyeBlinkParameterId(i));
|
||||
|
||||
_lipSyncIds.Clear();
|
||||
for (int i = 0; i < _modelSetting.GetLipSyncParameterCount(); i++)
|
||||
_lipSyncIds.Add(_modelSetting.GetLipSyncParameterId(i));
|
||||
|
||||
if (EnableEyeBlink && _eyeBlinkIds.Count > 0)
|
||||
{
|
||||
_eyeBlink = new CubismEyeBlink();
|
||||
var ids = new System.Collections.Generic.List<string>();
|
||||
for (int i = 0; i < _modelSetting.GetEyeBlinkParameterCount(); i++)
|
||||
ids.Add(_modelSetting.GetEyeBlinkParameterId(i));
|
||||
_eyeBlink.SetParameterIds(ids);
|
||||
_eyeBlink.SetParameterIds(_eyeBlinkIds);
|
||||
}
|
||||
|
||||
var physicsPath = _modelSetting.GetPhysicsFileName();
|
||||
@@ -439,25 +621,18 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
|
||||
unsafe
|
||||
{
|
||||
var values = _nativeModel.GetParameterValues();
|
||||
var idsPtr = _nativeModel.GetParameterIds();
|
||||
|
||||
for (int i = 0; i < _nativeModel.ParameterCount; i++)
|
||||
if (!_parameterIndexById.TryGetValue(id, out var index))
|
||||
{
|
||||
var paramId = CubismNativeModel.ReadStringFromPtrArray(idsPtr, i);
|
||||
if (paramId != id) continue;
|
||||
|
||||
values[i] = value;
|
||||
_manualParameters.Add(id);
|
||||
|
||||
if (i < Parameters.Length)
|
||||
Parameters[i].Value = value;
|
||||
|
||||
GameLogger.Log("LIVE2D", $"SetLiveParameter: parameter '{id}' not found", LogLevel.Warning);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
GameLogger.Log("LIVE2D", $"SetLiveParameter: parameter '{id}' not found", LogLevel.Warning);
|
||||
_nativeModel.GetParameterValues()[index] = value;
|
||||
_manualValues[id] = value;
|
||||
|
||||
if (index < Parameters.Length)
|
||||
Parameters[index].Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -484,6 +659,10 @@ namespace Cthangover.Live2D.Cubism.Framework
|
||||
if (fadeIn >= 0f) motion.FadeInSeconds = fadeIn;
|
||||
if (fadeOut >= 0f) motion.FadeOutSeconds = fadeOut;
|
||||
|
||||
// Wire the motion's model curves (EyeBlink/LipSync) to the parameter
|
||||
// ids from model3.json Groups — same as LAppModel.StartMotion.
|
||||
motion.SetEffectIds(_eyeBlinkIds, _lipSyncIds);
|
||||
|
||||
_motionManager.StartMotion(motion, true, _userTimeSeconds);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user