/** * Classes and utilities for generating shaders from literal style objects * @module ol/webgl/ShaderBuilder */ import {expressionToGlsl, ValueTypes} 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.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` * * @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(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; ${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 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 = ${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). * @returns {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; ${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/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").LiteralStyle} style Literal style. * @returns {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; /** * @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); /** * @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 = `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})`) .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) { 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) 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. 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) { 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 } }; }), uniforms: uniforms }; }