UNPKG

mapbox-gl

Version:
691 lines (603 loc) 29.7 kB
// @flow import {packUint8ToFloat} from '../shaders/encode_attribute.js'; import Color from '../style-spec/util/color.js'; import {supportsPropertyExpression} from '../style-spec/util/properties.js'; import {register} from '../util/web_worker_transfer.js'; import {PossiblyEvaluatedPropertyValue} from '../style/properties.js'; import {StructArrayLayout1f4, StructArrayLayout2f8, StructArrayLayout4f16, PatternLayoutArray, DashLayoutArray} from './array_types.js'; import {clamp} from '../util/util.js'; import patternAttributes from './bucket/pattern_attributes.js'; import dashAttributes from './bucket/dash_attributes.js'; import EvaluationParameters from '../style/evaluation_parameters.js'; import FeaturePositionMap from './feature_position_map.js'; import { Uniform, Uniform1f, UniformColor, Uniform4f } from '../render/uniform_binding.js'; import assert from 'assert'; import type {CanonicalTileID} from '../source/tile_id.js'; import type Context from '../gl/context.js'; import type {TypedStyleLayer} from '../style/style_layer/typed_style_layer.js'; import type {StructArray, StructArrayMember} from '../util/struct_array.js'; import type VertexBuffer from '../gl/vertex_buffer.js'; import type {SpritePosition, SpritePositions} from '../util/image.js'; import type { Feature, FeatureState, GlobalProperties, SourceExpression, CompositeExpression } from '../style-spec/expression/index.js'; import type {PossiblyEvaluated} from '../style/properties.js'; import type {FeatureStates} from '../source/source_state.js'; import type {FormattedSection} from '../style-spec/expression/types/formatted.js'; import type {IVectorTileLayer} from '@mapbox/vector-tile'; export type BinderUniform = { name: string, property: string, binding: Uniform<any> }; function packColor(color: Color): [number, number] { return [ packUint8ToFloat(255 * color.r, 255 * color.g), packUint8ToFloat(255 * color.b, 255 * color.a) ]; } /** * `Binder` is the interface definition for the strategies for constructing, * uploading, and binding paint property data as GLSL attributes. Most style- * spec properties have a 1:1 relationship to shader attribute/uniforms, but * some require multiple values per feature to be passed to the GPU, and in * those cases we bind multiple attributes/uniforms. * * It has three implementations, one for each of the three strategies we use: * * * For _constant_ properties -- those whose value is a constant, or the constant * result of evaluating a camera expression at a particular camera position -- we * don't need a vertex attribute buffer, and instead use a uniform. * * For data expressions, we use a vertex buffer with a single attribute value, * the evaluated result of the source function for the given feature. * * For composite expressions, we use a vertex buffer with two attributes: min and * max values covering the range of zooms at which we expect the tile to be * displayed. These values are calculated by evaluating the composite expression for * the given feature at strategically chosen zoom levels. In addition to this * attribute data, we also use a uniform value which the shader uses to interpolate * between the min and max value at the final displayed zoom level. The use of a * uniform allows us to cheaply update the value on every frame. * * Note that the shader source varies depending on whether we're using a uniform or * attribute. We dynamically compile shaders at runtime to accommodate this. * * @private */ interface AttributeBinder { populatePaintArray(length: number, feature: Feature, imagePositions: SpritePositions, availableImages: Array<string>, canonical?: CanonicalTileID, formattedSection?: FormattedSection): void; updatePaintArray(start: number, length: number, feature: Feature, featureState: FeatureState, availableImages: Array<string>, imagePositions: SpritePositions): void; upload(Context): void; destroy(): void; } interface UniformBinder { uniformNames: Array<string>; setUniform(program: WebGLProgram, uniform: Uniform<*>, globals: GlobalProperties, currentValue: PossiblyEvaluatedPropertyValue<*>, uniformName: string): void; getBinding(context: Context, name: string): $Shape<Uniform<*>>; } class ConstantBinder implements UniformBinder { value: mixed; type: string; uniformNames: Array<string>; constructor(value: mixed, names: Array<string>, type: string) { this.value = value; this.uniformNames = names.map(name => `u_${name}`); this.type = type; } setUniform(program: WebGLProgram, uniform: Uniform<*>, globals: GlobalProperties, currentValue: PossiblyEvaluatedPropertyValue<mixed>, uniformName: string): void { uniform.set(program, uniformName, currentValue.constantOr(this.value)); } // $FlowFixMe[method-unbinding] getBinding(context: Context, _: string): $Shape<Uniform<any>> { // $FlowFixMe[method-unbinding] return (this.type === 'color') ? new UniformColor(context) : new Uniform1f(context); } } class PatternConstantBinder implements UniformBinder { uniformNames: Array<string>; pattern: ?Array<number>; pixelRatio: number; constructor(value: mixed, names: Array<string>) { this.uniformNames = names.map(name => `u_${name}`); this.pattern = null; this.pixelRatio = 1; } setConstantPatternPositions(posTo: SpritePosition) { this.pixelRatio = posTo.pixelRatio || 1; this.pattern = posTo.tl.concat(posTo.br); } setUniform(program: WebGLProgram, uniform: Uniform<*>, globals: GlobalProperties, currentValue: PossiblyEvaluatedPropertyValue<mixed>, uniformName: string) { const pos = uniformName === 'u_pattern' || uniformName === 'u_dash' ? this.pattern : uniformName === 'u_pixel_ratio' ? this.pixelRatio : null; if (pos) uniform.set(program, uniformName, pos); } // $FlowFixMe[method-unbinding] getBinding(context: Context, name: string): $Shape<Uniform<any>> { // $FlowFixMe[method-unbinding] return name === 'u_pattern' || name === 'u_dash' ? new Uniform4f(context) : new Uniform1f(context); } } class SourceExpressionBinder implements AttributeBinder { expression: SourceExpression; type: string; maxValue: number; paintVertexArray: StructArray; paintVertexAttributes: Array<StructArrayMember>; paintVertexBuffer: ?VertexBuffer; constructor(expression: SourceExpression, names: Array<string>, type: string, PaintVertexArray: Class<StructArray>) { this.expression = expression; this.type = type; this.maxValue = 0; this.paintVertexAttributes = names.map((name) => ({ name: `a_${name}`, type: 'Float32', components: type === 'color' ? 2 : 1, offset: 0 })); this.paintVertexArray = new PaintVertexArray(); } populatePaintArray(newLength: number, feature: Feature, imagePositions: SpritePositions, availableImages: Array<string>, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { const start = this.paintVertexArray.length; assert(Array.isArray(availableImages)); // $FlowFixMe[method-unbinding] const value = this.expression.evaluate(new EvaluationParameters(0), feature, {}, canonical, availableImages, formattedSection); this.paintVertexArray.resize(newLength); this._setPaintValue(start, newLength, value); } updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, availableImages: Array<string>) { const value = this.expression.evaluate({zoom: 0}, feature, featureState, undefined, availableImages); this._setPaintValue(start, end, value); } _setPaintValue(start: number, end: number, value: any) { if (this.type === 'color') { const color = packColor(value); for (let i = start; i < end; i++) { this.paintVertexArray.emplace(i, color[0], color[1]); } } else { for (let i = start; i < end; i++) { this.paintVertexArray.emplace(i, value); } this.maxValue = Math.max(this.maxValue, Math.abs(value)); } } upload(context: Context) { if (this.paintVertexArray && this.paintVertexArray.arrayBuffer) { if (this.paintVertexBuffer && this.paintVertexBuffer.buffer) { this.paintVertexBuffer.updateData(this.paintVertexArray); } else { this.paintVertexBuffer = context.createVertexBuffer(this.paintVertexArray, this.paintVertexAttributes, this.expression.isStateDependent); } } } destroy() { if (this.paintVertexBuffer) { this.paintVertexBuffer.destroy(); } } } class CompositeExpressionBinder implements AttributeBinder, UniformBinder { expression: CompositeExpression; uniformNames: Array<string>; type: string; useIntegerZoom: boolean; zoom: number; maxValue: number; paintVertexArray: StructArray; paintVertexAttributes: Array<StructArrayMember>; paintVertexBuffer: ?VertexBuffer; constructor(expression: CompositeExpression, names: Array<string>, type: string, useIntegerZoom: boolean, zoom: number, PaintVertexArray: Class<StructArray>) { this.expression = expression; this.uniformNames = names.map(name => `u_${name}_t`); this.type = type; this.useIntegerZoom = useIntegerZoom; this.zoom = zoom; this.maxValue = 0; this.paintVertexAttributes = names.map((name) => ({ name: `a_${name}`, type: 'Float32', components: type === 'color' ? 4 : 2, offset: 0 })); this.paintVertexArray = new PaintVertexArray(); } populatePaintArray(newLength: number, feature: Feature, imagePositions: SpritePositions, availableImages: Array<string>, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { // $FlowFixMe[method-unbinding] const min = this.expression.evaluate(new EvaluationParameters(this.zoom), feature, {}, canonical, availableImages, formattedSection); // $FlowFixMe[method-unbinding] const max = this.expression.evaluate(new EvaluationParameters(this.zoom + 1), feature, {}, canonical, availableImages, formattedSection); const start = this.paintVertexArray.length; this.paintVertexArray.resize(newLength); this._setPaintValue(start, newLength, min, max); } updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, availableImages: Array<string>) { const min = this.expression.evaluate({zoom: this.zoom}, feature, featureState, undefined, availableImages); const max = this.expression.evaluate({zoom: this.zoom + 1}, feature, featureState, undefined, availableImages); this._setPaintValue(start, end, min, max); } _setPaintValue(start: number, end: number, min: any, max: any) { if (this.type === 'color') { const minColor = packColor(min); const maxColor = packColor(max); for (let i = start; i < end; i++) { this.paintVertexArray.emplace(i, minColor[0], minColor[1], maxColor[0], maxColor[1]); } } else { for (let i = start; i < end; i++) { this.paintVertexArray.emplace(i, min, max); } this.maxValue = Math.max(this.maxValue, Math.abs(min), Math.abs(max)); } } upload(context: Context) { if (this.paintVertexArray && this.paintVertexArray.arrayBuffer) { if (this.paintVertexBuffer && this.paintVertexBuffer.buffer) { this.paintVertexBuffer.updateData(this.paintVertexArray); } else { this.paintVertexBuffer = context.createVertexBuffer(this.paintVertexArray, this.paintVertexAttributes, this.expression.isStateDependent); } } } destroy() { if (this.paintVertexBuffer) { this.paintVertexBuffer.destroy(); } } setUniform(program: WebGLProgram, uniform: Uniform<*>, globals: GlobalProperties, _: PossiblyEvaluatedPropertyValue<*>, uniformName: string): void { const currentZoom = this.useIntegerZoom ? Math.floor(globals.zoom) : globals.zoom; const factor = clamp(this.expression.interpolationFactor(currentZoom, this.zoom, this.zoom + 1), 0, 1); uniform.set(program, uniformName, factor); } // $FlowFixMe[method-unbinding] getBinding(context: Context, _: string): Uniform1f { return new Uniform1f(context); } } class PatternCompositeBinder implements AttributeBinder { expression: CompositeExpression; layerId: string; paintVertexArray: StructArray; paintVertexBuffer: ?VertexBuffer; paintVertexAttributes: Array<StructArrayMember>; constructor(expression: CompositeExpression, names: Array<string>, type: string, PaintVertexArray: Class<StructArray>, layerId: string) { this.expression = expression; this.layerId = layerId; this.paintVertexAttributes = (type === 'array' ? dashAttributes : patternAttributes).members; for (let i = 0; i < names.length; ++i) { assert(`a_${names[i]}` === this.paintVertexAttributes[i].name); } this.paintVertexArray = new PaintVertexArray(); } populatePaintArray(length: number, feature: Feature, imagePositions: SpritePositions) { const start = this.paintVertexArray.length; this.paintVertexArray.resize(length); this._setPaintValues(start, length, feature.patterns && feature.patterns[this.layerId], imagePositions); } updatePaintArray(start: number, end: number, feature: Feature, featureState: FeatureState, availableImages: Array<string>, imagePositions: SpritePositions) { this._setPaintValues(start, end, feature.patterns && feature.patterns[this.layerId], imagePositions); } _setPaintValues(start: number, end: number, patterns: ?string, positions: SpritePositions) { if (!positions || !patterns) return; const pos = positions[patterns]; if (!pos) return; const {tl, br, pixelRatio} = pos; for (let i = start; i < end; i++) { this.paintVertexArray.emplace(i, tl[0], tl[1], br[0], br[1], pixelRatio); } } upload(context: Context) { if (this.paintVertexArray && this.paintVertexArray.arrayBuffer) { this.paintVertexBuffer = context.createVertexBuffer(this.paintVertexArray, this.paintVertexAttributes, this.expression.isStateDependent); } } destroy() { if (this.paintVertexBuffer) this.paintVertexBuffer.destroy(); } } /** * ProgramConfiguration contains the logic for binding style layer properties and tile * layer feature data into GL program uniforms and vertex attributes. * * Non-data-driven property values are bound to shader uniforms. Data-driven property * values are bound to vertex attributes. In order to support a uniform GLSL syntax over * both, [Mapbox GL Shaders](https://github.com/mapbox/mapbox-gl-shaders) defines a `#pragma` * abstraction, which ProgramConfiguration is responsible for implementing. At runtime, * it examines the attributes of a particular layer, combines this with fixed knowledge * about how layers of the particular type are implemented, and determines which uniforms * and vertex attributes will be required. It can then substitute the appropriate text * into the shader source code, create and link a program, and bind the uniforms and * vertex attributes in preparation for drawing. * * When a vector tile is parsed, this same configuration information is used to * populate the attribute buffers needed for data-driven styling using the zoom * level and feature property data. * * @private */ export default class ProgramConfiguration { binders: {[_: string]: (AttributeBinder | UniformBinder) }; cacheKey: string; _buffers: Array<VertexBuffer>; constructor(layer: TypedStyleLayer, zoom: number, filterProperties: (_: string) => boolean = () => true) { this.binders = {}; this._buffers = []; const keys = []; for (const property in layer.paint._values) { if (!filterProperties(property)) continue; const value = layer.paint.get(property); if (!(value instanceof PossiblyEvaluatedPropertyValue) || !supportsPropertyExpression(value.property.specification)) { continue; } const names = paintAttributeNames(property, layer.type); const expression = value.value; const type = value.property.specification.type; const useIntegerZoom = !!value.property.useIntegerZoom; const isPattern = property === 'line-dasharray' || property.endsWith('pattern'); const sourceException = property === 'line-dasharray' && (layer.layout: any).get('line-cap').value.kind !== 'constant'; if (expression.kind === 'constant' && !sourceException) { this.binders[property] = isPattern ? new PatternConstantBinder(expression.value, names) : new ConstantBinder(expression.value, names, type); keys.push(`/u_${property}`); } else if (expression.kind === 'source' || sourceException || isPattern) { const StructArrayLayout = layoutType(property, type, 'source'); this.binders[property] = isPattern ? // $FlowFixMe[prop-missing] // $FlowFixMe[incompatible-call] - expression can be a `composite` or `constant` kind expression new PatternCompositeBinder(expression, names, type, StructArrayLayout, layer.id) : // $FlowFixMe[prop-missing] // $FlowFixMe[incompatible-call] - expression can be a `composite` or `constant` kind expression new SourceExpressionBinder(expression, names, type, StructArrayLayout); keys.push(`/a_${property}`); } else { const StructArrayLayout = layoutType(property, type, 'composite'); // $FlowFixMe[prop-missing] // $FlowFixMe[incompatible-call] — expression can be a `constant` kind expression this.binders[property] = new CompositeExpressionBinder(expression, names, type, useIntegerZoom, zoom, StructArrayLayout); keys.push(`/z_${property}`); } } this.cacheKey = keys.sort().join(''); } getMaxValue(property: string): number { const binder = this.binders[property]; return binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder ? binder.maxValue : 0; } populatePaintArrays(newLength: number, feature: Feature, imagePositions: SpritePositions, availableImages: Array<string>, canonical?: CanonicalTileID, formattedSection?: FormattedSection) { for (const property in this.binders) { const binder = this.binders[property]; if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof PatternCompositeBinder) (binder: AttributeBinder).populatePaintArray(newLength, feature, imagePositions, availableImages, canonical, formattedSection); } } setConstantPatternPositions(posTo: SpritePosition) { for (const property in this.binders) { const binder = this.binders[property]; if (binder instanceof PatternConstantBinder) binder.setConstantPatternPositions(posTo); } } updatePaintArrays(featureStates: FeatureStates, featureMap: FeaturePositionMap, vtLayer: IVectorTileLayer, layer: TypedStyleLayer, availableImages: Array<string>, imagePositions: SpritePositions): boolean { let dirty: boolean = false; for (const id in featureStates) { const positions = featureMap.getPositions(id); for (const pos of positions) { const feature = vtLayer.feature(pos.index); for (const property in this.binders) { const binder = this.binders[property]; if ((binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof PatternCompositeBinder) && (binder: any).expression.isStateDependent === true) { //AHM: Remove after https://github.com/mapbox/mapbox-gl-js/issues/6255 const value = layer.paint.get(property); (binder: any).expression = value.value; (binder: AttributeBinder).updatePaintArray(pos.start, pos.end, feature, featureStates[id], availableImages, imagePositions); dirty = true; } } } } return dirty; } defines(): Array<string> { const result = []; for (const property in this.binders) { const binder = this.binders[property]; if (binder instanceof ConstantBinder || binder instanceof PatternConstantBinder) { result.push(...binder.uniformNames.map(name => `#define HAS_UNIFORM_${name}`)); } } return result; } getBinderAttributes(): Array<string> { const result = []; for (const property in this.binders) { const binder = this.binders[property]; if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof PatternCompositeBinder) { for (let i = 0; i < binder.paintVertexAttributes.length; i++) { result.push(binder.paintVertexAttributes[i].name); } } } return result; } getBinderUniforms(): Array<string> { const uniforms = []; for (const property in this.binders) { const binder = this.binders[property]; if (binder instanceof ConstantBinder || binder instanceof PatternConstantBinder || binder instanceof CompositeExpressionBinder) { for (const uniformName of binder.uniformNames) { uniforms.push(uniformName); } } } return uniforms; } getPaintVertexBuffers(): Array<VertexBuffer> { return this._buffers; } getUniforms(context: Context): Array<BinderUniform> { const uniforms = []; for (const property in this.binders) { const binder = this.binders[property]; if (binder instanceof ConstantBinder || binder instanceof PatternConstantBinder || binder instanceof CompositeExpressionBinder) { for (const name of binder.uniformNames) { uniforms.push({name, property, binding: binder.getBinding(context, name)}); } } } return uniforms; } setUniforms<Properties: Object>(program: WebGLProgram, context: Context, binderUniforms: Array<BinderUniform>, properties: PossiblyEvaluated<Properties>, globals: GlobalProperties) { // Uniform state bindings are owned by the Program, but we set them // from within the ProgramConfiguration's binder members. for (const {name, property, binding} of binderUniforms) { (this.binders[property]: any).setUniform(program, binding, globals, properties.get(property), name); } } updatePaintBuffers() { this._buffers = []; for (const property in this.binders) { const binder = this.binders[property]; if (( binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof PatternCompositeBinder) && binder.paintVertexBuffer) { this._buffers.push(binder.paintVertexBuffer); } } } upload(context: Context) { for (const property in this.binders) { const binder = this.binders[property]; if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof PatternCompositeBinder) binder.upload(context); } this.updatePaintBuffers(); } destroy() { for (const property in this.binders) { const binder = this.binders[property]; if (binder instanceof SourceExpressionBinder || binder instanceof CompositeExpressionBinder || binder instanceof PatternCompositeBinder) binder.destroy(); } } } export class ProgramConfigurationSet<Layer: TypedStyleLayer> { programConfigurations: {[_: string]: ProgramConfiguration}; needsUpload: boolean; _featureMap: FeaturePositionMap; _bufferOffset: number; constructor(layers: $ReadOnlyArray<Layer>, zoom: number, filterProperties: (_: string) => boolean = () => true) { this.programConfigurations = {}; for (const layer of layers) { this.programConfigurations[layer.id] = new ProgramConfiguration(layer, zoom, filterProperties); } this.needsUpload = false; this._featureMap = new FeaturePositionMap(); this._bufferOffset = 0; } populatePaintArrays(length: number, feature: Feature, index: number, imagePositions: SpritePositions, availableImages: Array<string>, canonical: CanonicalTileID, formattedSection?: FormattedSection) { for (const key in this.programConfigurations) { this.programConfigurations[key].populatePaintArrays(length, feature, imagePositions, availableImages, canonical, formattedSection); } if (feature.id !== undefined) { this._featureMap.add(feature.id, index, this._bufferOffset, length); } this._bufferOffset = length; this.needsUpload = true; } updatePaintArrays(featureStates: FeatureStates, vtLayer: IVectorTileLayer, layers: $ReadOnlyArray<TypedStyleLayer>, availableImages: Array<string>, imagePositions: SpritePositions) { for (const layer of layers) { this.needsUpload = this.programConfigurations[layer.id].updatePaintArrays(featureStates, this._featureMap, vtLayer, layer, availableImages, imagePositions) || this.needsUpload; } } get(layerId: string): ProgramConfiguration { return this.programConfigurations[layerId]; } upload(context: Context) { if (!this.needsUpload) return; for (const layerId in this.programConfigurations) { this.programConfigurations[layerId].upload(context); } this.needsUpload = false; } destroy() { for (const layerId in this.programConfigurations) { this.programConfigurations[layerId].destroy(); } } } const attributeNameExceptions = { 'text-opacity': ['opacity'], 'icon-opacity': ['opacity'], 'text-color': ['fill_color'], 'icon-color': ['fill_color'], 'text-halo-color': ['halo_color'], 'icon-halo-color': ['halo_color'], 'text-halo-blur': ['halo_blur'], 'icon-halo-blur': ['halo_blur'], 'text-halo-width': ['halo_width'], 'icon-halo-width': ['halo_width'], 'line-gap-width': ['gapwidth'], 'line-pattern': ['pattern', 'pixel_ratio'], 'fill-pattern': ['pattern', 'pixel_ratio'], 'fill-extrusion-pattern': ['pattern', 'pixel_ratio'], 'line-dasharray': ['dash'] }; function paintAttributeNames(property: string, type: string) { return attributeNameExceptions[property] || [property.replace(`${type}-`, '').replace(/-/g, '_')]; } const propertyExceptions = { 'line-pattern': { 'source': PatternLayoutArray, 'composite': PatternLayoutArray }, 'fill-pattern': { 'source': PatternLayoutArray, 'composite': PatternLayoutArray }, 'fill-extrusion-pattern':{ 'source': PatternLayoutArray, 'composite': PatternLayoutArray }, 'line-dasharray': { // temporary layout 'source': DashLayoutArray, 'composite': DashLayoutArray } }; const defaultLayouts = { 'color': { 'source': StructArrayLayout2f8, 'composite': StructArrayLayout4f16 }, 'number': { 'source': StructArrayLayout1f4, 'composite': StructArrayLayout2f8 } }; type LayoutType = 'array' | 'boolean' | 'color' | 'enum' | 'number' | 'resolvedImage' | 'string'; function layoutType(property: string, type: LayoutType, binderType: string) { const layoutException = propertyExceptions[property]; // $FlowFixMe[prop-missing] - we don't cover all types in defaultLayouts for some reason return (layoutException && layoutException[binderType]) || defaultLayouts[type][binderType]; } register(ConstantBinder, 'ConstantBinder'); register(PatternConstantBinder, 'PatternConstantBinder'); register(SourceExpressionBinder, 'SourceExpressionBinder'); register(PatternCompositeBinder, 'PatternCompositeBinder'); register(CompositeExpressionBinder, 'CompositeExpressionBinder'); register(ProgramConfiguration, 'ProgramConfiguration', {omit: ['_buffers']}); register(ProgramConfigurationSet, 'ProgramConfigurationSet');