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:
Erik 2026-07-22 05:54:00 +02:00
parent bc6f09f987
commit 6d6e5b5fa5
8 changed files with 1463 additions and 583 deletions

View file

@ -0,0 +1,135 @@
using System.Numerics;
using AcDream.App.Rendering.Vfx;
using AcDream.Core.Vfx;
using AcDream.Core.World;
namespace AcDream.App.Rendering;
/// <summary>
/// Owns the optional DAT-archaeology sky-PES experiment. Named retail shows
/// GameSky does not consume SkyObject.PesObjectId, so production invokes this
/// owner only when the explicit startup diagnostic is enabled.
/// </summary>
internal sealed class SkyPesFrameController
{
private readonly record struct SkyPesKey(
int ObjectIndex,
uint PesObjectId,
bool PostScene);
private readonly PhysicsScriptRunner _scripts;
private readonly ParticleHookSink _particles;
private readonly EntityEffectPoseRegistry _poses;
private readonly EntityEffectController? _effects;
private readonly HashSet<SkyPesKey> _active = [];
private readonly HashSet<SkyPesKey> _missing = [];
public SkyPesFrameController(
PhysicsScriptRunner scripts,
ParticleHookSink particles,
EntityEffectPoseRegistry poses,
EntityEffectController? effects)
{
_scripts = scripts ?? throw new ArgumentNullException(nameof(scripts));
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
_poses = poses ?? throw new ArgumentNullException(nameof(poses));
_effects = effects;
}
public void Update(
float dayFraction,
DayGroupData? dayGroup,
Vector3 cameraWorldPosition,
bool suppressSky)
{
var seen = new HashSet<SkyPesKey>();
if (!suppressSky && dayGroup is not null)
{
for (int index = 0; index < dayGroup.SkyObjects.Count; index++)
{
SkyObjectData skyObject = dayGroup.SkyObjects[index];
if (skyObject.PesObjectId == 0 || !skyObject.IsVisible(dayFraction))
continue;
var key = new SkyPesKey(
index,
skyObject.PesObjectId,
skyObject.IsPostScene);
seen.Add(key);
uint ownerId = EntityId(key);
ParticleRenderPass renderPass = skyObject.IsPostScene
? ParticleRenderPass.SkyPostScene
: ParticleRenderPass.SkyPreScene;
_particles.SetEntityRenderPass(ownerId, renderPass);
Vector3 anchor = Anchor(skyObject, cameraWorldPosition);
Quaternion rotation = Rotation(skyObject, dayFraction);
_poses.Publish(
ownerId,
Matrix4x4.CreateFromQuaternion(rotation)
* Matrix4x4.CreateTranslation(anchor),
Array.Empty<Matrix4x4>(),
cellId: 0u);
if (_active.Contains(key) || _missing.Contains(key))
continue;
_effects?.RegisterSyntheticOwner(ownerId);
if (_scripts.Play(skyObject.PesObjectId, ownerId, anchor))
{
_active.Add(key);
}
else
{
_missing.Add(key);
_effects?.UnregisterSyntheticOwner(ownerId);
_particles.ClearEntityRenderPass(ownerId);
_poses.Remove(ownerId);
}
}
}
foreach (SkyPesKey key in _active.ToArray())
{
if (seen.Contains(key))
continue;
uint ownerId = EntityId(key);
_scripts.StopAllForEntity(ownerId);
_effects?.UnregisterSyntheticOwner(ownerId);
_particles.StopAllForEntity(ownerId, fadeOut: true);
_poses.Remove(ownerId);
_active.Remove(key);
}
foreach (SkyPesKey key in _missing.ToArray())
{
if (!seen.Contains(key))
_missing.Remove(key);
}
}
private static uint EntityId(SkyPesKey key)
{
uint postScene = key.PostScene ? 0x08000000u : 0u;
return 0xF0000000u
| postScene
| ((uint)key.ObjectIndex & 0x07FFFFFFu);
}
private static Vector3 Anchor(
SkyObjectData skyObject,
Vector3 cameraWorldPosition)
{
if (skyObject.IsWeather && (skyObject.Properties & 0x08u) == 0u)
return cameraWorldPosition + new Vector3(0f, 0f, -120f);
return cameraWorldPosition;
}
private static Quaternion Rotation(
SkyObjectData skyObject,
float dayFraction)
{
float radians = skyObject.CurrentAngle(dayFraction) * (MathF.PI / 180f);
return Quaternion.CreateFromAxisAngle(Vector3.UnitY, -radians);
}
}