This commit is contained in:
2024-12-04 22:35:04 +03:00
commit 3f1740f41f
43 changed files with 1757 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea/
*.DotSettings.*

34
DoomDeathmatch.sln Normal file
View File

@@ -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

View File

@@ -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);
}

View File

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

20
DoomDeathmatch/Program.cs Normal file
View File

@@ -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();
}
}

View File

@@ -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;
}

32
Engine/Asset/Image.cs Normal file
View File

@@ -0,0 +1,32 @@
using Engine.Renderer.Pixel;
using Engine.Renderer.Texture;
namespace Engine.Asset;
public class Image<T>(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<T> ToDynamicTexture()
{
var texture = new DynamicTexture<T>(Width, Height);
texture.UploadPixels(this);
return texture;
}
public StaticTexture<T> ToStaticTexture()
{
var texture = new StaticTexture<T>(Width, Height);
texture.UploadPixels(this);
return texture;
}
}

View File

@@ -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<Mesh.Vertex, uint>();
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;
}
}

View File

@@ -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
}

View File

@@ -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<Vector3>();
var tempNormals = new List<Vector3>();
var tempUVs = new List<Vector2>();
var index = 0u;
var loadNormals = parameters.HasFlag(MeshLoaderParameters.LoadNormals);
var loadUVs = parameters.HasFlag(MeshLoaderParameters.LoadUVs);
using var reader = new StreamReader(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;
}
}

View File

@@ -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;
}
}

22
Engine/Asset/Mesh/Mesh.cs Normal file
View File

@@ -0,0 +1,22 @@
using OpenTK.Mathematics;
namespace Engine.Asset.Mesh;
public class Mesh
{
public IReadOnlyList<Vertex> Vertices => _vertices;
public IReadOnlyList<uint> Indices => _indices;
internal IList<Vertex> VerticesInternal => _vertices;
internal IList<uint> IndicesInternal => _indices;
private readonly List<Vertex> _vertices = [];
private readonly List<uint> _indices = [];
public record struct Vertex
{
public Vector3 Position { get; internal set; }
public Vector3 Normal { get; internal set; }
public Vector2 Uv { get; internal set; }
}
}

65
Engine/Engine.cs Normal file
View File

@@ -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<Engine>();
}
public void Run()
{
GL.ClearColor(0.2f, 0.3f, 0.3f, 1.0f);
GL.Viewport(0, 0, _window.Width, _window.Height);
while (!_window.IsExiting)
{
}
}
}

20
Engine/Engine.csproj Normal file
View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="OpenTK" Version="4.8.2" />
<PackageReference Include="Serilog" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="4.0.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
</ItemGroup>
</Project>

View File

@@ -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);
}

52
Engine/Input/Key.cs Normal file
View File

@@ -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,
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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<FieldInfo> GetFields<T>() => GetFields(typeof(T));
public static IOrderedEnumerable<FieldInfo> 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<VertexAttribute>();
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<Half>(),
VertexAttribType.Float => sizeof(float),
VertexAttribType.Double => sizeof(double),
_ => 0
};
}

View File

@@ -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;
}
}

View File

@@ -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<int, VertexBuffer<IVertex>> _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<T>(VertexBuffer<T> 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<T>();
var fields = IVertex.GetFields<T>();
GL.VertexArrayVertexBuffer(Handle, bindingIndex, buffer.Handle, 0, stride);
var location = 0;
foreach (var field in fields)
{
var attribute = field.GetCustomAttribute<VertexAttribute>()!;
var offset = Marshal.OffsetOf<T>(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);
}
}

View File

@@ -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<T> : OpenGlObject
where T : struct, IVertex
{
internal int Count { get; }
private readonly int _stride = Marshal.SizeOf<T>();
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);
}
}

View File

@@ -0,0 +1,9 @@
using OpenTK.Mathematics;
namespace Engine.Renderer.Camera;
public interface ICamera
{
public Matrix4 View { get; }
public Matrix4 Projection { get; }
}

View File

@@ -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;
}

View File

@@ -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<Rgb8> Texture => _texture;
internal Texture.Texture<Rgb8> TextureInternal => _texture;
private int _width;
private int _height;
private readonly DynamicTexture<Rgb8> _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<Rgb8>(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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,9 @@
using Engine.Renderer.Pixel;
using Engine.Renderer.Texture;
namespace Engine.Renderer;
public interface IPresenter<T> where T : struct, IPixel
{
public void Present(IConstTexture<T> texture);
}

View File

@@ -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);
}
}

View File

@@ -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; }
}

View File

@@ -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;
}

107
Engine/Renderer/Renderer.cs Normal file
View File

@@ -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<Rgb8> TextureInternal => _framebuffer.TextureInternal;
private readonly Framebuffer.Framebuffer _framebuffer;
private readonly Queue<Action<Renderer>> _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<Engine>();
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<Renderer> 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);
}
}

View File

@@ -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<string, int> _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<T>(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}");

View File

@@ -0,0 +1,36 @@
using Engine.Renderer.Pixel;
using OpenTK.Graphics.OpenGL;
namespace Engine.Renderer.Texture;
public class DynamicTexture<T> : Texture<T> 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);
}
}

View File

@@ -0,0 +1,37 @@
using Engine.Asset;
using Engine.Renderer.Pixel;
namespace Engine.Renderer.Texture;
public interface IConstTexture<T> 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<T>(this IConstTexture<T> texture) where T : struct, IPixel
=> texture.ReadPixels(0, 0, texture.Width, texture.Height);
public static T[,] ReadPixels<T>(this IConstTexture<T> 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<T>(this IConstTexture<T> texture, Image<T> image) where T : struct, IPixel
=> texture.ReadPixels(0, 0, image);
public static void ReadPixels<T>(this IConstTexture<T> texture, int x, int y, Image<T> image)
where T : struct, IPixel =>
texture.ReadPixels(x, y, image.Width, image.Height, image.Pixels);
public static void ReadPixels<T>(this IConstTexture<T> texture, T[,] pixels) where T : struct, IPixel
=> texture.ReadPixels(0, 0, texture.Width, texture.Height, pixels);
}

View File

@@ -0,0 +1,22 @@
using Engine.Asset;
using Engine.Renderer.Pixel;
namespace Engine.Renderer.Texture;
public interface ITexture<T> : IConstTexture<T> 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<T>(this ITexture<T> texture, Image<T> image) where T : struct, IPixel
=> texture.UploadPixels(0, 0, image);
public static void UploadPixels<T>(this ITexture<T> texture, int x, int y, Image<T> image)
where T : struct, IPixel =>
texture.UploadPixels(x, y, image.Width, image.Height, image.Pixels);
public static void UploadPixels<T>(this ITexture<T> texture, T[,] pixels) where T : struct, IPixel
=> texture.UploadPixels(0, 0, texture.Width, texture.Height, pixels);
}

View File

@@ -0,0 +1,13 @@
using Engine.Renderer.Pixel;
using OpenTK.Graphics.OpenGL;
namespace Engine.Renderer.Texture;
public class StaticTexture<T> : Texture<T> 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);
}
}

View File

@@ -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<T> : OpenGlObject, ITexture<T> 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<T>(),
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);
}
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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<Action> _componentActions = new();
private readonly List<Component.Component> _components = new();
private readonly ISet<Type> _addedComponentTypes = new HashSet<Type>();
public GameObject()
{
AddComponent<Transform>();
UpdateComponents();
Transform = GetComponent<Transform>()!;
}
public T? GetComponent<T>() where T : Component.Component
{
if (!_addedComponentTypes.Contains(typeof(T)))
return null;
return _components.OfType<T>().FirstOrDefault();
}
public void AddComponent<T>(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<T>() where T : Component.Component
{
if (!_addedComponentTypes.Contains(typeof(T)) || typeof(T) == typeof(Transform))
return;
var component = GetComponent<T>();
if (component == null)
return;
_componentActions.Enqueue(() =>
{
_components.Remove(component);
_addedComponentTypes.Remove(typeof(T));
});
}
private void UpdateComponents()
{
while (_componentActions.TryDequeue(out var action))
action();
}
}

15
Engine/Util/Math.cs Normal file
View File

@@ -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;
}
}

79
Engine/Window.cs Normal file
View File

@@ -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<Rgb8>
{
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<Rgb8> 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);
}
}