Skip to content

Commit

Permalink
Squash of #9694 on top of 9489d76
Browse files Browse the repository at this point in the history
Increase texture resolution for line gradient for step interpolant
- Step interpolant now uses gl.NEAREST for texture sampler: This allows
for hard transition between different line sections when using step
interpolation. Other use cases are covered by using a smooth interpolation
type along with gl.LINEAR texture sampler.
- Step interpolant now uses an increased texture resolution color ramp
based on the total line length tile coverage to limitate the potential
precision issues for high coverage. We are still limitated by the precision
of v_lineprogress currently encoded in a 15bits floating point value.

Reuse color ramp memory placement when already available

Only calculate max distance if clipstart and clipend are defined

Increase precision of line progress when needed

Tile line gradient textures
  • Loading branch information
karimnaaji committed Jun 2, 2020
1 parent 9489d76 commit 3fc990e
Show file tree
Hide file tree
Showing 21 changed files with 663 additions and 80 deletions.
2 changes: 2 additions & 0 deletions build/generate-struct-arrays.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,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 @@ -136,6 +137,7 @@ const layoutAttributes = {
'fill-extrusion': fillExtrusionAttributes,
heatmap: circleAttributes,
line: lineAttributes,
lineExt: lineAttributesExt,
pattern: patternAttributes
};
for (const name in layoutAttributes) {
Expand Down
38 changes: 38 additions & 0 deletions src/data/array_types.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,42 @@ class StructArrayLayout2i4ub8 extends StructArray {
StructArrayLayout2i4ub8.prototype.bytesPerElement = 8;
register('StructArrayLayout2i4ub8', StructArrayLayout2i4ub8);

/**
* Implementation of the StructArray layout:
* [0]: Float32[1]
* [4]: Float32[1]
* [8]: Uint8[1]
*
* @private
*/
class StructArrayLayout1f1f1ub12 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;
const o1 = i * 12;
this.float32[o4 + 0] = v0;
this.float32[o4 + 1] = v1;
this.uint8[o1 + 8] = v2;
return i;
}
}

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

/**
* Implementation of the StructArray layout:
* [0]: Uint16[8]
Expand Down Expand Up @@ -1054,6 +1090,7 @@ export {
StructArrayLayout4i8,
StructArrayLayout2i4i12,
StructArrayLayout2i4ub8,
StructArrayLayout1f1f1ub12,
StructArrayLayout8ui16,
StructArrayLayout4i4ui4i24,
StructArrayLayout3f12,
Expand All @@ -1078,6 +1115,7 @@ export {
StructArrayLayout2i4i12 as FillExtrusionLayoutArray,
StructArrayLayout2i4 as HeatmapLayoutArray,
StructArrayLayout2i4ub8 as LineLayoutArray,
StructArrayLayout1f1f1ub12 as LineExtLayoutArray,
StructArrayLayout8ui16 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: 'Uint8'},
], 4);

export default lineLayoutAttributesExt;
export const {members, size, alignment} = lineLayoutAttributesExt;
78 changes: 51 additions & 27 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 @@ -24,7 +25,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 @@ -66,15 +69,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 @@ -87,9 +95,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 @@ -107,11 +121,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(layoutAttributes, 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 @@ -188,6 +205,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 @@ -203,12 +223,21 @@ class LineBucket implements Bucket {
this.segments.destroy();
}

addFeature(feature: BucketFeature, geometry: Array<Array<Point>>, index: number, imagePositions: {[_: string]: ImagePosition}) {
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, index, imagePositions);
Expand All @@ -220,18 +249,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 @@ -484,20 +509,11 @@ class LineBucket implements Bucket {

this.addHalfVertex(p, leftX, leftY, round, false, endLeft, segment);
this.addHalfVertex(p, rightX, rightY, round, true, -endRight, segment);

// There is a maximum "distance along the line" that we can store in the buffers.
// When we get close to the distance, reset it to zero and add the vertex again with
// a distance of zero. The max distance is determined by the number of bits we allocate
// to `linesofar`.
if (this.distance > MAX_LINE_DISTANCE / 2 && this.totalDistance === 0) {
this.distance = 0;
this.addCurrentVertex(p, normal, endLeft, endRight, segment, round);
}
}

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 @@ -509,11 +525,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,
this.lineClips.end - this.lineClips.start,
this.lineClipsArray.length);
}

const e = segment.vertexLength++;
if (this.e1 >= 0 && this.e2 >= 0) {
Expand All @@ -532,8 +556,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 @@ -116,6 +117,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

0 comments on commit 3fc990e

Please sign in to comment.