fix(rendering): port retail shared alpha list
Queue translucent world GfxObj batches and scene particles in one stable far-to-near stream using transformed DAT sort centers, then drain it at retail's landscape and final-world boundaries. Preserve authored blend, cull, lighting, opacity, and adjacent-only batching so particles behind lifestones are composited through the crystal instead of overpainting it. Release build succeeds and all 5,914 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
ec1bb19609
commit
6b0472ee32
14 changed files with 1083 additions and 34 deletions
|
|
@ -375,6 +375,7 @@ public sealed class GameWindow : IDisposable
|
|||
private AcDream.App.Rendering.Vfx.EntityEffectController? _entityEffects;
|
||||
private double _physicsScriptGameTime;
|
||||
private AcDream.App.Rendering.ParticleRenderer? _particleRenderer;
|
||||
private readonly AcDream.App.Rendering.RetailAlphaQueue _retailAlphaQueue = new();
|
||||
// Retail GameSky copies SkyObject.PesObjectId into CelestialPosition but
|
||||
// never consumes it in CreateDeletePhysicsObjects/MakeObject/UseTime.
|
||||
// Keep the experimental path available for DAT archaeology only.
|
||||
|
|
@ -2627,7 +2628,8 @@ public sealed class GameWindow : IDisposable
|
|||
_classificationCache, _translucencyFades,
|
||||
_retailSelectionScene ??= new AcDream.App.Rendering.Selection.RetailSelectionScene(
|
||||
new AcDream.App.Rendering.Selection.RetailSelectionGeometryCache(
|
||||
_dats!, _datLock)));
|
||||
_dats!, _datLock)),
|
||||
_retailAlphaQueue);
|
||||
// A.5 T22.5: apply A2C gate from quality preset.
|
||||
_wbDrawDispatcher.AlphaToCoverage = _resolvedQuality.AlphaToCoverage;
|
||||
|
||||
|
|
@ -2686,7 +2688,8 @@ public sealed class GameWindow : IDisposable
|
|||
_particleSystem,
|
||||
_textureCache,
|
||||
_dats,
|
||||
_wbMeshAdapter);
|
||||
_wbMeshAdapter,
|
||||
_retailAlphaQueue);
|
||||
|
||||
// A.5 T22.5: apply radii from the already-resolved _resolvedQuality.
|
||||
// _resolvedQuality was set by the quality block immediately after
|
||||
|
|
@ -9632,6 +9635,7 @@ public sealed class GameWindow : IDisposable
|
|||
_retailSelectionScene?.BeginFrame();
|
||||
if (_cameraController is not null && !portalViewportVisible)
|
||||
{
|
||||
_retailAlphaQueue.BeginFrame();
|
||||
var activeCamera = _cameraController.Active;
|
||||
var camera = _teleportViewPlane.ApplyTo(activeCamera);
|
||||
var worldProjection = camera.Projection;
|
||||
|
|
@ -10212,6 +10216,7 @@ public sealed class GameWindow : IDisposable
|
|||
AcDream.Core.Vfx.ParticleRenderPass.Scene,
|
||||
emitter => emitter.AttachedObjectId == 0);
|
||||
},
|
||||
FlushLandscapeAlpha = _retailAlphaQueue.Flush,
|
||||
DrawCellParticles = sliceCtx =>
|
||||
DrawRetailPViewCellParticles(sliceCtx, camera, camPos),
|
||||
DrawDynamicsParticles = survivors =>
|
||||
|
|
@ -10543,6 +10548,7 @@ public sealed class GameWindow : IDisposable
|
|||
// pre-login screen shows a live, correctly-tinted sky and
|
||||
// nothing else.
|
||||
SkipWorldGeometry:
|
||||
_retailAlphaQueue.EndFrame();
|
||||
if (_terrain is not null)
|
||||
_particleVisibility.MarkVisibleCells(_terrain.VisibleCellIds);
|
||||
_particleVisibility.CompleteFrame();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,10 @@ using Silk.NET.OpenGL;
|
|||
namespace AcDream.App.Rendering;
|
||||
|
||||
/// <summary>
|
||||
/// Instanced renderer for retail particle emitters.
|
||||
/// Instanced renderer for retail particle emitters. Scene particles submit to
|
||||
/// <see cref="RetailAlphaQueue"/> while a world frame is active so their
|
||||
/// compositing order is shared with ordinary translucent GfxObj parts. Sky and
|
||||
/// sealed off-screen passes retain their independent immediate path.
|
||||
/// </summary>
|
||||
public sealed unsafe class ParticleRenderer : IDisposable
|
||||
{
|
||||
|
|
@ -24,6 +27,11 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
MeshBatchKey Key,
|
||||
ObjectRenderBatch Batch,
|
||||
MeshParticleInstance Instance);
|
||||
private readonly record struct DeferredParticleDraw(
|
||||
ParticleSubmissionKind Kind,
|
||||
ParticleDraw Billboard,
|
||||
MeshParticleDraw Mesh,
|
||||
Matrix4x4 ViewProjection);
|
||||
|
||||
private readonly struct ParticleInstance
|
||||
{
|
||||
|
|
@ -64,6 +72,8 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
private readonly DatCollection? _dats;
|
||||
private readonly WbMeshAdapter? _meshAdapter;
|
||||
private readonly ParticleSystem _particles;
|
||||
private readonly RetailAlphaQueue? _alphaQueue;
|
||||
private readonly AlphaDrawSource _alphaSource;
|
||||
private readonly Dictionary<uint, ParticleGfxInfo> _particleGfxInfoByGfxObj = new();
|
||||
private readonly Dictionary<uint, RetailParticleGeometryKind> _geometryKindByGfxObj = new();
|
||||
private readonly Dictionary<uint, TranslucencyKind> _meshBlendBySurface = new();
|
||||
|
|
@ -97,20 +107,33 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
private readonly List<MeshParticleDraw> _meshDrawListScratch = new(64);
|
||||
private readonly List<MeshParticleInstance> _meshRunScratch = new(64);
|
||||
private readonly List<ParticleSubmission> _submissionScratch = new(128);
|
||||
private readonly List<DeferredParticleDraw> _deferredAlpha = new(128);
|
||||
|
||||
public ParticleRenderer(
|
||||
private sealed class AlphaDrawSource(ParticleRenderer owner) : IRetailAlphaDrawSource
|
||||
{
|
||||
public void DrawAlphaBatch(ReadOnlySpan<int> tokens)
|
||||
=> owner.DrawDeferredAlphaBatch(tokens);
|
||||
|
||||
public void ResetAlphaSubmissions()
|
||||
=> owner._deferredAlpha.Clear();
|
||||
}
|
||||
|
||||
internal ParticleRenderer(
|
||||
GL gl,
|
||||
string shadersDir,
|
||||
ParticleSystem particles,
|
||||
TextureCache? textures = null,
|
||||
DatCollection? dats = null,
|
||||
WbMeshAdapter? meshAdapter = null)
|
||||
WbMeshAdapter? meshAdapter = null,
|
||||
RetailAlphaQueue? alphaQueue = null)
|
||||
{
|
||||
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
|
||||
_textures = textures;
|
||||
_dats = dats;
|
||||
_meshAdapter = meshAdapter;
|
||||
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
|
||||
_alphaQueue = alphaQueue;
|
||||
_alphaSource = new AlphaDrawSource(this);
|
||||
if (_meshAdapter is not null)
|
||||
{
|
||||
_meshReferences = new ParticleMeshReferenceTracker(
|
||||
|
|
@ -221,10 +244,43 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
Vector3 cameraRight = Vector3.Normalize(new Vector3(invView.M11, invView.M12, invView.M13));
|
||||
Vector3 cameraUp = Vector3.Normalize(new Vector3(invView.M21, invView.M22, invView.M23));
|
||||
BuildDrawLists(cameraWorldPos, renderPass, cameraRight, cameraUp, emitterFilter);
|
||||
if (_submissionScratch.Count > 0)
|
||||
if (_submissionScratch.Count == 0)
|
||||
return;
|
||||
|
||||
if (renderPass == ParticleRenderPass.Scene && _alphaQueue?.IsCollecting == true)
|
||||
DeferToRetailAlphaQueue(camera);
|
||||
else
|
||||
DrawOrdered(camera);
|
||||
}
|
||||
|
||||
private void DeferToRetailAlphaQueue(ICamera camera)
|
||||
{
|
||||
RetailAlphaQueue queue = _alphaQueue!;
|
||||
Matrix4x4 viewProjection = camera.View * camera.Projection;
|
||||
for (int i = 0; i < _submissionScratch.Count; i++)
|
||||
{
|
||||
ParticleSubmission submission = _submissionScratch[i];
|
||||
DeferredParticleDraw deferred = submission.Kind == ParticleSubmissionKind.Billboard
|
||||
? new DeferredParticleDraw(
|
||||
submission.Kind,
|
||||
_drawListScratch[submission.DrawIndex],
|
||||
default,
|
||||
viewProjection)
|
||||
: new DeferredParticleDraw(
|
||||
submission.Kind,
|
||||
default,
|
||||
_meshDrawListScratch[submission.DrawIndex],
|
||||
viewProjection);
|
||||
|
||||
int token = _deferredAlpha.Count;
|
||||
_deferredAlpha.Add(deferred);
|
||||
queue.Submit(
|
||||
_alphaSource,
|
||||
token,
|
||||
MathF.Sqrt(MathF.Max(0f, submission.DistanceSq)));
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawOrdered(ICamera camera)
|
||||
{
|
||||
ParticleSubmissionOrdering.Sort(_submissionScratch);
|
||||
|
|
@ -245,7 +301,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
BatchKey key = draw.Key;
|
||||
if (activeKind != ParticleSubmissionKind.Billboard)
|
||||
{
|
||||
PrepareBillboardPipeline(camera);
|
||||
PrepareBillboardPipeline(camera.View * camera.Projection);
|
||||
activeKind = ParticleSubmissionKind.Billboard;
|
||||
}
|
||||
|
||||
|
|
@ -281,7 +337,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
ObjectRenderBatch batch = meshDraw.Batch;
|
||||
if (activeKind != ParticleSubmissionKind.Mesh)
|
||||
{
|
||||
PrepareMeshPipeline(camera, global);
|
||||
PrepareMeshPipeline(camera.View * camera.Projection, global);
|
||||
activeKind = ParticleSubmissionKind.Mesh;
|
||||
}
|
||||
|
||||
|
|
@ -334,19 +390,137 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
_gl.Disable(EnableCap.CullFace);
|
||||
}
|
||||
|
||||
private void PrepareBillboardPipeline(ICamera camera)
|
||||
private void DrawDeferredAlphaBatch(ReadOnlySpan<int> tokens)
|
||||
{
|
||||
if (tokens.Length == 0)
|
||||
return;
|
||||
|
||||
GlobalMeshBuffer? global = _meshAdapter?.MeshManager?.GlobalBuffer;
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
_gl.DepthMask(false);
|
||||
_gl.ActiveTexture(TextureUnit.Texture0);
|
||||
|
||||
ParticleSubmissionKind? activeKind = null;
|
||||
Matrix4x4 activeViewProjection = default;
|
||||
int i = 0;
|
||||
while (i < tokens.Length)
|
||||
{
|
||||
DeferredParticleDraw deferred = _deferredAlpha[tokens[i]];
|
||||
if (deferred.Kind == ParticleSubmissionKind.Billboard)
|
||||
{
|
||||
ParticleDraw draw = deferred.Billboard;
|
||||
BatchKey key = draw.Key;
|
||||
if (activeKind != ParticleSubmissionKind.Billboard
|
||||
|| activeViewProjection != deferred.ViewProjection)
|
||||
{
|
||||
PrepareBillboardPipeline(deferred.ViewProjection);
|
||||
activeKind = ParticleSubmissionKind.Billboard;
|
||||
activeViewProjection = deferred.ViewProjection;
|
||||
}
|
||||
|
||||
_runScratch.Clear();
|
||||
do
|
||||
{
|
||||
_runScratch.Add(deferred.Billboard.Instance);
|
||||
i++;
|
||||
if (i >= tokens.Length)
|
||||
break;
|
||||
deferred = _deferredAlpha[tokens[i]];
|
||||
}
|
||||
while (deferred.Kind == ParticleSubmissionKind.Billboard
|
||||
&& deferred.Billboard.Key == key
|
||||
&& deferred.ViewProjection == activeViewProjection);
|
||||
|
||||
_gl.BlendFunc(
|
||||
BlendingFactor.SrcAlpha,
|
||||
key.Additive ? BlendingFactor.One : BlendingFactor.OneMinusSrcAlpha);
|
||||
_shader.SetInt("uUseTexture", key.UseTexture ? 1 : 0);
|
||||
_gl.BindTexture(TextureTarget.Texture2D, key.UseTexture ? key.TextureHandle : 0);
|
||||
DrawInstances(_runScratch);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_meshShader is null || global is null)
|
||||
{
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
MeshParticleDraw meshDraw = deferred.Mesh;
|
||||
MeshBatchKey meshKey = meshDraw.Key;
|
||||
ObjectRenderBatch batch = meshDraw.Batch;
|
||||
if (activeKind != ParticleSubmissionKind.Mesh
|
||||
|| activeViewProjection != deferred.ViewProjection)
|
||||
{
|
||||
PrepareMeshPipeline(deferred.ViewProjection, global);
|
||||
activeKind = ParticleSubmissionKind.Mesh;
|
||||
activeViewProjection = deferred.ViewProjection;
|
||||
}
|
||||
|
||||
_meshRunScratch.Clear();
|
||||
do
|
||||
{
|
||||
_meshRunScratch.Add(deferred.Mesh.Instance);
|
||||
i++;
|
||||
if (i >= tokens.Length)
|
||||
break;
|
||||
deferred = _deferredAlpha[tokens[i]];
|
||||
}
|
||||
while (deferred.Kind == ParticleSubmissionKind.Mesh
|
||||
&& deferred.Mesh.Key == meshKey
|
||||
&& deferred.ViewProjection == activeViewProjection);
|
||||
|
||||
ApplyMeshCullMode(batch.CullMode);
|
||||
TranslucencyKind blend = ResolveMeshBlend(batch);
|
||||
_gl.BlendFunc(
|
||||
blend == TranslucencyKind.InvAlpha
|
||||
? BlendingFactor.OneMinusSrcAlpha
|
||||
: BlendingFactor.SrcAlpha,
|
||||
blend switch
|
||||
{
|
||||
TranslucencyKind.Additive => BlendingFactor.One,
|
||||
TranslucencyKind.InvAlpha => BlendingFactor.SrcAlpha,
|
||||
_ => BlendingFactor.OneMinusSrcAlpha,
|
||||
});
|
||||
|
||||
_gl.ProgramUniform2(
|
||||
_meshShader.Program,
|
||||
_meshTextureHandleLoc,
|
||||
(uint)(batch.BindlessTextureHandle & 0xFFFFFFFFu),
|
||||
(uint)(batch.BindlessTextureHandle >> 32));
|
||||
_gl.ProgramUniform1(_meshShader.Program, _meshTextureLayerLoc, batch.TextureIndex);
|
||||
|
||||
UploadMeshInstances(_meshRunScratch);
|
||||
_gl.DrawElementsInstancedBaseVertex(
|
||||
PrimitiveType.Triangles,
|
||||
(uint)batch.IndexCount,
|
||||
DrawElementsType.UnsignedShort,
|
||||
(void*)(batch.FirstIndex * sizeof(ushort)),
|
||||
(uint)_meshRunScratch.Count,
|
||||
(int)batch.BaseVertex);
|
||||
}
|
||||
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
_gl.BindVertexArray(0);
|
||||
_gl.DepthMask(true);
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
}
|
||||
|
||||
private void PrepareBillboardPipeline(Matrix4x4 viewProjection)
|
||||
{
|
||||
_shader.Use();
|
||||
_shader.SetMatrix4("uViewProjection", camera.View * camera.Projection);
|
||||
_shader.SetMatrix4("uViewProjection", viewProjection);
|
||||
_shader.SetInt("uParticleTexture", 0);
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
_gl.BindVertexArray(_quadVao);
|
||||
}
|
||||
|
||||
private void PrepareMeshPipeline(ICamera camera, GlobalMeshBuffer global)
|
||||
private void PrepareMeshPipeline(Matrix4x4 viewProjection, GlobalMeshBuffer global)
|
||||
{
|
||||
_meshShader!.Use();
|
||||
_meshShader.SetMatrix4("uViewProjection", camera.View * camera.Projection);
|
||||
_meshShader.SetMatrix4("uViewProjection", viewProjection);
|
||||
_gl.FrontFace(FrontFaceDirection.CW);
|
||||
_gl.BindVertexArray(_meshVao);
|
||||
|
||||
|
|
@ -415,11 +589,10 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
// mirrors retail's live-parent-frame read at
|
||||
// ParticleEmitter::UpdateParticles 0x0051d2d4 for is_parent_local=1.
|
||||
Vector3 pos = p.Position;
|
||||
float distSq = Vector3.DistanceSquared(pos, cameraWorldPos);
|
||||
uint gfxObjId = em.Desc.HwGfxObjId != 0 ? em.Desc.HwGfxObjId : em.Desc.GfxObjId;
|
||||
if (gfxObjId != 0
|
||||
&& ResolveGeometryKind(gfxObjId) == RetailParticleGeometryKind.FullMesh
|
||||
&& TryAppendMeshDraws(em, p, gfxObjId, distSq, ref sequence))
|
||||
&& TryAppendMeshDraws(em, p, gfxObjId, cameraWorldPos, ref sequence))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
|
@ -447,6 +620,8 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
axisY = Vector3.Transform(gfxInfo.AxisY, orientation) * (gfxInfo.Size.Y * p.Size);
|
||||
}
|
||||
|
||||
float distSq = Vector3.DistanceSquared(pos, cameraWorldPos);
|
||||
|
||||
int drawIndex = draws.Count;
|
||||
draws.Add(new ParticleDraw(key, new ParticleInstance(pos, axisX, axisY, p.ColorArgb, distSq)));
|
||||
_submissionScratch.Add(new ParticleSubmission(
|
||||
|
|
@ -462,7 +637,7 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
AcDream.Core.Vfx.ParticleEmitter emitter,
|
||||
Particle particle,
|
||||
uint gfxObjId,
|
||||
float distanceSq,
|
||||
Vector3 cameraWorldPosition,
|
||||
ref int sequence)
|
||||
{
|
||||
if (_meshAdapter is null || _meshShader is null)
|
||||
|
|
@ -481,6 +656,11 @@ public sealed unsafe class ParticleRenderer : IDisposable
|
|||
Matrix4x4 model = Matrix4x4.CreateScale(particle.Size)
|
||||
* Matrix4x4.CreateFromQuaternion(orientation)
|
||||
* Matrix4x4.CreateTranslation(particle.Position);
|
||||
float viewerDistance = RetailAlphaOrdering.ComputeViewerDistance(
|
||||
renderData.SortCenter,
|
||||
model,
|
||||
cameraWorldPosition);
|
||||
float distanceSq = viewerDistance * viewerDistance;
|
||||
var instance = new MeshParticleInstance(model, particle.ColorArgb, distanceSq);
|
||||
|
||||
for (int batchIndex = 0; batchIndex < renderData.Batches.Count; batchIndex++)
|
||||
|
|
|
|||
171
src/AcDream.App/Rendering/RetailAlphaQueue.cs
Normal file
171
src/AcDream.App/Rendering/RetailAlphaQueue.cs
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
internal static class RetailAlphaOrdering
|
||||
{
|
||||
/// <summary>
|
||||
/// Retail <c>CPhysicsPart::UpdateViewerDistance</c> 0x0050E030:
|
||||
/// transform the GfxObj's authored sort center through the part draw frame,
|
||||
/// then store its Euclidean distance from the viewer as <c>CYpt</c>.
|
||||
/// </summary>
|
||||
public static float ComputeViewerDistance(
|
||||
Vector3 localSortCenter,
|
||||
Matrix4x4 model,
|
||||
Vector3 cameraWorldPosition)
|
||||
=> Vector3.Distance(Vector3.Transform(localSortCenter, model), cameraWorldPosition);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renderer-owned half of retail's delayed alpha list. A source keeps the
|
||||
/// immutable payload addressed by each token until the queue flushes, then
|
||||
/// releases the payload when <see cref="ResetAlphaSubmissions"/> is called.
|
||||
/// </summary>
|
||||
internal interface IRetailAlphaDrawSource
|
||||
{
|
||||
void DrawAlphaBatch(ReadOnlySpan<int> tokens);
|
||||
void ResetAlphaSubmissions();
|
||||
}
|
||||
|
||||
internal readonly record struct RetailAlphaSubmission(
|
||||
IRetailAlphaDrawSource Source,
|
||||
int Token,
|
||||
float ViewerDistance,
|
||||
long Sequence);
|
||||
|
||||
/// <summary>
|
||||
/// Frame-scoped port of retail's shared <c>D3DPolyRender</c> alpha list.
|
||||
/// Scene-particle parts and ordinary GfxObj parts enter one stable,
|
||||
/// far-to-near stream instead of being composited in renderer-local passes.
|
||||
///
|
||||
/// Retail oracle:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>CShadowPart::insertion_sort</c> 0x006B5130 orders all cell parts by
|
||||
/// <c>CPhysicsPart::CYpt</c>.</item>
|
||||
/// <item><c>D3DPolyRender::AddMeshToAlphaList</c> 0x0059C230 appends delayed
|
||||
/// surface subsets in that established order.</item>
|
||||
/// <item><c>D3DPolyRender::FlushAlphaList</c> 0x0059D2E0 drains without another
|
||||
/// material/renderer sort.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
internal sealed class RetailAlphaQueue
|
||||
{
|
||||
private readonly List<RetailAlphaSubmission> _submissions = new(256);
|
||||
private readonly List<IRetailAlphaDrawSource> _sources = new(4);
|
||||
private int[] _tokenScratch = new int[256];
|
||||
private long _nextSequence;
|
||||
|
||||
public bool IsCollecting { get; private set; }
|
||||
internal int PendingCount => _submissions.Count;
|
||||
|
||||
public void BeginFrame()
|
||||
{
|
||||
if (IsCollecting)
|
||||
throw new InvalidOperationException("Retail alpha frame is already active.");
|
||||
if (_submissions.Count != 0 || _sources.Count != 0)
|
||||
throw new InvalidOperationException("Retail alpha queue retained payload outside a frame.");
|
||||
|
||||
_nextSequence = 0;
|
||||
IsCollecting = true;
|
||||
}
|
||||
|
||||
public void Submit(IRetailAlphaDrawSource source, int token, float viewerDistance)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(source);
|
||||
if (!IsCollecting)
|
||||
throw new InvalidOperationException("Retail alpha submission requires an active frame.");
|
||||
if (token < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(token));
|
||||
|
||||
_submissions.Add(new RetailAlphaSubmission(
|
||||
source,
|
||||
token,
|
||||
NormalizeDistance(viewerDistance),
|
||||
_nextSequence++));
|
||||
|
||||
for (int i = 0; i < _sources.Count; i++)
|
||||
if (ReferenceEquals(_sources[i], source))
|
||||
return;
|
||||
_sources.Add(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drains the current retail alpha scope and keeps the frame open so the
|
||||
/// caller can collect the post-depth-clear scope. Only adjacent entries
|
||||
/// from one renderer are handed over as a batch; the queue never groups by
|
||||
/// renderer across another entry and therefore cannot alter compositing.
|
||||
/// </summary>
|
||||
public void Flush()
|
||||
{
|
||||
if (!IsCollecting)
|
||||
throw new InvalidOperationException("Retail alpha flush requires an active frame.");
|
||||
|
||||
try
|
||||
{
|
||||
if (_submissions.Count == 0)
|
||||
return;
|
||||
|
||||
_submissions.Sort(CompareRetailOrder);
|
||||
EnsureTokenCapacity(_submissions.Count);
|
||||
|
||||
int start = 0;
|
||||
while (start < _submissions.Count)
|
||||
{
|
||||
IRetailAlphaDrawSource source = _submissions[start].Source;
|
||||
int end = start + 1;
|
||||
while (end < _submissions.Count
|
||||
&& ReferenceEquals(_submissions[end].Source, source))
|
||||
end++;
|
||||
|
||||
int count = end - start;
|
||||
for (int i = 0; i < count; i++)
|
||||
_tokenScratch[i] = _submissions[start + i].Token;
|
||||
source.DrawAlphaBatch(_tokenScratch.AsSpan(0, count));
|
||||
start = end;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
for (int i = 0; i < _sources.Count; i++)
|
||||
_sources[i].ResetAlphaSubmissions();
|
||||
_sources.Clear();
|
||||
_submissions.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void EndFrame()
|
||||
{
|
||||
if (!IsCollecting)
|
||||
throw new InvalidOperationException("Retail alpha frame is not active.");
|
||||
|
||||
try
|
||||
{
|
||||
Flush();
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsCollecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private static int CompareRetailOrder(RetailAlphaSubmission left, RetailAlphaSubmission right)
|
||||
{
|
||||
// CShadowPart::insertion_sort 0x006B5130 produces descending CYpt
|
||||
// (far-to-near). Sequence makes the List.Sort result stable for equal
|
||||
// distances, preserving part/surface append order like retail.
|
||||
int distance = right.ViewerDistance.CompareTo(left.ViewerDistance);
|
||||
return distance != 0 ? distance : left.Sequence.CompareTo(right.Sequence);
|
||||
}
|
||||
|
||||
private static float NormalizeDistance(float value)
|
||||
=> float.IsFinite(value) && value >= 0f ? value : 0f;
|
||||
|
||||
private void EnsureTokenCapacity(int count)
|
||||
{
|
||||
if (_tokenScratch.Length >= count)
|
||||
return;
|
||||
Array.Resize(ref _tokenScratch, count + 256);
|
||||
}
|
||||
}
|
||||
|
|
@ -539,6 +539,11 @@ public sealed class RetailPViewRenderer
|
|||
if (!ctx.RootCell.IsOutdoorNode)
|
||||
ctx.DrawUnattachedSceneParticles?.Invoke();
|
||||
|
||||
// Retail PView::DrawCells 0x005A4872 drains the landscape alpha list
|
||||
// immediately after LScape::draw and before the optional depth clear.
|
||||
// The queue remains active for the post-clear/final-world scope.
|
||||
ctx.FlushLandscapeAlpha?.Invoke();
|
||||
|
||||
// T1: retail clears the FULL depth buffer ONCE between the outside
|
||||
// stage and the interior stage (PView::DrawCells, Ghidra 0x005a4840 —
|
||||
// Clear gated on portalsDrawnCount; exact gate semantics is a plan
|
||||
|
|
@ -1105,6 +1110,10 @@ public sealed class RetailPViewDrawContext : IRetailPViewCellDrawContext
|
|||
/// end of the landscape stage (pre-clear). Outdoor roots draw them via
|
||||
/// GameWindow's dedicated post-frame pass instead.</summary>
|
||||
public Action? DrawUnattachedSceneParticles { get; init; }
|
||||
/// <summary>Drains retail's delayed landscape alpha list before the
|
||||
/// optional outside-to-inside depth clear. The same frame-scoped queue
|
||||
/// remains open for the final world alpha scope.</summary>
|
||||
public Action? FlushLandscapeAlpha { get; init; }
|
||||
public Action<IReadOnlyList<WorldEntity>>? DrawDynamicsParticles { get; init; }
|
||||
public Action<RetailPViewFrameResult>? EmitDiagnostics { get; init; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// subPart contributes its own <see cref="CachedBatch"/> entries, with
|
||||
/// <see cref="RestPose"/> already containing the
|
||||
/// <c>subPart.PartTransform * meshRef.PartTransform</c> product.
|
||||
/// <see cref="LocalSortCenter"/> preserves the authored GfxObj key used by
|
||||
/// retail's delayed-alpha viewer-distance ordering on cache hits.
|
||||
///
|
||||
/// Accessibility: <c>internal</c> because <see cref="GroupKey"/> is
|
||||
/// <c>internal</c> and shows up in this struct's constructor / <c>Deconstruct</c>
|
||||
|
|
@ -18,7 +20,8 @@ namespace AcDream.App.Rendering.Wb;
|
|||
internal readonly record struct CachedBatch(
|
||||
GroupKey Key,
|
||||
ulong BindlessTextureHandle,
|
||||
Matrix4x4 RestPose);
|
||||
Matrix4x4 RestPose,
|
||||
Vector3 LocalSortCenter = default);
|
||||
|
||||
/// <summary>
|
||||
/// One entity's cached classification. <see cref="Batches"/> is flat across
|
||||
|
|
|
|||
|
|
@ -43,8 +43,10 @@ namespace AcDream.App.Rendering.Wb;
|
|||
/// each group becomes one <c>DrawElementsIndirectCommand</c>. Three GPU buffers
|
||||
/// are uploaded per frame: instance matrices (SSBO binding 0), per-group batch
|
||||
/// metadata/texture handles (SSBO binding 1), and the indirect draw commands.
|
||||
/// Two <c>glMultiDrawElementsIndirect</c> calls cover the opaque and transparent
|
||||
/// passes respectively — one GL call per pass regardless of group count.
|
||||
/// Opaque world groups remain MDI-batched. Transparent world instances enter
|
||||
/// <see cref="RetailAlphaQueue"/> so ordinary GfxObj parts and particles share
|
||||
/// retail's stable far-to-near stream; sealed off-screen consumers retain the
|
||||
/// immediate transparent MDI path.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -88,6 +90,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
private readonly EntitySpawnAdapter _entitySpawnAdapter;
|
||||
private readonly IRetailSelectionRenderSink? _selectionSink;
|
||||
private readonly IRetailSelectionLightingSource? _selectionLighting;
|
||||
private readonly RetailAlphaQueue? _alphaQueue;
|
||||
private readonly AlphaDrawSource _alphaSource;
|
||||
|
||||
private readonly BindlessSupport _bindless;
|
||||
|
||||
|
|
@ -240,10 +244,43 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
public uint Flags;
|
||||
}
|
||||
|
||||
private readonly record struct DeferredAlphaInstance(
|
||||
GroupKey Key,
|
||||
Matrix4x4 Model,
|
||||
uint ClipSlot,
|
||||
AlphaLightSet Lights,
|
||||
uint Indoor,
|
||||
float Opacity,
|
||||
Vector2 SelectionLighting);
|
||||
|
||||
private readonly record struct AlphaLightSet(
|
||||
int L0, int L1, int L2, int L3,
|
||||
int L4, int L5, int L6, int L7)
|
||||
{
|
||||
public int this[int index] => index switch
|
||||
{
|
||||
0 => L0, 1 => L1, 2 => L2, 3 => L3,
|
||||
4 => L4, 5 => L5, 6 => L6, 7 => L7,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
}
|
||||
|
||||
private sealed class AlphaDrawSource(WbDrawDispatcher owner) : IRetailAlphaDrawSource
|
||||
{
|
||||
public void DrawAlphaBatch(ReadOnlySpan<int> tokens)
|
||||
=> owner.DrawDeferredAlphaBatch(tokens);
|
||||
|
||||
public void ResetAlphaSubmissions()
|
||||
=> owner._deferredAlpha.Clear();
|
||||
}
|
||||
|
||||
// Per-frame scratch — reused across frames to avoid per-frame allocation.
|
||||
private readonly Dictionary<GroupKey, InstanceGroup> _groups = new();
|
||||
private readonly List<InstanceGroup> _opaqueDraws = new();
|
||||
private readonly List<InstanceGroup> _translucentDraws = new();
|
||||
private readonly List<DeferredAlphaInstance> _deferredAlpha = new(128);
|
||||
private TranslucencyKind[] _deferredAlphaKinds = new TranslucencyKind[128];
|
||||
private Matrix4x4 _deferredAlphaViewProjection;
|
||||
// A.5 T26 follow-up (Bug B): WalkEntities populates this scratch list
|
||||
// instead of allocating a fresh List<(WorldEntity, int)> per frame. At
|
||||
// ~10K entities × ~3 mesh refs = ~30K tuples × 16 bytes = ~480 KB / frame
|
||||
|
|
@ -363,7 +400,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
BindlessSupport bindless,
|
||||
EntityClassificationCache classificationCache,
|
||||
AcDream.Core.Rendering.TranslucencyFadeManager translucencyFades,
|
||||
IRetailSelectionRenderSink? selectionSink = null)
|
||||
IRetailSelectionRenderSink? selectionSink = null,
|
||||
RetailAlphaQueue? alphaQueue = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gl);
|
||||
ArgumentNullException.ThrowIfNull(shader);
|
||||
|
|
@ -382,6 +420,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_translucencyFades = translucencyFades;
|
||||
_selectionSink = selectionSink;
|
||||
_selectionLighting = selectionSink as IRetailSelectionLightingSource;
|
||||
_alphaQueue = alphaQueue;
|
||||
_alphaSource = new AlphaDrawSource(this);
|
||||
|
||||
_bindless = bindless ?? throw new ArgumentNullException(nameof(bindless));
|
||||
_instanceSsbo = _gl.GenBuffer();
|
||||
|
|
@ -1579,12 +1619,23 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
// stable 1k-draw live scene into hundreds of tiny MDI runs after a
|
||||
// landblock transition, which shows up as a GPU-command bottleneck
|
||||
// without a triangle-count spike.
|
||||
// Retail particles and ordinary object parts share CPartCell's
|
||||
// CShadowPart list before delayed alpha is flushed. During the world
|
||||
// alpha frame, preserve each transparent instance as an independent
|
||||
// submission so the shared queue can interleave it with particles.
|
||||
// Immediate mode remains for sealed off-screen consumers such as the
|
||||
// paperdoll and UI Studio render stack.
|
||||
bool deferTransparent = _alphaQueue?.IsCollecting == true;
|
||||
_opaqueDraws.Sort(CompareOpaqueSubmissionOrder);
|
||||
_translucentDraws.Sort(CompareTransparentSubmissionOrder);
|
||||
if (deferTransparent)
|
||||
DeferTransparentGroups(camPos, vp);
|
||||
else
|
||||
_translucentDraws.Sort(CompareTransparentSubmissionOrder);
|
||||
|
||||
// ── Phase 4: build IndirectGroupInput list (opaque sorted, then translucent),
|
||||
// fill via BuildIndirectArrays ──────────────────────────────────
|
||||
int totalDraws = _opaqueDraws.Count + _translucentDraws.Count;
|
||||
int immediateTransparentCount = deferTransparent ? 0 : _translucentDraws.Count;
|
||||
int totalDraws = _opaqueDraws.Count + immediateTransparentCount;
|
||||
if (_batchData.Length < totalDraws)
|
||||
_batchData = new BatchData[totalDraws + 64];
|
||||
if (_indirectCommands.Length < totalDraws)
|
||||
|
|
@ -1594,7 +1645,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
|
||||
var groupInputs = new List<IndirectGroupInput>(totalDraws);
|
||||
foreach (var g in _opaqueDraws) groupInputs.Add(ToInput(g));
|
||||
foreach (var g in _translucentDraws) groupInputs.Add(ToInput(g));
|
||||
if (!deferTransparent)
|
||||
foreach (var g in _translucentDraws) groupInputs.Add(ToInput(g));
|
||||
|
||||
// Cast _batchData (private BatchData) to public-mirror BatchDataPublic for BuildIndirectArrays.
|
||||
// Layout is asserted at test time (BatchDataPublic_LayoutMatchesPrivateBatchData test).
|
||||
|
|
@ -1907,6 +1959,200 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
Translucency: g.Translucency,
|
||||
CullMode: g.CullMode);
|
||||
|
||||
private static GroupKey ToKey(InstanceGroup g) => new(
|
||||
g.Ibo,
|
||||
g.FirstIndex,
|
||||
g.BaseVertex,
|
||||
g.IndexCount,
|
||||
g.BindlessTextureHandle,
|
||||
g.TextureLayer,
|
||||
g.Translucency,
|
||||
g.CullMode);
|
||||
|
||||
private void DeferTransparentGroups(Vector3 cameraWorldPosition, Matrix4x4 viewProjection)
|
||||
{
|
||||
RetailAlphaQueue queue = _alphaQueue!;
|
||||
if (_deferredAlpha.Count == 0)
|
||||
_deferredAlphaViewProjection = viewProjection;
|
||||
else if (_deferredAlphaViewProjection != viewProjection)
|
||||
throw new InvalidOperationException(
|
||||
"One retail alpha scope cannot combine different view-projection matrices.");
|
||||
|
||||
foreach (InstanceGroup group in _translucentDraws)
|
||||
{
|
||||
GroupKey key = ToKey(group);
|
||||
for (int i = 0; i < group.Matrices.Count; i++)
|
||||
{
|
||||
Matrix4x4 model = group.Matrices[i];
|
||||
Vector3 localSortCenter = group.LocalSortCenters[i];
|
||||
float viewerDistance = RetailAlphaOrdering.ComputeViewerDistance(
|
||||
localSortCenter,
|
||||
model,
|
||||
cameraWorldPosition);
|
||||
int lightOffset = i * LightManager.MaxLightsPerObject;
|
||||
var lights = new AlphaLightSet(
|
||||
group.LightSets[lightOffset + 0], group.LightSets[lightOffset + 1],
|
||||
group.LightSets[lightOffset + 2], group.LightSets[lightOffset + 3],
|
||||
group.LightSets[lightOffset + 4], group.LightSets[lightOffset + 5],
|
||||
group.LightSets[lightOffset + 6], group.LightSets[lightOffset + 7]);
|
||||
|
||||
int token = _deferredAlpha.Count;
|
||||
_deferredAlpha.Add(new DeferredAlphaInstance(
|
||||
key,
|
||||
model,
|
||||
group.Slots[i],
|
||||
lights,
|
||||
group.IndoorFlags[i],
|
||||
group.Opacities[i],
|
||||
group.SelectionLighting[i]));
|
||||
queue.Submit(_alphaSource, token, viewerDistance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawDeferredAlphaBatch(ReadOnlySpan<int> tokens)
|
||||
{
|
||||
if (tokens.Length == 0)
|
||||
return;
|
||||
|
||||
GlobalMeshBuffer? global = _meshAdapter.MeshManager?.GlobalBuffer;
|
||||
if (global is null || global.VAO == 0)
|
||||
return;
|
||||
|
||||
int count = tokens.Length;
|
||||
EnsureDeferredAlphaCapacity(count);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
DeferredAlphaInstance entry = _deferredAlpha[tokens[i]];
|
||||
WriteMatrix(_instanceData, i * 16, entry.Model);
|
||||
_clipSlotData[i] = entry.ClipSlot;
|
||||
_indoorData[i] = entry.Indoor;
|
||||
_alphaData[i] = entry.Opacity;
|
||||
_selectionLightingData[i] = entry.SelectionLighting;
|
||||
int lightOffset = i * LightManager.MaxLightsPerObject;
|
||||
for (int light = 0; light < LightManager.MaxLightsPerObject; light++)
|
||||
_lightSetData[lightOffset + light] = entry.Lights[light];
|
||||
|
||||
GroupKey key = entry.Key;
|
||||
_batchData[i] = new BatchData
|
||||
{
|
||||
TextureHandle = key.BindlessTextureHandle,
|
||||
TextureLayer = key.TextureLayer,
|
||||
Flags = 0,
|
||||
};
|
||||
_indirectCommands[i] = new DrawElementsIndirectCommand
|
||||
{
|
||||
Count = (uint)key.IndexCount,
|
||||
InstanceCount = 1,
|
||||
FirstIndex = key.FirstIndex,
|
||||
BaseVertex = key.BaseVertex,
|
||||
BaseInstance = (uint)i,
|
||||
};
|
||||
_drawCullModes[i] = key.CullMode;
|
||||
_deferredAlphaKinds[i] = key.Translucency;
|
||||
}
|
||||
|
||||
UploadDeferredAlphaBuffers(count);
|
||||
|
||||
_shader.Use();
|
||||
_shader.SetMatrix4("uViewProjection", _deferredAlphaViewProjection);
|
||||
_shader.SetInt("uLightingMode", 0);
|
||||
_shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode);
|
||||
_shader.SetInt("uRenderPass", 1);
|
||||
BindClipRegionBinding2();
|
||||
_gl.BindVertexArray(global.VAO);
|
||||
_gl.Enable(EnableCap.DepthTest);
|
||||
_gl.Enable(EnableCap.Blend);
|
||||
_gl.DepthMask(false);
|
||||
_gl.FrontFace(FrontFaceDirection.CW);
|
||||
|
||||
int runStart = 0;
|
||||
while (runStart < count)
|
||||
{
|
||||
TranslucencyKind blend = _deferredAlphaKinds[runStart];
|
||||
int runEnd = runStart + 1;
|
||||
while (runEnd < count && _deferredAlphaKinds[runEnd] == blend)
|
||||
runEnd++;
|
||||
|
||||
ApplyRetailBlend(blend);
|
||||
DrawIndirectRange(runStart, runEnd - runStart);
|
||||
runStart = runEnd;
|
||||
}
|
||||
|
||||
_gl.DepthMask(true);
|
||||
_gl.Disable(EnableCap.Blend);
|
||||
_gl.Disable(EnableCap.CullFace);
|
||||
_gl.BindVertexArray(0);
|
||||
}
|
||||
|
||||
private void EnsureDeferredAlphaCapacity(int count)
|
||||
{
|
||||
int neededMatrixFloats = count * 16;
|
||||
if (_instanceData.Length < neededMatrixFloats)
|
||||
_instanceData = new float[neededMatrixFloats + 256 * 16];
|
||||
if (_clipSlotData.Length < count)
|
||||
_clipSlotData = new uint[count + 256];
|
||||
if (_indoorData.Length < count)
|
||||
_indoorData = new uint[count + 256];
|
||||
if (_alphaData.Length < count)
|
||||
_alphaData = new float[count + 256];
|
||||
if (_selectionLightingData.Length < count)
|
||||
_selectionLightingData = new Vector2[count + 256];
|
||||
if (_lightSetData.Length < count * LightManager.MaxLightsPerObject)
|
||||
_lightSetData = new int[(count + 256) * LightManager.MaxLightsPerObject];
|
||||
if (_batchData.Length < count)
|
||||
_batchData = new BatchData[count + 64];
|
||||
if (_indirectCommands.Length < count)
|
||||
_indirectCommands = new DrawElementsIndirectCommand[count + 64];
|
||||
if (_drawCullModes.Length < count)
|
||||
_drawCullModes = new CullMode[count + 64];
|
||||
if (_deferredAlphaKinds.Length < count)
|
||||
_deferredAlphaKinds = new TranslucencyKind[count + 64];
|
||||
}
|
||||
|
||||
private void UploadDeferredAlphaBuffers(int count)
|
||||
{
|
||||
fixed (float* p = _instanceData)
|
||||
UploadSsbo(_instanceSsbo, 0, p, count * 16 * sizeof(float));
|
||||
fixed (BatchData* p = _batchData)
|
||||
UploadSsbo(_batchSsbo, 1, p, count * sizeof(BatchData));
|
||||
fixed (uint* p = _clipSlotData)
|
||||
UploadSsbo(_clipSlotSsbo, 3, p, count * sizeof(uint));
|
||||
fixed (int* p = _lightSetData)
|
||||
UploadSsbo(_instLightSetSsbo, 5, p, count * LightManager.MaxLightsPerObject * sizeof(int));
|
||||
fixed (uint* p = _indoorData)
|
||||
UploadSsbo(_instIndoorSsbo, 6, p, count * sizeof(uint));
|
||||
fixed (float* p = _alphaData)
|
||||
UploadSsbo(_instAlphaSsbo, 7, p, count * sizeof(float));
|
||||
fixed (Vector2* p = _selectionLightingData)
|
||||
UploadSsbo(_instSelectionLightingSsbo, 8, p, count * sizeof(float) * 2);
|
||||
UploadGlobalLights();
|
||||
|
||||
fixed (DrawElementsIndirectCommand* p = _indirectCommands)
|
||||
{
|
||||
_gl.BindBuffer(BufferTargetARB.DrawIndirectBuffer, _indirectBuffer);
|
||||
_gl.BufferData(
|
||||
BufferTargetARB.DrawIndirectBuffer,
|
||||
(nuint)(count * sizeof(DrawElementsIndirectCommand)),
|
||||
p,
|
||||
BufferUsageARB.DynamicDraw);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyRetailBlend(TranslucencyKind blend)
|
||||
{
|
||||
_gl.BlendFunc(
|
||||
blend == TranslucencyKind.InvAlpha
|
||||
? BlendingFactor.OneMinusSrcAlpha
|
||||
: BlendingFactor.SrcAlpha,
|
||||
blend switch
|
||||
{
|
||||
TranslucencyKind.Additive => BlendingFactor.One,
|
||||
TranslucencyKind.InvAlpha => BlendingFactor.SrcAlpha,
|
||||
_ => BlendingFactor.OneMinusSrcAlpha,
|
||||
});
|
||||
}
|
||||
|
||||
private static int CompareOpaqueSubmissionOrder(InstanceGroup a, InstanceGroup b)
|
||||
{
|
||||
int cull = a.CullMode.CompareTo(b.CullMode);
|
||||
|
|
@ -2117,11 +2363,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
internal static void ApplyCacheHit(
|
||||
EntityCacheEntry entry,
|
||||
Matrix4x4 entityWorld,
|
||||
Action<GroupKey, Matrix4x4> appendInstance)
|
||||
Action<GroupKey, Matrix4x4, Vector3> appendInstance)
|
||||
{
|
||||
foreach (var cached in entry.Batches)
|
||||
{
|
||||
appendInstance(cached.Key, cached.RestPose * entityWorld);
|
||||
appendInstance(cached.Key, cached.RestPose * entityWorld, cached.LocalSortCenter);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2188,7 +2434,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
/// creates an <see cref="InstanceGroup"/> for the given key in
|
||||
/// <c>_groups</c> and appends the per-instance world matrix.
|
||||
/// </summary>
|
||||
private void AppendInstanceToGroup(GroupKey key, Matrix4x4 model)
|
||||
private void AppendInstanceToGroup(GroupKey key, Matrix4x4 model, Vector3 localSortCenter)
|
||||
{
|
||||
if (!_groups.TryGetValue(key, out var grp))
|
||||
{
|
||||
|
|
@ -2206,6 +2452,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_groups[key] = grp;
|
||||
}
|
||||
grp.Matrices.Add(model);
|
||||
grp.LocalSortCenters.Add(localSortCenter);
|
||||
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
|
||||
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
|
||||
// #188: cache-hit entities are always non-animated (the Tier-1 cache
|
||||
|
|
@ -2392,11 +2639,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
_groups[key] = grp;
|
||||
}
|
||||
grp.Matrices.Add(model);
|
||||
grp.LocalSortCenters.Add(renderData.SortCenter);
|
||||
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
|
||||
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
|
||||
grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices
|
||||
grp.SelectionLighting.Add(_currentEntitySelectionLighting);
|
||||
collector?.Add(new CachedBatch(key, texHandle, restPose));
|
||||
collector?.Add(new CachedBatch(key, texHandle, restPose, renderData.SortCenter));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2659,6 +2907,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
public float SortDistance; // squared distance from camera to first instance, for opaque sort
|
||||
public readonly List<Matrix4x4> Matrices = new();
|
||||
|
||||
// Retail CPhysicsPart::CYpt uses the transformed GfxObj sort center,
|
||||
// not the entity origin. Parallel to Matrices so delayed-alpha
|
||||
// submissions retain the exact per-part key after material grouping.
|
||||
public readonly List<Vector3> LocalSortCenters = new();
|
||||
|
||||
// Phase U.4: per-instance clip-slot index, parallel to Matrices (Slots[i]
|
||||
// is the binding=2 CellClip slot for the instance whose matrix is
|
||||
// Matrices[i]). At layout time the dispatcher writes Slots[i] into
|
||||
|
|
@ -2704,6 +2957,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
|
|||
public void ClearPerInstanceData()
|
||||
{
|
||||
Matrices.Clear();
|
||||
LocalSortCenters.Clear();
|
||||
Slots.Clear();
|
||||
LightSets.Clear();
|
||||
IndoorFlags.Clear();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue