Skip to content

Commit da4d53b

Browse files
committed
Add MouseSelectSystem for trigger highlighting
1 parent bd34e2e commit da4d53b

File tree

2 files changed

+130
-0
lines changed

2 files changed

+130
-0
lines changed

src/OpenH2.Engine/RealtimeWorld.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public RealtimeWorld(GameWindow window,
3333
Systems.Add(animationSystem);
3434
Systems.Add(new ScriptSystem(this, audioSystem, cameraSystem, actorSystem, animationSystem));
3535
Systems.Add(new RenderCollectorSystem(this, graphics));
36+
Systems.Add(new MouseSelectSystem(this, window));
3637

3738
RenderSystems.Add(new RenderPipelineSystem(this, graphics));
3839

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
using OpenH2.Core.Architecture;
2+
using OpenH2.Engine.Stores;
3+
using OpenH2.Engine.Components;
4+
using OpenH2.Engine.Entities;
5+
using OpenH2.Rendering.Abstractions;
6+
using System.Numerics;
7+
using OpenTK.Windowing.Desktop;
8+
using System;
9+
10+
namespace OpenH2.Engine.Systems
11+
{
12+
public class MouseSelectSystem : WorldSystem
13+
{
14+
15+
private readonly GameWindow window;
16+
private TriggerVolume selectedVolume;
17+
public MouseSelectSystem(World world, GameWindow window) : base(world)
18+
{
19+
selectedVolume = null;
20+
this.window = window;
21+
}
22+
23+
// Find the trigger volume that the mouse is hovering over by raycasting from the camera.
24+
private TriggerVolume FindNextTriggerVolume(InputStore input_store)
25+
{
26+
var cameras = world.Components<CameraComponent>();
27+
var cam = cameras[0];
28+
29+
var viewMatrix = cam.ViewMatrix;
30+
var projectionMatrix = cam.ProjectionMatrix;
31+
var viewProjectionMat = viewMatrix * projectionMatrix;
32+
33+
// Get mouse position, and convert it to projection coordinates.
34+
// That is, scale [0, width) -> [-1, 1] and [0, height) -> [-1, 1].
35+
var mousePos = input_store.MousePos;
36+
var scaledMousePos = new Vector2((2 * mousePos.X / window.Size.X - 1),
37+
-(2 * mousePos.Y / window.Size.Y - 1));
38+
39+
if (!Matrix4x4.Invert(viewProjectionMat, out var viewProjectionInv))
40+
{
41+
System.Console.Error.WriteLine($"Unable to invert view+projection matrix: {viewProjectionMat}");
42+
return null;
43+
}
44+
45+
// Convert ray from projection space to world space.
46+
var rayOrigin = Vector4.Transform(new Vector4(0, 0, 0, 1), viewProjectionInv);
47+
rayOrigin /= rayOrigin.W;
48+
var rayTip = Vector4.Transform(new Vector4(scaledMousePos, 1, 1), viewProjectionInv);
49+
rayTip /= rayTip.W;
50+
51+
float min = float.MaxValue;
52+
TriggerVolume newVolume = null;
53+
foreach (var entity in this.world.Scene.Entities.Values)
54+
{
55+
if (entity is not TriggerVolume tv)
56+
{
57+
continue;
58+
}
59+
// All trigger volumes have a TriggerGeometryComponent.
60+
var trigGeomComp = entity.GetChildren<TriggerGeometryComponent>()[0];
61+
62+
var left_bottom = new Vector3(0, 0, 0);
63+
var top_right = trigGeomComp.Size;
64+
var entTransMat = trigGeomComp.Transform.TransformationMatrix;
65+
if (!Matrix4x4.Invert(entTransMat, out var entTransMatInv))
66+
{
67+
System.Console.Error.WriteLine($"Unable to invert entity transformation mat: {entTransMat} for entity {entity.FriendlyName}");
68+
return null;
69+
}
70+
71+
// Handle non-axis-aligned triggers: transform the ray into the local space of the trigger.
72+
var localRayOrigin = Vector4.Transform(rayOrigin, entTransMatInv);
73+
var localRayTip = Vector4.Transform(rayTip, entTransMatInv);
74+
75+
// Now do standard axis-aligned bounding-box + ray intersection.
76+
var localRayDir = localRayTip - localRayOrigin;
77+
localRayDir /= localRayDir.Length();
78+
var localRayDirRecip = new Vector3(1.0f / localRayDir.X, 1.0f / localRayDir.Y, 1.0f / localRayDir.Z);
79+
80+
// Calculate the distance `t` to all 6 planes of the box.
81+
float t1 = (left_bottom.X - localRayOrigin.X) * localRayDirRecip.X;
82+
float t2 = (top_right.X - localRayOrigin.X) * localRayDirRecip.X;
83+
float t3 = (left_bottom.Y - localRayOrigin.Y) * localRayDirRecip.Y;
84+
float t4 = (top_right.Y - localRayOrigin.Y) * localRayDirRecip.Y;
85+
float t5 = (left_bottom.Z - localRayOrigin.Z) * localRayDirRecip.Z;
86+
float t6 = (top_right.Z - localRayOrigin.Z) * localRayDirRecip.Z;
87+
88+
float tmin = Math.Max(Math.Max(Math.Min(t1, t2), Math.Min(t3, t4)), Math.Min(t5, t6));
89+
float tmax = Math.Min(Math.Min(Math.Max(t1, t2), Math.Max(t3, t4)), Math.Max(t5, t6));
90+
if (tmin < 0 || tmax < 0 || tmin > tmax)
91+
{
92+
// Discard the box if we are partially inside it (tmin < 0),
93+
// the box is completely behind us (tmax < 0), or the ray missed (tmin > tmax).
94+
continue;
95+
}
96+
if (tmin < min)
97+
{
98+
// Otherwise, tmin is our total distance (in trigger-local space) to the box.
99+
// Pick the lowest one (i.e., closest the camera).
100+
min = tmin;
101+
newVolume = tv;
102+
}
103+
}
104+
return newVolume;
105+
}
106+
107+
public override void Update(double timestep)
108+
{
109+
var input_store = this.world.GetGlobalResource<InputStore>();
110+
if (input_store.RightMouseDown)
111+
{
112+
var newVolume = FindNextTriggerVolume(input_store);
113+
if (newVolume != selectedVolume)
114+
{
115+
if (newVolume != null)
116+
{
117+
newVolume.ToggleSelected();
118+
System.Console.WriteLine($"[TRIG] {newVolume.FriendlyName} selected");
119+
}
120+
if (selectedVolume != null)
121+
{
122+
selectedVolume.ToggleSelected();
123+
}
124+
selectedVolume = newVolume;
125+
}
126+
}
127+
}
128+
}
129+
}

0 commit comments

Comments
 (0)