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:
parent
84b7d2d7cd
commit
bb5acab9e6
7 changed files with 297 additions and 7 deletions
|
|
@ -153,6 +153,15 @@ use from WB rather than re-implement.
|
|||
| `TextureAtlasManager` | Texture atlas builder |
|
||||
| `VertexLandscape` | Terrain vertex format |
|
||||
|
||||
**Modern terrain adapter:** acdream's bindless path uses `TerrainAtlas` plus
|
||||
`TerrainModernRenderer` rather than WB's draw manager, but retains
|
||||
`LandSurfaceManager`'s layer-indexed `TerrainTex.TexTiling` contract. The
|
||||
36-entry table is uploaded to `terrain_modern.frag`; base, overlay, and road
|
||||
layers each use their owning repeat count while alpha masks stay at cell scale.
|
||||
Retail `TexMerge::CopyAndTile` (`0x00503580`) and `TexMerge::Merge`
|
||||
(`0x005038C0`) are the behavior oracle; see
|
||||
`docs/research/2026-07-13-retail-terrain-texture-tiling-pseudocode.md`.
|
||||
|
||||
### Scenery (procedural placement: trees, bushes, rocks, fences)
|
||||
|
||||
| Component | What it does |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,109 @@
|
|||
# Retail terrain texture tiling
|
||||
|
||||
## Question
|
||||
|
||||
Why do acdream's grass, dirt, and road textures look lower-detail than the
|
||||
September 2013 retail client even though the modern terrain atlas loads 512 x
|
||||
512 source images, generates mipmaps, and enables anisotropic filtering?
|
||||
|
||||
## Retail oracle
|
||||
|
||||
Named retail symbols in
|
||||
`docs/research/named-retail/acclient_2013_pseudo_c.txt`:
|
||||
|
||||
- `TexMerge::CopyAndTile` at `0x00503580`
|
||||
- `TexMerge::Merge` at `0x005038C0`
|
||||
- `ImgTex::TileCSI` at `0x0053E740`
|
||||
- `ImgTex::GetSurfaceDID` at `0x0053F0E0`
|
||||
- `Render::ShouldDropHighDetail` at `0x0054C700`
|
||||
|
||||
The verbatim `TerrainTex` declaration in
|
||||
`docs/research/named-retail/acclient.h` contains `unsigned int tex_tiling`.
|
||||
|
||||
## Readable pseudocode
|
||||
|
||||
```text
|
||||
CopyAndTile(destination, destinationSize, terrainTexture):
|
||||
baseTexture = terrainTexture.base_texture
|
||||
if baseTexture is null:
|
||||
if terrainTexture.tex_gid is valid:
|
||||
terrainTexture.base_texture = load ImgTex(terrainTexture.tex_gid)
|
||||
baseTexture = terrainTexture.base_texture
|
||||
|
||||
if baseTexture is null:
|
||||
CopyCSI(destination, destinationSize, destinationSize, null, 1)
|
||||
return false
|
||||
|
||||
CopyCSI(
|
||||
destination,
|
||||
destinationSize,
|
||||
destinationSize,
|
||||
baseTexture,
|
||||
terrainTexture.tex_tiling)
|
||||
return true
|
||||
|
||||
Merge(destination, destinationSize, alphaTexture, rotation, terrainTexture):
|
||||
if alphaTexture is null:
|
||||
return false
|
||||
|
||||
if terrainTexture.base_texture is null:
|
||||
if not terrainTexture.InitEnd():
|
||||
return false
|
||||
|
||||
MergeTexture(
|
||||
destination,
|
||||
destinationSize,
|
||||
destinationSize,
|
||||
terrainTexture.base_texture,
|
||||
terrainTexture.tex_tiling,
|
||||
alphaTexture,
|
||||
rotation)
|
||||
return true
|
||||
|
||||
GetSurfaceDID(surfaceTexture):
|
||||
if surfaceTexture has one source level:
|
||||
return sourceLevels[0]
|
||||
if surfaceTexture has two source levels:
|
||||
if ShouldDropHighDetail():
|
||||
return sourceLevels[1]
|
||||
return sourceLevels[0]
|
||||
report invalid source-level count
|
||||
```
|
||||
|
||||
`ImgTex::TileCSI` repeats the source image `tex_tiling` times in both axes.
|
||||
`TexMerge::Merge` applies the same repeat count to every terrain layer before
|
||||
the cell-scale alpha mask is merged. Therefore the modern shader must multiply
|
||||
the terrain UV independently for the base layer, each overlay layer, and each
|
||||
road layer. The alpha-map UV must not be multiplied.
|
||||
|
||||
The source image selection is a separate policy. Retail uses source level zero
|
||||
unless its quality policy explicitly drops high detail, so changing acdream's
|
||||
atlas from `SurfaceTexture.Textures[0]` would invert the verified retail rule.
|
||||
|
||||
## Cross-references
|
||||
|
||||
- Extracted WorldBuilder lineage: `LandSurfaceManager` stores a 36-entry
|
||||
layer-indexed tiling table populated from `TerrainTex.TexTiling`, and its
|
||||
terrain shader multiplies each texture layer's UV by that value. The pinned
|
||||
WorldBuilder change introducing the path is commit `0d1405af65`.
|
||||
- ACE: `ACE.Server/Physics/Common/TexMerge.cs` forwards
|
||||
`TerrainTex.TexTiling` from both `CopyAndTile` and `Merge`; `ImgTex.cs`
|
||||
resolves the normal source as `Textures[0]`.
|
||||
- ACME-derived `tests/AcDream.Core.Tests/Terrain/ClientReference.cs` covers
|
||||
terrain split, pal/road codes, and blend selection, but not image tiling or
|
||||
source-level selection. It supplies no competing behavior for this path.
|
||||
|
||||
Retail remains the oracle where a reference differs: the pinned WorldBuilder
|
||||
reader selects the last surface source, while retail's `GetSurfaceDID` proves
|
||||
that the normal/high-detail source is index zero.
|
||||
|
||||
## acdream port shape
|
||||
|
||||
1. Capture `TerrainTex.TexTiling` while `TerrainAtlas` assigns each terrain
|
||||
type to an array layer.
|
||||
2. Convert the type-keyed values into the renderer's 36-entry layer-indexed
|
||||
uniform table. Unused layers default to `1.0`; mapped values are preserved
|
||||
verbatim.
|
||||
3. Upload the table after binding `terrain_modern.frag`.
|
||||
4. Sample base, overlay, and road textures with
|
||||
`cellUv * uTexTiling[layer]`; sample alpha masks with their original UV.
|
||||
|
|
@ -29,6 +29,7 @@ out vec4 fragColor;
|
|||
|
||||
uniform uvec2 uTerrainHandle;
|
||||
uniform uvec2 uAlphaHandle;
|
||||
uniform float uTexTiling[36];
|
||||
#define uTerrain sampler2DArray(uTerrainHandle)
|
||||
#define uAlpha sampler2DArray(uAlphaHandle)
|
||||
|
||||
|
|
@ -46,7 +47,12 @@ layout(std140, binding = 1) uniform SceneLighting {
|
|||
vec4 uCameraAndTime;
|
||||
};
|
||||
|
||||
const float TILE = 1.0;
|
||||
// Retail TexMerge::CopyAndTile (0x00503580) and TexMerge::Merge
|
||||
// (0x005038C0) pass TerrainTex::tex_tiling to every terrain source before
|
||||
// the cell-scale alpha mask is applied. The atlas stores that value by layer.
|
||||
float terrainTiling(float layer) {
|
||||
return uTexTiling[int(layer)];
|
||||
}
|
||||
|
||||
vec4 maskBlend3(vec4 t0, vec4 t1, vec4 t2, float h0, float h1, float h2) {
|
||||
float a0 = h0 == 0.0 ? 1.0 : t0.a;
|
||||
|
|
@ -68,21 +74,21 @@ vec4 combineOverlays(vec2 baseUV, vec4 pOverlay0, vec4 pOverlay1, vec4 pOverlay2
|
|||
vec4 t0 = vec4(0.0), t1 = vec4(0.0), t2 = vec4(0.0);
|
||||
|
||||
if (h0 > 0.0) {
|
||||
t0 = texture(uTerrain, vec3(baseUV * TILE, pOverlay0.z));
|
||||
t0 = texture(uTerrain, vec3(baseUV * terrainTiling(pOverlay0.z), pOverlay0.z));
|
||||
if (pOverlay0.w >= 0.0) {
|
||||
vec4 a = texture(uAlpha, vec3(pOverlay0.xy, pOverlay0.w));
|
||||
t0.a = a.a;
|
||||
}
|
||||
}
|
||||
if (h1 > 0.0) {
|
||||
t1 = texture(uTerrain, vec3(baseUV * TILE, pOverlay1.z));
|
||||
t1 = texture(uTerrain, vec3(baseUV * terrainTiling(pOverlay1.z), pOverlay1.z));
|
||||
if (pOverlay1.w >= 0.0) {
|
||||
vec4 a = texture(uAlpha, vec3(pOverlay1.xy, pOverlay1.w));
|
||||
t1.a = a.a;
|
||||
}
|
||||
}
|
||||
if (h2 > 0.0) {
|
||||
t2 = texture(uTerrain, vec3(baseUV * TILE, pOverlay2.z));
|
||||
t2 = texture(uTerrain, vec3(baseUV * terrainTiling(pOverlay2.z), pOverlay2.z));
|
||||
if (pOverlay2.w >= 0.0) {
|
||||
vec4 a = texture(uAlpha, vec3(pOverlay2.xy, pOverlay2.w));
|
||||
t2.a = a.a;
|
||||
|
|
@ -96,7 +102,7 @@ vec4 combineRoad(vec2 baseUV, vec4 pRoad0, vec4 pRoad1) {
|
|||
float h1 = pRoad1.z < 0.0 ? 0.0 : 1.0;
|
||||
vec4 result = vec4(0.0);
|
||||
if (h0 > 0.0) {
|
||||
result = texture(uTerrain, vec3(baseUV * TILE, pRoad0.z));
|
||||
result = texture(uTerrain, vec3(baseUV * terrainTiling(pRoad0.z), pRoad0.z));
|
||||
if (pRoad0.w >= 0.0) {
|
||||
vec4 a0 = texture(uAlpha, vec3(pRoad0.xy, pRoad0.w));
|
||||
result.a = 1.0 - a0.a;
|
||||
|
|
@ -123,7 +129,7 @@ vec3 applyFog(vec3 lit, vec3 worldPos) {
|
|||
void main() {
|
||||
vec4 baseColor = vec4(0.0);
|
||||
if (vBaseTexIdx >= 0.0) {
|
||||
baseColor = texture(uTerrain, vec3(vBaseUV * TILE, vBaseTexIdx));
|
||||
baseColor = texture(uTerrain, vec3(vBaseUV * terrainTiling(vBaseTexIdx), vBaseTexIdx));
|
||||
}
|
||||
|
||||
vec4 overlays = vec4(0.0);
|
||||
|
|
|
|||
|
|
@ -34,6 +34,12 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
public uint GlTexture { get; } // terrain atlas, kept as GlTexture for back-compat with TerrainRenderer
|
||||
public IReadOnlyDictionary<uint, uint> TerrainTypeToLayer { get; }
|
||||
public int LayerCount { get; }
|
||||
/// <summary>
|
||||
/// UV repeat count for each terrain-array layer. Retail forwards
|
||||
/// <c>TerrainTex::tex_tiling</c> to both base and merged terrain textures
|
||||
/// (`TexMerge::CopyAndTile` 0x00503580, `TexMerge::Merge` 0x005038C0).
|
||||
/// </summary>
|
||||
public IReadOnlyList<float> TilingByLayer { get; }
|
||||
|
||||
// --- Alpha atlas (new in Phase 3c.2) ---
|
||||
public uint GlAlphaTexture { get; }
|
||||
|
|
@ -86,6 +92,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
GL gl,
|
||||
Wb.BindlessSupport? bindless,
|
||||
uint glTexture, IReadOnlyDictionary<uint, uint> map, int layerCount,
|
||||
IReadOnlyList<float> tilingByLayer,
|
||||
uint glAlphaTexture, int alphaLayerCount,
|
||||
IReadOnlyList<byte> cornerLayers, IReadOnlyList<byte> sideLayers, IReadOnlyList<byte> roadLayers,
|
||||
IReadOnlyList<uint> cornerTCodes, IReadOnlyList<uint> sideTCodes, IReadOnlyList<uint> roadRCodes)
|
||||
|
|
@ -95,6 +102,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
GlTexture = glTexture;
|
||||
TerrainTypeToLayer = map;
|
||||
LayerCount = layerCount;
|
||||
TilingByLayer = tilingByLayer;
|
||||
GlAlphaTexture = glAlphaTexture;
|
||||
AlphaLayerCount = alphaLayerCount;
|
||||
CornerAlphaLayers = cornerLayers;
|
||||
|
|
@ -125,6 +133,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
|
||||
// ---- Terrain atlas (unchanged Phase 2b logic) ----
|
||||
var decodedByType = new Dictionary<uint, DecodedTexture>();
|
||||
var tilingByType = new Dictionary<uint, uint>();
|
||||
int maxW = 1, maxH = 1;
|
||||
foreach (var tmtd in terrainDesc)
|
||||
{
|
||||
|
|
@ -132,6 +141,12 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
if (decodedByType.ContainsKey(typeKey))
|
||||
continue;
|
||||
|
||||
// Retail terrain composition repeats this surface exactly
|
||||
// TerrainTex::tex_tiling times in each axis before applying the
|
||||
// cell-scale alpha mask. Preserve the dat field alongside the
|
||||
// decoded surface while layer assignment is still type-keyed.
|
||||
tilingByType[typeKey] = tmtd.TerrainTex.TexTiling;
|
||||
|
||||
var surfaceTextureId = (uint)tmtd.TerrainTex.TextureId;
|
||||
var st = dats.Get<SurfaceTexture>(surfaceTextureId);
|
||||
if (st is null || st.Textures.Count == 0)
|
||||
|
|
@ -141,6 +156,10 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
continue;
|
||||
}
|
||||
|
||||
// Retail ImgTex::GetSurfaceDID (0x0053F0E0) returns source level
|
||||
// zero unless Render::ShouldDropHighDetail explicitly selects
|
||||
// level one. acdream currently has no low-detail quality mode, so
|
||||
// index zero is the retail high-detail source.
|
||||
var rs = dats.Get<RenderSurface>((uint)st.Textures[0]);
|
||||
if (rs is null)
|
||||
{
|
||||
|
|
@ -183,6 +202,12 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
layerIdx++;
|
||||
}
|
||||
|
||||
var tilingByLayer = TerrainTextureTilingTable.Build(
|
||||
map.Select(entry =>
|
||||
(entry.Value, tilingByType.TryGetValue(entry.Key, out uint repeatCount)
|
||||
? repeatCount
|
||||
: 1u)));
|
||||
|
||||
// A.5 T19: generate mipmaps + trilinear + 16x anisotropic for distant-LB quality.
|
||||
gl.GenerateMipmap(TextureTarget.Texture2DArray);
|
||||
gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.LinearMipmapLinear);
|
||||
|
|
@ -203,7 +228,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
return new TerrainAtlas(
|
||||
gl,
|
||||
bindless,
|
||||
tex, map, layerCount,
|
||||
tex, map, layerCount, tilingByLayer,
|
||||
alphaBuild.gl, alphaBuild.layerCount,
|
||||
alphaBuild.corner, alphaBuild.side, alphaBuild.road,
|
||||
alphaBuild.cornerTCodes, alphaBuild.sideTCodes, alphaBuild.roadRCodes);
|
||||
|
|
@ -410,6 +435,7 @@ public sealed unsafe class TerrainAtlas : IDisposable
|
|||
gl,
|
||||
bindless,
|
||||
tex, new Dictionary<uint, uint> { [0] = 0u }, 1,
|
||||
TerrainTextureTilingTable.Build(Array.Empty<(uint Layer, uint RepeatCount)>()),
|
||||
alphaTex, 1,
|
||||
Array.Empty<byte>(), Array.Empty<byte>(), Array.Empty<byte>(),
|
||||
Array.Empty<uint>(), Array.Empty<uint>(), Array.Empty<uint>());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
40
src/AcDream.App/Rendering/TerrainTextureTilingTable.cs
Normal file
40
src/AcDream.App/Rendering/TerrainTextureTilingTable.cs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
public sealed class TerrainTextureTilingTableTests
|
||||
{
|
||||
[Fact]
|
||||
public void Build_MapsDatRepeatCountsToAtlasLayers()
|
||||
{
|
||||
var table = TerrainTextureTilingTable.Build(
|
||||
[
|
||||
(Layer: 0u, RepeatCount: 4u),
|
||||
(Layer: 7u, RepeatCount: 2u),
|
||||
(Layer: 32u, RepeatCount: 8u),
|
||||
]);
|
||||
|
||||
Assert.Equal(TerrainTextureTilingTable.LayerCapacity, table.Length);
|
||||
Assert.Equal(4f, table[0]);
|
||||
Assert.Equal(2f, table[7]);
|
||||
Assert.Equal(8f, table[32]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DefaultsOnlyUnusedLayersToOne()
|
||||
{
|
||||
var table = TerrainTextureTilingTable.Build(
|
||||
[
|
||||
(Layer: 3u, RepeatCount: 0u),
|
||||
]);
|
||||
|
||||
Assert.Equal(1f, table[2]);
|
||||
Assert.Equal(0f, table[3]);
|
||||
Assert.Equal(1f, table[4]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_RejectsLayersTheShaderCannotAddress()
|
||||
{
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
TerrainTextureTilingTable.Build(
|
||||
[
|
||||
(Layer: (uint)TerrainTextureTilingTable.LayerCapacity, RepeatCount: 1u),
|
||||
]));
|
||||
|
||||
Assert.Contains("exceeds the shader capacity", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModernShader_AppliesTheOwningLayersRepeatCountToEveryTerrainSample()
|
||||
{
|
||||
string shaderPath = Path.Combine(
|
||||
AppContext.BaseDirectory,
|
||||
"Rendering",
|
||||
"Shaders",
|
||||
"terrain_modern.frag");
|
||||
string shader = File.ReadAllText(shaderPath);
|
||||
|
||||
Assert.Contains("uniform float uTexTiling[36];", shader);
|
||||
Assert.Contains("baseUV * terrainTiling(pOverlay0.z)", shader);
|
||||
Assert.Contains("baseUV * terrainTiling(pOverlay1.z)", shader);
|
||||
Assert.Contains("baseUV * terrainTiling(pOverlay2.z)", shader);
|
||||
Assert.Contains("baseUV * terrainTiling(pRoad0.z)", shader);
|
||||
Assert.Contains("vBaseUV * terrainTiling(vBaseTexIdx)", shader);
|
||||
Assert.DoesNotContain("const float TILE", shader);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue