diff --git a/README.md b/README.md index e347021..26e51d6 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/docs/input.md b/docs/input.md new file mode 100644 index 0000000..f1de5d5 --- /dev/null +++ b/docs/input.md @@ -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. diff --git a/src/Aquarium.Engine/AquariumHost.cs b/src/Aquarium.Engine/AquariumHost.cs index 3bcf3ab..640c716 100644 --- a/src/Aquarium.Engine/AquariumHost.cs +++ b/src/Aquarium.Engine/AquariumHost.cs @@ -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) diff --git a/src/Aquarium.Engine/AquariumRuntime.cs b/src/Aquarium.Engine/AquariumRuntime.cs index fd82ca9..fd9f7e1 100644 --- a/src/Aquarium.Engine/AquariumRuntime.cs +++ b/src/Aquarium.Engine/AquariumRuntime.cs @@ -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); } } diff --git a/src/Aquarium.Engine/Input/InputState.cs b/src/Aquarium.Engine/Input/InputState.cs new file mode 100644 index 0000000..473518b --- /dev/null +++ b/src/Aquarium.Engine/Input/InputState.cs @@ -0,0 +1,76 @@ +using System.Numerics; + +namespace Aquarium.Engine.Input; + +public sealed class InputState +{ + private readonly HashSet 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, +} diff --git a/src/Aquarium.Engine/Platform/Win32Window.cs b/src/Aquarium.Engine/Platform/Win32Window.cs index cd31642..50d799d 100644 --- a/src/Aquarium.Engine/Platform/Win32Window.cs +++ b/src/Aquarium.Engine/Platform/Win32Window.cs @@ -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)] diff --git a/src/Aquarium.Engine/Render/D3D11Renderer.cs b/src/Aquarium.Engine/Render/D3D11Renderer.cs index 6b55aa5..fd780ef 100644 --- a/src/Aquarium.Engine/Render/D3D11Renderer.cs +++ b/src/Aquarium.Engine/Render/D3D11Renderer.cs @@ -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); } diff --git a/src/Aquarium.Engine/Render/Shaders/Aquarium.hlsl b/src/Aquarium.Engine/Render/Shaders/Aquarium.hlsl index 1b85e75..1c47db6 100644 --- a/src/Aquarium.Engine/Render/Shaders/Aquarium.hlsl +++ b/src/Aquarium.Engine/Render/Shaders/Aquarium.hlsl @@ -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); diff --git a/src/Aquarium.Engine/World/OrbitCameraRig.cs b/src/Aquarium.Engine/World/OrbitCameraRig.cs index 149794c..96e6394 100644 --- a/src/Aquarium.Engine/World/OrbitCameraRig.cs +++ b/src/Aquarium.Engine/World/OrbitCameraRig.cs @@ -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; + } }