acdream/src/AcDream.App/Rendering/Selection/RetailSelectionScene.cs
Erik f9b68f8f2a feat(rendering): complete current-path render referee
Extend the non-drawing oracle through ordered PView routes, dispatcher visibility and final instance payloads, and accepted retail selection parts. Lifecycle artifacts can now referee the later shadow scene without influencing production visibility or draw decisions.
2026-07-24 21:28:12 +02:00

177 lines
6.1 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Scene;
using AcDream.Core.Selection;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Selection;
/// <summary>
/// Render-thread owner of the last complete set of visible selectable parts.
/// The renderer builds one frame while input queries the previously completed
/// frame, avoiding partial visibility state during multi-slice portal drawing.
/// </summary>
internal interface IWorldSceneSelectionFrame
{
void BeginFrame();
void CompleteFrame();
void AbortFrame();
}
internal sealed class RetailSelectionScene :
IRetailSelectionRenderSink,
IRetailSelectionLightingSource,
IWorldSceneSelectionFrame
{
private readonly IRetailSelectionGeometrySource _geometry;
private readonly RetailSelectionLightingPulse _lightingPulse;
private List<RetailSelectionPart> _building = new();
private List<RetailSelectionPart> _published = new();
private readonly HashSet<PartKey> _buildingKeys = new();
private FrustumPlanes? _viewFrustum;
private ICurrentRenderSelectionObserver? _currentRenderSceneObserver;
private readonly record struct PartKey(uint LocalEntityId, int PartIndex, uint GfxObjId);
public RetailSelectionScene(
IRetailSelectionGeometrySource geometry,
RetailSelectionLightingPulse? lightingPulse = null)
{
_geometry = geometry ?? throw new ArgumentNullException(nameof(geometry));
_lightingPulse = lightingPulse ?? new RetailSelectionLightingPulse();
}
/// <summary>
/// Starts retail's SmartBox click pulse independently of persistent target
/// selection. Examine, use, and targeted-use clicks receive the same pulse.
/// </summary>
public void BeginLightingPulse(uint serverGuid, uint localEntityId)
=> _lightingPulse.Start(serverGuid, localEntityId);
public void TickLighting()
=> _lightingPulse.Tick();
public bool TryGetLighting(
uint serverGuid,
uint localEntityId,
out RetailSelectionLighting lighting)
=> _lightingPulse.TryGet(serverGuid, localEntityId, out lighting);
internal void SetCurrentRenderSceneObserver(
ICurrentRenderSelectionObserver? observer) =>
_currentRenderSceneObserver = observer;
/// <summary>Clears every session-owned frame and material pulse.</summary>
public void Reset()
{
_currentRenderSceneObserver?.AbortSelectionFrame();
_building.Clear();
_published.Clear();
_buildingKeys.Clear();
_viewFrustum = null;
_lightingPulse.Clear();
}
public void BeginFrame()
{
_building.Clear();
_buildingKeys.Clear();
_viewFrustum = null;
_currentRenderSceneObserver?.BeginSelectionFrame();
}
/// <summary>
/// Supplies retail DrawMesh's current view-cone gate. Animated entities
/// deliberately bypass acdream's coarse entity-AABB CPU cull, so this
/// per-part drawing sphere check is the load-bearing retail equivalent.
/// </summary>
public void SetViewFrustum(FrustumPlanes viewFrustum)
=> _viewFrustum = viewFrustum;
public void AddVisiblePart(
WorldEntity entity,
int partIndex,
uint gfxObjId,
Matrix4x4 partWorld)
{
if (entity.ServerGuid == 0u)
return;
if (!_buildingKeys.Add(new PartKey(entity.Id, partIndex, gfxObjId)))
return;
RetailSelectionMesh? mesh = _geometry.Resolve(gfxObjId);
if (mesh is null)
return;
if (_viewFrustum is not { } frustum
|| !DrawingSphereIntersectsFrustum(mesh, partWorld, frustum))
return;
_building.Add(new RetailSelectionPart(
entity.ServerGuid,
entity.Id,
partIndex,
partWorld,
mesh));
_currentRenderSceneObserver?.ObserveSelectionPart(
entity.ServerGuid,
entity.Id,
partIndex,
gfxObjId,
partWorld,
mesh);
}
public void CompleteFrame()
{
(_published, _building) = (_building, _published);
_currentRenderSceneObserver?.CompleteSelectionFrame();
}
/// <summary>Discards the incomplete building frame and preserves the last
/// completely published selection product.</summary>
public void AbortFrame()
{
_currentRenderSceneObserver?.AbortSelectionFrame();
_building.Clear();
_buildingKeys.Clear();
_viewFrustum = null;
}
public RetailSelectionHit? Pick(
float mouseX,
float mouseY,
Vector2 viewport,
Matrix4x4 view,
Matrix4x4 projection,
uint skipServerGuid)
{
if (viewport.X <= 0f || viewport.Y <= 0f)
return null;
var ray = WorldPicker.BuildRay(
mouseX, mouseY, viewport.X, viewport.Y, view, projection);
return RetailWorldPicker.Pick(
ray.Origin, ray.Direction, _published, skipServerGuid);
}
internal static bool DrawingSphereIntersectsFrustum(
RetailSelectionMesh mesh,
Matrix4x4 localToWorld,
FrustumPlanes frustum)
{
Vector3 center = Vector3.Transform(mesh.SphereCenter, localToWorld);
float scaleX = new Vector3(localToWorld.M11, localToWorld.M12, localToWorld.M13).Length();
float scaleY = new Vector3(localToWorld.M21, localToWorld.M22, localToWorld.M23).Length();
float scaleZ = new Vector3(localToWorld.M31, localToWorld.M32, localToWorld.M33).Length();
float radius = mesh.SphereRadius * MathF.Max(scaleX, MathF.Max(scaleY, scaleZ));
return TestPlane(frustum.Left, center, radius)
&& TestPlane(frustum.Right, center, radius)
&& TestPlane(frustum.Bottom, center, radius)
&& TestPlane(frustum.Top, center, radius)
&& TestPlane(frustum.Near, center, radius)
&& TestPlane(frustum.Far, center, radius);
}
private static bool TestPlane(Vector4 plane, Vector3 center, float radius)
=> plane.X * center.X + plane.Y * center.Y + plane.Z * center.Z + plane.W >= -radius;
}