294 lines
12 KiB
C#
294 lines
12 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Walk;
|
|
using AcDream.Core.Vfx;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>Campaign FW3.2b-2 (step 1): the walk driver's leaf draws over
|
|
/// this executor's renderers. Same leaf mechanics as the packed slice
|
|
/// methods — the cutover changes ORDER only.</summary>
|
|
internal sealed partial class RetailPViewPassExecutor
|
|
{
|
|
private IReadOnlySet<uint>? _walkTerrainInViewLandcells;
|
|
|
|
internal void SetWalkTerrainInViewLandcells(IReadOnlySet<uint>? cells) =>
|
|
_walkTerrainInViewLandcells = cells;
|
|
|
|
/// <summary>The walk's single sky turn: retail draws GameSky once
|
|
/// inside <c>LScape::draw</c>, clipped by the active views; looping
|
|
/// today's per-slice scissor + terrain-clip block reproduces today's
|
|
/// pixels while the driver sees ONE turn.</summary>
|
|
internal void DrawWalkSky(
|
|
RetailPViewFrameInput frame, ClipFrameAssembly clipAssembly)
|
|
{
|
|
if (!frame.RenderSky)
|
|
return;
|
|
|
|
// FW4 slice 6: an INTERIOR root draws the sky ONCE, unclipped —
|
|
// the same LScape::draw rule as the terrain turn below (view-culled
|
|
// landscape content is never view-clipped; the clear + seals +
|
|
// interior repaint own aperture exactness). The former per-slice
|
|
// scissor left stale sky color wherever the authored exit-portal
|
|
// bands were narrower than the real opening.
|
|
if (!frame.RootCell.IsOutdoorNode)
|
|
{
|
|
_sky?.RenderSky(
|
|
frame.Camera,
|
|
frame.CameraWorldPosition,
|
|
frame.DayFraction,
|
|
frame.ActiveDayGroup,
|
|
frame.SkyKeyframe,
|
|
frame.EnvironOverrideActive);
|
|
if (_particles is not null && _particleRenderer is not null)
|
|
{
|
|
_particleRenderer.Draw(
|
|
frame.Camera,
|
|
frame.CameraWorldPosition,
|
|
ParticleRenderPass.SkyPreScene);
|
|
}
|
|
return;
|
|
}
|
|
|
|
ReadOnlySpan<ClipViewSlice> slices = clipAssembly.OutsideViewSlices;
|
|
for (int sliceIndex = 0; sliceIndex < slices.Length; sliceIndex++)
|
|
{
|
|
ClipViewSlice slice = slices[sliceIndex];
|
|
bool scissor = BeginDoorwayScissor(slice.NdcAabb);
|
|
_surface.BindTerrainClip();
|
|
EnableClipDistances();
|
|
_sky?.RenderSky(
|
|
frame.Camera,
|
|
frame.CameraWorldPosition,
|
|
frame.DayFraction,
|
|
frame.ActiveDayGroup,
|
|
frame.SkyKeyframe,
|
|
frame.EnvironOverrideActive);
|
|
DisableClipDistances();
|
|
if (_particles is not null && _particleRenderer is not null)
|
|
{
|
|
_particleRenderer.Draw(
|
|
frame.Camera,
|
|
frame.CameraWorldPosition,
|
|
ParticleRenderPass.SkyPreScene);
|
|
}
|
|
if (scissor)
|
|
_surface.EndScissor();
|
|
}
|
|
}
|
|
|
|
/// <summary>The walk's per-slice terrain turn — the terrain block of
|
|
/// <c>DrawLandscapeSlice</c> (scissor + terrain clip + slice planes +
|
|
/// NDC AABB), without the sky/entity halves the walk owns separately.</summary>
|
|
internal void DrawWalkTerrainSlice(
|
|
RetailPViewFrameInput frame, ClipFrameAssembly clipAssembly, int sliceIndex)
|
|
{
|
|
ReadOnlySpan<ClipViewSlice> slices = clipAssembly.OutsideViewSlices;
|
|
if ((uint)sliceIndex >= (uint)slices.Length)
|
|
{
|
|
throw new ArgumentOutOfRangeException(
|
|
nameof(sliceIndex), sliceIndex,
|
|
$"walk terrain turn: slice {sliceIndex} of {slices.Length}");
|
|
}
|
|
|
|
// FW4 slice 6: an INTERIOR root draws the terrain UNCLIPPED —
|
|
// retail's LScape::draw draws whole blocks (view-culled, never
|
|
// view-clipped; "retail never clips ordinary meshes per view"),
|
|
// and aperture exactness comes from the depth clear + exit seals +
|
|
// interior repaint that follow in the walk's own order. The former
|
|
// per-slice scissor+clip under-painted terrain COLOR AND DEPTH
|
|
// wherever the authored exit-portal polygons are narrower than the
|
|
// real opening (the #456 cathedral seam bands) — leaving stale
|
|
// color and empty depth that unclipped alpha (the falls) then
|
|
// painted straight across. The outdoor root keeps its single
|
|
// full-screen slice (equivalent to unclipped by construction).
|
|
if (!frame.RootCell.IsOutdoorNode)
|
|
{
|
|
_terrainDiagnostics.Begin();
|
|
_terrain?.Draw(
|
|
frame.Camera,
|
|
frame.Frustum,
|
|
neverCullLandblockId: frame.PlayerLandblockId,
|
|
clipPlanes: default,
|
|
ndcClipAabb: new Vector4(-1f, -1f, 1f, 1f),
|
|
inViewLandcells: _walkTerrainInViewLandcells);
|
|
_terrainDiagnostics.Complete();
|
|
return;
|
|
}
|
|
|
|
ClipViewSlice slice = slices[sliceIndex];
|
|
bool scissor = BeginDoorwayScissor(slice.NdcAabb);
|
|
_surface.BindTerrainClip();
|
|
EnableClipDistances();
|
|
_terrainDiagnostics.Begin();
|
|
_terrain?.Draw(
|
|
frame.Camera,
|
|
frame.Frustum,
|
|
neverCullLandblockId: frame.PlayerLandblockId,
|
|
clipPlanes: slice.Planes,
|
|
ndcClipAabb: slice.NdcAabb,
|
|
inViewLandcells: _walkTerrainInViewLandcells);
|
|
_terrainDiagnostics.Complete();
|
|
DisableClipDistances();
|
|
if (scissor)
|
|
_surface.EndScissor();
|
|
}
|
|
|
|
/// <summary>The walk's punch-fan turn — <c>DrawPortalPolyInternal</c>
|
|
/// @0x0059bc90's far-Z punch through <c>PortalDepthMaskRenderer</c>,
|
|
/// clipped by the pinned view's slice planes (retail
|
|
/// <c>building_view</c> @0x0059f3bf). FW3.3: fans draw at the dat
|
|
/// aperture verbatim (the ShellDrawLiftZ retirement).</summary>
|
|
internal void DrawWalkPunchFan(
|
|
RetailPViewFrameInput frame,
|
|
ClipFrameAssembly clipAssembly,
|
|
WalkPolygon worldPolygon,
|
|
int activeViewIndex)
|
|
{
|
|
if (_portalDepthMask is null)
|
|
return;
|
|
Vector3[] vertices = worldPolygon.Vertices;
|
|
if (vertices.Length < 3)
|
|
return;
|
|
ReadOnlySpan<ClipViewSlice> slices = clipAssembly.OutsideViewSlices;
|
|
ReadOnlySpan<Vector4> planes = (uint)activeViewIndex < (uint)slices.Length
|
|
? slices[activeViewIndex].Planes
|
|
: default;
|
|
|
|
Span<Vector3> world = stackalloc Vector3[32];
|
|
int count = Math.Min(vertices.Length, world.Length);
|
|
for (int vertex = 0; vertex < count; vertex++)
|
|
world[vertex] = vertices[vertex];
|
|
_portalDepthMask.DrawDepthFan(
|
|
world[..count],
|
|
frame.ViewProjection,
|
|
planes,
|
|
forceFarZ: true);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign FW stage FW3.2b-2 (step 1, additive): the production
|
|
/// <see cref="IWalkFrameLeafRenderer"/> over this executor's leaf renderers.
|
|
/// Constructed per frame by the FW3.2b-2 rooting (plan §FW3 "FW3.2b-2 —
|
|
/// the production rooting"); nothing invokes it until the static cutover
|
|
/// flips. Each member maps a <see cref="WalkFrameDriver"/> turn onto the
|
|
/// SAME leaf calls the packed path uses today, so the cutover changes
|
|
/// ORDER, never the leaf mechanics:
|
|
///
|
|
/// <list type="bullet">
|
|
/// <item><see cref="DrawSky"/> → the sky block of <c>DrawLandscapeSlice</c>,
|
|
/// looped over the active slices internally (retail draws GameSky once
|
|
/// inside <c>LScape::draw</c> clipped by the active views; the per-slice
|
|
/// scissor+clip here reproduces today's pixels while the driver still sees
|
|
/// ONE sky turn).</item>
|
|
/// <item><see cref="DrawTerrainSlice"/> → the terrain block of
|
|
/// <c>DrawLandscapeSlice</c> (scissor + terrain clip + slice planes).</item>
|
|
/// <item><see cref="DrawCellShell"/> → <c>EnvCellRenderer</c> opaque +
|
|
/// transparent-ordered for ONE cell (retail <c>DrawEnvCell</c>
|
|
/// @0x0059f170 draws per cell at its flood turn).</item>
|
|
/// <item><see cref="DrawPunchFan"/> → <c>PortalDepthMaskRenderer.DrawDepthFan</c>
|
|
/// with <c>forceFarZ</c>, clipped by the pinned view's slice planes
|
|
/// (retail <c>building_view</c> @0x0059f3bf); FW3.3 draws fans at the dat
|
|
/// aperture verbatim (the ShellDrawLiftZ retirement).</item>
|
|
/// <item><see cref="ClearInteriorDepth"/>/<see cref="DrawExitSeals"/> →
|
|
/// caller-supplied actions (the renderer owns the pass scope and the
|
|
/// root-flood seal iteration; the adapter only provides the turns).</item>
|
|
/// <item><see cref="AlphaBarrier"/> → <c>FlushLandscapeAlpha</c>, retail's
|
|
/// flush-all <c>FlushAlphaList(0f)</c> @0x0059f30b.</item>
|
|
/// </list>
|
|
/// </summary>
|
|
internal sealed class WalkProductionLeafRenderer : IWalkFrameLeafRenderer
|
|
{
|
|
private RetailPViewPassExecutor _passes = null!;
|
|
private RetailPViewFrameInput _frame = null!;
|
|
private ClipFrameAssembly _clipAssembly = null!;
|
|
private Action _clearInteriorDepth = null!;
|
|
private Action _drawExitSeals = null!;
|
|
private readonly HashSet<uint> _singleCellScratch = new();
|
|
private readonly List<uint> _singleCellListScratch = new();
|
|
|
|
internal WalkProductionLeafRenderer(
|
|
RetailPViewPassExecutor passes,
|
|
RetailPViewFrameInput frame,
|
|
ClipFrameAssembly clipAssembly,
|
|
Action clearInteriorDepth,
|
|
Action drawExitSeals)
|
|
=> Reset(passes, frame, clipAssembly, clearInteriorDepth, drawExitSeals);
|
|
|
|
/// <summary>
|
|
/// FW6 allocation closeout: bind the retained leaf and its retained
|
|
/// one-cell collections to the current frame. Replay is synchronous, so
|
|
/// no frame may outlive these references.
|
|
/// </summary>
|
|
internal void Reset(
|
|
RetailPViewPassExecutor passes,
|
|
RetailPViewFrameInput frame,
|
|
ClipFrameAssembly clipAssembly,
|
|
Action clearInteriorDepth,
|
|
Action drawExitSeals)
|
|
{
|
|
_passes = passes ?? throw new ArgumentNullException(nameof(passes));
|
|
_frame = frame ?? throw new ArgumentNullException(nameof(frame));
|
|
_clipAssembly = clipAssembly
|
|
?? throw new ArgumentNullException(nameof(clipAssembly));
|
|
_clearInteriorDepth = clearInteriorDepth
|
|
?? throw new ArgumentNullException(nameof(clearInteriorDepth));
|
|
_drawExitSeals = drawExitSeals
|
|
?? throw new ArgumentNullException(nameof(drawExitSeals));
|
|
}
|
|
|
|
public void DrawSky() => _passes.DrawWalkSky(_frame, _clipAssembly);
|
|
|
|
public void DrawTerrainSlice(int sliceIndex) =>
|
|
_passes.DrawWalkTerrainSlice(_frame, _clipAssembly, sliceIndex);
|
|
|
|
public void DrawCellShell(uint cellId)
|
|
{
|
|
// Output-gated binary discriminator for the moving wall-textured
|
|
// triangles at the 0xF4180107/0112 floating stairs. Missing walls are
|
|
// intentional while enabled: this identifies the visual carrier and
|
|
// must not become a production admission rule.
|
|
if (AcDream.Core.Rendering.RenderingDiagnostics
|
|
.ProbeCathedralSkipFloatingStairCellShells
|
|
&& cellId is 0xF4180107u or 0xF4180112u)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Retail DrawEnvCell @0x0059F170 submits the prepared EnvCell mesh
|
|
// whole after stamping it drawn-this-frame. obj_view_set prepares
|
|
// legacy polygon/object tests, but the use_built_mesh branch calls
|
|
// D3DPolyRender::DrawMesh directly; it does not slice the shell mesh
|
|
// once per portal polygon.
|
|
_singleCellScratch.Clear();
|
|
_singleCellScratch.Add(cellId);
|
|
_passes.DrawOpaqueCellShells(_singleCellScratch);
|
|
if (_passes.CellHasTransparentShell(cellId))
|
|
{
|
|
_singleCellListScratch.Clear();
|
|
_singleCellListScratch.Add(cellId);
|
|
_passes.DrawTransparentCellShellsOrdered(_singleCellListScratch);
|
|
}
|
|
}
|
|
|
|
public void ClearInteriorDepth() => _clearInteriorDepth();
|
|
|
|
public void DrawStaticParticles(IReadOnlySet<uint> ownerIds) =>
|
|
_passes.DrawLandscapeStaticParticles(
|
|
_frame, new RetailPViewLandscapeStaticParticleContext(ownerIds));
|
|
|
|
public void DrawCellParticles(uint cellId, IReadOnlySet<uint> ownerIds)
|
|
{
|
|
_passes.DrawCellParticles(
|
|
_frame,
|
|
new RetailPViewCellSliceContext(cellId, default, ownerIds));
|
|
}
|
|
|
|
public void DrawExitSeals() => _drawExitSeals();
|
|
|
|
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) =>
|
|
_passes.DrawWalkPunchFan(_frame, _clipAssembly, worldPolygon, activeViewIndex);
|
|
|
|
public void AlphaBarrier() => _passes.FlushLandscapeAlpha();
|
|
}
|