125 lines
4.6 KiB
C#
125 lines
4.6 KiB
C#
using System.Numerics;
|
|
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 sealed class RetailSelectionScene : IRetailSelectionRenderSink, IRetailSelectionLightingSource
|
|
{
|
|
private readonly RetailSelectionGeometryCache _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 readonly record struct PartKey(uint LocalEntityId, int PartIndex, uint GfxObjId);
|
|
|
|
public RetailSelectionScene(
|
|
RetailSelectionGeometryCache 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)
|
|
=> _lightingPulse.Start(serverGuid);
|
|
|
|
public void TickLighting()
|
|
=> _lightingPulse.Tick();
|
|
|
|
public bool TryGetLighting(uint serverGuid, out RetailSelectionLighting lighting)
|
|
=> _lightingPulse.TryGet(serverGuid, out lighting);
|
|
|
|
public void BeginFrame()
|
|
{
|
|
_building.Clear();
|
|
_buildingKeys.Clear();
|
|
_viewFrustum = null;
|
|
}
|
|
|
|
/// <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,
|
|
partIndex,
|
|
partWorld,
|
|
mesh));
|
|
}
|
|
|
|
public void CompleteFrame()
|
|
{
|
|
(_published, _building) = (_building, _published);
|
|
}
|
|
|
|
public uint? 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)?.ServerGuid;
|
|
}
|
|
|
|
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;
|
|
}
|