feat(render): implement Campaign AR and terrain fidelity
This commit is contained in:
parent
99cf26e00c
commit
7a5f96ede5
368 changed files with 50611 additions and 950 deletions
542
src/AcDream.App/Rendering/Scene/DirectionalShadowCasterFrame.cs
Normal file
542
src/AcDream.App/Rendering/Scene/DirectionalShadowCasterFrame.cs
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
/// <summary>
|
||||
/// Projection-level membership. Opaque versus alpha-cutout remains an exact
|
||||
/// mesh-batch decision in the dispatcher; this product deliberately does not
|
||||
/// guess from an entity's texture set.
|
||||
/// </summary>
|
||||
internal enum DirectionalShadowCasterKind : byte
|
||||
{
|
||||
OutdoorStatic,
|
||||
Building,
|
||||
AnimatedStatic,
|
||||
LiveDynamic,
|
||||
EquippedChild,
|
||||
}
|
||||
|
||||
internal readonly record struct DirectionalShadowCaster(
|
||||
RenderProjectionRecord Projection,
|
||||
DirectionalShadowCasterKind Kind)
|
||||
{
|
||||
public bool UsesCurrentAnimatedTransforms =>
|
||||
Projection.ProjectionClass
|
||||
is RenderProjectionClass.ActiveAnimatedStatic
|
||||
or RenderProjectionClass.LiveDynamicRoot
|
||||
or RenderProjectionClass.EquippedChild;
|
||||
}
|
||||
|
||||
internal readonly struct DirectionalShadowChangedPose
|
||||
{
|
||||
internal DirectionalShadowChangedPose(
|
||||
int casterIndex,
|
||||
in DirectionalShadowTransformSnapshot snapshot)
|
||||
{
|
||||
CasterIndex = casterIndex;
|
||||
Snapshot = snapshot;
|
||||
}
|
||||
|
||||
internal readonly int CasterIndex;
|
||||
internal readonly DirectionalShadowTransformSnapshot Snapshot;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Accepted caster counts by the strongest class proven at render publication.
|
||||
/// TerrainCommands is populated by the terrain command producer. OutdoorStatics
|
||||
/// includes trees and all other outdoor DAT scenery; NonPlayerCreatures includes
|
||||
/// hostile monsters and non-hostile NPC creatures because neither source carries
|
||||
/// a narrower authoritative render-only discriminator.
|
||||
/// </summary>
|
||||
internal readonly record struct DirectionalShadowCasterClassDiagnostics(
|
||||
int TerrainCommands,
|
||||
int OutdoorStatics,
|
||||
int Buildings,
|
||||
int AnimatedStatics,
|
||||
int LocalPlayers,
|
||||
int RemotePlayers,
|
||||
int NonPlayerCreatures,
|
||||
int OtherLiveDynamics,
|
||||
int EquippedChildren);
|
||||
|
||||
internal readonly record struct DirectionalShadowCasterBuildStats(
|
||||
int SourceOutdoorStatics,
|
||||
int SourceOutdoorDynamics,
|
||||
int Accepted,
|
||||
int RejectedNotDrawable,
|
||||
int RejectedNotResident,
|
||||
int RejectedTransparent,
|
||||
int RejectedIndoor,
|
||||
int RejectedMissingMesh,
|
||||
int IndexCopies,
|
||||
int Classifications,
|
||||
int DynamicTransformRefreshes,
|
||||
bool TopologyRebuilt,
|
||||
int CopiedTransformChanges = 0,
|
||||
int DedupedChangedCasterSlots = 0,
|
||||
bool TransformJournalFullRefresh = false,
|
||||
int UpdateTransformChanges = 0,
|
||||
int UpdateAppearanceChanges = 0,
|
||||
int DynamicSynchronizationChanges = 0,
|
||||
int ActiveAnimatedStaticChanges = 0,
|
||||
int LiveDynamicRootChanges = 0,
|
||||
int EquippedChildChanges = 0,
|
||||
bool DensityBulkRefresh = false,
|
||||
int BatchedProjectionCopyCalls = 0)
|
||||
{
|
||||
public DirectionalShadowCasterClassDiagnostics CasterClasses { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reusable, streaming-bounded caster product. It copies and classifies the
|
||||
/// render scene's two resident outdoor indices only when the scene's shadow
|
||||
/// topology revision changes. Stable frames retain those topology records and
|
||||
/// emit only deduplicated slim root/part pose changes for prepared matrix slots.
|
||||
/// </summary>
|
||||
internal sealed class DirectionalShadowCasterFrame
|
||||
{
|
||||
private RenderProjectionRecord[] _outdoorStaticScratch = [];
|
||||
private RenderProjectionRecord[] _outdoorDynamicScratch = [];
|
||||
private DirectionalShadowCaster[] _casters = [];
|
||||
private int[] _refreshCasterSlots = [];
|
||||
private DirectionalShadowChangedPose[] _changedCasterPoses = [];
|
||||
private bool[] _changedCasterFlags = [];
|
||||
private RenderProjectionId[] _casterIds = [];
|
||||
private RenderProjectionClass[] _casterClasses = [];
|
||||
private RenderProjectionId[] _denseIdScratch = [];
|
||||
private RenderProjectionRecord[] _denseRecordScratch = [];
|
||||
private readonly DirectionalShadowTransformSnapshot[] _transformChangeScratch =
|
||||
new DirectionalShadowTransformSnapshot[
|
||||
DirectionalShadowTransformChangeJournal.Capacity];
|
||||
private readonly Dictionary<RenderProjectionId, int> _refreshCasterSlotById = [];
|
||||
private int _casterCount;
|
||||
private int _refreshCasterSlotCount;
|
||||
private int _changedCasterPoseCount;
|
||||
private ulong _topologyRevision;
|
||||
private ulong _transformRevision;
|
||||
private DirectionalShadowTransformChanges _lastTransformChanges;
|
||||
private bool _lastDensityBulkRefresh;
|
||||
private int _lastBatchedProjectionCopyCalls;
|
||||
|
||||
public RenderSceneGeneration Generation { get; private set; }
|
||||
|
||||
public ulong BuildSequence { get; private set; }
|
||||
|
||||
public ReadOnlySpan<DirectionalShadowCaster> Casters =>
|
||||
_casters.AsSpan(0, _casterCount);
|
||||
|
||||
internal ReadOnlySpan<int> RefreshCasterSlots =>
|
||||
_refreshCasterSlots.AsSpan(0, _refreshCasterSlotCount);
|
||||
|
||||
internal ReadOnlySpan<DirectionalShadowChangedPose> ChangedCasterPoses =>
|
||||
_changedCasterPoses.AsSpan(0, _changedCasterPoseCount);
|
||||
|
||||
internal ulong TransformRevision => _transformRevision;
|
||||
|
||||
public DirectionalShadowCasterBuildStats Stats { get; private set; }
|
||||
|
||||
public long RetainedScratchBytes =>
|
||||
checked(
|
||||
(long)_outdoorStaticScratch.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
|
||||
+ (long)_outdoorDynamicScratch.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
|
||||
+ (long)_casters.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<DirectionalShadowCaster>()
|
||||
+ (long)_refreshCasterSlots.Length * sizeof(int)
|
||||
+ (long)_changedCasterPoses.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
||||
DirectionalShadowChangedPose>()
|
||||
+ _changedCasterFlags.Length
|
||||
+ (long)_casterIds.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
||||
RenderProjectionId>()
|
||||
+ (long)_casterClasses.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
||||
RenderProjectionClass>()
|
||||
+ (long)_denseIdScratch.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionId>()
|
||||
+ (long)_denseRecordScratch.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<RenderProjectionRecord>()
|
||||
+ (long)_transformChangeScratch.Length
|
||||
* System.Runtime.CompilerServices.Unsafe.SizeOf<
|
||||
DirectionalShadowTransformSnapshot>()
|
||||
+ (long)_refreshCasterSlotById.EnsureCapacity(0)
|
||||
* (sizeof(int)
|
||||
+ System.Runtime.CompilerServices.Unsafe.SizeOf<
|
||||
KeyValuePair<RenderProjectionId, int>>()));
|
||||
|
||||
public void Build(in RenderSceneQuery query)
|
||||
{
|
||||
ulong topologyRevision = query.DirectionalShadowTopologyRevision;
|
||||
if (BuildSequence != 0
|
||||
&& Generation == query.Generation
|
||||
&& _topologyRevision == topologyRevision)
|
||||
{
|
||||
int refreshes = RefreshChangedTransforms(in query);
|
||||
Stats = Stats with
|
||||
{
|
||||
IndexCopies = 0,
|
||||
Classifications = 0,
|
||||
DynamicTransformRefreshes = refreshes,
|
||||
TopologyRebuilt = false,
|
||||
CopiedTransformChanges = _lastTransformChanges.Count,
|
||||
DedupedChangedCasterSlots = _changedCasterPoseCount,
|
||||
TransformJournalFullRefresh =
|
||||
_lastTransformChanges.RequiresFullRefresh,
|
||||
DensityBulkRefresh = _lastDensityBulkRefresh,
|
||||
BatchedProjectionCopyCalls = _lastBatchedProjectionCopyCalls,
|
||||
UpdateTransformChanges =
|
||||
_lastTransformChanges.UpdateTransformCount,
|
||||
UpdateAppearanceChanges =
|
||||
_lastTransformChanges.UpdateAppearanceCount,
|
||||
DynamicSynchronizationChanges =
|
||||
_lastTransformChanges.DynamicSynchronizationCount,
|
||||
ActiveAnimatedStaticChanges =
|
||||
_lastTransformChanges.ActiveAnimatedStaticCount,
|
||||
LiveDynamicRootChanges =
|
||||
_lastTransformChanges.LiveDynamicRootCount,
|
||||
EquippedChildChanges =
|
||||
_lastTransformChanges.EquippedChildCount,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
RenderSceneIndexCounts counts = query.IndexCounts;
|
||||
EnsureCapacity(ref _outdoorStaticScratch, counts.OutdoorStatic);
|
||||
EnsureCapacity(ref _outdoorDynamicScratch, counts.OutdoorDynamic);
|
||||
int staticCount = query.CopyIndexTo(
|
||||
RenderSceneIndex.OutdoorStatic,
|
||||
_outdoorStaticScratch.AsSpan(0, counts.OutdoorStatic));
|
||||
int dynamicCount = query.CopyIndexTo(
|
||||
RenderSceneIndex.OutdoorDynamic,
|
||||
_outdoorDynamicScratch.AsSpan(0, counts.OutdoorDynamic));
|
||||
EnsureCapacity(ref _casters, checked(staticCount + dynamicCount));
|
||||
_casterCount = 0;
|
||||
|
||||
int rejectedNotDrawable = 0;
|
||||
int rejectedNotResident = 0;
|
||||
int rejectedTransparent = 0;
|
||||
int rejectedIndoor = 0;
|
||||
int rejectedMissingMesh = 0;
|
||||
int outdoorStatics = 0;
|
||||
int buildings = 0;
|
||||
int animatedStatics = 0;
|
||||
int localPlayers = 0;
|
||||
int remotePlayers = 0;
|
||||
int nonPlayerCreatures = 0;
|
||||
int otherLiveDynamics = 0;
|
||||
int equippedChildren = 0;
|
||||
for (int i = 0; i < staticCount; i++)
|
||||
Add(_outdoorStaticScratch[i]);
|
||||
for (int i = 0; i < dynamicCount; i++)
|
||||
Add(_outdoorDynamicScratch[i]);
|
||||
|
||||
Array.Sort(
|
||||
_casters,
|
||||
0,
|
||||
_casterCount,
|
||||
DirectionalShadowCasterComparer.Instance);
|
||||
int refreshCasterCount = 0;
|
||||
for (int casterIndex = 0; casterIndex < _casterCount; casterIndex++)
|
||||
{
|
||||
if (_casters[casterIndex].UsesCurrentAnimatedTransforms)
|
||||
refreshCasterCount++;
|
||||
}
|
||||
EnsureCapacity(ref _refreshCasterSlots, refreshCasterCount);
|
||||
EnsureCapacity(ref _changedCasterPoses, refreshCasterCount);
|
||||
EnsureCapacity(ref _changedCasterFlags, _casterCount);
|
||||
EnsureCapacity(ref _casterIds, _casterCount);
|
||||
EnsureCapacity(ref _casterClasses, _casterCount);
|
||||
EnsureCapacity(ref _denseIdScratch, refreshCasterCount);
|
||||
EnsureCapacity(ref _denseRecordScratch, refreshCasterCount);
|
||||
_refreshCasterSlotCount = 0;
|
||||
_changedCasterPoseCount = 0;
|
||||
_refreshCasterSlotById.Clear();
|
||||
_refreshCasterSlotById.EnsureCapacity(refreshCasterCount);
|
||||
for (int casterIndex = 0; casterIndex < _casterCount; casterIndex++)
|
||||
{
|
||||
_casterIds[casterIndex] = _casters[casterIndex].Projection.Id;
|
||||
_casterClasses[casterIndex] =
|
||||
_casters[casterIndex].Projection.ProjectionClass;
|
||||
if (_casters[casterIndex].UsesCurrentAnimatedTransforms)
|
||||
{
|
||||
_refreshCasterSlots[_refreshCasterSlotCount++] = casterIndex;
|
||||
_refreshCasterSlotById.Add(
|
||||
_casterIds[casterIndex],
|
||||
casterIndex);
|
||||
}
|
||||
}
|
||||
Generation = query.Generation;
|
||||
_topologyRevision = topologyRevision;
|
||||
_transformRevision = query.DirectionalShadowTransformRevision;
|
||||
_lastTransformChanges = default;
|
||||
_lastDensityBulkRefresh = false;
|
||||
_lastBatchedProjectionCopyCalls = 0;
|
||||
BuildSequence = checked(BuildSequence + 1);
|
||||
Stats = new DirectionalShadowCasterBuildStats(
|
||||
staticCount,
|
||||
dynamicCount,
|
||||
_casterCount,
|
||||
rejectedNotDrawable,
|
||||
rejectedNotResident,
|
||||
rejectedTransparent,
|
||||
rejectedIndoor,
|
||||
rejectedMissingMesh,
|
||||
IndexCopies: 2,
|
||||
Classifications: _casterCount,
|
||||
DynamicTransformRefreshes: 0,
|
||||
TopologyRebuilt: true)
|
||||
{
|
||||
CasterClasses = new DirectionalShadowCasterClassDiagnostics(
|
||||
TerrainCommands: 0,
|
||||
outdoorStatics,
|
||||
buildings,
|
||||
animatedStatics,
|
||||
localPlayers,
|
||||
remotePlayers,
|
||||
nonPlayerCreatures,
|
||||
otherLiveDynamics,
|
||||
equippedChildren),
|
||||
};
|
||||
return;
|
||||
|
||||
void Add(in RenderProjectionRecord projection)
|
||||
{
|
||||
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
|
||||
{
|
||||
rejectedNotDrawable++;
|
||||
return;
|
||||
}
|
||||
if ((projection.Flags & RenderProjectionFlags.SpatiallyResident) == 0)
|
||||
{
|
||||
rejectedNotResident++;
|
||||
return;
|
||||
}
|
||||
// Transparent means a true blended projection. ClipMap/foliage is
|
||||
// retained here and separated from opaque batches later.
|
||||
if ((projection.Flags & RenderProjectionFlags.Translucent) != 0)
|
||||
{
|
||||
rejectedTransparent++;
|
||||
return;
|
||||
}
|
||||
if (projection.Source.ParentCellId != 0
|
||||
&& InteriorEntityPartition.IsIndoorCellId(
|
||||
projection.Source.ParentCellId))
|
||||
{
|
||||
rejectedIndoor++;
|
||||
return;
|
||||
}
|
||||
if (projection.MeshSet.MeshCount <= 0
|
||||
|| projection.EntityPayload.MeshRefs is null
|
||||
|| projection.EntityPayload.MeshRefs.Count == 0)
|
||||
{
|
||||
rejectedMissingMesh++;
|
||||
return;
|
||||
}
|
||||
|
||||
DirectionalShadowCasterKind kind = Classify(in projection);
|
||||
_casters[_casterCount++] = new DirectionalShadowCaster(
|
||||
projection,
|
||||
kind);
|
||||
switch (kind)
|
||||
{
|
||||
case DirectionalShadowCasterKind.OutdoorStatic:
|
||||
outdoorStatics++;
|
||||
break;
|
||||
case DirectionalShadowCasterKind.Building:
|
||||
buildings++;
|
||||
break;
|
||||
case DirectionalShadowCasterKind.AnimatedStatic:
|
||||
animatedStatics++;
|
||||
break;
|
||||
case DirectionalShadowCasterKind.EquippedChild:
|
||||
equippedChildren++;
|
||||
break;
|
||||
case DirectionalShadowCasterKind.LiveDynamic:
|
||||
switch (projection.EntityPayload.CasterIdentity)
|
||||
{
|
||||
case RenderCasterIdentityKind.LocalPlayer:
|
||||
localPlayers++;
|
||||
break;
|
||||
case RenderCasterIdentityKind.RemotePlayer:
|
||||
remotePlayers++;
|
||||
break;
|
||||
case RenderCasterIdentityKind.NonPlayerCreature:
|
||||
nonPlayerCreatures++;
|
||||
break;
|
||||
default:
|
||||
otherLiveDynamics++;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(
|
||||
nameof(kind), kind, "Unknown shadow caster kind.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int RefreshChangedTransforms(in RenderSceneQuery query)
|
||||
{
|
||||
_changedCasterPoseCount = 0;
|
||||
ulong latest = query.DirectionalShadowTransformRevision;
|
||||
if (latest == _transformRevision)
|
||||
{
|
||||
_lastTransformChanges = new DirectionalShadowTransformChanges(
|
||||
latest,
|
||||
0,
|
||||
false);
|
||||
_lastDensityBulkRefresh = false;
|
||||
_lastBatchedProjectionCopyCalls = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
DirectionalShadowTransformChanges changes =
|
||||
query.CopyDirectionalShadowTransformChanges(
|
||||
_transformRevision,
|
||||
_transformChangeScratch);
|
||||
_lastTransformChanges = changes;
|
||||
if (changes.RequiresFullRefresh)
|
||||
{
|
||||
_lastBatchedProjectionCopyCalls = 1;
|
||||
for (int index = 0; index < _refreshCasterSlotCount; index++)
|
||||
{
|
||||
int casterIndex = _refreshCasterSlots[index];
|
||||
_denseIdScratch[index] = _casters[casterIndex].Projection.Id;
|
||||
}
|
||||
int copied = query.CopyById(
|
||||
_denseIdScratch.AsSpan(0, _refreshCasterSlotCount),
|
||||
_denseRecordScratch.AsSpan(0, _refreshCasterSlotCount));
|
||||
if (copied != _refreshCasterSlotCount)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Dense directional-shadow refresh returned an incomplete record batch.");
|
||||
}
|
||||
for (int index = 0; index < _refreshCasterSlotCount; index++)
|
||||
{
|
||||
int casterIndex = _refreshCasterSlots[index];
|
||||
RefreshOne(in _denseRecordScratch[index], casterIndex);
|
||||
DirectionalShadowTransformSnapshot snapshot =
|
||||
DirectionalShadowTransformSnapshot.Capture(
|
||||
in _denseRecordScratch[index]);
|
||||
_changedCasterPoses[_changedCasterPoseCount++] =
|
||||
new DirectionalShadowChangedPose(casterIndex, in snapshot);
|
||||
}
|
||||
_lastDensityBulkRefresh = false;
|
||||
_transformRevision = changes.LatestRevision;
|
||||
return _changedCasterPoseCount;
|
||||
}
|
||||
_lastBatchedProjectionCopyCalls = 0;
|
||||
|
||||
try
|
||||
{
|
||||
ReadOnlySpan<DirectionalShadowTransformSnapshot> records =
|
||||
_transformChangeScratch.AsSpan(0, changes.Count);
|
||||
// Newest-first makes repeated publications of the same projection
|
||||
// resolve to the latest exact root/part payload without an ECS read.
|
||||
for (int index = records.Length - 1; index >= 0; index--)
|
||||
{
|
||||
if (!_refreshCasterSlotById.TryGetValue(
|
||||
records[index].Id,
|
||||
out int casterIndex)
|
||||
|| _changedCasterFlags[casterIndex])
|
||||
{
|
||||
continue;
|
||||
}
|
||||
_changedCasterFlags[casterIndex] = true;
|
||||
ValidateStablePose(in records[index], casterIndex);
|
||||
_changedCasterPoses[_changedCasterPoseCount++] =
|
||||
new DirectionalShadowChangedPose(
|
||||
casterIndex,
|
||||
in records[index]);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (int index = 0; index < _changedCasterPoseCount; index++)
|
||||
{
|
||||
_changedCasterFlags[
|
||||
_changedCasterPoses[index].CasterIndex] = false;
|
||||
}
|
||||
}
|
||||
_lastDensityBulkRefresh = _refreshCasterSlotCount >= 64
|
||||
&& _changedCasterPoseCount
|
||||
>= checked((_refreshCasterSlotCount * 3) / 4);
|
||||
_transformRevision = changes.LatestRevision;
|
||||
return _changedCasterPoseCount;
|
||||
}
|
||||
|
||||
private void ValidateStablePose(
|
||||
in DirectionalShadowTransformSnapshot current,
|
||||
int casterIndex)
|
||||
{
|
||||
if (current.Id != _casterIds[casterIndex]
|
||||
|| current.ProjectionClass != _casterClasses[casterIndex])
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Stable directional-shadow topology changed caster "
|
||||
+ $"{_casterIds[casterIndex]} identity or class.");
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshOne(
|
||||
in RenderProjectionRecord current,
|
||||
int casterIndex)
|
||||
{
|
||||
DirectionalShadowCaster retained = _casters[casterIndex];
|
||||
if (current.Id != retained.Projection.Id
|
||||
|| current.ProjectionClass != retained.Projection.ProjectionClass)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Stable directional-shadow topology changed caster "
|
||||
+ $"{retained.Projection.Id} identity or class.");
|
||||
}
|
||||
_casters[casterIndex] = retained with { Projection = current };
|
||||
}
|
||||
|
||||
private static DirectionalShadowCasterKind Classify(
|
||||
in RenderProjectionRecord projection)
|
||||
{
|
||||
if (projection.EntityPayload.IsBuildingShell)
|
||||
return DirectionalShadowCasterKind.Building;
|
||||
return projection.ProjectionClass switch
|
||||
{
|
||||
RenderProjectionClass.OutdoorStatic =>
|
||||
DirectionalShadowCasterKind.OutdoorStatic,
|
||||
RenderProjectionClass.ActiveAnimatedStatic =>
|
||||
DirectionalShadowCasterKind.AnimatedStatic,
|
||||
RenderProjectionClass.LiveDynamicRoot =>
|
||||
DirectionalShadowCasterKind.LiveDynamic,
|
||||
RenderProjectionClass.EquippedChild =>
|
||||
DirectionalShadowCasterKind.EquippedChild,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Outdoor shadow index carried unsupported {projection.ProjectionClass}."),
|
||||
};
|
||||
}
|
||||
|
||||
private static void EnsureCapacity<T>(ref T[] values, int required)
|
||||
{
|
||||
if (required < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(required));
|
||||
if (values.Length >= required)
|
||||
return;
|
||||
int capacity = values.Length == 0 ? 4 : values.Length;
|
||||
while (capacity < required)
|
||||
capacity = checked(capacity * 2);
|
||||
Array.Resize(ref values, capacity);
|
||||
}
|
||||
|
||||
private sealed class DirectionalShadowCasterComparer
|
||||
: IComparer<DirectionalShadowCaster>
|
||||
{
|
||||
public static DirectionalShadowCasterComparer Instance { get; } = new();
|
||||
|
||||
public int Compare(DirectionalShadowCaster left, DirectionalShadowCaster right)
|
||||
{
|
||||
int order = left.Projection.SortKey.Value.CompareTo(
|
||||
right.Projection.SortKey.Value);
|
||||
return order != 0
|
||||
? order
|
||||
: left.Projection.Id.CompareTo(right.Projection.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue