Where the halo comes from
A PNG stores colour and alpha separately (straight alpha), so a texel with alpha 0 still has an RGB value. Most programs write (0, 0, 0) or (255, 255, 255) there. You never see it at 1:1, because alpha 0 hides it.
Filtering and mipmaps mix neighbours. A bilinear sample halfway between an opaque edge texel and a transparent one averages both colours and both alphas; a mip level averages each 2 × 2 block of the level above. The result has the right, half-way alpha but a colour pulled towards the hidden black or white, so the edge darkens or glows — more at every smaller mip level, which is why the outline often appears only when the object is far away or scaled down.
Edge bleed replaces the hidden colour. Nerulio grows the colour one ring per round: every transparent texel next to one that already has colour takes the average RGB of those neighbours (all 8 directions), so after N rounds everything within N px of the sprite carries its edge colour. Texels with alpha above 0 are never touched and no alpha byte is written. Premultiplied-alpha textures avoid the problem in another way: their colour is already multiplied by alpha, so a transparent texel adds nothing.
How far to bleed. In a box-filtered mip chain a texel of level k covers 2ᵏ × 2ᵏ source texels, so a transparent texel mixed into an edge block is at most 2ᵏ − 1 px from the sprite. A 4 px bleed therefore keeps the levels down to 1/4 clean, 8 px down to 1/8 and 16 px down to 1/16 — the three levels the Lab's mipmap preview shows.
- Straight alpha
- Colour and alpha stored independently; RGB exists even where alpha is 0.
- Edge bleed (dilation)
- Copying edge colour outwards into transparent texels without changing their alpha.
- Mip level
- A pre-shrunk copy (1/2, 1/4, 1/8 …) the GPU uses when the texture is drawn small.
- Premultiplied alpha
- Colour already multiplied by alpha; hidden colour cannot leak.
Example: one edge block, before and after
A red sprite texel (210, 40, 30, 255) sits next to a transparent texel that holds black. Mip level 1 averages the 2 × 2 block (two opaque, two transparent):
Without bleed (210+210+0+0)/4, (40+40+0+0)/4, (30+30+0+0)/4, alpha (255+255+0+0)/4
= (105, 20, 15, 128) half transparent and half as bright: the dark rim
With bleed transparent texels now hold (210, 40, 30, 0)
= (210, 40, 30, 128) half transparent, same red
Texels filled around a 24 × 24 opaque square (bleed N px: (24 + 2N)² − 24²):
2 px → 208 4 px → 448 8 px → 1024 16 px → 2560
The 4 px figure is the one the Lab measured on its test sprite: 448 texels changed, all with alpha 0, and not a single alpha byte changed.
Check the result
- In Fix › Mipmaps, the edge at 1/4 … 1/16 should keep the sprite's colour after bleeding instead of turning darker.
- The result note says how many transparent texels were filled; open the saved PNG in the Channels stage: the A channel is unchanged and RGB under alpha 0 now carries the edge colour.
- In the engine, view the object small or far away with mipmaps on: the outline should be gone.
Troubleshooting
| Symptom | Likely cause | How to check | Fix |
|---|
| Dark or white outline only when the object is small or far away | Mip levels average the hidden RGB into the edge | Fix › Mipmaps: compare 1/4, 1/8, 1/16 before and after bleeding | Bleed at least as far as the smallest level you see (4 px → 1/4, 8 px → 1/8, 16 px → 1/16), or turn on the engine's import option |
|---|
| A thin fringe at 1:1 when the sprite moves, rotates or is scaled | Bilinear filtering samples between the edge and the transparent neighbour | Zoom the engine view: the fringe follows the silhouette | Bleed 2 px or more; for pixel art use Nearest filtering instead (blurry pixel art in Godot) |
|---|
| The bleed disappeared after resizing the texture | The resize went through a canvas, which discards colour under alpha 0 | Open the resized file in the Channels stage: RGB under alpha 0 is 0 again | Resize first and bleed last — the Lab's Export runs resize → bleed → encode in that order |
|---|
| The bleed is gone after saving as WebP or JPEG | Those formats are encoded through the browser canvas; JPEG has no alpha at all | Compare the PNG and the WebP in the Channels stage | Keep bled textures as PNG |
|---|
| Colour of the neighbouring sprite shows at the edge in an atlas | Sprites are packed without gaps, so filtering reaches the next sprite | Look at the atlas: are sprites touching? | That needs padding or extrusion between sprites, not bleed: see atlas padding and the sprite sheet packer |
|---|
| Semi-transparent edge pixels themselves are dark | The art was exported against a black matte, so the colour is baked into pixels with alpha 1–254 | The dark pixels have alpha above 0 | Nerulio cannot fix this (bleed never touches alpha > 0): re-export from the source without a matte |
|---|
Other ways to do it
| Option | When it is the better choice |
|---|
| Godot 4: Process › Fix Alpha Border (import option) | Textures imported into Godot: it fills transition pixels with the surrounding colour on import and is on by default, so a bled PNG is usually not needed there. |
|---|
| Unity: Alpha is Transparency (import option) | Textures imported into Unity: it dilates the colour channels to avoid filtering artifacts at alpha edges. |
|---|
| Premultiplied alpha | Your renderer blends premultiplied textures: hidden colour cannot leak at all. |
|---|
Versions and sources
Measured on the Texture Lab (docs/TEXTURE-LAB.md): edge bleed changed 448 texels around a 24 × 24 square (exactly four rings), none with alpha above 0; the alpha plane had 0 differing bytes; the first ring read (210, 40, 30, 0). The mipmap preview is the same per-channel box average as mipChain in src/game/texture-fix.js. Engine import options are quoted from the Godot 4.7 and Unity 6 documentation.
Official documentation used
Texture Edge Bleed
Push colour outwards under transparent texels so mipmaps and downscaling stop showing a dark rim.
How it works
- Add images
- Adjust settings and review the result
- Download a new file
Supported formats
PNG / JPG / WebP / AVIF / HEIC → PNG — Input decoding depends on browser support and optional engines.
What this tool does
Push colour outwards under transparent texels so mipmaps and downscaling stop showing a dark rim.
Implementation and verified quality
advanced · iterative nearest-opaque RGB dilation (alpha untouched). Quality qualification is in progress. Pro/Flagship quality has not been established.
Before you export
Alpha is never changed. It cannot repair an alpha channel that was already cut or compressed badly.