Commit Graph
171 Commits
Author SHA1 Message Date
Richard Zink 07017cfac6 Add tile borders and NS-mirrored corner labels (burned in)
User wants:
- Border around each tile so they can see where one ends
- Labels in the four corners, NS-mirrored (so N is at the
  bottom and S at the top, matching the tile's own NS axis)
- Labels burned in to the canvas so they always show the tile
  identity during flight (not just a live overlay)

Implementation:
- Thin dark border (rgba(0,0,0,0.55), 2px) via strokeRect just
  inside the tile bounds
- S at top-left, N at bottom-left (NS mirrored) in large bold
  text with white fill + black stroke for contrast
- y=NNN at top-right, x=NNN at bottom-right in smaller monospace
- All labels drawn directly on the canvas before texImage2D
  upload, so they're part of the texture the shader samples
2026-10-08 08:20:33 +02:00
Richard Zink 29924d07b0 Mirror O/W arrangement: east tiles to canvas right, west to left
User wants only the O/W arrangement mirrored (image X-flip
stays as it was). The drawing formula for canvasX now uses
(-perp + halfPerp + 0.5) instead of (perp + halfPerp + 0.5),
so tiles with perp<0 (world east, dx>0) land at large canvasX
(right side of canvas). Y axis (forward direction in N flight)
stays unchanged.
2026-10-08 08:15:49 +02:00
Richard Zink acac6dccfc NS mirror of tile image only (X-flip, no rotation)
User wants the tile texture mirrored across the NS axis only
(no 180 rotation). drawImage with width=-TILE, height=+TILE
flips only the X axis:
- Image top (north) stays at canvas top
- Image bottom (south) stays at canvas bottom
- Image left (west) -> canvas right
- Image right (east) -> canvas left

Labels N/S moved to match the new (un-Y-flipped) orientation:
N at canvas top, S at canvas bottom. The tile arrangement in
the corridor is unchanged.
2026-10-08 08:11:23 +02:00
Richard Zink 2f7c09fc16 Add O/W mirror: 180° rotation (image content only)
User wants the O/W axis mirrored within each tile. With
the previous Y-only flip, East was still on the right and West
on the left. Adding an X-flip makes it 180° rotation:
- Image North -> canvas bottom, South -> canvas top
- Image East -> canvas left, West -> canvas right

Labels N/S were already positioned at canvas bottom/top to match
the Y-flipped image, so they stay where they are. Tile arrangement
in the corridor is unchanged (still controlled by canvasX/canvasY
formulas with no swap on either axis).
2026-10-08 08:08:43 +02:00
Richard Zink 5ba7596ced Mirror tile content across NS axis (image + N/S labels only)
User feedback: the corridor arrangement is correct (lng=+12
at screen top in N flight), but the per-tile image and labels
were upside down relative to expectations.

- Image drawing: changed from 180° rotation (-TILE, -TILE on
  both axes) to Y-only flip (TILE, -TILE on Y). Image top
  (north) now lands at canvas bottom; image bottom (south) at
  canvas top. X axis is preserved so image east is still on the
  right within each tile.
- N/S labels: swapped so N is at canvas bottom (= image north)
  and S at canvas top (= image south).
2026-10-08 08:06:27 +02:00
Richard Zink 91ff8fb9d7 Bigger corridor + refetch-based continuous scroll
The per-frame redraw caused texImage2D upload of 8MB every frame,
which the browser couldn't keep up with -> tiles disappeared.

Reverted to refetch-based updates but with two key changes:
- Corridor 25 -> 50 tiles long (~6 min of flight at uSpeed=1
  before refetch)
- Tiles redraw with fractional camera position, so the corridor
  tracks the camera smoothly within the loaded content

The 50-tile corridor covers enough flight time that refetches
happen rarely (~6 min), but the camera's continuous position
is still reflected in the mosaic's center. So between refetches
the visible content scrolls smoothly.
2026-10-08 08:04:32 +02:00
Richard Zink 4f8fd4b62a Smooth ground scroll: redraw corridor every frame
Replaced refetch-based updates with continuous per-frame redraw.
The corridor now tracks the camera's continuous world position
(floating point, not floored to a tile) so the tile content
scrolls smoothly underneath the camera. No more 'jump by 1 tile'
at every refetch boundary.

Tradeoff: every frame draws ~65 images and does one texImage2D
upload. Browser caches the Image elements, so the cost is mainly
compositing + upload (~5MB at 6400x1280). Should still hit 30+ fps.

This is closer to how real flight sims work: the world content
scrolls continuously, not in tile-size jumps.
2026-10-08 07:59:11 +02:00
Richard Zink 23327a1cba Rotate tile images 180 degrees
drawImage(img, canvasX + TILE, canvasY + TILE, -TILE, -TILE)
flips the image both horizontally and vertically, so:
- Image north (high lat) ends up at canvas bottom (was top)
- Image south (low lat) ends up at canvas top
- Image east (high lon) ends up at canvas right
- Image west (low lon) ends up at canvas left

Combined with NO canvasY swap and the WebGL flip:
- North tiles drawn at canvas bottom -> after WebGL flip, still
  at screen bottom. Screen bottom has image north = high y. So
  y at screen bottom = high.
- Looking forward (close ground = screen bottom in cockpit) =
  high y. y increases in flight direction.

With NO canvasX swap:
- East tiles (high x) drawn at canvas left
- Image east at canvas right within each tile (high x)
- Image west at canvas left within each tile (low x)

So at screen left: east tiles with image x increasing left to
right (low to high within tile). At screen right: west tiles with
image x decreasing left to right. Overall: x at screen right (west
tiles, low x label) lower than at screen left (east tiles, high x
label). x decreases left to right.
2026-10-08 07:49:35 +02:00
Richard Zink 73920d0ce8 Both axes without swap: UNPACK flip for Y only
User clarified:
- x must decrease left to right (east at screen left, high x left)
- y must decrease in flight direction (south at front, low y in flight dir)

With no swap on canvasY (lng+halfLong+0.5 formula) and UNPACK
flip=true, canvas top -> texture top -> screen top. canvas bottom
-> screen bottom.

So:
- lng=+12 (north, ahead in N flight) -> canvasY=6272 (bottom)
  -> screen bottom. User at screen bottom sees north tiles = high y
- south tiles at canvas top -> screen top. User at screen top sees
  south tiles = low y. y decreases in flight direction (looking
  forward = top of screen = low y).

With no swap on canvasX:
- perp=-2 (east, dx=+2) -> canvasX=128 (left)
- After WebGL (no X flip): east at screen left, west at screen right.
  x decreases left to right (east at left, west at right).
2026-10-08 07:41:14 +02:00
Richard Zink d063e5e864 Revert canvasX swap: east on canvas left (matches user's x gradient)
User feedback: x decreasing left-to-right (matches NO swap), y
increasing in flight direction (matches swap). So the swap on
canvasY is right but the swap on canvasX was wrong direction.

With the no-swap canvasX formula:
- perp = -2 (east, dx=+2) at canvasX = 128 (left)
- perp = +2 (west, dx=-2) at canvasX = 1152 (right)

After UNPACK flip: canvasX stays as-is. User at canvas right sees
west (low x). User at canvas left sees east (high x). x decreases
left-to-right in the label content, matching what the user sees
in the satellite imagery too.

(The Y swap remains so lng=+12 -> canvasY=128 -> screen top, so
north tiles still appear at the top of the screen.)
2026-10-08 07:33:56 +02:00
Richard Zink 4cf3c00dfa Partial refetch: only draw new tiles, don't clear canvas
With BUFFER_LONG = 1, refetch fires every ~1 tile of motion (every
~8 sec at default speed). With BUFFER = 1 the center shifts by
~1 tile per refetch, so only ~1 net new tile needs fetching each
time (the one at the very front of the new corridor).

Switch to partial refetch:
- Don't clear the canvas before redrawing
- Skip tiles already in groundLoadedTiles (they're still on the
  canvas at the right position from a prior refetch)
- Only newly-positioned tiles are drawn (the ones in the new
  front of the corridor, ahead in flight direction)

Effect: user sees tiles appearing at the front of the corridor in
the direction of travel, not the whole canvas flashing every 8 sec.
With N flight, fetch activity shows up at the top of the screen
(where lng=+12 lands after the axis swap), confirming the flight
direction is correct.
2026-10-08 07:23:14 +02:00
Richard Zink bfe0a559b5 Re-apply NS+EW swap (user feedback: mirror needs fixing)
User reported 'N at close tiles' and 'NS mirrored, EW reversed'.
With the canvas axes swapped:
- lng=+12 (corridor front in N flight) at canvas top -> screen
  top -> ahead in flight direction (correct)
- perp<0 (world east) at canvas right -> screen right (correct)

Verifies user expectation: north tiles ahead (screen top), east tiles
to the right (screen right). N label appears at the far-ahead tiles
along the top of the screen, pointing forward in flight direction.
2026-10-08 07:06:01 +02:00
Richard Zink eeb2e3e5f6 Revert canvas axis swap, try original drawing formula
User reports NS axis mirror with current swap. Try the original
formula (canvasY = (lng + halfLong + 0.5)*TILE_SIZE) without
swapping. UNPACK_FLIP_Y_WEBGL stays true so text still reads
right-side up; just see if the user's visual expectations match
this simpler arrangement.
2026-10-08 07:02:01 +02:00
Richard Zink 2fd394a865 Right-side-up text: flip UNPACK flag + swap canvas axes
UNPACK_FLIP_Y_WEBGL=true was correct for screen orientation but the
shader's rotated-frame math expected it false. To get both right:

- Re-enable UNPACK_FLIP_Y_WEBGL=true so canvas top maps to texture
  V=1 (screen top). Text now reads right-side up.
- Invert the drawing axes: canvasY = (-lng + halfLong + 0.5)*TILE_SIZE
  so lng=+12 (corridor front, ahead) lands at small canvas Y, which
  after upload ends up at texture V=1, i.e. at screen top.
  Same inversion for perp.
- Shader's gUv.y = rotated.x / uGroundSize.y + 0.5 is unchanged:
  for offset.z=+12 (north) it samples V=0.98 -> texture row 6271
  -> canvas row 128 (the new top-of-corridor row) -> lng=+12. Correct.

Effect: N labels read upright, image orientation matches world
directions, and corridor scrolls in the visible flight direction.
2026-10-08 06:51:46 +02:00
Richard Zink a05951efbd Fix NS mirror: UNPACK_FLIP_Y_WEBGL was double-flipping
With the flag on, canvas row Y was uploaded to texture row (H-1-Y).
Sampling at V=0.58 then returned canvas row 2688 (in the 'south'
half of the corridor), but the shader expected row 3712 (in the
'north' half) for an offset.z=2 sample. So tiles appeared
mirrored across the NS axis — every N pointed south, every S north.

With the flag off, canvas row Y maps to texture row Y, and the
shader's expectation matches the drawing direction. Now N is at
the image's actual north, S at the image's actual south.
2026-10-08 06:48:10 +02:00
Richard Zink 6e96c5ffad Debug labels on each tile: N/S markers + x/y coords
For verifying the corridor arrangement, draw labels on every
loaded tile (per drawTileAt):
- 'N' at the canvas top edge of the tile (= image north after UNPACK_FLIP_Y_WEBGL)
- 'S' at the canvas bottom edge (= image south)
- 'x=NNN y=NNN' at the centre with the actual ESRI tile coords

This lets you see at a glance which canvas direction maps to which
world direction. Combined with #flight=N|... and #fetch=N|... you
can describe exactly what is wrong (e.g. N pointing in screen-up
direction when it should point at the corridor front).
2026-10-08 06:43:46 +02:00
Richard Zink c059d56f34 Fix Passau tile coords: 1099->1100, 706->707
Computed precisely: zoom 11, lat 48.5667, lon 13.4319.
x_norm = (lon+180)/360 = 0.5373, x_tile = 1100.41 -> floor 1100
y_norm = 0.3452, y_tile = 707.07 -> floor 707

Previously had 1099/706 which placed the centre one tile west and
one tile south of Passau. User observed the tile content was
'vertauscht' — it was correct ESRI imagery of a different geographic
location (near Schärding), not Passau itself.
2026-10-08 06:39:03 +02:00
Richard Zink 97dd480b21 Fix shader: remove leftover window.__flightDir references
Old shader code with fdx/fdz still referenced window.__flightDir,
which GLSL can't parse. Removed those lines. Now shader uses only
uFlightDir uniform passed from JS.
2026-10-08 06:23:22 +02:00
Richard Zink e17076a273 Fix shader: pass uFlightDir as uniform instead of window access
The shader had the JS code 'window.__flightDir.dx' baked into the
GLSL string, which GLSL can't parse. Added a uFlightDir uniform
(vec2) and pass the dx/dz from JS via gl.uniform2f. Shader reads
uFlightDir.x and uFlightDir.y.
2026-10-08 06:18:56 +02:00
Richard Zink f21a967643 Separate flight and fetch directions via URL params
Two independent URL parameters:
  #flight=N|NE|E|SE|S|SW|W|NW  (camera motion direction)
  #fetch=N|NE|E|SE|S|SW|W|NW   (tile collection direction)

Example: #flight=E&fetch=N -> camera flies east, corridor built
north (perpendicular to flight). Lets you test what happens when
the corridor orientation doesn't match the camera's flight path.

URL parsing supports & and ; as separators, plus multiple keys
in one hash. window.__flightDir and window.__fetchDir are exposed
separately so JS code can react to each.
2026-10-07 21:51:24 +02:00
Richard Zink 9027099d89 Add #flight=N|NE|E|SE|S|SW|W|NW URL parameter for corridor direction
Adds 8 compass directions for the tile-build axis. Camera position,
corridor rotation, and refetch math all read from window.__flightDir.

Test URLs:
  #flight=N   north  (default, +Z, current behaviour)
  #flight=E   east   (+X)
  #flight=S   south  (-Z)
  #flight=W   west   (-X)
  #flight=NE  northeast (diagonal)
  #flight=SE  southeast
  #flight=SW  southwest
  #flight=NW  northwest

Renders with no hash unchanged. Each direction rotates the
corridor so its long axis follows the chosen compass bearing, and
the camera drifts along that bearing.
2026-10-07 21:50:31 +02:00
Richard Zink c2d6fd38ba Fix canvas dimensions for swapped corridor axes
After swapping axes so lng runs along canvas Y (corridor long
axis), canvas height was still GROUND_CORRIDOR_PERP * 256 = 1280
(only 5 tiles tall). The 25-tile-long corridor was drawn off-canvas,
so only the back 5 tiles (lng -12..-7) were visible. That made
new tiles 'disappear' off the right side and the build appear 90
deg off from flight.

Set canvas height to corridor long (25 tiles = 6400 px) and
canvas width to perp (5 tiles = 1280 px). Now the full corridor
fits on canvas and the visual depth direction matches the
camera's flight direction along canvas Y.
2026-10-07 21:34:51 +02:00
Richard Zink 8a9fbd3956 Rotate corridor axes: long axis on canvas Y (vertical)
Previous mapping placed the corridor's long axis (flight direction
+Z) on canvas X (horizontal). User observed this as '90deg to
flight direction' because the tiles appeared to scroll sideways on
screen instead of along depth.

Swap so the long axis is on canvas Y (vertical on canvas):
- drawTileAt: canvasX = (perp + halfPerp + 0.5) * TILE_SIZE,
               canvasY = (lng + halfLong + 0.5) * TILE_SIZE
- shader gUv: gUv.x = rotated.y / uGroundSize.x + 0.5 (perp axis),
               gUv.y = rotated.x / uGroundSize.y + 0.5 (long axis)
- uGroundSize call: (PERP, LONG) instead of (LONG, PERP)

Now the corridor's long axis runs vertically on canvas. The screen
mapping (uv.y -> forward distance) reads the long axis as a vertical
strip, matching user expectation that 'forward' tiles scroll top-
to-bottom (or bottom-to-top) as the camera flies.
2026-10-07 21:31:33 +02:00
Richard Zink faabb44cdb Smaller refetch shifts: 1 tile at a time
With BUFFER_LONG = 1, refetch threshold was halfLong - 1 = 11,
which meant each refetch shifted the corridor centre by ~11 tiles.
From the camera's view that looked like the corridor suddenly
jumping 11 tiles forward — described as 'tiles jump to the right'.

BUFFER_LONG = 11 -> threshold = 1, so refetch fires as soon as
the camera crosses one tile beyond centre. Each refetch shifts by
~1 tile, which the user perceives as a smooth scroll rather than
a jump. Refetches now happen every 8 sec instead of every 92 sec,
but the load is small (only ~1 net new tile since browser caches
ESRI responses for 24h).
2026-10-07 21:25:57 +02:00
Richard Zink 14fda4c7d6 Fix half-tile offset in corridor canvas drawing
Shader maps world coords to UV via:
  gUv = rotated / uGroundSize + 0.5
  rotated = 0 -> canvas centre

But drawTileAt placed tiles at:
  canvasX = (lng + halfLong) * TILE_SIZE
  canvasY = (perp + halfPerp) * TILE_SIZE

With halfLong = 12 and lng iterating [-12..12], tile lng=0 lands
at canvas X = 3072 - but the shader wants gUv.x=0.5 (rotated=0)
at canvas centre X = 3200. Half a tile off.

Fix: shift drawing by +0.5 tiles so each tile CENTRE is at
(lng, perp), matching the shader's rotated-frame interpretation.
Tile centres now align with shader expectation; previous half-tile
diagonal wobble gone (which is what they meant by 'x und y
vertauscht' - the perpendicular axis was indeed offset).
2026-10-07 21:23:04 +02:00
Richard Zink a3f5bf6084 Soften UV edges + smaller refetch shifts
Two visual issues reported: stripes from sides at tilts of view, and
tiles 'hopping' one tile per refetch.

Stripes: caused by gUv.y going OOB at sideways rays where hitPoint.x
is large; CLAMP_TO_EDGE returned the same edge colour across a wide
band, looking like a stripe. Added shader edgeFade that fades OOB
samples smoothly to uBg instead.

Hopping: each refetch shifts centerTile by many tiles (depends on
when in the cycle it fires). With BUFFER_LONG = 1 (refetch at
projLong > halfLong - 1 = 11), the centerTile only ever shifts by
about 1 tile per refetch. Refetches happen more often (~ every
8 sec instead of ~ every 96 sec) but each is tiny.

reverted partial-refetch optimisation: causes ghosting because old
tiles stay at old canvas positions but uGroundCenter moves, so
the shader samples stale content. Better to clear canvas and rely
on edgeFade to hide the brief fog state.
2026-10-07 21:17:45 +02:00
Richard Zink af5cda467f Fix shader rotation: align with JS corridor constants (90 deg)
When I changed GROUND_TRAVEL_COS/SIN to (0, 1) (90 deg, +Z
direction) in JS, the shader still used the old (0.82, 0.57)
rotation (35 deg). The corridor was built along +Z but UVs were
computed as if the canvas were rotated 35 deg — meaning the
corridor was effectively at 35 deg from where tiles were loaded.
Tiles ended up diagonally to the flight path.
2026-10-07 21:12:05 +02:00
Richard Zink 425dbf4529 Slow down flight + extend corridor for longer tile residency
Camera speed was 0.45 units/sec which on 13km/tile tiles = ~5850
km/h = supersonic. User reported flying through tiles too fast.

- drift speed 0.45 -> 0.12 units/sec (~1500 km/h, fast plane)
- corridor 15 -> 25 tiles along travel (~3.5 min between refetches
  at uSpeed=1, instead of ~12 sec)
- buffer 2 -> 3 tiles (refetch starts earlier so we always have
  tiles loaded before the camera reaches the edge)
- Updated JS-side drift calculation to match the new shader speed

Camera now leisurely follows the corridor; refetch only once every
couple of minutes.
2026-10-07 21:11:04 +02:00
Richard Zink 587c8f4a19 Align motion, look and corridor on +Z (forward)
Camera was moving in (1, 0.7) but looking mostly in +Z — 55
deg between them meant tiles collected along motion weren't in
the user's forward view. Now:
- ro = (0, 2.2, drift) \u2014 motion purely in +Z
- GROUND_TRAVEL_COS/SIN = (0, 1) \u2014 corridor runs along +Z
- rd = mostly forward +Z, with downward tilt for the ground view

Everything (motion, look, tile collection) is now on the same
+Z axis. Tiles appear directly in front of the camera as it
moves forward, no more 45-55 deg offset.
2026-10-07 21:08:48 +02:00
Richard Zink 930fd2b2f1 Rotate corridor along travel direction (35 deg)
Camera moves at uTime * uSpeed * 0.45 in +X with +X*0.7 in +Z.
Previous axis-aligned corridor (long in X, perp in Z) was
perpendicular to the actual travel direction, wasting tiles in
the +Z / -X corner where the camera never goes.

Now the corridor is rotated 35 deg to match travel:
- GROUND_TRAVEL_COS/SIN = 0.82/0.57 (cos/sin of 35 deg)
- GROUND_CORRIDOR_LONG = 15 (tiles along travel)
- GROUND_CORRIDOR_PERP = 5 (tiles perpendicular)
- rebuildGroundMosaic iterates (lng, perp) in rotated frame,
  draws each tile at canvas position (lng, perp) but loads from
  tile (dx, dz) rotated from (lng, perp) by travel angle
- Shader rotates hitPoint into corridor frame, then divides by
  per-axis extent to get UV
2026-10-07 21:06:06 +02:00
Richard Zink 47f2c16eba Replace square mosaic with corridor elongated in travel direction
The camera moves primarily in +X (drift = uTime * uSpeed * 0.45)
with a slight +Z component (drift * 0.7). A square 9x9 mosaic
wastes half its tiles behind the camera. A corridor shape with
13 tiles along travel (X) and 5 tiles perpendicular (Z) uses 65
tiles to cover the same useful area — and refetches happen only
when the camera approaches the forward edge, not all four sides.

- GROUND_CORRIDOR_X = 13 (≈13s of forward travel at uSpeed=1)
- GROUND_CORRIDOR_Z = 5
- GROUND_BUFFER_X = 2, GROUND_BUFFER_Z = 1
- Shader uniform uGroundSize is now vec2 (sizeX, sizeZ)
- UV mapping: gUv = (hitPoint - centre) / size + 0.5 with per-axis size
2026-10-07 21:03:42 +02:00
Richard Zink e80a270b05 Fix tile coords + reduce mosaic for faster load
Tile coords were wrong — Passau at zoom 11 is (1099, 706), not
(1102, 710). My earlier hand-calc (1100/706) was close but
rounded in the wrong direction.

Mosaic 16x16 = 256 tiles took 5-10s to load due to browser
6-connection limit. Reduced to 9x9 = 81 tiles (~2s). Camera
already looks down enough that visible ground fits in 9 units.
2026-10-07 21:00:32 +02:00
Richard Zink 0f54adefda Fix clouds3d: missing closing quote on long string literal at line 202 2026-10-07 20:58:36 +02:00
Richard Zink a2998bdf45 Fix clouds3d: same JS-comment-in-FRAG_SRC bug as before
Multi-line '//' comment left outside any string in the FRAG_SRC
array. Wrap in single quotes (and stop making this mistake).
2026-10-07 20:57:15 +02:00
Richard Zink 9c17682aed Fix stripes: tilt camera more downward so visible ground fits in mosaic
Previous look direction had rd.y from -0.05 to -0.40 across
the screen, so most rays hit ground at tGround in [9, 44] — way
beyond the 16-unit mosaic. Those OOB pixels came back as solid
edge colour, creating the striped band of texture-then-edge
transitions.

New look: rd.y from -1.10 (bottom) to 0.20 (top). At uv.y=0
rd.y=-0.45 (26° below horizon) so the visible ground sits at
tGround < 6 — all within the 16-unit mosaic. Ground mask
threshold also tightened from -0.30 to -0.10 so the ground
fades to sky higher in the image (looks like atmospheric haze).
Fog distance 14 → matches the new closer-ground regime.
2026-10-07 20:56:08 +02:00
Richard Zink 043d2b689e Fix distorted ground: bigger mosaic + stronger atmospheric fog
- GROUND_MOSAIC_SIZE 9 -> 16 (16x16 = 256 tiles, covers 16 world
  units instead of 9 — wider safety margin at the mosaic edge)
- Fog: tGround / 30 -> tGround / 12 with full mix (was 75% mix)
  so distant ground fully fades to sky instead of staying visible
  with wrong UVs (which was the source of the striped look)
- Result: visible ground content matches mosaic; far ground
  cleanly fades to sky, close ground shows clear satellite tiles
2026-10-07 20:53:59 +02:00
Richard Zink de471dcb8c Fix rebuildGroundMosaic .then(): WebGL constants via groundGl
Inside the Promise.all().then() callback, the WebGL constants
(gl.TEXTURE_2D, gl.RGBA, gl.UNSIGNED_BYTE) were still bare 'gl.',
not groundGl.*. Fixed.
2026-10-07 20:51:31 +02:00
Richard Zink 447ff824af Fix initGroundSystem: WebGL constants must use groundGl, not gl
Last commit fixed gl calls inside the .then() but missed the
calls in the body of initGroundSystem itself. Replaced every
gl.X reference with groundGl.X in the new code.
2026-10-07 20:50:54 +02:00
Richard Zink 19139b01c7 Fix runtime tile system: gl out of scope in rebuildGroundMosaic
The function used bare 'gl' but it's a module-level closure, so
gl wasn't defined inside the .then() callback. Added groundGl
module variable, set it in initGroundSystem, used it everywhere
in rebuildGroundMosaic instead of bare 'gl'.
2026-10-07 20:49:59 +02:00
Richard Zink 979134102d Runtime tile fetching for ground texture (ESRI)
Replace static ground.jpg with a dynamic tile-based system that
fetches new tiles from ESRI World Imagery as the camera moves
through the world.

- GROUND_TILE_Z = 11 (~13 km per tile)
- GROUND_MOSAIC_SIZE = 9 (9x9 = 81 tiles, 2304x2304 canvas)
- GROUND_BUFFER_TILES = 2 (refetch when within 2 tiles of edge)
- Passau tile at zoom 11: x=1102, y=710

Shader:
- Two new uniforms: uGroundCenter (mosaic centre in world units),
  uGroundSize (mosaic side length = 9)
- gUv = (hitPoint - uGroundCenter) / uGroundSize + 0.5 so the
  mosaic centre stays at UV (0.5, 0.5) as the camera drifts

Runtime:
- maybeRefetchGround() called every 1.5s with current camera
  position; if camera is in the buffer zone, mosaic centre is
  shifted and missing tiles re-fetched in parallel
- Canvas re-uploaded to GPU after each rebuild

Camera can now roam indefinitely — every time we approach the
mosaic edge, a fresh 9x9 of tiles is fetched.
2026-10-07 20:48:25 +02:00
Richard Zink 052bbfbf49 Push ground horizon lower in frame
gmask smoothstep end -0.10 -> -0.30: ground only fully fills when
ray Y is below -0.30 (about 17° below horizon). The horizon line
sits higher in the frame, more sky visible above.
2026-10-07 20:44:30 +02:00
Richard Zink 4e035c9c35 Fix clouds3d: same JS-comment-in-FRAG_SRC-array bug as before
Wrapped the multi-line horizon comment into a single quoted
string. Same bug as the earlier fix but in the new ground code.
Going forward I'll make sure all multi-line comments inside
FRAG_SRC are wrapped in quotes.
2026-10-07 20:42:00 +02:00
Richard Zink 2ad51141c6 Ground now uses real ray cast onto y=0 plane
Instead of mapping rd.x/rd.y to UV directly (which gave a flat
orthographic look), compute where the ray actually intersects
the ground plane and sample the satellite texture at that world
(x,z) coordinate. As the camera drifts forward (uTime), the
ground scrolls, giving the illusion of flight over real terrain.

- tGround = -ro.y / rd.y (with rd.y < 0 guard)
- gUv = (hitPoint.x, hitPoint.z) * 0.04 + 0.5
- Atmospheric perspective: ground fades toward sky with distance
  (fog starts at hit-distance 0, full at 30 world units)
- gmask 1.0 only when tGround > 0 (camera above ground AND ray
  pointing down), 0 otherwise
2026-10-07 20:40:46 +02:00
Richard Zink c9159a8876 Fix clouds3d: temporal dead zone on 's' variable
loadImageTexture(s.gl, ...) referenced 's' before its 'var s =
canvas.__state' declaration later in the function. Used
canvas.__state.gl directly since __state was just assigned.
2026-10-07 20:38:49 +02:00
Richard Zink 40fcea0548 Fix clouds3d: JS line comments inside FRAG_SRC array broke parsing
When I added the ground-texture code I left the comment as
multiple JS-line-comments ('// Horizon...') between strings.
Those are NOT inside any string, so JS sees:
  '// Horizon: ... satellite    <- opens string, never closes
  // ground texture ...           <- JS thinks these are line comments
  ...
The string stays open until the next ',, then the next string
line parses as a continuation of an unterminated string and the
whole file is rejected. Wrap the comment into one quoted string.
2026-10-07 20:37:58 +02:00
Richard Zink 6323beccf9 Add satellite ground texture (ESRI World Imagery, Passau)
Pulled a 5x4 tile mosaic of the Passau region (zoom 10) from
ESRI's free ArcGIS World Imagery service, stitched + resized to
1024x768 JPEG (277 KB). Added to Dockerfile as ground.jpg.

Shader side:
- New uniform uGround (texture unit 1)
- horizonMask: rd.y in [-0.20, -0.02] -> ground fades in from 0
  to 1 as we look further down
- gUv derived from rd.xz so looking forward/down scrolls across
  the satellite image (parallax-like feel)
- Distant ground fades toward uBg via a 'fog' factor so the horizon
  blends naturally with the sky gradient

Loading is async — clouds render before ground texture arrives.
Falls back to a black sample if ground.jpg fails to load.
2026-10-07 20:15:53 +02:00
Richard Zink 2e745c26ad Soften night sky: 55% bright (was 85%), wider star band
- skyDarkness 0.85 -> 0.55 so night sky is deep but not pitch
  black, leaving room for stars to show through
- skyMask widened (-0.10 -> -0.20 lower bound, 0.30 -> 0.40 upper)
  so stars appear closer to the horizon in the cockpit view
- Star tint multiplier 0.9 -> 1.0 (full bright) for clearer dots
2026-10-07 20:12:32 +02:00
Richard Zink f567ebf768 Reduce cloud structure: 2 layers, 3 octaves
- fbm3: 4 -> 3 octaves
- cloudDensity: 4 layers (base + mid + fine1 + fine2) -> 2 layers
  (base + mid at 20%)
Cost per pixel: 16 noise3 -> 6 noise3 per step, ~62% cheaper.
Visual: clouds still have secondary detail but the fine wispy
edges are gone — back to the cleaner silhouette.
2026-10-07 20:11:13 +02:00
Richard Zink 7cb04686c7 Fix clouds3d: declare outer cloud variable
'cloud' was referenced outside the for-loop where it was
defined. Use 1.0 - trans (transmittance) as a cloud mask
instead — same information, properly scoped.
2026-10-07 20:09:56 +02:00
Richard Zink 48160b730f Dark night sky + slow star twinkle + moonlit clouds
clouds3d:
- Star twinkle: 3 Hz ±45% -> 0.5 Hz ±15% (gentle scintillation
  instead of static-like flickering)
- Night darkening: applied only to sky pixels via the cloud mask,
  not to clouds. The darkening factor jumped 0.45 -> 0.85 so the
  night sky really does go black around moonlit clouds. Clouds
  keep their uLight-tinted colour so they remain visible.

clouds2d:
- Same darkening approach but via multiplicative factor (no cloud
  mask in the 2D shader): col *= 1 - nightFactor * 0.7
2026-10-07 20:09:08 +02:00