mapbox-gl
Version:
A WebGL interactive maps library
414 lines (330 loc) • 13.9 kB
JavaScript
'use strict';
var glutil = require('./gl_util');
var browser = require('../util/browser');
var mat4 = require('gl-matrix').mat4;
var FrameHistory = require('./frame_history');
var TileCoord = require('../source/tile_coord');
var EXTENT = require('../data/buffer').EXTENT;
/*
* Initialize a new painter object.
*
* @param {Canvas} gl an experimental-webgl drawing context
*/
module.exports = Painter;
function Painter(gl, transform) {
this.gl = glutil.extend(gl);
this.transform = transform;
this.reusableTextures = {};
this.preFbos = {};
this.frameHistory = new FrameHistory();
this.setup();
// Within each layer there are 3 distinct z-planes that can be drawn to.
// This is implemented using the WebGL depth buffer.
this.numSublayers = 3;
this.depthEpsilon = 1 / Math.pow(2, 16);
}
/*
* Update the GL viewport, projection matrix, and transforms to compensate
* for a new width and height value.
*/
Painter.prototype.resize = function(width, height) {
var gl = this.gl;
this.width = width * browser.devicePixelRatio;
this.height = height * browser.devicePixelRatio;
gl.viewport(0, 0, this.width, this.height);
};
Painter.prototype.setup = function() {
var gl = this.gl;
gl.verbose = true;
// We are blending the new pixels *behind* the existing pixels. That way we can
// draw front-to-back and use then stencil buffer to cull opaque pixels early.
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
gl.enable(gl.STENCIL_TEST);
gl.enable(gl.DEPTH_TEST);
gl.depthFunc(gl.LEQUAL);
this._depthMask = false;
gl.depthMask(false);
// Initialize shaders
this.debugShader = gl.initializeShader('debug',
['a_pos'],
['u_matrix', 'u_color']);
this.rasterShader = gl.initializeShader('raster',
['a_pos', 'a_texture_pos'],
['u_matrix', 'u_brightness_low', 'u_brightness_high', 'u_saturation_factor', 'u_spin_weights', 'u_contrast_factor', 'u_opacity0', 'u_opacity1', 'u_image0', 'u_image1', 'u_tl_parent', 'u_scale_parent', 'u_buffer_scale']);
this.circleShader = gl.initializeShader('circle',
['a_pos'],
['u_matrix', 'u_exmatrix', 'u_blur', 'u_size', 'u_color']);
this.lineShader = gl.initializeShader('line',
['a_pos', 'a_data'],
['u_matrix', 'u_linewidth', 'u_color', 'u_ratio', 'u_blur', 'u_extra', 'u_antialiasingmatrix', 'u_offset', 'u_exmatrix']);
this.linepatternShader = gl.initializeShader('linepattern',
['a_pos', 'a_data'],
['u_matrix', 'u_linewidth', 'u_ratio', 'u_pattern_size_a', 'u_pattern_size_b', 'u_pattern_tl_a', 'u_pattern_br_a', 'u_pattern_tl_b', 'u_pattern_br_b', 'u_blur', 'u_fade', 'u_opacity', 'u_extra', 'u_antialiasingmatrix', 'u_offset']);
this.linesdfpatternShader = gl.initializeShader('linesdfpattern',
['a_pos', 'a_data'],
['u_matrix', 'u_linewidth', 'u_color', 'u_ratio', 'u_blur', 'u_patternscale_a', 'u_tex_y_a', 'u_patternscale_b', 'u_tex_y_b', 'u_image', 'u_sdfgamma', 'u_mix', 'u_extra', 'u_antialiasingmatrix', 'u_offset']);
this.sdfShader = gl.initializeShader('sdf',
['a_pos', 'a_offset', 'a_data1', 'a_data2'],
['u_matrix', 'u_exmatrix', 'u_texture', 'u_texsize', 'u_color', 'u_gamma', 'u_buffer', 'u_zoom', 'u_fadedist', 'u_minfadezoom', 'u_maxfadezoom', 'u_fadezoom', 'u_skewed', 'u_extra']);
this.iconShader = gl.initializeShader('icon',
['a_pos', 'a_offset', 'a_data1', 'a_data2'],
['u_matrix', 'u_exmatrix', 'u_texture', 'u_texsize', 'u_zoom', 'u_fadedist', 'u_minfadezoom', 'u_maxfadezoom', 'u_fadezoom', 'u_opacity', 'u_skewed', 'u_extra']);
this.outlineShader = gl.initializeShader('outline',
['a_pos'],
['u_matrix', 'u_color', 'u_world']
);
this.patternShader = gl.initializeShader('pattern',
['a_pos'],
['u_matrix', 'u_pattern_tl_a', 'u_pattern_br_a', 'u_pattern_tl_b', 'u_pattern_br_b', 'u_mix', 'u_patternscale_a', 'u_patternscale_b', 'u_opacity', 'u_image', 'u_offset_a', 'u_offset_b']
);
this.fillShader = gl.initializeShader('fill',
['a_pos'],
['u_matrix', 'u_color']
);
this.collisionBoxShader = gl.initializeShader('collisionbox',
['a_pos', 'a_extrude', 'a_data'],
['u_matrix', 'u_scale', 'u_zoom', 'u_maxzoom']
);
this.identityMatrix = mat4.create();
// The backgroundBuffer is used when drawing to the full *canvas*
this.backgroundBuffer = gl.createBuffer();
this.backgroundBuffer.itemSize = 2;
this.backgroundBuffer.itemCount = 4;
gl.bindBuffer(gl.ARRAY_BUFFER, this.backgroundBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Int16Array([-1, -1, 1, -1, -1, 1, 1, 1]), gl.STATIC_DRAW);
// The tileExtentBuffer is used when drawing to a full *tile*
this.tileExtentBuffer = gl.createBuffer();
this.tileExtentBuffer.itemSize = 4;
this.tileExtentBuffer.itemCount = 4;
gl.bindBuffer(gl.ARRAY_BUFFER, this.tileExtentBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Int16Array([
// tile coord x, tile coord y, texture coord x, texture coord y
0, 0, 0, 0,
EXTENT, 0, 32767, 0,
0, EXTENT, 0, 32767,
EXTENT, EXTENT, 32767, 32767
]),
gl.STATIC_DRAW);
// The debugBuffer is used to draw tile outlines for debugging
this.debugBuffer = gl.createBuffer();
this.debugBuffer.itemSize = 2;
this.debugBuffer.itemCount = 5;
gl.bindBuffer(gl.ARRAY_BUFFER, this.debugBuffer);
gl.bufferData(
gl.ARRAY_BUFFER,
new Int16Array([
0, 0, EXTENT, 0, EXTENT, EXTENT, 0, EXTENT, 0, 0]),
gl.STATIC_DRAW);
// The debugTextBuffer is used to draw tile IDs for debugging
this.debugTextBuffer = gl.createBuffer();
this.debugTextBuffer.itemSize = 2;
};
/*
* Reset the color buffers of the drawing canvas.
*/
Painter.prototype.clearColor = function() {
var gl = this.gl;
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
};
/*
* 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.
*/
Painter.prototype.clearStencil = function() {
var gl = this.gl;
gl.clearStencil(0x0);
gl.stencilMask(0xFF);
gl.clear(gl.STENCIL_BUFFER_BIT);
};
Painter.prototype.clearDepth = function() {
var gl = this.gl;
gl.clearDepth(1);
this.depthMask(true);
gl.clear(gl.DEPTH_BUFFER_BIT);
};
Painter.prototype._renderTileClippingMasks = function(coords, sourceMaxZoom) {
var gl = this.gl;
gl.colorMask(false, false, false, false);
this.depthMask(false);
gl.disable(gl.DEPTH_TEST);
gl.enable(gl.STENCIL_TEST);
// Only write clipping IDs to the last 5 bits. The first three are used for drawing fills.
gl.stencilMask(0xF8);
// Tests will always pass, and ref value will be written to stencil buffer.
gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);
var idNext = 1;
this._tileClippingMaskIDs = {};
for (var i = 0; i < coords.length; i++) {
var coord = coords[i];
var id = this._tileClippingMaskIDs[coord.id] = (idNext++) << 3;
gl.stencilFunc(gl.ALWAYS, id, 0xF8);
gl.switchShader(this.fillShader, this.calculatePosMatrix(coord, sourceMaxZoom));
// Draw the clipping mask
gl.bindBuffer(gl.ARRAY_BUFFER, this.tileExtentBuffer);
gl.vertexAttribPointer(this.fillShader.a_pos, this.tileExtentBuffer.itemSize, gl.SHORT, false, 8, 0);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, this.tileExtentBuffer.itemCount);
}
gl.stencilMask(0x00);
gl.colorMask(true, true, true, true);
this.depthMask(true);
gl.enable(gl.DEPTH_TEST);
};
Painter.prototype.enableTileClippingMask = function(coord) {
var gl = this.gl;
gl.stencilFunc(gl.EQUAL, this._tileClippingMaskIDs[coord.id], 0xF8);
};
// Overridden by headless tests.
Painter.prototype.prepareBuffers = function() {};
Painter.prototype.bindDefaultFramebuffer = function() {
var gl = this.gl;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
};
var draw = {
symbol: require('./draw_symbol'),
circle: require('./draw_circle'),
line: require('./draw_line'),
fill: require('./draw_fill'),
raster: require('./draw_raster'),
background: require('./draw_background'),
debug: require('./draw_debug')
};
Painter.prototype.render = function(style, options) {
this.style = style;
this.options = options;
this.lineAtlas = style.lineAtlas;
this.spriteAtlas = style.spriteAtlas;
this.spriteAtlas.setSprite(style.sprite);
this.glyphSource = style.glyphSource;
this.frameHistory.record(this.transform.zoom);
this.prepareBuffers();
this.clearColor();
this.clearDepth();
this.depthRange = (style._order.length + 2) * this.numSublayers * this.depthEpsilon;
this.renderPass({isOpaquePass: true});
this.renderPass({isOpaquePass: false});
};
Painter.prototype.renderPass = function(options) {
var groups = this.style._groups;
var isOpaquePass = options.isOpaquePass;
this.currentLayer = isOpaquePass ? this.style._order.length : -1;
for (var i = 0; i < groups.length; i++) {
var group = groups[isOpaquePass ? groups.length - 1 - i : i];
var source = this.style.sources[group.source];
var coords = [];
if (source) {
coords = source.getVisibleCoordinates();
this.clearStencil();
if (source.prepare) source.prepare();
if (source.isTileClipped) {
this._renderTileClippingMasks(coords, source.maxzoom);
}
}
if (isOpaquePass) {
this.gl.disable(this.gl.BLEND);
this.isOpaquePass = true;
} else {
this.gl.enable(this.gl.BLEND);
this.isOpaquePass = false;
coords.reverse();
}
for (var j = 0; j < group.length; j++) {
var layer = group[isOpaquePass ? group.length - 1 - j : j];
this.currentLayer += isOpaquePass ? -1 : 1;
this.renderLayer(this, source, layer, coords);
}
if (source) {
draw.debug(this, source, coords);
}
}
};
Painter.prototype.depthMask = function(mask) {
if (mask !== this._depthMask) {
this._depthMask = mask;
this.gl.depthMask(mask);
}
};
Painter.prototype.renderLayer = function(painter, source, layer, coords) {
if (layer.isHidden(this.transform.zoom)) return;
if (layer.type !== 'background' && !coords.length) return;
draw[layer.type](painter, source, layer, coords);
};
// Draws non-opaque areas. This is for debugging purposes.
Painter.prototype.drawStencilBuffer = function() {
var gl = this.gl;
gl.switchShader(this.fillShader, this.identityMatrix);
gl.stencilMask(0x00);
gl.stencilFunc(gl.EQUAL, 0x80, 0x80);
// Drw the filling quad where the stencil buffer isn't set.
gl.bindBuffer(gl.ARRAY_BUFFER, this.backgroundBuffer);
gl.vertexAttribPointer(this.fillShader.a_pos, this.backgroundBuffer.itemSize, gl.SHORT, false, 0, 0);
gl.uniform4fv(this.fillShader.u_color, [0, 0, 0, 0.5]);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, this.tileExtentBuffer.itemCount);
};
Painter.prototype.setDepthSublayer = function(n) {
var farDepth = 1 - ((1 + this.currentLayer) * this.numSublayers + n) * this.depthEpsilon;
var nearDepth = farDepth - 1 + this.depthRange;
this.gl.depthRange(nearDepth, farDepth);
};
Painter.prototype.translatePosMatrix = function(matrix, tile, translate, anchor) {
if (!translate[0] && !translate[1]) return matrix;
if (anchor === 'viewport') {
var sinA = Math.sin(-this.transform.angle);
var cosA = Math.cos(-this.transform.angle);
translate = [
translate[0] * cosA - translate[1] * sinA,
translate[0] * sinA + translate[1] * cosA
];
}
var translation = [
tile.pixelsToTileUnits(translate[0], this.transform.zoom),
tile.pixelsToTileUnits(translate[1], this.transform.zoom),
0
];
var translatedMatrix = new Float32Array(16);
mat4.translate(translatedMatrix, matrix, translation);
return translatedMatrix;
};
/**
* Calculate the posMatrix that this tile uses to display itself in a map,
* given a coordinate as (z, x, y) and a transform
* @param {Object} transform
* @private
*/
Painter.prototype.calculatePosMatrix = function(coord, maxZoom) {
if (coord instanceof TileCoord) {
coord = coord.toCoordinate();
}
var transform = this.transform;
if (maxZoom === undefined) maxZoom = Infinity;
// Initialize model-view matrix that converts from the tile coordinates
// to screen coordinates.
// if z > maxzoom then the tile is actually a overscaled maxzoom tile,
// so calculate the matrix the maxzoom tile would use.
var z = Math.min(coord.zoom, maxZoom);
var x = coord.column;
var y = coord.row;
var scale = transform.worldSize / Math.pow(2, z);
// The position matrix
var posMatrix = new Float64Array(16);
mat4.identity(posMatrix);
mat4.translate(posMatrix, posMatrix, [x * scale, y * scale, 0]);
mat4.scale(posMatrix, posMatrix, [ scale / EXTENT, scale / EXTENT, 1 ]);
mat4.multiply(posMatrix, transform.projMatrix, posMatrix);
return new Float32Array(posMatrix);
};
Painter.prototype.saveTexture = function(texture) {
var textures = this.reusableTextures[texture.size];
if (!textures) {
this.reusableTextures[texture.size] = [texture];
} else {
textures.push(texture);
}
};
Painter.prototype.getTexture = function(size) {
var textures = this.reusableTextures[size];
return textures && textures.length > 0 ? textures.pop() : null;
};