diff --git a/js/data/symbol_bucket.js b/js/data/symbol_bucket.js index 70954647980..e8d49dc8552 100644 --- a/js/data/symbol_bucket.js +++ b/js/data/symbol_bucket.js @@ -333,7 +333,7 @@ SymbolBucket.prototype.placeFeatures = function(collisionTile, buffers, collisio this.symbolInstances.sort(function(a, b) { var aRotated = sin * a.x + cos * a.y; var bRotated = sin * b.x + cos * b.y; - return bRotated - aRotated; + return aRotated - bRotated; }); } diff --git a/js/render/draw_background.js b/js/render/draw_background.js index 3dca5578506..d5cc4bc89d4 100644 --- a/js/render/draw_background.js +++ b/js/render/draw_background.js @@ -1,11 +1,13 @@ 'use strict'; -var mat3 = require('gl-matrix').mat3; +var TilePyramid = require('../source/tile_pyramid'); +var pyramid = new TilePyramid({ tileSize: 512 }); module.exports = drawBackground; -function drawBackground(painter, layer, posMatrix) { +function drawBackground(painter, source, layer) { var gl = painter.gl; + var transform = painter.transform; var color = layer.paint['background-color']; var image = layer.paint['background-pattern']; var opacity = layer.paint['background-opacity']; @@ -14,10 +16,14 @@ function drawBackground(painter, layer, posMatrix) { var imagePosA = image ? painter.spriteAtlas.getPosition(image.from, true) : null; var imagePosB = image ? painter.spriteAtlas.getPosition(image.to, true) : null; + painter.setDepthSublayer(0); if (imagePosA && imagePosB) { + + if (painter.isOpaquePass) return; + // Draw texture fill shader = painter.patternShader; - gl.switchShader(shader, posMatrix); + gl.switchShader(shader); gl.uniform1i(shader.u_image, 0); gl.uniform2fv(shader.u_pattern_tl_a, imagePosA.tl); gl.uniform2fv(shader.u_pattern_br_a, imagePosA.br); @@ -25,62 +31,48 @@ function drawBackground(painter, layer, posMatrix) { gl.uniform2fv(shader.u_pattern_br_b, imagePosB.br); gl.uniform1f(shader.u_opacity, opacity); - var transform = painter.transform; - var sizeA = imagePosA.size; - var sizeB = imagePosB.size; - var center = transform.locationCoordinate(transform.center); - var scale = 1 / Math.pow(2, transform.zoomFraction); - gl.uniform1f(shader.u_mix, image.t); - var matrixA = mat3.create(); - mat3.scale(matrixA, matrixA, [ - 1 / (sizeA[0] * image.fromScale), - 1 / (sizeA[1] * image.fromScale) - ]); - mat3.translate(matrixA, matrixA, [ - (center.column * transform.tileSize) % (sizeA[0] * image.fromScale), - (center.row * transform.tileSize) % (sizeA[1] * image.fromScale) - ]); - mat3.rotate(matrixA, matrixA, -transform.angle); - mat3.scale(matrixA, matrixA, [ - scale * transform.width / 2, - -scale * transform.height / 2 - ]); + var factor = (4096 / transform.tileSize) / Math.pow(2, 0); - var matrixB = mat3.create(); - mat3.scale(matrixB, matrixB, [ - 1 / (sizeB[0] * image.toScale), - 1 / (sizeB[1] * image.toScale) - ]); - mat3.translate(matrixB, matrixB, [ - (center.column * transform.tileSize) % (sizeB[0] * image.toScale), - (center.row * transform.tileSize) % (sizeB[1] * image.toScale) - ]); - mat3.rotate(matrixB, matrixB, -transform.angle); - mat3.scale(matrixB, matrixB, [ - scale * transform.width / 2, - -scale * transform.height / 2 + gl.uniform2fv(shader.u_patternscale_a, [ + 1 / (imagePosA.size[0] * factor * image.fromScale), + 1 / (imagePosA.size[1] * factor * image.fromScale) ]); - gl.uniformMatrix3fv(shader.u_patternmatrix_a, false, matrixA); - gl.uniformMatrix3fv(shader.u_patternmatrix_b, false, matrixB); + gl.uniform2fv(shader.u_patternscale_b, [ + 1 / (imagePosB.size[0] * factor * image.toScale), + 1 / (imagePosB.size[1] * factor * image.toScale) + ]); painter.spriteAtlas.bind(gl, true); } else { // Draw filling rectangle. + if (painter.isOpaquePass !== (color[3] === 1)) return; + shader = painter.fillShader; - gl.switchShader(shader, posMatrix); + gl.switchShader(shader); gl.uniform4fv(shader.u_color, color); } gl.disable(gl.STENCIL_TEST); - gl.bindBuffer(gl.ARRAY_BUFFER, painter.backgroundBuffer); - gl.vertexAttribPointer(shader.a_pos, painter.backgroundBuffer.itemSize, gl.SHORT, false, 0, 0); - gl.drawArrays(gl.TRIANGLE_STRIP, 0, painter.backgroundBuffer.itemCount); - gl.enable(gl.STENCIL_TEST); + gl.bindBuffer(gl.ARRAY_BUFFER, painter.tileExtentBuffer); + gl.vertexAttribPointer(shader.a_pos, painter.tileExtentBuffer.itemSize, gl.SHORT, false, 0, 0); + + // We need to draw the background in tiles in order to use calculatePosMatrix + // which applies the projection matrix (transform.projMatrix). Otherwise + // the depth and stencil buffers get into a bad state. + // This can be refactored into a single draw call once earcut lands and + // we don't have so much going on in the stencil buffer. + var coords = pyramid.coveringTiles(transform); + for (var c = 0; c < coords.length; c++) { + gl.setPosMatrix(painter.calculatePosMatrix(coords[c])); + gl.drawArrays(gl.TRIANGLE_STRIP, 0, painter.tileExtentBuffer.itemCount); + } + + gl.enable(gl.STENCIL_TEST); gl.stencilMask(0x00); gl.stencilFunc(gl.EQUAL, 0x80, 0x80); } diff --git a/js/render/draw_circle.js b/js/render/draw_circle.js index 02ba098d686..bf6f1ff63cc 100644 --- a/js/render/draw_circle.js +++ b/js/render/draw_circle.js @@ -4,27 +4,21 @@ var browser = require('../util/browser.js'); module.exports = drawCircles; -function drawCircles(painter, layer, posMatrix, tile) { - // short-circuit if tile is empty - if (!tile.buffers) return; +function drawCircles(painter, source, layer, coords) { + if (painter.isOpaquePass) return; - posMatrix = painter.translateMatrix(posMatrix, tile, layer.paint['circle-translate'], layer.paint['circle-translate-anchor']); + var gl = painter.gl; - if (!tile.elementGroups[layer.ref || layer.id]) return; - var elementGroups = tile.elementGroups[layer.ref || layer.id].circle; + var shader = painter.circleShader; + painter.gl.switchShader(shader); - var gl = painter.gl; + painter.setDepthSublayer(0); + painter.depthMask(false); // Allow circles to be drawn across boundaries, so that // large circles are not clipped to tiles gl.disable(gl.STENCIL_TEST); - gl.switchShader(painter.circleShader, posMatrix, tile.exMatrix); - - var vertex = tile.buffers.circleVertex; - var shader = painter.circleShader; - var elements = tile.buffers.circleElement; - // antialiasing factor: this is a minimum blur distance that serves as // a faux-antialiasing for the circle. since blur is a ratio of the circle's // size and the intent is to keep the blur at roughly 1px, the two @@ -35,18 +29,38 @@ function drawCircles(painter, layer, posMatrix, tile) { gl.uniform1f(shader.u_blur, Math.max(layer.paint['circle-blur'], antialias)); gl.uniform1f(shader.u_size, layer.paint['circle-radius']); - for (var k = 0; k < elementGroups.groups.length; k++) { - var group = elementGroups.groups[k]; - var offset = group.vertexStartIndex * vertex.itemSize; + for (var i = 0; i < coords.length; i++) { + var coord = coords[i]; + + var tile = source.getTile(coord); + if (!tile.buffers) continue; + if (!tile.elementGroups[layer.ref || layer.id].circle) continue; + + var elementGroups = tile.elementGroups[layer.ref || layer.id].circle; + var vertex = tile.buffers.circleVertex; + var elements = tile.buffers.circleElement; + + gl.setPosMatrix(painter.translatePosMatrix( + painter.calculatePosMatrix(coord, source.maxzoom), + tile, + layer.paint['circle-translate'], + layer.paint['circle-translate-anchor'] + )); + gl.setExMatrix(painter.transform.exMatrix); + + for (var k = 0; k < elementGroups.groups.length; k++) { + var group = elementGroups.groups[k]; + var offset = group.vertexStartIndex * vertex.itemSize; - vertex.bind(gl); - vertex.setAttribPointers(gl, shader, offset); + vertex.bind(gl); + vertex.setAttribPointers(gl, shader, offset); - elements.bind(gl); + elements.bind(gl); - var count = group.elementLength * 3; - var elementOffset = group.elementStartIndex * elements.itemSize; - gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset); + var count = group.elementLength * 3; + var elementOffset = group.elementStartIndex * elements.itemSize; + gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset); + } } gl.enable(gl.STENCIL_TEST); diff --git a/js/render/draw_collision_debug.js b/js/render/draw_collision_debug.js index 01f3b34edbd..986956fe492 100644 --- a/js/render/draw_collision_debug.js +++ b/js/render/draw_collision_debug.js @@ -1,19 +1,23 @@ 'use strict'; -module.exports = drawPlacementDebug; - -function drawPlacementDebug(painter, layer, posMatrix, tile) { +module.exports = drawCollisionDebug; +function drawCollisionDebug(painter, layer, coord, tile) { + if (!tile.elementGroups[layer.ref || layer.id]) return; var elementGroups = tile.elementGroups[layer.ref || layer.id].collisionBox; if (!elementGroups) return; + if (!tile.buffers) return; var gl = painter.gl; var buffer = tile.buffers.collisionBoxVertex; var shader = painter.collisionBoxShader; + var posMatrix = painter.calculatePosMatrix(coord); gl.enable(gl.STENCIL_TEST); + painter.enableTileClippingMask(coord); gl.switchShader(shader, posMatrix); + buffer.bind(gl); buffer.setAttribPointers(gl, shader, 0); diff --git a/js/render/draw_debug.js b/js/render/draw_debug.js index 8d1d6155ab3..9544a8fe405 100644 --- a/js/render/draw_debug.js +++ b/js/render/draw_debug.js @@ -5,33 +5,37 @@ var browser = require('../util/browser'); module.exports = drawDebug; -function drawDebug(painter, tile) { - var gl = painter.gl; +function drawDebug(painter, coords) { + if (painter.isOpaquePass) return; + if (!painter.options.debug) return; - // Blend to the front, not the back. - gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); + for (var i = 0; i < coords.length; i++) { + drawDebugTile(painter, coords[i]); + } +} - gl.switchShader(painter.debugShader, tile.posMatrix); +function drawDebugTile(painter, coord) { + var gl = painter.gl; + + var shader = painter.debugShader; + gl.switchShader(shader, painter.calculatePosMatrix(coord)); // draw bounding rectangle gl.bindBuffer(gl.ARRAY_BUFFER, painter.debugBuffer); - gl.vertexAttribPointer(painter.debugShader.a_pos, painter.debugBuffer.itemSize, gl.SHORT, false, 0, 0); - gl.uniform4f(painter.debugShader.u_color, 1, 0, 0, 1); + gl.vertexAttribPointer(shader.a_pos, painter.debugBuffer.itemSize, gl.SHORT, false, 0, 0); + gl.uniform4f(shader.u_color, 1, 0, 0, 1); gl.lineWidth(4); gl.drawArrays(gl.LINE_STRIP, 0, painter.debugBuffer.itemCount); - var vertices = textVertices(tile.coord.toString(), 50, 200, 5); + var vertices = textVertices(coord.toString(), 50, 200, 5); gl.bindBuffer(gl.ARRAY_BUFFER, painter.debugTextBuffer); gl.bufferData(gl.ARRAY_BUFFER, new Int16Array(vertices), gl.STREAM_DRAW); - gl.vertexAttribPointer(painter.debugShader.a_pos, painter.debugTextBuffer.itemSize, gl.SHORT, false, 0, 0); + gl.vertexAttribPointer(shader.a_pos, painter.debugTextBuffer.itemSize, gl.SHORT, false, 0, 0); gl.lineWidth(8 * browser.devicePixelRatio); - gl.uniform4f(painter.debugShader.u_color, 1, 1, 1, 1); + gl.uniform4f(shader.u_color, 1, 1, 1, 1); gl.drawArrays(gl.LINES, 0, vertices.length / painter.debugTextBuffer.itemSize); gl.lineWidth(2 * browser.devicePixelRatio); - gl.uniform4f(painter.debugShader.u_color, 0, 0, 0, 1); + gl.uniform4f(shader.u_color, 0, 0, 0, 1); gl.drawArrays(gl.LINES, 0, vertices.length / painter.debugTextBuffer.itemSize); - - // Revert blending mode to blend to the back. - gl.blendFunc(gl.ONE_MINUS_DST_ALPHA, gl.ONE); } diff --git a/js/render/draw_fill.js b/js/render/draw_fill.js index 92fc1748ed1..ac3b6d56c2e 100644 --- a/js/render/draw_fill.js +++ b/js/render/draw_fill.js @@ -1,119 +1,122 @@ 'use strict'; var browser = require('../util/browser'); -var mat3 = require('gl-matrix').mat3; -module.exports = drawFill; +module.exports = draw; -function drawFill(painter, layer, posMatrix, tile) { - // No data +function draw(painter, source, layer, coords) { + var gl = painter.gl; + + var color = layer.paint['fill-color']; + var image = layer.paint['fill-pattern']; + var strokeColor = layer.paint['fill-outline-color']; + + // Draw fill + if (image ? !painter.isOpaquePass : painter.isOpaquePass === (color[3] === 1)) { + // Once we switch to earcut drawing we can pull most of the WebGL setup + // outside of this coords loop. + for (var i = 0; i < coords.length; i++) { + drawFill(painter, source, layer, coords[i]); + } + } + + // Draw stroke + if (!painter.isOpaquePass && layer.paint['fill-antialias'] && !(layer.paint['fill-pattern'] && !strokeColor)) { + gl.switchShader(painter.outlineShader); + gl.lineWidth(2 * browser.devicePixelRatio * 10); + + if (strokeColor) { + // If we defined a different color for the fill outline, we are + // going to ignore the bits in 0x07 and just care about the global + // clipping mask. + painter.setDepthSublayer(2); + + } else { + // Otherwise, we only want to drawFill the antialiased parts that are + // *outside* the current shape. This is important in case the fill + // or stroke color is translucent. If we wouldn't clip to outside + // the current shape, some pixels from the outline stroke overlapped + // the (non-antialiased) fill. + painter.setDepthSublayer(0); + } + + gl.uniform2f(painter.outlineShader.u_world, gl.drawingBufferWidth, gl.drawingBufferHeight); + gl.uniform4fv(painter.outlineShader.u_color, strokeColor ? strokeColor : color); + + for (var j = 0; j < coords.length; j++) { + drawStroke(painter, source, layer, coords[j]); + } + } +} + +function drawFill(painter, source, layer, coord) { + var tile = source.getTile(coord); if (!tile.buffers) return; if (!tile.elementGroups[layer.ref || layer.id]) return; var elementGroups = tile.elementGroups[layer.ref || layer.id].fill; var gl = painter.gl; - var translatedPosMatrix = painter.translateMatrix(posMatrix, tile, layer.paint['fill-translate'], layer.paint['fill-translate-anchor']); var color = layer.paint['fill-color']; + var image = layer.paint['fill-pattern']; + var opacity = layer.paint['fill-opacity']; - var vertex, elements, group, count; + var posMatrix = painter.calculatePosMatrix(coord, source.maxzoom); + var translatedPosMatrix = painter.translatePosMatrix(posMatrix, tile, layer.paint['fill-translate'], layer.paint['fill-translate-anchor']); // Draw the stencil mask. + painter.setDepthSublayer(1); - // We're only drawing to the first seven bits (== support a maximum of - // 127 overlapping polygons in one place before we get rendering errors). - gl.stencilMask(0x3F); + // We're only drawFilling to the first seven bits (== support a maximum of + // 8 overlapping polygons in one place before we get rendering errors). + gl.stencilMask(0x07); gl.clear(gl.STENCIL_BUFFER_BIT); // Draw front facing triangles. Wherever the 0x80 bit is 1, we are // increasing the lower 7 bits by one if the triangle is a front-facing // triangle. This means that all visible polygons should be in CCW // orientation, while all holes (see below) are in CW orientation. - gl.stencilFunc(gl.NOTEQUAL, 0x80, 0x80); + painter.enableTileClippingMask(coord); // When we do a nonzero fill, we count the number of times a pixel is // covered by a counterclockwise polygon, and subtract the number of // times it is "uncovered" by a clockwise polygon. - gl.stencilOpSeparate(gl.FRONT, gl.INCR_WRAP, gl.KEEP, gl.KEEP); - gl.stencilOpSeparate(gl.BACK, gl.DECR_WRAP, gl.KEEP, gl.KEEP); + gl.stencilOpSeparate(gl.FRONT, gl.KEEP, gl.KEEP, gl.INCR_WRAP); + gl.stencilOpSeparate(gl.BACK, gl.KEEP, gl.KEEP, gl.DECR_WRAP); - // When drawing a shape, we first draw all shapes to the stencil buffer + // When drawFilling a shape, we first drawFill all shapes to the stencil buffer // and incrementing all areas where polygons are gl.colorMask(false, false, false, false); + painter.depthMask(false); // Draw the actual triangle fan into the stencil buffer. gl.switchShader(painter.fillShader, translatedPosMatrix); // Draw all buffers - vertex = tile.buffers.fillVertex; + var vertex = tile.buffers.fillVertex; vertex.bind(gl); - elements = tile.buffers.fillElement; + var elements = tile.buffers.fillElement; elements.bind(gl); - var offset, elementOffset; - for (var i = 0; i < elementGroups.groups.length; i++) { - group = elementGroups.groups[i]; - offset = group.vertexStartIndex * vertex.itemSize; + var group = elementGroups.groups[i]; + var offset = group.vertexStartIndex * vertex.itemSize; vertex.setAttribPointers(gl, painter.fillShader, offset); - count = group.elementLength * 3; - elementOffset = group.elementStartIndex * elements.itemSize; + var count = group.elementLength * 3; + var elementOffset = group.elementStartIndex * elements.itemSize; gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset); } // Now that we have the stencil mask in the stencil buffer, we can start // writing to the color buffer. gl.colorMask(true, true, true, true); + painter.depthMask(true); // From now on, we don't want to update the stencil buffer anymore. gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP); gl.stencilMask(0x0); - - var strokeColor = layer.paint['fill-outline-color']; - - // Because we're drawing top-to-bottom, and we update the stencil mask - // below, we have to draw the outline first (!) - if (layer.paint['fill-antialias'] === true && !(layer.paint['fill-pattern'] && !strokeColor)) { - gl.switchShader(painter.outlineShader, translatedPosMatrix); - gl.lineWidth(2 * browser.devicePixelRatio); - - if (strokeColor) { - // If we defined a different color for the fill outline, we are - // going to ignore the bits in 0x3F and just care about the global - // clipping mask. - gl.stencilFunc(gl.EQUAL, 0x80, 0x80); - } else { - // Otherwise, we only want to draw the antialiased parts that are - // *outside* the current shape. This is important in case the fill - // or stroke color is translucent. If we wouldn't clip to outside - // the current shape, some pixels from the outline stroke overlapped - // the (non-antialiased) fill. - gl.stencilFunc(gl.EQUAL, 0x80, 0xBF); - } - - gl.uniform2f(painter.outlineShader.u_world, gl.drawingBufferWidth, gl.drawingBufferHeight); - gl.uniform4fv(painter.outlineShader.u_color, strokeColor ? strokeColor : color); - - // Draw all buffers - vertex = tile.buffers.fillVertex; - elements = tile.buffers.fillSecondElement; - elements.bind(gl); - - for (var k = 0; k < elementGroups.groups.length; k++) { - group = elementGroups.groups[k]; - offset = group.vertexStartIndex * vertex.itemSize; - vertex.setAttribPointers(gl, painter.outlineShader, offset); - - count = group.secondElementLength * 2; - elementOffset = group.secondElementStartIndex * elements.itemSize; - gl.drawElements(gl.LINES, count, gl.UNSIGNED_SHORT, elementOffset); - } - } - - var image = layer.paint['fill-pattern']; - var opacity = layer.paint['fill-opacity'] || 1; var shader; if (image) { @@ -132,23 +135,18 @@ function drawFill(painter, layer, posMatrix, tile) { gl.uniform1f(shader.u_opacity, opacity); gl.uniform1f(shader.u_mix, image.t); - var factor = (tile.tileExtent / tile.tileSize) / Math.pow(2, painter.transform.tileZoom - tile.coord.z); + var factor = (4096 / tile.tileSize) / Math.pow(2, painter.transform.tileZoom - tile.coord.z); - var matrixA = mat3.create(); - mat3.scale(matrixA, matrixA, [ + gl.uniform2fv(shader.u_patternscale_a, [ 1 / (imagePosA.size[0] * factor * image.fromScale), 1 / (imagePosA.size[1] * factor * image.fromScale) ]); - var matrixB = mat3.create(); - mat3.scale(matrixB, matrixB, [ + gl.uniform2fv(shader.u_patternscale_b, [ 1 / (imagePosB.size[0] * factor * image.toScale), 1 / (imagePosB.size[1] * factor * image.toScale) ]); - gl.uniformMatrix3fv(shader.u_patternmatrix_a, false, matrixA); - gl.uniformMatrix3fv(shader.u_patternmatrix_b, false, matrixB); - painter.spriteAtlas.bind(gl, true); } else { @@ -159,11 +157,44 @@ function drawFill(painter, layer, posMatrix, tile) { } // Only draw regions that we marked - gl.stencilFunc(gl.NOTEQUAL, 0x0, 0x3F); + gl.stencilFunc(gl.NOTEQUAL, 0x0, 0x07); gl.bindBuffer(gl.ARRAY_BUFFER, painter.tileExtentBuffer); gl.vertexAttribPointer(shader.a_pos, painter.tileExtentBuffer.itemSize, gl.SHORT, false, 0, 0); gl.drawArrays(gl.TRIANGLE_STRIP, 0, painter.tileExtentBuffer.itemCount); gl.stencilMask(0x00); - gl.stencilFunc(gl.EQUAL, 0x80, 0x80); +} + +function drawStroke(painter, source, layer, coord) { + var tile = source.getTile(coord); + if (!tile.buffers) return; + if (!tile.elementGroups[layer.ref || layer.id]) return; + + var gl = painter.gl; + var elementGroups = tile.elementGroups[layer.ref || layer.id].fill; + + gl.setPosMatrix(painter.translatePosMatrix( + painter.calculatePosMatrix(coord, source.maxzoom), + tile, + layer.paint['fill-translate'], + layer.paint['fill-translate-anchor'] + )); + + // Draw all buffers + var vertex = tile.buffers.fillVertex; + var elements = tile.buffers.fillSecondElement; + vertex.bind(gl); + elements.bind(gl); + + painter.enableTileClippingMask(coord); + + for (var k = 0; k < elementGroups.groups.length; k++) { + var group = elementGroups.groups[k]; + var offset = group.vertexStartIndex * vertex.itemSize; + vertex.setAttribPointers(gl, painter.outlineShader, offset); + + var count = group.secondElementLength * 2; + var elementOffset = group.secondElementStartIndex * elements.itemSize; + gl.drawElements(gl.LINES, count, gl.UNSIGNED_SHORT, elementOffset); + } } diff --git a/js/render/draw_line.js b/js/render/draw_line.js index f9fc0f36f03..f4e87d9c30f 100644 --- a/js/render/draw_line.js +++ b/js/render/draw_line.js @@ -13,11 +13,19 @@ var mat2 = require('gl-matrix').mat2; * @returns {undefined} draws with the painter * @private */ -module.exports = function drawLine(painter, layer, posMatrix, tile) { - // No data - if (!tile.buffers) return; - if (!tile.elementGroups[layer.ref || layer.id]) return; - var elementGroups = tile.elementGroups[layer.ref || layer.id].line; +module.exports = function drawLine(painter, source, layer, coords) { + if (painter.isOpaquePass) return; + painter.setDepthSublayer(0); + painter.depthMask(false); + + var hasData = false; + for (var j = 0; j < coords.length; j++) { + if (source.getTile(coords[j]).hasLayerData(layer)) { + hasData = true; + break; + } + } + if (!hasData) return; var gl = painter.gl; @@ -43,14 +51,10 @@ module.exports = function drawLine(painter, layer, posMatrix, tile) { } var outset = offset + edgeWidth + antialiasing / 2 + shift; - var color = layer.paint['line-color']; - var ratio = painter.transform.scale / (1 << tile.coord.z) / (tile.tileExtent / tile.tileSize); - var vtxMatrix = painter.translateMatrix(posMatrix, tile, layer.paint['line-translate'], layer.paint['line-translate-anchor']); var tr = painter.transform; - var antialiasingMatrix = mat2.create(); mat2.scale(antialiasingMatrix, antialiasingMatrix, [1, Math.cos(tr._pitch)]); mat2.rotate(antialiasingMatrix, antialiasingMatrix, painter.transform.angle); @@ -61,42 +65,25 @@ module.exports = function drawLine(painter, layer, posMatrix, tile) { var x = tr.height / 2 * Math.tan(tr._pitch); var extra = (topedgelength + x) / topedgelength - 1; - // how much the tile is overscaled by - var overscaling = tile.tileSize / painter.transform.tileSize; - - var shader; - - var dasharray = layer.paint['line-dasharray']; var image = layer.paint['line-pattern']; + var shader, posA, posB, imagePosA, imagePosB; if (dasharray) { - shader = painter.linesdfpatternShader; - gl.switchShader(shader, vtxMatrix, tile.exMatrix); + gl.switchShader(shader); gl.uniform2fv(shader.u_linewidth, [ outset, inset ]); - gl.uniform1f(shader.u_ratio, ratio); gl.uniform1f(shader.u_blur, blur); gl.uniform4fv(shader.u_color, color); - var posA = painter.lineAtlas.getDash(dasharray.from, layer.layout['line-cap'] === 'round'); - var posB = painter.lineAtlas.getDash(dasharray.to, layer.layout['line-cap'] === 'round'); + posA = painter.lineAtlas.getDash(dasharray.from, layer.layout['line-cap'] === 'round'); + posB = painter.lineAtlas.getDash(dasharray.to, layer.layout['line-cap'] === 'round'); painter.lineAtlas.bind(gl); - var patternratio = Math.pow(2, Math.floor(Math.log(painter.transform.scale) / Math.LN2) - tile.coord.z) / 8 * overscaling; - var scaleA = [patternratio / posA.width / dasharray.fromScale, -posA.height / 2]; - var gammaA = painter.lineAtlas.width / (dasharray.fromScale * posA.width * 256 * browser.devicePixelRatio) / 2; - var scaleB = [patternratio / posB.width / dasharray.toScale, -posB.height / 2]; - var gammaB = painter.lineAtlas.width / (dasharray.toScale * posB.width * 256 * browser.devicePixelRatio) / 2; - - gl.uniform2fv(shader.u_patternscale_a, scaleA); gl.uniform1f(shader.u_tex_y_a, posA.y); - gl.uniform2fv(shader.u_patternscale_b, scaleB); gl.uniform1f(shader.u_tex_y_b, posB.y); - gl.uniform1i(shader.u_image, 0); - gl.uniform1f(shader.u_sdfgamma, Math.max(gammaA, gammaB)); gl.uniform1f(shader.u_mix, dasharray.t); gl.uniform1f(shader.u_extra, extra); @@ -104,22 +91,17 @@ module.exports = function drawLine(painter, layer, posMatrix, tile) { gl.uniformMatrix2fv(shader.u_antialiasingmatrix, false, antialiasingMatrix); } else if (image) { - var imagePosA = painter.spriteAtlas.getPosition(image.from, true); - var imagePosB = painter.spriteAtlas.getPosition(image.to, true); + imagePosA = painter.spriteAtlas.getPosition(image.from, true); + imagePosB = painter.spriteAtlas.getPosition(image.to, true); if (!imagePosA || !imagePosB) return; - var factor = tile.tileExtent / tile.tileSize / Math.pow(2, painter.transform.tileZoom - tile.coord.z) * overscaling; painter.spriteAtlas.bind(gl, true); shader = painter.linepatternShader; - gl.switchShader(shader, vtxMatrix, tile.exMatrix); + gl.switchShader(shader); gl.uniform2fv(shader.u_linewidth, [ outset, inset ]); - gl.uniform1f(shader.u_ratio, ratio); gl.uniform1f(shader.u_blur, blur); - - gl.uniform2fv(shader.u_pattern_size_a, [imagePosA.size[0] * factor * image.fromScale, imagePosB.size[1] ]); - gl.uniform2fv(shader.u_pattern_size_b, [imagePosB.size[0] * factor * image.toScale, imagePosB.size[1] ]); gl.uniform2fv(shader.u_pattern_tl_a, imagePosA.tl); gl.uniform2fv(shader.u_pattern_br_a, imagePosA.br); gl.uniform2fv(shader.u_pattern_tl_b, imagePosB.tl); @@ -133,31 +115,71 @@ module.exports = function drawLine(painter, layer, posMatrix, tile) { } else { shader = painter.lineShader; - gl.switchShader(shader, vtxMatrix, tile.exMatrix); + gl.switchShader(shader); gl.uniform2fv(shader.u_linewidth, [ outset, inset ]); - gl.uniform1f(shader.u_ratio, ratio); gl.uniform1f(shader.u_blur, blur); gl.uniform1f(shader.u_extra, extra); gl.uniform1f(shader.u_offset, -layer.paint['line-offset']); gl.uniformMatrix2fv(shader.u_antialiasingmatrix, false, antialiasingMatrix); - gl.uniform4fv(shader.u_color, color); } - var vertex = tile.buffers.lineVertex; - vertex.bind(gl); - var element = tile.buffers.lineElement; - element.bind(gl); - - for (var i = 0; i < elementGroups.groups.length; i++) { - var group = elementGroups.groups[i]; - var vtxOffset = group.vertexStartIndex * vertex.itemSize; - vertex.setAttribPointers(gl, shader, vtxOffset); - - var count = group.elementLength * 3; - var elementOffset = group.elementStartIndex * element.itemSize; - gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset); + for (var k = 0; k < coords.length; k++) { + var coord = coords[k]; + var tile = source.getTile(coord); + + var elementGroups = tile.buffers && tile.elementGroups[layer.ref || layer.id] && tile.elementGroups[layer.ref || layer.id].line; + if (!elementGroups) continue; + + painter.enableTileClippingMask(coord); + + // set uniforms that are different for each tile + var posMatrix = painter.translatePosMatrix(painter.calculatePosMatrix(coord, source.maxzoom), tile, layer.paint['line-translate'], layer.paint['line-translate-anchor']); + + gl.setPosMatrix(posMatrix); + gl.setExMatrix(painter.transform.exMatrix); + var ratio = painter.transform.scale / (1 << coord.z) / (4096 / tile.tileSize); + + // how much the tile is overscaled by + var overscaling = tile.tileSize / painter.transform.tileSize; + + if (dasharray) { + var patternratio = Math.pow(2, Math.floor(Math.log(painter.transform.scale) / Math.LN2) - coord.z) / 8 * overscaling; + var scaleA = [patternratio / posA.width / dasharray.fromScale, -posA.height / 2]; + var gammaA = painter.lineAtlas.width / (dasharray.fromScale * posA.width * 256 * browser.devicePixelRatio) / 2; + var scaleB = [patternratio / posB.width / dasharray.toScale, -posB.height / 2]; + var gammaB = painter.lineAtlas.width / (dasharray.toScale * posB.width * 256 * browser.devicePixelRatio) / 2; + gl.uniform1f(shader.u_ratio, ratio); + gl.uniform2fv(shader.u_patternscale_a, scaleA); + gl.uniform2fv(shader.u_patternscale_b, scaleB); + gl.uniform1f(shader.u_sdfgamma, Math.max(gammaA, gammaB)); + + } else if (image) { + var factor = 4096 / tile.tileSize / Math.pow(2, painter.transform.tileZoom - coord.z) * overscaling; + gl.uniform1f(shader.u_ratio, ratio); + gl.uniform2fv(shader.u_pattern_size_a, [imagePosA.size[0] * factor * image.fromScale, imagePosB.size[1] ]); + gl.uniform2fv(shader.u_pattern_size_b, [imagePosB.size[0] * factor * image.toScale, imagePosB.size[1] ]); + + } else { + gl.uniform1f(shader.u_ratio, ratio); + } + + var vertex = tile.buffers.lineVertex; + vertex.bind(gl); + var element = tile.buffers.lineElement; + element.bind(gl); + + for (var i = 0; i < elementGroups.groups.length; i++) { + var group = elementGroups.groups[i]; + var vtxOffset = group.vertexStartIndex * vertex.itemSize; + gl.vertexAttribPointer(shader.a_pos, 2, gl.SHORT, false, 8, vtxOffset + 0); + gl.vertexAttribPointer(shader.a_data, 4, gl.BYTE, false, 8, vtxOffset + 4); + + var count = group.elementLength * 3; + var elementOffset = group.elementStartIndex * element.itemSize; + gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset); + } } }; diff --git a/js/render/draw_raster.js b/js/render/draw_raster.js index d2f5e9ea32d..a125cba7b7b 100644 --- a/js/render/draw_raster.js +++ b/js/render/draw_raster.js @@ -4,11 +4,24 @@ var util = require('../util/util'); module.exports = drawRaster; -function drawRaster(painter, layer, posMatrix, tile) { +function drawRaster(painter, source, layer, coords) { + for (var i = 0; i < coords.length; i++) { + drawRasterTile(painter, source, layer, coords[i]); + } +} + +function drawRasterTile(painter, source, layer, coord) { + if (painter.isOpaquePass) return; + + painter.setDepthSublayer(0); + var gl = painter.gl; gl.disable(gl.STENCIL_TEST); + var tile = source.getTile(coord); + var posMatrix = painter.calculatePosMatrix(coord, source.maxzoom); + var shader = painter.rasterShader; gl.switchShader(shader, posMatrix); @@ -19,7 +32,7 @@ function drawRaster(painter, layer, posMatrix, tile) { gl.uniform1f(shader.u_contrast_factor, contrastFactor(layer.paint['raster-contrast'])); gl.uniform3fv(shader.u_spin_weights, spinWeights(layer.paint['raster-hue-rotate'])); - var parentTile = tile.source && tile.source._pyramid.findLoadedParent(tile.coord, 0, {}), + var parentTile = tile.source && tile.source._pyramid.findLoadedParent(coord, 0, {}), opacities = getOpacities(tile, parentTile, layer, painter.transform); var parentScaleBy, parentTL; diff --git a/js/render/draw_symbol.js b/js/render/draw_symbol.js index 505a6a9ceb0..4b45a7bd3dc 100644 --- a/js/render/draw_symbol.js +++ b/js/render/draw_symbol.js @@ -1,17 +1,14 @@ 'use strict'; -var browser = require('../util/browser'); var mat4 = require('gl-matrix').mat4; +var browser = require('../util/browser'); var drawCollisionDebug = require('./draw_collision_debug'); module.exports = drawSymbols; -function drawSymbols(painter, layer, posMatrix, tile) { - // No data - if (!tile.buffers) return; - var elementGroups = tile.elementGroups[layer.ref || layer.id]; - if (!elementGroups) return; +function drawSymbols(painter, source, layer, coords) { + if (painter.isOpaquePass) return; var drawAcrossEdges = !(layer.layout['text-allow-overlap'] || layer.layout['icon-allow-overlap'] || layer.layout['text-ignore-placement'] || layer.layout['icon-ignore-placement']); @@ -27,18 +24,48 @@ function drawSymbols(painter, layer, posMatrix, tile) { gl.disable(gl.STENCIL_TEST); } - if (elementGroups.glyph.groups.length) { - drawSymbol(painter, layer, posMatrix, tile, elementGroups.glyph, 'text', true); - } - if (elementGroups.icon.groups.length) { + painter.setDepthSublayer(0); + painter.depthMask(false); + gl.disable(gl.DEPTH_TEST); + + var tile, elementGroups, posMatrix; + + for (var i = 0; i < coords.length; i++) { + tile = source.getTile(coords[i]); + + if (!tile.buffers) continue; + elementGroups = tile.elementGroups[layer.ref || layer.id]; + if (!elementGroups) continue; + if (!elementGroups.icon.groups.length) continue; + + posMatrix = painter.calculatePosMatrix(coords[i], source.maxzoom); + painter.enableTileClippingMask(coords[i]); drawSymbol(painter, layer, posMatrix, tile, elementGroups.icon, 'icon', elementGroups.sdfIcons); } - drawCollisionDebug(painter, layer, posMatrix, tile); + for (var j = 0; j < coords.length; j++) { + tile = source.getTile(coords[j]); + + if (!tile.buffers) continue; + elementGroups = tile.elementGroups[layer.ref || layer.id]; + if (!elementGroups) continue; + if (!elementGroups.glyph.groups.length) continue; + + posMatrix = painter.calculatePosMatrix(coords[j], source.maxzoom); + painter.enableTileClippingMask(coords[j]); + drawSymbol(painter, layer, posMatrix, tile, elementGroups.glyph, 'text', true); + } + + for (var k = 0; k < coords.length; k++) { + tile = source.getTile(coords[k]); + painter.enableTileClippingMask(coords[k]); + drawCollisionDebug(painter, layer, coords[k], tile); + } if (drawAcrossEdges) { gl.enable(gl.STENCIL_TEST); } + gl.enable(gl.DEPTH_TEST); } var defaultSizes = { @@ -49,7 +76,7 @@ var defaultSizes = { function drawSymbol(painter, layer, posMatrix, tile, elementGroups, prefix, sdf) { var gl = painter.gl; - posMatrix = painter.translateMatrix(posMatrix, tile, layer.paint[prefix + '-translate'], layer.paint[prefix + '-translate-anchor']); + posMatrix = painter.translatePosMatrix(posMatrix, tile, layer.paint[prefix + '-translate'], layer.paint[prefix + '-translate-anchor']); var tr = painter.transform; var alignedWithMap = layer.layout[prefix + '-rotation-alignment'] === 'map'; @@ -61,7 +88,7 @@ function drawSymbol(painter, layer, posMatrix, tile, elementGroups, prefix, sdf) s = tile.tileExtent / tile.tileSize / Math.pow(2, painter.transform.zoom - tile.coord.z); gammaScale = 1 / Math.cos(tr._pitch); } else { - exMatrix = mat4.clone(tile.exMatrix); + exMatrix = mat4.clone(painter.transform.exMatrix); s = painter.transform.altitude; gammaScale = 1; } @@ -132,21 +159,6 @@ function drawSymbol(painter, layer, posMatrix, tile, elementGroups, prefix, sdf) var haloOffset = 6; var gamma = 0.105 * defaultSizes[prefix] / fontSize / browser.devicePixelRatio; - gl.uniform1f(shader.u_gamma, gamma * gammaScale); - gl.uniform4fv(shader.u_color, layer.paint[prefix + '-color']); - gl.uniform1f(shader.u_buffer, (256 - 64) / 256); - - for (var i = 0; i < elementGroups.groups.length; i++) { - group = elementGroups.groups[i]; - offset = group.vertexStartIndex * vertex.itemSize; - vertex.bind(gl); - vertex.setAttribPointers(gl, shader, offset); - - count = group.elementLength * 3; - elementOffset = group.elementStartIndex * elements.itemSize; - gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset); - } - if (layer.paint[prefix + '-halo-width']) { // Draw halo underneath the text. gl.uniform1f(shader.u_gamma, (layer.paint[prefix + '-halo-blur'] * blurOffset / fontScale / sdfPx + gamma) * gammaScale); @@ -164,6 +176,22 @@ function drawSymbol(painter, layer, posMatrix, tile, elementGroups, prefix, sdf) gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset); } } + + gl.uniform1f(shader.u_gamma, gamma * gammaScale); + gl.uniform4fv(shader.u_color, layer.paint[prefix + '-color']); + gl.uniform1f(shader.u_buffer, (256 - 64) / 256); + + for (var i = 0; i < elementGroups.groups.length; i++) { + group = elementGroups.groups[i]; + offset = group.vertexStartIndex * vertex.itemSize; + vertex.bind(gl); + vertex.setAttribPointers(gl, shader, offset); + + count = group.elementLength * 3; + elementOffset = group.elementStartIndex * elements.itemSize; + gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset); + } + } else { gl.uniform1f(shader.u_opacity, layer.paint['icon-opacity']); for (var k = 0; k < elementGroups.groups.length; k++) { diff --git a/js/render/draw_vertices.js b/js/render/draw_vertices.js deleted file mode 100644 index 6d3d34164fe..00000000000 --- a/js/render/draw_vertices.js +++ /dev/null @@ -1,51 +0,0 @@ -'use strict'; - -var browser = require('../util/browser'); -var mat4 = require('gl-matrix').mat4; - -module.exports = drawVertices; - -function drawVertices(painter, layer, posMatrix, tile) { - var gl = painter.gl; - - if (!tile || !tile.buffers) return; - var elementGroups = tile.elementGroups[layer.ref || layer.id]; - if (!elementGroups) return; - - // Blend to the front, not the back. - gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); - - // Draw all buffers - if (layer.type === 'fill') { - drawPoints(tile.buffers.fillVertex, elementGroups.groups, posMatrix); - } else if (layer.type === 'symbol') { - drawPoints(tile.buffers.iconVertex, elementGroups.icon.groups, posMatrix); - drawPoints(tile.buffers.glyphVertex, elementGroups.glyph.groups, posMatrix); - } else if (layer.type === 'line') { - var newPosMatrix = mat4.clone(posMatrix); - mat4.scale(newPosMatrix, newPosMatrix, [0.5, 0.5, 1]); - drawPoints(tile.buffers.lineVertex, elementGroups.groups, newPosMatrix); - } - - function drawPoints(vertex, groups, matrix) { - gl.switchShader(painter.dotShader, matrix); - - gl.uniform1f(painter.dotShader.u_size, 4 * browser.devicePixelRatio); - gl.uniform1f(painter.dotShader.u_blur, 0.25); - gl.uniform4fv(painter.dotShader.u_color, [0.1, 0, 0, 0.1]); - - vertex.bind(gl); - for (var i = 0; i < groups.length; i++) { - var group = groups[i]; - var begin = group.vertexStartIndex; - var count = group.vertexLength; - - gl.vertexAttribPointer(painter.dotShader.a_pos, 2, gl.SHORT, false, vertex.itemSize, 0); - - gl.drawArrays(gl.POINTS, begin, count); - } - } - - // Revert blending mode to blend to the back. - gl.blendFunc(gl.ONE_MINUS_DST_ALPHA, gl.ONE); -} diff --git a/js/render/gl_util.js b/js/render/gl_util.js index edc8cf19c71..d30629cd9a6 100644 --- a/js/render/gl_util.js +++ b/js/render/gl_util.js @@ -61,10 +61,6 @@ exports.extend = function(context) { // Switches to a different shader program. context.switchShader = function(shader, posMatrix, exMatrix) { - if (!posMatrix) { - console.trace('posMatrix does not have required argument'); - } - if (this.currentShader !== shader) { this.useProgram(shader.program); @@ -89,20 +85,32 @@ exports.extend = function(context) { this.currentShader = shader; } - // Update the matrices if necessary. Note: This relies on object identity! - // This means changing the matrix values without the actual matrix object - // will FAIL to update the matrix properly. + if (posMatrix !== undefined) context.setPosMatrix(posMatrix); + if (exMatrix !== undefined) context.setExMatrix(exMatrix); + }; + + // Update the matrices if necessary. Note: This relies on object identity! + // This means changing the matrix values without the actual matrix object + // will FAIL to update the matrix properly. + context.setPosMatrix = function(posMatrix) { + var shader = this.currentShader; if (shader.posMatrix !== posMatrix) { this.uniformMatrix4fv(shader.u_matrix, false, posMatrix); shader.posMatrix = posMatrix; } + }; + + // Update the matrices if necessary. Note: This relies on object identity! + // This means changing the matrix values without the actual matrix object + // will FAIL to update the matrix properly. + context.setExMatrix = function(exMatrix) { + var shader = this.currentShader; if (exMatrix && shader.exMatrix !== exMatrix && shader.u_exmatrix) { this.uniformMatrix4fv(shader.u_exmatrix, false, exMatrix); shader.exMatrix = exMatrix; } }; - context.vertexAttrib2fv = function(attribute, values) { context.vertexAttrib2f(attribute, values[0], values[1]); }; diff --git a/js/render/painter.js b/js/render/painter.js index 1cf6072e07a..cd3e46a2a8b 100644 --- a/js/render/painter.js +++ b/js/render/painter.js @@ -4,6 +4,7 @@ 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'); /* * Initialize a new painter object. @@ -21,6 +22,11 @@ function Painter(gl, transform) { 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); } /* @@ -45,10 +51,16 @@ Painter.prototype.setup = function() { // 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_MINUS_DST_ALPHA, gl.ONE); + 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'], @@ -64,7 +76,7 @@ Painter.prototype.setup = function() { 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_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'], @@ -93,7 +105,7 @@ Painter.prototype.setup = function() { 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_patternmatrix_a', 'u_patternmatrix_b', 'u_opacity', 'u_image'] + ['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'] ); this.fillShader = gl.initializeShader('fill', @@ -186,35 +198,49 @@ Painter.prototype.clearStencil = function() { gl.clear(gl.STENCIL_BUFFER_BIT); }; -Painter.prototype.drawClippingMask = function(tile) { +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.switchShader(this.fillShader, tile.posMatrix); gl.colorMask(false, false, false, false); + this.depthMask(false); + gl.disable(gl.DEPTH_TEST); - // Clear the entire stencil buffer, except for the 7th bit, which stores - // the global clipping mask that allows us to avoid drawing in regions of - // tiles we've already painted in. - gl.clearStencil(0x0); - gl.stencilMask(0xBF); - gl.clear(gl.STENCIL_BUFFER_BIT); + // 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); - // The stencil test will fail always, meaning we set all pixels covered - // by this geometry to 0x80. We use the highest bit 0x80 to mark the regions - // we want to draw in. All pixels that have this bit *not* set will never be - // drawn in. - gl.stencilFunc(gl.EQUAL, 0xC0, 0x40); - gl.stencilMask(0xC0); - gl.stencilOp(gl.REPLACE, gl.KEEP, gl.KEEP); + 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; - // 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.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.stencilFunc(gl.EQUAL, 0x80, 0x80); - gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE); 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. @@ -231,8 +257,7 @@ var draw = { fill: require('./draw_fill'), raster: require('./draw_raster'), background: require('./draw_background'), - debug: require('./draw_debug'), - vertices: require('./draw_vertices') + debug: require('./draw_debug') }; Painter.prototype.render = function(style, options) { @@ -251,53 +276,70 @@ Painter.prototype.render = function(style, options) { 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 = style._groups.length - 1; i >= 0; i--) { - var group = style._groups[i]; - var source = style.sources[group.source]; + 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(); - source.render(group, this); + if (source.prepare) source.prepare(); + if (source.isTileClipped) { + this._renderTileClippingMasks(coords, source.maxzoom); + } + } - } else if (group.source === undefined) { - this.drawLayers(group, this.identityMatrix); + if (isOpaquePass) { + this.gl.disable(this.gl.BLEND); + this.isOpaquePass = true; + } else { + this.gl.enable(this.gl.BLEND); + this.isOpaquePass = false; + coords.reverse(); } - } -}; -Painter.prototype.drawTile = function(tile, layers) { - this.setExtent(tile.tileExtent); - this.drawClippingMask(tile); - this.drawLayers(layers, tile.posMatrix, tile); + 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 (this.options.debug) { - draw.debug(this, tile); + draw.debug(this, coords); } }; -Painter.prototype.drawLayers = function(layers, matrix, tile) { - for (var i = layers.length - 1; i >= 0; i--) { - var layer = layers[i]; - - if (layer.hidden) - continue; - - draw[layer.type](this, layer, matrix, tile); - - if (this.options.vertices) { - draw.vertices(this, layer, matrix, tile); - } +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.hidden) 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); - // Blend to the front, not the back. - gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); gl.stencilMask(0x00); gl.stencilFunc(gl.EQUAL, 0x80, 0x80); @@ -307,12 +349,15 @@ Painter.prototype.drawStencilBuffer = function() { gl.uniform4fv(this.fillShader.u_color, [0, 0, 0, 0.5]); gl.drawArrays(gl.TRIANGLE_STRIP, 0, this.tileExtentBuffer.itemCount); +}; - // Revert blending mode to blend to the back. - gl.blendFunc(gl.ONE_MINUS_DST_ALPHA, gl.ONE); +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.translateMatrix = function(matrix, tile, translate, anchor) { +Painter.prototype.translatePosMatrix = function(matrix, tile, translate, anchor) { if (!translate[0] && !translate[1]) return matrix; if (anchor === 'viewport') { @@ -336,6 +381,43 @@ Painter.prototype.translateMatrix = function(matrix, tile, translate, anchor) { 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) { + var tileExtent = 4096; + 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 / tileExtent, scale / tileExtent, 1 ]); + mat4.multiply(posMatrix, transform.projMatrix, posMatrix); + + return new Float32Array(posMatrix); +}; + Painter.prototype.saveTexture = function(texture) { var textures = this.reusableTextures[texture.size]; if (!textures) { diff --git a/js/source/geojson_source.js b/js/source/geojson_source.js index be50e1fbb04..7c7613e0697 100644 --- a/js/source/geojson_source.js +++ b/js/source/geojson_source.js @@ -64,6 +64,7 @@ GeoJSONSource.prototype = util.inherit(Evented, /** @lends GeoJSONSource.prototy minzoom: 0, maxzoom: 14, _dirty: true, + isTileClipped: true, /** * Update source geojson data and rerender map @@ -107,7 +108,9 @@ GeoJSONSource.prototype = util.inherit(Evented, /** @lends GeoJSONSource.prototy } }, - render: Source._renderTiles, + getVisibleCoordinates: Source._getVisibleCoordinates, + getTile: Source._getTile, + featuresAt: Source._vectorFeaturesAt, featuresIn: Source._vectorFeaturesIn, diff --git a/js/source/image_source.js b/js/source/image_source.js index c20d3d484c7..d94e4f9e383 100644 --- a/js/source/image_source.js +++ b/js/source/image_source.js @@ -30,8 +30,6 @@ module.exports = ImageSource; * map.removeSource('some id'); // remove */ function ImageSource(options) { - this.coordinates = options.coordinates; - ajax.getImage(options.url, function(err, image) { // @TODO handle errors via event. if (err) return; @@ -45,7 +43,7 @@ function ImageSource(options) { this._loaded = true; if (this.map) { - this.createTile(); + this.createTile(options.coordinates); this.fire('change'); } }.bind(this)); @@ -65,20 +63,20 @@ ImageSource.prototype = util.inherit(Evented, { * may be outside the tile, because raster tiles aren't clipped when rendering. * @private */ - createTile: function() { + createTile: function(cornerGeoCoords) { var map = this.map; - var coords = this.coordinates.map(function(lnglat) { - var loc = LngLat.convert(lnglat); - return map.transform.locationCoordinate(loc).zoomTo(0); + var cornerZ0Coords = cornerGeoCoords.map(function(coord) { + return map.transform.locationCoordinate(LngLat.convert(coord)).zoomTo(0); }); - var center = util.getCoordinatesCenter(coords); + var centerCoord = this.centerCoord = util.getCoordinatesCenter(cornerZ0Coords); + var tileExtent = 4096; - var tileCoords = coords.map(function(coord) { - var zoomedCoord = coord.zoomTo(center.zoom); + var tileCoords = cornerZ0Coords.map(function(coord) { + var zoomedCoord = coord.zoomTo(centerCoord.zoom); return new Point( - Math.round((zoomedCoord.column - center.column) * tileExtent), - Math.round((zoomedCoord.row - center.row) * tileExtent)); + Math.round((zoomedCoord.column - centerCoord.column) * tileExtent), + Math.round((zoomedCoord.row - centerCoord.row) * tileExtent)); }); var gl = map.painter.gl; @@ -96,8 +94,6 @@ ImageSource.prototype = util.inherit(Evented, { this.tile.boundsBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.tile.boundsBuffer); gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW); - - this.center = center; }, loaded: function() { @@ -112,12 +108,10 @@ ImageSource.prototype = util.inherit(Evented, { // noop }, - render: function(layers, painter) { + prepare: function() { if (!this._loaded || !this.loaded()) return; - var c = this.center; - this.tile.calculateMatrices(c.zoom, c.column, c.row, this.map.transform, painter); - + var painter = this.map.painter; var gl = painter.gl; if (!this.tile.texture) { @@ -132,8 +126,15 @@ ImageSource.prototype = util.inherit(Evented, { gl.bindTexture(gl.TEXTURE_2D, this.tile.texture); gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.image); } + }, + + getVisibleCoordinates: function() { + if (this.centerCoord) return [this.centerCoord]; + else return []; + }, - painter.drawLayers(layers, this.tile.posMatrix, this.tile); + getTile: function() { + return this.tile; }, /** diff --git a/js/source/raster_tile_source.js b/js/source/raster_tile_source.js index f96bb833e58..cf848e2850d 100644 --- a/js/source/raster_tile_source.js +++ b/js/source/raster_tile_source.js @@ -39,7 +39,8 @@ RasterTileSource.prototype = util.inherit(Evented, { // noop }, - render: Source._renderTiles, + getVisibleCoordinates: Source._getVisibleCoordinates, + getTile: Source._getTile, _loadTile: function(tile) { var url = normalizeURL(tile.coord.url(this.tiles), this.url); diff --git a/js/source/source.js b/js/source/source.js index e248d2996bd..15bfcaef9af 100644 --- a/js/source/source.js +++ b/js/source/source.js @@ -4,8 +4,8 @@ var util = require('../util/util'); var ajax = require('../util/ajax'); var browser = require('../util/browser'); var TilePyramid = require('./tile_pyramid'); -var TileCoord = require('./tile_coord'); var normalizeURL = require('../util/mapbox').normalizeSourceURL; +var TileCoord = require('./tile_coord'); exports._loadTileJSON = function(options) { var loaded = function(err, tileJSON) { @@ -59,30 +59,13 @@ exports.redoPlacement = function() { } }; -exports._renderTiles = function(layers, painter) { - if (!this._pyramid) - return; +exports._getTile = function(coord) { + return this._pyramid.getTile(coord.id); +}; - var ids = this._pyramid.renderedIDs(); - for (var i = 0; i < ids.length; i++) { - var tile = this._pyramid.getTile(ids[i]), - // coord is different than tile.coord for wrapped tiles since the actual - // tile object is shared between all the visible copies of that tile. - coord = TileCoord.fromID(ids[i]), - z = coord.z, - x = coord.x, - y = coord.y, - w = coord.w; - - // if z > maxzoom then the tile is actually a overscaled maxzoom tile, - // so calculate the matrix the maxzoom tile would use. - z = Math.min(z, this.maxzoom); - - x += w * (1 << z); - tile.calculateMatrices(z, x, y, painter.transform, painter); - - painter.drawTile(tile, layers); - } +exports._getVisibleCoordinates = function() { + if (!this._pyramid) return []; + else return this._pyramid.renderedIDs().map(TileCoord.fromID); }; exports._vectorFeaturesAt = function(coord, params, callback) { diff --git a/js/source/tile.js b/js/source/tile.js index cc66bd2c750..c6519981465 100644 --- a/js/source/tile.js +++ b/js/source/tile.js @@ -1,6 +1,5 @@ 'use strict'; -var mat4 = require('gl-matrix').mat4; var util = require('../util/util'); var Buffer = require('../data/buffer'); @@ -26,39 +25,6 @@ Tile.prototype = { // todo unhardcode tileExtent: 4096, - /** - * Calculate the internal posMatrix that this tile uses to display - * itself in a map, given a coordinate as (z, x, y) and a transform - * @param {number} z - * @param {number} x - * @param {number} y - * @param {Object} transform - * @private - */ - calculateMatrices: function(z, x, y, transform) { - - // Initialize model-view matrix that converts from the tile coordinates - // to screen coordinates. - var tileScale = Math.pow(2, z); - var scale = transform.worldSize / tileScale; - - // TODO: remove - this.scale = scale; - - // The position matrix - this.posMatrix = new Float64Array(16); - - mat4.identity(this.posMatrix); - mat4.translate(this.posMatrix, this.posMatrix, [x * scale, y * scale, 0]); - mat4.scale(this.posMatrix, this.posMatrix, [ scale / this.tileExtent, scale / this.tileExtent, 1 ]); - mat4.multiply(this.posMatrix, transform.projMatrix, this.posMatrix); - - this.posMatrix = new Float32Array(this.posMatrix); - - this.exMatrix = transform.exMatrix; - this.rotationMatrix = transform.rotationMatrix; - }, - /** * Given a coordinate position, zoom that coordinate to my zoom and * scale and return a position in x, y, scale @@ -169,7 +135,15 @@ Tile.prototype = { this.redoWhenDone = false; } } + }, + /** + * Return whether this tile has any data for the given layer. + * @param {Object} style layer object + * @returns {boolean} + */ + hasLayerData: function(layer) { + return Boolean(this.buffers && this.elementGroups[layer.ref || layer.id] && Object.keys(this.elementGroups[layer.ref || layer.id]).length); } }; diff --git a/js/source/tile_coord.js b/js/source/tile_coord.js index 34e4b39f005..7c3bbbc74de 100644 --- a/js/source/tile_coord.js +++ b/js/source/tile_coord.js @@ -1,16 +1,14 @@ 'use strict'; var assert = require('assert'); +var Coordinate = require('../geo/coordinate'); module.exports = TileCoord; function TileCoord(z, x, y, w) { - assert(!isNaN(z)); - assert(!isNaN(x)); - assert(!isNaN(y)); - assert(z >= 0); - assert(x >= 0); - assert(y >= 0); + assert(!isNaN(z) && z >= 0 && z % 1 === 0); + assert(!isNaN(x) && x >= 0 && x % 1 === 0); + assert(!isNaN(y) && y >= 0 && y % 1 === 0); if (isNaN(w)) w = 0; @@ -30,6 +28,14 @@ TileCoord.prototype.toString = function() { return this.z + "/" + this.x + "/" + this.y; }; +TileCoord.prototype.toCoordinate = function() { + var zoom = this.z; + var tileScale = Math.pow(2, zoom); + var row = this.y; + var column = this.x + tileScale * this.w; + return new Coordinate(column, row, zoom); +}; + // Parse a packed integer id into a TileCoord object TileCoord.fromID = function(id) { var z = id % 32, dim = 1 << z; diff --git a/js/source/vector_tile_source.js b/js/source/vector_tile_source.js index d640f5a7d24..82cbb7dea70 100644 --- a/js/source/vector_tile_source.js +++ b/js/source/vector_tile_source.js @@ -23,6 +23,7 @@ VectorTileSource.prototype = util.inherit(Evented, { tileSize: 512, reparseOverscaled: true, _loaded: false, + isTileClipped: true, onAdd: function(map) { this.map = map; @@ -44,7 +45,9 @@ VectorTileSource.prototype = util.inherit(Evented, { } }, - render: Source._renderTiles, + getVisibleCoordinates: Source._getVisibleCoordinates, + getTile: Source._getTile, + featuresAt: Source._vectorFeaturesAt, featuresIn: Source._vectorFeaturesIn, diff --git a/js/source/video_source.js b/js/source/video_source.js index 6a4ad020f46..b8f27a883b6 100644 --- a/js/source/video_source.js +++ b/js/source/video_source.js @@ -32,8 +32,6 @@ module.exports = VideoSource; * map.removeSource('some id'); // remove */ function VideoSource(options) { - this.coordinates = options.coordinates; - ajax.getVideo(options.urls, function(err, video) { // @TODO handle errors via event. if (err) return; @@ -58,7 +56,7 @@ function VideoSource(options) { if (this.map) { this.video.play(); - this.createTile(); + this.createTile(options.coordinates); this.fire('change'); } }.bind(this)); @@ -84,25 +82,25 @@ VideoSource.prototype = util.inherit(Evented, /** @lends VideoSource.prototype * } }, - createTile: function() { + createTile: function(cornerGeoCoords) { /* * Calculate which mercator tile is suitable for rendering the video in * and create a buffer with the corner coordinates. These coordinates * may be outside the tile, because raster tiles aren't clipped when rendering. */ var map = this.map; - var coords = this.coordinates.map(function(lnglat) { - var loc = LngLat.convert(lnglat); - return map.transform.locationCoordinate(loc).zoomTo(0); + var cornerZ0Coords = cornerGeoCoords.map(function(coord) { + return map.transform.locationCoordinate(LngLat.convert(coord)).zoomTo(0); }); - var center = util.getCoordinatesCenter(coords); + var centerCoord = this.centerCoord = util.getCoordinatesCenter(cornerZ0Coords); + var tileExtent = 4096; - var tileCoords = coords.map(function(coord) { - var zoomedCoord = coord.zoomTo(center.zoom); + var tileCoords = cornerZ0Coords.map(function(coord) { + var zoomedCoord = coord.zoomTo(centerCoord.zoom); return new Point( - Math.round((zoomedCoord.column - center.column) * tileExtent), - Math.round((zoomedCoord.row - center.row) * tileExtent)); + Math.round((zoomedCoord.column - centerCoord.column) * tileExtent), + Math.round((zoomedCoord.row - centerCoord.row) * tileExtent)); }); var gl = map.painter.gl; @@ -120,8 +118,6 @@ VideoSource.prototype = util.inherit(Evented, /** @lends VideoSource.prototype * this.tile.boundsBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, this.tile.boundsBuffer); gl.bufferData(gl.ARRAY_BUFFER, array, gl.STATIC_DRAW); - - this.center = center; }, loaded: function() { @@ -136,14 +132,11 @@ VideoSource.prototype = util.inherit(Evented, /** @lends VideoSource.prototype * // noop }, - render: function(layers, painter) { + prepare: function() { if (!this._loaded) return; if (this.video.readyState < 2) return; // not enough data for current position - var c = this.center; - this.tile.calculateMatrices(c.zoom, c.column, c.row, this.map.transform, painter); - - var gl = painter.gl; + var gl = this.map.painter.gl; if (!this.tile.texture) { this.tile.texture = gl.createTexture(); gl.bindTexture(gl.TEXTURE_2D, this.tile.texture); @@ -157,7 +150,16 @@ VideoSource.prototype = util.inherit(Evented, /** @lends VideoSource.prototype * gl.texSubImage2D(gl.TEXTURE_2D, 0, 0, 0, gl.RGBA, gl.UNSIGNED_BYTE, this.video); } - painter.drawLayers(layers, this.tile.posMatrix, this.tile); + this._currentTime = this.video.currentTime; + }, + + getVisibleCoordinates: function() { + if (this.centerCoord) return [this.centerCoord]; + else return []; + }, + + getTile: function() { + return this.tile; }, featuresAt: function(point, params, callback) { diff --git a/js/util/browser/canvas.js b/js/util/browser/canvas.js index 662430db738..61592be9b2e 100644 --- a/js/util/browser/canvas.js +++ b/js/util/browser/canvas.js @@ -33,7 +33,7 @@ var requiredContextAttributes = { antialias: false, alpha: true, stencil: true, - depth: false + depth: true }; Canvas.prototype.getWebGLContext = function(attributes) { diff --git a/js/util/canvas.js b/js/util/canvas.js index 5de89a52084..14a30d144b3 100644 --- a/js/util/canvas.js +++ b/js/util/canvas.js @@ -13,7 +13,7 @@ function Canvas(parent, container) { antialias: false, alpha: true, stencil: true, - depth: false, + depth: true, preserveDrawingBuffer: true }; diff --git a/package.json b/package.json index e04032d785b..ce059cb1e89 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "eslint": "^1.5.0", "eslint-config-mourner": "^1.0.0", "istanbul": "^0.4.1", - "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#78bde0077848b4af0efd490d124bde3ea9f56ec9", + "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#42d780d6a3ff28417a8a0248b85d7ac0c7ca219f", "prova": "^2.1.2", "sinon": "^1.15.4", "st": "^1.0.0", diff --git a/shaders/pattern.vertex.glsl b/shaders/pattern.vertex.glsl index 560dc737227..22d9cf2f8a4 100644 --- a/shaders/pattern.vertex.glsl +++ b/shaders/pattern.vertex.glsl @@ -1,8 +1,8 @@ precision mediump float; uniform mat4 u_matrix; -uniform mat3 u_patternmatrix_a; -uniform mat3 u_patternmatrix_b; +uniform vec2 u_patternscale_a; +uniform vec2 u_patternscale_b; attribute vec2 a_pos; @@ -11,6 +11,6 @@ varying vec2 v_pos_b; void main() { gl_Position = u_matrix * vec4(a_pos, 0, 1); - v_pos_a = (u_patternmatrix_a * vec3(a_pos, 1)).xy; - v_pos_b = (u_patternmatrix_b * vec3(a_pos, 1)).xy; + v_pos_a = u_patternscale_a * a_pos; + v_pos_b = u_patternscale_b * a_pos; }