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
This commit is contained in:
Richard Zink committed 2026-10-07 21:03:42 +02:00
1 parent e80a270b05
commit 47f2c16eba
1 file changed
+35 -21
+35 -21
View File
@@ -55,7 +55,7 @@ var FRAG_SRC = [
'uniform float uTime;',
'uniform sampler2D uGround;',
'uniform vec2 uGroundCenter;', // current mosaic centre (world units)
'uniform float uGroundSize;', // mosaic side length (world units)
'uniform vec2 uGroundSize;', // mosaic side extents in X and Z (world units)
'uniform vec3 uSky;',
'uniform vec3 uCloud;',
'uniform vec3 uCloudShadow;',
@@ -270,8 +270,13 @@ function init(canvas) {
var GROUND_TILE_URL = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/';
var GROUND_TILE_Z = 11;
var GROUND_TILE_SIZE = 256;
var GROUND_MOSAIC_SIZE = 9; // 9x9 tiles around the camera (~81 tiles, fast initial load)
var GROUND_BUFFER_TILES = 2; // refetch when this close to edge
// Camera travels at uTime * uSpeed * 0.45 in +X (and +X*0.7 in +Z).
// A corridor elongated in X gives more forward coverage per tile.
// 13 tiles along X (≈13s of travel) x 5 tiles wide in Z = 65 tiles.
var GROUND_CORRIDOR_X = 13; // tiles along travel direction
var GROUND_CORRIDOR_Z = 5; // tiles perpendicular
var GROUND_BUFFER_X = 2; // refetch when this close to forward edge
var GROUND_BUFFER_Z = 1; // refetch when this close to side edge
// Passau tile at zoom 11
var GROUND_BASE_X = 1099;
var GROUND_BASE_Y = 706;
@@ -296,22 +301,25 @@ function fetchTileImage(tileX, tileY) {
});
}
function drawTileAt(tileX, tileY, centerTile) {
var r = Math.floor(GROUND_MOSAIC_SIZE / 2);
var dx = (tileX - centerTile.x + r) * GROUND_TILE_SIZE;
var dy = (tileY - centerTile.y + r) * GROUND_TILE_SIZE;
if (dx < 0 || dx >= groundCanvas.width || dy < 0 || dy >= groundCanvas.height) {
function drawTileAt(dx, dz, centerTileX, centerTileY) {
var tileX = centerTileX + dx;
var tileY = centerTileY + dz;
var halfX = Math.floor(GROUND_CORRIDOR_X / 2);
var halfZ = Math.floor(GROUND_CORRIDOR_Z / 2);
var canvasX = (dx + halfX) * GROUND_TILE_SIZE;
var canvasY = (dz + halfZ) * GROUND_TILE_SIZE;
if (canvasX < 0 || canvasX >= groundCanvas.width || canvasY < 0 || canvasY >= groundCanvas.height) {
return Promise.resolve(false);
}
return fetchTileImage(tileX, tileY).then(function(img) {
groundCtx.drawImage(img, dx, dy);
groundCtx.drawImage(img, canvasX, canvasY);
groundLoadedTiles[tileX + '_' + tileY] = true;
return true;
}).catch(function() {
// On failure draw a neutral gray so the gap is at least
// not the bright fog colour
groundCtx.fillStyle = '#3a4458';
groundCtx.fillRect(dx, dy, GROUND_TILE_SIZE, GROUND_TILE_SIZE);
groundCtx.fillRect(canvasX, canvasY, GROUND_TILE_SIZE, GROUND_TILE_SIZE);
return false;
});
}
@@ -320,14 +328,15 @@ function drawTileAt(tileX, tileY, centerTile) {
// and loads every tile in the new range.
function rebuildGroundMosaic(centerTileX, centerTileY) {
groundCenterTile = { x: centerTileX, y: centerTileY };
var r = Math.floor(GROUND_MOSAIC_SIZE / 2);
var halfX = Math.floor(GROUND_CORRIDOR_X / 2);
var halfZ = Math.floor(GROUND_CORRIDOR_Z / 2);
// Clear with fog colour so the texture is never undefined
groundCtx.fillStyle = '#7c9aa1';
groundCtx.fillRect(0, 0, groundCanvas.width, groundCanvas.height);
var promises = [];
for (var x = centerTileX - r; x <= centerTileX + r; x++) {
for (var y = centerTileY - r; y <= centerTileY + r; y++) {
promises.push(drawTileAt(x, y, groundCenterTile));
for (var dx = -halfX; dx <= halfX; dx++) {
for (var dz = -halfZ; dz <= halfZ; dz++) {
promises.push(drawTileAt(dx, dz, centerTileX, centerTileY));
}
}
return Promise.all(promises).then(function() {
@@ -343,10 +352,15 @@ function rebuildGroundMosaic(centerTileX, centerTileY) {
function maybeRefetchGround(cameraWorldX, cameraWorldZ, gl) {
var cameraTileX = GROUND_BASE_X + Math.floor(cameraWorldX);
var cameraTileY = GROUND_BASE_Y + Math.floor(cameraWorldZ);
var r = Math.floor(GROUND_MOSAIC_SIZE / 2);
var dx = Math.abs(cameraTileX - groundCenterTile.x);
var dy = Math.abs(cameraTileY - groundCenterTile.y);
if (dx > r - GROUND_BUFFER_TILES || dy > r - GROUND_BUFFER_TILES) {
var halfX = Math.floor(GROUND_CORRIDOR_X / 2);
var halfZ = Math.floor(GROUND_CORRIDOR_Z / 2);
var dx = cameraTileX - groundCenterTile.x;
var dy = cameraTileY - groundCenterTile.y;
// Only refetch when camera approaches the FRONT of the corridor
// (the only direction the camera moves toward). Side and back
// buffers keep refetches minimal.
if (dx > halfX - GROUND_BUFFER_X || dx < -halfX + GROUND_BUFFER_X ||
dy > halfZ - GROUND_BUFFER_Z || dy < -halfZ + GROUND_BUFFER_Z) {
if (groundUpdatePending) return;
groundUpdatePending = true;
rebuildGroundMosaic(cameraTileX, cameraTileY).then(function() {
@@ -361,8 +375,8 @@ function initGroundSystem(glCtx) {
if (groundCanvas) return; // already initialised
groundGl = glCtx;
groundCanvas = document.createElement('canvas');
groundCanvas.width = GROUND_TILE_SIZE * GROUND_MOSAIC_SIZE;
groundCanvas.height = GROUND_TILE_SIZE * GROUND_MOSAIC_SIZE;
groundCanvas.width = GROUND_TILE_SIZE * GROUND_CORRIDOR_X;
groundCanvas.height = GROUND_TILE_SIZE * GROUND_CORRIDOR_Z;
groundCtx = groundCanvas.getContext('2d');
// Pre-fill with fog colour so partially-loaded mosaic still
// shows the right base colour
@@ -487,7 +501,7 @@ window.clouds3dRender = function(target) {
y: groundCenterTile.y - GROUND_BASE_Y
};
gl.uniform2f(s.uGroundCenter, centerWorld.x, centerWorld.y);
gl.uniform1f(s.uGroundSize, GROUND_MOSAIC_SIZE);
gl.uniform2f(s.uGroundSize, GROUND_CORRIDOR_X, GROUND_CORRIDOR_Z);
var drift = now * speed * 0.45;
if (performance.now() - groundLastCheck > 1500) {