From f0af0a6b5e54041e3f97ee610a4e8d397023cc96 Mon Sep 17 00:00:00 2001 From: emil28092005 Date: Wed, 17 Jun 2026 21:28:53 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20rewrite=20OpenTK=20renderer=20from=20scr?= =?UTF-8?q?atch=20=E2=80=94=20correct=20matrix=20transpose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SetUniformMat4: copies Matrix4 to float[16] row-major, passes with transpose=true (OpenGL transposes row-major → column-major) - Previous version used transpose=false with manual column-major copy which caused double-transpose → garbage rendering (strobe) - VertexAttribPointer with stride=0 (tightly packed per-attribute buffers) - MakeCurrent() explicitly in constructor to ensure GL context is ready - 66/66 tests, 1400+ FPS --- src/Engine.Graphics.OpenTK/OpenTKRenderer.cs | 418 +++++++++++-------- src/Engine.Graphics.OpenTK/OpenTKWindow.cs | 10 +- 2 files changed, 241 insertions(+), 187 deletions(-) diff --git a/src/Engine.Graphics.OpenTK/OpenTKRenderer.cs b/src/Engine.Graphics.OpenTK/OpenTKRenderer.cs index ec833aa..122161f 100644 --- a/src/Engine.Graphics.OpenTK/OpenTKRenderer.cs +++ b/src/Engine.Graphics.OpenTK/OpenTKRenderer.cs @@ -8,31 +8,29 @@ 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 int _program; + private int _shadowProgram; + private int _shadowFbo; + private int _shadowTexture; private readonly Dictionary _meshCache = new(); + private readonly float[] _matrixBuf = new float[16]; + // 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; + private int _uMVP, _uModel, _uViewPos, _uMaterialColor, _uRoughness, _uMetallic; + private int _uAmbient, _uLightCount, _uLightDirs, _uLightIntensities, _uLightColors; + private int _uLightPositions, _uLightTypes, _uLightRanges; + private int _uLightViewProj, _uShadowMap, _uUseTexture; // Light data private readonly float[] _lightDirs = new float[12]; @@ -43,17 +41,16 @@ public sealed class OpenTKRenderer : IRenderer private readonly float[] _lightRanges = new float[4]; private int _lightCount; - private int _screenWidth = 1280; - private int _screenHeight = 720; + private int _screenW = 1280, _screenH = 720; private bool _disposed; public OpenTKRenderer() { - GL.Enable(EnableCap.DepthTest); - - _program = CreateProgram(VertexShaderSource, FragmentShaderSource); - _shadowProgram = CreateProgram(ShadowVertexSource, ShadowFragmentSource); + // Compile shaders + _program = CreateProgram(VertexSrc, FragmentSrc); + _shadowProgram = CreateProgram(ShadowVertSrc, ShadowFragSrc); + // Get uniform locations _uMVP = GL.GetUniformLocation(_program, "mvp"); _uModel = GL.GetUniformLocation(_program, "model"); _uViewPos = GL.GetUniformLocation(_program, "viewPos"); @@ -72,9 +69,10 @@ public sealed class OpenTKRenderer : IRenderer _uShadowMap = GL.GetUniformLocation(_program, "shadowMap"); _uUseTexture = GL.GetUniformLocation(_program, "useTexture"); - // Shadow FBO with depth texture + // Shadow FBO _shadowFbo = GL.GenFramebuffer(); GL.BindFramebuffer(FramebufferTarget.Framebuffer, _shadowFbo); + _shadowTexture = GL.GenTexture(); GL.BindTexture(TextureTarget.Texture2D, _shadowTexture); GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.DepthComponent, @@ -88,13 +86,17 @@ public sealed class OpenTKRenderer : IRenderer GL.DrawBuffer(DrawBufferMode.None); GL.ReadBuffer(ReadBufferMode.None); GL.BindFramebuffer(FramebufferTarget.Framebuffer, 0); + + // GL state + GL.Enable(EnableCap.DepthTest); + GL.Disable(EnableCap.CullFace); } - public void SetScreenSize(int w, int h) { _screenWidth = w; _screenHeight = h; } + public void SetScreenSize(int w, int h) { _screenW = w; _screenH = h; } - public void RequestScreenshot(string outputPath) { } + public void RequestScreenshot(string path) { } public bool IsScreenshotRequested => false; - public IScreenshotProvider ScreenshotProvider => new OpenTKScreenshotProvider(); + public IScreenshotProvider ScreenshotProvider => new DummyScreenshotProvider(); public void RenderWorld(World world) { @@ -102,19 +104,18 @@ public sealed class OpenTKRenderer : IRenderer CollectLights(world); var hasDirLight = _lightCount > 0 && _lightTypes[0] == (int)LightType.Directional; - OTKMatrix lightViewProj = OTKMatrix.Identity; + OTKMatrix lightVP = OTKMatrix.Identity; - // === PASS 1: Shadow map === + // === PASS 1: Shadow === 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; + var lightDir = new Vector3(_lightDirs[0], _lightDirs[1], _lightDirs[2]); + var center = new Vector3(0, 0.5f, 0); + var lightPos = center - lightDir * 30f; + var up = MathF.Abs(Vector3.Dot(lightDir, Vector3.UnitY)) > 0.99f ? Vector3.UnitZ : Vector3.UnitY; - lightViewProj = OTKMatrix.CreateOrthographicOffCenter(-15, 15, -15, 15, 1, 80) - * OTKMatrix.LookAt(ToOTK(lightPos), ToOTK(sceneCenter), ToOTK(up)); + lightVP = OTKMatrix.CreateOrthographicOffCenter(-15, 15, -15, 15, 1, 80) + * OTKMatrix.LookAt(ToV3(lightPos), ToV3(center), ToV3(up)); GL.Viewport(0, 0, ShadowMapSize, ShadowMapSize); GL.BindFramebuffer(FramebufferTarget.Framebuffer, _shadowFbo); @@ -122,34 +123,31 @@ public sealed class OpenTKRenderer : IRenderer GL.UseProgram(_shadowProgram); GL.CullFace(CullFaceMode.Front); - var shadowMvpLoc = GL.GetUniformLocation(_shadowProgram, "mvp"); + int sMvp = GL.GetUniformLocation(_shadowProgram, "mvp"); - world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform transform) => + world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform t) => { 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); + var gm = GetOrUploadMesh(e, mesh); + var model = ToM4(t.GetMatrix()); + var mvp = lightVP * model; + SetUniformMat4(sMvp, mvp); + DrawMesh(gm); }); 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); + // === PASS 2: Main === + GL.Viewport(0, 0, _screenW, _screenH); + GL.ClearColor(0.098f, 0.118f, 0.157f, 1f); GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); GL.UseProgram(_program); - var view = OTKMatrix.LookAt(ToOTK(camera.Position), ToOTK(camera.Target), ToOTK(camera.Up)); + var view = OTKMatrix.LookAt(ToV3(camera.Position), ToV3(camera.Target), ToV3(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); @@ -162,123 +160,133 @@ public sealed class OpenTKRenderer : IRenderer if (hasDirLight) { - GL.UniformMatrix4(_uLightViewProj, false, ref lightViewProj); + SetUniformMat4(_uLightViewProj, lightVP); 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) => + world.Each((Entity e, ref EngineMesh mesh, ref EngineTransform t) => { if (e.Name() == "Grid") return; - var material = e.Has() ? e.Get() : EngineMaterial.Default; - var glMesh = GetOrUploadMesh(e, mesh); - var model = ToOTK(transform.GetMatrix()); + var mat = e.Has() ? e.Get() : EngineMaterial.Default; + var gm = GetOrUploadMesh(e, mesh); + var model = ToM4(t.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); + SetUniformMat4(_uMVP, mvp); + SetUniformMat4(_uModel, model); + GL.Uniform4(_uMaterialColor, mat.Albedo.X, mat.Albedo.Y, mat.Albedo.Z, 1f); + GL.Uniform1(_uRoughness, mat.Roughness); + GL.Uniform1(_uMetallic, mat.Metallic); GL.Uniform1(_uUseTexture, 0); - DrawMeshImmediate(glMesh); + DrawMesh(gm); }); } - private void DrawMeshImmediate(GLMesh mesh) + private void DrawMesh(GLMesh m) { - GL.BindVertexArray(mesh.Vao); - GL.DrawElements(PrimitiveType.Triangles, mesh.IndexCount, DrawElementsType.UnsignedInt, 0); + GL.BindVertexArray(m.Vao); + GL.DrawElements(PrimitiveType.Triangles, m.Count, DrawElementsType.UnsignedInt, 0); GL.BindVertexArray(0); } + private void SetUniformMat4(int loc, OTKMatrix mat) + { + // OpenTK Matrix4 is row-major in memory; OpenGL expects column-major with transpose=false. + // Use transpose=true so OpenGL transposes our row-major data into column-major. + _matrixBuf[0] = mat.M11; _matrixBuf[1] = mat.M12; _matrixBuf[2] = mat.M13; _matrixBuf[3] = mat.M14; + _matrixBuf[4] = mat.M21; _matrixBuf[5] = mat.M22; _matrixBuf[6] = mat.M23; _matrixBuf[7] = mat.M24; + _matrixBuf[8] = mat.M31; _matrixBuf[9] = mat.M32; _matrixBuf[10] = mat.M33; _matrixBuf[11] = mat.M34; + _matrixBuf[12] = mat.M41; _matrixBuf[13] = mat.M42; _matrixBuf[14] = mat.M43; _matrixBuf[15] = mat.M44; + GL.UniformMatrix4(loc, 1, true, _matrixBuf); + } + private GLMesh GetOrUploadMesh(Entity e, EngineMesh mesh) { if (_meshCache.TryGetValue(e, out var existing)) return existing; - var vao = GL.GenVertexArray(); + int 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++) + // Position + float[] pos = new float[mesh.Vertices.Length * 3]; + for (int 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; + pos[i*3] = mesh.Vertices[i].Position.X; + pos[i*3+1] = mesh.Vertices[i].Position.Y; + pos[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); + int vboPos = GL.GenBuffer(); + GL.BindBuffer(BufferTarget.ArrayBuffer, vboPos); + GL.BufferData(BufferTarget.ArrayBuffer, pos.Length * sizeof(float), pos, BufferUsageHint.StaticDraw); GL.EnableVertexAttribArray(0); - GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0); + GL.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 0, 0); - // Normal (location 1) - var normals = new float[mesh.Vertices.Length * 3]; - for (var i = 0; i < mesh.Vertices.Length; i++) + // Normal + float[] nrm = new float[mesh.Vertices.Length * 3]; + for (int 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; + nrm[i*3] = mesh.Vertices[i].Normal.X; + nrm[i*3+1] = mesh.Vertices[i].Normal.Y; + nrm[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); + int vboNrm = GL.GenBuffer(); + GL.BindBuffer(BufferTarget.ArrayBuffer, vboNrm); + GL.BufferData(BufferTarget.ArrayBuffer, nrm.Length * sizeof(float), nrm, BufferUsageHint.StaticDraw); GL.EnableVertexAttribArray(1); - GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 3 * sizeof(float), 0); + GL.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, 0, 0); - // Color (location 2) - var colors = new float[mesh.Vertices.Length * 4]; - for (var i = 0; i < mesh.Vertices.Length; i++) + // Color + float[] col = new float[mesh.Vertices.Length * 4]; + for (int 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; + col[i*4] = mesh.Vertices[i].Color.X; + col[i*4+1] = mesh.Vertices[i].Color.Y; + col[i*4+2] = mesh.Vertices[i].Color.Z; + col[i*4+3] = 1f; } - var colVbo = GL.GenBuffer(); - GL.BindBuffer(BufferTarget.ArrayBuffer, colVbo); - GL.BufferData(BufferTarget.ArrayBuffer, colors.Length * sizeof(float), colors, BufferUsageHint.StaticDraw); + int vboCol = GL.GenBuffer(); + GL.BindBuffer(BufferTarget.ArrayBuffer, vboCol); + GL.BufferData(BufferTarget.ArrayBuffer, col.Length * sizeof(float), col, BufferUsageHint.StaticDraw); GL.EnableVertexAttribArray(2); - GL.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, 4 * sizeof(float), 0); + GL.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, 0, 0); // Indices - var ebo = GL.GenBuffer(); + int 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; + var gm = new GLMesh(vao, mesh.Indices.Length); + _meshCache[e] = gm; + return gm; } private void CollectLights(World world) { - var count = 0; + int 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; + _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; + _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; @@ -286,51 +294,49 @@ public sealed class OpenTKRenderer : IRenderer _lightTypes[0] = (int)LightType.Directional; _lightRanges[0] = 20; count = 1; } - for (var i = count; i < 4; i++) { _lightIntensities[i] = 0; _lightTypes[i] = 0; } + for (int 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); + 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 static OTKMatrix ToOTK(System.Numerics.Matrix4x4 m) => new( + private static OTKMatrix ToM4(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 OTKVector3 ToV3(Vector3 v) => new(v.X, v.Y, v.Z); - private static int CreateProgram(string vs, string fs) + private static int CreateProgram(string vsSrc, string fsSrc) { - 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)}"); + int vs = GL.CreateShader(ShaderType.VertexShader); + GL.ShaderSource(vs, vsSrc); + GL.CompileShader(vs); + GL.GetShader(vs, ShaderParameter.CompileStatus, out int vsOk); + if (vsOk == 0) throw new Exception($"VS compile: {GL.GetShaderInfoLog(vs)}"); - 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)}"); + int fs = GL.CreateShader(ShaderType.FragmentShader); + GL.ShaderSource(fs, fsSrc); + GL.CompileShader(fs); + GL.GetShader(fs, ShaderParameter.CompileStatus, out int fsOk); + if (fsOk == 0) throw new Exception($"FS compile: {GL.GetShaderInfoLog(fs)}"); - 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)}"); + int prog = GL.CreateProgram(); + GL.AttachShader(prog, vs); + GL.AttachShader(prog, fs); + GL.LinkProgram(prog); + GL.GetProgram(prog, GetProgramParameterName.LinkStatus, out int linkOk); + if (linkOk == 0) throw new Exception($"Link: {GL.GetProgramInfoLog(prog)}"); - GL.DeleteShader(vertex); - GL.DeleteShader(fragment); - return program; + GL.DeleteShader(vs); + GL.DeleteShader(fs); + return prog; } public void Dispose() @@ -345,15 +351,16 @@ public sealed class OpenTKRenderer : IRenderer GL.DeleteTexture(_shadowTexture); } - // Shaders (copied from Silk.NET OpenGL backend — backend-agnostic GLSL 330 core) - private const string ShadowVertexSource = @"#version 330 core + // === Shaders === + private const string ShadowVertSrc = @"#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(){ gl_Position = mvp * vec4(aPos, 1.0); }"; + + private const string ShadowFragSrc = @"#version 330 core void main(){}"; - private const string VertexShaderSource = @"#version 330 core + private const string VertexSrc = @"#version 330 core layout(location=0) in vec3 aPos; layout(location=1) in vec3 aNormal; layout(location=2) in vec4 aColor; @@ -362,15 +369,16 @@ 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); +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 + private const string FragmentSrc = @"#version 330 core in vec3 vNormal; in vec3 vWorldPos; in vec4 vColor; @@ -390,48 +398,94 @@ 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 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 dpt = texture(shadowMap, uvw.xy + vec2(x, y) * ts).r; + s += (uvw.z - bias > dpt) ? 0.3 : 1.0; + } } - result=ACESFilm(result*1.2); - result=pow(result,vec3(1.0/2.2)); - finalColor=vec4(result,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 specColor = mix(fresnel, albedo * fresnel, metal); + vec3 diffuse = albedo * lightColors[i] * NdotL * lightIntensities[i] * atten * 1.5 * lightShadow; + vec3 specular = specColor * 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(int Vao, int IndexCount); + private readonly record struct GLMesh(int Vao, int Count); - private sealed class OpenTKScreenshotProvider : IScreenshotProvider + private sealed class DummyScreenshotProvider : IScreenshotProvider { public Task CaptureAsync(string outputPath) => Task.FromResult(Array.Empty()); } diff --git a/src/Engine.Graphics.OpenTK/OpenTKWindow.cs b/src/Engine.Graphics.OpenTK/OpenTKWindow.cs index c62241e..b93b334 100644 --- a/src/Engine.Graphics.OpenTK/OpenTKWindow.cs +++ b/src/Engine.Graphics.OpenTK/OpenTKWindow.cs @@ -2,14 +2,9 @@ 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(); @@ -30,8 +25,13 @@ public sealed class OpenTKWindow : GameWindow, IWindow, IDisposable Profile = ContextProfile.Core, Flags = ContextFlags.ForwardCompatible, Vsync = VSyncMode.Off, + NumberOfSamples = 0, }) { + // GameWindow creates the GL context in the base constructor. + // MakeCurrent is called automatically by OpenTK on first ProcessEvents. + // We call it explicitly here to ensure it's ready before renderer creation. + MakeCurrent(); } public void PumpEvents()