perf: add distance-dependent terrain mesh LOD
This commit is contained in:
1 parent
cf41b72133
commit
b906986718
3 files changed
+151
-23
No files matched your search
@@ -262,7 +262,7 @@ Groups:
|
|||||||
| `fog.distanceKm` | distance haze (0/false = off) |
|
| `fog.distanceKm` | distance haze (0/false = off) |
|
||||||
| `clouds` | deck height (`top`/`bottom` in world units), `density` per weather level, `wind` motion, soft edge/horizon fading, `maxMarchUnits` and `terrainOcclusion` |
|
| `clouds` | deck height (`top`/`bottom` in world units), `density` per weather level, `wind` motion, soft edge/horizon fading, `maxMarchUnits` and `terrainOcclusion` |
|
||||||
| `flights` | aircraft count, altitudes, speeds, destination weights, contrails, night strobes |
|
| `flights` | aircraft count, altitudes, speeds, destination weights, contrails, night strobes |
|
||||||
| `terrain` | DEM mesh, `exaggeration`, edge fade/skirt, `outerDem`, `carLights`, `cityLights`, `wireframe` |
|
| `terrain` | DEM mesh, `exaggeration`, edge fade/skirt, `outerDem`, distance-dependent `lod`, `carLights`, `cityLights`, `wireframe` |
|
||||||
| `carLights` | road width, light sizes, lane separation, traffic range/fade, `trafficSide`, spacing, speed, density, brightness |
|
| `carLights` | road width, light sizes, lane separation, traffic range/fade, `trafficSide`, spacing, speed, density, brightness |
|
||||||
| `lights` | twilight switching window of every artificial light (`offDeg`, `onDeg`, `softDeg`) |
|
| `lights` | twilight switching window of every artificial light (`offDeg`, `onDeg`, `softDeg`) |
|
||||||
| `atmosphere` | turbidity, moon/star gains, night floor, albedo/sky/air gains, haze blocks, cloud albedo/haze, cloud shadows, terrain shadows |
|
| `atmosphere` | turbidity, moon/star gains, night floor, albedo/sky/air gains, haze blocks, cloud albedo/haze, cloud shadows, terrain shadows |
|
||||||
@@ -303,8 +303,13 @@ Other main performance knobs:
|
|||||||
- `antialias: false`: disables MSAA.
|
- `antialias: false`: disables MSAA.
|
||||||
- `maxFps`: caps the animation loop.
|
- `maxFps`: caps the animation loop.
|
||||||
- `clouds.maxMarchUnits`: maximum cloud raymarch length.
|
- `clouds.maxMarchUnits`: maximum cloud raymarch length.
|
||||||
|
- Cloud raymarch uses 48 samples with distance-adaptive spacing: finer near
|
||||||
|
the camera, coarser toward the march limit.
|
||||||
- `clouds.softEdges`, `edgeSoftness`, `horizonSoftness`: soft cloud edges;
|
- `clouds.softEdges`, `edgeSoftness`, `horizonSoftness`: soft cloud edges;
|
||||||
disabling `softEdges` restores the old hard shell behaviour.
|
disabling `softEdges` restores the old hard shell behaviour.
|
||||||
|
- `terrain.lod`: distance-dependent terrain mesh LOD; fine near the centre,
|
||||||
|
coarser toward the border. Set `terrain.lod.enabled: false` for the old
|
||||||
|
uniform mesh grid.
|
||||||
- `atmosphere.cloudShadows` and `atmosphere.terrainShadows`: disable if the
|
- `atmosphere.cloudShadows` and `atmosphere.terrainShadows`: disable if the
|
||||||
scene is GPU-bound.
|
scene is GPU-bound.
|
||||||
- `flights.count`, `flights.contrailChance`, `carLights.density`,
|
- `flights.count`, `flights.contrailChance`, `carLights.density`,
|
||||||
|
|||||||
@@ -474,6 +474,16 @@ window.CONFIG = {
|
|||||||
edgeSkirtKm: 800, // coarse outer terrain pulled past the horizon
|
edgeSkirtKm: 800, // coarse outer terrain pulled past the horizon
|
||||||
edgeSkirtRings: 24, // coarse rings in the outer skirt
|
edgeSkirtRings: 24, // coarse rings in the outer skirt
|
||||||
edgeSkirtNoise: 0.28, // fallback height variation if outer DEM is off
|
edgeSkirtNoise: 0.28, // fallback height variation if outer DEM is off
|
||||||
|
// Distance-dependent terrain mesh LOD: fine near the mosaic centre,
|
||||||
|
// coarser toward the border. The outer skirt remains the farthest LOD.
|
||||||
|
lod: {
|
||||||
|
enabled: true,
|
||||||
|
nearStepPx: 32, // mesh vertex spacing near the centre
|
||||||
|
farStepPx: 96, // mesh vertex spacing near the border
|
||||||
|
innerTiles: 8, // fully fine area around the centre
|
||||||
|
outerTiles: 20 // fully coarse area toward the border
|
||||||
|
// lod: { enabled: false } // old uniform mesh
|
||||||
|
},
|
||||||
outerDem: {
|
outerDem: {
|
||||||
enabled: true, // false uses procedural fallback heights
|
enabled: true, // false uses procedural fallback heights
|
||||||
zoom: 7, // reduced-resolution terrarium zoom for the skirt
|
zoom: 7, // reduced-resolution terrarium zoom for the skirt
|
||||||
|
|||||||
Vendored
+135
-22
@@ -1591,6 +1591,16 @@ var TERRAIN_ON = TERRAIN_CFG.enabled === false ? 0.0 : 1.0;
|
|||||||
var EDGE_FADE_TILES = (typeof TERRAIN_CFG.edgeFadeTiles === 'number')
|
var EDGE_FADE_TILES = (typeof TERRAIN_CFG.edgeFadeTiles === 'number')
|
||||||
? Math.max(0.5, TERRAIN_CFG.edgeFadeTiles) : 4.0;
|
? Math.max(0.5, TERRAIN_CFG.edgeFadeTiles) : 4.0;
|
||||||
var EDGE_SINK_TILES = 0.06;
|
var EDGE_SINK_TILES = 0.06;
|
||||||
|
var TERRAIN_LOD_CFG = TERRAIN_CFG.lod || {};
|
||||||
|
var TERRAIN_LOD_ON = TERRAIN_LOD_CFG.enabled !== false;
|
||||||
|
var TERRAIN_LOD_NEAR_PX = (typeof TERRAIN_LOD_CFG.nearStepPx === 'number')
|
||||||
|
? Math.max(8, Math.min(128, TERRAIN_LOD_CFG.nearStepPx)) : 32;
|
||||||
|
var TERRAIN_LOD_FAR_PX = (typeof TERRAIN_LOD_CFG.farStepPx === 'number')
|
||||||
|
? Math.max(TERRAIN_LOD_NEAR_PX, Math.min(256, TERRAIN_LOD_CFG.farStepPx)) : 96;
|
||||||
|
var TERRAIN_LOD_INNER_TILES = (typeof TERRAIN_LOD_CFG.innerTiles === 'number')
|
||||||
|
? Math.max(1, Math.min(64, TERRAIN_LOD_CFG.innerTiles)) : 8;
|
||||||
|
var TERRAIN_LOD_OUTER_TILES = (typeof TERRAIN_LOD_CFG.outerTiles === 'number')
|
||||||
|
? Math.max(TERRAIN_LOD_INNER_TILES + 1, Math.min(64, TERRAIN_LOD_CFG.outerTiles)) : 20;
|
||||||
var OUTER_DEM_CFG = TERRAIN_CFG.outerDem || {};
|
var OUTER_DEM_CFG = TERRAIN_CFG.outerDem || {};
|
||||||
var OUTER_DEM_ON = OUTER_DEM_CFG.enabled !== false && TERRAIN_ON > 0.5;
|
var OUTER_DEM_ON = OUTER_DEM_CFG.enabled !== false && TERRAIN_ON > 0.5;
|
||||||
var OUTER_DEM_Z = Math.max(4, Math.min(11,
|
var OUTER_DEM_Z = Math.max(4, Math.min(11,
|
||||||
@@ -2584,24 +2594,77 @@ function buildTerrainMesh(gl, state) {
|
|||||||
}
|
}
|
||||||
var tilesX = groundTilesX(), tilesY = groundTilesY();
|
var tilesX = groundTilesX(), tilesY = groundTilesY();
|
||||||
var halfX = tilesX / 2, halfY = tilesY / 2;
|
var halfX = tilesX / 2, halfY = tilesY / 2;
|
||||||
// Decode elevation in metres on the sampling grid: one vertex per
|
// Decode elevation in metres on a distance-dependent LOD grid: fine near
|
||||||
// STEP canvas px (z10 mosaic ~101 m/px source -> ~3.2 km mesh step,
|
// the mosaic centre, coarser toward the border. The outer skirt remains
|
||||||
// 4x finer per axis than the old 13 km tile-centre grid that made
|
// the farthest LOD stage.
|
||||||
// ridges look faceted).
|
function makeLodPositions(size, nearStep, farStep, innerPx, outerPx) {
|
||||||
var STEP = 32; // canvas px between mesh vertices
|
var center = (size - 1) * 0.5;
|
||||||
var s = STEP / ELEV_UNIT_PX; // world units (tiles) per grid step
|
var pos = [center];
|
||||||
var gw = Math.floor((W - 1) / STEP) + 1;
|
function smooth01(t) {
|
||||||
var gh = Math.floor((H - 1) / STEP) + 1;
|
t = Math.max(0, Math.min(1, t));
|
||||||
|
return t * t * (3 - 2 * t);
|
||||||
|
}
|
||||||
|
function stepAt(p) {
|
||||||
|
var d = Math.abs(p - center);
|
||||||
|
var t = (outerPx > innerPx) ? smooth01((d - innerPx) / (outerPx - innerPx)) : 0;
|
||||||
|
return Math.max(1, nearStep + (farStep - nearStep) * t);
|
||||||
|
}
|
||||||
|
var p = center;
|
||||||
|
while (p < size - 1) {
|
||||||
|
p = Math.min(size - 1, p + stepAt(p));
|
||||||
|
pos.push(p);
|
||||||
|
}
|
||||||
|
p = center;
|
||||||
|
while (p > 0) {
|
||||||
|
p = Math.max(0, p - stepAt(p));
|
||||||
|
pos.push(p);
|
||||||
|
}
|
||||||
|
pos.push(0);
|
||||||
|
pos.push(size - 1);
|
||||||
|
pos.sort(function(a, b) { return a - b; });
|
||||||
|
var out = [];
|
||||||
|
for (var i = 0; i < pos.length; i++) {
|
||||||
|
var v = pos[i];
|
||||||
|
if (!out.length || Math.abs(v - out[out.length - 1]) > 0.5) out.push(v);
|
||||||
|
}
|
||||||
|
out[0] = 0;
|
||||||
|
out[out.length - 1] = size - 1;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
var UNIFORM_STEP = 32;
|
||||||
|
var xs, ys;
|
||||||
|
if (TERRAIN_LOD_ON) {
|
||||||
|
var innerPx = TERRAIN_LOD_INNER_TILES * ELEV_UNIT_PX;
|
||||||
|
var outerPx = TERRAIN_LOD_OUTER_TILES * ELEV_UNIT_PX;
|
||||||
|
xs = makeLodPositions(W, TERRAIN_LOD_NEAR_PX, TERRAIN_LOD_FAR_PX, innerPx, outerPx);
|
||||||
|
ys = makeLodPositions(H, TERRAIN_LOD_NEAR_PX, TERRAIN_LOD_FAR_PX, innerPx, outerPx);
|
||||||
|
} else {
|
||||||
|
xs = [];
|
||||||
|
ys = [];
|
||||||
|
for (var ux = 0; ux < W - 1; ux += UNIFORM_STEP) xs.push(ux);
|
||||||
|
xs.push(W - 1);
|
||||||
|
for (var uy = 0; uy < H - 1; uy += UNIFORM_STEP) ys.push(uy);
|
||||||
|
ys.push(H - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var gw = xs.length;
|
||||||
|
var gh = ys.length;
|
||||||
|
var xUnits = new Float32Array(gw);
|
||||||
|
var yUnits = new Float32Array(gh);
|
||||||
|
for (var xi = 0; xi < gw; xi++) xUnits[xi] = xs[xi] / ELEV_UNIT_PX;
|
||||||
|
for (var yi = 0; yi < gh; yi++) yUnits[yi] = ys[yi] / ELEV_UNIT_PX;
|
||||||
|
|
||||||
var h = new Float32Array(gw * gh);
|
var h = new Float32Array(gw * gh);
|
||||||
for (var j = 0; j < gh; j++) {
|
for (var j = 0; j < gh; j++) {
|
||||||
var py = Math.min(H - 1, j * STEP);
|
var py = Math.max(0, Math.min(H - 1, Math.round(ys[j])));
|
||||||
var row = j * gw;
|
var row = j * gw;
|
||||||
for (var i = 0; i < gw; i++) {
|
for (var i = 0; i < gw; i++) {
|
||||||
// mirrored X: the DEM mosaic runs west-right while the world
|
// mirrored X: the DEM mosaic runs west-right while the world
|
||||||
// axis +x runs east (verified against the street texture) -
|
// axis +x runs east (verified against the street texture) -
|
||||||
// read the height column mirrored so ridges sit on the same
|
// read the height column mirrored so ridges sit on the same
|
||||||
// side as the roads.
|
// side as the roads.
|
||||||
var pxx = Math.max(0, W - 1 - i * STEP);
|
var pxx = Math.max(0, Math.min(W - 1, W - 1 - Math.round(xs[i])));
|
||||||
var idx = (py * W + pxx) * 4;
|
var idx = (py * W + pxx) * 4;
|
||||||
h[row + i] = data[idx] * 256 + data[idx + 1] + data[idx + 2] / 256 - 32768;
|
h[row + i] = data[idx] * 256 + data[idx + 1] + data[idx + 2] / 256 - 32768;
|
||||||
}
|
}
|
||||||
@@ -2637,9 +2700,9 @@ function buildTerrainMesh(gl, state) {
|
|||||||
// horizon instead of ending in a cut-off ridge line.
|
// horizon instead of ending in a cut-off ridge line.
|
||||||
for (var ej = 0; ej < gh; ej++) {
|
for (var ej = 0; ej < gh; ej++) {
|
||||||
var erow = ej * gw;
|
var erow = ej * gw;
|
||||||
var dz = Math.min(ej, gh - 1 - ej) * s;
|
var dz = Math.min(yUnits[ej], tilesY - yUnits[ej]);
|
||||||
for (var ei = 0; ei < gw; ei++) {
|
for (var ei = 0; ei < gw; ei++) {
|
||||||
var dx = Math.min(ei, gw - 1 - ei) * s;
|
var dx = Math.min(xUnits[ei], tilesX - xUnits[ei]);
|
||||||
var t = Math.max(0, Math.min(1, Math.min(dx, dz) / EDGE_FADE_TILES));
|
var t = Math.max(0, Math.min(1, Math.min(dx, dz) / EDGE_FADE_TILES));
|
||||||
t = t * t * (3 - 2 * t);
|
t = t * t * (3 - 2 * t);
|
||||||
h[erow + ei] = h[erow + ei] * t - (1 - t) * EDGE_SINK_TILES;
|
h[erow + ei] = h[erow + ei] * t - (1 - t) * EDGE_SINK_TILES;
|
||||||
@@ -2664,14 +2727,15 @@ function buildTerrainMesh(gl, state) {
|
|||||||
function setVertex(i, j) {
|
function setVertex(i, j) {
|
||||||
var iL = Math.max(0, i - 1), iR = Math.min(gw - 1, i + 1);
|
var iL = Math.max(0, i - 1), iR = Math.min(gw - 1, i + 1);
|
||||||
var jD = Math.max(0, j - 1), jU = Math.min(gh - 1, j + 1);
|
var jD = Math.max(0, j - 1), jU = Math.min(gh - 1, j + 1);
|
||||||
var dx = (hAt(iR, j) - hAt(iL, j)) / ((iR - iL) * s);
|
var dx = (hAt(iR, j) - hAt(iL, j)) / Math.max(1e-6, xUnits[iR] - xUnits[iL]);
|
||||||
var dz = (hAt(i, jU) - hAt(i, jD)) / ((jU - jD) * s);
|
var dz = (hAt(i, jU) - hAt(i, jD)) / Math.max(1e-6, yUnits[jU] - yUnits[jD]);
|
||||||
var nx = -dx, ny = 1, nz = -dz;
|
var nx = -dx, ny = 1, nz = -dz;
|
||||||
var nl = Math.hypot(nx, ny, nz) || 1;
|
var nl = Math.hypot(nx, ny, nz) || 1;
|
||||||
nx /= nl; ny /= nl; nz /= nl;
|
nx /= nl; ny /= nl; nz /= nl;
|
||||||
verts[vi++] = i * s - halfX;
|
var _x = xUnits[i] - halfX, _z = halfY - yUnits[j];
|
||||||
|
verts[vi++] = _x;
|
||||||
verts[vi++] = hAt(i, j);
|
verts[vi++] = hAt(i, j);
|
||||||
verts[vi++] = halfY - j * s;
|
verts[vi++] = _z;
|
||||||
verts[vi++] = nx;
|
verts[vi++] = nx;
|
||||||
verts[vi++] = ny;
|
verts[vi++] = ny;
|
||||||
verts[vi++] = nz;
|
verts[vi++] = nz;
|
||||||
@@ -2679,7 +2743,6 @@ function buildTerrainMesh(gl, state) {
|
|||||||
// world +x (verified against landmarks), so u = 0.5 - x/tilesX.
|
// world +x (verified against landmarks), so u = 0.5 - x/tilesX.
|
||||||
// Heights are read with the same mirror (see decode loop), so
|
// Heights are read with the same mirror (see decode loop), so
|
||||||
// ridges and roads agree. V matches the cloud-pass convention.
|
// ridges and roads agree. V matches the cloud-pass convention.
|
||||||
var _x = i * s - halfX, _z = halfY - j * s;
|
|
||||||
verts[vi++] = 0.5 - _x / tilesX;
|
verts[vi++] = 0.5 - _x / tilesX;
|
||||||
verts[vi++] = _z / tilesY + 0.5;
|
verts[vi++] = _z / tilesY + 0.5;
|
||||||
// Detailed terrain uses the basemap texture; the color attribute is
|
// Detailed terrain uses the basemap texture; the color attribute is
|
||||||
@@ -2754,8 +2817,8 @@ function buildTerrainMesh(gl, state) {
|
|||||||
function add(i, j) {
|
function add(i, j) {
|
||||||
var o = outwardFor(i, j);
|
var o = outwardFor(i, j);
|
||||||
loop.push({
|
loop.push({
|
||||||
x: i * s - halfX,
|
x: xUnits[i] - halfX,
|
||||||
z: halfY - j * s,
|
z: halfY - yUnits[j],
|
||||||
y: hAt(i, j),
|
y: hAt(i, j),
|
||||||
ox: o[0], oz: o[1]
|
ox: o[0], oz: o[1]
|
||||||
});
|
});
|
||||||
@@ -2813,9 +2876,9 @@ function buildTerrainMesh(gl, state) {
|
|||||||
var wire = new Float32Array(wireVerts);
|
var wire = new Float32Array(wireVerts);
|
||||||
var wi = 0;
|
var wi = 0;
|
||||||
function pushVert(i, j) {
|
function pushVert(i, j) {
|
||||||
wire[wi++] = i * s - halfX;
|
wire[wi++] = xUnits[i] - halfX;
|
||||||
wire[wi++] = h[j * gw + i] * ELEV_SCALE_UNITS;
|
wire[wi++] = h[j * gw + i] * ELEV_SCALE_UNITS;
|
||||||
wire[wi++] = halfY - j * s;
|
wire[wi++] = halfY - yUnits[j];
|
||||||
}
|
}
|
||||||
for (var j = 0; j < rows; j++) {
|
for (var j = 0; j < rows; j++) {
|
||||||
for (var i = 0; i < cols - 1; i++) {
|
for (var i = 0; i < cols - 1; i++) {
|
||||||
@@ -2837,7 +2900,57 @@ function buildTerrainMesh(gl, state) {
|
|||||||
state.terrain.wireVbo = wireBuf;
|
state.terrain.wireVbo = wireBuf;
|
||||||
state.terrain.wireVertCount = (cols - 1) * rows * 2 + cols * (rows - 1) * 2;
|
state.terrain.wireVertCount = (cols - 1) * rows * 2 + cols * (rows - 1) * 2;
|
||||||
}
|
}
|
||||||
terrainSample = { h: h, gw: gw, gh: gh, s: s, halfX: halfX, halfY: halfY };
|
// Sampling grid for cars/settlements stays uniform even when the visual
|
||||||
|
// mesh uses distance-dependent LOD. This keeps the car/city worker code
|
||||||
|
// simple and stable; the mesh itself is the LOD-optimised surface.
|
||||||
|
var SAMPLE_STEP = 32;
|
||||||
|
var sampleS = SAMPLE_STEP / ELEV_UNIT_PX;
|
||||||
|
var sgw = Math.floor((W - 1) / SAMPLE_STEP) + 1;
|
||||||
|
var sgh = Math.floor((H - 1) / SAMPLE_STEP) + 1;
|
||||||
|
var sh = new Float32Array(sgw * sgh);
|
||||||
|
for (var sj = 0; sj < sgh; sj++) {
|
||||||
|
var spy = Math.min(H - 1, sj * SAMPLE_STEP);
|
||||||
|
var srow = sj * sgw;
|
||||||
|
for (var si = 0; si < sgw; si++) {
|
||||||
|
var spx = Math.max(0, W - 1 - si * SAMPLE_STEP);
|
||||||
|
var sidx = (spy * W + spx) * 4;
|
||||||
|
sh[srow + si] = data[sidx] * 256 + data[sidx + 1] + data[sidx + 2] / 256 - 32768;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function shblur(src, out) {
|
||||||
|
for (var bj = 0; bj < sgh; bj++) {
|
||||||
|
var brow = bj * sgw;
|
||||||
|
for (var bi = 0; bi < sgw; bi++) {
|
||||||
|
var a = src[brow + (bi > 0 ? bi - 1 : 0)];
|
||||||
|
var b = src[brow + bi];
|
||||||
|
var c = src[brow + (bi < sgw - 1 ? bi + 1 : sgw - 1)];
|
||||||
|
out[brow + bi] = (a + 2 * b + c) * 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function svblur(src, out) {
|
||||||
|
for (var bj = 0; bj < sgh; bj++) {
|
||||||
|
var jm = (bj > 0 ? bj - 1 : 0) * sgw;
|
||||||
|
var jp = (bj < sgh - 1 ? bj + 1 : sgh - 1) * sgw;
|
||||||
|
var brow = bj * sgw;
|
||||||
|
for (var bi = 0; bi < sgw; bi++) {
|
||||||
|
out[brow + bi] = (src[jm + bi] + 2 * src[brow + bi] + src[jp + bi]) * 0.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var st1 = new Float32Array(sgw * sgh);
|
||||||
|
shblur(sh, st1); svblur(st1, sh);
|
||||||
|
for (var sej = 0; sej < sgh; sej++) {
|
||||||
|
var serow = sej * sgw;
|
||||||
|
var sdz = Math.min(sej, sgh - 1 - sej) * sampleS;
|
||||||
|
for (var sei = 0; sei < sgw; sei++) {
|
||||||
|
var sdx = Math.min(sei, sgw - 1 - sei) * sampleS;
|
||||||
|
var st = Math.max(0, Math.min(1, Math.min(sdx, sdz) / EDGE_FADE_TILES));
|
||||||
|
st = st * st * (3 - 2 * st);
|
||||||
|
sh[serow + sei] = sh[serow + sei] * st - (1 - st) * EDGE_SINK_TILES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
terrainSample = { h: sh, gw: sgw, gh: sgh, s: sampleS, halfX: halfX, halfY: halfY };
|
||||||
// Terrain just became available: (re)build both off-thread layers. They
|
// Terrain just became available: (re)build both off-thread layers. They
|
||||||
// upload themselves once the worker answers, so nothing here blocks.
|
// upload themselves once the worker answers, so nothing here blocks.
|
||||||
kickCarBuild(false);
|
kickCarBuild(false);
|
||||||
|
|||||||
Reference in new issue
Block a user