/** * Utilities for parsing literal style objects * @module ol/webgl/styleparser */ import {ShaderBuilder} from './ShaderBuilder.js'; import { ValueTypes, expressionToGlsl, getStringNumberEquivalent, uniformNameForVariable, } from '../style/expressions.js'; import {asArray} from '../color.js'; /** * @param {import('../style/literal.js').SymbolType} type Symbol type * @param {string} sizeExpressionGlsl Size expression * @return {string} The GLSL opacity function */ export function getSymbolOpacityGlslFunction(type, sizeExpressionGlsl) { switch (type) { case 'square': case 'image': return '1.0'; // taken from https://thebookofshaders.com/07/ case 'circle': return `(1.0-smoothstep(1.-4./${sizeExpressionGlsl},1.,dot(v_quadCoord-.5,v_quadCoord-.5)*4.))`; case 'triangle': const st = '(v_quadCoord*2.-1.)'; const a = `(atan(${st}.x,${st}.y))`; return `(1.0-smoothstep(.5-3./${sizeExpressionGlsl},.5,cos(floor(.5+${a}/2.094395102)*2.094395102-${a})*length(${st})))`; default: throw new Error(`Unexpected symbol type: ${type}`); } } /** * Packs all components of a color into a two-floats array * @param {import("../color.js").Color|string} color Color as array of numbers or string * @return {Array} Vec2 array containing the color in compressed form */ export function packColor(color) { const array = asArray(color); const r = array[0] * 256; const g = array[1]; const b = array[2] * 256; const a = Math.round(array[3] * 255); return [r + g, b + a]; } const UNPACK_COLOR_FN = `vec4 unpackColor(vec2 packedColor) { return fract(packedColor[1] / 256.0) * vec4( fract(floor(packedColor[0] / 256.0) / 256.0), fract(packedColor[0] / 256.0), fract(floor(packedColor[1] / 256.0) / 256.0), 1.0 ); }`; /** * @param {ValueTypes} type Value type * @return {1|2|3|4} The amount of components for this value */ function getGlslSizeFromType(type) { if (type === ValueTypes.COLOR) { return 2; } if (type === ValueTypes.NUMBER_ARRAY) { return 4; } return 1; } /** * @param {ValueTypes} type Value type * @return {'float'|'vec2'|'vec3'|'vec4'} The corresponding GLSL type for this value */ function getGlslTypeFromType(type) { const size = getGlslSizeFromType(type); if (size > 1) { return /** @type {'vec2'|'vec3'|'vec4'} */ (`vec${size}`); } return 'float'; } /** * @param {import("../style/literal").LiteralStyle} style Style * @param {ShaderBuilder} builder Shader builder * @param {Object} uniforms Uniforms * @param {import("../style/expressions.js").ParsingContext} vertContext Vertex shader parsing context * @param {import("../style/expressions.js").ParsingContext} fragContext Fragment shader parsing context */ function parseSymbolProperties( style, builder, uniforms, vertContext, fragContext ) { if (!('symbol' in style)) { return; } const symbolStyle = style.symbol; if ('color' in symbolStyle) { const color = symbolStyle.color; const opacity = symbolStyle.opacity !== undefined ? symbolStyle.opacity : 1; const parsedColor = expressionToGlsl(fragContext, color, ValueTypes.COLOR); const parsedOpacity = expressionToGlsl( fragContext, opacity, ValueTypes.NUMBER ); builder.setSymbolColorExpression( `vec4(${parsedColor}.rgb, ${parsedColor}.a * ${parsedOpacity})` ); } if (symbolStyle.symbolType === 'image' && symbolStyle.src) { const texture = new Image(); texture.crossOrigin = symbolStyle.crossOrigin === undefined ? 'anonymous' : symbolStyle.crossOrigin; texture.src = symbolStyle.src; builder .addUniform('sampler2D u_texture') .setSymbolColorExpression( `${builder.getSymbolColorExpression()} * texture2D(u_texture, v_texCoord)` ); uniforms['u_texture'] = texture; } else if ('symbolType' in symbolStyle) { let visibleSize = builder.getSymbolSizeExpression(); if ('size' in symbolStyle) { visibleSize = expressionToGlsl( fragContext, symbolStyle.size, ValueTypes.NUMBER_ARRAY | ValueTypes.NUMBER ); } const symbolOpacity = getSymbolOpacityGlslFunction( symbolStyle.symbolType, `vec2(${visibleSize}).x` ); builder.setSymbolColorExpression( `${builder.getSymbolColorExpression()} * vec4(1.0, 1.0, 1.0, ${symbolOpacity})` ); } if ('size' in symbolStyle) { const size = symbolStyle.size; const parsedSize = expressionToGlsl( vertContext, size, ValueTypes.NUMBER_ARRAY | ValueTypes.NUMBER ); builder.setSymbolSizeExpression(`vec2(${parsedSize})`); } if ('textureCoord' in symbolStyle) { builder.setTextureCoordinateExpression( expressionToGlsl( vertContext, symbolStyle.textureCoord, ValueTypes.NUMBER_ARRAY ) ); } if ('offset' in symbolStyle) { builder.setSymbolOffsetExpression( expressionToGlsl(vertContext, symbolStyle.offset, ValueTypes.NUMBER_ARRAY) ); } if ('rotation' in symbolStyle) { builder.setSymbolRotationExpression( expressionToGlsl(vertContext, symbolStyle.rotation, ValueTypes.NUMBER) ); } if ('rotateWithView' in symbolStyle) { builder.setSymbolRotateWithView(!!symbolStyle.rotateWithView); } } /** * @param {import("../style/literal").LiteralStyle} style Style * @param {ShaderBuilder} builder Shader Builder * @param {Object} uniforms Uniforms * @param {import("../style/expressions.js").ParsingContext} vertContext Vertex shader parsing context * @param {import("../style/expressions.js").ParsingContext} fragContext Fragment shader parsing context */ function parseStrokeProperties( style, builder, uniforms, vertContext, fragContext ) { if ('stroke-color' in style) { builder.setStrokeColorExpression( expressionToGlsl(fragContext, style['stroke-color'], ValueTypes.COLOR) ); } if ('stroke-width' in style) { builder.setStrokeWidthExpression( expressionToGlsl(vertContext, style['stroke-width'], ValueTypes.NUMBER) ); } } /** * @param {import("../style/literal").LiteralStyle} style Style * @param {ShaderBuilder} builder Shader Builder * @param {Object} uniforms Uniforms * @param {import("../style/expressions.js").ParsingContext} vertContext Vertex shader parsing context * @param {import("../style/expressions.js").ParsingContext} fragContext Fragment shader parsing context */ function parseFillProperties( style, builder, uniforms, vertContext, fragContext ) { if ('fill-color' in style) { builder.setFillColorExpression( expressionToGlsl(fragContext, style['fill-color'], ValueTypes.COLOR) ); } } /** * @typedef {Object} StyleParseResult * @property {ShaderBuilder} builder Shader builder pre-configured according to a given style * @property {import("../render/webgl/VectorStyleRenderer.js").UniformDefinitions} uniforms Uniform definitions * @property {import("../render/webgl/VectorStyleRenderer.js").AttributeDefinitions} attributes Attribute definitions */ /** * Parses a {@link import("../style/literal").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/literal").LiteralStyle} style Literal style. * @return {StyleParseResult} Result containing shader params, attributes and uniforms. */ export function parseLiteralStyle(style) { /** * @type {import("../style/expressions.js").ParsingContext} */ const vertContext = { inFragmentShader: false, variables: [], attributes: [], stringLiteralsMap: {}, functions: {}, style: style, }; /** * @type {import("../style/expressions.js").ParsingContext} */ const fragContext = { inFragmentShader: true, variables: vertContext.variables, attributes: [], stringLiteralsMap: vertContext.stringLiteralsMap, functions: {}, style: style, }; const builder = new ShaderBuilder(); /** @type {Object} */ const uniforms = {}; parseSymbolProperties(style, builder, uniforms, vertContext, fragContext); parseStrokeProperties(style, builder, uniforms, vertContext, fragContext); parseFillProperties(style, builder, uniforms, vertContext, fragContext); if (style.filter) { const parsedFilter = expressionToGlsl( fragContext, style.filter, ValueTypes.BOOLEAN ); builder.setFragmentDiscardExpression(`!${parsedFilter}`); } // define one uniform per variable fragContext.variables.forEach(function (variable) { const uniformName = uniformNameForVariable(variable.name); builder.addUniform(`${getGlslTypeFromType(variable.type)} ${uniformName}`); let callback; if (variable.type === ValueTypes.STRING) { callback = () => getStringNumberEquivalent( vertContext, /** @type {string} */ (style.variables[variable.name]) ); } else if (variable.type === ValueTypes.COLOR) { callback = () => packColor([ ...asArray( /** @type {string|Array} */ ( style.variables[variable.name] ) || '#eee' ), ]); } else if (variable.type === ValueTypes.BOOLEAN) { callback = () => /** @type {boolean} */ (style.variables[variable.name]) ? 1.0 : 0.0; } else { callback = () => /** @type {number} */ (style.variables[variable.name]); } uniforms[uniformName] = callback; }); // 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) fragContext.attributes.forEach(function (attribute) { if (!vertContext.attributes.find((a) => a.name === attribute.name)) { vertContext.attributes.push(attribute); } let type = getGlslTypeFromType(attribute.type); let expression = `a_${attribute.name}`; if (attribute.type === ValueTypes.COLOR) { type = 'vec4'; expression = `unpackColor(${expression})`; builder.addVertexShaderFunction(UNPACK_COLOR_FN); } builder.addVarying(`v_${attribute.name}`, type, expression); }); // for each feature attribute used in the vertex shader, define an attribute in the vertex shader. vertContext.attributes.forEach(function (attribute) { builder.addAttribute( `${getGlslTypeFromType(attribute.type)} a_${attribute.name}` ); }); const attributes = vertContext.attributes.map(function (attribute) { let callback; if (attribute.callback) { callback = attribute.callback; } else if (attribute.type === ValueTypes.STRING) { callback = (feature) => getStringNumberEquivalent(vertContext, feature.get(attribute.name)); } else if (attribute.type === ValueTypes.COLOR) { callback = (feature) => packColor([...asArray(feature.get(attribute.name) || '#eee')]); } else if (attribute.type === ValueTypes.BOOLEAN) { callback = (feature) => (feature.get(attribute.name) ? 1.0 : 0.0); } else { callback = (feature) => feature.get(attribute.name); } return { name: attribute.name, size: getGlslSizeFromType(attribute.type), callback, }; }); // add functions that were collected in the parsing contexts for (const functionName in vertContext.functions) { builder.addVertexShaderFunction(vertContext.functions[functionName]); } for (const functionName in fragContext.functions) { builder.addFragmentShaderFunction(fragContext.functions[functionName]); } return { builder: builder, attributes: attributes.reduce( (prev, curr) => ({ ...prev, [curr.name]: {callback: curr.callback, size: curr.size}, }), {} ), uniforms: uniforms, }; }