using System; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// /// Fullscreen black quad at a given alpha — the teleport fade cover (retail-teleport /// spec §C, 2026-06-22). Drawn LAST in the frame (over the world + UI) so it covers /// everything; the drives the alpha /// (opaque-black through the transit, ramping the world back in on WorldFadeIn). The /// authentic 3D portal-swirl is deferred — this black cover replaces it for now. /// /// Draws in NDC, so it needs no view/projection. Self-contained GL state /// (feedback_render_self_contained_gl_state): sets blend + disables depth, restores the /// frame-global convention (depth test/write on) on exit. /// public sealed class FadeOverlay : IDisposable { private const string VertSrc = @"#version 430 core layout(location = 0) in vec2 aPos; void main() { gl_Position = vec4(aPos, 0.0, 1.0); }"; private const string FragSrc = @"#version 430 core uniform float uAlpha; out vec4 FragColor; void main() { FragColor = vec4(0.0, 0.0, 0.0, uAlpha); }"; private readonly GL _gl; private readonly uint _program; private readonly uint _vao; private readonly uint _vbo; private readonly int _locAlpha; // Two triangles covering the whole NDC viewport. private static readonly float[] QuadVerts = { -1f, -1f, 1f, -1f, 1f, 1f, -1f, -1f, 1f, 1f, -1f, 1f, }; public FadeOverlay(GL gl) { _gl = gl ?? throw new ArgumentNullException(nameof(gl)); uint vs = Compile(ShaderType.VertexShader, VertSrc); uint fs = Compile(ShaderType.FragmentShader, FragSrc); _program = _gl.CreateProgram(); _gl.AttachShader(_program, vs); _gl.AttachShader(_program, fs); _gl.LinkProgram(_program); _gl.GetProgram(_program, ProgramPropertyARB.LinkStatus, out int linked); if (linked == 0) throw new InvalidOperationException($"FadeOverlay link failed: {_gl.GetProgramInfoLog(_program)}"); _gl.DeleteShader(vs); _gl.DeleteShader(fs); _locAlpha = _gl.GetUniformLocation(_program, "uAlpha"); _vao = _gl.GenVertexArray(); _vbo = _gl.GenBuffer(); _gl.BindVertexArray(_vao); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo); unsafe { fixed (float* v = QuadVerts) _gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)(QuadVerts.Length * sizeof(float)), v, BufferUsageARB.StaticDraw); _gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 2 * sizeof(float), (void*)0); } _gl.EnableVertexAttribArray(0); _gl.BindVertexArray(0); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0); } private uint Compile(ShaderType type, string src) { uint s = _gl.CreateShader(type); _gl.ShaderSource(s, src); _gl.CompileShader(s); _gl.GetShader(s, ShaderParameterName.CompileStatus, out int ok); if (ok == 0) throw new InvalidOperationException($"FadeOverlay {type} compile failed: {_gl.GetShaderInfoLog(s)}"); return s; } /// /// Draw the fullscreen black cover at (0 = clear → no-op, /// 1 = opaque). Must be called LAST in the frame, after the world and UI, so it covers /// everything on screen. /// public void Draw(float alpha) { if (alpha <= 0f) return; alpha = Math.Clamp(alpha, 0f, 1f); // ---- set state (everything this draw depends on) ---- _gl.Disable(EnableCap.DepthTest); _gl.DepthMask(false); _gl.Disable(EnableCap.CullFace); _gl.Enable(EnableCap.Blend); _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); _gl.UseProgram(_program); _gl.Uniform1(_locAlpha, alpha); _gl.BindVertexArray(_vao); _gl.DrawArrays(PrimitiveType.Triangles, 0, 6); _gl.BindVertexArray(0); _gl.UseProgram(0); // ---- restore the frame-global convention ---- _gl.DepthMask(true); _gl.Enable(EnableCap.DepthTest); } public void Dispose() { _gl.DeleteProgram(_program); _gl.DeleteVertexArray(_vao); _gl.DeleteBuffer(_vbo); } }