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>
231 lines
9.3 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|