526 lines
22 KiB
C#
526 lines
22 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.App.Rendering.Selection;
|
|
using AcDream.App.Rendering.Walk;
|
|
using AcDream.Core.Lighting;
|
|
using AcDream.Core.Meshing;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.Enums;
|
|
|
|
namespace AcDream.App.Rendering.Wb;
|
|
|
|
/// <summary>
|
|
/// Campaign FW stage FW3.2a: the walk-order population layer. This partial
|
|
/// holds two things.
|
|
///
|
|
/// <para><b>The shared per-batch classify core</b> (<see cref="TryClassifyBatch"/>):
|
|
/// extracted from <c>ClassifyBatches</c> (this file's sibling
|
|
/// <c>WbDrawDispatcher.cs</c>) and <c>ClassifyPackedBatches</c>
|
|
/// (<c>WbDrawDispatcher.PackedOracle.cs</c>), which carried byte-identical
|
|
/// per-batch logic (the #426 untextured-subset gate, the #188 opacity
|
|
/// promotion, texture resolution, Campaign VM foliage classification, the
|
|
/// <see cref="GroupKey"/> construction) with only their SURROUNDING
|
|
/// bookkeeping differing (classic appends to an <see cref="InstanceGroup"/>
|
|
/// via caller-scoped instance fields; packed appends via explicit
|
|
/// parameters). Both call sites now call this one method per batch and keep
|
|
/// their own append logic — the classic and packed paths are unchanged in
|
|
/// every observable way (same hermetic + <c>Lane=InstalledDat</c> suites
|
|
/// stay the referee); this stage's OWN new walk classifier
|
|
/// (<see cref="ClassifyEntityForWalk"/>) is the third caller.</para>
|
|
///
|
|
/// <para><b>The per-entity walk classify seam</b>
|
|
/// (<see cref="ClassifyEntityForWalk"/>): given ONE <see cref="RenderProjectionRecord"/>
|
|
/// — the same shape <c>ClassifyPackedEntity</c> consumes, but read straight
|
|
/// off the record's own <see cref="RenderEntityPayload"/> via
|
|
/// <c>RenderInstanceCandidate.FromProjection</c> rather than the packed
|
|
/// route's frame-arena mesh-part flattening — yields one
|
|
/// <see cref="WalkClassifiedBatch"/> per surviving (part, batch) pair WITHOUT
|
|
/// touching any <see cref="InstanceGroup"/>, plus the per-part selection
|
|
/// data <c>AddPackedSelectionPart</c> would have published, for
|
|
/// <c>WalkStaticStreamPopulator</c> to publish itself (deliverable
|
|
/// 1 leaves the publish decision — timing, stage/cell provenance — to the
|
|
/// caller; see <see cref="PublishWalkSelectionPart"/>, the thin internal seam
|
|
/// that keeps <c>_selectionSink</c> encapsulated).</para>
|
|
///
|
|
/// <para>Scope: this stage classifies static content only (no production
|
|
/// frame wiring — see plan §FW3.2a). Per-part translucency-fade
|
|
/// (<c>TranslucencyFadeManager</c>/<c>EntityOpacity</c>) is NOT threaded
|
|
/// through: that mechanic is <c>TransparentPartHook</c>, a retail LIVE-entity
|
|
/// behavior keyed by ServerGuid, not something world statics undergo — every
|
|
/// classified batch here carries <c>Alpha = 1f</c>. Likewise the async
|
|
/// mesh-miss self-heal request (<c>_missRequested</c>/<c>EnsureLoaded</c>,
|
|
/// frame-scoped state cleared by <c>BeginEntityDispatch</c>) is not fired
|
|
/// here — this seam has no production frame to be scoped to yet; a missing
|
|
/// mesh is simply skipped, matching every other unwired FW2/FW3.2a path.</para>
|
|
/// </summary>
|
|
public sealed partial class WbDrawDispatcher
|
|
{
|
|
/// <summary>
|
|
/// One walk-classified (entity, part, batch) draw candidate — exactly
|
|
/// <c>OrderedDrawCommand</c>'s per-instance field set (minus
|
|
/// the walk-provenance <c>Stage</c>/<c>CellId</c> fields, which the
|
|
/// populator stamps on since the classifier has no notion of either)
|
|
/// plus two fields an opaque command has no use for but a translucent
|
|
/// one needs to reach the alpha queue: <see cref="IsOpaque"/> (so the
|
|
/// populator can route without re-deriving it from
|
|
/// <see cref="GroupKey.Translucency"/>) and <see cref="LocalSortCenter"/>
|
|
/// (the authored GfxObj sort center <c>RetailAlphaOrdering.ComputeViewerDistance</c>
|
|
/// transforms through <see cref="Transform"/> — the same value
|
|
/// <c>InstanceGroup.LocalSortCenters</c> carries per instance today).
|
|
/// </summary>
|
|
internal readonly record struct WalkClassifiedBatch(
|
|
GroupKey Key,
|
|
Matrix4x4 Transform,
|
|
uint ClipSlot,
|
|
InstanceLightSet Lights,
|
|
uint IndoorFlag,
|
|
float Alpha,
|
|
Vector2 SelectionLighting,
|
|
uint DetailCategory,
|
|
bool IsOpaque,
|
|
Vector3 LocalSortCenter);
|
|
|
|
/// <summary>
|
|
/// One retail-picking part surfaced by <see cref="ClassifyEntityForWalk"/>
|
|
/// — the exact argument tuple <c>AddPackedSelectionPart</c>'s
|
|
/// <c>publishSelection: true</c> branch passes to
|
|
/// <c>IRetailSelectionRenderSink.AddVisiblePart</c>. The classify seam
|
|
/// surfaces this; <see cref="PublishWalkSelectionPart"/> is the caller's
|
|
/// publish call.
|
|
/// </summary>
|
|
internal readonly record struct WalkClassifiedSelectionPart(
|
|
uint ServerGuid,
|
|
uint LocalEntityId,
|
|
int PartIndex,
|
|
uint GfxObjId,
|
|
Matrix4x4 LocalToWorld);
|
|
|
|
/// <summary>
|
|
/// The shared per-batch classify core. Given one already-resolved
|
|
/// <paramref name="renderData"/> and the batch at <paramref name="batchIndex"/>,
|
|
/// applies — in this exact order, matching both <c>ClassifyBatches</c> and
|
|
/// <c>ClassifyPackedBatches</c> before this extraction — the #426
|
|
/// untextured-subset-on-a-shell-only gate, the #188 mid-fade-forces-
|
|
/// AlphaBlend promotion, texture resolution, and Campaign VM foliage
|
|
/// classification, then builds the surviving batch's <see cref="GroupKey"/>.
|
|
///
|
|
/// <para>Returns false when the batch does not survive — either the
|
|
/// untextured-subset gate rejected it (in which case
|
|
/// <paramref name="compositePending"/> is always false: <c>ResolveTexture</c>
|
|
/// is never called) or its resolved texture slot is unassigned (in which
|
|
/// case <paramref name="compositePending"/> still reflects whatever
|
|
/// <c>ResolveTexture</c> reported — a caller must apply it to its own
|
|
/// reusability/readiness tracking regardless of the false return, exactly
|
|
/// as both existing callers did before this extraction).</para>
|
|
/// </summary>
|
|
private bool TryClassifyBatch(
|
|
ObjectRenderData renderData,
|
|
int batchIndex,
|
|
in RenderInstanceCandidate entity,
|
|
MeshRef meshRef,
|
|
PaletteCompositeIdentity paletteIdentity,
|
|
float opacityMultiplier,
|
|
bool entityHasCutoutSubset,
|
|
out GroupKey key,
|
|
out bool compositePending)
|
|
{
|
|
key = default;
|
|
compositePending = false;
|
|
ObjectRenderBatch batch = renderData.Batches[batchIndex];
|
|
|
|
// #426 — see RetailUntexturedSubsetPolicy for the retail citation.
|
|
// ONE shared predicate for every classifier so they cannot drift.
|
|
if (!RetailUntexturedSubsetPolicy.Draws(entity.IsBuildingShell, batch.Key.IsSolid))
|
|
return false;
|
|
|
|
TranslucencyKind translucency = batch.Translucency;
|
|
|
|
// #188: a mid-fade instance whose surface is otherwise Opaque/ClipMap
|
|
// must route through the alpha-blend pass so mesh_modern.frag's
|
|
// (blend-enabled) shader actually composites the reduced alpha — the
|
|
// no-blend opaque pass would ignore it.
|
|
if (opacityMultiplier < 1.0f && IsOpaque(translucency))
|
|
translucency = TranslucencyKind.AlphaBlend;
|
|
|
|
ResolvedTexture texture = ResolveTexture(
|
|
in entity, meshRef, batch, paletteIdentity, out compositePending);
|
|
if (!texture.Slot.IsAssigned)
|
|
return false;
|
|
|
|
// Classify from the RAW (pre-#188-promotion) batch.Translucency — a
|
|
// mid-fade trunk is still a trunk, it just landed in the alpha-blend
|
|
// group instead of opaque (Campaign VM VM6 review fix round).
|
|
uint foliageFlags = FoliageWindClassification.Classify(
|
|
entity.LocalEntityId,
|
|
FoliageWindExclusions.Contains(meshRef.GfxObjId),
|
|
batch.Translucency,
|
|
entityHasCutoutSubset);
|
|
key = new GroupKey(
|
|
batch.FirstIndex, (int)batch.BaseVertex,
|
|
batch.IndexCount, texture.Slot, texture.Layer, translucency,
|
|
FoliageFlags: foliageFlags,
|
|
CullMode: batch.CullMode);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Classifies one static entity for the walk populator: resolves its clip
|
|
/// slot / light set / selection lighting exactly as
|
|
/// <c>ClassifyPackedEntity</c> does (via the shared
|
|
/// <see cref="ResolveSlotForFrame"/> / <c>ResolveWalkLightSet</c>
|
|
/// helpers), walks its Setup parts or single mesh (mirroring
|
|
/// <c>ClassifyPackedEntity</c>'s own shape), and appends one
|
|
/// <see cref="WalkClassifiedBatch"/> per surviving batch to
|
|
/// <paramref name="batches"/> plus one <see cref="WalkClassifiedSelectionPart"/>
|
|
/// per part to <paramref name="selectionParts"/> — WITHOUT touching
|
|
/// <c>_groups</c>/<c>_packedGroups</c> or publishing selection itself (see
|
|
/// this file's type doc comment).
|
|
///
|
|
/// <para>A culled entity (<see cref="ResolveSlotForFrame"/> returns
|
|
/// <c>Culled: true</c> — not visible through the active clip route)
|
|
/// contributes nothing, matching every other classifier's cull gate.</para>
|
|
/// </summary>
|
|
internal void ClassifyEntityForWalk(
|
|
in RenderProjectionRecord projection,
|
|
uint tupleLandblockId,
|
|
List<WalkClassifiedBatch> batches,
|
|
List<WalkClassifiedSelectionPart> selectionParts,
|
|
bool liveDynamic = false,
|
|
IWalkLookInViewSource? lookInViews = null,
|
|
int lookInRouteIndex = -1,
|
|
uint lookInCellId = 0)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(batches);
|
|
ArgumentNullException.ThrowIfNull(selectionParts);
|
|
|
|
RenderInstanceCandidate entity =
|
|
RenderInstanceCandidate.FromProjection(
|
|
in projection,
|
|
tupleLandblockId,
|
|
animated: liveDynamic);
|
|
|
|
(uint slot, bool culled) = ResolveSlotForFrame(
|
|
_clipRoutingActive, entity.ServerGuid, entity.ParentCell,
|
|
_cellIdToSlot, _outdoorSlot, _outdoorVisible);
|
|
if (culled)
|
|
return;
|
|
|
|
ResolveWalkLightSet(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);
|
|
uint detailCategory = entity.IsBuildingShell ? 1u : 0u;
|
|
|
|
PaletteCompositeIdentity paletteIdentity = default;
|
|
if (entity.PaletteOverride is not null)
|
|
paletteIdentity = TextureCache.GetPaletteIdentity(entity.PaletteOverride);
|
|
|
|
IReadOnlyList<MeshRef>? meshRefs = projection.EntityPayload.MeshRefs;
|
|
if (meshRefs is null)
|
|
return;
|
|
|
|
for (int partIndex = 0; partIndex < meshRefs.Count; partIndex++)
|
|
{
|
|
MeshRef meshRef = meshRefs[partIndex];
|
|
if (_meshAdapter.IsRuntimeHiddenMarker(meshRef.GfxObjId))
|
|
continue;
|
|
|
|
ObjectRenderData? renderData = _meshAdapter.TryGetRenderData(meshRef.GfxObjId);
|
|
if (renderData is null)
|
|
{
|
|
if (_missRequested.Add(meshRef.GfxObjId))
|
|
_meshAdapter.EnsureLoaded(meshRef.GfxObjId);
|
|
continue;
|
|
}
|
|
|
|
if (renderData.IsSetup && renderData.SetupParts.Count > 0)
|
|
{
|
|
// Same entity-scoped OR the classic/packed classifiers compute
|
|
// — a Setup composite's parts are separate GfxObjs with their
|
|
// own independently cached HasCutoutSubset (Campaign VM VM6
|
|
// review fix round A4/F1).
|
|
bool entityHasCutoutSubset = FoliageWindClassification.ComputeEntityHasCutoutSubset(
|
|
renderData.SetupParts,
|
|
_meshAdapter,
|
|
static (adapter, part) => adapter.TryGetRenderData(part.GfxObjId)
|
|
is { HasCutoutSubset: true });
|
|
|
|
for (int setupPartIndex = 0; setupPartIndex < renderData.SetupParts.Count; setupPartIndex++)
|
|
{
|
|
(ulong gfxObjId, Matrix4x4 partTransform) = renderData.SetupParts[setupPartIndex];
|
|
if (_meshAdapter.IsRuntimeHiddenMarker((uint)gfxObjId))
|
|
continue;
|
|
|
|
ObjectRenderData? partData = _meshAdapter.TryGetRenderData(gfxObjId);
|
|
if (partData is null)
|
|
{
|
|
if (_missRequested.Add(gfxObjId))
|
|
_meshAdapter.EnsureLoaded(gfxObjId);
|
|
continue;
|
|
}
|
|
|
|
float opacity = liveDynamic
|
|
? WalkPartOpacity(
|
|
entity.ServerGuid,
|
|
entity.LocalEntityId,
|
|
(uint)setupPartIndex)
|
|
: 1f;
|
|
if (opacity <= 0f)
|
|
continue;
|
|
|
|
Matrix4x4 restPose = partTransform * meshRef.PartTransform;
|
|
Matrix4x4 model = restPose * entity.RootWorld;
|
|
int selectionPartIndex = unchecked((partIndex << 16) | (setupPartIndex & 0xFFFF));
|
|
|
|
if (!PartVisibleInLookInTurn(
|
|
lookInViews,
|
|
lookInRouteIndex,
|
|
lookInCellId,
|
|
in entity,
|
|
selectionPartIndex,
|
|
(uint)gfxObjId,
|
|
partData,
|
|
model))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
EmitClassifiedBatches(
|
|
partData, model, in entity, meshRef, paletteIdentity,
|
|
entityHasCutoutSubset, slot, lights, indoor, selectionLighting,
|
|
detailCategory, opacity, batches);
|
|
selectionParts.Add(new WalkClassifiedSelectionPart(
|
|
entity.ServerGuid, entity.LocalEntityId, selectionPartIndex,
|
|
(uint)gfxObjId, model));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
float opacity = liveDynamic
|
|
? WalkPartOpacity(
|
|
entity.ServerGuid,
|
|
entity.LocalEntityId,
|
|
(uint)partIndex)
|
|
: 1f;
|
|
if (opacity <= 0f)
|
|
continue;
|
|
|
|
Matrix4x4 model = meshRef.PartTransform * entity.RootWorld;
|
|
if (!PartVisibleInLookInTurn(
|
|
lookInViews,
|
|
lookInRouteIndex,
|
|
lookInCellId,
|
|
in entity,
|
|
partIndex,
|
|
(uint)meshRef.GfxObjId,
|
|
renderData,
|
|
model))
|
|
{
|
|
continue;
|
|
}
|
|
EmitClassifiedBatches(
|
|
renderData, model, in entity, meshRef, paletteIdentity,
|
|
entityHasCutoutSubsetOverride: null, slot, lights, indoor,
|
|
selectionLighting, detailCategory, opacity, batches);
|
|
selectionParts.Add(new WalkClassifiedSelectionPart(
|
|
entity.ServerGuid, entity.LocalEntityId, partIndex,
|
|
(uint)meshRef.GfxObjId, model));
|
|
}
|
|
}
|
|
}
|
|
|
|
private float WalkPartOpacity(
|
|
uint serverGuid,
|
|
uint localEntityId,
|
|
uint setupPartIndex)
|
|
{
|
|
float opacity = EntityOpacity(serverGuid);
|
|
if (opacity <= 0f)
|
|
return 0f;
|
|
if (!_translucencyFades.TryGetCurrentValue(
|
|
localEntityId,
|
|
setupPartIndex,
|
|
out float translucency))
|
|
{
|
|
return opacity;
|
|
}
|
|
|
|
return translucency >= 1f
|
|
? 0f
|
|
: opacity * (1f - translucency);
|
|
}
|
|
|
|
private void ResolveWalkLightSet(
|
|
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 static bool PartVisibleInLookInTurn(
|
|
IWalkLookInViewSource? lookInViews,
|
|
int routeIndex,
|
|
uint cellId,
|
|
in RenderInstanceCandidate entity,
|
|
int partIndex,
|
|
uint gfxObjId,
|
|
ObjectRenderData renderData,
|
|
Matrix4x4 localToWorld)
|
|
{
|
|
if (lookInViews is null)
|
|
return true;
|
|
if (renderData.SelectionSphere is not { Radius: > 0f } sphere)
|
|
return true;
|
|
|
|
return LookInDrawingSphereVisible(
|
|
lookInViews,
|
|
routeIndex,
|
|
sphere,
|
|
localToWorld,
|
|
out _,
|
|
out _);
|
|
}
|
|
|
|
internal static bool LookInDrawingSphereVisible(
|
|
IWalkLookInViewSource lookInViews,
|
|
int routeIndex,
|
|
DatReaderWriter.Types.Sphere sphere,
|
|
Matrix4x4 localToWorld,
|
|
out Vector3 center,
|
|
out float radius)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(lookInViews);
|
|
ArgumentNullException.ThrowIfNull(sphere);
|
|
|
|
center = Vector3.Transform(sphere.Origin, 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();
|
|
radius = sphere.Radius
|
|
* MathF.Max(scaleX, MathF.Max(scaleY, scaleZ));
|
|
return lookInViews.SphereVisibleInLookInTurn(
|
|
routeIndex,
|
|
in center,
|
|
radius);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Walks one resolved mesh's batches through <see cref="TryClassifyBatch"/>
|
|
/// and appends every surviving one to <paramref name="sink"/> at
|
|
/// <c>Alpha = 1f</c> (see this file's type doc comment for why statics
|
|
/// never carry a per-part opacity multiplier here).
|
|
/// </summary>
|
|
private void EmitClassifiedBatches(
|
|
ObjectRenderData renderData,
|
|
Matrix4x4 model,
|
|
in RenderInstanceCandidate entity,
|
|
MeshRef meshRef,
|
|
PaletteCompositeIdentity paletteIdentity,
|
|
bool? entityHasCutoutSubsetOverride,
|
|
uint slot,
|
|
InstanceLightSet lights,
|
|
bool indoor,
|
|
Vector2 selectionLighting,
|
|
uint detailCategory,
|
|
float opacity,
|
|
List<WalkClassifiedBatch> sink)
|
|
{
|
|
bool entityHasCutoutSubset = entityHasCutoutSubsetOverride ?? renderData.HasCutoutSubset;
|
|
for (int batchIdx = 0; batchIdx < renderData.Batches.Count; batchIdx++)
|
|
{
|
|
bool survives = TryClassifyBatch(
|
|
renderData, batchIdx, in entity, meshRef, paletteIdentity,
|
|
opacityMultiplier: opacity, entityHasCutoutSubset,
|
|
out GroupKey key, out _);
|
|
if (!survives)
|
|
continue;
|
|
|
|
sink.Add(new WalkClassifiedBatch(
|
|
key, model, slot, lights, indoor ? 1u : 0u, Alpha: opacity,
|
|
selectionLighting, detailCategory, IsOpaque: IsOpaque(key.Translucency),
|
|
LocalSortCenter: renderData.SortCenter));
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The thin internal publish seam <c>WalkStaticStreamPopulator</c>
|
|
/// calls for every <see cref="WalkClassifiedSelectionPart"/>
|
|
/// <see cref="ClassifyEntityForWalk"/> surfaced — same call shape as
|
|
/// <c>AddPackedSelectionPart</c>'s <c>publishSelection: true</c> branch.
|
|
/// Keeps <c>_selectionSink</c> encapsulated: the populator lives outside
|
|
/// the dispatcher and must not reach the field directly.
|
|
/// </summary>
|
|
internal void PublishWalkSelectionPart(in WalkClassifiedSelectionPart part) =>
|
|
_selectionSink?.AddVisiblePart(
|
|
part.ServerGuid, part.LocalEntityId, part.PartIndex, part.GfxObjId, part.LocalToWorld);
|
|
|
|
/// <summary>
|
|
/// The walk populator's per-instance sibling of <c>DeferTransparentGroups</c>
|
|
/// (see that method for the retail citations this mirrors): submits ONE
|
|
/// translucent <see cref="WalkClassifiedBatch"/> into the same
|
|
/// <c>_deferredAlpha</c>/<see cref="RetailAlphaQueue"/> machinery the
|
|
/// classic material-grouped path uses, so scenery, particles, and walk
|
|
/// content share retail's one stable far-to-near stream. Same
|
|
/// view-projection consistency check, same
|
|
/// <see cref="RetailAlphaOrdering.ComputeViewerDistance"/> call, same
|
|
/// <c>queue.Submit</c> contract — the walk path denormalizes to one
|
|
/// instance per call instead of flattening a material group.
|
|
/// </summary>
|
|
internal void SubmitWalkAlphaInstance(
|
|
in WalkClassifiedBatch batch,
|
|
Vector3 cameraWorldPosition,
|
|
Matrix4x4 viewProjection)
|
|
{
|
|
RetailAlphaQueue queue = _alphaQueue
|
|
?? throw new InvalidOperationException(
|
|
"SubmitWalkAlphaInstance requires an active RetailAlphaQueue.");
|
|
|
|
if (_deferredAlpha.Count == 0)
|
|
_deferredAlphaViewProjection = viewProjection;
|
|
else if (_deferredAlphaViewProjection != viewProjection)
|
|
throw new InvalidOperationException(
|
|
"One retail alpha scope cannot combine different view-projection matrices.");
|
|
|
|
float viewerDistance = RetailAlphaOrdering.ComputeViewerDistance(
|
|
batch.LocalSortCenter, batch.Transform, cameraWorldPosition);
|
|
if (!float.IsFinite(viewerDistance) || viewerDistance <= 0f)
|
|
viewerDistance = 0f;
|
|
|
|
int token = _deferredAlpha.Count;
|
|
_deferredAlpha.Add(new DeferredAlphaInstance(
|
|
batch.Key, batch.Transform, batch.ClipSlot, batch.Lights,
|
|
batch.IndoorFlag, batch.DetailCategory, batch.Alpha, batch.SelectionLighting));
|
|
queue.Submit(_alphaSource, token, viewerDistance);
|
|
}
|
|
}
|