package.src.render.painter.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mapbox-gl Show documentation
Show all versions of mapbox-gl Show documentation
A WebGL interactive maps library
The newest version!
// @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;