/** * Classes and utilities for generating shaders from literal style objects * @module ol/webgl/ShaderBuilder */ import { ValueTypes, expressionToGlsl, getStringNumberEquivalent, uniformNameForVariable, } from '../style/expressions.js'; /** * @typedef {Object} VaryingDescription * @property {string} name Varying name, as will be declared in the header. * @property {string} type Varying type, either `float`, `vec2`, `vec4`... * @property {string} expression Expression which will be assigned to the varying in the vertex shader, and * passed on to the fragment shader. */ /** * @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} * @private */ this.uniforms = []; /** * Attributes; these will be declared in the header (should include the type). * @type {Array} * @private */ this.attributes = []; /** * Varyings with a name, a type and an expression. * @type {Array} * @private */ this.varyings = []; /** * @type {string} * @private */ this.sizeExpression = 'vec2(1.0)'; /** * @type {string} * @private */ this.rotationExpression = '0.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 rotation of the shape. * This expression can use all the uniforms and attributes available * in the vertex shader, and should evaluate to a `float` value in radians. * @param {string} expression Size expression * @return {ShaderBuilder} the builder object */ setRotationExpression(expression) { this.rotationExpression = 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; } /** * @return {string} Previously set size expression */ getSizeExpression() { return this.sizeExpression; } /** * @return {string} Previously set symbol offset expression */ getOffsetExpression() { return this.offsetExpression; } /** * @return {string} Previously set color expression */ getColorExpression() { return this.colorExpression; } /** * @return {string} Previously set texture coordinate expression */ getTextureCoordinateExpression() { return this.texCoordExpression; } /** * @return {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` * * @param {boolean} [forHitDetection] If true, the shader will be modified to include hit detection variables * (namely, hit color with encoded feature id). * @return {string} The full shader as a string. */ 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; ${attributes .map(function (attribute) { return 'attribute ' + attribute + ';'; }) .join('\n')} varying vec2 v_texCoord; varying vec2 v_quadCoord; ${varyings .map(function (varying) { return 'varying ' + varying.type + ' ' + varying.name + ';'; }) .join('\n')} void main(void) { mat4 offsetMatrix = ${offsetMatrix}; vec2 halfSize = ${this.sizeExpression} * 0.5; vec2 offset = ${this.offsetExpression}; float angle = ${this.rotationExpression}; float offsetX; float offsetY; if (a_index == 0.0) { offsetX = (offset.x - halfSize.x) * cos(angle) + (offset.y - halfSize.y) * sin(angle); offsetY = (offset.y - halfSize.y) * cos(angle) - (offset.x - halfSize.x) * sin(angle); } else if (a_index == 1.0) { offsetX = (offset.x + halfSize.x) * cos(angle) + (offset.y - halfSize.y) * sin(angle); offsetY = (offset.y - halfSize.y) * cos(angle) - (offset.x + halfSize.x) * sin(angle); } else if (a_index == 2.0) { offsetX = (offset.x + halfSize.x) * cos(angle) + (offset.y + halfSize.y) * sin(angle); offsetY = (offset.y + halfSize.y) * cos(angle) - (offset.x + halfSize.x) * sin(angle); } else { offsetX = (offset.x - halfSize.x) * cos(angle) + (offset.y + halfSize.y) * sin(angle); offsetY = (offset.y + halfSize.y) * cos(angle) - (offset.x - halfSize.x) * sin(angle); } 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.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); ${varyings .map(function (varying) { return ' ' + varying.name + ' = ' + varying.expression + ';'; }) .join('\n')} }`; } /** * 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` * * @param {boolean} [forHitDetection] If true, the shader will be modified to include hit detection variables * (namely, hit color with encoded feature id). * @return {string} The full shader as a string. */ 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; ${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} }`; } } /** * @typedef {Object} StyleParseResult * @property {ShaderBuilder} builder Shader builder pre-configured according to a given style * @property {Object} uniforms Uniform definitions. * @property {Array} attributes Attribute descriptions. */ /** * 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) { const symbStyle = style.symbol; 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 rotation = symbStyle.rotation !== undefined ? symbStyle.rotation : 0; /** * @type {import("../style/expressions.js").ParsingContext} */ const vertContext = { inFragmentShader: false, variables: [], attributes: [], stringLiteralsMap: {}, functions: {}, }; 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 parsedRotation = expressionToGlsl( vertContext, rotation, ValueTypes.NUMBER ); /** * @type {import("../style/expressions.js").ParsingContext} */ const fragContext = { inFragmentShader: true, variables: vertContext.variables, attributes: [], stringLiteralsMap: vertContext.stringLiteralsMap, functions: {}, }; const parsedColor = expressionToGlsl(fragContext, color, ValueTypes.COLOR); const parsedOpacity = expressionToGlsl( fragContext, opacity, ValueTypes.NUMBER ); let opacityFilter = '1.0'; const visibleSize = `vec2(${expressionToGlsl( fragContext, size, ValueTypes.NUMBER_ARRAY | ValueTypes.NUMBER )}).x`; switch (symbStyle.symbolType) { case 'square': break; case 'image': break; // taken from https://thebookofshaders.com/07/ case 'circle': opacityFilter = `(1.0-smoothstep(1.-4./${visibleSize},1.,dot(v_quadCoord-.5,v_quadCoord-.5)*4.))`; break; case 'triangle': const st = '(v_quadCoord*2.-1.)'; const a = `(atan(${st}.x,${st}.y))`; 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: ' + symbStyle.symbolType); } const builder = new ShaderBuilder() .setSizeExpression(`vec2(${parsedSize})`) .setRotationExpression(parsedRotation) .setSymbolOffsetExpression(parsedOffset) .setTextureCoordinateExpression(parsedTexCoord) .setSymbolRotateWithView(!!symbStyle.rotateWithView) .setColorExpression( `vec4(${parsedColor}.rgb, ${parsedColor}.a * ${parsedOpacity} * ${opacityFilter})` ); if (style.filter) { const parsedFilter = expressionToGlsl( fragContext, style.filter, ValueTypes.BOOLEAN ); builder.setFragmentDiscardExpression(`!${parsedFilter}`); } /** @type {Object} */ const uniforms = {}; // define one uniform per variable fragContext.variables.forEach(function (varName) { const uniformName = uniformNameForVariable(varName); builder.addUniform(`float ${uniformName}`); uniforms[uniformName] = function () { if (!style.variables || style.variables[varName] === undefined) { throw new Error( `The following variable is missing from the style: ${varName}` ); } let value = style.variables[varName]; if (typeof value === 'string') { value = getStringNumberEquivalent(vertContext, value); } return value !== undefined ? value : -9999999; // to avoid matching with the first string literal }; }); if (symbStyle.symbolType === 'image' && symbStyle.src) { const texture = new Image(); texture.crossOrigin = symbStyle.crossOrigin === undefined ? 'anonymous' : symbStyle.crossOrigin; 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) fragContext.attributes.forEach(function (attrName) { if (!vertContext.attributes.includes(attrName)) { 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. vertContext.attributes.forEach(function (attrName) { builder.addAttribute(`float a_${attrName}`); }); return { builder: builder, attributes: vertContext.attributes.map(function (attributeName) { return { name: attributeName, callback: function (feature, props) { let value = props[attributeName]; if (typeof value === 'string') { value = getStringNumberEquivalent(vertContext, value); } return value !== undefined ? value : -9999999; // to avoid matching with the first string literal }, }; }), uniforms: uniforms, }; }