Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

In-browser polygon tesselation #948

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions debug/style.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,7 @@
}
}, {
"id": "water_offset",
"type": "fill",
"source": "mapbox",
"source-layer": "water",
"ref": "water",
"paint": {
"fill-color": "white",
"fill-opacity": 0.3,
Expand Down
6 changes: 2 additions & 4 deletions js/data/buffer/fill_elements_buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
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.index);
this.groups.push(this.current);
}
return this.current;
};

function ElementGroup(vertexStartIndex, elementStartIndex, secondElementStartIndex) {
Expand Down
99 changes: 59 additions & 40 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 @@ -10,67 +12,84 @@ function FillBucket(buffers) {
}

FillBucket.prototype.addFeatures = function() {
var start = self.performance.now();
self.tesselateTime = self.tesselateTime || 0;

var features = this.features;
for (var i = 0; i < features.length; i++) {
for (var i = this.features.length - 1; i >= 0; i--) {
var feature = features[i];
this.addFeature(feature.loadGeometry());
}

self.tesselateTime += self.performance.now() - start;
};

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) {

// 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 numVertices = 0;
for (var k = 0; k < polygon.length; k++) {
numVertices += polygon[k].length;
}

var len = vertices.length;
var fillVertex = this.buffers.fillVertex,
fillElement = this.buffers.fillElement,
outlineElement = this.buffers.outlineElement,
elementGroup = this.elementGroups.makeRoomFor(numVertices),
startIndex = fillVertex.index - elementGroup.vertexStartIndex,
flattened = [],
holeIndices = [],
prevIndex;

// Check whether this geometry buffer can hold all the required vertices.
this.elementGroups.makeRoomFor(len + 1);
var elementGroup = this.elementGroups.current;
for (var r = 0; r < polygon.length; r++) {
var ring = polygon[r];
prevIndex = undefined;

var fillVertex = this.buffers.fillVertex;
var fillElement = this.buffers.fillElement;
var outlineElement = this.buffers.outlineElement;
if (r > 0) holeIndices.push(flattened.length / 2);

// Start all lines with a degenerate vertex
elementGroup.vertexLength++;
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.index - elementGroup.vertexStartIndex,
prevIndex, currentIndex, currentVertex;
var currentIndex = fillVertex.index - elementGroup.vertexStartIndex;
fillVertex.add(vertex[0], vertex[1]);
elementGroup.vertexLength++;

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

fillVertex.add(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.add(firstIndex, prevIndex, currentIndex);
elementGroup.elementLength++;
// convert to format used by earcut
flattened.push(vertex[0]);
flattened.push(vertex[1]);
}
}

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

prevIndex = currentIndex;
for (var i = 0; i < triangleIndices.length; i++) {
fillElement.add(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;
}
133 changes: 37 additions & 96 deletions js/render/draw_fill.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,99 +18,12 @@ function drawFill(painter, layer, posMatrix, tile) {

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;
gl.vertexAttribPointer(painter.fillShader.a_pos, 2, gl.SHORT, false, 4, offset + 0);

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

// 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;
gl.vertexAttribPointer(painter.outlineShader.a_pos, 2, gl.SHORT, false, 4, offset + 0);

count = group.secondElementLength * 2;
elementOffset = group.secondElementStartIndex * elements.itemSize;
gl.drawElements(gl.LINES, count, gl.UNSIGNED_SHORT, elementOffset);
}
}

var image = layer.paint['fill-image'];
var opacity = layer.paint['fill-opacity'] || 1;
var shader;
Expand Down Expand Up @@ -149,20 +62,48 @@ function drawFill(painter, layer, posMatrix, tile) {
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.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;
gl.vertexAttribPointer(shader.a_pos, 2, gl.SHORT, false, 4, offset + 0);

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

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

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

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;
gl.vertexAttribPointer(painter.outlineShader.a_pos, 2, gl.SHORT, false, 4, offset + 0);

gl.stencilMask(0x00);
gl.stencilFunc(gl.EQUAL, 0x80, 0x80);
count = group.secondElementLength * 2;
elementOffset = group.secondElementStartIndex * elements.itemSize;
gl.drawElements(gl.LINES, count, gl.UNSIGNED_SHORT, elementOffset);
}
}
}
2 changes: 2 additions & 0 deletions js/source/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
Loading