diff --git a/docs/plans/2026-04-13-rendering-rebuild.md b/docs/plans/2026-04-13-rendering-rebuild.md new file mode 100644 index 0000000..b4eb17c --- /dev/null +++ b/docs/plans/2026-04-13-rendering-rebuild.md @@ -0,0 +1,96 @@ +# Rendering Rebuild from ACME + +Port ACME's rendering pipeline to acdream. Each step produces a +visually testable result. The animation system stays unchanged (ACME +has none — ours is ported from the decompiled client). + +## Step 1: Port StaticObject shader + instanced rendering + +The biggest performance win. Replace per-entity DrawElements with +instanced rendering using a shared instance VBO. + +**From ACME:** +- StaticObject.vert: aInstanceMatrix (mat4 at locations 3-6, divisor=1) +- StaticObject.frag: sampler2DArray + alpha cutout +- Instance buffer pattern: single float[] upload per frame + +**Changes:** +- New shader: mesh_instanced.vert/frag (from ACME StaticObject.vert/frag) +- Rewrite StaticMeshRenderer to use instance buffer pattern +- Group entities by (GfxObjId, textureAtlas) → one DrawElementsInstanced per group +- Per-GfxObj TextureAtlasManager (grow-on-demand, starts at 32 slots) +- ushort indices (not uint32) for objects + +**Test:** Same visual output, fewer draw calls (check perf overlay). + +## Step 2: Port Landscape shader + terrain chunks + +Replace per-landblock terrain draws with chunk batching. + +**From ACME:** +- Landscape.vert/frag: 8 packed uvec4 attributes, sampler2DArray terrain + alpha +- TerrainChunk: N×N landblocks baked into one VBO/IBO +- TerrainGPUResourceManager: buffer creation + partial updates + +**Changes:** +- New shader: terrain_acme.vert/frag (from ACME Landscape.vert/frag) +- New TerrainChunkRenderer (replaces TerrainRenderer) +- LandblockMesh.Build outputs VertexLandscape-compatible structs +- Single DrawElements per chunk (multiple landblocks) + +**Test:** Same terrain appearance, one draw call per chunk. + +## Step 3: Port AdjustPlanes lighting + +Replace guessed sun direction with decompiled retail values. + +**From decompiled:** +- FUN_00532440 (AdjustPlanes): face-normal accumulation + per-vertex lighting +- DAT constants: sun direction, ambient, diffuse + +**Changes:** +- LandblockMesh: face-normal accumulation (replaces central differences) +- Shader uniforms: xLightDirection, xAmbient from decompiled constants +- Static objects: same lighting model + +**Test:** Side-by-side with retail client shows matching lighting. + +## Step 4: Port EnvCell portal visibility + +Render only visible interior cells. + +**From ACME:** +- EnvCellManager: portal visibility BFS from camera cell +- Portal occluder pass: depth-only draw of portal polygons +- Conditional depth clear when camera is inside a cell + +**Changes:** +- New CellVisibility system (BFS through CellPortals) +- Portal occluder depth pass before EnvCell geometry +- Conditional depth clear in render order + +**Test:** Enter a building — only visible rooms render. + +## Step 5: Wire animation into instanced pipeline + +The AnimationSequencer outputs per-part transforms. These need to flow +into the instance buffer alongside static transforms. + +**Changes:** +- Animated entities write their per-part instance matrices into the + shared instance buffer every frame (same buffer as static objects) +- AnimationSequencer.Advance(dt) is called BEFORE the instance buffer + upload so the latest frame's transforms are included + +**Test:** NPCs breathe, player walks — all through the instanced pipeline. + +## Render Order (target) + +``` +1. Terrain (one DrawElements per chunk, PolygonOffset on) +2. Conditional depth clear (if camera inside EnvCell) +3. EnvCell geometry (DrawElementsInstanced, portal visibility culled) +4. Static objects opaque (DrawElementsInstanced, alpha cutout) +5. Static objects translucent (DrawElementsInstanced, blend on) +6. Particles (additive blend — future) +``` diff --git a/src/AcDream.App/Rendering/InstancedMeshRenderer.cs b/src/AcDream.App/Rendering/InstancedMeshRenderer.cs index 5b95ef6..f21cf57 100644 --- a/src/AcDream.App/Rendering/InstancedMeshRenderer.cs +++ b/src/AcDream.App/Rendering/InstancedMeshRenderer.cs @@ -227,6 +227,7 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable // group share the same GfxObj so they have compatible overrides // only in the degenerate case of mixed-palette entities using the // same GfxObj — rare enough to accept the approximation here). + if (grp.Count == 0) continue; var firstEntry = grp.Entries[0]; uint tex = ResolveTex(firstEntry.Entity, firstEntry.MeshRef, sub); _gl.ActiveTexture(TextureUnit.Texture0); @@ -295,6 +296,7 @@ public sealed unsafe class InstancedMeshRenderer : IDisposable false, 64, (void*)(byteOffset + row * 16)); } + if (grp.Count == 0) continue; var firstEntry = grp.Entries[0]; uint tex = ResolveTex(firstEntry.Entity, firstEntry.MeshRef, sub); _gl.ActiveTexture(TextureUnit.Texture0);