Arms playtest-2 fixes: flip-proof elbow (angle clamp + rate limit, controller-driven), Stage 2b hand coupling (blocked arm holds the hand back)
- Elbow stored as an angle around the arm axis vs a torso-frame reference: 85% hand-suggested, from the controller pose (not the physics palm), clamped +-110 deg, 6 rad/s rate limit (flip spike: 1.8 cm/tick max vs 13.8 unclamped) - PhysicsArms.ConstrainHand: while an arm segment is in contact the hand may lead the forearm end by 3 cm; PhysicsHands applies it and suspends stuck-snap while active - ArmSpike: forearm-rest-over-anvil (owner pose: collider fine, drawn forearm 3.9 cm -> 0), flip sweep, bar test now asserts the hand is held back; all six spikes PASS x3 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
464cda2f71
commit
7bc6a866eb
5 files changed
+193
-51
No files matched your search
+62
-30
@@ -27,8 +27,11 @@ public partial class PhysicsArms : Node
|
||||
private const float LeanSmoothRate = 6f;
|
||||
private const float WristOffset = 0.06f;
|
||||
private const float SoftIKFactor = 0.98f;
|
||||
private const float PoleHandWeight = 0.75f;
|
||||
private const float PoleHandWeight = 0.85f;
|
||||
private const float PoleSmoothRate = 12f;
|
||||
private const float PoleMaxAngle = 1.92f; // 110 deg either way from down/back/out
|
||||
private const float PoleMaxSpeed = 6f; // rad/s
|
||||
public const float HandArmSlack = 0.03f;
|
||||
private const float ElbowRiseAllowance = 0.05f;
|
||||
|
||||
public static float ForearmMaxForce => BodyMassStat * ForearmForcePerKg;
|
||||
@@ -39,6 +42,10 @@ public partial class PhysicsArms : Node
|
||||
public PhysicsTorso Torso;
|
||||
public RigidBody3D HandLeft;
|
||||
public RigidBody3D HandRight;
|
||||
// Hand ORIENTATION source for the elbow: the tracked controllers, not the physics palms
|
||||
// (a palm fighting contact must not swing the elbow). Falls back to the hand body.
|
||||
public Node3D OrientLeft;
|
||||
public Node3D OrientRight;
|
||||
|
||||
public bool Calibrated { get; private set; }
|
||||
public float UpperLen { get; private set; }
|
||||
@@ -50,7 +57,8 @@ public partial class PhysicsArms : Node
|
||||
private readonly Vector3[] _shoulder = new Vector3[2];
|
||||
private readonly Vector3[] _elbowTarget = new Vector3[2];
|
||||
private readonly Vector3[] _wristTarget = new Vector3[2];
|
||||
private readonly Vector3[] _lastPole = new Vector3[2];
|
||||
private readonly float[] _poleAngle = new float[2];
|
||||
private readonly Vector3[] _lastRef = new Vector3[2];
|
||||
private readonly RigidBody3D[] _held = new RigidBody3D[2];
|
||||
private Vector3 _torsoOffset;
|
||||
private Basis _torsoBasis = Basis.Identity;
|
||||
@@ -75,6 +83,8 @@ public partial class PhysicsArms : Node
|
||||
Torso = GetNode<PhysicsTorso>("../PhysicsTorso");
|
||||
HandLeft = PhysicsHands.GetBody(GetNode<XRController3D>("../XROrigin3D/LeftHand"));
|
||||
HandRight = PhysicsHands.GetBody(GetNode<XRController3D>("../XROrigin3D/RightHand"));
|
||||
OrientLeft = GetNode<XRController3D>("../XROrigin3D/LeftHand");
|
||||
OrientRight = GetNode<XRController3D>("../XROrigin3D/RightHand");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,8 +104,8 @@ public partial class PhysicsArms : Node
|
||||
_shoulder[0] = topCenter - _torsoBasis.X * ShoulderHalfWidth;
|
||||
_shoulder[1] = topCenter + _torsoBasis.X * ShoulderHalfWidth;
|
||||
|
||||
SolveArm(0, -_torsoBasis.X, HandLeft, dt);
|
||||
SolveArm(1, _torsoBasis.X, HandRight, dt);
|
||||
SolveArm(0, -_torsoBasis.X, HandLeft, OrientLeft, dt);
|
||||
SolveArm(1, _torsoBasis.X, HandRight, OrientRight, dt);
|
||||
UpdateLean(dt);
|
||||
|
||||
for (int side = 0; side < 2; side++)
|
||||
@@ -135,6 +145,8 @@ public partial class PhysicsArms : Node
|
||||
ContinuousCd = true,
|
||||
CollisionLayer = 0b10000, // layer 5 = player arms
|
||||
CollisionMask = 0b1, // world + props only; own chest/hands/arms excluded
|
||||
ContactMonitor = true,
|
||||
MaxContactsReported = 4,
|
||||
};
|
||||
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = size } });
|
||||
AddChild(body);
|
||||
@@ -213,6 +225,25 @@ public partial class PhysicsArms : Node
|
||||
}
|
||||
}
|
||||
|
||||
// Stage 2b coupling: while an arm segment is in contact, the hand may lead the forearm's
|
||||
// end by at most HandArmSlack — a blocked forearm holds the hand back (the hand ghost
|
||||
// shows the controller). Free arms never limit the hand. PhysicsHands calls this with
|
||||
// the controller target before chasing; returns true when the limit is active.
|
||||
public bool ConstrainHand(int side, ref Transform3D target)
|
||||
{
|
||||
if (!Calibrated || _fore[side] == null)
|
||||
return false;
|
||||
if (_fore[side].GetContactCount() == 0 && _upper[side].GetContactCount() == 0)
|
||||
return false;
|
||||
Vector3 wristOffset = target.Basis * new Vector3(0f, 0f, WristOffset);
|
||||
Vector3 forearmEnd = Wrist(side);
|
||||
Vector3 lead = target.Origin + wristOffset - forearmEnd;
|
||||
if (lead.Length() <= HandArmSlack)
|
||||
return true;
|
||||
target.Origin = forearmEnd + lead.Normalized() * HandArmSlack - wristOffset;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Torso lean toward overreaching hands, computed OPEN-LOOP from the head-derived nominal
|
||||
// chest pose — never from the body's own shoulders (that loop made the chest hunt:
|
||||
// lean moves shoulders → overreach changes → lean changes). Exact two-arm combination:
|
||||
@@ -255,7 +286,7 @@ public partial class PhysicsArms : Node
|
||||
// "down/back/out" pole fills in where the hand axis says nothing (arm pointing straight
|
||||
// along the hand). The pole is smoothed by ROTATING it at a bounded rate around the arm
|
||||
// axis, so it can never pass through zero and flip.
|
||||
private void SolveArm(int index, Vector3 side, RigidBody3D hand, float delta)
|
||||
private void SolveArm(int index, Vector3 side, RigidBody3D hand, Node3D orient, float delta)
|
||||
{
|
||||
Vector3 shoulder = _shoulder[index];
|
||||
Vector3 rawTarget = hand != null ? WristPoint(hand) : shoulder + _torsoBasis * new Vector3(0f, -0.25f, -0.3f);
|
||||
@@ -275,38 +306,39 @@ public partial class PhysicsArms : Node
|
||||
float radius = Mathf.Sqrt(Mathf.Max(UpperLen * UpperLen - along * along, 0f));
|
||||
Vector3 center = shoulder + dir * along;
|
||||
|
||||
Vector3 natural = Vector3.Down * 2f + _torsoBasis * Vector3.Back * 0.8f + side * 0.3f;
|
||||
natural -= dir * natural.Dot(dir);
|
||||
Vector3 pole = natural.LengthSquared() > 1e-8f ? natural.Normalized() * (1f - PoleHandWeight) : Vector3.Zero;
|
||||
if (hand != null)
|
||||
// Reference = torso-frame down/back/out projected off the arm axis. The elbow is
|
||||
// stored as an ANGLE around the arm axis relative to it, clamped to ±110° and
|
||||
// rate-limited — it can never swing through the inward/up side (the 180° flip).
|
||||
Vector3 reference = Vector3.Down * 2f + _torsoBasis * Vector3.Back * 0.8f + side * 0.3f;
|
||||
reference -= dir * reference.Dot(dir);
|
||||
if (reference.LengthSquared() > 0.06f)
|
||||
_lastRef[index] = reference.Normalized();
|
||||
else
|
||||
{
|
||||
Vector3 handForward = (hand.GlobalTransform.Basis * Vector3.Forward).Normalized();
|
||||
// Arm pointing along the reference: keep the previous one, re-projected.
|
||||
reference = _lastRef[index] - dir * _lastRef[index].Dot(dir);
|
||||
reference = reference.LengthSquared() > 1e-8f ? reference.Normalized() : dir.Cross(_torsoBasis.X).Normalized();
|
||||
}
|
||||
Vector3 refN = reference.Normalized();
|
||||
Vector3 refB = dir.Cross(refN);
|
||||
|
||||
float targetAngle = 0f;
|
||||
Basis handBasis = orient != null ? orient.GlobalTransform.Basis : hand?.GlobalTransform.Basis ?? Basis.Identity;
|
||||
if (orient != null || hand != null)
|
||||
{
|
||||
Vector3 handForward = (handBasis * Vector3.Forward).Normalized();
|
||||
Vector3 handElbow = wrist - handForward * ForeLen - center;
|
||||
handElbow -= dir * handElbow.Dot(dir);
|
||||
float confidence = Mathf.Clamp(handElbow.Length() / (0.5f * radius + 1e-4f), 0f, 1f);
|
||||
if (handElbow.LengthSquared() > 1e-8f)
|
||||
pole += handElbow.Normalized() * (PoleHandWeight * confidence);
|
||||
targetAngle = Mathf.Atan2(handElbow.Dot(refB), handElbow.Dot(refN)) * PoleHandWeight * confidence;
|
||||
}
|
||||
targetAngle = Mathf.Clamp(targetAngle, -PoleMaxAngle, PoleMaxAngle);
|
||||
float step = (targetAngle - _poleAngle[index]) * (1f - Mathf.Exp(-delta * PoleSmoothRate));
|
||||
_poleAngle[index] += Mathf.Clamp(step, -PoleMaxSpeed * delta, PoleMaxSpeed * delta);
|
||||
Vector3 pole = refN.Rotated(dir, _poleAngle[index]);
|
||||
|
||||
Vector3 last = _lastPole[index];
|
||||
last -= dir * last.Dot(dir);
|
||||
if (last.LengthSquared() < 1e-8f)
|
||||
last = pole.LengthSquared() > 1e-8f ? pole : natural;
|
||||
last = last.LengthSquared() > 1e-8f ? last.Normalized() : Vector3.Down;
|
||||
if (pole.LengthSquared() > 1e-8f)
|
||||
{
|
||||
pole = pole.Normalized();
|
||||
float angle = last.AngleTo(pole);
|
||||
if (angle > 1e-5f)
|
||||
{
|
||||
Vector3 axis = last.Cross(pole);
|
||||
axis = axis.LengthSquared() > 1e-10f ? axis.Normalized() : dir;
|
||||
last = last.Rotated(axis, angle * (1f - Mathf.Exp(-delta * PoleSmoothRate)));
|
||||
}
|
||||
}
|
||||
_lastPole[index] = last;
|
||||
|
||||
Vector3 elbow = center + CapElbowHeight(last, dir, center, radius, Mathf.Max(shoulder.Y, wrist.Y) + ElbowRiseAllowance) * radius;
|
||||
Vector3 elbow = center + CapElbowHeight(pole, dir, center, radius, Mathf.Max(shoulder.Y, wrist.Y) + ElbowRiseAllowance) * radius;
|
||||
_elbowTarget[index] = elbow;
|
||||
_wristTarget[index] = elbow + (wrist - elbow).Normalized() * ForeLen;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user