Some checks failed
Headless portability / portable-headless (ubuntu-latest) (push) Has been cancelled
Headless portability / portable-headless (windows-latest) (push) Has been cancelled
Headless portability / linux-graphical (push) Has been cancelled
Headless portability / linux-vulkan (push) Has been cancelled
The user reported crystal shards hovering in the air above every Bind
Stone on Coldeve (setup 0x020010AC) that do not exist in the retail
client. The DAT truth, extracted with the new tools/SetupInspect probe:
the model authors SEVEN parts - pedestal, spinning column, inner
crystal, and four shard meshes parked in a static ring at Z=3.0 in the
placement frame and every frame of the idle cycle - and frame 0 of that
idle cycle fires four TransparentPartHooks (parts 3-6, start=end=1.0)
each loop. Retail hides the shards through those hooks; the model
simply ships with permanently-hooked-invisible parts.
acdream's hook chain was intact end to end - the static-animating
workset captures the hooks (RetailStaticAnimatingObjectScheduler ->
AnimationHookFrameQueue -> TranslucencyHookSink), and
TranslucencyFadeManager committed translucency 1.0 for parts 3-6 -
but BOTH dispatchers' bare-GfxObj branch read the fade with a
hard-coded part index 0 under a false #188-era assumption ("a bare
GfxObj entity has exactly one part"). Every live server object is a
FLATTENED multi-part entity in exactly that branch: SetupMesh.Flatten
emits one bare-GfxObj MeshRef per Setup.Parts[i], order preserved,
AnimPartChanges replacing in place - so the MeshRef ordinal IS the
retail CPartArray ordinal TransparentPartHook.PartIndex addresses.
The committed invisibility for parts 3-6 was never consulted and the
shards drew forever. Proof the ordinal was trustworthy all along:
click-selection in the same loops already publishes it as the part
identity (Slice 4 picking runs on it in production).
Fix: both the legacy classifier and the packed oracle now pass the
per-part ordinal (partIdx / packedPart.PartIndex) to the translucency
lookup. Single-part objects still read index 0; the #188 door fades
are unchanged; the Setup-expanded branch already indexed correctly.
Any other object hiding authored parts via idle-loop hooks gets its
retail appearance from the same change.
tools/SetupInspect is the new reusable DAT probe that cracked this:
dumps a Setup's parts, parent indices, GfxObj vertex bounds, placement
frames, motion-table default cycle, sampled animation frames, and all
animation hooks.
Closes task #32's code side; the connected visual gate (shards gone at
the Bind Stone, base crystals and spin retained) is the acceptance.
App Release suite 3,968 / 3 skips.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
816 lines
28 KiB
C#
816 lines
28 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
|
|
{
|
|
// #188/#32: the packed part ordinal IS the retail CPartArray
|
|
// part ordinal TransparentPartHook.PartIndex addresses (one
|
|
// bare-GfxObj MeshRef per Setup.Parts[i] for flattened live
|
|
// entities; trivially 0 for single-part objects). The previous
|
|
// constant 0 mirrored the legacy dispatcher's false
|
|
// one-part assumption and kept the Bind Stone's four
|
|
// hook-hidden shard parts visible.
|
|
float opacity = PackedPartOpacity(
|
|
entity.LocalEntityId,
|
|
(uint)partIndex);
|
|
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.Slot.IsAssigned)
|
|
continue;
|
|
|
|
var key = new GroupKey(
|
|
batch.FirstIndex,
|
|
(int)batch.BaseVertex,
|
|
batch.IndexCount,
|
|
texture.Slot,
|
|
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,
|
|
TextureSlot = key.TextureSlot,
|
|
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);
|
|
}
|