// ==UserScript==
// @name Better Resolution Terrain (Realistic)
// @namespace http://tampermonkey.net/
// @version 2026-10-17
// @description Higher resolution satellite imagery, better terrain, and realism tweaks for GeoFS
// @author drakeerv (modified)
// @match https://www.geo-fs.com/geofs.php?v=*
// @icon https://www.google.com/s2/favicons?sz=64&domain=geo-fs.com
// @grant none
// @license MIT
// ==/UserScript==
(function () {
"use strict";
// ------------------------------------------------------------------
// CONFIG
// ------------------------------------------------------------------
// Imagery provider:
// "esri" - Esri World Imagery, no key needed (default, good quality)
// "mapbox" - Mapbox Satellite, needs free token (https://account.mapbox.com)
// "maptiler" - MapTiler Satellite, needs free key (https://cloud.maptiler.com)
// "bing" - Bing Aerial, needs your own key (https://www.bingmapsportal.com)
// "ion" - Cesium ion (Bing aerial + World Terrain), needs free token (https://ion.cesium.com)
// "cache" - your own local tile server
const provider = "ion";
// Paste YOUR OWN keys here. Only the one for your chosen provider is needed.
const keys = {
mapbox: "",
maptiler: "",
bing: "",
ion: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJub25jZSI6InlndmNRbjQ5MTBuZF9pb0YiLCJqdGkiOiI0YzRhNTBkYS1hODJhLTQ4NGEtYTQwMi02MTc5NTEyYWEyYjUiLCJpZCI6NTI4NzA5LCJzdWIiOiJjb29sZHVkZSIsImlzcyI6Imh0dHBzOi8vYXBpLmNlc2l1bS5jb20iLCJhdWQiOiJVbnRpdGxlZCIsImlhdCI6MTc5MjI3Mjk2OH0.0-jUl72tQeVXrsL4Aeft2D1qUYyB8DHHrN7jGkNpdNI"
};
const multiplayerServer = "default";
// Terrain detail when using ion (Cesium World Terrain). Higher = finer mountains and hills but more
// downloading and a heavier load. 13 light, 14 balanced, 15 very detailed.
// Which terrain shape to use:
// "geofs" - GeoFS's own terrain. Matches the ground the aircraft actually collides with. USE THIS to avoid
// sinking into, or floating above, the ground.
// "ion" - Cesium World Terrain. Finer to look at, but GeoFS does not collide with it, so the aircraft
// can sink below the visible ground or hover over it.
const terrainSource = "geofs";
const terrainMaxLevel = 13;
// Real roads, highways and trees from OpenStreetMap, loaded around you as you fly.
// Data comes free from the public Overpass server (OpenStreetMap contributors, ODbL).
const osm = {
roads: true, // draped roads and highways with correct widths and a lighter shoulder
smallRoads: false, // also residential streets and service roads (many more shapes, heavier)
trees: true, // trees inside woods, forests and parks, plus single mapped trees
treeSpacing: 14, // metres between trees (lower = denser forest, heavier)
maxTreesPerTile: 400, // trees per 2 km tile; keeps the frame rate safe
maxAltitude: 3500, // only load and show these below this height in metres
server: "https://overpass-api.de/api/interpreter"
};
// Wavy water. The script finds the sea by its terrain height (the ocean is flat at sea level) and draws
// animated waves, sun glints and shoreline foam over it. No ion or extra downloads needed, and it does not
// touch collisions. Lakes and rivers above sea level are not detected.
const water = {
enabled: true,
seaLevel: 0, // terrain height in metres that counts as sea. Fly over the sea, press F12 and run btHeightHere()
tolerance: 0.8, // how far from seaLevel still counts as water
waveHeight: 1.0, // how choppy the waves look (0.5 calm, 1 normal, 2 rough)
waveSpeed: 1.0, // how fast the waves move
opacity: 0.6, // how much the water colour covers the satellite photo (0 = photo only, 1 = solid)
shallowColor: "#1f6f86",
deepColor: "#06263a"
};
// Visual tweaks (set any to false/1 to disable)
const visuals = {
sharpness: 1.5, // globe.maximumScreenSpaceError; lower = sharper but heavier (try 1 to 2.5)
fpsCap: 60, // stops the GPU running flat out, which is what makes fans loud. 0 = no cap
maxRenderScale: 1.25, // limits render resolution on scaled/high-DPI screens (the heaviest single cost)
bloomOnlyAtNight: true, // bloom is only drawn at dusk and night, when lights matter, and skipped in daytime
saturation: 1.18, // satellite tiles look washed out; 1 = unchanged
contrast: 1.08,
gamma: 1.0,
bloom: true, // soft glow around lights and bright highlights (MSFS-style night lights). First thing to turn off if the screen goes black
atmosphereBoost: true, // richer sky and horizon haze, warmer colours
timeOfDay: true, // colours follow the time of day: warm sunrise/sunset, cool dusk and night, neutral midday
timeStrength: 1, // how strong the time-of-day colours are (0 = none, 1 = normal, 1.5 = dramatic)
sunRays: 1, // light shafts streaming from the sun, strongest at sunrise and sunset (0 = off, 2 = strong)
sunLighting: true, // sun-lit terrain: shading on hills and a darker unlit side (more depth, like MSFS)
sunGlare: true, // bright sun with glow around it
hdr: false, // filmic HDR tone mapping, the biggest MSFS-like lighting change. OFF by default: it can blank the screen on some GPUs
exposure: 1.1, // brightness used when hdr is on
groundAtmosphere: true,
fog: true,
fxaa: true,
hiDpi: true, // render at the device pixel ratio
lightBoost: 1.6, // scales runway, approach and PAPI light size (1 = unchanged, 0 = skip)
// supersample and msaa are OFF by default. sharpen is on at 0.3 and is test-compiled first.
// If the screen goes black after enabling one, that one is the cause: set it back.
supersample: 1, // extra render scale on top of hiDpi (1 = off, 1.5 = nicer but heavy). Too high can blank the canvas
msaa: 0, // multisample anti-aliasing (0 = off, 4 = on). Only works on newer Cesium/WebGL2
sharpen: 0.3 // post-process sharpening (0 = off, try 0.3). A shader problem here shows as a black screen
};
// ------------------------------------------------------------------
const Cesium = () => window.Cesium;
function buildImageryProvider() {
const C = Cesium();
switch (provider) {
case "esri":
return new C.UrlTemplateImageryProvider({
maximumLevel: 19,
hasAlphaChannel: false,
url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
credit: "Esri, Maxar, Earthstar Geographics"
});
case "mapbox":
if (!keys.mapbox) break;
return new C.UrlTemplateImageryProvider({
maximumLevel: 21,
tileWidth: 512,
tileHeight: 512,
hasAlphaChannel: false,
url: "https://api.mapbox.com/v4/mapbox.satellite/{z}/{x}/{y}@2x.jpg90?access_token=" + keys.mapbox,
credit: "Mapbox, Maxar"
});
case "maptiler":
if (!keys.maptiler) break;
return new C.UrlTemplateImageryProvider({
maximumLevel: 20,
hasAlphaChannel: false,
url: "https://api.maptiler.com/tiles/satellite-v2/{z}/{x}/{y}.jpg?key=" + keys.maptiler,
credit: "MapTiler, Maxar"
});
case "bing":
if (!keys.bing) break;
return new C.BingMapsImageryProvider({
url: "https://dev.virtualearth.net",
key: keys.bing,
mapStyle: C.BingMapsStyle.AERIAL
});
case "ion":
if (!keys.ion) break;
C.Ion.defaultAccessToken = keys.ion;
return new C.IonImageryProvider({ assetId: 2 }); // Bing Maps Aerial
case "cache":
return new C.UrlTemplateImageryProvider({
maximumLevel: 21,
hasAlphaChannel: false,
url: "http://localhost/map/{z}/{x}/{y}"
});
}
if (provider !== "esri") {
console.warn("[BetterTerrain] Missing key for '" + provider + "', falling back to Esri.");
}
return new C.UrlTemplateImageryProvider({
maximumLevel: 19,
hasAlphaChannel: false,
url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}"
});
}
function buildBaseTerrain() {
const C = Cesium();
if (terrainSource === "ion" && keys.ion) {
// Cesium World Terrain (asset 1): much finer than SRTM in many regions
return new C.CesiumTerrainProvider({
url: C.IonResource.fromAssetId(1),
requestWaterMask: false,
requestVertexNormals: true
});
}
return new C.CesiumTerrainProvider({
url: "https://data.geo-fs.com/srtm/",
requestWaterMask: false,
requestVertexNormals: true
});
}
// Looks through GeoFS's preferences for a setting whose name contains "build".
function findPref(re) {
const root = window.geofs && window.geofs.preferences;
if (!root) return null;
const seen = [];
const stack = [[root, "geofs.preferences"]];
while (stack.length) {
const item = stack.pop();
const obj = item[0];
if (!obj || typeof obj !== "object" || seen.indexOf(obj) !== -1) continue;
seen.push(obj);
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
const k = keys[i], v = obj[k];
if (re.test(k) && (v === null || typeof v !== "object")) {
return { obj: obj, key: k, path: item[1] + "." + k };
}
if (v && typeof v === "object") stack.push([v, item[1] + "." + k]);
}
}
return null;
}
function settingIsOn(v) {
return !(v === false || v === 0 || v === "0" || v === "false" || v === "off" || v === null || v === undefined);
}
// Contrast-adaptive style sharpening. Stretched satellite tiles are soft;
// this restores edge definition so the ground reads as detail, not blur.
function installSharpen(scene) {
try {
const C = Cesium();
if (window.__btSharpenStage) {
scene.postProcessStages.remove(window.__btSharpenStage);
window.__btSharpenStage = null;
}
if (!visuals.sharpen || !C.PostProcessStage) return;
const sharpFs = [
"uniform sampler2D colorTexture;",
"uniform float strength;",
"varying vec2 v_textureCoordinates;",
"void main() {",
" vec2 px = 1.0 / czm_viewport.zw;",
" vec2 uv = v_textureCoordinates;",
" vec4 c = texture2D(colorTexture, uv);",
" vec3 n = texture2D(colorTexture, uv + vec2(0.0, px.y)).rgb;",
" vec3 s = texture2D(colorTexture, uv - vec2(0.0, px.y)).rgb;",
" vec3 e = texture2D(colorTexture, uv + vec2(px.x, 0.0)).rgb;",
" vec3 w = texture2D(colorTexture, uv - vec2(px.x, 0.0)).rgb;",
" vec3 mn = min(c.rgb, min(min(n, s), min(e, w)));",
" vec3 mx = max(c.rgb, max(max(n, s), max(e, w)));",
" vec3 amp = sqrt(clamp(min(mn, 1.0 - mx) / max(mx, vec3(0.0001)), 0.0, 1.0));",
" vec3 k = -amp * mix(0.125, 0.2, strength);",
" vec3 res = (c.rgb + (n + s + e + w) * k) / (1.0 + 4.0 * k);",
" gl_FragColor = vec4(clamp(res, 0.0, 1.0), c.a);",
"}"
].join("\n");
if (!shaderCompiles(sharpFs, "uniform vec4 czm_viewport;\n")) return;
const stage = new C.PostProcessStage({
name: "better_terrain_sharpen",
uniforms: { strength: visuals.sharpen },
fragmentShader: sharpFs
});
scene.postProcessStages.add(stage);
window.__btSharpenStage = stage;
} catch (e) {
console.warn("[BetterTerrain] Sharpen stage unavailable:", e);
}
}
// ---- Time of day: colour grade + sun rays ----------------------------------
// Reads the sun's position straight from the renderer each frame, so it follows
// whatever the GeoFS time changer does, as long as GeoFS moves Cesium's sun.
// In the browser console, btSunElevation() shows the current sun height in degrees.
const timeState = { elevation: 45, tint: [1, 1, 1], rays: 0, sunUV: [0.5, 0.5] };
const TIME_KEYS = [
// sun elevation (degrees), colour tint, light-shaft strength
[-12, [0.72, 0.82, 1.14], 0.0], // night: cool and blue
[-4, [0.92, 0.86, 1.06], 0.15], // dusk / dawn glow
[0, [1.20, 0.95, 0.76], 1.0], // sunrise / sunset: warm orange
[8, [1.12, 1.00, 0.86], 0.85], // golden hour
[20, [1.04, 1.00, 0.96], 0.4], // morning / afternoon
[60, [1.00, 1.00, 1.00], 0.2] // midday: neutral
];
function sampleTime(e) {
const k = TIME_KEYS;
const last = k[k.length - 1];
if (e <= k[0][0]) return { tint: k[0][1], rays: k[0][2] };
if (e >= last[0]) return { tint: last[1], rays: last[2] };
for (let i = 0; i < k.length - 1; i++) {
const a = k[i], b = k[i + 1];
if (e >= a[0] && e <= b[0]) {
const t = (e - a[0]) / (b[0] - a[0]);
return {
tint: [
a[1][0] + (b[1][0] - a[1][0]) * t,
a[1][1] + (b[1][1] - a[1][1]) * t,
a[1][2] + (b[1][2] - a[1][2]) * t
],
rays: a[2] + (b[2] - a[2]) * t
};
}
}
return { tint: [1, 1, 1], rays: 0 };
}
function updateTimeState(scene) {
const C = Cesium();
const us = scene.context && scene.context.uniformState;
if (!us || !us.sunDirectionWC || !us.viewProjection) return;
const sun = us.sunDirectionWC;
const pos = scene.camera.positionWC;
const len = Math.sqrt(pos.x * pos.x + pos.y * pos.y + pos.z * pos.z) || 1;
const sinE = (sun.x * pos.x + sun.y * pos.y + sun.z * pos.z) / len;
const e = Math.asin(Math.max(-1, Math.min(1, sinE))) * 180 / Math.PI;
timeState.elevation = e;
const k = sampleTime(e);
const st = visuals.timeStrength;
timeState.tint = [1 + (k.tint[0] - 1) * st, 1 + (k.tint[1] - 1) * st, 1 + (k.tint[2] - 1) * st];
// Where is the sun on screen? Project its direction with the view-projection matrix.
let rays = k.rays * visuals.sunRays;
const clip = C.Matrix4.multiplyByVector(
us.viewProjection, new C.Cartesian4(sun.x, sun.y, sun.z, 0), new C.Cartesian4()
);
if (clip.w > 0.0001) {
const u = clip.x / clip.w * 0.5 + 0.5;
const v = clip.y / clip.w * 0.5 + 0.5;
timeState.sunUV = [u, v];
const off = Math.max(Math.abs(u - 0.5), Math.abs(v - 0.5));
rays *= Math.max(0, Math.min(1, 1 - (off - 0.5) / 0.5)); // fade as the sun leaves the screen
} else {
rays = 0; // sun is behind the camera
}
timeState.rays = rays;
// Bloom costs several full-screen passes; only run it when the sun is low or down.
try {
const bloom = scene.postProcessStages && scene.postProcessStages.bloom;
if (visuals.bloom && visuals.bloomOnlyAtNight && bloom) {
const want = e < 8;
if (want !== window.__btBloomOn) {
bloom.enabled = want;
window.__btBloomOn = want;
}
}
} catch (err) { /* ignore */ }
}
// Test-compile a shader first. A bad shader inside Cesium shows up as a black screen,
// so if it fails here the effect is skipped instead.
function shaderCompiles(src, prelude) {
try {
const gl = document.createElement("canvas").getContext("webgl");
if (!gl) return true; // can't test, assume it is fine
const sh = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(sh, "precision highp float;\n" + (prelude || "") + src);
gl.compileShader(sh);
const ok = gl.getShaderParameter(sh, gl.COMPILE_STATUS);
if (!ok) console.warn("[BetterTerrain] Shader failed to compile:", gl.getShaderInfoLog(sh));
return !!ok;
} catch (e) {
return true;
}
}
function installTimeOfDay(scene) {
try {
const C = Cesium();
if (window.__btTimeStage) {
scene.postProcessStages.remove(window.__btTimeStage);
window.__btTimeStage = null;
}
if (window.__btTimeRemove) {
window.__btTimeRemove();
window.__btTimeRemove = null;
}
if (!visuals.timeOfDay || !C.PostProcessStage) return;
const fs = [
"uniform sampler2D colorTexture;",
"uniform vec3 tint;",
"uniform vec2 sunUV;",
"uniform float rays;",
"varying vec2 v_textureCoordinates;",
"const int SAMPLES = 24;",
"void main() {",
" vec4 base = texture2D(colorTexture, v_textureCoordinates);",
" vec3 col = base.rgb;",
" if (rays > 0.001) {",
" vec2 uv = v_textureCoordinates;",
" vec2 delta = (sunUV - uv) * (0.85 / float(SAMPLES));",
" float decay = 1.0;",
" vec3 acc = vec3(0.0);",
" for (int i = 0; i < SAMPLES; i++) {",
" uv += delta;",
" vec3 s = texture2D(colorTexture, uv).rgb;",
" float l = dot(s, vec3(0.299, 0.587, 0.114));",
" acc += s * smoothstep(0.82, 1.0, l) * decay;",
" decay *= 0.965;",
" }",
" float d = distance(v_textureCoordinates, sunUV);",
" float falloff = 1.0 - smoothstep(0.0, 0.9, d);",
" float baseL = dot(col, vec3(0.299, 0.587, 0.114));",
" vec3 add = acc / float(SAMPLES) * rays * 0.9 * falloff * vec3(1.0, 0.92, 0.8);",
" col += min(add, vec3(0.25)) * (1.0 - baseL * 0.6);",
" }",
" col *= tint;",
" gl_FragColor = vec4(clamp(col, 0.0, 1.0), base.a);",
"}"
].join("\n");
if (!shaderCompiles(fs)) return;
const tintC = new C.Cartesian3(1, 1, 1);
const uvC = new C.Cartesian2(0.5, 0.5);
const stage = new C.PostProcessStage({
name: "better_terrain_time_of_day",
uniforms: {
tint: function () {
tintC.x = timeState.tint[0]; tintC.y = timeState.tint[1]; tintC.z = timeState.tint[2];
return tintC;
},
sunUV: function () {
uvC.x = timeState.sunUV[0]; uvC.y = timeState.sunUV[1];
return uvC;
},
rays: function () { return timeState.rays; }
},
fragmentShader: fs
});
scene.postProcessStages.add(stage);
window.__btTimeStage = stage;
window.__btTimeRemove = scene.preRender.addEventListener(function () {
updateTimeState(scene);
});
window.btSunElevation = function () { return timeState.elevation; };
} catch (e) {
console.warn("[BetterTerrain] Time-of-day lighting unavailable:", e);
}
}
// Lighting: sun-lit terrain, sun glare, and optional filmic HDR tone mapping.
// Each part is guarded so a Cesium build without it simply skips it.
function applyLighting(scene) {
const C = Cesium();
try {
if (visuals.sunLighting) scene.globe.enableLighting = true;
} catch (e) {
console.warn("[BetterTerrain] Sun lighting unavailable:", e);
}
try {
if (visuals.sunGlare) {
if (scene.sun) scene.sun.show = true;
if ("sunBloom" in scene) scene.sunBloom = true;
}
} catch (e) {
console.warn("[BetterTerrain] Sun glare unavailable:", e);
}
try {
if (visuals.hdr && "highDynamicRange" in scene) {
scene.highDynamicRange = true;
const stages = scene.postProcessStages;
if (stages) {
if (C.Tonemapper && "tonemapper" in stages) stages.tonemapper = C.Tonemapper.ACES;
if ("exposure" in stages) stages.exposure = visuals.exposure;
}
}
} catch (e) {
console.warn("[BetterTerrain] HDR unavailable:", e);
}
}
// ---- Wavy water ---------------------------------------------------------------
// A custom globe material. Where the terrain height is at sea level it blends in animated waves,
// sun glints and shoreline foam; everywhere else it does nothing (alpha 0).
const WATER_GLSL = [
"uniform float time;",
"uniform float seaLevel;",
"uniform float tolerance;",
"uniform float waveHeight;",
"uniform float opacity;",
"uniform vec3 shallowColor;",
"uniform vec3 deepColor;",
"czm_material czm_getMaterial(czm_materialInput materialInput) {",
" czm_material material = czm_getDefaultMaterial(materialInput);",
" float h = materialInput.height - seaLevel;",
" float water = 1.0 - smoothstep(tolerance * 0.5, tolerance * 1.5, abs(h));",
" if (water <= 0.001) {",
" material.alpha = 0.0;",
" return material;",
" }",
" vec3 posEC = -materialInput.positionToEyeEC;",
" vec3 posWC = (czm_inverseView * vec4(posEC, 1.0)).xyz;",
" vec3 d1 = normalize(vec3(0.74, 0.52, 0.43));",
" vec3 d2 = normalize(vec3(-0.35, 0.81, 0.47));",
" vec3 d3 = normalize(vec3(0.58, -0.62, 0.53));",
" vec3 d4 = normalize(vec3(-0.81, -0.21, 0.55));",
" float k1 = 6.2832 / 160.0;",
" float k2 = 6.2832 / 38.0;",
" float k3 = 6.2832 / 17.0;",
" float k4 = 6.2832 / 7.0;",
" vec3 g = d1 * cos(dot(posWC, d1) * k1 + time * 0.9) * k1 * 1.0",
" + d2 * cos(dot(posWC, d2) * k2 + time * 1.4) * k2 * 0.6",
" + d3 * cos(dot(posWC, d3) * k3 + time * 1.9) * k3 * 0.35",
" + d4 * cos(dot(posWC, d4) * k4 + time * 2.6) * k4 * 0.2;",
" vec3 nEC = normalize(materialInput.normalEC);",
" vec3 gEC = czm_viewRotation * g;",
" gEC -= dot(gEC, nEC) * nEC;",
" vec3 nW = normalize(nEC - gEC * waveHeight * 0.6);",
" vec3 vEC = normalize(materialInput.positionToEyeEC);",
" float sunDot = dot(nEC, czm_sunDirectionEC);",
" float day = smoothstep(-0.15, 0.25, sunDot);",
" float ndv = max(dot(nW, vEC), 0.0);",
" float fres = 0.02 + 0.98 * pow(1.0 - ndv, 5.0);",
" vec3 hv = normalize(czm_sunDirectionEC + vEC);",
" float spec = pow(max(dot(nW, hv), 0.0), 220.0) * day * 2.0;",
" float crest = clamp(0.5 + length(gEC) * waveHeight * 0.8, 0.0, 1.0);",
" vec3 base = mix(deepColor, shallowColor, crest * 0.6);",
" vec3 sky = vec3(0.55, 0.7, 0.9) * day;",
" vec3 col = mix(base, sky + base * 0.2, fres);",
" col += vec3(1.0, 0.95, 0.85) * spec;",
" float edge = water * (1.0 - water) * 4.0;",
" float fn = 0.5 + 0.5 * sin(dot(posWC, vec3(0.31, 0.27, 0.19)) * 0.9 + time * 1.3) * sin(dot(posWC, d2) * 0.7 - time);",
" float foam = edge * smoothstep(0.3, 0.9, fn);",
" col = mix(col, vec3(1.0), foam * 0.8);",
" material.diffuse = col * (0.25 + 0.75 * day);",
" material.alpha = water * clamp(opacity + spec * 0.5 + foam, 0.0, 1.0);",
" return material;",
"}"
].join("\n");
// Stand-ins for the parts Cesium supplies, used only to test-compile the shader above.
const WATER_TEST_PRELUDE = [
"struct czm_materialInput { float s; vec2 st; vec3 str; vec3 normalEC; mat3 tangentToEyeMatrix; vec3 positionToEyeEC; float slope; float height; float aspect; };",
"struct czm_material { vec3 diffuse; float specular; float shininess; vec3 normal; vec3 emission; float alpha; };",
"czm_material czm_getDefaultMaterial(czm_materialInput m) { czm_material r; r.diffuse = vec3(0.0); r.specular = 0.0; r.shininess = 1.0; r.normal = m.normalEC; r.emission = vec3(0.0); r.alpha = 1.0; return r; }",
"uniform mat3 czm_viewRotation;",
"uniform mat4 czm_inverseView;",
"uniform vec3 czm_sunDirectionEC;",
""
].join("\n");
function applyWater(scene) {
try {
const C = Cesium();
const globe = scene.globe;
if (!water.enabled) {
if (window.__btWaterMat && globe.material === window.__btWaterMat) globe.material = undefined;
return;
}
if (window.__btWaterMat && globe.material === window.__btWaterMat) return; // already on
const minor = parseInt(String(C.VERSION || "0.0").split(".")[1], 10);
if (!C.Material || !("material" in globe) || !(minor >= 50)) {
console.info("[BetterTerrain] This GeoFS Cesium version (" + C.VERSION + ") cannot draw wavy water; skipping.");
return;
}
if (globe.material) {
console.info("[BetterTerrain] GeoFS already uses a globe material, so wavy water is skipped.");
return;
}
if (!shaderCompiles(WATER_GLSL, WATER_TEST_PRELUDE)) return;
const v3 = function (css) {
const c = C.Color.fromCssColorString(css);
return new C.Cartesian3(c.red, c.green, c.blue);
};
const mat = new C.Material({
fabric: {
type: "BetterTerrainWater",
uniforms: {
time: 0.0,
seaLevel: water.seaLevel,
tolerance: Math.max(0.05, water.tolerance),
waveHeight: water.waveHeight,
opacity: water.opacity,
shallowColor: v3(water.shallowColor),
deepColor: v3(water.deepColor)
},
source: WATER_GLSL
},
translucent: false
});
globe.material = mat;
window.__btWaterMat = mat;
const t0 = performance.now();
scene.preRender.addEventListener(function () {
mat.uniforms.time = (performance.now() - t0) / 1000 * water.waveSpeed;
});
window.btHeightHere = function () {
const cp = scene.camera.positionCartographic;
return globe.getHeight(cp);
};
} catch (e) {
console.warn("[BetterTerrain] Wavy water unavailable:", e);
}
}
// MSFS-style look: glow around lights, richer atmosphere. Each part is guarded
// so an older Cesium build just skips what it doesn't support.
function applyMsfsLook(scene) {
try {
const stages = scene.postProcessStages;
if (visuals.bloom && stages && stages.bloom) {
stages.bloom.enabled = true;
const u = stages.bloom.uniforms;
if (u) {
u.contrast = 128;
u.brightness = -0.45;
u.glowOnly = false;
u.delta = 1.0;
u.sigma = 2.5;
u.stepSize = 1.0;
}
}
} catch (e) {
console.warn("[BetterTerrain] Bloom unavailable:", e);
}
try {
if (!visuals.atmosphereBoost) return;
if (scene.skyAtmosphere) scene.skyAtmosphere.show = true;
if (scene.atmosphere) {
scene.atmosphere.saturationShift = 0.1;
scene.atmosphere.brightnessShift = 0.05;
}
if ("atmosphereLightIntensity" in scene.globe) scene.globe.atmosphereLightIntensity = 10;
} catch (e) {
console.warn("[BetterTerrain] Atmosphere tweaks unavailable:", e);
}
}
// Makes runway, approach and PAPI lights bigger and visible from further away.
// Keeps GeoFS's own distance curve and just scales it. Applied once per page load;
// lights that already exist may only update after you load another airport.
function tweakAirportLights() {
try {
if (window.__btLightsDone || !visuals.lightBoost || visuals.lightBoost === 1) return;
const C = Cesium();
const fx = window.geofs && window.geofs.fx;
if (!fx) return;
["lightBillboardOptions", "papiBillboardOptions"].forEach(function (name) {
const opts = fx[name];
const s = opts && opts.scaleByDistance;
if (!s) return;
opts.scaleByDistance = new C.NearFarScalar(
s.near, s.nearValue * visuals.lightBoost,
s.far, s.farValue * visuals.lightBoost
);
});
window.__btLightsDone = true;
} catch (e) {
console.warn("[BetterTerrain] Could not adjust airport lights:", e);
}
}
function applyVisuals() {
tweakAirportLights();
try {
const viewer = window.geofs.api.viewer;
const scene = viewer.scene;
const globe = scene.globe;
globe.maximumScreenSpaceError = visuals.sharpness;
globe.showGroundAtmosphere = visuals.groundAtmosphere;
globe.tileCacheSize = 1000;
if (scene.fog) scene.fog.enabled = visuals.fog;
if (scene.postProcessStages && scene.postProcessStages.fxaa) {
scene.postProcessStages.fxaa.enabled = visuals.fxaa;
}
const dpr = Math.min(window.devicePixelRatio || 1, visuals.maxRenderScale || 1.25);
let scale = (visuals.hiDpi ? dpr : 1) * (visuals.supersample || 1);
if (visuals.fpsCap && "targetFrameRate" in viewer) viewer.targetFrameRate = visuals.fpsCap;
viewer.resolutionScale = Math.min(scale, 3);
if (visuals.msaa && "msaaSamples" in scene) scene.msaaSamples = visuals.msaa;
installSharpen(scene);
applyMsfsLook(scene);
applyLighting(scene);
installTimeOfDay(scene);
applyWater(scene);
const layers = viewer.imageryLayers;
for (let i = 0; i < layers.length; i++) {
const layer = layers.get(i);
layer.saturation = visuals.saturation;
layer.contrast = visuals.contrast;
layer.gamma = visuals.gamma;
}
} catch (e) {
console.warn("[BetterTerrain] Could not apply visual tweaks:", e);
}
}
// ---- OSM extras: roads, highways and trees ---------------
const ROAD_WIDTH = {
motorway: 15, trunk: 13, primary: 11, secondary: 9, tertiary: 7.5,
motorway_link: 7, trunk_link: 7, primary_link: 6.5, secondary_link: 6, tertiary_link: 6,
residential: 6, unclassified: 6, service: 4, living_street: 5
};
const MAIN_ROADS = "motorway|trunk|primary|secondary|tertiary|motorway_link|trunk_link|primary_link|secondary_link|tertiary_link";
const ALL_ROADS = MAIN_ROADS + "|residential|unclassified|service|living_street";
const OSM_TILE = 0.02; // degrees, about 2 km
const osmTiles = {};
let osmBusy = false;
let osmVegPref; // undefined = not looked up yet, null = not found
let osmVegTries = 0;
function treeSpriteUrl() {
if (window.__btTreeSprite) return window.__btTreeSprite;
const c = document.createElement("canvas");
c.width = 64;
c.height = 96;
const g = c.getContext("2d");
g.fillStyle = "#4a3a28";
g.fillRect(29, 66, 6, 28);
const blobs = [[32, 56, 24, "#2f5a2a"], [22, 44, 16, "#36662f"], [42, 44, 16, "#36662f"], [32, 30, 18, "#3f7436"], [32, 16, 11, "#4a8240"]];
blobs.forEach(function (b) {
const grad = g.createRadialGradient(b[0] - 4, b[1] - 5, 2, b[0], b[1], b[2]);
grad.addColorStop(0, "#5f9a4e");
grad.addColorStop(1, b[3]);
g.fillStyle = grad;
g.beginPath();
g.arc(b[0], b[1], b[2], 0, Math.PI * 2);
g.fill();
});
window.__btTreeSprite = c.toDataURL("image/png");
return window.__btTreeSprite;
}
function inPoly(x, y, p) {
let inside = false;
for (let i = 0, j = p.length - 2; i < p.length; j = i, i += 2) {
const xi = p[i], yi = p[i + 1], xj = p[j], yj = p[j + 1];
if (((yi > y) !== (yj > y)) && (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) inside = !inside;
}
return inside;
}
function polyAreaM2(p, latC) {
let a = 0;
for (let i = 0, j = p.length - 2; i < p.length; j = i, i += 2) {
a += p[j] * p[i + 1] - p[i] * p[j + 1];
}
const k = 111320;
return Math.abs(a) / 2 * k * k * Math.cos(latC * Math.PI / 180);
}
// Fills a wood or park outline with trees on a jittered grid, never more than "budget".
function scatterTrees(poly, budget, addTree) {
if (budget <= 0 || poly.length < 8) return 0;
let minX = 1e9, maxX = -1e9, minY = 1e9, maxY = -1e9;
for (let i = 0; i < poly.length; i += 2) {
minX = Math.min(minX, poly[i]); maxX = Math.max(maxX, poly[i]);
minY = Math.min(minY, poly[i + 1]); maxY = Math.max(maxY, poly[i + 1]);
}
const latC = (minY + maxY) / 2;
const area = polyAreaM2(poly, latC);
if (area < 400) return 0;
let sp = osm.treeSpacing;
const est = area / (sp * sp);
if (est > budget) sp *= Math.sqrt(est / budget);
const dLat = sp / 111320;
const dLon = sp / (111320 * Math.cos(latC * Math.PI / 180));
let n = 0;
for (let y = minY; y <= maxY && n < budget; y += dLat) {
for (let x = minX; x <= maxX && n < budget; x += dLon) {
const jx = x + (Math.random() - 0.5) * dLon * 0.7;
const jy = y + (Math.random() - 0.5) * dLat * 0.7;
if (inPoly(jx, jy, poly)) {
addTree(jx, jy);
n++;
}
}
}
return n;
}
function buildOsmTile(tile, elements) {
const C = Cesium();
const scene = window.geofs.api.viewer.scene;
const roadsOk = osm.roads && C.GroundPrimitive && C.CorridorGeometry &&
(!C.GroundPrimitive.isSupported || C.GroundPrimitive.isSupported(scene));
const asphalt = [], shoulder = [];
const asphaltColor = C.ColorGeometryInstanceAttribute.fromColor(C.Color.fromCssColorString("#3b3e43"));
const shoulderColor = C.ColorGeometryInstanceAttribute.fromColor(C.Color.fromCssColorString("#9a9a95"));
const trees = osm.trees ? new C.BillboardCollection({ scene: scene }) : null;
const sprite = trees ? treeSpriteUrl() : null;
let treeCount = 0;
function addTree(lon, lat) {
if (!trees || treeCount >= osm.maxTreesPerTile) return;
const sz = 0.8 + Math.random() * 0.5;
trees.add({
position: C.Cartesian3.fromDegrees(lon, lat),
image: sprite,
width: 6 * sz,
height: 9 * sz,
sizeInMeters: true,
verticalOrigin: C.VerticalOrigin.BOTTOM,
heightReference: C.HeightReference.CLAMP_TO_GROUND,
distanceDisplayCondition: new C.DistanceDisplayCondition(0, 4500)
});
treeCount++;
}
elements.forEach(function (el) {
const tags = el.tags || {};
if (el.type === "node" && tags.natural === "tree") {
if (typeof el.lon === "number") addTree(el.lon, el.lat);
return;
}
if (el.type !== "way" || !el.geometry) return;
if (tags.highway && roadsOk && ROAD_WIDTH[tags.highway]) {
const pts = [];
let last = null;
el.geometry.forEach(function (g) {
if (!g) return;
if (!last || Math.abs(g.lon - last.lon) > 1e-5 || Math.abs(g.lat - last.lat) > 1e-5) {
pts.push(g.lon, g.lat);
last = g;
}
});
if (pts.length < 4) return;
const w = ROAD_WIDTH[tags.highway];
const positions = C.Cartesian3.fromDegreesArray(pts);
shoulder.push(new C.GeometryInstance({
geometry: new C.CorridorGeometry({ positions: positions, width: w + 3, cornerType: C.CornerType.ROUNDED }),
attributes: { color: shoulderColor }
}));
asphalt.push(new C.GeometryInstance({
geometry: new C.CorridorGeometry({ positions: positions, width: w, cornerType: C.CornerType.ROUNDED }),
attributes: { color: asphaltColor }
}));
} else if (trees && (tags.natural === "wood" || tags.landuse === "forest" || tags.leisure === "park")) {
const poly = [];
el.geometry.forEach(function (g) { if (g) poly.push(g.lon, g.lat); });
scatterTrees(poly, osm.maxTreesPerTile - treeCount, addTree);
}
});
function addGround(instances) {
if (!instances.length) return;
const prim = new C.GroundPrimitive({
geometryInstances: instances,
classificationType: C.ClassificationType ? C.ClassificationType.TERRAIN : undefined
});
scene.primitives.add(prim);
if (prim.readyPromise && prim.readyPromise.catch) {
prim.readyPromise.catch(function (err) {
console.warn("[BetterTerrain] Roads failed for a tile:", err);
try { scene.primitives.remove(prim); } catch (e) { /* ignore */ }
});
}
tile.prims.push(prim);
}
addGround(shoulder); // wider, lighter strip first, asphalt on top
addGround(asphalt);
if (trees && treeCount > 0) {
scene.primitives.add(trees);
tile.trees = trees;
}
}
function destroyOsmTile(tile) {
try {
const scene = window.geofs.api.viewer.scene;
tile.prims.forEach(function (p) { try { scene.primitives.remove(p); } catch (e) { /* ignore */ } });
if (tile.trees) scene.primitives.remove(tile.trees);
} catch (e) { /* ignore */ }
}
async function loadOsmTile(ix, iy) {
const key = ix + "," + iy;
const tile = { state: "loading", prims: [], ix: ix, iy: iy };
osmTiles[key] = tile;
osmBusy = true;
try {
const south = iy * OSM_TILE, west = ix * OSM_TILE;
const bb = [south, west, south + OSM_TILE, west + OSM_TILE].map(function (v) { return v.toFixed(5); }).join(",");
const roadClasses = osm.smallRoads ? ALL_ROADS : MAIN_ROADS;
const q = "[out:json][timeout:25];(" +
(osm.roads ? 'way["highway"~"^(' + roadClasses + ')$"](' + bb + ");" : "") +
(osm.trees ? 'way["natural"="wood"](' + bb + ');way["landuse"="forest"](' + bb + ');way["leisure"="park"](' + bb + ');node["natural"="tree"](' + bb + ");" : "") +
");out geom 8000;";
const res = await fetch(osm.server, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "data=" + encodeURIComponent(q)
});
if (!res.ok) throw new Error("HTTP " + res.status);
const data = await res.json();
if (osmTiles[key] !== tile) return; // flew away while it loaded
buildOsmTile(tile, data.elements || []);
tile.state = "done";
} catch (err) {
tile.state = "error";
tile.retryAt = Date.now() + 60000;
console.warn("[BetterTerrain] Could not load OpenStreetMap data for a tile (will retry):", err);
} finally {
osmBusy = false;
}
}
function osmTick() {
try {
const C = Cesium();
const scene = window.geofs.api.viewer.scene;
if (!osm.roads && !osm.trees) return;
const cp = scene.camera.positionCartographic;
if (!cp) return;
const lon = C.Math.toDegrees(cp.longitude), lat = C.Math.toDegrees(cp.latitude);
const active = cp.height < osm.maxAltitude;
const cix = Math.floor(lon / OSM_TILE), ciy = Math.floor(lat / OSM_TILE);
// forget tiles that are far away, and retry ones that failed
Object.keys(osmTiles).forEach(function (k) {
const t = osmTiles[k];
if (Math.abs(t.ix - cix) > 2 || Math.abs(t.iy - ciy) > 2 || (t.state === "error" && Date.now() > t.retryAt)) {
destroyOsmTile(t);
delete osmTiles[k];
}
});
// follow GeoFS's vegetation setting if it can be found
if (osmVegPref === undefined && osmVegTries < 5) {
osmVegTries++;
const f = findPref(/veget|tree/i);
if (f) {
osmVegPref = f;
console.info("[BetterTerrain] Trees follow GeoFS setting " + f.path + " (currently " + f.obj[f.key] + ")");
} else if (osmVegTries >= 5) {
osmVegPref = null;
console.info("[BetterTerrain] Could not find GeoFS's vegetation setting, so trees are always on.");
}
}
const treesOn = osmVegPref ? settingIsOn(osmVegPref.obj[osmVegPref.key]) : true;
Object.keys(osmTiles).forEach(function (k) {
const t = osmTiles[k];
t.prims.forEach(function (p) { p.show = active; });
if (t.trees) t.trees.show = active && treesOn;
});
if (active && !osmBusy) {
let best = null, bd = 1e9;
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
if (osmTiles[(cix + dx) + "," + (ciy + dy)]) continue;
const d = dx * dx + dy * dy;
if (d < bd) { bd = d; best = [cix + dx, ciy + dy]; }
}
}
if (best) loadOsmTile(best[0], best[1]);
}
} catch (e) {
console.warn("[BetterTerrain] OSM extras error:", e);
}
}
function startOsmExtras() {
if (window.__btOsmTimer) clearInterval(window.__btOsmTimer);
window.__btOsmTimer = setInterval(osmTick, 4000);
setTimeout(osmTick, 1500);
}
window.geofsNewHDState = true;
window.geofs.geoIpUpdate = function () {
delete window.geofs.api.analytics;
document.body.classList.add("geofs-hd");
if (multiplayerServer !== "default") {
window.geofs.multiplayerHost = multiplayerServer;
}
window.geofs.api.imageryProvider = buildImageryProvider();
window.geofs.api.setImageryProvider(window.geofs.api.imageryProvider, false);
window.geofs.api.viewer.terrainProvider = window.geofs.api.flatRunwayTerrainProviderInstance =
new window.geofs.api.FlatRunwayTerrainProvider({
baseProvider: (window.__btBase = buildBaseTerrain()),
bypass: false,
maximumLevel: terrainSource === "ion" && keys.ion ? terrainMaxLevel : 12
});
applyVisuals();
startOsmExtras();
};
window.executeOnEventDone("geofsStarted", function () {
if (window.geofs.api.hdOn === window.geofsNewHDState) return;
window.jQuery("body").trigger("terrainProviderWillUpdate");
window.geofs.geoIpUpdate();
window.geofs.api.hdOn = window.geofsNewHDState;
window.geofs.api.renderingQuality();
window.jQuery("body").trigger("terrainProviderUpdate");
// renderingQuality() can reset settings, so re-apply afterwards
setTimeout(applyVisuals, 1000);
});
window.executeOnEventDone("afterDeferredload", function () {
window.geofs.mapXYZ = "https://data.geo-fs.com/osm/{z}/{x}/{y}.png";
});
})();