acdream/src/AcDream.App/Rendering/Wb/ParticleBatcher.cs
Erik d16d8cd4e5 feat(O-T4): extract ObjectMeshManager + mesh pipeline closure into AcDream.App.Rendering.Wb
Phase O Task 4: extract the WB mesh pipeline (ObjectMeshManager + 7 support files)
from references/WorldBuilder into src/AcDream.App/Rendering/Wb/ and bridge dat I/O
through our DatCollection via a thin DatCollectionAdapter.

O-D7 adapter path taken: ObjectMeshManager has 26 _dats.X call sites (threshold 20),
so a DatCollectionAdapter : IDatReaderWriter is introduced rather than refactoring
ObjectMeshManager's internal dat access directly.

Files added (verbatim copies, namespace-only changes):
- ObjectMeshManager.cs — mesh pipeline hub; IDatReaderWriter field satisfied by adapter
- GlobalMeshBuffer.cs — single global VAO/VBO/IBO manager
- EdgeLineBuilder.cs — wireframe edge geometry from CellStruct polygons
- ModernRenderData.cs — ModernBatchData + LandblockMdiCommand structs
- TextureAtlasManager.cs — texture array grouping by (Width, Height, Format)
- ParticleBatcher.cs — GPU particle batching; T4 interim uses BaseObjectRenderManager
  static fields from Chorizite.OpenGLSDLBackend.Lib (stays until T7)
- ParticleEmitterRenderer.cs — per-emitter particle lifecycle + rendering
- ActiveParticleEmitter.cs — wrapper holding renderer + part index + local offset
- DatCollectionAdapter.cs — NEW: bridges DatCollection → IDatReaderWriter; implements
  ResolveId() via DatDatabase.TypeFromId + Tree.TryGetFile in HighRes→Portal→Language→Cell
  order matching DefaultDatReaderWriter; DatDatabaseWrapper wraps DatDatabase as IDatDatabase

WbMeshAdapter.cs changes (T4 Step 6):
- _graphicsDevice switched from Chorizite.OpenGLSDLBackend.OpenGLGraphicsDevice to
  extracted AcDream.App.Rendering.Wb.OpenGLGraphicsDevice
- ParticleBatcher = new ParticleBatcher(_graphicsDevice) restored (T3 had null! placeholder)
- ObjectMeshManager now constructed with new DatCollectionAdapter(dats) instead of _wbDats
- _wbDats field + its construction + disposal + [indoor-upload] NULL_RESULT diagnostic block
  left intact — T7 cleanup removes these once WorldBuilder project ref is dropped

EmbeddedResourceReader.cs: replaced assembly manifest lookup (wrong prefix for our assembly)
with disk-based lookup mapping "Shaders.Particle.vert" → Rendering/Shaders/wb_particle.vert;
consistent with all other acdream shaders.

wb_particle.vert / wb_particle.frag: WB particle shaders copied verbatim with wb_ prefix
to distinguish from acdream's own particle.vert.

OpenGLGraphicsDevice.cs: ParticleBatcher property type updated to extracted ParticleBatcher;
setter changed from private to internal so WbMeshAdapter (same assembly) can assign post-ctor.

Build: green (0 errors, 0 warnings in AcDream.App).
Tests: 1147+8 baseline maintained (8 pre-existing failures unchanged).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 16:37:55 +02:00

231 lines
9.3 KiB
C#

using System;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using Chorizite.Core.Render;
using Chorizite.OpenGLSDLBackend.Lib;
using Silk.NET.OpenGL;
namespace AcDream.App.Rendering.Wb {
[StructLayout(LayoutKind.Sequential)]
public struct ParticleInstance {
public Vector3 Position;
public Vector3 ScaleOpacityActive; // x=scale, y=opacity, z=active (1.0 or 0.0)
public float TextureIndex;
public Quaternion Rotation;
public Vector2 Size;
public float IsBillboard; // 1.0 for true, 0.0 for false
}
public struct ParticleRenderData {
public ParticleInstance Instance;
public float DistanceSq;
public ManagedGLTextureArray? Atlas;
public bool IsAdditive;
}
public unsafe class ParticleBatcher : IDisposable {
private const int MAX_PARTICLES_TOTAL = 65536;
private readonly OpenGLGraphicsDevice _graphicsDevice;
private readonly uint _vao;
private readonly uint _vbo;
private readonly uint _ibo;
private readonly uint _instanceVbo;
private readonly IShader _shader;
private readonly ParticleInstance[] _instanceData = new ParticleInstance[MAX_PARTICLES_TOTAL];
private readonly List<ParticleRenderData> _allParticles = new();
private int _currentInstanceCount = 0;
private ManagedGLTextureArray? _currentAtlas;
private bool _currentIsAdditive;
private Matrix4x4 _viewProjection;
private Vector3 _cameraUp;
private Vector3 _cameraRight;
public ParticleBatcher(OpenGLGraphicsDevice graphicsDevice) {
_graphicsDevice = graphicsDevice;
var gl = _graphicsDevice.GL;
var vertSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.vert");
var fragSource = EmbeddedResourceReader.GetEmbeddedResource("Shaders.Particle.frag");
_shader = _graphicsDevice.CreateShader("Particle", vertSource, fragSource);
// Create quad vertices - centered to match ACViewer expansion logic
float[] vertices = {
// x, y, z, u, v
-0.5f, 0.0f, -0.5f, 0.0f, 1.0f,
0.5f, 0.0f, -0.5f, 1.0f, 1.0f,
0.5f, 0.0f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.0f, 0.5f, 0.0f, 0.0f
};
ushort[] indices = { 0, 1, 2, 2, 3, 0 };
_vao = gl.GenVertexArray();
gl.BindVertexArray(_vao);
_vbo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _vbo);
unsafe {
fixed (float* p = vertices) {
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(vertices.Length * sizeof(float)), p, BufferUsageARB.StaticDraw);
}
}
_ibo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, _ibo);
unsafe {
fixed (ushort* p = indices) {
gl.BufferData(BufferTargetARB.ElementArrayBuffer, (uint)(indices.Length * sizeof(ushort)), p, BufferUsageARB.StaticDraw);
}
}
// Quad attributes
gl.EnableVertexAttribArray(0);
gl.VertexAttribPointer(0, 3, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)0);
gl.EnableVertexAttribArray(1);
gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 5 * sizeof(float), (void*)(3 * sizeof(float)));
// Instance attributes
_instanceVbo = gl.GenBuffer();
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
gl.BufferData(BufferTargetARB.ArrayBuffer, (uint)(MAX_PARTICLES_TOTAL * Marshal.SizeOf<ParticleInstance>()), (void*)0, BufferUsageARB.DynamicDraw);
uint stride = (uint)Marshal.SizeOf<ParticleInstance>();
// iPosition
gl.EnableVertexAttribArray(2);
gl.VertexAttribPointer(2, 3, VertexAttribPointerType.Float, false, stride, (void*)0);
gl.VertexAttribDivisor(2, 1);
// iScaleOpacityActive
gl.EnableVertexAttribArray(3);
gl.VertexAttribPointer(3, 3, VertexAttribPointerType.Float, false, stride, (void*)(3 * sizeof(float)));
gl.VertexAttribDivisor(3, 1);
// iTextureIndex
gl.EnableVertexAttribArray(4);
gl.VertexAttribPointer(4, 1, VertexAttribPointerType.Float, false, stride, (void*)(6 * sizeof(float)));
gl.VertexAttribDivisor(4, 1);
// iRotation (Quaternion)
gl.EnableVertexAttribArray(5);
gl.VertexAttribPointer(5, 4, VertexAttribPointerType.Float, false, stride, (void*)(7 * sizeof(float)));
gl.VertexAttribDivisor(5, 1);
// iSize
gl.EnableVertexAttribArray(6);
gl.VertexAttribPointer(6, 2, VertexAttribPointerType.Float, false, stride, (void*)(11 * sizeof(float)));
gl.VertexAttribDivisor(6, 1);
// iIsBillboard
gl.EnableVertexAttribArray(7);
gl.VertexAttribPointer(7, 1, VertexAttribPointerType.Float, false, stride, (void*)(13 * sizeof(float)));
gl.VertexAttribDivisor(7, 1);
gl.BindVertexArray(0);
_shader.Bind();
_shader.SetUniform("uTextureArray", 0);
_shader.Unbind();
}
public void Begin(Matrix4x4 viewProjection, Vector3 cameraUp, Vector3 cameraRight) {
_viewProjection = viewProjection;
_cameraUp = cameraUp;
_cameraRight = cameraRight;
_allParticles.Clear();
}
public void AddParticle(ManagedGLTextureArray? atlas, bool isAdditive, ParticleInstance instance, float distanceSq) {
_allParticles.Add(new ParticleRenderData {
Instance = instance,
DistanceSq = distanceSq,
Atlas = atlas,
IsAdditive = isAdditive
});
}
public void Flush() {
if (_allParticles.Count == 0) return;
// Sort back-to-front
_allParticles.Sort((a, b) => b.DistanceSq.CompareTo(a.DistanceSq));
var gl = _graphicsDevice.GL;
gl.BindVertexArray(_vao);
gl.DepthMask(false);
gl.Enable(EnableCap.DepthTest);
gl.Disable(EnableCap.StencilTest);
gl.Disable(EnableCap.CullFace);
gl.Disable(EnableCap.SampleAlphaToCoverage);
gl.Disable(EnableCap.SampleAlphaToOne);
gl.Enable(EnableCap.Blend);
int i = 0;
while (i < _allParticles.Count) {
var p = _allParticles[i];
_currentAtlas = p.Atlas;
_currentIsAdditive = p.IsAdditive;
_currentInstanceCount = 0;
while (i < _allParticles.Count && _allParticles[i].Atlas == _currentAtlas && _allParticles[i].IsAdditive == _currentIsAdditive) {
_instanceData[_currentInstanceCount++] = _allParticles[i].Instance;
i++;
if (_currentInstanceCount >= MAX_PARTICLES_TOTAL) break;
}
if (_currentInstanceCount > 0 && _currentAtlas != null) {
if (_currentIsAdditive) {
gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One);
}
else {
gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
}
gl.ActiveTexture(TextureUnit.Texture0);
gl.BindTexture(GLEnum.Texture2DArray, (uint)_currentAtlas.NativePtr);
// T4 interim: BaseObjectRenderManager state fields stay on the WB type until T7
// when the WorldBuilder project reference is dropped entirely.
BaseObjectRenderManager.CurrentAtlas = (uint)_currentAtlas.Slot;
gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo);
unsafe {
fixed (ParticleInstance* pData = _instanceData) {
gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, (uint)(_currentInstanceCount * Marshal.SizeOf<ParticleInstance>()), pData);
}
}
_shader.Bind();
_shader.SetUniform("uViewProjection", _viewProjection);
_shader.SetUniform("uCameraUp", _cameraUp);
_shader.SetUniform("uCameraRight", _cameraRight);
gl.DrawElementsInstanced(PrimitiveType.Triangles, 6, DrawElementsType.UnsignedShort, (void*)0, (uint)_currentInstanceCount);
}
}
gl.DepthMask(true);
_allParticles.Clear();
// T4 interim: BaseObjectRenderManager state fields stay on the WB type until T7
BaseObjectRenderManager.CurrentVAO = 0;
BaseObjectRenderManager.CurrentIBO = 0;
}
public void End() {
Flush();
}
public void Dispose() {
var gl = _graphicsDevice.GL;
gl.DeleteVertexArray(_vao);
gl.DeleteBuffer(_vbo);
gl.DeleteBuffer(_instanceVbo);
gl.DeleteBuffer(_ibo);
(_shader as IDisposable)?.Dispose();
}
}
}