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; } }