Files
VR-Mess/scripts/Locomotion.cs
T

245 lines
8.4 KiB
C#

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