acdream/src/AcDream.App/Rendering/TerrainTextureTilingTable.cs
Erik bb5acab9e6 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>
2026-07-13 21:04:22 +02:00

40 lines
1.4 KiB
C#

namespace AcDream.App.Rendering;
/// <summary>
/// Builds the layer-indexed adapter table consumed by
/// <c>terrain_modern.frag</c>. Retail passes <c>TerrainTex::tex_tiling</c>
/// directly to <c>ImgTex::TileCSI</c> / <c>ImgTex::MergeTexture</c>
/// (`TexMerge::CopyAndTile` 0x00503580, `TexMerge::Merge` 0x005038C0).
/// The table is the modern texture-array equivalent of that per-texture
/// argument.
/// </summary>
internal static class TerrainTextureTilingTable
{
// WorldBuilder's extracted terrain-array path uses the same 36-layer
// contract. Packed terrain layer indices are validated against this
// capacity before the shader table is built.
internal const int LayerCapacity = 36;
internal static float[] Build(IEnumerable<(uint Layer, uint RepeatCount)> entries)
{
ArgumentNullException.ThrowIfNull(entries);
var table = new float[LayerCapacity];
Array.Fill(table, 1f);
foreach (var (layer, repeatCount) in entries)
{
if (layer >= LayerCapacity)
{
throw new InvalidOperationException(
$"Terrain atlas layer {layer} exceeds the shader capacity of {LayerCapacity} layers.");
}
// Preserve the dat value exactly. Retail forwards the unsigned
// field without normalizing it in both CopyAndTile and Merge.
table[layer] = repeatCount;
}
return table;
}
}