hemisphere-small.mp4
Drives the Gakken WorldEye spherical display from a Raspberry Pi 3, or from a Windows PC (see Windows PC). The screen is a 180 degree dome lit from inside through a fisheye lens. The core here owns the lens calibration and everything else that is the same for every project, so a new visual is just a scene: a page or two of C and GLSL.
The Gakken WorldEye is an interesting product, and surprisingly little has been made for it. I wanted to drive mine from a Raspberry Pi 3 with my own visuals, starting with a Death Star. I chose the Pi 3 on purpose to optimise for cheap hardware (and I had one lying around).
The first version didn't work. It rendered the model with a normal camera and then fisheye-warped the image, leaving a small flat picture in the middle of the dome. No tuning could fix that; the approach was wrong. From photos of the real globe, I worked out the geometry and mapped every point straight onto the dome through a model of the lens. I built a calibration grid and settings to measure and tune the physical device. A strip of paper on the globe was the test equipment.
With the calibration right, I realised a framework others could use would be worth more than one Death Star, so I separated the two. Any new visual became a small scene file. At the time of writing it has eleven scenes (maybe more by now), live satellite and earthquake data, and a web remote.
It reflects how I work: reproduce the problem on the real hardware, find the root cause rather than tuning around it, test against measurements, and write it up (see Debugging notes).
I built it with AI-assisted tooling (Claude Code). I directed the work, tested every change on the real display, and did the calibration myself.
Nineteen scenes come with it. The first ten are on the number keys; the
rest are on , and . and the web remote.
| Key | Scene | What it shows |
|---|---|---|
| 1 or D | Death Star | Lit, panelled hull with the green grid, superlaser dish and trench drawn over it |
| 2 | Planet | Any planet map wrapped round the globe, with day/night shading and an atmosphere glow. With no maps installed it shows a procedural gas giant |
| 3 | Plasma | A flowing full-colour field in eight colour themes, and the template for new scenes |
| 4 | Julia | A Julia set fractal wrapped round the globe on the Riemann sphere, morphing as its constant walks the edge of the Mandelbrot set. Also the Mandelbrot set itself, in eight colour themes |
| 5 | Kaleidoscope | A mirrored folding fractal: 48 identical tiles, or classic wedges round the pole, slowly turning inside the mirrors, in eight colour themes |
| 6 | Game of Life | Life on a globe with no edges, under three sets of rules and in eight colour themes. Fresh soup drops in whenever it settles down |
| 7 | Moon tonight | The Moon filling the dome, lit to tonight's real phase, with its age and the next full and new moon written across the equator. No internet needed |
| 8 | Orbit watch | The live Earth with real day and night, the ISS and its next orbit, a few hundred satellites, and the last day's earthquakes |
| 9 | Groovy | 60s and 70s patterns: op art, psychedelic swirl, flower power, lava lamp, liquid light, tie-dye, paisley, spirograph |
| 0 | Sci-fi | Hologram globe, radar sweep (with real satellites if the orbit feed is running), hyperspace, planet scan, reactor core, ship's computer |
| , and . | Computers | Classic computing: a front panel of switches and lamps with a program being toggled in, oscilloscope figures, a scrolling assembler listing, a circuit board, core memory, a packet network round the globe, and a globe of blinking lamp panels |
| , and . | Hypnotic | Tesseract, quasicrystal, moire, Fibonacci globe, Truchet maze, hypno spiral, tunnel, pendulum, breathing hexagons, geodesic sphere |
| , and . | Sports balls | The globe as a ball, rolling: football, basketball, tennis, cricket, baseball, volleyball, golf, pool 8-ball, beach ball, bowling ball, and two plastic puzzles that solve themselves: a puzzle cube (2x2 to 5x5) and a megaminx |
| , and . | 8-bit retro | Chunky-pixel effects: digital rain, copper bars with a sine scroller, a rotozoomer, a maze chase, Rim defender, Asteroids on a globe, 16-colour plasma, tape loading (a random picture each time, or your own photos), pixel fire. The three games are full games that play themselves across the whole dome |
| , and . | Retro screen | 80s film and TV screens: a defence command globe with missile tracks, noughts and crosses played to a draw on 23 boards, a cyborg's visor, a ship's computer terminal, light-cycles on a neon grid, and a TV closedown |
| , and . | Weather | Today's clouds over the turning Earth (still or drifting), an infrared-style picture, a sharp view zoomed in over home, tonight's aurora forecast round the pole, and famous hurricanes and typhoons replayed from the record |
| , and . | Eyes | One big eye filling the dome, looking round the room, blinking, its pupil opening and closing: brown, blue, green and hazel human eyes, a cat, an owl, a reptile, and an original fire eye of glowing embers in an obsidian eyeball |
| , and . | Music | Built for sound from your WLED lights (see "Sound from WLED"): 21 styles: spectra, rings, ripples, a starfield, aurora, oscilloscope and Lissajous figures, a wire tunnel and a spectrum landscape, particle bursts, a galaxy, rain, fireworks, an 8-bit analyser, plasma, a kaleidoscope, liquid light and a ring of fire. With no music each style idles calmly |
| , and . | Now playing | What your HEOS amp is playing (see Now playing): the cover filling the dome, a turning record or CD, the track as a day with a moon, today's covers as an atlas of countries, the cover in dots dancing to the music, or a status display with the time, volume and up next. Moves lightly with your WLED lights |
Hemisphere grew out of my earlier worldeye program, a single-file Death
Star renderer. The lens calibration from that project is the core here,
and its settings files still load (see Settings).
See the gallery for each scene on the globe.
![]() Death Star |
![]() Planet |
![]() Plasma |
![]() Julia |
![]() Kaleidoscope |
![]() Game of Life |
![]() Moon tonight |
![]() Orbit watch |
![]() Groovy |
![]() Sci-fi |
![]() Computers |
![]() Hypnotic |
![]() Sports balls |
![]() 8-bit retro |
![]() Retro screen |
![]() Lens grid |
Photos of the real globe, taken in a dark room with the phone's exposure turned down a little so the colours don't burn out.
hemisphere/
main.c lists the scenes, in key order
Makefile
dome/dome.h the framework API (read this first)
dome/dome.c window, calibration, lens maths, settings, input
dome/glload.h, glload.c OpenGL ES functions on Windows (nothing to do on the Pi)
dome/shaders.h GLSL shared by every scene
dome/text.c dot-matrix text on the dome
dome/font5x7.h the font (X11 misc-fixed 5x7, public domain)
dome/audio.h, audio.c sound from WLED audio sync (self-contained)
scenes/deathstar.c 1 scenes/life.c 6
scenes/planet.c 2 scenes/moon.c 7
scenes/plasma.c 3 scenes/orbit.c 8
scenes/julia.c 4 scenes/groovy.c 9
scenes/kaleido.c 5 scenes/scifi.c 0
scenes/computers.c the rest are on , and .:
scenes/hypno.c, balls.c, retro.c, screen.c,
weather.c, eyes.c, music.c, nowplaying.c
third_party/stb_image.h JPEG/PNG loader (public domain, single header)
textures/ planet and moon maps go here
data/ live data from the orbit feed (created by it)
tools/prep_textures.py resizes maps for the Pi
tools/orbitfeed.py live satellite and earthquake data
tools/orbitfeed.service runs the feed at boot
tools/remote/app.py the web remote (Flask) and its API
tools/remote/playlist.py the remote's playlist (runs in the background)
tools/remote/static/ the remote's page
tools/remote/remote.service runs the remote at boot
tools/start-hemisphere.sh starts Hemisphere with the desktop and keeps it running
tools/requirements.txt Python packages for the tools
tools/audio/replay.py replays a WLED audio capture, for testing
tools/audio/billie.txt one minute of real audio sync data
tools/nowplaying/ what a HEOS amp is playing, for the dome (read only)
tools/windows/ start-up scripts for a Windows PC
deploy.sh copy to the Pi and build (Git Bash, WSL or Linux)
docs/screenshots/ photos for the gallery
calibration.cfg calibration (my WorldEye)
deathstar.cfg Death Star look
Tested on a Raspberry Pi 3 with 64-bit Raspberry Pi OS, SDL2 and OpenGL ES 2.0.
sudo apt install libsdl2-dev libgles2-mesa-dev
make
DISPLAY=:0 ./hemisphereThe first build takes roughly half a minute on a Pi 3, mostly for the image
loader; after that make only rebuilds what changed. At start-up the
scene fades in over two seconds.
The examples here use pi@raspberrypi.local and /home/pi; put your own
Pi's user and address in their place. From PowerShell in the hemisphere
folder, copy the code across (this leaves the settings you have saved on
the Pi alone):
scp main.c Makefile README.md pi@raspberrypi.local:~/hemisphere/
scp -r dome scenes tools third_party pi@raspberrypi.local:~/hemisphere/
scp textures\*.jpg pi@raspberrypi.local:~/hemisphere/textures/then on the Pi: cd ~/hemisphere && make && tools/start-hemisphere.sh --detach.
The first time, copy the whole folder instead (scp -r hemisphere pi@raspberrypi.local:~/). From Git Bash, WSL or Linux, ./deploy.sh does
all of this in one go: it copies the files (only copying settings files the
Pi doesn't already have), builds, and starts Hemisphere again in the
background, the same way it starts at boot (see "Running on its own"), so
it keeps running when the SSH session ends. Tell it where your Pi is:
PI=me@my-pi DEST=/home/me/hemisphere ./deploy.sh. If the Pi lost power
part way through a build, deploy.sh also clears out the empty files that
leaves behind, which make would otherwise take as up to date.
To adjust settings, stop the background copy first
(~/hemisphere/tools/start-hemisphere.sh --stop), then run
cd ~/hemisphere && DISPLAY=:0 ./hemisphere in the foreground in your SSH
session. It prints each setting as you change it, and you can press keys
right there in the SSH window. Press S to save and q to quit.
Quitting does not save. It starts on the Death Star; press G for the
calibration grids, and H for a list of keys. When you're done,
tools/start-hemisphere.sh --detach sets it running in the background
again.
Every scene can draw two ways, and the Death Star uses both.
Surface (full colour). The scene writes one GLSL function:
vec4 surface(vec3 dir, vec3 view)dir is the point on the globe in the model's own coordinates (it turns
as the model spins; +Y is north). view is the same point fixed to the
globe on your desk (+Z out of the front, +Y up), for lighting. The core
runs this once for every pixel of the dome, so every pixel is filled:
solid planets, gradients, textures.
Points. Dots placed on the unit sphere, or recessed into it, mapped onto the dome one by one. Use them for crisp lines, grids and overlays on top of a surface.
The projector's fisheye lens points straight out of the front of the dome. For a point at angle phi from the dome's centre (0 at the centre, 90 at the rim) and direction theta round it, the image radius is:
alpha(phi) = atan2(sin(phi), cos(phi) + offset)
rho(phi) = radius * alpha(phi) / alpha(90 deg) * corr(phi)
offset is how far the lens sits behind the centre of the globe (0 means
at the centre), radius is where the rim lands on screen, and corr is
the ring30/ring60 fine correction.
Points use this forwards in the vertex shader. Surfaces need it backwards:
for this pixel, which point of the dome is it? The core tabulates
rho(phi), inverts the table, and bakes the answer for every pixel into a
lookup texture whenever the calibration changes. Scenes never see the lens,
and dots and colour line up exactly because both come from the same
numbers. Press O in any scene to draw the globe grid over it and check.
The surface is rendered at res pixels across (640 by default) and
stretched onto the screen. That keeps the Pi 3 fast; raise res for a
sharper image if the frame rate holds up (DOME_FPS=1 ./hemisphere prints
it every ten seconds).
The planet scene uses equirectangular maps: twice as wide as tall, longitude -180 to +180 from left to right, north at the top. NASA's Blue Marble and the Solar System Scope set (Mars, Jupiter, Moon and so on) are good free sources. The Pi 3 can't load textures over 2048 pixels, so resize them first:
python3 -m venv venv
venv/bin/pip install -r tools/requirements.txt
venv/bin/python3 tools/prep_textures.py ~/Downloads/earth_8k.jpg ~/Downloads/mars.jpgThey land in textures/. The 2K maps from
Solar System Scope are already
2048x1024, so they can go straight in. In the planet scene, N and M
step through them in alphabetical order. The Moon scene uses any map with
"moon" in its name, and orbit watch uses the Earth day and night maps.
Night maps. A map with "night" in its name is never shown on its own.
If the current map has "day" in its name and a matching file with
"night" in its place exists, such as 2k_earth_daymap.jpg and
2k_earth_nightmap.jpg, its city lights glow on the dark side. The
lights setting sets how bright they are.
Settings are kept in files next to the program, in groups:
| File | Group | Adjusted in | What it covers |
|---|---|---|---|
calibration.cfg |
Calibration | Lens grid (G), globe grid (B) | How this WorldEye's lens fits the dome. Set once per device. |
view.cfg |
View | Any scene | Orientation and spin, shared by every scene |
deathstar.cfg |
Death Star | Death Star (1 or D) | The Death Star's look |
planet.cfg |
Planet | Planet (2) | Brightness, shading, atmosphere |
plasma.cfg |
Plasma | Plasma (3) | Theme, speed, scale, colour |
julia.cfg |
Julia | Julia (4) | Theme, Julia or Mandelbrot, shape, speed, detail |
kaleido.cfg |
Kaleidoscope | Kaleidoscope (5) | Theme, mirrors, fractal shape |
life.cfg |
Game of Life | Game of Life (6) | Theme, rules, grid size, speed, look |
moon.cfg |
Moon tonight | Moon (7) | Earthshine, rocking, text, text band |
orbit.cfg |
Orbit watch | Orbit watch (8) | View, home, what to show |
groovy.cfg |
Groovy | Groovy (9) | Style, speed, scale |
scifi.cfg |
Sci-fi | Sci-fi (0) | Style, speed, scale, radar range and centre |
computers.cfg |
Computers | Computers (, and .) | Style, speed, phosphor colour, text |
hypno.cfg |
Hypnotic | Hypnotic (, and .) | Style, colours, speed, scale |
balls.cfg |
Sports balls | Sports balls (, and .) | Ball, roll, shine |
retro.cfg |
8-bit retro | 8-bit retro (, and .) | Style, speed, pixel size, pixel gaps |
screen.cfg |
Retro screen | Retro screen (, and .) | Style, speed, text |
eyes.cfg |
Eyes | Eyes (, and .) | Style, speed, how far it looks, blinks, lids, pupil size, shine |
weather.cfg |
Weather | Weather (, and .) | Style, home, zoom, clouds, coasts, storm speed |
music.cfg |
Music | Music (, and .) | Style, sound on or off, how strongly it reacts, speed, colours |
nowplaying.cfg |
Now playing | Now playing (, and .) | Style, sound on or off, how strongly it reacts, speed, colours, rim text, dots |
In a scene, TAB steps through the six view settings first (pitch,
tilt, wobble, rev_secs, res, view_dist) and then that scene's
own, so press TAB six times to reach a scene's first setting. Calibration
is only reachable from the grids, so you can't knock it out while styling.
S saves all the files. R resets the group you are in: on a grid,
the calibration; in a scene, the view settings and that scene's own. L
reloads what you last saved. Settings a file has that the program no
longer uses are ignored, and the next save drops them.
Coming from worldeye. If calibration.cfg or deathstar.cfg is
missing, the old worldeye.cfg and look.cfg are read instead, including
the view settings that used to live in look.cfg. The first save writes
the new files, and the old ones can then be deleted. To move an existing
install on the Pi, copy the two old files into /home/pi/hemisphere.
View settings:
| Setting | What it does |
|---|---|
pitch |
Tips the model towards you, moving the pole from the top edge of the dome to the top of the globe |
tilt |
Leans the axis sideways (0 upright, 23 Earth-like) |
wobble |
Slow nodding as it turns, in degrees (0 for steady) |
rev_secs |
Seconds per full turn |
res |
Colour resolution across the dome: lower is faster, higher is sharper |
view_dist |
How far away you stand, in globe radii. Sets how strongly recesses such as the dish show depth |
display |
Which screen the WorldEye is on: 0 is the main one. Only matters on a PC with more than one screen (V steps through them) |
The Moon, and the text in the Moon and orbit watch, follow pitch and
tilt but never spin. The lava lamp, the sunshine face, smiley rain and rainbow face, radar, hyperspace and ship's computer
are drawn square to the dome, ignoring the view settings.
Death Star settings: the scene opens with the wireframe zooming out from a single dot at the centre of the dome until it fills the globe, then the hull fades in; Z replays this and C cycles the wireframe colour.
| Setting | What it does |
|---|---|
style |
0 wireframe only, 1 hull and wireframe, 2 wireframe fades to hull after wire_out seconds, 3 hull only |
phosphor |
Wireframe colour: 0 green, 1 orange, 2 blue, 3 white (C cycles) |
lines |
Wireframe brightness. 0 gives a solid model with no grid |
dot_size |
Wireframe line thickness in pixels (N/M thinner/thicker); the dots always join up |
grid_deg |
Spacing of the wireframe grid in degrees |
dish_dep |
Depth of the superlaser dish, as a fraction of the globe's radius (0.10 is like the film; I/K shallower/deeper) |
hull |
Brightness of the solid hull |
detail |
Small hull detail: sub-plates, raised blocks, grooves (0 for plain plates) |
windows |
Brightness of the small lit windows, seen best on the dark side |
intro |
Zoom out from a dot when the scene starts (1 is on; Z replays it) |
intro_s |
Seconds the zoom out takes |
wire_out |
Style 2 only: seconds after the start when the wireframe fades away |
audio |
1 makes the wireframe bend to music from your WLED lights (see "Sound from WLED"); 0, the default, leaves it as it is |
warp |
How far it bends, 0 to 1 (0.4 to start). The bass swells the whole sphere out and back, the snare body sends ripples out from the dish, each beat rolls a wave down from the pole, and each line of latitude wobbles with its own band. Only the lines bend: in the hull styles they move a little over the still hull |
The hull's panelling, panel size, seams, dish position and pole dimming are now Mark's chosen look, fixed in the code rather than adjustable settings.
Planet settings: bright, sun (0 lit all over, 1 sharp night side),
night (how much of the night side shows), atmos (rim glow), bands
(gas giant only), lights (city lights on the night side).
Colour themes. Plasma, Julia, the kaleidoscope and Life share eight
colour themes, and N steps through them: rainbow, fire, ice, neon,
aurora, gold, amber and jewel (stained-glass colours with dark leading). In
Life the first theme is its own classic colouring instead of rainbow. The
hue setting, where a scene has one, slides the colours along within a
theme.
Plasma settings: theme, speed, scale, bands, sat, bright.
Julia settings: theme, set (0 Julia, 1 Mandelbrot; M switches),
edge (under 1 gives solid shapes, over 1 lacy dust), speed, iter
(detail; more is slower, 32 to start), zoom, hue, bands (how fast the colours
cycle), glow (brightness inside the set). Infinity sits at the north
pole, so the top of the globe is the dark outside of the set. The
Mandelbrot set is the map of every Julia set: it stays still while the
globe turns it.
Kaleidoscope settings: theme, style (0 cube mirrors, 1 wedges;
M switches), mirrors (wedges, style 1), fold (the fractal's shape:
tiny changes give very different patterns, so step it with Shift for fine
steps), depth, speed, hue, sat, bright.
Game of Life settings: theme, rule (M steps through them, and
reseeds: 0 Conway's original B3/S23; 1 HighLife B36/S23, which adds
self-copying replicators; 2 Day & Night B3678/S34678, big blobs that grow
and shrink), cells (cells along each cube face edge; the globe has six
faces, so 48 gives 13,824 cells. Changing it reseeds), speed (generations
per second), density (how full a reseed is), gap (space between tiles),
trail (how long dead cells leave a ghost), glow, hue. Newborn cells
are the brightest, cooling as they age. C reseeds the whole globe. The
globe is laid out as an inflated cube, so at its eight corners three faces
meet and a cell has seven neighbours instead of eight.
Moon settings: shine (earthshine on the dark side), rock (a
slow, gentle rocking, like the Moon's real libration), text (0 hides the
text), band (how dark the band behind the text is), bright. The Moon
fills the whole dome, facing you, with north at the top of the globe as set
by the view settings (pitch, tilt). Uses any map with "moon" in its name
from textures/. Four lines across the equator give the phase and how much
is lit, the Moon's age, and the next full and new moon in local time. They
are worked out from the Pi's clock (Meeus, "Astronomical Algorithms", good
to a few minutes) and update every minute. To preview another night, start
it with DOME_DAYS=5 ./hemisphere for five days from now.
Orbit watch settings: view (0 turning, 1 following the ISS, 2 held
over home; N cycles), home_lat and home_lon (home for view 2),
sats and quakes (brightness; 0 hides them), lights (city lights),
text, band (how dark the band behind the text is), bright. Four
lines across the equator give the ISS's position, altitude and speed, and
the quake count and biggest magnitude. The text stays put while the Earth
turns, and satellites and quakes passing behind it are hidden so it stays
clear. The ISS is the bright pulsing marker, with its next orbit
in dashed cyan ahead of it and a short white trail behind. Satellites are
coloured by type: yellow space stations, white bright satellites, green GPS,
blue weather satellites. Earthquakes pulse as rings, yellow under M4, orange
to M6, red above; the last six hours are brighter. Home starts at Greenwich;
set home_lat and home_lon to your own location.
Groovy settings: style (N steps through them: 0 op art, 1
psychedelic swirl, 2 flower power, 3 lava lamp, 4 liquid light, 5 tie-dye,
6 paisley, 7 spirograph), speed, scale (bigger gives a finer pattern),
bright. Tie-dye winds five dyes into a spiral round a fold; every 40
seconds a new fold is dyed and spreads over the old one. The spirograph
draws four gear patterns out to the rim, showing the ring and the rolling
gear as it goes, then fades and starts a fresh sheet. The patterns turn
with the globe, except the lava lamp and spirograph, which face you. (The
style numbers changed in October 2026, when 70s wallpaper, mood ring,
mirror ball and glam stars were dropped; an old groovy.cfg just opens on
a different style.)
Computers settings: style (N: 0 front panel, 1 oscilloscope, 2
code listing, 3 circuit board, 4 core memory, 5 packet network, 6
blinkenlights), speed, phosphor (M: one screen colour for every
style, 0 green, 1 amber, 2 white, or 3 full colour, the default), text (labels
and readouts), bright. It has no number key: press , or . to reach
it, or pick it on the web remote. The front panel's switches are toggled
one at a time to load an address and deposit a short made-up program, word
by word, which then runs with the lamps flickering before it halts and
starts again. Core memory, blinkenlights and the packet network turn with
the globe; the rest face you.
Hypnotic settings: style (N: 0 tesseract, 1 quasicrystal, 2 moire,
3 Fibonacci globe, 4 Truchet maze, 5 hypno spiral, 6 tunnel, 7 pendulum, 8
breathing hexagons, 9 geodesic sphere), theme (M: 0 each style's own
colours, 1 to 8 the shared themes), speed, scale, bright. The tesseract
is a 4D cube turned through the fourth dimension and projected twice; the
quasicrystal is five plane waves at 36 degree steps, a pattern that never
repeats. The Fibonacci globe spreads 610 seeds evenly over the sphere, each
a golden angle round from the last, which forms crossing spirals of 21 and
34. The pendulum traces rosettes over the whole globe, and the geodesic
sphere has a coarser one turning the other way inside it, drawn recessed
so it shows depth. The moire, Fibonacci globe, pendulum and geodesic turn
with the globe; the rest face you.
Sports balls settings: style (N: 0 football, 1 basketball, 2
tennis, 3 cricket, 4 baseball, 5 volleyball, 6 golf, 7 pool 8-ball, 8 beach
ball, 9 bowling ball, 10 puzzle cube, 11 megaminx), roll (tumbling end
over end on top of the spin), bright, shine (the highlight), size (the
puzzle cube's layers: 2 to 5, or 1 for a different size each time it mixes
itself up). The puzzles are blown up
into a ball, glossy stickers on black plastic: they start mixed up, solve
themselves a turn at a time, glint when they're done, then mix themselves
up again. The megaminx is the twelve-sided puzzle, with straight-edged
pieces. (The "solve" is a replay: each puzzle undoes its own mixing moves in
reverse rather than working out a solution, and mixes itself with enough
turns to look properly mixed for its size.) The football is the real solid, a
truncated icosahedron: every point belongs to whichever face plane it
reaches first. The tennis and baseball seams follow the curve where height
equals x squared minus y squared.
8-bit retro settings: style (N: 0 digital rain, 1 copper bars, 2
rotozoomer, 3 maze chase, 4 Rim defender, 5 Asteroids, 6 16-colour
plasma, 7 tape loading, 8 pixel fire), speed, scale (pixel size),
bright, grid (show the gaps between pixels). The three games are
full games, each played by a small robot in C, with lives, a score and high
score, levels that get harder, and GAME OVER then a fresh game.
-
Tape loading: a home-computer screen loading off cassette, done the way the real thing worked. The border shows the red and cyan pilot tone, then the blue and yellow data;
Program: NAMEappears; then the picture arrives in black on white in the real screen-memory order (in thirds, one pixel line of every character row at a time), and the colours flood in last. Each 8x8 cell has only two colours, so the colours clash at the cell edges just as they did. Every load is a different picture, shuffled so none repeats until all have been shown: eleven made-up ones built in (lighthouse, castle, night city, pyramids, mountains, island, galleon, spaceman, explorer, wizard, racer), plus any photos you put in atapesfolder next to the program (.jpg,.png,.bmpor.gif; the file name becomes the tape name; about 1024 pixels across is plenty, as big files take a moment to read on a Pi 3). Photos are converted when they load, so nothing needs preparing. Thetapesfolder is not committed. -
Rotozoomer: the demo-scene classic. A made-up pixel tile (a framed diamond gem on a chequer) turns and zooms in and out while its colours cycle through the 16-colour palette. One small shader, no dots.
-
Maze chase: an original maze game. A little round robot clears the dots while four spiky chasers hunt it, each in its own way (one chases, one heads it off, one flanks, one wanders). Power cells turn them blue for a while so the robot can eat them; bonus prizes appear twice a level. The robot stays in the middle of the dome and the maze slides round it, repeating left and right so the side tunnel carries straight on. Levels get faster and there is an extra life at 10,000. The hidden treat: clear level 4 and level 5 is the planet maze, the whole maze laid over a small dark planet that turns under the robot, with every point doubled. The robot is an expert: it tries each way a couple of seconds ahead, with the chasers moving as they really would, so it usually gets well past level 10 before it slips up.
-
Rim defender: the ship circles the rim with rapid fire, shooting inwards at aliens that spiral out from a hive at the centre in groups and fire back. The robot leads its shots and dodges incoming fire.
-
Asteroids: the whole globe is the playfield, so rocks and shots travel round the sphere and come back over the horizon, and the view follows the ship. Big rocks split into medium and then small ones; saucers turn up and shoot (the small one aims). The robot aims ahead, thrusts clear of trouble and jumps to hyperspace in a tight spot. An extra ship every 10,000 points.
The aliens, ships and pictures are made up for this program.
Retro screen settings: style (N: 0 defence command, 1 noughts
and crosses, 2 cyborg HUD, 3 ship's terminal, 4 light grid, 5
closedown), speed, text (the readouts round
the rim), bright. It has no number key: press , or . to reach it, or
pick it on the web remote. Defence command draws the coastlines from the
Earth map in textures/ (made-up continents if there isn't one), holds the
globe over the North Atlantic so Europe and North America face you, and runs
a 66-second simulation from DEFCON 5 to 1 that ends with no winner. In
noughts and crosses both sides play perfectly, so every game is a draw; the
score sits under the middle board, and at the end the boards dim and the
verdict comes up in the middle. The
light-cycles are four small robots that turn towards open space; one that
hits a trail crashes, its trail fades and it comes back. The closedown runs
colour bars, a test card, the caption, static, then the picture collapses to
a dot. Text in this scene is drawn by the shader from a font texture rather
than as dots, which keeps it quick on the Pi. All original designs in the
spirit of the era.
Eyes settings: style (N: 0 brown, 1 blue, 2 green, 3 hazel, 4 cat,
5 owl, 6 reptile, 7 fire eye), speed (how lively it is), look (how far
it glances round; 0 stares straight at you), blink (0 or 1), lids (0 the
bare eyeball, 1 with its lids: skin, fur, feathers, scales or rock),
pupil (smaller or wider than it rests), shine (the wet sheen and the
window reflected in the cornea), bright. For realism on a Pi 3, the
detail is worked out once when the style changes (about a fifth of a second
on a PC): the iris fibres, crypts and the ring round the pupil, the veins
in the white, and the lids go into three textures, and the shader only
looks them up and adds the light. The eye flicks to a new spot and holds
there, blinks every few seconds (sometimes twice), and its pupil drifts
wider and narrower. The fire eye is an original design: a round pupil in an
iris of streaming embers, an obsidian eyeball with lava veins, dark rock
with glowing seams for lids, heat shimmer and rising sparks.
Music settings: style (N, 21 styles, listed below), audio (1,
the default here, reacts to music from your WLED lights; 0 shows only the
calm idle), react (how strongly it responds), speed, theme (M:
the eight shared colour themes, then 8 album: the colours of what your
HEOS amp is playing, see Now playing), bright. Every style has a slow idle of
its own for when no music is playing, and the change between the two is
eased over about a second. Nothing flashes: beats swell, move and light
small areas, and sound changes the overall brightness by 15% at most.
| Style | What it shows |
|---|---|
| 0 spectrum ring | 16 bars standing in from the rim, bass at the bottom and treble at the top; loud bass reaches nearly to the middle |
| 1 pulse globe | A glowing ball in the middle swelling with the bass and the beat, its halo following the treble |
| 2 ripples | Each snare hit sends a ring out from the middle |
| 3 starfield | Stars streaming out from the middle, faster with the bass, stretching into streaks (the stars are soft fixed tints; the theme colours the glow behind them) |
| 4 aurora | Curtains of light across the top, swaying in time, further when the snare is loud |
| 5 radial bars | 64 thin bars standing out from the middle, their tips glowing a little more on the beat |
| 6 oscilloscope | A trace round the rim in which every band drives its own ripple, bass long and slow, treble fine and fast, with fading after-images (WLED sends no waveform, so the trace is made from the levels) |
| 7 Lissajous | A turning figure, a quarter turn a beat, sized by the bass, morphing to a new shape every 20 seconds |
| 8 wire tunnel | Rings flying out of the middle on the bass, bending with it and bulging on the beat |
| 9 terrain | A wireframe landscape below the horizon, each row a spectrum as it played, scrolling towards you |
| 10 bursts | Each beat sends a ring of particles out from the middle |
| 11 dot globe | A sphere of dots turning with the globe, breathing with the bass, warming with the treble |
| 12 galaxy | Two spiral arms spinning with the tempo, the core glowing with the bass |
| 13 rain | Streaks falling faster and thicker with the treble, splashing at the bottom rim |
| 14 fireworks | Beats launch shells that burst into drifting sparks |
| 15 8-bit spectrum | Chunky LED columns with peak caps that drop slowly |
| 16 bass plasma | A flowing colour field, faster with the bass |
| 17 kaleidoscope | Six mirrored folds turning slowly, the zoom breathing on the beat |
| 18 colour tunnel | Flying down a tunnel of colour on the bass |
| 19 liquid light | Five soft blobs of coloured oil, mixing where they meet, moving with the snare body |
| 20 fire ring | Flames rising from the rim, each part burning with its own band, flaring with the bass and throwing sparks on beats; embers when idle (fire keeps its own colours) |
Every style holds 60 frames a second on a Pi 3. Each has its own small shader, the costly maths is baked into textures or worked out once a frame on the CPU, and lines and particles are dots.
Now playing settings: style (N, 6 styles, listed below; the
now-playing tool picks one to suit the source), audio (1, moves lightly
with the music from your WLED lights), react, speed, theme (M;
8, album, the cover's own colours, is the default), bright, text (1
shows the title and artist round the rim) and dots (style 4: 96 or 128
across). It needs tools/nowplaying running (see Now playing from a
HEOS amp); without it every style shows
its idle look in the current colours. Between tracks the globe turns
half a revolution: the new cover comes in from behind the horizon and the
old one goes round the back.
| Style | What it shows |
|---|---|
| 0 album globe | The cover filling the whole dome, centre to rim, swaying a little and breathing with the bass; the title round the top of the rim, artist and album (or station) round the bottom |
| 1 record planet | The globe as a record facing you: grooves, a sheen that stays with the light and shimmers with the mids, the cover as the label turning at 33 1/3 rpm (stripes in your colours on phono, which sends no cover), a tonearm moving in through the track; it spins down on pause. For a CD, a silver disc with a rainbow sheen and a label in your colours. Labels swell slightly on the beat |
| 2 track as a day | The cover lit by a sun crossing it through the track, dawn to dusk; lights across the night side twinkle with the treble, and a moon orbits once per track, glowing on the beat |
| 3 album atlas | Today's covers tiled over the globe like countries; it turns to bring each new one to the front, the coastlines rippling with the bass |
| 4 art into particles | The cover as 128 x 128 dots, each moving with its band of the music and settling back into the picture between phrases |
| 5 track info | Title, artist and album, play state and time, a progress ring, up next, volume, shuffle and repeat, and the source with a small icon, with a thin spectrum ring round the rim |
All six hold 60 frames a second on a Pi 3 (measured at 58.5 to 60 with the CPU throttled to 600 MHz by a weak power supply).
Weather settings: style (N: 0 live clouds, 1 infrared, 2 over
home, 3 aurora, 4 storms), speed (the drift and shimmer), home_lat and
home_lon, zoom (over home: 1, the default, shows the whole side of
the Earth round home; up to 6 zooms in), drift (clouds still or drifting with
the prevailing winds: trade winds west, westerlies east, a made-up motion
between the real three-hourly pictures), cloud_op (how thick the clouds
look), coast (coastlines glowing through the clouds), lights (city
lights), storm_speed (hours of storm per second), storm_zoom (1, the
default, shows the whole side of the Earth; up to 4 zooms in on the
storm), text, bright. It
has no number key: press , or . or pick it on the web remote. It needs
tools/orbitfeed.py running (see below) for its data.
- Live clouds: the Earth turning with real day and night and city lights, the latest clouds lit by the Sun, coastlines glowing through.
- Infrared: the same clouds coloured like a forecaster's enhanced infrared picture, brightest and highest tops in red and white. It is a look made from how bright the clouds are, not measured temperatures.
- Over home: zoomed in on home (
home_lat,home_lon; orbitfeed reads them fromweather.cfgand cuts out the sharper picture round them), with a marker on home and a faint 5-degree grid. - Aurora: looking down on the North Pole, the aurora oval from NOAA's 30-minute forecast, shimmering green (red where it is strongest), brighter on the night side.
- Storms: one famous storm after another (Gilbert, Andrew, Katrina, Haiyan, Patricia, Irma, Maria, Ophelia, Dorian, Lorenzo, Idai, Ian, Milton and more) from NOAA's IBTrACS archive. The view follows the storm as its track is drawn, coloured by strength from depression to category 5, with the name, category, wind and date.
Sci-fi settings: style (N steps through them: 0 hologram globe,
1 radar sweep, 2 hyperspace, 3 planet scan, 4 reactor core, 5 ship's
computer, with five rings of hex readouts out to the rim), speed, scale, range (radar range in km), home_lat and
home_lon (the radar's centre), text (readouts on or off), bright.
The radar shows the real satellites passing within range of home when
orbitfeed is running (north up, east right), and simulated contacts
otherwise, and says so. Hyperspace runs a 24-second loop: cruise, spool
up, jump, then drop back to normal space. The radar's home starts at
Greenwich, like orbit watch's; set yours here too.
Orbit watch needs tools/orbitfeed.py running. It downloads satellite orbits
from CelesTrak twice a day and earthquakes from the USGS every five
minutes, works out every satellite's position every five seconds with the
SGP4 orbit model, and writes small files to data/ that the scene reads.
It also fetches the data for the Weather scene: a map of the world's clouds
every three hours (2048 x 1024, plus a sharper 4096 x 2048 one, cut down
to the area round home, which needs Pillow), NOAA's aurora forecast every
half hour, and, about once a month, the famous storms picked out of NOAA's
best-track archive. That last one is a big download, so it runs in the
background and the storms style says so until it is done. Start the feed
with --no-weather to skip all of it.
If the network drops, it carries on with what it last downloaded. Without
the feed, or if its data is more than a minute old, the scene still shows
the Earth and says "NO LIVE DATA" across the equator. The radar in the
sci-fi scene uses the same data. Orbits come from
CelesTrak (space stations, bright satellites, GPS
and weather satellites) and earthquakes of magnitude 2.5 and over from the
USGS feed for the past day.
cd ~/hemisphere
python3 -m venv venv
venv/bin/pip install -r tools/requirements.txt
venv/bin/python3 tools/orbitfeed.py # try it: Ctrl+C to stop
sudo cp tools/orbitfeed.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now orbitfeed # start now and at every boot
journalctl -u orbitfeed -f # watch what it is doingThe service file assumes the program lives in /home/pi/hemisphere and
runs as pi; edit User= and the paths to match your Pi. --once updates everything once
and exits, which is handy for checking the network.
Hemisphere can react to music, using the sound analysis your WLED lights
already share. In WLED's AudioReactive settings, set one device (the one
with the microphone) to Send under Audio Sync, V2 format. Hemisphere
picks the packets up by itself: they go to the multicast group 239.0.0.1
on port 11988, so no IP addresses are needed. It prints
Audio: listening on 239.0.0.1:11988 at start-up.
Scenes only react if their audio setting is 1. It is 0 (off) for every
scene that isn't built for music.
| Variable | Effect |
|---|---|
DOME_AUDIO_PORT=n |
Listen on another port (WLED's default is 11988); 0 turns sound off |
DOME_AUDIO_GROUP=a.b.c.d |
Listen to another multicast group (WLED's is 239.0.0.1) |
DOME_AUDIO_DEBUG=1 |
Print the levels, beat, tempo and packet rate twice a second |
The web remote's /api/state shows the same values under audio.
What it hears. Of WLED's 16 bands, Hemisphere makes four smooth levels, each scaled to the loudest it has heard lately: bass (bands 0-2), body (3-6, the weight of the snare), mid (7-12, usually quiet) and air (13-15, the snare and hi-hat). In most music the bass never stops, so a loud bass is not a beat; the beat is a sharp jump in the air bands. From the beats it works out the tempo. If the packets stop for a second, everything eases back to nothing.
Testing with no music. tools/audio/billie.txt is one minute of real
audio sync data. Replay it as live packets:
python3 tools/audio/replay.py # this machine only
DOME_AUDIO_DEBUG=1 ./hemisphere # in another terminal
The tempo should settle at about 117 BPM. By default the replay stays on
the machine it runs on; --ttl 1 sends it across your network (to test the
Pi from a PC), but then your WLED lights will dance to it too. If a WLED
device is already sending, Hemisphere keeps following that one, so give
the replay its own port: --port 11989 with DOME_AUDIO_PORT=11989.
Making a scene react. Add an audio setting (0 off, 1 on), and in
draw(), between dome_surface_begin and dome_surface_end, call
dome_audio_uniforms(prog, f, L.audio). Then declare the values you want
in the GLSL: uniform float u_bass, u_body, u_mid, u_air, u_level, u_beat, u_phase, u_live;. f->audio has them all on the CPU too, with the tempo
and the 16 bands. Two rules:
- For speed that follows the music, keep your own clock and add to it each
frame (
t += f->dt * speed * (1.0f + bass)) and pass that in, rather than multiplyingu_timeby a changing speed, which jumps. - Never flash the screen. Let beats swell sizes, move things and light small areas, and ease anything big.
dome/audio.c and audio.h need nothing from the rest of Hemisphere, so
they can be lifted into another project.
Credit. The sound comes from WLED's AudioReactive Audio Sync (V2). No WLED code is included: the packet format follows WLED's public Sound Sync docs and was checked against live packets.
If your amplifier or streamer speaks HEOS (Denon and Marantz network
models), tools/nowplaying/nowplaying.py follows what it is playing and
hands it to the dome:
- the cover, downloaded once, cached in
data/nowplaying/cache/and resized to 512 x 512 (data/nowplaying/art.rgba, read straight into a texture). The Pi 3's GPU only mipmaps power-of-two sizes, covers come at 300 to 640 pixels, and the front of the globe is about 600 pixels across, so 512 is the useful size. Covers stay on the Pi and are never committed. - the palette: four colours from the cover. Music's theme 8, album, is built from them, so any Music style takes on the album's colours. With no cover it falls back to a neutral set.
- the details (title, artist, album or station, source, and what the
HEOS queue plays next) in
data/nowplaying/now.txt, the progress about once a second, and the volume, mute, repeat and shuffle as they change. - switching: when music starts it shows the Now playing scene in a
style that suits the source (the record planet for phono and CD, the
album globe for the rest; Music, in the album colours, if there is no
Now playing scene), and when it has been paused or stopped for five
minutes it puts back the scene and style that were there. It never switches within ten minutes of
you changing scene by hand (keyboard or remote), never over a calibration
grid, and if you change scene while it has the dome it leaves the dome
alone until the music has stopped for a while. The playlist waits while
the music has the dome and carries on afterwards.
--no-autoturns switching off.
It is read only: it only asks the amp what it is playing (and its
volume, mute, play mode and queue) and listens for changes. It cannot play, pause, skip, change the volume or the input:
anything else is refused before it is sent (ALLOWED in heos.py).
What each source does is in tools/nowplaying/sources.json: streaming
services show the cover, radio shows the station's picture if there is
one, phono is a record (no cover needed), the CD player on the coaxial
input (or the analogue CD input) is a disc (keeps the current colours),
other inputs show what
there is, and so does anything unknown. A source set to release (none
by default; a TV input, say) gives the dome back after ten seconds.
Inputs are shown by a short label ("Phono", "CD") rather than the amp's
own " - Coaxial In". For inputs where the amp cannot tell
whether music is playing (a
turntable, a CD player), sound from your WLED audio sync counts as
playing. Copy the file, change it and pass it with --config to suit
your own inputs. The times are in there too.
cd ~/hemisphere
venv/bin/pip install -r tools/nowplaying/requirements.txt
HEOS_HOST=<your amp's address> venv/bin/python3 tools/nowplaying/nowplaying.pyLeave out HEOS_HOST and it searches the network. HEOS_PLAYER (or
--player) picks one player by name if you have several, --once prints
what is playing and stops, and --dry-run prints the dome commands
instead of sending them. To run it at boot, put the address in
/etc/default/hemisphere-nowplaying (as HEOS_HOST=...), not in the repo:
sudo cp tools/nowplaying/nowplaying.service /etc/systemd/system/hemisphere-nowplaying.service
sudo systemctl daemon-reload
sudo systemctl enable --now hemisphere-nowplaying
journalctl -u hemisphere-nowplaying -fControl commands it uses, for anyone writing their own feed (UDP on
127.0.0.1:5005, one per packet): palette RRGGBB RRGGBB RRGGBB RRGGBB
(brightest first, or palette off), np <serial> (new now.txt and
art.rgba are ready), progress <pos_ms> <dur_ms> <play|pause|stop>,
volume <0..100> <on|off> (the level, and muted or not), mode <off|on_all|on_one> <on|off> (repeat and shuffle), and scene <n> <by>, where by labels who changed the scene (state reports
it as by, with since in seconds). Scenes read it all with
dome_nowplaying() in dome/dome.h.
Testing without the amp. tools/nowplaying/fakeheos.py is a fake
HEOS device that plays made-up tracks with made-up covers drawn at run
time; point the tool at it with HEOS_HOST=127.0.0.1:1255. The tests use
only the fake and recorded, anonymised answers, and fail if anything
tries to reach the network:
venv/bin/pip install -r tools/nowplaying/requirements-dev.txt
venv/bin/python -m pytest tools/nowplaying/tests--record DIR --seconds N saves what your amp says as new test fixtures,
with player names, addresses, serial numbers, model, network and any
account details replaced, then checks the file for each of those values
and prints found or not found.
Credit. The protocol follows the HEOS CLI Protocol Specification published by Denon (HEOS is a trademark of its owners). No HEOS code is included.
A phone-friendly remote control page, so you don't need a keyboard or SSH:
scene buttons, the N, M and C keys, pause, faster and slower, save, and a
slider for every setting of the scene you're on. Numbered choices show
their names (theme 1 · fire). It also shows the frame rate and whether
the orbit feed is live.
Playlist. The remote can run the dome by itself, moving through a list
of scenes and styles. Build the list on the page: pick a scene and a style
and press Add, or Add all its styles, or Add everything; tick
items on and off, reorder them with the arrows, and give any item its own
time. Then choose the order (each in turn, shuffle, which shows each
item once per round, or random), how long each item stays up, how much
to vary that by at random, and how long to wait after you pick something by
hand before carrying on (0 to carry straight on). Start playlist and it
keeps going with no browser open, and picks up again after a reboot; Back
and Skip move along now. The list is saved in playlist.json in the
hemisphere folder as you edit it. It sets the style without saving it, so
press Save only if you want to keep what's showing.
cd ~/hemisphere
venv/bin/pip install -r tools/requirements.txt # adds Flask
venv/bin/python3 tools/remote/app.py # try it: Ctrl+C to stopThen open http://raspberrypi.local:5000 on your phone. To run it at boot:
sudo cp tools/remote/remote.service /etc/systemd/system/hemisphere-remote.service
sudo systemctl daemon-reload
sudo systemctl enable --now hemisphere-remoteHow it works. Hemisphere listens for commands on 127.0.0.1:5005
(UDP), which only programs on the Pi itself can reach. The Flask app serves
the page on port 5000 and passes commands through. Anyone on your home
network can open the page and there's no login, so keep port 5000 off the
internet. DOME_CTL_PORT changes the port (0 turns it off).
API, for scripts or Home Assistant:
| Call | Body | Does |
|---|---|---|
GET /api/state |
Current scene, its settings with ranges and hints, fps, orbit feed age, sound levels | |
POST /api/scene |
{"scene": 2} |
Switch scene, counting from 0 (-2 lens grid, -1 globe grid) |
POST /api/key |
{"key": "n"} |
Press a key: letters, digits, , ., space, up, down |
POST /api/setting |
{"name": "speed", "value": 2.5} |
Set a setting of the current scene or view |
POST /api/save |
Save all settings files, like pressing S | |
GET /api/catalog |
Every scene, with the setting that picks its style (style, view or theme) and its range | |
GET /api/playlist |
The playlist and what it's showing now | |
POST /api/playlist |
the whole playlist | Replace it (the format is described in tools/remote/playlist.py) |
POST /api/playlist/skip |
{"step": 1} |
Next item now (-1 goes back) |
For example: curl -X POST -H 'Content-Type: application/json' -d '{"scene":6}' http://raspberrypi.local:5000/api/scene
switches to the Moon.
Once it's set up you don't need an SSH session open. Hemisphere starts with
the Pi's desktop (it needs the screen, which a background service can't
easily get), and the orbit feed and web remote run as services. After a
reboot all three come back by themselves. The examples use the pi user
and /home/pi; put your own in their place.
The web remote and orbit feed as services. The service files in
tools/ assume User=pi and /home/pi/hemisphere. If yours differ, this
installs a copy with your user and folder filled in:
cd ~/hemisphere
for s in tools/remote/remote.service:hemisphere-remote tools/orbitfeed.service:orbitfeed; do
sudo sh -c "sed 's#/home/pi/#$HOME/#g; s#User=pi#User=$USER#' ${s%%:*} > /etc/systemd/system/${s##*:}.service"
done
sudo systemctl daemon-reload
sudo systemctl enable --now hemisphere-remote orbitfeed
systemctl status hemisphere-remote orbitfeed --no-pagerHemisphere with the desktop. Raspberry Pi OS's labwc desktop runs the
commands in ~/.config/labwc/autostart when it logs in. Hemisphere starts
from there through tools/start-hemisphere.sh, which waits until the
desktop's display is ready, restarts Hemisphere if it ever stops, and logs
to ~/hemisphere/hemisphere.log (the run before is kept as
hemisphere.log.1). No sudo is needed. Check what's already there, take
out any older Hemisphere line, then add it:
cat ~/.config/labwc/autostart 2>/dev/null
mkdir -p ~/.config/labwc
echo "bash $HOME/hemisphere/tools/start-hemisphere.sh &" >> ~/.config/labwc/autostartThe Pi needs to log in to the desktop by itself at boot (sudo raspi-config,
System Options, Boot / Auto Login, Desktop Autologin).
Start it now without rebooting, so it keeps running after you close
the SSH session (deploy.sh does this for you after each build):
~/hemisphere/tools/start-hemisphere.sh --detachLiving with it:
- Keys typed over SSH only reach Hemisphere when you started it in that
SSH session. Running on its own, use the web remote instead. For a long
keyboard session, such as calibrating, run
~/hemisphere/tools/start-hemisphere.sh --stop(a plainpkill hemisphereonly makes it restart) and start it in the foreground in SSH as before;--detachputs it back in the background afterwards. cat ~/hemisphere/hemisphere.logshows what Hemisphere printed, if something seems wrong. If it says./hemisphere is missing or empty, a build was cut short: runmakeagain, ordeploy.sh.- If the Pi resets or freezes during a build, check its power supply:
vcgencmd get_throttledshowing anything other thanthrottled=0x0means it has run short of power. Use a 5.1 V 2.5 A supply. - To stop it starting at boot, delete its line from
~/.config/labwc/autostart; for the services,sudo systemctl disable --now hemisphere-remote orbitfeed. - If another program on the Pi already uses port 5000 (
sudo ss -tlnpshows it), run the remote on another port: addEnvironment=REMOTE_PORT=5001under[Service]in its service file, thensudo systemctl daemon-reloadand restart it.
The same code runs on a Windows 10 or 11 PC, scenes and all. Windows has no
OpenGL ES library, but NVIDIA, Intel and AMD drivers offer the same
functions, so the core fetches them from the driver at start-up
(dome/glload.c) and nothing above it changes. If a driver will not give
an OpenGL ES context, Hemisphere falls back to desktop OpenGL 2.1 and
adjusts the shaders to suit; the start-up line says (desktop fallback)
when that happens. It needs a real graphics driver, not the "Microsoft
Basic Display Adapter".
A PC has far more graphics power than a Pi 3, so turn things up: res
1024 or more for a sharper dome, and Julia's iter back to 128. Settings
files are per machine, so this doesn't touch the Pi's.
Install MSYS2, open the MSYS2 UCRT64 terminal from the Start menu, then:
pacman -S --needed make mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-SDL2
cd /d/portfolio/hemisphere # your copy of the repo
make
./hemisphere.exemake builds hemisphere.exe and copies SDL2.dll next to it. Those two
files are all it needs, so the folder can be copied to another PC without
MSYS2. Press ESC in the dome window, or Ctrl+C in the terminal, to quit.
For the orbit feed and web remote, make the Python venv from PowerShell in the repo folder:
py -m venv venv
venv\Scripts\pip install -r tools\requirements.txtPlug the WorldEye into a spare HDMI output and set Windows to Extend
these displays (Settings, System, Display). Hemisphere opens full screen
on display 0, the main one. Press V until it moves onto the WorldEye,
then S to save; the choice is the display view setting, also on the
web remote. Clicking on your other screen doesn't minimise the dome.
Check the calibration on the lens grid (G) the first time. The calibration is in fractions of the screen, so it carries over from the Pi if the WorldEye gets the same resolution, but a different HDMI mode moves things slightly.
With the dome on a second screen, keys only reach Hemisphere when its window has focus, so the web remote is the easy way to drive it. The terminal key reader the Pi uses over SSH isn't on Windows.
tools\windows\start-hemisphere.ps1 is the Windows counterpart of the
Pi's services: it starts Hemisphere with its window hidden, plus the orbit
feed and web remote from the venv, restarts any that stop, and writes logs
to logs\. ESC on the dome stops the lot. To start it at sign-in, run once
from an administrator PowerShell in the repo folder:
powershell -ExecutionPolicy Bypass -File tools\windows\install-autostart.ps1That adds a scheduled task, Hemisphere, and a firewall rule letting
phones on your home network reach the web remote on port 5000 (Private
networks only; -Port 5050 if you changed REMOTE_PORT, -Remove to undo
it). Then open http://<pc-name>:5000 on your phone.
For a PC that comes back by itself after a power cut:
- Sign in automatically. The dome needs a signed-in desktop. Microsoft's free Autologon tool sets this up and stores the password encrypted.
- Never sleep or blank the screen. Settings, System, Power: set screen and sleep to Never, and turn off any screen saver.
Don't start Hemisphere from a Remote Desktop session. A program started under RDP draws to the remote screen, not the PC's HDMI outputs, so the dome stays dark. Closing RDP the normal way also locks the session, which blanks the dome until someone signs in at the PC.
If you do RDP in for admin, leave by running tools\windows\rdp-to-console.bat
as administrator instead of closing the window. It hands your session back
to the PC's own screens, still signed in, and the dome carries on. Remote
tools that share the real screen, such as VNC or Chrome Remote Desktop,
don't have this problem.
On the Pi's keyboard:
| Key | Action |
|---|---|
| 1 to 9, 0 | Death Star, planet, plasma, Julia, kaleidoscope, Game of Life, Moon, orbit watch, groovy, sci-fi (D also gives the Death Star) |
| , and . | Previous / next scene |
| G / B | Lens grid / globe grid (adjust calibration) |
| TAB / Shift+TAB | Next / previous setting in the current group |
| LEFT / RIGHT | Adjust the selected setting (hold Shift for fine steps) |
| O | Draw the globe grid over the current scene |
| V | Move to the next screen (a PC with the WorldEye as a second display) |
| X | Mirror left/right |
| T | Spin the current grid on/off |
| S or F5 | Save all settings files |
| L | Reload all settings files |
| R | Reset the current group to defaults |
| P / H | Print all settings / the key list |
| SPACE | Pause rotation |
| UP / DOWN | Faster / slower rotation |
| N | Next colour theme (plasma, Julia, kaleidoscope, Life), style (Groovy, Sci-fi, Computers, Hypnotic, Sports balls, 8-bit retro, Retro screen, Weather, Music, Now playing), map (planet) or view (orbit watch). Death Star: thinner lines |
| M | Hypnotic, Music, Now playing: next colours. Computers: phosphor colour. Julia: Julia / Mandelbrot. Kaleidoscope: mirror style. Life: next rules. Planet: previous map. Death Star: thicker lines |
| C | Life: reseed. Death Star: cycle phosphor colour |
| I / K | Death Star: dish shallower / deeper |
| Z | Death Star: replay the intro zoom |
| ESC | Quit |
In the SSH window: UP/DOWN pick a setting, LEFT/RIGHT adjust it, [
and ] make fine adjustments, +/- change rotation speed and q quits.
Letter and number keys, and , and ., work as above.
Lens grid (G). Rings round the centre of the dome every 15 degrees: white at 30 and 60, yellow at 90 (the rim). Twelve spokes, with the up spoke yellow and the right spoke red, and a small cross at the centre.
Globe grid (B). Lines of latitude and longitude, as a globe seen from the front: yellow equator and front meridian, red back meridian.
What "correct" looks like. On the lens grid the rings should be round, centred on the dome, and evenly spaced along the surface. Measure with a strip of paper from the centre: centre to 30, 30 to 60 and 60 to the rim should all match.
Press G and TAB through the settings, in this order:
- centre_x, centre_y. Put the centre cross in the middle of the dome.
- aspect. Make the rings round.
- radius. Put the yellow 90 degree ring on the rim of the dome.
- offset. If the 30 and 60 rings are crowded towards the centre, increase it; if crowded towards the rim, decrease it. The rim won't move.
- ring30, ring60. Nudge either ring if it's still a little off.
- turn. Rotate until the yellow spoke points straight up. If the red spoke is on the left, press X to mirror.
- cutoff. Raise it until nothing is drawn past the rim. 90 is right for a true half globe.
- T spins the lens grid: the rings should stay still.
- B for the globe grid: check the equator is level.
- S to save, then 1 for the Death Star. Set
pitchfirst, to put the pole at the top of the globe (pause with SPACE to judge it).
Copy scenes/plasma.c, which is about 50 lines. julia.c shows a scene
that animates its own uniforms in update(). A scene is:
- A struct of settings and a
Paramtable describing them (name, step, range, a hint). The core handles TAB, adjusting, printing, saving and loading. - Optionally, a
surface()function in GLSL, built withdome_surface_program()and drawn betweendome_surface_begin()anddome_surface_end(). Set your own uniforms in between; texture units 1 to 6 are yours (the core keeps the last one, 7, for the themes).dome_surface_program_themed()adds ready-made noise helpers and the shared colour themes (theme(id, x)); seeplasma.c. The themes are a table drawn once at start, so every theme costs one texture read;dome_theme_rgb()gives the same colours on the CPU, for dots. - Optionally, dots:
dome_points_init,dome_points_line,dome_points_upload, thendome_draw_pointseach frame. - Optionally, text:
dome_text_blockfor lines across the equator (it returns the size of the block, for a band behind it),dome_text_linefor one line, ordome_text_arcround the rim. Draw text withdome_view_rot()so it sits the right way up and doesn't spin. - A
Scenestruct naming its settings file and callbacks.
Then add it to the list in main.c and to SRC in the Makefile. Ten
scenes fit on the number keys; up to 24 can be reached with , and .
(DOME_MAX_SCENES in dome/dome.h). A
different project, such as a planet projector on its own, is just a new
main.c listing its own scenes. dome/dome.h documents every call.
Keep it quick on the Pi 3. surface() runs for every pixel, every
frame. Layers of noise are the expensive part: the first liquid light and
reactor core stuttered for that reason, and were rewritten from a handful
of sin() waves, which look much the same at a fraction of the cost.
Lowering res is the quick fix for any scene.
These environment variables are for testing and previewing:
| Variable | Effect |
|---|---|
DOME_FPS=1 |
Print the frame rate every ten seconds |
DOME_MODE=n |
Start on a scene, counting from 0 (so DOME_MODE=6 is the Moon, scene 7); -2 is the lens grid, -1 the globe grid |
DOME_DAYS=n |
Move the Moon scene's clock n days on (or back, if negative) |
DOME_OVERLAY=1 |
Start with the globe grid drawn over the scene (as if O had been pressed) |
DOME_SHOT=n |
Save frame n to shot.ppm and quit |
DOME_AUDIO_DEBUG=1 |
Print the sound levels, beat and tempo twice a second (see "Sound from WLED") |
DOME_THEME_CHECK=1 |
At start, draw every colour theme both ways (its formula and the lookup table) and print the largest difference: about 0.008, a little more at jewel's sharp edges |
- Surfaces and the lens. Running the lens formula backwards in the shader for every pixel would cost several trig functions per pixel and wouldn't agree exactly with the ring corrections. Baking it into a lookup texture on the CPU (a fraction of a second, only when calibration changes) makes surfaces cheap and exact.
- Settings file location. Settings files are kept beside the program
(via
realpath()), so launching from another folder doesn't lose them. - F5 on compact keyboards. Some small keyboards send
AC Searchinstead of F5. Both are handled, and S saves as well. - Dish depth. Recessed dots are placed along the line of sight from a viewer in front of the globe, which gives real parallax. Near the edge of the dome the near rim of the bowl hides the dots behind it.
- Grid squares vanishing. A straight chord between two points on the globe dips inside it, so wide grid squares used to look recessed and got hidden. Every line dot is pushed back out to the surface.
- Bright poles. All the meridians meet at the poles, so they are thinned out above 60 and 75 degrees.
- Calibrating from a PC. The program window only sees the Pi's own keyboard, so keys typed in the SSH window are read separately and fed in as the same key presses.
- Slow scenes. Liquid light, the reactor core, the hologram globe and
the planet scan were too slow on the Pi 3 because they stacked noise for
every pixel. All four now use sine waves. Julia's
iternow starts at 32 rather than 64, and the Death Star's motion trail is one faint copy rather than two. The newer scenes give each style its own small shader, so only the one on screen costs anything. If another style stutters, lowerres, or rebuild its pattern from sine waves the same way. - The original two-pass design. Version one filmed the model with a normal camera and fisheye-warped the picture. A camera only sees the front of a ball, so the warp put a small flat picture in the middle of the dome. Mapping every point straight onto the dome fixed it, and the surface lookup texture uses the same idea for colour.
- Planet and Moon maps: Solar System Scope, licensed CC BY 4.0.
- Image loading: stb_image by Sean Barrett, public domain.
- Dot-matrix font: the X11 misc-fixed 5x7 font, public domain.
- Moon phases: Jean Meeus, "Astronomical Algorithms".
- Satellite orbits from CelesTrak, worked out with the sgp4 library; earthquakes from the USGS.
- Live clouds: Matt Eason's live cloud maps (CC0). Contains modified EUMETSAT data.
- Aurora forecast: NOAA Space Weather Prediction Center (OVATION model).
- Storm tracks: NOAA NCEI IBTrACS, version 4.
- Now playing: the HEOS CLI Protocol Specification (Denon); covers and palettes with Pillow (MIT-CMU licence). Album covers belong to their owners and are only shown live, never stored in the repo.
Built with AI-assisted tooling (Claude Code), the now-playing tool included.















