diff --git a/src/ol/style/LiteralStyle.js b/src/ol/style/LiteralStyle.js index 15b916c293..8c38ce92e8 100644 --- a/src/ol/style/LiteralStyle.js +++ b/src/ol/style/LiteralStyle.js @@ -12,6 +12,9 @@ * * * 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 @@ -39,6 +42,8 @@ * @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} [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. */ diff --git a/src/ol/webgl/ShaderBuilder.js b/src/ol/webgl/ShaderBuilder.js index f03b7e7e91..31b0a9f95a 100644 --- a/src/ol/webgl/ShaderBuilder.js +++ b/src/ol/webgl/ShaderBuilder.js @@ -41,22 +41,24 @@ export function formatColor(colorArray) { * 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 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. + * 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. * - * A prefix must be specified so that the attributes can either be written as `a_name` or `v_name` in - * the final assignment string. + * 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). * * @param {import("../style/LiteralStyle").ExpressionValue} value Either literal or an operator. * @param {Array} attributes Array containing the attribute names **without a prefix**; - * it passed along recursively + * it is passed along recursively * @param {string} attributePrefix Prefix added to attribute names in the final output (typically `a_` or `v_`). + * @param {Array} variables Array containing the variable names **without a prefix**; + * it is passed along recursively * @returns {string} Assignment string. */ -export function parse(value, attributes, attributePrefix) { +export function parse(value, attributes, attributePrefix, variables) { const v = value; function p(value) { - return parse(value, attributes, attributePrefix); + return parse(value, attributes, attributePrefix, variables); } if (Array.isArray(v)) { switch (v[0]) { @@ -66,6 +68,11 @@ export function parse(value, attributes, attributePrefix) { attributes.push(v[1]); } return attributePrefix + v[1]; + case 'var': + if (variables.indexOf(v[1]) === -1) { + variables.push(v[1]); + } + return `u_${v[1]}`; case 'time': return 'u_time'; @@ -444,16 +451,17 @@ export function parseLiteralStyle(style) { 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_'); + return parse(value, vertAttributes, 'a_', variables); } const fragAttributes = []; // parse function for fragment shader function pFrag(value) { - return parse(value, fragAttributes, 'v_'); + return parse(value, fragAttributes, 'v_', variables); } let opacityFilter = '1.0'; @@ -487,6 +495,27 @@ export function parseLiteralStyle(style) { builder.setFragmentDiscardExpression(`${pFrag(style.filter)} <= 0.0`); } + /** @type {Object.} */ + const uniforms = {}; + + // 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 = 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) { @@ -501,18 +530,6 @@ export function parseLiteralStyle(style) { builder.addAttribute(`float a_${attrName}`); }); - /** @type {Object.} */ - const uniforms = {}; - - if (symbStyle.symbolType === 'image' && symbStyle.src) { - const texture = new Image(); - texture.src = symbStyle.src; - builder.addUniform('sampler2D u_texture') - .setColorExpression(builder.getColorExpression() + - ' * texture2D(u_texture, v_texCoord)'); - uniforms['u_texture'] = texture; - } - return { builder: builder, attributes: vertAttributes.map(function(attributeName) { diff --git a/test/spec/ol/webgl/shaderbuilder.test.js b/test/spec/ol/webgl/shaderbuilder.test.js index 5ade1799fc..ace6047082 100644 --- a/test/spec/ol/webgl/shaderbuilder.test.js +++ b/test/spec/ol/webgl/shaderbuilder.test.js @@ -201,19 +201,21 @@ void main(void) { }); describe('parse', function() { - let attributes, prefix, parseFn; + let attributes, prefix, variables, parseFn; beforeEach(function() { attributes = []; + variables = []; prefix = 'a_'; parseFn = function(value) { - return parse(value, attributes, prefix); + return parse(value, attributes, prefix, variables); }; }); it('parses expressions & literal values', function() { expect(parseFn(1)).to.eql('1.0'); expect(parseFn(['get', 'myAttr'])).to.eql('a_myAttr'); + expect(parseFn(['var', 'myValue'])).to.eql('u_myValue'); expect(parseFn(['time'])).to.eql('u_time'); expect(parseFn(['+', ['*', ['get', 'size'], 0.001], 12])).to.eql('((a_size * 0.001) + 12.0)'); expect(parseFn(['clamp', ['get', 'attr2'], ['get', 'attr3'], 20])).to.eql('clamp(a_attr2, a_attr3, 20.0)'); @@ -226,12 +228,16 @@ void main(void) { expect(parseFn(['between', ['get', 'attr4'], -4.0, 5.0])).to.eql('(a_attr4 >= -4.0 && a_attr4 <= 5.0 ? 1.0 : 0.0)'); expect(parseFn(['!', ['get', 'attr4']])).to.eql('(a_attr4 > 0.0 ? 0.0 : 1.0)'); expect(attributes).to.eql(['myAttr', 'size', 'attr2', 'attr3', 'attr4']); + expect(variables).to.eql(['myValue']); }); it('does not register an attribute several times', function() { parseFn(['get', 'myAttr']); + parseFn(['var', 'myVar']); parseFn(['clamp', ['get', 'attr2'], ['get', 'attr2'], ['get', 'myAttr']]); + parseFn(['*', ['get', 'attr2'], ['var', 'myVar']]); expect(attributes).to.eql(['myAttr', 'attr2']); + expect(variables).to.eql(['myVar']); }); }); @@ -319,6 +325,39 @@ void main(void) { expect(result.uniforms).to.have.property('u_texture'); }); + it('parses a style with variables', function() { + const result = parseLiteralStyle({ + variables: { + lower: 100, + higher: 400 + }, + symbol: { + symbolType: 'square', + size: ['stretch', ['get', 'population'], ['var', 'lower'], ['var', 'higher'], 4, 8], + color: '#336699', + opacity: 0.5 + } + }); + + expect(result.builder.uniforms).to.eql(['float u_lower', 'float u_higher']); + expect(result.builder.attributes).to.eql(['float a_population']); + expect(result.builder.varyings).to.eql([{ + name: 'v_population', + type: 'float', + expression: 'a_population' + }]); + expect(result.builder.colorExpression).to.eql('vec4(0.2, 0.4, 0.6, 1.0 * 0.5 * 1.0)'); + expect(result.builder.sizeExpression).to.eql( + 'vec2((clamp(a_population, u_lower, u_higher) * ((8.0 - 4.0) / (u_higher - u_lower)) + 4.0), (clamp(a_population, u_lower, u_higher) * ((8.0 - 4.0) / (u_higher - u_lower)) + 4.0))'); + expect(result.builder.offsetExpression).to.eql('vec2(0.0, 0.0)'); + expect(result.builder.texCoordExpression).to.eql('vec4(0.0, 0.0, 1.0, 1.0)'); + expect(result.builder.rotateWithView).to.eql(false); + expect(result.attributes.length).to.eql(1); + expect(result.attributes[0].name).to.eql('population'); + expect(result.uniforms).to.have.property('u_lower'); + expect(result.uniforms).to.have.property('u_higher'); + }); + it('parses a style with a filter', function() { const result = parseLiteralStyle({ filter: ['between', ['get', 'attr0'], 0, 10],