using System; using System.Collections.Generic; namespace Cthangover.Live2D.Cubism.Framework { /// /// 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: /// /// holds the complete rig structure: /// sub-rigs, inputs, outputs, and particles /// Each sub-rig is an independent chain of particles anchored to one /// or more model parameters via inputs /// Inputs map model parameters (normalized and weighted) to particle /// chain root position/angle /// Outputs map particle chain state back to model parameters after /// physics integration /// /// /// 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. /// public class CubismPhysics { private const float AirResistance = 5f; private const float MaximumWeight = 100f; private readonly CubismPhysicsRig _rig = new(); private readonly List _currentOutputs = new(); private readonly List _previousOutputs = new(); private float _currentRemainTime; private float _physicsDeltaTime; /// /// Parses a .physics3.json byte buffer and builds the full physics rig /// with sub-rigs, inputs, outputs, and particles. /// 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); } } /// /// Overrides gravity and wind at runtime. /// public void SetOptions(float gravityX, float gravityY, float windX, float windY) { _rig.GravityX = gravityX; _rig.GravityY = gravityY; _rig.WindX = windX; _rig.WindY = windY; } /// /// Forces the physics to stabilize immediately by running one update /// cycle in stabilization mode. Particles snap to their rest positions /// along the chain direction. /// public void Stabilization(CubismNativeModel model) { UpdatePhysics(model, true); } /// /// 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 every frame. /// 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; } } }