From f2759813d01f5211259c409b9de717ad165a5127 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Sun, 10 Aug 2014 18:28:11 -0700 Subject: [PATCH 01/19] tesselate polygons with libtess Rendering performance noticeably improves. Tesselation is slow though: 100-500ms for the buildings layer. --- js/data/buffer/fill_elements_buffer.js | 6 +- js/data/fill_bucket.js | 96 ++++++++++++-------------- js/render/draw_fill.js | 94 ++++++++++--------------- package.json | 1 + 4 files changed, 85 insertions(+), 112 deletions(-) diff --git a/js/data/buffer/fill_elements_buffer.js b/js/data/buffer/fill_elements_buffer.js index 1797545be70..b377036a0e6 100644 --- a/js/data/buffer/fill_elements_buffer.js +++ b/js/data/buffer/fill_elements_buffer.js @@ -10,17 +10,15 @@ function FillElementsBuffer(buffer) { } FillElementsBuffer.prototype = util.inherit(Buffer, { - itemSize: 6, // bytes per triangle (3 * unsigned short == 6 bytes) + itemSize: 2, // bytes per triangle (3 * unsigned short == 6 bytes) arrayType: 'ELEMENT_ARRAY_BUFFER', - add: function(a, b, c) { + add: function(a) { var pos2 = this.pos / 2; this.resize(); this.ushorts[pos2 + 0] = a; - this.ushorts[pos2 + 1] = b; - this.ushorts[pos2 + 2] = c; this.pos += this.itemSize; } diff --git a/js/data/fill_bucket.js b/js/data/fill_bucket.js index c4c278ef65c..c3fbec6110b 100644 --- a/js/data/fill_bucket.js +++ b/js/data/fill_bucket.js @@ -1,6 +1,9 @@ 'use strict'; var ElementGroups = require('./element_groups'); +var libtess = require('libtess'); + +var tesselator = initTesselator(); module.exports = FillBucket; @@ -11,66 +14,55 @@ function FillBucket(buffers) { FillBucket.prototype.addFeatures = function() { var features = this.features; - for (var i = 0; i < features.length; i++) { - var feature = features[i]; - this.addFeature(feature.loadGeometry()); - } -}; - -FillBucket.prototype.addFeature = function(lines) { - for (var i = 0; i < lines.length; i++) { - this.addFill(lines[i]); - } -}; - -FillBucket.prototype.addFill = function(vertices) { - if (vertices.length < 3) { - //console.warn('a fill must have at least three vertices'); - return; - } - - // Calculate the total number of vertices we're going to produce so that we - // can resize the buffer beforehand, or detect whether the current line - // won't fit into the buffer anymore. - // In order to be able to use the vertex buffer for drawing the antialiased - // outlines, we separate all polygon vertices with a degenerate (out-of- - // viewplane) vertex. - - var len = vertices.length; - - // Check whether this geometry buffer can hold all the required vertices. - this.elementGroups.makeRoomFor(len + 1); - var elementGroup = this.elementGroups.current; - var fillVertex = this.buffers.fillVertex; var fillElement = this.buffers.fillElement; - var outlineElement = this.buffers.outlineElement; + tesselator.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, addVertex); - // Start all lines with a degenerate vertex - elementGroup.vertexLength++; + var n = 0; + var elementGroups = this.elementGroups; - // We're generating triangle fans, so we always start with the first coordinate in this polygon. - var firstIndex = fillVertex.index - elementGroup.vertexStartIndex, - prevIndex, currentIndex, currentVertex; + //var start = self.performance.now(); - for (var i = 0; i < vertices.length; i++) { - currentIndex = fillVertex.index - elementGroup.vertexStartIndex; - currentVertex = vertices[i]; + features = features.reverse(); - fillVertex.add(currentVertex.x, currentVertex.y); - elementGroup.vertexLength++; - - // Only add triangles that have distinct vertices. - if (i >= 2 && (currentVertex.x !== vertices[0].x || currentVertex.y !== vertices[0].y)) { - fillElement.add(firstIndex, prevIndex, currentIndex); - elementGroup.elementLength++; + var elementGroup; + for (var i = 0; i < features.length; i++) { + var feature = features[i]; + var lines = feature.loadGeometry(); + + tesselator.gluTessBeginPolygon(); + for (var k = 0; k < lines.length; k++) { + var vertices = lines[0]; + + tesselator.gluTessBeginContour(); + for (var m = 0; m < vertices.length; m++) { + var coords = [vertices[m].x, vertices[m].y, 0]; + tesselator.gluTessVertex(coords, coords); + } + tesselator.gluTessEndContour(); } + tesselator.gluTessEndPolygon(); + } - if (i >= 1) { - outlineElement.add(prevIndex, currentIndex); - elementGroup.secondElementLength++; - } + //console.log(this.name + '\t polygons: ' + i + ', ms: ' + Math.round(self.performance.now() - start)); - prevIndex = currentIndex; + function addVertex(data) { + if (n % 3 === 0) { + elementGroups.makeRoomFor(10); + elementGroup = elementGroups.current; + } + var index = fillVertex.index - elementGroup.vertexStartIndex; + fillVertex.add(data[0], data[1]); + fillElement.add(index); + elementGroup.elementLength++; + n++; } }; + +function initTesselator() { + var tesselator = new libtess.GluTesselator(); + tesselator.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, function(coords) { return coords; }); + tesselator.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, function() {}); + tesselator.gluTessNormal(0, 0, 1); + return tesselator; +} diff --git a/js/render/draw_fill.js b/js/render/draw_fill.js index 4cb33965da1..fed5d931c67 100644 --- a/js/render/draw_fill.js +++ b/js/render/draw_fill.js @@ -15,34 +15,47 @@ function drawFill(painter, layer, posMatrix, tile) { 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-image']; + var opacity = layer.paint['fill-opacity']; + var shader; - var vertex, elements, group, count; + if (image) { + // Draw texture fill + var imagePos = painter.spriteAtlas.getPosition(image, true); + if (!imagePos) return; - // Draw the stencil mask. + shader = painter.patternShader; + gl.switchShader(shader, posMatrix); + gl.uniform1i(shader.u_image, 0); + gl.uniform2fv(shader.u_pattern_tl, imagePos.tl); + gl.uniform2fv(shader.u_pattern_br, imagePos.br); + gl.uniform1f(shader.u_mix, painter.transform.zoomFraction); + gl.uniform1f(shader.u_opacity, opacity); - // 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); - gl.clear(gl.STENCIL_BUFFER_BIT); + var factor = 8 / Math.pow(2, painter.transform.tileZoom - params.z); - // 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); + var matrix = mat3.create(); + mat3.scale(matrix, matrix, [ + 1 / (imagePos.size[0] * factor), + 1 / (imagePos.size[1] * factor), + 1, 1 + ]); - // 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.uniformMatrix3fv(shader.u_patternmatrix, false, matrix); - // When drawing a shape, we first draw all shapes to the stencil buffer - // and incrementing all areas where polygons are - gl.colorMask(false, false, false, false); + painter.spriteAtlas.bind(gl, true); - // Draw the actual triangle fan into the stencil buffer. - gl.switchShader(painter.fillShader, translatedPosMatrix); + } else { + // Draw filling rectangle. + shader = painter.fillShader; + gl.switchShader(shader, params.padded || posMatrix); + gl.uniform4fv(shader.u_color, color); + } + + var vertex, elements, group, count; + + //gl.switchShader(painter.fillShader, translatedPosMatrix, painter.tile.exMatrix); + //gl.uniform4fv(painter.fillShader.u_color, color); // Draw all buffers vertex = tile.buffers.fillVertex; @@ -55,43 +68,21 @@ function drawFill(painter, layer, posMatrix, tile) { for (var i = 0; i < elementGroups.groups.length; i++) { group = elementGroups.groups[i]; offset = group.vertexStartIndex * vertex.itemSize; - gl.vertexAttribPointer(painter.fillShader.a_pos, 2, gl.SHORT, false, 4, offset + 0); + gl.vertexAttribPointer(shader.a_pos, 2, gl.SHORT, false, 4, offset + 0); - count = group.elementLength * 3; + count = group.elementLength; elementOffset = group.elementStartIndex * elements.itemSize; gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset); + if (i > 0) console.log(i); } - // 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); - - // 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 (!) + // Because we're drawing top-to-bottom below, we have to draw the outline first (!) if (layer.paint['fill-antialias'] === true && !(layer.paint['fill-image'] && !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); @@ -156,13 +147,4 @@ function drawFill(painter, layer, posMatrix, tile) { gl.switchShader(shader, posMatrix); gl.uniform4fv(shader.u_color, color); } - - // Only draw regions that we marked - gl.stencilFunc(gl.NOTEQUAL, 0x0, 0x3F); - 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); } diff --git a/package.json b/package.json index 14716cd128a..d6803e92409 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "mapbox-gl-style-spec": "7.0.0", "minifyify": "^6.1.0", "pbf": "^1.2.0", + "libtess": "^1.0.2", "pngjs": "^0.4.0", "point-geometry": "0.0.0", "rbush": "^1.3.4", From f3dd9d92815ef14499d05ab9f27c20245c90e4c2 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Fri, 22 Aug 2014 13:59:40 +0300 Subject: [PATCH 02/19] fix holes & ditch array.reverse --- js/data/fill_bucket.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/js/data/fill_bucket.js b/js/data/fill_bucket.js index c3fbec6110b..4c9a0f59713 100644 --- a/js/data/fill_bucket.js +++ b/js/data/fill_bucket.js @@ -21,18 +21,16 @@ FillBucket.prototype.addFeatures = function() { var n = 0; var elementGroups = this.elementGroups; - //var start = self.performance.now(); - - features = features.reverse(); + var start = self.performance.now(); var elementGroup; - for (var i = 0; i < features.length; i++) { + for (var i = features.length - 1; i >= 0; i--) { var feature = features[i]; var lines = feature.loadGeometry(); tesselator.gluTessBeginPolygon(); for (var k = 0; k < lines.length; k++) { - var vertices = lines[0]; + var vertices = lines[k]; tesselator.gluTessBeginContour(); for (var m = 0; m < vertices.length; m++) { @@ -44,7 +42,9 @@ FillBucket.prototype.addFeatures = function() { tesselator.gluTessEndPolygon(); } - //console.log(this.name + '\t polygons: ' + i + ', ms: ' + Math.round(self.performance.now() - start)); + self.tesselateTime = self.tesselateTime || 0; + self.tesselateTime += self.performance.now() - start; + console.log(Math.round(self.tesselateTime) + ' ms'); function addVertex(data) { if (n % 3 === 0) { From 71503e6e60ac439325f2c1f9c5a3d2f163523d4b Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Mon, 27 Oct 2014 20:09:31 +0200 Subject: [PATCH 03/19] use earcut on simple polygons --- js/data/fill_bucket.js | 146 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 136 insertions(+), 10 deletions(-) diff --git a/js/data/fill_bucket.js b/js/data/fill_bucket.js index 4c9a0f59713..a37ec94573b 100644 --- a/js/data/fill_bucket.js +++ b/js/data/fill_bucket.js @@ -22,27 +22,47 @@ FillBucket.prototype.addFeatures = function() { var elementGroups = this.elementGroups; var start = self.performance.now(); + self.tesselateTime = self.tesselateTime || 0; var elementGroup; for (var i = features.length - 1; i >= 0; i--) { var feature = features[i]; var lines = feature.loadGeometry(); - tesselator.gluTessBeginPolygon(); - for (var k = 0; k < lines.length; k++) { - var vertices = lines[k]; + if (lines.length > 1) { + tesselator.gluTessBeginPolygon(); + for (var k = 0; k < lines.length; k++) { + var vertices = lines[k]; + + tesselator.gluTessBeginContour(); + for (var m = 0; m < vertices.length; m++) { + var coords = [vertices[m].x, vertices[m].y, 0]; + tesselator.gluTessVertex(coords, coords); + } + tesselator.gluTessEndContour(); + } + tesselator.gluTessEndPolygon(); + // console.count('complex'); + + } else { + // console.count('simple'); + var contour = []; + var vertices = lines[0]; + for (var m = 1; m < vertices.length; m++) { + var x = vertices[m].x, + y = vertices[m].y; + if (vertices[m - 1].x !== x || vertices[m - 1].y !== y) contour.push([x, y]); + } + var triangles = earcut(contour); - tesselator.gluTessBeginContour(); - for (var m = 0; m < vertices.length; m++) { - var coords = [vertices[m].x, vertices[m].y, 0]; - tesselator.gluTessVertex(coords, coords); + for (var m = 0; m < triangles.length; m++) { + for (var z = 0; z < 3; z++) { + addVertex(triangles[m][z]); + } } - tesselator.gluTessEndContour(); } - tesselator.gluTessEndPolygon(); } - self.tesselateTime = self.tesselateTime || 0; self.tesselateTime += self.performance.now() - start; console.log(Math.round(self.tesselateTime) + ' ms'); @@ -66,3 +86,109 @@ function initTesselator() { tesselator.gluTessNormal(0, 0, 1); return tesselator; } + + +// 'use strict'; + +// module.exports = earcut; + +function earcut(points) { + + var triangles = [], + sum = 0, + len = points.length, + i, j, last, clockwise, ear, prev, next; + + // create a doubly linked list from polygon points, detecting winding order along the way + for (i = 0, j = len - 1; i < len; j = i++) { + last = insertNode(points[i], last); + sum += (points[i][0] - points[j][0]) * (points[i][1] + points[j][1]); + } + clockwise = sum < 0; + + var k = 0; + + // iterate through ears, slicing them one by one + ear = last; + while (len > 2) { + prev = ear.prev; + next = ear.next; + + if (len === 3 || isEar(ear, clockwise)) { + triangles.push([prev.p, ear.p, next.p]); + ear.next.prev = ear.prev; + ear.prev.next = ear.next; + len--; + k = 0; + } + ear = next; + k++; + if (k > len) { + // console.log(ear); + break; + } + } + + return triangles; +} + +// iterate through points to check if there's a reflex point inside a potential ear +function isEar(ear, clockwise) { + + var a = ear.prev.p, + b = ear.p, + c = ear.next.p, + + ax = a[0], bx = b[0], cx = c[0], + ay = a[1], by = b[1], cy = c[1], + + abd = ax * by - ay * bx, + acd = ax * cy - ay * cx, + cbd = cx * by - cy * bx, + A = abd - acd - cbd; + + if (clockwise !== (A > 0)) return false; // reflex + + var sign = clockwise ? 1 : -1, + node = ear.next.next, + cay = cy - ay, + acx = ax - cx, + aby = ay - by, + bax = bx - ax, + p, px, py, s, t; + + while (node !== ear.prev) { + p = node.p; + px = p[0]; + py = p[1]; + + s = (cay * px + acx * py - acd) * sign; + t = (aby * px + bax * py + abd) * sign; + + if (s >= 0 && t >= 0 && (s + t) <= A * sign) return false; + + node = node.next; + } + return true; +} + +function insertNode(point, last) { + var node = { + p: point, + prev: null, + next: null + }; + + if (!last) { + node.prev = node; + node.next = node; + + } else { + node.next = last.next; + node.prev = last; + last.next.prev = node; + last.next = node; + } + return node; +} + From 4435c686f2e429fea6cab298b311302ebc608237 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Wed, 14 Jan 2015 20:59:49 +0200 Subject: [PATCH 04/19] handle bad data in earcut triangulation algorithm --- js/data/fill_bucket.js | 146 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 131 insertions(+), 15 deletions(-) diff --git a/js/data/fill_bucket.js b/js/data/fill_bucket.js index a37ec94573b..98fea0923e1 100644 --- a/js/data/fill_bucket.js +++ b/js/data/fill_bucket.js @@ -53,6 +53,7 @@ FillBucket.prototype.addFeatures = function() { y = vertices[m].y; if (vertices[m - 1].x !== x || vertices[m - 1].y !== y) contour.push([x, y]); } + if (!contour.length) continue; var triangles = earcut(contour); for (var m = 0; m < triangles.length; m++) { @@ -94,42 +95,67 @@ function initTesselator() { function earcut(points) { - var triangles = [], - sum = 0, + var sum = 0, len = points.length, - i, j, last, clockwise, ear, prev, next; + i, j, last; // create a doubly linked list from polygon points, detecting winding order along the way for (i = 0, j = len - 1; i < len; j = i++) { last = insertNode(points[i], last); sum += (points[i][0] - points[j][0]) * (points[i][1] + points[j][1]); } - clockwise = sum < 0; + var clockwise = sum < 0; + + var node = last; + do { + if (clipped(node.p, node.next.p, node.next.next.p)) { + var removed = node.next; + node.next = removed.next; + removed.next.prev = node; + if (removed === last) break; + continue; + } + node = node.next; + } while (node !== last) + + var triangles = []; + earcutLinked(node, clockwise, triangles); + return triangles; +} + +function clipped(p1, p2, p3) { + return (p1[0] === p2[0] && p2[0] === p3[0]) || (p1[1] === p2[1] && p2[1] === p3[1]); +} - var k = 0; +function earcutLinked(ear, clockwise, triangles) { + var stop = ear, + k = 0, + prev, next; // iterate through ears, slicing them one by one - ear = last; - while (len > 2) { + while (ear.prev !== ear.next) { prev = ear.prev; next = ear.next; - if (len === 3 || isEar(ear, clockwise)) { + if (isEar(ear, clockwise)) { triangles.push([prev.p, ear.p, next.p]); - ear.next.prev = ear.prev; - ear.prev.next = ear.next; - len--; + ear.next.prev = prev; + ear.prev.next = next; + stop = next; k = 0; } ear = next; k++; - if (k > len) { - // console.log(ear); + + if (ear.next === stop) { + splitEarcut(ear, clockwise, triangles); + break; + } + if (k > 10000) { + throw new Error('infinite loop, should never happen'); break; } } - - return triangles; } // iterate through points to check if there's a reflex point inside a potential ear @@ -158,6 +184,7 @@ function isEar(ear, clockwise) { p, px, py, s, t; while (node !== ear.prev) { + p = node.p; px = p[0]; py = p[1]; @@ -192,3 +219,92 @@ function insertNode(point, last) { return node; } +function splitEarcut(start, clockwise, triangles) { + var a = start, + split = false; + do { + var b = a.next.next; + while (b !== a.prev) { + if (middleInside(start, a.p, b.p) && !intersectsPolygon(start, a.p, b.p)) { + split = true; + break; + } + b = b.next; + } + if (split) break; + a = a.next; + } while (a !== start) + + if (!split) return; + + var a2 = { + p: a.p, + prev: null, + next: null + }; + var b2 = { + p: b.p, + prev: null, + next: null + }; + + var an = a.next; + var bp = b.prev; + + a.next = b; + b.prev = a; + + a2.next = an; + a2.prev = b2; + + b2.next = a2; + b2.prev = bp; + + an.prev = a2; + + bp.next = b2; + + earcutLinked(a, clockwise, triangles); + earcutLinked(a2, clockwise, triangles); +} + +function orient(p, q, r) { + return Math.sign((q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])); +} + +function intersects(p1, q1, p2, q2) { + return orient(p1, q1, p2) !== orient(p1, q1, q2) && + orient(p2, q2, p1) !== orient(p2, q2, q1); +} + +function intersectsPolygon(start, a, b) { + var node = start; + do { + var p1 = node.p, + p2 = node.next.p; + + if (p1 !== a && p2 !== a && p1 !== b && p2 !== b && intersects(p1, p2, a, b)) return true; + + node = node.next; + } while (node !== start) + + return false; +} + +function middleInside(start, a, b) { + var node = start, + inside = false, + px = (a[0] + b[0]) / 2, + py = (a[1] + b[1]) / 2; + do { + var p1 = node.p, + p2 = node.next.p; + + if (((p1[1] > py) !== (p2[1] > py)) && (px < (p2[0] - p1[0]) * (py - p1[1]) / (p2[1] - p1[1]) + p1[0])) { + inside = !inside; + } + node = node.next; + } while (node !== start) + + return inside; +} From a44ebcc002563ebbd97f64cc8b986e3e0fbdae0d Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 14 Jan 2015 16:27:34 -0500 Subject: [PATCH 05/19] fix fill-translate --- js/render/draw_fill.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/render/draw_fill.js b/js/render/draw_fill.js index fed5d931c67..f2c9d97a18a 100644 --- a/js/render/draw_fill.js +++ b/js/render/draw_fill.js @@ -48,7 +48,7 @@ function drawFill(painter, layer, posMatrix, tile) { } else { // Draw filling rectangle. shader = painter.fillShader; - gl.switchShader(shader, params.padded || posMatrix); + gl.switchShader(shader, params.padded || translatedPosMatrix); gl.uniform4fv(shader.u_color, color); } From b42471c5ed2c9e756e4357c588c8084f063a3591 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Thu, 15 Jan 2015 00:17:47 +0200 Subject: [PATCH 06/19] improve triangulation algorithm, cut into separate file --- js/data/element_groups.js | 1 + js/data/fill_bucket.js | 249 ++------------------------------------ js/util/triangulate.js | 227 ++++++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+), 236 deletions(-) create mode 100644 js/util/triangulate.js diff --git a/js/data/element_groups.js b/js/data/element_groups.js index b1442e2388e..57485c9aa51 100644 --- a/js/data/element_groups.js +++ b/js/data/element_groups.js @@ -17,6 +17,7 @@ ElementGroups.prototype.makeRoomFor = function(numVertices) { this.secondElementBuffer && this.secondElementBuffer.index); this.groups.push(this.current); } + return this.current; }; function ElementGroup(vertexStartIndex, elementStartIndex, secondElementStartIndex) { diff --git a/js/data/fill_bucket.js b/js/data/fill_bucket.js index 98fea0923e1..c7e30918b96 100644 --- a/js/data/fill_bucket.js +++ b/js/data/fill_bucket.js @@ -2,6 +2,7 @@ var ElementGroups = require('./element_groups'); var libtess = require('libtess'); +var triangulate = require('../util/triangulate'); var tesselator = initTesselator(); @@ -24,7 +25,6 @@ FillBucket.prototype.addFeatures = function() { var start = self.performance.now(); self.tesselateTime = self.tesselateTime || 0; - var elementGroup; for (var i = features.length - 1; i >= 0; i--) { var feature = features[i]; var lines = feature.loadGeometry(); @@ -49,33 +49,31 @@ FillBucket.prototype.addFeatures = function() { var contour = []; var vertices = lines[0]; for (var m = 1; m < vertices.length; m++) { - var x = vertices[m].x, - y = vertices[m].y; - if (vertices[m - 1].x !== x || vertices[m - 1].y !== y) contour.push([x, y]); + contour.push([vertices[m].x, vertices[m].y]); } if (!contour.length) continue; - var triangles = earcut(contour); + var triangles = triangulate(contour); + + var elementGroup = this.elementGroups.makeRoomFor(m); for (var m = 0; m < triangles.length; m++) { - for (var z = 0; z < 3; z++) { - addVertex(triangles[m][z]); - } + var index = fillVertex.index - elementGroup.vertexStartIndex; + fillVertex.add(triangles[m][0], triangles[m][1]); + fillElement.add(index); + elementGroup.elementLength++; } } } self.tesselateTime += self.performance.now() - start; - console.log(Math.round(self.tesselateTime) + ' ms'); + // console.log(Math.round(self.tesselateTime) + ' ms'); function addVertex(data) { - if (n % 3 === 0) { - elementGroups.makeRoomFor(10); - elementGroup = elementGroups.current; - } - var index = fillVertex.index - elementGroup.vertexStartIndex; + if (n % 3 === 0) elementGroups.makeRoomFor(10); + var index = fillVertex.index - elementGroups.current.vertexStartIndex; fillVertex.add(data[0], data[1]); fillElement.add(index); - elementGroup.elementLength++; + elementGroups.current.elementLength++; n++; } }; @@ -87,224 +85,3 @@ function initTesselator() { tesselator.gluTessNormal(0, 0, 1); return tesselator; } - - -// 'use strict'; - -// module.exports = earcut; - -function earcut(points) { - - var sum = 0, - len = points.length, - i, j, last; - - // create a doubly linked list from polygon points, detecting winding order along the way - for (i = 0, j = len - 1; i < len; j = i++) { - last = insertNode(points[i], last); - sum += (points[i][0] - points[j][0]) * (points[i][1] + points[j][1]); - } - var clockwise = sum < 0; - - var node = last; - do { - if (clipped(node.p, node.next.p, node.next.next.p)) { - var removed = node.next; - node.next = removed.next; - removed.next.prev = node; - if (removed === last) break; - continue; - } - node = node.next; - } while (node !== last) - - var triangles = []; - earcutLinked(node, clockwise, triangles); - return triangles; -} - -function clipped(p1, p2, p3) { - return (p1[0] === p2[0] && p2[0] === p3[0]) || (p1[1] === p2[1] && p2[1] === p3[1]); -} - -function earcutLinked(ear, clockwise, triangles) { - var stop = ear, - k = 0, - prev, next; - - // iterate through ears, slicing them one by one - while (ear.prev !== ear.next) { - prev = ear.prev; - next = ear.next; - - if (isEar(ear, clockwise)) { - triangles.push([prev.p, ear.p, next.p]); - ear.next.prev = prev; - ear.prev.next = next; - stop = next; - k = 0; - } - ear = next; - k++; - - if (ear.next === stop) { - splitEarcut(ear, clockwise, triangles); - break; - } - if (k > 10000) { - throw new Error('infinite loop, should never happen'); - break; - } - } -} - -// iterate through points to check if there's a reflex point inside a potential ear -function isEar(ear, clockwise) { - - var a = ear.prev.p, - b = ear.p, - c = ear.next.p, - - ax = a[0], bx = b[0], cx = c[0], - ay = a[1], by = b[1], cy = c[1], - - abd = ax * by - ay * bx, - acd = ax * cy - ay * cx, - cbd = cx * by - cy * bx, - A = abd - acd - cbd; - - if (clockwise !== (A > 0)) return false; // reflex - - var sign = clockwise ? 1 : -1, - node = ear.next.next, - cay = cy - ay, - acx = ax - cx, - aby = ay - by, - bax = bx - ax, - p, px, py, s, t; - - while (node !== ear.prev) { - - p = node.p; - px = p[0]; - py = p[1]; - - s = (cay * px + acx * py - acd) * sign; - t = (aby * px + bax * py + abd) * sign; - - if (s >= 0 && t >= 0 && (s + t) <= A * sign) return false; - - node = node.next; - } - return true; -} - -function insertNode(point, last) { - var node = { - p: point, - prev: null, - next: null - }; - - if (!last) { - node.prev = node; - node.next = node; - - } else { - node.next = last.next; - node.prev = last; - last.next.prev = node; - last.next = node; - } - return node; -} - -function splitEarcut(start, clockwise, triangles) { - var a = start, - split = false; - do { - var b = a.next.next; - while (b !== a.prev) { - if (middleInside(start, a.p, b.p) && !intersectsPolygon(start, a.p, b.p)) { - split = true; - break; - } - b = b.next; - } - if (split) break; - a = a.next; - } while (a !== start) - - if (!split) return; - - var a2 = { - p: a.p, - prev: null, - next: null - }; - var b2 = { - p: b.p, - prev: null, - next: null - }; - - var an = a.next; - var bp = b.prev; - - a.next = b; - b.prev = a; - - a2.next = an; - a2.prev = b2; - - b2.next = a2; - b2.prev = bp; - - an.prev = a2; - - bp.next = b2; - - earcutLinked(a, clockwise, triangles); - earcutLinked(a2, clockwise, triangles); -} - -function orient(p, q, r) { - return Math.sign((q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])); -} - -function intersects(p1, q1, p2, q2) { - return orient(p1, q1, p2) !== orient(p1, q1, q2) && - orient(p2, q2, p1) !== orient(p2, q2, q1); -} - -function intersectsPolygon(start, a, b) { - var node = start; - do { - var p1 = node.p, - p2 = node.next.p; - - if (p1 !== a && p2 !== a && p1 !== b && p2 !== b && intersects(p1, p2, a, b)) return true; - - node = node.next; - } while (node !== start) - - return false; -} - -function middleInside(start, a, b) { - var node = start, - inside = false, - px = (a[0] + b[0]) / 2, - py = (a[1] + b[1]) / 2; - do { - var p1 = node.p, - p2 = node.next.p; - - if (((p1[1] > py) !== (p2[1] > py)) && (px < (p2[0] - p1[0]) * (py - p1[1]) / (p2[1] - p1[1]) + p1[0])) { - inside = !inside; - } - node = node.next; - } while (node !== start) - - return inside; -} diff --git a/js/util/triangulate.js b/js/util/triangulate.js new file mode 100644 index 00000000000..7149c256eef --- /dev/null +++ b/js/util/triangulate.js @@ -0,0 +1,227 @@ +'use strict'; + +module.exports = triangulate; + +function triangulate(points) { + + var sum = 0, + len = points.length, + i, j, last; + + // create a doubly linked list from polygon points, detecting winding order along the way + for (i = 0, j = len - 1; i < len; j = i++) { + last = insertNode(points[i], last); + sum += (points[i][0] - points[j][0]) * (points[i][1] + points[j][1]); + } + + // eliminate vertically or horizontally colinear points (clipping-induced) + var node = last; + do { + var next = node.next; + if (equals(node.p, next.p) || clipped(node.p, next.p, next.next.p)) { + node.next = next.next; + next.next.prev = node; + if (next === last) break; + continue; + } + node = next; + } while (node !== last); + + var triangles = [], + clockwise = sum < 0; + + earcutLinked(node, clockwise, triangles); + + return triangles; +} + +function clipped(p1, p2, p3) { + return (p1[0] === p2[0] && p2[0] === p3[0]) || (p1[1] === p2[1] && p2[1] === p3[1]); +} + +function equals(p1, p2) { + return p1[0] === p2[0] && p1[1] === p2[1]; +} + +function earcutLinked(ear, clockwise, triangles) { + var stop = ear, + k = 0, + prev, next; + + // iterate through ears, slicing them one by one + while (ear.prev !== ear.next) { + prev = ear.prev; + next = ear.next; + + if (isEar(ear, clockwise)) { + triangles.push(prev.p, ear.p, next.p); + next.prev = prev; + prev.next = next; + stop = next; + k = 0; + } + ear = next; + k++; + + if (ear.next === stop) { + // if we can't find valid ears anymore, split remaining polygon into two + splitEarcut(ear, clockwise, triangles); + break; + } + } +} + +// iterate through points to check if there's a reflex point inside a potential ear +function isEar(ear, clockwise) { + + var a = ear.prev.p, + b = ear.p, + c = ear.next.p, + + ax = a[0], bx = b[0], cx = c[0], + ay = a[1], by = b[1], cy = c[1], + + abd = ax * by - ay * bx, + acd = ax * cy - ay * cx, + cbd = cx * by - cy * bx, + A = abd - acd - cbd; + + if (clockwise !== (A > 0)) return false; // reflex + + var sign = clockwise ? 1 : -1, + node = ear.next.next, + cay = cy - ay, + acx = ax - cx, + aby = ay - by, + bax = bx - ax, + p, px, py, s, t; + + while (node !== ear.prev) { + + p = node.p; + px = p[0]; + py = p[1]; + + s = (cay * px + acx * py - acd) * sign; + t = (aby * px + bax * py + abd) * sign; + + if (s >= 0 && t >= 0 && (s + t) <= A * sign) return false; + + node = node.next; + } + return true; +} + +function insertNode(point, last) { + var node = { + p: point, + prev: null, + next: null + }; + + if (!last) { + node.prev = node; + node.next = node; + + } else { + node.next = last.next; + node.prev = last; + last.next.prev = node; + last.next = node; + } + return node; +} + +function splitEarcut(start, clockwise, triangles) { + + // find a valid diagonal that divides the polygon into two + var a = start, + split, b; + do { + b = a.next.next; + while (b !== a.prev) { + if (middleInside(start, a.p, b.p) && !intersectsPolygon(start, a.p, b.p)) { + splitEarcutByDiag(a, b, clockwise, triangles); + return; + } + b = b.next; + } + if (split) break; + a = a.next; + } while (a !== start); +} + +function splitEarcutByDiag(a, b, clockwise, triangles) { + // split the polygon vertices circular doubly-linked linked list into two + var a2 = { + p: a.p, + prev: null, + next: null + }; + var b2 = { + p: b.p, + prev: null, + next: null + }; + + var an = a.next; + var bp = b.prev; + + a.next = b; + b.prev = a; + + a2.next = an; + a2.prev = b2; + + b2.next = a2; + b2.prev = bp; + + an.prev = a2; + + bp.next = b2; + + // run earcut on each half + earcutLinked(a, clockwise, triangles); + earcutLinked(a2, clockwise, triangles); +} + +function orient(p, q, r) { + return Math.sign((q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])); +} + +function intersects(p1, q1, p2, q2) { + return orient(p1, q1, p2) !== orient(p1, q1, q2) && + orient(p2, q2, p1) !== orient(p2, q2, q1); +} + +function intersectsPolygon(start, a, b) { + var node = start; + do { + var p1 = node.p, + p2 = node.next.p; + + if (p1 !== a && p2 !== a && p1 !== b && p2 !== b && intersects(p1, p2, a, b)) return true; + + node = node.next; + } while (node !== start); + + return false; +} + +function middleInside(start, a, b) { + var node = start, + inside = false, + px = (a[0] + b[0]) / 2, + py = (a[1] + b[1]) / 2; + do { + var p1 = node.p, + p2 = node.next.p; + + if (((p1[1] > py) !== (p2[1] > py)) && (px < (p2[0] - p1[0]) * (py - p1[1]) / (p2[1] - p1[1]) + p1[0])) { + inside = !inside; + } + node = node.next; + } while (node !== start); + + return inside; +} From f7c262bcee3993a9025a1deecd638236d3c8b1c7 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Thu, 15 Jan 2015 13:35:47 +0200 Subject: [PATCH 07/19] better and more robust triangulation, less sliver triangles --- js/data/fill_bucket.js | 13 ++--- js/source/worker.js | 2 + js/util/triangulate.js | 109 ++++++++++++++++++++--------------------- 3 files changed, 61 insertions(+), 63 deletions(-) diff --git a/js/data/fill_bucket.js b/js/data/fill_bucket.js index c7e30918b96..0c58818d000 100644 --- a/js/data/fill_bucket.js +++ b/js/data/fill_bucket.js @@ -25,6 +25,8 @@ FillBucket.prototype.addFeatures = function() { var start = self.performance.now(); self.tesselateTime = self.tesselateTime || 0; + var vertices, m; + for (var i = features.length - 1; i >= 0; i--) { var feature = features[i]; var lines = feature.loadGeometry(); @@ -32,10 +34,10 @@ FillBucket.prototype.addFeatures = function() { if (lines.length > 1) { tesselator.gluTessBeginPolygon(); for (var k = 0; k < lines.length; k++) { - var vertices = lines[k]; + vertices = lines[k]; tesselator.gluTessBeginContour(); - for (var m = 0; m < vertices.length; m++) { + for (m = 0; m < vertices.length; m++) { var coords = [vertices[m].x, vertices[m].y, 0]; tesselator.gluTessVertex(coords, coords); } @@ -47,8 +49,8 @@ FillBucket.prototype.addFeatures = function() { } else { // console.count('simple'); var contour = []; - var vertices = lines[0]; - for (var m = 1; m < vertices.length; m++) { + vertices = lines[0]; + for (m = 1; m < vertices.length; m++) { contour.push([vertices[m].x, vertices[m].y]); } if (!contour.length) continue; @@ -56,7 +58,7 @@ FillBucket.prototype.addFeatures = function() { var triangles = triangulate(contour); var elementGroup = this.elementGroups.makeRoomFor(m); - for (var m = 0; m < triangles.length; m++) { + for (m = 0; m < triangles.length; m++) { var index = fillVertex.index - elementGroup.vertexStartIndex; fillVertex.add(triangles[m][0], triangles[m][1]); fillElement.add(index); @@ -66,7 +68,6 @@ FillBucket.prototype.addFeatures = function() { } self.tesselateTime += self.performance.now() - start; - // console.log(Math.round(self.tesselateTime) + ' ms'); function addVertex(data) { if (n % 3 === 0) elementGroups.makeRoomFor(10); diff --git a/js/source/worker.js b/js/source/worker.js index 12b54a31daf..eec9789dd91 100644 --- a/js/source/worker.js +++ b/js/source/worker.js @@ -46,6 +46,8 @@ util.extend(Worker.prototype, { tile.data = new vt.VectorTile(new Protobuf(new Uint8Array(data))); tile.parse(tile.data, this.layers, this.actor, callback); + console.log(Math.round(self.tesselateTime) + ' ms'); + this.loaded[source] = this.loaded[source] || {}; this.loaded[source][id] = tile; }.bind(this)); diff --git a/js/util/triangulate.js b/js/util/triangulate.js index 7149c256eef..a7de7510439 100644 --- a/js/util/triangulate.js +++ b/js/util/triangulate.js @@ -1,8 +1,8 @@ 'use strict'; -module.exports = triangulate; +module.exports = earcut; -function triangulate(points) { +function earcut(points) { var sum = 0, len = points.length, @@ -14,38 +14,39 @@ function triangulate(points) { sum += (points[i][0] - points[j][0]) * (points[i][1] + points[j][1]); } - // eliminate vertically or horizontally colinear points (clipping-induced) - var node = last; + last = filterPoints(last); + + var triangles = [], + ccw = sum < 0; + + earcutLinked(last, ccw, triangles); + + return triangles; +} + +function filterPoints(start) { + // eliminate colinear or duplicate points + var node = start; do { var next = node.next; - if (equals(node.p, next.p) || clipped(node.p, next.p, next.next.p)) { + if (equals(node.p, next.p) || orient(node.p, next.p, next.next.p) === 0) { node.next = next.next; next.next.prev = node; - if (next === last) break; + if (next === start) return next.next; continue; } node = next; - } while (node !== last); - - var triangles = [], - clockwise = sum < 0; - - earcutLinked(node, clockwise, triangles); - - return triangles; -} + } while (node !== start); -function clipped(p1, p2, p3) { - return (p1[0] === p2[0] && p2[0] === p3[0]) || (p1[1] === p2[1] && p2[1] === p3[1]); + return start; } function equals(p1, p2) { return p1[0] === p2[0] && p1[1] === p2[1]; } -function earcutLinked(ear, clockwise, triangles) { +function earcutLinked(ear, ccw, triangles) { var stop = ear, - k = 0, prev, next; // iterate through ears, slicing them one by one @@ -53,26 +54,24 @@ function earcutLinked(ear, clockwise, triangles) { prev = ear.prev; next = ear.next; - if (isEar(ear, clockwise)) { + if (isEar(ear, ccw)) { triangles.push(prev.p, ear.p, next.p); next.prev = prev; prev.next = next; - stop = next; - k = 0; + stop = next.next; } - ear = next; - k++; + ear = next.next; - if (ear.next === stop) { + if (ear.next.next === stop) { // if we can't find valid ears anymore, split remaining polygon into two - splitEarcut(ear, clockwise, triangles); + splitEarcut(ear, ccw, triangles); break; } } } // iterate through points to check if there's a reflex point inside a potential ear -function isEar(ear, clockwise) { +function isEar(ear, ccw) { var a = ear.prev.p, b = ear.p, @@ -86,9 +85,9 @@ function isEar(ear, clockwise) { cbd = cx * by - cy * bx, A = abd - acd - cbd; - if (clockwise !== (A > 0)) return false; // reflex + if (ccw !== (A > 0)) return false; // reflex - var sign = clockwise ? 1 : -1, + var sign = ccw ? 1 : -1, node = ear.next.next, cay = cy - ay, acx = ax - cx, @@ -132,57 +131,46 @@ function insertNode(point, last) { return node; } -function splitEarcut(start, clockwise, triangles) { +function splitEarcut(start, ccw, triangles) { // find a valid diagonal that divides the polygon into two - var a = start, - split, b; + var a = start; do { - b = a.next.next; + var b = a.next.next; while (b !== a.prev) { - if (middleInside(start, a.p, b.p) && !intersectsPolygon(start, a.p, b.p)) { - splitEarcutByDiag(a, b, clockwise, triangles); + if (!intersectsPolygon(start, a.p, b.p) && locallyInside(a, b, ccw) && locallyInside(b, a, ccw) && + middleInside(start, a.p, b.p)) { + splitEarcutByDiag(a, b, ccw, triangles); return; } b = b.next; } - if (split) break; a = a.next; } while (a !== start); } -function splitEarcutByDiag(a, b, clockwise, triangles) { - // split the polygon vertices circular doubly-linked linked list into two - var a2 = { - p: a.p, - prev: null, - next: null - }; - var b2 = { - p: b.p, - prev: null, - next: null - }; - - var an = a.next; - var bp = b.prev; +function splitEarcutByDiag(a, b, ccw, triangles) { + var a2 = {p: a.p, prev: null, next: null}, + b2 = {p: b.p, prev: null, next: null}, + an = a.next, + bp = b.prev; + // split the polygon vertices circular doubly-linked linked list into two a.next = b; b.prev = a; a2.next = an; - a2.prev = b2; + an.prev = a2; b2.next = a2; - b2.prev = bp; - - an.prev = a2; + a2.prev = b2; bp.next = b2; + b2.prev = bp; // run earcut on each half - earcutLinked(a, clockwise, triangles); - earcutLinked(a2, clockwise, triangles); + earcutLinked(a, ccw, triangles); + earcutLinked(a2, ccw, triangles); } function orient(p, q, r) { @@ -208,6 +196,13 @@ function intersectsPolygon(start, a, b) { return false; } +function locallyInside(a, b, ccw) { + var sign = ccw ? -1 : 1; + return orient(a.prev.p, a.p, a.next.p) === sign ? + orient(a.p, b.p, a.next.p) !== sign && orient(a.p, a.prev.p, b.p) !== sign : + orient(a.p, b.p, a.prev.p) === sign || orient(a.p, a.next.p, b.p) === sign; +} + function middleInside(start, a, b) { var node = start, inside = false, From df827bfa22c1eaae22a655b2dc135beccfd0c35d Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Mon, 19 Jan 2015 14:19:37 +0200 Subject: [PATCH 08/19] remove duplicate water bucket --- debug/style.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/debug/style.json b/debug/style.json index 3f1e6e6f246..771f1b79a5a 100644 --- a/debug/style.json +++ b/debug/style.json @@ -229,9 +229,7 @@ } }, { "id": "water_offset", - "type": "fill", - "source": "mapbox", - "source-layer": "water", + "ref": "water", "paint": { "fill-color": "white", "fill-opacity": 0.3, From 6824fc7d724447e1760435b611bb44cf351d27ba Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Mon, 19 Jan 2015 14:33:50 +0200 Subject: [PATCH 09/19] switch to earcut npm package --- js/data/fill_bucket.js | 4 +- js/util/triangulate.js | 222 ----------------------------------------- package.json | 1 + 3 files changed, 3 insertions(+), 224 deletions(-) delete mode 100644 js/util/triangulate.js diff --git a/js/data/fill_bucket.js b/js/data/fill_bucket.js index 0c58818d000..94124dbecf3 100644 --- a/js/data/fill_bucket.js +++ b/js/data/fill_bucket.js @@ -2,9 +2,9 @@ var ElementGroups = require('./element_groups'); var libtess = require('libtess'); -var triangulate = require('../util/triangulate'); var tesselator = initTesselator(); +var earcut = require('earcut'); module.exports = FillBucket; @@ -55,7 +55,7 @@ FillBucket.prototype.addFeatures = function() { } if (!contour.length) continue; - var triangles = triangulate(contour); + var triangles = earcut([contour]); var elementGroup = this.elementGroups.makeRoomFor(m); for (m = 0; m < triangles.length; m++) { diff --git a/js/util/triangulate.js b/js/util/triangulate.js deleted file mode 100644 index a7de7510439..00000000000 --- a/js/util/triangulate.js +++ /dev/null @@ -1,222 +0,0 @@ -'use strict'; - -module.exports = earcut; - -function earcut(points) { - - var sum = 0, - len = points.length, - i, j, last; - - // create a doubly linked list from polygon points, detecting winding order along the way - for (i = 0, j = len - 1; i < len; j = i++) { - last = insertNode(points[i], last); - sum += (points[i][0] - points[j][0]) * (points[i][1] + points[j][1]); - } - - last = filterPoints(last); - - var triangles = [], - ccw = sum < 0; - - earcutLinked(last, ccw, triangles); - - return triangles; -} - -function filterPoints(start) { - // eliminate colinear or duplicate points - var node = start; - do { - var next = node.next; - if (equals(node.p, next.p) || orient(node.p, next.p, next.next.p) === 0) { - node.next = next.next; - next.next.prev = node; - if (next === start) return next.next; - continue; - } - node = next; - } while (node !== start); - - return start; -} - -function equals(p1, p2) { - return p1[0] === p2[0] && p1[1] === p2[1]; -} - -function earcutLinked(ear, ccw, triangles) { - var stop = ear, - prev, next; - - // iterate through ears, slicing them one by one - while (ear.prev !== ear.next) { - prev = ear.prev; - next = ear.next; - - if (isEar(ear, ccw)) { - triangles.push(prev.p, ear.p, next.p); - next.prev = prev; - prev.next = next; - stop = next.next; - } - ear = next.next; - - if (ear.next.next === stop) { - // if we can't find valid ears anymore, split remaining polygon into two - splitEarcut(ear, ccw, triangles); - break; - } - } -} - -// iterate through points to check if there's a reflex point inside a potential ear -function isEar(ear, ccw) { - - var a = ear.prev.p, - b = ear.p, - c = ear.next.p, - - ax = a[0], bx = b[0], cx = c[0], - ay = a[1], by = b[1], cy = c[1], - - abd = ax * by - ay * bx, - acd = ax * cy - ay * cx, - cbd = cx * by - cy * bx, - A = abd - acd - cbd; - - if (ccw !== (A > 0)) return false; // reflex - - var sign = ccw ? 1 : -1, - node = ear.next.next, - cay = cy - ay, - acx = ax - cx, - aby = ay - by, - bax = bx - ax, - p, px, py, s, t; - - while (node !== ear.prev) { - - p = node.p; - px = p[0]; - py = p[1]; - - s = (cay * px + acx * py - acd) * sign; - t = (aby * px + bax * py + abd) * sign; - - if (s >= 0 && t >= 0 && (s + t) <= A * sign) return false; - - node = node.next; - } - return true; -} - -function insertNode(point, last) { - var node = { - p: point, - prev: null, - next: null - }; - - if (!last) { - node.prev = node; - node.next = node; - - } else { - node.next = last.next; - node.prev = last; - last.next.prev = node; - last.next = node; - } - return node; -} - -function splitEarcut(start, ccw, triangles) { - - // find a valid diagonal that divides the polygon into two - var a = start; - do { - var b = a.next.next; - while (b !== a.prev) { - if (!intersectsPolygon(start, a.p, b.p) && locallyInside(a, b, ccw) && locallyInside(b, a, ccw) && - middleInside(start, a.p, b.p)) { - splitEarcutByDiag(a, b, ccw, triangles); - return; - } - b = b.next; - } - a = a.next; - } while (a !== start); -} - -function splitEarcutByDiag(a, b, ccw, triangles) { - var a2 = {p: a.p, prev: null, next: null}, - b2 = {p: b.p, prev: null, next: null}, - an = a.next, - bp = b.prev; - - // split the polygon vertices circular doubly-linked linked list into two - a.next = b; - b.prev = a; - - a2.next = an; - an.prev = a2; - - b2.next = a2; - a2.prev = b2; - - bp.next = b2; - b2.prev = bp; - - // run earcut on each half - earcutLinked(a, ccw, triangles); - earcutLinked(a2, ccw, triangles); -} - -function orient(p, q, r) { - return Math.sign((q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])); -} - -function intersects(p1, q1, p2, q2) { - return orient(p1, q1, p2) !== orient(p1, q1, q2) && - orient(p2, q2, p1) !== orient(p2, q2, q1); -} - -function intersectsPolygon(start, a, b) { - var node = start; - do { - var p1 = node.p, - p2 = node.next.p; - - if (p1 !== a && p2 !== a && p1 !== b && p2 !== b && intersects(p1, p2, a, b)) return true; - - node = node.next; - } while (node !== start); - - return false; -} - -function locallyInside(a, b, ccw) { - var sign = ccw ? -1 : 1; - return orient(a.prev.p, a.p, a.next.p) === sign ? - orient(a.p, b.p, a.next.p) !== sign && orient(a.p, a.prev.p, b.p) !== sign : - orient(a.p, b.p, a.prev.p) === sign || orient(a.p, a.next.p, b.p) === sign; -} - -function middleInside(start, a, b) { - var node = start, - inside = false, - px = (a[0] + b[0]) / 2, - py = (a[1] + b[1]) / 2; - do { - var p1 = node.p, - p2 = node.next.p; - - if (((p1[1] > py) !== (p2[1] > py)) && (px < (p2[0] - p1[0]) * (py - p1[1]) / (p2[1] - p1[1]) + p1[0])) { - inside = !inside; - } - node = node.next; - } while (node !== start); - - return inside; -} diff --git a/package.json b/package.json index d6803e92409..597e970909f 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "brfs": "1.2.0", "csscolorparser": "~1.0.2", + "earcut": "^1.0.0", "envify": "2.0.1", "feature-filter": "1.0.0", "geojson-rewind": "~0.1.0", From 26fb9d59bce5df7783dd50bca16bddc27fd21e16 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Tue, 20 Jan 2015 11:56:00 +0200 Subject: [PATCH 10/19] classify rings; switch to earcut only for tesselation --- js/data/fill_bucket.js | 81 ++++++++++++--------------------------- js/util/classify_rings.js | 61 +++++++++++++++++++++++++++++ package.json | 3 +- 3 files changed, 87 insertions(+), 58 deletions(-) create mode 100644 js/util/classify_rings.js diff --git a/js/data/fill_bucket.js b/js/data/fill_bucket.js index 94124dbecf3..853c55d0f05 100644 --- a/js/data/fill_bucket.js +++ b/js/data/fill_bucket.js @@ -1,10 +1,8 @@ 'use strict'; var ElementGroups = require('./element_groups'); -var libtess = require('libtess'); - -var tesselator = initTesselator(); var earcut = require('earcut'); +var classifyRings = require('../util/classify_rings'); module.exports = FillBucket; @@ -14,75 +12,46 @@ function FillBucket(buffers) { } FillBucket.prototype.addFeatures = function() { - var features = this.features; - var fillVertex = this.buffers.fillVertex; - var fillElement = this.buffers.fillElement; - tesselator.gluTessCallback(libtess.gluEnum.GLU_TESS_VERTEX_DATA, addVertex); - - var n = 0; - var elementGroups = this.elementGroups; + var fillVertex = this.buffers.fillVertex, + fillElement = this.buffers.fillElement; var start = self.performance.now(); self.tesselateTime = self.tesselateTime || 0; - var vertices, m; - - for (var i = features.length - 1; i >= 0; i--) { - var feature = features[i]; - var lines = feature.loadGeometry(); - - if (lines.length > 1) { - tesselator.gluTessBeginPolygon(); - for (var k = 0; k < lines.length; k++) { - vertices = lines[k]; - - tesselator.gluTessBeginContour(); - for (m = 0; m < vertices.length; m++) { - var coords = [vertices[m].x, vertices[m].y, 0]; - tesselator.gluTessVertex(coords, coords); - } - tesselator.gluTessEndContour(); - } - tesselator.gluTessEndPolygon(); - // console.count('complex'); - - } else { - // console.count('simple'); - var contour = []; - vertices = lines[0]; - for (m = 1; m < vertices.length; m++) { - contour.push([vertices[m].x, vertices[m].y]); - } - if (!contour.length) continue; + for (var i = this.features.length - 1; i >= 0; i--) { + var rings = this.features[i].loadGeometry(); + var polygons = classifyRings(convertCoords(rings)); - var triangles = earcut([contour]); + for (var j = 0; j < polygons.length; j++) { + var triangles = earcut(polygons[j]); + var elementGroup = this.elementGroups.makeRoomFor(triangles.length); - var elementGroup = this.elementGroups.makeRoomFor(m); - for (m = 0; m < triangles.length; m++) { + for (var m = 0; m < triangles.length; m++) { var index = fillVertex.index - elementGroup.vertexStartIndex; fillVertex.add(triangles[m][0], triangles[m][1]); fillElement.add(index); elementGroup.elementLength++; + elementGroup.vertexLength++; } } } self.tesselateTime += self.performance.now() - start; +}; - function addVertex(data) { - if (n % 3 === 0) elementGroups.makeRoomFor(10); - var index = fillVertex.index - elementGroups.current.vertexStartIndex; - fillVertex.add(data[0], data[1]); - fillElement.add(index); - elementGroups.current.elementLength++; - n++; - } +FillBucket.prototype.hasData = function() { + return !!this.elementGroups.current; }; -function initTesselator() { - var tesselator = new libtess.GluTesselator(); - tesselator.gluTessCallback(libtess.gluEnum.GLU_TESS_COMBINE, function(coords) { return coords; }); - tesselator.gluTessCallback(libtess.gluEnum.GLU_TESS_EDGE_FLAG, function() {}); - tesselator.gluTessNormal(0, 0, 1); - return tesselator; +function convertCoords(rings) { + var result = []; + for (var i = 0; i < rings.length; i++) { + var ring = []; + for (var j = 0; j < rings[i].length; j++) { + var p = rings[i][j]; + ring.push([p.x, p.y]); + } + result.push(ring); + } + return result; } diff --git a/js/util/classify_rings.js b/js/util/classify_rings.js new file mode 100644 index 00000000000..89705678ecc --- /dev/null +++ b/js/util/classify_rings.js @@ -0,0 +1,61 @@ +'use strict'; + +module.exports = classifyRings; + +// classifies an array of rings into polygons with outer rings and holes + +function classifyRings(rings) { + var len = rings.length; + + if (len <= 1) return [rings]; + + var i, j, + config = new Array(len); + + for (i = 0; i < len; i++) { + if (config[i]) continue; + config[i] = false; + for (j = 0; j < len; j++) { + if (i === j || config[j]) continue; + + if (ringPartiallyContains(rings[i], rings[j])) { + + // mark i as outer ring; add j as inner ring + config[i] = config[i] || [rings[i]]; + config[i].push(rings[j]); + config[j] = true; // mark j as inner ring + } + } + } + + var polygons = []; + for (i = 0; i < len; i++) { + if (config[i] === false) polygons.push([rings[i]]); + else if (config[i].length) polygons.push(config[i]); + } + + return polygons; +} + +function ringPartiallyContains(outer, inner) { + var threshold = Math.min(Math.ceil(inner.length * 0.01), 10), + num = 0; + for (var i = 0; i < threshold * 2; i++) { + if (ringContains(outer, inner[i])) num++; + } + if (num >= threshold) return true; +} + +function ringContains(points, p) { + var len = points.length, + inside = false, + i, j, p1, p2; + + for (i = 0, j = len - 1; i < len; j = i++) { + p1 = points[i]; + p2 = points[j]; + if (((p1[1] > p[1]) !== (p2[1] > p[1])) && + (p[0] < (p2[0] - p1[0]) * (p[1] - p1[1]) / (p2[1] - p1[1]) + p1[0])) inside = !inside; + } + return inside; +} diff --git a/package.json b/package.json index 597e970909f..d22f985e836 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dependencies": { "brfs": "1.2.0", "csscolorparser": "~1.0.2", - "earcut": "^1.0.0", + "earcut": "^1.0.3", "envify": "2.0.1", "feature-filter": "1.0.0", "geojson-rewind": "~0.1.0", @@ -22,7 +22,6 @@ "mapbox-gl-style-spec": "7.0.0", "minifyify": "^6.1.0", "pbf": "^1.2.0", - "libtess": "^1.0.2", "pngjs": "^0.4.0", "point-geometry": "0.0.0", "rbush": "^1.3.4", From 5c6bdfe4e91d8a03a062b6a504a7db7d38301a66 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Tue, 20 Jan 2015 19:17:24 +0200 Subject: [PATCH 11/19] more fool-proof ring classification --- js/util/classify_rings.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/js/util/classify_rings.js b/js/util/classify_rings.js index 89705678ecc..2e5a6e0fbd7 100644 --- a/js/util/classify_rings.js +++ b/js/util/classify_rings.js @@ -38,12 +38,19 @@ function classifyRings(rings) { } function ringPartiallyContains(outer, inner) { - var threshold = Math.min(Math.ceil(inner.length * 0.01), 10), - num = 0; - for (var i = 0; i < threshold * 2; i++) { - if (ringContains(outer, inner[i])) num++; + var len = inner.length, + num = 0, + counted = 0; + + for (var i = 0; i < len; i++) { + var p = inner[i]; + if (p[0] === -128 || p[1] === -128 || p[0] === 4224 || p[1] === 4224) continue; + counted++; + if (ringContains(outer, p)) num++; + if (counted >= 10) break; } - if (num >= threshold) return true; + if (counted === 0) return false; + return num / counted >= 0.8; } function ringContains(points, p) { From 912e115bcddd57912e93dd94063fc7d7f9b8b0a0 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Tue, 20 Jan 2015 19:24:48 +0200 Subject: [PATCH 12/19] update earcut --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d22f985e836..7e59b5c40d1 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dependencies": { "brfs": "1.2.0", "csscolorparser": "~1.0.2", - "earcut": "^1.0.3", + "earcut": "^1.1.0", "envify": "2.0.1", "feature-filter": "1.0.0", "geojson-rewind": "~0.1.0", From 070d326eb6ffbb7a4c432d38b16f854c4aa3b3b4 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 21 Jan 2015 18:57:12 -0500 Subject: [PATCH 13/19] re-add polygon antialiasing --- js/data/fill_bucket.js | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/js/data/fill_bucket.js b/js/data/fill_bucket.js index 853c55d0f05..69f88619bda 100644 --- a/js/data/fill_bucket.js +++ b/js/data/fill_bucket.js @@ -13,18 +13,49 @@ function FillBucket(buffers) { FillBucket.prototype.addFeatures = function() { var fillVertex = this.buffers.fillVertex, - fillElement = this.buffers.fillElement; + fillElement = this.buffers.fillElement, + outlineElement = this.buffers.outlineElement; var start = self.performance.now(); self.tesselateTime = self.tesselateTime || 0; + var geometries = []; + var currentIndex; + var prevIndex; + var elementGroup; + + // add outlines + for (var k = this.features.length - 1; k >= 0; k--) { + var lines = geometries[k] = this.features[k].loadGeometry(); + for (var l = 0; l < lines.length; l++) { + var line = lines[l]; + elementGroup = this.elementGroups.makeRoomFor(line.length); + + for (var v = 0; v < line.length; v++) { + var vertex = line[v]; + + currentIndex = fillVertex.index - elementGroup.vertexStartIndex; + fillVertex.add(vertex.x, vertex.y); + elementGroup.vertexLength++; + + if (v >= 1) { + outlineElement.add(prevIndex, currentIndex); + elementGroup.secondElementLength++; + } + + prevIndex = currentIndex; + } + } + } + + // add fills for (var i = this.features.length - 1; i >= 0; i--) { - var rings = this.features[i].loadGeometry(); + var rings = geometries[i]; var polygons = classifyRings(convertCoords(rings)); for (var j = 0; j < polygons.length; j++) { var triangles = earcut(polygons[j]); - var elementGroup = this.elementGroups.makeRoomFor(triangles.length); + elementGroup = this.elementGroups.makeRoomFor(triangles.length); for (var m = 0; m < triangles.length; m++) { var index = fillVertex.index - elementGroup.vertexStartIndex; From 923b8835951aa81d911fc74c613a1b95cbd3c9b3 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Wed, 21 Jan 2015 20:18:00 -0500 Subject: [PATCH 14/19] fix pattern fills --- js/render/draw_fill.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/render/draw_fill.js b/js/render/draw_fill.js index f2c9d97a18a..19d2555fd3d 100644 --- a/js/render/draw_fill.js +++ b/js/render/draw_fill.js @@ -25,14 +25,14 @@ function drawFill(painter, layer, posMatrix, tile) { if (!imagePos) return; shader = painter.patternShader; - gl.switchShader(shader, posMatrix); + gl.switchShader(shader, translatedPosMatrix); gl.uniform1i(shader.u_image, 0); gl.uniform2fv(shader.u_pattern_tl, imagePos.tl); gl.uniform2fv(shader.u_pattern_br, imagePos.br); gl.uniform1f(shader.u_mix, painter.transform.zoomFraction); gl.uniform1f(shader.u_opacity, opacity); - var factor = 8 / Math.pow(2, painter.transform.tileZoom - params.z); + var factor = 8 / Math.pow(2, painter.transform.tileZoom - tile.zoom); var matrix = mat3.create(); mat3.scale(matrix, matrix, [ From e0fa6af9c1cf3945b1bb504e396374a83ae5abde Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Thu, 22 Jan 2015 13:25:08 +0200 Subject: [PATCH 15/19] cleanup after rebase --- js/render/draw_fill.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/render/draw_fill.js b/js/render/draw_fill.js index 19d2555fd3d..4be739f598b 100644 --- a/js/render/draw_fill.js +++ b/js/render/draw_fill.js @@ -48,7 +48,7 @@ function drawFill(painter, layer, posMatrix, tile) { } else { // Draw filling rectangle. shader = painter.fillShader; - gl.switchShader(shader, params.padded || translatedPosMatrix); + gl.switchShader(shader, translatedPosMatrix); gl.uniform4fv(shader.u_color, color); } From 5e2bb1930b1d63c025dcd872df60240538fd880f Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Tue, 27 Jan 2015 16:05:14 +0200 Subject: [PATCH 16/19] upgrade earcut to 1.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7e59b5c40d1..9c9deab0684 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dependencies": { "brfs": "1.2.0", "csscolorparser": "~1.0.2", - "earcut": "^1.1.0", + "earcut": "^1.3.0", "envify": "2.0.1", "feature-filter": "1.0.0", "geojson-rewind": "~0.1.0", From 32861b1984b4853291be90030d2c18472e333001 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Thu, 26 Feb 2015 15:08:49 +0200 Subject: [PATCH 17/19] refactor fill_bucket.js for clarity --- js/data/fill_bucket.js | 97 +++++++++++++++++++++--------------------- 1 file changed, 48 insertions(+), 49 deletions(-) diff --git a/js/data/fill_bucket.js b/js/data/fill_bucket.js index 69f88619bda..8af83d3ecc8 100644 --- a/js/data/fill_bucket.js +++ b/js/data/fill_bucket.js @@ -12,66 +12,65 @@ function FillBucket(buffers) { } FillBucket.prototype.addFeatures = function() { - var fillVertex = this.buffers.fillVertex, - fillElement = this.buffers.fillElement, - outlineElement = this.buffers.outlineElement; - var start = self.performance.now(); self.tesselateTime = self.tesselateTime || 0; - var geometries = []; - var currentIndex; - var prevIndex; - var elementGroup; - - // add outlines - for (var k = this.features.length - 1; k >= 0; k--) { - var lines = geometries[k] = this.features[k].loadGeometry(); - for (var l = 0; l < lines.length; l++) { - var line = lines[l]; - elementGroup = this.elementGroups.makeRoomFor(line.length); - - for (var v = 0; v < line.length; v++) { - var vertex = line[v]; - - currentIndex = fillVertex.index - elementGroup.vertexStartIndex; - fillVertex.add(vertex.x, vertex.y); - elementGroup.vertexLength++; + var features = this.features; + for (var i = this.features.length - 1; i >= 0; i--) { + var feature = features[i]; + this.addFeature(feature.loadGeometry()); + } - if (v >= 1) { - outlineElement.add(prevIndex, currentIndex); - elementGroup.secondElementLength++; - } + self.tesselateTime += self.performance.now() - start; +}; - prevIndex = currentIndex; - } - } +FillBucket.prototype.addFeature = function(lines) { + var i; + for (i = 0; i < lines.length; i++) { + this.addOutline(lines[i]); } - // add fills - for (var i = this.features.length - 1; i >= 0; i--) { - var rings = geometries[i]; - var polygons = classifyRings(convertCoords(rings)); - - for (var j = 0; j < polygons.length; j++) { - var triangles = earcut(polygons[j]); - elementGroup = this.elementGroups.makeRoomFor(triangles.length); - - for (var m = 0; m < triangles.length; m++) { - var index = fillVertex.index - elementGroup.vertexStartIndex; - fillVertex.add(triangles[m][0], triangles[m][1]); - fillElement.add(index); - elementGroup.elementLength++; - elementGroup.vertexLength++; - } - } + var polygons = classifyRings(convertCoords(lines)); + for (i = 0; i < polygons.length; i++) { + this.addFill(polygons[i]); } +}; - self.tesselateTime += self.performance.now() - start; +FillBucket.prototype.addFill = function(polygon) { + var fillVertex = this.buffers.fillVertex, + fillElement = this.buffers.fillElement, + triangles = earcut(polygon), + elementGroup = this.elementGroups.makeRoomFor(triangles.length); + + for (var i = 0; i < triangles.length; i++) { + var index = fillVertex.index - elementGroup.vertexStartIndex; + fillVertex.add(triangles[i][0], triangles[i][1]); + fillElement.add(index); + elementGroup.elementLength++; + elementGroup.vertexLength++; + } }; -FillBucket.prototype.hasData = function() { - return !!this.elementGroups.current; +FillBucket.prototype.addOutline = function(vertices) { + var elementGroup = this.elementGroups.makeRoomFor(vertices.length), + fillVertex = this.buffers.fillVertex, + outlineElement = this.buffers.outlineElement, + currentIndex, prevIndex, vertex, i; + + for (i = 0; i < vertices.length; i++) { + vertex = vertices[i]; + + currentIndex = fillVertex.index - elementGroup.vertexStartIndex; + fillVertex.add(vertex.x, vertex.y); + elementGroup.vertexLength++; + + if (i >= 1) { + outlineElement.add(prevIndex, currentIndex); + elementGroup.secondElementLength++; + } + + prevIndex = currentIndex; + } }; function convertCoords(rings) { From 299449e9efba18749638c359e0451e1fbebb3014 Mon Sep 17 00:00:00 2001 From: Vladimir Agafonkin Date: Thu, 26 Feb 2015 15:29:54 +0200 Subject: [PATCH 18/19] fix draw fill after rebase --- js/render/draw_fill.js | 105 +++++++++++++---------------------------- 1 file changed, 32 insertions(+), 73 deletions(-) diff --git a/js/render/draw_fill.js b/js/render/draw_fill.js index 4be739f598b..753df16056a 100644 --- a/js/render/draw_fill.js +++ b/js/render/draw_fill.js @@ -15,36 +15,53 @@ function drawFill(painter, layer, posMatrix, tile) { var translatedPosMatrix = painter.translateMatrix(posMatrix, tile, layer.paint['fill-translate'], layer.paint['fill-translate-anchor']); var color = layer.paint['fill-color']; + + var vertex, elements, group, count; + + // Draw all buffers + vertex = tile.buffers.fillVertex; + vertex.bind(gl); + elements = tile.buffers.fillElement; + elements.bind(gl); + var image = layer.paint['fill-image']; - var opacity = layer.paint['fill-opacity']; + var opacity = layer.paint['fill-opacity'] || 1; var shader; if (image) { // Draw texture fill - var imagePos = painter.spriteAtlas.getPosition(image, true); - if (!imagePos) return; + var imagePosA = painter.spriteAtlas.getPosition(image.from, true); + var imagePosB = painter.spriteAtlas.getPosition(image.to, true); + if (!imagePosA || !imagePosB) return; shader = painter.patternShader; - gl.switchShader(shader, translatedPosMatrix); + gl.switchShader(shader, posMatrix); gl.uniform1i(shader.u_image, 0); - gl.uniform2fv(shader.u_pattern_tl, imagePos.tl); - gl.uniform2fv(shader.u_pattern_br, imagePos.br); - gl.uniform1f(shader.u_mix, painter.transform.zoomFraction); + 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); + gl.uniform2fv(shader.u_pattern_br_b, imagePosB.br); gl.uniform1f(shader.u_opacity, opacity); + gl.uniform1f(shader.u_mix, image.t); var factor = 8 / Math.pow(2, painter.transform.tileZoom - tile.zoom); - var matrix = mat3.create(); - mat3.scale(matrix, matrix, [ - 1 / (imagePos.size[0] * factor), - 1 / (imagePos.size[1] * factor), - 1, 1 + var matrixA = mat3.create(); + mat3.scale(matrixA, matrixA, [ + 1 / (imagePosA.size[0] * factor * image.fromScale), + 1 / (imagePosA.size[1] * factor * image.fromScale) + ]); + + var matrixB = mat3.create(); + mat3.scale(matrixB, matrixB, [ + 1 / (imagePosB.size[0] * factor * image.toScale), + 1 / (imagePosB.size[1] * factor * image.toScale) ]); - gl.uniformMatrix3fv(shader.u_patternmatrix, false, matrix); + gl.uniformMatrix3fv(shader.u_patternmatrix_a, false, matrixA); + gl.uniformMatrix3fv(shader.u_patternmatrix_b, false, matrixB); painter.spriteAtlas.bind(gl, true); - } else { // Draw filling rectangle. shader = painter.fillShader; @@ -52,17 +69,6 @@ function drawFill(painter, layer, posMatrix, tile) { gl.uniform4fv(shader.u_color, color); } - var vertex, elements, group, count; - - //gl.switchShader(painter.fillShader, translatedPosMatrix, painter.tile.exMatrix); - //gl.uniform4fv(painter.fillShader.u_color, color); - - // Draw all buffers - vertex = tile.buffers.fillVertex; - vertex.bind(gl); - elements = tile.buffers.fillElement; - elements.bind(gl); - var offset, elementOffset; for (var i = 0; i < elementGroups.groups.length; i++) { @@ -73,12 +79,11 @@ function drawFill(painter, layer, posMatrix, tile) { count = group.elementLength; elementOffset = group.elementStartIndex * elements.itemSize; gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset); - if (i > 0) console.log(i); } var strokeColor = layer.paint['fill-outline-color']; - // Because we're drawing top-to-bottom below, we have to draw the outline first (!) + // Because we're drawing top-to-bottom, we have to draw the outline first (!) if (layer.paint['fill-antialias'] === true && !(layer.paint['fill-image'] && !strokeColor)) { gl.switchShader(painter.outlineShader, translatedPosMatrix); gl.lineWidth(2 * browser.devicePixelRatio); @@ -101,50 +106,4 @@ function drawFill(painter, layer, posMatrix, tile) { gl.drawElements(gl.LINES, count, gl.UNSIGNED_SHORT, elementOffset); } } - - var image = layer.paint['fill-image']; - var opacity = layer.paint['fill-opacity'] || 1; - var shader; - - if (image) { - // Draw texture fill - var imagePosA = painter.spriteAtlas.getPosition(image.from, true); - var imagePosB = painter.spriteAtlas.getPosition(image.to, true); - if (!imagePosA || !imagePosB) return; - - shader = painter.patternShader; - gl.switchShader(shader, posMatrix); - gl.uniform1i(shader.u_image, 0); - 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); - gl.uniform2fv(shader.u_pattern_br_b, imagePosB.br); - gl.uniform1f(shader.u_opacity, opacity); - gl.uniform1f(shader.u_mix, image.t); - - var factor = 8 / Math.pow(2, painter.transform.tileZoom - tile.zoom); - - var matrixA = mat3.create(); - mat3.scale(matrixA, matrixA, [ - 1 / (imagePosA.size[0] * factor * image.fromScale), - 1 / (imagePosA.size[1] * factor * image.fromScale) - ]); - - var matrixB = mat3.create(); - mat3.scale(matrixB, matrixB, [ - 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 { - // Draw filling rectangle. - shader = painter.fillShader; - gl.switchShader(shader, posMatrix); - gl.uniform4fv(shader.u_color, color); - } } From ec0de491dbb69465f108c32d7b4967df85808b17 Mon Sep 17 00:00:00 2001 From: Ansis Brammanis Date: Thu, 30 Apr 2015 12:43:18 -0700 Subject: [PATCH 19/19] update to earcut v2.0.0 and reuse vertices for outline and fill instead of adding them twice --- js/data/fill_bucket.js | 74 +++++++++++++++++++++++------------------- package.json | 2 +- 2 files changed, 42 insertions(+), 34 deletions(-) diff --git a/js/data/fill_bucket.js b/js/data/fill_bucket.js index 8af83d3ecc8..eba8f151172 100644 --- a/js/data/fill_bucket.js +++ b/js/data/fill_bucket.js @@ -25,51 +25,59 @@ FillBucket.prototype.addFeatures = function() { }; FillBucket.prototype.addFeature = function(lines) { - var i; - for (i = 0; i < lines.length; i++) { - this.addOutline(lines[i]); - } - var polygons = classifyRings(convertCoords(lines)); - for (i = 0; i < polygons.length; i++) { - this.addFill(polygons[i]); + for (var i = 0; i < polygons.length; i++) { + this.addPolygon(polygons[i]); } }; -FillBucket.prototype.addFill = function(polygon) { - var fillVertex = this.buffers.fillVertex, - fillElement = this.buffers.fillElement, - triangles = earcut(polygon), - elementGroup = this.elementGroups.makeRoomFor(triangles.length); - - for (var i = 0; i < triangles.length; i++) { - var index = fillVertex.index - elementGroup.vertexStartIndex; - fillVertex.add(triangles[i][0], triangles[i][1]); - fillElement.add(index); - elementGroup.elementLength++; - elementGroup.vertexLength++; +FillBucket.prototype.addPolygon = function(polygon) { + + var numVertices = 0; + for (var k = 0; k < polygon.length; k++) { + numVertices += polygon[k].length; } -}; -FillBucket.prototype.addOutline = function(vertices) { - var elementGroup = this.elementGroups.makeRoomFor(vertices.length), - fillVertex = this.buffers.fillVertex, + var fillVertex = this.buffers.fillVertex, + fillElement = this.buffers.fillElement, outlineElement = this.buffers.outlineElement, - currentIndex, prevIndex, vertex, i; + elementGroup = this.elementGroups.makeRoomFor(numVertices), + startIndex = fillVertex.index - elementGroup.vertexStartIndex, + flattened = [], + holeIndices = [], + prevIndex; + + for (var r = 0; r < polygon.length; r++) { + var ring = polygon[r]; + prevIndex = undefined; + + if (r > 0) holeIndices.push(flattened.length / 2); + + for (var v = 0; v < ring.length; v++) { + var vertex = ring[v]; - for (i = 0; i < vertices.length; i++) { - vertex = vertices[i]; + var currentIndex = fillVertex.index - elementGroup.vertexStartIndex; + fillVertex.add(vertex[0], vertex[1]); + elementGroup.vertexLength++; - currentIndex = fillVertex.index - elementGroup.vertexStartIndex; - fillVertex.add(vertex.x, vertex.y); - elementGroup.vertexLength++; + if (v >= 1) { + outlineElement.add(prevIndex, currentIndex); + elementGroup.secondElementLength++; + } - if (i >= 1) { - outlineElement.add(prevIndex, currentIndex); - elementGroup.secondElementLength++; + prevIndex = currentIndex; + + // convert to format used by earcut + flattened.push(vertex[0]); + flattened.push(vertex[1]); } + } + + var triangleIndices = earcut(flattened, holeIndices); - prevIndex = currentIndex; + for (var i = 0; i < triangleIndices.length; i++) { + fillElement.add(triangleIndices[i] + startIndex); + elementGroup.elementLength += 1; } }; diff --git a/package.json b/package.json index 9c9deab0684..82b78f9acc8 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "dependencies": { "brfs": "1.2.0", "csscolorparser": "~1.0.2", - "earcut": "^1.3.0", + "earcut": "^2.0.0", "envify": "2.0.1", "feature-filter": "1.0.0", "geojson-rewind": "~0.1.0",