Skip to content

Commit

Permalink
improve line label legibility in pitched views
Browse files Browse the repository at this point in the history
The correct position of a glyph along is now found by doing the work on
the CPU where we have access to the entire line geometry.

For performance, as much work as possible is left to be done in the
shaders. This includes all the work for point horizontal labels.

ref #4718
  • Loading branch information
ansis committed Jun 12, 2017
1 parent 3cdc10b commit 1d1b669
Show file tree
Hide file tree
Showing 34 changed files with 676 additions and 617 deletions.
8 changes: 8 additions & 0 deletions src/data/array_group.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ class Segment {
* A group has:
*
* * A "layout" vertex array, with fixed attributes, containing values calculated from layout properties.
* * Zero or one dynamic "layout" vertex arrays, with fixed attributes containing values that can be
* * recalculated each frame on the cpu.
* * Zero, one, or two element arrays, with fixed layout, for eventual `gl.drawElements` use.
* * Zero or more "paint" vertex arrays keyed by layer ID, each with a dynamic layout which depends
* on which paint properties of that layer use data-driven-functions (property functions or
Expand All @@ -38,6 +40,11 @@ class ArrayGroup {
const LayoutVertexArrayType = createVertexArrayType(programInterface.layoutAttributes);
this.layoutVertexArray = new LayoutVertexArrayType();

if (programInterface.dynamicLayoutAttributes) {
const DynamicLayoutVertexArrayType = createVertexArrayType(programInterface.dynamicLayoutAttributes);
this.dynamicLayoutVertexArray = new DynamicLayoutVertexArrayType();
}

const ElementArrayType = programInterface.elementArrayType;
if (ElementArrayType) this.elementArray = new ElementArrayType();

Expand Down Expand Up @@ -99,6 +106,7 @@ class ArrayGroup {
serialize(transferables) {
return {
layoutVertexArray: this.layoutVertexArray.serialize(transferables),
dynamicLayoutVertexArray: this.dynamicLayoutVertexArray && this.dynamicLayoutVertexArray.serialize(transferables),
elementArray: this.elementArray && this.elementArray.serialize(transferables),
elementArray2: this.elementArray2 && this.elementArray2.serialize(transferables),
paintVertexArrays: serializePaintVertexArrays(this.layerData, transferables),
Expand Down
177 changes: 124 additions & 53 deletions src/data/bucket/symbol_bucket.js

Large diffs are not rendered by default.

21 changes: 18 additions & 3 deletions src/data/buffer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const AttributeType = {
Int8: 'BYTE',
Uint8: 'UNSIGNED_BYTE',
Int16: 'SHORT',
Uint16: 'UNSIGNED_SHORT'
Uint16: 'UNSIGNED_SHORT',
Float32: 'FLOAT'
};

/**
Expand All @@ -22,14 +23,16 @@ class Buffer {
* @param {Object} array A serialized StructArray.
* @param {Object} arrayType A serialized StructArrayType.
* @param {BufferType} type
* @param {boolean} dynamicDraw Whether this buffer will be repeatedly updated.
*/
constructor(array, arrayType, type) {
constructor(array, arrayType, type, dynamicDraw) {
this.arrayBuffer = array.arrayBuffer;
this.length = array.length;
this.attributes = arrayType.members;
this.itemSize = arrayType.bytesPerElement;
this.type = type;
this.arrayType = arrayType;
this.dynamicDraw = dynamicDraw;
}

static fromStructArray(array, type) {
Expand All @@ -47,15 +50,27 @@ class Buffer {
this.gl = gl;
this.buffer = gl.createBuffer();
gl.bindBuffer(type, this.buffer);
gl.bufferData(type, this.arrayBuffer, gl.STATIC_DRAW);
gl.bufferData(type, this.arrayBuffer, this.dynamicDraw ? gl.DYNAMIC_DRAW : gl.STATIC_DRAW);

// dump array buffer once it's bound to gl
this.arrayBuffer = null;
} else {
gl.bindBuffer(type, this.buffer);

if (this.dynamicDraw && this.arrayBuffer) {
gl.bufferSubData(type, 0, this.arrayBuffer);
this.arrayBuffer = null;
}
}
}

/*
* @param {Object} array A serialized StructArray.
*/
updateData(array) {
this.arrayBuffer = array.arrayBuffer;
}

enableAttributes (gl, program) {
for (let j = 0; j < this.attributes.length; j++) {
const member = this.attributes[j];
Expand Down
10 changes: 10 additions & 0 deletions src/data/buffer_group.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ class BufferGroup {
this.layoutVertexBuffer = new Buffer(arrays.layoutVertexArray,
LayoutVertexArrayType.serialize(), Buffer.BufferType.VERTEX);

if (arrays.dynamicLayoutVertexArray) {
const DynamicLayoutVertexArrayType = createVertexArrayType(programInterface.dynamicLayoutAttributes);
this.dynamicLayoutVertexArray = new DynamicLayoutVertexArrayType(arrays.dynamicLayoutVertexArray);
this.dynamicLayoutVertexBuffer = new Buffer(arrays.dynamicLayoutVertexArray,
DynamicLayoutVertexArrayType.serialize(), Buffer.BufferType.VERTEX, true);
}

if (arrays.elementArray) {
this.elementBuffer = new Buffer(arrays.elementArray,
programInterface.elementArrayType.serialize(), Buffer.BufferType.ELEMENT);
Expand Down Expand Up @@ -49,6 +56,9 @@ class BufferGroup {
destroy() {
this.layoutVertexBuffer.destroy();

if (this.dynamicLayoutVertexBuffer) {
this.dynamicLayoutVertexBuffer.destroy();
}
if (this.elementBuffer) {
this.elementBuffer.destroy();
}
Expand Down
109 changes: 30 additions & 79 deletions src/render/draw_symbol.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
'use strict';

const assert = require('assert');
const util = require('../util/util');
const drawCollisionDebug = require('./draw_collision_debug');
const pixelsToTileUnits = require('../source/pixels_to_tile_units');
const interpolationFactor = require('../style-spec/function').interpolationFactor;
const symbolProjection = require('../symbol/projection');
const symbolSize = require('../symbol/symbol_size');
const mat4 = require('@mapbox/gl-matrix').mat4;
const identityMat4 = mat4.identity(new Float32Array(16));

module.exports = drawSymbols;

Expand Down Expand Up @@ -39,14 +40,16 @@ function drawSymbols(painter, sourceCache, layer, coords) {
layer.layout['icon-rotation-alignment'],
// icon-pitch-alignment is not yet implemented
// and we simply inherit the rotation alignment
layer.layout['icon-rotation-alignment']
layer.layout['icon-rotation-alignment'],
layer.layout['icon-keep-upright']
);

drawLayerSymbols(painter, sourceCache, layer, coords, true,
layer.paint['text-translate'],
layer.paint['text-translate-anchor'],
layer.layout['text-rotation-alignment'],
layer.layout['text-pitch-alignment']
layer.layout['text-pitch-alignment'],
layer.layout['text-keep-upright']
);

if (sourceCache.map.showCollisionBoxes) {
Expand All @@ -55,7 +58,7 @@ function drawSymbols(painter, sourceCache, layer, coords) {
}

function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate, translateAnchor,
rotationAlignment, pitchAlignment) {
rotationAlignment, pitchAlignment, keepUpright) {

if (!isText && painter.style.sprite && !painter.style.sprite.loaded())
return;
Expand All @@ -64,6 +67,7 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate

const rotateWithMap = rotationAlignment === 'map';
const pitchWithMap = pitchAlignment === 'map';
const alongLine = rotateWithMap && layer.layout['symbol-placement'] === 'line';

const depthOn = pitchWithMap;

Expand Down Expand Up @@ -97,13 +101,23 @@ function drawLayerSymbols(painter, sourceCache, layer, coords, isText, translate

painter.enableTileClippingMask(coord);

gl.uniformMatrix4fv(program.u_matrix, false,
painter.translatePosMatrix(coord.posMatrix, tile, translate, translateAnchor));
gl.uniformMatrix4fv(program.u_matrix, false, painter.translatePosMatrix(coord.posMatrix, tile, translate, translateAnchor));

const s = pixelsToTileUnits(tile, 1, painter.transform.zoom);
const labelPlaneMatrix = symbolProjection.getLabelPlaneMatrix(coord.posMatrix, pitchWithMap, rotateWithMap, painter.transform, s);
const glCoordMatrix = symbolProjection.getGlCoordMatrix(coord.posMatrix, pitchWithMap, rotateWithMap, painter.transform, s);
gl.uniformMatrix4fv(program.u_gl_coord_matrix, false, painter.translatePosMatrix(glCoordMatrix, tile, translate, translateAnchor, true));

if (alongLine) {
gl.uniformMatrix4fv(program.u_label_plane_matrix, false, identityMat4);
symbolProjection.updateLineLabels(bucket, coord.posMatrix, painter, isText, labelPlaneMatrix, keepUpright, s, layer);
} else {
gl.uniformMatrix4fv(program.u_label_plane_matrix, false, labelPlaneMatrix);
}

gl.uniform1f(program.u_collision_y_stretch, tile.collisionTile.yStretch);

drawTileSymbols(program, programConfiguration, painter, layer, tile, buffers, isText, isSDF,
pitchWithMap);
drawTileSymbols(program, programConfiguration, painter, layer, tile, buffers, isText, isSDF, pitchWithMap);

prevFontstack = bucket.fontstack;
}
Expand All @@ -116,7 +130,6 @@ function setSymbolDrawState(program, painter, layer, tileZoom, isText, isSDF, ro
const gl = painter.gl;
const tr = painter.transform;

gl.uniform1i(program.u_rotate_with_map, rotateWithMap);
gl.uniform1i(program.u_pitch_with_map, pitchWithMap);

gl.activeTexture(gl.TEXTURE0);
Expand Down Expand Up @@ -147,89 +160,27 @@ function setSymbolDrawState(program, painter, layer, tileZoom, isText, isSDF, ro
painter.frameHistory.bind(gl);
gl.uniform1i(program.u_fadetexture, 1);

gl.uniform1f(program.u_zoom, tr.zoom);

gl.uniform1f(program.u_pitch, tr.pitch / 360 * 2 * Math.PI);
gl.uniform1f(program.u_bearing, tr.bearing / 360 * 2 * Math.PI);
gl.uniform1f(program.u_aspect_ratio, tr.width / tr.height);

gl.uniform1i(program.u_is_size_zoom_constant, sizeData.isZoomConstant ? 1 : 0);
gl.uniform1i(program.u_is_size_feature_constant, sizeData.isFeatureConstant ? 1 : 0);

if (!sizeData.isZoomConstant && !sizeData.isFeatureConstant) {
// composite function
const t = interpolationFactor(tr.zoom,
sizeData.functionBase,
sizeData.coveringZoomRange[0],
sizeData.coveringZoomRange[1]
);
gl.uniform1f(program.u_size_t, util.clamp(t, 0, 1));
} else if (sizeData.isFeatureConstant && !sizeData.isZoomConstant) {
// camera function
let size;
if (sizeData.functionType === 'interval') {
size = layer.getLayoutValue(isText ? 'text-size' : 'icon-size',
{zoom: tr.zoom});
} else {
assert(sizeData.functionType === 'exponential');
// Even though we could get the exact value of the camera function
// at z = tr.zoom, we intentionally do not: instead, we interpolate
// between the camera function values at a pair of zoom stops covering
// [tileZoom, tileZoom + 1] in order to be consistent with this
// restriction on composite functions
const t = sizeData.functionType === 'interval' ? 0 :
interpolationFactor(tr.zoom,
sizeData.functionBase,
sizeData.coveringZoomRange[0],
sizeData.coveringZoomRange[1]);

const lowerValue = sizeData.coveringStopValues[0];
const upperValue = sizeData.coveringStopValues[1];
size = lowerValue + (upperValue - lowerValue) * util.clamp(t, 0, 1);
}

gl.uniform1f(program.u_size, size);
gl.uniform1f(program.u_layout_size, sizeData.layoutSize);
} else if (sizeData.isFeatureConstant && sizeData.isZoomConstant) {
gl.uniform1f(program.u_size, sizeData.layoutSize);
}
gl.uniform1f(program.u_camera_to_center_distance, tr.cameraToCenterDistance);
if (layer.layout['symbol-placement'] === 'line' &&
layer.layout['text-rotation-alignment'] === 'map' &&
layer.layout['text-pitch-alignment'] === 'viewport' &&
layer.layout['text-field']) {
// We hide line labels with viewport alignment as they move into the distance
// because the approximations we use for drawing their glyphs get progressively worse
// The "1.5" here means we start hiding them when the distance from the label
// to the camera is 50% greater than the distance from the center of the map
// to the camera. Depending on viewport properties, you might expect this to filter
// the top third of the screen at pitch 60, and do almost nothing at pitch 45
gl.uniform1f(program.u_max_camera_distance, 1.5);
} else {
// "10" is effectively infinite at any pitch we support
gl.uniform1f(program.u_max_camera_distance, 10);
}

const size = symbolSize.evaluateSizeForZoom(sizeData, tr, layer, isText);
if (size.uSizeT !== undefined) gl.uniform1f(program.u_size_t, size.uSizeT);
if (size.uSize !== undefined) gl.uniform1f(program.u_size, size.uSize);
}

function drawTileSymbols(program, programConfiguration, painter, layer, tile, buffers, isText, isSDF, pitchWithMap) {

const gl = painter.gl;
const tr = painter.transform;

if (pitchWithMap) {
const s = pixelsToTileUnits(tile, 1, tr.zoom);
gl.uniform2f(program.u_extrude_scale, s, s);
} else {
const s = tr.cameraToCenterDistance;
gl.uniform2f(program.u_extrude_scale,
tr.pixelsToGLUnits[0] * s,
tr.pixelsToGLUnits[1] * s);
}

if (isSDF) {
const haloWidthProperty = `${isText ? 'text' : 'icon'}-halo-width`;
const hasHalo = !layer.isPaintValueFeatureConstant(haloWidthProperty) || layer.paint[haloWidthProperty];
const gammaScale = (pitchWithMap ? Math.cos(tr._pitch) : 1) * tr.cameraToCenterDistance;
const gammaScale = (pitchWithMap ? Math.cos(tr._pitch) * tr.cameraToCenterDistance : 1);
gl.uniform1f(program.u_gamma_scale, gammaScale);

if (hasHalo) { // Draw halo underneath the text.
Expand All @@ -248,7 +199,7 @@ function drawSymbolElements(buffers, layer, gl, program) {
const paintVertexBuffer = layerData && layerData.paintVertexBuffer;

for (const segment of buffers.segments) {
segment.vaos[layer.id].bind(gl, program, buffers.layoutVertexBuffer, buffers.elementBuffer, paintVertexBuffer, segment.vertexOffset);
segment.vaos[layer.id].bind(gl, program, buffers.layoutVertexBuffer, buffers.elementBuffer, paintVertexBuffer, segment.vertexOffset, buffers.dynamicLayoutVertexBuffer);
gl.drawElements(gl.TRIANGLES, segment.primitiveLength * 3, gl.UNSIGNED_SHORT, segment.primitiveOffset * 3 * 2);
}
}
4 changes: 4 additions & 0 deletions src/render/frame_history.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ class FrameHistory {
this.previousZoom = zoom;
}

isVisible(zoom) {
return this.opacities[Math.floor(zoom * 10)] !== 0;
}

bind(gl) {
if (!this.texture) {
this.texture = gl.createTexture();
Expand Down
28 changes: 23 additions & 5 deletions src/render/painter.js
Original file line number Diff line number Diff line change
Expand Up @@ -292,19 +292,37 @@ class Painter {
this.gl.depthRange(nearDepth, farDepth);
}

translatePosMatrix(matrix, tile, translate, anchor) {
/**
* Transform a matrix to incorporate the *-translate and *-translate-anchor properties into it.
* @param {Float32Array} matrix
* @param {Tile} tile
* @param {Array<number>} translate
* @param {string} anchor
* @param {boolean} inViewportPixelUnitsUnits True when the units accepted by the matrix are in viewport pixels instead of tile units.
*
* @returns {Float32Array} matrix
*/
translatePosMatrix(matrix, tile, translate, translateAnchor, inViewportPixelUnitsUnits) {
if (!translate[0] && !translate[1]) return matrix;

if (anchor === 'viewport') {
const sinA = Math.sin(-this.transform.angle);
const cosA = Math.cos(-this.transform.angle);
const angle = inViewportPixelUnitsUnits ?
(translateAnchor === 'map' ? this.transform.angle : 0) :
(translateAnchor === 'viewport' ? -this.transform.angle : 0);

if (angle) {
const sinA = Math.sin(angle);
const cosA = Math.cos(angle);
translate = [
translate[0] * cosA - translate[1] * sinA,
translate[0] * sinA + translate[1] * cosA
];
}

const translation = [
const translation = inViewportPixelUnitsUnits ? [
translate[0],
translate[1],
0
] : [
pixelsToTileUnits(tile, translate[0], this.transform.zoom),
pixelsToTileUnits(tile, translate[1], this.transform.zoom),
0
Expand Down
Loading

0 comments on commit 1d1b669

Please sign in to comment.