commit 3f1740f41f238586446da553813d4377d13b5869 Author: lionarius Date: Wed Dec 4 22:35:04 2024 +0300 . diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e755d9f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ +.idea/ +*.DotSettings.* \ No newline at end of file diff --git a/DoomDeathmatch.sln b/DoomDeathmatch.sln new file mode 100644 index 0000000..8d9962d --- /dev/null +++ b/DoomDeathmatch.sln @@ -0,0 +1,34 @@ + +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}") = "DoomDeathmatchConsole", "DoomDeathmatchConsole\DoomDeathmatchConsole.csproj", "{B9A652EE-4267-4D6B-B1A6-2447F870A06D}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DoomDeathmatchWPF", "DoomDeathmatchWPF\DoomDeathmatchWPF.csproj", "{B712A719-5EB3-4869-AA4A-3BFFA3B9C918}" +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 + {5EE134DE-2275-40C0-8B9D-4EFF22474F63}.Release|Any CPU.Build.0 = Release|Any CPU + {B9A652EE-4267-4D6B-B1A6-2447F870A06D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B9A652EE-4267-4D6B-B1A6-2447F870A06D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B9A652EE-4267-4D6B-B1A6-2447F870A06D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B9A652EE-4267-4D6B-B1A6-2447F870A06D}.Release|Any CPU.Build.0 = Release|Any CPU + {B712A719-5EB3-4869-AA4A-3BFFA3B9C918}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B712A719-5EB3-4869-AA4A-3BFFA3B9C918}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B712A719-5EB3-4869-AA4A-3BFFA3B9C918}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B712A719-5EB3-4869-AA4A-3BFFA3B9C918}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/DoomDeathmatch/Assets/test.shader b/DoomDeathmatch/Assets/test.shader new file mode 100644 index 0000000..464f8b5 --- /dev/null +++ b/DoomDeathmatch/Assets/test.shader @@ -0,0 +1,19 @@ +#shader vertex +#version 330 core + +layout(location = 0) in vec2 aPos; + +void main() +{ + gl_Position = vec4(aPos, 0.0, 1.0); +} + +#shader fragment +#version 330 core + +out vec4 FragColor; + +void main() +{ + FragColor = vec4(1.0, 1.0, 0.0, 1.0); +} \ No newline at end of file diff --git a/DoomDeathmatch/DoomDeathmatch.csproj b/DoomDeathmatch/DoomDeathmatch.csproj new file mode 100644 index 0000000..90e66bb --- /dev/null +++ b/DoomDeathmatch/DoomDeathmatch.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/DoomDeathmatch/Program.cs b/DoomDeathmatch/Program.cs new file mode 100644 index 0000000..2989378 --- /dev/null +++ b/DoomDeathmatch/Program.cs @@ -0,0 +1,20 @@ +using System.Diagnostics; +using System.Drawing; +using System.Runtime.InteropServices; + +namespace DoomDeathmatch; + +internal abstract class Program +{ + [DllImport("kernel32.dll")] + static extern IntPtr GetConsoleWindow(); + + [DllImport("User32.dll", SetLastError = true)] + public static extern IntPtr GetDC(IntPtr hwnd); + + public static void Main(string[] args) + { + var engine = new Engine.Engine(1280, 720, false, "Doom Deathmatch"); + engine.Run(); + } +} \ No newline at end of file diff --git a/DoomDeathmatch/QuadVertex.cs b/DoomDeathmatch/QuadVertex.cs new file mode 100644 index 0000000..5582151 --- /dev/null +++ b/DoomDeathmatch/QuadVertex.cs @@ -0,0 +1,21 @@ +using Engine.Renderer.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; +} \ No newline at end of file diff --git a/Engine/Asset/Image.cs b/Engine/Asset/Image.cs new file mode 100644 index 0000000..ced57eb --- /dev/null +++ b/Engine/Asset/Image.cs @@ -0,0 +1,32 @@ +using Engine.Renderer.Pixel; +using Engine.Renderer.Texture; + +namespace Engine.Asset; + +public class Image(T[,] pixels) + where T : struct, IPixel +{ + public int Width { get; } = pixels.GetLength(0); + public int Height { get; } = pixels.GetLength(1); + public T[,] Pixels { get; } = pixels; + + public T this[int x, int y] => Pixels[x, y]; + + public Image(int width, int height) : this(new T[width, height]) + { + } + + public DynamicTexture ToDynamicTexture() + { + var texture = new DynamicTexture(Width, Height); + texture.UploadPixels(this); + return texture; + } + + public StaticTexture ToStaticTexture() + { + var texture = new StaticTexture(Width, Height); + texture.UploadPixels(this); + return texture; + } +} \ No newline at end of file diff --git a/Engine/Asset/Mesh/Loader/IMeshLoader.cs b/Engine/Asset/Mesh/Loader/IMeshLoader.cs new file mode 100644 index 0000000..e6129c6 --- /dev/null +++ b/Engine/Asset/Mesh/Loader/IMeshLoader.cs @@ -0,0 +1,29 @@ +namespace Engine.Asset.Mesh.Loader; + +public interface IMeshLoader +{ + public Mesh LoadMesh(string path, MeshLoaderParameters parameters = MeshLoaderParameters.Default); + + public static Mesh Optimize(Mesh mesh) + { + var optimizedMesh = new Mesh(); + var vertexMap = new Dictionary(); + uint index = 0; + + foreach (var vertex in mesh.Vertices) + { + if (vertexMap.TryGetValue(vertex, out var existingIndex)) + optimizedMesh.IndicesInternal.Add(existingIndex); + else + { + vertexMap.Add(vertex, index); + optimizedMesh.VerticesInternal.Add(vertex); + optimizedMesh.IndicesInternal.Add(index); + + index++; + } + } + + return optimizedMesh; + } +} \ No newline at end of file diff --git a/Engine/Asset/Mesh/Loader/MeshLoaderParameters.cs b/Engine/Asset/Mesh/Loader/MeshLoaderParameters.cs new file mode 100644 index 0000000..a33df1b --- /dev/null +++ b/Engine/Asset/Mesh/Loader/MeshLoaderParameters.cs @@ -0,0 +1,12 @@ +namespace Engine.Asset.Mesh.Loader; + +[Flags] +public enum MeshLoaderParameters +{ + None = 0, + LoadNormals = 1 << 0, + LoadUVs = 1 << 1, + Optimize = 1 << 2, + + Default = LoadNormals | LoadUVs | Optimize +} \ No newline at end of file diff --git a/Engine/Asset/Mesh/Loader/ObjMeshLoader.cs b/Engine/Asset/Mesh/Loader/ObjMeshLoader.cs new file mode 100644 index 0000000..b493583 --- /dev/null +++ b/Engine/Asset/Mesh/Loader/ObjMeshLoader.cs @@ -0,0 +1,90 @@ +using System.Globalization; +using OpenTK.Mathematics; + +namespace Engine.Asset.Mesh.Loader; + +public class ObjMeshLoader : IMeshLoader +{ + private static readonly ObjMeshLoader Instance = new(); + + public static Mesh Load(string path, MeshLoaderParameters parameters = MeshLoaderParameters.Default) + => Instance.LoadMesh(path, parameters); + + public Mesh LoadMesh(string path, MeshLoaderParameters parameters = MeshLoaderParameters.Default) + { + var mesh = new Mesh(); + + var tempVertices = new List(); + var tempNormals = new List(); + var tempUVs = new List(); + var index = 0u; + + var loadNormals = parameters.HasFlag(MeshLoaderParameters.LoadNormals); + var loadUVs = parameters.HasFlag(MeshLoaderParameters.LoadUVs); + + using var reader = new StreamReader(path); + while (reader.ReadLine() is { } line) + { + var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + if (parts.Length == 0 || parts[0].StartsWith('#')) + continue; + + switch (parts[0]) + { + case "v": + tempVertices.Add(new Vector3( + float.Parse(parts[1], CultureInfo.InvariantCulture), + float.Parse(parts[2], CultureInfo.InvariantCulture), + float.Parse(parts[3], CultureInfo.InvariantCulture) + )); + break; + + case "vt" when loadUVs: + tempUVs.Add(new Vector2( + float.Parse(parts[1], CultureInfo.InvariantCulture), + float.Parse(parts[2], CultureInfo.InvariantCulture) + )); + break; + + case "vn" when loadNormals: + tempNormals.Add(new Vector3( + float.Parse(parts[1], CultureInfo.InvariantCulture), + float.Parse(parts[2], CultureInfo.InvariantCulture), + float.Parse(parts[3], CultureInfo.InvariantCulture) + )); + break; + + case "f": + for (var i = 1; i <= 3; i++) + { + var faceComponents = parts[i].Split('/'); + var meshVertex = new Mesh.Vertex + { + Position = tempVertices[int.Parse(faceComponents[0]) - 1] + }; + + if (loadUVs && faceComponents.Length > 1 && faceComponents[1] != "") + { + meshVertex.Uv = tempUVs[int.Parse(faceComponents[1]) - 1]; + } + + if (loadNormals && faceComponents.Length > 2 && faceComponents[2] != "") + { + meshVertex.Normal = tempNormals[int.Parse(faceComponents[2]) - 1]; + } + + mesh.VerticesInternal.Add(meshVertex); + mesh.IndicesInternal.Add(index++); + } + + break; + } + } + + if (parameters.HasFlag(MeshLoaderParameters.Optimize)) + mesh = IMeshLoader.Optimize(mesh); + + return mesh; + } +} \ No newline at end of file diff --git a/Engine/Asset/Mesh/Loader/StlMeshLoader.cs b/Engine/Asset/Mesh/Loader/StlMeshLoader.cs new file mode 100644 index 0000000..baa1133 --- /dev/null +++ b/Engine/Asset/Mesh/Loader/StlMeshLoader.cs @@ -0,0 +1,66 @@ +using System.Globalization; +using OpenTK.Mathematics; + +namespace Engine.Asset.Mesh.Loader; + +public class StlMeshLoader : IMeshLoader +{ + private static readonly StlMeshLoader Instance = new(); + + public static Mesh Load(string path, MeshLoaderParameters parameters = MeshLoaderParameters.Default) + => Instance.LoadMesh(path, parameters); + + public Mesh LoadMesh(string path, MeshLoaderParameters parameters = MeshLoaderParameters.Default) + { + var mesh = new Mesh(); + + var currentNormal = new Vector3(); + var index = 0u; + + using var reader = new StreamReader(path); + while (reader.ReadLine() is { } line) + { + line = line.Trim(); + + if (string.IsNullOrWhiteSpace(line) || line.StartsWith("solid") || line.StartsWith("outer loop") || + line.StartsWith("endloop")) + continue; + if (line.StartsWith("endsolid")) + break; + + var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + switch (parts[0]) + { + case "facet" when parts[1] == "normal" && parameters.HasFlag(MeshLoaderParameters.LoadNormals): + currentNormal = new Vector3( + float.Parse(parts[2], CultureInfo.InvariantCulture), + float.Parse(parts[3], CultureInfo.InvariantCulture), + float.Parse(parts[4], CultureInfo.InvariantCulture) + ); + break; + case "vertex": + { + var vertex = new Vector3( + float.Parse(parts[1], CultureInfo.InvariantCulture), + float.Parse(parts[2], CultureInfo.InvariantCulture), + float.Parse(parts[3], CultureInfo.InvariantCulture) + ); + + mesh.VerticesInternal.Add(new Mesh.Vertex + { + Position = vertex, + Normal = currentNormal + }); + mesh.IndicesInternal.Add(index++); + break; + } + } + } + + if (parameters.HasFlag(MeshLoaderParameters.Optimize)) + mesh = IMeshLoader.Optimize(mesh); + + return mesh; + } +} \ No newline at end of file diff --git a/Engine/Asset/Mesh/Mesh.cs b/Engine/Asset/Mesh/Mesh.cs new file mode 100644 index 0000000..8cee22f --- /dev/null +++ b/Engine/Asset/Mesh/Mesh.cs @@ -0,0 +1,22 @@ +using OpenTK.Mathematics; + +namespace Engine.Asset.Mesh; + +public class Mesh +{ + public IReadOnlyList Vertices => _vertices; + public IReadOnlyList Indices => _indices; + + internal IList VerticesInternal => _vertices; + internal IList IndicesInternal => _indices; + + private readonly List _vertices = []; + private readonly List _indices = []; + + public record struct Vertex + { + public Vector3 Position { get; internal set; } + public Vector3 Normal { get; internal set; } + public Vector2 Uv { get; internal set; } + } +} \ No newline at end of file diff --git a/Engine/Engine.cs b/Engine/Engine.cs new file mode 100644 index 0000000..9787f8e --- /dev/null +++ b/Engine/Engine.cs @@ -0,0 +1,65 @@ +using System.Runtime.InteropServices; +using System.Text; +using Engine.Renderer.Buffer; +using Engine.Renderer.Buffer.Vertex; +using Engine.Renderer.Shader; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; +using OpenTK.Windowing.Common; +using OpenTK.Windowing.Desktop; +using Serilog; +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; + +namespace Engine; + +public sealed class Engine +{ + public Renderer.Renderer Renderer => _renderer; + + private readonly Window _window; + private readonly Renderer.Renderer _renderer; + private readonly ILogger _logger; + + public Engine(int width, int height, bool headless = false, string title = "") + { + var settings = new NativeWindowSettings + { + ClientSize = headless ? new Vector2i(1, 1) : new Vector2i(width, height), + Title = title, + StartVisible = !headless, + APIVersion = new Version(4, 5), + Profile = ContextProfile.Compatability + }; + + _window = new Window(this, new NativeWindow(settings), headless); + _renderer = new Renderer.Renderer(width, height); + + Thread.CurrentThread.Name = "RendererThread"; + + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithThreadName() + .Enrich.WithThreadId() + .Enrich.FromLogContext() + .WriteTo.Console( + outputTemplate: + "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff}] [{Level:u3}] [{ThreadName,-15:l}:{ThreadId,-4:d4}] [{SourceContext:l}] {Message:lj}{NewLine}{Exception}", + theme: AnsiConsoleTheme.Literate) + .CreateLogger(); + + _logger = Log.ForContext(); + } + + public void Run() + { + GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f); + + GL.Viewport(0, 0, _window.Width, _window.Height); + + while (!_window.IsExiting) + { + + } + } +} \ No newline at end of file diff --git a/Engine/Engine.csproj b/Engine/Engine.csproj new file mode 100644 index 0000000..31d943d --- /dev/null +++ b/Engine/Engine.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + true + + + + + + + + + + diff --git a/Engine/Input/IInputHandler.cs b/Engine/Input/IInputHandler.cs new file mode 100644 index 0000000..4cdc45c --- /dev/null +++ b/Engine/Input/IInputHandler.cs @@ -0,0 +1,11 @@ +namespace Engine.Input; + +public interface IInputHandler +{ + bool IsKeyPressed(Key key); + bool IsKeyJustPressed(Key key); + bool IsKeyRepeat(Key key); + + bool IsMouseButtonPressed(MouseButton button); + bool IsMouseButtonJustPressed(MouseButton button); +} \ No newline at end of file diff --git a/Engine/Input/Key.cs b/Engine/Input/Key.cs new file mode 100644 index 0000000..cf838ae --- /dev/null +++ b/Engine/Input/Key.cs @@ -0,0 +1,52 @@ +namespace Engine.Input; + +public enum Key +{ + A, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + + Ctrl, + Alt, + Shift, + + Up, + Down, + Left, + Right, + + Escape, + Enter, + Space, + Tab, + Backspace, + Delete, + Insert, + Home, + End, + PageUp, + PageDown, +} \ No newline at end of file diff --git a/Engine/Input/MouseButton.cs b/Engine/Input/MouseButton.cs new file mode 100644 index 0000000..79c4755 --- /dev/null +++ b/Engine/Input/MouseButton.cs @@ -0,0 +1,13 @@ +namespace Engine.Input; + +public enum MouseButton +{ + Left, + Right, + Middle, + + Button4, + Button5, + Button6, + Button7, +} \ No newline at end of file diff --git a/Engine/Renderer/Buffer/IndexBuffer.cs b/Engine/Renderer/Buffer/IndexBuffer.cs new file mode 100644 index 0000000..497d731 --- /dev/null +++ b/Engine/Renderer/Buffer/IndexBuffer.cs @@ -0,0 +1,52 @@ +using OpenTK.Graphics.OpenGL; +using Serilog; + +namespace Engine.Renderer.Buffer; + +public class IndexBuffer : OpenGlObject +{ + internal int Count { get; } + + public IndexBuffer(int count, BufferStorageFlags flags) + { + Count = count; + + GL.CreateBuffers(1, out int handle); + Handle = handle; + + GL.NamedBufferStorage(Handle, Count * sizeof(uint), IntPtr.Zero, flags); + + Log.Debug("Index buffer {Handle} created with {Count} elements", Handle, Count); + } + + public void UploadData(uint[] data) + { + UploadData(0, data); + } + + public void UploadData(int offset, uint[] data) + { + if (offset < 0) + throw new ArgumentException("Offset must be greater than 0"); + + if (data.Length + offset > Count) + throw new ArgumentException("Data array is too large"); + + GL.NamedBufferSubData(Handle, offset, data.Length * sizeof(uint), data); + } + + internal override void Bind() + { + GL.BindBuffer(BufferTarget.ElementArrayBuffer, Handle); + } + + internal override void Unbind() + { + GL.BindBuffer(BufferTarget.ElementArrayBuffer, 0); + } + + protected override void Destroy() + { + GL.DeleteBuffer(Handle); + } +} \ No newline at end of file diff --git a/Engine/Renderer/Buffer/Vertex/IVertex.cs b/Engine/Renderer/Buffer/Vertex/IVertex.cs new file mode 100644 index 0000000..44c4317 --- /dev/null +++ b/Engine/Renderer/Buffer/Vertex/IVertex.cs @@ -0,0 +1,50 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using OpenTK.Graphics.OpenGL; + +namespace Engine.Renderer.Buffer.Vertex; + +public interface IVertex +{ + public static IOrderedEnumerable GetFields() => GetFields(typeof(T)); + + public static IOrderedEnumerable GetFields(Type type) => + type.GetFields(BindingFlags.Public | BindingFlags.Instance).OrderBy(f => f.MetadataToken); + + public static bool IsValid(Type type) + { + if (!type.IsValueType || !type.IsAssignableTo(typeof(IVertex))) + return false; + + var fields = GetFields(type); + var totalSize = 0; + foreach (var field in fields) + { + if (!field.FieldType.IsValueType) + return false; + + var attribute = field.GetCustomAttribute(); + if (attribute == null) + return false; + + var size = AttributeSize(attribute.Type) * attribute.ComponentCount * attribute.RepeatCount; + if (size != Marshal.SizeOf(field.FieldType)) + return false; + + totalSize += size; + } + + return totalSize == Marshal.SizeOf(type); + } + + public static int AttributeSize(VertexAttribType type) => type switch + { + VertexAttribType.Byte or VertexAttribType.UnsignedByte => sizeof(byte), + VertexAttribType.Short or VertexAttribType.UnsignedShort => sizeof(short), + VertexAttribType.Int or VertexAttribType.UnsignedInt => sizeof(int), + VertexAttribType.HalfFloat => Marshal.SizeOf(), + VertexAttribType.Float => sizeof(float), + VertexAttribType.Double => sizeof(double), + _ => 0 + }; +} \ No newline at end of file diff --git a/Engine/Renderer/Buffer/Vertex/VertexAttribute.cs b/Engine/Renderer/Buffer/Vertex/VertexAttribute.cs new file mode 100644 index 0000000..7671b9d --- /dev/null +++ b/Engine/Renderer/Buffer/Vertex/VertexAttribute.cs @@ -0,0 +1,26 @@ +using OpenTK.Graphics.OpenGL; + +namespace Engine.Renderer.Buffer.Vertex; + +[AttributeUsage(AttributeTargets.Field)] +public class VertexAttribute : Attribute +{ + public VertexAttribType Type { get; } + public int ComponentCount { get; } + public bool Normalized { get; } + public int RepeatCount { get; } + + public VertexAttribute(VertexAttribType type, int componentCount = 1, bool normalized = false, int repeatCount = 1) + { + if (componentCount <= 0) + throw new ArgumentException("Count must be greater than 0"); + + if (repeatCount <= 0) + throw new ArgumentException("Repeat must be greater than 0"); + + Type = type; + ComponentCount = componentCount; + Normalized = normalized; + RepeatCount = repeatCount; + } +} \ No newline at end of file diff --git a/Engine/Renderer/Buffer/VertexArray.cs b/Engine/Renderer/Buffer/VertexArray.cs new file mode 100644 index 0000000..3f7f7c2 --- /dev/null +++ b/Engine/Renderer/Buffer/VertexArray.cs @@ -0,0 +1,89 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using Engine.Renderer.Buffer.Vertex; +using OpenTK.Graphics.OpenGL; +using Serilog; + +namespace Engine.Renderer.Buffer; + +public class VertexArray : OpenGlObject +{ + // private IndexBuffer? _boundIndexBuffer; + // private readonly Dictionary> _boundVertexBuffers = new(); + + public VertexArray() + { + GL.CreateVertexArrays(1, out int handle); + Handle = handle; + } + + public void BindIndexBuffer(IndexBuffer buffer) + { + GL.VertexArrayElementBuffer(Handle, buffer.Handle); + + Log.Debug("Vertex array {Handle} bound to index buffer {Buffer}", Handle, buffer.Handle); + } + + public void BindVertexBuffer(VertexBuffer buffer, int bindingIndex = 0, int divisor = 0) + where T : struct, IVertex + { + if (bindingIndex < 0) + throw new ArgumentException("Binding index must be greater than 0"); + + if (divisor < 0) + throw new ArgumentException("Divisor must be greater than 0"); + + var stride = Marshal.SizeOf(); + var fields = IVertex.GetFields(); + + GL.VertexArrayVertexBuffer(Handle, bindingIndex, buffer.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, bindingIndex); + + location += attribute.RepeatCount; + } + + GL.VertexArrayBindingDivisor(Handle, bindingIndex, divisor); + + Log.Debug( + "Vertex array {Handle} bound to vertex buffer {Buffer} at {BindingIndex} binding with {Divisor} divisor", + Handle, buffer.Handle, bindingIndex, divisor); + } + + private void SetupAttribute(VertexAttribute attribute, int baseLocation, int baseOffset, int bindingIndex) + { + var size = attribute.ComponentCount * IVertex.AttributeSize(attribute.Type); + + for (var i = 0; i < attribute.RepeatCount; i++) + { + var location = baseLocation + i; + var offset = baseOffset + i * size; + + GL.EnableVertexArrayAttrib(Handle, location); + GL.VertexArrayAttribFormat(Handle, location, attribute.ComponentCount, attribute.Type, attribute.Normalized, + offset); + GL.VertexArrayAttribBinding(Handle, location, bindingIndex); + } + } + + internal override void Bind() + { + GL.BindVertexArray(Handle); + } + + internal override void Unbind() + { + GL.BindVertexArray(0); + } + + protected override void Destroy() + { + GL.DeleteVertexArray(Handle); + } +} \ No newline at end of file diff --git a/Engine/Renderer/Buffer/VertexBuffer.cs b/Engine/Renderer/Buffer/VertexBuffer.cs new file mode 100644 index 0000000..aea1fd1 --- /dev/null +++ b/Engine/Renderer/Buffer/VertexBuffer.cs @@ -0,0 +1,63 @@ +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 count, BufferStorageFlags flags) + { + if (!IVertex.IsValid(typeof(T))) + throw new ArgumentException($"Type {typeof(T).Name} is not a valid vertex type"); + + if (count <= 0) + throw new ArgumentException("Count must be greater than 0"); + + Count = count; + + GL.CreateBuffers(1, out int handle); + Handle = handle; + + GL.NamedBufferStorage(Handle, Count * _stride, IntPtr.Zero, flags); + + Log.Debug("Vertex buffer {Handle} created with {Count} elements of type {Type}", Handle, Count, typeof(T).Name); + } + + public void UploadData(T[] data) + { + UploadData(0, data); + } + + public void UploadData(int offset, T[] data) + { + if (offset < 0) + throw new ArgumentException("Offset must be greater than 0"); + + if (data.Length + offset > Count) + throw new ArgumentException("Data array is too large"); + + GL.NamedBufferSubData(Handle, offset * _stride, data.Length * _stride, data); + } + + internal override void Bind() + { + GL.BindBuffer(BufferTarget.ArrayBuffer, Handle); + } + + internal 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/Renderer/Camera/ICamera.cs b/Engine/Renderer/Camera/ICamera.cs new file mode 100644 index 0000000..af339af --- /dev/null +++ b/Engine/Renderer/Camera/ICamera.cs @@ -0,0 +1,9 @@ +using OpenTK.Mathematics; + +namespace Engine.Renderer.Camera; + +public interface ICamera +{ + public Matrix4 View { get; } + public Matrix4 Projection { get; } +} \ No newline at end of file diff --git a/Engine/Renderer/Camera/ScreenspaceCamera.cs b/Engine/Renderer/Camera/ScreenspaceCamera.cs new file mode 100644 index 0000000..0e5db51 --- /dev/null +++ b/Engine/Renderer/Camera/ScreenspaceCamera.cs @@ -0,0 +1,9 @@ +using OpenTK.Mathematics; + +namespace Engine.Renderer.Camera; + +public class ScreenspaceCamera : ICamera +{ + public Matrix4 View => Matrix4.Identity; + public Matrix4 Projection => Matrix4.Identity; +} \ No newline at end of file diff --git a/Engine/Renderer/Framebuffer/Framebuffer.cs b/Engine/Renderer/Framebuffer/Framebuffer.cs new file mode 100644 index 0000000..fe32ec4 --- /dev/null +++ b/Engine/Renderer/Framebuffer/Framebuffer.cs @@ -0,0 +1,88 @@ +using Engine.Renderer.Pixel; +using Engine.Renderer.Texture; +using OpenTK.Graphics.OpenGL; + +namespace Engine.Renderer.Framebuffer; + +public class Framebuffer : OpenGlObject +{ + public int Width + { + get => _width; + protected set + { + if (value <= 0) + throw new ArgumentException("Width must be greater than 0"); + + _width = value; + } + } + + public int Height + { + get => _height; + protected set + { + if (value <= 0) + throw new ArgumentException("Height must be greater than 0"); + + _height = value; + } + } + + public IConstTexture Texture => _texture; + internal Texture.Texture TextureInternal => _texture; + + private int _width; + private int _height; + + private readonly DynamicTexture _texture; + private readonly Renderbuffer _renderbuffer; + + public Framebuffer(int width, int height) + { + Width = width; + Height = height; + + GL.CreateFramebuffers(1, out int handle); + Handle = handle; + + _texture = new DynamicTexture(width, height); + GL.NamedFramebufferTexture(Handle, FramebufferAttachment.ColorAttachment0, _texture.Handle, 0); + + _renderbuffer = new Renderbuffer(width, height, RenderbufferStorage.Depth24Stencil8); + GL.NamedFramebufferRenderbuffer(Handle, FramebufferAttachment.DepthStencilAttachment, + RenderbufferTarget.Renderbuffer, _renderbuffer.Handle); + + var status = GL.CheckNamedFramebufferStatus(Handle, FramebufferTarget.Framebuffer); + if (status != FramebufferStatus.FramebufferComplete) + throw new Exception($"Framebuffer is not complete: {status}"); + } + + public void Resize(int width, int height) + { + if (Width == width && Height == height) + return; + + Width = width; + Height = height; + + _texture.Resize(width, height); + _renderbuffer.Resize(width, height); + } + + internal override void Bind() + { + GL.BindFramebuffer(FramebufferTarget.Framebuffer, Handle); + } + + internal override void Unbind() + { + GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); + } + + protected override void Destroy() + { + GL.DeleteFramebuffer(Handle); + } +} \ No newline at end of file diff --git a/Engine/Renderer/Framebuffer/Renderbuffer.cs b/Engine/Renderer/Framebuffer/Renderbuffer.cs new file mode 100644 index 0000000..02857a8 --- /dev/null +++ b/Engine/Renderer/Framebuffer/Renderbuffer.cs @@ -0,0 +1,49 @@ +using OpenTK.Graphics.OpenGL; + +namespace Engine.Renderer.Framebuffer; + +public class Renderbuffer : OpenGlObject +{ + public int Width { get; private set; } + public int Height { get; private set; } + + private readonly RenderbufferStorage _format; + + public Renderbuffer(int width, int height, RenderbufferStorage format) + { + Width = width; + Height = height; + _format = format; + + GL.CreateRenderbuffers(1, out int handle); + Handle = handle; + + GL.NamedRenderbufferStorage(Handle, _format, Width, Height); + } + + public void Resize(int width, int height) + { + if (Width == width && Height == height) + return; + + Width = width; + Height = height; + + GL.NamedRenderbufferStorage(Handle, _format, Width, Height); + } + + internal override void Bind() + { + GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, Handle); + } + + internal override void Unbind() + { + GL.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0); + } + + protected override void Destroy() + { + GL.DeleteRenderbuffer(Handle); + } +} \ No newline at end of file diff --git a/Engine/Renderer/IPresenter.cs b/Engine/Renderer/IPresenter.cs new file mode 100644 index 0000000..110ae7f --- /dev/null +++ b/Engine/Renderer/IPresenter.cs @@ -0,0 +1,9 @@ +using Engine.Renderer.Pixel; +using Engine.Renderer.Texture; + +namespace Engine.Renderer; + +public interface IPresenter where T : struct, IPixel +{ + public void Present(IConstTexture texture); +} \ No newline at end of file diff --git a/Engine/Renderer/OpenGLObject.cs b/Engine/Renderer/OpenGLObject.cs new file mode 100644 index 0000000..5069989 --- /dev/null +++ b/Engine/Renderer/OpenGLObject.cs @@ -0,0 +1,21 @@ +using Serilog; + +namespace Engine.Renderer; + +public abstract class OpenGlObject +{ + public int Handle { get; protected set; } = -1; + + internal abstract void Bind(); + internal abstract void Unbind(); + + protected abstract void Destroy(); + + ~OpenGlObject() + { + Destroy(); + Handle = -1; + + Log.Debug("OpenGL object {Handle} destroyed", Handle); + } +} \ No newline at end of file diff --git a/Engine/Renderer/Pixel/IPixel.cs b/Engine/Renderer/Pixel/IPixel.cs new file mode 100644 index 0000000..ba3efdc --- /dev/null +++ b/Engine/Renderer/Pixel/IPixel.cs @@ -0,0 +1,12 @@ +using OpenTK.Graphics.OpenGL; + +namespace Engine.Renderer.Pixel; + +public interface IPixel +{ + public PixelFormat Format { get; } + public PixelType Type { get; } + + public PixelInternalFormat InternalFormat { get; } + public SizedInternalFormat SizedInternalFormat { get; } +} \ No newline at end of file diff --git a/Engine/Renderer/Pixel/Rgb8.cs b/Engine/Renderer/Pixel/Rgb8.cs new file mode 100644 index 0000000..e0c6306 --- /dev/null +++ b/Engine/Renderer/Pixel/Rgb8.cs @@ -0,0 +1,16 @@ +using OpenTK.Graphics.OpenGL; + +namespace Engine.Renderer.Pixel; + +public struct Rgb8 : IPixel +{ + public PixelFormat Format => PixelFormat.Rgb; + public PixelType Type => PixelType.UnsignedByte; + + public PixelInternalFormat InternalFormat => PixelInternalFormat.Rgb8; + public SizedInternalFormat SizedInternalFormat => SizedInternalFormat.Rgb8; + + public byte R; + public byte G; + public byte B; +} \ No newline at end of file diff --git a/Engine/Renderer/Renderer.cs b/Engine/Renderer/Renderer.cs new file mode 100644 index 0000000..b1ed6d9 --- /dev/null +++ b/Engine/Renderer/Renderer.cs @@ -0,0 +1,107 @@ +using System.Runtime.InteropServices; +using Engine.Renderer.Pixel; +using OpenTK.Graphics.OpenGL; +using Serilog; +using Serilog.Events; + +namespace Engine.Renderer; + +public class Renderer +{ + internal Texture.Texture TextureInternal => _framebuffer.TextureInternal; + + private readonly Framebuffer.Framebuffer _framebuffer; + private readonly Queue> _renderActions = new(); + + public Renderer(int width, int height) + { + InitializeOpenGl(); + + _framebuffer = new Framebuffer.Framebuffer(width, height); + } + + private void InitializeOpenGl() + { + GL.Enable(EnableCap.DebugOutput); + GL.DebugMessageCallback(DebugCallback, IntPtr.Zero); + + GL.Enable(EnableCap.DepthTest); + + GL.Enable(EnableCap.FramebufferSrgb); + + GL.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); + GL.Enable(EnableCap.Blend); + } + + private static void DebugCallback(DebugSource source, DebugType type, int id, DebugSeverity severity, int length, + IntPtr message, IntPtr userParam) + { + var logger = Log.ForContext(); + + var messageText = Marshal.PtrToStringAnsi(message, length) ?? "Unknown OpenGL Error"; + + var typePrefix = type switch + { + DebugType.DebugTypeError => "Error", + DebugType.DebugTypeDeprecatedBehavior => "Deprecated", + DebugType.DebugTypeUndefinedBehavior => "Undefined", + DebugType.DebugTypePortability => "Portability", + DebugType.DebugTypePerformance => "Performance", + DebugType.DebugTypeMarker => "Marker", + DebugType.DebugTypePushGroup => "PushGroup", + DebugType.DebugTypePopGroup => "PopGroup", + DebugType.DebugTypeOther => "Info", + _ => "Unknown" + }; + + var sourcePrefix = source switch + { + DebugSource.DebugSourceApi => "API", + DebugSource.DebugSourceWindowSystem => "Window", + DebugSource.DebugSourceShaderCompiler => "Shader", + DebugSource.DebugSourceThirdParty => "ThirdParty", + DebugSource.DebugSourceApplication => "Application", + DebugSource.DebugSourceOther => "Other", + _ => "Unknown" + }; + + logger.Write( + GetLogLevel(severity), + "[OpenGL {TypePrefix}] [{Source}] {Message} (ID: 0x{Id:X8})", + typePrefix, + sourcePrefix, + messageText, + id + ); + } + + private static LogEventLevel GetLogLevel(DebugSeverity severity) + { + return severity switch + { + DebugSeverity.DebugSeverityNotification => LogEventLevel.Information, + DebugSeverity.DebugSeverityHigh => LogEventLevel.Error, + DebugSeverity.DebugSeverityMedium => LogEventLevel.Warning, + DebugSeverity.DebugSeverityLow => LogEventLevel.Debug, + _ => LogEventLevel.Debug + }; + } + + internal void Commit(Action renderAction) + { + _renderActions.Enqueue(renderAction); + } + + internal void Render() + { + _framebuffer.Bind(); + while (_renderActions.TryDequeue(out var renderAction)) + renderAction(this); + _framebuffer.Unbind(); + } + + internal void Resize(int width, int height) + { + _framebuffer.Resize(width, height); + } +} \ No newline at end of file diff --git a/Engine/Renderer/Shader/ShaderProgram.cs b/Engine/Renderer/Shader/ShaderProgram.cs new file mode 100644 index 0000000..1b22aef --- /dev/null +++ b/Engine/Renderer/Shader/ShaderProgram.cs @@ -0,0 +1,181 @@ +using System.Runtime.CompilerServices; +using System.Text; +using OpenTK.Graphics.OpenGL; +using OpenTK.Mathematics; +using Serilog; + +namespace Engine.Renderer.Shader; + +public class ShaderProgram : OpenGlObject +{ + private readonly Dictionary _uniforms = new(); + + public static ShaderProgram CreateFromSource(string source) + { + var vertexSource = new StringBuilder(); + var fragmentSource = new StringBuilder(); + var inFragment = false; + var inVertex = false; + + foreach (var line in source.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 vertexSource, string fragmentSource) + { + var vertexShader = CompileSource(vertexSource, ShaderType.VertexShader); + var fragmentShader = CompileSource(fragmentSource, ShaderType.FragmentShader); + + Handle = LinkProgram(vertexShader, fragmentShader); + } + + public void SetUniform(string name, T value, [CallerMemberName] string caller = "") + { + try + { + var location = GetUniformLocation(name); + + switch (value) + { + case bool boolValue: + GL.ProgramUniform1(Handle, location, boolValue ? 1 : 0); + break; + case int intValue: + GL.ProgramUniform1(Handle, location, intValue); + break; + case float floatValue: + GL.ProgramUniform1(Handle, location, floatValue); + break; + case Vector2 vec2: + GL.ProgramUniform2(Handle, location, vec2); + break; + case Vector3 vec3: + GL.ProgramUniform3(Handle, location, vec3); + break; + case Vector4 vec4: + GL.ProgramUniform4(Handle, location, vec4); + break; + case Matrix4 matrix: + GL.ProgramUniformMatrix4(Handle, location, false, ref matrix); + break; + default: + throw new ArgumentException($"Unsupported uniform type: {typeof(T).Name}"); + } + } + catch (Exception ex) + { + Log.Error(ex, "Failed to set uniform {UniformName} from {Caller}", name, caller); + throw; + } + } + + internal override void Bind() + { + GL.UseProgram(Handle); + } + + internal override void Unbind() + { + GL.UseProgram(0); + } + + protected override void Destroy() + { + GL.DeleteProgram(Handle); + } + + private int GetUniformLocation(string name) + { + if (_uniforms.TryGetValue(name, out var location)) + return location; + + location = GL.GetUniformLocation(Handle, name); + if (location < 0) + throw new ArgumentException($"Uniform '{name}' not found in shader program"); + + _uniforms.Add(name, location); + return location; + } + + private static int CompileSource(string source, ShaderType type) + { + var shaderId = GL.CreateShader(type); + GL.ShaderSource(shaderId, source); + GL.CompileShader(shaderId); + + GL.GetShader(shaderId, ShaderParameter.CompileStatus, out var status); + if (status == 0) + { + var log = GL.GetShaderInfoLog(shaderId); + GL.DeleteShader(shaderId); + + throw new ShaderCompilationException(type, log); + } + + return shaderId; + } + + private static int LinkProgram(int vertexShader, int fragmentShader) + { + var programId = GL.CreateProgram(); + try + { + GL.AttachShader(programId, vertexShader); + GL.AttachShader(programId, fragmentShader); + GL.LinkProgram(programId); + + GL.GetProgram(programId, GetProgramParameterName.LinkStatus, out var linkStatus); + if (linkStatus == 0) + { + var log = GL.GetProgramInfoLog(programId); + throw new ShaderLinkException(log); + } + + GL.ValidateProgram(programId); + GL.GetProgram(programId, GetProgramParameterName.ValidateStatus, out var validateStatus); + if (validateStatus == 0) + { + var log = GL.GetProgramInfoLog(programId); + throw new ShaderValidationException(log); + } + } + finally + { + GL.DeleteShader(vertexShader); + GL.DeleteShader(fragmentShader); + } + + return programId; + } +} + +public class ShaderCompilationException(ShaderType type, string message) + : Exception($"Failed to compile {type} shader: {message}") +{ + public ShaderType ShaderType { get; } = type; +} + +public class ShaderLinkException(string message) : Exception($"Failed to link shader program: {message}"); + +public class ShaderValidationException(string message) : Exception($"Failed to validate shader program: {message}"); \ No newline at end of file diff --git a/Engine/Renderer/Texture/DynamicTexture.cs b/Engine/Renderer/Texture/DynamicTexture.cs new file mode 100644 index 0000000..5e75fdd --- /dev/null +++ b/Engine/Renderer/Texture/DynamicTexture.cs @@ -0,0 +1,36 @@ +using Engine.Renderer.Pixel; +using OpenTK.Graphics.OpenGL; + +namespace Engine.Renderer.Texture; + +public class DynamicTexture : Texture where T : struct, IPixel +{ + private readonly PixelFormat _format; + private readonly PixelType _type; + private readonly PixelInternalFormat _internalFormat; + + public DynamicTexture(int width, int height) : base(width, height) + { + var pixel = default(T); + _format = pixel.Format; + _type = pixel.Type; + _internalFormat = pixel.InternalFormat; + + GL.BindTexture(TextureTarget.Texture2D, Handle); + GL.TexImage2D(TextureTarget.Texture2D, 0, _internalFormat, Width, Height, 0, _format, _type, + IntPtr.Zero); + } + + public void Resize(int width, int height) + { + if (Width == width && Height == height) + return; + + Width = width; + Height = height; + + Bind(); + GL.TexImage2D(TextureTarget.Texture2D, 0, _internalFormat, Width, Height, 0, _format, _type, + IntPtr.Zero); + } +} \ No newline at end of file diff --git a/Engine/Renderer/Texture/IConstTexture.cs b/Engine/Renderer/Texture/IConstTexture.cs new file mode 100644 index 0000000..c599c8c --- /dev/null +++ b/Engine/Renderer/Texture/IConstTexture.cs @@ -0,0 +1,37 @@ +using Engine.Asset; +using Engine.Renderer.Pixel; + +namespace Engine.Renderer.Texture; + +public interface IConstTexture where T : struct, IPixel +{ + public int Width { get; } + public int Height { get; } + + public void ReadPixels(int x, int y, int width, int height, T[,] pixels); +} + +public static class ConstTextureExtensions +{ + public static T[,] ReadPixels(this IConstTexture texture) where T : struct, IPixel + => texture.ReadPixels(0, 0, texture.Width, texture.Height); + + public static T[,] ReadPixels(this IConstTexture texture, int x, int y, int width, int height) + where T : struct, IPixel + { + var pixels = new T[width, height]; + texture.ReadPixels(x, y, width, height, pixels); + + return pixels; + } + + public static void ReadPixels(this IConstTexture texture, Image image) where T : struct, IPixel + => texture.ReadPixels(0, 0, image); + + public static void ReadPixels(this IConstTexture texture, int x, int y, Image image) + where T : struct, IPixel => + texture.ReadPixels(x, y, image.Width, image.Height, image.Pixels); + + public static void ReadPixels(this IConstTexture texture, T[,] pixels) where T : struct, IPixel + => texture.ReadPixels(0, 0, texture.Width, texture.Height, pixels); +} \ No newline at end of file diff --git a/Engine/Renderer/Texture/ITexture.cs b/Engine/Renderer/Texture/ITexture.cs new file mode 100644 index 0000000..0caea61 --- /dev/null +++ b/Engine/Renderer/Texture/ITexture.cs @@ -0,0 +1,22 @@ +using Engine.Asset; +using Engine.Renderer.Pixel; + +namespace Engine.Renderer.Texture; + +public interface ITexture : IConstTexture where T : struct, IPixel +{ + public void UploadPixels(int x, int y, int width, int height, T[,] pixels); +} + +public static class TextureExtensions +{ + public static void UploadPixels(this ITexture texture, Image image) where T : struct, IPixel + => texture.UploadPixels(0, 0, image); + + public static void UploadPixels(this ITexture texture, int x, int y, Image image) + where T : struct, IPixel => + texture.UploadPixels(x, y, image.Width, image.Height, image.Pixels); + + public static void UploadPixels(this ITexture texture, T[,] pixels) where T : struct, IPixel + => texture.UploadPixels(0, 0, texture.Width, texture.Height, pixels); +} \ No newline at end of file diff --git a/Engine/Renderer/Texture/StaticTexture.cs b/Engine/Renderer/Texture/StaticTexture.cs new file mode 100644 index 0000000..94653e7 --- /dev/null +++ b/Engine/Renderer/Texture/StaticTexture.cs @@ -0,0 +1,13 @@ +using Engine.Renderer.Pixel; +using OpenTK.Graphics.OpenGL; + +namespace Engine.Renderer.Texture; + +public class StaticTexture : Texture where T : struct, IPixel +{ + public StaticTexture(int width, int height) : base(width, height) + { + var format = default(T).SizedInternalFormat; + GL.TextureStorage2D(Handle, 1, format, Width, Height); + } +} \ No newline at end of file diff --git a/Engine/Renderer/Texture/Texture.cs b/Engine/Renderer/Texture/Texture.cs new file mode 100644 index 0000000..f5a6e97 --- /dev/null +++ b/Engine/Renderer/Texture/Texture.cs @@ -0,0 +1,106 @@ +using System.Runtime.InteropServices; +using Engine.Asset; +using Engine.Renderer.Pixel; +using OpenTK.Graphics.OpenGL; + +namespace Engine.Renderer.Texture; + +public abstract class Texture : OpenGlObject, ITexture where T : struct, IPixel +{ + public int Width + { + get => _width; + protected set + { + if (value <= 0) + throw new ArgumentException("Width must be greater than 0"); + + _width = value; + } + } + + public int Height + { + get => _height; + protected set + { + if (value <= 0) + throw new ArgumentException("Height must be greater than 0"); + + _height = value; + } + } + + private int _width; + private int _height; + + protected Texture(int width, int height) + { + Width = width; + Height = height; + + GL.CreateTextures(TextureTarget.Texture2D, 1, out int handle); + Handle = handle; + } + + public void UploadPixels(int x, int y, int width, int height, T[,] pixels) + { + if (x < 0 || y < 0) + throw new ArgumentException("x and y must be greater than 0"); + + if (width <= 0 || height <= 0) + throw new ArgumentException("Width and height must be greater than 0"); + + if (x + width > Width || y + height > Height) + throw new ArgumentException("x + width and y + height must be less than width and height"); + + if (pixels.Length != width * height) + throw new ArgumentException("Pixels array must be of size width * height"); + + var format = pixels[0, 0].Format; + var type = pixels[0, 0].Type; + + GL.TextureSubImage2D(Handle, 0, x, y, width, height, format, type, pixels); + } + + public void ReadPixels(int x, int y, int width, int height, T[,] pixels) + { + if (x < 0 || y < 0) + throw new ArgumentException("x and y must be greater than 0"); + + if (width <= 0 || height <= 0) + throw new ArgumentException("Width and height must be greater than 0"); + + if (x + width > Width || y + height > Height) + throw new ArgumentException("x + width and y + height must be less than width and height"); + + if (pixels.Length != width * height) + throw new ArgumentException("Pixels array must be of size width * height"); + + var format = default(T).Format; + var type = default(T).Type; + + GL.GetTextureSubImage(Handle, 0, x, y, 0, width, height, 1, format, type, pixels.Length * Marshal.SizeOf(), + pixels); + } + + internal void BindUnit(int unit) + { + GL.BindTextureUnit(unit, Handle); + } + + internal override void Bind() + { + GL.BindTexture(TextureTarget.Texture2D, Handle); + } + + internal override void Unbind() + { + GL.BindTexture(TextureTarget.Texture2D, 0); + } + + protected override void Destroy() + { + GL.DeleteTexture(Handle); + } +} \ No newline at end of file diff --git a/Engine/Scene/Component/Component.cs b/Engine/Scene/Component/Component.cs new file mode 100644 index 0000000..37066e4 --- /dev/null +++ b/Engine/Scene/Component/Component.cs @@ -0,0 +1,8 @@ +namespace Engine.Scene.Component; + +public abstract class Component(GameObject gameObject) +{ + public Guid Id { get; } = Guid.NewGuid(); + + public GameObject GameObject { get; } = gameObject; +} \ No newline at end of file diff --git a/Engine/Scene/Component/PerspectiveCamera.cs b/Engine/Scene/Component/PerspectiveCamera.cs new file mode 100644 index 0000000..7b58b2e --- /dev/null +++ b/Engine/Scene/Component/PerspectiveCamera.cs @@ -0,0 +1,34 @@ +using Engine.Renderer.Camera; +using Engine.Util; +using OpenTK.Mathematics; + +namespace Engine.Scene.Component; + +public class PerspectiveCamera( + GameObject gameObject, + float aspectRatio, + float fieldOfView, + float nearPlane, + float farPlane) + : Component(gameObject), ICamera +{ + public float AspectRatio { get; set; } = aspectRatio; + public float FieldOfView { get; set; } = fieldOfView; + public float NearPlane { get; set; } = nearPlane; + public float FarPlane { get; set; } = farPlane; + + public Matrix4 View + { + get + { + 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(); + + return Matrix4.LookAt(eye.Xyz, forward.Xyz, up.Xyz); + } + } + + public Matrix4 Projection => Matrix4.CreatePerspectiveFieldOfView(FieldOfView, AspectRatio, NearPlane, FarPlane); +} \ No newline at end of file diff --git a/Engine/Scene/Component/Transform.cs b/Engine/Scene/Component/Transform.cs new file mode 100644 index 0000000..cfa80a9 --- /dev/null +++ b/Engine/Scene/Component/Transform.cs @@ -0,0 +1,19 @@ +using OpenTK.Mathematics; + +namespace Engine.Scene.Component; + +public class Transform(GameObject gameObject) : Component(gameObject) +{ + 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; + + public Matrix4 LocalTransformMatrix => Matrix4.CreateScale(Scale) * + Matrix4.CreateFromQuaternion(Rotation) * + Matrix4.CreateTranslation(Position); + + public Matrix4 TransformMatrix => LocalTransformMatrix * ParentTransformMatrix; + + private Matrix4 ParentTransformMatrix => Matrix4.Identity; +} \ No newline at end of file diff --git a/Engine/Scene/GameObject.cs b/Engine/Scene/GameObject.cs new file mode 100644 index 0000000..61e9c58 --- /dev/null +++ b/Engine/Scene/GameObject.cs @@ -0,0 +1,73 @@ +using Engine.Scene.Component; + +namespace Engine.Scene; + +public sealed class GameObject +{ + public Guid Id { get; } = Guid.NewGuid(); + public Transform Transform { get; } + + private readonly Queue _componentActions = new(); + + private readonly List _components = new(); + private readonly ISet _addedComponentTypes = new HashSet(); + + public GameObject() + { + AddComponent(); + UpdateComponents(); + + Transform = GetComponent()!; + } + + public T? GetComponent() where T : Component.Component + { + if (!_addedComponentTypes.Contains(typeof(T))) + return null; + + return _components.OfType().FirstOrDefault(); + } + + public void AddComponent(params object?[] args) where T : Component.Component + { + if (_addedComponentTypes.Contains(typeof(T))) + return; + + var newArgs = new object?[args.Length + 1]; + newArgs[0] = this; + for (var i = 0; i < args.Length; i++) + newArgs[i + 1] = args[i]; + + var component = (T?)Activator.CreateInstance(typeof(T), newArgs); + if (component == null) + throw new InvalidOperationException($"Failed to create component of type {typeof(T)}"); + + _componentActions.Enqueue(() => + { + _components.Add(component); + _addedComponentTypes.Add(typeof(T)); + }); + } + + public void RemoveComponent() where T : Component.Component + { + if (!_addedComponentTypes.Contains(typeof(T)) || typeof(T) == typeof(Transform)) + return; + + var component = GetComponent(); + if (component == null) + return; + + _componentActions.Enqueue(() => + { + _components.Remove(component); + _addedComponentTypes.Remove(typeof(T)); + }); + } + + private void UpdateComponents() + { + while (_componentActions.TryDequeue(out var action)) + action(); + } +} \ No newline at end of file diff --git a/Engine/Util/Math.cs b/Engine/Util/Math.cs new file mode 100644 index 0000000..481755a --- /dev/null +++ b/Engine/Util/Math.cs @@ -0,0 +1,15 @@ +using OpenTK.Mathematics; + +namespace Engine.Util; + +public static class Math +{ + public static Vector4 MulProject(this Vector4 a, in Matrix4 m) + { + var result = a * m; + if (result.W != 0.0) + result /= result.W; + + return result; + } +} \ No newline at end of file diff --git a/Engine/Window.cs b/Engine/Window.cs new file mode 100644 index 0000000..28d39bf --- /dev/null +++ b/Engine/Window.cs @@ -0,0 +1,79 @@ +using Engine.Renderer; +using Engine.Renderer.Pixel; +using Engine.Renderer.Texture; +using OpenTK.Graphics.OpenGL; +using OpenTK.Windowing.Desktop; +using OpenTK.Windowing.GraphicsLibraryFramework; + +namespace Engine; + +public class Window : IPresenter +{ + public bool IsExiting => _window.IsExiting; + public int Width => _window.ClientSize.X; + public int Height => _window.ClientSize.Y; + + private readonly Engine _engine; + private readonly NativeWindow _window; + private readonly bool _headless; + + public Window(Engine engine, NativeWindow window, bool headless) + { + _engine = engine; + _window = window; + _headless = headless; + + _window.MakeCurrent(); + _window.Resize += args => GL.Viewport(0, 0, args.Width, args.Height); + } + + public void Update() + { + if (!_headless) + { + NativeWindow.ProcessWindowEvents(false); + _window.SwapBuffers(); + } + } + + public void Present(IConstTexture texture) + { + if (_headless) + return; + + GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); + + _engine.Renderer.TextureInternal.Bind(); + + GL.Enable(EnableCap.Texture2D); + GL.Begin(PrimitiveType.Quads); + GL.Color3(1, 1, 1); + + GL.TexCoord2(0, 0); + GL.Vertex2(0, 0); + + GL.TexCoord2(1, 0); + GL.Vertex2(1, 0); + + GL.TexCoord2(1, 1); + GL.Vertex2(1, 1); + + GL.TexCoord2(0, 1); + GL.Vertex2(0, 1); + + GL.End(); + + GL.Disable(EnableCap.Texture2D); + GL.Flush(); + + _engine.Renderer.TextureInternal.Unbind(); + } +} + +public static class NativeWindowExtensions +{ + public static unsafe void SwapBuffers(this NativeWindow window) + { + GLFW.SwapBuffers(window.WindowPtr); + } +} \ No newline at end of file