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:
Erik 2026-07-18 08:14:05 +02:00
parent ec1bb19609
commit 6b0472ee32
14 changed files with 1083 additions and 34 deletions

View file

@ -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++)