acdream/src/AcDream.App/Rendering/FadeOverlay.cs
Erik 3ce1fae332 feat(teleport C): TAS-driven fade transit + retire TeleportArrivalController
Wires the dormant TeleportAnimSequencer as the transit driver: on PlayerTeleport
the player holds in PortalSpace behind a full-screen fade (FadeOverlay) until the
destination terrain is resident (TeleportWorldReady, gated on the priority-applied
landblock), then materializes (Place), and after the world fades back in regains
control + acks the server (FireLoginComplete). No movement resolves against the
empty world, so the outbound cell frame can't corrupt. Outdoor changes from
place-immediately back to hold-until-resident (now fast, not a band-aid).

- FadeOverlay: fullscreen NDC black quad, alpha = ShowTunnel ? 1 : FadeAlpha.
- Retires TeleportArrivalController + its 2 tests (TAS subsumes the driver role).
- Divergence register: AD-2 updated to the new mechanism; AD-31 (fade vs swirl).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 14:03:25 +02:00

121 lines
4.3 KiB
C#

using System;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
/// <summary>
/// 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 <see cref="AcDream.Core.World.TeleportAnimSequencer"/> 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.
///
/// <para>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.</para>
/// </summary>
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;
}
/// <summary>
/// Draw the fullscreen black cover at <paramref name="alpha"/> (0 = clear → no-op,
/// 1 = opaque). Must be called LAST in the frame, after the world and UI, so it covers
/// everything on screen.
/// </summary>
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);
}
}