Move fallback and PView world drawing, shared alpha, particles, visibility, selection, and diagnostics behind focused frame owners. Make exceptional frames failure-atomic by restoring the shared GL baseline and preserving primary alpha failures through cleanup. Co-authored-by: Codex <codex@openai.com>
657 lines
23 KiB
C#
657 lines
23 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Audio;
|
|
using AcDream.App.Input;
|
|
using AcDream.App.Rendering.Selection;
|
|
using AcDream.App.Rendering.Vfx;
|
|
using AcDream.App.Rendering.Wb;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Lighting;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Rendering;
|
|
using AcDream.Core.World;
|
|
using AcDream.UI.Abstractions.Panels.Settings;
|
|
|
|
namespace AcDream.App.Rendering;
|
|
|
|
/// <summary>Camera facts borrowed for one world render preparation.</summary>
|
|
internal readonly record struct WorldCameraFrame(
|
|
ICamera Camera,
|
|
Matrix4x4 Projection,
|
|
Matrix4x4 ViewProjection,
|
|
FrustumPlanes Frustum,
|
|
Matrix4x4 InverseView,
|
|
Vector3 Position);
|
|
|
|
/// <summary>Player-lighting and collided-viewer roots for one frame.</summary>
|
|
internal readonly record struct WorldRootFrame(
|
|
LoadedCell? PlayerRoot,
|
|
bool PlayerSeenOutside,
|
|
uint ViewerCellId,
|
|
Vector3 ViewerEyePosition,
|
|
Vector3 PlayerViewPosition,
|
|
LoadedCell? ViewerRoot,
|
|
bool CameraInsideCell,
|
|
bool RootSeenOutside,
|
|
bool PlayerInsideCell,
|
|
uint? PlayerLandblockId,
|
|
int RenderCenterLandblockX,
|
|
int RenderCenterLandblockY,
|
|
uint PlayerCellId,
|
|
bool PlayerIndoorGate)
|
|
{
|
|
public bool RenderSky => ViewerRoot is null || RootSeenOutside;
|
|
}
|
|
|
|
/// <summary>Borrowed building scratch, valid only until the next build.</summary>
|
|
internal readonly record struct WorldBuildingFrame(
|
|
LoadedCell? OutdoorNode,
|
|
IReadOnlyList<LoadedCell> NearbyBuildingCells);
|
|
|
|
/// <summary>
|
|
/// Immutable one-frame world facts. Collection members borrow reusable owner
|
|
/// scratch and must not be retained beyond the next <see cref="WorldRenderFrameBuilder.Build"/>.
|
|
/// </summary>
|
|
internal readonly record struct WorldRenderFrame(
|
|
WorldCameraFrame Camera,
|
|
WorldRootFrame Roots,
|
|
WorldBuildingFrame Buildings,
|
|
HashSet<uint> AnimatedEntityIds)
|
|
{
|
|
public LoadedCell? ClipRoot => Roots.ViewerRoot ?? Buildings.OutdoorNode;
|
|
}
|
|
|
|
internal interface IWorldRenderFrameBuilder
|
|
{
|
|
WorldRenderFrame Build(
|
|
in RenderFrameFoundation foundation,
|
|
bool waitingForLogin,
|
|
DayGroupData? activeDayGroup);
|
|
|
|
void ObserveDrawableCells(IReadOnlySet<uint> drawableCells);
|
|
|
|
void ClearDrawableCells();
|
|
}
|
|
|
|
internal interface IWorldFrameCameraSource
|
|
{
|
|
WorldCameraFrame Resolve();
|
|
}
|
|
|
|
internal interface IWorldFrameRootSource
|
|
{
|
|
WorldRootFrame Resolve(in WorldCameraFrame camera);
|
|
}
|
|
|
|
internal interface IWorldFrameVisibilityPreparation
|
|
{
|
|
void Begin(in WorldCameraFrame camera, bool waitingForLogin);
|
|
|
|
void PublishViewProjection(in WorldCameraFrame camera);
|
|
}
|
|
|
|
internal interface IWorldFrameSettingsPreview
|
|
{
|
|
void Apply(in WorldCameraFrame camera);
|
|
}
|
|
|
|
internal interface IWorldFrameEnvironmentPreparation
|
|
{
|
|
void Prepare(
|
|
in WorldCameraFrame camera,
|
|
in WorldRootFrame roots,
|
|
in RenderFrameFoundation foundation,
|
|
DayGroupData? activeDayGroup);
|
|
|
|
void ObserveDrawableCells(IReadOnlySet<uint> drawableCells);
|
|
|
|
void ClearDrawableCells();
|
|
}
|
|
|
|
internal interface IWorldFrameAnimatedEntitySource
|
|
{
|
|
HashSet<uint> Capture();
|
|
}
|
|
|
|
internal interface IWorldFrameBuildingSource
|
|
{
|
|
WorldBuildingFrame Gather(
|
|
LoadedCell? viewerRoot,
|
|
uint viewerCellId,
|
|
in FrustumPlanes frustum);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Orders typed world-frame fact sources without owning their borrowed render
|
|
/// resources. It makes no draw decision and retains no result from a prior call.
|
|
/// </summary>
|
|
internal sealed class WorldRenderFrameBuilder : IWorldRenderFrameBuilder
|
|
{
|
|
private readonly IWorldFrameCameraSource _camera;
|
|
private readonly IWorldFrameVisibilityPreparation _visibility;
|
|
private readonly IWorldFrameSettingsPreview _settings;
|
|
private readonly IWorldFrameRootSource _roots;
|
|
private readonly IWorldFrameEnvironmentPreparation _environment;
|
|
private readonly IWorldFrameAnimatedEntitySource _animated;
|
|
private readonly IWorldFrameBuildingSource _buildings;
|
|
|
|
public WorldRenderFrameBuilder(
|
|
IWorldFrameCameraSource camera,
|
|
IWorldFrameVisibilityPreparation visibility,
|
|
IWorldFrameSettingsPreview settings,
|
|
IWorldFrameRootSource roots,
|
|
IWorldFrameEnvironmentPreparation environment,
|
|
IWorldFrameAnimatedEntitySource animated,
|
|
IWorldFrameBuildingSource buildings)
|
|
{
|
|
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
|
|
_visibility = visibility ?? throw new ArgumentNullException(nameof(visibility));
|
|
_settings = settings ?? throw new ArgumentNullException(nameof(settings));
|
|
_roots = roots ?? throw new ArgumentNullException(nameof(roots));
|
|
_environment = environment ?? throw new ArgumentNullException(nameof(environment));
|
|
_animated = animated ?? throw new ArgumentNullException(nameof(animated));
|
|
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
|
|
}
|
|
|
|
public WorldRenderFrame Build(
|
|
in RenderFrameFoundation foundation,
|
|
bool waitingForLogin,
|
|
DayGroupData? activeDayGroup)
|
|
{
|
|
WorldCameraFrame camera = _camera.Resolve();
|
|
_visibility.Begin(in camera, waitingForLogin);
|
|
_settings.Apply(in camera);
|
|
WorldRootFrame roots = _roots.Resolve(in camera);
|
|
_environment.Prepare(in camera, in roots, in foundation, activeDayGroup);
|
|
_visibility.PublishViewProjection(in camera);
|
|
HashSet<uint> animated = _animated.Capture();
|
|
FrustumPlanes frustum = camera.Frustum;
|
|
WorldBuildingFrame buildings = _buildings.Gather(
|
|
roots.ViewerRoot,
|
|
roots.ViewerCellId,
|
|
in frustum);
|
|
return new WorldRenderFrame(camera, roots, buildings, animated);
|
|
}
|
|
|
|
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells) =>
|
|
_environment.ObserveDrawableCells(drawableCells);
|
|
|
|
public void ClearDrawableCells() => _environment.ClearDrawableCells();
|
|
}
|
|
|
|
internal sealed class RuntimeWorldFrameCameraSource : IWorldFrameCameraSource
|
|
{
|
|
private readonly CameraController _cameras;
|
|
private readonly LocalPlayerTeleportController _teleport;
|
|
|
|
public RuntimeWorldFrameCameraSource(
|
|
CameraController cameras,
|
|
LocalPlayerTeleportController teleport)
|
|
{
|
|
_cameras = cameras ?? throw new ArgumentNullException(nameof(cameras));
|
|
_teleport = teleport ?? throw new ArgumentNullException(nameof(teleport));
|
|
}
|
|
|
|
public WorldCameraFrame Resolve()
|
|
{
|
|
ICamera camera = _teleport.ApplyViewPlane(_cameras.Active);
|
|
Matrix4x4 projection = camera.Projection;
|
|
Matrix4x4 viewProjection = camera.View * projection;
|
|
FrustumPlanes frustum = FrustumPlanes.FromViewProjection(viewProjection);
|
|
Matrix4x4.Invert(camera.View, out Matrix4x4 inverseView);
|
|
var position = new Vector3(inverseView.M41, inverseView.M42, inverseView.M43);
|
|
return new WorldCameraFrame(
|
|
camera,
|
|
projection,
|
|
viewProjection,
|
|
frustum,
|
|
inverseView,
|
|
position);
|
|
}
|
|
}
|
|
|
|
internal sealed class RuntimeWorldFrameRootSource : IWorldFrameRootSource
|
|
{
|
|
private const float LandblockSize = 192f;
|
|
|
|
private readonly PhysicsEngine _physics;
|
|
private readonly CellVisibility _cells;
|
|
private readonly ILocalPlayerModeSource _mode;
|
|
private readonly IChaseCameraSource _chase;
|
|
private readonly ILocalPlayerControllerSource _player;
|
|
private readonly LiveWorldOriginState _origin;
|
|
|
|
public RuntimeWorldFrameRootSource(
|
|
PhysicsEngine physics,
|
|
CellVisibility cells,
|
|
ILocalPlayerModeSource mode,
|
|
IChaseCameraSource chase,
|
|
ILocalPlayerControllerSource player,
|
|
LiveWorldOriginState origin)
|
|
{
|
|
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
|
|
_cells = cells ?? throw new ArgumentNullException(nameof(cells));
|
|
_mode = mode ?? throw new ArgumentNullException(nameof(mode));
|
|
_chase = chase ?? throw new ArgumentNullException(nameof(chase));
|
|
_player = player ?? throw new ArgumentNullException(nameof(player));
|
|
_origin = origin ?? throw new ArgumentNullException(nameof(origin));
|
|
}
|
|
|
|
public WorldRootFrame Resolve(in WorldCameraFrame camera)
|
|
{
|
|
// Retail keeps these as two related but distinct positions:
|
|
// CellManager::ChangePosition @ 0x004559B0 uses the player's current
|
|
// cell for seen_outside/lighting, while RenderNormalMode @ 0x00453AA0
|
|
// passes the collided camera's viewer_cell to DrawInside.
|
|
LoadedCell? playerRoot = null;
|
|
if (_physics.DataCache?.CellGraph.CurrCell is AcDream.Core.World.Cells.EnvCell playerCell
|
|
&& _cells.TryGetCell(playerCell.Id, out LoadedCell? registeredPlayer))
|
|
{
|
|
playerRoot = registeredPlayer;
|
|
}
|
|
|
|
bool playerSeenOutside = playerRoot?.SeenOutside ?? true;
|
|
uint viewerCellId = _mode.IsPlayerMode
|
|
&& _chase.Retail is { } retailChase
|
|
&& CameraDiagnostics.UseRetailChaseCamera
|
|
? retailChase.ViewerCellId
|
|
: playerRoot?.CellId ?? 0u;
|
|
LoadedCell? viewerRoot = null;
|
|
if (viewerCellId != 0u
|
|
&& _cells.TryGetCell(viewerCellId, out LoadedCell? registeredViewer))
|
|
{
|
|
viewerRoot = registeredViewer;
|
|
}
|
|
|
|
var player = _player.Controller;
|
|
Vector3 playerViewPosition = player?.RenderPosition
|
|
?? player?.Position
|
|
?? camera.Position;
|
|
bool cameraInsideCell = viewerRoot is not null;
|
|
bool rootSeenOutside = viewerRoot?.SeenOutside ?? true;
|
|
bool playerInsideCell = playerRoot is not null && !playerSeenOutside;
|
|
|
|
uint? playerLandblockId = null;
|
|
if (_mode.IsPlayerMode && player is not null)
|
|
{
|
|
int playerX = _origin.CenterX + (int)Math.Floor(player.Position.X / LandblockSize);
|
|
int playerY = _origin.CenterY + (int)Math.Floor(player.Position.Y / LandblockSize);
|
|
playerLandblockId = (uint)((playerX << 24) | (playerY << 16) | 0xFFFF);
|
|
}
|
|
|
|
int renderCenterX = _origin.CenterX
|
|
+ (int)Math.Floor(camera.Position.X / LandblockSize);
|
|
int renderCenterY = _origin.CenterY
|
|
+ (int)Math.Floor(camera.Position.Y / LandblockSize);
|
|
uint playerCellId = _physics.DataCache?.CellGraph.CurrCell?.Id ?? 0u;
|
|
bool playerIndoorGate = RenderingDiagnostics.ShouldRenderIndoor(
|
|
playerCellId,
|
|
playerRoot is not null);
|
|
|
|
return new WorldRootFrame(
|
|
playerRoot,
|
|
playerSeenOutside,
|
|
viewerCellId,
|
|
camera.Position,
|
|
playerViewPosition,
|
|
viewerRoot,
|
|
cameraInsideCell,
|
|
rootSeenOutside,
|
|
playerInsideCell,
|
|
playerLandblockId,
|
|
renderCenterX,
|
|
renderCenterY,
|
|
playerCellId,
|
|
playerIndoorGate);
|
|
}
|
|
}
|
|
|
|
internal sealed class RuntimeWorldFrameVisibilityPreparation
|
|
: IWorldFrameVisibilityPreparation
|
|
{
|
|
private readonly RetailSelectionScene? _selection;
|
|
private readonly ParticleVisibilityController _particles;
|
|
private readonly TerrainModernRenderer? _terrain;
|
|
private readonly WorldRevealCoordinator? _reveal;
|
|
private readonly WbFrustum? _environmentFrustum;
|
|
|
|
public RuntimeWorldFrameVisibilityPreparation(
|
|
RetailSelectionScene? selection,
|
|
ParticleVisibilityController particles,
|
|
TerrainModernRenderer? terrain,
|
|
WorldRevealCoordinator? reveal,
|
|
WbFrustum? environmentFrustum)
|
|
{
|
|
_selection = selection;
|
|
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
|
|
_terrain = terrain;
|
|
_reveal = reveal;
|
|
_environmentFrustum = environmentFrustum;
|
|
}
|
|
|
|
public void Begin(in WorldCameraFrame camera, bool waitingForLogin)
|
|
{
|
|
_selection?.SetViewFrustum(camera.Frustum);
|
|
_particles.BeginFrame(camera.Position);
|
|
_terrain?.BeginVisibilityFrame();
|
|
if (waitingForLogin)
|
|
return;
|
|
|
|
_particles.UseWorldView();
|
|
_reveal?.ObserveWorldViewportVisible();
|
|
_reveal?.Complete();
|
|
}
|
|
|
|
public void PublishViewProjection(in WorldCameraFrame camera) =>
|
|
_environmentFrustum?.Update(camera.ViewProjection);
|
|
}
|
|
|
|
internal sealed class RuntimeWorldFrameSettingsPreview : IWorldFrameSettingsPreview
|
|
{
|
|
private readonly SettingsVM? _settings;
|
|
private readonly OpenAlAudioEngine? _audio;
|
|
private readonly CameraController _cameras;
|
|
private readonly DisplayFramePacingController _pacing;
|
|
|
|
public RuntimeWorldFrameSettingsPreview(
|
|
SettingsVM? settings,
|
|
OpenAlAudioEngine? audio,
|
|
CameraController cameras,
|
|
DisplayFramePacingController pacing)
|
|
{
|
|
_settings = settings;
|
|
_audio = audio;
|
|
_cameras = cameras ?? throw new ArgumentNullException(nameof(cameras));
|
|
_pacing = pacing ?? throw new ArgumentNullException(nameof(pacing));
|
|
}
|
|
|
|
public void Apply(in WorldCameraFrame camera)
|
|
{
|
|
if (_audio is { IsAvailable: true } && _settings is not null)
|
|
{
|
|
var audio = _settings.AudioDraft;
|
|
_audio.MasterVolume = audio.Master;
|
|
_audio.MusicVolume = audio.Music;
|
|
_audio.SfxVolume = audio.Sfx;
|
|
_audio.AmbientVolume = audio.Ambient;
|
|
}
|
|
|
|
if (_settings is not null)
|
|
{
|
|
var display = _settings.DisplayDraft;
|
|
float fieldOfView = display.FieldOfView * (MathF.PI / 180f);
|
|
_cameras.Orbit.FovY = fieldOfView;
|
|
_cameras.Fly.FovY = fieldOfView;
|
|
if (_cameras.Chase is not null)
|
|
_cameras.Chase.FovY = fieldOfView;
|
|
_pacing.ApplyPreference(display.VSync);
|
|
}
|
|
|
|
if (_audio is not { IsAvailable: true })
|
|
return;
|
|
|
|
Matrix4x4 inverse = camera.InverseView;
|
|
var forward = new Vector3(-inverse.M31, -inverse.M32, -inverse.M33);
|
|
var up = new Vector3(inverse.M21, inverse.M22, inverse.M23);
|
|
Vector3 position = camera.Position;
|
|
_audio.SetListener(
|
|
position.X, position.Y, position.Z,
|
|
forward.X, forward.Y, forward.Z,
|
|
up.X, up.Y, up.Z);
|
|
}
|
|
}
|
|
|
|
internal interface IWorldRenderRangeSource
|
|
{
|
|
int NearRadius { get; }
|
|
|
|
int FarRadius { get; }
|
|
}
|
|
|
|
internal sealed class WorldRenderRangeState : IWorldRenderRangeSource
|
|
{
|
|
public WorldRenderRangeState(int nearRadius, int farRadius)
|
|
{
|
|
NearRadius = nearRadius;
|
|
FarRadius = farRadius;
|
|
}
|
|
|
|
public int NearRadius { get; set; }
|
|
|
|
public int FarRadius { get; set; }
|
|
}
|
|
|
|
internal sealed class RuntimeWorldFrameEnvironmentPreparation
|
|
: IWorldFrameEnvironmentPreparation
|
|
{
|
|
private const float LandblockSize = 192f;
|
|
|
|
private readonly RuntimeOptions _options;
|
|
private readonly WorldTimeService _worldTime;
|
|
private readonly LightManager _lighting;
|
|
private readonly WbDrawDispatcher? _dispatcher;
|
|
private readonly EnvCellRenderer? _environmentCells;
|
|
private readonly SceneLightingUboBinding? _lightingUbo;
|
|
private readonly IWorldRenderRangeSource _ranges;
|
|
private readonly SkyPesFrameController? _skyPes;
|
|
private readonly HashSet<uint> _visibleCells = [];
|
|
private bool _visibleCellsValid;
|
|
|
|
public RuntimeWorldFrameEnvironmentPreparation(
|
|
RuntimeOptions options,
|
|
WorldTimeService worldTime,
|
|
LightManager lighting,
|
|
WbDrawDispatcher? dispatcher,
|
|
EnvCellRenderer? environmentCells,
|
|
SceneLightingUboBinding? lightingUbo,
|
|
IWorldRenderRangeSource ranges,
|
|
SkyPesFrameController? skyPes)
|
|
{
|
|
_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
_worldTime = worldTime ?? throw new ArgumentNullException(nameof(worldTime));
|
|
_lighting = lighting ?? throw new ArgumentNullException(nameof(lighting));
|
|
_dispatcher = dispatcher;
|
|
_environmentCells = environmentCells;
|
|
_lightingUbo = lightingUbo;
|
|
_ranges = ranges ?? throw new ArgumentNullException(nameof(ranges));
|
|
_skyPes = skyPes;
|
|
}
|
|
|
|
public void Prepare(
|
|
in WorldCameraFrame camera,
|
|
in WorldRootFrame roots,
|
|
in RenderFrameFoundation foundation,
|
|
DayGroupData? activeDayGroup)
|
|
{
|
|
if (_options.EnableSkyPesDebug)
|
|
{
|
|
_skyPes?.Update(
|
|
(float)_worldTime.DayFraction,
|
|
activeDayGroup,
|
|
camera.Position,
|
|
roots.CameraInsideCell);
|
|
}
|
|
|
|
UpdateSunFromSky(foundation.Sky, roots.PlayerInsideCell);
|
|
_lighting.UpdateViewerLight(roots.PlayerViewPosition);
|
|
_lighting.Tick(camera.Position);
|
|
_lighting.BuildPointLightSnapshot(
|
|
roots.PlayerViewPosition,
|
|
_visibleCellsValid ? _visibleCells : null);
|
|
_dispatcher?.SetSceneLights(_lighting.PointSnapshot);
|
|
_environmentCells?.SetPointSnapshot(_lighting.PointSnapshot);
|
|
|
|
AtmosphereSnapshot atmosphere = foundation.Atmosphere;
|
|
SceneLightingUbo ubo = SceneLightingUbo.Build(
|
|
_lighting,
|
|
in atmosphere,
|
|
camera.Position,
|
|
(float)_worldTime.DayFraction);
|
|
float fogStart = _ranges.NearRadius
|
|
* LandblockSize
|
|
* _options.FogStartMultiplier;
|
|
float fogEnd = _ranges.FarRadius
|
|
* LandblockSize
|
|
* _options.FogEndMultiplier;
|
|
ubo.FogParams = new Vector4(
|
|
fogStart,
|
|
fogEnd,
|
|
ubo.FogParams.Z,
|
|
ubo.FogParams.W);
|
|
_lightingUbo?.Upload(ubo);
|
|
|
|
RenderingDiagnostics.EmitLight(
|
|
insideCell: roots.PlayerInsideCell,
|
|
ambientR: _lighting.CurrentAmbient.AmbientColor.X,
|
|
ambientG: _lighting.CurrentAmbient.AmbientColor.Y,
|
|
ambientB: _lighting.CurrentAmbient.AmbientColor.Z,
|
|
sunIntensity: _lighting.Sun?.Intensity ?? 0f,
|
|
registeredLights: _lighting.RegisteredCount,
|
|
activeLights: (int)ubo.CellAmbient.W,
|
|
playerCellId: roots.PlayerRoot?.CellId ?? 0u,
|
|
lights: _lighting);
|
|
}
|
|
|
|
public void ObserveDrawableCells(IReadOnlySet<uint> drawableCells)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(drawableCells);
|
|
_visibleCells.Clear();
|
|
_visibleCells.UnionWith(drawableCells);
|
|
_visibleCellsValid = true;
|
|
}
|
|
|
|
public void ClearDrawableCells() => _visibleCellsValid = false;
|
|
|
|
private void UpdateSunFromSky(SkyKeyframe keyframe, bool playerInsideCell)
|
|
{
|
|
// CellManager::ChangePosition @ 0x004559B0 keys this on the player
|
|
// cell's seen_outside flag and installs neutral 0.2 ambient indoors.
|
|
Vector3 sunToWorld = -SkyStateProvider.SunDirectionFromKeyframe(keyframe);
|
|
if (playerInsideCell)
|
|
{
|
|
_lighting.Sun = new LightSource
|
|
{
|
|
Kind = LightKind.Directional,
|
|
WorldForward = sunToWorld,
|
|
ColorLinear = Vector3.Zero,
|
|
Intensity = 0f,
|
|
Range = 1f,
|
|
};
|
|
_lighting.CurrentAmbient = new CellAmbientState(
|
|
new Vector3(0.20f, 0.20f, 0.20f),
|
|
Vector3.Zero,
|
|
sunToWorld);
|
|
return;
|
|
}
|
|
|
|
_lighting.Sun = new LightSource
|
|
{
|
|
Kind = LightKind.Directional,
|
|
WorldForward = sunToWorld,
|
|
ColorLinear = keyframe.SunColor,
|
|
Intensity = 1f,
|
|
Range = 1f,
|
|
};
|
|
_lighting.CurrentAmbient = new CellAmbientState(
|
|
keyframe.AmbientColor,
|
|
keyframe.SunColor,
|
|
sunToWorld);
|
|
}
|
|
}
|
|
|
|
internal sealed class RuntimeWorldFrameAnimatedEntitySource
|
|
: IWorldFrameAnimatedEntitySource
|
|
{
|
|
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _live;
|
|
private readonly RetailStaticAnimatingObjectScheduler? _statics;
|
|
private readonly EquippedChildRenderController? _equipped;
|
|
private readonly HashSet<uint> _scratch = [];
|
|
|
|
public RuntimeWorldFrameAnimatedEntitySource(
|
|
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> live,
|
|
RetailStaticAnimatingObjectScheduler? statics,
|
|
EquippedChildRenderController? equipped)
|
|
{
|
|
_live = live ?? throw new ArgumentNullException(nameof(live));
|
|
_statics = statics;
|
|
_equipped = equipped;
|
|
}
|
|
|
|
public HashSet<uint> Capture()
|
|
{
|
|
// Membership means "classify from the current pose this frame," not
|
|
// merely "animation is advancing." A settled door or translucency
|
|
// hook can differ permanently from its cached rest pose, so every
|
|
// sequenced/attached entity remains on the live path.
|
|
_live.CopySpatialIdsTo(_scratch);
|
|
_statics?.CopyAnimatedEntityIdsTo(_scratch);
|
|
if (_equipped is not null)
|
|
{
|
|
foreach (uint entityId in _equipped.AttachedEntityIds)
|
|
_scratch.Add(entityId);
|
|
}
|
|
|
|
return _scratch;
|
|
}
|
|
}
|
|
|
|
internal sealed class RuntimeWorldFrameBuildingSource : IWorldFrameBuildingSource
|
|
{
|
|
private readonly LandblockPresentationPipeline _presentation;
|
|
private readonly CellVisibility _cells;
|
|
private readonly List<LoadedCell> _scratch = [];
|
|
|
|
public RuntimeWorldFrameBuildingSource(
|
|
LandblockPresentationPipeline presentation,
|
|
CellVisibility cells)
|
|
{
|
|
_presentation = presentation
|
|
?? throw new ArgumentNullException(nameof(presentation));
|
|
_cells = cells ?? throw new ArgumentNullException(nameof(cells));
|
|
}
|
|
|
|
public WorldBuildingFrame Gather(
|
|
LoadedCell? viewerRoot,
|
|
uint viewerCellId,
|
|
in FrustumPlanes frustum)
|
|
{
|
|
_scratch.Clear();
|
|
LoadedCell? outdoorNode = null;
|
|
if (viewerRoot is null && viewerCellId == 0u)
|
|
return new WorldBuildingFrame(null, _scratch);
|
|
|
|
// DrawBuilding @ 0x0059F2A0 gates each building by its visible aperture
|
|
// before walking its portal BSP. The portal-bounds frustum test is the
|
|
// corresponding coarse admission gate; there is no distance cutoff.
|
|
foreach (BuildingRegistry registry in _presentation.BuildingRegistries)
|
|
{
|
|
foreach (Building building in registry.All())
|
|
{
|
|
if (building.HasPortalBounds
|
|
&& !FrustumCuller.IsAabbVisible(
|
|
frustum,
|
|
building.PortalBounds.Min,
|
|
building.PortalBounds.Max))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
foreach (uint cellId in building.EnvCellIds)
|
|
{
|
|
if (_cells.TryGetCell(cellId, out LoadedCell? cell) && cell is not null)
|
|
_scratch.Add(cell);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (viewerRoot is null)
|
|
outdoorNode = OutdoorCellNode.Build(viewerCellId);
|
|
if (RenderingDiagnostics.ProbeFlapEnabled)
|
|
{
|
|
Console.WriteLine(FormattableString.Invariant(
|
|
$"[outdoor-node] cell=0x{viewerCellId:X8} root={(viewerRoot is null ? "OUT" : "IN")} nearbyCells={_scratch.Count} (T2 frustum-gated per-building floods)"));
|
|
}
|
|
|
|
return new WorldBuildingFrame(outdoorNode, _scratch);
|
|
}
|
|
}
|