Skip to content

Commit 6bf36a5

Browse files
Fix terrain globe Mercator tile warping
1 parent f147137 commit 6bf36a5

2 files changed

Lines changed: 104 additions & 17 deletions

File tree

modules/geo-layers/src/terrain-layer/terrain-layer.ts

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '@deck.gl/core';
1717
import {SimpleMeshLayer} from '@deck.gl/mesh-layers';
1818
import {COORDINATE_SYSTEM} from '@deck.gl/core';
19-
import type {MeshAttributes} from '@loaders.gl/schema';
19+
import type {Mesh} from '@loaders.gl/schema';
2020
import {TerrainWorkerLoader} from '@loaders.gl/terrain';
2121
import TileLayer, {TileLayerProps} from '../tile-layer/tile-layer';
2222
import type {
@@ -33,6 +33,8 @@ const TILE_OVERLAP_PIXELS = 1;
3333
const MIN_TERRAIN_MESH_MAX_ERROR = 1;
3434
const MAX_LATITUDE = 90;
3535
const MAX_LONGITUDE = 180;
36+
const DEGREES_TO_RADIANS = Math.PI / 180;
37+
const RADIANS_TO_DEGREES = 180 / Math.PI;
3638

3739
const defaultProps: DefaultProps<TerrainLayerProps> = {
3840
...TileLayer.defaultProps,
@@ -111,9 +113,9 @@ type TerrainLoadProps = {
111113
signal?: AbortSignal;
112114
};
113115

114-
type MeshAndTexture = [MeshAttributes | null, TextureSource | null];
116+
type MeshAndTexture = [Mesh | null, TextureSource | null];
115117
type MeshBoundingBox = [min: number[], max: number[]];
116-
type MeshWithBoundingBox = MeshAttributes & {
118+
type MeshWithBoundingBox = Mesh & {
117119
header?: {
118120
boundingBox?: MeshBoundingBox;
119121
};
@@ -165,7 +167,7 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
165167

166168
state!: {
167169
isTiled?: boolean;
168-
terrain?: MeshAttributes;
170+
terrain?: Mesh;
169171
zRange?: ZRange | null;
170172
};
171173

@@ -204,7 +206,7 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
204206
elevationDecoder,
205207
meshMaxError,
206208
signal
207-
}: TerrainLoadProps): Promise<MeshAttributes> | null {
209+
}: TerrainLoadProps): Promise<Mesh> | null {
208210
if (!elevationData) {
209211
return null;
210212
}
@@ -249,13 +251,16 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
249251
Boolean(viewport.resolution && viewport.resolution > 0)
250252
);
251253

252-
const terrain = this.loadTerrain({
253-
elevationData: dataUrl,
254-
bounds: overlappedBounds,
255-
elevationDecoder,
256-
meshMaxError,
257-
signal
258-
});
254+
const terrain =
255+
this.loadTerrain({
256+
elevationData: dataUrl,
257+
bounds: overlappedBounds,
258+
elevationDecoder,
259+
meshMaxError,
260+
signal
261+
})?.then(mesh =>
262+
viewport.resolution && mesh ? remapMeshToWebMercatorTile(mesh, overlappedBounds) : mesh
263+
) ?? Promise.resolve(null);
259264
const surface = textureUrl
260265
? // If surface image fails to load, the tile should still be displayed
261266
fetch(textureUrl, {propName: 'texture', layer: this, loaders: [], signal}).catch(_ => null)
@@ -319,11 +324,9 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
319324
const {zRange} = this.state;
320325
const ranges = tiles
321326
.map(tile => tile.content)
322-
.filter(Boolean)
323-
.map(arr => {
324-
// @ts-ignore
325-
const bounds = arr[0].header.boundingBox;
326-
return bounds.map(bound => bound[2]);
327+
.flatMap(arr => {
328+
const bounds = arr?.[0]?.header?.boundingBox;
329+
return bounds ? [bounds.map(bound => bound[2])] : [];
327330
});
328331
if (ranges.length === 0) {
329332
return;
@@ -417,3 +420,45 @@ export default class TerrainLayer<ExtraPropsT extends {} = {}> extends Composite
417420

418421
const isTileSetURL = (url: string): boolean =>
419422
url.includes('{x}') && (url.includes('{y}') || url.includes('{-y}'));
423+
424+
function remapMeshToWebMercatorTile(mesh: Mesh, bounds: Bounds): Mesh {
425+
const positionAttribute = mesh.attributes.POSITION;
426+
const texCoordAttribute = mesh.attributes.TEXCOORD_0;
427+
const positions = positionAttribute?.value;
428+
const texCoords = texCoordAttribute?.value;
429+
if (!positions || !texCoords) {
430+
return mesh;
431+
}
432+
433+
const [, south, , north] = bounds;
434+
const northY = lngLatToMercatorY(north);
435+
const southY = lngLatToMercatorY(south);
436+
const remappedPositions = new Float32Array(positions);
437+
438+
for (let i = 0; i < texCoords.length / 2; i++) {
439+
const v = texCoords[i * 2 + 1];
440+
const mercatorY = northY + (southY - northY) * v;
441+
remappedPositions[i * 3 + 1] = mercatorYToLat(mercatorY);
442+
}
443+
444+
return {
445+
...mesh,
446+
attributes: {
447+
...mesh.attributes,
448+
POSITION: {
449+
...positionAttribute,
450+
value: remappedPositions
451+
}
452+
}
453+
};
454+
}
455+
456+
function lngLatToMercatorY(latitude: number): number {
457+
const clampedLatitude = Math.max(-85.051129, Math.min(85.051129, latitude));
458+
const sin = Math.sin(clampedLatitude * DEGREES_TO_RADIANS);
459+
return 0.5 - Math.log((1 + sin) / (1 - sin)) / (4 * Math.PI);
460+
}
461+
462+
function mercatorYToLat(y: number): number {
463+
return Math.atan(Math.sinh(Math.PI * (1 - 2 * y))) * RADIANS_TO_DEGREES;
464+
}

test/modules/geo-layers/terrain-layer.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import {test, expect} from 'vitest';
66
import {generateLayerTests, testLayerAsync} from '@deck.gl/test-utils/vitest';
77
import {TerrainLayer, TileLayer} from '@deck.gl/geo-layers';
8+
import {_GlobeView as GlobeView} from '@deck.gl/core';
89
import {SimpleMeshLayer} from '@deck.gl/mesh-layers';
910
import {TerrainLoader} from '@loaders.gl/terrain';
1011

@@ -47,3 +48,44 @@ test('TerrainLayer', async () => {
4748
onError: err => expect(err).toBeFalsy()
4849
});
4950
});
51+
52+
test('TerrainLayer#globe remaps WebMercator tile rows to lng/lat mesh positions', async () => {
53+
const sourceMesh = {
54+
attributes: {
55+
POSITION: {value: new Float32Array([0, 80, 0, 0.5, 40, 0, 1, 0, 0]), size: 3},
56+
TEXCOORD_0: {value: new Float32Array([0, 0, 0.5, 0.5, 1, 1]), size: 2}
57+
}
58+
};
59+
const layer = new TerrainLayer({
60+
id: 'terrain-globe-mercator',
61+
elevationData: 'terrain/{z}/{x}/{y}.png',
62+
fetch: () => Promise.resolve(sourceMesh)
63+
});
64+
layer.context = {
65+
viewport: new GlobeView().makeViewport({
66+
width: 512,
67+
height: 512,
68+
viewState: {
69+
longitude: 0,
70+
latitude: 0,
71+
zoom: 1
72+
}
73+
})
74+
};
75+
layer.state = {isTiled: true};
76+
77+
const [mesh] = await layer.getTiledTerrainData({
78+
index: {x: 0, y: 0, z: 1},
79+
id: '0-0-1',
80+
bbox: {west: 0, south: 0, east: 1, north: 80},
81+
zoom: 1
82+
});
83+
const positions = mesh.attributes.POSITION.value;
84+
85+
expect(positions[1], 'top row latitude is preserved').toBeGreaterThan(80);
86+
expect(
87+
positions[4],
88+
'middle row uses Mercator latitude instead of linear latitude'
89+
).toBeGreaterThan(40);
90+
expect(positions[7], 'bottom row latitude is preserved').toBeLessThan(0);
91+
});

0 commit comments

Comments
 (0)