Files
decePubClient/tools/globe/prepare-day-texture.py
T
thepraandClaude Opus 5.5 69d0a641fe The globe draws the poles as they are, and nothing while idle
The globe is a UV sphere: towards the poles its triangles narrow to
slivers, and texture coordinates interpolated across them pinched the
map into a star around each pole. Each pixel now computes its own
coordinates from its exact point on the sphere, with seam-safe mipmap
gradients; the map wraps horizontally and gets 8x anisotropic
filtering.

That showed a flaw in the day texture: NASA's mosaic has a lighter
strip at its east edge near the poles, a line from each pole once drawn
exactly. The day texture is now built from NASA's original Blue Marble
(December 2004) by tools/globe/prepare-day-texture.py, which blends
that strip into the map where both sides of the seam agree; same
4096x2048 size, provenance and hashes in SOURCES.md.

globe.gl redrew 60 times a second even when nothing moved; it now draws
only during camera flights, hover pulses, auto-rotation and the
reader's drags, so an idle globe costs no GPU time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LsXgEaXee4GCU1hwYgPJXw
2026-10-04 17:46:44 +02:00

62 lines
2.9 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
# Builds wwwroot/vendor/globe/earth-blue-marble.jpg from NASA's Blue Marble Next Generation (topography and bathymetry,
# December 2004, public domain), downloaded once:
# https://eoimages.gsfc.nasa.gov/images/imagerecords/73000/73909/world.topo.bathy.200412.3x5400x2700.jpg
# usage: tools/globe/prepare-day-texture.py <that jpg> <output jpg> (needs Pillow; not part of the build)
#
# NASA's mosaic does not wrap cleanly: towards the poles a strip at its east edge is lighter than the map on both sides
# of the 180° meridian, which the globe draws as a line from each pole. On every row where the map agrees on both sides
# of the seam but the strip around it does not, the strip is replaced by a blend between the two sides. A real feature
# on the meridian (Fiji, the Ross Ice Shelf) makes the two sides differ, so its rows are left alone. Within half a degree
# of each pole a row is a circle a few kilometres across, so it gets one colour, its median, which removes the strip
# there too. Then the map is scaled to 4096×2048 (the globe's texture size) with Lanczos.
import sys
from PIL import Image
BAND = 16 # pixels on each side of the seam that may be repaired
ANCHOR = 4 # pixels averaged just outside the band, on each side
AGREE = 24 # the two sides agree when their mean channels differ by less than this
ODD = 30 # a band pixel is an artefact when it differs from the blend by more than this
POLAR = 0.5 # degrees from a pole within which a row is one colour
def mean(pixels):
return tuple(sum(p[i] for p in pixels) / len(pixels) for i in range(3))
def distance(a, b):
return sum(abs(x - y) for x, y in zip(a, b)) / 3
source, target = sys.argv[1], sys.argv[2]
image = Image.open(source).convert("RGB")
width, height = image.size
pixels = image.load()
repaired = 0
for y in range(height):
west = mean([pixels[width - BAND - 1 - i, y] for i in range(ANCHOR)])
east = mean([pixels[BAND + i, y] for i in range(ANCHOR)])
if distance(west, east) >= AGREE:
continue
columns = [width - BAND + i for i in range(BAND)] + list(range(BAND))
blend = [tuple(w + (e - w) * (i + 1) / (2 * BAND + 1) for w, e in zip(west, east)) for i in range(2 * BAND)]
if max(distance(pixels[x, y], b) for x, b in zip(columns, blend)) <= ODD:
continue
for x, b in zip(columns, blend):
pixels[x, y] = tuple(round(c) for c in b)
repaired += 1
polar = 0
for y in range(height):
latitude = 90 - (y + 0.5) * 180 / height
if abs(latitude) < 90 - POLAR:
continue
row = [pixels[x, y] for x in range(width)]
median = tuple(sorted(p[i] for p in row)[len(row) // 2] for i in range(3))
for x in range(width):
pixels[x, y] = median
polar += 1
image.resize((4096, 2048), Image.LANCZOS).save(target, "JPEG", quality=90, optimize=True, progressive=True)
print(f"{repaired} of {height} rows repaired at the seam, {polar} polar rows made one colour; wrote {target}")