Hook Win32 camera input

This commit is contained in:
Meta Vens 2026-05-08 02:33:50 +01:00
commit a68fa3bb8b
9 changed files with 261 additions and 16 deletions

View file

@ -52,3 +52,5 @@ dotnet run --project src\Aquarium.Engine\Aquarium.Engine.csproj -- --headless
The first cut opens a Win32 window directly and owns a D3D11 swapchain through
Vortice. No Stride runtime, no asset protocol, no borrowed game loop wearing a
fake mustache. See `docs/vortice-spine.md`.
Current camera controls are documented in `docs/input.md`.

21
docs/input.md Normal file
View file

@ -0,0 +1,21 @@
# Input
Input is currently Win32-backed and intentionally small.
## Flow
- `Win32Window` translates window messages into `InputState`.
- `AquariumHost` begins each input frame, pumps messages, then updates runtime.
- `AquariumRuntime` passes input into `OrbitCameraRig`.
- The renderer receives the updated camera and Grid frame as constants.
## Controls
- Middle mouse drag: orbit camera around the Grid center.
- Mouse wheel: exponential zoom.
- Right mouse drag: pan along the Grid plane.
- `WASD`: pan along the Grid plane.
The camera target remains the Grid center. Panning moves that shared target;
orbiting changes only yaw/pitch; zoom changes only camera distance and therefore
Grid radius.

View file

@ -1,4 +1,5 @@
using System.Diagnostics;
using Aquarium.Engine.Input;
using Aquarium.Engine.Platform;
using Aquarium.Engine.Render;
@ -9,9 +10,10 @@ public static class AquariumHost
public static int Run(string[] args)
{
var runtime = new AquariumRuntime(new AquariumRuntimeOptions(ParseHeadless(args)));
var input = new InputState();
var width = runtime.Options.Headless ? 640 : 1280;
var height = runtime.Options.Headless ? 360 : 720;
using var window = Win32Window.Create("Aquarium Engine", width, height);
using var window = Win32Window.Create("Aquarium Engine", width, height, input);
using var renderer = new D3D11Renderer(window.Handle, window.ClientWidth, window.ClientHeight);
runtime.Start();
@ -20,13 +22,19 @@ public static class AquariumHost
var lastFrame = frameClock.Elapsed;
var frames = 0;
while (window.PumpMessages())
while (true)
{
input.BeginFrame();
if (!window.PumpMessages())
{
break;
}
var now = frameClock.Elapsed;
var deltaSeconds = (float)(now - lastFrame).TotalSeconds;
lastFrame = now;
runtime.Update(deltaSeconds);
runtime.Update(deltaSeconds, input);
renderer.Render(runtime.Frame);
if (runtime.Options.Headless && ++frames >= 2)

View file

@ -1,3 +1,6 @@
using System.Numerics;
using Aquarium.Engine.Input;
namespace Aquarium.Engine;
public sealed class AquariumRuntime
@ -29,11 +32,10 @@ public sealed class AquariumRuntime
Console.WriteLine("Vortice D3D11 is present as the host shell; Aquarium owns the invariants.");
}
public void Update(float deltaSeconds)
public void Update(float deltaSeconds, InputState input)
{
_ = deltaSeconds;
timeSeconds += Math.Max(deltaSeconds, 0.0f);
cameraRig.ApplyInput(input, deltaSeconds);
gridFrame = GridFrame.FromCamera(cameraRig);
}
}

View file

@ -0,0 +1,76 @@
using System.Numerics;
namespace Aquarium.Engine.Input;
public sealed class InputState
{
private readonly HashSet<KeyCode> downKeys = [];
public Vector2 MousePosition { get; private set; }
public Vector2 MouseDelta { get; private set; }
public float WheelDelta { get; private set; }
public bool MiddleMouseDown { get; private set; }
public bool RightMouseDown { get; private set; }
public bool IsKeyDown(KeyCode key) => downKeys.Contains(key);
public void BeginFrame()
{
MouseDelta = Vector2.Zero;
WheelDelta = 0.0f;
}
public void SetMousePosition(Vector2 position)
{
MouseDelta += position - MousePosition;
MousePosition = position;
}
public void AddWheelDelta(float delta)
{
WheelDelta += delta;
}
public void SetMouseButton(MouseButton button, bool isDown)
{
switch (button)
{
case MouseButton.Middle:
MiddleMouseDown = isDown;
break;
case MouseButton.Right:
RightMouseDown = isDown;
break;
}
}
public void SetKey(KeyCode key, bool isDown)
{
if (isDown)
{
downKeys.Add(key);
}
else
{
downKeys.Remove(key);
}
}
}
public enum MouseButton
{
Middle,
Right,
}
public enum KeyCode
{
W,
A,
S,
D,
}

View file

@ -1,4 +1,6 @@
using System.Runtime.InteropServices;
using System.Numerics;
using Aquarium.Engine.Input;
namespace Aquarium.Engine.Platform;
@ -6,23 +8,37 @@ public sealed class Win32Window : IDisposable
{
private const int CW_USEDEFAULT = unchecked((int)0x80000000);
private const uint WM_DESTROY = 0x0002;
private const uint WM_SIZE = 0x0005;
private const uint WM_CLOSE = 0x0010;
private const uint WM_KEYDOWN = 0x0100;
private const uint WM_KEYUP = 0x0101;
private const uint WM_MOUSEMOVE = 0x0200;
private const uint WM_RBUTTONDOWN = 0x0204;
private const uint WM_RBUTTONUP = 0x0205;
private const uint WM_MBUTTONDOWN = 0x0207;
private const uint WM_MBUTTONUP = 0x0208;
private const uint WM_MOUSEWHEEL = 0x020A;
private const uint PM_REMOVE = 0x0001;
private const uint WS_OVERLAPPEDWINDOW = 0x00CF0000;
private const uint WS_VISIBLE = 0x10000000;
private const int SW_SHOW = 5;
private const int IDC_ARROW = 32512;
private const int VK_W = 0x57;
private const int VK_A = 0x41;
private const int VK_S = 0x53;
private const int VK_D = 0x44;
private const int WHEEL_DELTA = 120;
private readonly WndProc windowProcedure;
private readonly string className;
private readonly InputState input;
private bool disposed;
private Win32Window(IntPtr handle, string className, WndProc windowProcedure, int width, int height)
private Win32Window(IntPtr handle, string className, WndProc windowProcedure, InputState input, int width, int height)
{
Handle = handle;
this.className = className;
this.windowProcedure = windowProcedure;
this.input = input;
ClientWidth = width;
ClientHeight = height;
}
@ -33,7 +49,7 @@ public sealed class Win32Window : IDisposable
public int ClientHeight { get; private set; }
public static Win32Window Create(string title, int width, int height)
public static Win32Window Create(string title, int width, int height, InputState input)
{
var className = $"AquariumEngineWindow-{Guid.NewGuid():N}";
var instance = GetModuleHandle(null);
@ -49,6 +65,34 @@ public sealed class Win32Window : IDisposable
case WM_DESTROY:
PostQuitMessage(0);
return IntPtr.Zero;
case WM_MOUSEMOVE:
input.SetMousePosition(new Vector2(GetSignedLowWord(lParam), GetSignedHighWord(lParam)));
return IntPtr.Zero;
case WM_MOUSEWHEEL:
input.AddWheelDelta(GetSignedHighWord(wParam) / (float)WHEEL_DELTA);
return IntPtr.Zero;
case WM_MBUTTONDOWN:
input.SetMouseButton(MouseButton.Middle, true);
SetCapture(handle);
return IntPtr.Zero;
case WM_MBUTTONUP:
input.SetMouseButton(MouseButton.Middle, false);
ReleaseCapture();
return IntPtr.Zero;
case WM_RBUTTONDOWN:
input.SetMouseButton(MouseButton.Right, true);
SetCapture(handle);
return IntPtr.Zero;
case WM_RBUTTONUP:
input.SetMouseButton(MouseButton.Right, false);
ReleaseCapture();
return IntPtr.Zero;
case WM_KEYDOWN:
SetKey(input, wParam, true);
return IntPtr.Zero;
case WM_KEYUP:
SetKey(input, wParam, false);
return IntPtr.Zero;
}
return DefWindowProc(handle, message, wParam, lParam);
@ -90,7 +134,7 @@ public sealed class Win32Window : IDisposable
ShowWindow(handle, SW_SHOW);
UpdateWindow(handle);
return new Win32Window(handle, className, windowProcedure, width, height);
return new Win32Window(handle, className, windowProcedure, input, width, height);
}
public bool PumpMessages()
@ -183,6 +227,37 @@ public sealed class Win32Window : IDisposable
[DllImport("user32.dll", SetLastError = true)]
private static extern bool GetClientRect(IntPtr handle, out RECT rect);
[DllImport("user32.dll")]
private static extern IntPtr SetCapture(IntPtr handle);
[DllImport("user32.dll")]
private static extern bool ReleaseCapture();
private static void SetKey(InputState input, IntPtr virtualKey, bool isDown)
{
switch (virtualKey.ToInt32())
{
case VK_W:
input.SetKey(KeyCode.W, isDown);
break;
case VK_A:
input.SetKey(KeyCode.A, isDown);
break;
case VK_S:
input.SetKey(KeyCode.S, isDown);
break;
case VK_D:
input.SetKey(KeyCode.D, isDown);
break;
}
}
private static short GetSignedLowWord(IntPtr value) => unchecked((short)((long)value & 0xFFFF));
private static short GetSignedHighWord(IntPtr value) => unchecked((short)(((long)value >> 16) & 0xFFFF));
private static short GetSignedHighWord(UIntPtr value) => unchecked((short)(((long)value >> 16) & 0xFFFF));
private delegate IntPtr WndProc(IntPtr handle, uint message, IntPtr wParam, IntPtr lParam);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]

View file

@ -65,7 +65,7 @@ public sealed class D3D11Renderer : IDisposable
vertexShader = device.CreateVertexShader(vertexShaderBytecode.Span, null);
pixelShader = device.CreatePixelShader(pixelShaderBytecode.Span, null);
frameConstantBuffer = device.CreateBuffer(
32,
48,
BindFlags.ConstantBuffer,
ResourceUsage.Default,
CpuAccessFlags.None,
@ -80,7 +80,9 @@ public sealed class D3D11Renderer : IDisposable
frame.TimeSeconds,
frame.Grid.Radius,
(float3)frame.CameraPosition,
0.0f);
0.0f,
(float2)frame.Grid.Center,
new float2(0.0f, 0.0f));
context.OMSetRenderTargets(renderTargetView);
context.RSSetViewport(0.0f, 0.0f, width, height, 0.0f, 1.0f);
@ -121,5 +123,7 @@ public sealed class D3D11Renderer : IDisposable
float TimeSeconds,
float GridRadius,
float3 CameraPosition,
float Pad0);
float CameraPad,
float2 GridCenter,
float2 Pad0);
}

View file

@ -5,6 +5,8 @@ cbuffer AquariumFrame : register(b0)
float gridRadius;
float3 cameraPosition;
float _pad0;
float2 gridCenter;
float2 _pad1;
};
static const float PI = 3.14159265359;
@ -94,7 +96,7 @@ float3 planetCenter(int index)
float f = (float)index;
float angle = f * 0.8975979 + timeSeconds * (0.08 + 0.011 * f);
float radius = 4.1 + f * 0.77;
float2 xy = float2(cos(angle), sin(angle)) * radius;
float2 xy = gridCenter + float2(cos(angle), sin(angle)) * radius;
return float3(xy, 1.15 + planetRadius(index) * 0.72);
}
@ -150,7 +152,7 @@ float gridLine(float2 p)
float terrainMask(float2 p)
{
float r = length(p) / gridRadius;
float r = length(p - gridCenter) / gridRadius;
return 1.0 - smoothstep(0.78, 1.0, r);
}
@ -235,7 +237,7 @@ float4 AquariumPS(VertexOut input) : SV_Target
float2 pixel = input.uv * resolution;
float2 ndc = (pixel * 2.0 - resolution) / resolution.y;
float3 target = float3(0.0, 0.0, 0.0);
float3 target = float3(gridCenter, 0.0);
float3 forward = normalize(target - cameraPosition);
float3 right = normalize(cross(forward, float3(0.0, 0.0, 1.0)));
float3 up = cross(right, forward);

View file

@ -1,4 +1,5 @@
using System.Numerics;
using Aquarium.Engine.Input;
namespace Aquarium.Engine;
@ -50,4 +51,58 @@ public sealed class OrbitCameraRig
{
Target += gridDelta;
}
public void ApplyInput(InputState input, float deltaSeconds)
{
if (input.MiddleMouseDown)
{
Orbit(input.MouseDelta.X * -0.0065f, input.MouseDelta.Y * 0.0065f);
}
if (MathF.Abs(input.WheelDelta) > 0.0f)
{
Zoom(input.WheelDelta);
}
var keyboardPan = Vector2.Zero;
if (input.IsKeyDown(KeyCode.W))
{
keyboardPan.Y += 1.0f;
}
if (input.IsKeyDown(KeyCode.S))
{
keyboardPan.Y -= 1.0f;
}
if (input.IsKeyDown(KeyCode.D))
{
keyboardPan.X += 1.0f;
}
if (input.IsKeyDown(KeyCode.A))
{
keyboardPan.X -= 1.0f;
}
if (keyboardPan != Vector2.Zero)
{
Pan(GridPanVector(Vector2.Normalize(keyboardPan) * Distance * 0.45f * deltaSeconds));
}
if (input.RightMouseDown && input.MouseDelta != Vector2.Zero)
{
Pan(GridPanVector(new Vector2(input.MouseDelta.X, -input.MouseDelta.Y) * Distance * 0.0018f));
}
}
private Vector2 GridPanVector(Vector2 localDelta)
{
var forward = Vector2.Normalize(new Vector2(
MathF.Cos(YawRadians + MathF.PI),
MathF.Sin(YawRadians + MathF.PI)));
var right = new Vector2(forward.Y, -forward.X);
return right * localDelta.X + forward * localDelta.Y;
}
}