UNPKG

maplibre-gl

Version:

BSD licensed community fork of mapbox-gl, a WebGL interactive maps library

185 lines (151 loc) 8.79 kB
import {clamp} from '../util/util'; import {ImageSource} from '../source/image_source'; import {browser} from '../util/browser'; import {StencilMode} from '../gl/stencil_mode'; import {DepthMode} from '../gl/depth_mode'; import {CullFaceMode} from '../gl/cull_face_mode'; import {rasterUniformValues} from './program/raster_program'; import {EXTENT} from '../data/extent'; import {coveringZoomLevel} from '../geo/projection/covering_tiles'; import Point from '@mapbox/point-geometry'; import type {Painter, RenderOptions} from './painter'; import type {SourceCache} from '../source/source_cache'; import type {RasterStyleLayer} from '../style/style_layer/raster_style_layer'; import type {OverscaledTileID} from '../source/tile_id'; import type {IReadonlyTransform} from '../geo/transform_interface'; import type {Tile} from '../source/tile'; import type {Terrain} from './terrain'; const cornerCoords = [ new Point(0, 0), new Point(EXTENT, 0), new Point(EXTENT, EXTENT), new Point(0, EXTENT), ]; export function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterStyleLayer, tileIDs: Array<OverscaledTileID>, renderOptions: RenderOptions) { if (painter.renderPass !== 'translucent') return; if (layer.paint.get('raster-opacity') === 0) return; if (!tileIDs.length) return; const {isRenderingToTexture} = renderOptions; const source = sourceCache.getSource(); const projection = painter.style.projection; const useSubdivision = projection.useSubdivision; // When rendering globe (or any other subdivided projection), two passes are needed. // Subdivided tiles with different granularities might have tiny gaps between them. // To combat this, tile meshes for globe have a slight border region. // However tiles borders will overlap, and a part of a tile often // gets hidden by its neighbour's border, which displays an ugly stretched texture. // To both hide the border stretch and avoid tiny gaps, tiles are first drawn without borders (with gaps), // and then any missing pixels (gaps, not marked in stencil) get overdrawn with tile borders. // This approach also avoids pixel shader overdraw, as any pixel is drawn at most once. // Stencil mask and two-pass is not used for ImageSource sources regardless of projection. if (source instanceof ImageSource) { // Image source - no stencil is used drawTiles(painter, sourceCache, layer, tileIDs, null, false, false, source.tileCoords, source.flippedWindingOrder, isRenderingToTexture); } else if (useSubdivision) { // Two-pass rendering const [stencilBorderless, stencilBorders, coords] = painter.stencilConfigForOverlapTwoPass(tileIDs); drawTiles(painter, sourceCache, layer, coords, stencilBorderless, false, true, cornerCoords, false, isRenderingToTexture); // draw without borders drawTiles(painter, sourceCache, layer, coords, stencilBorders, true, true, cornerCoords, false, isRenderingToTexture); // draw with borders } else { // Simple rendering const [stencil, coords] = painter.getStencilConfigForOverlapAndUpdateStencilID(tileIDs); drawTiles(painter, sourceCache, layer, coords, stencil, false, true, cornerCoords, false, isRenderingToTexture); } } function drawTiles( painter: Painter, sourceCache: SourceCache, layer: RasterStyleLayer, coords: Array<OverscaledTileID>, stencilModes: {[_: number]: Readonly<StencilMode>} | null, useBorder: boolean, allowPoles: boolean, corners: Array<Point>, flipCullfaceMode: boolean = false, isRenderingToTexture: boolean = false) { const minTileZ = coords[coords.length - 1].overscaledZ; const context = painter.context; const gl = context.gl; const program = painter.useProgram('raster'); const transform = painter.transform; const projection = painter.style.projection; const colorMode = painter.colorModeForRenderPass(); const align = !painter.options.moving; // Draw all tiles for (const coord of coords) { // Set the lower zoom level to sublayer 0, and higher zoom levels to higher sublayers // Use gl.LESS to prevent double drawing in areas where tiles overlap. const depthMode = painter.getDepthModeForSublayer(coord.overscaledZ - minTileZ, layer.paint.get('raster-opacity') === 1 ? DepthMode.ReadWrite : DepthMode.ReadOnly, gl.LESS); const tile = sourceCache.getTile(coord); tile.registerFadeDuration(layer.paint.get('raster-fade-duration')); const parentTile = sourceCache.findLoadedParent(coord, 0); const siblingTile = sourceCache.findLoadedSibling(coord); // Prefer parent tile if present const fadeTileReference = parentTile || siblingTile || null; const fade = getFadeValues(tile, fadeTileReference, sourceCache, layer, painter.transform, painter.style.map.terrain); let parentScaleBy, parentTL; const textureFilter = layer.paint.get('raster-resampling') === 'nearest' ? gl.NEAREST : gl.LINEAR; context.activeTexture.set(gl.TEXTURE0); tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); context.activeTexture.set(gl.TEXTURE1); if (parentTile) { parentTile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); parentScaleBy = Math.pow(2, parentTile.tileID.overscaledZ - tile.tileID.overscaledZ); parentTL = [tile.tileID.canonical.x * parentScaleBy % 1, tile.tileID.canonical.y * parentScaleBy % 1]; } else { tile.texture.bind(textureFilter, gl.CLAMP_TO_EDGE, gl.LINEAR_MIPMAP_NEAREST); } // Enable anisotropic filtering only when the pitch is greater than 20 degrees // to preserve image sharpness on flat or slightly tilted maps. if (tile.texture.useMipmap && context.extTextureFilterAnisotropic && painter.transform.pitch > 20) { gl.texParameterf(gl.TEXTURE_2D, context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT, context.extTextureFilterAnisotropicMax); } const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord); const projectionData = transform.getProjectionData({overscaledTileID: coord, aligned: align, applyGlobeMatrix: !isRenderingToTexture, applyTerrainMatrix: true}); const uniformValues = rasterUniformValues(parentTL || [0, 0], parentScaleBy || 1, fade, layer, corners); const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, allowPoles, 'raster'); const stencilMode = stencilModes ? stencilModes[coord.overscaledZ] : StencilMode.disabled; program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, flipCullfaceMode ? CullFaceMode.frontCCW : CullFaceMode.backCCW, uniformValues, terrainData, projectionData, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments); } } function getFadeValues(tile: Tile, parentTile: Tile, sourceCache: SourceCache, layer: RasterStyleLayer, transform: IReadonlyTransform, terrain: Terrain) { const fadeDuration = layer.paint.get('raster-fade-duration'); if (!terrain && fadeDuration > 0) { const now = browser.now(); const sinceTile = (now - tile.timeAdded) / fadeDuration; const sinceParent = parentTile ? (now - parentTile.timeAdded) / fadeDuration : -1; const source = sourceCache.getSource(); const idealZ = coveringZoomLevel(transform, { tileSize: source.tileSize, roundZoom: source.roundZoom }); // if no parent or parent is older, fade in; if parent is younger, fade out const fadeIn = !parentTile || Math.abs(parentTile.tileID.overscaledZ - idealZ) > Math.abs(tile.tileID.overscaledZ - idealZ); const childOpacity = (fadeIn && tile.refreshedUponExpiration) ? 1 : clamp(fadeIn ? sinceTile : 1 - sinceParent, 0, 1); // we don't crossfade tiles that were just refreshed upon expiring: // once they're old enough to pass the crossfading threshold // (fadeDuration), unset the `refreshedUponExpiration` flag so we don't // incorrectly fail to crossfade them when zooming if (tile.refreshedUponExpiration && sinceTile >= 1) tile.refreshedUponExpiration = false; if (parentTile) { return { opacity: 1, mix: 1 - childOpacity }; } else { return { opacity: childOpacity, mix: 0 }; } } else { return { opacity: 1, mix: 0 }; } }