using System; using System.Collections.Generic; using System.Numerics; using AcDream.Core.Meshing; using AcDream.Core.Terrain; using AcDream.Core.World; using Chorizite.OpenGLSDLBackend.Lib; using Silk.NET.OpenGL; namespace AcDream.App.Rendering.Wb; /// /// Draws entities using WB's (VAO/VBO per GfxObj, /// per-batch IBO) with acdream's for texture resolution /// and for translucency classification. /// /// /// Atlas-tier entities (ServerGuid == 0): mesh data comes from WB's /// via . /// Textures resolve through using the batch's /// SurfaceId. /// /// /// /// Per-instance-tier entities (ServerGuid != 0): mesh data also from /// WB, but textures resolve through with palette and /// surface overrides applied. Part overrides and hidden-parts from /// control which GfxObj renders per part. /// /// /// /// GL strategy: per-entity single-instance drawing. Each draw call uploads /// one model matrix to the instance VBO, binds WB's VAO (with instance attribute /// slots patched on first use), binds the batch's IBO, and calls DrawElements with /// instance count 1. True instancing grouping deferred to N.6. /// /// /// /// Shader: reuses mesh_instanced (vert locations 0-2 = Position/ /// Normal/UV from WB's VertexPositionNormalTexture; locations 3-6 = instance /// matrix from our VBO). WB's 32-byte vertex stride is compatible. /// /// public sealed unsafe class WbDrawDispatcher : IDisposable { private readonly GL _gl; private readonly Shader _shader; private readonly TextureCache _textures; private readonly WbMeshAdapter _meshAdapter; private readonly EntitySpawnAdapter _entitySpawnAdapter; private readonly uint _instanceVbo; private readonly float[] _matrixBuf = new float[16]; private readonly HashSet _patchedVaos = new(); private bool _disposed; public WbDrawDispatcher( GL gl, Shader shader, TextureCache textures, WbMeshAdapter meshAdapter, EntitySpawnAdapter entitySpawnAdapter) { ArgumentNullException.ThrowIfNull(gl); ArgumentNullException.ThrowIfNull(shader); ArgumentNullException.ThrowIfNull(textures); ArgumentNullException.ThrowIfNull(meshAdapter); ArgumentNullException.ThrowIfNull(entitySpawnAdapter); _gl = gl; _shader = shader; _textures = textures; _meshAdapter = meshAdapter; _entitySpawnAdapter = entitySpawnAdapter; _instanceVbo = _gl.GenBuffer(); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo); _gl.BufferData(BufferTargetARB.ArrayBuffer, 64, null, BufferUsageARB.DynamicDraw); } public static Matrix4x4 ComposePartWorldMatrix( Matrix4x4 entityWorld, Matrix4x4 animOverride, Matrix4x4 restPose) => restPose * animOverride * entityWorld; public void Draw( ICamera camera, IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax, IReadOnlyList Entities)> landblockEntries, FrustumPlanes? frustum = null, uint? neverCullLandblockId = null, HashSet? visibleCellIds = null, HashSet? animatedEntityIds = null) { _shader.Use(); var vp = camera.View * camera.Projection; _shader.SetMatrix4("uViewProjection", vp); var metaTable = _meshAdapter.MetadataTable; // Collect visible entities into opaque and translucent lists for two-pass rendering. // We walk entities once and classify each (entity, meshRef, batch) triple. var opaqueDraws = new List(); var translucentDraws = new List(); foreach (var entry in landblockEntries) { bool landblockVisible = frustum is null || entry.LandblockId == neverCullLandblockId || FrustumCuller.IsAabbVisible(frustum.Value, entry.AabbMin, entry.AabbMax); if (!landblockVisible && (animatedEntityIds is null || animatedEntityIds.Count == 0)) continue; foreach (var entity in entry.Entities) { if (entity.MeshRefs.Count == 0) continue; bool isAnimated = animatedEntityIds?.Contains(entity.Id) == true; if (!landblockVisible && !isAnimated) continue; if (entity.ParentCellId.HasValue && visibleCellIds is not null && !visibleCellIds.Contains(entity.ParentCellId.Value)) continue; var entityWorld = Matrix4x4.CreateFromQuaternion(entity.Rotation) * Matrix4x4.CreateTranslation(entity.Position); bool isPerInstance = entity.ServerGuid != 0; AnimatedEntityState? animState = isPerInstance ? _entitySpawnAdapter.GetState(entity.ServerGuid) : null; for (int partIdx = 0; partIdx < entity.MeshRefs.Count; partIdx++) { if (animState is not null && animState.IsPartHidden(partIdx)) continue; var meshRef = entity.MeshRefs[partIdx]; ulong gfxObjId = meshRef.GfxObjId; if (animState is not null) gfxObjId = animState.ResolvePartGfxObj(partIdx, gfxObjId); var renderData = _meshAdapter.TryGetRenderData(gfxObjId); if (renderData is null) continue; // For Setup objects, WB stores sub-parts in SetupParts. For // single GfxObjs, SetupParts is empty and the render data // itself contains the batches. if (renderData.IsSetup && renderData.SetupParts.Count > 0) { foreach (var (partGfxObjId, partTransform) in renderData.SetupParts) { var partData = _meshAdapter.TryGetRenderData(partGfxObjId); if (partData is null) continue; var model = ComposePartWorldMatrix( entityWorld, meshRef.PartTransform, partTransform); ClassifyBatches(partData, partGfxObjId, model, entity, meshRef, metaTable, opaqueDraws, translucentDraws); } } else { var model = meshRef.PartTransform * entityWorld; ClassifyBatches(renderData, gfxObjId, model, entity, meshRef, metaTable, opaqueDraws, translucentDraws); } } } } // ── Pass 1: Opaque + ClipMap ───────────────────────────────────────── if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_NO_CULL"), "1", StringComparison.Ordinal)) _gl.Disable(EnableCap.CullFace); foreach (var item in opaqueDraws) { _shader.SetInt("uTranslucencyKind", (int)item.Translucency); UploadMatrixAndDraw(item); } // ── Pass 2: Translucent ────────────────────────────────────────────── _gl.Enable(EnableCap.Blend); _gl.DepthMask(false); if (string.Equals(Environment.GetEnvironmentVariable("ACDREAM_NO_CULL"), "1", StringComparison.Ordinal)) { _gl.Disable(EnableCap.CullFace); } else { _gl.Enable(EnableCap.CullFace); _gl.CullFace(TriangleFace.Back); _gl.FrontFace(FrontFaceDirection.Ccw); } foreach (var item in translucentDraws) { switch (item.Translucency) { case TranslucencyKind.Additive: _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.One); break; case TranslucencyKind.InvAlpha: _gl.BlendFunc(BlendingFactor.OneMinusSrcAlpha, BlendingFactor.SrcAlpha); break; default: _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha); break; } _shader.SetInt("uTranslucencyKind", (int)item.Translucency); UploadMatrixAndDraw(item); } _gl.DepthMask(true); _gl.Disable(EnableCap.Blend); _gl.Disable(EnableCap.CullFace); _gl.BindVertexArray(0); } private void ClassifyBatches( ObjectRenderData renderData, ulong gfxObjId, Matrix4x4 model, WorldEntity entity, MeshRef meshRef, AcSurfaceMetadataTable metaTable, List opaqueDraws, List translucentDraws) { for (int batchIdx = 0; batchIdx < renderData.Batches.Count; batchIdx++) { var batch = renderData.Batches[batchIdx]; TranslucencyKind translucency; if (metaTable.TryLookup(gfxObjId, batchIdx, out var meta)) { translucency = meta.Translucency; } else { // Fallback: derive from WB batch flags. translucency = batch.IsAdditive ? TranslucencyKind.Additive : batch.IsTransparent ? TranslucencyKind.AlphaBlend : TranslucencyKind.Opaque; } uint texHandle = ResolveTexture(entity, meshRef, batch); if (texHandle == 0) continue; var item = new DrawItem { Vao = renderData.VAO, Ibo = batch.IBO, IndexCount = batch.IndexCount, Model = model, TextureHandle = texHandle, Translucency = translucency, }; if (translucency == TranslucencyKind.Opaque || translucency == TranslucencyKind.ClipMap) opaqueDraws.Add(item); else translucentDraws.Add(item); } } private uint ResolveTexture(WorldEntity entity, MeshRef meshRef, ObjectRenderBatch batch) { uint surfaceId = batch.SurfaceId; if (surfaceId == 0) return 0; uint overrideOrigTex = 0; bool hasOrigTexOverride = meshRef.SurfaceOverrides is not null && meshRef.SurfaceOverrides.TryGetValue(surfaceId, out overrideOrigTex); uint? origTexOverride = hasOrigTexOverride ? overrideOrigTex : (uint?)null; if (entity.PaletteOverride is not null) { return _textures.GetOrUploadWithPaletteOverride( surfaceId, origTexOverride, entity.PaletteOverride); } else if (hasOrigTexOverride) { return _textures.GetOrUploadWithOrigTextureOverride(surfaceId, overrideOrigTex); } else { return _textures.GetOrUpload(surfaceId); } } private void EnsureInstanceAttribs(uint vao) { if (!_patchedVaos.Add(vao)) return; _gl.BindVertexArray(vao); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo); for (uint row = 0; row < 4; row++) { uint loc = 3 + row; _gl.EnableVertexAttribArray(loc); _gl.VertexAttribPointer(loc, 4, VertexAttribPointerType.Float, false, 64, (void*)(row * 16)); _gl.VertexAttribDivisor(loc, 1); } } private void UploadMatrixAndDraw(in DrawItem item) { WriteMatrix(_matrixBuf, 0, item.Model); _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo); fixed (float* p = _matrixBuf) _gl.BufferSubData(BufferTargetARB.ArrayBuffer, 0, 64, p); EnsureInstanceAttribs(item.Vao); _gl.BindVertexArray(item.Vao); // Re-point instance attributes to offset 0 (single matrix). _gl.BindBuffer(BufferTargetARB.ArrayBuffer, _instanceVbo); for (uint row = 0; row < 4; row++) _gl.VertexAttribPointer(3 + row, 4, VertexAttribPointerType.Float, false, 64, (void*)(row * 16)); _gl.ActiveTexture(TextureUnit.Texture0); _gl.BindTexture(TextureTarget.Texture2D, item.TextureHandle); _gl.BindBuffer(BufferTargetARB.ElementArrayBuffer, item.Ibo); _gl.DrawElementsInstanced(PrimitiveType.Triangles, (uint)item.IndexCount, DrawElementsType.UnsignedShort, (void*)0, 1); } private static void WriteMatrix(float[] buf, int offset, in Matrix4x4 m) { buf[offset + 0] = m.M11; buf[offset + 1] = m.M12; buf[offset + 2] = m.M13; buf[offset + 3] = m.M14; buf[offset + 4] = m.M21; buf[offset + 5] = m.M22; buf[offset + 6] = m.M23; buf[offset + 7] = m.M24; buf[offset + 8] = m.M31; buf[offset + 9] = m.M32; buf[offset + 10] = m.M33; buf[offset + 11] = m.M34; buf[offset + 12] = m.M41; buf[offset + 13] = m.M42; buf[offset + 14] = m.M43; buf[offset + 15] = m.M44; } public void Dispose() { if (_disposed) return; _disposed = true; _gl.DeleteBuffer(_instanceVbo); } private struct DrawItem { public uint Vao; public uint Ibo; public int IndexCount; public Matrix4x4 Model; public uint TextureHandle; public TranslucencyKind Translucency; } }