Skip to content

Commit

Permalink
switch to tesselated fill rendering with earcut
Browse files Browse the repository at this point in the history
  • Loading branch information
mourner committed Oct 8, 2015
1 parent 739aa39 commit 9ccb6a5
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 141 deletions.
6 changes: 5 additions & 1 deletion js/data/buffer_set.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ var outlineElementOptions = {type: Buffer.BufferType.ELEMENT, attributes: [
{name: 'vertices', components: 2, type: Buffer.ELEMENT_ATTRIBUTE_TYPE}
]};

var fillElementOptions = {type: Buffer.BufferType.ELEMENT, attributes: [
{name: 'vertices', components: 1, type: Buffer.ELEMENT_ATTRIBUTE_TYPE}
]};

var bufferOptions = {
glyphVertex: symbolVertexOptions,
glyphElement: triangleElementOptions,
Expand All @@ -42,7 +46,7 @@ var bufferOptions = {
circleVertex: fillVertexOptions,
circleElement: triangleElementOptions,
fillVertex: fillVertexOptions,
fillElement: triangleElementOptions,
fillElement: fillElementOptions,
outlineElement: outlineElementOptions,
lineVertex: lineVertexOptions,
lineElement: triangleElementOptions,
Expand Down
1 change: 1 addition & 0 deletions js/data/element_groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ElementGroups.prototype.makeRoomFor = function(numVertices) {
this.secondElementBuffer && this.secondElementBuffer.length);
this.groups.push(this.current);
}
return this.current;
};

function ElementGroup(vertexStartIndex, elementStartIndex, secondElementStartIndex) {
Expand Down
87 changes: 51 additions & 36 deletions js/data/fill_bucket.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
'use strict';

var ElementGroups = require('./element_groups');
var earcut = require('earcut');
var classifyRings = require('../util/classify_rings');

module.exports = FillBucket;

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

FillBucket.prototype.addFeature = function(lines) {
for (var i = 0; i < lines.length; i++) {
this.addFill(lines[i]);
var polygons = classifyRings(convertCoords(lines));
for (var i = 0; i < polygons.length; i++) {
this.addPolygon(polygons[i]);
}
};

FillBucket.prototype.addFill = function(vertices) {
if (vertices.length < 3) {
//console.warn('a fill must have at least three vertices');
return;
FillBucket.prototype.addPolygon = function(polygon) {
var numVertices = 0;
for (var k = 0; k < polygon.length; k++) {
numVertices += polygon[k].length;
}

// 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 fillVertex = this.buffers.fillVertex,
fillElement = this.buffers.fillElement,
outlineElement = this.buffers.outlineElement,
elementGroup = this.elementGroups.makeRoomFor(numVertices),
startIndex = fillVertex.length - elementGroup.vertexStartIndex,
flattened = [],
holeIndices = [],
prevIndex;

var len = vertices.length;
for (var r = 0; r < polygon.length; r++) {
var ring = polygon[r];

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

var fillVertex = this.buffers.fillVertex;
var fillElement = this.buffers.fillElement;
var outlineElement = this.buffers.outlineElement;
for (var v = 0; v < ring.length; v++) {
var vertex = ring[v];

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

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

fillVertex.push(currentVertex.x, currentVertex.y);
elementGroup.vertexLength++;
prevIndex = currentIndex;

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

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

prevIndex = currentIndex;
for (var i = 0; i < triangleIndices.length; i++) {
fillElement.push(triangleIndices[i] + startIndex);
elementGroup.elementLength += 1;
}
};

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;
}
139 changes: 39 additions & 100 deletions js/render/draw_fill.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,107 +15,18 @@ 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-pattern'];
var opacity = layer.paint['fill-opacity'] || 1;
var shader;

var vertex, elements, group, count;

// Draw the stencil mask.

// 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);

// 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);

// 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);

// 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);

// Draw the actual triangle fan into the stencil buffer.
gl.switchShader(painter.fillShader, translatedPosMatrix);

// 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++) {
group = elementGroups.groups[i];
offset = group.vertexStartIndex * vertex.itemSize;
vertex.setAttribPointers(gl, painter.fillShader, offset);

count = group.elementLength * 3;
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);

// 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.outlineElement;
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) {
// Draw texture fill
var imagePosA = painter.spriteAtlas.getPosition(image.from, true);
Expand Down Expand Up @@ -154,16 +65,44 @@ function drawFill(painter, layer, posMatrix, tile) {
} else {
// Draw filling rectangle.
shader = painter.fillShader;
gl.switchShader(shader, posMatrix);
gl.switchShader(shader, translatedPosMatrix);
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);
var offset, elementOffset;

for (var i = 0; i < elementGroups.groups.length; i++) {
group = elementGroups.groups[i];
offset = group.vertexStartIndex * vertex.itemSize;
vertex.setAttribPointers(gl, painter.fillShader, offset);

count = group.elementLength;
elementOffset = group.elementStartIndex * elements.itemSize;
gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
}

var strokeColor = layer.paint['fill-outline-color'];

gl.stencilMask(0x00);
gl.stencilFunc(gl.EQUAL, 0x80, 0x80);
// Because we're drawing top-to-bottom, 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);

gl.uniform2f(painter.outlineShader.u_world, gl.drawingBufferWidth, gl.drawingBufferHeight);
gl.uniform4fv(painter.outlineShader.u_color, strokeColor ? strokeColor : color);

// Draw all buffers
elements = tile.buffers.outlineElement;
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);
}
}
}
43 changes: 43 additions & 0 deletions js/util/classify_rings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
'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 polygons = [],
polygon,
ccw;

for (var i = 0; i < len; i++) {
var area = signedArea(rings[i]);
if (area === 0) continue;

if (!ccw) ccw = area < 0;

if (ccw === area < 0) {
if (polygon) polygons.push(polygon);
polygon = [rings[i]];

} else {
polygon.push(rings[i]);
}
}
polygons.push(polygon);

return polygons;
}

function signedArea(ring) {
var sum = 0;
for (var i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
p1 = ring[i];
p2 = ring[j];
sum += (p2[0] - p1[0]) * (p1[1] + p2[1]);
}
return sum;
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"dependencies": {
"brfs": "^1.4.0",
"csscolorparser": "^1.0.2",
"earcut": "^2.0.3",
"envify": "^3.4.0",
"feature-filter": "^1.0.2",
"geojson-vt": "^2.1.0",
Expand Down
8 changes: 4 additions & 4 deletions test/js/data/fill_bucket.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ test('FillBucket', function(t) {
var bucket = new FillBucket(buffers);
t.ok(bucket);

t.equal(bucket.addFill([
t.equal(bucket.addFeature([[
new Point(0, 0),
new Point(10, 10)
]), undefined);
]]), undefined);

t.equal(bucket.addFill([
t.equal(bucket.addFeature([[
new Point(0, 0),
new Point(10, 10),
new Point(10, 20)
]), undefined);
]]), undefined);

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

Expand Down

0 comments on commit 9ccb6a5

Please sign in to comment.