Skip to content

Commit

Permalink
Data-driven line-dasharray and line-cap (#10591)
Browse files Browse the repository at this point in the history
* WIP data-driven line-dasharray / line-cap (not working yet)

* WIP pass the per-tile line atlas through

* WIP switch to integer coords for line atlas

* add a dds dasharray debug page (temp)

* fix debug page

* use different attribute names for dashes to avoid collision

The attribute names used for dashes were the same as those used for
patterns. This was leading to incorrectly generated shaders.

Since there was no pattern set for the dash layer it was setting the
defines that generate the shader without those attributes.

The line atlas texture is now bound with gl.REPEAT to render more than
one dash.

* fix DDS dasharray rendering

* refactor to support constant patterns

* fix constant dasharrays

* dasharray fixes

* more dasharray fixes

* fix line atlas unit tests

* more unit test fixes

* fix remaining render test

* one more unit test fix

* optimize dash buffer layout

* fixup line pattern

* fix constant dash + dds line-cap

* fixup

* fix dasharray + composite line-cap

* fix dasharray flickering when crossing zoom stops

* dasharray perf optimizations

* clean up leftovers

* minor optimization in program_configuration

* add data-driven dash/cap render tests

* add a render test for solid line data-driven line-cap

* make shaders more consistent for easier porting

* rename conflicting shader constant

* fix out of atlas space warning in some cases

Co-authored-by: Ansis Brammanis <[email protected]>
  • Loading branch information
mourner and ansis authored May 6, 2021
1 parent 6de0217 commit 291ba36
Show file tree
Hide file tree
Showing 33 changed files with 827 additions and 209 deletions.
4 changes: 3 additions & 1 deletion build/generate-struct-arrays.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ import fillAttributes from '../src/data/bucket/fill_attributes.js';
import lineAttributes from '../src/data/bucket/line_attributes.js';
import lineAttributesExt from '../src/data/bucket/line_attributes_ext.js';
import patternAttributes from '../src/data/bucket/pattern_attributes.js';
import dashAttributes from '../src/data/bucket/dash_attributes.js';
import skyboxAttributes from '../src/render/skybox_attributes.js';
import {fillExtrusionAttributes, centroidAttributes} from '../src/data/bucket/fill_extrusion_attributes.js';

Expand All @@ -139,7 +140,8 @@ const layoutAttributes = {
heatmap: circleAttributes,
line: lineAttributes,
lineExt: lineAttributesExt,
pattern: patternAttributes
pattern: patternAttributes,
dash: dashAttributes
};
for (const name in layoutAttributes) {
createStructArrayType(`${name.replace(/-/g, '_')}_layout`, layoutAttributes[name]);
Expand Down
72 changes: 72 additions & 0 deletions debug/dasharray.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html>
<head>
<title>Mapbox GL JS debug page</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
</style>
</head>

<body>
<div id='map'></div>

<script src='../dist/mapbox-gl-dev.js'></script>
<script src='../debug/access_token_generated.js'></script>
<script>

var map = window.map = new mapboxgl.Map({
container: 'map',
zoom: 4,
center: [0, 0],
style: {sources: {}, version: 8, layers: []}
});

map.on('load', () => {
map.addSource('geojson', {
"type": "geojson",
"data": {
"type": "FeatureCollection",
"features": [{
"type": "Feature",
"properties": {"property": 1},
"geometry": {"type": "LineString", "coordinates": [[-10, -5], [10, -5]]}
}, {
"type": "Feature",
"properties": {"property": 2},
"geometry": {"type": "LineString", "coordinates": [[-10, 0], [10, 0]]}
}, {
"type": "Feature",
"properties": {"property": 3},
"geometry": {"type": "LineString", "coordinates": [[-10, 5], [10, 5]]}
}]
}
});

map.addLayer({
"id": "dash-dds",
"type": "line",
"source": "geojson",
"paint": {
"line-width": 10,
// "line-dasharray": [1, 2],
"line-dasharray": [
"match", ["get", "property"],
1, ["literal", [1, 2]],
2, ["literal", [2, 2]],
3, ["literal", [3, 2]],
["literal", [1, 1]]
]
},
"layout": {
"line-cap": ["match", ["get", "property"], 2, "round", "butt"]
}
});
});

</script>
</body>
</html>
40 changes: 40 additions & 0 deletions src/data/array_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,44 @@ class StructArrayLayout10ui20 extends StructArray {
StructArrayLayout10ui20.prototype.bytesPerElement = 20;
register('StructArrayLayout10ui20', StructArrayLayout10ui20);

/**
* Implementation of the StructArray layout:
* [0]: Uint16[8]
*
* @private
*/
class StructArrayLayout8ui16 extends StructArray {
uint8: Uint8Array;
uint16: Uint16Array;

_refreshViews() {
this.uint8 = new Uint8Array(this.arrayBuffer);
this.uint16 = new Uint16Array(this.arrayBuffer);
}

emplaceBack(v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) {
const i = this.length;
this.resize(i + 1);
return this.emplace(i, v0, v1, v2, v3, v4, v5, v6, v7);
}

emplace(i: number, v0: number, v1: number, v2: number, v3: number, v4: number, v5: number, v6: number, v7: number) {
const o2 = i * 8;
this.uint16[o2 + 0] = v0;
this.uint16[o2 + 1] = v1;
this.uint16[o2 + 2] = v2;
this.uint16[o2 + 3] = v3;
this.uint16[o2 + 4] = v4;
this.uint16[o2 + 5] = v5;
this.uint16[o2 + 6] = v6;
this.uint16[o2 + 7] = v7;
return i;
}
}

StructArrayLayout8ui16.prototype.bytesPerElement = 16;
register('StructArrayLayout8ui16', StructArrayLayout8ui16);

/**
* Implementation of the StructArray layout:
* [0]: Int16[4]
Expand Down Expand Up @@ -1104,6 +1142,7 @@ export {
StructArrayLayout2i4ub1f12,
StructArrayLayout2f8,
StructArrayLayout10ui20,
StructArrayLayout8ui16,
StructArrayLayout4i4ui4i24,
StructArrayLayout3f12,
StructArrayLayout1ul4,
Expand All @@ -1129,6 +1168,7 @@ export {
StructArrayLayout2i4ub1f12 as LineLayoutArray,
StructArrayLayout2f8 as LineExtLayoutArray,
StructArrayLayout10ui20 as PatternLayoutArray,
StructArrayLayout8ui16 as DashLayoutArray,
StructArrayLayout4i4ui4i24 as SymbolLayoutArray,
StructArrayLayout3f12 as SymbolDynamicLayoutArray,
StructArrayLayout1ul4 as SymbolOpacityArray,
Expand Down
4 changes: 3 additions & 1 deletion src/data/bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type FeatureIndex from './feature_index.js';
import type Context from '../gl/context.js';
import type {FeatureStates} from '../source/source_state.js';
import type {ImagePosition} from '../render/image_atlas.js';
import type LineAtlas from '../render/line_atlas.js';
import type {CanonicalTileID} from '../source/tile_id.js';

export type BucketParameters<Layer: TypedStyleLayer> = {
Expand All @@ -26,7 +27,8 @@ export type PopulateParameters = {
iconDependencies: {},
patternDependencies: {},
glyphDependencies: {},
availableImages: Array<string>
availableImages: Array<string>,
lineAtlas: LineAtlas
}

export type IndexedFeature = {
Expand Down
9 changes: 9 additions & 0 deletions src/data/bucket/dash_attributes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @flow
import {createLayout} from '../../util/struct_array.js';

const dashAttributes = createLayout([
{name: 'a_dash_to', components: 4, type: 'Uint16'}, // [x, y, width, unused]
{name: 'a_dash_from', components: 4, type: 'Uint16'}
]);

export default dashAttributes;
86 changes: 83 additions & 3 deletions src/data/bucket/line_bucket.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type IndexBuffer from '../../gl/index_buffer.js';
import type VertexBuffer from '../../gl/vertex_buffer.js';
import type {FeatureStates} from '../../source/source_state.js';
import type {ImagePosition} from '../../render/image_atlas.js';
import type LineAtlas from '../../render/line_atlas.js';

// NOTE ON EXTRUDE SCALE:
// scale the extrusion vector so that the normal length is this value.
Expand Down Expand Up @@ -169,21 +170,100 @@ class LineBucket implements Bucket {
});
}

const {lineAtlas, featureIndex} = options;
const hasFeatureDashes = this.addConstantDashes(lineAtlas);

for (const bucketFeature of bucketFeatures) {
const {geometry, index, sourceLayerIndex} = bucketFeature;

if (hasFeatureDashes) {
this.addFeatureDashes(bucketFeature, lineAtlas);
}

if (this.hasPattern) {
const patternBucketFeature = addPatternDependencies('line', this.layers, bucketFeature, this.zoom, options);
// pattern features are added only once the pattern is loaded into the image atlas
// so are stored during populate until later updated with positions by tile worker in addFeatures
this.patternFeatures.push(patternBucketFeature);

} else {
this.addFeature(bucketFeature, geometry, index, canonical, {});
this.addFeature(bucketFeature, geometry, index, canonical, lineAtlas.positions);
}

const feature = features[index].feature;
options.featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index);
featureIndex.insert(feature, geometry, index, sourceLayerIndex, this.index);
}
}

addConstantDashes(lineAtlas: LineAtlas) {
let hasFeatureDashes = false;

for (const layer of this.layers) {
const dashPropertyValue = layer.paint.get('line-dasharray').value;
const capPropertyValue = layer.layout.get('line-cap').value;

if (dashPropertyValue.kind !== 'constant' || capPropertyValue.kind !== 'constant') {
hasFeatureDashes = true;

} else {
const round = capPropertyValue.value === 'round';
const constDash = dashPropertyValue.value;
if (!constDash) continue;
lineAtlas.addDash(constDash.from, round);
lineAtlas.addDash(constDash.to, round);
if (constDash.other) lineAtlas.addDash(constDash.other, round);
}
}

return hasFeatureDashes;
}

addFeatureDashes(feature: BucketFeature, lineAtlas: LineAtlas) {

const zoom = this.zoom;

for (const layer of this.layers) {
const dashPropertyValue = layer.paint.get('line-dasharray').value;
const capPropertyValue = layer.layout.get('line-cap').value;

if (dashPropertyValue.kind === 'constant' && capPropertyValue.kind === 'constant') continue;

let minDashArray, midDashArray, maxDashArray, minRound, midRound, maxRound;

if (dashPropertyValue.kind === 'constant') {
const constDash = dashPropertyValue.value;
if (!constDash) continue;
minDashArray = constDash.other || constDash.to;
midDashArray = constDash.to;
maxDashArray = constDash.from;

} else {
minDashArray = dashPropertyValue.evaluate({zoom: zoom - 1}, feature);
midDashArray = dashPropertyValue.evaluate({zoom}, feature);
maxDashArray = dashPropertyValue.evaluate({zoom: zoom + 1}, feature);
}

if (capPropertyValue.kind === 'constant') {
minRound = midRound = maxRound = capPropertyValue.value === 'round';

} else {
minRound = capPropertyValue.evaluate({zoom: zoom - 1}, feature) === 'round';
midRound = capPropertyValue.evaluate({zoom}, feature) === 'round';
maxRound = capPropertyValue.evaluate({zoom: zoom + 1}, feature) === 'round';
}

lineAtlas.addDash(minDashArray, minRound);
lineAtlas.addDash(midDashArray, midRound);
lineAtlas.addDash(maxDashArray, maxRound);

const min = lineAtlas.getKey(minDashArray, minRound);
const mid = lineAtlas.getKey(midDashArray, midRound);
const max = lineAtlas.getKey(maxDashArray, maxRound);

// save positions for paint array
feature.patterns[layer.id] = {min, mid, max};
}

}

update(states: FeatureStates, vtLayer: VectorTileLayer, imagePositions: {[_: string]: ImagePosition}) {
Expand Down Expand Up @@ -236,7 +316,7 @@ class LineBucket implements Bucket {
addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, canonical: CanonicalTileID, imagePositions: {[_: string]: ImagePosition}) {
const layout = this.layers[0].layout;
const join = layout.get('line-join').evaluate(feature, {});
const cap = layout.get('line-cap');
const cap = layout.get('line-cap').evaluate(feature, {});
const miterLimit = layout.get('line-miter-limit');
const roundLimit = layout.get('line-round-limit');
this.lineClips = this.lineFeatureClips(feature);
Expand Down
Loading

0 comments on commit 291ba36

Please sign in to comment.