End of Phase O extraction. Final cleanup: - Dropped <ProjectReference> entries to WorldBuilder.Shared and Chorizite.OpenGLSDLBackend from both AcDream.App.csproj and AcDream.Core.csproj. - Added Chorizite.Core NuGet PackageReference to AcDream.Core.csproj (needed by Core.Rendering.Wb.TextureHelpers for TextureFormat enum; previously transitive through the WB project ref). - Added BCnEncoder.Net.ImageSharp (1.1.2) + SixLabors.ImageSharp (3.1.12) as direct PackageReferences to AcDream.App.csproj — previously transitive via Chorizite.OpenGLSDLBackend project; used directly by ObjectMeshManager. Item A (BaseObjectRenderManager static fields): - Inlined CurrentAtlas/CurrentVAO/CurrentIBO into a new RenderStateCache.cs static class (AcDream.App.Rendering.Wb namespace) — the 4 consumers (ManagedGLIndexBuffer, ManagedGLTexture, ManagedGLTextureArray, ParticleBatcher) all reference RenderStateCache.* instead of BaseObjectRenderManager.*. - Dropped using Chorizite.OpenGLSDLBackend.Lib from all 4 consumers and from WbDrawDispatcher (which had it only as a dead import). Item B (ActiveParticleEmitter.ObjectLandblock): - ObjectLandblock? erased to object?; WorldBuilder.Shared.Models.ObjectId? erased to ulong? — both fields are stored but never read by any consumer in our codebase. - Dropped both WB using directives from ActiveParticleEmitter.cs. Item C (IDatReaderWriter / IDatDatabase): - Verbatim copy of both interfaces into IDatReaderWriter.cs in AcDream.App.Rendering.Wb namespace — DatCollectionAdapter and ObjectMeshManager already live in that namespace, so no using changes needed. - Dropped using WorldBuilder.Shared.Services from DatCollectionAdapter.cs and ObjectMeshManager.cs. Additional extractions required by the reference drop: - GeometryUtils.cs: verbatim copy of WorldBuilder.Shared.Lib.GeometryUtils (float-precision overloads only; Vector3d double-precision overloads omitted — ObjectMeshManager uses only the float versions). - Dropped using WorldBuilder.Shared.Lib from ObjectMeshManager.cs. WbMeshAdapter.cs cleanup (spec O-D12): - Deleted _wbDats (DefaultDatReaderWriter) field + ctor init + Dispose call. - Deleted the [indoor-upload] NULL_RESULT diagnostic block (lines ~205-262) — its Phase 2 cell-resolution investigation is complete; its _wbDats.ResolveId dependency goes with this commit. - Deleted _pendingEnvCellRequests field + isPendingEnvCell tracking in Tick(). - Simplified Tick() to a clean drain loop. Deleted SplitFormulaDivergenceTest.cs — one-time N.5b data-collection sweep; job done. Verified acceptance criteria: - Zero <ProjectReference> to WorldBuilder.* / Chorizite.OpenGLSDLBackend.* in any csproj. - Zero 'using WorldBuilder.*' / 'using Chorizite.OpenGLSDLBackend.*' in src/. - DefaultDatReaderWriter referenced in zero places in src/ (comments only). Build green (0 warnings, 0 errors). Tests: 1154 total (-1 from deleted SplitFormulaDivergenceTest), 1146 pass, 8 pre-existing failures (unchanged from baseline — physics/input tests unrelated to this change). Spec: docs/superpowers/specs/2026-05-21-phase-o-dat-path-unification-design.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
227 lines
9 KiB
C#
227 lines
9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using System.Runtime.InteropServices;
|
|
using Chorizite.Core.Render;
|
|
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);
|
|
RenderStateCache.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();
|
|
|
|
RenderStateCache.CurrentVAO = 0;
|
|
RenderStateCache.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();
|
|
}
|
|
}
|
|
}
|