phase(N.4) Tasks 22+23: WbDrawDispatcher + surface metadata side-table
WbDrawDispatcher draws all entities through WB's ObjectRenderData (VAO/VBO per GfxObj, per-batch IBO) using acdream's TextureCache for texture resolution. Two-pass rendering (opaque+ClipMap, then translucent) matching the existing InstancedMeshRenderer pattern. Per-entity single-instance drawing for N.4 simplicity — true instancing grouping deferred to N.6. Atlas-tier entities: mesh from WB, texture from TextureCache via batch SurfaceId. Per-instance-tier entities: AnimatedEntityState drives part overrides + hidden-parts, palette/surface overrides resolve through TextureCache's composite-key caches. Side-table population (Task 23 folded in): WbMeshAdapter now takes DatCollection and populates AcSurfaceMetadataTable on first IncrementRefCount per GfxObj. The side-table provides TranslucencyKind (critical for ClipMap alpha-test on vegetation) plus Luminosity, Diffuse, SurfOpacity, NeedsUvRepeat, DisableFog for sky-pass and lighting. GameWindow wiring: when WbFoundationFlag is enabled, WbDrawDispatcher draws everything and InstancedMeshRenderer is skipped. Flag-off path is unchanged. Matrix composition: restPose * animOverride * entityWorld, matching the spec. Three MatrixCompositionTests verify the contract. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5b4fd4b61d
commit
01cff4144f
5 changed files with 507 additions and 8 deletions
|
|
@ -31,6 +31,8 @@ public sealed class GameWindow : IDisposable
|
||||||
/// <summary>Phase N.4: WB-backed rendering pipeline adapter. Non-null only
|
/// <summary>Phase N.4: WB-backed rendering pipeline adapter. Non-null only
|
||||||
/// when <c>ACDREAM_USE_WB_FOUNDATION=1</c> is set; null otherwise.</summary>
|
/// when <c>ACDREAM_USE_WB_FOUNDATION=1</c> is set; null otherwise.</summary>
|
||||||
private AcDream.App.Rendering.Wb.WbMeshAdapter? _wbMeshAdapter;
|
private AcDream.App.Rendering.Wb.WbMeshAdapter? _wbMeshAdapter;
|
||||||
|
private AcDream.App.Rendering.Wb.EntitySpawnAdapter? _wbEntitySpawnAdapter;
|
||||||
|
private AcDream.App.Rendering.Wb.WbDrawDispatcher? _wbDrawDispatcher;
|
||||||
private SamplerCache? _samplerCache;
|
private SamplerCache? _samplerCache;
|
||||||
private DebugLineRenderer? _debugLines;
|
private DebugLineRenderer? _debugLines;
|
||||||
// K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder
|
// K-fix4 (2026-04-26): default OFF. The orange BSP / green cylinder
|
||||||
|
|
@ -1434,7 +1436,7 @@ public sealed class GameWindow : IDisposable
|
||||||
if (AcDream.App.Rendering.Wb.WbFoundationFlag.IsEnabled)
|
if (AcDream.App.Rendering.Wb.WbFoundationFlag.IsEnabled)
|
||||||
{
|
{
|
||||||
var wbLogger = Microsoft.Extensions.Logging.Abstractions.NullLogger<AcDream.App.Rendering.Wb.WbMeshAdapter>.Instance;
|
var wbLogger = Microsoft.Extensions.Logging.Abstractions.NullLogger<AcDream.App.Rendering.Wb.WbMeshAdapter>.Instance;
|
||||||
_wbMeshAdapter = new AcDream.App.Rendering.Wb.WbMeshAdapter(_gl, _datDir, wbLogger);
|
_wbMeshAdapter = new AcDream.App.Rendering.Wb.WbMeshAdapter(_gl, _datDir, _dats, wbLogger);
|
||||||
Console.WriteLine("[N.4] WbFoundation flag is ENABLED — routing static content through ObjectMeshManager.");
|
Console.WriteLine("[N.4] WbFoundation flag is ENABLED — routing static content through ObjectMeshManager.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1486,12 +1488,20 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
wbEntitySpawnAdapter = new AcDream.App.Rendering.Wb.EntitySpawnAdapter(
|
wbEntitySpawnAdapter = new AcDream.App.Rendering.Wb.EntitySpawnAdapter(
|
||||||
_textureCache, SequencerFactory);
|
_textureCache, SequencerFactory);
|
||||||
|
_wbEntitySpawnAdapter = wbEntitySpawnAdapter;
|
||||||
}
|
}
|
||||||
_worldState = new AcDream.App.Streaming.GpuWorldState(wbSpawnAdapter, wbEntitySpawnAdapter);
|
_worldState = new AcDream.App.Streaming.GpuWorldState(wbSpawnAdapter, wbEntitySpawnAdapter);
|
||||||
}
|
}
|
||||||
|
|
||||||
_staticMesh = new InstancedMeshRenderer(_gl, _meshShader, _textureCache, _wbMeshAdapter);
|
_staticMesh = new InstancedMeshRenderer(_gl, _meshShader, _textureCache, _wbMeshAdapter);
|
||||||
|
|
||||||
|
if (AcDream.App.Rendering.Wb.WbFoundationFlag.IsEnabled
|
||||||
|
&& _wbMeshAdapter is not null && _wbEntitySpawnAdapter is not null)
|
||||||
|
{
|
||||||
|
_wbDrawDispatcher = new AcDream.App.Rendering.Wb.WbDrawDispatcher(
|
||||||
|
_gl, _meshShader, _textureCache, _wbMeshAdapter, _wbEntitySpawnAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
// Phase G.1 sky renderer — its own shader (sky.vert / sky.frag)
|
||||||
// with depth writes off + far plane 1e6 so celestial meshes
|
// with depth writes off + far plane 1e6 so celestial meshes
|
||||||
// never clip. Shares the TextureCache with the static pipeline.
|
// never clip. Shares the TextureCache with the static pipeline.
|
||||||
|
|
@ -6326,10 +6336,20 @@ public sealed class GameWindow : IDisposable
|
||||||
animatedIds.Add(k);
|
animatedIds.Add(k);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_wbDrawDispatcher is not null)
|
||||||
|
{
|
||||||
|
_wbDrawDispatcher.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||||
|
neverCullLandblockId: playerLb,
|
||||||
|
visibleCellIds: visibility?.VisibleCellIds,
|
||||||
|
animatedEntityIds: animatedIds);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
_staticMesh?.Draw(camera, _worldState.LandblockEntries, frustum,
|
_staticMesh?.Draw(camera, _worldState.LandblockEntries, frustum,
|
||||||
neverCullLandblockId: playerLb,
|
neverCullLandblockId: playerLb,
|
||||||
visibleCellIds: visibility?.VisibleCellIds,
|
visibleCellIds: visibility?.VisibleCellIds,
|
||||||
animatedEntityIds: animatedIds);
|
animatedEntityIds: animatedIds);
|
||||||
|
}
|
||||||
|
|
||||||
// Phase G.1 / E.3: draw all live particles after opaque
|
// Phase G.1 / E.3: draw all live particles after opaque
|
||||||
// scene geometry so alpha blending composites correctly.
|
// scene geometry so alpha blending composites correctly.
|
||||||
|
|
@ -8710,6 +8730,7 @@ public sealed class GameWindow : IDisposable
|
||||||
_combatChatTranslator?.Dispose();
|
_combatChatTranslator?.Dispose();
|
||||||
_liveSession?.Dispose();
|
_liveSession?.Dispose();
|
||||||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||||
|
_wbDrawDispatcher?.Dispose();
|
||||||
_staticMesh?.Dispose();
|
_staticMesh?.Dispose();
|
||||||
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
_skyRenderer?.Dispose(); // depends on sampler cache; dispose first
|
||||||
_samplerCache?.Dispose();
|
_samplerCache?.Dispose();
|
||||||
|
|
|
||||||
364
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
Normal file
364
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Draws entities using WB's <see cref="ObjectRenderData"/> (VAO/VBO per GfxObj,
|
||||||
|
/// per-batch IBO) with acdream's <see cref="TextureCache"/> for texture resolution
|
||||||
|
/// and <see cref="AcSurfaceMetadataTable"/> for translucency classification.
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// <b>Atlas-tier</b> entities (<c>ServerGuid == 0</c>): mesh data comes from WB's
|
||||||
|
/// <see cref="ObjectMeshManager"/> via <see cref="WbMeshAdapter.TryGetRenderData"/>.
|
||||||
|
/// Textures resolve through <see cref="TextureCache.GetOrUpload"/> using the batch's
|
||||||
|
/// <c>SurfaceId</c>.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// <b>Per-instance-tier</b> entities (<c>ServerGuid != 0</c>): mesh data also from
|
||||||
|
/// WB, but textures resolve through <see cref="TextureCache"/> with palette and
|
||||||
|
/// surface overrides applied. Part overrides and hidden-parts from
|
||||||
|
/// <see cref="AnimatedEntityState"/> control which GfxObj renders per part.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// <b>GL strategy:</b> 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.
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para>
|
||||||
|
/// <b>Shader:</b> reuses <c>mesh_instanced</c> (vert locations 0-2 = Position/
|
||||||
|
/// Normal/UV from WB's <c>VertexPositionNormalTexture</c>; locations 3-6 = instance
|
||||||
|
/// matrix from our VBO). WB's 32-byte vertex stride is compatible.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
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<uint> _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<WorldEntity> Entities)> landblockEntries,
|
||||||
|
FrustumPlanes? frustum = null,
|
||||||
|
uint? neverCullLandblockId = null,
|
||||||
|
HashSet<uint>? visibleCellIds = null,
|
||||||
|
HashSet<uint>? 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<DrawItem>();
|
||||||
|
var translucentDraws = new List<DrawItem>();
|
||||||
|
|
||||||
|
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<DrawItem> opaqueDraws,
|
||||||
|
List<DrawItem> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using AcDream.Core.Meshing;
|
||||||
using Chorizite.OpenGLSDLBackend;
|
using Chorizite.OpenGLSDLBackend;
|
||||||
using Chorizite.OpenGLSDLBackend.Lib;
|
using Chorizite.OpenGLSDLBackend.Lib;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using Silk.NET.OpenGL;
|
using Silk.NET.OpenGL;
|
||||||
|
|
@ -26,6 +30,9 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
private readonly OpenGLGraphicsDevice? _graphicsDevice;
|
private readonly OpenGLGraphicsDevice? _graphicsDevice;
|
||||||
private readonly DefaultDatReaderWriter? _wbDats;
|
private readonly DefaultDatReaderWriter? _wbDats;
|
||||||
private readonly ObjectMeshManager? _meshManager;
|
private readonly ObjectMeshManager? _meshManager;
|
||||||
|
private readonly DatCollection? _dats;
|
||||||
|
private readonly AcSurfaceMetadataTable _metadataTable = new();
|
||||||
|
private readonly HashSet<ulong> _metadataPopulated = new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// True when this instance was created via <see cref="CreateUninitialized"/>;
|
/// True when this instance was created via <see cref="CreateUninitialized"/>;
|
||||||
|
|
@ -43,14 +50,19 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
/// thread (construction runs GL queries; call from OnLoad).</param>
|
/// thread (construction runs GL queries; call from OnLoad).</param>
|
||||||
/// <param name="datDir">Path to the dat directory (same as the one supplied
|
/// <param name="datDir">Path to the dat directory (same as the one supplied
|
||||||
/// to our DatCollection). DefaultDatReaderWriter opens its own file handles.</param>
|
/// to our DatCollection). DefaultDatReaderWriter opens its own file handles.</param>
|
||||||
|
/// <param name="dats">acdream's DatCollection, used to populate the surface
|
||||||
|
/// metadata side-table via <c>GfxObjMesh.Build</c>. Shares file handles with
|
||||||
|
/// the rest of the client; read-only access from the render thread.</param>
|
||||||
/// <param name="logger">Logger for the adapter; ObjectMeshManager uses
|
/// <param name="logger">Logger for the adapter; ObjectMeshManager uses
|
||||||
/// NullLogger internally.</param>
|
/// NullLogger internally.</param>
|
||||||
public WbMeshAdapter(GL gl, string datDir, ILogger<WbMeshAdapter> logger)
|
public WbMeshAdapter(GL gl, string datDir, DatCollection dats, ILogger<WbMeshAdapter> logger)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(gl);
|
ArgumentNullException.ThrowIfNull(gl);
|
||||||
ArgumentNullException.ThrowIfNull(datDir);
|
ArgumentNullException.ThrowIfNull(datDir);
|
||||||
|
ArgumentNullException.ThrowIfNull(dats);
|
||||||
ArgumentNullException.ThrowIfNull(logger);
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
|
||||||
|
_dats = dats;
|
||||||
_graphicsDevice = new OpenGLGraphicsDevice(gl, logger, new DebugRenderSettings());
|
_graphicsDevice = new OpenGLGraphicsDevice(gl, logger, new DebugRenderSettings());
|
||||||
_wbDats = new DefaultDatReaderWriter(datDir);
|
_wbDats = new DefaultDatReaderWriter(datDir);
|
||||||
_meshManager = new ObjectMeshManager(
|
_meshManager = new ObjectMeshManager(
|
||||||
|
|
@ -70,9 +82,18 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
/// underlying mesh manager. Public methods are all no-ops.</summary>
|
/// underlying mesh manager. Public methods are all no-ops.</summary>
|
||||||
public static WbMeshAdapter CreateUninitialized() => new();
|
public static WbMeshAdapter CreateUninitialized() => new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The surface metadata side-table populated on each first
|
||||||
|
/// <see cref="IncrementRefCount"/>. Queried by the draw dispatcher
|
||||||
|
/// to determine translucency, luminosity, and fog behavior per batch.
|
||||||
|
/// </summary>
|
||||||
|
public AcSurfaceMetadataTable MetadataTable => _metadataTable;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the WB render data for <paramref name="id"/>, or null if not
|
/// Returns the WB render data for <paramref name="id"/>, or null if not
|
||||||
/// yet uploaded or if this adapter is uninitialized.
|
/// yet uploaded or if this adapter is uninitialized. Increments WB's
|
||||||
|
/// internal usage counter — use <see cref="TryGetRenderData"/> for
|
||||||
|
/// render-loop lookups that should not affect lifecycle.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ObjectRenderData? GetRenderData(ulong id)
|
public ObjectRenderData? GetRenderData(ulong id)
|
||||||
{
|
{
|
||||||
|
|
@ -80,11 +101,25 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
return _meshManager.GetRenderData(id);
|
return _meshManager.GetRenderData(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the WB render data for <paramref name="id"/> without
|
||||||
|
/// modifying reference counts. Returns null if the mesh is not yet
|
||||||
|
/// uploaded. Safe for render-loop lookups.
|
||||||
|
/// </summary>
|
||||||
|
public ObjectRenderData? TryGetRenderData(ulong id)
|
||||||
|
{
|
||||||
|
if (_isUninitialized || _meshManager is null) return null;
|
||||||
|
return _meshManager.TryGetRenderData(id);
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void IncrementRefCount(ulong id)
|
public void IncrementRefCount(ulong id)
|
||||||
{
|
{
|
||||||
if (_isUninitialized || _meshManager is null) return;
|
if (_isUninitialized || _meshManager is null) return;
|
||||||
_meshManager.IncrementRefCount(id);
|
_meshManager.IncrementRefCount(id);
|
||||||
|
|
||||||
|
if (_metadataPopulated.Add(id))
|
||||||
|
PopulateMetadata(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
|
|
@ -126,6 +161,21 @@ public sealed class WbMeshAdapter : IDisposable, IWbMeshAdapter
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PopulateMetadata(ulong id)
|
||||||
|
{
|
||||||
|
if (_dats is null) return;
|
||||||
|
if (!_dats.Portal.TryGet<GfxObj>((uint)id, out var gfxObj)) return;
|
||||||
|
|
||||||
|
var subMeshes = GfxObjMesh.Build(gfxObj, _dats);
|
||||||
|
for (int i = 0; i < subMeshes.Count; i++)
|
||||||
|
{
|
||||||
|
var sm = subMeshes[i];
|
||||||
|
_metadataTable.Add(id, i, new AcSurfaceMetadata(
|
||||||
|
sm.Translucency, sm.Luminosity, sm.Diffuse,
|
||||||
|
sm.SurfOpacity, sm.NeedsUvRepeat, sm.DisableFog));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
|
namespace AcDream.Core.Tests.Rendering.Wb;
|
||||||
|
|
||||||
|
public sealed class MatrixCompositionTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Compose_EntityAnimRest_ProducesExpectedWorldMatrix()
|
||||||
|
{
|
||||||
|
var entityWorld = Matrix4x4.CreateTranslation(100, 200, 300);
|
||||||
|
var animOverride = Matrix4x4.CreateRotationZ(MathF.PI / 4);
|
||||||
|
var restPose = Matrix4x4.CreateTranslation(1, 0, 0);
|
||||||
|
|
||||||
|
var result = WbDrawDispatcher.ComposePartWorldMatrix(entityWorld, animOverride, restPose);
|
||||||
|
|
||||||
|
var expected = restPose * animOverride * entityWorld;
|
||||||
|
AssertMatrixEqual(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Compose_IdentityAnim_EqualsRestTimesEntity()
|
||||||
|
{
|
||||||
|
var entityWorld = Matrix4x4.CreateFromQuaternion(
|
||||||
|
Quaternion.CreateFromYawPitchRoll(0.5f, 0, 0)) *
|
||||||
|
Matrix4x4.CreateTranslation(10, 20, 30);
|
||||||
|
var restPose = Matrix4x4.CreateTranslation(0.5f, -0.3f, 0.1f);
|
||||||
|
|
||||||
|
var result = WbDrawDispatcher.ComposePartWorldMatrix(
|
||||||
|
entityWorld, Matrix4x4.Identity, restPose);
|
||||||
|
|
||||||
|
var expected = restPose * entityWorld;
|
||||||
|
AssertMatrixEqual(expected, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Compose_AllIdentity_ReturnsIdentity()
|
||||||
|
{
|
||||||
|
var result = WbDrawDispatcher.ComposePartWorldMatrix(
|
||||||
|
Matrix4x4.Identity, Matrix4x4.Identity, Matrix4x4.Identity);
|
||||||
|
|
||||||
|
AssertMatrixEqual(Matrix4x4.Identity, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AssertMatrixEqual(Matrix4x4 expected, Matrix4x4 actual, float eps = 1e-5f)
|
||||||
|
{
|
||||||
|
Assert.Equal(expected.M11, actual.M11, eps);
|
||||||
|
Assert.Equal(expected.M12, actual.M12, eps);
|
||||||
|
Assert.Equal(expected.M13, actual.M13, eps);
|
||||||
|
Assert.Equal(expected.M14, actual.M14, eps);
|
||||||
|
Assert.Equal(expected.M21, actual.M21, eps);
|
||||||
|
Assert.Equal(expected.M22, actual.M22, eps);
|
||||||
|
Assert.Equal(expected.M23, actual.M23, eps);
|
||||||
|
Assert.Equal(expected.M24, actual.M24, eps);
|
||||||
|
Assert.Equal(expected.M31, actual.M31, eps);
|
||||||
|
Assert.Equal(expected.M32, actual.M32, eps);
|
||||||
|
Assert.Equal(expected.M33, actual.M33, eps);
|
||||||
|
Assert.Equal(expected.M34, actual.M34, eps);
|
||||||
|
Assert.Equal(expected.M41, actual.M41, eps);
|
||||||
|
Assert.Equal(expected.M42, actual.M42, eps);
|
||||||
|
Assert.Equal(expected.M43, actual.M43, eps);
|
||||||
|
Assert.Equal(expected.M44, actual.M44, eps);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ public sealed class WbMeshAdapterTests
|
||||||
// We can't pass a real GL (no context in tests), so we verify only the
|
// We can't pass a real GL (no context in tests), so we verify only the
|
||||||
// null-GL guard. The real pipeline is tested via integration.
|
// null-GL guard. The real pipeline is tested via integration.
|
||||||
Assert.Throws<ArgumentNullException>(() =>
|
Assert.Throws<ArgumentNullException>(() =>
|
||||||
new WbMeshAdapter(gl: null!, datDir: "some/path", logger: NullLogger<WbMeshAdapter>.Instance));
|
new WbMeshAdapter(gl: null!, datDir: "some/path", dats: null!, logger: NullLogger<WbMeshAdapter>.Instance));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue