From d812078912e955308645819d86d9eb39c2182b72 Mon Sep 17 00:00:00 2001 From: Karim Naaji Date: Tue, 9 Jun 2020 12:31:52 -0400 Subject: [PATCH] Revise step interpolation for lines, increase precision of line progress as needed (#9694) * Vastly increase precision for line gradients - 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 - Reuse color ramp memory placement when already available - Precision is increased by two factors for geojson with line metrics on: - Tile line gradient textures for higher representation in texture space - Add an optional extension vertex buffer for increased line progress precision * Review comments (Thanks @mourner) - Eliminate Math.log2 (not available on ie11) - Inline and compress evaluation interpolations for line clip by specializing it for the use case * Optimization pass - Use single attribute for uv on x, move computation cpu side (Thanks @ansis) - Offload fragment shader from a few operations by moving it to vertex - More explicit addHalfVertex function block --- build/generate-struct-arrays.js | 2 + src/data/array_types.js | 67 ++++++------ src/data/bucket/line_attributes_ext.js | 10 ++ src/data/bucket/line_bucket.js | 72 ++++++++++--- src/gl/context.js | 2 + src/render/draw_line.js | 48 +++++++-- src/render/program/line_program.js | 12 ++- src/shaders/line_gradient.fragment.glsl | 8 +- src/shaders/line_gradient.vertex.glsl | 13 +-- src/style/style_layer/heatmap_style_layer.js | 8 +- src/style/style_layer/line_style_layer.js | 23 ++-- src/util/color_ramp.js | 57 +++++++--- src/util/util.js | 20 ++++ .../expected.png | Bin 0 -> 232 bytes .../style.json | 98 ++++++++++++++++++ .../gradient-step-zoomed/expected.png | Bin 0 -> 3397 bytes .../gradient-step-zoomed/style.json | 98 ++++++++++++++++++ .../line-gradient/gradient-step/expected.png | Bin 0 -> 1955 bytes .../line-gradient/gradient-step/style.json | 87 ++++++++++++++++ test/unit/util/color_ramp.test.js | 64 +++++++++++- test/unit/util/util.test.js | 35 ++++++- 21 files changed, 619 insertions(+), 105 deletions(-) create mode 100644 src/data/bucket/line_attributes_ext.js create mode 100644 test/integration/render-tests/line-gradient/gradient-step-zoomed-cross-continental/expected.png create mode 100644 test/integration/render-tests/line-gradient/gradient-step-zoomed-cross-continental/style.json create mode 100644 test/integration/render-tests/line-gradient/gradient-step-zoomed/expected.png create mode 100644 test/integration/render-tests/line-gradient/gradient-step-zoomed/style.json create mode 100644 test/integration/render-tests/line-gradient/gradient-step/expected.png create mode 100644 test/integration/render-tests/line-gradient/gradient-step/style.json diff --git a/build/generate-struct-arrays.js b/build/generate-struct-arrays.js index 92f8bfde695..188e3991435 100644 --- a/build/generate-struct-arrays.js +++ b/build/generate-struct-arrays.js @@ -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 @@ -136,6 +137,7 @@ const layoutAttributes = { 'fill-extrusion': fillExtrusionAttributes, heatmap: circleAttributes, line: lineAttributes, + lineExt: lineAttributesExt, pattern: patternAttributes }; for (const name in layoutAttributes) { diff --git a/src/data/array_types.js b/src/data/array_types.js index c502d723a53..91cfd05191f 100644 --- a/src/data/array_types.js +++ b/src/data/array_types.js @@ -147,6 +147,38 @@ class StructArrayLayout2i4ub8 extends StructArray { StructArrayLayout2i4ub8.prototype.bytesPerElement = 8; register('StructArrayLayout2i4ub8', StructArrayLayout2i4ub8); +/** + * Implementation of the StructArray layout: + * [0]: Float32[2] + * + * @private + */ +class StructArrayLayout2f8 extends StructArray { + uint8: Uint8Array; + float32: Float32Array; + + _refreshViews() { + this.uint8 = new Uint8Array(this.arrayBuffer); + this.float32 = new Float32Array(this.arrayBuffer); + } + + emplaceBack(v0: number, v1: number) { + const i = this.length; + this.resize(i + 1); + return this.emplace(i, v0, v1); + } + + emplace(i: number, v0: number, v1: number) { + const o4 = i * 2; + this.float32[o4 + 0] = v0; + this.float32[o4 + 1] = v1; + return i; + } +} + +StructArrayLayout2f8.prototype.bytesPerElement = 8; +register('StructArrayLayout2f8', StructArrayLayout2f8); + /** * Implementation of the StructArray layout: * [0]: Uint16[8] @@ -747,38 +779,6 @@ class StructArrayLayout1ui2 extends StructArray { StructArrayLayout1ui2.prototype.bytesPerElement = 2; register('StructArrayLayout1ui2', StructArrayLayout1ui2); -/** - * Implementation of the StructArray layout: - * [0]: Float32[2] - * - * @private - */ -class StructArrayLayout2f8 extends StructArray { - uint8: Uint8Array; - float32: Float32Array; - - _refreshViews() { - this.uint8 = new Uint8Array(this.arrayBuffer); - this.float32 = new Float32Array(this.arrayBuffer); - } - - emplaceBack(v0: number, v1: number) { - const i = this.length; - this.resize(i + 1); - return this.emplace(i, v0, v1); - } - - emplace(i: number, v0: number, v1: number) { - const o4 = i * 2; - this.float32[o4 + 0] = v0; - this.float32[o4 + 1] = v1; - return i; - } -} - -StructArrayLayout2f8.prototype.bytesPerElement = 8; -register('StructArrayLayout2f8', StructArrayLayout2f8); - /** * Implementation of the StructArray layout: * [0]: Float32[4] @@ -1058,6 +1058,7 @@ export { StructArrayLayout4i8, StructArrayLayout2i4i12, StructArrayLayout2i4ub8, + StructArrayLayout2f8, StructArrayLayout8ui2ub18, StructArrayLayout4i4ui4i24, StructArrayLayout3f12, @@ -1073,7 +1074,6 @@ export { StructArrayLayout3ui6, StructArrayLayout2ui4, StructArrayLayout1ui2, - StructArrayLayout2f8, StructArrayLayout4f16, StructArrayLayout2i4 as PosArray, StructArrayLayout4i8 as RasterBoundsArray, @@ -1082,6 +1082,7 @@ export { StructArrayLayout2i4i12 as FillExtrusionLayoutArray, StructArrayLayout2i4 as HeatmapLayoutArray, StructArrayLayout2i4ub8 as LineLayoutArray, + StructArrayLayout2f8 as LineExtLayoutArray, StructArrayLayout8ui2ub18 as PatternLayoutArray, StructArrayLayout4i4ui4i24 as SymbolLayoutArray, StructArrayLayout3f12 as SymbolDynamicLayoutArray, diff --git a/src/data/bucket/line_attributes_ext.js b/src/data/bucket/line_attributes_ext.js new file mode 100644 index 00000000000..d963b19957e --- /dev/null +++ b/src/data/bucket/line_attributes_ext.js @@ -0,0 +1,10 @@ +// @flow +import {createLayout} from '../../util/struct_array'; + +const lineLayoutAttributesExt = createLayout([ + {name: 'a_uv_x', components: 1, type: 'Float32'}, + {name: 'a_split_index', components: 1, type: 'Float32'}, +]); + +export default lineLayoutAttributesExt; +export const {members, size, alignment} = lineLayoutAttributesExt; diff --git a/src/data/bucket/line_bucket.js b/src/data/bucket/line_bucket.js index db661fa67a3..b9eaf2159d6 100644 --- a/src/data/bucket/line_bucket.js +++ b/src/data/bucket/line_bucket.js @@ -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'; @@ -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'; @@ -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; @@ -88,9 +96,15 @@ class LineBucket implements Bucket { stateDependentLayers: Array; stateDependentLayerIds: Array; patternFeatures: Array; + lineClipsArray: Array; layoutVertexArray: LineLayoutArray; layoutVertexBuffer: VertexBuffer; + layoutVertexArray2: LineExtLayoutArray; + layoutVertexBuffer2: VertexBuffer; + gradientTexture: Texture; + gradient: ?RGBAImage; + gradientVersion: number; indexArray: TriangleIndexArray; indexBuffer: IndexBuffer; @@ -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(layoutAttributes, options.layers, options.zoom); this.segments = new SegmentVector(); + this.maxLineLength = 0; this.stateDependentLayerIds = this.layers.filter((l) => l.isStateDependent()).map((l) => l.id); } @@ -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); } @@ -211,12 +231,25 @@ class LineBucket implements Bucket { this.segments.destroy(); } +<<<<<<< HEAD addFeature(feature: BucketFeature, geometry: Array>, index: number, canonical: CanonicalTileID, 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>, index: number, imagePositions: {[_: string]: ImagePosition}) { +>>>>>>> 4e6fde313... Revise step interpolation for lines, increase precision of line progress as needed (#9694) 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, canonical, imagePositions); @@ -228,18 +261,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'; @@ -504,8 +533,9 @@ class LineBucket implements Bucket { } addHalfVertex({x, y}: Point, extrudeX: number, extrudeY: number, round: boolean, up: boolean, dir: number, segment: Segment) { + const totalDistance = this.lineClips ? this.scaledDistance * (MAX_LINE_DISTANCE - 1) : this.scaledDistance; // scale down so that we can store longer distances while sacrificing precision. - const linesofar = this.scaledDistance * LINE_DISTANCE_SCALE; + const linesofarScaled = totalDistance * LINE_DISTANCE_SCALE; this.layoutVertexArray.emplaceBack( // a_pos_normal @@ -517,11 +547,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) { + const progressRealigned = this.scaledDistance - this.lineClips.start; + const endClipRealigned = this.lineClips.end - this.lineClips.start; + const uvX = progressRealigned / endClipRealigned; + this.layoutVertexArray2.emplaceBack(uvX, this.lineClipsArray.length); + } const e = segment.vertexLength++; if (this.e1 >= 0 && this.e2 >= 0) { @@ -540,8 +578,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; } diff --git a/src/gl/context.js b/src/gl/context.js index 550f97ba4ab..127be9f5faf 100644 --- a/src/gl/context.js +++ b/src/gl/context.js @@ -27,6 +27,7 @@ class Context { gl: WebGLRenderingContext; extVertexArrayObject: any; currentNumAttributes: ?number; + maxTextureSize: number; clearColor: ClearColor; clearDepth: ClearDepth; @@ -116,6 +117,7 @@ class Context { } this.extTimerQuery = gl.getExtension('EXT_disjoint_timer_query'); + this.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE); } setDefault() { diff --git a/src/render/draw_line.js b/src/render/draw_line.js index c9939941e5b..0bd037e3c01 100644 --- a/src/render/draw_line.js +++ b/src/render/draw_line.js @@ -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) { if (painter.renderPass !== 'translucent') return; @@ -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); @@ -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) { @@ -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 diff --git a/src/render/program/line_program.js b/src/render/program/line_program.js index 4ab16c3e5c8..79fa83c2340 100644 --- a/src/render/program/line_program.js +++ b/src/render/program/line_program.js @@ -32,7 +32,8 @@ export type LineGradientUniformsType = {| 'u_ratio': Uniform1f, 'u_device_pixel_ratio': Uniform1f, 'u_units_to_pixels': Uniform2f, - 'u_image': Uniform1i + 'u_image': Uniform1i, + 'u_image_height': Uniform1f, |}; export type LinePatternUniformsType = {| @@ -72,7 +73,8 @@ const lineGradientUniforms = (context: Context, locations: UniformLocations): Li 'u_ratio': new Uniform1f(context, locations.u_ratio), 'u_device_pixel_ratio': new Uniform1f(context, locations.u_device_pixel_ratio), 'u_units_to_pixels': new Uniform2f(context, locations.u_units_to_pixels), - 'u_image': new Uniform1i(context, locations.u_image) + 'u_image': new Uniform1i(context, locations.u_image), + 'u_image_height': new Uniform1f(context, locations.u_image_height), }); const linePatternUniforms = (context: Context, locations: UniformLocations): LinePatternUniformsType => ({ @@ -121,10 +123,12 @@ const lineUniformValues = ( const lineGradientUniformValues = ( painter: Painter, tile: Tile, - layer: LineStyleLayer + layer: LineStyleLayer, + imageHeight: number ): UniformValues => { return extend(lineUniformValues(painter, tile, layer), { - 'u_image': 0 + 'u_image': 0, + 'u_image_height': imageHeight, }); }; diff --git a/src/shaders/line_gradient.fragment.glsl b/src/shaders/line_gradient.fragment.glsl index ea168c4966e..1bb3806e9d8 100644 --- a/src/shaders/line_gradient.fragment.glsl +++ b/src/shaders/line_gradient.fragment.glsl @@ -4,7 +4,7 @@ uniform sampler2D u_image; varying vec2 v_width2; varying vec2 v_normal; varying float v_gamma_scale; -varying highp float v_lineprogress; +varying highp vec2 v_uv; #pragma mapbox: define lowp float blur #pragma mapbox: define lowp float opacity @@ -22,9 +22,9 @@ void main() { float blur2 = (blur + 1.0 / u_device_pixel_ratio) * v_gamma_scale; float alpha = clamp(min(dist - (v_width2.t - blur2), v_width2.s - dist) / blur2, 0.0, 1.0); - // For gradient lines, v_lineprogress is the ratio along the entire line, - // scaled to [0, 2^15), and the gradient ramp is stored in a texture. - vec4 color = texture2D(u_image, vec2(v_lineprogress, 0.5)); + // For gradient lines, v_lineprogress is the ratio along the + // entire line, the gradient ramp is stored in a texture. + vec4 color = texture2D(u_image, v_uv); gl_FragColor = color * (alpha * opacity); diff --git a/src/shaders/line_gradient.vertex.glsl b/src/shaders/line_gradient.vertex.glsl index 98281e2843c..4b02ba5337e 100644 --- a/src/shaders/line_gradient.vertex.glsl +++ b/src/shaders/line_gradient.vertex.glsl @@ -1,7 +1,3 @@ - -// the attribute conveying progress along a line is scaled to [0, 2^15) -#define MAX_LINE_DISTANCE 32767.0 - // floor(127 / 2) == 63.0 // the maximum allowed miter limit is 2.0 at the moment. the extrude normal is // stored in a byte (-128..127). we scale regular normals up to length 63, but @@ -12,16 +8,19 @@ attribute vec2 a_pos_normal; attribute vec4 a_data; +attribute float a_uv_x; +attribute float a_split_index; uniform mat4 u_matrix; uniform mediump float u_ratio; uniform lowp float u_device_pixel_ratio; uniform vec2 u_units_to_pixels; +uniform float u_image_height; varying vec2 v_normal; varying vec2 v_width2; varying float v_gamma_scale; -varying highp float v_lineprogress; +varying highp vec2 v_uv; #pragma mapbox: define lowp float blur #pragma mapbox: define lowp float opacity @@ -43,7 +42,9 @@ void main() { vec2 a_extrude = a_data.xy - 128.0; float a_direction = mod(a_data.z, 4.0) - 1.0; - v_lineprogress = (floor(a_data.z / 4.0) + a_data.w * 64.0) * 2.0 / MAX_LINE_DISTANCE; + highp float texel_height = 1.0 / u_image_height; + highp float half_texel_height = 0.5 * texel_height; + v_uv = vec2(a_uv_x, a_split_index * texel_height - half_texel_height); vec2 pos = floor(a_pos_normal * 0.5); diff --git a/src/style/style_layer/heatmap_style_layer.js b/src/style/style_layer/heatmap_style_layer.js index 2ae9575346d..a8f115c798e 100644 --- a/src/style/style_layer/heatmap_style_layer.js +++ b/src/style/style_layer/heatmap_style_layer.js @@ -5,7 +5,7 @@ import StyleLayer from '../style_layer'; import HeatmapBucket from '../../data/bucket/heatmap_bucket'; import {RGBAImage} from '../../util/image'; import properties from './heatmap_style_layer_properties'; -import renderColorRamp from '../../util/color_ramp'; +import {renderColorRamp} from '../../util/color_ramp'; import {Transitionable, Transitioning, PossiblyEvaluated} from '../properties'; import type Texture from '../../render/texture'; @@ -42,7 +42,11 @@ class HeatmapStyleLayer extends StyleLayer { _updateColorRamp() { const expression = this._transitionablePaint._values['heatmap-color'].value.expression; - this.colorRamp = renderColorRamp(expression, 'heatmapDensity'); + this.colorRamp = renderColorRamp({ + expression, + evaluationKey: 'heatmapDensity', + image: this.colorRamp + }); this.colorRampTexture = null; } diff --git a/src/style/style_layer/line_style_layer.js b/src/style/style_layer/line_style_layer.js index 336e60d45ec..480975c7428 100644 --- a/src/style/style_layer/line_style_layer.js +++ b/src/style/style_layer/line_style_layer.js @@ -4,20 +4,18 @@ import Point from '@mapbox/point-geometry'; import StyleLayer from '../style_layer'; import LineBucket from '../../data/bucket/line_bucket'; -import {RGBAImage} from '../../util/image'; import {polygonIntersectsBufferedMultiLine} from '../../util/intersection_tests'; import {getMaximumPaintValue, translateDistance, translate} from '../query_utils'; import properties from './line_style_layer_properties'; -import {extend} from '../../util/util'; +import {extend, MAX_SAFE_INTEGER} from '../../util/util'; import EvaluationParameters from '../evaluation_parameters'; -import renderColorRamp from '../../util/color_ramp'; import {Transitionable, Transitioning, Layout, PossiblyEvaluated, DataDrivenProperty} from '../properties'; -import type {FeatureState} from '../../style-spec/expression'; +import Step from '../../style-spec/expression/definitions/step'; +import type {FeatureState, ZoomConstantExpression} from '../../style-spec/expression'; import type {Bucket, BucketParameters} from '../../data/bucket'; import type {LayoutProps, PaintProps} from './line_style_layer_properties'; import type Transform from '../../geo/transform'; -import type Texture from '../../render/texture'; import type {LayerSpecification} from '../../style-spec/types'; class LineFloorwidthProperty extends DataDrivenProperty { @@ -46,8 +44,8 @@ class LineStyleLayer extends StyleLayer { _unevaluatedLayout: Layout; layout: PossiblyEvaluated; - gradient: ?RGBAImage; - gradientTexture: ?Texture; + gradientVersion: number; + stepInterpolant: boolean; _transitionablePaint: Transitionable; _transitioningPaint: Transitioning; @@ -55,18 +53,19 @@ class LineStyleLayer extends StyleLayer { constructor(layer: LayerSpecification) { super(layer, properties); + this.gradientVersion = 0; } _handleSpecialPaintPropertyUpdate(name: string) { if (name === 'line-gradient') { - this._updateGradient(); + const expression: ZoomConstantExpression<'source'> = ((this._transitionablePaint._values['line-gradient'].value.expression): any); + this.stepInterpolant = expression._styleExpression.expression instanceof Step; + this.gradientVersion = (this.gradientVersion + 1) % MAX_SAFE_INTEGER; } } - _updateGradient() { - const expression = this._transitionablePaint._values['line-gradient'].value.expression; - this.gradient = renderColorRamp(expression, 'lineProgress'); - this.gradientTexture = null; + gradientExpression() { + return this._transitionablePaint._values['line-gradient'].value.expression; } recalculate(parameters: EvaluationParameters, availableImages: Array) { diff --git a/src/util/color_ramp.js b/src/util/color_ramp.js index a6c93a6c535..24dc0758699 100644 --- a/src/util/color_ramp.js +++ b/src/util/color_ramp.js @@ -1,28 +1,61 @@ // @flow import {RGBAImage} from './image'; +import {isPowerOfTwo} from './util'; +import assert from 'assert'; import type {StylePropertyExpression} from '../style-spec/expression/index'; +export type ColorRampParams = { + expression: StylePropertyExpression; + evaluationKey: string; + resolution?: number; + image?: RGBAImage; + clips?: Array; +} + /** - * Given an expression that should evaluate to a color ramp, return - * a 256x1 px RGBA image representing that ramp expression. + * Given an expression that should evaluate to a color ramp, + * return a RGBA image representing that ramp expression. * * @private */ -export default function renderColorRamp(expression: StylePropertyExpression, colorRampEvaluationParameter: string): RGBAImage { - const colorRampData = new Uint8Array(256 * 4); +export function renderColorRamp(params: ColorRampParams): RGBAImage { const evaluationGlobals = {}; - for (let i = 0, j = 0; i < 256; i++, j += 4) { - evaluationGlobals[colorRampEvaluationParameter] = i / 255; - const pxColor = expression.evaluate((evaluationGlobals: any)); + const width = params.resolution || 256; + const height = params.clips ? params.clips.length : 1; + const image = params.image || new RGBAImage({width, height}); + + assert(isPowerOfTwo(width)); + + const renderPixel = (stride, index, progress) => { + evaluationGlobals[params.evaluationKey] = progress; + const pxColor = params.expression.evaluate((evaluationGlobals: any)); // the colors are being unpremultiplied because Color uses // premultiplied values, and the Texture class expects unpremultiplied ones - colorRampData[j + 0] = Math.floor(pxColor.r * 255 / pxColor.a); - colorRampData[j + 1] = Math.floor(pxColor.g * 255 / pxColor.a); - colorRampData[j + 2] = Math.floor(pxColor.b * 255 / pxColor.a); - colorRampData[j + 3] = Math.floor(pxColor.a * 255); + image.data[stride + index + 0] = Math.floor(pxColor.r * 255 / pxColor.a); + image.data[stride + index + 1] = Math.floor(pxColor.g * 255 / pxColor.a); + image.data[stride + index + 2] = Math.floor(pxColor.b * 255 / pxColor.a); + image.data[stride + index + 3] = Math.floor(pxColor.a * 255); + }; + + if (!params.clips) { + for (let i = 0, j = 0; i < width; i++, j += 4) { + const progress = i / (width - 1); + + renderPixel(0, j, progress); + } + } else { + for (let clip = 0, stride = 0; clip < height; ++clip, stride += width * 4) { + for (let i = 0, j = 0; i < width; i++, j += 4) { + // Remap progress between clips + const progress = i / (width - 1); + const {start, end} = params.clips[clip]; + const evaluationProgress = start * (1 - progress) + end * progress; + renderPixel(stride, j, evaluationProgress); + } + } } - return new RGBAImage({width: 256, height: 1}, colorRampData); + return image; } diff --git a/src/util/util.js b/src/util/util.js index d41a3a31edc..f5e3d55c58a 100644 --- a/src/util/util.js +++ b/src/util/util.js @@ -7,6 +7,9 @@ import window from './window'; import type {Callback} from '../types/callback'; +// Number.MAX_SAFE_INTEGER not available in IE +export const MAX_SAFE_INTEGER = Math.pow(2, 53) - 1; + /** * @module util * @private @@ -209,6 +212,23 @@ export function uuid(): string { return b(); } +/** + * Return whether a given value is a power of two + * @private + */ +export function isPowerOfTwo(value: number): boolean { + return (Math.log(value) / Math.LN2) % 1 === 0; +} + +/** + * Return the next power of two, or the input value if already a power of two + * @private + */ +export function nextPowerOfTwo(value: number): number { + if (value <= 1) return 1; + return Math.pow(2, Math.ceil(Math.log(value) / Math.LN2)); +} + /** * Validate a string to match UUID(v4) of the * form: xxxxxxxx-xxxx-4xxx-[89ab]xxx-xxxxxxxxxxxx diff --git a/test/integration/render-tests/line-gradient/gradient-step-zoomed-cross-continental/expected.png b/test/integration/render-tests/line-gradient/gradient-step-zoomed-cross-continental/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..e3ca9002c1944a9c0a2f5f386871121816418e44 GIT binary patch literal 232 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K595~p3%DgT z+uth|M??6oI^WrMvHouP#i}(Qr7lM_ZQdQ#Z`iAPBPvPCyWm@F?GY2ctLbMR?2Ub$ i-@9EZ9_&I~R&03B|KpON7`yH%urg0qKbLh*2~7aY>qy!F literal 0 HcmV?d00001 diff --git a/test/integration/render-tests/line-gradient/gradient-step-zoomed-cross-continental/style.json b/test/integration/render-tests/line-gradient/gradient-step-zoomed-cross-continental/style.json new file mode 100644 index 00000000000..821d6d5042d --- /dev/null +++ b/test/integration/render-tests/line-gradient/gradient-step-zoomed-cross-continental/style.json @@ -0,0 +1,98 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 256, + "height": 64, + "allowed": 0.0015 + } + }, + "zoom": 17, + "center": [ + -77.0336537, + 38.89992645 + ], + "sources": { + "gradient": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -77.033643, + 38.899926 + ], + [ + -120.033643, + 38.899926 + ] + ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -77.033643, + 38.899926 + ], + [ + -120.033643, + 38.899926 + ] + ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -77.033643, + 38.899926 + ], + [ + -120.033643, + 38.899926 + ] + ] + } + } + ] + }, + "lineMetrics": true + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "gradient", + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": 5, + "line-gradient": [ + "step", + ["line-progress"], + "rgba(0, 0, 255, 0.1)", + 0.000001, "red", + 0.000002, "yellow", + 0.0000025, "orange", + 0.000003, "black", + 1, "black" + ] + } + } + ] +} diff --git a/test/integration/render-tests/line-gradient/gradient-step-zoomed/expected.png b/test/integration/render-tests/line-gradient/gradient-step-zoomed/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..b8a81340fecd1aabb0f54c779451e114e9a9ceb0 GIT binary patch literal 3397 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 Uh9goVKVC@~m!r;&=o6X$1ma*oWB>pF literal 0 HcmV?d00001 diff --git a/test/integration/render-tests/line-gradient/gradient-step-zoomed/style.json b/test/integration/render-tests/line-gradient/gradient-step-zoomed/style.json new file mode 100644 index 00000000000..67161b62bac --- /dev/null +++ b/test/integration/render-tests/line-gradient/gradient-step-zoomed/style.json @@ -0,0 +1,98 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 256, + "height": 128, + "allowed": 0.0015 + } + }, + "zoom": 13, + "center": [ + -79.6343, + 33.6367 + ], + "sources": { + "gradient": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -79.65087890624999, + 33.63291573870479 + ], + [ + -77.9725456237793, + 34.864383802309696 + ] + ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -79.64087890624999, + 33.63291573870479 + ], + [ + -77.9625456237793, + 34.864383802309696 + ] + ] + } + }, + { + "type": "Feature", + "geometry": { + "type": "LineString", + "coordinates": [ + [ + -79.63087890624999, + 33.63291573870479 + ], + [ + -77.9525456237793, + 34.864383802309696 + ] + ] + } + } + ] + }, + "lineMetrics": true + } + }, + "layers": [ + { + "id": "line", + "type": "line", + "source": "gradient", + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": 5, + "line-gradient": [ + "step", + ["line-progress"], + "rgba(0, 0, 255, 0.1)", + 0.001, "red", + 0.002, "yellow", + 0.0025, "orange", + 0.003, "black", + 1, "black" + ] + } + } + ] +} diff --git a/test/integration/render-tests/line-gradient/gradient-step/expected.png b/test/integration/render-tests/line-gradient/gradient-step/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..33fd6f5e87d212d1def1a75e188715de39a08593 GIT binary patch literal 1955 zcmV;U2VD4xP)@9(C(l_Gt9(nBvzJPwPi+IX%_aqFCebgvZ7W_ z`Z2&w2NxK1IZEz;QQGJSf3oNgjleQ3bA%Z81_r@>e|Ny$cW&MD7&2tYkRd}FDL1zJ(KUvOPky*KomfEq3a% zS_2d{SYl^lS!5f|wo7W&Q!rVpS31=l*4beI6%CTay2#z3q*ZW)O5;4+1t$5qPJc2$ ziUv{Qw_4qXae}cbjl$(xY|&-C0Z=s9x;(DSBCST?EY$?(YjKn=9r{_(GexJBTKy4I zQBW>8U5geIt+mEu`bqE%kh80Ec6JCJzU5wPXDro*Q&jhetWB~^KL`d<&fbGrc!_0@Cw;FCGU3-iUwT12JM(=ml%^i8Wp4Nl&rSKXtTK)w*o z{wFV?aG$#J3-AFI@;wyVRTEuQd7v{%U(!PixjwMjOpqo|I&*bRc2>RT>&XSmc&X_maFbK-F}!CnyGgWs&pc`s6uYWhcQuY+f4a*QLsQY-P+8u zSsxYEk=k{|X?6-Gt0p)%USxC}Z$;{oRh708UxQ)x3D`t>%+vDm#J6Aq4pZCx#&&c# z??X2Uj#VzZ+H5yF07bn>?N-OP`lW)iFU<;ulh6}(ZUMUsy>Kt+9!zT<(?#g>Mw8Dms>VQ6+)j5eiYg9>ptUZ#g! zAhZxpQr{}V64E20tMO5lHpt$h%fB6iqxH?b4yFIDt*liaV< z?>sFYk7Lx|_4xLA7nb ze8O7gqR-%5<+4?hnGR9VAEIk9TbW$4*SBZPSUmHVTQ0d6$Ju8Kc@>3?swuftuu!E( zJmn16`JF>l)E_#n*5a3fDR_=ua>@QqzUZR%vyVCEFSmdH`)_^-f5&>l?W9LlQ_?J0 zj%O*C%`nH0^@E^4m5jv#K}#;F<|(v=;AXvv?!sxxWhKdM{iLWrb$L{mZ{<~v#%U@Q z@z$7(NUT&#bQ8{3X^G?t{itXFq@*K8TQOC+V7Id(XGS^`kK13#m4Z*H^lRA#x@^~v z3I<3BZo~xqN_FSpCz3bWtE5%%Mb!jtlE3R`MFS`$55;I9PQo}rxg%1m8Cq@A;sL?! z7^z(LA< Math.abs(e - b[i]) <= 3); } -test('renderColorRamp', (t) => { +test('renderColorRamp linear', (t) => { const expression = createPropertyExpression([ 'interpolate', @@ -34,7 +34,7 @@ test('renderColorRamp', (t) => { 1, 'red' ], spec, {handleErrors: false}).value; - const ramp = renderColorRamp(expression, 'lineProgress'); + const ramp = renderColorRamp({expression, evaluationKey: 'lineProgress'}); t.equal(ramp.width, 256); t.equal(ramp.height, 1); @@ -47,3 +47,61 @@ test('renderColorRamp', (t) => { t.end(); }); + +test('renderColorRamp step', (t) => { + + const expression = createPropertyExpression([ + 'step', + ['line-progress'], + 'rgba(0, 0, 255, 0.1)', + 0.1, 'red', + 0.2, 'yellow', + 0.3, 'white', + 0.5, 'black', + 1, 'black' + ], spec, {handleErrors: false}).value; + + const ramp = renderColorRamp({expression, evaluationKey: 'lineProgress', resolution: 512}); + + t.equal(ramp.width, 512); + t.equal(ramp.height, 1); + + t.equal(pixelAt(ramp, 0)[3], 25, 'pixel at 0.0 matches input alpha'); + t.ok(nearlyEquals(pixelAt(ramp, 50), [0, 0, 255, 25]), 'pixel < 0.1 matches input'); + t.ok(nearlyEquals(pixelAt(ramp, 53), [255, 0, 0, 255]), 'pixel > 0.1 & < 0.2 matches input'); + t.ok(nearlyEquals(pixelAt(ramp, 103), [255, 255, 0, 255]), 'pixel > 0.2 & < 0.3 matches input'); + t.ok(nearlyEquals(pixelAt(ramp, 160), [255, 255, 255, 255]), 'pixel > 0.3 & < 0.5 matches input'); + t.ok(nearlyEquals(pixelAt(ramp, 256), [0, 0, 0, 255]), 'pixel > 0.5 matches input'); + + t.end(); +}); + +test('renderColorRamp usePlacement', (t) => { + + const expression = createPropertyExpression([ + 'step', + ['line-progress'], + 'rgba(255, 0, 0, 0.5)', + 0.1, 'black', + 0.2, 'red', + 0.3, 'blue', + 0.5, 'white', + 1, 'white' + ], spec, {handleErrors: false}).value; + + const ramp = renderColorRamp({expression, evaluationKey: 'lineProgress', resolution: 512}); + + t.equal(ramp.width, 512); + t.equal(ramp.height, 1); + + renderColorRamp({expression, evaluationKey: 'lineProgress', resolution: 512, image: ramp}); + + t.equal(pixelAt(ramp, 0)[3], 127, 'pixel at 0.0 matches input alpha'); + t.ok(nearlyEquals(pixelAt(ramp, 50), [255, 0, 0, 127]), 'pixel < 0.1 matches input'); + t.ok(nearlyEquals(pixelAt(ramp, 53), [0, 0, 0, 255]), 'pixel > 0.1 & < 0.2 matches input'); + t.ok(nearlyEquals(pixelAt(ramp, 103), [255, 0, 0, 255]), 'pixel > 0.2 & < 0.3 matches input'); + t.ok(nearlyEquals(pixelAt(ramp, 160), [0, 0, 255, 255]), 'pixel > 0.3 & < 0.5 matches input'); + t.ok(nearlyEquals(pixelAt(ramp, 256), [255, 255, 255, 255]), 'pixel > 0.5 matches input'); + + t.end(); +}); diff --git a/test/unit/util/util.test.js b/test/unit/util/util.test.js index b858b5b77f5..bad799c54bb 100644 --- a/test/unit/util/util.test.js +++ b/test/unit/util/util.test.js @@ -2,7 +2,7 @@ import {test} from '../../util/test'; -import {easeCubicInOut, keysDifference, extend, pick, uniqueId, bindAll, asyncAll, clamp, wrap, bezier, endsWith, mapObject, filterObject, deepEqual, clone, arraysIntersect, isCounterClockwise, isClosedPolygon, parseCacheControl, uuid, validateUuid} from '../../../src/util/util'; +import {easeCubicInOut, keysDifference, extend, pick, uniqueId, bindAll, asyncAll, clamp, wrap, bezier, endsWith, mapObject, filterObject, deepEqual, clone, arraysIntersect, isCounterClockwise, isClosedPolygon, parseCacheControl, uuid, validateUuid, nextPowerOfTwo, isPowerOfTwo} from '../../../src/util/util'; import Point from '@mapbox/point-geometry'; test('util', (t) => { @@ -74,6 +74,39 @@ test('util', (t) => { t.end(); }); + t.test('isPowerOfTwo', (t) => { + t.equal(isPowerOfTwo(1), true); + t.equal(isPowerOfTwo(2), true); + t.equal(isPowerOfTwo(256), true); + t.equal(isPowerOfTwo(-256), false); + t.equal(isPowerOfTwo(0), false); + t.equal(isPowerOfTwo(-42), false); + t.equal(isPowerOfTwo(42), false); + t.end(); + }); + + t.test('nextPowerOfTwo', (t) => { + t.equal(nextPowerOfTwo(1), 1); + t.equal(nextPowerOfTwo(2), 2); + t.equal(nextPowerOfTwo(256), 256); + t.equal(nextPowerOfTwo(-256), 1); + t.equal(nextPowerOfTwo(0), 1); + t.equal(nextPowerOfTwo(-42), 1); + t.equal(nextPowerOfTwo(42), 64); + t.end(); + }); + + t.test('nextPowerOfTwo', (t) => { + t.equal(isPowerOfTwo(nextPowerOfTwo(1)), true); + t.equal(isPowerOfTwo(nextPowerOfTwo(2)), true); + t.equal(isPowerOfTwo(nextPowerOfTwo(256)), true); + t.equal(isPowerOfTwo(nextPowerOfTwo(-256)), true); + t.equal(isPowerOfTwo(nextPowerOfTwo(0)), true); + t.equal(isPowerOfTwo(nextPowerOfTwo(-42)), true); + t.equal(isPowerOfTwo(nextPowerOfTwo(42)), true); + t.end(); + }); + t.test('clamp', (t) => { t.equal(clamp(0, 0, 1), 0); t.equal(clamp(1, 0, 1), 1);