map: a 1.12 zone sheet is opaque, so the feather belongs on

What was left after the stitch's geometry was hard rectangles at the
zone zooms. Bisected with OpenMapClassic's own /omc layer switches:
baked and gaps draw nothing on this build, zoneart draws them all.

A zone's sheet is masked to its land only for ids in ZoneMaskIDs, the
contiguous Classic range 1411-1452. On this realm that leaves
thirty-four without one -- every city, the dungeons, and every zone the
realm added of its own. Cities already decline to draw unmasked; zones
do not, and Gilneas puts its whole sheet, painted sea and torn parchment
border, over Silverpine.

PlaceBase takes a feather for exactly this and the zone walk passed nil
behind `(not era) and FEATHER`. Sound on Era, where a C_Map zone texture
carries a transparent surround; not true here, where the realm's sheets
are alpha 255 across every texel. The port passes it. PlaceBase prefers
the land mask where there is one, so the forty masked zones are
untouched -- 0.57% of pixels moved in a Dun Morogh view.

scripts/make-map-feather.py generates the three masks as 256x256
uncompressed TGA. A feather is a smoothstep ramp, which upstream's own
NOTICE calls generated rather than cut from art. A per-zone LAND mask
was deliberately not attempted: on this art sea and land do not separate
by colour (a sea patch reads R-B +28, a land patch +34) and the realm's
zone bitmap is 13x13 cells for its largest zone. Either would be a guess
dressed as art.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-03 18:17:14 -07:00
co-authored by Claude Opus 5
parent 7cb45769c5
commit 758665d4d8
2 changed files with 108 additions and 0 deletions
+46
View File
@@ -3015,3 +3015,49 @@ Both ends reachable, which is what "scroll all the way out to the world map and
the minimap" asks for. Captured at zoom 1, 3, 9 and 40 with grom's own settings and no setup
chunk: the world sheet with both continents, Eastern Kingdoms with its zones, Dun Morogh with its
neighbours stitched in, and the minimap's terrain drawn through the map.
## The rectangles: a 1.12 zone sheet is opaque, and Era's is not
With the stitch's geometry right, what was left at the zone zooms was hard rectangles. Measured
rather than guessed, with OpenMapClassic's own `/omc layer` switches as the bisect: turning
`baked` or `gaps` off changed nothing (neither draws on this build), turning `zoneart` off took
them all. So the rectangles are the per-zone sheets.
`PlaceBase` masks a zone's sheet to its land and says why: *"Full C_Map zone textures include
parchment borders and painted sea; without a land mask they draw as overlapping rectangles."* The
mask comes from `ZoneMask`, which answers only for ids in `S.ZoneMaskIDs` — the contiguous Classic
range **1411–1452**, forty zones. On this realm that leaves **thirty-four** without one: every
city, the dungeons, and every zone the realm added of its own (Gilneas, Hyjal, Alah'Thalas,
Tel'Abim, Grim Reaches, Thalassian Highlands, Northwind, Balor, Gillijim's Isle …).
Cities are already handled — the code declines to draw one with no outline mask, naming Stormwind
and Ironforge. The zones are not, and Gilneas is the clearest case: its whole 1002x668 sheet draws
over Silverpine, painted sea, "THE GREAT SEA" lettering, compass rose and a torn parchment border
and all.
### Why the feather was off, and why it belongs on here
`PlaceBase` takes a feather for exactly this and the zone walk passes `nil` for it, behind
`(not era) and FEATHER`. The reason is sound on Era: a `C_Map` zone texture there carries a
transparent surround, so a feather would do nothing. **It is not true of this client.** Measured
on the realm's own art: `Interface\WorldMap\AlahThalas\AlahThalas1.blp` and its siblings are alpha
255 across every texel. A 1.12 sheet is an opaque rectangle that paints its own sea.
So the port passes the feather. `PlaceBase` prefers the land mask when there is one, so the forty
masked zones are untouched — measured, 0.57% of pixels moved in a Dun Morogh view, all of it at
the edges where a neighbour now fades. Gilneas loses its border and blends into Silverpine.
### Generating the feather, and what was *not* generated
`scripts/make-map-feather.py` writes `MapFeather`, `MapFeatherThin` and `MapFeatherWide` as
256x256 uncompressed 32-bit TGA, the format the shipped masks use. A feather is a smoothstep ramp
in from each edge — upstream's own NOTICE calls these "generated from noise" rather than cut from
art, so this reproduces them rather than imitating an artist.
A per-zone **land** mask is a different thing and was deliberately not attempted. Upstream bakes
those by classifying painted sea against land by colour, and on this art the two do not separate:
sampled on the Gilneas sheet, a sea patch reads `R−B +28` and a land patch `+34`, overlapping
across the whole zone. The realm's own `<Continent>.zmp` is authoritative about territory but is
128x128 over a continent — the largest zone on Azeroth owns 169 cells, about 13x13, which is a
28-px block on a 256 mask. Either route would be a guess dressed as art. The feather is not: it is
the mask the addon already asks for.
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env python3
"""Generate OpenMapClassic's edge-feather masks.
A feather is a mask over a map sheet's whole rect: opaque in the middle, falling to nothing at
the border, so the sheet blends into what is around it instead of ending in a straight line.
Upstream's own NOTICE calls these "generated from noise" rather than cut from art -- they are
parametric, so this reproduces them rather than approximating an artist.
1.12 needs them where Classic Era does not. Era's `C_Map` zone textures carry a transparent
surround; the 1.12 sheets in this client's archives are **fully opaque** (measured: alpha 255
everywhere) and paint their own sea, lettering and a torn parchment border right to the edge. So
a zone drawn without a mask covers its neighbours with a rectangle -- which is what
`OpenMapClassic/Map/Stitch.lua`'s own comment predicts, and what the Gilneas sheet does over
Silverpine.
Writes 256x256 uncompressed 32-bit TGA, matching the shipped `MapZoneDilate` masks: RGB black,
alpha the coverage.
"""
import sys
from pathlib import Path
import numpy as np
from PIL import Image
SIZE = 256
# Margin as a fraction of the sheet, per variant. The parchment border on a 1002-px sheet runs
# ~35 px (3.5%); a feather has to clear that and then blend, so the plain one takes 9%.
VARIANTS = {"MapFeather": 0.09, "MapFeatherThin": 0.045, "MapFeatherWide": 0.16}
def ramp(n: int, margin: float) -> np.ndarray:
"""1 in the middle, 0 at both ends, smoothstep across `margin` of the span."""
x = (np.arange(n) + 0.5) / n
d = np.minimum(x, 1.0 - x) / margin # distance from the nearer edge, in margins
t = np.clip(d, 0.0, 1.0)
return t * t * (3.0 - 2.0 * t)
def feather(margin: float) -> Image.Image:
a = np.outer(ramp(SIZE, margin), ramp(SIZE, margin))
rgba = np.zeros((SIZE, SIZE, 4), dtype=np.uint8)
rgba[:, :, 3] = np.round(a * 255.0).astype(np.uint8)
return Image.fromarray(rgba, "RGBA")
def main() -> int:
if len(sys.argv) != 2:
print("usage: make-map-feather.py <OpenMapClassic/Media/Textures>", file=sys.stderr)
return 2
out = Path(sys.argv[1])
if not out.is_dir():
print(f"not a directory: {out}", file=sys.stderr)
return 2
for name, margin in VARIANTS.items():
path = out / f"{name}.tga"
feather(margin).save(path)
print(f"wrote {path} ({margin:.0%} margin)")
return 0
if __name__ == "__main__":
raise SystemExit(main())