// @flow import browser from '../util/browser'; import window from '../util/window'; import {mat4} from 'gl-matrix'; import SourceCache from '../source/source_cache'; import EXTENT from '../data/extent'; import pixelsToTileUnits from '../source/pixels_to_tile_units'; import SegmentVector from '../data/segment'; import {RasterBoundsArray, PosArray, TriangleIndexArray, LineStripIndexArray} from '../data/array_types'; import {values} from '../util/util'; import rasterBoundsAttributes from '../data/raster_bounds_attributes'; import posAttributes from '../data/pos_attributes'; import ProgramConfiguration from '../data/program_configuration'; import CrossTileSymbolIndex from '../symbol/cross_tile_symbol_index'; import * as shaders from '../shaders'; import Program from './program'; import {programUniforms} from './program/program_uniforms'; import Context from '../gl/context'; import DepthMode from '../gl/depth_mode'; import StencilMode from '../gl/stencil_mode'; import ColorMode from '../gl/color_mode'; import CullFaceMode from '../gl/cull_face_mode'; import Texture from './texture'; import {clippingMaskUniformValues} from './program/clipping_mask_program'; import Color from '../style-spec/util/color'; import symbol from './draw_symbol'; import circle from './draw_circle'; import heatmap from './draw_heatmap'; import line from './draw_line'; import fill from './draw_fill'; import fillExtrusion from './draw_fill_extrusion'; import hillshade from './draw_hillshade'; import raster from './draw_raster'; import background from './draw_background'; import debug, {drawDebugPadding} from './draw_debug'; import custom from './draw_custom'; const draw = { symbol, circle, heatmap, line, fill, 'fill-extrusion': fillExtrusion, hillshade, raster, background, debug, custom }; import type Transform from '../geo/transform'; import type Tile from '../source/tile'; import type {OverscaledTileID} from '../source/tile_id'; import type Style from '../style/style'; import type StyleLayer from '../style/style_layer'; import type {CrossFaded} from '../style/properties'; import type LineAtlas from './line_atlas'; import type ImageManager from './image_manager'; import type GlyphManager from './glyph_manager'; import type VertexBuffer from '../gl/vertex_buffer'; import type IndexBuffer from '../gl/index_buffer'; import type {DepthRangeType, DepthMaskType, DepthFuncType} from '../gl/types'; import type ResolvedImage from '../style-spec/expression/types/resolved_image'; export type RenderPass = 'offscreen' | 'opaque' | 'translucent'; type PainterOptions = { showOverdrawInspector: boolean, showTileBoundaries: boolean, showPadding: boolean, rotating: boolean, zooming: boolean, moving: boolean, gpuTiming: boolean, fadeDuration: number } /** * Initialize a new painter object. * * @param {Canvas} gl an experimental-webgl drawing context * @private */ class Painter { context: Context; transform: Transform; _tileTextures: {[_: number]: Array }; numSublayers: number; depthEpsilon: number; emptyProgramConfiguration: ProgramConfiguration; width: number; height: number; tileExtentBuffer: VertexBuffer; tileExtentSegments: SegmentVector; debugBuffer: VertexBuffer; debugSegments: SegmentVector; rasterBoundsBuffer: VertexBuffer; rasterBoundsSegments: SegmentVector; viewportBuffer: VertexBuffer; viewportSegments: SegmentVector; quadTriangleIndexBuffer: IndexBuffer; tileBorderIndexBuffer: IndexBuffer; _tileClippingMaskIDs: {[_: string]: number }; stencilClearMode: StencilMode; style: Style; options: PainterOptions; lineAtlas: LineAtlas; imageManager: ImageManager; glyphManager: GlyphManager; depthRangeFor3D: DepthRangeType; opaquePassCutoff: number; renderPass: RenderPass; currentLayer: number; currentStencilSource: ?string; nextStencilID: number; id: string; _showOverdrawInspector: boolean; cache: {[_: string]: Program<*> }; crossTileSymbolIndex: CrossTileSymbolIndex; symbolFadeChange: number; gpuTimers: {[_: string]: any }; emptyTexture: Texture; debugOverlayTexture: Texture; debugOverlayCanvas: HTMLCanvasElement; constructor(gl: WebGLRenderingContext, transform: Transform) { this.context = new Context(gl); this.transform = transform; this._tileTextures = {}; this.setup(); // Within each layer there are multiple distinct z-planes that can be drawn to. // This is implemented using the WebGL depth buffer. this.numSublayers = SourceCache.maxUnderzooming + SourceCache.maxOverzooming + 1; this.depthEpsilon = 1 / Math.pow(2, 16); this.crossTileSymbolIndex = new CrossTileSymbolIndex(); this.gpuTimers = {}; } /* * Update the GL viewport, projection matrix, and transforms to compensate * for a new width and height value. */ resize(width: number, height: number) { this.width = width * browser.devicePixelRatio; this.height = height * browser.devicePixelRatio; this.context.viewport.set([0, 0, this.width, this.height]); if (this.style) { for (const layerId of this.style._order) { this.style._layers[layerId].resize(); } } } setup() { const context = this.context; const tileExtentArray = new PosArray(); tileExtentArray.emplaceBack(0, 0); tileExtentArray.emplaceBack(EXTENT, 0); tileExtentArray.emplaceBack(0, EXTENT); tileExtentArray.emplaceBack(EXTENT, EXTENT); this.tileExtentBuffer = context.createVertexBuffer(tileExtentArray, posAttributes.members); this.tileExtentSegments = SegmentVector.simpleSegment(0, 0, 4, 2); const debugArray = new PosArray(); debugArray.emplaceBack(0, 0); debugArray.emplaceBack(EXTENT, 0); debugArray.emplaceBack(0, EXTENT); debugArray.emplaceBack(EXTENT, EXTENT); this.debugBuffer = context.createVertexBuffer(debugArray, posAttributes.members); this.debugSegments = SegmentVector.simpleSegment(0, 0, 4, 5); const rasterBoundsArray = new RasterBoundsArray(); rasterBoundsArray.emplaceBack(0, 0, 0, 0); rasterBoundsArray.emplaceBack(EXTENT, 0, EXTENT, 0); rasterBoundsArray.emplaceBack(0, EXTENT, 0, EXTENT); rasterBoundsArray.emplaceBack(EXTENT, EXTENT, EXTENT, EXTENT); this.rasterBoundsBuffer = context.createVertexBuffer(rasterBoundsArray, rasterBoundsAttributes.members); this.rasterBoundsSegments = SegmentVector.simpleSegment(0, 0, 4, 2); const viewportArray = new PosArray(); viewportArray.emplaceBack(0, 0); viewportArray.emplaceBack(1, 0); viewportArray.emplaceBack(0, 1); viewportArray.emplaceBack(1, 1); this.viewportBuffer = context.createVertexBuffer(viewportArray, posAttributes.members); this.viewportSegments = SegmentVector.simpleSegment(0, 0, 4, 2); const tileLineStripIndices = new LineStripIndexArray(); tileLineStripIndices.emplaceBack(0); tileLineStripIndices.emplaceBack(1); tileLineStripIndices.emplaceBack(3); tileLineStripIndices.emplaceBack(2); tileLineStripIndices.emplaceBack(0); this.tileBorderIndexBuffer = context.createIndexBuffer(tileLineStripIndices); const quadTriangleIndices = new TriangleIndexArray(); quadTriangleIndices.emplaceBack(0, 1, 2); quadTriangleIndices.emplaceBack(2, 1, 3); this.quadTriangleIndexBuffer = context.createIndexBuffer(quadTriangleIndices); this.emptyTexture = new Texture(context, { width: 1, height: 1, data: new Uint8Array([0, 0, 0, 0]) }, context.gl.RGBA); const gl = this.context.gl; this.stencilClearMode = new StencilMode({func: gl.ALWAYS, mask: 0}, 0x0, 0xFF, gl.ZERO, gl.ZERO, gl.ZERO); } /* * Reset the drawing canvas by clearing the stencil buffer so that we can draw * new tiles at the same location, while retaining previously drawn pixels. */ clearStencil() { const context = this.context; const gl = context.gl; this.nextStencilID = 1; this.currentStencilSource = undefined; // As a temporary workaround for https://github.com/mapbox/mapbox-gl-js/issues/5490, // pending an upstream fix, we draw a fullscreen stencil=0 clipping mask here, // effectively clearing the stencil buffer: once an upstream patch lands, remove // this function in favor of context.clear({ stencil: 0x0 }) const matrix = mat4.create(); mat4.ortho(matrix, 0, this.width, this.height, 0, 0, 1); mat4.scale(matrix, matrix, [gl.drawingBufferWidth, gl.drawingBufferHeight, 0]); this.useProgram('clippingMask').draw(context, gl.TRIANGLES, DepthMode.disabled, this.stencilClearMode, ColorMode.disabled, CullFaceMode.disabled, clippingMaskUniformValues(matrix), '$clipping', this.viewportBuffer, this.quadTriangleIndexBuffer, this.viewportSegments); } _renderTileClippingMasks(layer: StyleLayer, tileIDs: Array) { if (this.currentStencilSource === layer.source || !layer.isTileClipped() || !tileIDs || !tileIDs.length) return; this.currentStencilSource = layer.source; const context = this.context; const gl = context.gl; if (this.nextStencilID + tileIDs.length > 256) { // we'll run out of fresh IDs so we need to clear and start from scratch this.clearStencil(); } context.setColorMode(ColorMode.disabled); context.setDepthMode(DepthMode.disabled); const program = this.useProgram('clippingMask'); this._tileClippingMaskIDs = {}; for (const tileID of tileIDs) { const id = this._tileClippingMaskIDs[tileID.key] = this.nextStencilID++; 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.posMatrix), '$clipping', this.tileExtentBuffer, this.quadTriangleIndexBuffer, this.tileExtentSegments); } } stencilModeFor3D(): StencilMode { this.currentStencilSource = undefined; if (this.nextStencilID + 1 > 256) { this.clearStencil(); } const id = this.nextStencilID++; const gl = this.context.gl; return new StencilMode({func: gl.NOTEQUAL, mask: 0xFF}, id, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE); } stencilModeForClipping(tileID: OverscaledTileID): StencilMode { const gl = this.context.gl; return new StencilMode({func: gl.EQUAL, mask: 0xFF}, this._tileClippingMaskIDs[tileID.key], 0x00, gl.KEEP, gl.KEEP, gl.REPLACE); } /* * Sort coordinates by Z as drawing tiles is done in Z-descending order. * All children with the same Z write the same stencil value. Children * stencil values are greater than parent's. This is used only for raster * and raster-dem tiles, which are already clipped to tile boundaries, to * mask area of tile overlapped by children tiles. * Stencil ref values continue range used in _tileClippingMaskIDs. * * Returns [StencilMode for tile overscaleZ map, sortedCoords]. */ stencilConfigForOverlap(tileIDs: Array): [{[_: number]: $ReadOnly}, Array] { const gl = this.context.gl; const coords = tileIDs.sort((a, b) => b.overscaledZ - a.overscaledZ); const minTileZ = coords[coords.length - 1].overscaledZ; const stencilValues = coords[0].overscaledZ - minTileZ + 1; if (stencilValues > 1) { this.currentStencilSource = undefined; if (this.nextStencilID + stencilValues > 256) { this.clearStencil(); } const zToStencilMode = {}; for (let i = 0; i < stencilValues; i++) { zToStencilMode[i + minTileZ] = new StencilMode({func: gl.GEQUAL, mask: 0xFF}, i + this.nextStencilID, 0xFF, gl.KEEP, gl.KEEP, gl.REPLACE); } this.nextStencilID += stencilValues; return [zToStencilMode, coords]; } return [{[minTileZ]: StencilMode.disabled}, coords]; } colorModeForRenderPass(): $ReadOnly { const gl = this.context.gl; if (this._showOverdrawInspector) { const numOverdrawSteps = 8; const a = 1 / numOverdrawSteps; return new ColorMode([gl.CONSTANT_COLOR, gl.ONE], new Color(a, a, a, 0), [true, true, true, true]); } else if (this.renderPass === 'opaque') { return ColorMode.unblended; } else { return ColorMode.alphaBlended; } } depthModeForSublayer(n: number, mask: DepthMaskType, func: ?DepthFuncType): $ReadOnly { if (!this.opaquePassEnabledForLayer()) return DepthMode.disabled; const depth = 1 - ((1 + this.currentLayer) * this.numSublayers + n) * this.depthEpsilon; return new DepthMode(func || this.context.gl.LEQUAL, mask, [depth, depth]); } /* * The opaque pass and 3D layers both use the depth buffer. * Layers drawn above 3D layers need to be drawn using the * painter's algorithm so that they appear above 3D features. * This returns true for layers that can be drawn using the * opaque pass. */ opaquePassEnabledForLayer() { return this.currentLayer < this.opaquePassCutoff; } render(style: Style, options: PainterOptions) { this.style = style; this.options = options; this.lineAtlas = style.lineAtlas; this.imageManager = style.imageManager; this.glyphManager = style.glyphManager; this.symbolFadeChange = style.placement.symbolFadeChange(browser.now()); this.imageManager.beginFrame(); const layerIds = this.style._order; const sourceCaches = this.style.sourceCaches; for (const id in sourceCaches) { const sourceCache = sourceCaches[id]; if (sourceCache.used) { sourceCache.prepare(this.context); } } const coordsAscending: {[_: string]: Array} = {}; const coordsDescending: {[_: string]: Array} = {}; const coordsDescendingSymbol: {[_: string]: Array} = {}; for (const id in sourceCaches) { const sourceCache = sourceCaches[id]; coordsAscending[id] = sourceCache.getVisibleCoordinates(); coordsDescending[id] = coordsAscending[id].slice().reverse(); coordsDescendingSymbol[id] = sourceCache.getVisibleCoordinates(true).reverse(); } this.opaquePassCutoff = Infinity; for (let i = 0; i < layerIds.length; i++) { const layerId = layerIds[i]; if (this.style._layers[layerId].is3D()) { this.opaquePassCutoff = i; break; } } // Offscreen pass =============================================== // We first do all rendering that requires rendering to a separate // framebuffer, and then save those for rendering back to the map // later: in doing this we avoid doing expensive framebuffer restores. this.renderPass = 'offscreen'; for (const layerId of layerIds) { const layer = this.style._layers[layerId]; if (!layer.hasOffscreenPass() || layer.isHidden(this.transform.zoom)) continue; const coords = coordsDescending[layer.source]; if (layer.type !== 'custom' && !coords.length) continue; this.renderLayer(this, sourceCaches[layer.source], layer, coords); } // Rebind the main framebuffer now that all offscreen layers have been rendered: this.context.bindFramebuffer.set(null); // Clear buffers in preparation for drawing to the main framebuffer this.context.clear({color: options.showOverdrawInspector ? Color.black : Color.transparent, depth: 1}); this.clearStencil(); this._showOverdrawInspector = options.showOverdrawInspector; this.depthRangeFor3D = [0, 1 - ((style._order.length + 2) * this.numSublayers * this.depthEpsilon)]; // Opaque pass =============================================== // Draw opaque layers top-to-bottom first. this.renderPass = 'opaque'; for (this.currentLayer = layerIds.length - 1; this.currentLayer >= 0; this.currentLayer--) { const layer = this.style._layers[layerIds[this.currentLayer]]; const sourceCache = sourceCaches[layer.source]; const coords = coordsAscending[layer.source]; this._renderTileClippingMasks(layer, coords); this.renderLayer(this, sourceCache, layer, coords); } // Translucent pass =============================================== // Draw all other layers bottom-to-top. this.renderPass = 'translucent'; for (this.currentLayer = 0; this.currentLayer < layerIds.length; this.currentLayer++) { const layer = this.style._layers[layerIds[this.currentLayer]]; const sourceCache = sourceCaches[layer.source]; // For symbol layers in the translucent pass, we add extra tiles to the renderable set // for cross-tile symbol fading. Symbol layers don't use tile clipping, so no need to render // separate clipping masks const coords = (layer.type === 'symbol' ? coordsDescendingSymbol : coordsDescending)[layer.source]; this._renderTileClippingMasks(layer, coordsAscending[layer.source]); this.renderLayer(this, sourceCache, layer, coords); } if (this.options.showTileBoundaries) { //Use source with highest maxzoom let selectedSource; let sourceCache; const layers = values(this.style._layers); layers.forEach((layer) => { if (layer.source && !layer.isHidden(this.transform.zoom)) { if (layer.source !== (sourceCache && sourceCache.id)) { sourceCache = this.style.sourceCaches[layer.source]; } if (!selectedSource || (selectedSource.getSource().maxzoom < sourceCache.getSource().maxzoom)) { selectedSource = sourceCache; } } }); if (selectedSource) { draw.debug(this, selectedSource, selectedSource.getVisibleCoordinates()); } } if (this.options.showPadding) { drawDebugPadding(this); } // Set defaults for most GL values so that anyone using the state after the render // encounters more expected values. this.context.setDefault(); } renderLayer(painter: Painter, sourceCache: SourceCache, layer: StyleLayer, coords: Array) { if (layer.isHidden(this.transform.zoom)) return; if (layer.type !== 'background' && layer.type !== 'custom' && !coords.length) return; this.id = layer.id; this.gpuTimingStart(layer); draw[layer.type](painter, sourceCache, layer, coords, this.style.placement.variableOffsets); this.gpuTimingEnd(); } gpuTimingStart(layer: StyleLayer) { if (!this.options.gpuTiming) return; const ext = this.context.extTimerQuery; // This tries to time the draw call itself, but note that the cost for drawing a layer // may be dominated by the cost of uploading vertices to the GPU. // To instrument that, we'd need to pass the layerTimers object down into the bucket // uploading logic. let layerTimer = this.gpuTimers[layer.id]; if (!layerTimer) { layerTimer = this.gpuTimers[layer.id] = { calls: 0, cpuTime: 0, query: ext.createQueryEXT() }; } layerTimer.calls++; ext.beginQueryEXT(ext.TIME_ELAPSED_EXT, layerTimer.query); } gpuTimingEnd() { if (!this.options.gpuTiming) return; const ext = this.context.extTimerQuery; ext.endQueryEXT(ext.TIME_ELAPSED_EXT); } collectGpuTimers() { const currentLayerTimers = this.gpuTimers; this.gpuTimers = {}; return currentLayerTimers; } queryGpuTimers(gpuTimers: {[_: string]: any}) { const layers = {}; for (const layerId in gpuTimers) { const gpuTimer = gpuTimers[layerId]; const ext = this.context.extTimerQuery; const gpuTime = ext.getQueryObjectEXT(gpuTimer.query, ext.QUERY_RESULT_EXT) / (1000 * 1000); ext.deleteQueryEXT(gpuTimer.query); layers[layerId] = gpuTime; } return layers; } /** * Transform a matrix to incorporate the *-translate and *-translate-anchor properties into it. * @param inViewportPixelUnitsUnits True when the units accepted by the matrix are in viewport pixels instead of tile units. * @returns {Float32Array} matrix * @private */ translatePosMatrix(matrix: Float32Array, tile: Tile, translate: [number, number], translateAnchor: 'map' | 'viewport', inViewportPixelUnitsUnits?: boolean) { if (!translate[0] && !translate[1]) return matrix; const angle = inViewportPixelUnitsUnits ? (translateAnchor === 'map' ? this.transform.angle : 0) : (translateAnchor === 'viewport' ? -this.transform.angle : 0); if (angle) { const sinA = Math.sin(angle); const cosA = Math.cos(angle); translate = [ translate[0] * cosA - translate[1] * sinA, translate[0] * sinA + translate[1] * cosA ]; } const translation = [ inViewportPixelUnitsUnits ? translate[0] : pixelsToTileUnits(tile, translate[0], this.transform.zoom), inViewportPixelUnitsUnits ? translate[1] : pixelsToTileUnits(tile, translate[1], this.transform.zoom), 0 ]; const translatedMatrix = new Float32Array(16); mat4.translate(translatedMatrix, matrix, translation); return translatedMatrix; } saveTileTexture(texture: Texture) { const textures = this._tileTextures[texture.size[0]]; if (!textures) { this._tileTextures[texture.size[0]] = [texture]; } else { textures.push(texture); } } getTileTexture(size: number) { const textures = this._tileTextures[size]; return textures && textures.length > 0 ? textures.pop() : null; } /** * Checks whether a pattern image is needed, and if it is, whether it is not loaded. * * @returns true if a needed image is missing and rendering needs to be skipped. * @private */ isPatternMissing(image: ?CrossFaded): boolean { if (!image) return false; if (!image.from || !image.to) return true; const imagePosA = this.imageManager.getPattern(image.from.toString()); const imagePosB = this.imageManager.getPattern(image.to.toString()); return !imagePosA || !imagePosB; } useProgram(name: string, programConfiguration: ?ProgramConfiguration): Program { this.cache = this.cache || {}; const key = `${name}${programConfiguration ? programConfiguration.cacheKey : ''}${this._showOverdrawInspector ? '/overdraw' : ''}`; if (!this.cache[key]) { this.cache[key] = new Program(this.context, name, shaders[name], programConfiguration, programUniforms[name], this._showOverdrawInspector); } return this.cache[key]; } /* * Reset some GL state to default values to avoid hard-to-debug bugs * in custom layers. */ setCustomLayerDefaults() { // Prevent custom layers from unintentionally modify the last VAO used. // All other state is state is restored on it's own, but for VAOs it's // simpler to unbind so that we don't have to track the state of VAOs. this.context.unbindVAO(); // The default values for this state is meaningful and often expected. // Leaving this state dirty could cause a lot of confusion for users. this.context.cullFace.setDefault(); this.context.activeTexture.setDefault(); this.context.pixelStoreUnpack.setDefault(); this.context.pixelStoreUnpackPremultiplyAlpha.setDefault(); this.context.pixelStoreUnpackFlipY.setDefault(); } /* * Set GL state that is shared by all layers. */ setBaseState() { const gl = this.context.gl; this.context.cullFace.set(false); this.context.viewport.set([0, 0, this.width, this.height]); this.context.blendEquation.set(gl.FUNC_ADD); } initDebugOverlayCanvas() { if (this.debugOverlayCanvas == null) { this.debugOverlayCanvas = window.document.createElement('canvas'); this.debugOverlayCanvas.width = 512; this.debugOverlayCanvas.height = 512; const gl = this.context.gl; this.debugOverlayTexture = new Texture(this.context, this.debugOverlayCanvas, gl.RGBA); } } destroy() { this.emptyTexture.destroy(); if (this.debugOverlayTexture) { this.debugOverlayTexture.destroy(); } } } export default Painter;