From fa7b12c88ccd993084b71655d43ac94cc0212548 Mon Sep 17 00:00:00 2001 From: lionarius Date: Fri, 13 Dec 2024 15:15:23 +0300 Subject: [PATCH] . --- .editorconfig | 4 +- DoomDeathmatch/src/Program.cs | 2 +- DoomDeathmatch/src/QuadVertex.cs | 2 +- Engine/Engine.csproj | 22 +- Engine/assets/shader/mesh.shader | 34 +++ Engine/assets/shader/quad.shader | 46 ++++ Engine/src/Asset/Image.cs | 12 +- Engine/src/Asset/Mesh/Mesh.cs | 7 +- Engine/src/Engine.cs | 55 +++-- .../Buffer/IndexBuffer.cs | 17 +- .../Buffer/Vertex/IVertex.cs | 4 +- .../Buffer/Vertex/VertexAttribute.cs | 7 +- .../Buffer/VertexArray.cs | 10 +- Engine/src/Graphics/Buffer/VertexBuffer.cs | 108 +++++++++ .../{Renderer => Graphics}/Camera/ICamera.cs | 2 +- .../Camera/ScreenspaceCamera.cs | 2 +- Engine/src/{Renderer => Graphics}/Debug.cs | 2 +- .../Framebuffer/Framebuffer.cs | 14 +- .../Framebuffer/FramebufferBuilder.cs | 6 +- .../Framebuffer/IFramebufferAttachment.cs | 6 +- .../Framebuffer/Renderbuffer.cs | 4 +- .../src/{Renderer => Graphics}/IPresenter.cs | 4 +- .../{Renderer => Graphics}/OpenGLObject.cs | 5 +- .../{Renderer => Graphics}/Pixel/IPixel.cs | 2 +- Engine/src/{Renderer => Graphics}/Pixel/R8.cs | 2 +- .../src/{Renderer => Graphics}/Pixel/Rgb8.cs | 4 +- .../src/Graphics/Render/InstancedRenderer.cs | 79 +++++++ .../Render/Mesh/MeshInstanceVertex.cs | 10 + .../src/Graphics/Render/Mesh/MeshRenderer.cs | 26 +++ .../Graphics/Render/Quad/QuadCommonVertex.cs | 11 + .../Render/Quad/QuadInstanceVertex.cs | 12 + .../src/Graphics/Render/Quad/QuadRenderer.cs | 59 +++++ Engine/src/{Renderer => Graphics}/Renderer.cs | 38 +++- .../Shader/Program.cs} | 50 +---- Engine/src/Graphics/Shader/ProgramLoader.cs | 38 ++++ .../Texture/DynamicTexture.cs | 11 +- .../Texture/IConstTexture.cs | 6 +- .../Texture/ITexture.cs | 4 +- .../Texture/StaticTexture.cs | 4 +- .../{Renderer => Graphics}/Texture/Texture.cs | 20 +- Engine/src/Graphics/Texture/TextureUnitMap.cs | 50 +++++ Engine/src/Input/MouseButton.cs | 14 +- Engine/src/Renderer/Buffer/VertexBuffer.cs | 71 ------ .../src/Resource/ShaderResource.Designer.cs | 101 +++++++++ Engine/src/Resource/ShaderResource.resx | 29 +++ Engine/src/Scene/Component/BuiltIn/Camera.cs | 2 +- Engine/src/Scene/Scene.cs | 2 +- Engine/src/Window.cs | 36 ++- PresenterConsole/PresenterConsole.csproj | 26 ++- PresenterConsole/assets/shader/ascii.shader | 84 +++++++ PresenterConsole/src/AsciiPixel.cs | 21 ++ PresenterConsole/src/AsciiVertex.cs | 11 + PresenterConsole/src/ConsoleFastOutput.cs | 158 +++++++++++++ PresenterConsole/src/ConsoleInputHandler.cs | 212 +++++++++--------- PresenterConsole/src/ConsolePresenter.cs | 136 ++++++++--- PresenterConsole/src/Program.cs | 12 + .../src/Resource/ShaderResource.Designer.cs | 95 ++++++++ .../src/Resource/ShaderResource.resx | 30 +++ PresenterWPF/PresenterWPF.csproj | 32 --- PresenterWpf/PresenterWpf.csproj | 31 +++ PresenterWpf/src/App.xaml | 5 + PresenterWpf/src/App.xaml.cs | 83 +++++++ PresenterWpf/src/AssemblyInfo.cs | 10 + PresenterWpf/src/MainWindow.xaml | 13 ++ PresenterWpf/src/MainWindow.xaml.cs | 101 +++++++++ PresenterWpf/src/Program.cs | 5 + 66 files changed, 1732 insertions(+), 389 deletions(-) create mode 100644 Engine/assets/shader/mesh.shader create mode 100644 Engine/assets/shader/quad.shader rename Engine/src/{Renderer => Graphics}/Buffer/IndexBuffer.cs (69%) rename Engine/src/{Renderer => Graphics}/Buffer/Vertex/IVertex.cs (93%) rename Engine/src/{Renderer => Graphics}/Buffer/Vertex/VertexAttribute.cs (85%) rename Engine/src/{Renderer => Graphics}/Buffer/VertexArray.cs (91%) create mode 100644 Engine/src/Graphics/Buffer/VertexBuffer.cs rename Engine/src/{Renderer => Graphics}/Camera/ICamera.cs (84%) rename Engine/src/{Renderer => Graphics}/Camera/ScreenspaceCamera.cs (86%) rename Engine/src/{Renderer => Graphics}/Debug.cs (98%) rename Engine/src/{Renderer => Graphics}/Framebuffer/Framebuffer.cs (88%) rename Engine/src/{Renderer => Graphics}/Framebuffer/FramebufferBuilder.cs (96%) rename Engine/src/{Renderer => Graphics}/Framebuffer/IFramebufferAttachment.cs (73%) rename Engine/src/{Renderer => Graphics}/Framebuffer/Renderbuffer.cs (93%) rename Engine/src/{Renderer => Graphics}/IPresenter.cs (82%) rename Engine/src/{Renderer => Graphics}/OpenGLObject.cs (92%) rename Engine/src/{Renderer => Graphics}/Pixel/IPixel.cs (87%) rename Engine/src/{Renderer => Graphics}/Pixel/R8.cs (92%) rename Engine/src/{Renderer => Graphics}/Pixel/Rgb8.cs (83%) create mode 100644 Engine/src/Graphics/Render/InstancedRenderer.cs create mode 100644 Engine/src/Graphics/Render/Mesh/MeshInstanceVertex.cs create mode 100644 Engine/src/Graphics/Render/Mesh/MeshRenderer.cs create mode 100644 Engine/src/Graphics/Render/Quad/QuadCommonVertex.cs create mode 100644 Engine/src/Graphics/Render/Quad/QuadInstanceVertex.cs create mode 100644 Engine/src/Graphics/Render/Quad/QuadRenderer.cs rename Engine/src/{Renderer => Graphics}/Renderer.cs (58%) rename Engine/src/{Renderer/Shader/ShaderProgram.cs => Graphics/Shader/Program.cs} (78%) create mode 100644 Engine/src/Graphics/Shader/ProgramLoader.cs rename Engine/src/{Renderer => Graphics}/Texture/DynamicTexture.cs (84%) rename Engine/src/{Renderer => Graphics}/Texture/IConstTexture.cs (91%) rename Engine/src/{Renderer => Graphics}/Texture/ITexture.cs (92%) rename Engine/src/{Renderer => Graphics}/Texture/StaticTexture.cs (87%) rename Engine/src/{Renderer => Graphics}/Texture/Texture.cs (77%) create mode 100644 Engine/src/Graphics/Texture/TextureUnitMap.cs delete mode 100644 Engine/src/Renderer/Buffer/VertexBuffer.cs create mode 100644 Engine/src/Resource/ShaderResource.Designer.cs create mode 100644 Engine/src/Resource/ShaderResource.resx create mode 100644 PresenterConsole/assets/shader/ascii.shader create mode 100644 PresenterConsole/src/AsciiPixel.cs create mode 100644 PresenterConsole/src/AsciiVertex.cs create mode 100644 PresenterConsole/src/ConsoleFastOutput.cs create mode 100644 PresenterConsole/src/Program.cs create mode 100644 PresenterConsole/src/Resource/ShaderResource.Designer.cs create mode 100644 PresenterConsole/src/Resource/ShaderResource.resx delete mode 100644 PresenterWPF/PresenterWPF.csproj create mode 100644 PresenterWpf/PresenterWpf.csproj create mode 100644 PresenterWpf/src/App.xaml create mode 100644 PresenterWpf/src/App.xaml.cs create mode 100644 PresenterWpf/src/AssemblyInfo.cs create mode 100644 PresenterWpf/src/MainWindow.xaml create mode 100644 PresenterWpf/src/MainWindow.xaml.cs create mode 100644 PresenterWpf/src/Program.cs diff --git a/.editorconfig b/.editorconfig index f8e79e5..ce75165 100644 --- a/.editorconfig +++ b/.editorconfig @@ -19,7 +19,7 @@ insert_final_newline = false # Organize usings dotnet_separate_import_directive_groups = false -dotnet_sort_system_directives_first = false +dotnet_sort_system_directives_first = true file_header_template = unset # this. and Me. preferences @@ -101,7 +101,7 @@ csharp_style_conditional_delegate_call = true # Modifier preferences csharp_prefer_static_local_function = true -csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async +csharp_preferred_modifier_order = public, private, protected, internal, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, volatile, async # Code-block preferences csharp_prefer_braces = true diff --git a/DoomDeathmatch/src/Program.cs b/DoomDeathmatch/src/Program.cs index 2989378..9e8ab43 100644 --- a/DoomDeathmatch/src/Program.cs +++ b/DoomDeathmatch/src/Program.cs @@ -14,7 +14,7 @@ internal abstract class Program public static void Main(string[] args) { - var engine = new Engine.Engine(1280, 720, false, "Doom Deathmatch"); + var engine = new Engine.Engine(480, 270, false, "Doom Deathmatch"); engine.Run(); } } \ No newline at end of file diff --git a/DoomDeathmatch/src/QuadVertex.cs b/DoomDeathmatch/src/QuadVertex.cs index 5582151..b29ae81 100644 --- a/DoomDeathmatch/src/QuadVertex.cs +++ b/DoomDeathmatch/src/QuadVertex.cs @@ -1,4 +1,4 @@ -using Engine.Renderer.Buffer.Vertex; +using Engine.Graphics.Buffer.Vertex; using OpenTK.Graphics.OpenGL; using OpenTK.Mathematics; using Half = System.Half; diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj index 67df8d7..efd2e0e 100644 --- a/Engine/Engine.csproj +++ b/Engine/Engine.csproj @@ -11,12 +11,30 @@ - - + + + + + + + ResXFileCodeGenerator + Test.Designer.cs + PreserveNewest + + + + + + True + True + ShaderResource.resx + + + diff --git a/Engine/assets/shader/mesh.shader b/Engine/assets/shader/mesh.shader new file mode 100644 index 0000000..3d4ec80 --- /dev/null +++ b/Engine/assets/shader/mesh.shader @@ -0,0 +1,34 @@ +// #type vertex +#version 460 core + +uniform mat4 uProjectionMatrix; +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 = 0) out vec2 oTexCoords; + +void main() +{ + gl_Position = uProjectionMatrix * uViewMatrix * aModelMatrix * vec4(aPos, 1.0); + oTexCoords = aTexCoords; +} + +// #type fragment +#version 460 core + +uniform sampler2D uTexture; + +layout (location = 0) in vec2 iTexCoords; + +layout (location = 0) out vec4 FragColor; + +void main() +{ + FragColor = texture(uTexture, iTexCoords); + if (FragColor.a == 0.0) + discard; +} \ No newline at end of file diff --git a/Engine/assets/shader/quad.shader b/Engine/assets/shader/quad.shader new file mode 100644 index 0000000..b92e1b0 --- /dev/null +++ b/Engine/assets/shader/quad.shader @@ -0,0 +1,46 @@ +// #type vertex +#version 460 core + +uniform mat4 uProjectionMatrix; +uniform mat4 uViewMatrix; + +layout (location = 0) in vec3 aPosition; +layout (location = 1) in vec2 aUV; +layout (location = 2) in vec4 aColor; +layout (location = 3) in int aTextureId; +layout (location = 4) in mat4 aModel; + +layout (location = 0) out vec4 oColor; +layout (location = 1) out vec2 oUV; +layout (location = 2) out int oTextureId; + +void main() +{ + oColor = aColor; + oUV = aUV; + oTextureId = aTextureId; + + gl_Position = uProjectionMatrix * uViewMatrix * aModel * vec4(aPosition, 1.0); +} + +// #type fragment +#version 460 core + +uniform sampler2D uTexture[16]; + +layout (location = 0) in vec4 iColor; +layout (location = 1) in vec2 iUV; +layout (location = 2) flat in int iTextureId; + +layout (location = 0) out vec4 FragColor; + +void main() +{ + FragColor = iColor; + + if (iTextureId >= 0) + FragColor *= texture(uTexture[iTextureId], iUV); + + if (FragColor.a == 0.0) + discard; +} \ No newline at end of file diff --git a/Engine/src/Asset/Image.cs b/Engine/src/Asset/Image.cs index 25bd691..8b1ee4b 100644 --- a/Engine/src/Asset/Image.cs +++ b/Engine/src/Asset/Image.cs @@ -1,18 +1,18 @@ -using Engine.Renderer.Pixel; -using Engine.Renderer.Texture; +using Engine.Graphics.Pixel; +using Engine.Graphics.Texture; namespace Engine.Asset; public class Image(T[,] parPixels) where T : struct, IPixel { - public int Width { get; } = parPixels.GetLength(0); - public int Height { get; } = parPixels.GetLength(1); + public int Width { get; } = parPixels.GetLength(1); + public int Height { get; } = parPixels.GetLength(0); public T[,] Pixels { get; } = parPixels; - public T this[int parX, int parY] => Pixels[parX, parY]; + public T this[int parY, int parX] => Pixels[parY, parX]; - public Image(int parWidth, int parHeight) : this(new T[parWidth, parHeight]) + public Image(int parWidth, int parHeight) : this(new T[parHeight, parWidth]) { } diff --git a/Engine/src/Asset/Mesh/Mesh.cs b/Engine/src/Asset/Mesh/Mesh.cs index 683fd5a..aa02542 100644 --- a/Engine/src/Asset/Mesh/Mesh.cs +++ b/Engine/src/Asset/Mesh/Mesh.cs @@ -1,4 +1,4 @@ -using Engine.Renderer.Buffer.Vertex; +using Engine.Graphics.Buffer.Vertex; using OpenTK.Graphics.OpenGL; using OpenTK.Mathematics; @@ -15,6 +15,11 @@ public class Mesh private readonly List _vertices = []; private readonly List _indices = []; + public override int GetHashCode() + { + return HashCode.Combine(Indices, Vertices); + } + public record struct Vertex : IVertex { [Vertex(VertexAttribType.Float, 3)] public Vector3 _position; diff --git a/Engine/src/Engine.cs b/Engine/src/Engine.cs index e6fee70..853b80d 100644 --- a/Engine/src/Engine.cs +++ b/Engine/src/Engine.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Engine.Graphics; using Engine.Input; using Engine.Scene; using OpenTK.Graphics.OpenGL; @@ -12,14 +13,15 @@ namespace Engine; public sealed class Engine { - public Renderer.Renderer Renderer => _renderer; + public Renderer Renderer => _renderer; public SceneManager SceneManager => _sceneManager; private readonly Window _window; - private readonly Renderer.Renderer _renderer; + private readonly Renderer _renderer; private readonly SceneManager _sceneManager = new(); private readonly ILogger _logger; private readonly IInputHandler _inputHandler; + private readonly IPresenter _presenter; private Thread? _updateThread; @@ -30,12 +32,20 @@ public sealed class Engine ClientSize = parHeadless ? new Vector2i(1, 1) : new Vector2i(parWidth, parHeight), Title = parTitle, StartVisible = !parHeadless, - APIVersion = new Version(4, 5), + APIVersion = new Version(4, 6), Profile = ContextProfile.Compatability }; _window = new Window(this, new NativeWindow(settings), parHeadless); - _renderer = new Renderer.Renderer(parWidth, parHeight); + _renderer = new Renderer(parWidth, parHeight); + if (!parHeadless) + { + _window.Resize += parArgs => + { + _renderer.Resize(parArgs.Width, parArgs.Height); + }; + _presenter = _window; + } Thread.CurrentThread.Name = "RendererThread"; @@ -53,18 +63,36 @@ public sealed class Engine _logger = Log.ForContext(); } + public Engine(int parWidth, int parHeight, Func parPresenter) : this(parWidth, parHeight, true) + { + _presenter = parPresenter(this); + } + public void Run() { _updateThread = new Thread(RunUpdate); _updateThread.Start(); - GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f); - - GL.Viewport(0, 0, _window.Width, _window.Height); - - while (!_window.IsExiting) + var timer = Stopwatch.StartNew(); + var deltaTime = 0.0; + while (!_presenter.IsExiting) { - _window.Update(); + var time = deltaTime; + _renderer.Commit(_ => GL.ClearColor(0.6f, 0.0f, 0.6f, 1.0f)); + _renderer.Commit(_ => GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit)); + _renderer.Commit(parRenderer => + { + parRenderer.QuadRenderer.Commit( + Matrix4.CreateScale(MathF.Sin((float)time) * 2) * Matrix4.CreateRotationZ(MathF.Sin((float)time)), + new Vector4((MathF.Sin((float)time * 3) + 1.0f) / 2, (MathF.Sin((float)time) + 1.0f) / 2, (MathF.Sin((float)time * 5) + 1.0f) / 2, 1.0f)); + parRenderer.QuadRenderer.Render(Matrix4.Identity, Matrix4.Identity); + parRenderer.QuadRenderer.Reset(); + }); + _renderer.Render(); + _presenter.Present(_renderer.TextureInternal); + + _presenter.Update(deltaTime); + deltaTime = timer.Elapsed.TotalSeconds; } _updateThread.Join(); @@ -73,11 +101,12 @@ public sealed class Engine private void RunUpdate() { var timer = Stopwatch.StartNew(); - while (!_window.IsExiting) + var deltaTime = 0.0; + while (!_presenter.IsExiting) { - _window.Update(); - _sceneManager.Update(timer.Elapsed.TotalSeconds); + _sceneManager.Update(deltaTime); timer.Restart(); + deltaTime = timer.Elapsed.TotalSeconds; } } } \ No newline at end of file diff --git a/Engine/src/Renderer/Buffer/IndexBuffer.cs b/Engine/src/Graphics/Buffer/IndexBuffer.cs similarity index 69% rename from Engine/src/Renderer/Buffer/IndexBuffer.cs rename to Engine/src/Graphics/Buffer/IndexBuffer.cs index 81ef8c8..2d7b13a 100644 --- a/Engine/src/Renderer/Buffer/IndexBuffer.cs +++ b/Engine/src/Graphics/Buffer/IndexBuffer.cs @@ -1,13 +1,13 @@ using OpenTK.Graphics.OpenGL; using Serilog; -namespace Engine.Renderer.Buffer; +namespace Engine.Graphics.Buffer; public class IndexBuffer : OpenGlObject { internal int Count { get; } - public IndexBuffer(int parCount, BufferStorageFlags parFlags) + public IndexBuffer(int parCount, BufferStorageFlags parFlags = BufferStorageFlags.None) { Count = parCount; @@ -19,6 +19,19 @@ public class IndexBuffer : OpenGlObject Log.Debug("Index buffer {Handle} created with {Count} elements", Handle, Count); } + + public IndexBuffer(uint[] parData, BufferStorageFlags parFlags = BufferStorageFlags.None) + { + Count = parData.Length; + + GL.CreateBuffers(1, out int handle); + Handle = handle; + + GL.NamedBufferStorage(Handle, Count * sizeof(uint), parData, parFlags); + + Log.Debug("Index buffer {Handle} created with {Count} elements", Handle, Count); + } + public void UploadData(uint[] parData) { UploadData(0, parData); diff --git a/Engine/src/Renderer/Buffer/Vertex/IVertex.cs b/Engine/src/Graphics/Buffer/Vertex/IVertex.cs similarity index 93% rename from Engine/src/Renderer/Buffer/Vertex/IVertex.cs rename to Engine/src/Graphics/Buffer/Vertex/IVertex.cs index 9276fc8..ee7d3a4 100644 --- a/Engine/src/Renderer/Buffer/Vertex/IVertex.cs +++ b/Engine/src/Graphics/Buffer/Vertex/IVertex.cs @@ -2,7 +2,7 @@ using System.Runtime.InteropServices; using OpenTK.Graphics.OpenGL; -namespace Engine.Renderer.Buffer.Vertex; +namespace Engine.Graphics.Buffer.Vertex; public interface IVertex { @@ -23,7 +23,7 @@ public interface IVertex return false; } - IOrderedEnumerable? fields = GetFields(parType); + var fields = GetFields(parType); var totalSize = 0; foreach (var field in fields) { diff --git a/Engine/src/Renderer/Buffer/Vertex/VertexAttribute.cs b/Engine/src/Graphics/Buffer/Vertex/VertexAttribute.cs similarity index 85% rename from Engine/src/Renderer/Buffer/Vertex/VertexAttribute.cs rename to Engine/src/Graphics/Buffer/Vertex/VertexAttribute.cs index 2a79028..630af55 100644 --- a/Engine/src/Renderer/Buffer/Vertex/VertexAttribute.cs +++ b/Engine/src/Graphics/Buffer/Vertex/VertexAttribute.cs @@ -1,6 +1,6 @@ using OpenTK.Graphics.OpenGL; -namespace Engine.Renderer.Buffer.Vertex; +namespace Engine.Graphics.Buffer.Vertex; [AttributeUsage(AttributeTargets.Field)] public class VertexAttribute : Attribute @@ -10,8 +10,9 @@ public class VertexAttribute : Attribute public bool Normalized { get; } public int RepeatCount { get; } - public VertexAttribute(VertexAttribType parType, int parComponentCount = 1, bool parNormalized = false, - int parRepeatCount = 1) + public VertexAttribute(VertexAttribType parType, int parComponentCount = 1, int parRepeatCount = 1, + bool parNormalized = false + ) { if (parComponentCount <= 0) { diff --git a/Engine/src/Renderer/Buffer/VertexArray.cs b/Engine/src/Graphics/Buffer/VertexArray.cs similarity index 91% rename from Engine/src/Renderer/Buffer/VertexArray.cs rename to Engine/src/Graphics/Buffer/VertexArray.cs index d3d3f1b..9a5d0e9 100644 --- a/Engine/src/Renderer/Buffer/VertexArray.cs +++ b/Engine/src/Graphics/Buffer/VertexArray.cs @@ -1,15 +1,16 @@ using System.Reflection; using System.Runtime.InteropServices; -using Engine.Renderer.Buffer.Vertex; +using Engine.Graphics.Buffer.Vertex; using OpenTK.Graphics.OpenGL; using Serilog; -namespace Engine.Renderer.Buffer; +namespace Engine.Graphics.Buffer; public class VertexArray : OpenGlObject { // private IndexBuffer? _boundIndexBuffer; // private readonly Dictionary> _boundVertexBuffers = new(); + private int _enabledAttribs = 0; public VertexArray() { @@ -42,15 +43,14 @@ public class VertexArray : OpenGlObject GL.VertexArrayVertexBuffer(Handle, parBindingIndex, parBuffer.Handle, 0, stride); - var location = 0; foreach (var field in fields) { var attribute = field.GetCustomAttribute()!; var offset = Marshal.OffsetOf(field.Name).ToInt32(); - SetupAttribute(attribute, location, offset, parBindingIndex); + SetupAttribute(attribute, _enabledAttribs, offset, parBindingIndex); - location += attribute.RepeatCount; + _enabledAttribs += attribute.RepeatCount; } GL.VertexArrayBindingDivisor(Handle, parBindingIndex, parDivisor); diff --git a/Engine/src/Graphics/Buffer/VertexBuffer.cs b/Engine/src/Graphics/Buffer/VertexBuffer.cs new file mode 100644 index 0000000..88fc71e --- /dev/null +++ b/Engine/src/Graphics/Buffer/VertexBuffer.cs @@ -0,0 +1,108 @@ +using System.Runtime.InteropServices; +using Engine.Graphics.Buffer.Vertex; +using OpenTK.Graphics.OpenGL; +using Serilog; + +namespace Engine.Graphics.Buffer; + +public class VertexBuffer : OpenGlObject + where T : struct, IVertex +{ + internal int Count { get; } + + private readonly int _stride = Marshal.SizeOf(); + + public VertexBuffer(int parCount, BufferStorageFlags parFlags = BufferStorageFlags.None) + { + if (!IVertex.IsValid(typeof(T))) + { + throw new ArgumentException($"Type {typeof(T).Name} is not a valid vertex type"); + } + + if (parCount <= 0) + { + throw new ArgumentException("Count must be greater than 0"); + } + + Count = parCount; + + GL.CreateBuffers(1, out int handle); + Handle = handle; + + GL.NamedBufferStorage(Handle, Count * _stride, IntPtr.Zero, parFlags); + + Log.Debug("Vertex buffer {Handle} created with {Count} elements of type {Type}", Handle, Count, typeof(T).Name); + } + + public VertexBuffer(T[] parData, BufferStorageFlags parFlags = BufferStorageFlags.None) + { + if (!IVertex.IsValid(typeof(T))) + { + throw new ArgumentException($"Type {typeof(T).Name} is not a valid vertex type"); + } + + if (parData.Length <= 0) + { + throw new ArgumentException("Count must be greater than 0"); + } + + Count = parData.Length; + + GL.CreateBuffers(1, out int handle); + Handle = handle; + + GL.NamedBufferStorage(Handle, Count * _stride, parData, parFlags); + + Log.Debug("Vertex buffer {Handle} created with {Count} elements of type {Type}", Handle, Count, typeof(T).Name); + } + + public void UploadData(T[] parData) + { + UploadData(0, parData, parData.Length); + } + + public void UploadData(T[] parData, int parCount) + { + UploadData(0, parData, parCount); + } + + public void UploadData(int parOffset, T[] parData, int parCount) + { + if (parOffset < 0) + { + throw new ArgumentException("Offset must be greater than 0"); + } + + if (parCount <= 0) + { + throw new ArgumentException("Count must be greater than 0"); + } + + if (parData.Length < parCount) + { + throw new ArgumentException("Data array is too small"); + } + + if (parCount + parOffset > Count) + { + throw new ArgumentException("Data array is too large"); + } + + GL.NamedBufferSubData(Handle, parOffset * _stride, parCount * _stride, parData); + } + + public override void Bind() + { + GL.BindBuffer(BufferTarget.ArrayBuffer, Handle); + } + + public override void Unbind() + { + GL.BindBuffer(BufferTarget.ArrayBuffer, 0); + } + + protected override void Destroy() + { + GL.DeleteBuffer(Handle); + } +} \ No newline at end of file diff --git a/Engine/src/Renderer/Camera/ICamera.cs b/Engine/src/Graphics/Camera/ICamera.cs similarity index 84% rename from Engine/src/Renderer/Camera/ICamera.cs rename to Engine/src/Graphics/Camera/ICamera.cs index 5a8d140..f1caed7 100644 --- a/Engine/src/Renderer/Camera/ICamera.cs +++ b/Engine/src/Graphics/Camera/ICamera.cs @@ -1,6 +1,6 @@ using OpenTK.Mathematics; -namespace Engine.Renderer.Camera; +namespace Engine.Graphics.Camera; public interface ICamera { diff --git a/Engine/src/Renderer/Camera/ScreenspaceCamera.cs b/Engine/src/Graphics/Camera/ScreenspaceCamera.cs similarity index 86% rename from Engine/src/Renderer/Camera/ScreenspaceCamera.cs rename to Engine/src/Graphics/Camera/ScreenspaceCamera.cs index a29fa99..2587f5d 100644 --- a/Engine/src/Renderer/Camera/ScreenspaceCamera.cs +++ b/Engine/src/Graphics/Camera/ScreenspaceCamera.cs @@ -1,6 +1,6 @@ using OpenTK.Mathematics; -namespace Engine.Renderer.Camera; +namespace Engine.Graphics.Camera; public class ScreenspaceCamera : ICamera { diff --git a/Engine/src/Renderer/Debug.cs b/Engine/src/Graphics/Debug.cs similarity index 98% rename from Engine/src/Renderer/Debug.cs rename to Engine/src/Graphics/Debug.cs index cbaa41a..9fdb771 100644 --- a/Engine/src/Renderer/Debug.cs +++ b/Engine/src/Graphics/Debug.cs @@ -3,7 +3,7 @@ using OpenTK.Graphics.OpenGL; using Serilog; using Serilog.Events; -namespace Engine.Renderer; +namespace Engine.Graphics; internal static class Debug { diff --git a/Engine/src/Renderer/Framebuffer/Framebuffer.cs b/Engine/src/Graphics/Framebuffer/Framebuffer.cs similarity index 88% rename from Engine/src/Renderer/Framebuffer/Framebuffer.cs rename to Engine/src/Graphics/Framebuffer/Framebuffer.cs index 9e8d1e2..3fe3a38 100644 --- a/Engine/src/Renderer/Framebuffer/Framebuffer.cs +++ b/Engine/src/Graphics/Framebuffer/Framebuffer.cs @@ -1,9 +1,9 @@ using System.Collections.ObjectModel; -using Engine.Renderer.Pixel; -using Engine.Renderer.Texture; +using Engine.Graphics.Texture; using OpenTK.Graphics.OpenGL; +using Serilog; -namespace Engine.Renderer.Framebuffer; +namespace Engine.Graphics.Framebuffer; public class Framebuffer : OpenGlObject { @@ -55,10 +55,10 @@ public class Framebuffer : OpenGlObject { switch (attachment.Value.Type) { - case IFramebufferAttachment.AttachmentType._texture: + case IFramebufferAttachment.AttachmentType.Texture: GL.NamedFramebufferTexture(Handle, attachment.Key, attachment.Value.Handle, 0); break; - case IFramebufferAttachment.AttachmentType._renderbuffer: + case IFramebufferAttachment.AttachmentType.Renderbuffer: GL.NamedFramebufferRenderbuffer(Handle, attachment.Key, RenderbufferTarget.Renderbuffer, attachment.Value.Handle); break; @@ -102,10 +102,12 @@ public class Framebuffer : OpenGlObject Width = parWidth; Height = parHeight; - foreach (KeyValuePair attachment in _attachments) + foreach (var attachment in _attachments) { attachment.Value.Resize(parWidth, parHeight); } + + Log.Debug("Framebuffer {Handle} resized to {Width}x{Height}", Handle, parWidth, parHeight); } public override void Bind() diff --git a/Engine/src/Renderer/Framebuffer/FramebufferBuilder.cs b/Engine/src/Graphics/Framebuffer/FramebufferBuilder.cs similarity index 96% rename from Engine/src/Renderer/Framebuffer/FramebufferBuilder.cs rename to Engine/src/Graphics/Framebuffer/FramebufferBuilder.cs index e1cac61..a5cf4b6 100644 --- a/Engine/src/Renderer/Framebuffer/FramebufferBuilder.cs +++ b/Engine/src/Graphics/Framebuffer/FramebufferBuilder.cs @@ -1,8 +1,8 @@ -using Engine.Renderer.Pixel; -using Engine.Renderer.Texture; +using Engine.Graphics.Pixel; +using Engine.Graphics.Texture; using OpenTK.Graphics.OpenGL; -namespace Engine.Renderer.Framebuffer; +namespace Engine.Graphics.Framebuffer; public class FramebufferBuilder(int parWidth, int parHeight) { diff --git a/Engine/src/Renderer/Framebuffer/IFramebufferAttachment.cs b/Engine/src/Graphics/Framebuffer/IFramebufferAttachment.cs similarity index 73% rename from Engine/src/Renderer/Framebuffer/IFramebufferAttachment.cs rename to Engine/src/Graphics/Framebuffer/IFramebufferAttachment.cs index e95069c..5ae762d 100644 --- a/Engine/src/Renderer/Framebuffer/IFramebufferAttachment.cs +++ b/Engine/src/Graphics/Framebuffer/IFramebufferAttachment.cs @@ -1,4 +1,4 @@ -namespace Engine.Renderer.Framebuffer; +namespace Engine.Graphics.Framebuffer; public interface IFramebufferAttachment { @@ -9,7 +9,7 @@ public interface IFramebufferAttachment public enum AttachmentType { - _texture, - _renderbuffer + Texture, + Renderbuffer } } \ No newline at end of file diff --git a/Engine/src/Renderer/Framebuffer/Renderbuffer.cs b/Engine/src/Graphics/Framebuffer/Renderbuffer.cs similarity index 93% rename from Engine/src/Renderer/Framebuffer/Renderbuffer.cs rename to Engine/src/Graphics/Framebuffer/Renderbuffer.cs index 98a95d8..301aeda 100644 --- a/Engine/src/Renderer/Framebuffer/Renderbuffer.cs +++ b/Engine/src/Graphics/Framebuffer/Renderbuffer.cs @@ -1,13 +1,13 @@ using OpenTK.Graphics.OpenGL; -namespace Engine.Renderer.Framebuffer; +namespace Engine.Graphics.Framebuffer; public class Renderbuffer : OpenGlObject, IFramebufferAttachment { public int Width { get; private set; } public int Height { get; private set; } - public IFramebufferAttachment.AttachmentType Type => IFramebufferAttachment.AttachmentType._renderbuffer; + public IFramebufferAttachment.AttachmentType Type => IFramebufferAttachment.AttachmentType.Renderbuffer; private readonly RenderbufferStorage _format; diff --git a/Engine/src/Renderer/IPresenter.cs b/Engine/src/Graphics/IPresenter.cs similarity index 82% rename from Engine/src/Renderer/IPresenter.cs rename to Engine/src/Graphics/IPresenter.cs index aa435fd..445301b 100644 --- a/Engine/src/Renderer/IPresenter.cs +++ b/Engine/src/Graphics/IPresenter.cs @@ -1,8 +1,8 @@ -using Engine.Renderer.Texture; +using Engine.Graphics.Texture; using Engine.Scene; using OpenTK.Windowing.Common; -namespace Engine.Renderer; +namespace Engine.Graphics; public interface IPresenter : IUpdate { diff --git a/Engine/src/Renderer/OpenGLObject.cs b/Engine/src/Graphics/OpenGLObject.cs similarity index 92% rename from Engine/src/Renderer/OpenGLObject.cs rename to Engine/src/Graphics/OpenGLObject.cs index 9593b22..2d69d00 100644 --- a/Engine/src/Renderer/OpenGLObject.cs +++ b/Engine/src/Graphics/OpenGLObject.cs @@ -1,6 +1,6 @@ using Serilog; -namespace Engine.Renderer; +namespace Engine.Graphics; public abstract class OpenGlObject { @@ -14,8 +14,9 @@ public abstract class OpenGlObject ~OpenGlObject() { Destroy(); - Handle = -1; Log.Debug("OpenGL object {Handle} destroyed", Handle); + + Handle = -1; } } \ No newline at end of file diff --git a/Engine/src/Renderer/Pixel/IPixel.cs b/Engine/src/Graphics/Pixel/IPixel.cs similarity index 87% rename from Engine/src/Renderer/Pixel/IPixel.cs rename to Engine/src/Graphics/Pixel/IPixel.cs index 8ad0a8f..afdfa09 100644 --- a/Engine/src/Renderer/Pixel/IPixel.cs +++ b/Engine/src/Graphics/Pixel/IPixel.cs @@ -1,6 +1,6 @@ using OpenTK.Graphics.OpenGL; -namespace Engine.Renderer.Pixel; +namespace Engine.Graphics.Pixel; public interface IPixel { diff --git a/Engine/src/Renderer/Pixel/R8.cs b/Engine/src/Graphics/Pixel/R8.cs similarity index 92% rename from Engine/src/Renderer/Pixel/R8.cs rename to Engine/src/Graphics/Pixel/R8.cs index 95fc236..751cc38 100644 --- a/Engine/src/Renderer/Pixel/R8.cs +++ b/Engine/src/Graphics/Pixel/R8.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using OpenTK.Graphics.OpenGL; -namespace Engine.Renderer.Pixel; +namespace Engine.Graphics.Pixel; [StructLayout(LayoutKind.Sequential)] public struct R8 : IPixel diff --git a/Engine/src/Renderer/Pixel/Rgb8.cs b/Engine/src/Graphics/Pixel/Rgb8.cs similarity index 83% rename from Engine/src/Renderer/Pixel/Rgb8.cs rename to Engine/src/Graphics/Pixel/Rgb8.cs index 9440901..fd40fa9 100644 --- a/Engine/src/Renderer/Pixel/Rgb8.cs +++ b/Engine/src/Graphics/Pixel/Rgb8.cs @@ -1,7 +1,7 @@ using System.Runtime.InteropServices; using OpenTK.Graphics.OpenGL; -namespace Engine.Renderer.Pixel; +namespace Engine.Graphics.Pixel; [StructLayout(LayoutKind.Sequential)] public struct Rgb8 : IPixel @@ -15,4 +15,6 @@ public struct Rgb8 : IPixel public byte R; public byte G; public byte B; + + public override string ToString() => $"{R}, {G}, {B}"; } \ No newline at end of file diff --git a/Engine/src/Graphics/Render/InstancedRenderer.cs b/Engine/src/Graphics/Render/InstancedRenderer.cs new file mode 100644 index 0000000..e0a0e9e --- /dev/null +++ b/Engine/src/Graphics/Render/InstancedRenderer.cs @@ -0,0 +1,79 @@ +using Engine.Graphics.Buffer; +using Engine.Graphics.Buffer.Vertex; +using Engine.Graphics.Shader; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; + +namespace Engine.Graphics.Render; + +public abstract class InstancedRenderer + where C : struct, IVertex + where I : struct, IVertex +{ + protected readonly Renderer _renderer; + + protected readonly IndexBuffer _indexBuffer; + protected readonly VertexBuffer _commonVertexBuffer; + protected readonly VertexBuffer _instanceVertexBuffer; + protected readonly VertexArray _vertexArray; + + protected readonly int _instanceCount; + protected int _queuedInstanceCount; + protected readonly I[] _instanceVertices; + + private readonly PrimitiveType _primitiveType; + private readonly Program _program; + + protected InstancedRenderer(Renderer parRenderer, PrimitiveType parPrimitiveType, int parInstanceCount, + uint[] parIndexBuffer, C[] parInstanceBuffer, + Program parProgram) + { + _program = parProgram; + _renderer = parRenderer; + _instanceCount = parInstanceCount; + _queuedInstanceCount = 0; + _instanceVertices = new I[parInstanceCount]; + _primitiveType = parPrimitiveType; + + _indexBuffer = new IndexBuffer(parIndexBuffer); + _commonVertexBuffer = new VertexBuffer(parInstanceBuffer); + _instanceVertexBuffer = new VertexBuffer(parInstanceCount, + BufferStorageFlags.DynamicStorageBit); + _vertexArray = new VertexArray(); + + _vertexArray.BindIndexBuffer(_indexBuffer); + _vertexArray.BindVertexBuffer(_commonVertexBuffer, 0, 0); + _vertexArray.BindVertexBuffer(_instanceVertexBuffer, 1, 1); + } + + public void Render(in Matrix4 parProjectionMatrix, in Matrix4 parViewMatrix) + { + if (_queuedInstanceCount <= 0) + { + return; + } + + _renderer.EnsureRenderThread(); + + _instanceVertexBuffer.UploadData(_instanceVertices, _queuedInstanceCount); + _vertexArray.Bind(); + + _program.Bind(); + _program.SetUniform("uProjectionMatrix", in parProjectionMatrix); + _program.SetUniform("uViewMatrix", in parViewMatrix); + SetAdditionalUniforms(_program); + + GL.DrawElementsInstanced(_primitiveType, _indexBuffer.Count * _queuedInstanceCount, + DrawElementsType.UnsignedInt, 0, + _queuedInstanceCount); + } + + public virtual void Reset() + { + _queuedInstanceCount = 0; + } + + protected virtual void SetAdditionalUniforms(Program parProgram) + { + } +} \ No newline at end of file diff --git a/Engine/src/Graphics/Render/Mesh/MeshInstanceVertex.cs b/Engine/src/Graphics/Render/Mesh/MeshInstanceVertex.cs new file mode 100644 index 0000000..49d8a6e --- /dev/null +++ b/Engine/src/Graphics/Render/Mesh/MeshInstanceVertex.cs @@ -0,0 +1,10 @@ +using Engine.Graphics.Buffer.Vertex; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; + +namespace Engine.Graphics.Render.Mesh; + +public struct MeshInstanceVertex : IVertex +{ + [Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix; +} \ No newline at end of file diff --git a/Engine/src/Graphics/Render/Mesh/MeshRenderer.cs b/Engine/src/Graphics/Render/Mesh/MeshRenderer.cs new file mode 100644 index 0000000..1b60b0e --- /dev/null +++ b/Engine/src/Graphics/Render/Mesh/MeshRenderer.cs @@ -0,0 +1,26 @@ +using Engine.Graphics.Shader; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; + +namespace Engine.Graphics.Render.Mesh; + +public class MeshRenderer : InstancedRenderer +{ + public MeshRenderer(Renderer parRenderer, int parInstanceCount, Asset.Mesh.Mesh parMesh) + : base(parRenderer, PrimitiveType.Triangles, parInstanceCount, parMesh.Indices.ToArray(), + parMesh.Vertices.ToArray(), + ProgramLoader.LoadFromSource(ShaderResource.Mesh)) + { + } + + public void Commit(Matrix4 parModelMatrix) + { + if (_queuedInstanceCount >= _instanceCount) + { + throw new InvalidOperationException("Instance count exceeded"); + } + + _instanceVertices[_queuedInstanceCount]._modelMatrix = parModelMatrix; + _queuedInstanceCount++; + } +} \ No newline at end of file diff --git a/Engine/src/Graphics/Render/Quad/QuadCommonVertex.cs b/Engine/src/Graphics/Render/Quad/QuadCommonVertex.cs new file mode 100644 index 0000000..ca304e7 --- /dev/null +++ b/Engine/src/Graphics/Render/Quad/QuadCommonVertex.cs @@ -0,0 +1,11 @@ +using Engine.Graphics.Buffer.Vertex; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; + +namespace Engine.Graphics.Render.Quad; + +public struct QuadCommonVertex : IVertex +{ + [Vertex(VertexAttribType.Float, 3)] public Vector3 _position; + [Vertex(VertexAttribType.Float, 2)] public Vector2 _uv; +} \ No newline at end of file diff --git a/Engine/src/Graphics/Render/Quad/QuadInstanceVertex.cs b/Engine/src/Graphics/Render/Quad/QuadInstanceVertex.cs new file mode 100644 index 0000000..248499f --- /dev/null +++ b/Engine/src/Graphics/Render/Quad/QuadInstanceVertex.cs @@ -0,0 +1,12 @@ +using Engine.Graphics.Buffer.Vertex; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; + +namespace Engine.Graphics.Render.Quad; + +public struct QuadInstanceVertex : IVertex +{ + [Vertex(VertexAttribType.Float, 4)] public Vector4 _color; + [Vertex(VertexAttribType.Int)] public int _textureId; + [Vertex(VertexAttribType.Float, 4, 4)] public Matrix4 _modelMatrix; +} \ No newline at end of file diff --git a/Engine/src/Graphics/Render/Quad/QuadRenderer.cs b/Engine/src/Graphics/Render/Quad/QuadRenderer.cs new file mode 100644 index 0000000..1657552 --- /dev/null +++ b/Engine/src/Graphics/Render/Quad/QuadRenderer.cs @@ -0,0 +1,59 @@ +using Engine.Graphics.Shader; +using Engine.Graphics.Texture; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; + +namespace Engine.Graphics.Render.Quad; + +public class QuadRenderer : InstancedRenderer +{ + private readonly TextureUnitMap _textureUnitMap = new(16); + + public QuadRenderer(Renderer parRenderer, int parInstanceCount) + : base(parRenderer, PrimitiveType.Triangles, parInstanceCount, [0, 1, 2, 2, 3, 0], [ + new QuadCommonVertex { _position = new Vector3(-0.5f, -0.5f, 0), _uv = new Vector2(0, 1) }, + new QuadCommonVertex { _position = new Vector3(0.5f, -0.5f, 0), _uv = new Vector2(1, 1) }, + new QuadCommonVertex { _position = new Vector3(0.5f, 0.5f, 0), _uv = new Vector2(1, 0) }, + new QuadCommonVertex { _position = new Vector3(-0.5f, 0.5f, 0), _uv = new Vector2(0, 0) } + ], + ProgramLoader.LoadFromSource(ShaderResource.Quad)) + { + } + + public void Commit(in Matrix4 parModelMatrix, in Vector4 parColor, 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]._modelMatrix = parModelMatrix; + _instanceVertices[_queuedInstanceCount]._color = parColor; + _instanceVertices[_queuedInstanceCount]._textureId = textureId; + _queuedInstanceCount++; + } + + protected override void SetAdditionalUniforms(Program parProgram) + { + var samplers = new int[_textureUnitMap.Size]; + foreach (var (texture, unit) in _textureUnitMap) + { + texture.BindUnit(unit); + samplers[unit] = unit; + } + + parProgram.SetUniform("uTexture", samplers); + } + + public override void Reset() + { + base.Reset(); + _textureUnitMap.Reset(); + } +} \ No newline at end of file diff --git a/Engine/src/Renderer/Renderer.cs b/Engine/src/Graphics/Renderer.cs similarity index 58% rename from Engine/src/Renderer/Renderer.cs rename to Engine/src/Graphics/Renderer.cs index 888c86b..1a53718 100644 --- a/Engine/src/Renderer/Renderer.cs +++ b/Engine/src/Graphics/Renderer.cs @@ -1,30 +1,35 @@ -using System.Runtime.InteropServices; -using Engine.Renderer.Pixel; -using Engine.Renderer.Shader; +using Engine.Graphics.Pixel; +using Engine.Graphics.Render.Quad; using OpenTK.Graphics.OpenGL; -using Serilog; -using Serilog.Events; -namespace Engine.Renderer; +namespace Engine.Graphics; public class Renderer { internal Texture.Texture TextureInternal => _framebuffer.TextureInternal!; + public readonly QuadRenderer QuadRenderer; + private readonly Framebuffer.Framebuffer _framebuffer; private readonly Queue> _renderActions = new(); + private readonly Thread _renderThread; + public Renderer(int parWidth, int parHeight) { - InitializeOpenGl(); + InitializeOpenGl(parWidth, parHeight); + + _renderThread = Thread.CurrentThread; _framebuffer = Framebuffer.Framebuffer.Builder(parWidth, parHeight) .AddColorAttachment() .AddDepthAttachment() .Build(); + + QuadRenderer = new QuadRenderer(this, 1024 * 8); } - private void InitializeOpenGl() + private void InitializeOpenGl(int parWidth, int parHeight) { #if DEBUG Debug.Setup(); @@ -36,6 +41,18 @@ public class Renderer GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); GL.Enable(EnableCap.Blend); + + GL.Viewport(0, 0, parWidth, parHeight); + } + + public void EnsureRenderThread() + { + if (Thread.CurrentThread == _renderThread) + { + return; + } + + throw new InvalidOperationException("Renderer is not on render thread"); } internal void Commit(Action parRenderAction) @@ -45,8 +62,10 @@ public class Renderer internal void Render() { + EnsureRenderThread(); + _framebuffer.Bind(); - while (_renderActions.TryDequeue(out Action? renderAction)) + while (_renderActions.TryDequeue(out var renderAction)) { renderAction(this); } @@ -57,5 +76,6 @@ public class Renderer internal void Resize(int parWidth, int parHeight) { _framebuffer.Resize(parWidth, parHeight); + GL.Viewport(0, 0, parWidth, parHeight); } } \ No newline at end of file diff --git a/Engine/src/Renderer/Shader/ShaderProgram.cs b/Engine/src/Graphics/Shader/Program.cs similarity index 78% rename from Engine/src/Renderer/Shader/ShaderProgram.cs rename to Engine/src/Graphics/Shader/Program.cs index 1f428b7..457511b 100644 --- a/Engine/src/Renderer/Shader/ShaderProgram.cs +++ b/Engine/src/Graphics/Shader/Program.cs @@ -1,48 +1,14 @@ -using System.Runtime.CompilerServices; -using System.Text; -using OpenTK.Graphics.OpenGL; +using OpenTK.Graphics.OpenGL; using OpenTK.Mathematics; using Serilog; -namespace Engine.Renderer.Shader; +namespace Engine.Graphics.Shader; -public class ShaderProgram : OpenGlObject +public class Program : OpenGlObject { private readonly Dictionary _uniforms = new(); - public static ShaderProgram CreateFromSource(string parSource) - { - var vertexSource = new StringBuilder(); - var fragmentSource = new StringBuilder(); - var inFragment = false; - var inVertex = false; - - foreach (var line in parSource.Split('\n')) - { - if (line.StartsWith("#shader vertex")) - { - inVertex = true; - inFragment = false; - } - else if (line.StartsWith("#shader fragment")) - { - inVertex = false; - inFragment = true; - } - else if (inVertex) - { - vertexSource.AppendLine(line); - } - else if (inFragment) - { - fragmentSource.AppendLine(line); - } - } - - return new ShaderProgram(vertexSource.ToString(), fragmentSource.ToString()); - } - - public ShaderProgram(string parVertexSource, string parFragmentSource) + public Program(string parVertexSource, string parFragmentSource) { var vertexShader = CompileSource(parVertexSource, ShaderType.VertexShader); var fragmentShader = CompileSource(parFragmentSource, ShaderType.FragmentShader); @@ -50,7 +16,7 @@ public class ShaderProgram : OpenGlObject Handle = LinkProgram(vertexShader, fragmentShader); } - public void SetUniform(string parName, T parValue) + public void SetUniform(string parName, in T parValue) { try { @@ -79,6 +45,12 @@ public class ShaderProgram : OpenGlObject case Matrix4 matrix: GL.ProgramUniformMatrix4(Handle, location, false, ref matrix); break; + case int[] intArray: + GL.ProgramUniform1(Handle, location, intArray.Length, intArray); + break; + case float[] floatArray: + GL.ProgramUniform1(Handle, location, floatArray.Length, floatArray); + break; default: throw new ArgumentException($"Unsupported uniform type: {typeof(T).Name}"); } diff --git a/Engine/src/Graphics/Shader/ProgramLoader.cs b/Engine/src/Graphics/Shader/ProgramLoader.cs new file mode 100644 index 0000000..212dffb --- /dev/null +++ b/Engine/src/Graphics/Shader/ProgramLoader.cs @@ -0,0 +1,38 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace Engine.Graphics.Shader; + +public static partial class ProgramLoader +{ + [GeneratedRegex(@"^//\s+#type\s+(?[a-z]+)$", RegexOptions.Compiled)] + private static partial Regex TypeRegex(); + + public static Program LoadFromSource(string parSource) + { + var vertexSource = new StringBuilder(); + var fragmentSource = new StringBuilder(); + var inFragment = false; + var inVertex = false; + + foreach (var line in parSource.Split('\n').Select(parLine => parLine.TrimEnd())) + { + var match = TypeRegex().Match(line); + if (match.Success) + { + inVertex = match.Groups["type"].Value == "vertex"; + inFragment = match.Groups["type"].Value == "fragment"; + } + else if (inVertex) + { + vertexSource.AppendLine(line); + } + else if (inFragment) + { + fragmentSource.AppendLine(line); + } + } + + return new Program(vertexSource.ToString(), fragmentSource.ToString()); + } +} \ No newline at end of file diff --git a/Engine/src/Renderer/Texture/DynamicTexture.cs b/Engine/src/Graphics/Texture/DynamicTexture.cs similarity index 84% rename from Engine/src/Renderer/Texture/DynamicTexture.cs rename to Engine/src/Graphics/Texture/DynamicTexture.cs index 666c51a..7ba4e03 100644 --- a/Engine/src/Renderer/Texture/DynamicTexture.cs +++ b/Engine/src/Graphics/Texture/DynamicTexture.cs @@ -1,12 +1,13 @@ -using Engine.Renderer.Framebuffer; -using Engine.Renderer.Pixel; +using Engine.Graphics.Framebuffer; +using Engine.Graphics.Pixel; using OpenTK.Graphics.OpenGL; +using Serilog; -namespace Engine.Renderer.Texture; +namespace Engine.Graphics.Texture; public class DynamicTexture : Texture, IFramebufferAttachment { - public IFramebufferAttachment.AttachmentType Type => IFramebufferAttachment.AttachmentType._texture; + public IFramebufferAttachment.AttachmentType Type => IFramebufferAttachment.AttachmentType.Texture; private readonly PixelFormat _format; private readonly PixelType _type; @@ -45,5 +46,7 @@ public class DynamicTexture : Texture, IFramebufferAttachment Bind(); GL.TexImage2D(TextureTarget.Texture2D, 0, _internalFormat, Width, Height, 0, _format, _type, IntPtr.Zero); + + Log.Debug("Texture {Handle} resized to {Width}x{Height}", Handle, Width, Height); } } \ No newline at end of file diff --git a/Engine/src/Renderer/Texture/IConstTexture.cs b/Engine/src/Graphics/Texture/IConstTexture.cs similarity index 91% rename from Engine/src/Renderer/Texture/IConstTexture.cs rename to Engine/src/Graphics/Texture/IConstTexture.cs index 87ec8b0..0317b91 100644 --- a/Engine/src/Renderer/Texture/IConstTexture.cs +++ b/Engine/src/Graphics/Texture/IConstTexture.cs @@ -1,10 +1,12 @@ using Engine.Asset; -using Engine.Renderer.Pixel; +using Engine.Graphics.Pixel; +using OpenTK.Mathematics; -namespace Engine.Renderer.Texture; +namespace Engine.Graphics.Texture; public interface IConstTexture { + public Vector2i Size { get; } public int Width { get; } public int Height { get; } diff --git a/Engine/src/Renderer/Texture/ITexture.cs b/Engine/src/Graphics/Texture/ITexture.cs similarity index 92% rename from Engine/src/Renderer/Texture/ITexture.cs rename to Engine/src/Graphics/Texture/ITexture.cs index 124e554..0d3547c 100644 --- a/Engine/src/Renderer/Texture/ITexture.cs +++ b/Engine/src/Graphics/Texture/ITexture.cs @@ -1,7 +1,7 @@ using Engine.Asset; -using Engine.Renderer.Pixel; +using Engine.Graphics.Pixel; -namespace Engine.Renderer.Texture; +namespace Engine.Graphics.Texture; public interface ITexture : IConstTexture { diff --git a/Engine/src/Renderer/Texture/StaticTexture.cs b/Engine/src/Graphics/Texture/StaticTexture.cs similarity index 87% rename from Engine/src/Renderer/Texture/StaticTexture.cs rename to Engine/src/Graphics/Texture/StaticTexture.cs index f180d44..60db787 100644 --- a/Engine/src/Renderer/Texture/StaticTexture.cs +++ b/Engine/src/Graphics/Texture/StaticTexture.cs @@ -1,7 +1,7 @@ -using Engine.Renderer.Pixel; +using Engine.Graphics.Pixel; using OpenTK.Graphics.OpenGL; -namespace Engine.Renderer.Texture; +namespace Engine.Graphics.Texture; public class StaticTexture : Texture { diff --git a/Engine/src/Renderer/Texture/Texture.cs b/Engine/src/Graphics/Texture/Texture.cs similarity index 77% rename from Engine/src/Renderer/Texture/Texture.cs rename to Engine/src/Graphics/Texture/Texture.cs index 7324103..c5e49ce 100644 --- a/Engine/src/Renderer/Texture/Texture.cs +++ b/Engine/src/Graphics/Texture/Texture.cs @@ -1,11 +1,15 @@ using System.Runtime.InteropServices; -using Engine.Renderer.Pixel; +using Engine.Graphics.Pixel; using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; +using Serilog; -namespace Engine.Renderer.Texture; +namespace Engine.Graphics.Texture; public abstract class Texture : OpenGlObject, ITexture { + public Vector2i Size => new(Width, Height); + public int Width { get => _width; @@ -44,6 +48,13 @@ public abstract class Texture : OpenGlObject, ITexture GL.CreateTextures(TextureTarget.Texture2D, 1, out int handle); Handle = handle; + + GL.TextureParameter(Handle, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); + GL.TextureParameter(Handle, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear); + GL.TextureParameter(Handle, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat); + GL.TextureParameter(Handle, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat); + + Log.Debug("Texture {Handle} created with {Width}x{Height}", Handle, Width, Height); } public void UploadPixels(int parX, int parY, int parWidth, int parHeight, T[,] parPixels) @@ -101,6 +112,10 @@ public abstract class Texture : OpenGlObject, ITexture var format = default(T).Format; var type = default(T).Type; + // Set alignment to avoid row padding issues + GL.PixelStore(PixelStoreParameter.PackAlignment, 1); + GL.PixelStore(PixelStoreParameter.UnpackAlignment, 1); + GL.GetTextureSubImage(Handle, 0, parX, parY, 0, parWidth, parHeight, 1, format, type, parPixels.Length * Marshal.SizeOf(), parPixels); @@ -108,6 +123,7 @@ public abstract class Texture : OpenGlObject, ITexture public void BindUnit(int parUnit = 0) { + GL.ActiveTexture(TextureUnit.Texture0 + parUnit); GL.BindTextureUnit(parUnit, Handle); } diff --git a/Engine/src/Graphics/Texture/TextureUnitMap.cs b/Engine/src/Graphics/Texture/TextureUnitMap.cs new file mode 100644 index 0000000..eda650a --- /dev/null +++ b/Engine/src/Graphics/Texture/TextureUnitMap.cs @@ -0,0 +1,50 @@ +using System.Collections; + +namespace Engine.Graphics.Texture; + +public class TextureUnitMap : IEnumerable> +{ + public int Size => _textures.Count; + public int Capacity => _capacity; + + private readonly Dictionary _textures = new(); + private readonly int _capacity; + + public TextureUnitMap(int parCapacity) + { + _capacity = parCapacity; + } + + public int GetUnit(Texture parTexture) + { + if (_textures.TryGetValue(parTexture, out var unit)) + { + return unit; + } + + if (_textures.Count >= _capacity) + { + throw new InvalidOperationException("Texture unit map is full"); + } + + unit = _textures.Count; + _textures.Add(parTexture, unit); + + return unit; + } + + public void Reset() + { + _textures.Clear(); + } + + public IEnumerator> GetEnumerator() + { + return _textures.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} \ No newline at end of file diff --git a/Engine/src/Input/MouseButton.cs b/Engine/src/Input/MouseButton.cs index aedd735..41712a4 100644 --- a/Engine/src/Input/MouseButton.cs +++ b/Engine/src/Input/MouseButton.cs @@ -2,12 +2,12 @@ public enum MouseButton { - _left, - _right, - _middle, + Left, + Right, + Middle, - _button4, - _button5, - _button6, - _button7 + Button4, + Button5, + Button6, + Button7 } \ No newline at end of file diff --git a/Engine/src/Renderer/Buffer/VertexBuffer.cs b/Engine/src/Renderer/Buffer/VertexBuffer.cs deleted file mode 100644 index 65100a5..0000000 --- a/Engine/src/Renderer/Buffer/VertexBuffer.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System.Runtime.InteropServices; -using Engine.Renderer.Buffer.Vertex; -using OpenTK.Graphics.OpenGL; -using Serilog; - -namespace Engine.Renderer.Buffer; - -public class VertexBuffer : OpenGlObject - where T : struct, IVertex -{ - internal int Count { get; } - - private readonly int _stride = Marshal.SizeOf(); - - public VertexBuffer(int parCount, BufferStorageFlags parFlags) - { - if (!IVertex.IsValid(typeof(T))) - { - throw new ArgumentException($"Type {typeof(T).Name} is not a valid vertex type"); - } - - if (parCount <= 0) - { - throw new ArgumentException("Count must be greater than 0"); - } - - Count = parCount; - - GL.CreateBuffers(1, out int handle); - Handle = handle; - - GL.NamedBufferStorage(Handle, Count * _stride, IntPtr.Zero, parFlags); - - Log.Debug("Vertex buffer {Handle} created with {Count} elements of type {Type}", Handle, Count, typeof(T).Name); - } - - public void UploadData(T[] parData) - { - UploadData(0, parData); - } - - public void UploadData(int parOffset, T[] parData) - { - if (parOffset < 0) - { - throw new ArgumentException("Offset must be greater than 0"); - } - - if (parData.Length + parOffset > Count) - { - throw new ArgumentException("Data array is too large"); - } - - GL.NamedBufferSubData(Handle, parOffset * _stride, parData.Length * _stride, parData); - } - - public override void Bind() - { - GL.BindBuffer(BufferTarget.ArrayBuffer, Handle); - } - - public override void Unbind() - { - GL.BindBuffer(BufferTarget.ArrayBuffer, 0); - } - - protected override void Destroy() - { - GL.DeleteBuffer(Handle); - } -} \ No newline at end of file diff --git a/Engine/src/Resource/ShaderResource.Designer.cs b/Engine/src/Resource/ShaderResource.Designer.cs new file mode 100644 index 0000000..a13d62f --- /dev/null +++ b/Engine/src/Resource/ShaderResource.Designer.cs @@ -0,0 +1,101 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Engine { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ShaderResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ShaderResource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Engine.src.Resource.ShaderResource", typeof(ShaderResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to #shader vertex + ///#version 330 core + /// + ///uniform mat4 uViewMatrix; + ///uniform mat4 uProjectionMatrix; + /// + ///layout(location = 0) in vec3 aPos; + ///layout(location = 1) in vec3 aNormal; + ///layout(location = 2) in vec2 aTexCoords; + ///layout(location = 3) in mat4 aModelMatrix; + /// + ///out vec2 outTexCoords; + /// + ///void main() + ///{ + /// gl_Position = uProjectionMatrix * uViewMatrix * aModelMatrix * vec4(aPos, 1.0); + /// outTexCoords = aTexCoords; + ///} + /// + ///#shader fragment + ///#version 330 core + /// + ///uniform sampler2D uTexture; + /// + ///layout(location [rest of string was truncated]";. + /// + internal static string Mesh { + get { + return ResourceManager.GetString("Mesh", resourceCulture); + } + } + + internal static string Quad { + get { + return ResourceManager.GetString("Quad", resourceCulture); + } + } + } +} diff --git a/Engine/src/Resource/ShaderResource.resx b/Engine/src/Resource/ShaderResource.resx new file mode 100644 index 0000000..a854c31 --- /dev/null +++ b/Engine/src/Resource/ShaderResource.resx @@ -0,0 +1,29 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ../../assets/shader/mesh.shader;System.String, mscorlib, Version=4.0.0.0, Culture=neutral;utf-8 + + + + ../../assets/shader/quad.shader;System.String, mscorlib, Version=4.0.0.0, Culture=neutral;utf-8 + + \ No newline at end of file diff --git a/Engine/src/Scene/Component/BuiltIn/Camera.cs b/Engine/src/Scene/Component/BuiltIn/Camera.cs index 1d9fc9f..94072d2 100644 --- a/Engine/src/Scene/Component/BuiltIn/Camera.cs +++ b/Engine/src/Scene/Component/BuiltIn/Camera.cs @@ -1,4 +1,4 @@ -using Engine.Renderer.Camera; +using Engine.Graphics.Camera; using OpenTK.Mathematics; namespace Engine.Scene.Component.BuiltIn; diff --git a/Engine/src/Scene/Scene.cs b/Engine/src/Scene/Scene.cs index d5c2119..1eef787 100644 --- a/Engine/src/Scene/Scene.cs +++ b/Engine/src/Scene/Scene.cs @@ -1,4 +1,4 @@ -using Engine.Renderer.Camera; +using Engine.Graphics.Camera; using Engine.Scene.Component.BuiltIn; namespace Engine.Scene; diff --git a/Engine/src/Window.cs b/Engine/src/Window.cs index 947fe97..93e586e 100644 --- a/Engine/src/Window.cs +++ b/Engine/src/Window.cs @@ -1,13 +1,21 @@ -using OpenTK.Windowing.Desktop; +using Engine.Asset; +using Engine.Graphics; +using Engine.Graphics.Pixel; +using Engine.Graphics.Texture; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; +using OpenTK.Windowing.Common; +using OpenTK.Windowing.Desktop; using OpenTK.Windowing.GraphicsLibraryFramework; namespace Engine; -public class Window +public class Window : IPresenter { public bool IsExiting => _window.IsExiting; public int Width { get; private set; } public int Height { get; private set; } + public event Action? Resize; private readonly Engine _engine; private readonly NativeWindow _window; @@ -26,16 +34,32 @@ public class Window { Width = parArgs.Width; Height = parArgs.Height; + Resize?.Invoke(parArgs); }; } - public void Update() + public void Update(double parDeltaTime) { - if (!_headless) + if (_headless) { - NativeWindow.ProcessWindowEvents(false); - _window.SwapBuffers(); + return; } + + NativeWindow.ProcessWindowEvents(false); + _window.SwapBuffers(); + } + + public void Present(IConstTexture parTexture) + { + GL.Viewport(0, 0, Width, Height); + + GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f); + GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + + var texture = (Texture)parTexture; + _engine.Renderer.QuadRenderer.Commit(Matrix4.CreateScale(2f), Vector4.One, texture); + _engine.Renderer.QuadRenderer.Render(Matrix4.Identity, Matrix4.Identity); + _engine.Renderer.QuadRenderer.Reset(); } } diff --git a/PresenterConsole/PresenterConsole.csproj b/PresenterConsole/PresenterConsole.csproj index 20ec187..426a52e 100644 --- a/PresenterConsole/PresenterConsole.csproj +++ b/PresenterConsole/PresenterConsole.csproj @@ -5,20 +5,30 @@ net8.0 enable enable - DoomDeathmatchConsole - + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + True + True + ShaderResource.resx + + + True + True + ShaderResource.resx + + + + + + ResXFileCodeGenerator + ShaderResource.Designer.cs + - - diff --git a/PresenterConsole/assets/shader/ascii.shader b/PresenterConsole/assets/shader/ascii.shader new file mode 100644 index 0000000..d0ba834 --- /dev/null +++ b/PresenterConsole/assets/shader/ascii.shader @@ -0,0 +1,84 @@ +// #type vertex +#version 460 core + +layout (location = 0) in vec2 aPos; +layout (location = 1) in vec2 aUV; + +layout (location = 0) out vec2 oUV; + +void main() +{ + oUV = aUV; + gl_Position = vec4(aPos, 0.0, 1.0); +} + +// #type fragment +#version 460 core + +precision highp float; + +// All components are in the range [0…1], including hue. +vec3 rgb2hsv(vec3 c) +{ + vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r)); + + float d = q.x - min(q.w, q.y); + float e = 1.0e-10; + return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x); +} + +// 16 Windows color palette (approximated) +const vec3 windowsPalette[7] = vec3[]( +vec3(rgb2hsv(vec3(0.0, 0.0, 1.0)).rg, 0.0), // Blue (FOREGROUND_BLUE) +vec3(rgb2hsv(vec3(0.0, 1.0, 0.0)).rg, 0.0), // Green (FOREGROUND_GREEN) +vec3(rgb2hsv(vec3(0.0, 1.0, 1.0)).rg, 0.0), // Cyan (FOREGROUND_GREEN | FOREGROUND_BLUE) +vec3(rgb2hsv(vec3(1.0, 0.0, 0.0)).rg, 0.0), // Red (FOREGROUND_RED) +vec3(rgb2hsv(vec3(1.0, 0.0, 1.0)).rg, 0.0), // Magenta (FOREGROUND_RED | FOREGROUND_BLUE) +vec3(rgb2hsv(vec3(1.0, 1.0, 0.0)).rg, 0.0), // Yellow (FOREGROUND_RED | FOREGROUND_GREEN) +vec3(rgb2hsv(vec3(1.0, 1.0, 1.0)).rg, 0.0) // White (all colors set) +); + +// Find the closest color in the Windows palette +int findClosestColor(vec3 color) { + int closestIndex = 0; + float minDistance = distance(color, windowsPalette[0]); + + for (int i = 1; i < 7; i++) { + float dist = distance(color, windowsPalette[i]); + if (dist < minDistance) { + minDistance = dist; + closestIndex = i; + } + } + + return closestIndex + 1; +} + +float calculateLuminosity(vec3 color) { + // Standard luminosity calculation + float luminosity = dot(color, vec3(0.299, 0.587, 0.114)); + return clamp(luminosity, 0.0, 1.0); +} + +uniform sampler2D uInputTexture; + +layout (location = 0) in vec2 iUV; +layout (location = 0) out vec4 FragColor; + +void main() { + vec3 pixelColor = texture(uInputTexture, iUV).rgb; + vec3 pixelColorHsv = rgb2hsv(pixelColor); + + // Find closest color index (4 bits) + int colorIndex = findClosestColor(vec3(pixelColorHsv.rg, 0)); + + // Calculate luminosity (4 bits) + float luminosity = calculateLuminosity(pixelColor); + + // Combine into a single byte-like value + // High 4 bits: color index + // Low 4 bits: luminosity + FragColor = vec4(luminosity, colorIndex / 255.0, 0, 1); +} \ No newline at end of file diff --git a/PresenterConsole/src/AsciiPixel.cs b/PresenterConsole/src/AsciiPixel.cs new file mode 100644 index 0000000..09a058a --- /dev/null +++ b/PresenterConsole/src/AsciiPixel.cs @@ -0,0 +1,21 @@ +using Engine.Graphics.Pixel; +using OpenTK.Graphics.OpenGL; + +namespace PresenterConsole; + +public struct AsciiPixel : IPixel +{ + public PixelFormat Format => PixelFormat.Rg; + public PixelType Type => PixelType.UnsignedByte; + + public PixelInternalFormat InternalFormat => PixelInternalFormat.Rg8; + public SizedInternalFormat SizedInternalFormat => SizedInternalFormat.Rg8; + + public byte LightnessIndex => R; + public byte ColorIndex => G; + + public byte R; + public byte G; + + public override string ToString() => $"{R}, {G}"; +} \ No newline at end of file diff --git a/PresenterConsole/src/AsciiVertex.cs b/PresenterConsole/src/AsciiVertex.cs new file mode 100644 index 0000000..e7a106b --- /dev/null +++ b/PresenterConsole/src/AsciiVertex.cs @@ -0,0 +1,11 @@ +using Engine.Graphics.Buffer.Vertex; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; + +namespace PresenterConsole; + +public struct AsciiVertex : IVertex +{ + [Vertex(VertexAttribType.Float, 2)] public Vector2 _position; + [Vertex(VertexAttribType.Float, 2)] public Vector2 _uv; +} \ No newline at end of file diff --git a/PresenterConsole/src/ConsoleFastOutput.cs b/PresenterConsole/src/ConsoleFastOutput.cs new file mode 100644 index 0000000..ca340d0 --- /dev/null +++ b/PresenterConsole/src/ConsoleFastOutput.cs @@ -0,0 +1,158 @@ +using System.Runtime.InteropServices; +using Microsoft.Win32.SafeHandles; + +namespace PresenterConsole; + +public class ConsoleFastOutput +{ + [DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)] + static extern SafeFileHandle CreateFile( + string fileName, + [MarshalAs(UnmanagedType.U4)] uint fileAccess, + [MarshalAs(UnmanagedType.U4)] uint fileShare, + IntPtr securityAttributes, + [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition, + [MarshalAs(UnmanagedType.U4)] int flags, + IntPtr template); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool WriteConsoleOutput( + SafeFileHandle hConsoleOutput, + CharInfo[] lpBuffer, + Coord dwBufferSize, + Coord dwBufferCoord, + ref SmallRect lpWriteRegion); + + [StructLayout(LayoutKind.Sequential)] + public struct Coord + { + public short X; + public short Y; + + public Coord(short X, short Y) + { + this.X = X; + this.Y = Y; + } + }; + + [StructLayout(LayoutKind.Explicit)] + public struct CharUnion + { + [FieldOffset(0)] public char UnicodeChar; + [FieldOffset(0)] public byte AsciiChar; + } + + [StructLayout(LayoutKind.Explicit)] + public struct CharInfo + { + [FieldOffset(0)] public CharUnion Char; + [FieldOffset(2)] public short Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SmallRect + { + public short Left; + public short Top; + public short Right; + public short Bottom; + } + + private readonly SafeFileHandle _handle; + private CharInfo[] _buffer; + private SmallRect _region; + private int _width; + private int _height; + + public ConsoleFastOutput(int parWidth, int parHeight) + { + _width = parWidth; + _height = parHeight; + _handle = CreateFile("CONOUT$", 0x40000000, 2, IntPtr.Zero, FileMode.Open, 0, IntPtr.Zero); + if (_handle.IsInvalid) + { + throw new Exception("Failed to open console"); + } + + _buffer = new CharInfo[parWidth * parHeight]; + _region = new SmallRect { Left = 0, Top = 0, Right = (short)_width, Bottom = (short)_height }; + } + + public void WriteChar(char parChar, short parX, short parY, ushort parForeground, ushort parBackground) + { + var index = parX + parY * _width; + if (index >= _buffer.Length) + return; + + _buffer[parX + parY * _width].Char.UnicodeChar = parChar; + _buffer[parX + parY * _width].Attributes = (short)(parForeground | (parBackground << 4)); + } + + public void Flush() + { + var regions = SplitRegion(_region, 1, 1); + for (var i = 0; i < regions.Length; i++) + { + var region = regions[i]; + var bufferSize = new Coord(region.Right, region.Bottom); + var bufferCoord = new Coord(region.Left, region.Top); + WriteConsoleOutput(_handle, _buffer, bufferSize, bufferCoord, ref region); + } + + // var bufferSize = new Coord(_region.Right, _region.Bottom); + // var bufferCoord = new Coord(0, 0); + // WriteConsoleOutput(_handle, _buffer, bufferSize, bufferCoord, ref _region); + // + // _region.Left = 0; + // _region.Top = 0; + // _region.Right = (short)_width; + // _region.Bottom = (short)_height; + + // run in parallel + // Parallel.ForEach(regions, region => + // { + // var bufferSize = new Coord(_region.Right, _region.Bottom); + // var bufferCoord = new Coord(region.Left, region.Top); + // WriteConsoleOutput(_handle, _buffer, bufferSize, bufferCoord, ref region); + // }); + } + + private SmallRect[] SplitRegion(SmallRect parRegion, int countX, int countY) + { + var regions = new SmallRect[countX * countY]; + for (var y = 0; y < countY; y++) + { + for (var x = 0; x < countX; x++) + { + var region = parRegion; + region.Left = (short)(parRegion.Left + x * parRegion.Right / countX); + region.Right = (short)(parRegion.Left + (x + 1) * parRegion.Right / countX); + region.Top = (short)(parRegion.Top + y * parRegion.Bottom / countY); + region.Bottom = (short)(parRegion.Top + (y + 1) * parRegion.Bottom / countY); + regions[y * countX + x] = region; + } + } + + return regions; + } + + public void Resize(int parWidth, int parHeight) + { + if (parWidth <= 0 || parHeight <= 0) + { + throw new ArgumentException("Width and height must be greater than 0"); + } + + if (parWidth == _width && parHeight == _height) + { + return; + } + + _width = parWidth; + _height = parHeight; + + _buffer = new CharInfo[parWidth * parHeight]; + _region = new SmallRect { Left = 0, Top = 0, Right = (short)parWidth, Bottom = (short)parHeight }; + } +} \ No newline at end of file diff --git a/PresenterConsole/src/ConsoleInputHandler.cs b/PresenterConsole/src/ConsoleInputHandler.cs index 111fd3e..ed57f26 100644 --- a/PresenterConsole/src/ConsoleInputHandler.cs +++ b/PresenterConsole/src/ConsoleInputHandler.cs @@ -4,126 +4,126 @@ namespace PresenterConsole; public class ConsoleInputHandler : IInputHandler { - private readonly HashSet _currentKeys = []; - private readonly HashSet _previousKeys = []; - private ConsoleModifiers _currentModifiers; - private ConsoleModifiers _previousModifiers; + private readonly HashSet _currentKeys = []; + private readonly HashSet _previousKeys = []; + private ConsoleModifiers _currentModifiers; + private ConsoleModifiers _previousModifiers; - public void Update(double deltaTime) + public void Update(double parDeltaTime) + { + // Save previous state + _previousKeys.Clear(); + foreach (var key in _currentKeys) { - // Save previous state - _previousKeys.Clear(); - foreach (var key in _currentKeys) - { - _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.Add(key); } - public bool IsKeyPressed(KeyCode keyCode) - { - if (IsModifierKey(keyCode)) - return IsModifierActive(keyCode); + _previousModifiers = _currentModifiers; - return _currentKeys.Contains(ConvertToConsoleKey(keyCode)); + // 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; } + } - public bool IsKeyJustPressed(KeyCode keyCode) - { - if (IsModifierKey(keyCode)) - return IsModifierActive(keyCode) && !WasModifierActive(keyCode); + public bool IsKeyPressed(KeyCode keyCode) + { + if (IsModifierKey(keyCode)) + return IsModifierActive(keyCode); - var consoleKey = ConvertToConsoleKey(keyCode); - return _currentKeys.Contains(consoleKey) && !_previousKeys.Contains(consoleKey); - } + return _currentKeys.Contains(ConvertToConsoleKey(keyCode)); + } - public bool IsKeyRepeat(KeyCode keyCode) - { - if (IsModifierKey(keyCode)) - return IsModifierActive(keyCode) && WasModifierActive(keyCode); + public bool IsKeyJustPressed(KeyCode keyCode) + { + if (IsModifierKey(keyCode)) + return IsModifierActive(keyCode) && !WasModifierActive(keyCode); - var consoleKey = ConvertToConsoleKey(keyCode); - return _currentKeys.Contains(consoleKey) && _previousKeys.Contains(consoleKey); - } + var consoleKey = ConvertToConsoleKey(keyCode); + return _currentKeys.Contains(consoleKey) && !_previousKeys.Contains(consoleKey); + } - public bool IsMouseButtonPressed(MouseButton button) - { - return false; - } + public bool IsKeyRepeat(KeyCode keyCode) + { + if (IsModifierKey(keyCode)) + return IsModifierActive(keyCode) && WasModifierActive(keyCode); - public bool IsMouseButtonJustPressed(MouseButton button) - { - return false; - } + var consoleKey = ConvertToConsoleKey(keyCode); + return _currentKeys.Contains(consoleKey) && _previousKeys.Contains(consoleKey); + } - private static bool IsModifierKey(KeyCode keyCode) => keyCode is KeyCode.Ctrl or KeyCode.Backspace or KeyCode.Shift; + public bool IsMouseButtonPressed(MouseButton button) + { + return false; + } - 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 - }; + public bool IsMouseButtonJustPressed(MouseButton button) + { + return false; + } - private bool WasModifierActive(KeyCode keyCode) => keyCode switch - { - KeyCode.Ctrl => (_previousModifiers & ConsoleModifiers.Control) != 0, - KeyCode.Alt => (_previousModifiers & ConsoleModifiers.Alt) != 0, - KeyCode.Shift => (_previousModifiers & ConsoleModifiers.Shift) != 0, - _ => false - }; + private static bool IsModifierKey(KeyCode keyCode) => keyCode is KeyCode.Ctrl or KeyCode.Backspace or KeyCode.Shift; - 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}") - }; + 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 + }; + + private bool WasModifierActive(KeyCode keyCode) => keyCode 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}") + }; } \ No newline at end of file diff --git a/PresenterConsole/src/ConsolePresenter.cs b/PresenterConsole/src/ConsolePresenter.cs index 6a4a194..2ff51a7 100644 --- a/PresenterConsole/src/ConsolePresenter.cs +++ b/PresenterConsole/src/ConsolePresenter.cs @@ -1,51 +1,117 @@ using Engine.Asset; -using Engine.Renderer; -using Engine.Renderer.Framebuffer; -using Engine.Renderer.Texture; +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; +using OpenTK.Mathematics; using OpenTK.Windowing.Common; namespace PresenterConsole; public class ConsolePresenter : IPresenter { - public bool IsExiting => false; - public int Width => Console.WindowWidth; - public int Height => Console.WindowHeight; - public event Action? Resize; + public bool IsExiting => false; + public int Width => Console.WindowWidth; + public int Height => Console.WindowHeight; + public event Action? Resize; - private readonly Framebuffer _framebuffer; - private Image _asciiImage; + private int _cachedWidth; + private int _cachedHeight; - public ConsolePresenter(int width, int height) + private readonly Engine.Engine _engine; + private readonly Framebuffer _framebuffer; + private readonly Engine.Graphics.Shader.Program _asciiProgram; + private Image? _asciiImage; + + private readonly IndexBuffer _indexBuffer; + private readonly VertexBuffer _vertexBuffer; + private readonly VertexArray _vertexArray; + + private readonly ConsoleFastOutput _consoleOutput; + + public ConsolePresenter(Engine.Engine parEngine) + { + _engine = parEngine; + _asciiProgram = ProgramLoader.LoadFromSource(Resource.ShaderResource.Ascii); + + _framebuffer = Framebuffer.Builder(Width, Height) + .AddColorAttachment() + .Build(); + + _indexBuffer = new IndexBuffer([0, 1, 2, 2, 3, 0], BufferStorageFlags.MapReadBit); + _vertexBuffer = new VertexBuffer([ + new AsciiVertex { _position = new Vector2(-1.0f, -1.0f), _uv = new Vector2(0.0f, 0.0f) }, + new AsciiVertex { _position = new Vector2(1.0f, -1.0f), _uv = new Vector2(1.0f, 0.0f) }, + new AsciiVertex { _position = new Vector2(1.0f, 1.0f), _uv = new Vector2(1.0f, 1.0f) }, + new AsciiVertex { _position = new Vector2(-1.0f, 1.0f), _uv = new Vector2(0.0f, 1.0f) } + ], BufferStorageFlags.MapReadBit); + _vertexArray = new VertexArray(); + _vertexArray.BindIndexBuffer(_indexBuffer); + _vertexArray.BindVertexBuffer(_vertexBuffer); + + _consoleOutput = new ConsoleFastOutput(Width, Height); + } + + public void Present(IConstTexture parTexture) + { + var openglTexture = (Texture)parTexture; + + _framebuffer.Bind(); + + GL.ClearColor(0.0f, 0.0f, 0.0f, 1.0f); + GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + + openglTexture.BindUnit(); + + _asciiProgram.Bind(); + _asciiProgram.SetUniform("uInputTexture", 0); + + _vertexArray.Bind(); + GL.DrawElements(PrimitiveType.Triangles, _indexBuffer.Count, DrawElementsType.UnsignedInt, 0); + + _framebuffer.Unbind(); + + var asciiTexture = _framebuffer.TextureInternal; + if (asciiTexture == null) + throw new InvalidOperationException("Framebuffer texture is null"); + + if (_asciiImage == null || asciiTexture.Width != _asciiImage.Width || asciiTexture.Height != _asciiImage.Height) + _asciiImage = new Image(asciiTexture.Width, asciiTexture.Height); + + asciiTexture.ReadPixels(_asciiImage); + Output(_asciiImage); + } + + private void Output(Image parImage) + { + var lightmap = " .-:=+*#%@".Reverse().ToArray(); + for (var y = 0; y < parImage.Height; y++) { - _framebuffer = Framebuffer.Builder(width, height) - .AddColorAttachment() - .Build(); + for (var x = 0; x < parImage.Width; x++) + { + var pixel = parImage[y, x]; + var lightnessIndex = (int)(pixel.LightnessIndex / 255.0f * (lightmap.Length - 1)); + _consoleOutput.WriteChar(lightmap[lightnessIndex], (short)x, (short)y, 0, + pixel.ColorIndex); + } } - public void Present(IConstTexture texture) + _consoleOutput.Flush(); + } + + public void Update(double parDeltaTime) + { + if (_cachedWidth != Width || _cachedHeight != Height) { - var openglTexture = (Texture)texture; + _cachedWidth = Width; + _cachedHeight = Height; - _framebuffer.Bind(); - openglTexture.BindUnit(); - - // TODO: render with ascii shader - - _framebuffer.Unbind(); - - var asciiTexture = _framebuffer.TextureInternal; - if (asciiTexture == null) - throw new InvalidOperationException("Framebuffer texture is null"); - - if (asciiTexture.Width != _asciiImage.Width || asciiTexture.Height != _asciiImage.Height) - _asciiImage = new Image(asciiTexture.Width, asciiTexture.Height); - - asciiTexture.ReadPixels(_asciiImage); - } - - public void Update(double deltaTime) - { - throw new NotImplementedException(); + _framebuffer.Resize(Width, Height); + _engine.Renderer.Resize(Width, Height); + _consoleOutput.Resize(Width, Height); } + } } \ No newline at end of file diff --git a/PresenterConsole/src/Program.cs b/PresenterConsole/src/Program.cs new file mode 100644 index 0000000..8d99222 --- /dev/null +++ b/PresenterConsole/src/Program.cs @@ -0,0 +1,12 @@ +// See https://aka.ms/new-console-template for more information + +using PresenterConsole; + +internal class Program +{ + public static void Main(string[] args) + { + var engine = new Engine.Engine(240, 135, (parEngine) => new ConsolePresenter(parEngine)); + engine.Run(); + } +} \ No newline at end of file diff --git a/PresenterConsole/src/Resource/ShaderResource.Designer.cs b/PresenterConsole/src/Resource/ShaderResource.Designer.cs new file mode 100644 index 0000000..ca6e474 --- /dev/null +++ b/PresenterConsole/src/Resource/ShaderResource.Designer.cs @@ -0,0 +1,95 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PresenterConsole.Resource { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class ShaderResource { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ShaderResource() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PresenterConsole.src.Resource.ShaderResource", typeof(ShaderResource).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to // #type vertex + ///#version 460 core + /// + ///layout (location = 0) in vec2 aPos; + ///layout (location = 1) in vec2 aUV; + /// + ///layout (location = 0) out vec2 oUV; + /// + ///void main() + ///{ + /// oUV = aUV; + /// gl_Position = vec4(aPos, 0.0, 1.0); + ///} + /// + /// // #type fragment + ///#version 460 core + /// + ///precision highp float; + /// + /// // All components are in the range [0…1], including hue. + ///vec3 rgb2hsv(vec3 c) + ///{ + /// vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0); + /// vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g)); + /// vec4 [rest of string was truncated]";. + /// + internal static string Ascii { + get { + return ResourceManager.GetString("Ascii", resourceCulture); + } + } + } +} diff --git a/PresenterConsole/src/Resource/ShaderResource.resx b/PresenterConsole/src/Resource/ShaderResource.resx new file mode 100644 index 0000000..4d2ff4c --- /dev/null +++ b/PresenterConsole/src/Resource/ShaderResource.resx @@ -0,0 +1,30 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + + ../../assets/shader/ascii.shader;System.String, mscorlib, Version=4.0.0.0, Culture=neutral;utf-8 + + \ No newline at end of file diff --git a/PresenterWPF/PresenterWPF.csproj b/PresenterWPF/PresenterWPF.csproj deleted file mode 100644 index 4147e0f..0000000 --- a/PresenterWPF/PresenterWPF.csproj +++ /dev/null @@ -1,32 +0,0 @@ - - - - WinExe - net8.0-windows - enable - enable - true - DoomDeathmatchWPF - - - - - - - - - MSBuild:Compile - Wpf - Designer - - - - - - MSBuild:Compile - Wpf - Designer - - - - diff --git a/PresenterWpf/PresenterWpf.csproj b/PresenterWpf/PresenterWpf.csproj new file mode 100644 index 0000000..9588090 --- /dev/null +++ b/PresenterWpf/PresenterWpf.csproj @@ -0,0 +1,31 @@ + + + + WinExe + net8.0-windows + enable + enable + true + + + + + + + + + MSBuild:Compile + Wpf + Designer + + + + + + MSBuild:Compile + Wpf + Designer + + + + diff --git a/PresenterWpf/src/App.xaml b/PresenterWpf/src/App.xaml new file mode 100644 index 0000000..f841d25 --- /dev/null +++ b/PresenterWpf/src/App.xaml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/PresenterWpf/src/App.xaml.cs b/PresenterWpf/src/App.xaml.cs new file mode 100644 index 0000000..c8c74ac --- /dev/null +++ b/PresenterWpf/src/App.xaml.cs @@ -0,0 +1,83 @@ +using System.Configuration; +using System.Data; +using System.Windows; +using Engine.Graphics; +using Engine.Graphics.Texture; +using OpenTK.Windowing.Common; + +namespace PresenterWpf; + +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application +{ + // Hijack the default startup event to start the engine + protected override void OnStartup(StartupEventArgs e) + { + var presenter = new PresenterWrapper(); + var engine = new Engine.Engine(1, 1, _ => presenter); + + // Since engine claims current thread for rendering, we need to create a new thread to run WPF + var thread = new Thread(() => + { + var window = new MainWindow(engine); + presenter.Presenter = window; + window.Show(); + System.Windows.Threading.Dispatcher.Run(); + }); + + thread.SetApartmentState(ApartmentState.STA); + thread.IsBackground = true; + thread.Start(); + + engine.Run(); + + // Shutdown WPF + Shutdown(); + } + + private class PresenterWrapper : IPresenter + { + private IPresenter? _presenter; + + public IPresenter? Presenter + { + get => _presenter; + set + { + if (_presenter != null) + { + _presenter.Resize -= PresenterResize; + } + + if (value != null) + { + value.Resize += PresenterResize; + } + + _presenter = value; + } + } + + public bool IsExiting => Presenter?.IsExiting ?? false; + public int Width => Presenter?.Width ?? 0; + public int Height => Presenter?.Height ?? 0; + public event Action? Resize; + + public void Present(IConstTexture parTexture) + { + Presenter?.Present(parTexture); + } + + public void Update(double parDeltaTime) + { + Presenter?.Update(parDeltaTime); + } + + private void PresenterResize(ResizeEventArgs e) + { + Resize?.Invoke(e); + } + } +} \ No newline at end of file diff --git a/PresenterWpf/src/AssemblyInfo.cs b/PresenterWpf/src/AssemblyInfo.cs new file mode 100644 index 0000000..d49fd6c --- /dev/null +++ b/PresenterWpf/src/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] \ No newline at end of file diff --git a/PresenterWpf/src/MainWindow.xaml b/PresenterWpf/src/MainWindow.xaml new file mode 100644 index 0000000..91633ef --- /dev/null +++ b/PresenterWpf/src/MainWindow.xaml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/PresenterWpf/src/MainWindow.xaml.cs b/PresenterWpf/src/MainWindow.xaml.cs new file mode 100644 index 0000000..f8e8420 --- /dev/null +++ b/PresenterWpf/src/MainWindow.xaml.cs @@ -0,0 +1,101 @@ +using System.ComponentModel; +using System.IO; +using System.Text; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Documents; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using System.Windows.Navigation; +using System.Windows.Shapes; +using System.Windows.Threading; +using Engine.Asset; +using Engine.Graphics; +using Engine.Graphics.Pixel; +using Engine.Graphics.Texture; +using OpenTK.Windowing.Common; + +namespace PresenterWpf; + +/// +/// Interaction logic for MainWindow.xaml +/// +public partial class MainWindow : Window, IPresenter +{ + public bool IsExiting { get; set; } + public int Width => (int)_cachedWidth; + public int Height => (int)_cachedHeight; + public event Action? Resize; + + + private Engine.Engine _engine; + private Image? _image; + private WriteableBitmap? _bitmap; + + private int _cachedWidth; + private int _cachedHeight; + + public MainWindow(Engine.Engine parEngine) + { + InitializeComponent(); + + _engine = parEngine; + } + + public void Update(double parDeltaTime) + { + Dispatcher.Invoke(() => + { + if ((int)Image.Width != _cachedWidth || (int)Image.Height != _cachedHeight) + { + _cachedWidth = (int)Image.Width; + _cachedHeight = (int)Image.Height; + } + }); + + // Resizes are lazy so resizing only happens when the window's size actually changes + if (Width != 0 && Height != 0) + _engine.Renderer.Resize(Width, Height); + } + + public void Present(IConstTexture parTexture) + { + if (_image == null || parTexture.Width != _image.Width || parTexture.Height != _image.Height) + { + _image = new Image(parTexture.Width, parTexture.Height); + } + + parTexture.ReadPixels(_image); + Dispatcher.Invoke(() => + { + if (_bitmap == null || _bitmap.PixelWidth != _image.Width || _bitmap.PixelHeight != _image.Height) + { + _bitmap = new WriteableBitmap(_image.Width, _image.Height, 96, 96, PixelFormats.Rgb24, null); + } + + DrawImage(_image); + Image.Source = _bitmap; + }); + } + + private void DrawImage(Image parImage) + { + try + { + _bitmap!.Lock(); + _bitmap.WritePixels(new Int32Rect(0, 0, parImage.Width, parImage.Height), parImage.Pixels, parImage.Width * 3, 0, + 0); + } + finally + { + _bitmap!.Unlock(); + } + } + + private void MainWindow_OnClosing(object? parSender, CancelEventArgs parE) + { + IsExiting = true; + } +} \ No newline at end of file diff --git a/PresenterWpf/src/Program.cs b/PresenterWpf/src/Program.cs new file mode 100644 index 0000000..bd8481d --- /dev/null +++ b/PresenterWpf/src/Program.cs @@ -0,0 +1,5 @@ +namespace PresenterWpf; + +internal class Program +{ +} \ No newline at end of file