using System; using System.Collections.Generic; using System.Numerics; using AcDream.App.Rendering.Wb; using AcDream.App.UI; using AcDream.Core.Lighting; using AcDream.Core.World; using Silk.NET.OpenGL; namespace AcDream.App.Rendering; /// /// Render-to-texture renderer for the paperdoll 3-D doll (the C# analog of retail /// CreatureMode::Render). Draws ONE re-dressed player clone (a , /// built by + wired by GameWindow) into a private off-screen /// framebuffer with a fixed + one distant light, then hands the color /// texture to the widget which blits it as a normal 2-D sprite. /// /// The whole 3-D pass is sealed in a so it can't disturb the /// surrounding world / UI GL state ([[feedback_render_self_contained_gl_state]]). It runs in the /// per-frame pre-UI hook (GameWindow), gated on inventory-open ∧ doll-view — NOT from the widget's /// 2-D OnDraw. /// /// Reuses the existing + the player's already-resolved, /// already-GPU-resident MeshRefs (the doll clones them), so no separate mesh/texture upload is /// needed. With frustum: null the doll's synthetic landblock is always "visible" and the /// entity is walked from entry.Entities and drawn from its current MeshRefs — a STATIC pose /// for now; idle animation (TickAnimations rebuilding the doll's MeshRefs) is a later slice. /// public sealed unsafe class PaperdollViewportRenderer : IUiViewportRenderer, IDisposable { private readonly GL _gl; private readonly WbDrawDispatcher _dispatcher; private readonly SceneLightingUboBinding _lightUbo; private readonly DollCamera _camera = new(); // Off-screen target (lazily (re)created on size change). private uint _fbo; private uint _colorTex; private uint _depthRbo; private int _fbW; private int _fbH; // The doll WorldEntity (held by reference: when GameWindow's TickAnimations later rebuilds its // MeshRefs each frame, this renderer sees the updated pose automatically). null = nothing to draw. private WorldEntity? _doll; // Synthetic landblock for the doll's one-entry Draw tuple. With frustum:null + this as // neverCullLandblockId the entity is never culled. 0 is unused by live streaming on the doll path. private const uint DollLandblockId = 0u; // The doll's render id, passed as "animated" so the dispatcher bypasses the Tier-1 classification // cache (WbDrawDispatcher.cs:1142) and re-classifies from the current MeshRefs every frame. This is // what makes a re-dress (a new WorldEntity built with the same DollRenderId) actually take effect, // AND what lets the idle animation (later slice, TickAnimations rebuilds MeshRefs) show. private static readonly HashSet _dollAnimatedIds = new() { DollEntityBuilder.DollRenderId }; public PaperdollViewportRenderer(GL gl, WbDrawDispatcher dispatcher, SceneLightingUboBinding lightUbo) { _gl = gl ?? throw new ArgumentNullException(nameof(gl)); _dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher)); _lightUbo = lightUbo ?? throw new ArgumentNullException(nameof(lightUbo)); } /// Set (or clear) the doll entity. GameWindow builds/re-dresses it from the live player. public void SetDoll(WorldEntity? doll) => _doll = doll; /// public uint Render(int width, int height) { var doll = _doll; if (doll is null || doll.MeshRefs.Count == 0 || width <= 0 || height <= 0) return 0u; EnsureFramebuffer(width, height); if (_fbo == 0) return 0u; _camera.Aspect = width / (float)height; // Seal the entire 3-D pass — GLStateScope restores viewport/scissor/depth/cull/blend/program/ // FBO bindings on dispose, so the surrounding world + 2-D UI passes are untouched. using var scope = new GLStateScope(_gl); _gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo); _gl.Viewport(0, 0, (uint)width, (uint)height); _gl.Disable(EnableCap.ScissorTest); _gl.ClearColor(0f, 0f, 0f, 0f); // transparent — the window backdrop shows through behind the doll _gl.ClearDepth(1.0); _gl.DepthMask(true); // depth clears honor glDepthMask _gl.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit); _gl.Enable(EnableCap.DepthTest); _gl.DepthFunc(DepthFunction.Less); _gl.Enable(EnableCap.CullFace); _gl.CullFace(TriangleFace.Back); _gl.FrontFace(FrontFaceDirection.Ccw); // matches the world object pass; if the doll renders inside-out at the gate, flip to Cw _gl.Disable(EnableCap.Blend); UploadDollLight(); // One synthetic landblock entry holding just the doll. frustum:null ⇒ always "visible" ⇒ // walked from entry.Entities and drawn from doll.MeshRefs (WbDrawDispatcher.cs:692,1190). var entities = new WorldEntity[] { doll }; var entries = new (uint, Vector3, Vector3, IReadOnlyList, IReadOnlyDictionary?)[] { (DollLandblockId, new Vector3(-4f, -4f, -4f), new Vector3(4f, 4f, 4f), entities, null), }; _dispatcher.Draw( _camera, entries, frustum: null, neverCullLandblockId: DollLandblockId, visibleCellIds: null, animatedEntityIds: _dollAnimatedIds); // doll treated as animated ⇒ cache-bypass (re-dress correctness) return _colorTex; } /// Overwrite the shared scene-lighting UBO (binding=1) with the doll's single distant /// light. Safe: nothing else draws 3-D after the doll this frame, and GameWindow rebuilds the /// world UBO at the start of the next frame. Retail values: DISTANT_LIGHT dir (0.3,1.9,0.65) /// intensity 2.0 (gmPaperDollUI::PostInit decomp 175529-175533). private void UploadDollLight() { var dir = Vector3.Normalize(new Vector3(0.3f, 1.9f, 0.65f)); var ubo = new SceneLightingUbo { Light0 = new UboLight { PosAndKind = new Vector4(0f, 0f, 0f, 0f), // kind 0 = directional DirAndRange = new Vector4(dir, 1e9f), // huge range = no distance cutoff ColorAndIntensity = new Vector4(1f, 1f, 1f, 2.0f), // white @ retail intensity 2.0 ConeAngleEtc = Vector4.Zero, }, CellAmbient = new Vector4(0.30f, 0.30f, 0.30f, 1f), // retail CreatureMode default ambient (ctor 0x004543cf = 0.3,0.3,0.3) FogParams = new Vector4(1e9f, 1e9f, 0f, 0f), // fog pushed to infinity = no fog on the doll FogColor = Vector4.Zero, CameraAndTime = new Vector4(0.12f, -2.4f, 0.88f, 0f), // camera world pos (matches DollCamera eye) }; _lightUbo.Upload(ubo); } /// (Re)create the FBO + color texture + depth-stencil renderbuffer when the requested /// size changes. Color is RGBA8 with linear filtering + clamp; depth is Depth24Stencil8. private void EnsureFramebuffer(int width, int height) { if (_fbo != 0 && width == _fbW && height == _fbH) return; DeleteFramebuffer(); _fbW = width; _fbH = height; _colorTex = _gl.GenTexture(); _gl.BindTexture(TextureTarget.Texture2D, _colorTex); _gl.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba8, (uint)width, (uint)height, 0, PixelFormat.Rgba, PixelType.UnsignedByte, (void*)0); _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear); _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMinFilter.Linear); _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge); _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge); _gl.BindTexture(TextureTarget.Texture2D, 0); _depthRbo = _gl.GenRenderbuffer(); _gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthRbo); _gl.RenderbufferStorage(RenderbufferTarget.Renderbuffer, InternalFormat.Depth24Stencil8, (uint)width, (uint)height); _gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0); _fbo = _gl.GenFramebuffer(); _gl.BindFramebuffer(FramebufferTarget.Framebuffer, _fbo); _gl.FramebufferTexture2D(FramebufferTarget.Framebuffer, FramebufferAttachment.ColorAttachment0, TextureTarget.Texture2D, _colorTex, 0); _gl.FramebufferRenderbuffer(FramebufferTarget.Framebuffer, FramebufferAttachment.DepthStencilAttachment, RenderbufferTarget.Renderbuffer, _depthRbo); var status = _gl.CheckFramebufferStatus(FramebufferTarget.Framebuffer); _gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0); if (status != GLEnum.FramebufferComplete) { Console.WriteLine($"[paperdoll] framebuffer incomplete: {status} ({width}x{height})"); DeleteFramebuffer(); } } private void DeleteFramebuffer() { if (_fbo != 0) { _gl.DeleteFramebuffer(_fbo); _fbo = 0; } if (_colorTex != 0) { _gl.DeleteTexture(_colorTex); _colorTex = 0; } if (_depthRbo != 0) { _gl.DeleteRenderbuffer(_depthRbo); _depthRbo = 0; } _fbW = _fbH = 0; } public void Dispose() => DeleteFramebuffer(); }