using System.Collections.Generic;
using System.Text.Json;
namespace Cthangover.Live2D.Cubism.Framework
{
///
/// Physics input/output dimensionality.
///
public enum CubismPhysicsSource { X, Y, Angle }
///
/// Normalization range for mapping raw parameter values to normalized physics inputs.
///
public struct CubismPhysicsNormalization { public float Minimum; public float Maximum; public float Default; }
///
/// A single particle in a physics chain. Holds position, velocity, force accumulators,
/// and per-particle properties (mobility, delay, acceleration, radius) from the
/// .physics3.json file. Mirrors SDK CubismPhysicsParticle.
///
public struct CubismPhysicsParticle
{
public float InitialX, InitialY;
public float Mobility;
public float Delay;
public float Acceleration;
public float Radius;
public float PositionX, PositionY;
public float LastPositionX, LastPositionY;
public float LastGravityX, LastGravityY;
public float ForceX, ForceY;
public float VelocityX, VelocityY;
}
///
/// A sub-rig groups a contiguous range of inputs, outputs, and particles
/// that form one independent physics chain. Each sub-rig has its own
/// normalization settings for position and angle inputs.
///
public struct CubismPhysicsSubRig
{
public int InputCount, OutputCount, ParticleCount;
public int BaseInputIndex, BaseOutputIndex, BaseParticleIndex;
public CubismPhysicsNormalization NormalizationPosition;
public CubismPhysicsNormalization NormalizationAngle;
}
///
/// Maps a model parameter to a physics chain input (X, Y, or angle)
/// with a configurable weight and optional reflection.
///
public struct CubismPhysicsInput
{
public string ParameterId;
public int SourceParameterIndex;
public float Weight;
public CubismPhysicsSource Type;
public bool Reflect;
}
///
/// Maps a physics chain output back to a model parameter.
/// is the pendulum vertex index relative to the
/// owning sub-rig's base particle (SDK semantics). The translation scale is
/// deliberately never loaded: native CubismPhysicsJson reads only the
/// single "Scale" value into , so X/Y-typed outputs
/// scale by zero exactly like the native implementation.
///
public struct CubismPhysicsOutput
{
public string ParameterId;
public int DestinationParameterIndex;
public int VertexIndex;
public float TranslationScaleX, TranslationScaleY;
public float AngleScale;
public float Weight;
public CubismPhysicsSource Type;
public bool Reflect;
public float ValueBelowMinimum;
public float ValueExceededMaximum;
}
///
/// Complete physics rig data structure. Holds all sub-rigs, flattened
/// input/output/particle arrays, and global gravity/wind/fps settings.
/// Used by as its internal state.
///
public class CubismPhysicsRig
{
public List SubRigs = new();
public List Inputs = new();
public List Outputs = new();
public CubismPhysicsParticle[] Particles = System.Array.Empty();
public float GravityX, GravityY;
public float WindX, WindY;
public float Fps;
}
///
/// JSON parser for the Cubism .physics3.json format. Field-by-field port of
/// Cubism SDK for Native 5-r.5 Physics/CubismPhysicsJson.cpp: every getter
/// reads the same key at the same JSON path. Missing numeric values yield
/// 0 / false like the native error-value accessors; in particular a
/// missing Meta.Fps yields 0 (the simulation then steps per display frame).
/// Note: native does not read InitialPosition/InitialVelocity for vertices —
/// those keys are ignored here as well; particles are laid out by
/// CubismPhysics.Initialize().
///
public class CubismPhysicsJson
{
private readonly JsonDocument _json;
/// Parses the given .physics3.json bytes.
public CubismPhysicsJson(byte[] jsonBytes)
{
_json = JsonDocument.Parse(jsonBytes);
}
// ------------------------------------------------------------------
// Meta
// ------------------------------------------------------------------
/// Gravity vector from Meta.EffectiveForces.Gravity (missing values read as 0).
public void GetGravity(out float x, out float y)
{
x = 0f;
y = 0f;
if (_json.RootElement.TryGetProperty("Meta", out var meta) &&
meta.TryGetProperty("EffectiveForces", out var effectiveForces) &&
effectiveForces.TryGetProperty("Gravity", out var gravity))
{
x = GetFloat(gravity, "X");
y = GetFloat(gravity, "Y");
}
}
/// Wind vector from Meta.EffectiveForces.Wind (missing values read as 0).
public void GetWind(out float x, out float y)
{
x = 0f;
y = 0f;
if (_json.RootElement.TryGetProperty("Meta", out var meta) &&
meta.TryGetProperty("EffectiveForces", out var effectiveForces) &&
effectiveForces.TryGetProperty("Wind", out var wind))
{
x = GetFloat(wind, "X");
y = GetFloat(wind, "Y");
}
}
/// Simulation FPS from Meta.Fps; 0 when absent (native behavior).
public float GetFps()
{
return _json.RootElement.TryGetProperty("Meta", out var meta) ? GetFloat(meta, "Fps") : 0f;
}
/// Sub-rig count from Meta.PhysicsSettingCount.
public int GetSubRigCount()
{
if (_json.RootElement.TryGetProperty("Meta", out var meta) &&
meta.TryGetProperty("PhysicsSettingCount", out var count))
{
return count.GetInt32();
}
// Defensive fallback: use the actual settings array length
// (native would return 0 when the key is absent).
return _json.RootElement.TryGetProperty("PhysicsSettings", out var settings)
? settings.GetArrayLength()
: 0;
}
/// Total input count from Meta.TotalInputCount.
public int GetTotalInputCount() => GetMetaInt("TotalInputCount");
/// Total output count from Meta.TotalOutputCount.
public int GetTotalOutputCount() => GetMetaInt("TotalOutputCount");
/// Total vertex/particle count from Meta.VertexCount.
public int GetVertexCount() => GetMetaInt("VertexCount");
// ------------------------------------------------------------------
// Normalization
// ------------------------------------------------------------------
/// Position normalization (Minimum/Maximum/Default) for a sub-rig; missing values read as 0.
public void GetNormalizationPosition(int subRigIndex, out CubismPhysicsNormalization norm)
{
norm = default;
if (GetSubRig(subRigIndex).TryGetProperty("Normalization", out var n) &&
n.TryGetProperty("Position", out var pos))
{
norm.Minimum = GetFloat(pos, "Minimum");
norm.Maximum = GetFloat(pos, "Maximum");
norm.Default = GetFloat(pos, "Default");
}
}
/// Angle normalization (Minimum/Maximum/Default) for a sub-rig; missing values read as 0.
public void GetNormalizationAngle(int subRigIndex, out CubismPhysicsNormalization norm)
{
norm = default;
if (GetSubRig(subRigIndex).TryGetProperty("Normalization", out var n) &&
n.TryGetProperty("Angle", out var angle))
{
norm.Minimum = GetFloat(angle, "Minimum");
norm.Maximum = GetFloat(angle, "Maximum");
norm.Default = GetFloat(angle, "Default");
}
}
// ------------------------------------------------------------------
// Input
// ------------------------------------------------------------------
/// Number of input entries in the given sub-rig.
public int GetInputCount(int subRigIndex) => GetEntriesLength(subRigIndex, "Input");
/// Input weight (Input[i].Weight).
public float GetInputWeight(int subRigIndex, int inputIndex) =>
GetFloat(GetInputEntry(subRigIndex, inputIndex), "Weight");
/// Input reflect flag (Input[i].Reflect).
public bool GetInputReflect(int subRigIndex, int inputIndex) =>
GetBool(GetInputEntry(subRigIndex, inputIndex), "Reflect");
/// Input type tag (Input[i].Type): "X", "Y" or "Angle".
public string GetInputType(int subRigIndex, int inputIndex) =>
GetString(GetInputEntry(subRigIndex, inputIndex), "Type");
/// Source parameter id (Input[i].Source.Id).
public string GetInputSourceId(int subRigIndex, int inputIndex)
{
var entry = GetInputEntry(subRigIndex, inputIndex);
return entry.TryGetProperty("Source", out var source) ? GetString(source, "Id") : null;
}
// ------------------------------------------------------------------
// Output
// ------------------------------------------------------------------
/// Number of output entries in the given sub-rig.
public int GetOutputCount(int subRigIndex) => GetEntriesLength(subRigIndex, "Output");
/// Pendulum vertex index (Output[i].VertexIndex), relative to the sub-rig base particle.
public int GetOutputVertexIndex(int subRigIndex, int outputIndex) =>
GetInt(GetOutputEntry(subRigIndex, outputIndex), "VertexIndex");
/// Angle scale (Output[i].Scale). Native maps the single "Scale" value to the angle scale only.
public float GetOutputAngleScale(int subRigIndex, int outputIndex) =>
GetFloat(GetOutputEntry(subRigIndex, outputIndex), "Scale");
/// Output weight (Output[i].Weight).
public float GetOutputWeight(int subRigIndex, int outputIndex) =>
GetFloat(GetOutputEntry(subRigIndex, outputIndex), "Weight");
/// Destination parameter id (Output[i].Destination.Id).
public string GetOutputsDestinationId(int subRigIndex, int outputIndex)
{
var entry = GetOutputEntry(subRigIndex, outputIndex);
return entry.TryGetProperty("Destination", out var destination) ? GetString(destination, "Id") : null;
}
/// Output type tag (Output[i].Type): "X", "Y" or "Angle".
public string GetOutputType(int subRigIndex, int outputIndex) =>
GetString(GetOutputEntry(subRigIndex, outputIndex), "Type");
/// Output reflect flag (Output[i].Reflect).
public bool GetOutputReflect(int subRigIndex, int outputIndex) =>
GetBool(GetOutputEntry(subRigIndex, outputIndex), "Reflect");
// ------------------------------------------------------------------
// Particle (Vertices)
// ------------------------------------------------------------------
/// Number of vertex/particle entries in the given sub-rig.
public int GetParticleCount(int subRigIndex) => GetEntriesLength(subRigIndex, "Vertices");
/// Particle mobility (Vertices[i].Mobility).
public float GetParticleMobility(int subRigIndex, int vertexIndex) =>
GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Mobility");
/// Particle delay (Vertices[i].Delay).
public float GetParticleDelay(int subRigIndex, int vertexIndex) =>
GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Delay");
/// Particle acceleration (Vertices[i].Acceleration).
public float GetParticleAcceleration(int subRigIndex, int vertexIndex) =>
GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Acceleration");
/// Particle radius (Vertices[i].Radius).
public float GetParticleRadius(int subRigIndex, int vertexIndex) =>
GetFloat(GetVertexEntry(subRigIndex, vertexIndex), "Radius");
/// Particle position (Vertices[i].Position.X/Y).
public void GetParticlePosition(int subRigIndex, int vertexIndex, out float x, out float y)
{
var entry = GetVertexEntry(subRigIndex, vertexIndex);
x = 0f;
y = 0f;
if (entry.TryGetProperty("Position", out var position))
{
x = GetFloat(position, "X");
y = GetFloat(position, "Y");
}
}
// ------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------
private JsonElement GetSubRig(int index) =>
_json.RootElement.GetProperty("PhysicsSettings")[index];
private JsonElement GetInputEntry(int subRigIndex, int inputIndex) =>
GetSubRig(subRigIndex).GetProperty("Input")[inputIndex];
private JsonElement GetOutputEntry(int subRigIndex, int outputIndex) =>
GetSubRig(subRigIndex).GetProperty("Output")[outputIndex];
private JsonElement GetVertexEntry(int subRigIndex, int vertexIndex) =>
GetSubRig(subRigIndex).GetProperty("Vertices")[vertexIndex];
private int GetEntriesLength(int subRigIndex, string name)
{
var subRig = GetSubRig(subRigIndex);
return subRig.TryGetProperty(name, out var entries) ? entries.GetArrayLength() : 0;
}
private int GetMetaInt(string name) =>
_json.RootElement.TryGetProperty("Meta", out var meta) ? GetInt(meta, name) : 0;
private static float GetFloat(JsonElement element, string name) =>
element.TryGetProperty(name, out var value) ? value.GetSingle() : 0f;
private static int GetInt(JsonElement element, string name) =>
element.TryGetProperty(name, out var value) ? value.GetInt32() : 0;
private static bool GetBool(JsonElement element, string name) =>
element.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.True;
private static string GetString(JsonElement element, string name) =>
element.TryGetProperty(name, out var value) && value.ValueKind == JsonValueKind.String
? value.GetString()
: string.Empty;
}
}