The seam is two correct edges added wrong
Sooner or later a field-rendered element has to sit against something drawn another way: a sprite, a mesh, a UI quad, another pass. The two pieces are each correct, they are the same color, they touch exactly, and there is a line down the join.
Look along the horizon line where the two pieces meet. On the left there is a darker band; on the right there is nothing, because there is no join. Both halves draw the same shape in the same color.
The arithmetic, which is the whole explanation
Take a pixel sitting exactly on the shared edge. The dome covers half of it. The base covers the other half. Composite them one after the other:
result = bg
result = mix(result, mat, 0.5) // base: half background survives
result = mix(result, mat, 0.5) // dome: half of THAT survives
// total material coverage = 0.5 + 0.5 * (1 - 0.5) = 0.75
So a quarter of the background remains along the entire seam, forever, no matter how accurate each edge is. The two pieces are not overlapping and they are not leaving a gap. They are each correctly reporting partial coverage of a pixel that is in fact fully covered, and "over" has no way to know the two halves are adjacent rather than stacked.
Worth noticing that it is not a rounding error and it does not get better with more precision or more samples. It is what alpha compositing means.
The fixes, in order of preference
- Do not have a seam. Union the fields and resolve the edge once. This is available far more often than people assume, and it is the only fix that is exactly right rather than approximately right.
- Overlap the pieces. If they must be separate, make them intersect by a pixel or two rather than abut. The overlap region is fully covered by each piece, so there is no partial coverage to combine. Crude, universal, works with anything.
- Resolve coverage in one pass. Render both into a coverage buffer, take the max, and shade once. Correct, and it costs a target.
- Match the antialiasing width on both sides. This does not fix the arithmetic, it just stops the seam from also changing width along its length, which is what turns a faint line into a visibly wobbly one.
Everywhere this shows up
- An SDF character over a sprite background, where the character's contact shadow is a separate draw.
- Tiled or chunked rendering, where each tile antialiases its own boundary. The seams form a grid, which is the most recognizable version of this.
- A UI panel meeting a UI panel. Two rounded rectangles sharing an edge, both antialiased, hairline between them.
- Anything drawn in two passes for sorting reasons, which is the case where the fix is hardest because the split exists for a reason.
- Premultiplied alpha getting confused with straight alpha, which produces a similar-looking dark seam from a completely different cause. Worth ruling out: check whether the line is dark and desaturated, which points at premultiplication, rather than just showing background.
How to tell it apart from the things it looks like
A one-pixel dark line has several possible causes and they need different fixes:
- It shows the background color exactly: this note. Coverage, not color.
- It is dark but not the background color: a premultiplication mismatch.
- It moves when the camera moves and snaps at whole pixels: a rounding difference between the two systems' vertex positions.
- It appears only at some zoom levels: texture sampling, not compositing.
Change the background to something loud and unmissable. If the seam turns that color, it is coverage, and the answer is on this page.
Rules of thumb
- Two abutting antialiased edges composite to 75% coverage, not 100%. That is definitional, not a bug to hunt.
- The best fix is to not have a seam: union the fields, resolve the edge once.
- If they must be separate pieces, overlap them by a pixel. Never abut.
- Matching antialiasing widths does not fix the seam, it just stops it from wobbling.
- Set the background to a loud color to identify it. A coverage seam shows exactly that color.
One email when something new goes up. No newsletter, no schedule, nothing else.
Double opt-in, so watch for a confirmation email. Unsubscribe any time.