Skip to content

Commit

Permalink
Add high precision mode to PathStyleExtension (#4951)
Browse files Browse the repository at this point in the history
  • Loading branch information
Pessimistress authored Sep 21, 2020
1 parent b92388e commit e0cd727
Show file tree
Hide file tree
Showing 10 changed files with 155 additions and 35 deletions.
27 changes: 26 additions & 1 deletion docs/api-reference/extensions/path-style-extension.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ new PathStyleExtension({dash});
```

* `dash` (Boolean) - add capability to render dashed lines. Default `false`.
* `highPrecisionDash` (Boolean) - improve dash rendering quality in certain circumstances. Note that this option introduces additional performance overhead, see "Remarks" below. Default `false`.
* `offset` (Boolean) - add capability to offset lines. Default `false`.

## Layer Properties
Expand All @@ -77,7 +78,7 @@ The dash array to draw each path with: `[dashSize, gapSize]` relative to the wid

* Default: `false`

Only effective if `getDashArray` is specified. If `true`, adjust gaps for the dashes to align at both ends.
Only effective if `getDashArray` is specified. If `true`, adjust gaps for the dashes to align at both ends. Overrides the effect of `highPrecisionDash`.


##### `getOffset` ([Function](/docs/developer-guide/using-layers.md#accessors)|Number)
Expand All @@ -89,6 +90,30 @@ The offset to draw each path with, relative to the width of the path. Negative o
* If a number is provided, it is used as the offset for all paths.
* If a function is provided, it is called on each path to retrieve its offset.

## Remarks

### Limitations

WebGL has guaranteed support for up to 16 attributes per shader. The current implementation of `PathLayer` uses 13 attributes. Each one of the options of this extension adds one more attribute. In other words, if all options are enabled, the layer will not be able to use other extensions.

### Tips on Rendering Dash Lines

There are three modes to render dash lines with this extension:

1. Default: dash starts from the beginning of each line segment
2. Justified: dash is stretched to center on each line segment
3. High precision: dash is evaluated continuously from the beginning of a path

![Comparison between dash modes](https://user-images.githubusercontent.com/2059298/93418881-33555280-f860-11ea-82cc-b57ecf2e48ce.png)

The above table illustrates the visual behavior of the three modes.

The default mode works best if the data consists of long, disjoint paths. It renders dashes at exactly the defined lengths.

The justified mode is guaranteed to render sharp, well-defined corners. This is great for rendering polyline shapes. However, the gap size may look inconsistent across line segments due to stretching.

The high precision mode pre-calculates path length on the CPU, so it may be slower and use more resources for large datasets. When a path contains a lot of short segments, this mode yields the best result.


## Source

Expand Down
2 changes: 1 addition & 1 deletion examples/layer-browser/src/examples/core-layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ const PathLayerExample = {
widthMinPixels: 1,
pickable: true,
dashJustified: true,
extensions: [new PathStyleExtension({dash: true, offset: true})]
extensions: [new PathStyleExtension({dash: true, offset: true, highPrecisionDash: true})]
}
};

Expand Down
2 changes: 1 addition & 1 deletion modules/core/src/shaderlib/project/project-functions.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export function getWorldPosition(
position,
{viewport, modelMatrix, coordinateSystem, coordinateOrigin, offsetMode}
) {
let [x, y, z] = position;
let [x, y, z = 0] = position;

if (modelMatrix) {
[x, y, z] = vec4.transformMat4([], [x, y, z, 1.0], modelMatrix);
Expand Down
3 changes: 2 additions & 1 deletion modules/extensions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@luma.gl/shadertools": "^8.2.0"
},
"peerDependencies": {
"@deck.gl/core": "^8.0.0"
"@deck.gl/core": "^8.0.0",
"gl-matrix": "^3.0.0"
}
}
35 changes: 33 additions & 2 deletions modules/extensions/src/path-style/path-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import {LayerExtension, _mergeShaders as mergeShaders} from '@deck.gl/core';
import {dashShaders, offsetShaders} from './shaders.glsl';
import {dist} from 'gl-matrix/vec3';

const defaultProps = {
getDashArray: {type: 'accessor', value: [0, 0]},
Expand All @@ -28,8 +29,8 @@ const defaultProps = {
};

export default class PathStyleExtension extends LayerExtension {
constructor({dash = false, offset = false} = {}) {
super({dash, offset});
constructor({dash = false, offset = false, highPrecisionDash = false} = {}) {
super({dash: dash || highPrecisionDash, offset, highPrecisionDash});
}

isEnabled(layer) {
Expand Down Expand Up @@ -67,6 +68,15 @@ export default class PathStyleExtension extends LayerExtension {
instanceDashArrays: {size: 2, accessor: 'getDashArray'}
});
}
if (extension.opts.highPrecisionDash) {
attributeManager.addInstanced({
instanceDashOffsets: {
size: 1,
accessor: 'getPath',
transform: extension.getDashOffsets.bind(this)
}
});
}
if (extension.opts.offset) {
attributeManager.addInstanced({
instanceOffsets: {size: 1, accessor: 'getOffset'}
Expand All @@ -87,6 +97,27 @@ export default class PathStyleExtension extends LayerExtension {

this.state.model.setUniforms(uniforms);
}

getDashOffsets(path) {
const result = [0];
const positionSize = this.props.positionFormat === 'XY' ? 2 : 3;
const isNested = Array.isArray(path[0]);
const geometrySize = isNested ? path.length : path.length / positionSize;

let p;
let prevP;
for (let i = 0; i < geometrySize - 1; i++) {
p = isNested ? path[i] : path.slice(i * positionSize, i * positionSize + positionSize);
p = this.projectPosition(p);

if (i > 0) {
result[i] = result[i - 1] + dist(prevP, p);
}

prevP = p;
}
return result;
}
}

PathStyleExtension.extensionName = 'PathStyleExtension';
Expand Down
30 changes: 14 additions & 16 deletions modules/extensions/src/path-style/shaders.glsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,20 @@ export const dashShaders = {
inject: {
'vs:#decl': `
attribute vec2 instanceDashArrays;
attribute float instanceDashOffsets;
varying vec2 vDashArray;
varying float vDashOffset;
`,

'vs:#main-end': `
vDashArray = instanceDashArrays;
vDashOffset = instanceDashOffsets / width.x;
`,

'fs:#decl': `
uniform float dashAlignMode;
varying vec2 vDashArray;
// mod doesn't work correctly for negative numbers
float mod2(float a, float b) {
return a - floor(a / b) * b;
}
varying float vDashOffset;
float round(float x) {
return floor(x + 0.5);
Expand All @@ -36,24 +35,23 @@ float round(float x) {
float gapLength = vDashArray.y;
float unitLength = solidLength + gapLength;
if (unitLength > 0.0) {
unitLength = mix(
unitLength,
vPathLength / round(vPathLength / unitLength),
dashAlignMode
);
float offset;
float offset = dashAlignMode * solidLength / 2.0;
if (unitLength > 0.0) {
if (dashAlignMode == 0.0) {
offset = vDashOffset;
} else {
unitLength = vPathLength / round(vPathLength / unitLength);
offset = solidLength / 2.0;
}
if (
gapLength > 0.0 &&
vPathPosition.y >= 0.0 &&
vPathPosition.y <= vPathLength &&
mod2(vPathPosition.y + offset, unitLength) > solidLength
mod(clamp(vPathPosition.y, 0.0, vPathLength) + offset, unitLength) > solidLength
) {
discard;
}
}
}
`
}
};
Expand Down
5 changes: 3 additions & 2 deletions modules/layers/src/path-layer/path-layer-vertex.glsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ void main() {
vec2 widthPixels = vec2(clamp(project_size_to_pixel(instanceStrokeWidths * widthScale),
widthMinPixels, widthMaxPixels) / 2.0);
vec2 width;
vColor = vec4(instanceColors.rgb, instanceColors.a * opacity);
Expand All @@ -197,7 +198,7 @@ void main() {
clipLine(nextPositionScreen, currPositionScreen);
clipLine(currPositionScreen, mix(nextPositionScreen, prevPositionScreen, isEnd));
vec2 width = project_pixel_size_to_clipspace(widthPixels);
width = project_pixel_size_to_clipspace(widthPixels);
vec3 pos = lineJoin(
prevPositionScreen.xyz / prevPositionScreen.w,
Expand All @@ -213,7 +214,7 @@ void main() {
currPosition = project_position(currPosition, currPosition64Low);
nextPosition = project_position(nextPosition, nextPosition64Low);
vec2 width = project_pixel_size(widthPixels);
width = project_pixel_size(widthPixels);
vec4 pos = vec4(
lineJoin(prevPosition, currPosition, nextPosition, width),
Expand Down
41 changes: 31 additions & 10 deletions test/modules/extensions/path.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,36 +14,57 @@ test('PathStyleExtension#PathLayer', t => {
getPath: d => d.path,
getDashArray: [0, 0],
getOffset: 0,
extensions: [new PathStyleExtension({dash: true, offset: true})]
extensions: [new PathStyleExtension({highPrecisionDash: true, offset: true})]
},
onAfterUpdate: ({layer}) => {
const uniforms = layer.state.model.getUniforms();
t.is(uniforms.dashAlignMode, 0, 'has dashAlignMode uniform');
t.ok(
layer.getAttributeManager().getAttributes().instanceDashArrays.value,
const attributes = layer.getAttributeManager().getAttributes();
t.deepEqual(
attributes.instanceDashArrays.value,
[0, 0],
'instanceDashArrays attribute is populated'
);
t.ok(
layer.getAttributeManager().getAttributes().instanceOffsets.value,
t.deepEqual(
attributes.instanceOffsets.value,
[0],
'instanceOffsets attribute is populated'
);

let dashOffsetValid = true;
let i;
for (i = 0; i < FIXTURES.zigzag[0].path.length - 2; i++) {
dashOffsetValid =
dashOffsetValid &&
attributes.instanceDashOffsets.value[i] <= attributes.instanceDashOffsets.value[i + 1];
}
dashOffsetValid = dashOffsetValid && attributes.instanceDashOffsets.value[i + 1] === 0;

t.ok(dashOffsetValid, 'instanceDashOffsets attribute is populated');
}
},
{
updateProps: {
dashJustified: true,
getDashArray: d => [3, 1],
getOffset: d => 0.5
getOffset: d => 0.5,
updateTriggers: {
getDashArray: 1,
getOffset: 1
}
},
onAfterUpdate: ({layer}) => {
const uniforms = layer.state.model.getUniforms();
t.is(uniforms.dashAlignMode, 1, 'has dashAlignMode uniform');
t.ok(
layer.getAttributeManager().getAttributes().instanceDashArrays.value,
const attributes = layer.getAttributeManager().getAttributes();
t.deepEqual(
attributes.instanceDashArrays.value.slice(0, 4),
[3, 1, 3, 1],
'instanceDashArrays attribute is populated'
);
t.ok(
layer.getAttributeManager().getAttributes().instanceOffsets.value,
t.deepEqual(
attributes.instanceOffsets.value.slice(0, 4),
[0.5, 0.5, 0.5, 0.5],
'instanceOffsets attribute is populated'
);
}
Expand Down
Binary file added test/render/golden-images/path-dash.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
45 changes: 44 additions & 1 deletion test/render/test-cases/path-layer.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import {COORDINATE_SYSTEM, _GlobeView as GlobeView} from '@deck.gl/core';
import {COORDINATE_SYSTEM, OrthographicView, _GlobeView as GlobeView} from '@deck.gl/core';
import {PathLayer} from '@deck.gl/layers';
import {PathStyleExtension} from '@deck.gl/extensions';
import {zigzag, zigzag3D, meterPaths, positionOrigin} from 'deck.gl-test/data';

// prettier-ignore
const DASH_TEST_DATA = [
[53.38,218.93,43.55,179.03,26.22,158.15,-2.25,138.62,-38.51,128.07,-72.23,127.35,-103.39,133.87,
-117.30,141.74,-126.97,153.52,-130.41,168.93,-126.97,184.34,-117.30,196.12,-103.39,203.99,-72.23,210.51,
-38.51,209.79,-2.25,199.24,26.22,179.71,43.55,158.83,53.38,118.93,43.55,79.03,26.22,58.15,-2.25,38.62,
-38.51,28.07,-72.23,27.35,-103.39,33.87,-117.30,41.74,-126.97,53.52,-130.41,68.93,-126.97,84.34,-117.30,96.12,
-103.39,103.99,-72.23,110.51,-38.51,109.79,-2.25,99.24,26.22,79.71,43.55,58.83,53.38,18.93],
[-147.88,-152.35,-97.88,-238.95,2.12,-238.95,52.12,-152.35,2.12,-65.75,-97.88,-65.75,-147.88,-152.35]
];

export default [
{
name: 'path-miter',
Expand Down Expand Up @@ -138,6 +148,39 @@ export default [
],
goldenImage: './test/render/golden-images/path-meter.png'
},
{
name: 'path-dash',
views: new OrthographicView(),
viewState: {
target: [0, 0, 0],
zoom: -0.5
},
layers: [
new PathLayer({
id: 'path-dash-justified',
data: DASH_TEST_DATA,
getPath: d => d,
positionFormat: 'XY',
getDashArray: [4, 5],
getLineColor: [200, 0, 0],
widthMinPixels: 10,
dashJustified: true,
extensions: [new PathStyleExtension({dash: true})]
}),
new PathLayer({
id: 'path-dash-high-precision',
modelMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 300, 0, 0, 1],
data: DASH_TEST_DATA,
getPath: d => d,
positionFormat: 'XY',
getDashArray: [4, 5],
getLineColor: [200, 0, 0],
widthMinPixels: 10,
extensions: [new PathStyleExtension({highPrecisionDash: true})]
})
],
goldenImage: './test/render/golden-images/path-dash.png'
},
{
name: 'path-offset',
viewState: {
Expand Down

0 comments on commit e0cd727

Please sign in to comment.