using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering.Wb;
using AcDream.Content;
using AcDream.Core.Meshing;
using AcDream.Core.Vfx;
using DatReaderWriter;
using DatReaderWriter.DBObjs;
using DatReaderWriter.Enums;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
///
/// Instanced renderer for retail particle emitters.
///
public sealed unsafe class ParticleRenderer : IDisposable
{
private readonly record struct BatchKey(uint TextureHandle, bool UseTexture, bool Additive);
private readonly record struct ParticleDraw(BatchKey Key, ParticleInstance Instance);
private readonly record struct MeshBatchKey(uint GfxObjId, int BatchIndex);
private readonly record struct MeshParticleDraw(
MeshBatchKey Key,
ObjectRenderBatch Batch,
MeshParticleInstance Instance);
private readonly struct ParticleInstance
{
public readonly Vector3 Position;
public readonly Vector3 AxisX;
public readonly Vector3 AxisY;
public readonly uint ColorArgb;
public readonly float DistanceSq;
public ParticleInstance(Vector3 position, Vector3 axisX, Vector3 axisY, uint colorArgb, float distanceSq)
{
Position = position;
AxisX = axisX;
AxisY = axisY;
ColorArgb = colorArgb;
DistanceSq = distanceSq;
}
}
private readonly struct MeshParticleInstance
{
public readonly Matrix4x4 Model;
public readonly uint ColorArgb;
public readonly float DistanceSq;
public MeshParticleInstance(Matrix4x4 model, uint colorArgb, float distanceSq)
{
Model = model;
ColorArgb = colorArgb;
DistanceSq = distanceSq;
}
}
private readonly GL _gl;
private readonly Shader _shader;
private readonly Shader? _meshShader;
private readonly TextureCache? _textures;
private readonly DatCollection? _dats;
private readonly WbMeshAdapter? _meshAdapter;
private readonly ParticleSystem _particles;
private readonly Dictionary _particleGfxInfoByGfxObj = new();
private readonly Dictionary _geometryKindByGfxObj = new();
private readonly Dictionary _meshBlendBySurface = new();
private readonly ParticleMeshReferenceTracker? _meshReferences;
private readonly HashSet _meshLoadRequestedThisFrame = new();
private readonly int _meshTextureHandleLoc = -1;
private readonly int _meshTextureLayerLoc = -1;
private readonly uint _quadVao;
private readonly uint _quadVbo;
private readonly uint _quadEbo;
private readonly uint _instanceVbo;
private readonly uint _meshVao;
private readonly uint _meshInstanceVbo;
private float[] _instanceScratch = new float[256 * 16];
private float[] _meshInstanceScratch = new float[256 * 20];
// MP-Alloc (2026-07-05): Draw() is called up to ~11 times per frame
// (sky pre/post, scene, per-visible-cell, dynamics, unattached passes),
// each previously `new`ing a List (BuildDrawList) and a
// List (the per-batch `run` list) that became garbage
// as soon as the call returned. All Draw() calls happen sequentially on
// the render thread (verified: every call site in GameWindow.cs is a
// plain synchronous invocation from the single-threaded OnRender chain,
// none dispatched via Task.Run/Parallel) and each call fully drains its
// lists before returning, so a single pair of reused fields is safe -
// no call overlaps another's use of these buffers.
private readonly List _drawListScratch = new(64);
private readonly List _runScratch = new(64);
private readonly List _meshDrawListScratch = new(64);
private readonly List _meshRunScratch = new(64);
private readonly List _submissionScratch = new(128);
public ParticleRenderer(
GL gl,
string shadersDir,
ParticleSystem particles,
TextureCache? textures = null,
DatCollection? dats = null,
WbMeshAdapter? meshAdapter = null)
{
_gl = gl ?? throw new ArgumentNullException(nameof(gl));
_textures = textures;
_dats = dats;
_meshAdapter = meshAdapter;
_particles = particles ?? throw new ArgumentNullException(nameof(particles));
if (_meshAdapter is not null)
{
_meshReferences = new ParticleMeshReferenceTracker(
gfxObjId => _meshAdapter.IncrementRefCount(gfxObjId),
gfxObjId => _meshAdapter.DecrementRefCount(gfxObjId));
}
_shader = new Shader(_gl,
System.IO.Path.Combine(shadersDir, "particle.vert"),
System.IO.Path.Combine(shadersDir, "particle.frag"));
if (_meshAdapter?.MeshManager?.GlobalBuffer is not null)
{
_meshShader = new Shader(_gl,
System.IO.Path.Combine(shadersDir, "particle_mesh.vert"),
System.IO.Path.Combine(shadersDir, "particle_mesh.frag"));
_meshTextureHandleLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureHandle");
_meshTextureLayerLoc = _gl.GetUniformLocation(_meshShader.Program, "uTextureLayer");
}
float[] quadVerts =
{
-0.5f, -0.5f, 0f, 0f,
0.5f, -0.5f, 1f, 0f,
0.5f, 0.5f, 1f, 1f,
-0.5f, 0.5f, 0f, 1f,
};
uint[] quadIdx = { 0, 1, 2, 0, 2, 3 };
_quadVao = _gl.GenVertexArray();
_gl.BindVertexArray(_quadVao);
_quadVbo = _gl.GenBuffer();
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _quadVbo);
fixed (void* p = quadVerts)
{
_gl.BufferData(
BufferTargetARB.ArrayBuffer,
(nuint)(quadVerts.Length * sizeof(float)),
p,
BufferUsageARB.StaticDraw);
}
_gl.EnableVertexAttribArray(0);
_gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), (void*)0);
_gl.EnableVertexAttribArray(1);
_gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), (void*)(2 * sizeof(float)));
_quadEbo = _gl.GenBuffer();
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _quadEbo);
fixed (void* p = quadIdx)
{
_gl.BufferData(
BufferTargetARB.ElementArrayBuffer,
(nuint)(quadIdx.Length * sizeof(uint)),
p,
BufferUsageARB.StaticDraw);
}
_instanceVbo = _gl.GenBuffer();
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
_gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)(256 * 16 * sizeof(float)), (void*)0, BufferUsageARB.DynamicDraw);
_gl.EnableVertexAttribArray(2);
_gl.VertexAttribPointer(2, 4, VertexAttribPointerType.Float, false, 16 * sizeof(float), (void*)0);
_gl.VertexAttribDivisor(2, 1);
_gl.EnableVertexAttribArray(3);
_gl.VertexAttribPointer(3, 4, VertexAttribPointerType.Float, false, 16 * sizeof(float), (void*)(4 * sizeof(float)));
_gl.VertexAttribDivisor(3, 1);
_gl.EnableVertexAttribArray(4);
_gl.VertexAttribPointer(4, 4, VertexAttribPointerType.Float, false, 16 * sizeof(float), (void*)(8 * sizeof(float)));
_gl.VertexAttribDivisor(4, 1);
_gl.EnableVertexAttribArray(5);
_gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, 16 * sizeof(float), (void*)(12 * sizeof(float)));
_gl.VertexAttribDivisor(5, 1);
_gl.BindVertexArray(0);
_meshVao = _gl.GenVertexArray();
_meshInstanceVbo = _gl.GenBuffer();
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _meshInstanceVbo);
_gl.BufferData(
BufferTargetARB.ArrayBuffer,
(nuint)(256 * 20 * sizeof(float)),
(void*)0,
BufferUsageARB.DynamicDraw);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, 0);
_particles.EmitterDied += OnEmitterDied;
}
///
/// Starts one render frame. Wb point-of-use recovery is limited to one
/// request per missing GfxObj even though portal slicing may invoke Draw
/// many times during the frame.
///
public void BeginFrame()
=> _meshLoadRequestedThisFrame.Clear();
public void Draw(
ICamera camera,
Vector3 cameraWorldPos,
ParticleRenderPass renderPass = ParticleRenderPass.Scene,
Func? emitterFilter = null)
{
if (camera is null)
return;
Matrix4x4.Invert(camera.View, out var invView);
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)
DrawOrdered(camera);
}
private void DrawOrdered(ICamera camera)
{
ParticleSubmissionOrdering.Sort(_submissionScratch);
GlobalMeshBuffer? global = _meshAdapter?.MeshManager?.GlobalBuffer;
_gl.Enable(EnableCap.DepthTest);
_gl.Enable(EnableCap.Blend);
_gl.DepthMask(false);
_gl.ActiveTexture(TextureUnit.Texture0);
ParticleSubmissionKind? activeKind = null;
for (int i = 0; i < _submissionScratch.Count;)
{
ParticleSubmission submission = _submissionScratch[i];
if (submission.Kind == ParticleSubmissionKind.Billboard)
{
ParticleDraw draw = _drawListScratch[submission.DrawIndex];
BatchKey key = draw.Key;
if (activeKind != ParticleSubmissionKind.Billboard)
{
PrepareBillboardPipeline(camera);
activeKind = ParticleSubmissionKind.Billboard;
}
_runScratch.Clear();
do
{
_runScratch.Add(_drawListScratch[submission.DrawIndex].Instance);
i++;
if (i >= _submissionScratch.Count)
break;
submission = _submissionScratch[i];
}
while (submission.Kind == ParticleSubmissionKind.Billboard
&& _drawListScratch[submission.DrawIndex].Key == key);
_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 = _meshDrawListScratch[submission.DrawIndex];
MeshBatchKey meshKey = meshDraw.Key;
ObjectRenderBatch batch = meshDraw.Batch;
if (activeKind != ParticleSubmissionKind.Mesh)
{
PrepareMeshPipeline(camera, global);
activeKind = ParticleSubmissionKind.Mesh;
}
_meshRunScratch.Clear();
do
{
_meshRunScratch.Add(_meshDrawListScratch[submission.DrawIndex].Instance);
i++;
if (i >= _submissionScratch.Count)
break;
submission = _submissionScratch[i];
}
while (submission.Kind == ParticleSubmissionKind.Mesh
&& _meshDrawListScratch[submission.DrawIndex].Key == meshKey);
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(ICamera camera)
{
_shader.Use();
_shader.SetMatrix4("uViewProjection", camera.View * camera.Projection);
_shader.SetInt("uParticleTexture", 0);
_gl.Disable(EnableCap.CullFace);
_gl.BindVertexArray(_quadVao);
}
private void PrepareMeshPipeline(ICamera camera, GlobalMeshBuffer global)
{
_meshShader!.Use();
_meshShader.SetMatrix4("uViewProjection", camera.View * camera.Projection);
_gl.FrontFace(FrontFaceDirection.CW);
_gl.BindVertexArray(_meshVao);
// GlobalMeshBuffer may grow and replace either backing buffer. Bind
// today's buffer names on every pipeline switch instead of caching
// them in this VAO at construction time.
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, global.VBO);
int vertexStride = VertexPositionNormalTexture.Size;
_gl.EnableVertexAttribArray(0);
_gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)0);
_gl.EnableVertexAttribArray(1);
_gl.VertexAttribPointer(1, 3, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)(3 * sizeof(float)));
_gl.EnableVertexAttribArray(2);
_gl.VertexAttribPointer(2, 2, VertexAttribPointerType.Float, false, (uint)vertexStride, (void*)(6 * sizeof(float)));
_gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, global.IBO);
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _meshInstanceVbo);
const int instanceStride = 20 * sizeof(float);
for (uint column = 0; column < 4; column++)
{
uint location = 3 + column;
_gl.EnableVertexAttribArray(location);
_gl.VertexAttribPointer(
location,
4,
VertexAttribPointerType.Float,
false,
instanceStride,
(void*)(column * 4 * sizeof(float)));
_gl.VertexAttribDivisor(location, 1);
}
_gl.EnableVertexAttribArray(7);
_gl.VertexAttribPointer(7, 4, VertexAttribPointerType.Float, false, instanceStride, (void*)(16 * sizeof(float)));
_gl.VertexAttribDivisor(7, 1);
}
private void BuildDrawLists(
Vector3 cameraWorldPos,
ParticleRenderPass renderPass,
Vector3 cameraRight,
Vector3 cameraUp,
Func? emitterFilter)
{
var draws = _drawListScratch;
draws.Clear();
_meshDrawListScratch.Clear();
_submissionScratch.Clear();
int sequence = 0;
foreach (var em in _particles.EnumerateEmitters())
{
if (!em.PresentationVisible || !em.ViewEligible)
continue;
if (em.RenderPass != renderPass)
continue;
if (emitterFilter is not null && !emitterFilter(em))
continue;
for (int idx = 0; idx < em.Particles.Length; idx++)
{
ref var p = ref em.Particles[idx];
if (!p.Alive)
continue;
// `p.Position` is already in world coordinates: AttachLocal
// emitters get their AnchorPos refreshed each frame by the
// owning subsystem (sky-PES driver, animation tick, etc.) which
// 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))
{
continue;
}
var gfxInfo = ResolveParticleGfxInfo(em.Desc);
uint texture = gfxInfo.TextureHandle;
bool useTexture = texture != 0;
bool additive = gfxInfo.HasMaterial
? gfxInfo.Additive
: (em.Desc.Flags & EmitterFlags.Additive) != 0;
var key = new BatchKey(texture, useTexture, additive);
Vector3 axisX;
Vector3 axisY;
if (gfxInfo.IsBillboard)
{
pos += Vector3.UnitZ * (gfxInfo.CenterOffset.Z * p.Size);
axisX = cameraRight * (gfxInfo.Size.X * p.Size);
axisY = cameraUp * (gfxInfo.Size.Y * p.Size);
}
else
{
Quaternion orientation = ParticleOrientation(em, p);
pos += Vector3.Transform(gfxInfo.CenterOffset * p.Size, orientation);
axisX = Vector3.Transform(gfxInfo.AxisX, orientation) * (gfxInfo.Size.X * p.Size);
axisY = Vector3.Transform(gfxInfo.AxisY, orientation) * (gfxInfo.Size.Y * p.Size);
}
int drawIndex = draws.Count;
draws.Add(new ParticleDraw(key, new ParticleInstance(pos, axisX, axisY, p.ColorArgb, distSq)));
_submissionScratch.Add(new ParticleSubmission(
ParticleSubmissionKind.Billboard,
drawIndex,
distSq,
sequence++));
}
}
}
private bool TryAppendMeshDraws(
AcDream.Core.Vfx.ParticleEmitter emitter,
Particle particle,
uint gfxObjId,
float distanceSq,
ref int sequence)
{
if (_meshAdapter is null || _meshShader is null)
return true;
_meshReferences!.Register(emitter.Handle, gfxObjId);
ObjectRenderData? renderData = _meshAdapter.TryGetRenderData(gfxObjId);
if (renderData is null)
{
if (_meshLoadRequestedThisFrame.Add(gfxObjId))
_meshAdapter.EnsureLoaded(gfxObjId);
return true;
}
Quaternion orientation = ParticleOrientation(emitter, particle);
Matrix4x4 model = Matrix4x4.CreateScale(particle.Size)
* Matrix4x4.CreateFromQuaternion(orientation)
* Matrix4x4.CreateTranslation(particle.Position);
var instance = new MeshParticleInstance(model, particle.ColorArgb, distanceSq);
for (int batchIndex = 0; batchIndex < renderData.Batches.Count; batchIndex++)
{
ObjectRenderBatch batch = renderData.Batches[batchIndex];
if (batch.IndexCount <= 0 || batch.BindlessTextureHandle == 0)
continue;
int drawIndex = _meshDrawListScratch.Count;
_meshDrawListScratch.Add(new MeshParticleDraw(
new MeshBatchKey(gfxObjId, batchIndex),
batch,
instance));
_submissionScratch.Add(new ParticleSubmission(
ParticleSubmissionKind.Mesh,
drawIndex,
distanceSq,
sequence++));
}
return true;
}
private void DrawInstances(List instances)
{
if (instances.Count == 0)
return;
int needed = instances.Count * 16;
if (_instanceScratch.Length < needed)
_instanceScratch = new float[needed + 256 * 16];
for (int i = 0; i < instances.Count; i++)
{
var p = instances[i];
int o = i * 16;
_instanceScratch[o + 0] = p.Position.X;
_instanceScratch[o + 1] = p.Position.Y;
_instanceScratch[o + 2] = p.Position.Z;
_instanceScratch[o + 3] = 0f;
_instanceScratch[o + 4] = p.AxisX.X;
_instanceScratch[o + 5] = p.AxisX.Y;
_instanceScratch[o + 6] = p.AxisX.Z;
_instanceScratch[o + 7] = 0f;
_instanceScratch[o + 8] = p.AxisY.X;
_instanceScratch[o + 9] = p.AxisY.Y;
_instanceScratch[o + 10] = p.AxisY.Z;
_instanceScratch[o + 11] = 0f;
_instanceScratch[o + 12] = ((p.ColorArgb >> 16) & 0xFF) / 255f;
_instanceScratch[o + 13] = ((p.ColorArgb >> 8) & 0xFF) / 255f;
_instanceScratch[o + 14] = (p.ColorArgb & 0xFF) / 255f;
_instanceScratch[o + 15] = ((p.ColorArgb >> 24) & 0xFF) / 255f;
}
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
fixed (void* bp = _instanceScratch)
{
_gl.BufferData(
BufferTargetARB.ArrayBuffer,
(nuint)(instances.Count * 16 * sizeof(float)),
bp,
BufferUsageARB.DynamicDraw);
}
_gl.BindVertexArray(_quadVao);
_gl.DrawElementsInstanced(PrimitiveType.Triangles, 6, DrawElementsType.UnsignedInt, (void*)0, (uint)instances.Count);
}
private void UploadMeshInstances(List instances)
{
int needed = instances.Count * 20;
if (_meshInstanceScratch.Length < needed)
_meshInstanceScratch = new float[needed + 256 * 20];
for (int i = 0; i < instances.Count; i++)
{
MeshParticleInstance instance = instances[i];
Matrix4x4 model = instance.Model;
int o = i * 20;
_meshInstanceScratch[o + 0] = model.M11;
_meshInstanceScratch[o + 1] = model.M12;
_meshInstanceScratch[o + 2] = model.M13;
_meshInstanceScratch[o + 3] = model.M14;
_meshInstanceScratch[o + 4] = model.M21;
_meshInstanceScratch[o + 5] = model.M22;
_meshInstanceScratch[o + 6] = model.M23;
_meshInstanceScratch[o + 7] = model.M24;
_meshInstanceScratch[o + 8] = model.M31;
_meshInstanceScratch[o + 9] = model.M32;
_meshInstanceScratch[o + 10] = model.M33;
_meshInstanceScratch[o + 11] = model.M34;
_meshInstanceScratch[o + 12] = model.M41;
_meshInstanceScratch[o + 13] = model.M42;
_meshInstanceScratch[o + 14] = model.M43;
_meshInstanceScratch[o + 15] = model.M44;
_meshInstanceScratch[o + 16] = ((instance.ColorArgb >> 16) & 0xFF) / 255f;
_meshInstanceScratch[o + 17] = ((instance.ColorArgb >> 8) & 0xFF) / 255f;
_meshInstanceScratch[o + 18] = (instance.ColorArgb & 0xFF) / 255f;
_meshInstanceScratch[o + 19] = ((instance.ColorArgb >> 24) & 0xFF) / 255f;
}
_gl.BindBuffer(BufferTargetARB.ArrayBuffer, _meshInstanceVbo);
fixed (void* bp = _meshInstanceScratch)
{
_gl.BufferData(
BufferTargetARB.ArrayBuffer,
(nuint)(instances.Count * 20 * sizeof(float)),
bp,
BufferUsageARB.DynamicDraw);
}
}
private void ApplyMeshCullMode(CullMode mode)
{
_gl.FrontFace(FrontFaceDirection.CW);
switch (mode)
{
case CullMode.None:
_gl.Disable(EnableCap.CullFace);
break;
case CullMode.Clockwise:
_gl.Enable(EnableCap.CullFace);
_gl.CullFace(TriangleFace.Front);
break;
case CullMode.CounterClockwise:
case CullMode.Landblock:
_gl.Enable(EnableCap.CullFace);
_gl.CullFace(TriangleFace.Back);
break;
}
}
private TranslucencyKind ResolveMeshBlend(ObjectRenderBatch batch)
{
uint surfaceId = batch.Key.SurfaceId;
if (surfaceId == 0 || _dats is null)
return batch.IsAdditive ? TranslucencyKind.Additive : TranslucencyKind.AlphaBlend;
if (_meshBlendBySurface.TryGetValue(surfaceId, out TranslucencyKind blend))
return blend;
blend = RetailParticleBlendResolver.Resolve(
surfaceId,
batch.IsAdditive,
id => _dats.Get(id),
Console.Error.WriteLine);
_meshBlendBySurface[surfaceId] = blend;
return blend;
}
private RetailParticleGeometryKind ResolveGeometryKind(uint gfxObjId)
{
if (_geometryKindByGfxObj.TryGetValue(gfxObjId, out RetailParticleGeometryKind kind))
return kind;
uint? firstDegradeMode = null;
try
{
if (_dats?.Get(gfxObjId) is { } gfx
&& gfx.Flags.HasFlag(GfxObjFlags.HasDIDDegrade)
&& gfx.DIDDegrade != 0
&& _dats.Get(gfx.DIDDegrade) is { Degrades.Count: > 0 } degrade)
{
firstDegradeMode = degrade.Degrades[0].DegradeMode;
}
}
catch (Exception ex)
{
// Missing/corrupt content must not invent a billboard. Cache the
// retail full-mesh choice and let WbMeshAdapter's normal missing-
// asset diagnostics decide whether geometry can be presented.
Console.Error.WriteLine(
$"[particle-geometry] Failed to decode GfxObj 0x{gfxObjId:X8} degrade metadata: {ex.Message}");
}
kind = RetailParticleGeometryClassifier.Classify(firstDegradeMode);
_geometryKindByGfxObj[gfxObjId] = kind;
return kind;
}
private void OnEmitterDied(int handle)
=> _meshReferences?.Release(handle);
private ParticleGfxInfo ResolveParticleGfxInfo(EmitterDesc desc)
{
if (_textures is null)
return ParticleGfxInfo.Default;
if (desc.TextureSurfaceId != 0)
return ParticleGfxInfo.Billboard(
_textures.GetOrUpload(desc.TextureSurfaceId),
Vector2.One,
Vector3.Zero,
additive: (desc.Flags & EmitterFlags.Additive) != 0,
hasMaterial: false);
uint gfxObjId = desc.HwGfxObjId != 0 ? desc.HwGfxObjId : desc.GfxObjId;
if (gfxObjId == 0 || _dats is null)
return ParticleGfxInfo.Default;
if (!_particleGfxInfoByGfxObj.TryGetValue(gfxObjId, out var info))
{
info = ReadParticleGfxInfo(gfxObjId);
_particleGfxInfoByGfxObj[gfxObjId] = info;
}
return info.TextureHandle != 0 ? info : ParticleGfxInfo.Default;
}
private ParticleGfxInfo ReadParticleGfxInfo(uint gfxObjId)
{
try
{
var gfx = _dats?.Get(gfxObjId);
if (gfx is null)
return ParticleGfxInfo.Default;
uint surfaceId = gfx.Surfaces.Count > 0 ? gfx.Surfaces[0].DataId : 0u;
uint texture = surfaceId != 0 && _textures is not null ? _textures.GetOrUpload(surfaceId) : 0u;
bool additive = false;
if (surfaceId != 0)
{
var surface = _dats?.Get(surfaceId);
additive = surface is not null && surface.Type.HasFlag(SurfaceType.Additive);
}
return AuthoredParticleGfxInfo(gfx, texture, additive, surfaceId != 0);
}
catch
{
return ParticleGfxInfo.Default;
}
}
private ParticleGfxInfo AuthoredParticleGfxInfo(GfxObj gfx, uint texture, bool additive, bool hasMaterial)
{
if (gfx.VertexArray.Vertices.Count == 0)
return ParticleGfxInfo.Billboard(texture, Vector2.One, Vector3.Zero, additive, hasMaterial);
var min = new Vector3(float.PositiveInfinity);
var max = new Vector3(float.NegativeInfinity);
foreach (var (_, v) in gfx.VertexArray.Vertices)
{
min = Vector3.Min(min, v.Origin);
max = Vector3.Max(max, v.Origin);
}
var size = max - min;
var center = (min + max) * 0.5f;
if (IsPointSprite(gfx))
{
float sx = FallbackParticleExtent(size.X) * 0.9f;
float sy = FallbackParticleExtent(size.Z) * 0.9f;
return ParticleGfxInfo.Billboard(texture, new Vector2(sx, sy), center, additive, hasMaterial);
}
Vector3 axisX;
Vector3 axisY;
Vector2 planeSize;
if (size.Y > size.X && size.Y > size.Z)
{
if (size.X > size.Z)
{
axisX = Vector3.UnitX;
axisY = Vector3.UnitY;
planeSize = new Vector2(size.X, size.Y);
}
else
{
axisX = Vector3.UnitY;
axisY = Vector3.UnitZ;
planeSize = new Vector2(size.Y, size.Z);
}
}
else if (size.X > size.Y && size.X > size.Z)
{
if (size.Z > size.Y)
{
axisX = Vector3.UnitX;
axisY = Vector3.UnitZ;
planeSize = new Vector2(size.X, size.Z);
}
else
{
axisX = Vector3.UnitX;
axisY = Vector3.UnitY;
planeSize = new Vector2(size.X, size.Y);
}
}
else
{
if (size.X > size.Y)
{
axisX = Vector3.UnitX;
axisY = Vector3.UnitZ;
planeSize = new Vector2(size.X, size.Z);
}
else
{
axisX = Vector3.UnitY;
axisY = Vector3.UnitZ;
planeSize = new Vector2(size.Y, size.Z);
}
}
planeSize.X = FallbackParticleExtent(planeSize.X);
planeSize.Y = FallbackParticleExtent(planeSize.Y);
return new ParticleGfxInfo(texture, planeSize, axisX, axisY, center, false, additive, hasMaterial);
}
private bool IsPointSprite(GfxObj gfx)
{
if (!gfx.Flags.HasFlag(GfxObjFlags.HasDIDDegrade) || gfx.DIDDegrade == 0 || _dats is null)
return false;
try
{
var degrade = _dats.Get(gfx.DIDDegrade);
return degrade?.Degrades.Count > 0 && degrade.Degrades[0].DegradeMode == 2;
}
catch
{
return false;
}
}
private static float FallbackParticleExtent(float value)
=> value > 1e-4f ? Math.Clamp(value, 1e-4f, 10_000f) : 1f;
private static Quaternion ParticleOrientation(AcDream.Core.Vfx.ParticleEmitter em, Particle p)
{
Quaternion orientation = (em.Desc.Flags & EmitterFlags.AttachLocal) != 0
? em.AnchorRot
: p.SpawnRotation;
if (em.Desc.Type is AcDream.Core.Vfx.ParticleType.ParabolicLVGAGR
or AcDream.Core.Vfx.ParticleType.ParabolicLVLALR
or AcDream.Core.Vfx.ParticleType.ParabolicGVGAGR)
{
Vector3 angular = p.C * p.Age;
float radians = angular.Length();
if (radians > 1e-6f)
orientation = Quaternion.Normalize(orientation * Quaternion.CreateFromAxisAngle(angular / radians, radians));
}
return orientation;
}
public void Dispose()
{
_particles.EmitterDied -= OnEmitterDied;
_meshReferences?.Dispose();
_gl.DeleteBuffer(_quadVbo);
_gl.DeleteBuffer(_quadEbo);
_gl.DeleteBuffer(_instanceVbo);
_gl.DeleteBuffer(_meshInstanceVbo);
_gl.DeleteVertexArray(_quadVao);
_gl.DeleteVertexArray(_meshVao);
_shader.Dispose();
_meshShader?.Dispose();
}
private readonly record struct ParticleGfxInfo(
uint TextureHandle,
Vector2 Size,
Vector3 AxisX,
Vector3 AxisY,
Vector3 CenterOffset,
bool IsBillboard,
bool Additive,
bool HasMaterial)
{
public static ParticleGfxInfo Default { get; } =
Billboard(0u, Vector2.One, Vector3.Zero, additive: false, hasMaterial: false);
public static ParticleGfxInfo Billboard(
uint textureHandle,
Vector2 size,
Vector3 centerOffset,
bool additive,
bool hasMaterial) =>
new(textureHandle, size, Vector3.UnitX, Vector3.UnitY, centerOffset, true, additive, hasMaterial);
}
}