Terrain Anti-Tiling: distance dual-scale + stochastic “by-example” (no asset pipeline)
Before anti-tile:
After anti-tile:
Works best when looking out over the terrain, looking straight down at terrain it doesn't help as much.
Written by AI:
Repeating ground textures betray themselves two different ways, and they need two different fixes:
- The far-field grid — from a hill or a grazing camera, the regular periodicity of the texture reads as a checkerboard receding to the horizon.
- The close/mid feature repeat — that one distinctive grass clump / rock / crack showing up again every few meters right in front of you.
Two complementary techniques in the deferred terrain shader (
Code:
Engine/Source/Shaders/Deferred.cpp
) address each. Both run on the
single- and multi-material terrain paths, up to 5 blended materials, and both are
gated to terrain only (
Code:
#if HEIGHTMAP && SET_POS
), so buildings, props, and characters are completely untouched.
1. Distance-based dual-scale: kills the far grid
Sample the same ground texture at
two UV scales and blend in the lower-frequency, larger-tile one with camera distance:
Code:
tex = lerp(near, RTex(Col, uv * TileFarScale), fade(distance));
Near the camera you get full detail. Far away, the tight repeat dissolves into a coarser, non-aligned pattern. This is the standard Unreal-landscape-style trick, and it is
mip-safe for free, because the two samples use implicit derivatives, so no
is needed.
2. Stochastic “by-example”: kills the near/mid feature repeat
This is
Heitz–Neyret’s by-example noise, minus the histogram LUT, which means it needs
no precomputed assets and no per-texture bake.
- Triangular / “hex” grid — 3 samples, no axis-aligned blend seams.
- Per-cell random offset only, no mirror — the mirror in the cheaper grid variants is what causes those directional scratches/scuffs on grass. Dropping it removes them.
- Variance-preserving blend — naive averaging washes out contrast, so instead of
, we blend the deviation from the mean and renormalize by
Code:
1 / sqrt(sum(w_i^2))
. The mean is read from the texture’s smallest mip, approximately its average color, so we recover contrast with zero precomputation.
-
keeps mipmapping correct.
The result: a single tiling texture stops repeating and reads as a natural, non-periodic surface — no scratches, no muddy blend blobs.
How to use it
Everything is driven by a single
shader cbuffer, set from the public shader-param API:
Code:
SPSet("TerrainTileEnable", 1.0f); // distance effect
SPSet("TileFarScale", 0.22f);
SPSet("TileFadeStart", 5.0f); // ...TileFadeRange, TileFarMax
SPSet("TerrainStochastic", 1.0f); // stochastic de-tiling
It is off by default. The cbuffer is zero-initialized, so nothing changes unless your app sets the params. The editor and all other content render exactly as before.
The params only exist once the terrain shader has loaded, so set them
each frame before drawing the world.
safely no-ops until then. There is no engine-wide default yet; you opt in from your render loop.
Parameters
-
— 0/1, distance dual-scale on/off.
-
— coarse UV multiplier. Smaller means bigger far tiles and a stronger frequency break.
-
— meters: distance where the coarse blend begins.
-
— meters: distance over which it ramps to
.
-
— max fraction of the coarse sample blended in at far distance.
-
— 0/1, stochastic de-tiling on/off.
-
— performance limit: only de-tile within this distance.
means no limit.
-
— performance limit: skip materials whose blend weight is below this. means de-tile all.
Performance limits: opt-in, runtime
Multi-material stochastic can hit up to
5 materials × 4 taps = 20 albedo taps, so there are two runtime knobs:
-
— only run the stochastic taps within this distance. The far grid is handled by the distance blend anyway, so far pixels drop straight back to 1 tap.
-
— skip stochastic for a material whose per-pixel blend weight is below a threshold, so a barely-visible layer does not cost 4 taps.
Both default to “no practical limit.”
Large-world / floating-origin
Terrain UVs are
world-continuous, built from the absolute area index, so far out in a big world they grow large.
A
hash loses precision there and the de-tiling starts aliasing, so the stochastic cell hash is a
PCG2D integer hash on the integer cell ID, exact at any magnitude.
The pattern stays locked to the ground and is transparent to floating-origin rebasing. UVs deliberately
do not rebase. Shifting them would move the non-integer-aligned stochastic grid and pop the pattern every rebase.
Tutorials
Code:
Tutorial_14_TerrainTiling
Code:
Tutorials/Source/14 - Game Basics/36 - Terrain Tiling.cpp
The live A/B demo. It loads a textured world and lets you toggle and tune everything in real time:
- T — distance anti-tiling on/off.
- Y — stochastic de-tiling on/off.
- 1–8 — distance params: strength, fade start, fade range, coarse scale.
- G / H — stochastic max distance, for performance.
- J / K — stochastic min weight, for performance.
- 9 — reset all to defaults.
- Hold RMB — look.
- Mouse wheel — zoom.
- ESC — quit.
The HUD shows every value live, and the
env vars let you drive it headless for A/B captures:
Code:
EE_TILE
EE_TILE_STOCH
EE_TILE_CAM_*
EE_TILE_FARMAX
EE_TILE_STOCHDIST
Since the effect lives in the shader, it shows up on
any terrain. The other world tutorials are good places to see it on different content once an app enables the params:
-
Code:
Tutorial_14_WorldWithCharacter
Code:
Tutorial_14_ProceduralWorld
Caveats / future work
- The only thing the full Heitz histogram LUT would add over this is slightly sharper blend zones. It carries a per-texture asset-bake cost, so it is left out. The variance-preserving approximation looks clean in practice.
- One residual issue: the triangular-grid barycentric weights lose a little float precision at extreme UV. The cell hash is exact, so the de-tiling holds; only the blend transitions get marginally coarser very far out.
- Backends: Vulkan and GL shader paks are regenerated. DX still needs a Windows tree to recompile, only tested on Linux+Vulkan.
in this repo:
https://github.com/DrewGilpin/EsenthelEngine