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

366 lines
17 KiB
C#

using System;
using System.Collections.Generic;
namespace Cthangover.Live2D.Cubism.Framework
{
/// <summary>
/// Cubism physics simulation engine. Drives secondary motion (hair, cloth,
/// accessories) using a chain of particles influenced by parameter inputs
/// (X, Y, angle from model parameters), gravity, and wind.
///
/// Architecture:
/// <list type="bullet">
/// <item><see cref="CubismPhysicsRig"/> holds the complete rig structure:
/// sub-rigs, inputs, outputs, and particles</item>
/// <item>Each sub-rig is an independent chain of particles anchored to one
/// or more model parameters via inputs</item>
/// <item>Inputs map model parameters (normalized and weighted) to particle
/// chain root position/angle</item>
/// <item>Outputs map particle chain state back to model parameters after
/// physics integration</item>
/// </list>
///
/// The simulation runs at the rig's configured FPS (from .physics3.json),
/// using sub-frame accumulation when the display delta time is smaller
/// than the physics step. Output values are interpolated between steps
/// for smooth visual results.
/// </summary>
public class CubismPhysics
{
private const float AirResistance = 5f;
private const float MaximumWeight = 100f;
private readonly CubismPhysicsRig _rig = new();
private readonly List<float> _currentOutputs = new();
private readonly List<float> _previousOutputs = new();
private float _currentRemainTime;
private float _physicsDeltaTime;
/// <summary>
/// Parses a .physics3.json byte buffer and builds the full physics rig
/// with sub-rigs, inputs, outputs, and particles.
/// </summary>
public CubismPhysics(byte[] physics3JsonBytes)
{
var json = new CubismPhysicsJson(physics3JsonBytes);
json.GetGravity(out _rig.GravityX, out _rig.GravityY);
json.GetWind(out _rig.WindX, out _rig.WindY);
_rig.Fps = json.GetFps();
_physicsDeltaTime = 1f / _rig.Fps;
int inputOffset = 0, outputOffset = 0, particleOffset = 0;
for (int si = 0; si < json.GetSubRigCount(); si++)
{
var subRig = new CubismPhysicsSubRig
{
InputCount = json.GetInputCount(si),
OutputCount = json.GetOutputCount(si),
ParticleCount = json.GetVertexCount(si),
BaseInputIndex = inputOffset,
BaseOutputIndex = outputOffset,
BaseParticleIndex = particleOffset
};
json.GetNormalizationPosition(si, out subRig.NormalizationPosition);
json.GetNormalizationAngle(si, out subRig.NormalizationAngle);
_rig.SubRigs.Add(subRig);
for (int ii = 0; ii < subRig.InputCount; ii++)
{
json.GetInput(si, ii, out var pid, out var w, out var type, out var reflect);
_rig.Inputs.Add(new CubismPhysicsInput
{
ParameterId = pid, Weight = w, Type = type, Reflect = reflect
});
}
for (int oi = 0; oi < subRig.OutputCount; oi++)
{
json.GetOutput(si, oi, out var pid, out var vi, out var sx, out var sy,
out var w, out var type, out var reflect);
_rig.Outputs.Add(new CubismPhysicsOutput
{
ParameterId = pid, VertexIndex = vi + particleOffset,
TranslationScaleX = sx, TranslationScaleY = sy, Weight = w,
Type = type, Reflect = reflect
});
}
int totalParticles = particleOffset + subRig.ParticleCount;
var newParticles = new CubismPhysicsParticle[totalParticles];
if (_rig.Particles.Length > 0)
System.Array.Copy(_rig.Particles, newParticles, _rig.Particles.Length);
_rig.Particles = newParticles;
for (int pi = 0; pi < subRig.ParticleCount; pi++)
{
var pIdx = particleOffset + pi;
json.GetParticle(si, pi, out var mob, out var del, out var acc, out var rad,
out var px, out var py);
_rig.Particles[pIdx] = new CubismPhysicsParticle
{
InitialX = px, InitialY = py,
Mobility = mob, Delay = del, Acceleration = acc, Radius = rad,
PositionX = px, PositionY = py,
LastPositionX = px, LastPositionY = py
};
}
inputOffset += subRig.InputCount;
outputOffset += subRig.OutputCount;
particleOffset += subRig.ParticleCount;
}
_currentOutputs.Capacity = _rig.Outputs.Count;
_previousOutputs.Capacity = _rig.Outputs.Count;
for (int i = 0; i < _rig.Outputs.Count; i++)
{
_currentOutputs.Add(0f);
_previousOutputs.Add(0f);
}
}
/// <summary>
/// Overrides gravity and wind at runtime.
/// </summary>
public void SetOptions(float gravityX, float gravityY, float windX, float windY)
{
_rig.GravityX = gravityX;
_rig.GravityY = gravityY;
_rig.WindX = windX;
_rig.WindY = windY;
}
/// <summary>
/// Forces the physics to stabilize immediately by running one update
/// cycle in stabilization mode. Particles snap to their rest positions
/// along the chain direction.
/// </summary>
public void Stabilization(CubismNativeModel model)
{
UpdatePhysics(model, true);
}
/// <summary>
/// Evaluates physics for one display frame.
/// Accumulates time and runs physics steps at the rig's native FPS.
/// Output values are linearly interpolated between the previous and
/// current step for smooth visual results.
/// Called from <see cref="CubismModelNode.OnUpdate"/> every frame.
/// </summary>
public unsafe void Evaluate(CubismNativeModel model, float deltaTimeSeconds)
{
_currentRemainTime += deltaTimeSeconds;
var physicsDeltaCalc = _physicsDeltaTime > 0f ? _physicsDeltaTime : deltaTimeSeconds;
var prevValues = model.GetParameterValues();
var paramCache = stackalloc float[_rig.Inputs.Count];
for (int i = 0; i < _rig.Inputs.Count; i++)
paramCache[i] = 0f;
while (_currentRemainTime >= physicsDeltaCalc)
{
_currentRemainTime -= physicsDeltaCalc;
for (int i = 0; i < _rig.Outputs.Count; i++)
{
_previousOutputs[i] = _currentOutputs[i];
_currentOutputs[i] = 0f;
}
UpdatePhysics(model, false);
for (int i = 0; i < _rig.Outputs.Count; i++)
_currentOutputs[i] = EvaluateOutputValue(i);
}
var alpha = physicsDeltaCalc > 0f ? _currentRemainTime / physicsDeltaCalc : 0f;
var idsPtr = model.GetParameterIds();
var values = model.GetParameterValues();
var maxValues = model.GetParameterMaximumValues();
var minValues = model.GetParameterMinimumValues();
for (int oi = 0; oi < _rig.Outputs.Count; oi++)
{
var output = _rig.Outputs[oi];
var interpolated = _previousOutputs[oi] * (1f - alpha) + _currentOutputs[oi] * alpha;
for (int pi = 0; pi < model.ParameterCount; pi++)
{
if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, pi) == output.ParameterId)
{
var result = interpolated * MaximumWeight;
if (result < minValues[pi]) result = minValues[pi];
if (result > maxValues[pi]) result = maxValues[pi];
values[pi] = values[pi] * (1f - output.Weight) + result * output.Weight;
break;
}
}
}
}
private unsafe void UpdatePhysics(CubismNativeModel model, bool stabilization)
{
var idsPtr = model.GetParameterIds();
var values = model.GetParameterValues();
var minValues = model.GetParameterMinimumValues();
var maxValues = model.GetParameterMaximumValues();
var defaultValues = model.GetParameterDefaultValues();
foreach (var subRig in _rig.SubRigs)
{
float totalTranslationX = 0f, totalTranslationY = 0f, totalAngle = 0f;
for (int ii = 0; ii < subRig.InputCount; ii++)
{
var input = _rig.Inputs[subRig.BaseInputIndex + ii];
var paramValue = GetParameterValue(idsPtr, values, model.ParameterCount, input.ParameterId);
float tx = 0f, ty = 0f, angle = 0f;
switch (input.Type)
{
case CubismPhysicsSource.X:
tx = NormalizeParameterValue(paramValue, subRig.NormalizationPosition, input.Reflect) * input.Weight;
break;
case CubismPhysicsSource.Y:
ty = NormalizeParameterValue(paramValue, subRig.NormalizationPosition, input.Reflect) * input.Weight;
break;
case CubismPhysicsSource.Angle:
angle = NormalizeParameterValue(paramValue, subRig.NormalizationAngle, input.Reflect) * input.Weight;
break;
}
totalTranslationX += tx;
totalTranslationY += ty;
totalAngle += angle;
}
var particles = _rig.Particles;
int baseP = subRig.BaseParticleIndex;
float prevRootX = particles[baseP].PositionX;
float prevRootY = particles[baseP].PositionY;
float smoothRate = MathF.Min(1f, _physicsDeltaTime * 12f);
particles[baseP].PositionX = prevRootX + (totalTranslationX - prevRootX) * smoothRate;
particles[baseP].PositionY = prevRootY + (totalTranslationY - prevRootY) * smoothRate;
for (int pi = 1; pi < subRig.ParticleCount; pi++)
{
var pIdx = baseP + pi;
var prev = particles[pIdx - 1];
if (stabilization)
{
var dirX = particles[pIdx].InitialX - prev.InitialX;
var dirY = particles[pIdx].InitialY - prev.InitialY;
var dist = MathF.Sqrt(dirX * dirX + dirY * dirY);
if (dist > 0f)
{
dirX /= dist;
dirY /= dist;
}
particles[pIdx].PositionX = particles[pIdx - 1].PositionX + dirX * particles[pIdx].Radius;
particles[pIdx].PositionY = particles[pIdx - 1].PositionY + dirY * particles[pIdx].Radius;
}
else
{
var forceX = _rig.GravityX * particles[pIdx].Acceleration + _rig.WindX;
var forceY = _rig.GravityY * particles[pIdx].Acceleration + _rig.WindY;
var delay = particles[pIdx].Delay * _physicsDeltaTime * 30f;
if (delay > 0f)
{
var delay2 = delay * delay;
var newX = particles[pIdx].PositionX + particles[pIdx].VelocityX * delay + 0.5f * forceX * delay2;
var newY = particles[pIdx].PositionY + particles[pIdx].VelocityY * delay + 0.5f * forceY * delay2;
var radian = CubismMath.DirectionToRadian(
particles[pIdx].LastGravityX, particles[pIdx].LastGravityY, forceX, forceY);
radian /= AirResistance;
var dirX = newX - particles[pIdx - 1].PositionX;
var dirY = newY - particles[pIdx - 1].PositionY;
var currentDist = MathF.Sqrt(dirX * dirX + dirY * dirY);
if (currentDist > 0f)
{
var ndirX = dirX / currentDist;
var ndirY = dirY / currentDist;
var cosR = MathF.Cos(radian);
var sinR = MathF.Sin(radian);
dirX = ndirX * cosR - ndirY * sinR;
dirY = ndirX * sinR + ndirY * cosR;
}
particles[pIdx].VelocityX = (newX - particles[pIdx].LastPositionX) / delay * particles[pIdx].Mobility;
particles[pIdx].VelocityY = (newY - particles[pIdx].LastPositionY) / delay * particles[pIdx].Mobility;
}
particles[pIdx].LastGravityX = forceX;
particles[pIdx].LastGravityY = forceY;
particles[pIdx].ForceX = forceX;
particles[pIdx].ForceY = forceY;
var curDirX = particles[pIdx].PositionX - particles[pIdx - 1].PositionX;
var curDirY = particles[pIdx].PositionY - particles[pIdx - 1].PositionY;
var curDist = MathF.Sqrt(curDirX * curDirX + curDirY * curDirY);
if (curDist > 0f)
{
curDirX = curDirX / curDist * particles[pIdx].Radius;
curDirY = curDirY / curDist * particles[pIdx].Radius;
}
particles[pIdx].PositionX = particles[pIdx - 1].PositionX + curDirX;
particles[pIdx].PositionY = particles[pIdx - 1].PositionY + curDirY;
particles[pIdx].LastPositionX = particles[pIdx].PositionX;
particles[pIdx].LastPositionY = particles[pIdx].PositionY;
}
}
for (int oi = 0; oi < subRig.OutputCount; oi++)
{
var outIdx = subRig.BaseOutputIndex + oi;
var output = _rig.Outputs[outIdx];
var particle = particles[output.VertexIndex];
var value = EvaluateOutputValueStatic(particle, particles, output, baseP, _currentOutputs[outIdx]);
_currentOutputs[outIdx] = value * output.Weight;
}
}
}
private float EvaluateOutputValue(int outputIndex)
{
return _currentOutputs[outputIndex];
}
private static float EvaluateOutputValueStatic(CubismPhysicsParticle particle,
CubismPhysicsParticle[] particles, CubismPhysicsOutput output, int baseParticleIndex, float currentValue)
{
var pIdx = output.VertexIndex;
var p = particles[pIdx];
var prev = pIdx > baseParticleIndex ? particles[pIdx - 1] : particles[pIdx];
return output.Type switch
{
CubismPhysicsSource.X => p.PositionX - prev.PositionX,
CubismPhysicsSource.Y => p.PositionY - prev.PositionY,
CubismPhysicsSource.Angle =>
MathF.Atan2(p.PositionY - prev.PositionY, p.PositionX - prev.PositionX) * 180f / MathF.PI,
_ => p.PositionX
};
}
private static unsafe float GetParameterValue(IntPtr idsPtr, float* values, int count, string id)
{
for (int i = 0; i < count; i++)
if (CubismNativeModel.ReadStringFromPtrArray(idsPtr, i) == id)
return values[i];
return 0f;
}
private static float NormalizeParameterValue(float value, CubismPhysicsNormalization norm, bool reflect)
{
var range = norm.Maximum - norm.Minimum;
if (MathF.Abs(range) < 1e-6f) return 0f;
var result = (value - norm.Default) / range;
return reflect ? -result : result;
}
}
}