Body step 1: one-time T-pose calibration (prefs.json, forced gate) + True Body solver + shared ArmSolver
- BodyProfile (Winter anthropometry, T-pose: arm = (span - shoulder width)/2 - 1.5 cm) and Prefs (gitignored prefs.json next to the executable / project folder in editor builds) - Calibration node: loads prefs, else head-locked prompt + jitter-proof TPoseDetector; body systems wait for BodyProfile.Current; Recalibrate() in group "calibration"; recal rebuilds arms - ArmSolver: PhysicsArms' elbow solve extracted (behaviour-identical) + min reach |U-F| fix - TrueBody: derived real-body pose (neck, chest yaw + crouch lean at neck pivot, lagging pelvis, moving shoulder girdle with scapulohumeral shrug, arms) + debug skeleton - PhysicsArms/PhysicsTorso/BodyRig now profile-driven (arm length no longer from head height) - TrueBodySpike PASS x3; all spikes PASS x3 (arm spike thresholds re-based for correct 0.57 m arms; CollapseSpike uses a fixed standing profile) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3a990c6dbe
commit
81a9b1fca2
15 files changed
+994
-106
No files matched your search
@@ -0,0 +1,132 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using Godot;
|
||||
|
||||
// The player's measured body (True Body metrics), from the one-time T-pose calibration.
|
||||
// All heights are above the tracking floor; lengths in metres. Ratios: Winter, Biomechanics and
|
||||
// Motor Control of Human Movement, Fig 4.1 (Drillis & Contini) — see research/nimso-project-tx.md §3.
|
||||
public sealed class BodyProfile
|
||||
{
|
||||
public const float EyeToStature = 0.936f;
|
||||
public const float ShoulderWidthRatio = 0.259f; // biacromial, joint to joint
|
||||
public const float ShoulderHeightRatio = 0.818f;
|
||||
public const float UpperArmRatio = 0.186f;
|
||||
public const float ForearmRatio = 0.146f;
|
||||
public const float ArmSafetyMargin = 0.015f; // store arms slightly short: the real elbow must never lock first
|
||||
|
||||
public int Version { get; set; } = 1;
|
||||
public float EyeHeight { get; set; }
|
||||
public float Stature { get; set; }
|
||||
public float ShoulderHalfWidth { get; set; }
|
||||
public float ShoulderDrop { get; set; } // eye height − shoulder-joint height
|
||||
public float UpperArm { get; set; }
|
||||
public float Forearm { get; set; }
|
||||
public float MeasuredSpan { get; set; } // wrist-to-wrist in the T-pose (0 = estimated, not measured)
|
||||
public string Date { get; set; } = "";
|
||||
|
||||
public float ArmLength => UpperArm + Forearm;
|
||||
|
||||
// Current profile for the session; null until calibrated (main scene) or estimated (headless spikes).
|
||||
public static BodyProfile Current { get; set; }
|
||||
|
||||
public static event Action<BodyProfile> Changed;
|
||||
|
||||
public static void Set(BodyProfile profile)
|
||||
{
|
||||
Current = profile;
|
||||
Changed?.Invoke(profile);
|
||||
}
|
||||
|
||||
// Anthropometric estimate from eye height alone — used by headless spikes and as a fallback.
|
||||
public static BodyProfile Estimate(float eyeHeight)
|
||||
{
|
||||
float stature = eyeHeight / EyeToStature;
|
||||
float arm = (UpperArmRatio + ForearmRatio) * stature - ArmSafetyMargin;
|
||||
return FromMeasurements(eyeHeight, stature, ShoulderWidthRatio * stature * 0.5f,
|
||||
eyeHeight - ShoulderHeightRatio * stature, arm, 0f);
|
||||
}
|
||||
|
||||
// From a T-pose: wrist span = shoulder width + 2 arms. Shoulder width comes from the stature
|
||||
// ratio — a few cm of error there only moves where the arm starts, not the total reach.
|
||||
public static BodyProfile FromTPose(float eyeHeight, float wristSpan, float wristHeight)
|
||||
{
|
||||
float stature = eyeHeight / EyeToStature;
|
||||
float halfWidth = ShoulderWidthRatio * stature * 0.5f;
|
||||
float arm = (wristSpan - 2f * halfWidth) * 0.5f - ArmSafetyMargin;
|
||||
return FromMeasurements(eyeHeight, stature, halfWidth, eyeHeight - wristHeight, arm, wristSpan);
|
||||
}
|
||||
|
||||
private static BodyProfile FromMeasurements(float eye, float stature, float halfWidth, float drop, float arm, float span)
|
||||
{
|
||||
float split = UpperArmRatio / (UpperArmRatio + ForearmRatio);
|
||||
return new BodyProfile
|
||||
{
|
||||
EyeHeight = eye,
|
||||
Stature = stature,
|
||||
ShoulderHalfWidth = halfWidth,
|
||||
ShoulderDrop = drop,
|
||||
UpperArm = arm * split,
|
||||
Forearm = arm * (1f - split),
|
||||
MeasuredSpan = span,
|
||||
Date = DateTime.Now.ToString("yyyy-MM-dd HH:mm"),
|
||||
};
|
||||
}
|
||||
|
||||
public bool IsPlausible() =>
|
||||
EyeHeight is > 1.0f and < 2.3f && ArmLength is > 0.35f and < 0.9f
|
||||
&& ShoulderHalfWidth is > 0.12f and < 0.32f && ShoulderDrop is > 0.05f and < 0.5f;
|
||||
|
||||
public override string ToString() =>
|
||||
$"eye {EyeHeight:F3} m, stature {Stature:F2} m, arm {ArmLength:F3} m (upper {UpperArm:F3} / fore {Forearm:F3}), " +
|
||||
$"shoulder half-width {ShoulderHalfWidth:F3} m, shoulder drop {ShoulderDrop:F3} m";
|
||||
}
|
||||
|
||||
// prefs.json next to the executable (exported builds) or in the project folder (editor builds,
|
||||
// incl. headless runs) — portable: copy the file to carry your calibration. Gitignored.
|
||||
public static class Prefs
|
||||
{
|
||||
private sealed class Document
|
||||
{
|
||||
public BodyProfile Body { get; set; }
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions Options = new() { WriteIndented = true };
|
||||
|
||||
public static string FilePath =>
|
||||
OS.HasFeature("editor")
|
||||
? ProjectSettings.GlobalizePath("res://prefs.json")
|
||||
: Path.Combine(Path.GetDirectoryName(OS.GetExecutablePath()) ?? ".", "prefs.json");
|
||||
|
||||
public static BodyProfile LoadBody(string path = null)
|
||||
{
|
||||
path ??= FilePath;
|
||||
try
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
return null;
|
||||
Document doc = JsonSerializer.Deserialize<Document>(File.ReadAllText(path), Options);
|
||||
return doc?.Body != null && doc.Body.IsPlausible() ? doc.Body : null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GD.PushWarning($"Prefs: could not read {path}: {e.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool SaveBody(BodyProfile body, string path = null)
|
||||
{
|
||||
path ??= FilePath;
|
||||
try
|
||||
{
|
||||
File.WriteAllText(path, JsonSerializer.Serialize(new Document { Body = body }, Options));
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GD.PushWarning($"Prefs: could not write {path}: {e.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user