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,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user