This commit is contained in:
2024-12-16 04:28:45 +03:00
parent ef922486eb
commit dbe7aebd4f
57 changed files with 895 additions and 374 deletions

View File

@@ -1,7 +1,5 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DoomDeathmatch", "DoomDeathmatch\DoomDeathmatch.csproj", "{7AE4D009-6590-4E44-9B68-FE37B6650F70}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine", "Engine\Engine.csproj", "{5EE134DE-2275-40C0-8B9D-4EFF22474F63}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EngineTests", "EngineTests\EngineTests.csproj", "{CC28C26C-0998-4C13-8855-978658B8B0D6}"
@@ -12,16 +10,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterConsole", "Present
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PresenterNative", "PresenterNative\PresenterNative.csproj", "{3B8B7867-5B38-4013-A366-159083F5C095}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DoomDeathmatch", "DoomDeathmatch\DoomDeathmatch.csproj", "{4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7AE4D009-6590-4E44-9B68-FE37B6650F70}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7AE4D009-6590-4E44-9B68-FE37B6650F70}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7AE4D009-6590-4E44-9B68-FE37B6650F70}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7AE4D009-6590-4E44-9B68-FE37B6650F70}.Release|Any CPU.Build.0 = Release|Any CPU
{5EE134DE-2275-40C0-8B9D-4EFF22474F63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5EE134DE-2275-40C0-8B9D-4EFF22474F63}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5EE134DE-2275-40C0-8B9D-4EFF22474F63}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -42,5 +38,9 @@ Global
{3B8B7867-5B38-4013-A366-159083F5C095}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3B8B7867-5B38-4013-A366-159083F5C095}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3B8B7867-5B38-4013-A366-159083F5C095}.Release|Any CPU.Build.0 = Release|Any CPU
{4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4FA5E1F8-B647-4764-9147-B6F5A4E9D1D0}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -1,21 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Engine\Engine.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Serilog" Version="4.1.0"/>
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0"/>
<PackageReference Include="System.Drawing.Common" Version="8.0.10"/>
<ProjectReference Include="..\Engine\Engine.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,33 @@
using Engine.Input;
using OpenTK.Mathematics;
namespace DoomDeathmatch.Component;
public class ControllerComponent : Engine.Scene.Component.Component
{
public float Speed { get; set; } = 10.0f;
private readonly IInputHandler _inputHandler = Engine.Engine.Instance.InputHandler!;
public override void Update(double parDeltaTime)
{
var movement = Vector3.Zero;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.W))
movement.Z += 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.S))
movement.Z -= 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.A))
movement.X -= 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.D))
movement.X += 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.Space))
movement.Y += 1;
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.Shift))
movement.Y -= 1;
if (movement.LengthSquared > 0)
movement.Normalize();
GameObject.Transform.Translation += movement * Speed * (float)parDeltaTime;
}
}

View File

@@ -0,0 +1,22 @@
using Engine.Input;
using OpenTK.Mathematics;
namespace DoomDeathmatch.Component;
public class RotateComponent : Engine.Scene.Component.Component
{
private readonly IInputHandler _inputHandler = Engine.Engine.Instance.InputHandler!;
public override void Update(double parDeltaTime)
{
if (_inputHandler.IsKeyPressed(KeyboardButtonCode.Q))
GameObject.Transform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitY, (float)parDeltaTime * 0.5f);
else if (_inputHandler.IsKeyPressed(KeyboardButtonCode.E))
GameObject.Transform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitY, -(float)parDeltaTime * 0.5f);
if (_inputHandler.IsMouseButtonPressed(MouseButtonCode.Left))
GameObject.Transform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitZ, (float)parDeltaTime * 0.5f);
else if (_inputHandler.IsMouseButtonPressed(MouseButtonCode.Right))
GameObject.Transform.Rotation *= Quaternion.FromAxisAngle(Vector3.UnitZ, -(float)parDeltaTime * 0.5f);
}
}

View File

@@ -0,0 +1,37 @@
using DoomDeathmatch.Component;
using Engine.Asset.Mesh.Loader;
using Engine.Scene;
using Engine.Scene.Component.BuiltIn;
using Engine.Scene.Component.BuiltIn.Renderer;
using OpenTK.Mathematics;
namespace DoomDeathmatch;
public static class DoomDeathmatch
{
public static void Initialize(Engine.Engine parEngine)
{
parEngine.SceneManager.TransitionTo(MainScene());
}
private static Scene MainScene()
{
var cameraObject = new GameObject();
cameraObject.Transform.Translation.Z = -6;
cameraObject.AddComponent<PerspectiveCamera>();
cameraObject.AddComponent<ControllerComponent>();
using var reader = new StreamReader("../DoomDeathmatch/asset/model/test2.obj");
var box2dRenderer = new GameObject();
box2dRenderer.AddComponent(new MeshRenderer { Mesh = ObjMeshLoader.Load(reader) });
box2dRenderer.AddComponent<Box2DRenderer>();
box2dRenderer.AddComponent<RotateComponent>();
var scene = new Scene();
scene.Add(cameraObject);
scene.Add(box2dRenderer);
return scene;
}
}

View File

@@ -1,12 +0,0 @@
using System.Diagnostics;
using System.Drawing;
using System.Runtime.InteropServices;
namespace DoomDeathmatch;
internal abstract class Program
{
public static void Main(string[] args)
{
}
}

View File

@@ -1,17 +0,0 @@
using Engine.Graphics.Buffer.Vertex;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
using Half = System.Half;
namespace DoomDeathmatch;
public struct QuadVertex : IVertex
{
[Vertex(VertexAttribType.Float, 2)] public Vector2 Position2;
[Vertex(VertexAttribType.Float, 2)] public Vector2 Position;
[Vertex(VertexAttribType.Float, 2)] public Vector2 Position4;
[Vertex(VertexAttribType.Float, 2)] public Vector2 Position3;
}

View File

@@ -6,29 +6,40 @@ uniform mat4 uViewMatrix;
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in mat4 aModelMatrix;
layout (location = 2) in vec2 aUV;
layout (location = 3) in int aTextureId;
layout (location = 4) in mat4 aModelMatrix;
layout (location = 0) out vec2 oTexCoords;
layout (location = 0) out vec2 oUV;
layout (location = 1) out int oTextureId;
layout (location = 2) out vec3 oNormal;
void main()
{
oUV = aUV;
oTextureId = aTextureId;
oNormal = aNormal;
gl_Position = uProjectionMatrix * uViewMatrix * aModelMatrix * vec4(aPos, 1.0);
oTexCoords = aTexCoords;
}
// #type fragment
#version 460 core
uniform sampler2D uTexture;
uniform sampler2D uTexture[16];
layout (location = 0) in vec2 iTexCoords;
layout (location = 0) in vec2 iUV;
layout (location = 1) flat in int iTextureId;
layout (location = 2) in vec3 iNormal;
layout (location = 0) out vec4 FragColor;
void main()
{
FragColor = texture(uTexture, iTexCoords);
vec3 lightColor = vec3(iUV, 0);
FragColor = vec4(lightColor, 1.0);
if (iTextureId >= 0)
FragColor *= texture(uTexture[iTextureId], iUV);
if (FragColor.a == 0.0)
discard;
}

View File

@@ -2,9 +2,9 @@
public interface IMeshLoader
{
public Mesh LoadMesh(string parPath, MeshLoaderParameters parAmeters = MeshLoaderParameters._default);
public Mesh LoadMesh(TextReader parReader, MeshLoaderParameters parParameters = MeshLoaderParameters.Default);
public static Mesh Optimize(Mesh parMesh)
internal static Mesh Optimize(Mesh parMesh)
{
var optimizedMesh = new Mesh();
var vertexMap = new Dictionary<Mesh.Vertex, uint>();

View File

@@ -3,10 +3,10 @@
[Flags]
public enum MeshLoaderParameters
{
_none = 0,
_loadNormals = 1 << 0,
_loadUVs = 1 << 1,
_optimize = 1 << 2,
None = 0,
LoadNormals = 1 << 0,
LoadUVs = 1 << 1,
Optimize = 1 << 2,
_default = _loadNormals | _loadUVs | _optimize
Default = LoadNormals | LoadUVs | Optimize
}

View File

@@ -7,12 +7,16 @@ public class ObjMeshLoader : IMeshLoader
{
private static readonly ObjMeshLoader _instance = new();
public static Mesh Load(string parPath, MeshLoaderParameters parAmeters = MeshLoaderParameters._default)
public static Mesh Load(TextReader parReader, MeshLoaderParameters parParameters = MeshLoaderParameters.Default)
{
return _instance.LoadMesh(parPath, parAmeters);
return _instance.LoadMesh(parReader, parParameters);
}
public Mesh LoadMesh(string parPath, MeshLoaderParameters parAmeters = MeshLoaderParameters._default)
private ObjMeshLoader()
{
}
public Mesh LoadMesh(TextReader parReader, MeshLoaderParameters parParameters = MeshLoaderParameters.Default)
{
var mesh = new Mesh();
@@ -21,13 +25,11 @@ public class ObjMeshLoader : IMeshLoader
var tempUVs = new List<Vector2>();
var index = 0u;
var loadNormals = parAmeters.HasFlag(MeshLoaderParameters._loadNormals);
var loadUVs = parAmeters.HasFlag(MeshLoaderParameters._loadUVs);
using var reader = new StreamReader(parPath);
while (reader.ReadLine() is { } line)
var loadNormals = parParameters.HasFlag(MeshLoaderParameters.LoadNormals);
var loadUVs = parParameters.HasFlag(MeshLoaderParameters.LoadUVs);
while (parReader.ReadLine() is { } line)
{
string[]? parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 0 || parts[0].StartsWith('#'))
{
@@ -62,7 +64,7 @@ public class ObjMeshLoader : IMeshLoader
case "f":
for (var i = 1; i <= 3; i++)
{
string[]? faceComponents = parts[i].Split('/');
var faceComponents = parts[i].Split('/');
var meshVertex = new Mesh.Vertex { _position = tempVertices[int.Parse(faceComponents[0]) - 1] };
if (loadUVs && faceComponents.Length > 1 && faceComponents[1] != "")
@@ -83,7 +85,7 @@ public class ObjMeshLoader : IMeshLoader
}
}
if (parAmeters.HasFlag(MeshLoaderParameters._optimize))
if (parParameters.HasFlag(MeshLoaderParameters.Optimize))
{
mesh = IMeshLoader.Optimize(mesh);
}

View File

@@ -7,20 +7,25 @@ public class StlMeshLoader : IMeshLoader
{
private static readonly StlMeshLoader _instance = new();
public static Mesh Load(string parPath, MeshLoaderParameters parAmeters = MeshLoaderParameters._default)
public static Mesh Load(TextReader parReader, MeshLoaderParameters parParameters = MeshLoaderParameters.Default)
{
return _instance.LoadMesh(parPath, parAmeters);
return _instance.LoadMesh(parReader, parParameters);
}
public Mesh LoadMesh(string parPath, MeshLoaderParameters parAmeters = MeshLoaderParameters._default)
private StlMeshLoader()
{
}
public Mesh LoadMesh(TextReader parReader, MeshLoaderParameters parParameters = MeshLoaderParameters.Default)
{
var mesh = new Mesh();
var loadNormals = parParameters.HasFlag(MeshLoaderParameters.LoadNormals);
var currentNormal = new Vector3();
var index = 0u;
using var reader = new StreamReader(parPath);
while (reader.ReadLine() is { } line)
while (parReader.ReadLine() is { } line)
{
line = line.Trim();
@@ -35,11 +40,11 @@ public class StlMeshLoader : IMeshLoader
break;
}
string[]? parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
switch (parts[0])
{
case "facet" when parts[1] == "normal" && parAmeters.HasFlag(MeshLoaderParameters._loadNormals):
case "facet" when parts[1] == "normal" && loadNormals:
currentNormal = new Vector3(
float.Parse(parts[2], CultureInfo.InvariantCulture),
float.Parse(parts[3], CultureInfo.InvariantCulture),
@@ -61,7 +66,7 @@ public class StlMeshLoader : IMeshLoader
}
}
if (parAmeters.HasFlag(MeshLoaderParameters._optimize))
if (parParameters.HasFlag(MeshLoaderParameters.Optimize))
{
mesh = IMeshLoader.Optimize(mesh);
}

View File

@@ -12,6 +12,8 @@ namespace Engine;
public sealed class Engine
{
public static Engine Instance { get; private set; } = null!;
public Renderer Renderer { get; }
public SceneManager SceneManager { get; } = new();
@@ -36,10 +38,10 @@ public sealed class Engine
}
}
internal IInputHandler? InputHandler
public IInputHandler? InputHandler
{
get => _inputHandler;
set => _inputHandler = value;
internal set => _inputHandler = value;
}
private readonly ILogger _logger;
@@ -51,6 +53,13 @@ public sealed class Engine
public Engine(int parWidth, int parHeight, bool parHeadless, string parTitle, ILogger parLogger)
{
if (Instance != null)
{
throw new InvalidOperationException("Engine is already running");
}
Instance = this;
var settings = new NativeWindowSettings
{
ClientSize = parHeadless ? new Vector2i(1, 1) : new Vector2i(parWidth, parHeight),
@@ -67,46 +76,69 @@ public sealed class Engine
_logger = Log.ForContext<Engine>();
}
private readonly object _sceneLock = new();
public void Run()
{
_updateThread = new Thread(RunUpdate);
_updateThread = new Thread(RunUpdate) { Name = "UpdateThread" };
_updateThread.Start();
var timer = Stopwatch.StartNew();
var deltaTime = 0.0;
RunRender();
_updateThread.Join();
}
private void RunRender()
{
while (!Presenter?.IsExiting ?? false)
{
Renderer.StartFrame();
GL.ClearColor(0.6f, 0.0f, 0.6f, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
var view = Matrix4.Identity;
var projection = Matrix4.Identity;
lock (_sceneLock)
{
var camera = SceneManager.CurrentScene?.MainCamera;
if (camera != null)
{
camera.ScreenSize = new Vector2i(Renderer.ViewportWidth, Renderer.ViewportHeight);
view = camera.View;
projection = camera.Projection;
}
Renderer.QuadRenderer.Commit(
Matrix4.CreateRotationZ((float)deltaTime),
new Vector4((MathF.Sin((float)deltaTime) + 1.0f) / 2, (MathF.Sin((float)deltaTime) + 1.0f) / 2,
(MathF.Sin((float)deltaTime) + 1.0f) / 2, 1.0f));
Renderer.QuadRenderer.Render(Matrix4.Identity, Matrix4.Identity);
SceneManager.Render();
}
Renderer.QuadRenderer.Render(projection, view);
Renderer.QuadRenderer.Reset();
Renderer.GlobalMeshRenderer.Render(projection, view);
Renderer.GlobalMeshRenderer.Reset();
Renderer.EndFrame();
Presenter!.Present(Renderer.TextureInternal);
Presenter!.Update(deltaTime);
deltaTime = timer.Elapsed.TotalSeconds;
Presenter!.Render();
}
_updateThread.Join();
}
private void RunUpdate()
{
var timer = Stopwatch.StartNew();
var deltaTime = 0.0;
while (!Presenter?.IsExiting ?? false)
{
Presenter!.Update(deltaTime);
InputHandler?.Update(deltaTime);
lock (_sceneLock)
{
SceneManager.Update(deltaTime);
timer.Restart();
}
deltaTime = timer.Elapsed.TotalSeconds;
timer.Restart();
}
}

View File

@@ -23,8 +23,6 @@ public sealed class EngineBuilder
private string? _logFilePath;
private LogEventLevel _logLevel = LogEventLevel.Information;
public EngineBuilder() { }
public EngineBuilder Title(string parTitle)
{
_title = parTitle;
@@ -53,12 +51,6 @@ public sealed class EngineBuilder
return this;
}
public EngineBuilder Presenter(IPresenter parPresenter)
{
_presenterFunc = _ => parPresenter;
return this;
}
public EngineBuilder Presenter(Func<Engine, IPresenter> parPresenterFunc)
{
_presenterFunc = parPresenterFunc;

View File

@@ -8,14 +8,14 @@ namespace Engine.Graphics.Buffer;
public class VertexArray : OpenGlObject
{
// private IndexBuffer? _boundIndexBuffer;
// private readonly Dictionary<int, VertexBuffer<IVertex>> _boundVertexBuffers = new();
private int _enabledAttribs = 0;
public VertexArray()
{
GL.CreateVertexArrays(1, out int handle);
Handle = handle;
Log.Debug("Vertex array {Handle} created", Handle);
}
public void BindIndexBuffer(IndexBuffer parBuffer)
@@ -28,18 +28,11 @@ public class VertexArray : OpenGlObject
public void BindVertexBuffer<T>(VertexBuffer<T> parBuffer, int parBindingIndex = 0, int parDivisor = 0)
where T : struct, IVertex
{
if (parBindingIndex < 0)
{
throw new ArgumentException("Binding index must be greater than 0");
}
if (parDivisor < 0)
{
throw new ArgumentException("Divisor must be greater than 0");
}
ArgumentOutOfRangeException.ThrowIfNegative(parBindingIndex);
ArgumentOutOfRangeException.ThrowIfNegative(parDivisor);
var stride = Marshal.SizeOf<T>();
IOrderedEnumerable<FieldInfo>? fields = IVertex.GetFields<T>();
var fields = IVertex.GetFields<T>();
GL.VertexArrayVertexBuffer(Handle, parBindingIndex, parBuffer.Handle, 0, stride);

View File

@@ -10,12 +10,9 @@ public class Framebuffer : OpenGlObject
public int Width
{
get => _width;
protected set
private set
{
if (value <= 0)
{
throw new ArgumentException("Width must be greater than 0");
}
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
_width = value;
}
@@ -24,12 +21,9 @@ public class Framebuffer : OpenGlObject
public int Height
{
get => _height;
protected set
private set
{
if (value <= 0)
{
throw new ArgumentException("Height must be greater than 0");
}
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(value);
_height = value;
}
@@ -51,18 +45,9 @@ public class Framebuffer : OpenGlObject
GL.CreateFramebuffers(1, out int handle);
Handle = handle;
foreach (KeyValuePair<FramebufferAttachment, IFramebufferAttachment> attachment in _attachments)
foreach (var attachment in _attachments)
{
switch (attachment.Value.Type)
{
case IFramebufferAttachment.AttachmentType.Texture:
GL.NamedFramebufferTexture(Handle, attachment.Key, attachment.Value.Handle, 0);
break;
case IFramebufferAttachment.AttachmentType.Renderbuffer:
GL.NamedFramebufferRenderbuffer(Handle, attachment.Key, RenderbufferTarget.Renderbuffer,
attachment.Value.Handle);
break;
}
attachment.Value.Attach(this, attachment.Key);
}
var status = GL.CheckNamedFramebufferStatus(Handle, FramebufferTarget.Framebuffer);

View File

@@ -55,7 +55,7 @@ public class FramebufferBuilder(int parWidth, int parHeight)
public Framebuffer Build()
{
Dictionary<FramebufferAttachment, IFramebufferAttachment>? attachments = _attachments
var attachments = _attachments
.ToDictionary(
parAttachment => parAttachment.FramebufferAttachment,
parAttachment => parAttachment.Create(parWidth, parHeight)

View File

@@ -1,15 +1,12 @@
namespace Engine.Graphics.Framebuffer;
using OpenTK.Graphics.OpenGL;
namespace Engine.Graphics.Framebuffer;
public interface IFramebufferAttachment
{
public AttachmentType Type { get; }
internal int Handle { get; }
public void Resize(int parWidth, int parHeight);
public enum AttachmentType
{
Texture,
Renderbuffer
}
internal void Attach(Framebuffer parFramebuffer, FramebufferAttachment parAttachment);
}

View File

@@ -7,8 +7,6 @@ public class Renderbuffer : OpenGlObject, IFramebufferAttachment
public int Width { get; private set; }
public int Height { get; private set; }
public IFramebufferAttachment.AttachmentType Type => IFramebufferAttachment.AttachmentType.Renderbuffer;
private readonly RenderbufferStorage _format;
public Renderbuffer(int parWidth, int parHeight, RenderbufferStorage parFormat)
@@ -36,6 +34,11 @@ public class Renderbuffer : OpenGlObject, IFramebufferAttachment
GL.NamedRenderbufferStorage(Handle, _format, Width, Height);
}
public void Attach(Framebuffer parFramebuffer, FramebufferAttachment parAttachment)
{
GL.NamedFramebufferRenderbuffer(parFramebuffer.Handle, parAttachment, RenderbufferTarget.Renderbuffer, Handle);
}
public override void Bind()
{
GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, Handle);

View File

@@ -4,7 +4,7 @@ using OpenTK.Windowing.Common;
namespace Engine.Graphics;
public interface IPresenter : IUpdate
public interface IPresenter : IUpdate, IRender
{
public bool IsExiting { get; }
public int Width { get; }

View File

@@ -12,11 +12,19 @@ public abstract class OpenGlObject
protected abstract void Destroy();
~OpenGlObject()
{
if (Handle == -1)
{
return;
}
Engine.Instance.Renderer.Schedule(() =>
{
Destroy();
Log.Debug("OpenGL object {Handle} destroyed", Handle);
Handle = -1;
});
}
}

View File

@@ -12,10 +12,10 @@ public abstract class InstancedRenderer<C, I>
{
protected readonly Renderer _renderer;
protected readonly IndexBuffer _indexBuffer;
protected readonly VertexBuffer<C> _commonVertexBuffer;
protected readonly VertexBuffer<I> _instanceVertexBuffer;
protected readonly VertexArray _vertexArray;
private readonly IndexBuffer _indexBuffer;
private readonly VertexBuffer<C> _commonVertexBuffer;
private readonly VertexBuffer<I> _instanceVertexBuffer;
private readonly VertexArray _vertexArray;
protected readonly int _instanceCount;
protected int _queuedInstanceCount;

View File

@@ -0,0 +1,54 @@
using Engine.Graphics.Shader;
using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Mesh;
public class GlobalMeshRenderer(Renderer parRenderer, int parMaxInstanceCount)
{
private readonly Dictionary<Asset.Mesh.Mesh, MeshRenderer> _meshRenderers = new();
private readonly HashSet<Asset.Mesh.Mesh> _frameMeshes = [];
private readonly Program _program = ProgramLoader.LoadFromSource(ShaderResource.Mesh);
public void Commit(Asset.Mesh.Mesh parMesh, Matrix4 parModelMatrix)
{
if (_meshRenderers.TryGetValue(parMesh, out var meshRenderer))
{
meshRenderer.Commit(parModelMatrix);
}
else
{
var newMeshRenderer = new MeshRenderer(parRenderer, parMesh, parMaxInstanceCount, _program);
newMeshRenderer.Commit(parModelMatrix);
_meshRenderers.Add(parMesh, newMeshRenderer);
}
_frameMeshes.Add(parMesh);
}
public void Render(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix)
{
foreach (var meshRenderer in _meshRenderers.Values)
{
meshRenderer.Render(parProjectionMatrix, parViewMatrix);
}
}
public void Reset()
{
foreach (var meshRenderer in _meshRenderers.Values)
{
meshRenderer.Reset();
}
var meshes = _meshRenderers.Keys;
var unusedMeshes = meshes.Where(parMesh => !_frameMeshes.Contains(parMesh));
foreach (var unusedMesh in unusedMeshes)
{
_meshRenderers.Remove(unusedMesh);
}
_frameMeshes.Clear();
}
}

View File

@@ -6,5 +6,6 @@ namespace Engine.Graphics.Render.Mesh;
public struct MeshInstanceVertex : IVertex
{
[Vertex(VertexAttribType.Int)] public int _textureId;
[Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix;
}

View File

@@ -1,26 +1,48 @@
using Engine.Graphics.Shader;
using Engine.Graphics.Texture;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
namespace Engine.Graphics.Render.Mesh;
public class MeshRenderer : InstancedRenderer<Asset.Mesh.Mesh.Vertex, MeshInstanceVertex>
{
public MeshRenderer(Renderer parRenderer, int parInstanceCount, Asset.Mesh.Mesh parMesh)
: base(parRenderer, PrimitiveType.Triangles, parInstanceCount, parMesh.Indices.ToArray(),
public class MeshRenderer(Renderer parRenderer, Asset.Mesh.Mesh parMesh, int parInstanceCount, Program parProgram)
: InstancedRenderer<Asset.Mesh.Mesh.Vertex, MeshInstanceVertex>(
parRenderer, PrimitiveType.Triangles,
parInstanceCount,
parMesh.Indices.ToArray(),
parMesh.Vertices.ToArray(),
ProgramLoader.LoadFromSource(ShaderResource.Mesh))
{
}
parProgram
)
{
private readonly TextureUnitMap _textureUnitMap = new(16);
private readonly int[] _textureUnitIndices = new int[16];
public void Commit(Matrix4 parModelMatrix)
public void Commit(Matrix4 parModelMatrix, Texture.Texture? parTexture = null)
{
if (_queuedInstanceCount >= _instanceCount)
{
throw new InvalidOperationException("Instance count exceeded");
}
var textureId = -1;
if (parTexture != null)
{
textureId = _textureUnitMap.GetUnit(parTexture);
}
_instanceVertices[_queuedInstanceCount]._textureId = textureId;
_instanceVertices[_queuedInstanceCount]._modelMatrix = parModelMatrix;
_queuedInstanceCount++;
}
protected override void SetAdditionalUniforms(Program parProgram)
{
foreach (var (texture, unit) in _textureUnitMap.Textures)
{
texture.BindUnit(unit);
_textureUnitIndices[unit] = unit;
}
parProgram.SetUniform("uTexture", _textureUnitIndices);
}
}

View File

@@ -1,4 +1,5 @@
using Engine.Graphics.Pixel;
using Engine.Graphics.Render.Mesh;
using Engine.Graphics.Render.Quad;
using OpenTK.Graphics.OpenGL;
@@ -9,11 +10,15 @@ public class Renderer
internal Texture.Texture TextureInternal => _framebuffer.TextureInternal!;
public QuadRenderer QuadRenderer { get; }
public GlobalMeshRenderer GlobalMeshRenderer { get; }
public int ViewportWidth => _framebuffer.Width;
public int ViewportHeight => _framebuffer.Height;
private readonly Framebuffer.Framebuffer _framebuffer;
private readonly Thread _renderThread;
private readonly Queue<Action> _scheduleActions = new();
public Renderer(int parWidth, int parHeight)
{
Thread.CurrentThread.Name = "RendererThread";
@@ -28,6 +33,7 @@ public class Renderer
.Build();
QuadRenderer = new QuadRenderer(this, 1024 * 8);
GlobalMeshRenderer = new GlobalMeshRenderer(this, 1024);
}
private void InitializeOpenGl(int parWidth, int parHeight)
@@ -54,15 +60,25 @@ public class Renderer
return;
}
throw new InvalidOperationException("Renderer is not on render thread");
throw new InvalidOperationException("Not on render thread");
#endif
}
internal void Schedule(Action parAction)
{
_scheduleActions.Enqueue(parAction);
}
internal void StartFrame()
{
EnsureRenderThread();
RunScheduledActions();
_framebuffer.Bind();
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
}
internal void EndFrame()
@@ -77,4 +93,12 @@ public class Renderer
_framebuffer.Resize(parWidth, parHeight);
GL.Viewport(0, 0, parWidth, parHeight);
}
private void RunScheduledActions()
{
while (_scheduleActions.TryDequeue(out var action))
{
action();
}
}
}

View File

@@ -7,8 +7,6 @@ namespace Engine.Graphics.Texture;
public class DynamicTexture : Texture, IFramebufferAttachment
{
public IFramebufferAttachment.AttachmentType Type => IFramebufferAttachment.AttachmentType.Texture;
private readonly PixelFormat _format;
private readonly PixelType _type;
private readonly PixelInternalFormat _internalFormat;
@@ -49,4 +47,9 @@ public class DynamicTexture : Texture, IFramebufferAttachment
Log.Debug("Texture {Handle} resized to {Width}x{Height}", Handle, Width, Height);
}
public void Attach(Framebuffer.Framebuffer parFramebuffer, FramebufferAttachment parAttachment)
{
GL.NamedFramebufferTexture(parFramebuffer.Handle, parAttachment, Handle, 0);
}
}

View File

@@ -4,10 +4,9 @@ namespace Engine.Input;
public interface IInputHandler : IUpdate
{
bool IsKeyPressed(KeyCode parKeyCode);
bool IsKeyJustPressed(KeyCode parKeyCode);
bool IsKeyRepeat(KeyCode parKeyCode);
bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode);
bool IsKeyJustPressed(KeyboardButtonCode parKeyboardButtonCode);
bool IsMouseButtonPressed(MouseButton parButton);
bool IsMouseButtonJustPressed(MouseButton parButton);
bool IsMouseButtonPressed(MouseButtonCode parButtonCode);
bool IsMouseButtonJustPressed(MouseButtonCode parButtonCode);
}

View File

@@ -1,6 +1,6 @@
namespace Engine.Input;
public enum KeyCode
public enum KeyboardButtonCode
{
A,
B,
@@ -48,5 +48,7 @@ public enum KeyCode
Home,
End,
PageUp,
PageDown
PageDown,
TotalCount = PageDown + 1
}

View File

@@ -1,13 +0,0 @@
namespace Engine.Input;
public enum MouseButton
{
Left,
Right,
Middle,
Button4,
Button5,
Button6,
Button7
}

View File

@@ -0,0 +1,10 @@
namespace Engine.Input;
public enum MouseButtonCode
{
Left,
Right,
Middle,
TotalCount = Middle + 1
}

View File

@@ -0,0 +1,99 @@
using OpenTK.Windowing.GraphicsLibraryFramework;
namespace Engine.Input;
public class WindowInputHandler(Window parWindow) : IInputHandler
{
private KeyboardState _previousKeyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
private KeyboardState _keyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
public void Update(double parDeltaTime)
{
_previousKeyboardState = _keyboardState;
_keyboardState = parWindow.NativeWindow.KeyboardState.GetSnapshot();
}
public bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode)
{
return _keyboardState.IsKeyDown(MapKeyCode(parKeyboardButtonCode));
}
public bool IsKeyJustPressed(KeyboardButtonCode parKeyboardButtonCode)
{
return _keyboardState.IsKeyDown(MapKeyCode(parKeyboardButtonCode)) &&
!_previousKeyboardState.IsKeyDown(MapKeyCode(parKeyboardButtonCode));
}
public bool IsMouseButtonPressed(MouseButtonCode parButtonCode)
{
return parWindow.NativeWindow.MouseState.IsButtonDown(MapMouseButtonCode(parButtonCode));
}
public bool IsMouseButtonJustPressed(MouseButtonCode parButtonCode)
{
return parWindow.NativeWindow.MouseState.IsButtonPressed(MapMouseButtonCode(parButtonCode));
}
private static MouseButton MapMouseButtonCode(MouseButtonCode parButton)
{
return parButton switch
{
MouseButtonCode.Left => MouseButton.Left,
MouseButtonCode.Right => MouseButton.Right,
MouseButtonCode.Middle => MouseButton.Middle,
_ => throw new ArgumentOutOfRangeException(nameof(parButton), parButton, null)
};
}
private static Keys MapKeyCode(KeyboardButtonCode parKeyboardButtonCode)
{
return parKeyboardButtonCode switch
{
KeyboardButtonCode.A => Keys.A,
KeyboardButtonCode.B => Keys.B,
KeyboardButtonCode.C => Keys.C,
KeyboardButtonCode.D => Keys.D,
KeyboardButtonCode.E => Keys.E,
KeyboardButtonCode.F => Keys.F,
KeyboardButtonCode.G => Keys.G,
KeyboardButtonCode.H => Keys.H,
KeyboardButtonCode.I => Keys.I,
KeyboardButtonCode.J => Keys.J,
KeyboardButtonCode.K => Keys.K,
KeyboardButtonCode.L => Keys.L,
KeyboardButtonCode.M => Keys.M,
KeyboardButtonCode.N => Keys.N,
KeyboardButtonCode.O => Keys.O,
KeyboardButtonCode.P => Keys.P,
KeyboardButtonCode.Q => Keys.Q,
KeyboardButtonCode.R => Keys.R,
KeyboardButtonCode.S => Keys.S,
KeyboardButtonCode.T => Keys.T,
KeyboardButtonCode.U => Keys.U,
KeyboardButtonCode.V => Keys.V,
KeyboardButtonCode.W => Keys.W,
KeyboardButtonCode.X => Keys.X,
KeyboardButtonCode.Y => Keys.Y,
KeyboardButtonCode.Z => Keys.Z,
KeyboardButtonCode.Ctrl => Keys.LeftControl,
KeyboardButtonCode.Alt => Keys.LeftAlt,
KeyboardButtonCode.Shift => Keys.LeftShift,
KeyboardButtonCode.Up => Keys.Up,
KeyboardButtonCode.Down => Keys.Down,
KeyboardButtonCode.Left => Keys.Left,
KeyboardButtonCode.Right => Keys.Right,
KeyboardButtonCode.Escape => Keys.Escape,
KeyboardButtonCode.Enter => Keys.Enter,
KeyboardButtonCode.Space => Keys.Space,
KeyboardButtonCode.Tab => Keys.Tab,
KeyboardButtonCode.Backspace => Keys.Backspace,
KeyboardButtonCode.Delete => Keys.Delete,
KeyboardButtonCode.Insert => Keys.Insert,
KeyboardButtonCode.Home => Keys.Home,
KeyboardButtonCode.End => Keys.End,
KeyboardButtonCode.PageUp => Keys.PageUp,
KeyboardButtonCode.PageDown => Keys.PageDown,
_ => throw new ArgumentOutOfRangeException(nameof(parKeyboardButtonCode), parKeyboardButtonCode, null)
};
}
}

View File

@@ -1,5 +1,6 @@
using Engine.Graphics.Camera;
using OpenTK.Mathematics;
using Serilog;
namespace Engine.Scene.Component.BuiltIn;

View File

@@ -4,8 +4,8 @@ using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn;
public class OrthographicCamera(
float parNearPlane = 0.1f,
float parFarPlane = 1000f,
float parNearPlane = -10000f,
float parFarPlane = 10000f,
float parSize = 10f,
OrthographicCamera.Axis parAxis = OrthographicCamera.Axis.Y
)
@@ -17,8 +17,8 @@ public class OrthographicCamera(
public override Matrix4 View => GameObject.Transform.TransformMatrix.Inverted();
public override Matrix4 Projection => FixedAxis == Axis.X
? Matrix4.CreateOrthographic(Size, Size / AspectRatio, NearPlane, FarPlane)
: Matrix4.CreateOrthographic(Size * AspectRatio, Size, NearPlane, FarPlane);
? Matrix4.CreateOrthographic(Size, -Size / AspectRatio, NearPlane, FarPlane)
: Matrix4.CreateOrthographic(Size * AspectRatio, -Size, NearPlane, FarPlane);
public override Vector3 ScreenToWorld(Vector2 parScreenPosition)
{

View File

@@ -1,11 +1,12 @@
using Engine.Util;
using OpenTK.Mathematics;
using Math = System.Math;
namespace Engine.Scene.Component.BuiltIn;
public class PerspectiveCamera(
float parFieldOfView = 90.0f,
float parNearPlane = 0.1f,
float parNearPlane = 0.01f,
float parFarPlane = 1000f
)
: Camera(parNearPlane, parFarPlane)
@@ -19,14 +20,15 @@ public class PerspectiveCamera(
var transformMatrix = GameObject.Transform.TransformMatrix;
var forward = new Vector4(0, 0, 1, 1).MulProject(transformMatrix);
var eye = new Vector4(0, 0, 0, 1).MulProject(transformMatrix);
var up = (new Vector4(0, 1, 0, 1).MulProject(transformMatrix) - eye).Normalized();
var up = (new Vector4(0, -1, 0, 1).MulProject(transformMatrix) - eye).Normalized();
return Matrix4.LookAt(eye.Xyz, forward.Xyz, up.Xyz);
}
}
public override Matrix4 Projection =>
Matrix4.CreatePerspectiveFieldOfView(FieldOfView, AspectRatio, NearPlane, FarPlane);
Matrix4.CreatePerspectiveFieldOfView(MathHelper.DegreesToRadians(FieldOfView), AspectRatio,
NearPlane, FarPlane);
public override Vector3 ScreenToWorld(Vector2 parScreenPosition)
{

View File

@@ -0,0 +1,17 @@
using Engine.Graphics.Texture;
using OpenTK.Mathematics;
namespace Engine.Scene.Component.BuiltIn.Renderer;
public class Box2DRenderer : Component
{
public ref Vector4 Color => ref _color;
public Texture? Texture { get; set; } = null;
private Vector4 _color = Vector4.One;
public override void Render()
{
Engine.Instance.Renderer.QuadRenderer.Commit(GameObject.Transform.TransformMatrix, Color, Texture);
}
}

View File

@@ -0,0 +1,13 @@
using Engine.Asset.Mesh;
namespace Engine.Scene.Component.BuiltIn.Renderer;
public class MeshRenderer : Component
{
public Mesh Mesh { get; set; } = null!;
public override void Render()
{
Engine.Instance.Renderer.GlobalMeshRenderer.Commit(Mesh, GameObject.Transform.TransformMatrix);
}
}

View File

@@ -4,14 +4,19 @@ namespace Engine.Scene.Component.BuiltIn;
public class Transform : Component
{
public Vector3 Position { get; set; } = Vector3.Zero;
public Quaternion Rotation { get; set; } = Quaternion.Identity;
public Vector3 Scale { get; set; } = Vector3.One;
public Vector3 LocalScale { get; set; } = Vector3.One;
private Vector3 _translation = Vector3.Zero;
private Quaternion _rotation = Quaternion.Identity;
private Vector3 _scale = Vector3.One;
private Vector3 _localScale = Vector3.One;
public ref Vector3 Translation => ref _translation;
public ref Quaternion Rotation => ref _rotation;
public ref Vector3 Scale => ref _scale;
public ref Vector3 LocalScale => ref _localScale;
public Matrix4 LocalTransformMatrix => Matrix4.CreateScale(Scale) *
Matrix4.CreateFromQuaternion(Rotation) *
Matrix4.CreateTranslation(Position);
Matrix4.CreateTranslation(Translation);
public Matrix4 TransformMatrix => LocalTransformMatrix * ParentTransformMatrix;
@@ -27,7 +32,7 @@ public class Transform : Component
public Transform Clone()
{
var clone =
new Transform { Position = Position, Rotation = Rotation, Scale = Scale, LocalScale = LocalScale };
new Transform { Translation = Translation, Rotation = Rotation, Scale = Scale, LocalScale = LocalScale };
return clone;
}

View File

@@ -13,8 +13,8 @@ public sealed class GameObject : IUpdate, IRender
private readonly Queue<Action> _componentActions = new();
private readonly IList<Component.Component> _components = new List<Component.Component>();
private readonly ISet<Type> _addedComponentTypes = new HashSet<Type>();
private readonly List<Component.Component> _components = [];
private readonly HashSet<Type> _addedComponentTypes = [];
public GameObject()
{

View File

@@ -1,12 +1,13 @@
using System.Collections;
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using Engine.Util;
namespace Engine.Scene;
public class Hierarchy<T> : IEnumerable<T>
public class Hierarchy<T>
where T : class
{
internal Dictionary<T, T?>.KeyCollection Objects => _parentLookup.Keys;
private readonly Dictionary<NullableObject<T>, IList<T>> _childrenLookup = new();
private readonly Dictionary<T, T?> _parentLookup = new();
@@ -147,14 +148,4 @@ public class Hierarchy<T> : IEnumerable<T>
}
}
}
public IEnumerator<T> GetEnumerator()
{
return _parentLookup.Keys.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}

View File

@@ -8,7 +8,7 @@ public class Scene : IUpdate, IRender
public bool IsPlaying { get; private set; }
public ICamera? MainCamera { get; private set; }
internal Hierarchy<GameObject> Hierarchy { get; } = [];
internal Hierarchy<GameObject> Hierarchy { get; } = new();
private readonly Queue<Action> _sceneActions = [];
@@ -28,7 +28,7 @@ public class Scene : IUpdate, IRender
public T? FindFirstComponent<T>() where T : Component.Component
{
return Hierarchy.Select(parGameObject => parGameObject.GetComponent<T>()).FirstOrDefault();
return Hierarchy.Objects.Select(parGameObject => parGameObject.GetComponent<T>()).FirstOrDefault();
}
public void Update(double parDeltaTime)
@@ -40,7 +40,7 @@ public class Scene : IUpdate, IRender
ProcessChanges();
foreach (var gameObject in Hierarchy)
foreach (var gameObject in Hierarchy.Objects)
{
gameObject.Update(parDeltaTime);
}
@@ -53,7 +53,7 @@ public class Scene : IUpdate, IRender
throw new InvalidOperationException("Scene is not playing");
}
foreach (var gameObject in Hierarchy)
foreach (var gameObject in Hierarchy.Objects)
{
gameObject.Render();
}
@@ -66,7 +66,7 @@ public class Scene : IUpdate, IRender
throw new InvalidOperationException("Scene is not playing");
}
foreach (var gameObject in Hierarchy)
foreach (var gameObject in Hierarchy.Objects)
{
gameObject.Destroy();
}
@@ -114,7 +114,7 @@ public class Scene : IUpdate, IRender
action();
}
foreach (var gameObject in Hierarchy)
foreach (var gameObject in Hierarchy.Objects)
{
gameObject.ProcessChanges();
}

View File

@@ -1,6 +1,4 @@
using Engine.Scene.Component.BuiltIn;
namespace Engine.Scene;
namespace Engine.Scene;
public class SceneManager : IUpdate, IRender
{

View File

@@ -1,6 +1,4 @@
using Engine.Asset;
using Engine.Graphics;
using Engine.Graphics.Pixel;
using Engine.Graphics;
using Engine.Graphics.Texture;
using OpenTK.Graphics.OpenGL;
using OpenTK.Mathematics;
@@ -17,6 +15,8 @@ public class Window : IPresenter
public int Height { get; private set; }
public event Action<ResizeEventArgs>? Resize;
internal NativeWindow NativeWindow => _window;
private readonly Engine _engine;
private readonly NativeWindow _window;
private readonly bool _headless;
@@ -36,21 +36,33 @@ public class Window : IPresenter
Height = parArgs.Height;
Resize?.Invoke(parArgs);
};
_window.VSync = VSyncMode.On;
}
public void Update(double parDeltaTime)
{
}
public void Render()
{
if (_headless)
{
return;
}
_window.NewInputFrame();
NativeWindow.ProcessWindowEvents(false);
_window.SwapBuffers();
}
public void Present(IConstTexture parTexture)
{
if (_headless)
{
return;
}
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

View File

@@ -9,7 +9,7 @@ public class HierarchyTests
[SetUp]
public void Setup()
{
_hierarchy = [];
_hierarchy = new Hierarchy<object>();
}
[Test]

View File

@@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DoomDeathmatch\DoomDeathmatch.csproj" />
<ProjectReference Include="..\Engine\Engine.csproj"/>
</ItemGroup>

View File

@@ -37,7 +37,8 @@ const vec3 ConsoleColorVec3[16] = vec3[](
vec3(1.0, 1.0, 1.0) // White
);
vec3 f(vec3 x) {
// RGB to LAB conversion (simplified approximation)
vec3 rgb2lab(vec3 x) {
const float epsilon = 0.008856;
const float k = 903.3;
vec3 fx;
@@ -49,10 +50,8 @@ vec3 f(vec3 x) {
// Perceptually weighted color difference (CIEDE2000-inspired)
float perceptualColorDistance(vec3 color1, vec3 color2) {
// RGB to LAB conversion (simplified approximation)
vec3 lab1 = f(color1);
vec3 lab2 = f(color2);
vec3 lab1 = rgb2lab(color1);
vec3 lab2 = rgb2lab(color2);
// Compute LAB-like color difference with perceptual weighting
float deltaL = lab1.x - lab2.x;
@@ -98,7 +97,6 @@ float interleavedGradientNoise(vec2 pixel) {
}
uniform sampler2D uInputTexture;
uniform vec2 uResolution;
layout (location = 0) in vec2 iUV;
layout (location = 0) out vec4 FragColor;

View File

@@ -34,13 +34,10 @@ public sealed class ConsoleFastOutput : IDisposable
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void WriteChar(char parCharacter, int parX, int parY, ConsoleColor parForeground, ConsoleColor parBackground)
{
if (parX < 0 || parX >= _width || parY < 0 || parY >= _height)
return;
var index = parX + parY * _width;
ref var charInfo = ref _buffer[index];
charInfo.Char.UnicodeChar = parCharacter;
charInfo.Attributes = (short)((int)parForeground | ((int)parBackground << 4));
charInfo.Attributes = (short)((ushort)parForeground | ((ushort)parBackground << 4));
}
public void Flush()
@@ -60,7 +57,7 @@ public sealed class ConsoleFastOutput : IDisposable
_width = parWidth;
_height = parHeight;
_buffer = new WindowsFFI.CharInfo[parWidth * parHeight];
_buffer = new WindowsFFI.CharInfo[_width * _height];
_bufferSize = new WindowsFFI.Coord((short)_width, (short)_height);
}

View File

@@ -4,126 +4,98 @@ namespace PresenterConsole;
public class ConsoleInputHandler : IInputHandler
{
private readonly HashSet<ConsoleKey> _currentKeys = [];
private readonly HashSet<ConsoleKey> _previousKeys = [];
private ConsoleModifiers _currentModifiers;
private ConsoleModifiers _previousModifiers;
private readonly bool[] _currentKeys = new bool[256];
private readonly bool[] _previousKeys = new bool[256];
public void Update(double parDeltaTime)
{
// Save previous state
_previousKeys.Clear();
foreach (var key in _currentKeys)
for (var i = 0; i < _currentKeys.Length; i++)
{
_previousKeys.Add(key);
}
_previousModifiers = _currentModifiers;
// Clear current state
_currentKeys.Clear();
_currentModifiers = 0;
// Read keys
while (Console.KeyAvailable)
{
var keyInfo = Console.ReadKey(intercept: true);
_currentKeys.Add(keyInfo.Key);
_currentModifiers |= keyInfo.Modifiers;
_previousKeys[i] = _currentKeys[i];
_currentKeys[i] = (WindowsFFI.GetAsyncKeyState(i) & 0x8000) != 0;
}
}
public bool IsKeyPressed(KeyCode keyCode)
public bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode)
{
if (IsModifierKey(keyCode))
return IsModifierActive(keyCode);
return _currentKeys.Contains(ConvertToConsoleKey(keyCode));
var consoleKey = ConvertToConsoleKey(parKeyboardButtonCode);
return _currentKeys[consoleKey];
}
public bool IsKeyJustPressed(KeyCode keyCode)
public bool IsKeyJustPressed(KeyboardButtonCode parKeyboardButtonCode)
{
if (IsModifierKey(keyCode))
return IsModifierActive(keyCode) && !WasModifierActive(keyCode);
var consoleKey = ConvertToConsoleKey(keyCode);
return _currentKeys.Contains(consoleKey) && !_previousKeys.Contains(consoleKey);
var consoleKey = ConvertToConsoleKey(parKeyboardButtonCode);
return _currentKeys[consoleKey] && !_previousKeys[consoleKey];
}
public bool IsKeyRepeat(KeyCode keyCode)
public bool IsMouseButtonPressed(MouseButtonCode parButtonCode)
{
if (IsModifierKey(keyCode))
return IsModifierActive(keyCode) && WasModifierActive(keyCode);
var consoleKey = ConvertToConsoleKey(keyCode);
return _currentKeys.Contains(consoleKey) && _previousKeys.Contains(consoleKey);
var consoleKey = ConvertToConsoleKey(parButtonCode);
return _currentKeys[consoleKey];
}
public bool IsMouseButtonPressed(MouseButton button)
public bool IsMouseButtonJustPressed(MouseButtonCode parButtonCode)
{
return false;
var consoleKey = ConvertToConsoleKey(parButtonCode);
return _currentKeys[consoleKey] && !_previousKeys[consoleKey];
}
public bool IsMouseButtonJustPressed(MouseButton button)
private static int ConvertToConsoleKey(MouseButtonCode parMouseButtonCode) =>
parMouseButtonCode switch
{
return false;
}
private static bool IsModifierKey(KeyCode keyCode) => keyCode is KeyCode.Ctrl or KeyCode.Backspace or KeyCode.Shift;
private bool IsModifierActive(KeyCode keyCode) => keyCode switch
{
KeyCode.Ctrl => (_currentModifiers & ConsoleModifiers.Control) != 0,
KeyCode.Alt => (_currentModifiers & ConsoleModifiers.Alt) != 0,
KeyCode.Shift => (_currentModifiers & ConsoleModifiers.Shift) != 0,
_ => false
MouseButtonCode.Left => 0x01,
MouseButtonCode.Right => 0x02,
MouseButtonCode.Middle => 0x04,
_ => throw new ArgumentOutOfRangeException(nameof(parMouseButtonCode), parMouseButtonCode, null)
};
private bool WasModifierActive(KeyCode keyCode) => keyCode switch
private static int ConvertToConsoleKey(KeyboardButtonCode parKeyboardButtonCode) =>
parKeyboardButtonCode switch
{
KeyCode.Ctrl => (_previousModifiers & ConsoleModifiers.Control) != 0,
KeyCode.Alt => (_previousModifiers & ConsoleModifiers.Alt) != 0,
KeyCode.Shift => (_previousModifiers & ConsoleModifiers.Shift) != 0,
_ => false
};
private static ConsoleKey ConvertToConsoleKey(KeyCode keyCode) => keyCode switch
{
KeyCode.A => ConsoleKey.A,
KeyCode.B => ConsoleKey.B,
KeyCode.C => ConsoleKey.C,
KeyCode.D => ConsoleKey.D,
KeyCode.E => ConsoleKey.E,
KeyCode.F => ConsoleKey.F,
KeyCode.G => ConsoleKey.G,
KeyCode.H => ConsoleKey.H,
KeyCode.I => ConsoleKey.I,
KeyCode.J => ConsoleKey.J,
KeyCode.K => ConsoleKey.K,
KeyCode.L => ConsoleKey.L,
KeyCode.M => ConsoleKey.M,
KeyCode.N => ConsoleKey.N,
KeyCode.O => ConsoleKey.O,
KeyCode.P => ConsoleKey.P,
KeyCode.Q => ConsoleKey.Q,
KeyCode.R => ConsoleKey.R,
KeyCode.S => ConsoleKey.S,
KeyCode.T => ConsoleKey.T,
KeyCode.U => ConsoleKey.U,
KeyCode.V => ConsoleKey.V,
KeyCode.W => ConsoleKey.W,
KeyCode.X => ConsoleKey.X,
KeyCode.Y => ConsoleKey.Y,
KeyCode.Z => ConsoleKey.Z,
KeyCode.Enter => ConsoleKey.Enter,
KeyCode.Escape => ConsoleKey.Escape,
KeyCode.Space => ConsoleKey.Spacebar,
KeyCode.Tab => ConsoleKey.Tab,
KeyCode.Backspace => ConsoleKey.Backspace,
KeyCode.Up => ConsoleKey.UpArrow,
KeyCode.Down => ConsoleKey.DownArrow,
KeyCode.Left => ConsoleKey.LeftArrow,
KeyCode.Right => ConsoleKey.RightArrow,
_ => throw new ArgumentOutOfRangeException(nameof(keyCode), $"No mapping defined for {keyCode}")
KeyboardButtonCode.A => 0x41,
KeyboardButtonCode.B => 0x42,
KeyboardButtonCode.C => 0x43,
KeyboardButtonCode.D => 0x44,
KeyboardButtonCode.E => 0x45,
KeyboardButtonCode.F => 0x46,
KeyboardButtonCode.G => 0x47,
KeyboardButtonCode.H => 0x48,
KeyboardButtonCode.I => 0x49,
KeyboardButtonCode.J => 0x4A,
KeyboardButtonCode.K => 0x4B,
KeyboardButtonCode.L => 0x4C,
KeyboardButtonCode.M => 0x4D,
KeyboardButtonCode.N => 0x4E,
KeyboardButtonCode.O => 0x4F,
KeyboardButtonCode.P => 0x50,
KeyboardButtonCode.Q => 0x51,
KeyboardButtonCode.R => 0x52,
KeyboardButtonCode.S => 0x53,
KeyboardButtonCode.T => 0x54,
KeyboardButtonCode.U => 0x55,
KeyboardButtonCode.V => 0x56,
KeyboardButtonCode.W => 0x57,
KeyboardButtonCode.X => 0x58,
KeyboardButtonCode.Y => 0x59,
KeyboardButtonCode.Z => 0x5A,
KeyboardButtonCode.Enter => 0x0D,
KeyboardButtonCode.Escape => 0x1B,
KeyboardButtonCode.Space => 0x20,
KeyboardButtonCode.Tab => 0x09,
KeyboardButtonCode.Backspace => 0x08,
KeyboardButtonCode.Up => 0x26,
KeyboardButtonCode.Down => 0x28,
KeyboardButtonCode.Left => 0x25,
KeyboardButtonCode.Right => 0x27,
KeyboardButtonCode.Shift => 0x10,
KeyboardButtonCode.Ctrl => 0x11,
KeyboardButtonCode.Alt => 0x12,
KeyboardButtonCode.Delete => 0x2E,
KeyboardButtonCode.Insert => 0x2D,
KeyboardButtonCode.Home => 0x24,
KeyboardButtonCode.End => 0x23,
KeyboardButtonCode.PageUp => 0x21,
KeyboardButtonCode.PageDown => 0x22,
_ => throw new ArgumentOutOfRangeException(nameof(parKeyboardButtonCode), parKeyboardButtonCode, null)
};
}

View File

@@ -2,7 +2,6 @@
using Engine.Graphics;
using Engine.Graphics.Buffer;
using Engine.Graphics.Framebuffer;
using Engine.Graphics.Pixel;
using Engine.Graphics.Shader;
using Engine.Graphics.Texture;
using OpenTK.Graphics.OpenGL;
@@ -14,14 +13,10 @@ namespace PresenterConsole;
public class ConsolePresenter : IPresenter
{
public bool IsExiting => false;
public int Width => Console.WindowWidth;
public int Height => Console.WindowHeight;
public int Width { get; private set; } = 2;
public int Height { get; private set; } = 1;
public event Action<ResizeEventArgs>? Resize;
private int _cachedWidth;
private int _cachedHeight;
private readonly Engine.Engine _engine;
private readonly Framebuffer _framebuffer;
private readonly Engine.Graphics.Shader.Program _asciiProgram;
private Image<AsciiPixel>? _asciiImage;
@@ -31,14 +26,13 @@ public class ConsolePresenter : IPresenter
private readonly VertexArray _vertexArray;
private readonly ConsoleFastOutput _consoleOutput;
private static readonly char[] LIGHTMAP = " .:-+=#%@".Reverse().ToArray();
private static readonly char[] LIGHTMAP = " .,:;=*#%@".Reverse().ToArray();
public ConsolePresenter(Engine.Engine parEngine)
public ConsolePresenter()
{
_engine = parEngine;
_asciiProgram = ProgramLoader.LoadFromSource(Resource.ShaderResource.Ascii);
_framebuffer = Framebuffer.Builder(Width, Height)
_framebuffer = Framebuffer.Builder(Width / 2, Height)
.AddColorAttachment<AsciiPixel>()
.Build();
@@ -63,9 +57,6 @@ public class ConsolePresenter : IPresenter
_framebuffer.Bind();
GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
openglTexture.BindUnit();
_asciiProgram.Bind();
@@ -94,10 +85,10 @@ public class ConsolePresenter : IPresenter
for (var x = 0; x < parImage.Width; x++)
{
var pixel = parImage[y, x];
var lightnessIndex = (int)(pixel.Luminance / 255.0f * (LIGHTMAP.Length - 1));
var colorIndex = (int)(pixel.Color / 255.0f * 15.0f);
_consoleOutput.WriteChar(LIGHTMAP[lightnessIndex], (short)x, (short)y, 0,
(ConsoleColor)colorIndex);
var lightnessIndex = (byte)(pixel.Luminance / 255.0f * (LIGHTMAP.Length - 1));
var colorIndex = (ConsoleColor)(pixel.Color / 255.0f * 15.0f);
_consoleOutput.WriteChar(LIGHTMAP[lightnessIndex], 2 * x, y, 0, colorIndex);
_consoleOutput.WriteChar(LIGHTMAP[lightnessIndex], 2 * x + 1, y, 0, colorIndex);
}
}
@@ -106,14 +97,20 @@ public class ConsolePresenter : IPresenter
public void Update(double parDeltaTime)
{
if (_cachedWidth != Width || _cachedHeight != Height)
}
public void Render()
{
_cachedWidth = Width;
_cachedHeight = Height;
var consoleWidth = Console.WindowWidth;
var consoleHeight = Console.WindowHeight;
if (Width != consoleWidth || Height != consoleHeight)
{
Width = consoleWidth;
Height = consoleHeight;
Resize?.Invoke(new ResizeEventArgs(Width, Height));
Resize?.Invoke(new ResizeEventArgs(Width / 2, Height));
_framebuffer.Resize(Width, Height);
_framebuffer.Resize(Width / 2, Height);
_consoleOutput.Resize(Width, Height);
}
}

View File

@@ -1,6 +1,4 @@
// See https://aka.ms/new-console-template for more information
using Serilog.Events;
using Serilog.Events;
namespace PresenterConsole;
@@ -12,9 +10,12 @@ internal static class Program
.Headless()
.LogToFile(true, "log.txt")
.LogLevel(LogEventLevel.Debug)
.Presenter(parEngine => new ConsolePresenter(parEngine))
.Presenter(_ => new ConsolePresenter())
.InputHandler(_ => new ConsoleInputHandler())
.Build();
DoomDeathmatch.DoomDeathmatch.Initialize(engine);
engine.Run();
}
}

View File

@@ -60,4 +60,11 @@ public static partial class WindowsFFI
Coord parDwBufferCoord,
ref SmallRect parLpWriteRegion
);
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetKeyboardState")]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool GetKeyboardState(byte[] parKeyboardState);
[LibraryImport("user32.dll", SetLastError = true, EntryPoint = "GetAsyncKeyState")]
public static partial short GetAsyncKeyState(int parKeyCode);
}

View File

@@ -8,6 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DoomDeathmatch\DoomDeathmatch.csproj" />
<ProjectReference Include="..\Engine\Engine.csproj"/>
</ItemGroup>

View File

@@ -1,4 +1,5 @@
using Engine;
using Engine.Input;
using Serilog.Events;
namespace PresenterNative;
@@ -16,8 +17,11 @@ internal static class Program
.LogToFile(true, "log.txt")
.LogLevel(LogEventLevel.Debug)
.Presenter(parEngine => parEngine.Window)
.InputHandler(parEngine => new WindowInputHandler(parEngine.Window))
.Build();
DoomDeathmatch.DoomDeathmatch.Initialize(engine);
engine.Run();
}
}

View File

@@ -9,6 +9,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\DoomDeathmatch\DoomDeathmatch.csproj" />
<ProjectReference Include="..\Engine\Engine.csproj"/>
</ItemGroup>

View File

@@ -4,6 +4,7 @@ using System.Windows;
using Engine;
using Engine.Graphics;
using Engine.Graphics.Texture;
using Engine.Input;
using OpenTK.Windowing.Common;
using Serilog.Events;
@@ -18,9 +19,11 @@ public partial class App : Application
protected override void OnStartup(StartupEventArgs parEventArgs)
{
var presenter = new PresenterWrapper();
var inputHandler = new InputHandlerWrapper();
var engine = new EngineBuilder()
.Headless()
.Presenter(_ => presenter)
.InputHandler(_ => inputHandler)
.LogToConsole()
.LogToFile(true, "log.txt")
.LogLevel(LogEventLevel.Debug)
@@ -31,6 +34,7 @@ public partial class App : Application
{
var window = new MainWindow(engine);
presenter.Presenter = window;
inputHandler.InputHandler = new WpfInputHandler(window);
window.Show();
System.Windows.Threading.Dispatcher.Run();
});
@@ -39,12 +43,53 @@ public partial class App : Application
thread.IsBackground = true;
thread.Start();
DoomDeathmatch.DoomDeathmatch.Initialize(engine);
engine.Run();
// Shutdown WPF
Shutdown();
}
private class InputHandlerWrapper : IInputHandler
{
private IInputHandler? _inputHandler;
public IInputHandler? InputHandler
{
get => _inputHandler;
set
{
_inputHandler = value;
}
}
public bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode)
{
return _inputHandler?.IsKeyPressed(parKeyboardButtonCode) ?? false;
}
public bool IsKeyJustPressed(KeyboardButtonCode parKeyboardButtonCode)
{
return _inputHandler?.IsKeyJustPressed(parKeyboardButtonCode) ?? false;
}
public bool IsMouseButtonPressed(MouseButtonCode parButtonCode)
{
return _inputHandler?.IsMouseButtonPressed(parButtonCode) ?? false;
}
public bool IsMouseButtonJustPressed(MouseButtonCode parButtonCode)
{
return _inputHandler?.IsMouseButtonJustPressed(parButtonCode) ?? false;
}
public void Update(double parDeltaTime)
{
_inputHandler?.Update(parDeltaTime);
}
}
private class PresenterWrapper : IPresenter
{
private IPresenter? _presenter;
@@ -83,6 +128,11 @@ public partial class App : Application
Presenter?.Update(parDeltaTime);
}
public void Render()
{
Presenter?.Render();
}
private void PresenterResize(ResizeEventArgs e)
{
Resize?.Invoke(e);

View File

@@ -43,8 +43,10 @@ public partial class MainWindow : Window, IPresenter
_scheduledResize = true;
}
});
}
// Resizes are lazy so resizing only happens when the window's size actually changes
public void Render()
{
if (_scheduledResize)
{
_scheduledResize = false;
@@ -77,7 +79,7 @@ public partial class MainWindow : Window, IPresenter
try
{
_bitmap!.Lock();
_bitmap.WritePixels(new Int32Rect(0, 0, parImage.Width, parImage.Height), parImage.Pixels, parImage.Width * 3, 0,
_bitmap!.WritePixels(new Int32Rect(0, 0, parImage.Width, parImage.Height), parImage.Pixels, parImage.Width * 3, 0,
0);
}
finally

View File

@@ -0,0 +1,167 @@
using System.Windows;
using System.Windows.Input;
using Engine.Input;
namespace PresenterWpf;
public class WpfInputHandler : IInputHandler
{
private readonly Window _window;
private readonly bool[] _currentKeys = new bool[(int)KeyboardButtonCode.TotalCount];
private readonly bool[] _previousKeys = new bool[(int)KeyboardButtonCode.TotalCount];
private readonly bool[] _currentMouseButtons = new bool[(int)MouseButtonCode.TotalCount];
private readonly bool[] _previousMouseButtons = new bool[(int)MouseButtonCode.TotalCount];
public WpfInputHandler(Window parWindow)
{
_window = parWindow;
_window.PreviewKeyDown += Window_PreviewKeyDown;
_window.PreviewKeyUp += Window_PreviewKeyUp;
_window.PreviewMouseDown += Window_PreviewMouseDown;
_window.PreviewMouseUp += Window_PreviewMouseUp;
}
public void Update(double parDeltaTime)
{
for (var i = 0; i < _currentKeys.Length; i++)
{
_previousKeys[i] = _currentKeys[i];
}
for (var i = 0; i < _currentMouseButtons.Length; i++)
{
_previousMouseButtons[i] = _currentMouseButtons[i];
}
}
private void Window_PreviewKeyDown(object parSender, KeyEventArgs parEventArgs)
{
var keyCode = ConvertToKeyboardButtonCode(parEventArgs.Key);
if (keyCode >= 0 && keyCode < _currentKeys.Length)
{
_currentKeys[keyCode] = true;
}
}
private void Window_PreviewKeyUp(object parSender, KeyEventArgs parEventArgs)
{
var keyCode = ConvertToKeyboardButtonCode(parEventArgs.Key);
if (keyCode >= 0 && keyCode < _currentKeys.Length)
{
_currentKeys[keyCode] = false;
}
}
private void Window_PreviewMouseDown(object parSender, MouseButtonEventArgs parEventArgs)
{
var buttonCode = ConvertToMouseButtonCode(parEventArgs.ChangedButton);
if (buttonCode >= 0 && buttonCode < _currentMouseButtons.Length)
{
_currentMouseButtons[buttonCode] = true;
}
}
private void Window_PreviewMouseUp(object parSender, MouseButtonEventArgs parEventArgs)
{
var buttonCode = ConvertToMouseButtonCode(parEventArgs.ChangedButton);
if (buttonCode >= 0 && buttonCode < _currentMouseButtons.Length)
{
_currentMouseButtons[buttonCode] = false;
}
}
public bool IsKeyPressed(KeyboardButtonCode parKeyboardButtonCode)
{
var keyCode = (int)parKeyboardButtonCode;
return _currentKeys[keyCode];
}
public bool IsKeyJustPressed(KeyboardButtonCode parKeyboardButtonCode)
{
var keyCode = (int)parKeyboardButtonCode;
return _currentKeys[keyCode] && !_previousKeys[keyCode];
}
public bool IsMouseButtonPressed(MouseButtonCode parButtonCode)
{
var buttonCode = (int)parButtonCode;
return _currentMouseButtons[buttonCode];
}
public bool IsMouseButtonJustPressed(MouseButtonCode parButtonCode)
{
var buttonCode = (int)parButtonCode;
return _currentMouseButtons[buttonCode] && !_previousMouseButtons[buttonCode];
}
private static int ConvertToKeyboardButtonCode(Key parKey) => parKey switch
{
Key.A => (int)KeyboardButtonCode.A,
Key.B => (int)KeyboardButtonCode.B,
Key.C => (int)KeyboardButtonCode.C,
Key.D => (int)KeyboardButtonCode.D,
Key.E => (int)KeyboardButtonCode.E,
Key.F => (int)KeyboardButtonCode.F,
Key.G => (int)KeyboardButtonCode.G,
Key.H => (int)KeyboardButtonCode.H,
Key.I => (int)KeyboardButtonCode.I,
Key.J => (int)KeyboardButtonCode.J,
Key.K => (int)KeyboardButtonCode.K,
Key.L => (int)KeyboardButtonCode.L,
Key.M => (int)KeyboardButtonCode.M,
Key.N => (int)KeyboardButtonCode.N,
Key.O => (int)KeyboardButtonCode.O,
Key.P => (int)KeyboardButtonCode.P,
Key.Q => (int)KeyboardButtonCode.Q,
Key.R => (int)KeyboardButtonCode.R,
Key.S => (int)KeyboardButtonCode.S,
Key.T => (int)KeyboardButtonCode.T,
Key.U => (int)KeyboardButtonCode.U,
Key.V => (int)KeyboardButtonCode.V,
Key.W => (int)KeyboardButtonCode.W,
Key.X => (int)KeyboardButtonCode.X,
Key.Y => (int)KeyboardButtonCode.Y,
Key.Z => (int)KeyboardButtonCode.Z,
Key.LeftCtrl => (int)KeyboardButtonCode.Ctrl,
Key.LeftAlt => (int)KeyboardButtonCode.Alt,
Key.LeftShift => (int)KeyboardButtonCode.Shift,
Key.RightCtrl => (int)KeyboardButtonCode.Ctrl,
Key.RightAlt => (int)KeyboardButtonCode.Alt,
Key.RightShift => (int)KeyboardButtonCode.Shift,
Key.Space => (int)KeyboardButtonCode.Space,
Key.Tab => (int)KeyboardButtonCode.Tab,
Key.Back => (int)KeyboardButtonCode.Backspace,
Key.Return => (int)KeyboardButtonCode.Enter,
Key.Escape => (int)KeyboardButtonCode.Escape,
Key.PageUp => (int)KeyboardButtonCode.PageUp,
Key.PageDown => (int)KeyboardButtonCode.PageDown,
Key.Up => (int)KeyboardButtonCode.Up,
Key.Down => (int)KeyboardButtonCode.Down,
Key.Left => (int)KeyboardButtonCode.Left,
Key.Right => (int)KeyboardButtonCode.Right,
Key.Delete => (int)KeyboardButtonCode.Delete,
Key.Insert => (int)KeyboardButtonCode.Insert,
Key.Home => (int)KeyboardButtonCode.Home,
Key.End => (int)KeyboardButtonCode.End,
_ => -1
};
private static int ConvertToMouseButtonCode(MouseButton parButton) =>
parButton switch
{
MouseButton.Left => (int)MouseButtonCode.Left,
MouseButton.Right => (int)MouseButtonCode.Right,
MouseButton.Middle => (int)MouseButtonCode.Middle,
_ => -1
};
~WpfInputHandler()
{
_window.PreviewKeyDown -= Window_PreviewKeyDown;
_window.PreviewKeyUp -= Window_PreviewKeyUp;
_window.PreviewMouseDown -= Window_PreviewMouseDown;
_window.PreviewMouseUp -= Window_PreviewMouseUp;
}
}