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

Revise step interpolation for lines, increase precision of line progress as needed #9694

Merged
merged 2 commits into from
Jun 9, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions build/generate-struct-arrays.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ const circleAttributes = require('../src/data/bucket/circle_attributes').default
const fillAttributes = require('../src/data/bucket/fill_attributes').default;
const fillExtrusionAttributes = require('../src/data/bucket/fill_extrusion_attributes').default;
const lineAttributes = require('../src/data/bucket/line_attributes').default;
const lineAttributesExt = require('../src/data/bucket/line_attributes_ext').default;
const patternAttributes = require('../src/data/bucket/pattern_attributes').default;

// layout vertex arrays
Expand All @@ -138,6 +139,7 @@ const layoutAttributes = {
'fill-extrusion': fillExtrusionAttributes,
heatmap: circleAttributes,
line: lineAttributes,
lineExt: lineAttributesExt,
pattern: patternAttributes
};
for (const name in layoutAttributes) {
Expand Down
69 changes: 35 additions & 34 deletions src/data/array_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,39 @@ class StructArrayLayout2i4ub8 extends StructArray {
StructArrayLayout2i4ub8.prototype.bytesPerElement = 8;
register('StructArrayLayout2i4ub8', StructArrayLayout2i4ub8);

/**
* Implementation of the StructArray layout:
* [0]: Float32[3]
*
* @private
*/
class StructArrayLayout3f12 extends StructArray {
uint8: Uint8Array;
float32: Float32Array;

_refreshViews() {
this.uint8 = new Uint8Array(this.arrayBuffer);
this.float32 = new Float32Array(this.arrayBuffer);
}

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

emplace(i: number, v0: number, v1: number, v2: number) {
const o4 = i * 3;
this.float32[o4 + 0] = v0;
this.float32[o4 + 1] = v1;
this.float32[o4 + 2] = v2;
return i;
}
}

StructArrayLayout3f12.prototype.bytesPerElement = 12;
register('StructArrayLayout3f12', StructArrayLayout3f12);

/**
* Implementation of the StructArray layout:
* [0]: Uint16[10]
Expand Down Expand Up @@ -233,39 +266,6 @@ class StructArrayLayout4i4ui4i24 extends StructArray {
StructArrayLayout4i4ui4i24.prototype.bytesPerElement = 24;
register('StructArrayLayout4i4ui4i24', StructArrayLayout4i4ui4i24);

/**
* Implementation of the StructArray layout:
* [0]: Float32[3]
*
* @private
*/
class StructArrayLayout3f12 extends StructArray {
uint8: Uint8Array;
float32: Float32Array;

_refreshViews() {
this.uint8 = new Uint8Array(this.arrayBuffer);
this.float32 = new Float32Array(this.arrayBuffer);
}

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

emplace(i: number, v0: number, v1: number, v2: number) {
const o4 = i * 3;
this.float32[o4 + 0] = v0;
this.float32[o4 + 1] = v1;
this.float32[o4 + 2] = v2;
return i;
}
}

StructArrayLayout3f12.prototype.bytesPerElement = 12;
register('StructArrayLayout3f12', StructArrayLayout3f12);

/**
* Implementation of the StructArray layout:
* [0]: Uint32[1]
Expand Down Expand Up @@ -1095,9 +1095,9 @@ export {
StructArrayLayout4i8,
StructArrayLayout2i4i12,
StructArrayLayout2i4ub8,
StructArrayLayout3f12,
StructArrayLayout10ui20,
StructArrayLayout4i4ui4i24,
StructArrayLayout3f12,
StructArrayLayout1ul4,
StructArrayLayout6i1ul2ui20,
StructArrayLayout2i2i2i12,
Expand All @@ -1120,6 +1120,7 @@ export {
StructArrayLayout2i4i12 as FillExtrusionLayoutArray,
StructArrayLayout2i4 as HeatmapLayoutArray,
StructArrayLayout2i4ub8 as LineLayoutArray,
StructArrayLayout3f12 as LineExtLayoutArray,
StructArrayLayout10ui20 as PatternLayoutArray,
StructArrayLayout4i4ui4i24 as SymbolLayoutArray,
StructArrayLayout3f12 as SymbolDynamicLayoutArray,
Expand Down
11 changes: 11 additions & 0 deletions src/data/bucket/line_attributes_ext.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @flow
import {createLayout} from '../../util/struct_array';

const lineLayoutAttributesExt = createLayout([
{name: 'a_line_progress', components: 1, type: 'Float32'},
{name: 'a_line_clip', components: 1, type: 'Float32'},
{name: 'a_split_index', components: 1, type: 'Float32'},
]);
karimnaaji marked this conversation as resolved.
Show resolved Hide resolved

export default lineLayoutAttributesExt;
export const {members, size, alignment} = lineLayoutAttributesExt;
67 changes: 50 additions & 17 deletions src/data/bucket/line_bucket.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// @flow

import {LineLayoutArray} from '../array_types';
import {LineLayoutArray, LineExtLayoutArray} from '../array_types';

import {members as layoutAttributes} from './line_attributes';
import {members as layoutAttributesExt} from './line_attributes_ext';
import SegmentVector from '../segment';
import {ProgramConfigurationSet} from '../program_configuration';
import {TriangleIndexArray} from '../index_array_type';
Expand All @@ -25,7 +26,9 @@ import type {
import type LineStyleLayer from '../../style/style_layer/line_style_layer';
import type Point from '@mapbox/point-geometry';
import type {Segment} from '../segment';
import {RGBAImage} from '../../util/image';
import type Context from '../../gl/context';
import type Texture from '../../render/texture';
import type IndexBuffer from '../../gl/index_buffer';
import type VertexBuffer from '../../gl/vertex_buffer';
import type {FeatureStates} from '../../source/source_state';
Expand Down Expand Up @@ -67,15 +70,20 @@ const LINE_DISTANCE_SCALE = 1 / 2;
// The maximum line distance, in tile units, that fits in the buffer.
const MAX_LINE_DISTANCE = Math.pow(2, LINE_DISTANCE_BUFFER_BITS - 1) / LINE_DISTANCE_SCALE;

type LineClips = {
start: number;
end: number;
}

/**
* @private
*/
class LineBucket implements Bucket {
distance: number;
totalDistance: number;
maxLineLength: number;
scaledDistance: number;
clipStart: number;
clipEnd: number;
lineClips: ?LineClips;

e1: number;
e2: number;
Expand All @@ -88,9 +96,15 @@ class LineBucket implements Bucket {
stateDependentLayers: Array<any>;
stateDependentLayerIds: Array<string>;
patternFeatures: Array<BucketFeature>;
lineClipsArray: Array<LineClips>;

layoutVertexArray: LineLayoutArray;
layoutVertexBuffer: VertexBuffer;
layoutVertexArray2: LineExtLayoutArray;
layoutVertexBuffer2: VertexBuffer;
gradientTexture: Texture;
gradient: ?RGBAImage;
gradientVersion: number;

indexArray: TriangleIndexArray;
indexBuffer: IndexBuffer;
Expand All @@ -108,11 +122,14 @@ class LineBucket implements Bucket {
this.index = options.index;
this.hasPattern = false;
this.patternFeatures = [];
this.lineClipsArray = [];

this.layoutVertexArray = new LineLayoutArray();
this.layoutVertexArray2 = new LineExtLayoutArray();
this.indexArray = new TriangleIndexArray();
this.programConfigurations = new ProgramConfigurationSet(options.layers, options.zoom);
this.segments = new SegmentVector();
this.maxLineLength = 0;

this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id);
}
Expand Down Expand Up @@ -196,6 +213,9 @@ class LineBucket implements Bucket {

upload(context: Context) {
if (!this.uploaded) {
if (this.layoutVertexArray2.length !== 0) {
this.layoutVertexBuffer2 = context.createVertexBuffer(this.layoutVertexArray2, layoutAttributesExt);
}
this.layoutVertexBuffer = context.createVertexBuffer(this.layoutVertexArray, layoutAttributes);
this.indexBuffer = context.createIndexBuffer(this.indexArray);
}
Expand All @@ -211,12 +231,21 @@ class LineBucket implements Bucket {
this.segments.destroy();
}

lineFeatureClips(feature: BucketFeature): ?LineClips {
if (!!feature.properties && feature.properties.hasOwnProperty('mapbox_clip_start') && feature.properties.hasOwnProperty('mapbox_clip_end')) {
const start = +feature.properties['mapbox_clip_start'];
const end = +feature.properties['mapbox_clip_end'];
return {start, end};
}
}

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 miterLimit = layout.get('line-miter-limit');
const roundLimit = layout.get('line-round-limit');
this.lineClips = this.lineFeatureClips(feature);

for (const line of geometry) {
this.addLine(line, feature, join, cap, miterLimit, roundLimit);
Expand All @@ -230,18 +259,14 @@ class LineBucket implements Bucket {
this.scaledDistance = 0;
this.totalDistance = 0;

if (!!feature.properties &&
feature.properties.hasOwnProperty('mapbox_clip_start') &&
feature.properties.hasOwnProperty('mapbox_clip_end')) {

this.clipStart = +feature.properties['mapbox_clip_start'];
this.clipEnd = +feature.properties['mapbox_clip_end'];

if (this.lineClips) {
this.lineClipsArray.push(this.lineClips);
// Calculate the total distance, in tile units, of this tiled line feature
for (let i = 0; i < vertices.length - 1; i++) {
this.totalDistance += vertices[i].dist(vertices[i + 1]);
}
this.updateScaledDistance();
this.maxLineLength = Math.max(this.maxLineLength, this.totalDistance);
}

const isPolygon = vectorTileFeatureTypes[feature.type] === 'Polygon';
Expand Down Expand Up @@ -505,7 +530,7 @@ class LineBucket implements Bucket {

addHalfVertex({x, y}: Point, extrudeX: number, extrudeY: number, round: boolean, up: boolean, dir: number, segment: Segment) {
// scale down so that we can store longer distances while sacrificing precision.
const linesofar = this.scaledDistance * LINE_DISTANCE_SCALE;
const linesofarScaled = (this.totalDistance > 0 ? this.scaledDistance * (MAX_LINE_DISTANCE - 1) : this.scaledDistance) * LINE_DISTANCE_SCALE;

this.layoutVertexArray.emplaceBack(
// a_pos_normal
Expand All @@ -517,11 +542,19 @@ class LineBucket implements Bucket {
Math.round(EXTRUDE_SCALE * extrudeX) + 128,
Math.round(EXTRUDE_SCALE * extrudeY) + 128,
// Encode the -1/0/1 direction value into the first two bits of .z of a_data.
// Combine it with the lower 6 bits of `linesofar` (shifted by 2 bites to make
// room for the direction value). The upper 8 bits of `linesofar` are placed in
// Combine it with the lower 6 bits of `linesofarScaled` (shifted by 2 bits to make
// room for the direction value). The upper 8 bits of `linesofarScaled` are placed in
// the `w` component.
((dir === 0 ? 0 : (dir < 0 ? -1 : 1)) + 1) | ((linesofar & 0x3F) << 2),
linesofar >> 6);
((dir === 0 ? 0 : (dir < 0 ? -1 : 1)) + 1) | ((linesofarScaled & 0x3F) << 2),
linesofarScaled >> 6);

// Constructs a second vertex buffer with higher precision line progress
if (this.lineClips) {
this.layoutVertexArray2.emplaceBack(
this.scaledDistance - this.lineClips.start,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

By always realigning the distance with clip start and rendering the gradient between start and end while tiling it, we allow to have way larger distances represented, this is where the increase in precision comes from.

The last parameter of this new attribute layout is used to select which line the gradient falls through when multiple lines are represented in the same bucket geometry.

this.lineClips.end - this.lineClips.start,
this.lineClipsArray.length);
}

const e = segment.vertexLength++;
if (this.e1 >= 0 && this.e2 >= 0) {
Expand All @@ -540,8 +573,8 @@ class LineBucket implements Bucket {
// as the total distance (in tile units) of this tiled feature, and the distance
// (in tile units) of the current vertex, we can determine the relative distance
// of this vertex along the full linestring feature and scale it to [0, 2^15)
this.scaledDistance = this.totalDistance > 0 ?
(this.clipStart + (this.clipEnd - this.clipStart) * this.distance / this.totalDistance) * (MAX_LINE_DISTANCE - 1) :
this.scaledDistance = this.lineClips ?
this.lineClips.start + (this.lineClips.end - this.lineClips.start) * this.distance / this.totalDistance :
this.distance;
}

Expand Down
2 changes: 2 additions & 0 deletions src/gl/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class Context {
gl: WebGLRenderingContext;
extVertexArrayObject: any;
currentNumAttributes: ?number;
maxTextureSize: number;

clearColor: ClearColor;
clearDepth: ClearDepth;
Expand Down Expand Up @@ -118,6 +119,7 @@ class Context {
}

this.extTimerQuery = gl.getExtension('EXT_disjoint_timer_query');
this.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
}

setDefault() {
Expand Down
48 changes: 37 additions & 11 deletions src/render/draw_line.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import type SourceCache from '../source/source_cache';
import type LineStyleLayer from '../style/style_layer/line_style_layer';
import type LineBucket from '../data/bucket/line_bucket';
import type {OverscaledTileID} from '../source/tile_id';
import {clamp, nextPowerOfTwo} from '../util/util';
import {renderColorRamp} from '../util/color_ramp';
import EXTENT from '../data/extent';

export default function drawLine(painter: Painter, sourceCache: SourceCache, layer: LineStyleLayer, coords: Array<OverscaledTileID>) {
if (painter.renderPass !== 'translucent') return;
Expand Down Expand Up @@ -43,15 +46,6 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay

let firstTile = true;

if (gradient) {
context.activeTexture.set(gl.TEXTURE0);

let gradientTexture = layer.gradientTexture;
if (!layer.gradient) return;
if (!gradientTexture) gradientTexture = layer.gradientTexture = new Texture(context, layer.gradient, gl.RGBA);
gradientTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
}

for (const coord of coords) {
const tile = sourceCache.getTile(coord);

Expand All @@ -75,7 +69,7 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay

const uniformValues = image ? linePatternUniformValues(painter, tile, layer, crossfade) :
dasharray ? lineSDFUniformValues(painter, tile, layer, dasharray, crossfade) :
gradient ? lineGradientUniformValues(painter, tile, layer) :
gradient ? lineGradientUniformValues(painter, tile, layer, bucket.lineClipsArray.length) :
lineUniformValues(painter, tile, layer);

if (image) {
Expand All @@ -85,12 +79,44 @@ export default function drawLine(painter: Painter, sourceCache: SourceCache, lay
} else if (dasharray && (programChanged || painter.lineAtlas.dirty)) {
context.activeTexture.set(gl.TEXTURE0);
painter.lineAtlas.bind(context);
} else if (gradient) {
let gradientTexture = bucket.gradientTexture;
if (!gradientTexture || (gradientTexture && layer.gradientVersion !== bucket.gradientVersion)) {
let textureResolution = 256;
if (layer.stepInterpolant) {
const sourceMaxZoom = sourceCache.getSource().maxzoom;
const potentialOverzoom = coord.canonical.z === sourceMaxZoom ?
Math.ceil(1 << (painter.transform.maxZoom - coord.canonical.z)) : 1;
const lineLength = bucket.maxLineLength / EXTENT;
// Logical pixel tile size is 512px, and 1024px right before current zoom + 1
const maxTilePixelSize = 1024;
// Maximum possible texture coverage heuristic, bound by hardware max texture size
const maxTextureCoverage = lineLength * maxTilePixelSize * potentialOverzoom;
textureResolution = clamp(nextPowerOfTwo(maxTextureCoverage), 256, context.maxTextureSize);
}
bucket.gradient = renderColorRamp({
expression: layer.gradientExpression(),
evaluationKey: 'lineProgress',
resolution: textureResolution,
image: bucket.gradient || undefined,
clips: bucket.lineClipsArray
});
if (bucket.gradientTexture) {
bucket.gradientTexture.update(bucket.gradient);
} else {
bucket.gradientTexture = new Texture(context, bucket.gradient, gl.RGBA);
}
bucket.gradientVersion = layer.gradientVersion;
gradientTexture = bucket.gradientTexture;
}
context.activeTexture.set(gl.TEXTURE0);
gradientTexture.bind(layer.stepInterpolant ? gl.NEAREST : gl.LINEAR, gl.CLAMP_TO_EDGE);
}

program.draw(context, gl.TRIANGLES, depthMode,
painter.stencilModeForClipping(coord), colorMode, CullFaceMode.disabled, uniformValues,
layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer, bucket.segments,
layer.paint, painter.transform.zoom, programConfiguration);
layer.paint, painter.transform.zoom, programConfiguration, bucket.layoutVertexBuffer2);

firstTile = false;
// once refactored so that bound texture state is managed, we'll also be able to remove this firstTile/programChanged logic
Expand Down
Loading