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