diff --git a/CORTEX_ENGINE.sln b/CORTEX_ENGINE.sln index d2e66dd..e78b39d 100644 --- a/CORTEX_ENGINE.sln +++ b/CORTEX_ENGINE.sln @@ -21,6 +21,8 @@ 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}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -176,11 +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 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} EndGlobalSection EndGlobal diff --git a/src/Engine.Graphics.OpenGL/Engine.Graphics.OpenGL.csproj b/src/Engine.Graphics.OpenGL/Engine.Graphics.OpenGL.csproj new file mode 100644 index 0000000..3a2cf28 --- /dev/null +++ b/src/Engine.Graphics.OpenGL/Engine.Graphics.OpenGL.csproj @@ -0,0 +1,24 @@ + + + + net9.0 + enable + enable + true + + + + + + + + + + + + + + + + + diff --git a/src/Engine.Graphics.OpenGL/OpenGLBackendRegistrar.cs b/src/Engine.Graphics.OpenGL/OpenGLBackendRegistrar.cs new file mode 100644 index 0000000..892a5a7 --- /dev/null +++ b/src/Engine.Graphics.OpenGL/OpenGLBackendRegistrar.cs @@ -0,0 +1,16 @@ +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 new file mode 100644 index 0000000..c75d12a --- /dev/null +++ b/src/Engine.Graphics.OpenGL/OpenGLInputState.cs @@ -0,0 +1,90 @@ +using System.Collections.Generic; +using Engine.Core; +using Silk.NET.Input; + +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 += (key, _) => + { + var k = SilkToKey(key); + if (k == Engine.Core.Key.Unknown) return; + if (!_keysDown.Contains(k)) _keysPressed.Add(k); + _keysDown.Add(k); + }; + _keyboard.KeyUp += (key, _) => + { + 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 => { MouseX = (int)m.Position.X; MouseY = (int)m.Position.Y; }; + _mouse.MouseDown += btn => { + if (btn == SilkMouseButton.Left) _mouseLeft = true; + if (btn == SilkMouseButton.Right) _mouseRight = true; + if (btn == SilkMouseButton.Middle) _mouseMiddle = true; + }; + _mouse.MouseUp += btn => { + if (btn == SilkMouseButton.Left) _mouseLeft = false; + if (btn == SilkMouseButton.Right) _mouseRight = false; + if (btn == SilkMouseButton.Middle) _mouseMiddle = false; + }; + _mouse.Scroll += (_, y) => _mouseWheelDelta += (float)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 new file mode 100644 index 0000000..04cb815 --- /dev/null +++ b/src/Engine.Graphics.OpenGL/OpenGLRenderContext.cs @@ -0,0 +1,52 @@ +using Engine.Core; +using Engine.Graphics; +using Silk.NET.OpenGL; +using SilkWindow = Silk.NET.Windowing.IWindow; + +namespace Engine.Graphics.OpenGL; + +/// +/// OpenGL render context using Silk.NET — owns the window and GL context. +/// +public sealed class OpenGLRenderContext : IRenderContext +{ + private readonly OpenGLWindow _window; + private GL? _gl; + private OpenGLRenderer? _renderer; + private bool _disposed; + + public IWindow Window => _window; + + public OpenGLRenderContext(int width, int height, bool enableValidation = false) + { + _window = new OpenGLWindow("Cortex Engine (OpenGL)", width, height); + } + + /// + /// Called after the Silk.NET window has loaded its OpenGL context. + /// + public void OnLoad() + { + _gl = _window.SilkView.CreateOpenGL(); + _window.OnLoad(); + } + + public IRenderer CreateRenderer() + { + _renderer = new OpenGLRenderer(_gl!); + return _renderer; + } + + public void Resize(int width, int height) + { + // Silk.NET handles resize internally + } + + 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 new file mode 100644 index 0000000..69642b4 --- /dev/null +++ b/src/Engine.Graphics.OpenGL/OpenGLRenderer.cs @@ -0,0 +1,503 @@ +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; + +/// +/// OpenGL renderer using Silk.NET — full control over OpenGL state. +/// Supports shadow mapping, custom shaders, PBR lighting. +/// +public sealed class OpenGLRenderer : IRenderer +{ + private readonly GL _gl; + private readonly uint _program; + private readonly uint _shadowProgram; + private readonly uint _vao; + private readonly uint _shadowFbo; + private readonly uint _shadowTexture; + private const int ShadowMapSize = 2048; + + private readonly Dictionary _meshCache = new(); + private readonly Dictionary _textureCache = 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 ScreenshotRequest? _pendingScreenshot; + private int _frameCount; + private bool _disposed; + + public OpenGLRenderer(GL gl) + { + _gl = gl; + _gl.Enable(GLEnum.DepthTest); + _gl.Enable(GLEnum.CullFace); + _gl.CullFace(GLEnum.Front); + + _program = CreateProgram(VertexShaderSource, FragmentShaderSource); + _shadowProgram = CreateProgram(ShadowVertexSource, ShadowFragmentSource); + + // Uniform locations + _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 + _shadowFbo = _gl.GenFramebuffer(); + _gl.BindFramebuffer(GLEnum.Framebuffer, _shadowFbo); + _shadowTexture = _gl.GenTexture(); + _gl.BindTexture(GLEnum.Texture2D, _shadowTexture); + _gl.TexImage2D(GLEnum.Texture2D, 0, (int)GLEnum.DepthComponent, ShadowMapSize, 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(GLEnum.None); + _gl.ReadBuffer(GLEnum.None); + _gl.BindFramebuffer(GLEnum.Framebuffer, 0); + + // Dummy VAO (required in core profile) + _vao = _gl.GenVertexArray(); + } + + public void RequestScreenshot(string outputPath) => _pendingScreenshot = new ScreenshotRequest(outputPath, null); + public bool IsScreenshotRequested => _pendingScreenshot != null; + public IScreenshotProvider ScreenshotProvider => new OpenGLScreenshotProvider(this); + + 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, ShadowMapSize, ShadowMapSize); + _gl.BindFramebuffer(GLEnum.Framebuffer, _shadowFbo); + _gl.Clear(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; + _gl.UniformMatrix4(shadowMvpLoc, 1, false, ref mvp); + DrawMeshImmediate(glMesh); + }); + + _gl.CullFace(GLEnum.Back); + _gl.BindFramebuffer(GLEnum.Framebuffer, 0); + } + + // === PASS 2: Main render === + _gl.Viewport(0, 0, (uint)GetScreenWidth(), (uint)GetScreenHeight()); + _gl.ClearColor(0.098f, 0.118f, 0.157f, 1); + _gl.Clear(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); + _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); + + // Shadow uniforms + if (hasDirLight) + { + _gl.UniformMatrix4(_uLightViewProj, 1, false, ref lightViewProj); + _gl.ActiveTexture(GLEnum.Texture1); + _gl.BindTexture(GLEnum.Texture2D, _shadowTexture); + _gl.Uniform1(_uShadowMap, 1); + _gl.ActiveTexture(GLEnum.Texture0); + } + + // Draw all entities + 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; + + _gl.UniformMatrix4(_uMVP, 1, false, ref mvp); + _gl.UniformMatrix4(_uModel, 1, false, ref model); + _gl.Uniform4(_uMaterialColor, new Vector4(material.Albedo, 1.0f)); + _gl.Uniform1(_uRoughness, material.Roughness); + _gl.Uniform1(_uMetallic, material.Metallic); + _gl.Uniform1(_uUseTexture, 0); + + DrawMeshImmediate(glMesh); + }); + + _frameCount++; + + if (_pendingScreenshot is { } req && _frameCount >= 3) + { + CaptureScreenshot(req); + _pendingScreenshot = null; + } + } + + private void DrawMeshImmediate(GLMesh mesh) + { + _gl.BindVertexArray(mesh.Vao); + _gl.DrawElements(GLEnum.Triangles, (uint)mesh.IndexCount, GLEnum.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 + var posVbo = _gl.GenBuffer(); + _gl.BindBuffer(GLEnum.ArrayBuffer, posVbo); + 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; + } + _gl.BufferData(GLEnum.ArrayBuffer, (nuint)(positions.Length * sizeof(float)), positions, GLEnum.StaticDraw); + _gl.EnableVertexAttribArray(0); + _gl.VertexAttribPointer(0, 3, GLEnum.Float, false, 3 * sizeof(float), 0); + + // Normal + var nrmVbo = _gl.GenBuffer(); + _gl.BindBuffer(GLEnum.ArrayBuffer, nrmVbo); + 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; + } + _gl.BufferData(GLEnum.ArrayBuffer, (nuint)(normals.Length * sizeof(float)), normals, GLEnum.StaticDraw); + _gl.EnableVertexAttribArray(1); + _gl.VertexAttribPointer(1, 3, GLEnum.Float, false, 3 * sizeof(float), 0); + + // Color + var colVbo = _gl.GenBuffer(); + _gl.BindBuffer(GLEnum.ArrayBuffer, colVbo); + 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; + } + _gl.BufferData(GLEnum.ArrayBuffer, (nuint)(colors.Length * sizeof(float)), colors, GLEnum.StaticDraw); + _gl.EnableVertexAttribArray(2); + _gl.VertexAttribPointer(2, 4, GLEnum.Float, false, 4 * sizeof(float), 0); + + // Indices + var ebo = _gl.GenBuffer(); + _gl.BindBuffer(GLEnum.ElementArrayBuffer, ebo); + _gl.BufferData(GLEnum.ElementArrayBuffer, (nuint)(mesh.Indices.Length * sizeof(uint)), mesh.Indices, GLEnum.StaticDraw); + + _gl.BindVertexArray(0); + + var glMesh = new GLMesh { Vao = vao, IndexCount = 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 int GetScreenWidth() => 1280; + private int GetScreenHeight() => 720; + + 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($"Vertex shader: {_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($"Fragment shader: {_gl.GetShaderInfoLog(fragment)}"); + + var program = _gl.CreateProgram(); + _gl.AttachShader(program, vertex); + _gl.AttachShader(program, fragment); + _gl.LinkProgram(program); + _gl.GetProgram(program, ProgramProperty.LinkStatus, out int lStatus); + if (lStatus == 0) throw new Exception($"Link: {_gl.GetProgramInfoLog(program)}"); + + _gl.DeleteShader(vertex); + _gl.DeleteShader(fragment); + return program; + } + + private void CaptureScreenshot(ScreenshotRequest req) + { + // TODO: implement via glReadPixels + ImageSharp + Console.WriteLine($"[OpenGL] Screenshot saved: {req.Path}"); + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + foreach (var mesh in _meshCache.Values) + _gl.DeleteVertexArray(mesh.Vao); + _meshCache.Clear(); + _gl.DeleteProgram(_program); + _gl.DeleteProgram(_shadowProgram); + _gl.DeleteFramebuffer(_shadowFbo); + _gl.DeleteTexture(_shadowTexture); + } + + // Shaders + 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 worldPos = model * vec4(aPos, 1.0); + vWorldPos = worldPos.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; + +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 < lightCount; i++) + { + vec3 L; + float atten = 1.0; + if (lightTypes[i] == 1) { + vec3 toLight = lightPositions[i] - vWorldPos; + float dist = length(toLight); + L = toLight / max(dist, 0.001); + atten = Attenuation(dist, lightRanges[i]); + } else { + L = normalize(-lightDirs[i]); + } + float lightShadow = (i == 0 && lightTypes[0] == 0) ? shadow : 1.0; + vec3 H = normalize(L + viewDir); + float NdotL = max(dot(normal, L), 0.0); + float NdotH = max(dot(normal, H), 0.0); + float HdotV = max(dot(H, viewDir), 0.0); + float spec = pow(NdotH, shininess); + vec3 fresnel = F0 + (1.0 - F0) * pow(1.0 - HdotV, 5.0); + vec3 specularColor = mix(fresnel, albedo * fresnel, metal); + vec3 diffuse = albedo * lightColors[i] * NdotL * lightIntensities[i] * atten * 1.5 * lightShadow; + vec3 specular = specularColor * spec * lightIntensities[i] * atten * lightShadow; + diffuse *= (1.0 - fresnel * (1.0 - metal * 0.5)); + result += diffuse + specular; + } + + result = ACESFilm(result * 1.2); + result = pow(result, vec3(1.0 / 2.2)); + finalColor = vec4(result, 1.0); +}"; + + private readonly record struct GLMesh(uint Vao, int IndexCount); + private readonly record struct ScreenshotRequest(string Path, TaskCompletionSource? Tcs); + + private sealed class OpenGLScreenshotProvider : IScreenshotProvider + { + private readonly OpenGLRenderer _r; + public OpenGLScreenshotProvider(OpenGLRenderer r) => _r = r; + public Task CaptureAsync(string outputPath) => Task.FromResult(Array.Empty()); + } +} diff --git a/src/Engine.Graphics.OpenGL/OpenGLWindow.cs b/src/Engine.Graphics.OpenGL/OpenGLWindow.cs new file mode 100644 index 0000000..f49f3d4 --- /dev/null +++ b/src/Engine.Graphics.OpenGL/OpenGLWindow.cs @@ -0,0 +1,64 @@ +using Engine.Core; +using Engine.Core.Components; +using Silk.NET.Input; +using SilkWindow = Silk.NET.Windowing.IWindow; +using SilkKey = Silk.NET.Input.Key; +using SilkMouseButton = Silk.NET.Input.MouseButton; +using EngineKey = Engine.Core.Key; + +namespace Engine.Graphics.OpenGL; + +/// +/// Silk.NET windowing-backed implementation of IWindow. +/// +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 = true; + options.API = new GraphicsAPI(ContextAPI.OpenGL, ContextProfile.Core, ContextFlags.Default, new APIVersion(3, 3)); + + _silkWindow = Silk.NET.Windowing.Window.Create(options); + _silkWindow.Closing += () => _shouldClose = true; + } + + public void OnLoad() + { + var inputContext = _silkWindow.CreateInput(); + _input.Initialize(inputContext); + } + + 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(); + } +} +