Merge pull request #10168 from jahow/webgl-style-add-filter

Webgl / Add support for filtering and color interpolation in style expressions
This commit is contained in:
Olivier Guyot
2019-10-23 09:45:16 +02:00
committed by GitHub
12 changed files with 1211 additions and 466 deletions

View File

@@ -223,3 +223,14 @@ export function toString(color) {
const a = color[3] === undefined ? 1 : color[3];
return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
}
/**
* @param {string} s String.
* @return {boolean} Whether the string is actually a valid color
*/
export function isStringColor(s) {
if (NAMED_COLOR_RE_.test(s)) {
s = fromNamed(s);
}
return HEX_COLOR_RE_.test(s) || s.indexOf('rgba(') === 0 || s.indexOf('rgb(') === 0;
}

View File

@@ -3,7 +3,7 @@
*/
import {assign} from '../obj.js';
import WebGLPointsLayerRenderer from '../renderer/webgl/PointsLayer.js';
import {getSymbolFragmentShader, getSymbolVertexShader, parseSymbolStyle} from '../webgl/ShaderBuilder.js';
import {parseLiteralStyle} from '../webgl/ShaderBuilder.js';
import Layer from './Layer.js';
@@ -74,7 +74,7 @@ class WebGLPointsLayer extends Layer {
* @private
* @type {import('../webgl/ShaderBuilder.js').StyleParseResult}
*/
this.parseResult_ = parseSymbolStyle(options.style.symbol);
this.parseResult_ = parseLiteralStyle(options.style);
}
/**
@@ -82,8 +82,8 @@ class WebGLPointsLayer extends Layer {
*/
createRenderer() {
return new WebGLPointsLayerRenderer(this, {
vertexShader: getSymbolVertexShader(this.parseResult_.params),
fragmentShader: getSymbolFragmentShader(this.parseResult_.params),
vertexShader: this.parseResult_.builder.getSymbolVertexShader(),
fragmentShader: this.parseResult_.builder.getSymbolFragmentShader(),
uniforms: this.parseResult_.uniforms,
attributes: this.parseResult_.attributes
});

View File

@@ -132,6 +132,10 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
options.vertexShader
);
if (this.getShaderCompileErrors()) {
throw new Error(this.getShaderCompileErrors());
}
/**
* @type {boolean}
* @private

View File

@@ -4,8 +4,58 @@
* @module ol/style/LiteralStyle
*/
/**
* Base type used for literal style parameters; can be a number literal or the output of an operator,
* which in turns takes {@link ExpressionValue} arguments.
*
* The following operators can be used:
*
* * Reading operators:
* * `['get', 'attributeName']` fetches a feature attribute (it will be prefixed by `a_` in the shader)
* Note: those will be taken from the attributes provided to the renderer
* * `['var', 'varName']` fetches a value from the style variables, or 0 if undefined
* * `['time']` returns the time in seconds since the creation of the layer
*
* * Math operators:
* * `['*', value1, value1]` multiplies `value1` by `value2`
* * `['/', value1, value1]` divides `value1` by `value2`
* * `['+', value1, value1]` adds `value1` and `value2`
* * `['-', value1, value1]` subtracts `value2` from `value1`
* * `['clamp', value, low, high]` clamps `value` between `low` and `high`
* * `['stretch', value, low1, high1, low2, high2]` maps `value` from [`low1`, `high1`] range to
* [`low2`, `high2`] range, clamping values along the way
* * `['mod', value1, value1]` returns the result of `value1 % value2` (modulo)
* * `['pow', value1, value1]` returns the value of `value1` raised to the `value2` power
*
* * Color operators:
* * `['interpolate', ratio, color1, color2]` returns a color through interpolation between `color1` and
* `color2` with the given `ratio`
*
* * Logical operators:
* * `['<', value1, value2]` returns `1` if `value1` is strictly lower than value 2, or `0` otherwise.
* * `['<=', value1, value2]` returns `1` if `value1` is lower than or equals value 2, or `0` otherwise.
* * `['>', value1, value2]` returns `1` if `value1` is strictly greater than value 2, or `0` otherwise.
* * `['>=', value1, value2]` returns `1` if `value1` is greater than or equals value 2, or `0` otherwise.
* * `['==', value1, value2]` returns `1` if `value1` equals value 2, or `0` otherwise.
* * `['!', value1]` returns `0` if `value1` strictly greater than `0`, or `1` otherwise.
* * `['between', value1, value2, value3]` returns `1` if `value1` is contained between `value2` and `value3`
* (inclusively), or `0` otherwise.
*
* Values can either be literals or another operator, as they will be evaluated recursively.
* Literal values can be of the following types:
* * `number`
* * `string`
* * {@link module:ol/color~Color}
*
* @typedef {Array<*>|import("../color.js").Color|string|number} ExpressionValue
*/
/**
* @typedef {Object} LiteralStyle
* @property {ExpressionValue} [filter] Filter expression. If it resolves to a number strictly greater than 0, the
* point will be displayed. If undefined, all points will show.
* @property {Object<string, number>} [variables] Style variables; each variable must hold a number.
* Note: **this object is meant to be mutated**: changes to the values will immediately be visible on the rendered features
* @property {LiteralSymbolStyle} [symbol] Symbol representation.
*/
@@ -22,12 +72,12 @@ export const SymbolType = {
/**
* @typedef {Object} LiteralSymbolStyle
* @property {number|Array.<number, number>} size Size, mandatory.
* @property {ExpressionValue|Array<ExpressionValue, ExpressionValue>} size Size, mandatory.
* @property {SymbolType} symbolType Symbol type to use, either a regular shape or an image.
* @property {string} [src] Path to the image to be used for the symbol. Only required with `symbolType: 'image'`.
* @property {import("../color.js").Color|string} [color='#FFFFFF'] Color used for the representation (either fill, line or symbol).
* @property {number} [opacity=1] Opacity.
* @property {Array.<number, number>} [offset] Offset on X and Y axis for symbols. If not specified, the symbol will be centered.
* @property {Array.<number, number, number, number>} [textureCoord] Texture coordinates. If not specified, the whole texture will be used (range for 0 to 1 on both axes).
* @property {import("../color.js").Color|Array<ExpressionValue>|string} [color='#FFFFFF'] Color used for the representation (either fill, line or symbol).
* @property {ExpressionValue} [opacity=1] Opacity.
* @property {Array<ExpressionValue, ExpressionValue>} [offset] Offset on X and Y axis for symbols. If not specified, the symbol will be centered.
* @property {Array<ExpressionValue, ExpressionValue, ExpressionValue, ExpressionValue>} [textureCoord] Texture coordinates. If not specified, the whole texture will be used (range for 0 to 1 on both axes).
* @property {boolean} [rotateWithView=false] Specify whether the symbol must rotate with the view or stay upwards.
*/

View File

@@ -42,7 +42,8 @@ export const ShaderType = {
export const DefaultUniform = {
PROJECTION_MATRIX: 'u_projectionMatrix',
OFFSET_SCALE_MATRIX: 'u_offsetScaleMatrix',
OFFSET_ROTATION_MATRIX: 'u_offsetRotateMatrix'
OFFSET_ROTATION_MATRIX: 'u_offsetRotateMatrix',
TIME: 'u_time'
};
/**
@@ -355,6 +356,12 @@ class WebGLHelper extends Disposable {
* @private
*/
this.shaderCompileErrors_ = null;
/**
* @type {number}
* @private
*/
this.startTime_ = Date.now();
}
/**
@@ -548,6 +555,8 @@ class WebGLHelper extends Disposable {
this.setUniformMatrixValue(DefaultUniform.OFFSET_SCALE_MATRIX, fromTransform(this.tmpMat4_, offsetScaleMatrix));
this.setUniformMatrixValue(DefaultUniform.OFFSET_ROTATION_MATRIX, fromTransform(this.tmpMat4_, offsetRotateMatrix));
this.setUniformFloatValue(DefaultUniform.TIME, (Date.now() - this.startTime_) * 0.001);
}
/**

View File

@@ -1,9 +1,9 @@
/**
* Utilities for generating shaders from literal style objects
* Classes and utilities for generating shaders from literal style objects
* @module ol/webgl/ShaderBuilder
*/
import {asArray} from '../color.js';
import {asArray, isStringColor} from '../color.js';
/**
* Will return the number as a float with a dot separator, which is required by GLSL.
@@ -18,23 +18,302 @@ export function formatNumber(v) {
/**
* Will return the number array as a float with a dot separator, concatenated with ', '.
* @param {Array<number>} array Numerical values array.
* @returns {string} The array as string, e. g.: `1.0, 2.0, 3.0`.
* @returns {string} The array as a vector, e. g.: `vec3(1.0, 2.0, 3.0)`.
*/
export function formatArray(array) {
return array.map(formatNumber).join(', ');
if (array.length < 2 || array.length > 4) {
throw new Error('`formatArray` can only output `vec2`, `vec3` or `vec4` arrays.');
}
return `vec${array.length}(${array.map(formatNumber).join(', ')})`;
}
/**
* Will normalize and converts to string a color array compatible with GLSL.
* @param {Array<number>} colorArray Color in [r, g, b, a] array form, with RGB components in the
* 0..255 range and the alpha component in the 0..1 range. Note that if the A component is
* missing, only 3 values will be output.
* @returns {string} The color components concatenated in `1.0, 1.0, 1.0, 1.0` form.
* Will normalize and converts to string a `vec4` color array compatible with GLSL.
* @param {string|import("../color.js").Color} color Color either in string format or [r, g, b, a] array format,
* with RGB components in the 0..255 range and the alpha component in the 0..1 range.
* Note that the final array will always have 4 components.
* @returns {string} The color expressed in the `vec4(1.0, 1.0, 1.0, 1.0)` form.
*/
export function formatColor(colorArray) {
return colorArray.map(function(c, i) {
return i < 3 ? c / 255 : c;
}).map(formatNumber).join(', ');
export function formatColor(color) {
const array = asArray(color).slice();
if (array.length < 4) {
array.push(1);
}
return formatArray(
array.map(function(c, i) {
return i < 3 ? c / 255 : c;
})
);
}
/**
* Possible inferred types from a given value or expression
* @enum {number}
*/
export const ValueTypes = {
UNKNOWN: -1,
NUMBER: 0,
STRING: 1,
COLOR: 2,
COLOR_OR_STRING: 3
};
function getValueType(value) {
if (typeof value === 'number') {
return ValueTypes.NUMBER;
}
if (typeof value === 'string') {
if (isStringColor(value)) {
return ValueTypes.COLOR_OR_STRING;
}
return ValueTypes.STRING;
}
if (!Array.isArray(value)) {
throw new Error(`Unrecognized value type: ${JSON.stringify(value)}`);
}
if (value.length === 3 || value.length === 4) {
const onlyNumbers = value.every(function(v) {
return typeof v === 'number';
});
if (onlyNumbers) {
return ValueTypes.COLOR;
}
}
if (typeof value[0] !== 'string') {
return ValueTypes.UNKNOWN;
}
switch (value[0]) {
case 'get':
case 'var':
case 'time':
case '*':
case '/':
case '+':
case '-':
case 'clamp':
case 'stretch':
case 'mod':
case 'pow':
case '>':
case '>=':
case '<':
case '<=':
case '==':
case '!':
case 'between':
return ValueTypes.NUMBER;
case 'interpolate':
return ValueTypes.COLOR;
default:
return ValueTypes.UNKNOWN;
}
}
/**
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
* @returns {boolean} True if a numeric value, false otherwise
*/
export function isValueTypeNumber(value) {
return getValueType(value) === ValueTypes.NUMBER;
}
/**
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
* @returns {boolean} True if a string value, false otherwise
*/
export function isValueTypeString(value) {
return getValueType(value) === ValueTypes.STRING || getValueType(value) === ValueTypes.COLOR_OR_STRING;
}
/**
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
* @returns {boolean} True if a color value, false otherwise
*/
export function isValueTypeColor(value) {
return getValueType(value) === ValueTypes.COLOR || getValueType(value) === ValueTypes.COLOR_OR_STRING;
}
/**
* Check that the provided value or expression is valid, and that the types used are compatible.
*
* Will throw an exception if found to be invalid.
*
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
*/
export function check(value) {
// these will be used to validate types in the expressions
function checkNumber(value) {
if (!isValueTypeNumber(value)) {
throw new Error(`A numeric value was expected, got ${JSON.stringify(value)} instead`);
}
}
function checkColor(value) {
if (!isValueTypeColor(value)) {
throw new Error(`A color value was expected, got ${JSON.stringify(value)} instead`);
}
}
function checkString(value) {
if (!isValueTypeString(value)) {
throw new Error(`A string value was expected, got ${JSON.stringify(value)} instead`);
}
}
// first check that the value is of a recognized kind
if (!isValueTypeColor(value) && !isValueTypeNumber(value) && !isValueTypeString(value)) {
throw new Error(`No type could be inferred from the following expression: ${JSON.stringify(value)}`);
}
// check operator arguments
if (Array.isArray(value) && typeof value[0] === 'string') {
switch (value[0]) {
case 'get':
case 'var':
checkString(value[1]);
break;
case 'time':
break;
case '*':
case '/':
case '+':
case '-':
case 'mod':
case 'pow':
checkNumber(value[1]);
checkNumber(value[2]);
break;
case 'clamp':
checkNumber(value[1]);
checkNumber(value[2]);
checkNumber(value[3]);
break;
case 'stretch':
checkNumber(value[1]);
checkNumber(value[2]);
checkNumber(value[3]);
checkNumber(value[4]);
checkNumber(value[5]);
break;
case '>':
case '>=':
case '<':
case '<=':
case '==':
checkNumber(value[1]);
checkNumber(value[2]);
break;
case '!':
checkNumber(value[1]);
break;
case 'between':
checkNumber(value[1]);
checkNumber(value[2]);
checkNumber(value[3]);
break;
case 'interpolate':
checkNumber(value[1]);
checkColor(value[2]);
checkColor(value[3]);
break;
default: throw new Error(`Unrecognized operator in style expression: ${JSON.stringify(value)}`);
}
}
}
/**
* Parses the provided expressions and produces a GLSL-compatible assignment string, such as:
* `['add', ['*', ['get', 'size'], 0.001], 12] => '(a_size * (0.001)) + (12.0)'
*
* Also takes in two arrays where new attributes and variables will be pushed, so that the user of the `parse` function
* knows which attributes/variables are expected to be available at evaluation time.
*
* For attributes, a prefix must be specified so that the attributes can either be written as `a_name` or `v_name` in
* the final assignment string (depending on whether we're outputting a vertex or fragment shader).
*
* If a wrong value type is supplied to an operator (i. e. using colors with the `clamp` operator), an exception
* will be thrown.
*
* Note that by default, the `string` value type will be given precedence over `color`, so for example the
* `'yellow'` literal value will be parsed as a `string` while being a valid CSS color. This can be changed with
* the `typeHint` optional parameter which disambiguates what kind of value is expected.
*
* @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator.
* @param {Array<string>} attributes Array containing the attribute names **without a prefix**;
* it is passed along recursively
* @param {string} attributePrefix Prefix added to attribute names in the final output (typically `a_` or `v_`).
* @param {Array<string>} variables Array containing the variable names **without a prefix**;
* it is passed along recursively
* @param {ValueTypes} [typeHint] Hint for inferred type
* @returns {string} Assignment string.
*/
export function parse(value, attributes, attributePrefix, variables, typeHint) {
check(value);
function p(value) {
return parse(value, attributes, attributePrefix, variables);
}
function pC(value) {
return parse(value, attributes, attributePrefix, variables, ValueTypes.COLOR);
}
// operator
if (Array.isArray(value) && typeof value[0] === 'string') {
switch (value[0]) {
// reading operators
case 'get':
if (attributes.indexOf(value[1]) === -1) {
attributes.push(value[1]);
}
return attributePrefix + value[1];
case 'var':
if (variables.indexOf(value[1]) === -1) {
variables.push(value[1]);
}
return `u_${value[1]}`;
case 'time':
return 'u_time';
// math operators
case '*':
case '/':
case '+':
case '-':
return `(${p(value[1])} ${value[0]} ${p(value[2])})`;
case 'clamp': return `clamp(${p(value[1])}, ${p(value[2])}, ${p(value[3])})`;
case 'stretch':
const low1 = p(value[2]);
const high1 = p(value[3]);
const low2 = p(value[4]);
const high2 = p(value[5]);
return `((clamp(${p(value[1])}, ${low1}, ${high1}) - ${low1}) * ((${high2} - ${low2}) / (${high1} - ${low1})) + ${low2})`;
case 'mod': return `mod(${p(value[1])}, ${p(value[2])})`;
case 'pow': return `pow(${p(value[1])}, ${p(value[2])})`;
// color operators
case 'interpolate':
return `mix(${pC(value[2])}, ${pC(value[3])}, ${p(value[1])})`;
// logical operators
case '>':
case '>=':
case '<':
case '<=':
case '==':
return `(${p(value[1])} ${value[0]} ${p(value[2])} ? 1.0 : 0.0)`;
case '!':
return `(${p(value[1])} > 0.0 ? 0.0 : 1.0)`;
case 'between':
return `(${p(value[1])} >= ${p(value[2])} && ${p(value[1])} <= ${p(value[3])} ? 1.0 : 0.0)`;
default: throw new Error('Invalid style expression: ' + JSON.stringify(value));
}
} else if (isValueTypeNumber(value)) {
return formatNumber(/** @type {number} */(value));
} else if (isValueTypeString(value) && (typeHint === undefined || typeHint == ValueTypes.STRING)) {
return `"${value}"`;
} else {
return formatColor(/** @type {number[]|string} */(value));
}
}
/**
@@ -46,209 +325,357 @@ export function formatColor(colorArray) {
*/
/**
* @typedef {Object} ShaderParameters
* @property {Array<string>} [uniforms] Uniforms; these will be declared in the header (should include the type).
* @property {Array<string>} [attributes] Attributes; these will be declared in the header (should include the type).
* @property {Array<VaryingDescription>} [varyings] Varyings with a name, a type and an expression.
* @property {string} sizeExpression This will be assigned to a `vec2 size` variable.
* @property {string} offsetExpression This will be assigned to a `vec2 offset` variable.
* @property {string} colorExpression This will be the value assigned to gl_FragColor
* @property {string} texCoordExpression This will be the value assigned to the `vec4 v_texCoord` varying.
* @property {boolean} [rotateWithView=false] Whether symbols should rotate with view
* @classdesc
* This class implements a classic builder pattern for generating many different types of shaders.
* Methods can be chained, e. g.:
*
* ```js
* const shader = new ShaderBuilder()
* .addVarying('v_width', 'float', 'a_width')
* .addUniform('u_time')
* .setColorExpression('...')
* .setSizeExpression('...')
* .outputSymbolFragmentShader();
* ```
*/
export class ShaderBuilder {
constructor() {
/**
* Uniforms; these will be declared in the header (should include the type).
* @type {Array<string>}
* @private
*/
this.uniforms = [];
/**
* Generates a symbol vertex shader from a set of parameters,
* intended to be used on point geometries.
*
* Three uniforms are hardcoded in all shaders: `u_projectionMatrix`, `u_offsetScaleMatrix` and
* `u_offsetRotateMatrix`.
*
* The following attributes are hardcoded and expected to be present in the vertex buffers:
* `vec2 a_position`, `float a_index` (being the index of the vertex in the quad, 0 to 3).
*
* The following varyings are hardcoded and gives the coordinate of the pixel both in the quad on the texture:
* `vec2 v_quadCoord`, `vec2 v_texCoord`
*
* @param {ShaderParameters} parameters Parameters for the shader.
* @returns {string} The full shader as a string.
*/
export function getSymbolVertexShader(parameters) {
const offsetMatrix = parameters.rotateWithView ?
'u_offsetScaleMatrix * u_offsetRotateMatrix' :
'u_offsetScaleMatrix';
/**
* Attributes; these will be declared in the header (should include the type).
* @type {Array<string>}
* @private
*/
this.attributes = [];
const uniforms = parameters.uniforms || [];
const attributes = parameters.attributes || [];
const varyings = parameters.varyings || [];
/**
* Varyings with a name, a type and an expression.
* @type {Array<VaryingDescription>}
* @private
*/
this.varyings = [];
const body = `precision mediump float;
/**
* @type {string}
* @private
*/
this.sizeExpression = 'vec2(1.0)';
/**
* @type {string}
* @private
*/
this.offsetExpression = 'vec2(0.0)';
/**
* @type {string}
* @private
*/
this.colorExpression = 'vec4(1.0)';
/**
* @type {string}
* @private
*/
this.texCoordExpression = 'vec4(0.0, 0.0, 1.0, 1.0)';
/**
* @type {string}
* @private
*/
this.discardExpression = 'false';
/**
* @type {boolean}
* @private
*/
this.rotateWithView = false;
}
/**
* Adds a uniform accessible in both fragment and vertex shaders.
* The given name should include a type, such as `sampler2D u_texture`.
* @param {string} name Uniform name
* @return {ShaderBuilder} the builder object
*/
addUniform(name) {
this.uniforms.push(name);
return this;
}
/**
* Adds an attribute accessible in the vertex shader, read from the geometry buffer.
* The given name should include a type, such as `vec2 a_position`.
* @param {string} name Attribute name
* @return {ShaderBuilder} the builder object
*/
addAttribute(name) {
this.attributes.push(name);
return this;
}
/**
* Adds a varying defined in the vertex shader and accessible from the fragment shader.
* The type and expression of the varying have to be specified separately.
* @param {string} name Varying name
* @param {'float'|'vec2'|'vec3'|'vec4'} type Type
* @param {string} expression Expression used to assign a value to the varying.
* @return {ShaderBuilder} the builder object
*/
addVarying(name, type, expression) {
this.varyings.push({
name: name,
type: type,
expression: expression
});
return this;
}
/**
* Sets an expression to compute the size of the shape.
* This expression can use all the uniforms and attributes available
* in the vertex shader, and should evaluate to a `vec2` value.
* @param {string} expression Size expression
* @return {ShaderBuilder} the builder object
*/
setSizeExpression(expression) {
this.sizeExpression = expression;
return this;
}
/**
* Sets an expression to compute the offset of the symbol from the point center.
* This expression can use all the uniforms and attributes available
* in the vertex shader, and should evaluate to a `vec2` value.
* Note: will only be used for point geometry shaders.
* @param {string} expression Offset expression
* @return {ShaderBuilder} the builder object
*/
setSymbolOffsetExpression(expression) {
this.offsetExpression = expression;
return this;
}
/**
* Sets an expression to compute the color of the shape.
* This expression can use all the uniforms, varyings and attributes available
* in the fragment shader, and should evaluate to a `vec4` value.
* @param {string} expression Color expression
* @return {ShaderBuilder} the builder object
*/
setColorExpression(expression) {
this.colorExpression = expression;
return this;
}
/**
* Sets an expression to compute the texture coordinates of the vertices.
* This expression can use all the uniforms and attributes available
* in the vertex shader, and should evaluate to a `vec4` value.
* @param {string} expression Texture coordinate expression
* @return {ShaderBuilder} the builder object
*/
setTextureCoordinateExpression(expression) {
this.texCoordExpression = expression;
return this;
}
/**
* Sets an expression to determine whether a fragment (pixel) should be discarded,
* i.e. not drawn at all.
* This expression can use all the uniforms, varyings and attributes available
* in the fragment shader, and should evaluate to a `bool` value (it will be
* used in an `if` statement)
* @param {string} expression Fragment discard expression
* @return {ShaderBuilder} the builder object
*/
setFragmentDiscardExpression(expression) {
this.discardExpression = expression;
return this;
}
/**
* Sets whether the symbols should rotate with the view or stay aligned with the map.
* Note: will only be used for point geometry shaders.
* @param {boolean} rotateWithView Rotate with view
* @return {ShaderBuilder} the builder object
*/
setSymbolRotateWithView(rotateWithView) {
this.rotateWithView = rotateWithView;
return this;
}
/**
* @returns {string} Previously set size expression
*/
getSizeExpression() {
return this.sizeExpression;
}
/**
* @returns {string} Previously set symbol offset expression
*/
getOffsetExpression() {
return this.offsetExpression;
}
/**
* @returns {string} Previously set color expression
*/
getColorExpression() {
return this.colorExpression;
}
/**
* @returns {string} Previously set texture coordinate expression
*/
getTextureCoordinateExpression() {
return this.texCoordExpression;
}
/**
* @returns {string} Previously set fragment discard expression
*/
getFragmentDiscardExpression() {
return this.discardExpression;
}
/**
* Generates a symbol vertex shader from the builder parameters,
* intended to be used on point geometries.
*
* Three uniforms are hardcoded in all shaders: `u_projectionMatrix`, `u_offsetScaleMatrix`,
* `u_offsetRotateMatrix`, `u_time`.
*
* The following attributes are hardcoded and expected to be present in the vertex buffers:
* `vec2 a_position`, `float a_index` (being the index of the vertex in the quad, 0 to 3).
*
* The following varyings are hardcoded and gives the coordinate of the pixel both in the quad and on the texture:
* `vec2 v_quadCoord`, `vec2 v_texCoord`
*
* @returns {string} The full shader as a string.
*/
getSymbolVertexShader() {
const offsetMatrix = this.rotateWithView ?
'u_offsetScaleMatrix * u_offsetRotateMatrix' :
'u_offsetScaleMatrix';
return `precision mediump float;
uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix;
uniform mat4 u_offsetRotateMatrix;
${uniforms.map(function(uniform) {
uniform float u_time;
${this.uniforms.map(function(uniform) {
return 'uniform ' + uniform + ';';
}).join('\n')}
attribute vec2 a_position;
attribute float a_index;
${attributes.map(function(attribute) {
${this.attributes.map(function(attribute) {
return 'attribute ' + attribute + ';';
}).join('\n')}
varying vec2 v_texCoord;
varying vec2 v_quadCoord;
${varyings.map(function(varying) {
${this.varyings.map(function(varying) {
return 'varying ' + varying.type + ' ' + varying.name + ';';
}).join('\n')}
void main(void) {
mat4 offsetMatrix = ${offsetMatrix};
vec2 size = ${parameters.sizeExpression};
vec2 offset = ${parameters.offsetExpression};
vec2 size = ${this.sizeExpression};
vec2 offset = ${this.offsetExpression};
float offsetX = a_index == 0.0 || a_index == 3.0 ? offset.x - size.x / 2.0 : offset.x + size.x / 2.0;
float offsetY = a_index == 0.0 || a_index == 1.0 ? offset.y - size.y / 2.0 : offset.y + size.y / 2.0;
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
vec4 texCoord = ${parameters.texCoordExpression};
vec4 texCoord = ${this.texCoordExpression};
float u = a_index == 0.0 || a_index == 3.0 ? texCoord.s : texCoord.q;
float v = a_index == 2.0 || a_index == 3.0 ? texCoord.t : texCoord.p;
v_texCoord = vec2(u, v);
u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 1.0;
v = a_index == 2.0 || a_index == 3.0 ? 0.0 : 1.0;
v_quadCoord = vec2(u, v);
${varyings.map(function(varying) {
${this.varyings.map(function(varying) {
return ' ' + varying.name + ' = ' + varying.expression + ';';
}).join('\n')}
}`;
}
return body;
}
/**
* Generates a symbol fragment shader intended to be used on point geometries.
*
* Expected the following varyings to be transmitted by the vertex shader:
* `vec2 v_texCoord`
*
* @param {ShaderParameters} parameters Parameters for the shader.
* @returns {string} The full shader as a string.
*/
export function getSymbolFragmentShader(parameters) {
const uniforms = parameters.uniforms || [];
const varyings = parameters.varyings || [];
const body = `precision mediump float;
${uniforms.map(function(uniform) {
/**
* Generates a symbol fragment shader from the builder parameters,
* intended to be used on point geometries.
*
* Expects the following varyings to be transmitted by the vertex shader:
* `vec2 v_quadCoord`, `vec2 v_texCoord`
*
* @returns {string} The full shader as a string.
*/
getSymbolFragmentShader() {
return `precision mediump float;
uniform float u_time;
${this.uniforms.map(function(uniform) {
return 'uniform ' + uniform + ';';
}).join('\n')}
varying vec2 v_texCoord;
varying vec2 v_quadCoord;
${varyings.map(function(varying) {
${this.varyings.map(function(varying) {
return 'varying ' + varying.type + ' ' + varying.name + ';';
}).join('\n')}
void main(void) {
gl_FragColor = ${parameters.colorExpression};
if (${this.discardExpression}) { discard; }
gl_FragColor = ${this.colorExpression};
gl_FragColor.rgb *= gl_FragColor.a;
}`;
return body;
}
/**
* Base type for values fed to operators; can be a number literal or the output of another operator
* @typedef {Array<*>|number} OperatorValue
*/
/**
* Parses the provided expressions and produces a GLSL-compatible assignment string, such as:
* `['add', ['*', ['get', 'size'], 0.001], 12] => '(a_size * (0.001)) + (12.0)'
*
* The following operators can be used:
* * `['get', 'attributeName']` fetches a feature attribute (it will be prefixed by `a_` in the shader)
* * `['*', value1, value1]` multiplies value1 by value2
* * `['+', value1, value1]` adds value1 and value2
* * `['clamp', value1, value2, value3]` clamps value1 between values2 and value3
* * `['stretch', value1, value2, value3, value4, value5]` maps value1 from [value2, value3] range to
* [value4, value5] range, clamping values along the way
*
* Values can either be literals (numbers) or another operator, as they will be evaluated recursively.
*
* Also takes in an array where new attributes will be pushed, so that the user of the `parse` function
* knows which attributes are expected to be available at evaluation time.
*
* A prefix must be specified so that the attributes can either be written as `a_name` or `v_name` in
* the final assignment string.
*
* @param {OperatorValue} value Either literal or an operator.
* @param {Array<string>} attributes Array containing the attribute names prefixed with `a_`; it
* it passed along recursively
* @param {string} attributePrefix Prefix added to attribute names in the final output (typically `a_` or `v_`).
* @returns {string} Assignment string.
*/
export function parse(value, attributes, attributePrefix) {
const v = value;
function p(value) {
return parse(value, attributes, attributePrefix);
}
if (Array.isArray(v)) {
switch (v[0]) {
case 'get':
if (attributes.indexOf(v[1]) === -1) {
attributes.push(v[1]);
}
return attributePrefix + v[1];
case '*': return `(${p(v[1])} * ${p(v[2])})`;
case '+': return `(${p(v[1])} + ${p(v[2])})`;
case 'clamp': return `clamp(${p(v[1])}, ${p(v[2])}, ${p(v[3])})`;
case 'stretch': return `(clamp(${p(v[1])}, ${p(v[2])}, ${p(v[3])}) * ((${p(v[5])} - ${p(v[4])}) / (${p(v[3])} - ${p(v[2])})) + ${p(v[4])})`;
default: throw new Error('Unrecognized literal style expression: ' + JSON.stringify(value));
}
} else if (typeof value === 'number') {
return formatNumber(value);
} else {
throw new Error('Invalid value type in expression: ' + JSON.stringify(value));
}
}
/**
* @typedef {Object} StyleParseResult
* @property {ShaderParameters} params Symbol shader params.
* @property {ShaderBuilder} builder Shader builder pre-configured according to a given style
* @property {Object.<string,import("./Helper").UniformValue>} uniforms Uniform definitions.
* @property {Array<import("../renderer/webgl/PointsLayer").CustomAttribute>} attributes Attribute descriptions.
*/
/**
* Parses a {@link import("../style/LiteralStyle").LiteralSymbolStyle} object and outputs shader parameters to be
* then fed to {@link getSymbolVertexShader} and {@link getSymbolFragmentShader}.
* Parses a {@link import("../style/LiteralStyle").LiteralStyle} object and returns a {@link ShaderBuilder}
* object that has been configured according to the given style, as well as `attributes` and `uniforms`
* arrays to be fed to the `WebGLPointsRenderer` class.
*
* Also returns `uniforms` and `attributes` properties as expected by the
* {@link module:ol/renderer/webgl/PointsLayer~WebGLPointsLayerRenderer}.
*
* @param {import("../style/LiteralStyle").LiteralSymbolStyle} style Symbol style.
* @param {import("../style/LiteralStyle").LiteralStyle} style Literal style.
* @returns {StyleParseResult} Result containing shader params, attributes and uniforms.
*/
export function parseSymbolStyle(style) {
const size = Array.isArray(style.size) && typeof style.size[0] == 'number' ?
style.size : [style.size, style.size];
const color = (typeof style.color === 'string' ?
asArray(style.color).map(function(c, i) {
return i < 3 ? c / 255 : c;
}) :
style.color || [255, 255, 255, 1]);
const texCoord = style.textureCoord || [0, 0, 1, 1];
const offset = style.offset || [0, 0];
const opacity = style.opacity !== undefined ? style.opacity : 1;
export function parseLiteralStyle(style) {
const symbStyle = style.symbol;
const size = Array.isArray(symbStyle.size) && typeof symbStyle.size[0] == 'number' ?
symbStyle.size : [symbStyle.size, symbStyle.size];
const color = symbStyle.color || 'white';
const texCoord = symbStyle.textureCoord || [0, 0, 1, 1];
const offset = symbStyle.offset || [0, 0];
const opacity = symbStyle.opacity !== undefined ? symbStyle.opacity : 1;
let attributes = [];
const varyings = [];
function pA(value) {
return parse(value, attributes, 'a_');
const variables = [];
const vertAttributes = [];
// parse function for vertex shader
function pVert(value) {
return parse(value, vertAttributes, 'a_', variables);
}
function pV(value) {
return parse(value, varyings, 'v_');
const fragAttributes = [];
// parse function for fragment shader
function pFrag(value, type) {
return parse(value, fragAttributes, 'v_', variables, type);
}
let opacityFilter = '1.0';
const visibleSize = pV(size[0]);
switch (style.symbolType) {
const visibleSize = pFrag(size[0]);
switch (symbStyle.symbolType) {
case 'square': break;
case 'image': break;
// taken from https://thebookofshaders.com/07/
@@ -261,49 +688,62 @@ export function parseSymbolStyle(style) {
opacityFilter = `(1.0-smoothstep(.5-3./${visibleSize},.5,cos(floor(.5+${a}/2.094395102)*2.094395102-${a})*length(${st})))`;
break;
default: throw new Error('Unexpected symbol type: ' + style.symbolType);
default: throw new Error('Unexpected symbol type: ' + symbStyle.symbolType);
}
/** @type {import('../webgl/ShaderBuilder.js').ShaderParameters} */
const params = {
uniforms: [],
colorExpression: `vec4(${pV(color[0])}, ${pV(color[1])}, ${pV(color[2])}, ${pV(color[3])})` +
` * vec4(1.0, 1.0, 1.0, ${pV(opacity)} * ${opacityFilter})`,
sizeExpression: `vec2(${pA(size[0])}, ${pA(size[1])})`,
offsetExpression: `vec2(${pA(offset[0])}, ${pA(offset[1])})`,
texCoordExpression: `vec4(${pA(texCoord[0])}, ${pA(texCoord[1])}, ${pA(texCoord[2])}, ${pA(texCoord[3])})`,
rotateWithView: !!style.rotateWithView
};
const parsedColor = pFrag(color, ValueTypes.COLOR);
attributes = attributes.concat(varyings).filter(function(attrName, index, arr) {
return arr.indexOf(attrName) === index;
});
params.attributes = attributes.map(function(attributeName) {
return `float a_${attributeName}`;
});
params.varyings = varyings.map(function(attributeName) {
return {
name: `v_${attributeName}`,
type: 'float',
expression: `a_${attributeName}`
};
});
const builder = new ShaderBuilder()
.setSizeExpression(`vec2(${pVert(size[0])}, ${pVert(size[1])})`)
.setSymbolOffsetExpression(`vec2(${pVert(offset[0])}, ${pVert(offset[1])})`)
.setTextureCoordinateExpression(
`vec4(${pVert(texCoord[0])}, ${pVert(texCoord[1])}, ${pVert(texCoord[2])}, ${pVert(texCoord[3])})`)
.setSymbolRotateWithView(!!symbStyle.rotateWithView)
.setColorExpression(
`vec4(${parsedColor}.rgb, ${parsedColor}.a * ${pFrag(opacity)} * ${opacityFilter})`);
if (style.filter) {
builder.setFragmentDiscardExpression(`${pFrag(style.filter)} <= 0.0`);
}
/** @type {Object.<string,import("../webgl/Helper").UniformValue>} */
const uniforms = {};
if (style.symbolType === 'image' && style.src) {
// define one uniform per variable
variables.forEach(function(varName) {
builder.addUniform(`float u_${varName}`);
uniforms[`u_${varName}`] = function() {
return style.variables && style.variables[varName] !== undefined ?
style.variables[varName] : 0;
};
});
if (symbStyle.symbolType === 'image' && symbStyle.src) {
const texture = new Image();
texture.src = style.src;
params.uniforms.push('sampler2D u_texture');
params.colorExpression = params.colorExpression +
' * texture2D(u_texture, v_texCoord)';
texture.src = symbStyle.src;
builder.addUniform('sampler2D u_texture')
.setColorExpression(builder.getColorExpression() +
' * texture2D(u_texture, v_texCoord)');
uniforms['u_texture'] = texture;
}
// for each feature attribute used in the fragment shader, define a varying that will be used to pass data
// from the vertex to the fragment shader, as well as an attribute in the vertex shader (if not already present)
fragAttributes.forEach(function(attrName) {
if (vertAttributes.indexOf(attrName) === -1) {
vertAttributes.push(attrName);
}
builder.addVarying(`v_${attrName}`, 'float', `a_${attrName}`);
});
// for each feature attribute used in the vertex shader, define an attribute in the vertex shader.
vertAttributes.forEach(function(attrName) {
builder.addAttribute(`float a_${attrName}`);
});
return {
params: params,
attributes: attributes.map(function(attributeName) {
builder: builder,
attributes: vertAttributes.map(function(attributeName) {
return {
name: attributeName,
callback: function(feature) {