feat(ui): port retail creature appraisal presentation
Render assessed creatures through the shared private viewport with retail heading, bounding-box camera, and light. Build the exact authored nine-row stat list and resolve creature names from the retail EnumMapper while keeping remaining font/sequencer adaptations explicit. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
f1a7912160
commit
7eaa68a5f4
26 changed files with 2780 additions and 237 deletions
407
src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs
Normal file
407
src/AcDream.App/Rendering/CreatureAppraisalPresentation.cs
Normal file
|
|
@ -0,0 +1,407 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Lighting;
|
||||
using AcDream.Core.World;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal interface ICreatureAppraisalRenderer
|
||||
{
|
||||
void SetCreature(
|
||||
WorldEntity? creature,
|
||||
Vector3 boundsMin,
|
||||
Vector3 boundsMax);
|
||||
|
||||
uint Render(int width, int height);
|
||||
}
|
||||
|
||||
internal interface ICreatureAppraisalFrameView
|
||||
{
|
||||
bool TryGetVisibleTarget(
|
||||
out uint serverGuid,
|
||||
out int width,
|
||||
out int height);
|
||||
|
||||
void SetTextureHandle(uint textureHandle);
|
||||
}
|
||||
|
||||
internal interface ICreatureAppraisalEntityLookup
|
||||
{
|
||||
bool TryGet(uint serverGuid, out WorldEntity entity);
|
||||
}
|
||||
|
||||
internal interface ICreatureAppraisalCloneFactory
|
||||
{
|
||||
bool TrySynchronize(
|
||||
uint serverGuid,
|
||||
WorldEntity? currentClone,
|
||||
out WorldEntity? synchronizedClone,
|
||||
out Vector3 boundsMin,
|
||||
out Vector3 boundsMax);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-frame owner for retail's examination <c>CreatureMode</c>. Unlike the
|
||||
/// inventory doll's held pose, the examination clone follows the live target's
|
||||
/// current animated part transforms every frame.
|
||||
/// </summary>
|
||||
internal sealed class CreatureAppraisalFramePresenter :
|
||||
IPrivateEntityViewportFrame
|
||||
{
|
||||
private readonly ICreatureAppraisalRenderer _renderer;
|
||||
private readonly ICreatureAppraisalFrameView _view;
|
||||
private readonly ICreatureAppraisalCloneFactory _factory;
|
||||
private WorldEntity? _clone;
|
||||
private uint _serverGuid;
|
||||
|
||||
public CreatureAppraisalFramePresenter(
|
||||
ICreatureAppraisalRenderer renderer,
|
||||
ICreatureAppraisalFrameView view,
|
||||
ICreatureAppraisalCloneFactory factory)
|
||||
{
|
||||
_renderer = renderer ?? throw new ArgumentNullException(nameof(renderer));
|
||||
_view = view ?? throw new ArgumentNullException(nameof(view));
|
||||
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
}
|
||||
|
||||
public void Render()
|
||||
{
|
||||
if (!_view.TryGetVisibleTarget(
|
||||
out uint serverGuid,
|
||||
out int width,
|
||||
out int height))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_serverGuid != serverGuid)
|
||||
{
|
||||
_serverGuid = serverGuid;
|
||||
_clone = null;
|
||||
}
|
||||
|
||||
if (!_factory.TrySynchronize(
|
||||
serverGuid,
|
||||
_clone,
|
||||
out WorldEntity? synchronized,
|
||||
out Vector3 boundsMin,
|
||||
out Vector3 boundsMax))
|
||||
{
|
||||
_clone = null;
|
||||
_renderer.SetCreature(null, Vector3.Zero, Vector3.Zero);
|
||||
_view.SetTextureHandle(0u);
|
||||
return;
|
||||
}
|
||||
|
||||
_clone = synchronized;
|
||||
_renderer.SetCreature(_clone, boundsMin, boundsMax);
|
||||
_view.SetTextureHandle(_renderer.Render(width, height));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retained-view adapter for LayoutDesc 0x2100006B element 0x10000148.
|
||||
/// Parent visibility is checked explicitly because the viewport remains locally
|
||||
/// visible while retail switches the examination subview to an item.
|
||||
/// </summary>
|
||||
internal sealed class RetailCreatureAppraisalFrameView :
|
||||
ICreatureAppraisalFrameView
|
||||
{
|
||||
private readonly UiViewport _viewport;
|
||||
private readonly UiElement _windowFrame;
|
||||
private readonly AppraisalUiController _controller;
|
||||
|
||||
public RetailCreatureAppraisalFrameView(
|
||||
UiViewport viewport,
|
||||
UiElement windowFrame,
|
||||
AppraisalUiController controller)
|
||||
{
|
||||
_viewport = viewport ?? throw new ArgumentNullException(nameof(viewport));
|
||||
_windowFrame = windowFrame ?? throw new ArgumentNullException(nameof(windowFrame));
|
||||
_controller = controller ?? throw new ArgumentNullException(nameof(controller));
|
||||
}
|
||||
|
||||
public bool TryGetVisibleTarget(
|
||||
out uint serverGuid,
|
||||
out int width,
|
||||
out int height)
|
||||
{
|
||||
serverGuid = 0u;
|
||||
width = 0;
|
||||
height = 0;
|
||||
if (_controller.ActiveView is not (
|
||||
AppraisalView.Creature or AppraisalView.Character)
|
||||
|| !IsEffectivelyVisible(_windowFrame)
|
||||
|| !IsEffectivelyVisible(_viewport)
|
||||
|| _controller.CurrentObjectId == 0u)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
serverGuid = _controller.CurrentObjectId;
|
||||
width = (int)_viewport.Width;
|
||||
height = (int)_viewport.Height;
|
||||
return width > 0 && height > 0;
|
||||
}
|
||||
|
||||
public void SetTextureHandle(uint textureHandle) =>
|
||||
_viewport.TextureHandle = textureHandle;
|
||||
|
||||
private static bool IsEffectivelyVisible(UiElement element)
|
||||
{
|
||||
for (UiElement? current = element; current is not null; current = current.Parent)
|
||||
if (!current.Visible)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class LiveCreatureAppraisalEntityLookup :
|
||||
ICreatureAppraisalEntityLookup
|
||||
{
|
||||
private readonly LiveEntityRuntime _liveEntities;
|
||||
|
||||
public LiveCreatureAppraisalEntityLookup(LiveEntityRuntime liveEntities)
|
||||
{
|
||||
_liveEntities = liveEntities
|
||||
?? throw new ArgumentNullException(nameof(liveEntities));
|
||||
}
|
||||
|
||||
public bool TryGet(uint serverGuid, out WorldEntity entity) =>
|
||||
_liveEntities.TryGetWorldEntity(serverGuid, out entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>BasicCreatureExamineUI::Init @ 0x004AB9C0</c> clones the selected
|
||||
/// physics object, fixes its heading at 191.367905 degrees, and lets the private
|
||||
/// CreatureMode animate that clone. ACDream shares the already-resolved live
|
||||
/// MeshRefs and updates that reference every frame, giving the private clone the
|
||||
/// same current animated pose without registering a second gameplay entity.
|
||||
/// </summary>
|
||||
internal sealed class RetailCreatureAppraisalCloneFactory :
|
||||
ICreatureAppraisalCloneFactory
|
||||
{
|
||||
private readonly ICreatureAppraisalEntityLookup _entities;
|
||||
|
||||
public RetailCreatureAppraisalCloneFactory(
|
||||
ICreatureAppraisalEntityLookup entities)
|
||||
{
|
||||
_entities = entities ?? throw new ArgumentNullException(nameof(entities));
|
||||
}
|
||||
|
||||
public bool TrySynchronize(
|
||||
uint serverGuid,
|
||||
WorldEntity? currentClone,
|
||||
out WorldEntity? synchronizedClone,
|
||||
out Vector3 boundsMin,
|
||||
out Vector3 boundsMax)
|
||||
{
|
||||
synchronizedClone = null;
|
||||
boundsMin = Vector3.Zero;
|
||||
boundsMax = Vector3.Zero;
|
||||
if (!_entities.TryGet(serverGuid, out WorldEntity source)
|
||||
|| source.MeshRefs.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
WorldEntity clone = currentClone is not null
|
||||
&& currentClone.SourceGfxObjOrSetupId == source.SourceGfxObjOrSetupId
|
||||
? currentClone
|
||||
: CreatureAppraisalEntityBuilder.Build(source);
|
||||
|
||||
clone.ApplyAppearance(
|
||||
source.MeshRefs,
|
||||
source.PaletteOverride,
|
||||
source.PartOverrides);
|
||||
clone.IsDrawVisible = source.IsDrawVisible;
|
||||
clone.IsAncestorDrawVisible = source.IsAncestorDrawVisible;
|
||||
if (source.HasLocalBounds)
|
||||
clone.SetLocalBounds(source.LocalBoundMin, source.LocalBoundMax);
|
||||
|
||||
(boundsMin, boundsMax) =
|
||||
CreatureAppraisalEntityBuilder.RotatedBounds(source);
|
||||
synchronizedClone = clone;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal static class CreatureAppraisalEntityBuilder
|
||||
{
|
||||
public const uint RenderId = 0xDA11_D022u;
|
||||
public const uint ServerGuid = 0xDA11_D021u;
|
||||
private const float HeadingDegrees = 191.367905f;
|
||||
private static readonly Quaternion Heading = Quaternion.CreateFromAxisAngle(
|
||||
Vector3.UnitZ,
|
||||
-HeadingDegrees * (MathF.PI / 180f));
|
||||
|
||||
public static WorldEntity Build(WorldEntity source)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
var clone = new WorldEntity
|
||||
{
|
||||
Id = RenderId,
|
||||
ServerGuid = ServerGuid,
|
||||
SourceGfxObjOrSetupId = source.SourceGfxObjOrSetupId,
|
||||
Position = Vector3.Zero,
|
||||
Rotation = Heading,
|
||||
MeshRefs = source.MeshRefs,
|
||||
PaletteOverride = source.PaletteOverride,
|
||||
PartOverrides = source.PartOverrides,
|
||||
HiddenPartsMask = source.HiddenPartsMask,
|
||||
Scale = source.Scale,
|
||||
ParentCellId = null,
|
||||
EffectCellId = null,
|
||||
};
|
||||
if (source.HasLocalBounds)
|
||||
clone.SetLocalBounds(source.LocalBoundMin, source.LocalBoundMax);
|
||||
return clone;
|
||||
}
|
||||
|
||||
public static (Vector3 Min, Vector3 Max) RotatedBounds(
|
||||
WorldEntity source)
|
||||
{
|
||||
Vector3 min;
|
||||
Vector3 max;
|
||||
if (source.HasLocalBounds)
|
||||
{
|
||||
min = source.LocalBoundMin;
|
||||
max = source.LocalBoundMax;
|
||||
}
|
||||
else
|
||||
{
|
||||
min = new Vector3(-0.5f, -0.5f, 0f);
|
||||
max = new Vector3(0.5f, 0.5f, 2f);
|
||||
foreach (MeshRef mesh in source.MeshRefs)
|
||||
{
|
||||
Vector3 p = mesh.PartTransform.Translation;
|
||||
min = Vector3.Min(min, p - new Vector3(0.5f));
|
||||
max = Vector3.Max(max, p + new Vector3(0.5f));
|
||||
}
|
||||
}
|
||||
|
||||
Vector3 rotatedMin = default;
|
||||
Vector3 rotatedMax = default;
|
||||
for (int corner = 0; corner < 8; corner++)
|
||||
{
|
||||
Vector3 point = new(
|
||||
(corner & 1) == 0 ? min.X : max.X,
|
||||
(corner & 2) == 0 ? min.Y : max.Y,
|
||||
(corner & 4) == 0 ? min.Z : max.Z);
|
||||
point = Vector3.Transform(point, Heading);
|
||||
if (corner == 0)
|
||||
rotatedMin = rotatedMax = point;
|
||||
else
|
||||
{
|
||||
rotatedMin = Vector3.Min(rotatedMin, point);
|
||||
rotatedMax = Vector3.Max(rotatedMax, point);
|
||||
}
|
||||
}
|
||||
return (rotatedMin, rotatedMax);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bounding-box camera from <c>BasicCreatureExamineUI::Init @ 0x004AB9C0</c>.
|
||||
/// The camera keeps retail's identity direction (+Y) and 45-degree vertical
|
||||
/// field of view. Its distance is the fitted vertical span times 1.20710683,
|
||||
/// plus half the creature's front-to-back depth.
|
||||
/// </summary>
|
||||
internal sealed class CreatureAppraisalCamera :
|
||||
IPrivateEntityViewportCamera
|
||||
{
|
||||
private const float FitDistanceFactor = 1.20710683f;
|
||||
private Vector3 _boundsMin = new(-0.5f, -0.5f, 0f);
|
||||
private Vector3 _boundsMax = new(0.5f, 0.5f, 2f);
|
||||
private float _aspect = 1f;
|
||||
private Vector3 _eye;
|
||||
|
||||
public CreatureAppraisalCamera() => Recalculate();
|
||||
|
||||
public float Aspect
|
||||
{
|
||||
get => _aspect;
|
||||
set
|
||||
{
|
||||
_aspect = float.IsFinite(value) && value > 0f ? value : 1f;
|
||||
Recalculate();
|
||||
}
|
||||
}
|
||||
|
||||
public Vector3 Eye => _eye;
|
||||
public float FovRadians { get; set; } = MathF.PI / 4f;
|
||||
public float Near { get; set; } = 0.1f;
|
||||
public float Far { get; set; } = 2048f;
|
||||
|
||||
public Matrix4x4 View =>
|
||||
Matrix4x4.CreateLookAt(_eye, _eye + Vector3.UnitY, Vector3.UnitZ);
|
||||
|
||||
public Matrix4x4 Projection => Matrix4x4.CreatePerspectiveFieldOfView(
|
||||
FovRadians,
|
||||
_aspect,
|
||||
Near,
|
||||
Far);
|
||||
|
||||
public void Fit(Vector3 min, Vector3 max)
|
||||
{
|
||||
_boundsMin = Vector3.Min(min, max);
|
||||
_boundsMax = Vector3.Max(min, max);
|
||||
Recalculate();
|
||||
}
|
||||
|
||||
private void Recalculate()
|
||||
{
|
||||
Vector3 span = Vector3.Max(
|
||||
_boundsMax - _boundsMin,
|
||||
new Vector3(0.001f));
|
||||
Vector3 center = (_boundsMin + _boundsMax) * 0.5f;
|
||||
float fittedVertical = MathF.Max(span.Z, span.X / _aspect);
|
||||
float distance = fittedVertical * FitDistanceFactor + span.Y * 0.5f;
|
||||
_eye = new Vector3(center.X, center.Y - distance, center.Z);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Examination-specific facade over the shared private renderer.</summary>
|
||||
internal sealed class CreatureAppraisalViewportRenderer :
|
||||
IUiViewportRenderer,
|
||||
ICreatureAppraisalRenderer,
|
||||
IDisposable
|
||||
{
|
||||
private readonly CreatureAppraisalCamera _camera = new();
|
||||
private readonly PrivateEntityViewportRenderer _renderer;
|
||||
|
||||
public CreatureAppraisalViewportRenderer(
|
||||
GL gl,
|
||||
WbDrawDispatcher dispatcher,
|
||||
SceneLightingUboBinding lightUbo,
|
||||
IEntityTextureLifetime textureLifetime)
|
||||
{
|
||||
_renderer = new PrivateEntityViewportRenderer(
|
||||
gl,
|
||||
dispatcher,
|
||||
lightUbo,
|
||||
textureLifetime,
|
||||
CreatureAppraisalEntityBuilder.RenderId,
|
||||
_camera,
|
||||
"creature examination");
|
||||
}
|
||||
|
||||
public void SetCreature(
|
||||
WorldEntity? creature,
|
||||
Vector3 boundsMin,
|
||||
Vector3 boundsMax)
|
||||
{
|
||||
if (creature is not null)
|
||||
_camera.Fit(boundsMin, boundsMax);
|
||||
_renderer.SetEntity(creature);
|
||||
}
|
||||
|
||||
public uint Render(int width, int height) =>
|
||||
_renderer.Render(width, height);
|
||||
|
||||
public void Dispose() => _renderer.Dispose();
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ namespace AcDream.App.Rendering;
|
|||
public sealed class DollCamera : ICamera
|
||||
{
|
||||
// Retail paperdoll camera origin (decomp 0x004a5a51–0x004a5a61).
|
||||
private static readonly Vector3 Eye = new(0.12f, -2.4f, 0.88f);
|
||||
internal static readonly Vector3 RetailEye = new(0.12f, -2.4f, 0.88f);
|
||||
// Identity view orientation ⇒ look straight down +Y (no yaw/pitch). Target = Eye + (0,1,0).
|
||||
private static readonly Vector3 Target = new(0.12f, -1.4f, 0.88f);
|
||||
private static readonly Vector3 Up = Vector3.UnitZ; // AC up-axis = +Z, same as ChaseCamera
|
||||
|
|
@ -45,9 +45,42 @@ public sealed class DollCamera : ICamera
|
|||
|
||||
/// <inheritdoc/>
|
||||
public Matrix4x4 View =>
|
||||
Matrix4x4.CreateLookAt(Eye, Target, Up);
|
||||
Matrix4x4.CreateLookAt(RetailEye, Target, Up);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Matrix4x4 Projection =>
|
||||
Matrix4x4.CreatePerspectiveFieldOfView(FovRadians, Aspect <= 0f ? 1f : Aspect, Near, Far);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal private-viewport adapter that preserves <see cref="DollCamera"/>'s
|
||||
/// existing public camera contract.
|
||||
/// </summary>
|
||||
internal sealed class DollViewportCamera : IPrivateEntityViewportCamera
|
||||
{
|
||||
private readonly DollCamera _camera = new();
|
||||
|
||||
public Vector3 Eye => DollCamera.RetailEye;
|
||||
public float FovRadians
|
||||
{
|
||||
get => _camera.FovRadians;
|
||||
set => _camera.FovRadians = value;
|
||||
}
|
||||
public float Near
|
||||
{
|
||||
get => _camera.Near;
|
||||
set => _camera.Near = value;
|
||||
}
|
||||
public float Far
|
||||
{
|
||||
get => _camera.Far;
|
||||
set => _camera.Far = value;
|
||||
}
|
||||
public float Aspect
|
||||
{
|
||||
get => _camera.Aspect;
|
||||
set => _camera.Aspect = value;
|
||||
}
|
||||
public Matrix4x4 View => _camera.View;
|
||||
public Matrix4x4 Projection => _camera.Projection;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -378,6 +378,10 @@ public sealed class GameWindow :
|
|||
// widget that blits it, the inventory frame (for the open-gate), and a dirty flag (re-dress on 0xF625).
|
||||
private AcDream.App.Rendering.PaperdollViewportRenderer? _paperdollViewportRenderer;
|
||||
private AcDream.App.Rendering.PaperdollFramePresenter? _paperdollFramePresenter;
|
||||
private AcDream.App.Rendering.CreatureAppraisalViewportRenderer?
|
||||
_creatureAppraisalViewportRenderer;
|
||||
private AcDream.App.Rendering.CreatureAppraisalFramePresenter?
|
||||
_creatureAppraisalFramePresenter;
|
||||
// Phase D.2b Task 9 — plugin UI registrations buffered before OnLoad; drained in OnLoad.
|
||||
private readonly AcDream.App.Plugins.BufferedUiRegistry? _uiRegistry;
|
||||
// Phase I.2: ImGui debug panel ViewModel. The devtools presenter owns
|
||||
|
|
@ -948,6 +952,8 @@ public sealed class GameWindow :
|
|||
|| _animationPresenter is not null
|
||||
|| _paperdollViewportRenderer is not null
|
||||
|| _paperdollFramePresenter is not null
|
||||
|| _creatureAppraisalViewportRenderer is not null
|
||||
|| _creatureAppraisalFramePresenter is not null
|
||||
|| _envCellRenderer is not null
|
||||
|| _envCellFrustum is not null
|
||||
|| _landblockPresentationPipeline is not null
|
||||
|
|
@ -983,6 +989,8 @@ public sealed class GameWindow :
|
|||
_retainedUiGameplayBinding = result.RetainedGameplay;
|
||||
_paperdollViewportRenderer = result.PaperdollRenderer;
|
||||
_paperdollFramePresenter = result.PaperdollPresenter;
|
||||
_creatureAppraisalViewportRenderer = result.CreatureAppraisalRenderer;
|
||||
_creatureAppraisalFramePresenter = result.CreatureAppraisalPresenter;
|
||||
_envCellFrustum = result.EnvCellFrustum;
|
||||
_envCellRenderer = result.EnvCellRenderer;
|
||||
_landblockPresentationPipeline = result.LandblockPipeline;
|
||||
|
|
@ -1581,6 +1589,7 @@ public sealed class GameWindow :
|
|||
_localPlayerTeleport,
|
||||
_portalTunnelFallback,
|
||||
_paperdollViewportRenderer,
|
||||
_creatureAppraisalViewportRenderer,
|
||||
_wbDrawDispatcher,
|
||||
_envCellRenderer,
|
||||
_portalDepthMask,
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ internal sealed record RenderShutdownRoots(
|
|||
LocalPlayerTeleportController? LocalTeleport,
|
||||
TransferableResourceSlot<PortalTunnelPresentation> PortalTunnelFallback,
|
||||
PaperdollViewportRenderer? Paperdoll,
|
||||
CreatureAppraisalViewportRenderer? CreatureAppraisal,
|
||||
WbDrawDispatcher? DrawDispatcher,
|
||||
EnvCellRenderer? EnvironmentCells,
|
||||
PortalDepthMaskRenderer? PortalDepthMask,
|
||||
|
|
@ -413,6 +414,9 @@ internal static class GameWindowShutdownManifest
|
|||
render.PortalTunnelFallback.ReleaseFallback();
|
||||
}),
|
||||
Hard("paperdoll viewport", () => render.Paperdoll?.Dispose()),
|
||||
Hard(
|
||||
"creature appraisal viewport",
|
||||
() => render.CreatureAppraisal?.Dispose()),
|
||||
Hard("mesh draw dispatcher", () => render.DrawDispatcher?.Dispose()),
|
||||
Hard("environment cells", () => render.EnvironmentCells?.Dispose()),
|
||||
Hard("portal depth mask", () => render.PortalDepthMask?.Dispose()),
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ internal interface IPaperdollPoseApplicator
|
|||
/// The GL renderer remains a borrowed resource disposed by the existing window
|
||||
/// shutdown transaction.
|
||||
/// </summary>
|
||||
internal sealed class PaperdollFramePresenter : IPrivatePaperdollFrame
|
||||
internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
|
||||
{
|
||||
private readonly IPaperdollDollRenderer _renderer;
|
||||
private readonly IPaperdollFrameView _view;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Core.Lighting;
|
||||
|
|
@ -10,52 +7,15 @@ 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 <see cref="PaperdollFramePresenter"/>) 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, 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 held pose.</para>
|
||||
/// Paperdoll-specific facade over the shared private creature viewport. The
|
||||
/// fixed camera remains the verbatim retail <c>gmPaperDollUI</c> camera.
|
||||
/// </summary>
|
||||
public sealed unsafe class PaperdollViewportRenderer :
|
||||
public sealed class PaperdollViewportRenderer :
|
||||
IUiViewportRenderer,
|
||||
IPaperdollDollRenderer,
|
||||
IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly WbDrawDispatcher _dispatcher;
|
||||
private readonly SceneLightingUboBinding _lightUbo;
|
||||
private readonly FixedEntityTextureOwnerLease _textureOwnerLease;
|
||||
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 static held-pose doll. 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<uint> _dollAnimatedIds = new() { DollEntityBuilder.DollRenderId };
|
||||
private readonly PrivateEntityViewportRenderer _renderer;
|
||||
|
||||
public PaperdollViewportRenderer(
|
||||
GL gl,
|
||||
|
|
@ -63,151 +23,20 @@ public sealed unsafe class PaperdollViewportRenderer :
|
|||
SceneLightingUboBinding lightUbo,
|
||||
IEntityTextureLifetime textureLifetime)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
_lightUbo = lightUbo ?? throw new ArgumentNullException(nameof(lightUbo));
|
||||
_textureOwnerLease = new FixedEntityTextureOwnerLease(
|
||||
_renderer = new PrivateEntityViewportRenderer(
|
||||
gl,
|
||||
dispatcher,
|
||||
lightUbo,
|
||||
textureLifetime,
|
||||
DollEntityBuilder.DollRenderId);
|
||||
DollEntityBuilder.DollRenderId,
|
||||
new DollViewportCamera(),
|
||||
"paperdoll");
|
||||
}
|
||||
|
||||
/// <summary>Set (or clear) the doll entity built from the live player projection.</summary>
|
||||
public void SetDoll(WorldEntity? doll)
|
||||
{
|
||||
if (ReferenceEquals(_doll, doll))
|
||||
return;
|
||||
_textureOwnerLease.Replace(doll is not null);
|
||||
_doll = doll;
|
||||
}
|
||||
public void SetDoll(WorldEntity? doll) => _renderer.SetEntity(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;
|
||||
public uint Render(int width, int height) =>
|
||||
_renderer.Render(width, height);
|
||||
|
||||
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: _dollAnimatedIds); // doll treated as animated ⇒ cache-bypass (re-dress correctness)
|
||||
|
||||
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.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);
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
_doll = null;
|
||||
_textureOwnerLease.Dispose();
|
||||
DeleteFramebuffer();
|
||||
}
|
||||
public void Dispose() => _renderer.Dispose();
|
||||
}
|
||||
|
|
|
|||
252
src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs
Normal file
252
src/AcDream.App/Rendering/PrivateEntityViewportRenderer.cs
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
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>
|
||||
/// Camera contract for retail <c>CreatureMode</c>-style UI viewports. The
|
||||
/// renderer supplies the authored viewport aspect before each draw and uses
|
||||
/// <see cref="Eye"/> when it publishes the private scene lighting UBO.
|
||||
/// </summary>
|
||||
internal interface IPrivateEntityViewportCamera : ICamera
|
||||
{
|
||||
Vector3 Eye { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shared render-to-texture implementation for the private 3-D creature
|
||||
/// viewports used by paperdoll and examination UI. Each instance owns one FBO,
|
||||
/// one synthetic render identity, and one balanced texture-owner lease.
|
||||
/// </summary>
|
||||
internal sealed unsafe class PrivateEntityViewportRenderer :
|
||||
IUiViewportRenderer,
|
||||
IDisposable
|
||||
{
|
||||
private const uint PrivateLandblockId = 0u;
|
||||
|
||||
private readonly GL _gl;
|
||||
private readonly WbDrawDispatcher _dispatcher;
|
||||
private readonly SceneLightingUboBinding _lightUbo;
|
||||
private readonly FixedEntityTextureOwnerLease _textureOwnerLease;
|
||||
private readonly IPrivateEntityViewportCamera _camera;
|
||||
private readonly HashSet<uint> _animatedIds;
|
||||
private readonly string _diagnosticName;
|
||||
|
||||
private uint _fbo;
|
||||
private uint _colorTex;
|
||||
private uint _depthRbo;
|
||||
private int _fbW;
|
||||
private int _fbH;
|
||||
private WorldEntity? _entity;
|
||||
|
||||
public PrivateEntityViewportRenderer(
|
||||
GL gl,
|
||||
WbDrawDispatcher dispatcher,
|
||||
SceneLightingUboBinding lightUbo,
|
||||
IEntityTextureLifetime textureLifetime,
|
||||
uint renderId,
|
||||
IPrivateEntityViewportCamera camera,
|
||||
string diagnosticName)
|
||||
{
|
||||
if (renderId == 0u)
|
||||
throw new ArgumentOutOfRangeException(nameof(renderId));
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
||||
_lightUbo = lightUbo ?? throw new ArgumentNullException(nameof(lightUbo));
|
||||
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
||||
_diagnosticName = string.IsNullOrWhiteSpace(diagnosticName)
|
||||
? "creature viewport"
|
||||
: diagnosticName;
|
||||
_animatedIds = [renderId];
|
||||
_textureOwnerLease = new FixedEntityTextureOwnerLease(
|
||||
textureLifetime ?? throw new ArgumentNullException(nameof(textureLifetime)),
|
||||
renderId);
|
||||
}
|
||||
|
||||
public void SetEntity(WorldEntity? entity)
|
||||
{
|
||||
if (ReferenceEquals(_entity, entity))
|
||||
return;
|
||||
_textureOwnerLease.Replace(entity is not null);
|
||||
_entity = entity;
|
||||
}
|
||||
|
||||
public uint Render(int width, int height)
|
||||
{
|
||||
WorldEntity? entity = _entity;
|
||||
if (entity is null || entity.MeshRefs.Count == 0 || width <= 0 || height <= 0)
|
||||
return 0u;
|
||||
|
||||
EnsureFramebuffer(width, height);
|
||||
if (_fbo == 0u)
|
||||
return 0u;
|
||||
_camera.Aspect = width / (float)height;
|
||||
|
||||
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);
|
||||
_gl.ClearDepth(1.0);
|
||||
_gl.DepthMask(true);
|
||||
_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);
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
|
||||
UploadCreatureLight();
|
||||
|
||||
WorldEntity[] entities = [entity];
|
||||
var entries =
|
||||
new (uint, Vector3, Vector3, IReadOnlyList<WorldEntity>,
|
||||
IReadOnlyDictionary<uint, WorldEntity>?)[]
|
||||
{
|
||||
(
|
||||
PrivateLandblockId,
|
||||
new Vector3(-1024f),
|
||||
new Vector3(1024f),
|
||||
entities,
|
||||
null),
|
||||
};
|
||||
|
||||
_dispatcher.Draw(
|
||||
_camera,
|
||||
entries,
|
||||
frustum: null,
|
||||
neverCullLandblockId: PrivateLandblockId,
|
||||
visibleCellIds: null,
|
||||
animatedEntityIds: _animatedIds);
|
||||
return _colorTex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Both retail paperdoll and creature examination call
|
||||
/// <c>UIElement_Viewport::SetLight(DISTANT_LIGHT, 2, (0.3,1.9,0.65))</c>.
|
||||
/// </summary>
|
||||
private void UploadCreatureLight()
|
||||
{
|
||||
Vector3 direction = Vector3.Normalize(new Vector3(0.3f, 1.9f, 0.65f));
|
||||
_lightUbo.Upload(new SceneLightingUbo
|
||||
{
|
||||
Light0 = new UboLight
|
||||
{
|
||||
PosAndKind = Vector4.Zero,
|
||||
DirAndRange = new Vector4(direction, 1e9f),
|
||||
ColorAndIntensity = new Vector4(1f, 1f, 1f, 2f),
|
||||
ConeAngleEtc = Vector4.Zero,
|
||||
},
|
||||
CellAmbient = new Vector4(0.3f, 0.3f, 0.3f, 1f),
|
||||
FogParams = new Vector4(1e9f, 1e9f, 0f, 0f),
|
||||
FogColor = Vector4.Zero,
|
||||
CameraAndTime = new Vector4(_camera.Eye, 0f),
|
||||
});
|
||||
}
|
||||
|
||||
private void EnsureFramebuffer(int width, int height)
|
||||
{
|
||||
if (_fbo != 0u && 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, 0u);
|
||||
|
||||
_depthRbo = _gl.GenRenderbuffer();
|
||||
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, _depthRbo);
|
||||
_gl.RenderbufferStorage(
|
||||
RenderbufferTarget.Renderbuffer,
|
||||
InternalFormat.Depth24Stencil8,
|
||||
(uint)width,
|
||||
(uint)height);
|
||||
_gl.BindRenderbuffer(RenderbufferTarget.Renderbuffer, 0u);
|
||||
|
||||
_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);
|
||||
|
||||
GLEnum status = _gl.CheckFramebufferStatus(
|
||||
FramebufferTarget.Framebuffer);
|
||||
_gl.BindFramebuffer(FramebufferTarget.Framebuffer, 0u);
|
||||
if (status != GLEnum.FramebufferComplete)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"[{_diagnosticName}] framebuffer incomplete: {status} ({width}x{height})");
|
||||
DeleteFramebuffer();
|
||||
}
|
||||
}
|
||||
|
||||
private void DeleteFramebuffer()
|
||||
{
|
||||
if (_fbo != 0u)
|
||||
{
|
||||
_gl.DeleteFramebuffer(_fbo);
|
||||
_fbo = 0u;
|
||||
}
|
||||
if (_colorTex != 0u)
|
||||
{
|
||||
_gl.DeleteTexture(_colorTex);
|
||||
_colorTex = 0u;
|
||||
}
|
||||
if (_depthRbo != 0u)
|
||||
{
|
||||
_gl.DeleteRenderbuffer(_depthRbo);
|
||||
_depthRbo = 0u;
|
||||
}
|
||||
_fbW = 0;
|
||||
_fbH = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_entity = null;
|
||||
_textureOwnerLease.Dispose();
|
||||
DeleteFramebuffer();
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ internal interface IPrivatePortalViewport
|
|||
void Draw(int width, int height);
|
||||
}
|
||||
|
||||
internal interface IPrivatePaperdollFrame
|
||||
internal interface IPrivateEntityViewportFrame
|
||||
{
|
||||
void Render();
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ internal sealed class PrivatePresentationRenderer : IPrivatePresentationFramePha
|
|||
{
|
||||
private readonly IPrivatePortalViewport _portal;
|
||||
private readonly IRenderFrameFoundationSource _foundation;
|
||||
private readonly IPrivatePaperdollFrame? _paperdoll;
|
||||
private readonly IPrivateEntityViewportFrame? _entityViewports;
|
||||
private readonly IRetainedGameplayUiFrame? _gameplayUi;
|
||||
private readonly IDevToolsFrameLifecycle? _devTools;
|
||||
private readonly IPrivateFrameScreenshot? _screenshots;
|
||||
|
|
@ -43,7 +43,7 @@ internal sealed class PrivatePresentationRenderer : IPrivatePresentationFramePha
|
|||
public PrivatePresentationRenderer(
|
||||
IPrivatePortalViewport portal,
|
||||
IRenderFrameFoundationSource foundation,
|
||||
IPrivatePaperdollFrame? paperdoll,
|
||||
IPrivateEntityViewportFrame? entityViewports,
|
||||
IRetainedGameplayUiFrame? gameplayUi,
|
||||
IDevToolsFrameLifecycle? devTools,
|
||||
IPrivateFrameScreenshot? screenshots)
|
||||
|
|
@ -51,7 +51,7 @@ internal sealed class PrivatePresentationRenderer : IPrivatePresentationFramePha
|
|||
_portal = portal ?? throw new ArgumentNullException(nameof(portal));
|
||||
_foundation = foundation
|
||||
?? throw new ArgumentNullException(nameof(foundation));
|
||||
_paperdoll = paperdoll;
|
||||
_entityViewports = entityViewports;
|
||||
_gameplayUi = gameplayUi;
|
||||
_devTools = devTools;
|
||||
_screenshots = screenshots;
|
||||
|
|
@ -68,7 +68,7 @@ internal sealed class PrivatePresentationRenderer : IPrivatePresentationFramePha
|
|||
bool portalViewportVisible =
|
||||
_foundation.Foundation.PortalViewportVisible;
|
||||
_portal.Draw(input.ViewportWidth, input.ViewportHeight);
|
||||
_paperdoll?.Render();
|
||||
_entityViewports?.Render();
|
||||
_gameplayUi?.Render(
|
||||
input.DeltaSeconds,
|
||||
input.ViewportWidth,
|
||||
|
|
@ -86,6 +86,30 @@ internal sealed class PrivatePresentationRenderer : IPrivatePresentationFramePha
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ordered private creature-view presentation. Retail renders each visible
|
||||
/// <c>CreatureMode</c> before the retained 2-D tree that displays its texture.
|
||||
/// </summary>
|
||||
internal sealed class PrivateEntityViewportFrameGroup :
|
||||
IPrivateEntityViewportFrame
|
||||
{
|
||||
private readonly IPrivateEntityViewportFrame[] _frames;
|
||||
|
||||
public PrivateEntityViewportFrameGroup(
|
||||
params IPrivateEntityViewportFrame?[] frames)
|
||||
{
|
||||
_frames = frames.Where(static frame => frame is not null)
|
||||
.Cast<IPrivateEntityViewportFrame>()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public void Render()
|
||||
{
|
||||
foreach (IPrivateEntityViewportFrame frame in _frames)
|
||||
frame.Render();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Typed portal viewport adapter over the canonical teleport owner.</summary>
|
||||
internal sealed class LocalPlayerPortalViewport : IPrivatePortalViewport
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue