refactor(render): extract world frame preparation
Move camera/root resolution, live settings and listener preview, sky/lighting/fog preparation, animated classification, and building visibility scratch behind a typed WorldRenderFrameBuilder while preserving retail frame order and borrowed lifetimes. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
bc6f09f987
commit
6d6e5b5fa5
8 changed files with 1463 additions and 583 deletions
645
src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs
Normal file
645
src/AcDream.App/Rendering/WorldRenderFrameBuilder.cs
Normal file
|
|
@ -0,0 +1,645 @@
|
|||
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 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
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue