Skip to content

Commit

Permalink
switch to using element groups for lines
Browse files Browse the repository at this point in the history
This starts bringing rendering closer to how its done in -native.

Lines are added to a single vertex and element buffer. If the number of
vertices exceeds the maximum number, instead of creating a new buffer it
creates a new element group, and stores the offset of that group into
the buffer.
  • Loading branch information
ansis committed Jun 13, 2014
1 parent 4b795ed commit 1acfa85
Show file tree
Hide file tree
Showing 7 changed files with 314 additions and 21 deletions.
14 changes: 14 additions & 0 deletions js/geometry/createbucket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
'use strict';

module.exports = createBucket;

var Bucket = require('./bucket.js');
var LineBucket = require('./linebucket.js');

function createBucket(info, geometry, placement, indices, buffers) {
if (info.line) {
return new LineBucket(info, buffers, placement, indices);
} else {
return new Bucket(info, geometry, placement, indices);
}
}
29 changes: 29 additions & 0 deletions js/geometry/elementgroups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

module.exports = ElementGroups;

function ElementGroups(vertexBuffer, elementBuffer, groups) {

this.vertexBuffer = vertexBuffer;
this.elementBuffer = elementBuffer;

if (groups) {
this.groups = groups;
} else {
this.groups = [];
}
}

ElementGroups.prototype.makeRoomFor = function(numVertices) {
if (!this.current || this.current.vertexLength + numVertices > 65535) {
this.current = new ElementGroup(this.vertexBuffer.index, this.elementBuffer.index);
this.groups.push(this.current);
}
};

function ElementGroup(vertexStartIndex, elementStartIndex) {
// the offset into the vertex buffer of the first vertex in this group
this.vertexStartIndex = vertexStartIndex;
this.elementStartIndex = elementStartIndex;
this.elementLength = 0;
}
224 changes: 224 additions & 0 deletions js/geometry/linebucket.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
'use strict';

var ElementGroups = require('./elementgroups.js');

module.exports = LineBucket;

function LineBucket(info, buffers, placement, elementGroups) {
this.info = info;
this.buffers = buffers;
this.elementGroups = elementGroups || new ElementGroups(buffers.lineVertex, buffers.lineElement);
}

LineBucket.prototype.addFeature = function(lines) {
var info = this.info;
for (var i = 0; i < lines.length; i++) {
this.addLine(lines[i], info['line-join'], info['line-cap'],
info['line-miter-limit'], info['line-round-limit']);
}
};

LineBucket.prototype.addLine = function(vertices, join, cap, miterLimit, roundLimit) {
if (vertices.length < 2) {
console.warn('a line must have at least two vertices');
return;
}

var len = vertices.length,
firstVertex = vertices[0],
lastVertex = vertices[len - 1],
closed = firstVertex.equals(lastVertex);

var lineVertex = this.buffers.lineVertex;
var lineElement = this.buffers.lineElement;

// we could be more precies, but it would only save a negligible amount of space
this.elementGroups.makeRoomFor(len * 4);
var elementGroup = this.elementGroups.current;
var vertexStartIndex = elementGroup.vertexStartIndex;

if (len == 2 && closed) {
// console.warn('a line may not have coincident points');
return;
}

join = join || 'miter';
cap = cap || 'butt';
miterLimit = miterLimit || 2;
roundLimit = roundLimit || 1;

var beginCap = cap,
endCap = closed ? 'butt' : cap,
flip = 1,
distance = 0,
currentVertex, prevVertex, nextVertex, prevNormal, nextNormal;

// the last three vertices added
var e1, e2, e3;

if (closed) {
currentVertex = vertices[len - 2];
nextNormal = firstVertex.sub(currentVertex)._unit()._perp();
}

for (var i = 0; i < len; i++) {

nextVertex = closed && i === len - 1 ?
vertices[1] : // if the line is closed, we treat the last vertex like the first
vertices[i + 1]; // just the next vertex

// if two consecutive vertices exist, skip the current one
if (nextVertex && vertices[i].equals(nextVertex)) continue;

if (nextNormal) prevNormal = nextNormal;
if (currentVertex) prevVertex = currentVertex;

currentVertex = vertices[i];

// Calculate how far along the line the currentVertex is
if (prevVertex) distance += currentVertex.dist(prevVertex);

// Calculate the normal towards the next vertex in this line. In case
// there is no next vertex, pretend that the line is continuing straight,
// meaning that we are just using the previous normal.
nextNormal = nextVertex ? nextVertex.sub(currentVertex)._unit()._perp() : prevNormal;

// If we still don't have a previous normal, this is the beginning of a
// non-closed line, so we're doing a straight "join".
prevNormal = prevNormal || nextNormal;

// Determine the normal of the join extrusion. It is the angle bisector
// of the segments between the previous line and the next line.
var joinNormal = prevNormal.add(nextNormal)._unit();

/* joinNormal prevNormal
* ↖ ↑
* .________. prevVertex
* |
* nextNormal ← | currentVertex
* |
* nextVertex !
*
*/

// Calculate the length of the miter (the ratio of the miter to the width).
// Find the cosine of the angle between the next and join normals
// using dot product. The inverse of that is the miter length.
var cosHalfAngle = joinNormal.x * nextNormal.x + joinNormal.y * nextNormal.y;
var miterLength = 1 / cosHalfAngle;

// Whether any vertices have been
var startOfLine = e1 === undefined || e2 === undefined;

// The join if a middle vertex, otherwise the cap.
var currentJoin = (prevVertex && nextVertex) ? join :
nextVertex ? beginCap : endCap;

if (currentJoin === 'round' && miterLength < roundLimit) {
currentJoin = 'miter';
}

if (currentJoin === 'miter' && miterLength > miterLimit && miterLength < Math.SQRT2) {
currentJoin = 'bevel';
}

// Mitered joins
if (currentJoin === 'miter') {

if (miterLength > 100) {
// Almost parallel lines
flip = -flip;
joinNormal = nextNormal;

} else if (miterLength > miterLimit) {
flip = -flip;
// miter is too big, flip the direction to make a beveled join
var bevelLength = miterLength * prevNormal.add(nextNormal).mag() / prevNormal.sub(nextNormal).mag();
joinNormal._perp()._mult(flip * bevelLength);

} else {
// scale the unit vector by the miter length
joinNormal._mult(miterLength);
}

addCurrentVertex(joinNormal, 0, false);

// All other types of joins
} else {

// Close previous segment with a butt or a square cap
if (!startOfLine) {
addCurrentVertex(prevNormal, currentJoin === 'square' ? 1 : 0, false);
}

// Add round cap or linejoin at end of segment
if (!startOfLine && currentJoin === 'round') {
addCurrentVertex(prevNormal, 1, true);
}

// Segment include cap are done, unset vertices to disconnect segments.
// Or leave them to create a bevel.
if (startOfLine || currentJoin !== 'bevel') {
e1 = e2 = -1;
flip = 1;
}

// Add round cap before first segment
if (startOfLine && beginCap === 'round') {
addCurrentVertex(nextNormal, -1, true);
}

// Start next segment with a butt or square cap
if (nextVertex) {
addCurrentVertex(nextNormal, currentJoin === 'square' ? -1 : 0, false);
}
}

}


/*
* Adds two vertices to the buffer that are
* normal and -normal from the currentVertex.
*
* endBox moves the extrude one unit in the direction of the line
* to create square or round cap.
*
* endBox === 1 moves the extrude in the direction of the line
* endBox === -1 moves the extrude in the reverse direction
*/
function addCurrentVertex(normal, endBox, round) {

var tx = round ? 1 : 0;
var extrude;

extrude = normal.mult(flip);
if (endBox) extrude._sub(normal.perp()._mult(endBox));
e3 = lineVertex.add(currentVertex, extrude, tx, 0, distance) - vertexStartIndex;
if (e1 >= 0 && e2 >= 0) {
lineElement.add(e1, e2, e3);
elementGroup.elementLength++;
}
e1 = e2; e2 = e3;

extrude = normal.mult(-flip);
if (endBox) extrude._sub(normal.perp()._mult(endBox));
e3 = lineVertex.add(currentVertex, extrude, tx, 1, distance) - vertexStartIndex;
if (e1 >= 0 && e2 >= 0) {
lineElement.add(e1, e2, e3);
elementGroup.elementLength++;
}
e1 = e2; e2 = e3;
}
};

LineBucket.prototype.toJSON = function() {
return {
indices: this.elementGroups
};
};

LineBucket.prototype.start = function() {
};
LineBucket.prototype.end = function() {
};
28 changes: 13 additions & 15 deletions js/render/drawline.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,21 @@ module.exports = function drawLine(gl, painter, bucket, layerStyle, posMatrix, p
}
gl.uniform4fv(shader.u_color, color);

var buffer = bucket.indices.lineBufferIndex;
while (buffer <= bucket.indices.lineBufferIndexEnd) {
var vertex = bucket.geometry.lineBuffers[buffer].vertex;
vertex.bind(gl);

var elements = bucket.geometry.lineBuffers[buffer].element;
elements.bind(gl);
var vertex = bucket.buffers.lineVertex;
vertex.bind(gl);
var element = bucket.buffers.lineElement;
element.bind(gl);

gl.vertexAttribPointer(shader.a_pos, 4, gl.SHORT, false, 8, 0);
gl.vertexAttribPointer(shader.a_extrude, 2, gl.BYTE, false, 8, 6);
gl.vertexAttribPointer(shader.a_linesofar, 2, gl.SHORT, false, 8, 4);
bucket.elementGroups.groups.forEach(function(group) {

This comment has been minimized.

Copy link
@mourner

mourner Jun 14, 2014

Member

It's better to avoid forEach in perf-sensitive places (like functions called many times on each frame). Simple for loop is much faster and doesn't create a closure every time.

var offset = group.vertexStartIndex * vertex.itemSize;
gl.vertexAttribPointer(shader.a_pos, 4, gl.SHORT, false, 8, offset + 0);
gl.vertexAttribPointer(shader.a_extrude, 2, gl.BYTE, false, 8, offset + 6);
gl.vertexAttribPointer(shader.a_linesofar, 2, gl.SHORT, false, 8, offset + 4);

var begin = buffer == bucket.indices.lineBufferIndex ? bucket.indices.lineElementIndex : 0;
var end = buffer == bucket.indices.lineBufferIndexEnd ? bucket.indices.lineElementIndexEnd : elements.index;
var count = group.elementLength * 3;
var elementOffset = group.elementStartIndex * 6;
gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, elementOffset);
});

gl.drawElements(gl.TRIANGLES, (end - begin) * 3, gl.UNSIGNED_SHORT, begin * 6);

buffer++;
}
};
2 changes: 1 addition & 1 deletion js/render/painter.js
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ GLPainter.prototype.applyStyle = function(layer, style, buckets, params) {

var bucket = buckets[layer.bucket];
// There are no vertices yet for this layer.
if (!bucket || !bucket.indices) return;
if (!bucket || !(bucket.indices || bucket.elementGroups)) return;

var info = bucket.info;

Expand Down
11 changes: 9 additions & 2 deletions js/ui/vectortile.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ var Tile = require('./tile.js'),
FillElementsBuffer = require('../geometry/fillelementsbuffer.js'),
GlyphVertexBuffer = require('../geometry/glyphvertexbuffer.js'),
PointVertexBuffer = require('../geometry/pointvertexbuffer.js'),
Bucket = require('../geometry/bucket.js'),
util = require('../util/util.js');

var createBucket = require('../geometry/createbucket.js');

module.exports = VectorTile;

function VectorTile(id, source, url, callback) {
Expand Down Expand Up @@ -72,9 +73,15 @@ VectorTile.prototype.onTileLoad = function(data) {
d.elements = new FillElementsBuffer(d.elements);
});

this.buffers = data.buffers;
this.buffers.glyphVertex = new GlyphVertexBuffer(this.buffers.glyphVertex);
this.buffers.pointVertex = new PointVertexBuffer(this.buffers.pointVertex);
this.buffers.lineVertex = new LineVertexBuffer(this.buffers.lineVertex);
this.buffers.lineElement = new LineElementBuffer(this.buffers.lineElement);

this.buckets = {};
for (var b in data.buckets) {
this.buckets[b] = new Bucket(this.map.style.stylesheet.buckets[b], this.geometry, undefined, data.buckets[b].indices);
this.buckets[b] = createBucket(this.map.style.stylesheet.buckets[b], this.geometry, undefined, data.buckets[b].indices, this.buffers);
}

this.loaded = true;
Expand Down
Loading

1 comment on commit 1acfa85

@kkaefer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Please sign in to comment.