File: //home/cafsindia/snap.cafsinfotech.in/node_modules/mapbox-gl/src/terrain/terrain.js
// @flow
import Point from '@mapbox/point-geometry';
import SourceCache from '../source/source_cache.js';
import {OverscaledTileID} from '../source/tile_id.js';
import Tile from '../source/tile.js';
import posAttributes from '../data/pos_attributes.js';
import {TriangleIndexArray, LineIndexArray, PosArray} from '../data/array_types.js';
import SegmentVector from '../data/segment.js';
import Texture from '../render/texture.js';
import Program from '../render/program.js';
import {Uniform1i, Uniform1f, Uniform2f, Uniform3f, Uniform4f, UniformMatrix4f} from '../render/uniform_binding.js';
import {prepareDEMTexture} from '../render/draw_hillshade.js';
import EXTENT from '../data/extent.js';
import {clamp, warnOnce} from '../util/util.js';
import assert from 'assert';
import {vec3, mat4, vec4} from 'gl-matrix';
import getWorkerPool from '../util/global_worker_pool.js';
import Dispatcher from '../util/dispatcher.js';
import GeoJSONSource from '../source/geojson_source.js';
import ImageSource from '../source/image_source.js';
import RasterDEMTileSource from '../source/raster_dem_tile_source.js';
import RasterTileSource from '../source/raster_tile_source.js';
import VectorTileSource from '../source/vector_tile_source.js';
import Color from '../style-spec/util/color.js';
import type {Callback} from '../types/callback.js';
import StencilMode from '../gl/stencil_mode.js';
import {DepthStencilAttachment} from '../gl/value.js';
import {drawTerrainRaster, drawTerrainDepth} from './draw_terrain_raster.js';
import type RasterStyleLayer from '../style/style_layer/raster_style_layer.js';
import type CustomStyleLayer from '../style/style_layer/custom_style_layer.js';
import type LineStyleLayer from '../style/style_layer/line_style_layer.js';
import {Elevation} from './elevation.js';
import Framebuffer from '../gl/framebuffer.js';
import ColorMode from '../gl/color_mode.js';
import DepthMode from '../gl/depth_mode.js';
import CullFaceMode from '../gl/cull_face_mode.js';
import {clippingMaskUniformValues} from '../render/program/clipping_mask_program.js';
import MercatorCoordinate, {mercatorZfromAltitude} from '../geo/mercator_coordinate.js';
import browser from '../util/browser.js';
import DEMData from '../data/dem_data.js';
import {DrapeRenderMode} from '../style/terrain.js';
import rasterFade from '../render/raster_fade.js';
import {create as createSource} from '../source/source.js';
import {RGBAImage} from '../util/image.js';
import {globeMetersToEcef} from '../geo/projection/globe_util.js';
import {ZoomDependentExpression} from '../style-spec/expression/index.js';
import type Map from '../ui/map.js';
import type Painter from '../render/painter.js';
import type Style from '../style/style.js';
import type StyleLayer from '../style/style_layer.js';
import type VertexBuffer from '../gl/vertex_buffer.js';
import type IndexBuffer from '../gl/index_buffer.js';
import type Context from '../gl/context.js';
import type {UniformValues} from '../render/uniform_binding.js';
import type Transform from '../geo/transform.js';
import type {DEMEncoding} from '../data/dem_data.js';
import type {Vec3, Vec4} from 'gl-matrix';
import type {CanonicalTileID} from '../source/tile_id.js';
const GRID_DIM = 128;
const FBO_POOL_SIZE = 5;
const RENDER_CACHE_MAX_SIZE = 50;
type RenderBatch = {
start: number;
end: number;
}
class MockSourceCache extends SourceCache {
constructor(map: Map) {
const sourceSpec = {type: 'raster-dem', maxzoom: map.transform.maxZoom};
const sourceDispatcher = new Dispatcher(getWorkerPool(), null);
const source = createSource('mock-dem', sourceSpec, sourceDispatcher, map.style);
super('mock-dem', source, false);
source.setEventedParent(this);
this._sourceLoaded = true;
}
_loadTile(tile: Tile, callback: Callback<void>) {
tile.state = 'loaded';
callback(null);
}
}
/**
* Proxy source cache gets ideal screen tile cover coordinates. All the other
* source caches's coordinates get mapped to subrects of proxy coordinates (or
* vice versa, subrects of larger tiles from all source caches get mapped to
* full proxy tile). This happens on every draw call in Terrain.updateTileBinding.
* Approach is used here for terrain : all the visible source tiles of all the
* source caches get rendered to proxy source cache textures and then draped over
* terrain. It is in future reusable for handling overscalling as buckets could be
* constructed only for proxy tile content, not for full overscalled vector tile.
*/
class ProxySourceCache extends SourceCache {
renderCache: Array<FBO>;
renderCachePool: Array<number>;
proxyCachedFBO: {[string | number]: {[string | number]: number}};
constructor(map: Map) {
const source = createSource('proxy', {
type: 'geojson',
maxzoom: map.transform.maxZoom
}, new Dispatcher(getWorkerPool(), null), map.style);
super('proxy', source, false);
source.setEventedParent(this);
// This source is not to be added as a map source: we use it's tile management.
// For that, initialize internal structures used for tile cover update.
this.map = ((this.getSource(): any): GeoJSONSource).map = map;
this.used = this._sourceLoaded = true;
this.renderCache = [];
this.renderCachePool = [];
this.proxyCachedFBO = {};
}
// Override for transient nature of cover here: don't cache and retain.
update(transform: Transform, tileSize?: number, updateForTerrain?: boolean) { // eslint-disable-line no-unused-vars
if (transform.freezeTileCoverage) { return; }
this.transform = transform;
const idealTileIDs = transform.coveringTiles({
tileSize: this._source.tileSize,
minzoom: this._source.minzoom,
maxzoom: this._source.maxzoom,
roundZoom: this._source.roundZoom,
reparseOverscaled: this._source.reparseOverscaled
});
const incoming: {[string]: string} = idealTileIDs.reduce((acc, tileID) => {
acc[tileID.key] = '';
if (!this._tiles[tileID.key]) {
const tile = new Tile(tileID, this._source.tileSize * tileID.overscaleFactor(), transform.tileZoom);
tile.state = 'loaded';
this._tiles[tileID.key] = tile;
}
return acc;
}, {});
for (const id in this._tiles) {
if (!(id in incoming)) {
this.freeFBO(id);
this._tiles[id].unloadVectorData();
delete this._tiles[id];
}
}
}
freeFBO(id: string) {
const fbos = this.proxyCachedFBO[id];
if (fbos !== undefined) {
const fboIds = ((Object.values(fbos): any): Array<number>);
this.renderCachePool.push(...fboIds);
delete this.proxyCachedFBO[id];
}
}
deallocRenderCache() {
this.renderCache.forEach(fbo => fbo.fb.destroy());
this.renderCache = [];
this.renderCachePool = [];
this.proxyCachedFBO = {};
}
}
/**
* Canonical, wrap and overscaledZ contain information of original source cache tile.
* This tile gets ortho-rendered to proxy tile (defined by proxyTileKey).
* `posMatrix` holds orthographic, scaling and translation information that is used
* for rendering original tile content to a proxy tile. Proxy tile covers whole
* or sub-rectangle of the original tile.
*/
class ProxiedTileID extends OverscaledTileID {
proxyTileKey: number;
constructor(tileID: OverscaledTileID, proxyTileKey: number, projMatrix: Float32Array) {
super(tileID.overscaledZ, tileID.wrap, tileID.canonical.z, tileID.canonical.x, tileID.canonical.y);
this.proxyTileKey = proxyTileKey;
this.projMatrix = projMatrix;
}
}
type OverlapStencilType = false | 'Clip' | 'Mask';
type FBO = {fb: Framebuffer, tex: Texture, dirty: boolean};
export class Terrain extends Elevation {
terrainTileForTile: {[number | string]: Tile};
prevTerrainTileForTile: {[number | string]: Tile};
painter: Painter;
sourceCache: SourceCache;
gridBuffer: VertexBuffer;
gridIndexBuffer: IndexBuffer;
gridSegments: SegmentVector;
gridNoSkirtSegments: SegmentVector;
wireframeSegments: SegmentVector;
wireframeIndexBuffer: IndexBuffer;
proxiedCoords: {[string]: Array<ProxiedTileID>};
proxyCoords: Array<OverscaledTileID>;
proxyToSource: {[number]: {[string]: Array<ProxiedTileID>}};
proxySourceCache: ProxySourceCache;
renderingToTexture: boolean;
_style: Style;
_mockSourceCache: MockSourceCache;
orthoMatrix: Float32Array;
enabled: boolean;
renderMode: number;
_visibleDemTiles: Array<Tile>;
_sourceTilesOverlap: {[string]: boolean};
_overlapStencilMode: StencilMode;
_overlapStencilType: OverlapStencilType;
_stencilRef: number;
_exaggeration: number;
_depthFBO: ?Framebuffer;
_depthTexture: ?Texture;
_previousZoom: number;
_updateTimestamp: number;
_useVertexMorphing: boolean;
pool: Array<FBO>;
renderedToTile: boolean;
_drapedRenderBatches: Array<RenderBatch>;
_sharedDepthStencil: ?WebGLRenderbuffer;
_findCoveringTileCache: {[string]: {[number]: ?number}};
_tilesDirty: {[string]: {[number]: boolean}};
_invalidateRenderCache: boolean;
_emptyDepthBufferTexture: ?Texture;
_emptyDEMTexture: ?Texture;
_initializing: ?boolean;
_emptyDEMTextureDirty: ?boolean;
constructor(painter: Painter, style: Style) {
super();
this.painter = painter;
this.terrainTileForTile = {};
this.prevTerrainTileForTile = {};
// Terrain rendering grid is 129x129 cell grid, made by 130x130 points.
// 130 vertices map to 128 DEM data + 1px padding on both sides.
// DEM texture is padded (1, 1, 1, 1) and padding pixels are backfilled
// by neighboring tile edges. This way we achieve tile stitching as
// edge vertices from neighboring tiles evaluate to the same 3D point.
const [triangleGridArray, triangleGridIndices, skirtIndicesOffset] = createGrid(GRID_DIM + 1);
const context = painter.context;
this.gridBuffer = context.createVertexBuffer(triangleGridArray, posAttributes.members);
this.gridIndexBuffer = context.createIndexBuffer(triangleGridIndices);
this.gridSegments = SegmentVector.simpleSegment(0, 0, triangleGridArray.length, triangleGridIndices.length);
this.gridNoSkirtSegments = SegmentVector.simpleSegment(0, 0, triangleGridArray.length, skirtIndicesOffset);
this.proxyCoords = [];
this.proxiedCoords = {};
this._visibleDemTiles = [];
this._drapedRenderBatches = [];
this._sourceTilesOverlap = {};
this.proxySourceCache = new ProxySourceCache(style.map);
this.orthoMatrix = mat4.create();
const epsilon = this.painter.transform.projection.name === 'globe' ? .015 : 0; // Experimentally the smallest value to avoid rendering artifacts (https://github.com/mapbox/mapbox-gl-js/issues/11975)
mat4.ortho(this.orthoMatrix, epsilon, EXTENT, 0, EXTENT, 0, 1);
const gl = context.gl;
this._overlapStencilMode = new StencilMode({func: gl.GEQUAL, mask: 0xFF}, 0, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE);
this._previousZoom = painter.transform.zoom;
this.pool = [];
this._findCoveringTileCache = {};
this._tilesDirty = {};
this.style = style;
this._useVertexMorphing = true;
this._exaggeration = 1;
this._mockSourceCache = new MockSourceCache(style.map);
}
set style(style: Style) {
// $FlowFixMe[method-unbinding]
style.on('data', this._onStyleDataEvent.bind(this));
// $FlowFixMe[method-unbinding]
style.on('neworder', this._checkRenderCacheEfficiency.bind(this));
this._style = style;
this._checkRenderCacheEfficiency();
this._style.map.on('moveend', () => {
this._clearLineLayersFromRenderCache();
});
}
/*
* Validate terrain and update source cache used for elevation.
* Explicitly pass transform to update elevation (Transform.updateElevation)
* before using transform for source cache update.
*/
update(style: Style, transform: Transform, adaptCameraAltitude: boolean) {
if (style && style.terrain) {
if (this._style !== style) {
this.style = style;
}
this.enabled = true;
const terrainProps = style.terrain.properties;
const isDrapeModeDeferred = style.terrain.drapeRenderMode === DrapeRenderMode.deferred;
this.sourceCache = isDrapeModeDeferred ? this._mockSourceCache :
((style._getSourceCache(terrainProps.get('source')): any): SourceCache);
this._exaggeration = terrainProps.get('exaggeration');
const updateSourceCache = () => {
if (this.sourceCache.used) {
warnOnce(`Raster DEM source '${this.sourceCache.id}' is used both for terrain and as layer source.\n` +
'This leads to lower resolution of hillshade. For full hillshade resolution but higher memory consumption, define another raster DEM source.');
}
// Lower tile zoom is sufficient for terrain, given the size of terrain grid.
const scaledDemTileSize = this.getScaledDemTileSize();
// Dem tile needs to be parent or at least of the same zoom level as proxy tile.
// Tile cover roundZoom behavior is set to the same as for proxy (false) in SourceCache.update().
this.sourceCache.update(transform, scaledDemTileSize, true);
// As a result of update, we get new set of tiles: reset lookup cache.
this.resetTileLookupCache(this.sourceCache.id);
};
if (!this.sourceCache.usedForTerrain) {
// Init cache entry.
this.resetTileLookupCache(this.sourceCache.id);
// When toggling terrain on/off load available terrain tiles from cache
// before reading elevation at center.
this.sourceCache.usedForTerrain = true;
updateSourceCache();
this._initializing = true;
}
updateSourceCache();
// Camera gets constrained over terrain. Issue constrainCameraOverTerrain = true
// here to cover potential under terrain situation on data, style, or other camera changes.
transform.updateElevation(true, adaptCameraAltitude);
// Reset tile lookup cache and update draped tiles coordinates.
this.resetTileLookupCache(this.proxySourceCache.id);
this.proxySourceCache.update(transform);
this._emptyDEMTextureDirty = true;
} else {
this._disable();
}
}
resetTileLookupCache(sourceCacheID: string) {
this._findCoveringTileCache[sourceCacheID] = {};
}
getScaledDemTileSize(): number {
const demScale = this.sourceCache.getSource().tileSize / GRID_DIM;
const proxyTileSize = this.proxySourceCache.getSource().tileSize;
return demScale * proxyTileSize;
}
_checkRenderCacheEfficiency() {
const renderCacheInfo = this.renderCacheEfficiency(this._style);
if (this._style.map._optimizeForTerrain) {
assert(renderCacheInfo.efficiency === 100);
} else if (renderCacheInfo.efficiency !== 100) {
warnOnce(`Terrain render cache efficiency is not optimal (${renderCacheInfo.efficiency}%) and performance
may be affected negatively, consider placing all background, fill and line layers before layer
with id '${renderCacheInfo.firstUndrapedLayer}' or create a map using optimizeForTerrain: true option.`);
}
}
_onStyleDataEvent(event: any) {
if (event.coord && event.dataType === 'source') {
this._clearRenderCacheForTile(event.sourceCacheId, event.coord);
} else if (event.dataType === 'style') {
this._invalidateRenderCache = true;
}
}
// Terrain
_disable() {
if (!this.enabled) return;
this.enabled = false;
this._sharedDepthStencil = undefined;
this.proxySourceCache.deallocRenderCache();
if (this._style) {
for (const id in this._style._sourceCaches) {
this._style._sourceCaches[id].usedForTerrain = false;
}
}
}
destroy() {
this._disable();
if (this._emptyDEMTexture) this._emptyDEMTexture.destroy();
if (this._emptyDepthBufferTexture) this._emptyDepthBufferTexture.destroy();
this.pool.forEach(fbo => fbo.fb.destroy());
this.pool = [];
if (this._depthFBO) {
this._depthFBO.destroy();
this._depthFBO = undefined;
this._depthTexture = undefined;
}
}
// Implements Elevation::_source.
_source(): ?SourceCache {
return this.enabled ? this.sourceCache : null;
}
isUsingMockSource(): boolean {
return this.sourceCache === this._mockSourceCache;
}
// Implements Elevation::exaggeration.
exaggeration(): number {
return this._exaggeration;
}
get visibleDemTiles(): Array<Tile> {
return this._visibleDemTiles;
}
get drapeBufferSize(): [number, number] {
const extent = this.proxySourceCache.getSource().tileSize * 2; // *2 is to avoid upscaling bitmap on zoom.
return [extent, extent];
}
set useVertexMorphing(enable: boolean) {
this._useVertexMorphing = enable;
}
// For every renderable coordinate in every source cache, assign one proxy
// tile (see _setupProxiedCoordsForOrtho). Mapping of source tile to proxy
// tile is modeled by ProxiedTileID. In general case, source and proxy tile
// are of different zoom: ProxiedTileID.projMatrix models ortho, scale and
// translate from source to proxy. This matrix is used when rendering source
// tile to proxy tile's texture.
// One proxy tile can have multiple source tiles, or pieces of source tiles,
// that get rendered to it.
// For each proxy tile we assign one terrain tile (_assignTerrainTiles). The
// terrain tile provides elevation data when rendering (draping) proxy tile
// texture over terrain grid.
updateTileBinding(sourcesCoords: {[string]: Array<OverscaledTileID>}) {
if (!this.enabled) return;
this.prevTerrainTileForTile = this.terrainTileForTile;
const psc = this.proxySourceCache;
const tr = this.painter.transform;
if (this._initializing) {
// Don't activate terrain until center tile gets loaded.
this._initializing = tr._centerAltitude === 0 && this.getAtPointOrZero(MercatorCoordinate.fromLngLat(tr.center), -1) === -1;
this._emptyDEMTextureDirty = !this._initializing;
}
const coords = this.proxyCoords = psc.getIds().map((id) => {
const tileID = psc.getTileByID(id).tileID;
tileID.projMatrix = tr.calculateProjMatrix(tileID.toUnwrapped());
return tileID;
});
sortByDistanceToCamera(coords, this.painter);
this._previousZoom = tr.zoom;
const previousProxyToSource = this.proxyToSource || {};
this.proxyToSource = {};
coords.forEach((tileID) => {
this.proxyToSource[tileID.key] = {};
});
this.terrainTileForTile = {};
const sourceCaches = this._style._sourceCaches;
for (const id in sourceCaches) {
const sourceCache = sourceCaches[id];
if (!sourceCache.used) continue;
if (sourceCache !== this.sourceCache) this.resetTileLookupCache(sourceCache.id);
this._setupProxiedCoordsForOrtho(sourceCache, sourcesCoords[id], previousProxyToSource);
if (sourceCache.usedForTerrain) continue;
const coordinates = sourcesCoords[id];
if (sourceCache.getSource().reparseOverscaled) {
// Do this for layers that are not rasterized to proxy tile.
this._assignTerrainTiles(coordinates);
}
}
// Background has no source. Using proxy coords with 1-1 ortho (this.proxiedCoords[psc.id])
// when rendering background to proxy tiles.
this.proxiedCoords[psc.id] = coords.map(tileID => new ProxiedTileID(tileID, tileID.key, this.orthoMatrix));
this._assignTerrainTiles(coords);
this._prepareDEMTextures();
this._setupDrapedRenderBatches();
this._initFBOPool();
this._setupRenderCache(previousProxyToSource);
this.renderingToTexture = false;
this._updateTimestamp = browser.now();
// Gather all dem tiles that are assigned to proxy tiles
const visibleKeys = {};
this._visibleDemTiles = [];
for (const id of this.proxyCoords) {
const demTile = this.terrainTileForTile[id.key];
if (!demTile)
continue;
const key = demTile.tileID.key;
if (key in visibleKeys)
continue;
this._visibleDemTiles.push(demTile);
visibleKeys[key] = key;
}
}
_assignTerrainTiles(coords: Array<OverscaledTileID>) {
if (this._initializing) return;
coords.forEach((tileID) => {
if (this.terrainTileForTile[tileID.key]) return;
const demTile = this._findTileCoveringTileID(tileID, this.sourceCache);
if (demTile) this.terrainTileForTile[tileID.key] = demTile;
});
}
_prepareDEMTextures() {
const context = this.painter.context;
const gl = context.gl;
for (const key in this.terrainTileForTile) {
const tile = this.terrainTileForTile[key];
const dem = tile.dem;
if (dem && (!tile.demTexture || tile.needsDEMTextureUpload)) {
context.activeTexture.set(gl.TEXTURE1);
prepareDEMTexture(this.painter, tile, dem);
}
}
}
_prepareDemTileUniforms(proxyTile: Tile, demTile: ?Tile, uniforms: UniformValues<TerrainUniformsType>, uniformSuffix: ?string): boolean {
if (!demTile || demTile.demTexture == null)
return false;
assert(demTile.dem);
const proxyId = proxyTile.tileID.canonical;
const demId = demTile.tileID.canonical;
const demScaleBy = Math.pow(2, demId.z - proxyId.z);
const suffix = uniformSuffix || "";
// $FlowFixMe[prop-missing]
uniforms[`u_dem_tl${suffix}`] = [proxyId.x * demScaleBy % 1, proxyId.y * demScaleBy % 1];
// $FlowFixMe[prop-missing]
uniforms[`u_dem_scale${suffix}`] = demScaleBy;
return true;
}
get emptyDEMTexture(): Texture {
return !this._emptyDEMTextureDirty && this._emptyDEMTexture ?
this._emptyDEMTexture : this._updateEmptyDEMTexture();
}
get emptyDepthBufferTexture(): Texture {
const context = this.painter.context;
const gl = context.gl;
if (!this._emptyDepthBufferTexture) {
const image = new RGBAImage({width: 1, height: 1}, Uint8Array.of(255, 255, 255, 255));
this._emptyDepthBufferTexture = new Texture(context, image, gl.RGBA, {premultiply: false});
}
return this._emptyDepthBufferTexture;
}
_getLoadedAreaMinimum(): number {
let nonzero = 0;
const min = this._visibleDemTiles.reduce((acc, tile) => {
if (!tile.dem) return acc;
const m = tile.dem.tree.minimums[0];
acc += m;
if (m > 0) nonzero++;
return acc;
}, 0);
return nonzero ? min / nonzero : 0;
}
_updateEmptyDEMTexture(): Texture {
const context = this.painter.context;
const gl = context.gl;
context.activeTexture.set(gl.TEXTURE2);
const min = this._getLoadedAreaMinimum();
const image = new RGBAImage(
{width: 1, height: 1},
new Uint8Array(DEMData.pack(min, ((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding))
);
this._emptyDEMTextureDirty = false;
let texture = this._emptyDEMTexture;
if (!texture) {
texture = this._emptyDEMTexture = new Texture(context, image, gl.RGBA, {premultiply: false});
} else {
texture.update(image, {premultiply: false});
}
return texture;
}
// useDepthForOcclusion: Pre-rendered depth to texture (this._depthTexture) is
// used to hide (actually moves all object's vertices out of viewport).
// useMeterToDem: u_meter_to_dem uniform is not used for all terrain programs,
// optimization to avoid unnecessary computation and upload.
setupElevationDraw(tile: Tile, program: Program<*>,
options?: {
useDepthForOcclusion?: boolean,
useMeterToDem?: boolean,
labelPlaneMatrixInv?: ?Float32Array,
morphing?: { srcDemTile: Tile, dstDemTile: Tile, phase: number },
useDenormalizedUpVectorScale?: boolean
}) {
const context = this.painter.context;
const gl = context.gl;
const uniforms = defaultTerrainUniforms(((this.sourceCache.getSource(): any): RasterDEMTileSource).encoding);
uniforms['u_dem_size'] = this.sourceCache.getSource().tileSize;
uniforms['u_exaggeration'] = this.exaggeration();
let demTile = null;
let prevDemTile = null;
let morphingPhase = 1.0;
if (options && options.morphing && this._useVertexMorphing) {
const srcTile = options.morphing.srcDemTile;
const dstTile = options.morphing.dstDemTile;
morphingPhase = options.morphing.phase;
if (srcTile && dstTile) {
if (this._prepareDemTileUniforms(tile, srcTile, uniforms, "_prev"))
prevDemTile = srcTile;
if (this._prepareDemTileUniforms(tile, dstTile, uniforms))
demTile = dstTile;
}
}
if (prevDemTile && demTile) {
// Both DEM textures are expected to be correctly set if geomorphing is enabled
context.activeTexture.set(gl.TEXTURE2);
(demTile.demTexture: any).bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST);
context.activeTexture.set(gl.TEXTURE4);
(prevDemTile.demTexture: any).bind(gl.NEAREST, gl.CLAMP_TO_EDGE, gl.NEAREST);
uniforms["u_dem_lerp"] = morphingPhase;
} else {
demTile = this.terrainTileForTile[tile.tileID.key];
context.activeTexture.set(gl.TEXTURE2);
const demTexture = this._prepareDemTileUniforms(tile, demTile, uniforms) ?
(demTile.demTexture: any) : this.emptyDEMTexture;
demTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE);
}
context.activeTexture.set(gl.TEXTURE3);
if (options && options.useDepthForOcclusion) {
if (this._depthTexture) this._depthTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE);
if (this._depthFBO) uniforms['u_depth_size_inv'] = [1 / this._depthFBO.width, 1 / this._depthFBO.height];
} else {
this.emptyDepthBufferTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE);
uniforms['u_depth_size_inv'] = [1, 1];
}
if (options && options.useMeterToDem && demTile) {
const meterToDEM = (1 << demTile.tileID.canonical.z) * mercatorZfromAltitude(1, this.painter.transform.center.lat) * this.sourceCache.getSource().tileSize;
uniforms['u_meter_to_dem'] = meterToDEM;
}
if (options && options.labelPlaneMatrixInv) {
uniforms['u_label_plane_matrix_inv'] = options.labelPlaneMatrixInv;
}
program.setTerrainUniformValues(context, uniforms);
if (this.painter.transform.projection.name === 'globe') {
const globeUniforms = this.globeUniformValues(this.painter.transform, tile.tileID.canonical, options && options.useDenormalizedUpVectorScale);
program.setGlobeUniformValues(context, globeUniforms);
}
}
globeUniformValues(tr: Transform, id: CanonicalTileID, useDenormalizedUpVectorScale: ?boolean): UniformValues<GlobeUniformsType> {
const projection = tr.projection;
return {
'u_tile_tl_up': (projection.upVector(id, 0, 0): any),
'u_tile_tr_up': (projection.upVector(id, EXTENT, 0): any),
'u_tile_br_up': (projection.upVector(id, EXTENT, EXTENT): any),
'u_tile_bl_up': (projection.upVector(id, 0, EXTENT): any),
'u_tile_up_scale': (useDenormalizedUpVectorScale ? globeMetersToEcef(1) : projection.upVectorScale(id, tr.center.lat, tr.worldSize).metersToTile: any)
};
}
renderToBackBuffer(accumulatedDrapes: Array<OverscaledTileID>) {
const painter = this.painter;
const context = this.painter.context;
if (accumulatedDrapes.length === 0) {
return;
}
context.bindFramebuffer.set(null);
context.viewport.set([0, 0, painter.width, painter.height]);
painter.gpuTimingDeferredRenderStart();
this.renderingToTexture = false;
drawTerrainRaster(painter, this, this.proxySourceCache, accumulatedDrapes, this._updateTimestamp);
this.renderingToTexture = true;
painter.gpuTimingDeferredRenderEnd();
accumulatedDrapes.splice(0, accumulatedDrapes.length);
}
// For each proxy tile, render all layers until the non-draped layer (and
// render the tile to the screen) before advancing to the next proxy tile.
// Returns the last drawn index that is used as a start
// layer for interleaved draped rendering.
// Apart to layer-by-layer rendering used in 2D, here we have proxy-tile-by-proxy-tile
// rendering.
renderBatch(startLayerIndex: number): number {
if (this._drapedRenderBatches.length === 0) {
return startLayerIndex + 1;
}
this.renderingToTexture = true;
const painter = this.painter;
const context = this.painter.context;
const psc = this.proxySourceCache;
const proxies = this.proxiedCoords[psc.id];
// Consume batch of sequential drape layers and move next
const drapedLayerBatch = this._drapedRenderBatches.shift();
assert(drapedLayerBatch.start === startLayerIndex);
const accumulatedDrapes = [];
const layerIds = painter.style.order;
let poolIndex = 0;
for (const proxy of proxies) {
// bind framebuffer and assign texture to the tile (texture used in drawTerrainRaster).
const tile = psc.getTileByID(proxy.proxyTileKey);
const renderCacheIndex = psc.proxyCachedFBO[proxy.key] ? psc.proxyCachedFBO[proxy.key][startLayerIndex] : undefined;
const fbo = renderCacheIndex !== undefined ? psc.renderCache[renderCacheIndex] : this.pool[poolIndex++];
const useRenderCache = renderCacheIndex !== undefined;
tile.texture = fbo.tex;
if (useRenderCache && !fbo.dirty) {
// Use cached render from previous pass, no need to render again.
accumulatedDrapes.push(tile.tileID);
continue;
}
context.bindFramebuffer.set(fbo.fb.framebuffer);
this.renderedToTile = false; // reset flag.
if (fbo.dirty) {
// Clear on start.
context.clear({color: Color.transparent, stencil: 0});
fbo.dirty = false;
}
let currentStencilSource; // There is no need to setup stencil for the same source for consecutive layers.
for (let j = drapedLayerBatch.start; j <= drapedLayerBatch.end; ++j) {
const layer = painter.style._layers[layerIds[j]];
const hidden = layer.isHidden(painter.transform.zoom);
assert(this._style.isLayerDraped(layer) || hidden);
if (hidden) continue;
const sourceCache = painter.style._getLayerSourceCache(layer);
const proxiedCoords = sourceCache ? this.proxyToSource[proxy.key][sourceCache.id] : [proxy];
if (!proxiedCoords) continue; // when tile is not loaded yet for the source cache.
const coords = ((proxiedCoords: any): Array<OverscaledTileID>);
context.viewport.set([0, 0, fbo.fb.width, fbo.fb.height]);
if (currentStencilSource !== (sourceCache ? sourceCache.id : null)) {
this._setupStencil(fbo, proxiedCoords, layer, sourceCache);
currentStencilSource = sourceCache ? sourceCache.id : null;
}
painter.renderLayer(painter, sourceCache, layer, coords);
}
if (this.renderedToTile) {
fbo.dirty = true;
accumulatedDrapes.push(tile.tileID);
} else if (!useRenderCache) {
--poolIndex;
assert(poolIndex >= 0);
}
if (poolIndex === FBO_POOL_SIZE) {
poolIndex = 0;
this.renderToBackBuffer(accumulatedDrapes);
}
}
// Reset states and render last drapes
this.renderToBackBuffer(accumulatedDrapes);
this.renderingToTexture = false;
context.bindFramebuffer.set(null);
context.viewport.set([0, 0, painter.width, painter.height]);
return drapedLayerBatch.end + 1;
}
postRender() {
// Make sure we consumed all the draped terrain batches at this point
assert(this._drapedRenderBatches.length === 0);
}
renderCacheEfficiency(style: Style): Object {
const layerCount = style.order.length;
if (layerCount === 0) {
return {efficiency: 100.0};
}
let uncacheableLayerCount = 0;
let drapedLayerCount = 0;
let reachedUndrapedLayer = false;
let firstUndrapedLayer;
for (let i = 0; i < layerCount; ++i) {
const layer = style._layers[style.order[i]];
if (!this._style.isLayerDraped(layer)) {
if (!reachedUndrapedLayer) {
reachedUndrapedLayer = true;
firstUndrapedLayer = layer.id;
}
} else {
if (reachedUndrapedLayer) {
++uncacheableLayerCount;
}
++drapedLayerCount;
}
}
if (drapedLayerCount === 0) {
return {efficiency: 100.0};
}
return {efficiency: (1.0 - uncacheableLayerCount / drapedLayerCount) * 100.0, firstUndrapedLayer};
}
getMinElevationBelowMSL(): number {
let min = 0.0;
// The maximum DEM error in meters to be conservative (SRTM).
const maxDEMError = 30.0;
this._visibleDemTiles.filter(tile => tile.dem).forEach(tile => {
const minMaxTree = (tile.dem: any).tree;
min = Math.min(min, minMaxTree.minimums[0]);
});
return min === 0.0 ? min : (min - maxDEMError) * this._exaggeration;
}
// Performs raycast against visible DEM tiles on the screen and returns the distance travelled along the ray.
// x & y components of the position are expected to be in normalized mercator coordinates [0, 1] and z in meters.
raycast(pos: Vec3, dir: Vec3, exaggeration: number): ?number {
if (!this._visibleDemTiles)
return null;
// Perform initial raycasts against root nodes of the available dem tiles
// and use this information to sort them from closest to furthest.
const preparedTiles = this._visibleDemTiles.filter(tile => tile.dem).map(tile => {
const id = tile.tileID;
const tiles = 1 << id.overscaledZ;
const {x, y} = id.canonical;
// Compute tile boundaries in mercator coordinates
const minx = x / tiles;
const maxx = (x + 1) / tiles;
const miny = y / tiles;
const maxy = (y + 1) / tiles;
const tree = (tile.dem: any).tree;
return {
minx, miny, maxx, maxy,
t: tree.raycastRoot(minx, miny, maxx, maxy, pos, dir, exaggeration),
tile
};
});
preparedTiles.sort((a, b) => {
const at = a.t !== null ? a.t : Number.MAX_VALUE;
const bt = b.t !== null ? b.t : Number.MAX_VALUE;
return at - bt;
});
for (const obj of preparedTiles) {
if (obj.t == null)
return null;
// Perform more accurate raycast against the dem tree. First intersection is the closest on
// as all tiles are sorted from closest to furthest
const tree = (obj.tile.dem: any).tree;
const t = tree.raycast(obj.minx, obj.miny, obj.maxx, obj.maxy, pos, dir, exaggeration);
if (t != null)
return t;
}
return null;
}
_createFBO(): FBO {
const painter = this.painter;
const context = painter.context;
const gl = context.gl;
const bufferSize = this.drapeBufferSize;
context.activeTexture.set(gl.TEXTURE0);
const tex = new Texture(context, {width: bufferSize[0], height: bufferSize[1], data: null}, gl.RGBA);
tex.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
const fb = context.createFramebuffer(bufferSize[0], bufferSize[1], false);
fb.colorAttachment.set(tex.texture);
fb.depthAttachment = new DepthStencilAttachment(context, fb.framebuffer);
if (this._sharedDepthStencil === undefined) {
this._sharedDepthStencil = context.createRenderbuffer(context.gl.DEPTH_STENCIL, bufferSize[0], bufferSize[1]);
this._stencilRef = 0;
fb.depthAttachment.set(this._sharedDepthStencil);
context.clear({stencil: 0});
} else {
fb.depthAttachment.set(this._sharedDepthStencil);
}
if (context.extTextureFilterAnisotropic && !context.extTextureFilterAnisotropicForceOff) {
gl.texParameterf(gl.TEXTURE_2D,
context.extTextureFilterAnisotropic.TEXTURE_MAX_ANISOTROPY_EXT,
context.extTextureFilterAnisotropicMax);
}
return {fb, tex, dirty: false};
}
_initFBOPool() {
while (this.pool.length < Math.min(FBO_POOL_SIZE, this.proxyCoords.length)) {
this.pool.push(this._createFBO());
}
}
_shouldDisableRenderCache(): boolean {
// Disable render caches on dynamic events due to fading or transitioning.
if (this._style.light && this._style.light.hasTransition()) {
return true;
}
for (const id in this._style._sourceCaches) {
if (this._style._sourceCaches[id].hasTransition()) {
return true;
}
}
const isTransitioning = (id: string) => {
const layer = this._style._layers[id];
const isHidden = layer.isHidden(this.painter.transform.zoom);
if (layer.type === 'custom') {
return !isHidden && ((layer: any): CustomStyleLayer).shouldRedrape();
}
return !isHidden && layer.hasTransition();
};
return this._style.order.some(isTransitioning);
}
_clearLineLayersFromRenderCache() {
let hasVectorSource = false;
for (const source of this._style._getSources()) {
if (source instanceof VectorTileSource) {
hasVectorSource = true;
break;
}
}
if (!hasVectorSource) return;
const clearSourceCaches = {};
for (let i = 0; i < this._style.order.length; ++i) {
const layer = this._style._layers[this._style.order[i]];
const sourceCache = this._style._getLayerSourceCache(layer);
if (!sourceCache || clearSourceCaches[sourceCache.id]) continue;
const isHidden = layer.isHidden(this.painter.transform.zoom);
if (isHidden || layer.type !== 'line') continue;
// Check if layer has a zoom dependent "line-width" expression
const widthExpression = ((layer: any): LineStyleLayer).widthExpression();
if (!(widthExpression instanceof ZoomDependentExpression)) continue;
// Mark sourceCache as cleared
clearSourceCaches[sourceCache.id] = true;
for (const proxy of this.proxyCoords) {
const proxiedCoords = this.proxyToSource[proxy.key][sourceCache.id];
const coords = ((proxiedCoords: any): Array<OverscaledTileID>);
if (!coords) continue;
for (const coord of coords) {
this._clearRenderCacheForTile(sourceCache.id, coord);
}
}
}
}
_clearRasterLayersFromRenderCache() {
let hasRasterSource = false;
for (const id in this._style._sourceCaches) {
if (this._style._sourceCaches[id]._source instanceof RasterTileSource) {
hasRasterSource = true;
break;
}
}
if (!hasRasterSource) return;
const clearSourceCaches = {};
for (let i = 0; i < this._style.order.length; ++i) {
const layer = this._style._layers[this._style.order[i]];
const sourceCache = this._style._getLayerSourceCache(layer);
if (!sourceCache || clearSourceCaches[sourceCache.id]) continue;
const isHidden = layer.isHidden(this.painter.transform.zoom);
if (isHidden || layer.type !== 'raster') continue;
// Check if any raster tile is in a fading state
const fadeDuration = ((layer: any): RasterStyleLayer).paint.get('raster-fade-duration');
for (const proxy of this.proxyCoords) {
const proxiedCoords = this.proxyToSource[proxy.key][sourceCache.id];
const coords = ((proxiedCoords: any): Array<OverscaledTileID>);
if (!coords) continue;
for (const coord of coords) {
const tile = sourceCache.getTile(coord);
const parent = sourceCache.findLoadedParent(coord, 0);
const fade = rasterFade(tile, parent, sourceCache, this.painter.transform, fadeDuration);
const isFading = fade.opacity !== 1 || fade.mix !== 0;
if (isFading) {
this._clearRenderCacheForTile(sourceCache.id, coord);
}
}
}
}
}
_setupDrapedRenderBatches() {
const layerIds = this._style.order;
const layerCount = layerIds.length;
if (layerCount === 0) {
return;
}
const batches: Array<RenderBatch> = [];
let currentLayer = 0;
let layer = this._style._layers[layerIds[currentLayer]];
while (!this._style.isLayerDraped(layer) && layer.isHidden(this.painter.transform.zoom) && ++currentLayer < layerCount) {
layer = this._style._layers[layerIds[currentLayer]];
}
let batchStart: number | void;
for (; currentLayer < layerCount; ++currentLayer) {
const layer = this._style._layers[layerIds[currentLayer]];
if (layer.isHidden(this.painter.transform.zoom)) {
continue;
}
if (!this._style.isLayerDraped(layer)) {
if (batchStart !== undefined) {
batches.push({start: batchStart, end: currentLayer - 1});
batchStart = undefined;
}
continue;
}
if (batchStart === undefined) {
batchStart = currentLayer;
}
}
if (batchStart !== undefined) {
batches.push({start: batchStart, end: currentLayer - 1});
}
if (this._style.map._optimizeForTerrain) {
// Draped first approach should result in a single or no batch
assert(batches.length === 1 || batches.length === 0);
}
this._drapedRenderBatches = batches;
}
_setupRenderCache(previousProxyToSource: {[number]: {[string]: Array<ProxiedTileID>}}) {
const psc = this.proxySourceCache;
if (this._shouldDisableRenderCache() || this._invalidateRenderCache) {
this._invalidateRenderCache = false;
if (psc.renderCache.length > psc.renderCachePool.length) {
const used = ((Object.values(psc.proxyCachedFBO): any): Array<{[string | number]: number}>);
psc.proxyCachedFBO = {};
for (let i = 0; i < used.length; ++i) {
const fbos = ((Object.values(used[i]): any): Array<number>);
psc.renderCachePool.push(...fbos);
}
assert(psc.renderCache.length === psc.renderCachePool.length);
}
return;
}
this._clearRasterLayersFromRenderCache();
const coords = this.proxyCoords;
const dirty = this._tilesDirty;
for (let i = coords.length - 1; i >= 0; i--) {
const proxy = coords[i];
const tile = psc.getTileByID(proxy.key);
if (psc.proxyCachedFBO[proxy.key] !== undefined) {
assert(tile.texture);
const prev = previousProxyToSource[proxy.key];
assert(prev);
// Reuse previous render from cache if there was no change of
// content that was used to render proxy tile.
const current = this.proxyToSource[proxy.key];
let equal = 0;
for (const source in current) {
const tiles = current[source];
const prevTiles = prev[source];
if (!prevTiles || prevTiles.length !== tiles.length ||
tiles.some((t, index) =>
(t !== prevTiles[index] ||
(dirty[source] && dirty[source].hasOwnProperty(t.key)
)))
) {
equal = -1;
break;
}
++equal;
}
// dirty === false: doesn't need to be rendered to, just use cached render.
for (const proxyFBO in psc.proxyCachedFBO[proxy.key]) {
psc.renderCache[psc.proxyCachedFBO[proxy.key][proxyFBO]].dirty = equal < 0 || equal !== Object.values(prev).length;
}
}
}
const sortedRenderBatches = [...this._drapedRenderBatches];
sortedRenderBatches.sort((batchA, batchB) => {
const batchASize = batchA.end - batchA.start;
const batchBSize = batchB.end - batchB.start;
return batchBSize - batchASize;
});
for (const batch of sortedRenderBatches) {
for (const id of coords) {
if (psc.proxyCachedFBO[id.key]) {
continue;
}
// Assign renderCache FBO if there are available FBOs in pool.
let index = psc.renderCachePool.pop();
if (index === undefined && psc.renderCache.length < RENDER_CACHE_MAX_SIZE) {
index = psc.renderCache.length;
psc.renderCache.push(this._createFBO());
}
if (index !== undefined) {
psc.proxyCachedFBO[id.key] = {};
psc.proxyCachedFBO[id.key][batch.start] = index;
psc.renderCache[index].dirty = true; // needs to be rendered to.
}
}
}
this._tilesDirty = {};
}
_setupStencil(fbo: FBO, proxiedCoords: Array<ProxiedTileID>, layer: StyleLayer, sourceCache?: SourceCache) {
if (!sourceCache || !this._sourceTilesOverlap[sourceCache.id]) {
if (this._overlapStencilType) this._overlapStencilType = false;
return;
}
const context = this.painter.context;
const gl = context.gl;
// If needed, setup stencilling. Don't bother to remove when there is no
// more need: in such case, if there is no overlap, stencilling is disabled.
if (proxiedCoords.length <= 1) { this._overlapStencilType = false; return; }
let stencilRange;
if (layer.isTileClipped()) {
stencilRange = proxiedCoords.length;
this._overlapStencilMode.test = {func: gl.EQUAL, mask: 0xFF};
this._overlapStencilType = 'Clip';
} else if (proxiedCoords[0].overscaledZ > proxiedCoords[proxiedCoords.length - 1].overscaledZ) {
stencilRange = 1;
this._overlapStencilMode.test = {func: gl.GREATER, mask: 0xFF};
this._overlapStencilType = 'Mask';
} else {
this._overlapStencilType = false;
return;
}
if (this._stencilRef + stencilRange > 255) {
context.clear({stencil: 0});
this._stencilRef = 0;
}
this._stencilRef += stencilRange;
this._overlapStencilMode.ref = this._stencilRef;
if (layer.isTileClipped()) {
this._renderTileClippingMasks(proxiedCoords, this._overlapStencilMode.ref);
}
}
clipOrMaskOverlapStencilType(): boolean {
return this._overlapStencilType === 'Clip' || this._overlapStencilType === 'Mask';
}
stencilModeForRTTOverlap(id: OverscaledTileID): $ReadOnly<StencilMode> {
if (!this.renderingToTexture || !this._overlapStencilType) {
return StencilMode.disabled;
}
// All source tiles contributing to the same proxy are processed in sequence, in zoom descending order.
// For raster / hillshade overlap masking, ref is based on zoom dif.
// For vector layer clipping, every tile gets dedicated stencil ref.
if (this._overlapStencilType === 'Clip') {
// In immediate 2D mode, we render rects to mark clipping area and handle behavior on tile borders.
// Here, there is no need for now for this:
// 1. overlap is handled by proxy render to texture tiles (there is no overlap there)
// 2. here we handle only brief zoom out semi-transparent color intensity flickering
// and that is avoided fine by stenciling primitives as part of drawing (instead of additional tile quad step).
this._overlapStencilMode.ref = this.painter._tileClippingMaskIDs[id.key];
} // else this._overlapStencilMode.ref is set to a single value used per proxy tile, in _setupStencil.
return this._overlapStencilMode;
}
_renderTileClippingMasks(proxiedCoords: Array<ProxiedTileID>, ref: number) {
const painter = this.painter;
const context = this.painter.context;
const gl = context.gl;
painter._tileClippingMaskIDs = {};
context.setColorMode(ColorMode.disabled);
context.setDepthMode(DepthMode.disabled);
const program = painter.useProgram('clippingMask');
for (const tileID of proxiedCoords) {
const id = painter._tileClippingMaskIDs[tileID.key] = --ref;
program.draw(context, gl.TRIANGLES, DepthMode.disabled,
// Tests will always pass, and ref value will be written to stencil buffer.
new StencilMode({func: gl.ALWAYS, mask: 0}, id, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE),
ColorMode.disabled, CullFaceMode.disabled, clippingMaskUniformValues(tileID.projMatrix),
'$clipping', painter.tileExtentBuffer,
painter.quadTriangleIndexBuffer, painter.tileExtentSegments);
}
}
// Casts a ray from a point on screen and returns the intersection point with the terrain.
// The returned point contains the mercator coordinates in its first 3 components, and elevation
// in meter in its 4th coordinate.
pointCoordinate(screenPoint: Point): ?Vec4 {
const transform = this.painter.transform;
if (screenPoint.x < 0 || screenPoint.x > transform.width ||
screenPoint.y < 0 || screenPoint.y > transform.height) {
return null;
}
const far = [screenPoint.x, screenPoint.y, 1, 1];
vec4.transformMat4(far, far, transform.pixelMatrixInverse);
vec4.scale(far, far, 1.0 / far[3]);
// x & y in pixel coordinates, z is altitude in meters
far[0] /= transform.worldSize;
far[1] /= transform.worldSize;
const camera = transform._camera.position;
const mercatorZScale = mercatorZfromAltitude(1, transform.center.lat);
const p = [camera[0], camera[1], camera[2] / mercatorZScale, 0.0];
const dir = vec3.subtract([], far.slice(0, 3), p);
vec3.normalize(dir, dir);
const exaggeration = this._exaggeration;
const distanceAlongRay = this.raycast(p, dir, exaggeration);
if (distanceAlongRay === null || !distanceAlongRay) return null;
vec3.scaleAndAdd(p, p, dir, distanceAlongRay);
p[3] = p[2];
p[2] *= mercatorZScale;
return p;
}
drawDepth() {
const painter = this.painter;
const context = painter.context;
const psc = this.proxySourceCache;
const width = Math.ceil(painter.width), height = Math.ceil(painter.height);
if (this._depthFBO && (this._depthFBO.width !== width || this._depthFBO.height !== height)) {
this._depthFBO.destroy();
this._depthFBO = undefined;
this._depthTexture = undefined;
}
if (!this._depthFBO) {
const gl = context.gl;
const fbo = context.createFramebuffer(width, height, true);
context.activeTexture.set(gl.TEXTURE0);
const texture = new Texture(context, {width, height, data: null}, gl.RGBA);
texture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE);
fbo.colorAttachment.set(texture.texture);
const renderbuffer = context.createRenderbuffer(context.gl.DEPTH_COMPONENT16, width, height);
fbo.depthAttachment.set(renderbuffer);
this._depthFBO = fbo;
this._depthTexture = texture;
}
context.bindFramebuffer.set(this._depthFBO.framebuffer);
context.viewport.set([0, 0, width, height]);
drawTerrainDepth(painter, this, psc, this.proxyCoords);
}
_setupProxiedCoordsForOrtho(sourceCache: SourceCache, sourceCoords: Array<OverscaledTileID>, previousProxyToSource: {[number]: {[string]: Array<ProxiedTileID>}}): void {
if (sourceCache.getSource() instanceof ImageSource) {
return this._setupProxiedCoordsForImageSource(sourceCache, sourceCoords, previousProxyToSource);
}
this._findCoveringTileCache[sourceCache.id] = this._findCoveringTileCache[sourceCache.id] || {};
const coords = this.proxiedCoords[sourceCache.id] = [];
const proxys = this.proxyCoords;
for (let i = 0; i < proxys.length; i++) {
const proxyTileID = proxys[i];
const proxied = this._findTileCoveringTileID(proxyTileID, sourceCache);
if (proxied) {
assert(proxied.hasData());
const id = this._createProxiedId(proxyTileID, proxied, previousProxyToSource[proxyTileID.key] && previousProxyToSource[proxyTileID.key][sourceCache.id]);
coords.push(id);
this.proxyToSource[proxyTileID.key][sourceCache.id] = [id];
}
}
let hasOverlap = false;
for (let i = 0; i < sourceCoords.length; i++) {
const tile = sourceCache.getTile(sourceCoords[i]);
if (!tile || !tile.hasData()) continue;
const proxy = this._findTileCoveringTileID(tile.tileID, this.proxySourceCache);
// Don't add the tile if already added in loop above.
if (proxy && proxy.tileID.canonical.z !== tile.tileID.canonical.z) {
const array = this.proxyToSource[proxy.tileID.key][sourceCache.id];
const id = this._createProxiedId(proxy.tileID, tile, previousProxyToSource[proxy.tileID.key] && previousProxyToSource[proxy.tileID.key][sourceCache.id]);
if (!array) {
this.proxyToSource[proxy.tileID.key][sourceCache.id] = [id];
} else {
// The last element is parent added in loop above. This way we get
// a list in Z descending order which is needed for stencil masking.
array.splice(array.length - 1, 0, id);
}
coords.push(id);
hasOverlap = true;
}
}
this._sourceTilesOverlap[sourceCache.id] = hasOverlap;
}
_setupProxiedCoordsForImageSource(sourceCache: SourceCache, sourceCoords: Array<OverscaledTileID>, previousProxyToSource: {[number]: {[string]: Array<ProxiedTileID>}}) {
if (!sourceCache.getSource().loaded()) return;
const coords = this.proxiedCoords[sourceCache.id] = [];
const proxys = this.proxyCoords;
const imageSource: ImageSource = ((sourceCache.getSource(): any): ImageSource);
const anchor = new Point(imageSource.tileID.x, imageSource.tileID.y)._div(1 << imageSource.tileID.z);
// $FlowFixMe[method-unbinding]
const aabb = imageSource.coordinates.map(MercatorCoordinate.fromLngLat).reduce((acc, coord) => {
acc.min.x = Math.min(acc.min.x, coord.x - anchor.x);
acc.min.y = Math.min(acc.min.y, coord.y - anchor.y);
acc.max.x = Math.max(acc.max.x, coord.x - anchor.x);
acc.max.y = Math.max(acc.max.y, coord.y - anchor.y);
return acc;
}, {min: new Point(Number.MAX_VALUE, Number.MAX_VALUE), max: new Point(-Number.MAX_VALUE, -Number.MAX_VALUE)});
// Fast conservative check using aabb: content outside proxy tile gets clipped out by on render, anyway.
const tileOutsideImage = (tileID: OverscaledTileID, imageTileID: OverscaledTileID) => {
const x = tileID.wrap + tileID.canonical.x / (1 << tileID.canonical.z);
const y = tileID.canonical.y / (1 << tileID.canonical.z);
const d = EXTENT / (1 << tileID.canonical.z);
const ix = imageTileID.wrap + imageTileID.canonical.x / (1 << imageTileID.canonical.z);
const iy = imageTileID.canonical.y / (1 << imageTileID.canonical.z);
return x + d < ix + aabb.min.x || x > ix + aabb.max.x || y + d < iy + aabb.min.y || y > iy + aabb.max.y;
};
for (let i = 0; i < proxys.length; i++) {
const proxyTileID = proxys[i];
for (let j = 0; j < sourceCoords.length; j++) {
const tile = sourceCache.getTile(sourceCoords[j]);
if (!tile || !tile.hasData()) continue;
// Setup proxied -> proxy mapping only if image on given tile wrap intersects the proxy tile.
if (tileOutsideImage(proxyTileID, tile.tileID)) continue;
const id = this._createProxiedId(proxyTileID, tile, previousProxyToSource[proxyTileID.key] && previousProxyToSource[proxyTileID.key][sourceCache.id]);
const array = this.proxyToSource[proxyTileID.key][sourceCache.id];
if (!array) {
this.proxyToSource[proxyTileID.key][sourceCache.id] = [id];
} else {
array.push(id);
}
coords.push(id);
}
}
}
// recycle is previous pass content that likely contains proxied ID combining proxy and source tile.
_createProxiedId(proxyTileID: OverscaledTileID, tile: Tile, recycle: Array<ProxiedTileID>): ProxiedTileID {
let matrix = this.orthoMatrix;
if (recycle) {
const recycled = recycle.find(proxied => (proxied.key === tile.tileID.key));
if (recycled) return recycled;
}
if (tile.tileID.key !== proxyTileID.key) {
const scale = proxyTileID.canonical.z - tile.tileID.canonical.z;
matrix = mat4.create();
let size, xOffset, yOffset;
const wrap = (tile.tileID.wrap - proxyTileID.wrap) << proxyTileID.overscaledZ;
if (scale > 0) {
size = EXTENT >> scale;
xOffset = size * ((tile.tileID.canonical.x << scale) - proxyTileID.canonical.x + wrap);
yOffset = size * ((tile.tileID.canonical.y << scale) - proxyTileID.canonical.y);
} else {
size = EXTENT << -scale;
xOffset = EXTENT * (tile.tileID.canonical.x - ((proxyTileID.canonical.x + wrap) << -scale));
yOffset = EXTENT * (tile.tileID.canonical.y - (proxyTileID.canonical.y << -scale));
}
mat4.ortho(matrix, 0, size, 0, size, 0, 1);
mat4.translate(matrix, matrix, [xOffset, yOffset, 0]);
}
return new ProxiedTileID(tile.tileID, proxyTileID.key, matrix);
}
// A variant of SourceCache.findLoadedParent that considers only visible
// tiles (and doesn't check SourceCache._cache). Another difference is in
// caching "not found" results along the lookup, to leave the lookup early.
// Not found is cached by this._findCoveringTileCache[key] = null;
_findTileCoveringTileID(tileID: OverscaledTileID, sourceCache: SourceCache): ?Tile {
let tile: ?Tile = sourceCache.getTile(tileID);
if (tile && tile.hasData()) return tile;
const lookup = this._findCoveringTileCache[sourceCache.id];
const key = lookup[tileID.key];
tile = key ? sourceCache.getTileByID(key) : null;
if ((tile && tile.hasData()) || key === null) return tile;
assert(!key || tile);
let sourceTileID = tile ? tile.tileID : tileID;
let z = sourceTileID.overscaledZ;
const minzoom = sourceCache.getSource().minzoom;
const path = [];
if (!key) {
const maxzoom = sourceCache.getSource().maxzoom;
if (tileID.canonical.z >= maxzoom) {
const downscale = tileID.canonical.z - maxzoom;
if (sourceCache.getSource().reparseOverscaled) {
z = Math.max(tileID.canonical.z + 2, sourceCache.transform.tileZoom);
sourceTileID = new OverscaledTileID(z, tileID.wrap, maxzoom,
tileID.canonical.x >> downscale, tileID.canonical.y >> downscale);
} else if (downscale !== 0) {
z = maxzoom;
sourceTileID = new OverscaledTileID(z, tileID.wrap, maxzoom,
tileID.canonical.x >> downscale, tileID.canonical.y >> downscale);
}
}
if (sourceTileID.key !== tileID.key) {
path.push(sourceTileID.key);
tile = sourceCache.getTile(sourceTileID);
}
}
const pathToLookup = (key: ?number) => {
path.forEach(id => { lookup[id] = key; });
path.length = 0;
};
for (z = z - 1; z >= minzoom && !(tile && tile.hasData()); z--) {
if (tile) {
pathToLookup(tile.tileID.key); // Store lookup to parents not loaded (yet).
}
const id = sourceTileID.calculateScaledKey(z);
tile = sourceCache.getTileByID(id);
if (tile && tile.hasData()) break;
const key = lookup[id];
if (key === null) {
break; // There's no tile loaded and no point searching further.
} else if (key !== undefined) {
tile = sourceCache.getTileByID(key);
assert(tile);
continue;
}
path.push(id);
}
pathToLookup(tile ? tile.tileID.key : null);
return tile && tile.hasData() ? tile : null;
}
findDEMTileFor(tileID: OverscaledTileID): ?Tile {
return this.enabled ? this._findTileCoveringTileID(tileID, this.sourceCache) : null;
}
/*
* Bookkeeping if something gets rendered to the tile.
*/
prepareDrawTile() {
this.renderedToTile = true;
}
_clearRenderCacheForTile(source: string, coord: OverscaledTileID) {
let sourceTiles = this._tilesDirty[source];
if (!sourceTiles) sourceTiles = this._tilesDirty[source] = {};
sourceTiles[coord.key] = true;
}
/*
* Lazily instantiate the wireframe index buffer and segment vector so that we don't
* allocate the geometry for rendering a debug wireframe until it's needed.
*/
getWirefameBuffer(): [IndexBuffer, SegmentVector] {
if (!this.wireframeSegments) {
const wireframeGridIndices = createWireframeGrid(GRID_DIM + 1);
this.wireframeIndexBuffer = this.painter.context.createIndexBuffer(wireframeGridIndices);
this.wireframeSegments = SegmentVector.simpleSegment(0, 0, this.gridBuffer.length, wireframeGridIndices.length);
}
return [this.wireframeIndexBuffer, this.wireframeSegments];
}
}
function sortByDistanceToCamera(tileIDs: Array<OverscaledTileID>, painter: Painter) {
const cameraCoordinate = painter.transform.pointCoordinate(painter.transform.getCameraPoint());
const cameraPoint = new Point(cameraCoordinate.x, cameraCoordinate.y);
tileIDs.sort((a, b) => {
if (b.overscaledZ - a.overscaledZ) return b.overscaledZ - a.overscaledZ;
const aPoint = new Point(a.canonical.x + (1 << a.canonical.z) * a.wrap, a.canonical.y);
const bPoint = new Point(b.canonical.x + (1 << b.canonical.z) * b.wrap, b.canonical.y);
const cameraScaled = cameraPoint.mult(1 << a.canonical.z);
cameraScaled.x -= 0.5;
cameraScaled.y -= 0.5;
return cameraScaled.distSqr(aPoint) - cameraScaled.distSqr(bPoint);
});
}
/**
* Creates uniform grid of triangles, covering EXTENT x EXTENT square, with two
* adjustent traigles forming a quad, so that there are |count| columns and rows
* of these quads in EXTENT x EXTENT square.
* e.g. for count of 2:
* -------------
* | /| /|
* | / | / |
* |/ |/ |
* -------------
* | /| /|
* | / | / |
* |/ |/ |
* -------------
* @param {number} count Count of rows and columns
* @private
*/
function createGrid(count: number): [PosArray, TriangleIndexArray, number] {
const boundsArray = new PosArray();
// Around the grid, add one more row/column padding for "skirt".
const indexArray = new TriangleIndexArray();
const size = count + 2;
boundsArray.reserve(size * size);
indexArray.reserve((size - 1) * (size - 1) * 2);
const step = EXTENT / (count - 1);
const gridBound = EXTENT + step / 2;
const bound = gridBound + step;
// Skirt offset of 0x5FFF is chosen randomly to encode boolean value (skirt
// on/off) with x position (max value EXTENT = 4096) to 16-bit signed integer.
const skirtOffset = 24575; // 0x5FFF
for (let y = -step; y < bound; y += step) {
for (let x = -step; x < bound; x += step) {
const offset = (x < 0 || x > gridBound || y < 0 || y > gridBound) ? skirtOffset : 0;
const xi = clamp(Math.round(x), 0, EXTENT);
const yi = clamp(Math.round(y), 0, EXTENT);
boundsArray.emplaceBack(xi + offset, yi);
}
}
// For cases when there's no need to render "skirt", the "inner" grid indices
// are followed by skirt indices.
const skirtIndicesOffset = (size - 3) * (size - 3) * 2;
const quad = (i: number, j: number) => {
const index = j * size + i;
indexArray.emplaceBack(index + 1, index, index + size);
indexArray.emplaceBack(index + size, index + size + 1, index + 1);
};
for (let j = 1; j < size - 2; j++) {
for (let i = 1; i < size - 2; i++) {
quad(i, j);
}
}
// Padding (skirt) indices:
[0, size - 2].forEach(j => {
for (let i = 0; i < size - 1; i++) {
quad(i, j);
quad(j, i);
}
});
return [boundsArray, indexArray, skirtIndicesOffset];
}
/**
* Creates a grid of indices corresponding to the grid constructed by createGrid
* in order to render that grid as a wireframe rather than a solid mesh. It does
* not create a skirt and so only goes from 1 to count + 1, e.g. for count of 2:
* -------------
* | /| /|
* | / | / |
* |/ |/ |
* -------------
* | /| /|
* | / | / |
* |/ |/ |
* -------------
* @param {number} count Count of rows and columns
* @private
*/
function createWireframeGrid(count: number): LineIndexArray {
let index = 0;
const indexArray = new LineIndexArray();
const size = count + 2;
// Draw two edges of a quad and its diagonal. The very last row and column have
// an additional line to close off the grid.
for (let j = 1; j < count; j++) {
for (let i = 1; i < count; i++) {
index = j * size + i;
indexArray.emplaceBack(index, index + 1);
indexArray.emplaceBack(index, index + size);
indexArray.emplaceBack(index + 1, index + size);
// Place an extra line at the end of each row
if (j === count - 1) indexArray.emplaceBack(index + size, index + size + 1);
}
// Place an extra line at the end of each col
indexArray.emplaceBack(index + 1, index + 1 + size);
}
return indexArray;
}
export type TerrainUniformsType = {|
'u_dem': Uniform1i,
'u_dem_prev': Uniform1i,
'u_dem_unpack': Uniform4f,
'u_dem_tl': Uniform2f,
'u_dem_scale': Uniform1f,
'u_dem_tl_prev': Uniform2f,
'u_dem_scale_prev': Uniform1f,
'u_dem_size': Uniform1f,
'u_dem_lerp': Uniform1f,
"u_exaggeration": Uniform1f,
'u_depth': Uniform1i,
'u_depth_size_inv': Uniform2f,
'u_meter_to_dem'?: Uniform1f,
'u_label_plane_matrix_inv'?: UniformMatrix4f,
|};
export const terrainUniforms = (context: Context): TerrainUniformsType => ({
'u_dem': new Uniform1i(context),
'u_dem_prev': new Uniform1i(context),
'u_dem_unpack': new Uniform4f(context),
'u_dem_tl': new Uniform2f(context),
'u_dem_scale': new Uniform1f(context),
'u_dem_tl_prev': new Uniform2f(context),
'u_dem_scale_prev': new Uniform1f(context),
'u_dem_size': new Uniform1f(context),
'u_dem_lerp': new Uniform1f(context),
'u_exaggeration': new Uniform1f(context),
'u_depth': new Uniform1i(context),
'u_depth_size_inv': new Uniform2f(context),
'u_meter_to_dem': new Uniform1f(context),
'u_label_plane_matrix_inv': new UniformMatrix4f(context),
});
function defaultTerrainUniforms(encoding: DEMEncoding): UniformValues<TerrainUniformsType> {
return {
'u_dem': 2,
'u_dem_prev': 4,
'u_dem_unpack': DEMData.getUnpackVector(encoding),
'u_dem_tl': [0, 0],
'u_dem_tl_prev': [0, 0],
'u_dem_scale': 0,
'u_dem_scale_prev': 0,
'u_dem_size': 0,
'u_dem_lerp': 1.0,
'u_depth': 3,
'u_depth_size_inv': [0, 0],
'u_exaggeration': 0,
};
}
export type GlobeUniformsType = {|
'u_tile_tl_up': Uniform3f,
'u_tile_tr_up': Uniform3f,
'u_tile_br_up': Uniform3f,
'u_tile_bl_up': Uniform3f,
'u_tile_up_scale': Uniform1f
|};
export const globeUniforms = (context: Context): GlobeUniformsType => ({
'u_tile_tl_up': new Uniform3f(context),
'u_tile_tr_up': new Uniform3f(context),
'u_tile_br_up': new Uniform3f(context),
'u_tile_bl_up': new Uniform3f(context),
'u_tile_up_scale': new Uniform1f(context)
});