feat(selection): port retail polygon picking and vivid marker
Replace the projected Setup-sphere rectangle and independent physics-wall ray with retail's render-coupled picker: only visible server-object parts participate, each exact drawing sphere broad-phases the camera-eye ray, and first-in-DAT-order visual polygon hits globally outrank sphere fallbacks. Replace the devtools-only procedural triangles with the retained gameplay VividTargetIndicator using retail client-enum surfaces 1..4, radar-blip colorization, Setup selection-sphere framing, and the exact eight-pixel viewport clamp. Release build succeeds with zero warnings and all 5,886 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
0f82a08f0a
commit
146a963aeb
26 changed files with 1302 additions and 1340 deletions
|
|
@ -42,6 +42,7 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Rendering.Wb.EntitySpawnAdapter? _wbEntitySpawnAdapter;
|
||||
private AcDream.App.Rendering.Vfx.EntityScriptActivator? _entityScriptActivator;
|
||||
private AcDream.App.Rendering.Wb.WbDrawDispatcher? _wbDrawDispatcher;
|
||||
private AcDream.App.Rendering.Selection.RetailSelectionScene? _retailSelectionScene;
|
||||
/// <summary>Phase N.5: ARB_bindless_texture + ARB_shader_draw_parameters
|
||||
/// support. Required at startup — missing bindless throws
|
||||
/// <see cref="NotSupportedException"/> in <c>OnLoad</c>.</summary>
|
||||
|
|
@ -828,11 +829,6 @@ public sealed class GameWindow : IDisposable
|
|||
// See docs/plans/2026-04-24-ui-framework.md for the staged UI strategy.
|
||||
private AcDream.UI.ImGui.ImGuiBootstrapper? _imguiBootstrap;
|
||||
private AcDream.UI.ImGui.ImGuiPanelHost? _panelHost;
|
||||
// B.7 (2026-05-15): Vivid Target Indicator — four corner triangles
|
||||
// around the selected entity, colour-coded by ItemType + PWD bits.
|
||||
// Lives alongside the debug panels; cheap to construct + ignore
|
||||
// when no selection. Spec: docs/superpowers/specs/2026-05-15-phase-b7-target-indicator-design.md
|
||||
private AcDream.App.UI.TargetIndicatorPanel? _targetIndicator;
|
||||
private AcDream.UI.Abstractions.Panels.Vitals.VitalsVM? _vitalsVm;
|
||||
private AcDream.UI.Abstractions.Panels.Chat.ChatVM? _retailChatVm;
|
||||
// Phase D.2b — retained host + composition runtime. Null unless ACDREAM_RETAIL_UI=1.
|
||||
|
|
@ -1637,58 +1633,6 @@ public sealed class GameWindow : IDisposable
|
|||
_imguiBootstrap = new AcDream.UI.ImGui.ImGuiBootstrapper(_gl!, _window!, _input!);
|
||||
_panelHost = new AcDream.UI.ImGui.ImGuiPanelHost();
|
||||
|
||||
// B.7 Vivid Target Indicator — corner-triangle highlight
|
||||
// around the currently-selected entity. Delegates pull
|
||||
// live state from this GameWindow instance every frame:
|
||||
// - selected guid → shared SelectionState
|
||||
// - entity resolver → position from the visible world view +
|
||||
// itemType from ClientObjectTable (Objects) + last spawn
|
||||
// - camera → _cameraController.Active or (zero) when not
|
||||
// yet ready, in which case the panel bails on viewport==0.
|
||||
_targetIndicator = new AcDream.App.UI.TargetIndicatorPanel(
|
||||
selectedGuidProvider: () => _selection.SelectedObjectId,
|
||||
entityResolver: guid =>
|
||||
{
|
||||
if (!_visibleEntitiesByServerGuid.TryGetValue(guid, out var entity))
|
||||
return null;
|
||||
uint rawItemType = (uint)LiveItemType(guid);
|
||||
uint pwdBits = 0;
|
||||
uint? useability = null;
|
||||
if (LastSpawns.TryGetValue(guid, out var spawn))
|
||||
{
|
||||
if (spawn.ObjectDescriptionFlags is { } odf) pwdBits = odf;
|
||||
useability = spawn.Useability;
|
||||
}
|
||||
// 2026-05-16 — retail-faithful path. Pass the
|
||||
// entity's Setup.SelectionSphere (scaled by entity
|
||||
// scale, rotated into world coords) through so
|
||||
// the panel projects the sphere as a screen
|
||||
// circle. Matches SmartBox::GetObjectBoundingBox
|
||||
// (decomp 0x00452e20). If the Setup didn't bake
|
||||
// a selection sphere (rare, zero-radius), the
|
||||
// panel falls back to per-type height heuristic.
|
||||
System.Numerics.Vector3? sphereCenter = null;
|
||||
float? sphereRadius = null;
|
||||
if (TryGetEntitySelectionSphere(guid, out var sCenter, out var sRadius))
|
||||
{
|
||||
sphereCenter = sCenter;
|
||||
sphereRadius = sRadius;
|
||||
}
|
||||
return new AcDream.App.UI.TargetIndicatorPanel.TargetInfo(
|
||||
entity.Position, rawItemType, pwdBits, entity.Scale, useability,
|
||||
sphereCenter, sphereRadius);
|
||||
},
|
||||
cameraProvider: () =>
|
||||
{
|
||||
if (_cameraController is null || _window is null)
|
||||
return (System.Numerics.Matrix4x4.Identity,
|
||||
System.Numerics.Matrix4x4.Identity,
|
||||
System.Numerics.Vector2.Zero);
|
||||
var cam = _cameraController.Active;
|
||||
return (cam.View, cam.Projection,
|
||||
new System.Numerics.Vector2(_window.Size.X, _window.Size.Y));
|
||||
});
|
||||
|
||||
// VitalsVM: GUID=0 at construction; set later at EnterWorld
|
||||
// (see the _playerServerGuid assignment path). Pre-login the
|
||||
// HP bar just reads 1.0 (safe default) — harmless. Stam/Mana
|
||||
|
|
@ -2345,6 +2289,13 @@ public sealed class GameWindow : IDisposable
|
|||
() => 1.0,
|
||||
() => _settingsVm?.DisplayDraft.ShowFps
|
||||
?? _persistedDisplay.ShowFps),
|
||||
VividTarget: new AcDream.App.UI.Layout.VividTargetRuntimeBindings(
|
||||
_selection,
|
||||
() => _playerServerGuid,
|
||||
() => _persistedGameplay.VividTargetingIndicator,
|
||||
ResolveVividTargetInfo,
|
||||
guid => _retailSelectionScene?.WasVisible(guid) == true,
|
||||
GetSelectionCamera),
|
||||
Indicators: new AcDream.App.UI.IndicatorRuntimeBindings(
|
||||
SpellBook,
|
||||
Objects,
|
||||
|
|
@ -2674,7 +2625,10 @@ public sealed class GameWindow : IDisposable
|
|||
|
||||
_wbDrawDispatcher = new AcDream.App.Rendering.Wb.WbDrawDispatcher(
|
||||
_gl, _meshShader!, _textureCache!, _wbMeshAdapter!, _wbEntitySpawnAdapter, _bindlessSupport!,
|
||||
_classificationCache, _translucencyFades);
|
||||
_classificationCache, _translucencyFades,
|
||||
_retailSelectionScene ??= new AcDream.App.Rendering.Selection.RetailSelectionScene(
|
||||
new AcDream.App.Rendering.Selection.RetailSelectionGeometryCache(
|
||||
_dats!, _datLock)));
|
||||
// A.5 T22.5: apply A2C gate from quality preset.
|
||||
_wbDrawDispatcher.AlphaToCoverage = _resolvedQuality.AlphaToCoverage;
|
||||
|
||||
|
|
@ -9676,12 +9630,14 @@ public sealed class GameWindow : IDisposable
|
|||
int visibleLandblocks = 0;
|
||||
int totalLandblocks = 0;
|
||||
|
||||
_retailSelectionScene?.BeginFrame();
|
||||
if (_cameraController is not null && !portalViewportVisible)
|
||||
{
|
||||
var activeCamera = _cameraController.Active;
|
||||
var camera = _teleportViewPlane.ApplyTo(activeCamera);
|
||||
var worldProjection = camera.Projection;
|
||||
var frustum = AcDream.App.Rendering.FrustumPlanes.FromViewProjection(camera.View * worldProjection);
|
||||
_retailSelectionScene?.SetViewFrustum(frustum);
|
||||
|
||||
// Extract camera world position from the inverse of the view
|
||||
// matrix — needed by the scene-lighting UBO (for fog distance)
|
||||
|
|
@ -10592,6 +10548,7 @@ public sealed class GameWindow : IDisposable
|
|||
_particleVisibility.MarkVisibleCells(_terrain.VisibleCellIds);
|
||||
_particleVisibility.CompleteFrame();
|
||||
}
|
||||
_retailSelectionScene?.CompleteFrame();
|
||||
|
||||
// Retail gmSmartBoxUI swaps the world SmartBox viewport for a
|
||||
// CreatureMode portal-space viewport. The world block above is skipped
|
||||
|
|
@ -10705,11 +10662,6 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
_panelHost.RenderAll(ctx);
|
||||
// B.7 Vivid Target Indicator: draws corner triangles to the
|
||||
// ImGui background draw list so it appears behind any docked
|
||||
// panels but still over the 3D scene. Cheap when no
|
||||
// selection — internal early-return on null guid.
|
||||
_targetIndicator?.Render();
|
||||
using (var _imguiStage = _frameProfiler.BeginStage(AcDream.App.Diagnostics.FrameStage.ImGui))
|
||||
{
|
||||
_imguiBootstrap.Render();
|
||||
|
|
@ -13143,63 +13095,27 @@ public sealed class GameWindow : IDisposable
|
|||
// ============================================================
|
||||
|
||||
/// <summary>
|
||||
/// Shared world pick at the current cursor — the 2026-05-16
|
||||
/// retail-faithful screen-rect picker (hit area = the target
|
||||
/// indicator's rect via the shared ScreenProjection helper; the old
|
||||
/// per-type radius/offset heuristics are retired). Used by the click
|
||||
/// select path AND the target-mode cursor hover (retail
|
||||
/// SmartBox::get_found_object_id analogue). <paramref name="includeSelf"/>:
|
||||
/// item target-use may pick the LOCAL PLAYER (retail lets you kit-heal
|
||||
/// yourself by clicking your own toon); plain selection never does.
|
||||
/// Shared world pick at the current cursor. The renderer supplies the
|
||||
/// exact visible CPhysicsPart equivalents and RetailWorldPicker performs
|
||||
/// retail's drawing-sphere broadphase followed by flat visual-polygon
|
||||
/// intersection. <paramref name="includeSelf"/> allows item target-use to
|
||||
/// pick the local player while plain selection excludes it.
|
||||
/// </summary>
|
||||
private uint? PickWorldGuidAtCursor(bool includeSelf)
|
||||
=> PickWorldGuidAt(_lastMouseX, _lastMouseY, includeSelf);
|
||||
|
||||
private uint? PickWorldGuidAt(float mouseX, float mouseY, bool includeSelf)
|
||||
{
|
||||
if (_cameraController is null || _window is null) return null;
|
||||
|
||||
var camera = _cameraController.Active;
|
||||
var viewport = new System.Numerics.Vector2((float)_window.Size.X, (float)_window.Size.Y);
|
||||
|
||||
// Indoor walking Phase 1 #86 (2026-05-19): snapshot the currently-
|
||||
// cached EnvCell physics so the picker can occlude entities behind
|
||||
// walls. Snapshot is per-pick (one click / one hover frame),
|
||||
// iteration is bounded by the streaming radius (~80 cells at radius 4).
|
||||
var loadedCellPhysics = new List<AcDream.Core.Physics.CellPhysics>();
|
||||
foreach (var cellId in _physicsDataCache.CellStructIds)
|
||||
{
|
||||
var cp = _physicsDataCache.GetCellStruct(cellId);
|
||||
if (cp is not null) loadedCellPhysics.Add(cp);
|
||||
}
|
||||
|
||||
return AcDream.Core.Selection.WorldPicker.Pick(
|
||||
mouseX: mouseX, mouseY: mouseY,
|
||||
view: camera.View, projection: camera.Projection,
|
||||
viewport: viewport,
|
||||
candidates: _visibleEntitiesByServerGuid.Values,
|
||||
skipServerGuid: includeSelf ? 0u : _playerServerGuid,
|
||||
// Resolver: Setup's SelectionSphere is the ONLY input. If the
|
||||
// entity's Setup didn't bake a SelectionSphere, return null —
|
||||
// the picker skips it, which matches retail behaviour
|
||||
// (Render::GfxObjUnderSelectionRay at 0x0054c740 skips
|
||||
// candidates with no drawing_sphere data). Earlier defensive
|
||||
// 1.5 m × scale synth was removed 2026-05-16 — it made
|
||||
// dat-incomplete entities click as phantom hitboxes the size
|
||||
// of an NPC, diverging from retail and masking real Setup-
|
||||
// loading bugs.
|
||||
sphereForEntity: e =>
|
||||
TryGetEntitySelectionSphere(e.ServerGuid, out var c, out var r)
|
||||
? ((System.Numerics.Vector3, float)?)(c, r)
|
||||
: null,
|
||||
// Match the indicator's TriangleSize (8 px) so the click area
|
||||
// extends out to the bracket corners — what the user perceives
|
||||
// as "selectable extent."
|
||||
inflatePixels: 8f,
|
||||
cellOccluder: loadedCellPhysics.Count > 0
|
||||
? (origin, direction) =>
|
||||
AcDream.Core.Selection.CellBspRayOccluder.NearestWallT(origin, direction, loadedCellPhysics)
|
||||
: null);
|
||||
if (_retailSelectionScene is null || _window is null)
|
||||
return null;
|
||||
var camera = GetSelectionCamera();
|
||||
return _retailSelectionScene.Pick(
|
||||
mouseX,
|
||||
mouseY,
|
||||
camera.Viewport,
|
||||
camera.View,
|
||||
camera.Projection,
|
||||
includeSelf ? 0u : _playerServerGuid);
|
||||
}
|
||||
|
||||
private void PickAndStoreSelection(bool useImmediately)
|
||||
|
|
@ -13729,6 +13645,35 @@ public sealed class GameWindow : IDisposable
|
|||
Objects.Get(_playerServerGuid),
|
||||
Objects.Get(guid));
|
||||
|
||||
private (System.Numerics.Matrix4x4 View,
|
||||
System.Numerics.Matrix4x4 Projection,
|
||||
System.Numerics.Vector2 Viewport) GetSelectionCamera()
|
||||
{
|
||||
if (_cameraController is null || _window is null)
|
||||
return (System.Numerics.Matrix4x4.Identity,
|
||||
System.Numerics.Matrix4x4.Identity,
|
||||
System.Numerics.Vector2.Zero);
|
||||
var camera = _teleportViewPlane.ApplyTo(_cameraController.Active);
|
||||
return (camera.View, camera.Projection,
|
||||
new System.Numerics.Vector2(_window.Size.X, _window.Size.Y));
|
||||
}
|
||||
|
||||
private AcDream.App.UI.Layout.VividTargetInfo? ResolveVividTargetInfo(uint guid)
|
||||
{
|
||||
if (!_visibleEntitiesByServerGuid.ContainsKey(guid)
|
||||
|| !TryGetEntitySelectionSphere(guid, out var center, out float radius))
|
||||
return null;
|
||||
|
||||
uint pwdBits = LastSpawns.TryGetValue(guid, out var spawn)
|
||||
? spawn.ObjectDescriptionFlags ?? 0u
|
||||
: 0u;
|
||||
return new AcDream.App.UI.Layout.VividTargetInfo(
|
||||
center,
|
||||
radius,
|
||||
(uint)LiveItemType(guid),
|
||||
pwdBits);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 2026-05-16 — retail-faithful port of
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Selection;
|
||||
|
||||
/// <summary>
|
||||
/// Narrow seam from the normal world draw traversal to retail mouse selection.
|
||||
/// Only entities which survive that traversal are published.
|
||||
/// </summary>
|
||||
internal interface IRetailSelectionRenderSink
|
||||
{
|
||||
void AddVisiblePart(
|
||||
WorldEntity entity,
|
||||
int partIndex,
|
||||
uint gfxObjId,
|
||||
Matrix4x4 partWorld);
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Selection;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Rendering.Selection;
|
||||
|
||||
/// <summary>
|
||||
/// Decodes the exact CPU geometry consumed by retail mouse selection. The broad
|
||||
/// sphere comes from the drawing-BSP root; polygons retain GfxObj DAT order.
|
||||
/// </summary>
|
||||
internal sealed class RetailSelectionGeometryCache
|
||||
{
|
||||
private readonly DatCollection _dats;
|
||||
private readonly object _datLock;
|
||||
// Render-thread owned. Dictionary permits a cached null for a GfxObj which
|
||||
// legitimately has no drawing BSP; ConcurrentDictionary does not.
|
||||
private readonly Dictionary<uint, RetailSelectionMesh?> _cache = new();
|
||||
|
||||
public RetailSelectionGeometryCache(DatCollection dats, object datLock)
|
||||
{
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
||||
}
|
||||
|
||||
public RetailSelectionMesh? Resolve(uint gfxObjId)
|
||||
{
|
||||
if (_cache.TryGetValue(gfxObjId, out var cached))
|
||||
return cached;
|
||||
RetailSelectionMesh? loaded = Load(gfxObjId);
|
||||
_cache[gfxObjId] = loaded;
|
||||
return loaded;
|
||||
}
|
||||
|
||||
private RetailSelectionMesh? Load(uint gfxObjId)
|
||||
{
|
||||
GfxObj? gfx;
|
||||
lock (_datLock)
|
||||
gfx = _dats.Get<GfxObj>(gfxObjId);
|
||||
|
||||
var root = gfx?.DrawingBSP?.Root;
|
||||
if (gfx is null || root is null || root.BoundingSphere.Radius <= 0f)
|
||||
return null;
|
||||
|
||||
var polygons = new List<RetailSelectionPolygon>(gfx.Polygons.Count);
|
||||
foreach (var entry in gfx.Polygons)
|
||||
{
|
||||
var source = entry.Value;
|
||||
if (source.VertexIds.Count < 3)
|
||||
continue;
|
||||
|
||||
var vertices = new Vector3[source.VertexIds.Count];
|
||||
bool valid = true;
|
||||
for (int i = 0; i < source.VertexIds.Count; i++)
|
||||
{
|
||||
if (!gfx.VertexArray.Vertices.TryGetValue((ushort)source.VertexIds[i], out var vertex))
|
||||
{
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
vertices[i] = vertex.Origin;
|
||||
}
|
||||
if (!valid)
|
||||
continue;
|
||||
|
||||
polygons.Add(new RetailSelectionPolygon(
|
||||
vertices,
|
||||
SingleSided: (int)source.SidesType == 0));
|
||||
}
|
||||
|
||||
return new RetailSelectionMesh(
|
||||
root.BoundingSphere.Origin,
|
||||
root.BoundingSphere.Radius,
|
||||
polygons);
|
||||
}
|
||||
}
|
||||
113
src/AcDream.App/Rendering/Selection/RetailSelectionScene.cs
Normal file
113
src/AcDream.App/Rendering/Selection/RetailSelectionScene.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
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
|
||||
{
|
||||
private readonly RetailSelectionGeometryCache _geometry;
|
||||
private List<RetailSelectionPart> _building = new();
|
||||
private List<RetailSelectionPart> _published = new();
|
||||
private readonly HashSet<PartKey> _buildingKeys = new();
|
||||
private HashSet<uint> _buildingGuids = new();
|
||||
private HashSet<uint> _publishedGuids = new();
|
||||
private FrustumPlanes? _viewFrustum;
|
||||
|
||||
private readonly record struct PartKey(uint LocalEntityId, int PartIndex, uint GfxObjId);
|
||||
|
||||
public RetailSelectionScene(RetailSelectionGeometryCache geometry)
|
||||
=> _geometry = geometry ?? throw new ArgumentNullException(nameof(geometry));
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
_building.Clear();
|
||||
_buildingKeys.Clear();
|
||||
_buildingGuids.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));
|
||||
_buildingGuids.Add(entity.ServerGuid);
|
||||
}
|
||||
|
||||
public void CompleteFrame()
|
||||
{
|
||||
(_published, _building) = (_building, _published);
|
||||
(_publishedGuids, _buildingGuids) = (_buildingGuids, _publishedGuids);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public bool WasVisible(uint serverGuid) => _publishedGuids.Contains(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;
|
||||
}
|
||||
|
|
@ -7,6 +7,7 @@ using AcDream.Core.Meshing;
|
|||
using AcDream.Core.Rendering;
|
||||
using AcDream.Core.Terrain;
|
||||
using AcDream.Core.World;
|
||||
using AcDream.App.Rendering.Selection;
|
||||
using DatReaderWriter.Enums;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
|
|
@ -85,6 +86,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
private readonly TextureCache _textures;
|
||||
private readonly WbMeshAdapter _meshAdapter;
|
||||
private readonly EntitySpawnAdapter _entitySpawnAdapter;
|
||||
private readonly IRetailSelectionRenderSink? _selectionSink;
|
||||
|
||||
private readonly BindlessSupport _bindless;
|
||||
|
||||
|
|
@ -354,7 +356,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
EntitySpawnAdapter entitySpawnAdapter,
|
||||
BindlessSupport bindless,
|
||||
EntityClassificationCache classificationCache,
|
||||
AcDream.Core.Rendering.TranslucencyFadeManager translucencyFades)
|
||||
AcDream.Core.Rendering.TranslucencyFadeManager translucencyFades,
|
||||
IRetailSelectionRenderSink? selectionSink = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(shader);
|
||||
|
|
@ -371,6 +374,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_entitySpawnAdapter = entitySpawnAdapter;
|
||||
_cache = classificationCache;
|
||||
_translucencyFades = translucencyFades;
|
||||
_selectionSink = selectionSink;
|
||||
|
||||
_bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
|
||||
_instanceSsbo = _gl.GenBuffer();
|
||||
|
|
@ -1187,6 +1191,13 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
ApplyCacheHit(cachedEntry!, entityWorld, AppendInstanceToGroup);
|
||||
|
||||
// The cache is populated only after every MeshRef rendered
|
||||
// successfully. Publish the same parts for retail picking now;
|
||||
// CPhysicsPart::Draw only participates after the visible draw
|
||||
// path has accepted a real part.
|
||||
if (_selectionSink is not null)
|
||||
PublishCachedSelectionParts(entity, entityWorld);
|
||||
|
||||
// anyVao recovery: when the first visible entity in the frame
|
||||
// takes the fast path, no slow-path lookup has populated
|
||||
// anyVao yet. Look up THIS entity's first MeshRef once via
|
||||
|
|
@ -1374,6 +1385,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
|
||||
ClassifyBatches(partData, partGfxObjId, model, entity, meshRef, palHash, metaTable, restPose, opacityMultiplier, collector);
|
||||
_selectionSink?.AddVisiblePart(
|
||||
entity,
|
||||
unchecked((partIdx << 16) | (setupPartIndex & 0xFFFF)),
|
||||
(uint)partGfxObjId,
|
||||
model);
|
||||
drewAny = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1394,6 +1410,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
{
|
||||
var model = meshRef.PartTransform * entityWorld;
|
||||
ClassifyBatches(renderData, gfxObjId, model, entity, meshRef, palHash, metaTable, restPose: meshRef.PartTransform, opacityMultiplier: opacityMultiplier, collector: collector);
|
||||
_selectionSink?.AddVisiblePart(
|
||||
entity,
|
||||
partIdx,
|
||||
(uint)gfxObjId,
|
||||
model);
|
||||
drewAny = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -1813,6 +1834,44 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
set: set);
|
||||
}
|
||||
|
||||
private void PublishCachedSelectionParts(WorldEntity entity, Matrix4x4 entityWorld)
|
||||
{
|
||||
for (int outerPartIndex = 0; outerPartIndex < entity.MeshRefs.Count; outerPartIndex++)
|
||||
{
|
||||
var meshRef = entity.MeshRefs[outerPartIndex];
|
||||
var renderData = _meshAdapter.TryGetRenderData(meshRef.GfxObjId);
|
||||
if (renderData is null)
|
||||
continue;
|
||||
|
||||
if (!renderData.IsSetup || renderData.SetupParts.Count == 0)
|
||||
{
|
||||
_selectionSink!.AddVisiblePart(
|
||||
entity,
|
||||
outerPartIndex,
|
||||
meshRef.GfxObjId,
|
||||
meshRef.PartTransform * entityWorld);
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int setupPartIndex = 0;
|
||||
setupPartIndex < renderData.SetupParts.Count;
|
||||
setupPartIndex++)
|
||||
{
|
||||
var (partGfxObjId, partTransform) = renderData.SetupParts[setupPartIndex];
|
||||
if (_meshAdapter.TryGetRenderData(partGfxObjId) is null)
|
||||
continue;
|
||||
_selectionSink!.AddVisiblePart(
|
||||
entity,
|
||||
unchecked((outerPartIndex << 16) | (setupPartIndex & 0xFFFF)),
|
||||
(uint)partGfxObjId,
|
||||
ComposePartWorldMatrix(
|
||||
entityWorld,
|
||||
meshRef.PartTransform,
|
||||
partTransform));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IndirectGroupInput ToInput(InstanceGroup g) => new(
|
||||
IndexCount: g.IndexCount,
|
||||
FirstIndex: g.FirstIndex,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue