feat(D.2b): Slice 2 — PaperdollViewportRenderer RTT pass (static doll)
The C# analog of CreatureMode::Render: draws one re-dressed player clone into a private FBO (RGBA8 + depth24-stencil8) with the fixed DollCamera + one distant light (retail 0.3,1.9,0.65 @ 2.0), sealed in a GLStateScope so it can't disturb world/UI GL state. frustum:null ⇒ the doll's synthetic landblock is always visible ⇒ walked from entry.Entities and drawn from its current MeshRefs (static pose; idle animation is a later slice). Manages the FBO directly via GL (ManagedGLFramebuffer is unused/bitrotted WB infra). UiViewport blit flips V (FBO bottom-left origin vs UI top-left) so the doll isn't upside-down. Not yet wired — GameWindow hook is next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
362d41aacf
commit
3cdecb536b
2 changed files with 191 additions and 2 deletions
187
src/AcDream.App/Rendering/PaperdollViewportRenderer.cs
Normal file
187
src/AcDream.App/Rendering/PaperdollViewportRenderer.cs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
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;
|
||||
|
||||
/// <summary>
|
||||
/// Render-to-texture renderer for the paperdoll 3-D doll (the C# analog of retail
|
||||
/// <c>CreatureMode::Render</c>). Draws ONE re-dressed player clone (a <see cref="WorldEntity"/>,
|
||||
/// built by <see cref="DollEntityBuilder"/> + wired by GameWindow) into a private off-screen
|
||||
/// framebuffer with a fixed <see cref="DollCamera"/> + one distant light, then hands the color
|
||||
/// texture to the <see cref="UiViewport"/> widget which blits it as a normal 2-D sprite.
|
||||
///
|
||||
/// <para>The whole 3-D pass is sealed in a <see cref="GLStateScope"/> 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.</para>
|
||||
///
|
||||
/// <para>Reuses the existing <see cref="WbDrawDispatcher"/> + the player's already-resolved,
|
||||
/// already-GPU-resident MeshRefs (the doll clones them), so no separate mesh/texture upload is
|
||||
/// needed. With <c>frustum: null</c> the doll's synthetic landblock is always "visible" and the
|
||||
/// entity is walked from <c>entry.Entities</c> and drawn from its current MeshRefs — a STATIC pose
|
||||
/// for now; idle animation (TickAnimations rebuilding the doll's MeshRefs) is a later slice.</para>
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
/// <summary>Set (or clear) the doll entity. GameWindow builds/re-dresses it from the live player.</summary>
|
||||
public void SetDoll(WorldEntity? doll) => _doll = doll;
|
||||
|
||||
/// <inheritdoc/>
|
||||
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<WorldEntity>, IReadOnlyDictionary<uint, WorldEntity>?)[]
|
||||
{
|
||||
(DollLandblockId, new Vector3(-4f, -4f, -4f), new Vector3(4f, 4f, 4f), entities, null),
|
||||
};
|
||||
|
||||
_dispatcher.Draw(
|
||||
_camera,
|
||||
entries,
|
||||
frustum: null,
|
||||
neverCullLandblockId: DollLandblockId,
|
||||
visibleCellIds: null,
|
||||
animatedEntityIds: null);
|
||||
|
||||
return _colorTex;
|
||||
}
|
||||
|
||||
/// <summary>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).</summary>
|
||||
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.40f, 0.40f, 0.40f, 1f), // 1 active light; ambient tunable at the gate
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>(Re)create the FBO + color texture + depth-stencil renderbuffer when the requested
|
||||
/// size changes. Color is RGBA8 with linear filtering + clamp; depth is Depth24Stencil8.</summary>
|
||||
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();
|
||||
}
|
||||
|
|
@ -19,7 +19,9 @@ public sealed class UiViewport : UiElement
|
|||
{
|
||||
if (!Visible || TextureHandle == 0) return;
|
||||
// Local origin is already at this widget's Left/Top (PushTransform applied by DrawSelfAndChildren).
|
||||
// Draw the full-texture sprite with UV 0..1 and white tint (no color modulation).
|
||||
ctx.DrawSprite(TextureHandle, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
// V is FLIPPED (v0=1, v1=0): TextureHandle is an off-screen FBO color texture, whose origin is
|
||||
// bottom-left (GL), while the UI sprite convention is top-left. Without the flip the doll renders
|
||||
// upside-down. (If the doll appears upside-down at the visual gate, this is the line to revisit.)
|
||||
ctx.DrawSprite(TextureHandle, 0f, 0f, Width, Height, 0f, 1f, 1f, 0f, Vector4.One);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue