Initial commit: VR Messs workspace (grab v5.3.1, body-physics research)
This commit is contained in:
commit
5d2c5bf265
57 files changed
+4990
No files matched your search
@@ -0,0 +1,95 @@
|
||||
using Godot;
|
||||
|
||||
public partial class AnchorMoveMode : Node
|
||||
{
|
||||
private const float GripThreshold = 0.6f;
|
||||
|
||||
private XROrigin3D _origin;
|
||||
private XRCamera3D _camera;
|
||||
private XRController3D _left;
|
||||
private XRController3D _right;
|
||||
|
||||
private bool _wasTwoHand;
|
||||
private Vector3 _leftPrev;
|
||||
private Vector3 _rightPrev;
|
||||
private Vector3 _twoHandMidPrev;
|
||||
private float _twoHandYawPrev;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_origin = GetNode<XROrigin3D>("../XROrigin3D");
|
||||
_camera = GetNode<XRCamera3D>("../XROrigin3D/XRCamera3D");
|
||||
_left = GetNode<XRController3D>("../XROrigin3D/LeftHand");
|
||||
_right = GetNode<XRController3D>("../XROrigin3D/RightHand");
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
bool leftGrip = _left.GetFloat("grip") > GripThreshold && !PropGrabber.IsGrabbing(_left);
|
||||
bool rightGrip = _right.GetFloat("grip") > GripThreshold && !PropGrabber.IsGrabbing(_right);
|
||||
|
||||
if (leftGrip && rightGrip && !WorldPanel.IsBlocking(_left) && !WorldPanel.IsBlocking(_right))
|
||||
TwoHandGrab();
|
||||
else
|
||||
{
|
||||
_wasTwoHand = false;
|
||||
if (leftGrip && !WorldPanel.IsBlocking(_left))
|
||||
GrabMove(_left, ref _leftPrev);
|
||||
else
|
||||
_leftPrev = _left.Position;
|
||||
if (rightGrip && !WorldPanel.IsBlocking(_right))
|
||||
GrabMove(_right, ref _rightPrev);
|
||||
else
|
||||
_rightPrev = _right.Position;
|
||||
}
|
||||
}
|
||||
|
||||
private void TwoHandGrab()
|
||||
{
|
||||
Vector3 mid = Midpoint();
|
||||
float yaw = HandsYaw();
|
||||
|
||||
if (!_wasTwoHand)
|
||||
{
|
||||
_wasTwoHand = true;
|
||||
_twoHandMidPrev = mid;
|
||||
_twoHandYawPrev = yaw;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector3 deltaWorld = _origin.GlobalTransform.Basis * (mid - _twoHandMidPrev);
|
||||
float yawDelta = _twoHandYawPrev - yaw;
|
||||
if (deltaWorld.LengthSquared() > 0.000001f || Mathf.Abs(yawDelta) > 0.000001f)
|
||||
{
|
||||
Vector3 hxz = new Vector3(_camera.GlobalPosition.X, 0f, _camera.GlobalPosition.Z);
|
||||
Transform3D turn = new Transform3D(Basis.Identity, hxz)
|
||||
* new Transform3D(Basis.FromEuler(new Vector3(0, yawDelta, 0)), Vector3.Zero)
|
||||
* new Transform3D(Basis.Identity, -hxz);
|
||||
_origin.GlobalTransform = new Transform3D(Basis.Identity, -deltaWorld) * turn * _origin.GlobalTransform;
|
||||
}
|
||||
_twoHandMidPrev = mid;
|
||||
_twoHandYawPrev = yaw;
|
||||
}
|
||||
|
||||
private void GrabMove(XRController3D controller, ref Vector3 prev)
|
||||
{
|
||||
Vector3 pos = controller.Position;
|
||||
Vector3 delta = pos - prev;
|
||||
prev = pos;
|
||||
if (delta.LengthSquared() == 0f)
|
||||
return;
|
||||
Vector3 deltaWorld = _origin.GlobalTransform.Basis * delta;
|
||||
_origin.GlobalTransform = new Transform3D(Basis.Identity, -deltaWorld) * _origin.GlobalTransform;
|
||||
}
|
||||
|
||||
private Vector3 Midpoint() => (_left.Position + _right.Position) * 0.5f;
|
||||
|
||||
private float HandsYaw()
|
||||
{
|
||||
Vector3 v = _right.Position - _left.Position;
|
||||
v.Y = 0f;
|
||||
if (v.LengthSquared() < 0.0001f)
|
||||
return _twoHandYawPrev;
|
||||
return Mathf.Atan2(v.X, v.Z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://00dofwddtngn
|
||||
@@ -0,0 +1,250 @@
|
||||
using Godot;
|
||||
|
||||
public partial class BodyRig : Node3D
|
||||
{
|
||||
private const float HeadBackOffset = 0.06f;
|
||||
private const float HeadSize = 0.15f;
|
||||
private const float TorsoDrop = 0.42f;
|
||||
private const float TorsoBackOffset = 0.06f;
|
||||
private const float ShoulderRise = 0.25f;
|
||||
private const float ReachBudgetMax = 0.25f;
|
||||
private const float ReachVerticalMin = -0.05f;
|
||||
private const float ReachVerticalMax = 0.12f;
|
||||
private const float ReachPushSpeed = 0.8f;
|
||||
private const float ReachDecaySpeed = 0.4f;
|
||||
private const float ReachDecayFactor = 0.92f;
|
||||
private const float WristOffset = 0.06f;
|
||||
private const float SoftIKFactor = 0.98f;
|
||||
private const float PoleHandWeight = 0.25f;
|
||||
private const float PoleSmoothRate = 10f;
|
||||
private const float TorsoYawHandsWeight = 0.6f;
|
||||
private const float TorsoYawForwardness = 0.3f;
|
||||
private const float TorsoYawSmoothRate = 12f;
|
||||
|
||||
private XRCamera3D _camera;
|
||||
private XRController3D _left;
|
||||
private XRController3D _right;
|
||||
|
||||
private MeshInstance3D _headMesh;
|
||||
private MeshInstance3D _torsoMesh;
|
||||
private readonly MeshInstance3D[] _upperArmMeshes = new MeshInstance3D[2];
|
||||
private readonly MeshInstance3D[] _forearmMeshes = new MeshInstance3D[2];
|
||||
|
||||
private bool _calibrated;
|
||||
private float _upperLen;
|
||||
private float _foreLen;
|
||||
private float _shoulderHalfWidth;
|
||||
private Vector3 _torsoOffset = Vector3.Zero;
|
||||
private Basis _yawBasis = Basis.Identity;
|
||||
private Basis _torsoBasis = Basis.Identity;
|
||||
private float _torsoYaw;
|
||||
private readonly Vector3[] _lastPole = new Vector3[2];
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_camera = GetNode<XRCamera3D>("../XROrigin3D/XRCamera3D");
|
||||
_left = GetNode<XRController3D>("../XROrigin3D/LeftHand");
|
||||
_right = GetNode<XRController3D>("../XROrigin3D/RightHand");
|
||||
|
||||
_headMesh = MakeMesh(new BoxMesh { Size = new Vector3(HeadSize, HeadSize, HeadSize) }, new Color(0.8f, 0.8f, 0.8f));
|
||||
_torsoMesh = MakeMesh(new BoxMesh { Size = new Vector3(0.22f, 0.5f, 0.13f) }, new Color(0.4f, 0.45f, 0.55f));
|
||||
_upperArmMeshes[0] = MakeMesh(new BoxMesh { Size = new Vector3(0.07f, 0.3f, 0.07f) }, new Color(0.3f, 0.3f, 0.33f));
|
||||
_upperArmMeshes[1] = MakeMesh(new BoxMesh { Size = new Vector3(0.07f, 0.3f, 0.07f) }, new Color(0.3f, 0.3f, 0.33f));
|
||||
_forearmMeshes[0] = MakeMesh(new BoxMesh { Size = new Vector3(0.06f, 0.3f, 0.06f) }, new Color(0.3f, 0.3f, 0.33f));
|
||||
_forearmMeshes[1] = MakeMesh(new BoxMesh { Size = new Vector3(0.06f, 0.3f, 0.06f) }, new Color(0.3f, 0.3f, 0.33f));
|
||||
|
||||
SetRigVisible(false);
|
||||
}
|
||||
|
||||
private MeshInstance3D MakeMesh(Mesh mesh, Color color)
|
||||
{
|
||||
MeshInstance3D node = new() { Mesh = mesh };
|
||||
node.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.6f });
|
||||
AddChild(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
private void SetRigVisible(bool visible)
|
||||
{
|
||||
_headMesh.Visible = visible;
|
||||
_torsoMesh.Visible = visible;
|
||||
foreach (MeshInstance3D m in _upperArmMeshes)
|
||||
m.Visible = visible;
|
||||
foreach (MeshInstance3D m in _forearmMeshes)
|
||||
m.Visible = visible;
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
UpdateYawBasis();
|
||||
float dt = (float)delta;
|
||||
|
||||
if (!_calibrated)
|
||||
{
|
||||
float headHeight = _camera.GlobalPosition.Y;
|
||||
if (headHeight <= 0.5f)
|
||||
return;
|
||||
Calibrate(headHeight);
|
||||
}
|
||||
|
||||
Vector3 headPos = _camera.GlobalPosition - _yawBasis * Vector3.Forward * HeadBackOffset;
|
||||
_headMesh.GlobalTransform = new Transform3D(_yawBasis, headPos);
|
||||
|
||||
UpdateTorsoYaw(headPos, dt);
|
||||
|
||||
Vector3 torsoBack = _torsoBasis * Vector3.Back;
|
||||
Vector3 torsoCenter = headPos + Vector3.Down * TorsoDrop + torsoBack * TorsoBackOffset + _torsoOffset;
|
||||
_torsoMesh.GlobalTransform = new Transform3D(_torsoBasis, torsoCenter);
|
||||
|
||||
Vector3 topCenter = torsoCenter + Vector3.Up * ShoulderRise;
|
||||
Vector3 leftShoulder = topCenter - _torsoBasis.X * _shoulderHalfWidth;
|
||||
Vector3 rightShoulder = topCenter + _torsoBasis.X * _shoulderHalfWidth;
|
||||
|
||||
Vector3 push = SolveArm(0, leftShoulder, -_torsoBasis.X, PhysicsHands.GetBody(_left), _upperArmMeshes[0], _forearmMeshes[0], dt, out bool decayLeft);
|
||||
push += SolveArm(1, rightShoulder, _torsoBasis.X, PhysicsHands.GetBody(_right), _upperArmMeshes[1], _forearmMeshes[1], dt, out bool decayRight);
|
||||
|
||||
if (push != Vector3.Zero)
|
||||
{
|
||||
_torsoOffset += push * (dt * ReachPushSpeed);
|
||||
}
|
||||
else if (decayLeft || decayRight)
|
||||
{
|
||||
_torsoOffset = _torsoOffset.MoveToward(Vector3.Zero, ReachDecaySpeed * dt);
|
||||
}
|
||||
|
||||
Vector3 horizontal = new(_torsoOffset.X, 0f, _torsoOffset.Z);
|
||||
horizontal = horizontal.LimitLength(ReachBudgetMax);
|
||||
_torsoOffset = new Vector3(horizontal.X, Mathf.Clamp(_torsoOffset.Y, ReachVerticalMin, ReachVerticalMax), horizontal.Z);
|
||||
}
|
||||
|
||||
private void UpdateYawBasis()
|
||||
{
|
||||
Vector3 fwd = -_camera.GlobalTransform.Basis.Z;
|
||||
fwd.Y = 0f;
|
||||
if (fwd.LengthSquared() < 0.0001f)
|
||||
return;
|
||||
_yawBasis = Basis.LookingAt(fwd.Normalized(), Vector3.Up);
|
||||
}
|
||||
|
||||
private void UpdateTorsoYaw(Vector3 headPos, float delta)
|
||||
{
|
||||
float hmdYaw = YawOf(-_yawBasis.Z);
|
||||
RigidBody3D leftBody = PhysicsHands.GetBody(_left);
|
||||
RigidBody3D rightBody = PhysicsHands.GetBody(_right);
|
||||
float targetYaw = hmdYaw;
|
||||
if (leftBody != null && rightBody != null)
|
||||
{
|
||||
Vector3 toHands = (leftBody.GlobalPosition + rightBody.GlobalPosition) * 0.5f - headPos;
|
||||
Vector3 horizontal = new(toHands.X, 0f, toHands.Z);
|
||||
if (horizontal.LengthSquared() > 0.0001f)
|
||||
{
|
||||
float handsYaw = YawOf(horizontal.Normalized());
|
||||
float forwardness = Mathf.Clamp(horizontal.Dot(-_yawBasis.Z) / TorsoYawForwardness, 0f, 1f);
|
||||
targetYaw = Mathf.LerpAngle(hmdYaw, handsYaw, forwardness * TorsoYawHandsWeight);
|
||||
}
|
||||
}
|
||||
_torsoYaw = Mathf.LerpAngle(_torsoYaw, targetYaw, 1f - Mathf.Exp(-delta * TorsoYawSmoothRate));
|
||||
Vector3 fwd = new(-Mathf.Sin(_torsoYaw), 0f, -Mathf.Cos(_torsoYaw));
|
||||
_torsoBasis = Basis.LookingAt(fwd, Vector3.Up);
|
||||
}
|
||||
|
||||
private static float YawOf(Vector3 forward)
|
||||
{
|
||||
return Mathf.Atan2(-forward.X, -forward.Z);
|
||||
}
|
||||
|
||||
private void Calibrate(float headHeight)
|
||||
{
|
||||
_calibrated = true;
|
||||
_torsoYaw = YawOf(-_yawBasis.Z);
|
||||
float armTotal = Mathf.Clamp(headHeight * 0.46f, 0.5f, 0.72f);
|
||||
_upperLen = 0.5f * armTotal;
|
||||
_foreLen = 0.5f * armTotal;
|
||||
_shoulderHalfWidth = 0.16f + headHeight * 0.02f;
|
||||
|
||||
((BoxMesh)_upperArmMeshes[0].Mesh).Size = new Vector3(0.07f, _upperLen, 0.07f);
|
||||
((BoxMesh)_upperArmMeshes[1].Mesh).Size = new Vector3(0.07f, _upperLen, 0.07f);
|
||||
((BoxMesh)_forearmMeshes[0].Mesh).Size = new Vector3(0.06f, _foreLen, 0.06f);
|
||||
((BoxMesh)_forearmMeshes[1].Mesh).Size = new Vector3(0.06f, _foreLen, 0.06f);
|
||||
|
||||
SetRigVisible(true);
|
||||
GD.Print($"BodyRig: proportions calibrated from head height {headHeight:F2} m " +
|
||||
$"(arm total {armTotal:F2} m, upper {_upperLen:F2} m, fore {_foreLen:F2} m, shoulder half-width {_shoulderHalfWidth:F2} m)");
|
||||
}
|
||||
|
||||
private Vector3 SolveArm(int index, Vector3 shoulder, Vector3 side, RigidBody3D body, MeshInstance3D upperMesh, MeshInstance3D foreMesh, float delta, out bool decay)
|
||||
{
|
||||
Vector3 rawTarget = body != null
|
||||
? body.GlobalPosition + body.GlobalTransform.Basis * new Vector3(0f, 0f, WristOffset)
|
||||
: shoulder + _torsoBasis * new Vector3(0f, -0.25f, -0.3f);
|
||||
|
||||
float reach = _upperLen + _foreLen;
|
||||
Vector3 shoulderToTarget = rawTarget - shoulder;
|
||||
if (shoulderToTarget.LengthSquared() < 0.0001f)
|
||||
shoulderToTarget = _torsoBasis * new Vector3(0f, -0.25f, -0.3f);
|
||||
|
||||
float rawDist = shoulderToTarget.Length();
|
||||
float d = Mathf.Clamp(rawDist, 0.01f, reach * SoftIKFactor);
|
||||
Vector3 dir = shoulderToTarget / rawDist;
|
||||
Vector3 clampedTarget = shoulder + dir * d;
|
||||
|
||||
Vector3 push = Vector3.Zero;
|
||||
decay = false;
|
||||
if (rawDist > reach)
|
||||
{
|
||||
push = (rawTarget - clampedTarget).Normalized();
|
||||
}
|
||||
else if (rawDist < reach * ReachDecayFactor)
|
||||
{
|
||||
decay = true;
|
||||
}
|
||||
|
||||
float cosA = (_upperLen * _upperLen + d * d - _foreLen * _foreLen) / (2f * _upperLen * d);
|
||||
float angle = Mathf.Acos(Mathf.Clamp(cosA, -1f, 1f));
|
||||
|
||||
Vector3 handForward = body != null ? body.GlobalTransform.Basis * Vector3.Forward : Vector3.Zero;
|
||||
Vector3 pole = Vector3.Down * 2f + _torsoBasis * Vector3.Back * 0.8f + side * 0.3f;
|
||||
pole -= dir * pole.Dot(dir);
|
||||
pole -= handForward * PoleHandWeight;
|
||||
Vector3 poleDir;
|
||||
if (pole.LengthSquared() < 0.0001f)
|
||||
poleDir = _lastPole[index] == Vector3.Zero ? Vector3.Down : _lastPole[index];
|
||||
else
|
||||
{
|
||||
poleDir = _lastPole[index].Lerp(pole, 1f - Mathf.Exp(-delta * PoleSmoothRate));
|
||||
if (poleDir.LengthSquared() < 0.0001f)
|
||||
poleDir = _lastPole[index];
|
||||
poleDir = poleDir.Normalized();
|
||||
_lastPole[index] = poleDir;
|
||||
}
|
||||
|
||||
Vector3 axis = dir.Cross(poleDir);
|
||||
if (axis.LengthSquared() < 0.0001f)
|
||||
axis = dir.Cross(Vector3.Up);
|
||||
if (axis.LengthSquared() < 0.0001f)
|
||||
axis = Vector3.Right;
|
||||
axis = axis.Normalized();
|
||||
|
||||
Vector3 elbowPos = shoulder + dir.Rotated(axis, angle) * _upperLen;
|
||||
Vector3 foreEnd = elbowPos + (clampedTarget - elbowPos).Normalized() * _foreLen;
|
||||
|
||||
PlaceSegment(upperMesh, shoulder, elbowPos);
|
||||
PlaceSegment(foreMesh, elbowPos, foreEnd);
|
||||
|
||||
return push;
|
||||
}
|
||||
|
||||
private static void PlaceSegment(MeshInstance3D mesh, Vector3 from, Vector3 to)
|
||||
{
|
||||
Vector3 segment = to - from;
|
||||
if (segment.LengthSquared() < 0.000001f)
|
||||
return;
|
||||
Vector3 y = segment.Normalized();
|
||||
Vector3 x = Vector3.Up.Cross(y);
|
||||
if (x.LengthSquared() < 0.0001f)
|
||||
x = Vector3.Right.Cross(y);
|
||||
x = x.Normalized();
|
||||
Vector3 z = x.Cross(y);
|
||||
mesh.GlobalTransform = new Transform3D(new Basis(x, y, z), (from + to) * 0.5f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dwaonvvnka0b
|
||||
@@ -0,0 +1,487 @@
|
||||
using Godot;
|
||||
|
||||
public partial class GrabSpike : Node3D
|
||||
{
|
||||
private const float HandMass = 0.4f;
|
||||
private const float HandMaxForce = 90f;
|
||||
private const float HandMaxSpeed = 2.5f;
|
||||
private const float ReachSpeed = 1f;
|
||||
private const float HandMaxSpin = 120f;
|
||||
private const float HandRadius = 0.05f;
|
||||
private const float MoveDistance = 2f;
|
||||
private const float MoveDuration = 2f;
|
||||
private const float DragRampTime = 1f;
|
||||
private const float LiftHeight = 0.3f;
|
||||
private const float MaxSpeed = 20f;
|
||||
private const float ReachTimeout = 3f;
|
||||
private const float RigidWeldBias = 0f;
|
||||
private const float RigidRotLimitDeg = 2f;
|
||||
private const float RigidSepLimit = 0.01f;
|
||||
private const float SpinRotLimitDeg = 3.5f;
|
||||
private const float TakeoverRotLimitDeg = 5f;
|
||||
private const float SpinLimit = 40f;
|
||||
private const float PullDistance = 0.3f;
|
||||
private const float SpinAngleDeg = 360f;
|
||||
private const float SpinDuration = 2.5f;
|
||||
private const float SpinHoldDuration = 0.5f;
|
||||
private const float TugDuration = 1.5f;
|
||||
private const float SettleDuration = 0.5f;
|
||||
private const float FollowDuration = 1f;
|
||||
private const float TugPull = 0.5f;
|
||||
private const float FollowSway = 0.03f;
|
||||
private const float TwoHandDuration = TugDuration + FollowDuration;
|
||||
private const float TakeoverDuration = 1.5f;
|
||||
|
||||
private enum Phase { Reach, Drag, Spin, Hand2Reach, TwoHanded, TwoSettle, TwoFollow, Takeover }
|
||||
|
||||
private RigidBody3D _prop;
|
||||
private RigidBody3D _hand;
|
||||
private RigidBody3D _hand2;
|
||||
private Node3D _target;
|
||||
private Node3D _target2;
|
||||
private ConeTwistJoint3D _joint;
|
||||
private ConeTwistJoint3D _joint2;
|
||||
private Vector3 _grabPoint;
|
||||
private Vector3 _grab2Point;
|
||||
private Vector3 _propAnchor;
|
||||
private Vector3 _handAnchor;
|
||||
private Vector3 _propAnchor2;
|
||||
private Vector3 _handAnchor2;
|
||||
private Quaternion _relRotation1;
|
||||
private Quaternion _relRotation2;
|
||||
private Quaternion _spinFrom;
|
||||
private Quaternion _spinTo;
|
||||
private Quaternion _cmd1 = Quaternion.Identity;
|
||||
private Vector3 _takeoverBase;
|
||||
private Phase _phase = Phase.Reach;
|
||||
private float _phaseStart;
|
||||
private float _elapsed;
|
||||
private bool _dragRotOk;
|
||||
private bool _dragSepOk;
|
||||
private bool _dragSpeedOk;
|
||||
private bool _spinRotOk;
|
||||
private bool _spinSepOk;
|
||||
private bool _spinSpeedOk;
|
||||
private bool _twoHandedOk;
|
||||
private bool _takeoverRotOk;
|
||||
private bool _takeoverSepOk;
|
||||
private bool _takeoverSpeedOk;
|
||||
private float _maxDragRot;
|
||||
private float _maxDragSep;
|
||||
private float _maxDragPropSpeed;
|
||||
private float _maxDragHandSpeed;
|
||||
private float _maxSpinRot;
|
||||
private float _maxSpinSep;
|
||||
private float _maxSpinPropSpeed;
|
||||
private float _maxSpinHandSpeed;
|
||||
private float _maxTwoAnchor1;
|
||||
private float _maxTugAnchor2;
|
||||
private float _maxFollowAnchor2;
|
||||
private float _maxTwoPropSpeed;
|
||||
private float _maxTwoHandSpeed;
|
||||
private float _maxTwoHand2Speed;
|
||||
private float _maxTwoSpin;
|
||||
private float _maxTakeoverRot;
|
||||
private float _maxTakeoverSep;
|
||||
private float _maxTakeoverPropSpeed;
|
||||
private float _maxTakeoverHandSpeed;
|
||||
private int _lastLogStep;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
StaticBody3D floor = new() { Position = new Vector3(0, -0.5f, 0) };
|
||||
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(20, 1, 20) } });
|
||||
AddChild(floor);
|
||||
|
||||
_prop = new RigidBody3D { Position = new Vector3(0, 0.15f, 0), Rotation = new Vector3(0, Mathf.Pi / 2f, 0), Mass = 1f };
|
||||
_prop.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = Vector3.One * 0.3f } });
|
||||
AddChild(_prop);
|
||||
|
||||
_hand = new RigidBody3D
|
||||
{
|
||||
Position = new Vector3(0, 0.9f, 0),
|
||||
Mass = HandMass,
|
||||
GravityScale = 0f,
|
||||
ContinuousCd = true,
|
||||
ContactMonitor = true,
|
||||
MaxContactsReported = 4,
|
||||
};
|
||||
_hand.AddChild(new CollisionShape3D { Shape = new SphereShape3D { Radius = HandRadius } });
|
||||
_hand.BodyEntered += OnHand1Touched;
|
||||
AddChild(_hand);
|
||||
|
||||
_hand2 = new RigidBody3D
|
||||
{
|
||||
Position = new Vector3(0, 0.15f, -1f),
|
||||
Mass = HandMass,
|
||||
GravityScale = 0f,
|
||||
ContinuousCd = true,
|
||||
ContactMonitor = true,
|
||||
MaxContactsReported = 4,
|
||||
};
|
||||
_hand2.AddChild(new CollisionShape3D { Shape = new SphereShape3D { Radius = HandRadius } });
|
||||
_hand2.BodyEntered += OnHand2Touched;
|
||||
AddChild(_hand2);
|
||||
|
||||
_target = new Node3D { Position = _prop.Position };
|
||||
AddChild(_target);
|
||||
_target2 = new Node3D { Position = _hand2.Position };
|
||||
AddChild(_target2);
|
||||
GD.Print("GRAB: grab spike scene ready");
|
||||
}
|
||||
|
||||
private void OnHand1Touched(Node body)
|
||||
{
|
||||
if (body != _prop || _phase != Phase.Reach)
|
||||
return;
|
||||
_grabPoint = _hand.GlobalPosition;
|
||||
_relRotation1 = _hand.GlobalTransform.Basis.GetRotationQuaternion().Inverse()
|
||||
* _prop.GlobalTransform.Basis.GetRotationQuaternion();
|
||||
_propAnchor = _prop.GlobalTransform.AffineInverse() * _grabPoint;
|
||||
_handAnchor = _hand.GlobalTransform.AffineInverse() * _grabPoint;
|
||||
_joint = MakeWeld(_hand, _prop, _grabPoint, RigidWeldBias);
|
||||
_maxDragRot = 0f;
|
||||
_maxDragSep = 0f;
|
||||
_maxDragPropSpeed = 0f;
|
||||
_maxDragHandSpeed = 0f;
|
||||
_phase = Phase.Drag;
|
||||
_phaseStart = _elapsed;
|
||||
_lastLogStep = 0;
|
||||
GD.Print($"GRAB: rigid weld grab at t={_elapsed:F2} s, pivot {_grabPoint}, server joint type = {PhysicsServer3D.JointGetType(_joint.GetRid())} (expect {PhysicsServer3D.JointType.ConeTwist})");
|
||||
}
|
||||
|
||||
private void OnHand2Touched(Node body)
|
||||
{
|
||||
if (body != _prop || _phase != Phase.Hand2Reach)
|
||||
return;
|
||||
_grab2Point = _hand2.GlobalPosition;
|
||||
_relRotation2 = _hand2.GlobalTransform.Basis.GetRotationQuaternion().Inverse()
|
||||
* _prop.GlobalTransform.Basis.GetRotationQuaternion();
|
||||
_propAnchor2 = _prop.GlobalTransform.AffineInverse() * _grab2Point;
|
||||
_handAnchor2 = _hand2.GlobalTransform.AffineInverse() * _grab2Point;
|
||||
_joint2 = MakeWeld(_hand2, _prop, _grab2Point, RigidWeldBias);
|
||||
_maxTwoAnchor1 = 0f;
|
||||
_maxTugAnchor2 = 0f;
|
||||
_maxFollowAnchor2 = 0f;
|
||||
_maxTwoPropSpeed = 0f;
|
||||
_maxTwoHandSpeed = 0f;
|
||||
_maxTwoHand2Speed = 0f;
|
||||
_maxTwoSpin = 0f;
|
||||
_phase = Phase.TwoHanded;
|
||||
_phaseStart = _elapsed;
|
||||
_lastLogStep = 0;
|
||||
GD.Print($"GRAB: second hand rigid weld at t={_elapsed:F2} s, pivot {_grab2Point}");
|
||||
}
|
||||
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
float dt = (float)delta;
|
||||
_elapsed += dt;
|
||||
float phaseT = _elapsed - _phaseStart;
|
||||
|
||||
switch (_phase)
|
||||
{
|
||||
case Phase.Reach:
|
||||
_target.GlobalPosition = _prop.GlobalPosition;
|
||||
_target2.GlobalPosition = _hand2.GlobalPosition;
|
||||
if (phaseT > ReachTimeout)
|
||||
Fail("hand never touched the prop");
|
||||
break;
|
||||
case Phase.Drag:
|
||||
{
|
||||
_target.GlobalPosition = _grabPoint + new Vector3(DragOffset(phaseT), LiftHeight * Mathf.Min(phaseT / DragRampTime, 1f), 0f);
|
||||
_maxDragPropSpeed = Mathf.Max(_maxDragPropSpeed, _prop.LinearVelocity.Length());
|
||||
_maxDragHandSpeed = Mathf.Max(_maxDragHandSpeed, _hand.LinearVelocity.Length());
|
||||
TrackRigidWeld(ref _maxDragRot, ref _maxDragSep);
|
||||
int step = (int)(phaseT / 0.5f);
|
||||
if (step > _lastLogStep && phaseT < MoveDuration)
|
||||
{
|
||||
_lastLogStep = step;
|
||||
GD.Print($"GRAB: drag t={phaseT:F2} rotDev={Mathf.RadToDeg(RotDev1()):F2} deg sep={Sep1():F4} m propV={_prop.LinearVelocity.Length():F2}");
|
||||
}
|
||||
if (phaseT >= MoveDuration)
|
||||
FinishDrag();
|
||||
break;
|
||||
}
|
||||
case Phase.Spin:
|
||||
{
|
||||
_cmd1 = _spinFrom * new Quaternion(Vector3.Up,
|
||||
Mathf.DegToRad(SpinAngleDeg) * Mathf.SmoothStep(0f, 1f, Mathf.Min(phaseT / SpinDuration, 1f)));
|
||||
_target.GlobalPosition = _grabPoint + new Vector3(MoveDistance, LiftHeight, 0f);
|
||||
_maxSpinPropSpeed = Mathf.Max(_maxSpinPropSpeed, _prop.LinearVelocity.Length());
|
||||
_maxSpinHandSpeed = Mathf.Max(_maxSpinHandSpeed, _hand.LinearVelocity.Length());
|
||||
TrackRigidWeld(ref _maxSpinRot, ref _maxSpinSep);
|
||||
int step = (int)(phaseT / 0.5f);
|
||||
if (step > _lastLogStep)
|
||||
{
|
||||
_lastLogStep = step;
|
||||
GD.Print($"GRAB: fast spin t={phaseT:F2} rotDev={Mathf.RadToDeg(RotDev1()):F2} deg sep={Sep1():F4} m handW={_hand.AngularVelocity.Length():F1} rad/s");
|
||||
}
|
||||
if (phaseT >= SpinDuration + SpinHoldDuration)
|
||||
FinishSpin();
|
||||
break;
|
||||
}
|
||||
case Phase.Hand2Reach:
|
||||
_target.GlobalPosition = _grabPoint + new Vector3(MoveDistance, LiftHeight, 0f);
|
||||
_target2.GlobalPosition = _prop.GlobalPosition;
|
||||
if (phaseT > ReachTimeout)
|
||||
Fail("second hand never touched the prop");
|
||||
break;
|
||||
case Phase.TwoHanded:
|
||||
{
|
||||
float pull = TugPull * Mathf.SmoothStep(0f, 1f, Mathf.Min(phaseT / 1f, 1f));
|
||||
float zSway = 0.35f * Mathf.Sin(2f * phaseT) * (1f - Mathf.SmoothStep(0f, 1f, Mathf.Min(phaseT / TugDuration, 1f)));
|
||||
_target.GlobalPosition = _grabPoint + new Vector3(MoveDistance + pull, LiftHeight, zSway);
|
||||
_target2.GlobalPosition = _grab2Point + new Vector3(-pull, 0f, -zSway);
|
||||
_maxTwoAnchor1 = Mathf.Max(_maxTwoAnchor1, Sep1());
|
||||
_maxTugAnchor2 = Mathf.Max(_maxTugAnchor2, Sep2());
|
||||
_maxTwoPropSpeed = Mathf.Max(_maxTwoPropSpeed, _prop.LinearVelocity.Length());
|
||||
_maxTwoHandSpeed = Mathf.Max(_maxTwoHandSpeed, _hand.LinearVelocity.Length());
|
||||
_maxTwoHand2Speed = Mathf.Max(_maxTwoHand2Speed, _hand2.LinearVelocity.Length());
|
||||
_maxTwoSpin = Mathf.Max(_maxTwoSpin, _prop.AngularVelocity.Length());
|
||||
int step = (int)(phaseT / 0.5f);
|
||||
if (step > _lastLogStep)
|
||||
{
|
||||
_lastLogStep = step;
|
||||
GD.Print($"GRAB: two-handed tug t={phaseT:F2} anchor1={Sep1():F3} anchor2={Sep2():F3} spin={_prop.AngularVelocity.Length():F1} propV={_prop.LinearVelocity.Length():F2}");
|
||||
}
|
||||
if (phaseT >= TugDuration)
|
||||
{
|
||||
_phase = Phase.TwoSettle;
|
||||
_phaseStart = _elapsed;
|
||||
_lastLogStep = 0;
|
||||
GD.Print("GRAB: tug done, settling before the follow assert");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Phase.TwoSettle:
|
||||
{
|
||||
_target.GlobalPosition = _grabPoint + new Vector3(MoveDistance + TugPull, LiftHeight, 0f);
|
||||
_target2.GlobalPosition = _prop.GlobalTransform * _propAnchor2;
|
||||
if (phaseT >= SettleDuration)
|
||||
{
|
||||
_phase = Phase.TwoFollow;
|
||||
_phaseStart = _elapsed;
|
||||
_lastLogStep = 0;
|
||||
GD.Print($"GRAB: settled at anchor2={Sep2():F3} m, second hand now follows with a small out-of-phase sway");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case Phase.TwoFollow:
|
||||
{
|
||||
_target.GlobalPosition = _grabPoint + new Vector3(MoveDistance + TugPull, LiftHeight, 0f);
|
||||
_target2.GlobalPosition = _prop.GlobalTransform * _propAnchor2 + new Vector3(0f, 0f, -FollowSway * Mathf.Sin(2f * Mathf.Pi * 1.5f * phaseT));
|
||||
_maxTwoAnchor1 = Mathf.Max(_maxTwoAnchor1, Sep1());
|
||||
_maxFollowAnchor2 = Mathf.Max(_maxFollowAnchor2, Sep2());
|
||||
_maxTwoPropSpeed = Mathf.Max(_maxTwoPropSpeed, _prop.LinearVelocity.Length());
|
||||
_maxTwoHandSpeed = Mathf.Max(_maxTwoHandSpeed, _hand.LinearVelocity.Length());
|
||||
_maxTwoHand2Speed = Mathf.Max(_maxTwoHand2Speed, _hand2.LinearVelocity.Length());
|
||||
_maxTwoSpin = Mathf.Max(_maxTwoSpin, _prop.AngularVelocity.Length());
|
||||
int step = (int)(phaseT / 0.5f);
|
||||
if (step > _lastLogStep)
|
||||
{
|
||||
_lastLogStep = step;
|
||||
GD.Print($"GRAB: two-handed follow t={phaseT:F2} anchor1={Sep1():F3} anchor2={Sep2():F3} propV={_prop.LinearVelocity.Length():F2}");
|
||||
}
|
||||
if (phaseT >= FollowDuration)
|
||||
FinishTwoHanded();
|
||||
break;
|
||||
}
|
||||
case Phase.Takeover:
|
||||
{
|
||||
_target.GlobalPosition = _grabPoint + new Vector3(MoveDistance, LiftHeight, 0f);
|
||||
float move = PullDistance * Mathf.SmoothStep(0f, 1f, Mathf.Min(phaseT / TakeoverDuration, 1f));
|
||||
_target2.GlobalPosition = _takeoverBase + new Vector3(-move, 0f, 0f);
|
||||
_maxTakeoverPropSpeed = Mathf.Max(_maxTakeoverPropSpeed, _prop.LinearVelocity.Length());
|
||||
_maxTakeoverHandSpeed = Mathf.Max(_maxTakeoverHandSpeed, _hand2.LinearVelocity.Length());
|
||||
TrackTakeoverWeld();
|
||||
int step = (int)(phaseT / 0.5f);
|
||||
if (step > _lastLogStep)
|
||||
{
|
||||
_lastLogStep = step;
|
||||
GD.Print($"GRAB: takeover t={phaseT:F2} rotDev={Mathf.RadToDeg(RotDev2()):F2} deg sep={Sep2():F4} m propV={_prop.LinearVelocity.Length():F2}");
|
||||
}
|
||||
if (phaseT >= TakeoverDuration)
|
||||
Finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Chase(_hand, _target, dt, _phase == Phase.Reach || _phase == Phase.Hand2Reach ? ReachSpeed : HandMaxSpeed);
|
||||
Chase(_hand2, _target2, dt, _phase == Phase.Reach || _phase == Phase.Hand2Reach ? ReachSpeed : HandMaxSpeed);
|
||||
SteerHand(_hand, _cmd1, dt);
|
||||
SteerHand(_hand2, Quaternion.Identity, dt);
|
||||
}
|
||||
|
||||
private ConeTwistJoint3D MakeWeld(RigidBody3D hand, RigidBody3D prop, Vector3 pivot, float bias)
|
||||
{
|
||||
ConeTwistJoint3D weld = new()
|
||||
{
|
||||
ExcludeNodesFromCollision = true,
|
||||
Position = pivot,
|
||||
NodeA = hand.GetPath(),
|
||||
NodeB = prop.GetPath(),
|
||||
SwingSpan = 0f,
|
||||
TwistSpan = 0f,
|
||||
Bias = bias,
|
||||
};
|
||||
AddChild(weld);
|
||||
return weld;
|
||||
}
|
||||
|
||||
private void TrackRigidWeld(ref float maxRot, ref float maxSep)
|
||||
{
|
||||
maxRot = Mathf.Max(maxRot, RotDev1());
|
||||
maxSep = Mathf.Max(maxSep, Sep1());
|
||||
}
|
||||
|
||||
private void TrackTakeoverWeld()
|
||||
{
|
||||
Quaternion relNow = _hand2.GlobalTransform.Basis.GetRotationQuaternion().Inverse()
|
||||
* _prop.GlobalTransform.Basis.GetRotationQuaternion();
|
||||
_maxTakeoverRot = Mathf.Max(_maxTakeoverRot, DevAngle(relNow, _relRotation2));
|
||||
_maxTakeoverSep = Mathf.Max(_maxTakeoverSep, Sep2());
|
||||
}
|
||||
|
||||
private float RotDev1() => DevAngle(
|
||||
_hand.GlobalTransform.Basis.GetRotationQuaternion().Inverse()
|
||||
* _prop.GlobalTransform.Basis.GetRotationQuaternion(),
|
||||
_relRotation1);
|
||||
|
||||
private float RotDev2() => DevAngle(
|
||||
_hand2.GlobalTransform.Basis.GetRotationQuaternion().Inverse()
|
||||
* _prop.GlobalTransform.Basis.GetRotationQuaternion(),
|
||||
_relRotation2);
|
||||
|
||||
private float Sep1() =>
|
||||
(_prop.GlobalTransform * _propAnchor).DistanceTo(_hand.GlobalTransform * _handAnchor);
|
||||
|
||||
private float Sep2() =>
|
||||
(_prop.GlobalTransform * _propAnchor2).DistanceTo(_hand2.GlobalTransform * _handAnchor2);
|
||||
|
||||
private static float DevAngle(Quaternion relNow, Quaternion rel0)
|
||||
{
|
||||
Quaternion dev = relNow * rel0.Inverse();
|
||||
if (dev.W < 0f)
|
||||
dev = new Quaternion(-dev.X, -dev.Y, -dev.Z, -dev.W);
|
||||
// GetAngle() NaNs when w exceeds 1 by a float epsilon; clamp like WeldSpike does
|
||||
if (float.IsNaN(dev.W))
|
||||
return float.NaN;
|
||||
return 2f * Mathf.Acos(Mathf.Clamp(Mathf.Abs(dev.W), 0f, 1f));
|
||||
}
|
||||
|
||||
private static float DragOffset(float t)
|
||||
{
|
||||
float cruiseSpeed = MoveDistance / (MoveDuration - DragRampTime);
|
||||
if (t <= DragRampTime)
|
||||
return 0.5f * cruiseSpeed / DragRampTime * t * t;
|
||||
if (t <= MoveDuration - DragRampTime)
|
||||
return 0.5f * cruiseSpeed * DragRampTime + cruiseSpeed * (t - DragRampTime);
|
||||
float remaining = MoveDuration - t;
|
||||
return MoveDistance - 0.5f * cruiseSpeed / DragRampTime * remaining * remaining;
|
||||
}
|
||||
|
||||
private static void SteerHand(RigidBody3D hand, Quaternion cmd, float dt)
|
||||
{
|
||||
Quaternion q = cmd * hand.GlobalTransform.Basis.GetRotationQuaternion().Inverse();
|
||||
Vector3 spin = q.GetAxis() * (q.GetAngle() / dt);
|
||||
if (spin.Length() > HandMaxSpin)
|
||||
spin = spin.Normalized() * HandMaxSpin;
|
||||
hand.AngularVelocity = spin;
|
||||
}
|
||||
|
||||
private static void Chase(RigidBody3D body, Node3D target, float dt, float maxSpeed)
|
||||
{
|
||||
Vector3 to = target.GlobalPosition - body.GlobalPosition;
|
||||
Vector3 desired = to / dt;
|
||||
if (desired.Length() > maxSpeed)
|
||||
desired = desired.Normalized() * maxSpeed;
|
||||
Vector3 dv = desired - body.LinearVelocity;
|
||||
Vector3 force = dv / dt * HandMass;
|
||||
if (force.Length() > HandMaxForce)
|
||||
force = force.Normalized() * HandMaxForce;
|
||||
body.LinearVelocity += force / HandMass * dt;
|
||||
}
|
||||
|
||||
private void FinishDrag()
|
||||
{
|
||||
_dragRotOk = _maxDragRot < Mathf.DegToRad(RigidRotLimitDeg) && !float.IsNaN(_maxDragRot);
|
||||
_dragSepOk = _maxDragSep < RigidSepLimit;
|
||||
_dragSpeedOk = _maxDragPropSpeed < MaxSpeed && _maxDragHandSpeed < MaxSpeed;
|
||||
GD.Print($"GRAB: drag rigid — rot dev max {Mathf.RadToDeg(_maxDragRot):F2} deg (limit {RigidRotLimitDeg}), sep max {_maxDragSep:F4} m (limit {RigidSepLimit}) → {(_dragRotOk && _dragSepOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"GRAB: drag speeds — prop max {_maxDragPropSpeed:F2} m/s, hand max {_maxDragHandSpeed:F2} m/s (limit {MaxSpeed}) → {(_dragSpeedOk ? "PASS" : "FAIL")}");
|
||||
_spinFrom = _hand.GlobalTransform.Basis.GetRotationQuaternion();
|
||||
_spinTo = _spinFrom * new Quaternion(Vector3.Up, Mathf.DegToRad(SpinAngleDeg));
|
||||
_maxSpinRot = 0f;
|
||||
_maxSpinSep = 0f;
|
||||
_maxSpinPropSpeed = 0f;
|
||||
_maxSpinHandSpeed = 0f;
|
||||
_phase = Phase.Spin;
|
||||
_phaseStart = _elapsed;
|
||||
_lastLogStep = 0;
|
||||
GD.Print($"GRAB: fast spin starting — {SpinAngleDeg:F0} deg in {SpinDuration:0.#} s around world up"); }
|
||||
|
||||
private void FinishSpin()
|
||||
{
|
||||
_spinRotOk = _maxSpinRot < Mathf.DegToRad(SpinRotLimitDeg) && !float.IsNaN(_maxSpinRot);
|
||||
_spinSepOk = _maxSpinSep < RigidSepLimit;
|
||||
_spinSpeedOk = _maxSpinPropSpeed < MaxSpeed && _maxSpinHandSpeed < MaxSpeed;
|
||||
GD.Print($"GRAB: fast spin rigid — rot dev max {Mathf.RadToDeg(_maxSpinRot):F2} deg (limit {SpinRotLimitDeg}), sep max {_maxSpinSep:F4} m (limit {RigidSepLimit}) → {(_spinRotOk && _spinSepOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"GRAB: fast spin speeds — prop max {_maxSpinPropSpeed:F2} m/s, hand max {_maxSpinHandSpeed:F2} m/s (limit {MaxSpeed}) → {(_spinSpeedOk ? "PASS" : "FAIL")}");
|
||||
_hand.AngularVelocity = Vector3.Zero;
|
||||
_hand2.GlobalTransform = new Transform3D(Basis.Identity, _prop.GlobalPosition + new Vector3(0.45f, 0f, 0f));
|
||||
_hand2.LinearVelocity = Vector3.Zero;
|
||||
_hand2.AngularVelocity = Vector3.Zero;
|
||||
_phase = Phase.Hand2Reach;
|
||||
_phaseStart = _elapsed;
|
||||
_lastLogStep = 0;
|
||||
}
|
||||
|
||||
private void FinishTwoHanded()
|
||||
{
|
||||
bool primaryRigidOk = _maxTwoAnchor1 < RigidSepLimit;
|
||||
bool tugRigidOk = _maxTugAnchor2 < RigidSepLimit;
|
||||
bool followOk = _maxFollowAnchor2 < RigidSepLimit;
|
||||
bool spinOk = _maxTwoSpin < SpinLimit;
|
||||
bool speedOk = _maxTwoPropSpeed < MaxSpeed && _maxTwoHandSpeed < MaxSpeed && _maxTwoHand2Speed < MaxSpeed;
|
||||
GD.Print($"GRAB: two-handed primary rigid — anchor1 max {_maxTwoAnchor1:F4} m (limit {RigidSepLimit}) → {(primaryRigidOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"GRAB: two-handed tug rigid — anchor2 max {_maxTugAnchor2:F4} m (limit {RigidSepLimit}) → {(tugRigidOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"GRAB: two-handed follow — anchor2 max {_maxFollowAnchor2:F4} m (limit {RigidSepLimit}) → {(followOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"GRAB: two-handed spin — prop max {_maxTwoSpin:F1} rad/s (limit {SpinLimit}) → {(spinOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"GRAB: two-handed speed — prop max {_maxTwoPropSpeed:F2} m/s, hands max {Mathf.Max(_maxTwoHandSpeed, _maxTwoHand2Speed):F2} m/s (limit {MaxSpeed}) → {(speedOk ? "PASS" : "FAIL")}");
|
||||
_twoHandedOk = primaryRigidOk && tugRigidOk && followOk && spinOk && speedOk;
|
||||
|
||||
_joint.QueueFree();
|
||||
_joint = null;
|
||||
_takeoverBase = _target2.GlobalPosition;
|
||||
_maxTakeoverRot = 0f;
|
||||
_maxTakeoverSep = 0f;
|
||||
_maxTakeoverPropSpeed = 0f;
|
||||
_maxTakeoverHandSpeed = 0f;
|
||||
_phase = Phase.Takeover;
|
||||
_phaseStart = _elapsed;
|
||||
_lastLogStep = 0;
|
||||
GD.Print($"GRAB: primary released, surviving rigid weld untouched (pivot {_grab2Point})");
|
||||
}
|
||||
|
||||
private void Finish()
|
||||
{
|
||||
_takeoverRotOk = _maxTakeoverRot < Mathf.DegToRad(TakeoverRotLimitDeg) && !float.IsNaN(_maxTakeoverRot);
|
||||
_takeoverSepOk = _maxTakeoverSep < RigidSepLimit;
|
||||
_takeoverSpeedOk = _maxTakeoverPropSpeed < MaxSpeed && _maxTakeoverHandSpeed < MaxSpeed;
|
||||
GD.Print($"GRAB: takeover rigid — rot dev max {Mathf.RadToDeg(_maxTakeoverRot):F2} deg (limit {TakeoverRotLimitDeg}), sep max {_maxTakeoverSep:F4} m (limit {RigidSepLimit}) → {(_takeoverRotOk && _takeoverSepOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"GRAB: takeover speeds — prop max {_maxTakeoverPropSpeed:F2} m/s, hand2 max {_maxTakeoverHandSpeed:F2} m/s (limit {MaxSpeed}) → {(_takeoverSpeedOk ? "PASS" : "FAIL")}");
|
||||
bool overall = _dragRotOk && _dragSepOk && _dragSpeedOk &&
|
||||
_spinRotOk && _spinSepOk && _spinSpeedOk &&
|
||||
_twoHandedOk &&
|
||||
_takeoverRotOk && _takeoverSepOk && _takeoverSpeedOk;
|
||||
GD.Print($"GRAB: overall {(overall ? "PASS — solver-owned rigid weld grab (v5.3) functional on Box3D" : "CHECK — behavior differs, inspect above")}");
|
||||
GetTree().Quit();
|
||||
}
|
||||
|
||||
private void Fail(string reason)
|
||||
{
|
||||
GD.Print($"GRAB: overall CHECK — {reason}");
|
||||
GetTree().Quit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://c7ala2jq058dh
|
||||
@@ -0,0 +1,244 @@
|
||||
using Godot;
|
||||
|
||||
public partial class Locomotion : Node
|
||||
{
|
||||
private const float GripThreshold = 0.6f;
|
||||
private const float TriggerThreshold = 0.6f;
|
||||
private const float RayLength = 50f;
|
||||
private const float LaserMinDistance = 0.35f;
|
||||
private const float MarkerNearDistance = 1.2f;
|
||||
|
||||
private XROrigin3D _origin;
|
||||
private XRCamera3D _camera;
|
||||
private XRController3D _left;
|
||||
private XRController3D _right;
|
||||
private Marker3D _spawn;
|
||||
private ColorRect _fade;
|
||||
private Main _main;
|
||||
|
||||
private Node3D _leftLaser;
|
||||
private Node3D _rightLaser;
|
||||
|
||||
private bool _placedAtSpawn;
|
||||
private bool _teleporting;
|
||||
private bool _loggedRecentered;
|
||||
private TeleportMarker _highlighted;
|
||||
|
||||
private bool _leftTrigger;
|
||||
private bool _rightTrigger;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_main = GetNode<Main>("..");
|
||||
_origin = GetNode<XROrigin3D>("../XROrigin3D");
|
||||
_camera = GetNode<XRCamera3D>("../XROrigin3D/XRCamera3D");
|
||||
_left = GetNode<XRController3D>("../XROrigin3D/LeftHand");
|
||||
_right = GetNode<XRController3D>("../XROrigin3D/RightHand");
|
||||
_spawn = GetNode<Marker3D>("../SpawnPoint");
|
||||
_fade = GetNode<ColorRect>("../FadeLayer/Fade");
|
||||
_main.FocusGained += OnFocusGained;
|
||||
_main.PoseRecentered += OnPoseRecentered;
|
||||
_leftLaser = MakeLaser(_left);
|
||||
_rightLaser = MakeLaser(_right);
|
||||
MakeTip(_left);
|
||||
MakeTip(_right);
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (_teleporting)
|
||||
return;
|
||||
|
||||
UpdateHighlight();
|
||||
UpdateNearMarkers();
|
||||
UpdatePointer(_left, _leftLaser);
|
||||
UpdatePointer(_right, _rightLaser);
|
||||
|
||||
CheckTeleport(_left, ref _leftTrigger);
|
||||
CheckTeleport(_right, ref _rightTrigger);
|
||||
}
|
||||
|
||||
private static Node3D MakeLaser(XRController3D controller)
|
||||
{
|
||||
StandardMaterial3D material = new()
|
||||
{
|
||||
AlbedoColor = new Color(1f, 0.55f, 0.15f),
|
||||
EmissionEnabled = true,
|
||||
Emission = new Color(1f, 0.55f, 0.15f),
|
||||
EmissionEnergyMultiplier = 2.0f,
|
||||
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
|
||||
};
|
||||
MeshInstance3D beam = new()
|
||||
{
|
||||
Mesh = new CylinderMesh { TopRadius = 0.0035f, BottomRadius = 0.0035f, Height = 1f },
|
||||
Position = new Vector3(0, 0.5f, 0),
|
||||
CastShadow = GeometryInstance3D.ShadowCastingSetting.Off,
|
||||
};
|
||||
beam.SetSurfaceOverrideMaterial(0, material);
|
||||
Node3D pivot = new() { Visible = false };
|
||||
pivot.AddChild(beam);
|
||||
Basis rot = Basis.FromEuler(new Vector3(-Mathf.Pi / 2f, 0, 0));
|
||||
pivot.Transform = new Transform3D(rot, new Vector3(0, 0, -0.12f));
|
||||
controller.AddChild(pivot);
|
||||
return pivot;
|
||||
}
|
||||
|
||||
private static void MakeTip(XRController3D controller)
|
||||
{
|
||||
StandardMaterial3D material = new()
|
||||
{
|
||||
AlbedoColor = new Color(1f, 0.9f, 0.6f),
|
||||
EmissionEnabled = true,
|
||||
Emission = new Color(1f, 0.9f, 0.6f),
|
||||
EmissionEnergyMultiplier = 1.5f,
|
||||
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
|
||||
};
|
||||
MeshInstance3D tip = new()
|
||||
{
|
||||
Mesh = new CylinderMesh { TopRadius = 0f, BottomRadius = 0.009f, Height = 0.09f },
|
||||
CastShadow = GeometryInstance3D.ShadowCastingSetting.Off,
|
||||
};
|
||||
tip.SetSurfaceOverrideMaterial(0, material);
|
||||
Basis rot = Basis.FromEuler(new Vector3(-Mathf.Pi / 2f, 0, 0));
|
||||
tip.Transform = new Transform3D(rot, rot * new Vector3(0, 0.045f, 0));
|
||||
controller.AddChild(tip);
|
||||
}
|
||||
|
||||
private void UpdateNearMarkers()
|
||||
{
|
||||
Vector3 head = _camera.GlobalPosition;
|
||||
foreach (Node node in GetTree().GetNodesInGroup("teleport_marker"))
|
||||
{
|
||||
if (node is not TeleportMarker marker)
|
||||
continue;
|
||||
Vector3 d = marker.GlobalPosition - head;
|
||||
d.Y = 0f;
|
||||
marker.SetNear(d.Length() < MarkerNearDistance);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePointer(XRController3D controller, Node3D laser)
|
||||
{
|
||||
Vector3 from = controller.GlobalPosition;
|
||||
Vector3 dir = -controller.GlobalTransform.Basis.Z;
|
||||
|
||||
var query = PhysicsRayQueryParameters3D.Create(from, from + dir * RayLength);
|
||||
var hit = GetViewport().GetWorld3D().DirectSpaceState.IntersectRay(query);
|
||||
|
||||
bool show = false;
|
||||
float dist = 0f;
|
||||
if (hit.TryGetValue("collider", out Variant collider) && collider.As<Node>() is Node node)
|
||||
{
|
||||
bool interactive;
|
||||
if (node.GetParent() is TeleportMarker marker)
|
||||
interactive = !marker.IsNear;
|
||||
else
|
||||
interactive = WorldPanel.IsPanelCollider(node);
|
||||
if (interactive && hit.TryGetValue("position", out Variant posVariant))
|
||||
{
|
||||
dist = from.DistanceTo(posVariant.As<Vector3>());
|
||||
show = dist > LaserMinDistance;
|
||||
}
|
||||
}
|
||||
|
||||
laser.Visible = show;
|
||||
if (show)
|
||||
laser.Scale = new Vector3(1, dist, 1);
|
||||
}
|
||||
|
||||
private void CheckTeleport(XRController3D controller, ref bool wasPressed)
|
||||
{
|
||||
bool pressed = controller.GetFloat("trigger") > TriggerThreshold;
|
||||
if (pressed && !wasPressed && !WorldPanel.IsBlocking(controller))
|
||||
{
|
||||
TeleportMarker marker = AimMarker(controller);
|
||||
if (marker is { IsNear: false })
|
||||
StartTeleport(marker);
|
||||
}
|
||||
wasPressed = pressed;
|
||||
}
|
||||
|
||||
private void UpdateHighlight()
|
||||
{
|
||||
TeleportMarker marker = AimMarker(_left) ?? AimMarker(_right);
|
||||
if (marker is { IsNear: true })
|
||||
marker = null;
|
||||
if (marker == _highlighted)
|
||||
return;
|
||||
_highlighted?.SetHighlighted(false);
|
||||
_highlighted = marker;
|
||||
_highlighted?.SetHighlighted(true);
|
||||
}
|
||||
|
||||
private TeleportMarker AimMarker(XRController3D controller)
|
||||
{
|
||||
Vector3 from = controller.GlobalPosition;
|
||||
Vector3 to = from - controller.GlobalTransform.Basis.Z * RayLength;
|
||||
var query = PhysicsRayQueryParameters3D.Create(from, to);
|
||||
var hit = GetViewport().GetWorld3D().DirectSpaceState.IntersectRay(query);
|
||||
if (hit.TryGetValue("collider", out Variant collider) && collider.As<Node>()?.GetParent() is TeleportMarker marker)
|
||||
return marker;
|
||||
return null;
|
||||
}
|
||||
|
||||
private void StartTeleport(TeleportMarker marker)
|
||||
{
|
||||
_teleporting = true;
|
||||
Vector3 target = marker.GlobalPosition;
|
||||
float yaw = ForwardYaw(marker.GlobalTransform);
|
||||
|
||||
var tween = CreateTween();
|
||||
tween.TweenProperty(_fade, "color:a", 1.0f, 0.12);
|
||||
tween.TweenCallback(Callable.From(() =>
|
||||
{
|
||||
PlacePlayer(target, yaw);
|
||||
GD.Print($"Locomotion: teleported to marker '{marker.MarkerLabel}'");
|
||||
}));
|
||||
tween.TweenProperty(_fade, "color:a", 0.0f, 0.25);
|
||||
tween.TweenCallback(Callable.From(() => _teleporting = false));
|
||||
}
|
||||
|
||||
public void ReturnToSpawn()
|
||||
{
|
||||
MoveToSpawn();
|
||||
GD.Print("Locomotion: returned to spawn point");
|
||||
}
|
||||
|
||||
private void OnFocusGained()
|
||||
{
|
||||
if (_placedAtSpawn)
|
||||
return;
|
||||
_placedAtSpawn = true;
|
||||
MoveToSpawn();
|
||||
GD.Print("Locomotion: player placed at spawn point");
|
||||
}
|
||||
|
||||
private void OnPoseRecentered()
|
||||
{
|
||||
if (_loggedRecentered)
|
||||
return;
|
||||
_loggedRecentered = true;
|
||||
GD.Print("Locomotion: pose_recentered received — WiVRN fires this continuously (~90 Hz), placement stays unchanged and further occurrences are not logged");
|
||||
}
|
||||
|
||||
private void MoveToSpawn() => PlacePlayer(_spawn.GlobalPosition, ForwardYaw(_spawn.GlobalTransform));
|
||||
|
||||
private void PlacePlayer(Vector3 targetXz, float yaw)
|
||||
{
|
||||
Transform3D h = _camera.GlobalTransform;
|
||||
Transform3D hLocal = _origin.GlobalTransform.AffineInverse() * h;
|
||||
float hmdYaw = h.Basis.GetEuler().Y;
|
||||
Basis basis = Basis.FromEuler(new Vector3(0, yaw - hmdYaw, 0)) * h.Basis;
|
||||
Transform3D desired = new Transform3D(basis, new Vector3(targetXz.X, h.Origin.Y, targetXz.Z));
|
||||
_origin.GlobalTransform = desired * hLocal.AffineInverse();
|
||||
}
|
||||
|
||||
private static float ForwardYaw(Transform3D transform)
|
||||
{
|
||||
Vector3 fwd = -transform.Basis.Z;
|
||||
fwd.Y = 0f;
|
||||
if (fwd.LengthSquared() < 0.0001f)
|
||||
return 0f;
|
||||
return Mathf.Atan2(-fwd.X, -fwd.Z);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://be6cxrxsc202g
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
using Godot;
|
||||
|
||||
public partial class Main : Node3D
|
||||
{
|
||||
[Signal]
|
||||
public delegate void FocusLostEventHandler();
|
||||
|
||||
[Signal]
|
||||
public delegate void FocusGainedEventHandler();
|
||||
|
||||
[Signal]
|
||||
public delegate void PoseRecenteredEventHandler();
|
||||
|
||||
[Export]
|
||||
public int MaximumRefreshRate { get; set; } = 90;
|
||||
|
||||
private OpenXRInterface _xrInterface;
|
||||
private bool _xrIsFocused;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_xrInterface = (OpenXRInterface)XRServer.FindInterface("OpenXR");
|
||||
if (_xrInterface != null && _xrInterface.IsInitialized())
|
||||
{
|
||||
GD.Print("OpenXR instantiated successfully.");
|
||||
Viewport vp = GetViewport();
|
||||
vp.UseXR = true;
|
||||
DisplayServer.WindowSetVsyncMode(DisplayServer.VSyncMode.Disabled);
|
||||
if (RenderingServer.GetRenderingDevice() != null)
|
||||
vp.VrsMode = Viewport.VrsModeEnum.XR;
|
||||
|
||||
_xrInterface.SessionBegun += OnSessionBegun;
|
||||
_xrInterface.SessionVisible += OnSessionVisible;
|
||||
_xrInterface.SessionFocussed += OnSessionFocussed;
|
||||
_xrInterface.SessionStopping += OnSessionStopping;
|
||||
_xrInterface.PoseRecentered += OnPoseRecentered;
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.PushWarning("OpenXR not instantiated, running in flat preview mode. Check headset and XR runtime.");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSessionBegun()
|
||||
{
|
||||
float currentRefreshRate = _xrInterface.DisplayRefreshRate;
|
||||
GD.Print(currentRefreshRate > 0.0f
|
||||
? $"OpenXR: refresh rate reported as {currentRefreshRate}"
|
||||
: "OpenXR: no refresh rate given by XR runtime");
|
||||
|
||||
float newRate = currentRefreshRate;
|
||||
var availableRates = _xrInterface.GetAvailableDisplayRefreshRates();
|
||||
if (availableRates.Count == 0)
|
||||
GD.Print("OpenXR: target does not support refresh rate extension");
|
||||
else if (availableRates.Count == 1)
|
||||
newRate = (float)availableRates[0];
|
||||
else
|
||||
{
|
||||
foreach (float rate in availableRates)
|
||||
{
|
||||
if (rate > newRate && rate <= MaximumRefreshRate)
|
||||
newRate = rate;
|
||||
}
|
||||
}
|
||||
|
||||
if (currentRefreshRate != newRate)
|
||||
{
|
||||
GD.Print($"OpenXR: setting refresh rate to {newRate}");
|
||||
_xrInterface.DisplayRefreshRate = newRate;
|
||||
currentRefreshRate = newRate;
|
||||
}
|
||||
|
||||
Engine.PhysicsTicksPerSecond = (int)currentRefreshRate;
|
||||
}
|
||||
|
||||
private void OnSessionVisible()
|
||||
{
|
||||
if (_xrIsFocused)
|
||||
{
|
||||
GD.Print("OpenXR lost focus");
|
||||
_xrIsFocused = false;
|
||||
GetTree().Paused = true;
|
||||
EmitSignal(SignalName.FocusLost);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSessionFocussed()
|
||||
{
|
||||
GD.Print("OpenXR gained focus");
|
||||
_xrIsFocused = true;
|
||||
GetTree().Paused = false;
|
||||
EmitSignal(SignalName.FocusGained);
|
||||
}
|
||||
|
||||
private void OnSessionStopping()
|
||||
{
|
||||
GD.Print("OpenXR is stopping");
|
||||
}
|
||||
|
||||
private void OnPoseRecentered()
|
||||
{
|
||||
EmitSignal(SignalName.PoseRecentered);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dsy2jm1gdcao0
|
||||
@@ -0,0 +1,138 @@
|
||||
using Godot;
|
||||
|
||||
public partial class PhysicsHands : Node
|
||||
{
|
||||
private const float HandMass = 0.4f;
|
||||
public const float HandMaxForce = 90f;
|
||||
private const float HandMaxSpin = 120f;
|
||||
public const float SnapDistance = 2f;
|
||||
private const float GhostLagDistance = 0.1f;
|
||||
private const float StuckDistance = 0.2f;
|
||||
private const float StuckTime = 1.0f;
|
||||
private static readonly Vector3 HandSize = new(0.035f, 0.09f, 0.12f);
|
||||
|
||||
private static PhysicsHands _instance;
|
||||
|
||||
private XRController3D _left;
|
||||
private XRController3D _right;
|
||||
private readonly RigidBody3D[] _bodies = new RigidBody3D[2];
|
||||
private readonly MeshInstance3D[] _ghosts = new MeshInstance3D[2];
|
||||
private readonly float[] _stuckTimer = new float[2];
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_instance = this;
|
||||
_left = GetNode<XRController3D>("../XROrigin3D/LeftHand");
|
||||
_right = GetNode<XRController3D>("../XROrigin3D/RightHand");
|
||||
_bodies[0] = MakeHand("LeftHandBody", new Color(0.9f, 0.3f, 0.35f));
|
||||
_bodies[1] = MakeHand("RightHandBody", new Color(0.3f, 0.5f, 0.9f));
|
||||
_ghosts[0] = MakeGhost();
|
||||
_ghosts[1] = MakeGhost();
|
||||
GD.Print("PhysicsHands: physical hand bodies spawned");
|
||||
}
|
||||
|
||||
private MeshInstance3D MakeGhost()
|
||||
{
|
||||
MeshInstance3D ghost = new()
|
||||
{
|
||||
Mesh = new BoxMesh { Size = HandSize },
|
||||
Visible = false,
|
||||
CastShadow = GeometryInstance3D.ShadowCastingSetting.Off,
|
||||
};
|
||||
ghost.SetSurfaceOverrideMaterial(0, new StandardMaterial3D
|
||||
{
|
||||
AlbedoColor = new Color(1f, 1f, 1f, 0.3f),
|
||||
Transparency = BaseMaterial3D.TransparencyEnum.Alpha,
|
||||
ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded,
|
||||
});
|
||||
AddChild(ghost);
|
||||
return ghost;
|
||||
}
|
||||
|
||||
public static RigidBody3D GetBody(XRController3D controller)
|
||||
{
|
||||
if (_instance == null)
|
||||
return null;
|
||||
return _instance._left == controller ? _instance._bodies[0] : _instance._bodies[1];
|
||||
}
|
||||
|
||||
private RigidBody3D MakeHand(string name, Color color)
|
||||
{
|
||||
RigidBody3D body = new()
|
||||
{
|
||||
Name = name,
|
||||
Mass = HandMass,
|
||||
GravityScale = 0f,
|
||||
ContinuousCd = true,
|
||||
ContactMonitor = true,
|
||||
MaxContactsReported = 8,
|
||||
CollisionLayer = 1,
|
||||
CollisionMask = 1,
|
||||
};
|
||||
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = HandSize } });
|
||||
MeshInstance3D mesh = new() { Mesh = new BoxMesh { Size = HandSize } };
|
||||
StandardMaterial3D material = new() { AlbedoColor = color, Roughness = 0.5f };
|
||||
mesh.SetSurfaceOverrideMaterial(0, material);
|
||||
body.AddChild(mesh);
|
||||
AddChild(body);
|
||||
return body;
|
||||
}
|
||||
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
Chase(_left, _bodies[0], (float)delta);
|
||||
Chase(_right, _bodies[1], (float)delta);
|
||||
}
|
||||
|
||||
private void Chase(XRController3D controller, RigidBody3D body, float delta)
|
||||
{
|
||||
if (body == null)
|
||||
return;
|
||||
int hand = _left == controller ? 0 : 1;
|
||||
Vector3 target = controller.GlobalPosition;
|
||||
|
||||
bool lagging = body.GlobalPosition.DistanceTo(target) > GhostLagDistance;
|
||||
if (_ghosts[hand] != null)
|
||||
{
|
||||
_ghosts[hand].Visible = lagging;
|
||||
if (lagging)
|
||||
_ghosts[hand].GlobalTransform = controller.GlobalTransform;
|
||||
}
|
||||
|
||||
if (!PropGrabber.IsGrabbing(controller))
|
||||
{
|
||||
if (body.GlobalPosition.DistanceTo(target) > StuckDistance)
|
||||
_stuckTimer[hand] += delta;
|
||||
else
|
||||
_stuckTimer[hand] = 0f;
|
||||
if (_stuckTimer[hand] > StuckTime)
|
||||
{
|
||||
_stuckTimer[hand] = 0f;
|
||||
body.GlobalTransform = controller.GlobalTransform;
|
||||
body.LinearVelocity = Vector3.Zero;
|
||||
GD.Print("PhysicsHands: hand stuck, snapped back");
|
||||
return;
|
||||
}
|
||||
|
||||
if (body.GlobalPosition.DistanceTo(target) > SnapDistance)
|
||||
{
|
||||
body.GlobalTransform = controller.GlobalTransform;
|
||||
body.LinearVelocity = Vector3.Zero;
|
||||
return;
|
||||
}
|
||||
}
|
||||
Vector3 desiredVelocity = (target - body.GlobalPosition) / delta;
|
||||
Vector3 dv = desiredVelocity - body.LinearVelocity;
|
||||
Vector3 force = dv / delta * body.Mass;
|
||||
if (force.Length() > HandMaxForce)
|
||||
force = force.Normalized() * HandMaxForce;
|
||||
body.LinearVelocity += force / body.Mass * delta;
|
||||
|
||||
Basis deltaBasis = controller.GlobalTransform.Basis * body.GlobalTransform.Basis.Inverse();
|
||||
Quaternion q = deltaBasis.GetRotationQuaternion();
|
||||
Vector3 spin = q.GetAxis() * (q.GetAngle() / delta);
|
||||
if (spin.Length() > HandMaxSpin)
|
||||
spin = spin.Normalized() * HandMaxSpin;
|
||||
body.AngularVelocity = spin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://b4mgat3v21u30
|
||||
@@ -0,0 +1,56 @@
|
||||
using Godot;
|
||||
|
||||
public partial class PhysicsSpike : Node3D
|
||||
{
|
||||
private RigidBody3D _ball;
|
||||
private RigidBody3D _chaser;
|
||||
private Node3D _chaseTarget;
|
||||
private float _elapsed;
|
||||
private bool _ballTouched;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
StaticBody3D floor = new() { Position = new Vector3(0, -0.5f, 0) };
|
||||
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(20, 1, 20) } });
|
||||
AddChild(floor);
|
||||
|
||||
_ball = new RigidBody3D { Position = new Vector3(0, 2, 0), ContactMonitor = true, MaxContactsReported = 4 };
|
||||
_ball.AddChild(new CollisionShape3D { Shape = new SphereShape3D { Radius = 0.25f } });
|
||||
_ball.BodyEntered += _ => _ballTouched = true;
|
||||
AddChild(_ball);
|
||||
|
||||
StaticBody3D wall = new() { Position = new Vector3(5, 2, 0) };
|
||||
wall.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.5f, 4, 4) } });
|
||||
AddChild(wall);
|
||||
|
||||
_chaser = new RigidBody3D { Position = new Vector3(0, 1, 0), GravityScale = 0f };
|
||||
_chaser.AddChild(new CollisionShape3D { Shape = new SphereShape3D { Radius = 0.25f } });
|
||||
AddChild(_chaser);
|
||||
|
||||
_chaseTarget = new Node3D();
|
||||
AddChild(_chaseTarget);
|
||||
GD.Print("SPIKE: physics spike scene ready");
|
||||
}
|
||||
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
_elapsed += (float)delta;
|
||||
|
||||
_chaseTarget.GlobalPosition = new Vector3(Mathf.Min(8f, _elapsed * 2f), 1, 0);
|
||||
Vector3 to = _chaseTarget.GlobalPosition - _chaser.GlobalPosition;
|
||||
Vector3 desired = to / (float)delta;
|
||||
if (desired.Length() > 3f)
|
||||
desired = desired.Normalized() * 3f;
|
||||
_chaser.LinearVelocity = desired;
|
||||
|
||||
if (_elapsed < 2.5f)
|
||||
return;
|
||||
|
||||
bool dropOk = _ballTouched && _ball.GlobalPosition.Y > 0.2f && _ball.GlobalPosition.Y < 0.35f;
|
||||
bool chaseOk = _chaser.GlobalPosition.X > 4.4f && _chaser.GlobalPosition.X < 5.05f;
|
||||
GD.Print($"SPIKE: drop test — ball y={_ball.GlobalPosition.Y:F3} (expected ~0.25), contact={_ballTouched} → {(dropOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"SPIKE: chase test — chaser x={_chaser.GlobalPosition.X:F3} (wall face 4.75, body radius 0.25), vel={_chaser.LinearVelocity.Length():F2} → {(chaseOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"SPIKE: overall {(dropOk && chaseOk ? "PASS — Box3D functional from C#" : "CHECK — behavior differs, inspect above")}");
|
||||
GetTree().Quit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://pmjecyvvet5c
|
||||
@@ -0,0 +1,225 @@
|
||||
using Godot;
|
||||
|
||||
public partial class PropGrabber : Node
|
||||
{
|
||||
private const float GripThreshold = 0.6f;
|
||||
private const float PalmGrabOffset = 0.045f;
|
||||
private const float GrabRadius = 0.16f;
|
||||
private const float CenterGrabRadius = 0.12f;
|
||||
private const float RigidWeldBias = 0f;
|
||||
private const float BreakDistance = 0.25f;
|
||||
private const float BreakTime = 0.25f;
|
||||
private const float BreakDistanceBoth = 0.4f;
|
||||
private const float GripRipDistance = 0.8f;
|
||||
|
||||
private static PropGrabber _instance;
|
||||
|
||||
private XRController3D _left;
|
||||
private XRController3D _right;
|
||||
|
||||
private readonly RigidBody3D[] _held = new RigidBody3D[2];
|
||||
private readonly ConeTwistJoint3D[] _joints = new ConeTwistJoint3D[2];
|
||||
private readonly Vector3[] _localAnchor = new Vector3[2];
|
||||
private readonly uint[] _grabSeq = new uint[2];
|
||||
private readonly float[] _breakTimer = new float[2];
|
||||
private readonly bool[] _gripPrev = new bool[2];
|
||||
private uint _grabCount;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_instance = this;
|
||||
_left = GetNode<XRController3D>("../XROrigin3D/LeftHand");
|
||||
_right = GetNode<XRController3D>("../XROrigin3D/RightHand");
|
||||
}
|
||||
|
||||
public static bool IsGrabbing(XRController3D controller) =>
|
||||
_instance != null &&
|
||||
((_instance._left == controller && _instance._held[0] != null) ||
|
||||
(_instance._right == controller && _instance._held[1] != null));
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
UpdateHand(_left, 0);
|
||||
UpdateHand(_right, 1);
|
||||
}
|
||||
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
float dt = (float)delta;
|
||||
HoldUpdate(_left, 0, dt);
|
||||
HoldUpdate(_right, 1, dt);
|
||||
}
|
||||
|
||||
private void UpdateHand(XRController3D controller, int hand)
|
||||
{
|
||||
bool grip = controller.GetFloat("grip") > GripThreshold;
|
||||
int other = 1 - hand;
|
||||
|
||||
if (grip && !_gripPrev[hand] && _held[hand] == null)
|
||||
{
|
||||
RigidBody3D prop = FindProp(controller, hand);
|
||||
RigidBody3D handBody = PhysicsHands.GetBody(controller);
|
||||
if (prop != null && handBody != null)
|
||||
{
|
||||
_held[hand] = prop;
|
||||
_grabSeq[hand] = _grabCount++;
|
||||
Vector3 pivot = PalmPoint(handBody, hand);
|
||||
_joints[hand] = MakeWeld(handBody, prop, pivot, RigidWeldBias);
|
||||
_localAnchor[hand] = prop.GlobalTransform.AffineInverse() * pivot;
|
||||
GD.Print($"PropGrabber: grabbed '{prop.Name}' ({prop.Mass:0.###} kg, {(hand == 0 ? "left" : "right")}{(_held[other] == prop ? ", two-handed" : "")})");
|
||||
}
|
||||
}
|
||||
|
||||
if (!grip && _held[hand] != null)
|
||||
{
|
||||
RigidBody3D prop = _held[hand];
|
||||
_held[hand] = null;
|
||||
DestroyJoint(hand);
|
||||
if (IsInstanceValid(prop))
|
||||
GD.Print($"PropGrabber: released '{prop.Name}' at {prop.LinearVelocity.Length():F2} m/s");
|
||||
}
|
||||
_gripPrev[hand] = grip;
|
||||
}
|
||||
|
||||
private void HoldUpdate(XRController3D controller, int hand, float delta)
|
||||
{
|
||||
RigidBody3D body = _held[hand];
|
||||
if (body == null)
|
||||
return;
|
||||
if (!IsInstanceValid(body))
|
||||
{
|
||||
_held[hand] = null;
|
||||
DestroyJoint(hand);
|
||||
return;
|
||||
}
|
||||
|
||||
RigidBody3D handBody = PhysicsHands.GetBody(controller);
|
||||
ConeTwistJoint3D joint = _joints[hand];
|
||||
if (handBody == null || !IsInstanceValid(handBody) || joint == null || !IsInstanceValid(joint))
|
||||
{
|
||||
_held[hand] = null;
|
||||
DestroyJoint(hand);
|
||||
return;
|
||||
}
|
||||
|
||||
if (handBody.GlobalPosition.DistanceTo(controller.GlobalPosition) > PhysicsHands.SnapDistance)
|
||||
{
|
||||
BreakGrip(hand, body, "grip dragged off");
|
||||
return;
|
||||
}
|
||||
|
||||
float separation = (body.GlobalTransform * _localAnchor[hand]).DistanceTo(PalmPoint(handBody, hand));
|
||||
|
||||
int other = 1 - hand;
|
||||
bool twoHanded = _held[other] == body && IsInstanceValid(_held[other]) &&
|
||||
_joints[other] != null && IsInstanceValid(_joints[other]);
|
||||
if (twoHanded)
|
||||
{
|
||||
RigidBody3D otherHandBody = PhysicsHands.GetBody(other == 0 ? _left : _right);
|
||||
float otherSeparation = otherHandBody == null
|
||||
? 0f
|
||||
: (body.GlobalTransform * _localAnchor[other]).DistanceTo(PalmPoint(otherHandBody, other));
|
||||
if (separation > BreakDistanceBoth && otherSeparation > BreakDistanceBoth)
|
||||
{
|
||||
BreakGrip(hand, body, $"both hands stretched ({separation:0.###} m)");
|
||||
BreakGrip(other, body, "both hands stretched");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (separation > GripRipDistance)
|
||||
{
|
||||
BreakGrip(hand, body, $"ripped ({separation:0.###} m)");
|
||||
return;
|
||||
}
|
||||
|
||||
if (separation > BreakDistance)
|
||||
_breakTimer[hand] += delta;
|
||||
else
|
||||
_breakTimer[hand] = 0f;
|
||||
if (_breakTimer[hand] > BreakTime)
|
||||
{
|
||||
BreakGrip(hand, body, $"stretched ({separation:0.###} m for {BreakTime:0.#} s)");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private int PrimaryHand(RigidBody3D body)
|
||||
{
|
||||
int primary = -1;
|
||||
for (int hand = 0; hand < 2; hand++)
|
||||
{
|
||||
if (_held[hand] == body && (primary == -1 || _grabSeq[hand] < _grabSeq[primary]))
|
||||
primary = hand;
|
||||
}
|
||||
return primary;
|
||||
}
|
||||
|
||||
private ConeTwistJoint3D MakeWeld(RigidBody3D handBody, RigidBody3D prop, Vector3 pivot, float bias)
|
||||
{
|
||||
ConeTwistJoint3D weld = new()
|
||||
{
|
||||
ExcludeNodesFromCollision = true,
|
||||
Position = pivot,
|
||||
NodeA = handBody.GetPath(),
|
||||
NodeB = prop.GetPath(),
|
||||
SwingSpan = 0f,
|
||||
TwistSpan = 0f,
|
||||
Bias = bias,
|
||||
};
|
||||
AddChild(weld);
|
||||
return weld;
|
||||
}
|
||||
|
||||
private void BreakGrip(int hand, RigidBody3D body, string reason)
|
||||
{
|
||||
if (_held[hand] != body)
|
||||
return;
|
||||
_held[hand] = null;
|
||||
DestroyJoint(hand);
|
||||
GD.Print($"PropGrabber: grip broke on '{body.Name}' ({reason})");
|
||||
}
|
||||
|
||||
private void DestroyJoint(int hand)
|
||||
{
|
||||
if (_joints[hand] != null && IsInstanceValid(_joints[hand]))
|
||||
_joints[hand].QueueFree();
|
||||
_joints[hand] = null;
|
||||
_breakTimer[hand] = 0f;
|
||||
}
|
||||
|
||||
private static Vector3 PalmPoint(RigidBody3D handBody, int hand) =>
|
||||
handBody.GlobalPosition + handBody.GlobalTransform.Basis * new Vector3(PalmOffset(hand), 0f, 0f);
|
||||
|
||||
private static float PalmOffset(int hand) => hand == 0 ? PalmGrabOffset : -PalmGrabOffset;
|
||||
|
||||
private RigidBody3D FindProp(XRController3D controller, int hand)
|
||||
{
|
||||
RigidBody3D handBody = PhysicsHands.GetBody(controller);
|
||||
if (handBody == null)
|
||||
return null;
|
||||
Vector3 palm = PalmPoint(handBody, hand);
|
||||
RigidBody3D nearest = NearestProp(palm, GrabRadius);
|
||||
if (nearest != null)
|
||||
return nearest;
|
||||
return NearestProp(handBody.GlobalPosition, CenterGrabRadius);
|
||||
}
|
||||
|
||||
private RigidBody3D NearestProp(Vector3 point, float radius)
|
||||
{
|
||||
RigidBody3D nearest = null;
|
||||
float nearestDist = radius;
|
||||
foreach (Node node in GetTree().GetNodesInGroup("prop"))
|
||||
{
|
||||
if (node is not RigidBody3D prop)
|
||||
continue;
|
||||
float dist = prop.GlobalPosition.DistanceTo(point);
|
||||
if (dist < nearestDist)
|
||||
{
|
||||
nearestDist = dist;
|
||||
nearest = prop;
|
||||
}
|
||||
}
|
||||
return nearest;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://70m2rsbntlm4
|
||||
@@ -0,0 +1,141 @@
|
||||
using Godot;
|
||||
|
||||
public partial class PropShop : Node3D
|
||||
{
|
||||
private struct PropSpec
|
||||
{
|
||||
public string Name;
|
||||
public float Radius;
|
||||
public Vector3 Box;
|
||||
public float Mass;
|
||||
public float Bounce;
|
||||
public float Friction;
|
||||
public Color Color;
|
||||
public Vector3 Position;
|
||||
}
|
||||
|
||||
private static readonly PropSpec[] Specs =
|
||||
{
|
||||
new() { Name = "Ping pong · 2.7 g", Radius = 0.02f, Mass = 0.0027f, Bounce = 0.9f, Friction = 0.3f, Color = new(1f, 1f, 1f), Position = new(-0.5f, 0.78f, -0.55f) },
|
||||
new() { Name = "Bouncy ball · 50 g", Radius = 0.03f, Mass = 0.05f, Bounce = 0.85f, Friction = 0.4f, Color = new(1f, 0.4f, 0.7f), Position = new(-0.3f, 0.79f, -0.45f) },
|
||||
new() { Name = "Apple · 150 g", Radius = 0.045f, Mass = 0.15f, Bounce = 0.3f, Friction = 0.6f, Color = new(0.85f, 0.15f, 0.1f), Position = new(-0.08f, 0.81f, -0.55f) },
|
||||
new() { Name = "Crate · 2 kg", Box = new(0.16f, 0.16f, 0.16f), Mass = 2f, Bounce = 0.1f, Friction = 0.8f, Color = new(0.7f, 0.5f, 0.25f), Position = new(0.18f, 0.84f, -0.5f) },
|
||||
new() { Name = "Brick · 3.5 kg", Box = new(0.2f, 0.06f, 0.095f), Mass = 3.5f, Bounce = 0.05f, Friction = 0.9f, Color = new(0.6f, 0.3f, 0.2f), Position = new(-0.32f, 0.79f, -0.25f) },
|
||||
new() { Name = "Bowling ball · 7.2 kg", Radius = 0.11f, Mass = 7.2f, Bounce = 0.1f, Friction = 0.5f, Color = new(0.1f, 0.2f, 0.5f), Position = new(0.05f, 0.87f, -0.25f) },
|
||||
new() { Name = "Anvil · 50 kg", Box = new(0.28f, 0.16f, 0.13f), Mass = 50f, Bounce = 0f, Friction = 0.95f, Color = new(0.25f, 0.25f, 0.28f), Position = new(0.38f, 0.84f, -0.35f) },
|
||||
};
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
BuildTable();
|
||||
foreach (PropSpec spec in Specs)
|
||||
SpawnProp(spec);
|
||||
}
|
||||
|
||||
public void Respawn()
|
||||
{
|
||||
foreach (Node node in GetTree().GetNodesInGroup("prop"))
|
||||
node.QueueFree();
|
||||
foreach (PropSpec spec in Specs)
|
||||
SpawnProp(spec);
|
||||
GD.Print("PropShop: table reset");
|
||||
}
|
||||
|
||||
private void BuildTable()
|
||||
{
|
||||
Color wood = new(0.45f, 0.3f, 0.18f);
|
||||
StaticBody3D table = new();
|
||||
table.AddChild(new CollisionShape3D
|
||||
{
|
||||
Position = new Vector3(0, 0.725f, -0.6f),
|
||||
Shape = new BoxShape3D { Size = new Vector3(1.4f, 0.05f, 0.8f) },
|
||||
});
|
||||
MeshInstance3D top = new()
|
||||
{
|
||||
Position = new Vector3(0, 0.725f, -0.6f),
|
||||
Mesh = new BoxMesh { Size = new Vector3(1.4f, 0.05f, 0.8f) },
|
||||
};
|
||||
top.SetSurfaceOverrideMaterial(0, MakeMaterial(wood));
|
||||
table.AddChild(top);
|
||||
|
||||
Vector3[] legOffsets =
|
||||
{
|
||||
new(-0.62f, 0, 0.32f),
|
||||
new(0.62f, 0, 0.32f),
|
||||
new(-0.62f, 0, -0.32f),
|
||||
new(0.62f, 0, -0.32f),
|
||||
};
|
||||
foreach (Vector3 offset in legOffsets)
|
||||
{
|
||||
Vector3 pos = new(offset.X, 0.3625f, -0.6f + offset.Z);
|
||||
table.AddChild(new CollisionShape3D
|
||||
{
|
||||
Position = pos,
|
||||
Shape = new BoxShape3D { Size = new Vector3(0.06f, 0.725f, 0.06f) },
|
||||
});
|
||||
MeshInstance3D leg = new()
|
||||
{
|
||||
Position = pos,
|
||||
Mesh = new BoxMesh { Size = new Vector3(0.06f, 0.725f, 0.06f) },
|
||||
};
|
||||
leg.SetSurfaceOverrideMaterial(0, MakeMaterial(wood * 0.8f));
|
||||
table.AddChild(leg);
|
||||
}
|
||||
AddChild(table);
|
||||
}
|
||||
|
||||
private void SpawnProp(PropSpec spec)
|
||||
{
|
||||
RigidBody3D body = new()
|
||||
{
|
||||
Name = spec.Name,
|
||||
Mass = spec.Mass,
|
||||
Position = spec.Position,
|
||||
PhysicsMaterialOverride = new PhysicsMaterial { Bounce = spec.Bounce, Friction = spec.Friction },
|
||||
ContinuousCd = spec.Mass < 0.1f,
|
||||
ContactMonitor = true,
|
||||
MaxContactsReported = 4,
|
||||
};
|
||||
body.AddToGroup("prop");
|
||||
|
||||
if (spec.Radius > 0f)
|
||||
{
|
||||
body.AddChild(new CollisionShape3D { Shape = new SphereShape3D { Radius = spec.Radius } });
|
||||
MeshInstance3D mesh = new()
|
||||
{
|
||||
Mesh = new SphereMesh { Radius = spec.Radius, Height = spec.Radius * 2f },
|
||||
};
|
||||
mesh.SetSurfaceOverrideMaterial(0, MakeMaterial(spec.Color));
|
||||
body.AddChild(mesh);
|
||||
}
|
||||
else
|
||||
{
|
||||
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = spec.Box } });
|
||||
MeshInstance3D mesh = new()
|
||||
{
|
||||
Mesh = new BoxMesh { Size = spec.Box },
|
||||
};
|
||||
mesh.SetSurfaceOverrideMaterial(0, MakeMaterial(spec.Color));
|
||||
body.AddChild(mesh);
|
||||
}
|
||||
|
||||
float halfHeight = spec.Radius > 0f ? spec.Radius : spec.Box.Y * 0.5f;
|
||||
body.AddChild(new Label3D
|
||||
{
|
||||
Text = spec.Name,
|
||||
FontSize = 64,
|
||||
PixelSize = 0.0004f,
|
||||
OutlineSize = 8,
|
||||
Billboard = BaseMaterial3D.BillboardModeEnum.Enabled,
|
||||
Position = new Vector3(0, halfHeight + 0.1f, 0),
|
||||
});
|
||||
|
||||
AddChild(body);
|
||||
}
|
||||
|
||||
private static StandardMaterial3D MakeMaterial(Color color) => new()
|
||||
{
|
||||
AlbedoColor = color,
|
||||
Roughness = 0.7f,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dathgkdgedhrj
|
||||
@@ -0,0 +1,59 @@
|
||||
using Godot;
|
||||
|
||||
public partial class TeleportMarker : Node3D
|
||||
{
|
||||
private static readonly Color IdleColor = new(0.2f, 0.55f, 0.9f);
|
||||
private static readonly Color HotColor = new(0.4f, 0.95f, 1.0f);
|
||||
|
||||
[Export]
|
||||
public string MarkerLabel { get; set; } = "1";
|
||||
|
||||
private StandardMaterial3D _discMaterial;
|
||||
private StandardMaterial3D _beamMaterial;
|
||||
private MeshInstance3D _beam;
|
||||
private Label3D _label;
|
||||
|
||||
public bool IsNear { get; private set; }
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
AddToGroup("teleport_marker");
|
||||
|
||||
_discMaterial = MakeMaterial(IdleColor, false);
|
||||
_beamMaterial = MakeMaterial(new Color(IdleColor, 0.35f), true);
|
||||
|
||||
GetNode<MeshInstance3D>("Disc").SetSurfaceOverrideMaterial(0, _discMaterial);
|
||||
_beam = GetNode<MeshInstance3D>("Beam");
|
||||
_beam.SetSurfaceOverrideMaterial(0, _beamMaterial);
|
||||
_label = GetNode<Label3D>("Label3D");
|
||||
_label.Text = MarkerLabel;
|
||||
}
|
||||
|
||||
public void SetNear(bool near)
|
||||
{
|
||||
if (IsNear == near)
|
||||
return;
|
||||
IsNear = near;
|
||||
GetNode<MeshInstance3D>("Disc").Visible = !near;
|
||||
_beam.Visible = !near;
|
||||
_label.Visible = !near;
|
||||
}
|
||||
|
||||
public void SetHighlighted(bool highlighted)
|
||||
{
|
||||
Color c = highlighted ? HotColor : IdleColor;
|
||||
_discMaterial.AlbedoColor = c;
|
||||
_discMaterial.Emission = c;
|
||||
_beamMaterial.AlbedoColor = new Color(c, 0.35f);
|
||||
_beamMaterial.Emission = c;
|
||||
}
|
||||
|
||||
private static StandardMaterial3D MakeMaterial(Color color, bool transparent) => new()
|
||||
{
|
||||
AlbedoColor = color,
|
||||
EmissionEnabled = true,
|
||||
Emission = color,
|
||||
EmissionEnergyMultiplier = 2.0f,
|
||||
Transparency = transparent ? BaseMaterial3D.TransparencyEnum.Alpha : BaseMaterial3D.TransparencyEnum.Disabled,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ccejrjq47ht5h
|
||||
@@ -0,0 +1,227 @@
|
||||
using Godot;
|
||||
|
||||
// Regression for the patched godot-box3d addon: ConeTwistJoint3D (swing 0, twist 0) maps to
|
||||
// a solver-owned Box3D weld joint that locks the two bodies at the relative pose captured at
|
||||
// creation. Phase 1 asserts a near-rigid weld under load; phase 2 asserts a soft weld
|
||||
// (bias > 0) stretches under sustained extreme force and recovers without exploding.
|
||||
public partial class WeldSpike : Node3D
|
||||
{
|
||||
private const float BodyAMass = 1f;
|
||||
private const float BodyBMass = 0.05f;
|
||||
private const float RigidWeldBias = 0f;
|
||||
private const float SoftWeldBias = 0.5f;
|
||||
private const float RigidDuration = 3f;
|
||||
private const float SoftDuration = 4f;
|
||||
private const float LoadDuration = 1.5f;
|
||||
private const float PullDuration = 2.5f;
|
||||
private const float RigidTorque = 0.5f;
|
||||
private const float RigidForce = 0.5f;
|
||||
private const float PullForce = 4f;
|
||||
private const float MaxSpeed = 10f;
|
||||
private const float RigidRotLimitDeg = 2f;
|
||||
private const float RigidSepLimit = 0.01f;
|
||||
private const float SoftRotLimitDeg = 90f;
|
||||
private const float SoftSepLimit = 0.3f;
|
||||
private const float SoftSettleRotLimitDeg = 10f;
|
||||
private const float SoftSettleSepLimit = 0.05f;
|
||||
|
||||
private enum Phase { RigidWeld, SoftWeld }
|
||||
|
||||
private RigidBody3D _bodyA;
|
||||
private RigidBody3D _bodyB;
|
||||
private ConeTwistJoint3D _joint;
|
||||
private Phase _phase = Phase.RigidWeld;
|
||||
private float _elapsed;
|
||||
private float _phaseStart;
|
||||
private Quaternion _relRotation0;
|
||||
private Vector3 _localAnchorA;
|
||||
private Vector3 _localAnchorB;
|
||||
private float _maxRelRot;
|
||||
private float _maxSep;
|
||||
private float _maxSpeed;
|
||||
private bool _typeOk;
|
||||
private bool _rigidRotOk;
|
||||
private bool _rigidSepOk;
|
||||
private bool _rigidSpeedOk;
|
||||
private bool _softStretchOk;
|
||||
private bool _softSpeedOk;
|
||||
private bool _softSettleOk;
|
||||
private int _lastLogStep;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_bodyA = new RigidBody3D
|
||||
{
|
||||
Position = new Vector3(-0.2f, 2f, 0f),
|
||||
Mass = BodyAMass,
|
||||
GravityScale = 0f,
|
||||
ContinuousCd = true,
|
||||
};
|
||||
_bodyA.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = Vector3.One * 0.4f } });
|
||||
AddChild(_bodyA);
|
||||
|
||||
_bodyB = new RigidBody3D
|
||||
{
|
||||
Position = new Vector3(0.25f, 2f, 0f),
|
||||
Mass = BodyBMass,
|
||||
GravityScale = 0f,
|
||||
ContinuousCd = true,
|
||||
};
|
||||
_bodyB.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = Vector3.One * 0.1f } });
|
||||
AddChild(_bodyB);
|
||||
|
||||
Weld(RigidWeldBias);
|
||||
_phaseStart = _elapsed;
|
||||
GD.Print($"WELD: rigid weld created at t={_elapsed:F2} s, server joint type = {PhysicsServer3D.JointGetType(_joint.GetRid())} (expect {PhysicsServer3D.JointType.ConeTwist})");
|
||||
_typeOk = PhysicsServer3D.JointGetType(_joint.GetRid()) == PhysicsServer3D.JointType.ConeTwist;
|
||||
GD.Print("WELD: weld spike scene ready");
|
||||
}
|
||||
|
||||
private void Weld(float bias)
|
||||
{
|
||||
Vector3 pivot = (_bodyA.GlobalPosition + _bodyB.GlobalPosition) * 0.5f;
|
||||
_relRotation0 = _bodyA.GlobalTransform.Basis.GetRotationQuaternion().Inverse()
|
||||
* _bodyB.GlobalTransform.Basis.GetRotationQuaternion();
|
||||
_localAnchorA = _bodyA.GlobalTransform.AffineInverse() * pivot;
|
||||
_localAnchorB = _bodyB.GlobalTransform.AffineInverse() * pivot;
|
||||
_maxRelRot = 0f;
|
||||
_maxSep = 0f;
|
||||
_maxSpeed = 0f;
|
||||
_joint = new ConeTwistJoint3D
|
||||
{
|
||||
ExcludeNodesFromCollision = true,
|
||||
Position = pivot,
|
||||
NodeA = _bodyA.GetPath(),
|
||||
NodeB = _bodyB.GetPath(),
|
||||
SwingSpan = 0f,
|
||||
TwistSpan = 0f,
|
||||
Bias = bias,
|
||||
};
|
||||
AddChild(_joint);
|
||||
}
|
||||
|
||||
public override void _PhysicsProcess(double delta)
|
||||
{
|
||||
float phaseT = _elapsed - _phaseStart;
|
||||
_elapsed += (float)delta;
|
||||
|
||||
switch (_phase)
|
||||
{
|
||||
case Phase.RigidWeld:
|
||||
{
|
||||
if (phaseT < LoadDuration)
|
||||
{
|
||||
_bodyA.ApplyTorque(new Vector3(0f, RigidTorque, 0f));
|
||||
_bodyB.ApplyCentralForce(new Vector3(RigidForce, 0f, 0f));
|
||||
}
|
||||
Track();
|
||||
LogBucket(phaseT);
|
||||
if (phaseT >= RigidDuration)
|
||||
FinishRigid();
|
||||
break;
|
||||
}
|
||||
case Phase.SoftWeld:
|
||||
{
|
||||
if (phaseT < PullDuration)
|
||||
{
|
||||
_bodyA.ApplyCentralForce(new Vector3(-PullForce, 0f, 0f));
|
||||
_bodyB.ApplyCentralForce(new Vector3(PullForce, 0f, 0f));
|
||||
}
|
||||
Track();
|
||||
LogBucket(phaseT);
|
||||
if (phaseT >= SoftDuration)
|
||||
Finish();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static float DevAngle(Quaternion relNow, Quaternion rel0)
|
||||
{
|
||||
Quaternion dev = relNow * rel0.Inverse();
|
||||
if (dev.W < 0f)
|
||||
dev = new Quaternion(-dev.X, -dev.Y, -dev.Z, -dev.W);
|
||||
// GetAngle() NaNs when w exceeds 1 by a float epsilon; clamp like GrabSpike does
|
||||
if (float.IsNaN(dev.W))
|
||||
return float.NaN;
|
||||
return 2f * Mathf.Acos(Mathf.Clamp(Mathf.Abs(dev.W), 0f, 1f));
|
||||
}
|
||||
|
||||
private void Track()
|
||||
{
|
||||
Quaternion relNow = _bodyA.GlobalTransform.Basis.GetRotationQuaternion().Inverse()
|
||||
* _bodyB.GlobalTransform.Basis.GetRotationQuaternion();
|
||||
float rotDev = DevAngle(relNow, _relRotation0);
|
||||
if (float.IsNaN(rotDev))
|
||||
{
|
||||
GD.Print("WELD: FAIL — NaN rotation error (explosion guard)");
|
||||
GetTree().Quit();
|
||||
return;
|
||||
}
|
||||
_maxRelRot = Mathf.Max(_maxRelRot, rotDev);
|
||||
float sep = (_bodyA.GlobalTransform * _localAnchorA).DistanceTo(_bodyB.GlobalTransform * _localAnchorB);
|
||||
_maxSep = Mathf.Max(_maxSep, sep);
|
||||
float speed = Mathf.Max(_bodyA.LinearVelocity.Length(), _bodyB.LinearVelocity.Length());
|
||||
if (float.IsNaN(speed) || speed > MaxSpeed)
|
||||
{
|
||||
GD.Print($"WELD: FAIL — body speed {speed} m/s exceeds {MaxSpeed} (explosion guard)");
|
||||
GetTree().Quit();
|
||||
return;
|
||||
}
|
||||
_maxSpeed = Mathf.Max(_maxSpeed, speed);
|
||||
}
|
||||
|
||||
private void LogBucket(float phaseT)
|
||||
{
|
||||
int step = (int)(phaseT / 0.5f);
|
||||
if (step == _lastLogStep)
|
||||
return;
|
||||
_lastLogStep = step;
|
||||
Quaternion relNow = _bodyA.GlobalTransform.Basis.GetRotationQuaternion().Inverse()
|
||||
* _bodyB.GlobalTransform.Basis.GetRotationQuaternion();
|
||||
float rotDev = DevAngle(relNow, _relRotation0);
|
||||
float sep = (_bodyA.GlobalTransform * _localAnchorA).DistanceTo(_bodyB.GlobalTransform * _localAnchorB);
|
||||
GD.Print($"WELD: t={_elapsed:F2} phase={_phase} rotDev={Mathf.RadToDeg(rotDev):F2} deg sep={sep:F4} m speedA={_bodyA.LinearVelocity.Length():F2} speedB={_bodyB.LinearVelocity.Length():F2}");
|
||||
}
|
||||
|
||||
private void FinishRigid()
|
||||
{
|
||||
_rigidRotOk = _maxRelRot < Mathf.DegToRad(RigidRotLimitDeg);
|
||||
_rigidSepOk = _maxSep < RigidSepLimit;
|
||||
_rigidSpeedOk = _maxSpeed < MaxSpeed;
|
||||
GD.Print($"WELD: rigid hold — max rot dev {Mathf.RadToDeg(_maxRelRot):F2} deg (limit {RigidRotLimitDeg}) → {(_rigidRotOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"WELD: rigid anchors — max sep {_maxSep:F4} m (limit {RigidSepLimit}) → {(_rigidSepOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"WELD: rigid speeds — max {_maxSpeed:F2} m/s (limit {MaxSpeed}) → {(_rigidSpeedOk ? "PASS" : "FAIL")}");
|
||||
|
||||
// fresh-grab conditions: stop the coasting bodies before re-welding
|
||||
_bodyA.LinearVelocity = Vector3.Zero;
|
||||
_bodyA.AngularVelocity = Vector3.Zero;
|
||||
_bodyB.LinearVelocity = Vector3.Zero;
|
||||
_bodyB.AngularVelocity = Vector3.Zero;
|
||||
_joint.QueueFree();
|
||||
_joint = null;
|
||||
_phase = Phase.SoftWeld;
|
||||
_phaseStart = _elapsed;
|
||||
_lastLogStep = 0;
|
||||
Weld(SoftWeldBias);
|
||||
GD.Print($"WELD: soft weld re-created at current pose, bias {SoftWeldBias} (weld hertz {SoftWeldBias * 30f:F0}), sustained ±{PullForce} N pull starting");
|
||||
}
|
||||
|
||||
private void Finish()
|
||||
{
|
||||
_softStretchOk = _maxRelRot < Mathf.DegToRad(SoftRotLimitDeg) && _maxSep < SoftSepLimit;
|
||||
_softSpeedOk = _maxSpeed < MaxSpeed;
|
||||
// settle: assert the stretch has recovered by the end
|
||||
Quaternion relNow = _bodyA.GlobalTransform.Basis.GetRotationQuaternion().Inverse()
|
||||
* _bodyB.GlobalTransform.Basis.GetRotationQuaternion();
|
||||
float settleRot = DevAngle(relNow, _relRotation0);
|
||||
float settleSep = (_bodyA.GlobalTransform * _localAnchorA).DistanceTo(_bodyB.GlobalTransform * _localAnchorB);
|
||||
_softSettleOk = settleRot < Mathf.DegToRad(SoftSettleRotLimitDeg) && settleSep < SoftSettleSepLimit;
|
||||
GD.Print($"WELD: soft excursion bounded — max rot dev {Mathf.RadToDeg(_maxRelRot):F2} deg (limit {SoftRotLimitDeg}), max sep {_maxSep:F4} m (limit {SoftSepLimit}) → {(_softStretchOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"WELD: soft speeds — max {_maxSpeed:F2} m/s (limit {MaxSpeed}) → {(_softSpeedOk ? "PASS" : "FAIL")}");
|
||||
GD.Print($"WELD: soft settle — rot dev {Mathf.RadToDeg(settleRot):F2} deg (limit {SoftSettleRotLimitDeg}), sep {settleSep:F4} m (limit {SoftSettleSepLimit}) → {(_softSettleOk ? "PASS" : "FAIL")}");
|
||||
bool overall = _typeOk && _rigidRotOk && _rigidSepOk && _rigidSpeedOk && _softStretchOk && _softSpeedOk && _softSettleOk;
|
||||
GD.Print($"WELD: overall {(overall ? "PASS — solver-owned rigid weld via ConeTwistJoint3D functional on patched Box3D" : "CHECK — behavior differs, inspect above")}");
|
||||
GetTree().Quit();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ck1ceetotj4pk
|
||||
@@ -0,0 +1,396 @@
|
||||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
public partial class WorldPanel : Node3D
|
||||
{
|
||||
private const float GripThreshold = 0.6f;
|
||||
private const float TriggerThreshold = 0.6f;
|
||||
private const float DirectGrabRadius = 0.45f;
|
||||
private const float TipHoverRadius = 0.07f;
|
||||
private const float PressContactDistance = 0.045f;
|
||||
private const ulong PressCooldownMs = 400;
|
||||
private const float TipLength = 0.12f;
|
||||
private const float RayLength = 50f;
|
||||
|
||||
private static WorldPanel _instance;
|
||||
|
||||
private XRController3D _left;
|
||||
private XRController3D _right;
|
||||
private XRCamera3D _camera;
|
||||
private Locomotion _locomotion;
|
||||
private PropShop _propShop;
|
||||
private AudioStreamPlayer3D _click;
|
||||
|
||||
private StaticBody3D _panelBody;
|
||||
|
||||
private struct Knob
|
||||
{
|
||||
public string Name;
|
||||
public Node3D Root;
|
||||
public MeshInstance3D Mesh;
|
||||
public StandardMaterial3D Material;
|
||||
public Callable Action;
|
||||
public ulong LastPressMs;
|
||||
}
|
||||
|
||||
private readonly List<Knob> _knobs = new();
|
||||
|
||||
private readonly bool[] _dragging = new bool[2];
|
||||
private readonly bool[] _directDrag = new bool[2];
|
||||
private readonly float[] _laserDist = new float[2];
|
||||
private readonly Vector3[] _directOffset = new Vector3[2];
|
||||
private readonly bool[] _triggerPrev = new bool[2];
|
||||
private readonly bool[] _summonPrev = new bool[2];
|
||||
private readonly int[] _hover = new int[2] { -1, -1 };
|
||||
private readonly bool[] _hoverPanel = new bool[2];
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_instance = this;
|
||||
_left = GetNode<XRController3D>("../LeftHand");
|
||||
_right = GetNode<XRController3D>("../RightHand");
|
||||
_camera = GetNode<XRCamera3D>("../XRCamera3D");
|
||||
_locomotion = GetNode<Locomotion>("../../Locomotion");
|
||||
_propShop = GetNode<PropShop>("../../PropShop");
|
||||
Visible = false;
|
||||
BuildPanel();
|
||||
BuildClickSound();
|
||||
SetPanelActive(false);
|
||||
}
|
||||
|
||||
public static bool IsBlocking(XRController3D controller) =>
|
||||
_instance != null && _instance.Visible &&
|
||||
_instance.GlobalPosition.DistanceTo(controller.GlobalPosition) < DirectGrabRadius;
|
||||
|
||||
public static bool IsPanelCollider(Node node) =>
|
||||
_instance != null && _instance.Visible && node != null &&
|
||||
(node.HasMeta("knob_index") || node == _instance._panelBody || node.GetParent() == _instance._panelBody);
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
PollSummon(_left, 0);
|
||||
PollSummon(_right, 1);
|
||||
if (!Visible)
|
||||
return;
|
||||
UpdateHand(_left, 0);
|
||||
UpdateHand(_right, 1);
|
||||
}
|
||||
|
||||
private void PollSummon(XRController3D controller, int hand)
|
||||
{
|
||||
bool summon = controller.IsButtonPressed("ax_button");
|
||||
if (summon && !_summonPrev[hand])
|
||||
{
|
||||
if (!Visible)
|
||||
PlaceInFrontOf(controller);
|
||||
SetPanelActive(!Visible);
|
||||
GD.Print($"WorldPanel: {(Visible ? "summoned" : "dismissed")} via ax_button ({(hand == 0 ? "left" : "right")})");
|
||||
}
|
||||
_summonPrev[hand] = summon;
|
||||
}
|
||||
|
||||
private void SetPanelActive(bool active)
|
||||
{
|
||||
if (!active)
|
||||
ResetInteractionState();
|
||||
Visible = active;
|
||||
SetShapesEnabled(this, active);
|
||||
}
|
||||
|
||||
private static void SetShapesEnabled(Node root, bool enabled)
|
||||
{
|
||||
foreach (Node child in root.GetChildren())
|
||||
{
|
||||
if (child is CollisionShape3D shape)
|
||||
shape.SetDeferred(CollisionShape3D.PropertyName.Disabled, !enabled);
|
||||
SetShapesEnabled(child, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
private void PlaceInFrontOf(XRController3D controller) =>
|
||||
OrientTo(controller.GlobalPosition - controller.GlobalTransform.Basis.Z * 0.5f);
|
||||
|
||||
private void PlaceInFrontOfHead() =>
|
||||
OrientTo(_camera.GlobalPosition - _camera.GlobalTransform.Basis.Z * 0.5f);
|
||||
|
||||
private void OrientTo(Vector3 pos)
|
||||
{
|
||||
Vector3 look = _camera.GlobalPosition - pos;
|
||||
if (look.LengthSquared() < 0.0001f)
|
||||
return;
|
||||
Vector3 zAxis = look.Normalized();
|
||||
Vector3 xAxis = Vector3.Up.Cross(zAxis);
|
||||
if (xAxis.LengthSquared() < 0.0001f)
|
||||
return;
|
||||
xAxis = xAxis.Normalized();
|
||||
Vector3 yAxis = zAxis.Cross(xAxis).Normalized();
|
||||
GlobalTransform = new Transform3D(new Basis(xAxis, yAxis, zAxis), pos);
|
||||
}
|
||||
|
||||
private void OrientTowardCamera()
|
||||
{
|
||||
Vector3 look = _camera.GlobalPosition - GlobalPosition;
|
||||
if (look.LengthSquared() < 0.0001f)
|
||||
return;
|
||||
Vector3 zAxis = look.Normalized();
|
||||
Vector3 xAxis = Vector3.Up.Cross(zAxis);
|
||||
if (xAxis.LengthSquared() < 0.0001f)
|
||||
return;
|
||||
xAxis = xAxis.Normalized();
|
||||
Vector3 yAxis = zAxis.Cross(xAxis).Normalized();
|
||||
GlobalBasis = GlobalBasis.Slerp(new Basis(xAxis, yAxis, zAxis), 0.35f).Orthonormalized();
|
||||
}
|
||||
|
||||
private void BuildPanel()
|
||||
{
|
||||
Node3D plate = new() { Name = "Plate" };
|
||||
AddChild(plate);
|
||||
|
||||
StandardMaterial3D plateMaterial = new()
|
||||
{
|
||||
AlbedoColor = new Color(0.08f, 0.09f, 0.12f),
|
||||
EmissionEnabled = true,
|
||||
Emission = new Color(0.1f, 0.2f, 0.35f),
|
||||
EmissionEnergyMultiplier = 0.4f,
|
||||
};
|
||||
MeshInstance3D plateMesh = new() { Mesh = new BoxMesh { Size = new Vector3(0.6f, 0.34f, 0.02f) } };
|
||||
plateMesh.SetSurfaceOverrideMaterial(0, plateMaterial);
|
||||
plate.AddChild(plateMesh);
|
||||
|
||||
_panelBody = new StaticBody3D
|
||||
{
|
||||
Name = "PanelBody",
|
||||
CollisionLayer = 2,
|
||||
CollisionMask = 0,
|
||||
};
|
||||
_panelBody.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.6f, 0.34f, 0.06f) } });
|
||||
plate.AddChild(_panelBody);
|
||||
|
||||
Vector3[] positions =
|
||||
{
|
||||
new(-0.21f, 0.06f, 0.03f),
|
||||
new(-0.07f, 0.06f, 0.03f),
|
||||
new(0.07f, 0.06f, 0.03f),
|
||||
new(0.21f, 0.06f, 0.03f),
|
||||
};
|
||||
string[] names = { "Spawn", "Ping", "Reset", "Close" };
|
||||
Color[] colors =
|
||||
{
|
||||
new(0.25f, 0.6f, 1f),
|
||||
new(0.95f, 0.75f, 0.25f),
|
||||
new(0.2f, 0.8f, 0.35f),
|
||||
new(0.9f, 0.3f, 0.3f),
|
||||
};
|
||||
Callable[] actions =
|
||||
{
|
||||
Callable.From(() =>
|
||||
{
|
||||
_locomotion.ReturnToSpawn();
|
||||
ResetInteractionState();
|
||||
PlaceInFrontOfHead();
|
||||
}),
|
||||
Callable.From(Ping),
|
||||
Callable.From(() => _propShop.Respawn()),
|
||||
Callable.From(Close),
|
||||
};
|
||||
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
{
|
||||
StandardMaterial3D material = new()
|
||||
{
|
||||
AlbedoColor = colors[i],
|
||||
EmissionEnabled = true,
|
||||
Emission = colors[i],
|
||||
EmissionEnergyMultiplier = 1.2f,
|
||||
};
|
||||
MeshInstance3D mesh = new()
|
||||
{
|
||||
Mesh = new CylinderMesh { TopRadius = 0.032f, BottomRadius = 0.032f, Height = 0.025f },
|
||||
RotationDegrees = new Vector3(90, 0, 0),
|
||||
};
|
||||
mesh.SetSurfaceOverrideMaterial(0, material);
|
||||
|
||||
StaticBody3D body = new()
|
||||
{
|
||||
CollisionLayer = 2,
|
||||
CollisionMask = 0,
|
||||
};
|
||||
body.SetMeta("knob_index", i);
|
||||
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(0.09f, 0.09f, 0.06f) } });
|
||||
|
||||
Node3D root = new() { Position = positions[i] };
|
||||
root.AddChild(mesh);
|
||||
root.AddChild(body);
|
||||
|
||||
Label3D label = new()
|
||||
{
|
||||
Text = names[i],
|
||||
FontSize = 96,
|
||||
PixelSize = 0.00025f,
|
||||
OutlineSize = 6,
|
||||
Position = new Vector3(0, -0.08f, 0.015f),
|
||||
};
|
||||
root.AddChild(label);
|
||||
|
||||
plate.AddChild(root);
|
||||
_knobs.Add(new Knob
|
||||
{
|
||||
Name = names[i],
|
||||
Root = root,
|
||||
Mesh = mesh,
|
||||
Material = material,
|
||||
Action = actions[i],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void BuildClickSound()
|
||||
{
|
||||
const int rate = 44100;
|
||||
const int samples = rate / 25;
|
||||
byte[] data = new byte[samples * 2];
|
||||
for (int i = 0; i < samples; i++)
|
||||
{
|
||||
float envelope = 1f - (float)i / samples;
|
||||
float value = Mathf.Sin(Mathf.Tau * 900f * i / rate) * envelope * 0.35f;
|
||||
short sample = (short)(value * short.MaxValue);
|
||||
data[i * 2] = (byte)sample;
|
||||
data[i * 2 + 1] = (byte)(sample >> 8);
|
||||
}
|
||||
_click = new AudioStreamPlayer3D
|
||||
{
|
||||
Stream = new AudioStreamWav
|
||||
{
|
||||
Format = AudioStreamWav.FormatEnum.Format16Bits,
|
||||
MixRate = rate,
|
||||
Stereo = false,
|
||||
Data = data,
|
||||
},
|
||||
};
|
||||
AddChild(_click);
|
||||
}
|
||||
|
||||
private void UpdateHand(XRController3D controller, int hand)
|
||||
{
|
||||
Vector3 from = controller.GlobalPosition;
|
||||
Vector3 dir = -controller.GlobalTransform.Basis.Z;
|
||||
Vector3 tip = from + dir * TipLength;
|
||||
|
||||
UpdateHover(controller, hand, from, dir, tip);
|
||||
|
||||
bool grip = controller.GetFloat("grip") > GripThreshold
|
||||
&& !PropGrabber.IsGrabbing(controller);
|
||||
if (grip && !_dragging[hand] && (_hoverPanel[hand] || GlobalPosition.DistanceTo(from) < DirectGrabRadius))
|
||||
{
|
||||
_dragging[hand] = true;
|
||||
_directDrag[hand] = !_hoverPanel[hand];
|
||||
if (_directDrag[hand])
|
||||
_directOffset[hand] = GlobalPosition - from;
|
||||
else
|
||||
_laserDist[hand] = Mathf.Max(0.2f, (GlobalPosition - from).Dot(dir));
|
||||
GD.Print($"WorldPanel: grab started ({(_directDrag[hand] ? "direct" : "laser")}, {(hand == 0 ? "left" : "right")})");
|
||||
}
|
||||
if (!grip && _dragging[hand])
|
||||
{
|
||||
_dragging[hand] = false;
|
||||
GD.Print("WorldPanel: grab released");
|
||||
}
|
||||
|
||||
if (_dragging[hand])
|
||||
{
|
||||
GlobalPosition = _directDrag[hand]
|
||||
? from + _directOffset[hand]
|
||||
: from + dir * _laserDist[hand];
|
||||
OrientTowardCamera();
|
||||
}
|
||||
|
||||
bool trigger = controller.GetFloat("trigger") > TriggerThreshold;
|
||||
if (trigger && !_triggerPrev[hand] && _hover[hand] >= 0)
|
||||
Press(_hover[hand]);
|
||||
_triggerPrev[hand] = trigger;
|
||||
|
||||
if (_hover[hand] >= 0)
|
||||
{
|
||||
int hovered = _hover[hand];
|
||||
Knob knob = _knobs[hovered];
|
||||
if (knob.Root.GlobalPosition.DistanceTo(tip) < PressContactDistance)
|
||||
{
|
||||
ulong now = Time.GetTicksMsec();
|
||||
if (now - knob.LastPressMs > PressCooldownMs)
|
||||
{
|
||||
knob.LastPressMs = now;
|
||||
_knobs[hovered] = knob;
|
||||
Press(hovered);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateHover(XRController3D controller, int hand, Vector3 from, Vector3 dir, Vector3 tip)
|
||||
{
|
||||
int knob = -1;
|
||||
bool panel = false;
|
||||
|
||||
var query = PhysicsRayQueryParameters3D.Create(from, from + dir * RayLength);
|
||||
var hit = GetViewport().GetWorld3D().DirectSpaceState.IntersectRay(query);
|
||||
if (hit.TryGetValue("collider", out Variant collider) && collider.As<Node>() is Node node)
|
||||
{
|
||||
if (node.HasMeta("knob_index"))
|
||||
knob = (int)node.GetMeta("knob_index");
|
||||
else
|
||||
panel = IsPanelCollider(node) || node.GetParent() == _panelBody;
|
||||
}
|
||||
|
||||
if (knob < 0)
|
||||
{
|
||||
for (int i = 0; i < _knobs.Count; i++)
|
||||
{
|
||||
if (_knobs[i].Root.GlobalPosition.DistanceTo(tip) < TipHoverRadius)
|
||||
{
|
||||
knob = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (knob != _hover[hand])
|
||||
{
|
||||
if (_hover[hand] >= 0)
|
||||
_knobs[_hover[hand]].Material.EmissionEnergyMultiplier = 1.2f;
|
||||
_hover[hand] = knob;
|
||||
if (knob >= 0)
|
||||
_knobs[knob].Material.EmissionEnergyMultiplier = 2.8f;
|
||||
}
|
||||
_hoverPanel[hand] = panel;
|
||||
}
|
||||
|
||||
private void Press(int index)
|
||||
{
|
||||
Knob knob = _knobs[index];
|
||||
GD.Print($"WorldPanel: pressed '{knob.Name}'");
|
||||
_click.Play();
|
||||
Tween tween = CreateTween();
|
||||
tween.TweenProperty(knob.Mesh, "position:z", -0.014f, 0.05);
|
||||
tween.Parallel().TweenProperty(knob.Material, "emission_energy_multiplier", 4.0f, 0.05);
|
||||
tween.TweenProperty(knob.Mesh, "position:z", 0f, 0.12);
|
||||
tween.Parallel().TweenProperty(knob.Material, "emission_energy_multiplier", 1.2f, 0.3);
|
||||
knob.Action.Call();
|
||||
}
|
||||
|
||||
private void Ping() => GD.Print("WorldPanel: ping");
|
||||
|
||||
private void Close() => SetPanelActive(false);
|
||||
|
||||
private void ResetInteractionState()
|
||||
{
|
||||
for (int hand = 0; hand < 2; hand++)
|
||||
{
|
||||
_dragging[hand] = false;
|
||||
_hoverPanel[hand] = false;
|
||||
_triggerPrev[hand] = false;
|
||||
if (_hover[hand] >= 0)
|
||||
_knobs[_hover[hand]].Material.EmissionEnergyMultiplier = 1.2f;
|
||||
_hover[hand] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://babrdgj2luqwv
|
||||
Reference in new issue
Block a user