fix(rendering): restore retail terrain texture tiling

Carry TerrainTex.TexTiling through the terrain atlas and upload a layer-indexed table to the modern bindless shader so base, overlay, and road textures repeat at retail scale. Keep alpha masks cell-scaled and preserve retail's verified source-level-zero high-detail selection.

Add the named-retail pseudocode, WorldBuilder/ACE/ACME cross-reference, adapter conformance tests, and inventory documentation so a future renderer migration keeps the contract.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-13 21:04:22 +02:00
parent 84b7d2d7cd
commit bb5acab9e6
7 changed files with 297 additions and 7 deletions

View file

@ -64,6 +64,8 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
// Cached uvec2-handle uniform locations (matrix uniforms are set by name via Shader.SetMatrix4).
private int _uTerrainHandleLoc;
private int _uAlphaHandleLoc;
private int _uTexTilingLoc;
private bool _textureTilingUploaded;
// Reusable per-frame buffers.
private readonly List<int> _visibleSlots = new();
@ -90,6 +92,9 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
_uTerrainHandleLoc = _gl.GetUniformLocation(_shader.Program, "uTerrainHandle");
_uAlphaHandleLoc = _gl.GetUniformLocation(_shader.Program, "uAlphaHandle");
_uTexTilingLoc = _gl.GetUniformLocation(_shader.Program, "uTexTiling[0]");
if (_uTexTilingLoc < 0)
throw new InvalidOperationException("terrain_modern.frag is missing the required uTexTiling uniform.");
_globalVao = _gl.GenVertexArray();
_globalVbo = _gl.GenBuffer();
@ -267,6 +272,7 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
// pre-positions terrain to the outdoor landcell, but acdream uses the
// unified camera matrix everywhere, so no separate viewpoint divergence can occur.
_shader.Use();
UploadTextureTilingOnce();
_shader.SetMatrix4("uView", camera.View);
_shader.SetMatrix4("uProjection", camera.Projection);
@ -331,6 +337,34 @@ public sealed unsafe class TerrainModernRenderer : IDisposable
// Private helpers
// ----------------------------------------------------------------
/// <summary>
/// Upload the texture-array adapter for retail's per-surface repeat count.
/// Retail passes <c>TerrainTex::tex_tiling</c> directly to
/// <c>ImgTex::TileCSI</c> / <c>ImgTex::MergeTexture</c>
/// (`TexMerge::CopyAndTile` 0x00503580, `TexMerge::Merge` 0x005038C0).
/// Uniform values persist for the lifetime of this linked shader program,
/// so the immutable atlas table is uploaded on its first bound draw.
/// </summary>
private void UploadTextureTilingOnce()
{
if (_textureTilingUploaded)
return;
if (_atlas.TilingByLayer.Count != TerrainTextureTilingTable.LayerCapacity)
{
throw new InvalidOperationException(
$"Terrain tiling table has {_atlas.TilingByLayer.Count} entries; " +
$"expected {TerrainTextureTilingTable.LayerCapacity}.");
}
Span<float> values = stackalloc float[TerrainTextureTilingTable.LayerCapacity];
for (int i = 0; i < values.Length; i++)
values[i] = _atlas.TilingByLayer[i];
_gl.Uniform1(_uTexTilingLoc, values);
_textureTilingUploaded = true;
}
/// <summary>
/// Phase U.3: bind the terrain clip UBO to binding=2. Prefers the shared
/// <see cref="ClipFrame"/> UBO (<see cref="SetClipUbo"/>); otherwise lazily