Tiling Normal Map Seams — Generate It with Wrap Edges
A bright or dark line along the border of a repeating texture usually means the normal map was generated as if the border were an edge: the kernel clamped at the image border instead of reading the pixels on the opposite side, so the border slopes where the surface should simply continue.
A line where a tiling normal map repeats has one of three causes: the normal map was generated as if the image border were an edge (the kernel clamped instead of reading the opposite side), the colour or height texture itself does not tile, or the engine does not repeat the texture. Generate the map with Wrap edges, check that the texture shifted by half gives the identical map, and look at it 3×3 under a moving light. In Nerulio's Studio Texture workspace, Wrap is suggested for pictures with no transparent pixels, and the wrap test reads 0 when the border is computed exactly like the inside.

- Godot 4 2D lightsVerified· Godot 4.7.2
- Unity 6 URP 2DVerified· Unity 6000.5.3f1 (URP 17.5)
- PNG set + JSONPlain files
Why the seam appears
A normal map generator measures the slope at each pixel from its neighbours. At the image border half of those neighbours do not exist. Clamp repeats the border pixel instead, so the last one or two columns get a smaller or wrong slope; Wrap reads the pixels on the opposite side — exactly the ones that sit next to the border once the texture repeats. Kernels with a larger reach (Sobel 5×5 reads 2 px) or bevel and blur steps widen the damaged band: Laigter 1.14's default left a 3 px tilted band on every side of the ambientCG bricks.
Mismatched edges are a different problem. If the albedo's last column differs from its first column more than neighbouring columns differ inside the picture, the picture does not tile, and no normal map can hide that. Nerulio's albedo seam ratio compares exactly those two quantities: about 1 means no visible seam; the Lab and the Studio call a picture seamless when both directions stay at or below 1.5.
The engine has to repeat too. Unity's Wrap Mode Clamp "stretches the texture's edges" instead of repeating; Godot's BaseMaterial3D.texture_repeat is on by default; glTF 2.0 samplers default to repeat. A clamped texture shows a smeared edge rather than a thin line.
Not every line is in the texture. A seam that follows the model's UV seam instead of the tile border comes from the mesh (UVs or tangents); changing the texture cannot fix it.
- Clamp
- Samples outside the image take the border pixel. Right for sprites, wrong for tiling textures.
- Wrap
- Samples outside the image come from the opposite side, like the next tile.
- Wrap (roll) test
- Generate the map from the texture shifted by half, shift the result back and compare the 2 px border band: 0 means wrap-correct.
- Seam ratio
- Step across the wrap border ÷ step between neighbours inside the image.
Example: a slope that should continue across the border
A height ramp rising 10 levels per pixel runs straight through the wrap of a 256 px texture. Central differences (the Studio's Pixel kernel):
height x=254: 100 x=255: 110 | x=0: 120 x=1: 130 inside slope = 10 per px
Wrap x=255: (h[0] − h[254]) / 2 = (120 − 100) / 2 = 10 x=0: (h[1] − h[255]) / 2 = 10
Clamp x=255: (h[255] − h[254]) / 2 = (110 − 100) / 2 = 5 x=0: (h[1] − h[0]) / 2 = 5
→ Clamp halves the slope in the two columns at the join: a 2 px line at every repeat
Roll error on the ambientCG bricks (0 = wrap-correct), 2026-09-24:
Nerulio (Wrap) 0 · Laigter 1.14 default 63.8, Tile preset 0 · PBR Forge 0.57, Tile 0.24 · NormalMap-Online 0On the same bricks, the step across the border of Nerulio's normal map was 1.19 times the step inside; Laigter's default gave 24.0. A texture that tiles and a wrap-correct generator keep that ratio near 1.
What it does
- For pictures with no transparent pixels the Studio suggests Tileable texture, whose edges are Wrap: every kernel reads across the border from the opposite side.
- The wrap test in the Check panel generates the map again from the texture shifted by half and compares the two; 0 means seamless.
- The albedo's own seam ratio (1 ≈ no visible seam) shows when the picture itself does not tile, because no normal map can hide a seam in the colour.
- Tile 3×3 (T) repeats the lit texture so you can move a light across the border and look for the line.
- Height maps work the same way: from the 8-bit brick height map the Wrap result was 8.8° from ambientCG's own normal map (7.7° when read as a height map), with roll error 0.
How to do it
- Drop the tileable texture (or its height map) into the Texture workspace.
- Keep Tileable texture, or set Edges to Wrap in the Normal map panel.
- Open the Check panel: the wrap test should read 0; if the albedo seam ratio is high, fix the picture first.
- Press T for the 3×3 view, switch to Lit and drag a light across the border.
- Export for Godot 4, Unity 6 or as the PNG set.
Check the result
- Studio › Check › Seams: the wrap test reports the average and largest border difference; below 0.01 it shows ✓ Wrap-correct.
- Press T for the 3×3 view, switch to Lit and drag a light across the border: no line should follow the tile edges.
- In the engine, tile the material at least 3×3 and move the light across a repeat; do the same with the normal map off to separate a colour seam from a lighting seam.
Edge modes and what happens at the border
| Edge mode | Use it for | When the texture repeats |
|---|---|---|
| Clamp | Sprites with a transparent outline | The border pixel is repeated; the slope breaks at the join and shows as a lit line |
| Wrap | Tileable textures (suggested when nothing is transparent) | Samples come from the opposite side; wrap test 0, no line |
| Mirror | Pictures that will not repeat but should not get a rim | Reflected at the border, not the next tile; a faint line can remain |
| Wrap, but the albedo has a seam | Nothing: fix the albedo first | The normal map is seamless, the colour still shows the join |
Roll error on the same CC0 bricks (0 = wrap-correct): Nerulio 0 · Laigter 1.14 default 63.8, Tile preset 0 · PBR Forge 0.57, Tile 0.24 · NormalMap-Online 0 on this asset.
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|---|---|---|
| A lit or dark line exactly where the texture repeats, only with the normal map on | The map was generated with Clamp at the border | Studio › Check › Seams: the wrap test is above 0 | Generate again with Edges = Wrap (Tileable texture); the test should show ✓ Wrap-correct |
| The line is still there with the normal map switched off | The albedo or height map itself does not tile | Albedo seam ratio well above 1, or the seamless tile checker | Fix the picture first; Nerulio cannot make a non-tiling picture tile |
| A band 2–3 px wide along all four borders | The generator treated the border as a sprite edge (bevel or edge detection) | Look at the normal map alone: the band tilts outward on every side | Use the generator's tile mode, or regenerate here with Wrap |
| The edge looks stretched and the texture does not repeat | The engine clamps the texture instead of repeating it | Unity: Wrap Mode; Godot: the material's texture_repeat | Set Wrap Mode to Repeat, or turn texture_repeat on |
| A faint line remains with Mirror edges | Mirror reads a reflection instead of the next tile | Edges setting in the Normal map panel | Use Wrap for textures that repeat |
| The line follows the model's UV seam, not the tile border | UV layout or tangents of the mesh | Show the UVs in your 3D app: the line sits on a UV island edge | Fix it in the mesh; the texture is not the cause |
| An existing normal map already has the seam | It was generated with clamped edges elsewhere | Run the Check panel on the imported map | Nerulio does not patch an existing map: regenerate from the albedo or height with Wrap |
Other ways to do it
| Option | When it is the better choice |
|---|---|
| The Texture Lab's Normal stage with Wrap | A quick single height → normal conversion: tick Wrap under Advanced (or open it with wrap=1); its Fix › Seam check measures the albedo. |
| Laigter's Tile preset | You already use Laigter: its Tile preset brought the roll error on the bricks from 63.8 to 0. |
| Bake the normal map from geometry in a 3D application | The surface is modelled in 3D: a bake of a tiling mesh does not depend on image-border handling at all. |
Engine exports and how each was checked
"Verified" means the exported files were loaded by the real engine or tool on real CC0 assets and the result was compared with the source. Anything that was not loaded is labelled as such.
| Target | Files | Check |
|---|---|---|
| Godot 4 2D lights | _lit.tscn · CanvasTexture .tres · normal / specular PNG | Verified Godot 4.7.2Six real CC0 cases rendered by Godot (gl_compatibility): every checked frame within 1/255 of the Studio's lit preview. The same render with green flipped is 3.8–37 levels off, so the check can see a wrong convention. |
| Unity 6 URP 2D | PNG · nerulio-texture.json · Editor/NerulioNormalMapImporter.cs | Verified Unity 6000.5.3f1 (URP 17.5)12 of 12 runs (6 cases in Gamma and Linear colour space): the lighting URP applied matches the exported normal map (mean error 0.0037–0.0085). URP draws its own light falloff, so brightness is Unity's, not the preview's. |
| PNG set + JSON | albedo · _n (OpenGL) · _n_dx (DirectX) · 16-bit height · AO · specular · manifest | Plain filesPlain files for any engine or tool; no engine run is claimed for them. |
Evidence
Godot 4.7.2 rendered 6 real CC0 cases (pixel art, an HD sprite, a tileable texture, an imported DirectX map): every checked frame within 1/255 of the Studio's preview. Unity 6000.5 URP 2D: 12 of 12 runs pass (6 cases × Gamma and Linear). OpenGL/DirectX detection on real maps of known convention: 128 full 1K maps 100 % right, and not one wrong "high" verdict in 1,562 samples (a naive "green-high means OpenGL" rule gets 46 %).
Limits
- On a picture without transparent edges (a tileable texture) a flipped red channel cannot be told apart from a DirectX green; the Check panel says so and offers Flip red.
- A sheet is lit as one picture until its frames are cut in the Sprite workspace; the Texture workspace does not cut frames.
- The rim light is preview only, and Unity gets no specular map (Godot does).
- Unity draws its own light falloff: the exported normal map is verified in Unity, the brightness curve is Unity's.
Versions and sources
Studio Texture workspace exports were loaded and rendered in Godot 4.7.2 (6/6 cases PASS, max difference 1/255) and in Unity 6000.5.3f1 with URP 17.5 (12/12 PASS), including the tileable ambientCG bricks case generated with Wrap (docs/STUDIO-TEXTURE.md). Seam and roll figures are from the 2026-09-24 head-to-head on the same CC0 bricks. Engine wrap settings are quoted from the Unity 6, Godot 4.7 and glTF 2.0 documentation.
Official documentation used
Questions
Why did the seam appear only after I made the normal map?
The colour tiled, but the generator treated the border as the edge of a sprite, so the last pixels got a slope or a rim. Laigter's default, for example, leaves a 3 px tilted band on every side; its Tile preset removes it.
Can the Studio repair a normal map that already has a seam?
No. Generate it again here from the albedo or the height map with Wrap; the Studio does not patch the border of an existing normal map.
When is Mirror the right edge mode?
For a picture that will not repeat but should not get a rim at its border. On a repeating texture, Mirror reads a reflection instead of the next tile, so a faint line can remain.
The wrap test says 0 but I still see a line.
Then the line is in the colour: check the albedo seam ratio, or run the picture through the seamless tile checker and fix the albedo before generating again.
Is it free, and do I need an account?
Everything on these pages is free and works without an account. There is no watermark and no export that is held back.
Are my sprites uploaded?
No. The Studio decodes, packs and exports in this browser tab. Projects are autosaved in the browser (IndexedDB) and can be saved as a .nerulio file.
Does it need Aseprite or an engine plugin installed?
No. .aseprite files are read and written in the browser, and each engine bundle carries what that engine needs (for Godot, the import settings and scene; for Unity and the Godot TileSet, a small editor script).