Skip to content

Commit da08869

Browse files
committed
switch to tesselated fill rendering with earcut
1 parent 739aa39 commit da08869

File tree

7 files changed

+144
-141
lines changed

7 files changed

+144
-141
lines changed

js/data/buffer_set.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ var outlineElementOptions = {type: Buffer.BufferType.ELEMENT, attributes: [
3434
{name: 'vertices', components: 2, type: Buffer.ELEMENT_ATTRIBUTE_TYPE}
3535
]};
3636

37+
var fillElementOptions = {type: Buffer.BufferType.ELEMENT, attributes: [
38+
{name: 'vertices', components: 1, type: Buffer.ELEMENT_ATTRIBUTE_TYPE}
39+
]};
40+
3741
var bufferOptions = {
3842
glyphVertex: symbolVertexOptions,
3943
glyphElement: triangleElementOptions,
@@ -42,7 +46,7 @@ var bufferOptions = {
4246
circleVertex: fillVertexOptions,
4347
circleElement: triangleElementOptions,
4448
fillVertex: fillVertexOptions,
45-
fillElement: triangleElementOptions,
49+
fillElement: fillElementOptions,
4650
outlineElement: outlineElementOptions,
4751
lineVertex: lineVertexOptions,
4852
lineElement: triangleElementOptions,

js/data/element_groups.js

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ElementGroups.prototype.makeRoomFor = function(numVertices) {
1717
this.secondElementBuffer && this.secondElementBuffer.length);
1818
this.groups.push(this.current);
1919
}
20+
return this.current;
2021
};
2122

2223
function ElementGroup(vertexStartIndex, elementStartIndex, secondElementStartIndex) {

js/data/fill_bucket.js

+51-36
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict';
22

33
var ElementGroups = require('./element_groups');
4+
var earcut = require('earcut');
5+
var classifyRings = require('../util/classify_rings');
46

57
module.exports = FillBucket;
68

@@ -18,56 +20,69 @@ FillBucket.prototype.addFeatures = function() {
1820
};
1921

2022
FillBucket.prototype.addFeature = function(lines) {
21-
for (var i = 0; i < lines.length; i++) {
22-
this.addFill(lines[i]);
23+
var polygons = classifyRings(convertCoords(lines));
24+
for (var i = 0; i < polygons.length; i++) {
25+
this.addPolygon(polygons[i]);
2326
}
2427
};
2528

26-
FillBucket.prototype.addFill = function(vertices) {
27-
if (vertices.length < 3) {
28-
//console.warn('a fill must have at least three vertices');
29-
return;
29+
FillBucket.prototype.addPolygon = function(polygon) {
30+
var numVertices = 0;
31+
for (var k = 0; k < polygon.length; k++) {
32+
numVertices += polygon[k].length;
3033
}
3134

32-
// Calculate the total number of vertices we're going to produce so that we
33-
// can resize the buffer beforehand, or detect whether the current line
34-
// won't fit into the buffer anymore.
35-
// In order to be able to use the vertex buffer for drawing the antialiased
36-
// outlines, we separate all polygon vertices with a degenerate (out-of-
37-
// viewplane) vertex.
35+
var fillVertex = this.buffers.fillVertex,
36+
fillElement = this.buffers.fillElement,
37+
outlineElement = this.buffers.outlineElement,
38+
elementGroup = this.elementGroups.makeRoomFor(numVertices),
39+
startIndex = fillVertex.length - elementGroup.vertexStartIndex,
40+
flattened = [],
41+
holeIndices = [],
42+
prevIndex;
3843

39-
var len = vertices.length;
44+
for (var r = 0; r < polygon.length; r++) {
45+
var ring = polygon[r];
4046

41-
// Check whether this geometry buffer can hold all the required vertices.
42-
this.elementGroups.makeRoomFor(len + 1);
43-
var elementGroup = this.elementGroups.current;
47+
if (r > 0) holeIndices.push(flattened.length / 2);
4448

45-
var fillVertex = this.buffers.fillVertex;
46-
var fillElement = this.buffers.fillElement;
47-
var outlineElement = this.buffers.outlineElement;
49+
for (var v = 0; v < ring.length; v++) {
50+
var vertex = ring[v];
4851

49-
// We're generating triangle fans, so we always start with the first coordinate in this polygon.
50-
var firstIndex = fillVertex.length - elementGroup.vertexStartIndex,
51-
prevIndex, currentIndex, currentVertex;
52+
var currentIndex = fillVertex.length - elementGroup.vertexStartIndex;
53+
fillVertex.push(vertex[0], vertex[1]);
54+
elementGroup.vertexLength++;
5255

53-
for (var i = 0; i < vertices.length; i++) {
54-
currentIndex = fillVertex.length - elementGroup.vertexStartIndex;
55-
currentVertex = vertices[i];
56+
if (v >= 1) {
57+
outlineElement.push(prevIndex, currentIndex);
58+
elementGroup.secondElementLength++;
59+
}
5660

57-
fillVertex.push(currentVertex.x, currentVertex.y);
58-
elementGroup.vertexLength++;
61+
prevIndex = currentIndex;
5962

60-
// Only add triangles that have distinct vertices.
61-
if (i >= 2 && (currentVertex.x !== vertices[0].x || currentVertex.y !== vertices[0].y)) {
62-
fillElement.push(firstIndex, prevIndex, currentIndex);
63-
elementGroup.elementLength++;
63+
// convert to format used by earcut
64+
flattened.push(vertex[0]);
65+
flattened.push(vertex[1]);
6466
}
67+
}
6568

66-
if (i >= 1) {
67-
outlineElement.push(prevIndex, currentIndex);
68-
elementGroup.secondElementLength++;
69-
}
69+
var triangleIndices = earcut(flattened, holeIndices);
7070

71-
prevIndex = currentIndex;
71+
for (var i = 0; i < triangleIndices.length; i++) {
72+
fillElement.push(triangleIndices[i] + startIndex);
73+
elementGroup.elementLength += 1;
7274
}
7375
};
76+
77+
function convertCoords(rings) {
78+
var result = [];
79+
for (var i = 0; i < rings.length; i++) {
80+
var ring = [];
81+
for (var j = 0; j < rings[i].length; j++) {
82+
var p = rings[i][j];
83+
ring.push([p.x, p.y]);
84+
}
85+
result.push(ring);
86+
}
87+
return result;
88+
}

js/render/draw_fill.js

+39-100
Original file line numberDiff line numberDiff line change
@@ -15,107 +15,18 @@ function drawFill(painter, layer, posMatrix, tile) {
1515
var translatedPosMatrix = painter.translateMatrix(posMatrix, tile, layer.paint['fill-translate'], layer.paint['fill-translate-anchor']);
1616

1717
var color = layer.paint['fill-color'];
18+
var image = layer.paint['fill-pattern'];
19+
var opacity = layer.paint['fill-opacity'] || 1;
20+
var shader;
1821

1922
var vertex, elements, group, count;
2023

21-
// Draw the stencil mask.
22-
23-
// We're only drawing to the first seven bits (== support a maximum of
24-
// 127 overlapping polygons in one place before we get rendering errors).
25-
gl.stencilMask(0x3F);
26-
gl.clear(gl.STENCIL_BUFFER_BIT);
27-
28-
// Draw front facing triangles. Wherever the 0x80 bit is 1, we are
29-
// increasing the lower 7 bits by one if the triangle is a front-facing
30-
// triangle. This means that all visible polygons should be in CCW
31-
// orientation, while all holes (see below) are in CW orientation.
32-
gl.stencilFunc(gl.NOTEQUAL, 0x80, 0x80);
33-
34-
// When we do a nonzero fill, we count the number of times a pixel is
35-
// covered by a counterclockwise polygon, and subtract the number of
36-
// times it is "uncovered" by a clockwise polygon.
37-
gl.stencilOpSeparate(gl.FRONT, gl.INCR_WRAP, gl.KEEP, gl.KEEP);
38-
gl.stencilOpSeparate(gl.BACK, gl.DECR_WRAP, gl.KEEP, gl.KEEP);
39-
40-
// When drawing a shape, we first draw all shapes to the stencil buffer
41-
// and incrementing all areas where polygons are
42-
gl.colorMask(false, false, false, false);
43-
44-
// Draw the actual triangle fan into the stencil buffer.
45-
gl.switchShader(painter.fillShader, translatedPosMatrix);
46-
47-
// Draw all buffers
4824
vertex = tile.buffers.fillVertex;
4925
vertex.bind(gl);
5026

5127
elements = tile.buffers.fillElement;
5228
elements.bind(gl);
5329

54-
var offset, elementOffset;
55-
56-
for (var i = 0; i < elementGroups.groups.length; i++) {
57-
group = elementGroups.groups[i];
58-
offset = group.vertexStartIndex * vertex.itemSize;
59-
vertex.setAttribPointers(gl, painter.fillShader, offset);
60-
61-
count = group.elementLength * 3;
62-
elementOffset = group.elementStartIndex * elements.itemSize;
63-
gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
64-
}
65-
66-
// Now that we have the stencil mask in the stencil buffer, we can start
67-
// writing to the color buffer.
68-
gl.colorMask(true, true, true, true);
69-
70-
// From now on, we don't want to update the stencil buffer anymore.
71-
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
72-
gl.stencilMask(0x0);
73-
74-
var strokeColor = layer.paint['fill-outline-color'];
75-
76-
// Because we're drawing top-to-bottom, and we update the stencil mask
77-
// below, we have to draw the outline first (!)
78-
if (layer.paint['fill-antialias'] === true && !(layer.paint['fill-pattern'] && !strokeColor)) {
79-
gl.switchShader(painter.outlineShader, translatedPosMatrix);
80-
gl.lineWidth(2 * browser.devicePixelRatio);
81-
82-
if (strokeColor) {
83-
// If we defined a different color for the fill outline, we are
84-
// going to ignore the bits in 0x3F and just care about the global
85-
// clipping mask.
86-
gl.stencilFunc(gl.EQUAL, 0x80, 0x80);
87-
} else {
88-
// Otherwise, we only want to draw the antialiased parts that are
89-
// *outside* the current shape. This is important in case the fill
90-
// or stroke color is translucent. If we wouldn't clip to outside
91-
// the current shape, some pixels from the outline stroke overlapped
92-
// the (non-antialiased) fill.
93-
gl.stencilFunc(gl.EQUAL, 0x80, 0xBF);
94-
}
95-
96-
gl.uniform2f(painter.outlineShader.u_world, gl.drawingBufferWidth, gl.drawingBufferHeight);
97-
gl.uniform4fv(painter.outlineShader.u_color, strokeColor ? strokeColor : color);
98-
99-
// Draw all buffers
100-
vertex = tile.buffers.fillVertex;
101-
elements = tile.buffers.outlineElement;
102-
elements.bind(gl);
103-
104-
for (var k = 0; k < elementGroups.groups.length; k++) {
105-
group = elementGroups.groups[k];
106-
offset = group.vertexStartIndex * vertex.itemSize;
107-
vertex.setAttribPointers(gl, painter.outlineShader, offset);
108-
109-
count = group.secondElementLength * 2;
110-
elementOffset = group.secondElementStartIndex * elements.itemSize;
111-
gl.drawElements(gl.LINES, count, gl.UNSIGNED_SHORT, elementOffset);
112-
}
113-
}
114-
115-
var image = layer.paint['fill-pattern'];
116-
var opacity = layer.paint['fill-opacity'] || 1;
117-
var shader;
118-
11930
if (image) {
12031
// Draw texture fill
12132
var imagePosA = painter.spriteAtlas.getPosition(image.from, true);
@@ -154,16 +65,44 @@ function drawFill(painter, layer, posMatrix, tile) {
15465
} else {
15566
// Draw filling rectangle.
15667
shader = painter.fillShader;
157-
gl.switchShader(shader, posMatrix);
68+
gl.switchShader(shader, translatedPosMatrix);
15869
gl.uniform4fv(shader.u_color, color);
15970
}
16071

161-
// Only draw regions that we marked
162-
gl.stencilFunc(gl.NOTEQUAL, 0x0, 0x3F);
163-
gl.bindBuffer(gl.ARRAY_BUFFER, painter.tileExtentBuffer);
164-
gl.vertexAttribPointer(shader.a_pos, painter.tileExtentBuffer.itemSize, gl.SHORT, false, 0, 0);
165-
gl.drawArrays(gl.TRIANGLE_STRIP, 0, painter.tileExtentBuffer.itemCount);
72+
var offset, elementOffset;
73+
74+
for (var i = 0; i < elementGroups.groups.length; i++) {
75+
group = elementGroups.groups[i];
76+
offset = group.vertexStartIndex * vertex.itemSize;
77+
vertex.setAttribPointers(gl, painter.fillShader, offset);
78+
79+
count = group.elementLength;
80+
elementOffset = group.elementStartIndex * elements.itemSize;
81+
gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
82+
}
83+
84+
var strokeColor = layer.paint['fill-outline-color'];
16685

167-
gl.stencilMask(0x00);
168-
gl.stencilFunc(gl.EQUAL, 0x80, 0x80);
86+
// Because we're drawing top-to-bottom, we have to draw the outline first
87+
if (layer.paint['fill-antialias'] === true && !(layer.paint['fill-pattern'] && !strokeColor)) {
88+
gl.switchShader(painter.outlineShader, translatedPosMatrix);
89+
gl.lineWidth(2 * browser.devicePixelRatio);
90+
91+
gl.uniform2f(painter.outlineShader.u_world, gl.drawingBufferWidth, gl.drawingBufferHeight);
92+
gl.uniform4fv(painter.outlineShader.u_color, strokeColor ? strokeColor : color);
93+
94+
// Draw all buffers
95+
elements = tile.buffers.outlineElement;
96+
elements.bind(gl);
97+
98+
for (var k = 0; k < elementGroups.groups.length; k++) {
99+
group = elementGroups.groups[k];
100+
offset = group.vertexStartIndex * vertex.itemSize;
101+
vertex.setAttribPointers(gl, painter.outlineShader, offset);
102+
103+
count = group.secondElementLength * 2;
104+
elementOffset = group.secondElementStartIndex * elements.itemSize;
105+
gl.drawElements(gl.LINES, count, gl.UNSIGNED_SHORT, elementOffset);
106+
}
107+
}
169108
}

js/util/classify_rings.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
module.exports = classifyRings;
4+
5+
// classifies an array of rings into polygons with outer rings and holes
6+
7+
function classifyRings(rings) {
8+
var len = rings.length;
9+
10+
if (len <= 1) return [rings];
11+
12+
var polygons = [],
13+
polygon,
14+
ccw;
15+
16+
for (var i = 0; i < len; i++) {
17+
var area = signedArea(rings[i]);
18+
if (area === 0) continue;
19+
20+
if (!ccw) ccw = area < 0;
21+
22+
if (ccw === area < 0) {
23+
if (polygon) polygons.push(polygon);
24+
polygon = [rings[i]];
25+
26+
} else {
27+
polygon.push(rings[i]);
28+
}
29+
}
30+
if (polygon) polygons.push(polygon);
31+
32+
return polygons;
33+
}
34+
35+
function signedArea(ring) {
36+
var sum = 0;
37+
for (var i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
38+
p1 = ring[i];
39+
p2 = ring[j];
40+
sum += (p2[0] - p1[0]) * (p1[1] + p2[1]);
41+
}
42+
return sum;
43+
}

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"dependencies": {
1212
"brfs": "^1.4.0",
1313
"csscolorparser": "^1.0.2",
14+
"earcut": "^2.0.3",
1415
"envify": "^3.4.0",
1516
"feature-filter": "^1.0.2",
1617
"geojson-vt": "^2.1.0",

test/js/data/fill_bucket.test.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,16 @@ test('FillBucket', function(t) {
2222
var bucket = new FillBucket(buffers);
2323
t.ok(bucket);
2424

25-
t.equal(bucket.addFill([
25+
t.equal(bucket.addFeature([[
2626
new Point(0, 0),
2727
new Point(10, 10)
28-
]), undefined);
28+
]]), undefined);
2929

30-
t.equal(bucket.addFill([
30+
t.equal(bucket.addFeature([[
3131
new Point(0, 0),
3232
new Point(10, 10),
3333
new Point(10, 20)
34-
]), undefined);
34+
]]), undefined);
3535

3636
t.equal(bucket.addFeature(feature.loadGeometry()), undefined);
3737

0 commit comments

Comments
 (0)