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