Files
VR-Mess/scripts/WorldPanel.cs
T

397 lines
13 KiB
C#

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