diff --git a/CORTEX_ENGINE.sln b/CORTEX_ENGINE.sln
index e78b39d..0855831 100644
--- a/CORTEX_ENGINE.sln
+++ b/CORTEX_ENGINE.sln
@@ -21,7 +21,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{827E0CD3-B72
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Physics", "src\Engine.Physics\Engine.Physics.csproj", "{3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Graphics.OpenGL", "src\Engine.Graphics.OpenGL\Engine.Graphics.OpenGL.csproj", "{ED34FE48-9D17-4564-82BA-FA8D3852719A}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Engine.Graphics.OpenTK", "src\Engine.Graphics.OpenTK\Engine.Graphics.OpenTK.csproj", "{641B69F3-E52C-4713-A97C-DAC1C814DD72}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -178,30 +178,30 @@ Global
{3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|x64.Build.0 = Debug|Any CPU
{3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|x86.ActiveCfg = Debug|Any CPU
{3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883}.ReleaseAOT|x86.Build.0 = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Debug|x64.ActiveCfg = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Debug|x64.Build.0 = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Debug|x86.ActiveCfg = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Debug|x86.Build.0 = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Release|Any CPU.Build.0 = Release|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Release|x64.ActiveCfg = Release|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Release|x64.Build.0 = Release|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Release|x86.ActiveCfg = Release|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.Release|x86.Build.0 = Release|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.ReleaseAOT|Any CPU.ActiveCfg = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.ReleaseAOT|Any CPU.Build.0 = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.ReleaseAOT|x64.ActiveCfg = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.ReleaseAOT|x64.Build.0 = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.ReleaseAOT|x86.ActiveCfg = Debug|Any CPU
- {ED34FE48-9D17-4564-82BA-FA8D3852719A}.ReleaseAOT|x86.Build.0 = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|x64.Build.0 = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Debug|x86.Build.0 = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|Any CPU.Build.0 = Release|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|x64.ActiveCfg = Release|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|x64.Build.0 = Release|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|x86.ActiveCfg = Release|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.Release|x86.Build.0 = Release|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|Any CPU.ActiveCfg = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|Any CPU.Build.0 = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|x64.ActiveCfg = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|x64.Build.0 = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|x86.ActiveCfg = Debug|Any CPU
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72}.ReleaseAOT|x86.Build.0 = Debug|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{3F5F10AA-A4CD-4E37-B4CE-B37BC60C3883} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- {ED34FE48-9D17-4564-82BA-FA8D3852719A} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
+ {641B69F3-E52C-4713-A97C-DAC1C814DD72} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
EndGlobalSection
EndGlobal
diff --git a/src/CortexEngine.App/CortexEngine.App.csproj b/src/CortexEngine.App/CortexEngine.App.csproj
index 0e30318..3339606 100644
--- a/src/CortexEngine.App/CortexEngine.App.csproj
+++ b/src/CortexEngine.App/CortexEngine.App.csproj
@@ -26,7 +26,7 @@
-
+
diff --git a/src/CortexEngine.App/Program.cs b/src/CortexEngine.App/Program.cs
index 0a4a254..c20edef 100644
--- a/src/CortexEngine.App/Program.cs
+++ b/src/CortexEngine.App/Program.cs
@@ -11,7 +11,7 @@ using Engine.Core;
using Engine.Core.Components;
using Engine.Graphics;
using Engine.Graphics.Loaders;
-using Engine.Graphics.OpenGL;
+using Engine.Graphics.OpenTK;
using Engine.Graphics.RaylibBackend;
using Engine.Graphics.Vulkan;
using Engine.Physics;
@@ -44,8 +44,8 @@ class Program
RaylibBackendRegistrar.EnsureRegistered();
VulkanBackendRegistrar.EnsureRegistered();
- OpenGLBackendRegistrar.EnsureRegistered();
- using var renderContext = RenderBackendFactory.Create("raylib", 1280, 720, enableValidation: false);
+ OpenTKBackendRegistrar.EnsureRegistered();
+ using var renderContext = RenderBackendFactory.Create("opentk", 1280, 720, enableValidation: false);
var window = renderContext.Window;
var input = window.Input;
using var renderer = renderContext.CreateRenderer();
@@ -296,8 +296,8 @@ class Program
queue.CompletePendingScreenshots();
// Swap buffers for OpenGL backend
- if (renderContext is OpenGLRenderContext glCtx2)
- glCtx2.SwapBuffers();
+ if (renderContext is OpenTKRenderContext otkCtx)
+ otkCtx.SwapBuffers();
frames++;
if (timing.TotalTime - lastFpsTime >= 1.0)
diff --git a/src/Engine.Graphics.OpenGL/OpenGLBackendRegistrar.cs b/src/Engine.Graphics.OpenGL/OpenGLBackendRegistrar.cs
deleted file mode 100644
index 892a5a7..0000000
--- a/src/Engine.Graphics.OpenGL/OpenGLBackendRegistrar.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using Engine.Graphics;
-
-namespace Engine.Graphics.OpenGL;
-
-///
-/// Triggers registration of the OpenGL backend with the HAL factory.
-///
-public static class OpenGLBackendRegistrar
-{
- static OpenGLBackendRegistrar()
- {
- RenderBackendFactory.Register("opengl", (width, height, _) => new OpenGLRenderContext(width, height));
- }
-
- public static void EnsureRegistered() { }
-}
diff --git a/src/Engine.Graphics.OpenGL/OpenGLInputState.cs b/src/Engine.Graphics.OpenGL/OpenGLInputState.cs
deleted file mode 100644
index 60c251b..0000000
--- a/src/Engine.Graphics.OpenGL/OpenGLInputState.cs
+++ /dev/null
@@ -1,93 +0,0 @@
-using System.Collections.Generic;
-using Engine.Core;
-using Silk.NET.Input;
-using System.Numerics;
-
-namespace Engine.Graphics.OpenGL;
-
-public sealed class OpenGLInputState : IInputState
-{
- private IKeyboard? _keyboard;
- private IMouse? _mouse;
- private readonly HashSet _keysDown = new();
- private readonly HashSet _keysPressed = new();
- private readonly HashSet _keysReleased = new();
- private float _mouseWheelDelta;
- private bool _mouseLeft, _mouseRight, _mouseMiddle;
-
- public int MouseX { get; private set; }
- public int MouseY { get; private set; }
- public bool MouseLeft => _mouseLeft;
- public bool MouseRight => _mouseRight;
- public bool MouseMiddle => _mouseMiddle;
- public float MouseWheelDelta => _mouseWheelDelta;
-
- public void Initialize(IInputContext input)
- {
- if (input.Keyboards.Count > 0)
- {
- _keyboard = input.Keyboards[0];
- _keyboard.KeyDown += (kb, key, scancode) =>
- {
- var k = SilkToKey(key);
- if (k == Engine.Core.Key.Unknown) return;
- if (!_keysDown.Contains(k)) _keysPressed.Add(k);
- _keysDown.Add(k);
- };
- _keyboard.KeyUp += (kb, key, scancode) =>
- {
- var k = SilkToKey(key);
- if (k == Engine.Core.Key.Unknown) return;
- _keysDown.Remove(k);
- _keysReleased.Add(k);
- };
- }
- if (input.Mice.Count > 0)
- {
- _mouse = input.Mice[0];
- _mouse.MouseMove += (m, pos) => { MouseX = (int)pos.X; MouseY = (int)pos.Y; };
- _mouse.MouseDown += (m, btn) =>
- {
- if (btn == MouseButton.Left) _mouseLeft = true;
- if (btn == MouseButton.Right) _mouseRight = true;
- if (btn == MouseButton.Middle) _mouseMiddle = true;
- };
- _mouse.MouseUp += (m, btn) =>
- {
- if (btn == MouseButton.Left) _mouseLeft = false;
- if (btn == MouseButton.Right) _mouseRight = false;
- if (btn == MouseButton.Middle) _mouseMiddle = false;
- };
- _mouse.Scroll += (m, wheel) => _mouseWheelDelta += (float)wheel.Y;
- }
- }
-
- public void Poll() { }
-
- public void BeginFrame()
- {
- _keysPressed.Clear();
- _keysReleased.Clear();
- _mouseWheelDelta = 0;
- }
-
- public bool IsKeyDown(Engine.Core.Key key) => _keysDown.Contains(key);
- public bool IsKeyPressed(Engine.Core.Key key) => _keysPressed.Contains(key);
- public bool IsKeyReleased(Engine.Core.Key key) => _keysReleased.Contains(key);
-
- private static Engine.Core.Key SilkToKey(Silk.NET.Input.Key key) => key switch
- {
- Silk.NET.Input.Key.Space => Engine.Core.Key.Space,
- Silk.NET.Input.Key.Escape => Engine.Core.Key.Escape,
- Silk.NET.Input.Key.Enter => Engine.Core.Key.Enter,
- Silk.NET.Input.Key.W => Engine.Core.Key.W,
- Silk.NET.Input.Key.A => Engine.Core.Key.A,
- Silk.NET.Input.Key.S => Engine.Core.Key.S,
- Silk.NET.Input.Key.D => Engine.Core.Key.D,
- Silk.NET.Input.Key.Q => Engine.Core.Key.Q,
- Silk.NET.Input.Key.E => Engine.Core.Key.E,
- Silk.NET.Input.Key.F => Engine.Core.Key.F,
- Silk.NET.Input.Key.ShiftLeft => Engine.Core.Key.LeftShift,
- _ => Engine.Core.Key.Unknown,
- };
-}
diff --git a/src/Engine.Graphics.OpenGL/OpenGLRenderContext.cs b/src/Engine.Graphics.OpenGL/OpenGLRenderContext.cs
deleted file mode 100644
index 538accd..0000000
--- a/src/Engine.Graphics.OpenGL/OpenGLRenderContext.cs
+++ /dev/null
@@ -1,46 +0,0 @@
-using Engine.Core;
-using Engine.Graphics;
-using Silk.NET.OpenGL;
-using Silk.NET.Windowing;
-using SilkWindow = Silk.NET.Windowing.IWindow;
-using CoreIWindow = Engine.Core.IWindow;
-
-namespace Engine.Graphics.OpenGL;
-
-public sealed class OpenGLRenderContext : IRenderContext
-{
- private readonly OpenGLWindow _window;
- private GL? _gl;
- private OpenGLRenderer? _renderer;
- private bool _disposed;
-
- public CoreIWindow Window => _window;
-
- public OpenGLRenderContext(int width, int height, bool enableValidation = false)
- {
- _window = new OpenGLWindow("Cortex Engine (OpenGL)", width, height);
- // Initialize() triggers the Load callback which creates the GL context
- _window.SilkView.Initialize();
- // After Initialize, CreateOpenGL should work
- _gl = _window.CreateGL();
- _window.OnLoad();
- }
-
- public IRenderer CreateRenderer()
- {
- _renderer = new OpenGLRenderer(_gl!);
- return _renderer;
- }
-
- public void Resize(int width, int height) => _renderer?.SetScreenSize(width, height);
-
- public void SwapBuffers() => _window.SilkView.SwapBuffers();
-
- public void Dispose()
- {
- if (_disposed) return;
- _disposed = true;
- _renderer?.Dispose();
- _window.Dispose();
- }
-}
diff --git a/src/Engine.Graphics.OpenGL/OpenGLRenderer.cs b/src/Engine.Graphics.OpenGL/OpenGLRenderer.cs
deleted file mode 100644
index f6a9201..0000000
--- a/src/Engine.Graphics.OpenGL/OpenGLRenderer.cs
+++ /dev/null
@@ -1,492 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Numerics;
-using System.Runtime.InteropServices;
-using Engine.Core;
-using Engine.Core.Components;
-using Engine.Graphics;
-using Flecs.NET.Core;
-using Silk.NET.OpenGL;
-using EngineMaterial = Engine.Core.Components.Material;
-using EngineMesh = Engine.Core.Components.Mesh;
-using EngineTransform = Engine.Core.Components.Transform;
-
-namespace Engine.Graphics.OpenGL;
-
-public sealed class OpenGLRenderer : IRenderer
-{
- private readonly GL _gl;
- private readonly uint _program;
- private readonly uint _shadowProgram;
- private readonly uint _shadowFbo;
- private readonly uint _shadowTexture;
- private const int ShadowMapSize = 2048;
-
- private readonly Dictionary _meshCache = new();
-
- private readonly int _uMVP, _uModel, _uViewPos, _uMaterialColor, _uRoughness, _uMetallic;
- private readonly int _uAmbient, _uLightCount, _uLightDirs, _uLightIntensities, _uLightColors;
- private readonly int _uLightPositions, _uLightTypes, _uLightRanges;
- private readonly int _uLightViewProj, _uShadowMap, _uUseTexture;
-
- private readonly float[] _lightDirs = new float[12];
- private readonly float[] _lightPositions = new float[12];
- private readonly float[] _lightIntensities = new float[4];
- private readonly float[] _lightColors = new float[12];
- private readonly int[] _lightTypes = new int[4];
- private readonly float[] _lightRanges = new float[4];
- private int _lightCount;
-
- private int _screenWidth = 1280;
- private int _screenHeight = 720;
- private bool _disposed;
- private readonly float[] _matrixBuffer = new float[16];
-
- public OpenGLRenderer(GL gl)
- {
- _gl = gl;
- _gl.Enable(GLEnum.DepthTest);
-
- _program = CreateProgram(VertexShaderSource, FragmentShaderSource);
- _shadowProgram = CreateProgram(ShadowVertexSource, ShadowFragmentSource);
-
- _uMVP = _gl.GetUniformLocation(_program, "mvp");
- _uModel = _gl.GetUniformLocation(_program, "model");
- _uViewPos = _gl.GetUniformLocation(_program, "viewPos");
- _uMaterialColor = _gl.GetUniformLocation(_program, "materialColor");
- _uRoughness = _gl.GetUniformLocation(_program, "roughness");
- _uMetallic = _gl.GetUniformLocation(_program, "metallic");
- _uAmbient = _gl.GetUniformLocation(_program, "ambientColor");
- _uLightCount = _gl.GetUniformLocation(_program, "lightCount");
- _uLightDirs = _gl.GetUniformLocation(_program, "lightDirs");
- _uLightIntensities = _gl.GetUniformLocation(_program, "lightIntensities");
- _uLightColors = _gl.GetUniformLocation(_program, "lightColors");
- _uLightPositions = _gl.GetUniformLocation(_program, "lightPositions");
- _uLightTypes = _gl.GetUniformLocation(_program, "lightTypes");
- _uLightRanges = _gl.GetUniformLocation(_program, "lightRanges");
- _uLightViewProj = _gl.GetUniformLocation(_program, "lightViewProj");
- _uShadowMap = _gl.GetUniformLocation(_program, "shadowMap");
- _uUseTexture = _gl.GetUniformLocation(_program, "useTexture");
-
- // Shadow FBO with depth texture
- _shadowFbo = _gl.GenFramebuffer();
- _gl.BindFramebuffer(GLEnum.Framebuffer, _shadowFbo);
- _shadowTexture = _gl.GenTexture();
- _gl.BindTexture(GLEnum.Texture2D, _shadowTexture);
- unsafe
- {
- _gl.TexImage2D(GLEnum.Texture2D, 0, (int)GLEnum.DepthComponent, (uint)ShadowMapSize, (uint)ShadowMapSize, 0, GLEnum.DepthComponent, GLEnum.Float, null);
- }
- _gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMinFilter, (int)GLEnum.Nearest);
- _gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureMagFilter, (int)GLEnum.Nearest);
- _gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapS, (int)GLEnum.ClampToEdge);
- _gl.TexParameter(GLEnum.Texture2D, GLEnum.TextureWrapT, (int)GLEnum.ClampToEdge);
- _gl.FramebufferTexture2D(GLEnum.Framebuffer, GLEnum.DepthAttachment, GLEnum.Texture2D, _shadowTexture, 0);
- _gl.DrawBuffer(DrawBufferMode.None);
- _gl.ReadBuffer(ReadBufferMode.None);
- _gl.BindFramebuffer(GLEnum.Framebuffer, 0);
- }
-
- public void RequestScreenshot(string outputPath) { }
- public bool IsScreenshotRequested => false;
- public IScreenshotProvider ScreenshotProvider => new OpenGLScreenshotProvider();
-
- public void SetScreenSize(int w, int h) { _screenWidth = w; _screenHeight = h; }
-
- public void RenderWorld(World world)
- {
- var camera = GetCamera(world);
- CollectLights(world);
-
- var hasDirLight = _lightCount > 0 && _lightTypes[0] == (int)LightType.Directional;
- Matrix4x4 lightViewProj = Matrix4x4.Identity;
-
- // === PASS 1: Shadow map ===
- if (hasDirLight)
- {
- var lightDir = new Vector3(_lightDirs[0], _lightDirs[1], _lightDirs[2]);
- var sceneCenter = new Vector3(0, 0.5f, 0);
- var lightPos = sceneCenter - lightDir * 30f;
- var up = MathF.Abs(Vector3.Dot(lightDir, Vector3.UnitY)) > 0.99f ? Vector3.UnitZ : Vector3.UnitY;
-
- lightViewProj = Matrix4x4.CreateOrthographicOffCenter(-15, 15, -15, 15, 1, 80)
- * Matrix4x4.CreateLookAt(lightPos, sceneCenter, up);
-
- _gl.Viewport(0, 0, (uint)ShadowMapSize, (uint)ShadowMapSize);
- _gl.BindFramebuffer(GLEnum.Framebuffer, _shadowFbo);
- _gl.Clear((uint)GLEnum.DepthBufferBit);
- _gl.UseProgram(_shadowProgram);
- _gl.CullFace(GLEnum.Front);
-
- var shadowMvpLoc = _gl.GetUniformLocation(_shadowProgram, "mvp");
-
- world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) =>
- {
- if (e.Name() == "Grid" || e.Name() == "Floor") return;
- var glMesh = GetOrUploadMesh(e, mesh);
- var model = transform.GetMatrix();
- var mvp = lightViewProj * model;
- CopyMatrixToBuffer(mvp);
- unsafe
- {
- fixed (float* p = _matrixBuffer)
- _gl.UniformMatrix4(shadowMvpLoc, 1, false, p);
- }
- DrawMeshImmediate(glMesh);
- });
-
- _gl.CullFace(GLEnum.Back);
- _gl.BindFramebuffer(GLEnum.Framebuffer, 0);
- }
-
- // === PASS 2: Main render ===
- _gl.Viewport(0, 0, (uint)_screenWidth, (uint)_screenHeight);
- _gl.Enable(GLEnum.DepthTest);
- _gl.Disable(GLEnum.CullFace);
- _gl.ClearColor(0.098f, 0.118f, 0.157f, 1);
- _gl.Clear((uint)(GLEnum.ColorBufferBit | GLEnum.DepthBufferBit));
- _gl.UseProgram(_program);
-
- var view = Matrix4x4.CreateLookAt(camera.Position, camera.Target, camera.Up);
- var proj = Matrix4x4.CreatePerspectiveFieldOfView(camera.FieldOfView, camera.AspectRatio, camera.NearPlane, camera.FarPlane);
-
- // Frame uniforms
- _gl.Uniform3(_uViewPos, camera.Position.X, camera.Position.Y, camera.Position.Z);
- _gl.Uniform3(_uAmbient, 0.35f, 0.35f, 0.4f);
- _gl.Uniform1(_uLightCount, _lightCount);
- unsafe
- {
- fixed (float* p = _lightDirs) _gl.Uniform3(_uLightDirs, 4, p);
- fixed (float* p = _lightIntensities) _gl.Uniform1(_uLightIntensities, 4, p);
- fixed (float* p = _lightColors) _gl.Uniform3(_uLightColors, 4, p);
- fixed (float* p = _lightPositions) _gl.Uniform3(_uLightPositions, 4, p);
- fixed (int* p = _lightTypes) _gl.Uniform1(_uLightTypes, 4, p);
- fixed (float* p = _lightRanges) _gl.Uniform1(_uLightRanges, 4, p);
- }
-
- if (hasDirLight)
- {
- CopyMatrixToBuffer(lightViewProj);
- unsafe
- {
- fixed (float* p = _matrixBuffer)
- _gl.UniformMatrix4(_uLightViewProj, 1, false, p);
- }
- _gl.ActiveTexture(GLEnum.Texture1);
- _gl.BindTexture(GLEnum.Texture2D, _shadowTexture);
- _gl.Uniform1(_uShadowMap, 1);
- _gl.ActiveTexture(GLEnum.Texture0);
- }
-
- world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) =>
- {
- if (e.Name() == "Grid") return;
- var material = e.Has() ? e.Get() : EngineMaterial.Default;
- var glMesh = GetOrUploadMesh(e, mesh);
- var model = transform.GetMatrix();
- var mvp = proj * view * model;
-
- CopyMatrixToBuffer(mvp);
- unsafe { fixed (float* pmvp = _matrixBuffer) _gl.UniformMatrix4(_uMVP, 1, false, pmvp); }
- CopyMatrixToBuffer(model);
- unsafe { fixed (float* pmodel = _matrixBuffer) _gl.UniformMatrix4(_uModel, 1, false, pmodel); }
- _gl.Uniform4(_uMaterialColor, material.Albedo.X, material.Albedo.Y, material.Albedo.Z, 1.0f);
- _gl.Uniform1(_uRoughness, material.Roughness);
- _gl.Uniform1(_uMetallic, material.Metallic);
- _gl.Uniform1(_uUseTexture, 0);
-
- DrawMeshImmediate(glMesh);
- });
- }
-
- private void DrawMeshImmediate(GLMesh mesh)
- {
- _gl.BindVertexArray(mesh.Vao);
- unsafe { _gl.DrawElements(GLEnum.Triangles, (uint)mesh.IndexCount, GLEnum.UnsignedInt, null); }
- _gl.BindVertexArray(0);
- }
-
- private GLMesh GetOrUploadMesh(Entity e, EngineMesh mesh)
- {
- if (_meshCache.TryGetValue(e, out var existing))
- return existing;
-
- var vao = _gl.GenVertexArray();
- _gl.BindVertexArray(vao);
-
- // Position (location 0)
- var positions = new float[mesh.Vertices.Length * 3];
- for (var i = 0; i < mesh.Vertices.Length; i++)
- {
- positions[i * 3] = mesh.Vertices[i].Position.X;
- positions[i * 3 + 1] = mesh.Vertices[i].Position.Y;
- positions[i * 3 + 2] = mesh.Vertices[i].Position.Z;
- }
- var posVbo = _gl.GenBuffer();
- _gl.BindBuffer(GLEnum.ArrayBuffer, posVbo);
- UploadBufferData(positions);
- _gl.EnableVertexAttribArray(0);
- unsafe { _gl.VertexAttribPointer(0, 3, GLEnum.Float, false, 3 * sizeof(float), (void*)0); }
-
- // Normal (location 1)
- var normals = new float[mesh.Vertices.Length * 3];
- for (var i = 0; i < mesh.Vertices.Length; i++)
- {
- normals[i * 3] = mesh.Vertices[i].Normal.X;
- normals[i * 3 + 1] = mesh.Vertices[i].Normal.Y;
- normals[i * 3 + 2] = mesh.Vertices[i].Normal.Z;
- }
- var nrmVbo = _gl.GenBuffer();
- _gl.BindBuffer(GLEnum.ArrayBuffer, nrmVbo);
- UploadBufferData(normals);
- _gl.EnableVertexAttribArray(1);
- unsafe { _gl.VertexAttribPointer(1, 3, GLEnum.Float, false, 3 * sizeof(float), (void*)0); }
-
- // Color (location 2)
- var colors = new float[mesh.Vertices.Length * 4];
- for (var i = 0; i < mesh.Vertices.Length; i++)
- {
- colors[i * 4] = mesh.Vertices[i].Color.X;
- colors[i * 4 + 1] = mesh.Vertices[i].Color.Y;
- colors[i * 4 + 2] = mesh.Vertices[i].Color.Z;
- colors[i * 4 + 3] = 1.0f;
- }
- var colVbo = _gl.GenBuffer();
- _gl.BindBuffer(GLEnum.ArrayBuffer, colVbo);
- UploadBufferData(colors);
- _gl.EnableVertexAttribArray(2);
- unsafe { _gl.VertexAttribPointer(2, 4, GLEnum.Float, false, 4 * sizeof(float), (void*)0); }
-
- // Indices — use pinned GCHandle
- var ebo = _gl.GenBuffer();
- _gl.BindBuffer(GLEnum.ElementArrayBuffer, ebo);
- var indexHandle = System.Runtime.InteropServices.GCHandle.Alloc(mesh.Indices, System.Runtime.InteropServices.GCHandleType.Pinned);
- unsafe
- {
- _gl.BufferData(GLEnum.ElementArrayBuffer, (nuint)(mesh.Indices.Length * sizeof(uint)), (void*)indexHandle.AddrOfPinnedObject(), GLEnum.StaticDraw);
- }
- indexHandle.Free();
-
- _gl.BindVertexArray(0);
-
- var glMesh = new GLMesh(vao, mesh.Indices.Length);
- _meshCache[e] = glMesh;
- return glMesh;
- }
-
- private void CollectLights(World world)
- {
- var count = 0;
- world.Each((Entity e, ref Light light) =>
- {
- if (count >= 4) return;
- _lightDirs[count * 3] = light.Direction.X;
- _lightDirs[count * 3 + 1] = light.Direction.Y;
- _lightDirs[count * 3 + 2] = light.Direction.Z;
- _lightPositions[count * 3] = light.Position.X;
- _lightPositions[count * 3 + 1] = light.Position.Y;
- _lightPositions[count * 3 + 2] = light.Position.Z;
- _lightIntensities[count] = light.Intensity;
- _lightColors[count * 3] = light.Color.X;
- _lightColors[count * 3 + 1] = light.Color.Y;
- _lightColors[count * 3 + 2] = light.Color.Z;
- _lightTypes[count] = (int)light.Type;
- _lightRanges[count] = light.Range;
- count++;
- });
-
- if (count == 0)
- {
- _lightDirs[0] = 0.5f; _lightDirs[1] = -1; _lightDirs[2] = -0.5f;
- _lightIntensities[0] = 1; _lightColors[0] = 1; _lightColors[1] = 0.95f; _lightColors[2] = 0.8f;
- _lightTypes[0] = (int)LightType.Directional; _lightRanges[0] = 20;
- count = 1;
- }
- for (var i = count; i < 4; i++) { _lightIntensities[i] = 0; _lightTypes[i] = 0; }
- _lightCount = count;
- }
-
- private Camera GetCamera(World world)
- {
- var cam = new Camera(new Vector3(0, 0.75f, -30), new Vector3(0, 0.5f, 0), Vector3.UnitY, MathF.PI / 12, 16f / 9f, 0.1f, 100f);
- world.Each((Entity e, ref Camera c) => cam = c);
- return cam;
- }
-
- private uint CreateProgram(string vs, string fs)
- {
- var vertex = _gl.CreateShader(GLEnum.VertexShader);
- _gl.ShaderSource(vertex, vs);
- _gl.CompileShader(vertex);
- _gl.GetShader(vertex, ShaderParameterName.CompileStatus, out int vStatus);
- if (vStatus == 0) throw new Exception($"VS: {_gl.GetShaderInfoLog(vertex)}");
-
- var fragment = _gl.CreateShader(GLEnum.FragmentShader);
- _gl.ShaderSource(fragment, fs);
- _gl.CompileShader(fragment);
- _gl.GetShader(fragment, ShaderParameterName.CompileStatus, out int fStatus);
- if (fStatus == 0) throw new Exception($"FS: {_gl.GetShaderInfoLog(fragment)}");
-
- var program = _gl.CreateProgram();
- _gl.AttachShader(program, vertex);
- _gl.AttachShader(program, fragment);
- _gl.LinkProgram(program);
- _gl.GetProgram(program, ProgramPropertyARB.LinkStatus, out int lStatus);
- if (lStatus == 0) throw new Exception($"Link: {_gl.GetProgramInfoLog(program)}");
-
- _gl.DeleteShader(vertex);
- _gl.DeleteShader(fragment);
- return program;
- }
-
- private unsafe void UploadBufferData(float[] data)
- {
- var handle = System.Runtime.InteropServices.GCHandle.Alloc(data, System.Runtime.InteropServices.GCHandleType.Pinned);
- try
- {
- _gl.BufferData(GLEnum.ArrayBuffer, (nuint)(data.Length * sizeof(float)), (void*)handle.AddrOfPinnedObject(), GLEnum.StaticDraw);
- }
- finally
- {
- handle.Free();
- }
- }
-
- private void CopyMatrixToBuffer(Matrix4x4 m)
- {
- // OpenGL expects column-major; System.Numerics is row-major, so transpose during copy
- _matrixBuffer[0] = m.M11; _matrixBuffer[1] = m.M21; _matrixBuffer[2] = m.M31; _matrixBuffer[3] = m.M41;
- _matrixBuffer[4] = m.M12; _matrixBuffer[5] = m.M22; _matrixBuffer[6] = m.M32; _matrixBuffer[7] = m.M42;
- _matrixBuffer[8] = m.M13; _matrixBuffer[9] = m.M23; _matrixBuffer[10] = m.M33; _matrixBuffer[11] = m.M43;
- _matrixBuffer[12] = m.M14; _matrixBuffer[13] = m.M24; _matrixBuffer[14] = m.M34; _matrixBuffer[15] = m.M44;
- }
-
- public void Dispose()
- {
- if (_disposed) return;
- _disposed = true;
- foreach (var m in _meshCache.Values) _gl.DeleteVertexArray(m.Vao);
- _meshCache.Clear();
- _gl.DeleteProgram(_program);
- _gl.DeleteProgram(_shadowProgram);
- _gl.DeleteFramebuffer(_shadowFbo);
- _gl.DeleteTexture(_shadowTexture);
- }
-
- private const string ShadowVertexSource = @"#version 330 core
-layout(location=0) in vec3 aPos;
-uniform mat4 mvp;
-void main(){gl_Position=mvp*vec4(aPos,1.0);}";
- private const string ShadowFragmentSource = @"#version 330 core
-void main(){}";
-
- private const string VertexShaderSource = @"#version 330 core
-layout(location=0) in vec3 aPos;
-layout(location=1) in vec3 aNormal;
-layout(location=2) in vec4 aColor;
-uniform mat4 mvp;
-uniform mat4 model;
-out vec3 vNormal;
-out vec3 vWorldPos;
-out vec4 vColor;
-void main(){
- vec4 wp=model*vec4(aPos,1.0);
- vWorldPos=wp.xyz;
- vNormal=mat3(transpose(inverse(model)))*aNormal;
- vColor=aColor;
- gl_Position=mvp*vec4(aPos,1.0);
-}";
-
- private const string FragmentShaderSource = @"#version 330 core
-in vec3 vNormal;
-in vec3 vWorldPos;
-in vec4 vColor;
-out vec4 finalColor;
-uniform vec4 materialColor;
-uniform float roughness;
-uniform float metallic;
-uniform vec3 viewPos;
-uniform vec3 ambientColor;
-uniform int lightCount;
-uniform vec3 lightDirs[4];
-uniform vec3 lightPositions[4];
-uniform float lightIntensities[4];
-uniform vec3 lightColors[4];
-uniform int lightTypes[4];
-uniform float lightRanges[4];
-uniform mat4 lightViewProj;
-uniform sampler2D shadowMap;
-uniform int useTexture;
-
-vec3 ACESFilm(vec3 x){
- const float a=2.51,b=0.03,c=2.43,d=0.59,e=0.14;
- return clamp((x*(a*x+b))/(x*(c*x+d)+e),0.0,1.0);
-}
-float Attenuation(float dist,float range){
- float r=max(range,0.001),d=max(dist,0.001);
- float x=d/r,x2=x*x,x4=x2*x2;
- return clamp(1.0/(1.0+25.0*x4),0.0,1.0)*smoothstep(1.0,0.0,x);
-}
-float CalculateShadow(vec3 worldPos){
- vec4 lp=lightViewProj*vec4(worldPos,1.0);
- vec3 ndc=lp.xyz/lp.w;
- vec3 uvw=ndc*0.5+0.5;
- if(uvw.x<0.0||uvw.x>1.0||uvw.y<0.0||uvw.y>1.0||uvw.z>1.0) return 1.0;
- float bias=0.005;
- vec2 ts=vec2(1.0/2048.0);
- float s=0.0;
- for(int x=-1;x<=1;x++){
- for(int y=-1;y<=1;y++){
- float d=texture(shadowMap,uvw.xy+vec2(x,y)*ts).r;
- s+=(uvw.z-bias>d)?0.3:1.0;
- }
- }
- return s/9.0;
-}
-void main(){
- vec3 normal=normalize(vNormal);
- vec3 albedo=pow(vColor.rgb*materialColor.rgb,vec3(2.2));
- vec3 viewDir=normalize(viewPos-vWorldPos);
- float rough=clamp(roughness,0.05,1.0);
- float metal=clamp(metallic,0.0,1.0);
- vec3 skyColor=ambientColor;
- vec3 groundColor=ambientColor*0.2;
- float hemisphere=0.5+0.5*normal.y;
- vec3 result=albedo*mix(groundColor,skyColor,hemisphere)*0.4;
- float shadow=1.0;
- if(lightCount>0&&lightTypes[0]==0) shadow=CalculateShadow(vWorldPos);
- vec3 F0=mix(vec3(0.04),albedo,metal);
- float shininess=mix(8.0,256.0,1.0-rough);
- for(int i=0;i CaptureAsync(string outputPath) => Task.FromResult(Array.Empty());
- }
-}
diff --git a/src/Engine.Graphics.OpenGL/OpenGLWindow.cs b/src/Engine.Graphics.OpenGL/OpenGLWindow.cs
deleted file mode 100644
index 920803c..0000000
--- a/src/Engine.Graphics.OpenGL/OpenGLWindow.cs
+++ /dev/null
@@ -1,60 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Engine.Core;
-using Silk.NET.Input;
-using Silk.NET.OpenGL;
-using Silk.NET.Windowing;
-using SilkWindow = Silk.NET.Windowing.IWindow;
-
-namespace Engine.Graphics.OpenGL;
-
-public sealed class OpenGLWindow : Engine.Core.IWindow, IDisposable
-{
- private readonly SilkWindow _silkWindow;
- private readonly OpenGLInputState _input = new();
- private bool _shouldClose;
- private bool _disposed;
-
- public int Width => _silkWindow.Size.X;
- public int Height => _silkWindow.Size.Y;
- public bool ShouldClose => _shouldClose || _silkWindow.IsClosing;
- IInputState Engine.Core.IWindow.Input => _input;
- public nint Handle => 0;
- public SilkWindow SilkView => _silkWindow;
-
- public OpenGLWindow(string title, int width, int height)
- {
- var options = WindowOptions.Default;
- options.Title = title;
- options.Size = new Silk.NET.Maths.Vector2D(width, height);
- options.VSync = false;
- options.API = new GraphicsAPI(ContextAPI.OpenGL, ContextProfile.Core, ContextFlags.Default, new APIVersion(3, 3));
- options.WindowState = WindowState.Normal;
- options.ShouldSwapAutomatically = false;
-
- _silkWindow = Window.Create(options);
- _silkWindow.Closing += () => _shouldClose = true;
- // Initialize without Run() — enables non-blocking event loop
- _silkWindow.Initialize();
- }
-
- public void OnLoad()
- {
- var inputContext = _silkWindow.CreateInput();
- _input.Initialize(inputContext);
- }
-
- public GL CreateGL() => _silkWindow.CreateOpenGL();
-
- public void PumpEvents() => _silkWindow.DoEvents();
- public void Close() => _shouldClose = true;
- public string[] GetRequiredVulkanExtensions() => Array.Empty();
-
- public void Dispose()
- {
- if (_disposed) return;
- _disposed = true;
- _silkWindow.Close();
- _silkWindow.Dispose();
- }
-}
diff --git a/src/Engine.Graphics.OpenGL/Engine.Graphics.OpenGL.csproj b/src/Engine.Graphics.OpenTK/Engine.Graphics.OpenTK.csproj
similarity index 72%
rename from src/Engine.Graphics.OpenGL/Engine.Graphics.OpenGL.csproj
rename to src/Engine.Graphics.OpenTK/Engine.Graphics.OpenTK.csproj
index 3a2cf28..8439a8d 100644
--- a/src/Engine.Graphics.OpenGL/Engine.Graphics.OpenGL.csproj
+++ b/src/Engine.Graphics.OpenTK/Engine.Graphics.OpenTK.csproj
@@ -8,12 +8,9 @@
-
-
-
+
-
diff --git a/src/Engine.Graphics.OpenTK/OpenTKBackendRegistrar.cs b/src/Engine.Graphics.OpenTK/OpenTKBackendRegistrar.cs
new file mode 100644
index 0000000..052bf07
--- /dev/null
+++ b/src/Engine.Graphics.OpenTK/OpenTKBackendRegistrar.cs
@@ -0,0 +1,16 @@
+using Engine.Graphics;
+
+namespace Engine.Graphics.OpenTK;
+
+///
+/// Triggers registration of the OpenTK backend with the HAL factory.
+///
+public static class OpenTKBackendRegistrar
+{
+ static OpenTKBackendRegistrar()
+ {
+ RenderBackendFactory.Register("opentk", (width, height, _) => new OpenTKRenderContext(width, height));
+ }
+
+ public static void EnsureRegistered() { }
+}
diff --git a/src/Engine.Graphics.OpenTK/OpenTKInputState.cs b/src/Engine.Graphics.OpenTK/OpenTKInputState.cs
new file mode 100644
index 0000000..abf486c
--- /dev/null
+++ b/src/Engine.Graphics.OpenTK/OpenTKInputState.cs
@@ -0,0 +1,104 @@
+using System.Collections.Generic;
+using Engine.Core;
+using OpenTK.Windowing.GraphicsLibraryFramework;
+using EngineKey = Engine.Core.Key;
+using OpenTKKey = OpenTK.Windowing.GraphicsLibraryFramework.Keys;
+
+namespace Engine.Graphics.OpenTK;
+
+///
+/// OpenTK input-backed implementation of IInputState.
+///
+public sealed class OpenTKInputState : IInputState
+{
+ private readonly HashSet _keysDown = new();
+ private readonly HashSet _keysPressed = new();
+ private readonly HashSet _keysReleased = new();
+ private readonly HashSet _prevDown = new();
+
+ public int MouseX { get; private set; }
+ public int MouseY { get; private set; }
+ public bool MouseLeft { get; private set; }
+ public bool MouseRight { get; private set; }
+ public bool MouseMiddle { get; private set; }
+ public float MouseWheelDelta { get; private set; }
+
+ public void Update(KeyboardState kb, MouseState ms)
+ {
+ MouseX = (int)ms.Position.X;
+ MouseY = (int)ms.Position.Y;
+ MouseLeft = ms.IsButtonDown(MouseButton.Left);
+ MouseRight = ms.IsButtonDown(MouseButton.Right);
+ MouseMiddle = ms.IsButtonDown(MouseButton.Middle);
+ MouseWheelDelta = (float)ms.ScrollDelta.Y;
+
+ // Compute pressed/released edges
+ _keysPressed.Clear();
+ _keysReleased.Clear();
+
+ var currentKeys = new HashSet();
+ foreach (var openTkKey in AllKeys)
+ {
+ if (kb.IsKeyDown(openTkKey))
+ {
+ var k = MapKey(openTkKey);
+ if (k == EngineKey.Unknown) continue;
+ currentKeys.Add(k);
+ if (!_prevDown.Contains(k))
+ _keysPressed.Add(k);
+ }
+ }
+
+ foreach (var k in _prevDown)
+ if (!currentKeys.Contains(k))
+ _keysReleased.Add(k);
+
+ _keysDown.Clear();
+ _keysDown.UnionWith(currentKeys);
+ _prevDown.Clear();
+ _prevDown.UnionWith(currentKeys);
+ }
+
+ public void BeginFrame()
+ {
+ _keysPressed.Clear();
+ _keysReleased.Clear();
+ MouseWheelDelta = 0;
+ }
+
+ public bool IsKeyDown(EngineKey key) => _keysDown.Contains(key);
+ public bool IsKeyPressed(EngineKey key) => _keysPressed.Contains(key);
+ public bool IsKeyReleased(EngineKey key) => _keysReleased.Contains(key);
+
+ private static readonly OpenTKKey[] AllKeys =
+ {
+ OpenTKKey.W, OpenTKKey.A, OpenTKKey.S, OpenTKKey.D,
+ OpenTKKey.Q, OpenTKKey.E, OpenTKKey.F,
+ OpenTKKey.LeftShift,
+ OpenTKKey.Space, OpenTKKey.Escape, OpenTKKey.Enter,
+ OpenTKKey.Tab, OpenTKKey.Backspace,
+ OpenTKKey.Up, OpenTKKey.Down, OpenTKKey.Left, OpenTKKey.Right,
+ };
+
+ private static EngineKey MapKey(OpenTKKey key) => key switch
+ {
+ OpenTKKey.W => EngineKey.W,
+ OpenTKKey.A => EngineKey.A,
+ OpenTKKey.S => EngineKey.S,
+ OpenTKKey.D => EngineKey.D,
+ OpenTKKey.Q => EngineKey.Q,
+ OpenTKKey.E => EngineKey.E,
+ OpenTKKey.F => EngineKey.F,
+ OpenTKKey.LeftShift => EngineKey.LeftShift,
+ OpenTKKey.Space => EngineKey.Space,
+ OpenTKKey.Escape => EngineKey.Escape,
+ OpenTKKey.Enter => EngineKey.Enter,
+ OpenTKKey.Tab => EngineKey.Tab,
+ OpenTKKey.Backspace => EngineKey.Backspace,
+ OpenTKKey.Up => EngineKey.Up,
+ OpenTKKey.Down => EngineKey.Down,
+ OpenTKKey.Left => EngineKey.Left,
+ OpenTKKey.Right => EngineKey.Right,
+ _ => EngineKey.Unknown,
+ };
+}
diff --git a/src/Engine.Graphics.OpenTK/OpenTKRenderContext.cs b/src/Engine.Graphics.OpenTK/OpenTKRenderContext.cs
new file mode 100644
index 0000000..0ea43a9
--- /dev/null
+++ b/src/Engine.Graphics.OpenTK/OpenTKRenderContext.cs
@@ -0,0 +1,47 @@
+using Engine.Core;
+using Engine.Graphics;
+
+namespace Engine.Graphics.OpenTK;
+
+///
+/// OpenTK render context — owns the GameWindow and creates the renderer.
+///
+public sealed class OpenTKRenderContext : IRenderContext
+{
+ private readonly OpenTKWindow _window;
+ private OpenTKRenderer? _renderer;
+ private bool _disposed;
+
+ public IWindow Window => _window;
+
+ public OpenTKRenderContext(int width, int height, bool enableValidation = false)
+ {
+ _window = new OpenTKWindow("Cortex Engine (OpenTK)", width, height);
+ // GameWindow creates OpenGL context in constructor — MakeCurrent is called automatically
+ _window.MakeCurrent();
+ }
+
+ public IRenderer CreateRenderer()
+ {
+ _renderer = new OpenTKRenderer();
+ return _renderer;
+ }
+
+ public void Resize(int width, int height)
+ {
+ _renderer?.SetScreenSize(width, height);
+ }
+
+ ///
+ /// Swap buffers — called after RenderWorld in the main loop.
+ ///
+ public void SwapBuffers() => _window.SwapBuffers();
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ _renderer?.Dispose();
+ _window.Dispose();
+ }
+}
diff --git a/src/Engine.Graphics.OpenTK/OpenTKRenderer.cs b/src/Engine.Graphics.OpenTK/OpenTKRenderer.cs
new file mode 100644
index 0000000..ec833aa
--- /dev/null
+++ b/src/Engine.Graphics.OpenTK/OpenTKRenderer.cs
@@ -0,0 +1,438 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using Engine.Core;
+using Engine.Core.Components;
+using Engine.Graphics;
+using Flecs.NET.Core;
+using OpenTK.Graphics.OpenGL4;
+using OTKMatrix = OpenTK.Mathematics.Matrix4;
+using OTKVector3 = OpenTK.Mathematics.Vector3;
+using OTKVector4 = OpenTK.Mathematics.Vector4;
+using EngineMaterial = Engine.Core.Components.Material;
+using EngineMesh = Engine.Core.Components.Mesh;
+using EngineTransform = Engine.Core.Components.Transform;
+
+namespace Engine.Graphics.OpenTK;
+
+///
+/// OpenTK OpenGL renderer — full control over OpenGL state for shadow mapping.
+///
+public sealed class OpenTKRenderer : IRenderer
+{
+ private const int ShadowMapSize = 2048;
+
+ private readonly int _program;
+ private readonly int _shadowProgram;
+ private readonly int _shadowFbo;
+ private readonly int _shadowTexture;
+ private readonly Dictionary _meshCache = new();
+
+ // Uniform locations
+ private readonly int _uMVP, _uModel, _uViewPos, _uMaterialColor, _uRoughness, _uMetallic;
+ private readonly int _uAmbient, _uLightCount, _uLightDirs, _uLightIntensities, _uLightColors;
+ private readonly int _uLightPositions, _uLightTypes, _uLightRanges;
+ private readonly int _uLightViewProj, _uShadowMap, _uUseTexture;
+
+ // Light data
+ private readonly float[] _lightDirs = new float[12];
+ private readonly float[] _lightPositions = new float[12];
+ private readonly float[] _lightIntensities = new float[4];
+ private readonly float[] _lightColors = new float[12];
+ private readonly int[] _lightTypes = new int[4];
+ private readonly float[] _lightRanges = new float[4];
+ private int _lightCount;
+
+ private int _screenWidth = 1280;
+ private int _screenHeight = 720;
+ private bool _disposed;
+
+ public OpenTKRenderer()
+ {
+ GL.Enable(EnableCap.DepthTest);
+
+ _program = CreateProgram(VertexShaderSource, FragmentShaderSource);
+ _shadowProgram = CreateProgram(ShadowVertexSource, ShadowFragmentSource);
+
+ _uMVP = GL.GetUniformLocation(_program, "mvp");
+ _uModel = GL.GetUniformLocation(_program, "model");
+ _uViewPos = GL.GetUniformLocation(_program, "viewPos");
+ _uMaterialColor = GL.GetUniformLocation(_program, "materialColor");
+ _uRoughness = GL.GetUniformLocation(_program, "roughness");
+ _uMetallic = GL.GetUniformLocation(_program, "metallic");
+ _uAmbient = GL.GetUniformLocation(_program, "ambientColor");
+ _uLightCount = GL.GetUniformLocation(_program, "lightCount");
+ _uLightDirs = GL.GetUniformLocation(_program, "lightDirs");
+ _uLightIntensities = GL.GetUniformLocation(_program, "lightIntensities");
+ _uLightColors = GL.GetUniformLocation(_program, "lightColors");
+ _uLightPositions = GL.GetUniformLocation(_program, "lightPositions");
+ _uLightTypes = GL.GetUniformLocation(_program, "lightTypes");
+ _uLightRanges = GL.GetUniformLocation(_program, "lightRanges");
+ _uLightViewProj = GL.GetUniformLocation(_program, "lightViewProj");
+ _uShadowMap = GL.GetUniformLocation(_program, "shadowMap");
+ _uUseTexture = GL.GetUniformLocation(_program, "useTexture");
+
+ // Shadow FBO with depth texture
+ _shadowFbo = GL.GenFramebuffer();
+ GL.BindFramebuffer(FramebufferTarget.Framebuffer, _shadowFbo);
+ _shadowTexture = GL.GenTexture();
+ GL.BindTexture(TextureTarget.Texture2D, _shadowTexture);
+ GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.DepthComponent,
+ ShadowMapSize, ShadowMapSize, 0, PixelFormat.DepthComponent, PixelType.Float, IntPtr.Zero);
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
+ GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
+ GL.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthAttachment,
+ TextureTarget.Texture2D, _shadowTexture, 0);
+ GL.DrawBuffer(DrawBufferMode.None);
+ GL.ReadBuffer(ReadBufferMode.None);
+ GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
+ }
+
+ public void SetScreenSize(int w, int h) { _screenWidth = w; _screenHeight = h; }
+
+ public void RequestScreenshot(string outputPath) { }
+ public bool IsScreenshotRequested => false;
+ public IScreenshotProvider ScreenshotProvider => new OpenTKScreenshotProvider();
+
+ public void RenderWorld(World world)
+ {
+ var camera = GetCamera(world);
+ CollectLights(world);
+
+ var hasDirLight = _lightCount > 0 && _lightTypes[0] == (int)LightType.Directional;
+ OTKMatrix lightViewProj = OTKMatrix.Identity;
+
+ // === PASS 1: Shadow map ===
+ if (hasDirLight)
+ {
+ var lightDir = new System.Numerics.Vector3(_lightDirs[0], _lightDirs[1], _lightDirs[2]);
+ var sceneCenter = new System.Numerics.Vector3(0, 0.5f, 0);
+ var lightPos = sceneCenter - lightDir * 30f;
+ var up = MathF.Abs(System.Numerics.Vector3.Dot(lightDir, System.Numerics.Vector3.UnitY)) > 0.99f
+ ? System.Numerics.Vector3.UnitZ : System.Numerics.Vector3.UnitY;
+
+ lightViewProj = OTKMatrix.CreateOrthographicOffCenter(-15, 15, -15, 15, 1, 80)
+ * OTKMatrix.LookAt(ToOTK(lightPos), ToOTK(sceneCenter), ToOTK(up));
+
+ GL.Viewport(0, 0, ShadowMapSize, ShadowMapSize);
+ GL.BindFramebuffer(FramebufferTarget.Framebuffer, _shadowFbo);
+ GL.Clear(ClearBufferMask.DepthBufferBit);
+ GL.UseProgram(_shadowProgram);
+ GL.CullFace(CullFaceMode.Front);
+
+ var shadowMvpLoc = GL.GetUniformLocation(_shadowProgram, "mvp");
+
+ world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) =>
+ {
+ if (e.Name() == "Grid" || e.Name() == "Floor") return;
+ var glMesh = GetOrUploadMesh(e, mesh);
+ var model = ToOTK(transform.GetMatrix());
+ var mvp = lightViewProj * model;
+ GL.UniformMatrix4(shadowMvpLoc, false, ref mvp);
+ DrawMeshImmediate(glMesh);
+ });
+
+ GL.CullFace(CullFaceMode.Back);
+ GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0);
+ }
+
+ // === PASS 2: Main render ===
+ GL.Viewport(0, 0, _screenWidth, _screenHeight);
+ GL.Enable(EnableCap.DepthTest);
+ GL.Disable(EnableCap.CullFace);
+ GL.ClearColor(0.098f, 0.118f, 0.157f, 1);
+ GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
+ GL.UseProgram(_program);
+
+ var view = OTKMatrix.LookAt(ToOTK(camera.Position), ToOTK(camera.Target), ToOTK(camera.Up));
+ var proj = OTKMatrix.CreatePerspectiveFieldOfView(camera.FieldOfView, camera.AspectRatio, camera.NearPlane, camera.FarPlane);
+
+ // Frame uniforms
+ GL.Uniform3(_uViewPos, camera.Position.X, camera.Position.Y, camera.Position.Z);
+ GL.Uniform3(_uAmbient, 0.35f, 0.35f, 0.4f);
+ GL.Uniform1(_uLightCount, _lightCount);
+ GL.Uniform3(_uLightDirs, 4, _lightDirs);
+ GL.Uniform1(_uLightIntensities, 4, _lightIntensities);
+ GL.Uniform3(_uLightColors, 4, _lightColors);
+ GL.Uniform3(_uLightPositions, 4, _lightPositions);
+ GL.Uniform1(_uLightTypes, 4, _lightTypes);
+ GL.Uniform1(_uLightRanges, 4, _lightRanges);
+
+ if (hasDirLight)
+ {
+ GL.UniformMatrix4(_uLightViewProj, false, ref lightViewProj);
+ GL.ActiveTexture(TextureUnit.Texture1);
+ GL.BindTexture(TextureTarget.Texture2D, _shadowTexture);
+ GL.Uniform1(_uShadowMap, 1);
+ GL.ActiveTexture(TextureUnit.Texture0);
+ }
+
+ world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) =>
+ {
+ if (e.Name() == "Grid") return;
+ var material = e.Has() ? e.Get() : EngineMaterial.Default;
+ var glMesh = GetOrUploadMesh(e, mesh);
+ var model = ToOTK(transform.GetMatrix());
+ var mvp = proj * view * model;
+
+ GL.UniformMatrix4(_uMVP, false, ref mvp);
+ GL.UniformMatrix4(_uModel, false, ref model);
+ GL.Uniform4(_uMaterialColor, material.Albedo.X, material.Albedo.Y, material.Albedo.Z, 1.0f);
+ GL.Uniform1(_uRoughness, material.Roughness);
+ GL.Uniform1(_uMetallic, material.Metallic);
+ GL.Uniform1(_uUseTexture, 0);
+
+ DrawMeshImmediate(glMesh);
+ });
+ }
+
+ private void DrawMeshImmediate(GLMesh mesh)
+ {
+ GL.BindVertexArray(mesh.Vao);
+ GL.DrawElements(PrimitiveType.Triangles, mesh.IndexCount, DrawElementsType.UnsignedInt, 0);
+ GL.BindVertexArray(0);
+ }
+
+ private GLMesh GetOrUploadMesh(Entity e, EngineMesh mesh)
+ {
+ if (_meshCache.TryGetValue(e, out var existing))
+ return existing;
+
+ var vao = GL.GenVertexArray();
+ GL.BindVertexArray(vao);
+
+ // Position (location 0)
+ var positions = new float[mesh.Vertices.Length * 3];
+ for (var i = 0; i < mesh.Vertices.Length; i++)
+ {
+ positions[i * 3] = mesh.Vertices[i].Position.X;
+ positions[i * 3 + 1] = mesh.Vertices[i].Position.Y;
+ positions[i * 3 + 2] = mesh.Vertices[i].Position.Z;
+ }
+ var posVbo = GL.GenBuffer();
+ GL.BindBuffer(BufferTarget.ArrayBuffer, posVbo);
+ GL.BufferData(BufferTarget.ArrayBuffer, positions.Length * sizeof(float), positions, BufferUsageHint.StaticDraw);
+ GL.EnableVertexAttribArray(0);
+ GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
+
+ // Normal (location 1)
+ var normals = new float[mesh.Vertices.Length * 3];
+ for (var i = 0; i < mesh.Vertices.Length; i++)
+ {
+ normals[i * 3] = mesh.Vertices[i].Normal.X;
+ normals[i * 3 + 1] = mesh.Vertices[i].Normal.Y;
+ normals[i * 3 + 2] = mesh.Vertices[i].Normal.Z;
+ }
+ var nrmVbo = GL.GenBuffer();
+ GL.BindBuffer(BufferTarget.ArrayBuffer, nrmVbo);
+ GL.BufferData(BufferTarget.ArrayBuffer, normals.Length * sizeof(float), normals, BufferUsageHint.StaticDraw);
+ GL.EnableVertexAttribArray(1);
+ GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0);
+
+ // Color (location 2)
+ var colors = new float[mesh.Vertices.Length * 4];
+ for (var i = 0; i < mesh.Vertices.Length; i++)
+ {
+ colors[i * 4] = mesh.Vertices[i].Color.X;
+ colors[i * 4 + 1] = mesh.Vertices[i].Color.Y;
+ colors[i * 4 + 2] = mesh.Vertices[i].Color.Z;
+ colors[i * 4 + 3] = 1.0f;
+ }
+ var colVbo = GL.GenBuffer();
+ GL.BindBuffer(BufferTarget.ArrayBuffer, colVbo);
+ GL.BufferData(BufferTarget.ArrayBuffer, colors.Length * sizeof(float), colors, BufferUsageHint.StaticDraw);
+ GL.EnableVertexAttribArray(2);
+ GL.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, 4 * sizeof(float), 0);
+
+ // Indices
+ var ebo = GL.GenBuffer();
+ GL.BindBuffer(BufferTarget.ElementArrayBuffer, ebo);
+ GL.BufferData(BufferTarget.ElementArrayBuffer, mesh.Indices.Length * sizeof(uint), mesh.Indices, BufferUsageHint.StaticDraw);
+
+ GL.BindVertexArray(0);
+
+ var glMesh = new GLMesh(vao, mesh.Indices.Length);
+ _meshCache[e] = glMesh;
+ return glMesh;
+ }
+
+ private void CollectLights(World world)
+ {
+ var count = 0;
+ world.Each((Entity e, ref Light light) =>
+ {
+ if (count >= 4) return;
+ _lightDirs[count * 3] = light.Direction.X;
+ _lightDirs[count * 3 + 1] = light.Direction.Y;
+ _lightDirs[count * 3 + 2] = light.Direction.Z;
+ _lightPositions[count * 3] = light.Position.X;
+ _lightPositions[count * 3 + 1] = light.Position.Y;
+ _lightPositions[count * 3 + 2] = light.Position.Z;
+ _lightIntensities[count] = light.Intensity;
+ _lightColors[count * 3] = light.Color.X;
+ _lightColors[count * 3 + 1] = light.Color.Y;
+ _lightColors[count * 3 + 2] = light.Color.Z;
+ _lightTypes[count] = (int)light.Type;
+ _lightRanges[count] = light.Range;
+ count++;
+ });
+
+ if (count == 0)
+ {
+ _lightDirs[0] = 0.5f; _lightDirs[1] = -1; _lightDirs[2] = -0.5f;
+ _lightIntensities[0] = 1; _lightColors[0] = 1; _lightColors[1] = 0.95f; _lightColors[2] = 0.8f;
+ _lightTypes[0] = (int)LightType.Directional; _lightRanges[0] = 20;
+ count = 1;
+ }
+ for (var i = count; i < 4; i++) { _lightIntensities[i] = 0; _lightTypes[i] = 0; }
+ _lightCount = count;
+ }
+
+ private Camera GetCamera(World world)
+ {
+ var cam = new Camera(new System.Numerics.Vector3(0, 0.75f, -30),
+ new System.Numerics.Vector3(0, 0.5f, 0), System.Numerics.Vector3.UnitY,
+ MathF.PI / 12, 16f / 9f, 0.1f, 100f);
+ world.Each((Entity e, ref Camera c) => cam = c);
+ return cam;
+ }
+
+ private static OTKMatrix ToOTK(System.Numerics.Matrix4x4 m) => new(
+ m.M11, m.M12, m.M13, m.M14,
+ m.M21, m.M22, m.M23, m.M24,
+ m.M31, m.M32, m.M33, m.M34,
+ m.M41, m.M42, m.M43, m.M44);
+
+ private static OTKVector3 ToOTK(System.Numerics.Vector3 v) => new(v.X, v.Y, v.Z);
+
+ private static int CreateProgram(string vs, string fs)
+ {
+ var vertex = GL.CreateShader(ShaderType.VertexShader);
+ GL.ShaderSource(vertex, vs);
+ GL.CompileShader(vertex);
+ GL.GetShader(vertex, ShaderParameter.CompileStatus, out int vStatus);
+ if (vStatus == 0) throw new Exception($"VS: {GL.GetShaderInfoLog(vertex)}");
+
+ var fragment = GL.CreateShader(ShaderType.FragmentShader);
+ GL.ShaderSource(fragment, fs);
+ GL.CompileShader(fragment);
+ GL.GetShader(fragment, ShaderParameter.CompileStatus, out int fStatus);
+ if (fStatus == 0) throw new Exception($"FS: {GL.GetShaderInfoLog(fragment)}");
+
+ var program = GL.CreateProgram();
+ GL.AttachShader(program, vertex);
+ GL.AttachShader(program, fragment);
+ GL.LinkProgram(program);
+ GL.GetProgram(program, GetProgramParameterName.LinkStatus, out int lStatus);
+ if (lStatus == 0) throw new Exception($"Link: {GL.GetProgramInfoLog(program)}");
+
+ GL.DeleteShader(vertex);
+ GL.DeleteShader(fragment);
+ return program;
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+ foreach (var m in _meshCache.Values) GL.DeleteVertexArray(m.Vao);
+ _meshCache.Clear();
+ GL.DeleteProgram(_program);
+ GL.DeleteProgram(_shadowProgram);
+ GL.DeleteFramebuffer(_shadowFbo);
+ GL.DeleteTexture(_shadowTexture);
+ }
+
+ // Shaders (copied from Silk.NET OpenGL backend — backend-agnostic GLSL 330 core)
+ private const string ShadowVertexSource = @"#version 330 core
+layout(location=0) in vec3 aPos;
+uniform mat4 mvp;
+void main(){gl_Position=mvp*vec4(aPos,1.0);}";
+ private const string ShadowFragmentSource = @"#version 330 core
+void main(){}";
+
+ private const string VertexShaderSource = @"#version 330 core
+layout(location=0) in vec3 aPos;
+layout(location=1) in vec3 aNormal;
+layout(location=2) in vec4 aColor;
+uniform mat4 mvp;
+uniform mat4 model;
+out vec3 vNormal;
+out vec3 vWorldPos;
+out vec4 vColor;
+void main(){
+ vec4 wp=model*vec4(aPos,1.0);
+ vWorldPos=wp.xyz;
+ vNormal=mat3(transpose(inverse(model)))*aNormal;
+ vColor=aColor;
+ gl_Position=mvp*vec4(aPos,1.0);
+}";
+
+ private const string FragmentShaderSource = @"#version 330 core
+in vec3 vNormal;
+in vec3 vWorldPos;
+in vec4 vColor;
+out vec4 finalColor;
+uniform vec4 materialColor;
+uniform float roughness;
+uniform float metallic;
+uniform vec3 viewPos;
+uniform vec3 ambientColor;
+uniform int lightCount;
+uniform vec3 lightDirs[4];
+uniform vec3 lightPositions[4];
+uniform float lightIntensities[4];
+uniform vec3 lightColors[4];
+uniform int lightTypes[4];
+uniform float lightRanges[4];
+uniform mat4 lightViewProj;
+uniform sampler2D shadowMap;
+uniform int useTexture;
+vec3 ACESFilm(vec3 x){const float a=2.51,b=0.03,c=2.43,d=0.59,e=0.14;return clamp((x*(a*x+b))/(x*(c*x+d)+e),0.0,1.0);}
+float Attenuation(float dist,float range){float r=max(range,0.001),d=max(dist,0.001);float x=d/r,x2=x*x,x4=x2*x2;return clamp(1.0/(1.0+25.0*x4),0.0,1.0)*smoothstep(1.0,0.0,x);}
+float CalculateShadow(vec3 worldPos){vec4 lp=lightViewProj*vec4(worldPos,1.0);vec3 ndc=lp.xyz/lp.w;vec3 uvw=ndc*0.5+0.5;if(uvw.x<0.0||uvw.x>1.0||uvw.y<0.0||uvw.y>1.0||uvw.z>1.0)return 1.0;float bias=0.005;vec2 ts=vec2(1.0/2048.0);float s=0.0;for(int x=-1;x<=1;x++){for(int y=-1;y<=1;y++){float d=texture(shadowMap,uvw.xy+vec2(x,y)*ts).r;s+=(uvw.z-bias>d)?0.3:1.0;}}return s/9.0;}
+void main(){
+ vec3 normal=normalize(vNormal);
+ vec3 albedo=pow(vColor.rgb*materialColor.rgb,vec3(2.2));
+ vec3 viewDir=normalize(viewPos-vWorldPos);
+ float rough=clamp(roughness,0.05,1.0);
+ float metal=clamp(metallic,0.0,1.0);
+ vec3 skyColor=ambientColor;
+ vec3 groundColor=ambientColor*0.2;
+ float hemisphere=0.5+0.5*normal.y;
+ vec3 result=albedo*mix(groundColor,skyColor,hemisphere)*0.4;
+ float shadow=1.0;
+ if(lightCount>0&&lightTypes[0]==0)shadow=CalculateShadow(vWorldPos);
+ vec3 F0=mix(vec3(0.04),albedo,metal);
+ float shininess=mix(8.0,256.0,1.0-rough);
+ for(int i=0;i CaptureAsync(string outputPath) => Task.FromResult(Array.Empty());
+ }
+}
diff --git a/src/Engine.Graphics.OpenTK/OpenTKWindow.cs b/src/Engine.Graphics.OpenTK/OpenTKWindow.cs
new file mode 100644
index 0000000..c62241e
--- /dev/null
+++ b/src/Engine.Graphics.OpenTK/OpenTKWindow.cs
@@ -0,0 +1,52 @@
+using System;
+using Engine.Core;
+using OpenTK.Windowing.Common;
+using OpenTK.Windowing.Desktop;
+using OpenTK.Windowing.GraphicsLibraryFramework;
+
+namespace Engine.Graphics.OpenTK;
+
+///
+/// OpenTK GameWindow-backed implementation of IWindow.
+/// Uses ProcessEvents() (non-blocking) instead of Run() to integrate with our main loop.
+///
+public sealed class OpenTKWindow : GameWindow, IWindow, IDisposable
+{
+ private readonly OpenTKInputState _input = new();
+ private bool _shouldClose;
+
+ public new int Width => Size.X;
+ public new int Height => Size.Y;
+ public bool ShouldClose => _shouldClose || IsExiting;
+ IInputState IWindow.Input => _input;
+ public nint Handle => 0;
+
+ public OpenTKWindow(string title, int width, int height)
+ : base(GameWindowSettings.Default, new NativeWindowSettings
+ {
+ Title = title,
+ Size = (width, height),
+ APIVersion = new Version(3, 3),
+ Profile = ContextProfile.Core,
+ Flags = ContextFlags.ForwardCompatible,
+ Vsync = VSyncMode.Off,
+ })
+ {
+ }
+
+ public void PumpEvents()
+ {
+ ProcessEvents(0);
+ _input.Update(KeyboardState, MouseState);
+ }
+
+ void IWindow.Close() => _shouldClose = true;
+
+ public string[] GetRequiredVulkanExtensions() => Array.Empty();
+
+ public new void Dispose()
+ {
+ Close();
+ base.Dispose();
+ }
+}