Hook Win32 camera input
This commit is contained in:
parent
1c6277b868
commit
a68fa3bb8b
9 changed files with 261 additions and 16 deletions
|
|
@ -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
21
docs/input.md
Normal 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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
76
src/Aquarium.Engine/Input/InputState.cs
Normal file
76
src/Aquarium.Engine/Input/InputState.cs
Normal 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,
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue