Merge pull request #10196 from jahow/webgl-add-string-operators

Webgl / add support for string and arrays in style expressions
This commit is contained in:
Olivier Guyot
2019-10-25 17:27:39 +02:00
committed by GitHub
14 changed files with 1489 additions and 851 deletions

View File

@@ -24,6 +24,8 @@ import Layer from './Layer.js';
* @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will
* be visible.
* @property {import("../source/Vector.js").default} [source] Source.
* @property {boolean} [disableHitDetection] Setting this to true will provide a slight performance boost, but will
* prevent all hit detection on the layer.
*/
@@ -75,6 +77,12 @@ class WebGLPointsLayer extends Layer {
* @type {import('../webgl/ShaderBuilder.js').StyleParseResult}
*/
this.parseResult_ = parseLiteralStyle(options.style);
/**
* @private
* @type {boolean}
*/
this.hitDetectionDisabled_ = !!options.disableHitDetection;
}
/**
@@ -84,6 +92,10 @@ class WebGLPointsLayer extends Layer {
return new WebGLPointsLayerRenderer(this, {
vertexShader: this.parseResult_.builder.getSymbolVertexShader(),
fragmentShader: this.parseResult_.builder.getSymbolFragmentShader(),
hitVertexShader: !this.hitDetectionDisabled_ &&
this.parseResult_.builder.getSymbolVertexShader(true),
hitFragmentShader: !this.hitDetectionDisabled_ &&
this.parseResult_.builder.getSymbolFragmentShader(true),
uniforms: this.parseResult_.uniforms,
attributes: this.parseResult_.attributes
});

View File

@@ -147,6 +147,10 @@ class WebGLPointsLayerRenderer extends WebGLLayerRenderer {
options.hitVertexShader
);
if (this.getShaderCompileErrors()) {
throw new Error(this.getShaderCompileErrors());
}
const customAttributes = options.attributes ?
options.attributes.map(function(attribute) {
return {

View File

@@ -5,49 +5,7 @@
*/
/**
* 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 {import("./expressions.js").ExpressionValue} ExpressionValue
*/
/**

598
src/ol/style/expressions.js Normal file
View File

@@ -0,0 +1,598 @@
/**
* Operators and utilities used for style expressions
* @module ol/style/expressions
*/
import {asArray, isStringColor} from '../color.js';
/**
* 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`
* * `['mod', value1, value1]` returns the result of `value1 % value2` (modulo)
* * `['^', value1, value1]` returns the value of `value1` raised to the `value2` power
*
* * Transform operators:
* * `['match', input, match1, output1, ...matchN, outputN, fallback]` compares the `input` value against all
* provided `matchX` values, returning the output associated with the first valid match. If no match is found,
* returns the `fallback` value.
* `input` and `matchX` values must all be of the same type, and can be `number` or `string`. `outputX` and
* `fallback` values must be of the same type, and can be of any kind.
* * `['interpolate', interpolation, input, stop1, output1, ...stopN, outputN]` returns a value by interpolating between
* pairs of inputs and outputs; `interpolation` can either be `['linear']` or `['exponential', base]` where `base` is
* the rate of increase from stop A to stop B (i.e. power to which the interpolation ratio is raised); a value
* of 1 is equivalent to `['linear']`.
* `input` and `stopX` values must all be of type `number`. `outputX` values can be `number` or `color` values.
* Note: `input` will be clamped between `stop1` and `stopN`, meaning that all output values will be comprised
* between `output1` and `outputN`.
*
* * 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.
*
* * Conversion operators:
* * `['array', value1, ...valueN]` creates a numerical array from `number` values; please note that the amount of
* values can currently only be 2, 3 or 4.
* * `['color', red, green, blue, alpha]` creates a `color` value from `number` values; the `alpha` parameter is
* optional; if not specified, it will be set to 1.
* Note: `red`, `green` and `blue` components must be values between 0 and 255; `alpha` between 0 and 1.
*
* 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|boolean} ExpressionValue
*/
/**
* Possible inferred types from a given value or expression.
* Note: these are binary flags.
* @enum {number}
*/
export const ValueTypes = {
NUMBER: 0b00001,
STRING: 0b00010,
COLOR: 0b00100,
BOOLEAN: 0b01000,
NUMBER_ARRAY: 0b10000,
ANY: 0b11111,
NONE: 0
};
/**
* An operator declaration must contain two methods: `getReturnType` which returns a type based on
* the operator arguments, and `toGlsl` which returns a GLSL-compatible string.
* Note: both methods can process arguments recursively.
* @typedef {Object} Operator
* @property {function(Array<ExpressionValue>): ValueTypes|number} getReturnType Returns one or several types
* @property {function(ParsingContext, Array<ExpressionValue>, ValueTypes=): string} toGlsl Returns a GLSL-compatible string
* Note: takes in an optional type hint as 3rd parameter
*/
/**
* Operator declarations
* @type {Object<string, Operator>}
*/
export const Operators = {};
/**
* Returns the possible types for a given value (each type being a binary flag)
* To test a value use e.g. `getValueType(v) & ValueTypes.BOOLEAN`
* @param {ExpressionValue} value Value
* @returns {ValueTypes|number} Type or types inferred from the value
*/
export function getValueType(value) {
if (typeof value === 'number') {
return ValueTypes.NUMBER;
}
if (typeof value === 'boolean') {
return ValueTypes.BOOLEAN;
}
if (typeof value === 'string') {
if (isStringColor(value)) {
return ValueTypes.COLOR | ValueTypes.STRING;
}
return ValueTypes.STRING;
}
if (!Array.isArray(value)) {
throw new Error(`Unhandled value type: ${JSON.stringify(value)}`);
}
const valueArr = /** @type {Array<*>} */(value);
const onlyNumbers = valueArr.every(function(v) {
return typeof v === 'number';
});
if (onlyNumbers) {
if (valueArr.length === 3 || valueArr.length === 4) {
return ValueTypes.COLOR | ValueTypes.NUMBER_ARRAY;
}
return ValueTypes.NUMBER_ARRAY;
}
if (typeof valueArr[0] !== 'string') {
throw new Error(`Expected an expression operator but received: ${JSON.stringify(valueArr)}`);
}
const operator = Operators[valueArr[0]];
if (operator === undefined) {
throw new Error(`Unrecognized expression operator: ${JSON.stringify(valueArr)}`);
}
return operator.getReturnType(valueArr.slice(1));
}
/**
* Checks if only one value type is enabled in the input number.
* @param {ValueTypes|number} valueType Number containing value type binary flags
* @return {boolean} True if only one type flag is enabled, false if zero or multiple
*/
export function isTypeUnique(valueType) {
return Math.log2(valueType) % 1 === 0;
}
/**
* Context available during the parsing of an expression.
* @typedef {Object} ParsingContext
* @property {boolean} [inFragmentShader] If false, means the expression output should be made for a vertex shader
* @property {Array<string>} variables List of variables used in the expression; contains **unprefixed names**
* @property {Array<string>} attributes List of attributes used in the expression; contains **unprefixed names**
* @property {Object<string, number>} stringLiteralsMap This object maps all encountered string values to a number
*/
/**
* Will return the number as a float with a dot separator, which is required by GLSL.
* @param {number} v Numerical value.
* @returns {string} The value as string.
*/
export function numberToGlsl(v) {
const s = v.toString();
return s.indexOf('.') === -1 ? s + '.0' : s;
}
/**
* 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 a vector, e. g.: `vec3(1.0, 2.0, 3.0)`.
*/
export function arrayToGlsl(array) {
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(numberToGlsl).join(', ')})`;
}
/**
* 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 colorToGlsl(color) {
const array = asArray(color).slice();
if (array.length < 4) {
array.push(1);
}
return arrayToGlsl(
array.map(function(c, i) {
return i < 3 ? c / 255 : c;
})
);
}
/**
* Returns a stable equivalent number for the string literal, for use in shaders. This number is then
* converted to be a GLSL-compatible string.
* @param {ParsingContext} context Parsing context
* @param {string} string String literal value
* @returns {string} GLSL-compatible string containing a number
*/
export function stringToGlsl(context, string) {
if (context.stringLiteralsMap[string] === undefined) {
context.stringLiteralsMap[string] = Object.keys(context.stringLiteralsMap).length;
}
return numberToGlsl(context.stringLiteralsMap[string]);
}
/**
* Recursively parses a style expression and outputs a GLSL-compatible string. Takes in a parsing context that
* will be read and modified during the parsing operation.
* @param {ParsingContext} context Parsing context
* @param {ExpressionValue} value Value
* @param {ValueTypes|number} [typeHint] Hint for the expected final type (can be several types combined)
* @returns {string} GLSL-compatible output
*/
export function expressionToGlsl(context, value, typeHint) {
// operator
if (Array.isArray(value) && typeof value[0] === 'string') {
const operator = Operators[value[0]];
if (operator === undefined) {
throw new Error(`Unrecognized expression operator: ${JSON.stringify(value)}`);
}
return operator.toGlsl(context, value.slice(1), typeHint);
} else if ((getValueType(value) & ValueTypes.NUMBER) > 0) {
return numberToGlsl(/** @type {number} */(value));
} else if ((getValueType(value) & ValueTypes.BOOLEAN) > 0) {
return value.toString();
} else if (
((getValueType(value) & ValueTypes.STRING) > 0) &&
(typeHint === undefined || typeHint == ValueTypes.STRING)
) {
return stringToGlsl(context, value.toString());
} else if (
((getValueType(value) & ValueTypes.COLOR) > 0) &&
(typeHint === undefined || typeHint == ValueTypes.COLOR)
) {
return colorToGlsl(/** @type {number[]|string} */(value));
} else if ((getValueType(value) & ValueTypes.NUMBER_ARRAY) > 0) {
return arrayToGlsl(/** @type {number[]} */(value));
}
}
function assertNumber(value) {
if (!(getValueType(value) & ValueTypes.NUMBER)) {
throw new Error(`A numeric value was expected, got ${JSON.stringify(value)} instead`);
}
}
function assertNumbers(arr) {
for (let i = 0; i < arr.length; i++) {
assertNumber(arr[i]);
}
}
function assertString(value) {
if (!(getValueType(value) & ValueTypes.STRING)) {
throw new Error(`A string value was expected, got ${JSON.stringify(value)} instead`);
}
}
function assertBoolean(value) {
if (!(getValueType(value) & ValueTypes.BOOLEAN)) {
throw new Error(`A boolean value was expected, got ${JSON.stringify(value)} instead`);
}
}
function assertArgsCount(args, count) {
if (args.length !== count) {
throw new Error(`Exactly ${count} arguments were expected, got ${args.length} instead`);
}
}
function assertArgsMinCount(args, count) {
if (args.length < count) {
throw new Error(`At least ${count} arguments were expected, got ${args.length} instead`);
}
}
function assertArgsMaxCount(args, count) {
if (args.length > count) {
throw new Error(`At most ${count} arguments were expected, got ${args.length} instead`);
}
}
function assertArgsEven(args) {
if (args.length % 2 !== 0) {
throw new Error(`An even amount of arguments was expected, got ${args} instead`);
}
}
function assertUniqueInferredType(args, types) {
if (!isTypeUnique(types)) {
throw new Error(`Could not infer only one type from the following expression: ${JSON.stringify(args)}`);
}
}
Operators['get'] = {
getReturnType: function(args) {
return ValueTypes.ANY;
},
toGlsl: function(context, args) {
assertArgsCount(args, 1);
assertString(args[0]);
const value = args[0].toString();
if (context.attributes.indexOf(value) === -1) {
context.attributes.push(value);
}
const prefix = context.inFragmentShader ? 'v_' : 'a_';
return prefix + value;
}
};
Operators['var'] = {
getReturnType: function(args) {
return ValueTypes.ANY;
},
toGlsl: function(context, args) {
assertArgsCount(args, 1);
assertString(args[0]);
const value = args[0].toString();
if (context.variables.indexOf(value) === -1) {
context.variables.push(value);
}
return `u_${value}`;
}
};
Operators['time'] = {
getReturnType: function(args) {
return ValueTypes.NUMBER;
},
toGlsl: function(context, args) {
assertArgsCount(args, 0);
return 'u_time';
}
};
Operators['zoom'] = {
getReturnType: function(args) {
return ValueTypes.NUMBER;
},
toGlsl: function(context, args) {
assertArgsCount(args, 0);
return 'u_zoom';
}
};
Operators['resolution'] = {
getReturnType: function(args) {
return ValueTypes.NUMBER;
},
toGlsl: function(context, args) {
assertArgsCount(args, 0);
return 'u_resolution';
}
};
Operators['*'] = {
getReturnType: function(args) {
return ValueTypes.NUMBER;
},
toGlsl: function(context, args) {
assertArgsCount(args, 2);
assertNumbers(args);
return `(${expressionToGlsl(context, args[0])} * ${expressionToGlsl(context, args[1])})`;
}
};
Operators['/'] = {
getReturnType: function(args) {
return ValueTypes.NUMBER;
},
toGlsl: function(context, args) {
assertArgsCount(args, 2);
assertNumbers(args);
return `(${expressionToGlsl(context, args[0])} / ${expressionToGlsl(context, args[1])})`;
}
};
Operators['+'] = {
getReturnType: function(args) {
return ValueTypes.NUMBER;
},
toGlsl: function(context, args) {
assertArgsCount(args, 2);
assertNumbers(args);
return `(${expressionToGlsl(context, args[0])} + ${expressionToGlsl(context, args[1])})`;
}
};
Operators['-'] = {
getReturnType: function(args) {
return ValueTypes.NUMBER;
},
toGlsl: function(context, args) {
assertArgsCount(args, 2);
assertNumbers(args);
return `(${expressionToGlsl(context, args[0])} - ${expressionToGlsl(context, args[1])})`;
}
};
Operators['clamp'] = {
getReturnType: function(args) {
return ValueTypes.NUMBER;
},
toGlsl: function(context, args) {
assertArgsCount(args, 3);
assertNumbers(args);
const min = expressionToGlsl(context, args[1]);
const max = expressionToGlsl(context, args[2]);
return `clamp(${expressionToGlsl(context, args[0])}, ${min}, ${max})`;
}
};
Operators['mod'] = {
getReturnType: function(args) {
return ValueTypes.NUMBER;
},
toGlsl: function(context, args) {
assertArgsCount(args, 2);
assertNumbers(args);
return `mod(${expressionToGlsl(context, args[0])}, ${expressionToGlsl(context, args[1])})`;
}
};
Operators['^'] = {
getReturnType: function(args) {
return ValueTypes.NUMBER;
},
toGlsl: function(context, args) {
assertArgsCount(args, 2);
assertNumbers(args);
return `pow(${expressionToGlsl(context, args[0])}, ${expressionToGlsl(context, args[1])})`;
}
};
Operators['>'] = {
getReturnType: function(args) {
return ValueTypes.BOOLEAN;
},
toGlsl: function(context, args) {
assertArgsCount(args, 2);
assertNumbers(args);
return `(${expressionToGlsl(context, args[0])} > ${expressionToGlsl(context, args[1])})`;
}
};
Operators['>='] = {
getReturnType: function(args) {
return ValueTypes.BOOLEAN;
},
toGlsl: function(context, args) {
assertArgsCount(args, 2);
assertNumbers(args);
return `(${expressionToGlsl(context, args[0])} >= ${expressionToGlsl(context, args[1])})`;
}
};
Operators['<'] = {
getReturnType: function(args) {
return ValueTypes.BOOLEAN;
},
toGlsl: function(context, args) {
assertArgsCount(args, 2);
assertNumbers(args);
return `(${expressionToGlsl(context, args[0])} < ${expressionToGlsl(context, args[1])})`;
}
};
Operators['<='] = {
getReturnType: function(args) {
return ValueTypes.BOOLEAN;
},
toGlsl: function(context, args) {
assertArgsCount(args, 2);
assertNumbers(args);
return `(${expressionToGlsl(context, args[0])} <= ${expressionToGlsl(context, args[1])})`;
}
};
Operators['=='] = {
getReturnType: function(args) {
return ValueTypes.BOOLEAN;
},
toGlsl: function(context, args) {
assertArgsCount(args, 2);
assertNumbers(args);
return `(${expressionToGlsl(context, args[0])} == ${expressionToGlsl(context, args[1])})`;
}
};
Operators['!'] = {
getReturnType: function(args) {
return ValueTypes.BOOLEAN;
},
toGlsl: function(context, args) {
assertArgsCount(args, 1);
assertBoolean(args[0]);
return `(!${expressionToGlsl(context, args[0])})`;
}
};
Operators['between'] = {
getReturnType: function(args) {
return ValueTypes.BOOLEAN;
},
toGlsl: function(context, args) {
assertArgsCount(args, 3);
assertNumbers(args);
const min = expressionToGlsl(context, args[1]);
const max = expressionToGlsl(context, args[2]);
const value = expressionToGlsl(context, args[0]);
return `(${value} >= ${min} && ${value} <= ${max})`;
}
};
Operators['array'] = {
getReturnType: function(args) {
return ValueTypes.NUMBER_ARRAY;
},
toGlsl: function(context, args) {
assertArgsMinCount(args, 2);
assertArgsMaxCount(args, 4);
assertNumbers(args);
const parsedArgs = args.map(function(val) {
return expressionToGlsl(context, val, ValueTypes.NUMBER);
});
return `vec${args.length}(${parsedArgs.join(', ')})`;
}
};
Operators['color'] = {
getReturnType: function(args) {
return ValueTypes.COLOR;
},
toGlsl: function(context, args) {
assertArgsMinCount(args, 3);
assertArgsMaxCount(args, 4);
assertNumbers(args);
const array = /** @type {number[]} */(args);
if (args.length === 3) {
array.push(1);
}
const parsedArgs = args.map(function(val, i) {
return expressionToGlsl(context, val, ValueTypes.NUMBER) + (i < 3 ? ' / 255.0' : '');
});
return `vec${args.length}(${parsedArgs.join(', ')})`;
}
};
Operators['interpolate'] = {
getReturnType: function(args) {
let type = ValueTypes.COLOR | ValueTypes.NUMBER;
for (let i = 3; i < args.length; i += 2) {
type = type & getValueType(args[i]);
}
return type;
},
toGlsl: function(context, args, opt_typeHint) {
assertArgsEven(args);
assertArgsMinCount(args, 6);
// validate interpolation type
const type = args[0];
let interpolation;
switch (type[0]) {
case 'linear': interpolation = 1; break;
case 'exponential': interpolation = type[1]; break;
default: interpolation = null;
}
if (!interpolation) {
throw new Error(`Invalid interpolation type for "interpolate" operator, received: ${JSON.stringify(type)}`);
}
// compute input/output types
const typeHint = opt_typeHint !== undefined ? opt_typeHint : ValueTypes.ANY;
const outputType = Operators['interpolate'].getReturnType(args) & typeHint;
assertUniqueInferredType(args, outputType);
const input = expressionToGlsl(context, args[1]);
let result = null;
for (let i = 2; i < args.length - 2; i += 2) {
const stop1 = expressionToGlsl(context, args[i]);
const output1 = expressionToGlsl(context, args[i + 1], outputType);
const stop2 = expressionToGlsl(context, args[i + 2]);
const output2 = expressionToGlsl(context, args[i + 3], outputType);
result = `mix(${result || output1}, ${output2}, pow(clamp((${input} - ${stop1}) / (${stop2} - ${stop1}), 0.0, 1.0), ${numberToGlsl(interpolation)}))`;
}
return result;
}
};
Operators['match'] = {
getReturnType: function(args) {
let type = ValueTypes.ANY;
for (let i = 2; i < args.length; i += 2) {
type = type & getValueType(args[i]);
}
type = type & getValueType(args[args.length - 1]);
return type;
},
toGlsl: function(context, args, opt_typeHint) {
assertArgsEven(args);
assertArgsMinCount(args, 4);
// compute input/output types
const typeHint = opt_typeHint !== undefined ? opt_typeHint : ValueTypes.ANY;
const outputType = Operators['match'].getReturnType(args) & typeHint;
assertUniqueInferredType(args, outputType);
const input = expressionToGlsl(context, args[0]);
const fallback = expressionToGlsl(context, args[args.length - 1], outputType);
let result = null;
for (let i = args.length - 3; i >= 1; i -= 2) {
const match = expressionToGlsl(context, args[i]);
const output = expressionToGlsl(context, args[i + 1], outputType);
result = `(${input} == ${match} ? ${output} : ${result || fallback})`;
}
return result;
}
};

View File

@@ -43,7 +43,9 @@ export const DefaultUniform = {
PROJECTION_MATRIX: 'u_projectionMatrix',
OFFSET_SCALE_MATRIX: 'u_offsetScaleMatrix',
OFFSET_ROTATION_MATRIX: 'u_offsetRotateMatrix',
TIME: 'u_time'
TIME: 'u_time',
ZOOM: 'u_zoom',
RESOLUTION: 'u_resolution'
};
/**
@@ -557,6 +559,8 @@ class WebGLHelper extends Disposable {
this.setUniformMatrixValue(DefaultUniform.OFFSET_ROTATION_MATRIX, fromTransform(this.tmpMat4_, offsetRotateMatrix));
this.setUniformFloatValue(DefaultUniform.TIME, (Date.now() - this.startTime_) * 0.001);
this.setUniformFloatValue(DefaultUniform.ZOOM, frameState.viewState.zoom);
this.setUniformFloatValue(DefaultUniform.RESOLUTION, frameState.viewState.resolution);
}
/**

View File

@@ -3,318 +3,7 @@
* @module ol/webgl/ShaderBuilder
*/
import {asArray, isStringColor} from '../color.js';
/**
* Will return the number as a float with a dot separator, which is required by GLSL.
* @param {number} v Numerical value.
* @returns {string} The value as string.
*/
export function formatNumber(v) {
const s = v.toString();
return s.indexOf('.') === -1 ? s + '.0' : s;
}
/**
* 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 a vector, e. g.: `vec3(1.0, 2.0, 3.0)`.
*/
export function formatArray(array) {
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 `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(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));
}
}
import {expressionToGlsl, ValueTypes} from '../style/expressions.js';
/**
* @typedef {Object} VaryingDescription
@@ -559,29 +248,45 @@ export class ShaderBuilder {
* 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`
*
* @param {boolean} [forHitDetection] If true, the shader will be modified to include hit detection variables
* (namely, hit color with encoded feature id).
* @returns {string} The full shader as a string.
*/
getSymbolVertexShader() {
getSymbolVertexShader(forHitDetection) {
const offsetMatrix = this.rotateWithView ?
'u_offsetScaleMatrix * u_offsetRotateMatrix' :
'u_offsetScaleMatrix';
let attributes = this.attributes;
let varyings = this.varyings;
if (forHitDetection) {
attributes = attributes.concat('vec4 a_hitColor');
varyings = varyings.concat({
name: 'v_hitColor',
type: 'vec4',
expression: 'a_hitColor'
});
}
return `precision mediump float;
uniform mat4 u_projectionMatrix;
uniform mat4 u_offsetScaleMatrix;
uniform mat4 u_offsetRotateMatrix;
uniform float u_time;
uniform float u_zoom;
uniform float u_resolution;
${this.uniforms.map(function(uniform) {
return 'uniform ' + uniform + ';';
}).join('\n')}
attribute vec2 a_position;
attribute float a_index;
${this.attributes.map(function(attribute) {
${attributes.map(function(attribute) {
return 'attribute ' + attribute + ';';
}).join('\n')}
varying vec2 v_texCoord;
varying vec2 v_quadCoord;
${this.varyings.map(function(varying) {
${varyings.map(function(varying) {
return 'varying ' + varying.type + ' ' + varying.name + ';';
}).join('\n')}
void main(void) {
@@ -593,13 +298,13 @@ void main(void) {
vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0);
gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;
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;
float u = a_index == 0.0 || a_index == 3.0 ? texCoord.s : texCoord.p;
float v = a_index == 2.0 || a_index == 3.0 ? texCoord.t : texCoord.q;
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);
${this.varyings.map(function(varying) {
${varyings.map(function(varying) {
return ' ' + varying.name + ' = ' + varying.expression + ';';
}).join('\n')}
}`;
@@ -612,23 +317,41 @@ ${this.varyings.map(function(varying) {
* Expects the following varyings to be transmitted by the vertex shader:
* `vec2 v_quadCoord`, `vec2 v_texCoord`
*
* @param {boolean} [forHitDetection] If true, the shader will be modified to include hit detection variables
* (namely, hit color with encoded feature id).
* @returns {string} The full shader as a string.
*/
getSymbolFragmentShader() {
getSymbolFragmentShader(forHitDetection) {
const hitDetectionBypass = forHitDetection ?
' if (gl_FragColor.a < 0.1) { discard; } gl_FragColor = v_hitColor;' : '';
let varyings = this.varyings;
if (forHitDetection) {
varyings = varyings.concat({
name: 'v_hitColor',
type: 'vec4',
expression: 'a_hitColor'
});
}
return `precision mediump float;
uniform float u_time;
uniform float u_zoom;
uniform float u_resolution;
${this.uniforms.map(function(uniform) {
return 'uniform ' + uniform + ';';
}).join('\n')}
varying vec2 v_texCoord;
varying vec2 v_quadCoord;
${this.varyings.map(function(varying) {
${varyings.map(function(varying) {
return 'varying ' + varying.type + ' ' + varying.name + ';';
}).join('\n')}
void main(void) {
if (${this.discardExpression}) { discard; }
gl_FragColor = ${this.colorExpression};
gl_FragColor.rgb *= gl_FragColor.a;
${hitDetectionBypass}
}`;
}
}
@@ -653,28 +376,39 @@ void main(void) {
*/
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 size = symbStyle.size !== undefined ? symbStyle.size : 1;
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;
const variables = [];
const vertAttributes = [];
// parse function for vertex shader
function pVert(value) {
return parse(value, vertAttributes, 'a_', variables);
}
/**
* @type {import("../style/expressions.js").ParsingContext}
*/
const vertContext = {
inFragmentShader: false,
variables: [],
attributes: [],
stringLiteralsMap: {}
};
const parsedSize = expressionToGlsl(vertContext, size, ValueTypes.NUMBER_ARRAY | ValueTypes.NUMBER);
const parsedOffset = expressionToGlsl(vertContext, offset, ValueTypes.NUMBER_ARRAY);
const parsedTexCoord = expressionToGlsl(vertContext, texCoord, ValueTypes.NUMBER_ARRAY);
const fragAttributes = [];
// parse function for fragment shader
function pFrag(value, type) {
return parse(value, fragAttributes, 'v_', variables, type);
}
/**
* @type {import("../style/expressions.js").ParsingContext}
*/
const fragContext = {
inFragmentShader: true,
variables: vertContext.variables,
attributes: [],
stringLiteralsMap: vertContext.stringLiteralsMap
};
const parsedColor = expressionToGlsl(fragContext, color, ValueTypes.COLOR);
const parsedOpacity = expressionToGlsl(fragContext, opacity, ValueTypes.NUMBER);
let opacityFilter = '1.0';
const visibleSize = pFrag(size[0]);
const visibleSize = `vec2(${expressionToGlsl(fragContext, size, ValueTypes.NUMBER_ARRAY | ValueTypes.NUMBER)}).x`;
switch (symbStyle.symbolType) {
case 'square': break;
case 'image': break;
@@ -691,26 +425,24 @@ export function parseLiteralStyle(style) {
default: throw new Error('Unexpected symbol type: ' + symbStyle.symbolType);
}
const parsedColor = pFrag(color, ValueTypes.COLOR);
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])})`)
.setSizeExpression(`vec2(${parsedSize})`)
.setSymbolOffsetExpression(parsedOffset)
.setTextureCoordinateExpression(parsedTexCoord)
.setSymbolRotateWithView(!!symbStyle.rotateWithView)
.setColorExpression(
`vec4(${parsedColor}.rgb, ${parsedColor}.a * ${pFrag(opacity)} * ${opacityFilter})`);
`vec4(${parsedColor}.rgb, ${parsedColor}.a * ${parsedOpacity} * ${opacityFilter})`);
if (style.filter) {
builder.setFragmentDiscardExpression(`${pFrag(style.filter)} <= 0.0`);
const parsedFilter = expressionToGlsl(fragContext, style.filter, ValueTypes.BOOLEAN);
builder.setFragmentDiscardExpression(`!${parsedFilter}`);
}
/** @type {Object.<string,import("../webgl/Helper").UniformValue>} */
const uniforms = {};
// define one uniform per variable
variables.forEach(function(varName) {
fragContext.variables.forEach(function(varName) {
builder.addUniform(`float u_${varName}`);
uniforms[`u_${varName}`] = function() {
return style.variables && style.variables[varName] !== undefined ?
@@ -729,25 +461,29 @@ export function parseLiteralStyle(style) {
// 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);
fragContext.attributes.forEach(function(attrName) {
if (vertContext.attributes.indexOf(attrName) === -1) {
vertContext.attributes.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) {
vertContext.attributes.forEach(function(attrName) {
builder.addAttribute(`float a_${attrName}`);
});
return {
builder: builder,
attributes: vertAttributes.map(function(attributeName) {
attributes: vertContext.attributes.map(function(attributeName) {
return {
name: attributeName,
callback: function(feature) {
return feature.get(attributeName) || 0;
let value = feature.get(attributeName);
if (typeof value === 'string') {
value = vertContext.stringLiteralsMap[value];
}
return value !== undefined ? value : -9999999; // to avoid matching with the first string literal
}
};
}),