SVT: align synthetic terrain horizon with the attitude horizon

The 3D terrain showed almost no sky — the horizon sat far above the attitude
horizon line. Base camera pitch was 72°, but with MapLibre's 36.87° vertical FOV
the flat horizon only reaches the attitude line (28% of the SVT box) at ~82°.
Invert the perspective to derive the camera pitch from aircraft pitch so the
synthetic horizon lands exactly on the attitude horizon and tracks it 1:1
(accounting for the 1.5× canvas scale). Raise maxPitch to 85.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 16:46:25 +02:00
parent 4a71e5f03d
commit 5b7cf13e9d
+23 -3
View File
@@ -63,6 +63,26 @@ function runwayGeoJSON(list) {
return { type: 'FeatureCollection', features: feats }; return { type: 'FeatureCollection', features: feats };
} }
// Place the synthetic horizon exactly where the PFD attitude horizon sits, so
// the 3D terrain and the 2D attitude agree. The attitude horizon is at 28% of
// the SVT box at level flight and moves PITCH_PX (9 px) per degree within the
// 706-px-tall box; the canvas is scaled 1.5× about that 28% line (to cover the
// corners when banked). We invert MapLibre's perspective to find the camera
// pitch that lands the flat horizon there. With the default vertical FOV of
// 36.87°, the focal-length/height ratio is 0.5/tan(fov/2) = 1.5, independent of
// resolution. Screen offset of the horizon above centre = f·tan(90°−pitch).
const CANVAS_SCALE = 1.5; // matches .svt-canvas CSS transform scale
const FOV_FH = 1.5; // 0.5 / tan(fov/2), fov = 36.87° (MapLibre default)
const HORIZON0 = (270 - 74) / 706; // attitude horizon as a fraction of the SVT box (≈0.2776)
const PX_PER_DEG = 9 / 706; // PITCH_PX / box height — displayed horizon travel per °
function cameraPitchForAircraft(aircraftPitchDeg) {
const dispFrac = HORIZON0 + PX_PER_DEG * aircraftPitchDeg; // where the horizon must appear
const rawFrac = HORIZON0 + (dispFrac - HORIZON0) / CANVAS_SCALE; // undo the 1.5× canvas scale
const t = (0.5 - rawFrac) / FOV_FH; // = tan(90°−pitch)
const pitch = 90 - (Math.atan(t) * 180) / Math.PI;
return Math.max(60, Math.min(85, pitch));
}
export default function SVT({ values }) { export default function SVT({ values }) {
const elRef = useRef(null); const elRef = useRef(null);
const mapRef = useRef(null); const mapRef = useRef(null);
@@ -77,9 +97,9 @@ export default function SVT({ values }) {
style: STYLE, style: STYLE,
center: [num(values.lon, -122.31), num(values.lat, 47.45)], center: [num(values.lon, -122.31), num(values.lat, 47.45)],
zoom: 11.5, zoom: 11.5,
pitch: 72, pitch: cameraPitchForAircraft(num(values.pitch)),
bearing: num(values.heading), bearing: num(values.heading),
maxPitch: 76, // lower max pitch = nearer horizon = less distant terrain maxPitch: 85, // horizon placement needs ~82° at level, more when pitched up
pixelRatio: 1, // don't render at 2× on retina — big perf/bandwidth win pixelRatio: 1, // don't render at 2× on retina — big perf/bandwidth win
renderWorldCopies: false, renderWorldCopies: false,
maxTileCacheSize: 40, maxTileCacheSize: 40,
@@ -154,7 +174,7 @@ export default function SVT({ values }) {
map.jumpTo({ map.jumpTo({
center: [num(v.lon, -122.31), num(v.lat, 47.45)], center: [num(v.lon, -122.31), num(v.lat, 47.45)],
bearing: num(v.heading), bearing: num(v.heading),
pitch: Math.max(58, Math.min(76, 72 + num(v.pitch))), pitch: cameraPitchForAircraft(num(v.pitch)),
zoom, zoom,
}); });
updateTerrainAwareness(num(v.altitude, 5500)); updateTerrainAwareness(num(v.altitude, 5500));