From e8ff7ee71461d5b9b65ed55f2199e5c511d32e28 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Mon, 12 Oct 2026 21:23:54 +0200 Subject: [PATCH] camera: bank the view when the flight path curves --- config.js | 13 ++++++++++++- vendor/clouds3d.js | 43 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/config.js b/config.js index 541211b..32d7c6e 100644 --- a/config.js +++ b/config.js @@ -480,7 +480,18 @@ window.CONFIG = { aim: 'off', // 'off' | 'moon' // aim: 'moon' // look at the moon while flying the path aimEase: 0.94, // 0 = instant turn, 0.99 = very slow - aimBob: 0.25 // share of the altitude bob kept while aiming + aimBob: 0.25, // share of the altitude bob kept while aiming + // Banking when the flight path curves. The camera keeps flying the + // same path; roll only rotates the view around the forward axis. + roll: { + enabled: true, + gain: 12, // turn-rate to bank-angle gain + maxDeg: 12, // maximum bank angle + ease: 0.92, // 0 = instant bank, 0.99 = very slow + sign: -1 // flip to +1 if the bank direction feels inverted + // roll: { enabled: false } + // roll: { gain: 20, maxDeg: 18 } + } // camera: { aim: 'moon' } // look at the moon }, diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index cf822a4..d30777b 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -1428,6 +1428,13 @@ var CAM_CFG = (window.CONFIG && window.CONFIG.camera) || {}; var CAM_AIM_MOON = (CAM_CFG.aim === 'moon'); var CAM_AIM_EASE = (typeof CAM_CFG.aimEase === 'number') ? Math.max(0, Math.min(0.999, CAM_CFG.aimEase)) : 0.94; var CAM_AIM_BOB = (typeof CAM_CFG.aimBob === 'number') ? Math.max(0, Math.min(1, CAM_CFG.aimBob)) : 0.25; +var CAM_ROLL_CFG = CAM_CFG.roll || {}; +var CAM_ROLL_ON = CAM_ROLL_CFG.enabled !== false; +var CAM_ROLL_GAIN = (typeof CAM_ROLL_CFG.gain === 'number') ? Math.max(0, CAM_ROLL_CFG.gain) : 12; +var CAM_ROLL_MAX = (typeof CAM_ROLL_CFG.maxDeg === 'number') ? Math.max(0, Math.min(35, CAM_ROLL_CFG.maxDeg)) * Math.PI / 180 : 12 * Math.PI / 180; +var CAM_ROLL_EASE = (typeof CAM_ROLL_CFG.ease === 'number') ? Math.max(0, Math.min(0.999, CAM_ROLL_CFG.ease)) : 0.92; +var CAM_ROLL_SIGN = (typeof CAM_ROLL_CFG.sign === 'number') ? CAM_ROLL_CFG.sign : -1; +var lastCamBearing = null, lastCamTime = null, camRoll = 0; var aimHdX = 0, aimHdZ = 1, aimPitch = 0.45, aimReady = false; // CONFIG.clouds = { top: ..., bottom: ... } overrides the defaults; the // deck must stay below the camera and keep a minimum thickness. @@ -6243,7 +6250,29 @@ window.clouds3dRender = function(target) { // View direction as compass bearing (world +X = east, +Z = north, // matching the sun-vector convention): 0 = north, clockwise. // The clock's compass needle reads this. - window.__flightBearing = Math.atan2(hdX, hdZ); + var flightBearing = Math.atan2(hdX, hdZ); + window.__flightBearing = flightBearing; + var viewRoll = 0; + if (CAM_ROLL_ON) { + if (lastCamBearing !== null && lastCamTime !== null) { + var _dt = Math.max(0.001, now - lastCamTime); + var _db = flightBearing - lastCamBearing; + while (_db > Math.PI) _db -= 2 * Math.PI; + while (_db < -Math.PI) _db += 2 * Math.PI; + var _turnRate = _db / _dt; + var _target = CAM_ROLL_SIGN * _turnRate * CAM_ROLL_GAIN; + if (_target > CAM_ROLL_MAX) _target = CAM_ROLL_MAX; + else if (_target < -CAM_ROLL_MAX) _target = -CAM_ROLL_MAX; + camRoll += (_target - camRoll) * (1 - CAM_ROLL_EASE); + } + lastCamBearing = flightBearing; + lastCamTime = now; + viewRoll = camRoll; + } else { + lastCamBearing = null; + lastCamTime = null; + camRoll = 0; + } // Periodic check: when the camera approaches the corridor edge, // re-centre the mosaic (whole-tile shift of the canvas content + // fetch the newly exposed columns). At uSpeed=1 the camera moves @@ -6309,10 +6338,20 @@ window.clouds3dRender = function(target) { // Debug override: point at world origin so the mesh is always in view. var fwdLen = Math.hypot(hdX, pitch, hdZ) || 1; var fwdX = hdX / fwdLen, fwdY = -pitch / fwdLen, fwdZ = hdZ / fwdLen; + var upX = 0, upY = 1, upZ = 0; + if (Math.abs(viewRoll) > 1e-5) { + var _rc = Math.cos(viewRoll), _rs = Math.sin(viewRoll); + // Rodrigues rotation of the world-up vector around the forward axis. + upX = -fwdZ * _rs; + upY = _rc; + upZ = fwdX * _rs; + var _ul = Math.hypot(upX, upY, upZ) || 1; + upX /= _ul; upY /= _ul; upZ /= _ul; + } var view = mat4LookAt( [camX, camY, camZ], [camX + fwdX * 5, camY + fwdY * 5, camZ + fwdZ * 5], - [0, 1, 0] + [upX, upY, upZ] ); // Inverse of (proj * view) is uploaded to the cloud/sky shader so // both can reconstruct the world ray from gl_FragCoord identically