From 4f8fd4b62afc8eb762c130b30ff004f1bf1c6578 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Thu, 8 Oct 2026 07:59:11 +0200 Subject: [PATCH] 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. --- vendor/clouds3d.js | 56 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index ec6ae60..9f08647 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -307,6 +307,8 @@ var groundCenterTile = { x: GROUND_BASE_X, y: GROUND_BASE_Y }; var groundLoadedTiles = {}; // "x_y" -> true var groundLastCheck = 0; var groundUpdatePending = false; +var groundCameraX = 0; +var groundCameraZ = 0; function fetchTileImage(tileX, tileY) { return new Promise(function(resolve, reject) { @@ -416,6 +418,44 @@ function rebuildGroundMosaic(centerTileX, centerTileY) { }); } +// Continuously redraw the corridor based on the live camera +// position. Without this the corridor would shift in tile-size jumps +// at every refetch; with it the corridor scrolls smoothly as the +// camera flies. The browser caches the loaded Image elements so the +// only per-frame cost is compositing + a texImage2D upload (~5MB). +function refreshGroundMosaic() { + if (!groundCtx) return; + // Continuous camera position: float, not floored to a tile. + var fdx = (window.__flightDir && window.__flightDir.dx) || 0; + var fdz = (window.__flightDir && window.__flightDir.dz) || 1; + var camX = (fdx !== 0) ? groundCameraX * fdx : 0; + var camZ = (fdz !== 0) ? groundCameraZ * fdz : 0; + var centerTileX = GROUND_BASE_X + Math.floor(camX); + var centerTileZ = GROUND_BASE_Y + Math.floor(camZ); + groundCenterTile = { x: centerTileX, z: centerTileZ }; + var halfLong = Math.floor(GROUND_CORRIDOR_LONG / 2); + var halfPerp = Math.floor(GROUND_CORRIDOR_PERP / 2); + // Clear and redraw every frame. + groundCtx.fillStyle = '#7c9aa1'; + groundCtx.fillRect(0, 0, groundCanvas.width, groundCanvas.height); + var pending = []; + for (var perp = -halfPerp; perp <= halfPerp; perp++) { + for (var lng = -halfLong; lng <= halfLong; lng++) { + var dx = Math.round(lng * GROUND_TRAVEL_COS - perp * GROUND_TRAVEL_SIN); + var dz = Math.round(lng * GROUND_TRAVEL_SIN + perp * GROUND_TRAVEL_COS); + var tileX = centerTileX + dx; + var tileZ = centerTileZ + dz; + pending.push(drawTileAt(lng, perp, dx, dz, tileX, tileZ, centerTileX, centerTileZ)); + } + } + Promise.all(pending).then(function() { + if (groundTexture && groundGl) { + groundGl.bindTexture(groundGl.TEXTURE_2D, groundTexture); + groundGl.texImage2D(groundGl.TEXTURE_2D, 0, groundGl.RGBA, groundGl.RGBA, groundGl.UNSIGNED_BYTE, groundCanvas); + } + }); +} + // Called periodically from the render loop with the camera world // position. Re-centres the mosaic if we're approaching its edge. function maybeRefetchGround(cameraWorldX, cameraWorldZ, gl) { @@ -586,10 +626,18 @@ window.clouds3dRender = function(target) { gl.uniform2f(s.uFlightDir, flightDir.dx, flightDir.dz); var drift = now * speed * 0.12; - if (performance.now() - groundLastCheck > 1500) { - groundLastCheck = performance.now(); - maybeRefetchGround(drift, drift, gl); - } + // Track continuous camera world position (X and Z independently, + // since flight direction is independent from fetch direction). + var fdx2 = (window.__flightDir && window.__flightDir.dx) || 0; + var fdz2 = (window.__flightDir && window.__flightDir.dz) || 1; + groundCameraX = (fdx2 !== 0) ? drift * fdx2 : 0; + groundCameraZ = (fdz2 !== 0) ? drift * fdz2 : 0; + // Continuous scroll: redraw the corridor every frame so the camera + // glides smoothly through the tile content rather than jumping at + // integer tile boundaries. Browser caches the loaded Image + // elements so the only per-frame cost is compositing + a small + // texImage2D upload. + refreshGroundMosaic(); gl.drawArrays(gl.TRIANGLES, 0, 3); };