#!/usr/bin/env python3 """Soften OpenMapClassic's per-zone land masks, so neighbouring sheets blend instead of butting. `Media/Textures/MapZoneDilate/.tga` is one 256x256 alpha mask per Classic outdoor zone, Voronoi-dilated so that the forty of them tile each continent with no gap. They are **binary**: measured over four of them, 19k-48k pixels at alpha 0, 12k-51k at alpha 255, and under 250 pixels anywhere in between -- the anti-aliasing of a traced outline and nothing more. A binary mask is why grom's map "looks super boxy and weird, edges of maps are the same". Every zone's sheet, and the shade over an unexplored one, ends on a crisp line against its neighbour, and the two sheets are different paintings of the same land at different brightnesses. Nothing blends because nothing was ever asked to. So: keep the mask fully opaque everywhere it already was, and let it fall off *outwards* over a few pixels. `DILATE` pushes the coverage out so the ramp starts at the old boundary rather than eating into the zone, and `BLUR` is the ramp. Where two zones meet, the one drawn second fades in over the one drawn first across roughly `BLUR` mask pixels -- about a dozen screen pixels at the zoom where a continent fills the window, which is the zoom the complaint came from. Outward, not symmetric, on purpose: a symmetric blur would take both neighbours to 50% at their shared border and let the sea-grey underlay show through as a bright seam -- trading a hard edge for a worse one. Reads `MapZoneDilate`, writes `MapZoneSoft`; the source folder is left alone so this is re-runnable and the two can be compared. 256x256 uncompressed 32-bit TGA either way: RGB black, alpha the coverage, as the shipped masks are. """ import argparse import sys from pathlib import Path import numpy as np from PIL import Image, ImageFilter # Mask pixels. A zone mask is 256 px across its own rect however far the map is zoomed, so these # are a fraction of a zone, not a count of screen pixels: 3 + 4 is a ramp a little under 3% of a # zone's width. DILATE = 2 BLUR = 3.0 # How far in from the sheet's own border the softened coverage is held at zero, as a fraction of # the sheet. A 1.12 zone sheet paints its own torn parchment frame and its own sea right to the # edge of its rect, and the shipped masks are cut so that none of that shows. A blur alone pulls # it back in -- measured: softening with no inset put a hard straight line of Wetlands' parchment # border across the view where the binary mask had a soft coast. So the ramp is multiplied by a # rect ramp that reaches zero at the border, and the neighbour's own (dilated) mask covers the # land the inset gives up. RECT_MARGIN = 0.05 def rect_ramp(n: int, margin: float) -> np.ndarray: """1 in the middle, 0 at both ends, smoothstep across `margin` of the span.""" if margin <= 0: return np.ones(n) x = (np.arange(n) + 0.5) / n t = np.clip(np.minimum(x, 1.0 - x) / margin, 0.0, 1.0) return t * t * (3.0 - 2.0 * t) def soften(src: Path, dilate: int, blur: float, margin: float) -> Image.Image: im = Image.open(src).convert("RGBA") a = im.split()[-1] # MaxFilter is a square dilation; its size must be odd and is the full width, not the radius. if dilate > 0: a = a.filter(ImageFilter.MaxFilter(2 * dilate + 1)) if blur > 0: a = a.filter(ImageFilter.GaussianBlur(blur)) w, h = im.size cover = np.asarray(a, dtype=np.float32) / 255.0 cover = cover * np.outer(rect_ramp(h, margin), rect_ramp(w, margin)) out = np.zeros((h, w, 4), dtype=np.uint8) out[:, :, 3] = np.round(cover * 255.0).astype(np.uint8) return Image.fromarray(out, "RGBA") def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("textures", type=Path, help="OpenMapClassic/Media/Textures") ap.add_argument("--from-dir", default="MapZoneDilate") ap.add_argument("--to-dir", default="MapZoneSoft") ap.add_argument("--dilate", type=int, default=DILATE) ap.add_argument("--blur", type=float, default=BLUR) ap.add_argument("--rect-margin", type=float, default=RECT_MARGIN) args = ap.parse_args() src = args.textures / args.from_dir dst = args.textures / args.to_dir if not src.is_dir(): print(f"not a directory: {src}", file=sys.stderr) return 2 dst.mkdir(parents=True, exist_ok=True) n = 0 for path in sorted(src.glob("*.tga")): soften(path, args.dilate, args.blur, args.rect_margin).save(dst / path.name) n += 1 print(f"wrote {n} mask(s) to {dst} (dilate {args.dilate}, blur {args.blur}, rect margin {args.rect_margin})") return 0 if __name__ == "__main__": raise SystemExit(main())