Files
VR-Mess/scripts/ProceduralLegs.cs
FoxyzandClaude Opus 5.5 2aff2b61da Body step 2.1: climbing, mantling, crawling, legs, playground, 144 Hz
- Step rule + lower-body collider: no walking onto tables, steps <= 0.35 m
- Structural hand strength: two-hand mantle/pull-up works, one hand can't
- Arms aim from the physical shoulder horizontally, keep True Body pose vertically
- Crawl pose, crawl carry, 35 deg slope limit, ground feed-forward ignores
  play-space corrections (fixes a runaway feedback loop)
- Grab static world (climbing), procedural visual legs
- Playground sandbox; menu strength slider, spectator (old drag) and turn toggles
- Physics 144 Hz with interpolation; legacy chase-body spikes pin 90 Hz
- RagdollSpike: 27 phases incl. mantle, pull-up, crawl, ledge drop, ramps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 07:56:08 +03:00

228 lines
9.9 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Godot;
// Visual procedural legs for the ragdoll player (Nimso: "a very basic procedural leg system", feet
// "don't actually hold the player up" — ground control does). Feet stay planted on the ground and
// step, alternating, when the hips move or turn away from them; the stride leads with the body's
// velocity. Airborne the feet hang under the hips; low (crawling) they trail behind. Knees bend
// forward via a 2-bone solve. Physics-tick state (PlayerRagdoll.Step calls Tick), drawn in
// _Process from the interpolated pelvis. No collision — the lower-body collider is separate.
public partial class ProceduralLegs : Node3D
{
public const float ThighRatio = 0.245f, ShinRatio = 0.246f, AnkleRatio = 0.039f; // Winter, × stature
public const float HipHalfWidth = 0.09f;
public const float StanceHalfWidth = 0.11f;
public const float StepTrigger = 0.2f; // m from the ideal spot
public const float StepTurnTrigger = 0.6f; // rad
public const float StepLead = 0.25f; // s of velocity the new foothold leads by
public const float StepTimeSlow = 0.32f, StepTimeFast = 0.2f;
public const float StepHeight = 0.09f;
public const float SettleTime = 0.4f; // standing still this long → the trailing foot squares up
private struct Foot
{
public Vector3 Planted, From, To;
public float Yaw, FromYaw, ToYaw;
public float T; // step progress 0..1 (1 = planted)
public Vector3 Current;
public float CurrentYaw;
}
private readonly Foot[] _feet = new Foot[2];
private readonly MeshInstance3D[] _thigh = new MeshInstance3D[2], _shin = new MeshInstance3D[2], _foot = new MeshInstance3D[2];
private RigidBody3D _pelvis;
private Vector3 _hipLocal; // pelvis-body-local hip centre
private float _thighLen, _shinLen, _ankle, _scale;
private bool _initialized;
private int _lastStepped;
private float _stillTime;
public void Setup(RigidBody3D pelvis, Vector3 hipLocal, float stature, Color pants, Color shoes)
{
_pelvis = pelvis;
_hipLocal = hipLocal;
_thighLen = ThighRatio * stature;
_shinLen = ShinRatio * stature;
_ankle = AnkleRatio * stature;
_scale = stature / 1.75f;
PhysicsInterpolationMode = PhysicsInterpolationModeEnum.Off; // meshes are placed every frame
TopLevel = true;
for (int i = 0; i < 2; i++)
{
_thigh[i] = MakeMesh(new Vector3(0.13f * _scale, 1f, 0.14f * _scale), pants);
_shin[i] = MakeMesh(new Vector3(0.1f * _scale, 1f, 0.11f * _scale), pants);
_foot[i] = MakeMesh(new Vector3(0.1f * _scale, 0.07f * _scale, 0.25f * _scale), shoes);
}
_initialized = false;
}
private MeshInstance3D MakeMesh(Vector3 size, Color color)
{
MeshInstance3D m = new() { Mesh = new BoxMesh { Size = size } };
m.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.7f });
AddChild(m);
return m;
}
private Vector3 Hip(Transform3D pelvis, int side) =>
pelvis * (_hipLocal + new Vector3(side == 0 ? -HipHalfWidth * _scale : HipHalfWidth * _scale, 0f, 0f));
private static float YawOf(Basis b)
{
Vector3 f = -b.Z;
return Mathf.Atan2(-f.X, -f.Z);
}
// groundY = where the feet stand (ground control's foot level); crawl = legs collider off.
public void Tick(float groundY, bool grounded, bool crawl, Basis yawBasis, float dt)
{
if (_pelvis == null)
return;
Transform3D pelvis = _pelvis.GlobalTransform;
Vector3 velocity = _pelvis.LinearVelocity;
velocity.Y = 0f;
float yaw = YawOf(yawBasis);
Vector3 back = yawBasis.Z;
for (int i = 0; i < 2; i++)
{
Vector3 hip = Hip(pelvis, i);
Vector3 side = yawBasis.X * (i == 0 ? -1f : 1f);
Vector3 ideal = new Vector3(hip.X, groundY, hip.Z) + side * (StanceHalfWidth - HipHalfWidth) * _scale;
if (crawl)
ideal += back * (_thighLen + _shinLen) * 0.8f;
if (!_initialized)
{
_feet[i] = new Foot { Planted = ideal, Current = ideal, To = ideal, Yaw = yaw, CurrentYaw = yaw, ToYaw = yaw, T = 1f };
continue;
}
ref Foot f = ref _feet[i];
if (!grounded)
{
// Hanging: feet dangle under the hips, a little forward, and replant on landing.
Vector3 hang = hip + Vector3.Down * (_thighLen + _shinLen) * 0.92f - back * 0.08f;
f.Current = f.Current.Lerp(hang, 1f - Mathf.Exp(-dt * 10f));
f.CurrentYaw = Mathf.LerpAngle(f.CurrentYaw, yaw, 1f - Mathf.Exp(-dt * 10f));
f.Planted = f.Current;
f.T = 1f;
continue;
}
if (f.T < 1f)
{
float speed = velocity.Length();
float duration = Mathf.Lerp(StepTimeSlow, StepTimeFast, Mathf.Clamp(speed / 2.5f, 0f, 1f));
f.T = Mathf.Min(1f, f.T + dt / duration);
f.To = new Vector3(f.To.X, groundY, f.To.Z);
float k = Mathf.SmoothStep(0f, 1f, f.T);
f.Current = f.From.Lerp(f.To, k) + Vector3.Up * (Mathf.Sin(Mathf.Pi * f.T) * (StepHeight + 0.03f * speed));
f.CurrentYaw = Mathf.LerpAngle(f.FromYaw, f.ToYaw, k);
if (f.T >= 1f)
{
f.Planted = f.To;
f.Yaw = f.ToYaw;
}
}
else
{
f.Planted = new Vector3(f.Planted.X, Mathf.MoveToward(f.Planted.Y, groundY, dt * 2f), f.Planted.Z);
f.Current = f.Planted;
f.CurrentYaw = f.Yaw;
}
}
if (!_initialized)
{
_initialized = true;
return;
}
if (!grounded)
return;
// One foot at a time: step the foot that is farthest from where it should be.
if (_feet[0].T >= 1f && _feet[1].T >= 1f)
{
float speed = velocity.Length();
_stillTime = speed < 0.05f ? _stillTime + dt : 0f;
int pick = -1;
float worst = 0f;
for (int i = 0; i < 2; i++)
{
Vector3 hip = Hip(pelvis, i);
Vector3 side = yawBasis.X * (i == 0 ? -1f : 1f);
Vector3 ideal = new Vector3(hip.X, groundY, hip.Z) + side * (StanceHalfWidth - HipHalfWidth) * _scale;
if (crawl)
ideal += back * (_thighLen + _shinLen) * 0.8f;
float off = new Vector2(_feet[i].Planted.X - ideal.X, _feet[i].Planted.Z - ideal.Z).Length();
float turn = Mathf.Abs(Mathf.AngleDifference(_feet[i].Yaw, yaw));
bool need = off > StepTrigger * _scale || turn > StepTurnTrigger || (_stillTime > SettleTime && off > 0.06f);
float score = off + turn * 0.2f + (i == _lastStepped ? -0.01f : 0f);
if (need && score > worst)
{
worst = score;
pick = i;
}
}
if (pick >= 0)
{
Vector3 hip = Hip(pelvis, pick);
Vector3 side = yawBasis.X * (pick == 0 ? -1f : 1f);
Vector3 to = new Vector3(hip.X, groundY, hip.Z) + side * (StanceHalfWidth - HipHalfWidth) * _scale + velocity * StepLead;
if (crawl)
to += back * (_thighLen + _shinLen) * 0.8f;
ref Foot f = ref _feet[pick];
f.From = f.Planted;
f.FromYaw = f.Yaw;
f.To = to;
f.ToYaw = yaw;
f.T = 0f;
_lastStepped = pick;
_stillTime = 0f;
}
}
}
public override void _Process(double delta)
{
if (_pelvis == null || !_initialized || !IsInstanceValid(_pelvis))
return;
Transform3D pelvis = _pelvis.GetGlobalTransformInterpolated();
Vector3 forward = -pelvis.Basis.Z;
forward.Y = 0f;
forward = forward.LengthSquared() > 1e-6f ? forward.Normalized() : Vector3.Forward;
for (int i = 0; i < 2; i++)
{
Vector3 hip = Hip(pelvis, i);
Vector3 ankle = _feet[i].Current + Vector3.Up * _ankle;
// 2-bone solve, knee bending forward.
Vector3 d = ankle - hip;
float reach = _thighLen + _shinLen;
float len = Mathf.Clamp(d.Length(), 0.05f, reach * 0.999f);
Vector3 dir = d.LengthSquared() > 1e-8f ? d.Normalized() : Vector3.Down;
ankle = hip + dir * len;
float along = (_thighLen * _thighLen - _shinLen * _shinLen + len * len) / (2f * len);
float radius = Mathf.Sqrt(Mathf.Max(_thighLen * _thighLen - along * along, 0f));
Vector3 pole = forward - dir * forward.Dot(dir);
pole = pole.LengthSquared() > 1e-6f ? pole.Normalized() : forward;
Vector3 knee = hip + dir * along + pole * radius;
Place(_thigh[i], hip, knee);
Place(_shin[i], knee, ankle);
Basis footBasis = new Basis(Vector3.Up, _feet[i].CurrentYaw);
_foot[i].GlobalTransform = new Transform3D(footBasis, _feet[i].Current + Vector3.Up * (0.035f * _scale) + footBasis * new Vector3(0f, 0f, -0.06f * _scale));
}
}
private static void Place(MeshInstance3D mesh, Vector3 from, Vector3 to)
{
Vector3 seg = to - from;
float len = seg.Length();
if (len < 1e-4f)
return;
Vector3 y = seg / len;
Vector3 x = Mathf.Abs(y.Y) < 0.99f ? Vector3.Up.Cross(y).Normalized() : Vector3.Right;
Vector3 z = x.Cross(y);
// Box mesh is 1 m tall along Y: stretch it to the segment.
mesh.GlobalTransform = new Transform3D(new Basis(x, y * len, z), (from + to) * 0.5f);
}
}