809 lines
27 KiB
C#
809 lines
27 KiB
C#
using System.Numerics;
|
|
using AcDream.Core.Rendering;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Selection;
|
|
using AcDream.Core.Lighting;
|
|
using AcDream.Core.Meshing;
|
|
using AcDream.Core.Selection;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.Enums;
|
|
|
|
namespace AcDream.App.Rendering.Wb;
|
|
|
|
/// <summary>
|
|
/// Scene-frame classification and production submission. The retained group
|
|
/// storage is also consumed by the G2/G3 compare oracle, while production
|
|
/// routes reuse the dispatcher's exact mesh, texture, light, translucency,
|
|
/// selection, upload, and draw owners.
|
|
/// </summary>
|
|
public sealed unsafe partial class WbDrawDispatcher
|
|
{
|
|
private readonly Dictionary<RenderProjectionId, RenderFrameEntityCandidate>
|
|
_packedEntityById = [];
|
|
private readonly Dictionary<GroupKey, InstanceGroup> _packedGroups = [];
|
|
private readonly List<GroupKey> _packedRetiredGroupKeys = [];
|
|
private readonly List<InstanceGroup> _packedOpaque = [];
|
|
private readonly List<InstanceGroup> _packedTransparent = [];
|
|
private readonly List<AlphaFingerprint>
|
|
_packedAlphaFingerprintScratch = [];
|
|
private readonly List<CurrentRenderDispatcherSubmission>
|
|
_packedSubmissions = [];
|
|
private readonly List<CurrentRenderSelectionFingerprint>
|
|
_packedSelectionParts = [];
|
|
private readonly HashSet<PackedSelectionKey> _packedSelectionKeys = [];
|
|
private readonly PackedProjectionClassificationCache
|
|
_packedClassificationCache = new();
|
|
private long _packedGroupFrame;
|
|
private long _nextPackedGroupRegistration = 1;
|
|
private int _nextPackedInstanceSubmissionOrder;
|
|
private bool _packedProductionFrameOpen;
|
|
private RenderSceneGeneration _packedProductionGeneration;
|
|
private ulong _packedProductionFrameSequence;
|
|
private int _packedProductionNextRange;
|
|
|
|
internal IReadOnlyList<CurrentRenderDispatcherSubmission>
|
|
PackedDispatcherSubmissions => _packedSubmissions;
|
|
|
|
internal IReadOnlyList<CurrentRenderSelectionFingerprint>
|
|
PackedSelectionParts => _packedSelectionParts;
|
|
|
|
internal PackedClassificationCacheSnapshot
|
|
PackedClassificationSnapshot =>
|
|
_packedClassificationCache.Snapshot;
|
|
|
|
internal void BuildPackedDispatcherOracle(
|
|
in RenderFrameView view,
|
|
uint tupleLandblockId,
|
|
Vector3 cameraWorldPosition)
|
|
{
|
|
if (_packedProductionFrameOpen)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The packed dispatcher oracle cannot replace an active production frame.");
|
|
}
|
|
|
|
BeginPackedFrameStorage(in view);
|
|
ReadOnlySpan<RenderFrameCandidateRange> ranges =
|
|
view.RouteRanges;
|
|
for (int rangeIndex = 0;
|
|
rangeIndex < ranges.Length;
|
|
rangeIndex++)
|
|
{
|
|
PackedRangeClassification classified =
|
|
ClassifyPackedRange(
|
|
in view,
|
|
rangeIndex,
|
|
tupleLandblockId,
|
|
publishSelection: false);
|
|
|
|
bool deferTransparent = ShouldDeferPackedTransparent(
|
|
classified.AnyVao,
|
|
_alphaQueue?.IsCollecting == true);
|
|
InstanceLayoutCounts counts = PartitionInstanceGroups(
|
|
_packedGroups.Values,
|
|
deferTransparent,
|
|
cameraWorldPosition,
|
|
_packedOpaque,
|
|
_packedTransparent);
|
|
_packedOpaque.Sort(CompareOpaqueSubmissionOrder);
|
|
if (!deferTransparent)
|
|
{
|
|
_packedTransparent.Sort(
|
|
CompareTransparentSubmissionOrder);
|
|
}
|
|
|
|
_packedSubmissions.Add(CreateDispatcherSubmission(
|
|
counts.VisibleInstances,
|
|
counts.ImmediateInstances,
|
|
deferTransparent,
|
|
_packedOpaque,
|
|
_packedTransparent,
|
|
cameraWorldPosition,
|
|
_packedAlphaFingerprintScratch));
|
|
}
|
|
|
|
_packedClassificationCache.EndFrame();
|
|
}
|
|
|
|
internal void BeginPackedProductionFrame(
|
|
in RenderFrameView view)
|
|
{
|
|
if (_packedProductionFrameOpen)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The packed dispatcher cannot begin a second production frame.");
|
|
}
|
|
|
|
BeginPackedFrameStorage(in view);
|
|
_packedProductionFrameOpen = true;
|
|
_packedProductionGeneration = view.Generation;
|
|
_packedProductionFrameSequence = view.FrameSequence;
|
|
_packedProductionNextRange = 0;
|
|
}
|
|
|
|
internal bool DrawPackedProductionRoute(
|
|
ICamera camera,
|
|
in RenderFrameView view,
|
|
RenderFrameCandidateRoute route,
|
|
int routeIndex,
|
|
uint cellId,
|
|
uint tupleLandblockId)
|
|
{
|
|
ValidatePackedProductionView(in view);
|
|
ReadOnlySpan<RenderFrameCandidateRange> ranges =
|
|
view.RouteRanges;
|
|
if (_packedProductionNextRange >= ranges.Length)
|
|
return false;
|
|
|
|
RenderFrameCandidateRange range =
|
|
ranges[_packedProductionNextRange];
|
|
if (range.Route != route
|
|
|| range.RouteIndex != routeIndex
|
|
|| range.CellId != cellId)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int rangePosition = _packedProductionNextRange++;
|
|
bool diagnosticsEnabled = BeginEntityDispatch(
|
|
camera,
|
|
out Matrix4x4 viewProjection,
|
|
out Vector3 cameraWorldPosition);
|
|
PackedRangeClassification classified =
|
|
ClassifyPackedRange(
|
|
in view,
|
|
rangePosition,
|
|
tupleLandblockId,
|
|
publishSelection: true);
|
|
ExecuteClassifiedGroups(
|
|
viewProjection,
|
|
cameraWorldPosition,
|
|
classified.AnyVao,
|
|
_packedGroups.Values,
|
|
EntitySet.All,
|
|
classified.CandidateCount,
|
|
classified.MeshPartCount,
|
|
diagnosticsEnabled,
|
|
observeCurrentPath: false);
|
|
return true;
|
|
}
|
|
|
|
internal void CompletePackedProductionFrame(
|
|
in RenderFrameView view)
|
|
{
|
|
ValidatePackedProductionView(in view);
|
|
int expected = view.RouteRanges.Length;
|
|
if (_packedProductionNextRange != expected)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The packed dispatcher did not consume the complete route stream: "
|
|
+ $"consumed={_packedProductionNextRange} expected={expected}.");
|
|
}
|
|
|
|
_packedClassificationCache.EndFrame();
|
|
ResetPackedProductionFrame();
|
|
}
|
|
|
|
internal void AbortPackedProductionFrame()
|
|
{
|
|
if (!_packedProductionFrameOpen)
|
|
return;
|
|
|
|
_packedClassificationCache.EndFrame();
|
|
ResetPackedProductionFrame();
|
|
}
|
|
|
|
private void BeginPackedFrameStorage(
|
|
in RenderFrameView view)
|
|
{
|
|
if (_packedGroupFrame == long.MaxValue)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Packed dispatcher frame identity was exhausted.");
|
|
}
|
|
|
|
_packedGroupFrame++;
|
|
PruneInstanceGroupsUnusedBeforeFrame(
|
|
_packedGroups,
|
|
_packedRetiredGroupKeys,
|
|
_packedGroupFrame - 1);
|
|
_packedEntityById.Clear();
|
|
_packedSubmissions.Clear();
|
|
_packedSelectionParts.Clear();
|
|
_packedSelectionKeys.Clear();
|
|
_packedClassificationCache.BeginFrame(view.Generation);
|
|
|
|
ReadOnlySpan<RenderFrameEntityCandidate> entities =
|
|
view.EntityCandidates;
|
|
for (int index = 0; index < entities.Length; index++)
|
|
{
|
|
if (!_packedEntityById.TryAdd(
|
|
entities[index].Projection.Id,
|
|
entities[index]))
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Packed dispatcher received duplicate projection "
|
|
+ $"{entities[index].Projection.Id}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
private PackedRangeClassification ClassifyPackedRange(
|
|
in RenderFrameView view,
|
|
int rangeIndex,
|
|
uint tupleLandblockId,
|
|
bool publishSelection)
|
|
{
|
|
ReadOnlySpan<RenderFrameCandidateRange> ranges =
|
|
view.RouteRanges;
|
|
if ((uint)rangeIndex >= (uint)ranges.Length)
|
|
throw new ArgumentOutOfRangeException(nameof(rangeIndex));
|
|
|
|
_nextPackedInstanceSubmissionOrder = 0;
|
|
foreach (InstanceGroup group in _packedGroups.Values)
|
|
group.ClearPerInstanceData();
|
|
|
|
uint anyVao = 0;
|
|
int meshPartCount = 0;
|
|
RenderFrameCandidateRange range = ranges[rangeIndex];
|
|
ReadOnlySpan<RenderProjectionRecord> routeCandidates =
|
|
view.RouteCandidates;
|
|
ReadOnlySpan<RenderFrameMeshPart> meshParts = view.MeshParts;
|
|
int end = checked(range.Offset + range.Count);
|
|
if ((uint)range.Offset > (uint)routeCandidates.Length
|
|
|| (uint)end > (uint)routeCandidates.Length)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Packed dispatcher route {rangeIndex} exceeds candidate storage.");
|
|
}
|
|
|
|
for (int candidateIndex = range.Offset;
|
|
candidateIndex < end;
|
|
candidateIndex++)
|
|
{
|
|
RenderProjectionRecord projection =
|
|
routeCandidates[candidateIndex];
|
|
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
|
|
continue;
|
|
if (!_packedEntityById.TryGetValue(
|
|
projection.Id,
|
|
out RenderFrameEntityCandidate source))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Packed route references absent entity {projection.Id}.");
|
|
}
|
|
|
|
int meshEnd = checked(
|
|
source.MeshPartOffset + source.MeshPartCount);
|
|
if ((uint)source.MeshPartOffset > (uint)meshParts.Length
|
|
|| (uint)meshEnd > (uint)meshParts.Length)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Packed entity {projection.Id} exceeds mesh part storage.");
|
|
}
|
|
|
|
meshPartCount = checked(
|
|
meshPartCount + source.MeshPartCount);
|
|
RenderInstanceCandidate candidate =
|
|
RenderInstanceCandidate.FromFrame(
|
|
in source,
|
|
tupleLandblockId);
|
|
ClassifyPackedEntity(
|
|
in projection,
|
|
in candidate,
|
|
meshParts.Slice(
|
|
source.MeshPartOffset,
|
|
source.MeshPartCount),
|
|
ref anyVao,
|
|
publishSelection);
|
|
}
|
|
|
|
return new PackedRangeClassification(
|
|
anyVao,
|
|
range.Count,
|
|
meshPartCount);
|
|
}
|
|
|
|
private void ValidatePackedProductionView(
|
|
in RenderFrameView view)
|
|
{
|
|
if (!_packedProductionFrameOpen
|
|
|| view.Generation != _packedProductionGeneration
|
|
|| view.FrameSequence != _packedProductionFrameSequence)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"The packed dispatcher received a stale or foreign production frame.");
|
|
}
|
|
}
|
|
|
|
private void ResetPackedProductionFrame()
|
|
{
|
|
_packedProductionFrameOpen = false;
|
|
_packedProductionGeneration = default;
|
|
_packedProductionFrameSequence = 0;
|
|
_packedProductionNextRange = 0;
|
|
}
|
|
|
|
private readonly record struct PackedRangeClassification(
|
|
uint AnyVao,
|
|
int CandidateCount,
|
|
int MeshPartCount);
|
|
|
|
private void ClassifyPackedEntity(
|
|
in RenderProjectionRecord projection,
|
|
in RenderInstanceCandidate entity,
|
|
ReadOnlySpan<RenderFrameMeshPart> meshParts,
|
|
ref uint anyVao,
|
|
bool publishSelection)
|
|
{
|
|
(uint slot, bool culled) = ResolveSlotForFrame(
|
|
_clipRoutingActive,
|
|
entity.ServerGuid,
|
|
entity.ParentCell,
|
|
_cellIdToSlot,
|
|
_outdoorSlot,
|
|
_outdoorVisible);
|
|
if (culled)
|
|
return;
|
|
|
|
ResolvePackedLightSet(
|
|
in entity,
|
|
out InstanceLightSet lights,
|
|
out bool indoor);
|
|
Vector2 selectionLighting =
|
|
_selectionLighting?.TryGetLighting(
|
|
entity.ServerGuid,
|
|
entity.LocalEntityId,
|
|
out RetailSelectionLighting lighting) == true
|
|
? new Vector2(
|
|
lighting.Luminosity,
|
|
lighting.Diffuse)
|
|
: new Vector2(0f, 1f);
|
|
|
|
PackedProjectionClassificationEntry? cacheEntry = null;
|
|
if (!entity.Animated)
|
|
{
|
|
PackedClassificationIdentity identity =
|
|
PackedClassificationIdentity.From(in projection);
|
|
if (_packedClassificationCache.TryGetReusable(
|
|
entity.ProjectionId,
|
|
in identity,
|
|
projection.DirtyMask,
|
|
out cacheEntry))
|
|
{
|
|
if (anyVao == 0 && !meshParts.IsEmpty)
|
|
{
|
|
ObjectRenderData? firstRenderData =
|
|
_meshAdapter.TryGetRenderData(
|
|
meshParts[0].MeshRef.GfxObjId);
|
|
if (firstRenderData is not null)
|
|
anyVao = firstRenderData.VAO;
|
|
}
|
|
ReplayPackedClassification(
|
|
cacheEntry!,
|
|
in entity,
|
|
slot,
|
|
lights,
|
|
indoor,
|
|
selectionLighting,
|
|
publishSelection);
|
|
return;
|
|
}
|
|
|
|
cacheEntry = _packedClassificationCache.BeginRebuild(
|
|
entity.ProjectionId,
|
|
in identity);
|
|
}
|
|
else
|
|
{
|
|
_packedClassificationCache.RecordAnimatedClassification();
|
|
}
|
|
|
|
PaletteCompositeIdentity paletteIdentity = default;
|
|
if (entity.PaletteOverride is not null)
|
|
{
|
|
paletteIdentity = TextureCache.GetPaletteIdentity(
|
|
entity.PaletteOverride);
|
|
}
|
|
|
|
bool reusableAcrossFrames = !entity.Animated;
|
|
for (int meshIndex = 0;
|
|
meshIndex < meshParts.Length;
|
|
meshIndex++)
|
|
{
|
|
RenderFrameMeshPart packedPart = meshParts[meshIndex];
|
|
if (packedPart.ProjectionId != entity.ProjectionId)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Packed mesh part {packedPart.PartIndex} belongs "
|
|
+ $"to {packedPart.ProjectionId}, expected "
|
|
+ $"{entity.ProjectionId}.");
|
|
}
|
|
|
|
int partIndex = packedPart.PartIndex;
|
|
MeshRef meshRef = packedPart.MeshRef;
|
|
ObjectRenderData? renderData =
|
|
_meshAdapter.TryGetRenderData(meshRef.GfxObjId);
|
|
if (renderData is null)
|
|
{
|
|
reusableAcrossFrames = false;
|
|
if (_missRequested.Add(meshRef.GfxObjId))
|
|
_meshAdapter.EnsureLoaded(meshRef.GfxObjId);
|
|
continue;
|
|
}
|
|
if (anyVao == 0)
|
|
anyVao = renderData.VAO;
|
|
|
|
if (renderData.IsSetup
|
|
&& renderData.SetupParts.Count > 0)
|
|
{
|
|
for (int setupPartIndex = 0;
|
|
setupPartIndex < renderData.SetupParts.Count;
|
|
setupPartIndex++)
|
|
{
|
|
(ulong gfxObjId, Matrix4x4 partTransform) =
|
|
renderData.SetupParts[setupPartIndex];
|
|
ObjectRenderData? partData =
|
|
_meshAdapter.TryGetRenderData(gfxObjId);
|
|
if (partData is null)
|
|
{
|
|
reusableAcrossFrames = false;
|
|
if (_missRequested.Add(gfxObjId))
|
|
_meshAdapter.EnsureLoaded(gfxObjId);
|
|
continue;
|
|
}
|
|
|
|
float opacity = PackedPartOpacity(
|
|
entity.LocalEntityId,
|
|
(uint)setupPartIndex);
|
|
if (opacity < 1f)
|
|
reusableAcrossFrames = false;
|
|
if (opacity <= 0f)
|
|
continue;
|
|
|
|
Matrix4x4 restPose =
|
|
partTransform * meshRef.PartTransform;
|
|
Matrix4x4 model =
|
|
restPose * entity.RootWorld;
|
|
if (!ClassifyPackedBatches(
|
|
partData,
|
|
restPose,
|
|
model,
|
|
in entity,
|
|
meshRef,
|
|
paletteIdentity,
|
|
slot,
|
|
lights,
|
|
indoor,
|
|
selectionLighting,
|
|
opacity,
|
|
cacheEntry))
|
|
{
|
|
reusableAcrossFrames = false;
|
|
}
|
|
int selectionPartIndex = unchecked(
|
|
(partIndex << 16)
|
|
| (setupPartIndex & 0xFFFF));
|
|
cacheEntry?.SelectionParts.Add(
|
|
new PackedClassifiedSelectionPart(
|
|
selectionPartIndex,
|
|
(uint)gfxObjId,
|
|
restPose));
|
|
AddPackedSelectionPart(
|
|
in entity,
|
|
selectionPartIndex,
|
|
(uint)gfxObjId,
|
|
model,
|
|
publishSelection);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float opacity = PackedPartOpacity(
|
|
entity.LocalEntityId,
|
|
0u);
|
|
if (opacity < 1f)
|
|
reusableAcrossFrames = false;
|
|
if (opacity <= 0f)
|
|
continue;
|
|
|
|
Matrix4x4 restPose = meshRef.PartTransform;
|
|
Matrix4x4 model = restPose * entity.RootWorld;
|
|
if (!ClassifyPackedBatches(
|
|
renderData,
|
|
restPose,
|
|
model,
|
|
in entity,
|
|
meshRef,
|
|
paletteIdentity,
|
|
slot,
|
|
lights,
|
|
indoor,
|
|
selectionLighting,
|
|
opacity,
|
|
cacheEntry))
|
|
{
|
|
reusableAcrossFrames = false;
|
|
}
|
|
cacheEntry?.SelectionParts.Add(
|
|
new PackedClassifiedSelectionPart(
|
|
partIndex,
|
|
meshRef.GfxObjId,
|
|
restPose));
|
|
AddPackedSelectionPart(
|
|
in entity,
|
|
partIndex,
|
|
meshRef.GfxObjId,
|
|
model,
|
|
publishSelection);
|
|
}
|
|
}
|
|
|
|
if (cacheEntry is not null)
|
|
{
|
|
_packedClassificationCache.CompleteRebuild(
|
|
cacheEntry,
|
|
reusableAcrossFrames);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Mirrors the production dispatcher's no-VAO early return. That return
|
|
/// records an empty submission with transparent deferral disabled even
|
|
/// when the frame alpha queue is collecting.
|
|
/// </summary>
|
|
internal static bool ShouldDeferPackedTransparent(
|
|
uint anyVao,
|
|
bool alphaQueueCollecting) =>
|
|
anyVao != 0 && alphaQueueCollecting;
|
|
|
|
private float PackedPartOpacity(
|
|
uint localEntityId,
|
|
uint setupPartIndex)
|
|
{
|
|
if (!_translucencyFades.TryGetCurrentValue(
|
|
localEntityId,
|
|
setupPartIndex,
|
|
out float translucency))
|
|
{
|
|
return 1f;
|
|
}
|
|
|
|
return translucency >= 1f
|
|
? 0f
|
|
: 1f - translucency;
|
|
}
|
|
|
|
private bool ClassifyPackedBatches(
|
|
ObjectRenderData renderData,
|
|
Matrix4x4 restPose,
|
|
Matrix4x4 model,
|
|
in RenderInstanceCandidate entity,
|
|
MeshRef meshRef,
|
|
PaletteCompositeIdentity paletteIdentity,
|
|
uint slot,
|
|
InstanceLightSet lights,
|
|
bool indoor,
|
|
Vector2 selectionLighting,
|
|
float opacity,
|
|
PackedProjectionClassificationEntry? cacheEntry)
|
|
{
|
|
bool reusableAcrossFrames = true;
|
|
for (int batchIndex = 0;
|
|
batchIndex < renderData.Batches.Count;
|
|
batchIndex++)
|
|
{
|
|
ObjectRenderBatch batch =
|
|
renderData.Batches[batchIndex];
|
|
TranslucencyKind translucency = batch.Translucency;
|
|
if (opacity < 1f && IsOpaque(translucency))
|
|
translucency = TranslucencyKind.AlphaBlend;
|
|
|
|
ResolvedTexture texture = ResolveTexture(
|
|
in entity,
|
|
meshRef,
|
|
batch,
|
|
paletteIdentity,
|
|
out bool compositePending);
|
|
if (compositePending)
|
|
reusableAcrossFrames = false;
|
|
if (texture.Handle == 0)
|
|
continue;
|
|
|
|
var key = new GroupKey(
|
|
batch.FirstIndex,
|
|
(int)batch.BaseVertex,
|
|
batch.IndexCount,
|
|
texture.Handle,
|
|
texture.Layer,
|
|
translucency,
|
|
batch.CullMode);
|
|
var classified = new PackedClassifiedBatch(
|
|
key,
|
|
restPose,
|
|
renderData.SortCenter);
|
|
cacheEntry?.Batches.Add(classified);
|
|
AppendPackedClassification(
|
|
in classified,
|
|
model,
|
|
slot,
|
|
lights,
|
|
indoor,
|
|
opacity,
|
|
selectionLighting);
|
|
}
|
|
|
|
return reusableAcrossFrames;
|
|
}
|
|
|
|
private void ReplayPackedClassification(
|
|
PackedProjectionClassificationEntry entry,
|
|
in RenderInstanceCandidate entity,
|
|
uint slot,
|
|
InstanceLightSet lights,
|
|
bool indoor,
|
|
Vector2 selectionLighting,
|
|
bool publishSelection)
|
|
{
|
|
for (int i = 0; i < entry.Batches.Count; i++)
|
|
{
|
|
PackedClassifiedBatch classified = entry.Batches[i];
|
|
AppendPackedClassification(
|
|
in classified,
|
|
classified.RestPose * entity.RootWorld,
|
|
slot,
|
|
lights,
|
|
indoor,
|
|
opacity: 1f,
|
|
selectionLighting);
|
|
}
|
|
|
|
for (int i = 0; i < entry.SelectionParts.Count; i++)
|
|
{
|
|
PackedClassifiedSelectionPart part =
|
|
entry.SelectionParts[i];
|
|
AddPackedSelectionPart(
|
|
in entity,
|
|
part.PartIndex,
|
|
part.GfxObjId,
|
|
part.RestPose * entity.RootWorld,
|
|
publishSelection);
|
|
}
|
|
}
|
|
|
|
private void AppendPackedClassification(
|
|
in PackedClassifiedBatch classified,
|
|
Matrix4x4 model,
|
|
uint slot,
|
|
InstanceLightSet lights,
|
|
bool indoor,
|
|
float opacity,
|
|
Vector2 selectionLighting)
|
|
{
|
|
InstanceGroup group =
|
|
GetOrCreatePackedGroup(classified.Key);
|
|
group.Matrices.Add(model);
|
|
group.LocalSortCenters.Add(
|
|
classified.LocalSortCenter);
|
|
group.SubmissionOrders.Add(
|
|
_nextPackedInstanceSubmissionOrder++);
|
|
group.Slots.Add(slot);
|
|
group.LightSets.Add(lights);
|
|
group.IndoorFlags.Add(indoor ? 1u : 0u);
|
|
group.Opacities.Add(opacity);
|
|
group.SelectionLighting.Add(selectionLighting);
|
|
}
|
|
|
|
private InstanceGroup GetOrCreatePackedGroup(GroupKey key)
|
|
{
|
|
if (_packedGroups.TryGetValue(
|
|
key,
|
|
out InstanceGroup? group))
|
|
{
|
|
group.LastUsedFrame = _packedGroupFrame;
|
|
return group;
|
|
}
|
|
if (_nextPackedGroupRegistration == long.MaxValue)
|
|
{
|
|
throw new InvalidOperationException(
|
|
"Packed instance-group registration space was exhausted.");
|
|
}
|
|
|
|
group = new InstanceGroup
|
|
{
|
|
FirstIndex = key.FirstIndex,
|
|
BaseVertex = key.BaseVertex,
|
|
IndexCount = key.IndexCount,
|
|
BindlessTextureHandle = key.BindlessTextureHandle,
|
|
TextureLayer = key.TextureLayer,
|
|
Translucency = key.Translucency,
|
|
CullMode = key.CullMode,
|
|
Registration = _nextPackedGroupRegistration++,
|
|
LastUsedFrame = _packedGroupFrame,
|
|
};
|
|
_packedGroups.Add(key, group);
|
|
return group;
|
|
}
|
|
|
|
private void ResolvePackedLightSet(
|
|
in RenderInstanceCandidate entity,
|
|
out InstanceLightSet lights,
|
|
out bool indoor)
|
|
{
|
|
indoor = IndoorObjectReceivesTorches(entity.ParentCell);
|
|
lights = InstanceLightSet.Disabled;
|
|
IReadOnlyList<LightSource>? snapshot = _pointSnapshot;
|
|
if (!indoor || snapshot is null || snapshot.Count == 0)
|
|
return;
|
|
|
|
Vector3 center =
|
|
(entity.Bounds.Minimum + entity.Bounds.Maximum) * 0.5f;
|
|
float radius =
|
|
(entity.Bounds.Maximum - entity.Bounds.Minimum)
|
|
.Length() * 0.5f;
|
|
Span<int> selected =
|
|
stackalloc int[LightManager.MaxLightsPerObject];
|
|
selected.Fill(-1);
|
|
LightManager.SelectForObject(
|
|
snapshot,
|
|
center,
|
|
radius,
|
|
selected);
|
|
lights = InstanceLightSet.From(selected);
|
|
}
|
|
|
|
private void AddPackedSelectionPart(
|
|
in RenderInstanceCandidate entity,
|
|
int partIndex,
|
|
uint gfxObjId,
|
|
Matrix4x4 localToWorld,
|
|
bool publishSelection)
|
|
{
|
|
if (!_packedSelectionKeys.Add(new PackedSelectionKey(
|
|
entity.LocalEntityId,
|
|
partIndex,
|
|
gfxObjId)))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (publishSelection)
|
|
{
|
|
_selectionSink?.AddVisiblePart(
|
|
entity.ServerGuid,
|
|
entity.LocalEntityId,
|
|
partIndex,
|
|
gfxObjId,
|
|
localToWorld);
|
|
return;
|
|
}
|
|
|
|
if (_selectionSink is not IRetailSelectionRenderOracle oracle
|
|
|| !oracle.TryCreateVisiblePart(
|
|
entity.ServerGuid,
|
|
entity.LocalEntityId,
|
|
partIndex,
|
|
gfxObjId,
|
|
localToWorld,
|
|
out RetailSelectionPart part))
|
|
{
|
|
return;
|
|
}
|
|
|
|
_packedSelectionParts.Add(
|
|
new CurrentRenderSelectionFingerprint(
|
|
Sequence: _packedSelectionParts.Count,
|
|
ServerGuid: part.ServerGuid,
|
|
LocalEntityId: part.LocalEntityId,
|
|
PartIndex: part.PartIndex,
|
|
GfxObjId: gfxObjId,
|
|
LocalToWorld: part.LocalToWorld,
|
|
Geometry: CurrentRenderSceneOracle
|
|
.FingerprintSelectionGeometry(part.Mesh)));
|
|
}
|
|
|
|
private readonly record struct PackedSelectionKey(
|
|
uint LocalEntityId,
|
|
int PartIndex,
|
|
uint GfxObjId);
|
|
}
|