diff --git a/debug/3d-playground.html b/debug/3d-playground.html
index 46ffbf39fe6..59871d0ad8c 100644
--- a/debug/3d-playground.html
+++ b/debug/3d-playground.html
@@ -87,7 +87,7 @@
enableTerrain.onFinishChange((value) => {
map.setTerrain(value ? {"source": "mapbox-dem", "exaggeration": demo3d.terrainExaggeration} : null);
});
- var terrainExaggeration = terrain.add(demo3d, 'terrainExaggeration', 0, 2);
+ var terrainExaggeration = terrain.add(demo3d, 'terrainExaggeration', 0, 100);
terrainExaggeration.onFinishChange((value) => {
if (demo3d.enableTerrain) {
map.setTerrain({"source": "mapbox-dem", "exaggeration": value});
diff --git a/src/geo/projection/globe_util.js b/src/geo/projection/globe_util.js
index 35f524112c2..edc826a2b73 100644
--- a/src/geo/projection/globe_util.js
+++ b/src/geo/projection/globe_util.js
@@ -378,7 +378,7 @@ function mercatorTileCornersInCameraSpace({x, y, z}: CanonicalTileID, numTiles:
// // Ensure that the tile viewed is the nearest when across the antimeridian
let wrap = 0;
- const tileCenterXFromCamera = (w + e) / 2 - camX;
+ const tileCenterXFromCamera = (w + e) / 2 - camX;
if (tileCenterXFromCamera > .5) {
wrap = -1;
} else if (tileCenterXFromCamera < -.5) {
@@ -389,10 +389,10 @@ function mercatorTileCornersInCameraSpace({x, y, z}: CanonicalTileID, numTiles:
camY *= numTiles;
// Transform Mercator coordinates to points on the plane tangent to the globe at cameraCenter.
- w = ((w + wrap) * numTiles - camX) * mercatorScale + camX;
- e = ((e + wrap) * numTiles - camX) * mercatorScale + camX;
- n = (n * numTiles - camY) * mercatorScale + camY;
- s = (s * numTiles - camY) * mercatorScale + camY;
+ w = ((w + wrap) * numTiles - camX) * mercatorScale + camX;
+ e = ((e + wrap) * numTiles - camX) * mercatorScale + camX;
+ n = (n * numTiles - camY) * mercatorScale + camY;
+ s = (s * numTiles - camY) * mercatorScale + camY;
return [[w, s, 0],
[e, s, 0],
@@ -691,6 +691,20 @@ const POLE_RAD = degToRad(85.0);
const POLE_COS = Math.cos(POLE_RAD);
const POLE_SIN = Math.sin(POLE_RAD);
+// Generate terrain grid with embedded skirts
+const EMBED_SKIRTS = true;
+
+type GridLodSegments = {
+ withoutSkirts: SegmentVector,
+ withSkirts: SegmentVector
+};
+
+type GridWithLods = {
+ vertices: PosArray,
+ indices: TriangleIndexArray,
+ segments: Array
+};
+
export class GlobeSharedBuffers {
_poleNorthVertexBuffer: VertexBuffer;
_poleSouthVertexBuffer: VertexBuffer;
@@ -699,7 +713,7 @@ export class GlobeSharedBuffers {
_gridBuffer: VertexBuffer;
_gridIndexBuffer: IndexBuffer;
- _gridSegments: Array;
+ _gridSegments: Array;
_wireframeIndexBuffer: IndexBuffer;
_wireframeSegments: Array;
@@ -716,7 +730,10 @@ export class GlobeSharedBuffers {
this._poleNorthVertexBuffer.destroy();
this._poleSouthVertexBuffer.destroy();
for (const segments of this._poleSegments) segments.destroy();
- for (const segments of this._gridSegments) segments.destroy();
+ for (const segments of this._gridSegments) {
+ segments.withSkirts.destroy();
+ segments.withoutSkirts.destroy();
+ }
if (this._wireframeIndexBuffer) {
this._wireframeIndexBuffer.destroy();
@@ -724,37 +741,134 @@ export class GlobeSharedBuffers {
}
}
- _createGrid(context: Context) {
- const gridVertices = new PosArray();
- const gridIndices = new TriangleIndexArray();
-
- const quadExt = GLOBE_VERTEX_GRID_SIZE;
- const vertexExt = quadExt + 1;
-
- for (let j = 0; j < vertexExt; j++)
- for (let i = 0; i < vertexExt; i++)
- gridVertices.emplaceBack(i, j);
-
- this._gridSegments = [];
- for (let k = 0, primitiveOffset = 0; k < GLOBE_LATITUDINAL_GRID_LOD_TABLE.length; k++) {
- const latitudinalLod = GLOBE_LATITUDINAL_GRID_LOD_TABLE[k];
- for (let j = 0; j < latitudinalLod; j++) {
- for (let i = 0; i < quadExt; i++) {
- const index = j * vertexExt + i;
- gridIndices.emplaceBack(index + 1, index, index + vertexExt);
- gridIndices.emplaceBack(index + vertexExt, index + vertexExt + 1, index + 1);
+ // Generate terrain grid vertices and indices for all LOD's
+ //
+ // Grid vertices memory layout:
+ //
+ // First line Skirt
+ // ┌───────────────┐
+ // │┌─────────────┐│
+ // Left ││┼┼┼┼┼┼┼┼┼┼┼┼┼││ Right
+ // Border ││┼┼┼┼┼┼┼┼┼┼┼┼┼││ Border
+ // Skirt │├─────────────┤│ Skirt
+ // ││ Main Grid ││
+ // │├─────────────┤│
+ // ││┼┼┼┼┼┼┼┼┼┼┼┼┼││
+ // ││┼┼┼┼┼┼┼┼┼┼┼┼┼││
+ // │└─────────────┘│
+ // ├───────────────┤
+ // ├───────────────┤
+ // └───────────────┘
+ // Bottom Skirt = Number of LOD's
+ //
+ _fillGridMeshWithLods(longitudinalCellsCount: number, latitudinalLods: number[]): GridWithLods {
+ const vertices = new PosArray();
+ const indices = new TriangleIndexArray();
+ const segments: Array = [];
+
+ const xVertices = longitudinalCellsCount + 1 + 2 * (EMBED_SKIRTS ? 1 : 0);
+ const yVerticesHighLodNoStrip = latitudinalLods[0] + 1;
+ const yVerticesHighLodWithStrip = latitudinalLods[0] + 1 + (EMBED_SKIRTS ? 1 + latitudinalLods.length : 0);
+
+ // Index adjustment, used to make strip (x, y) vertex input attribute data
+ // to match same data on ordinary grid edges
+ const prepareVertex = (x: number, y: number, isSkirt: boolean) => {
+ if (!EMBED_SKIRTS) return [x, y];
+
+ let adjustedX = (() => {
+ if (x === xVertices - 1) {
+ return x - 2;
+ } else if (x === 0) {
+ return x;
+ } else {
+ return x - 1;
+ }
+ })();
+
+ // Skirt factor is introduces as an offset to the .x coordinate, similar to how it's done for mercator grids
+ const skirtOffset = 24575;
+ adjustedX += isSkirt ? skirtOffset : 0;
+
+ return [adjustedX, y];
+ };
+
+ // Add first horizontal strip if present
+ if (EMBED_SKIRTS) {
+ for (let x = 0; x < xVertices; ++x) {
+ vertices.emplaceBack(...prepareVertex(x, 0, true));
+ }
+ }
+
+ // Add main grid part with vertices strips embedded
+ for (let y = 0; y < yVerticesHighLodNoStrip; ++y) {
+ for (let x = 0; x < xVertices; ++x) {
+ const isSideBorder = (x === 0 || x === xVertices - 1);
+
+ vertices.emplaceBack(...prepareVertex(x, y, isSideBorder && EMBED_SKIRTS));
+ }
+ }
+
+ // Add bottom strips for each LOD
+ if (EMBED_SKIRTS) {
+ for (let lodIdx = 0; lodIdx < latitudinalLods.length; ++lodIdx) {
+ const lastYRowForLod = latitudinalLods[lodIdx];
+ for (let x = 0; x < xVertices; ++x) {
+ vertices.emplaceBack(...prepareVertex(x, lastYRowForLod, true));
+ }
+ }
+ }
+
+ // Fill triangles
+ for (let lodIdx = 0; lodIdx < latitudinalLods.length; ++lodIdx) {
+ const indexOffset = indices.length;
+
+ const yVerticesLod = latitudinalLods[lodIdx] + 1 + 2 * (EMBED_SKIRTS ? 1 : 0);
+
+ const skirtsOnlyIndices = new TriangleIndexArray();
+
+ for (let y = 0; y < yVerticesLod - 1; y++) {
+ const isLastLine = (y === yVerticesLod - 2);
+ const offsetToNextRow =
+ (isLastLine && EMBED_SKIRTS ?
+ (xVertices * (yVerticesHighLodWithStrip - latitudinalLods.length + lodIdx - y)) :
+ xVertices);
+
+ for (let x = 0; x < xVertices - 1; x++) {
+ const idx = y * xVertices + x;
+
+ const isSkirt = EMBED_SKIRTS && (y === 0 || isLastLine || x === 0 || x === xVertices - 2);
+
+ if (isSkirt) {
+ skirtsOnlyIndices.emplaceBack(idx + 1, idx, idx + offsetToNextRow);
+ skirtsOnlyIndices.emplaceBack(idx + offsetToNextRow, idx + offsetToNextRow + 1, idx + 1);
+ } else {
+ indices.emplaceBack(idx + 1, idx, idx + offsetToNextRow);
+ indices.emplaceBack(idx + offsetToNextRow, idx + offsetToNextRow + 1, idx + 1);
+ }
}
}
- const numVertices = (latitudinalLod + 1) * vertexExt;
- const numPrimitives = latitudinalLod * quadExt * 2;
+ // Segments grid only
+ const withoutSkirts = SegmentVector.simpleSegment(0, indexOffset, vertices.length, indices.length - indexOffset);
+
+ for (let i = 0; i < skirtsOnlyIndices.uint16.length; i += 3) {
+ indices.emplaceBack(skirtsOnlyIndices.uint16[i], skirtsOnlyIndices.uint16[i + 1], skirtsOnlyIndices.uint16[i + 2]);
+ }
- this._gridSegments.push(SegmentVector.simpleSegment(0, primitiveOffset, numVertices, numPrimitives));
- primitiveOffset += numPrimitives;
+ // Segments grid + skirts only
+ const withSkirts = SegmentVector.simpleSegment(0, indexOffset, vertices.length, indices.length - indexOffset);
+ segments.push({withoutSkirts, withSkirts});
}
- this._gridBuffer = context.createVertexBuffer(gridVertices, posAttributes.members);
- this._gridIndexBuffer = context.createIndexBuffer(gridIndices, true);
+ return {vertices, indices, segments};
+ }
+
+ _createGrid(context: Context) {
+ const gridWithLods = this._fillGridMeshWithLods(GLOBE_VERTEX_GRID_SIZE, GLOBE_LATITUDINAL_GRID_LOD_TABLE);
+ this._gridSegments = gridWithLods.segments;
+
+ this._gridBuffer = context.createVertexBuffer(gridWithLods.vertices, posAttributes.members);
+ this._gridIndexBuffer = context.createIndexBuffer(gridWithLods.indices, true);
}
_createPoles(context: Context) {
@@ -794,8 +908,8 @@ export class GlobeSharedBuffers {
this._poleSouthVertexBuffer = context.createVertexBuffer(southVertices, globeLayoutAttributes, false);
}
- getGridBuffers(latitudinalLod: number): [VertexBuffer, IndexBuffer, SegmentVector] {
- return [this._gridBuffer, this._gridIndexBuffer, this._gridSegments[latitudinalLod]];
+ getGridBuffers(latitudinalLod: number, withSkirts: boolean): [VertexBuffer, IndexBuffer, SegmentVector] {
+ return [this._gridBuffer, this._gridIndexBuffer, withSkirts ? this._gridSegments[latitudinalLod].withSkirts : this._gridSegments[latitudinalLod].withoutSkirts];
}
getPoleBuffers(z: number): [VertexBuffer, VertexBuffer, IndexBuffer, SegmentVector] {
@@ -806,13 +920,15 @@ export class GlobeSharedBuffers {
if (!this._wireframeSegments) {
const wireframeIndices = new LineIndexArray();
const quadExt = GLOBE_VERTEX_GRID_SIZE;
- const vertexExt = quadExt + 1;
+ const vertexExt = quadExt + 1 + (EMBED_SKIRTS ? 2 : 0);
+
+ const iterOffset = EMBED_SKIRTS ? 1 : 0;
this._wireframeSegments = [];
for (let k = 0, primitiveOffset = 0; k < GLOBE_LATITUDINAL_GRID_LOD_TABLE.length; k++) {
const latitudinalLod = GLOBE_LATITUDINAL_GRID_LOD_TABLE[k];
- for (let j = 0; j < latitudinalLod; j++) {
- for (let i = 0; i < quadExt; i++) {
+ for (let j = iterOffset; j < latitudinalLod + iterOffset; j++) {
+ for (let i = iterOffset; i < quadExt + iterOffset; i++) {
const index = j * vertexExt + i;
wireframeIndices.emplaceBack(index, index + 1);
wireframeIndices.emplaceBack(index, index + vertexExt);
diff --git a/src/shaders/_prelude_terrain.vertex.glsl b/src/shaders/_prelude_terrain.vertex.glsl
index 4aaa1e5d086..6ed1faa51d3 100644
--- a/src/shaders/_prelude_terrain.vertex.glsl
+++ b/src/shaders/_prelude_terrain.vertex.glsl
@@ -24,6 +24,18 @@ vec3 elevationVector(vec2 pos) { return vec3(0, 0, 1); }
#endif
+// Handle skirt flag for terrain & globe shaders
+
+const float skirtOffset = 24575.0;
+vec3 decomposeToPosAndSkirt(vec2 posWithComposedSkirt)
+{
+ float skirt = float(posWithComposedSkirt.x >= skirtOffset);
+ vec2 pos = posWithComposedSkirt - vec2(skirt * skirtOffset, 0.0);
+
+ return vec3(pos, skirt);
+}
+
+
#ifdef TERRAIN
#ifdef TERRAIN_DEM_FLOAT_FORMAT
diff --git a/src/shaders/globe_raster.vertex.glsl b/src/shaders/globe_raster.vertex.glsl
index 73510b666b6..e34e6ea5a2f 100644
--- a/src/shaders/globe_raster.vertex.glsl
+++ b/src/shaders/globe_raster.vertex.glsl
@@ -5,12 +5,13 @@ uniform mat4 u_merc_matrix;
uniform float u_zoom_transition;
uniform vec2 u_merc_center;
uniform mat3 u_grid_matrix;
+uniform float u_skirt_height;
#ifdef GLOBE_POLES
attribute vec3 a_globe_pos;
attribute vec2 a_uv;
#else
-attribute vec2 a_pos;
+attribute vec2 a_pos; // .xy - grid coords, .z - 1 - skirt, 0 - grid
#endif
varying vec2 v_pos0;
@@ -52,7 +53,9 @@ void main() {
float idx = u_grid_matrix[1][2];
float idy = u_grid_matrix[2][2];
- vec3 latLng = u_grid_matrix * vec3(a_pos, 1.0);
+ vec3 decomposed_pos_and_skirt = decomposeToPosAndSkirt(a_pos);
+
+ vec3 latLng = u_grid_matrix * vec3(decomposed_pos_and_skirt.xy, 1.0);
float mercatorY = mercatorYfromLat(latLng[0]);
float uvY = mercatorY * tiles - idy;
@@ -68,13 +71,16 @@ void main() {
v_pos0 = uv;
vec2 tile_pos = uv * EXTENT;
+ // Used for poles and skirts
+ vec3 globe_derived_up_vector = normalize(globe_pos) * u_tile_up_scale;
#ifdef GLOBE_POLES
// Normal vector can be derived from the ecef position
// as "elevationVector" can't be queried outside of the tile
- vec3 up_vector = normalize(globe_pos) * u_tile_up_scale;
+ vec3 up_vector = globe_derived_up_vector;
#else
vec3 up_vector = elevationVector(tile_pos);
#endif
+
float height = elevation(tile_pos);
#ifdef TERRAIN_WIREFRAME
@@ -83,13 +89,18 @@ void main() {
globe_pos += up_vector * height;
+#ifndef GLOBE_POLES
+ // Apply skirts for grid and only by offsetting via globe_pos derived normal
+ globe_pos -= globe_derived_up_vector * u_skirt_height * decomposed_pos_and_skirt.z;
+#endif
+
#ifdef GLOBE_POLES
vec4 interpolated_pos = u_globe_matrix * vec4(globe_pos, 1.0);
#else
vec4 globe_world_pos = u_globe_matrix * vec4(globe_pos, 1.0);
vec4 merc_world_pos = vec4(0.0);
if (u_zoom_transition > 0.0) {
- merc_world_pos = vec4(merc_pos, height, 1.0);
+ merc_world_pos = vec4(merc_pos, height - u_skirt_height * decomposed_pos_and_skirt.z, 1.0);
merc_world_pos.xy -= u_merc_center;
merc_world_pos.x = wrap(merc_world_pos.x, -0.5, 0.5);
merc_world_pos = u_merc_matrix * merc_world_pos;
diff --git a/src/shaders/terrain_raster.vertex.glsl b/src/shaders/terrain_raster.vertex.glsl
index 76577a8ff1d..4ba414ef877 100644
--- a/src/shaders/terrain_raster.vertex.glsl
+++ b/src/shaders/terrain_raster.vertex.glsl
@@ -17,12 +17,12 @@ varying vec4 v_pos_light_view_1;
varying float v_depth;
#endif
-const float skirtOffset = 24575.0;
const float wireframeOffset = 0.00015;
void main() {
- float skirt = float(a_pos.x >= skirtOffset);
- vec2 decodedPos = a_pos - vec2(skirt * skirtOffset, 0.0);
+ vec3 decomposedPosAndSkirt = decomposeToPosAndSkirt(a_pos);
+ float skirt = decomposedPosAndSkirt.z;
+ vec2 decodedPos = decomposedPosAndSkirt.xy;
float elevation = elevation(decodedPos) - skirt * u_skirt_height;
#ifdef TERRAIN_WIREFRAME
elevation += wireframeOffset;
diff --git a/src/terrain/draw_terrain_raster.js b/src/terrain/draw_terrain_raster.js
index 42bba5e0230..a783476d5be 100644
--- a/src/terrain/draw_terrain_raster.js
+++ b/src/terrain/draw_terrain_raster.js
@@ -171,6 +171,9 @@ function drawTerrainForGlobe(painter: Painter, terrain: Terrain, sourceCache: So
const elevationOptions = {useDenormalizedUpVectorScale: true};
batches.forEach(isWireframe => {
+ const tr = painter.transform;
+ const skirtHeightValue = skirtHeight(tr.zoom) * terrain.exaggeration();
+
// This code assumes the rendering is batched into mesh terrain and then wireframe
// terrain (if applicable) so that this is enough to ensure the correct program is
// set when we switch from one to the other.
@@ -207,7 +210,7 @@ function drawTerrainForGlobe(painter: Painter, terrain: Terrain, sourceCache: So
const uniformValues = globeRasterUniformValues(
tr.projMatrix, globeMatrix, globeMercatorMatrix, normalizeMatrix, globeToMercatorTransition(tr.zoom),
mercatorCenter, tr.frustumCorners.TL, tr.frustumCorners.TR, tr.frustumCorners.BR,
- tr.frustumCorners.BL, tr.globeCenterInViewSpace, tr.globeRadius, viewport, gridMatrix);
+ tr.frustumCorners.BL, tr.globeCenterInViewSpace, tr.globeRadius, viewport, skirtHeightValue, gridMatrix);
setShaderMode(shaderMode, isWireframe);
@@ -218,7 +221,7 @@ function drawTerrainForGlobe(painter: Painter, terrain: Terrain, sourceCache: So
if (sharedBuffers) {
const [buffer, indexBuffer, segments] = isWireframe ?
sharedBuffers.getWirefameBuffers(painter.context, latitudinalLod) :
- sharedBuffers.getGridBuffers(latitudinalLod);
+ sharedBuffers.getGridBuffers(latitudinalLod, skirtHeightValue !== 0);
program.draw(context, primitive, depthMode, stencilMode, colorMode, CullFaceMode.backCCW,
uniformValues, "globe_raster", buffer, indexBuffer, segments);
@@ -254,7 +257,7 @@ function drawTerrainForGlobe(painter: Painter, terrain: Terrain, sourceCache: So
context, gl.TRIANGLES, depthMode, StencilMode.disabled, colorMode, CullFaceMode.disabled,
globeRasterUniformValues(tr.projMatrix, poleMatrix, poleMatrix, normalizeMatrix, 0.0, mercatorCenter,
tr.frustumCorners.TL, tr.frustumCorners.TR, tr.frustumCorners.BR, tr.frustumCorners.BL,
- tr.globeCenterInViewSpace, tr.globeRadius, viewport), "globe_pole_raster", vertexBuffer,
+ tr.globeCenterInViewSpace, tr.globeRadius, viewport, 0), "globe_pole_raster", vertexBuffer,
indexBuffer, segment);
terrain.setupElevationDraw(tile, program, elevationOptions);
diff --git a/src/terrain/globe_raster_program.js b/src/terrain/globe_raster_program.js
index 220570f6b43..a3e1c7993d1 100644
--- a/src/terrain/globe_raster_program.js
+++ b/src/terrain/globe_raster_program.js
@@ -24,6 +24,7 @@ export type GlobeRasterUniformsType = {|
'u_merc_center': Uniform2f,
'u_image0': Uniform1i,
'u_grid_matrix': UniformMatrix3f,
+ 'u_skirt_height': Uniform1f,
'u_frustum_tl': Uniform3f,
'u_frustum_tr': Uniform3f,
'u_frustum_br': Uniform3f,
@@ -61,6 +62,7 @@ const globeRasterUniforms = (context: Context): GlobeRasterUniformsType => ({
'u_merc_center': new Uniform2f(context),
'u_image0': new Uniform1i(context),
'u_grid_matrix': new UniformMatrix3f(context),
+ 'u_skirt_height': new Uniform1f(context),
'u_frustum_tl': new Uniform3f(context),
'u_frustum_tr': new Uniform3f(context),
'u_frustum_br': new Uniform3f(context),
@@ -103,6 +105,7 @@ const globeRasterUniformValues = (
globePosition: [number, number, number],
globeRadius: number,
viewport: [number, number],
+ skirtHeight: number,
gridMatrix: ?Mat4
): UniformValues => ({
'u_proj_matrix': Float32Array.from(projMatrix),
@@ -119,7 +122,8 @@ const globeRasterUniformValues = (
'u_globe_pos': globePosition,
'u_globe_radius': globeRadius,
'u_viewport': viewport,
- 'u_grid_matrix': gridMatrix ? Float32Array.from(gridMatrix) : new Float32Array(9)
+ 'u_grid_matrix': gridMatrix ? Float32Array.from(gridMatrix) : new Float32Array(9),
+ 'u_skirt_height': skirtHeight
});
const atmosphereUniformValues = (
diff --git a/test/integration/render-tests/globe/globe-antialiasing/high-exaggeration/expected.png b/test/integration/render-tests/globe/globe-antialiasing/high-exaggeration/expected.png
index 8ecd03ea412..8982dc535f5 100644
Binary files a/test/integration/render-tests/globe/globe-antialiasing/high-exaggeration/expected.png and b/test/integration/render-tests/globe/globe-antialiasing/high-exaggeration/expected.png differ
diff --git a/test/integration/render-tests/globe/globe-antialiasing/high-exaggeration/style.json b/test/integration/render-tests/globe/globe-antialiasing/high-exaggeration/style.json
index cf6b9dbe22b..480837100a1 100644
--- a/test/integration/render-tests/globe/globe-antialiasing/high-exaggeration/style.json
+++ b/test/integration/render-tests/globe/globe-antialiasing/high-exaggeration/style.json
@@ -5,7 +5,7 @@
"standardDerivatives": true,
"width": 512,
"height": 512,
- "allowed": 0.001,
+ "allowed": 0.002,
"operations": [
["setProjection", "globe"],
["wait"]
diff --git a/test/integration/render-tests/terrain/globe-high-exaggeration/expected.png b/test/integration/render-tests/terrain/globe-high-exaggeration/expected.png
new file mode 100644
index 00000000000..573876726c1
Binary files /dev/null and b/test/integration/render-tests/terrain/globe-high-exaggeration/expected.png differ
diff --git a/test/integration/render-tests/terrain/globe-high-exaggeration/style.json b/test/integration/render-tests/terrain/globe-high-exaggeration/style.json
new file mode 100644
index 00000000000..0032ca79d0c
--- /dev/null
+++ b/test/integration/render-tests/terrain/globe-high-exaggeration/style.json
@@ -0,0 +1,52 @@
+{
+ "version": 8,
+ "metadata": {
+ "test": {
+ "width": 1000,
+ "height": 512,
+ "allowed": 0.0004,
+ "operations": [
+ ["setProjection", "globe"],
+ ["wait", 500]
+ ]
+ }
+ },
+ "zoom": 5.13,
+ "terrain": {
+ "source": "rgbterrain",
+ "exaggeration": 100.0
+ },
+ "pitch": 45,
+ "bearing": -48,
+ "center": [99.13, 47.46],
+ "sources": {
+ "satellite": {
+ "type": "raster",
+ "tiles": [
+ "local://tiles/{z}-{x}-{y}.satellite.png"
+ ],
+ "tileSize": 256
+ },
+ "rgbterrain": {
+ "type": "raster-dem",
+ "tiles": [
+ "local://tiles/{z}-{x}-{y}.terrain.512.png"
+ ],
+ "maxzoom": 12,
+ "tileSize": 512
+ }
+ },
+ "fog": {
+ "star-intensity": 0
+ },
+ "layers": [
+ {
+ "id": "satellite",
+ "type": "raster",
+ "source": "satellite",
+ "paint": {
+ "raster-fade-duration": 0
+ }
+ }
+ ]
+}
\ No newline at end of file