From 34572da17b1489f02b29269af9da6fdb871a0259 Mon Sep 17 00:00:00 2001 From: Olivier Guyot Date: Wed, 25 Sep 2019 14:49:14 +0200 Subject: [PATCH] Webgl shader builder / expanded functionality Now takes in a custom object with attributes, uniforms etc. The WebGLPointsLayer uses this to handle image icons. --- src/ol/layer/WebGLPoints.js | 46 ++++- src/ol/webgl/ShaderBuilder.js | 116 +++++++---- test/spec/ol/webgl/shaderbuilder.test.js | 244 +++++++++++++---------- 3 files changed, 258 insertions(+), 148 deletions(-) diff --git a/src/ol/layer/WebGLPoints.js b/src/ol/layer/WebGLPoints.js index 8f6cdc4665..bb9f182883 100644 --- a/src/ol/layer/WebGLPoints.js +++ b/src/ol/layer/WebGLPoints.js @@ -4,8 +4,15 @@ import VectorLayer from './Vector.js'; import {assign} from '../obj.js'; import WebGLPointsLayerRenderer from '../renderer/webgl/PointsLayer.js'; -import {getSymbolFragmentShader, getSymbolVertexShader} from '../webgl/ShaderBuilder.js'; +import { + formatArray, + formatColor, + formatNumber, + getSymbolFragmentShader, + getSymbolVertexShader +} from '../webgl/ShaderBuilder.js'; import {assert} from '../asserts.js'; +import {asArray} from '../color.js'; /** @@ -59,9 +66,42 @@ class WebGLPointsLayer extends VectorLayer { * @inheritDoc */ createRenderer() { + const symbolStyle = this.literalStyle.symbol; + const size = Array.isArray(symbolStyle.size) ? + formatArray(symbolStyle.size) : formatNumber(symbolStyle.size); + const color = symbolStyle.color !== undefined ? + (typeof symbolStyle.color === 'string' ? asArray(symbolStyle.color) : symbolStyle.color) : + [255, 255, 255, 1]; + const texCoord = symbolStyle.textureCoord || [0, 0, 1, 1]; + const offset = symbolStyle.offset || [0, 0]; + const opacity = symbolStyle.opacity !== undefined ? symbolStyle.opacity : 1; + + /** @type {import('../webgl/ShaderBuilder.js').ShaderParameters} */ + const params = { + uniforms: [], + colorExpression: 'vec4(' + formatColor(color) + ') * vec4(1.0, 1.0, 1.0, ' + formatNumber(opacity) + ')', + sizeExpression: 'vec2(' + size + ')', + offsetExpression: 'vec2(' + formatArray(offset) + ')', + texCoordExpression: 'vec4(' + formatArray(texCoord) + ')', + rotateWithView: !!symbolStyle.rotateWithView + }; + + /** @type {Object.} */ + const uniforms = {}; + + if (symbolStyle.symbolType === 'image' && symbolStyle.src) { + const texture = new Image(); + texture.src = symbolStyle.src; + params.uniforms.push('sampler2D u_texture'); + params.colorExpression = params.colorExpression + + ' * texture2D(u_texture, v_texCoord);'; + uniforms['u_texture'] = texture; + } + return new WebGLPointsLayerRenderer(this, { - vertexShader: getSymbolVertexShader(this.literalStyle.symbol), - fragmentShader: getSymbolFragmentShader() + vertexShader: getSymbolVertexShader(params), + fragmentShader: getSymbolFragmentShader(params), + uniforms: uniforms }); } } diff --git a/src/ol/webgl/ShaderBuilder.js b/src/ol/webgl/ShaderBuilder.js index cecb78eb1b..e44e290b52 100644 --- a/src/ol/webgl/ShaderBuilder.js +++ b/src/ol/webgl/ShaderBuilder.js @@ -1,5 +1,3 @@ -import {asArray} from '../color.js'; - /** * Utilities for generating shaders from literal style objects * @module ol/webgl/ShaderBuilder @@ -15,6 +13,15 @@ export function formatNumber(v) { return s.indexOf('.') === -1 ? s + '.0' : s; } +/** + * Will return the number array as a float with a dot separator, concatenated with ', '. + * @param {Array} array Numerical values array. + * @returns {string} The array as string, e. g.: `1.0, 2.0, 3.0`. + */ +export function formatArray(array) { + return array.map(formatNumber).join(', '); +} + /** * Will normalize and converts to string a color array compatible with GLSL. * @param {Array} colorArray Color in [r, g, b, a] array form, with RGB components in the @@ -29,54 +36,78 @@ export function formatColor(colorArray) { } /** - * Generates a symbol vertex shader from a literal style, + * @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. + */ + +/** + * @typedef {Object} ShaderParameters + * @property {Array} [uniforms] Uniforms; these will be declared in the header (should include the type). + * @property {Array} [attributes] Attributes; these will be declared in the header (should include the type). + * @property {Array} [varyings] Varyings with a name, a type and an expression. + * @property {string} sizeExpression This will be assigned to a `vec2 size` variable. + * @property {string} offsetExpression This will be assigned to a `vec2 offset` variable. + * @property {string} colorExpression This will be the value assigned to gl_FragColor + * @property {string} texCoordExpression This will be the value assigned to the `vec4 v_texCoord` varying. + * @property {boolean} [rotateWithView=false] Whether symbols should rotate with view + */ + +/** + * Generates a symbol vertex shader from a set of parameters, * intended to be used on point geometries. * - * Expected the following attributes to be present in the attribute array: + * Three uniforms are hardcoded in all shaders: `u_projectionMatrix`, `u_offsetScaleMatrix` and + * `u_offsetRotateMatrix`. + * + * 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). * - * Transmits the following varyings to the fragment shader: - * `vec2 v_texCoord`, `float v_opacity`, `vec4 v_color` - * - * @param {import('../style/LiteralStyle.js').LiteralSymbolStyle} parameters Parameters for the shader. + * @param {ShaderParameters} parameters Parameters for the shader. * @returns {string} The full shader as a string. */ export function getSymbolVertexShader(parameters) { const offsetMatrix = parameters.rotateWithView ? - 'mat4 offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;' : - 'mat4 offsetMatrix = u_offsetScaleMatrix;'; + 'u_offsetScaleMatrix * u_offsetRotateMatrix' : + 'u_offsetScaleMatrix'; - const offset = parameters.offset || [0, 0]; - const size = Array.isArray(parameters.size) ? parameters.size : [parameters.size, parameters.size]; - const texCoord = parameters.textureCoord || [0, 0, 1, 1]; - const opacity = parameters.opacity !== undefined ? parameters.opacity : 1; - const color = parameters.color !== undefined ? - (typeof parameters.color === 'string' ? asArray(parameters.color) : parameters.color) : - [255, 255, 255, 1]; - - const f = formatNumber; + const uniforms = parameters.uniforms || []; + const attributes = parameters.attributes || []; + const varyings = parameters.varyings || []; const body = `precision mediump float; uniform mat4 u_projectionMatrix; uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetRotateMatrix; +${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 float v_opacity; -varying vec4 v_color; - +${varyings.map(function(varying) { + return 'varying ' + varying.type + ' ' + varying.name + ';'; + }).join('\n')} void main(void) { - ${offsetMatrix} - float offsetX = a_index == 0.0 || a_index == 3.0 ? ${f(offset[0] - size[0] / 2)} : ${f(offset[0] + size[0] / 2)}; - float offsetY = a_index == 0.0 || a_index == 1.0 ? ${f(offset[1] - size[1] / 2)} : ${f(offset[1] + size[1] / 2)}; + mat4 offsetMatrix = ${offsetMatrix}; + vec2 size = ${parameters.sizeExpression}; + vec2 offset = ${parameters.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; - float u = a_index == 0.0 || a_index == 3.0 ? ${f(texCoord[0])} : ${f(texCoord[2])}; - float v = a_index == 0.0 || a_index == 1.0 ? ${f(texCoord[1])} : ${f(texCoord[3])}; + vec4 texCoord = ${parameters.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; v_texCoord = vec2(u, v); - v_opacity = ${f(opacity)}; - v_color = vec4(${formatColor(color)}); +${varyings.map(function(varying) { + return ' ' + varying.name + ' = ' + varying.expression + ';'; + }).join('\n')} }`; return body; @@ -86,24 +117,25 @@ void main(void) { * Generates a symbol fragment shader intended to be used on point geometries. * * Expected the following varyings to be transmitted by the vertex shader: - * `vec2 v_texCoord`, `float v_opacity`, `vec4 v_color` + * `vec2 v_texCoord` * + * @param {ShaderParameters} parameters Parameters for the shader. * @returns {string} The full shader as a string. */ -export function getSymbolFragmentShader() { - const body = `precision mediump float; -uniform sampler2D u_texture; -varying vec2 v_texCoord; -varying float v_opacity; -varying vec4 v_color; +export function getSymbolFragmentShader(parameters) { + const uniforms = parameters.uniforms || []; + const varyings = parameters.varyings || []; + const body = `precision mediump float; +${uniforms.map(function(uniform) { + return 'uniform ' + uniform + ';'; + }).join('\n')} +varying vec2 v_texCoord; +${varyings.map(function(varying) { + return 'varying ' + varying.type + ' ' + varying.name + ';'; + }).join('\n')} void main(void) { - if (v_opacity == 0.0) { - discard; - } - vec4 textureColor = texture2D(u_texture, v_texCoord); - gl_FragColor = v_color * textureColor; - gl_FragColor.a *= v_opacity; + gl_FragColor = ${parameters.colorExpression}; gl_FragColor.rgb *= gl_FragColor.a; }`; diff --git a/test/spec/ol/webgl/shaderbuilder.test.js b/test/spec/ol/webgl/shaderbuilder.test.js index 03bbf416d8..9c3a2cd9d8 100644 --- a/test/spec/ol/webgl/shaderbuilder.test.js +++ b/test/spec/ol/webgl/shaderbuilder.test.js @@ -1,4 +1,9 @@ -import {getSymbolVertexShader, formatNumber, getSymbolFragmentShader} from '../../../../src/ol/webgl/ShaderBuilder.js'; +import { + getSymbolVertexShader, + formatNumber, + getSymbolFragmentShader, + formatColor, formatArray +} from '../../../../src/ol/webgl/ShaderBuilder.js'; describe('ol.webgl.ShaderBuilder', function() { @@ -12,147 +17,180 @@ describe('ol.webgl.ShaderBuilder', function() { }); }); + describe('formatArray', function() { + it('outputs numbers with dot separators', function() { + expect(formatArray([1, 0, 3.45, 0.8888])).to.eql('1.0, 0.0, 3.45, 0.8888'); + }); + }); + + describe('formatColor', function() { + it('normalizes color and outputs numbers with dot separators', function() { + expect(formatColor([100, 0, 255, 1])).to.eql('0.39215686274509803, 0.0, 1.0, 1.0'); + }); + }); + describe('getSymbolVertexShader', function() { - it('generates a symbol vertex shader without using additional attributes', function() { - expect(getSymbolVertexShader( - { - rotateWithView: false, - color: '#5000ff', - opacity: 0.4, - offset: [5, -7], - size: 6, - textureCoord: [0, 0.5, 0.5, 1] - })).to.eql(`precision mediump float; + it('generates a symbol vertex shader (with varying)', function() { + const parameters = { + varyings: [{ + name: 'v_opacity', + type: 'float', + expression: formatNumber(0.4) + }, { + name: 'v_test', + type: 'vec3', + expression: 'vec3(' + formatArray([1, 2, 3]) + ')' + }], + sizeExpression: 'vec2(' + formatNumber(6) + ')', + offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', + colorExpression: 'vec4(' + formatColor([80, 0, 255, 1]) + ')', + texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')', + rotateWithView: false + }; + + expect(getSymbolVertexShader(parameters)).to.eql(`precision mediump float; uniform mat4 u_projectionMatrix; uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetRotateMatrix; + attribute vec2 a_position; attribute float a_index; + varying vec2 v_texCoord; varying float v_opacity; -varying vec4 v_color; +varying vec3 v_test; +void main(void) { + mat4 offsetMatrix = u_offsetScaleMatrix; + vec2 size = vec2(6.0); + vec2 offset = vec2(5.0, -7.0); + 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 = vec4(0.0, 0.5, 0.5, 1.0); + 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; + v_texCoord = vec2(u, v); + v_opacity = 0.4; + v_test = vec3(1.0, 2.0, 3.0); +}`); + }); + it('generates a symbol vertex shader (with uniforms and attributes)', function() { + const parameters = { + uniforms: ['float u_myUniform'], + attributes: ['vec2 a_myAttr'], + sizeExpression: 'vec2(' + formatNumber(6) + ')', + offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', + colorExpression: 'vec4(' + formatColor([80, 0, 255, 1]) + ')', + texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')' + }; + + expect(getSymbolVertexShader(parameters)).to.eql(`precision mediump float; +uniform mat4 u_projectionMatrix; +uniform mat4 u_offsetScaleMatrix; +uniform mat4 u_offsetRotateMatrix; +uniform float u_myUniform; +attribute vec2 a_position; +attribute float a_index; +attribute vec2 a_myAttr; +varying vec2 v_texCoord; void main(void) { mat4 offsetMatrix = u_offsetScaleMatrix; - float offsetX = a_index == 0.0 || a_index == 3.0 ? 2.0 : 8.0; - float offsetY = a_index == 0.0 || a_index == 1.0 ? -10.0 : -4.0; + vec2 size = vec2(6.0); + vec2 offset = vec2(5.0, -7.0); + 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; - float u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 0.5; - float v = a_index == 0.0 || a_index == 1.0 ? 0.5 : 1.0; + vec4 texCoord = vec4(0.0, 0.5, 0.5, 1.0); + 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; v_texCoord = vec2(u, v); - v_opacity = 0.4; - v_color = vec4(0.3137254901960784, 0.0, 1.0, 1.0); -}`); - }); - it('correctly handles size as an array', function() { - expect(getSymbolVertexShader( - { - rotateWithView: false, - color: '#5000ff', - opacity: 0.4, - offset: [5, -7], - size: [10, 12], - textureCoord: [0, 0.5, 0.5, 1] - })).to.eql(`precision mediump float; -uniform mat4 u_projectionMatrix; -uniform mat4 u_offsetScaleMatrix; -uniform mat4 u_offsetRotateMatrix; -attribute vec2 a_position; -attribute float a_index; -varying vec2 v_texCoord; -varying float v_opacity; -varying vec4 v_color; -void main(void) { - mat4 offsetMatrix = u_offsetScaleMatrix; - float offsetX = a_index == 0.0 || a_index == 3.0 ? 0.0 : 10.0; - float offsetY = a_index == 0.0 || a_index == 1.0 ? -13.0 : -1.0; - vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0); - gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets; - float u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 0.5; - float v = a_index == 0.0 || a_index == 1.0 ? 0.5 : 1.0; - v_texCoord = vec2(u, v); - v_opacity = 0.4; - v_color = vec4(0.3137254901960784, 0.0, 1.0, 1.0); }`); }); - it('correctly handles rotate with view', function() { - expect(getSymbolVertexShader( - { - rotateWithView: true, - color: '#5000ff', - opacity: 0.4, - size: 6, - textureCoord: [0, 0.5, 0.5, 1] - })).to.eql(`precision mediump float; + it('generates a symbol vertex shader (with rotateWithView)', function() { + const parameters = { + sizeExpression: 'vec2(' + formatNumber(6) + ')', + offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', + colorExpression: 'vec4(' + formatColor([80, 0, 255, 1]) + ')', + texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')', + rotateWithView: true + }; + + expect(getSymbolVertexShader(parameters)).to.eql(`precision mediump float; uniform mat4 u_projectionMatrix; uniform mat4 u_offsetScaleMatrix; uniform mat4 u_offsetRotateMatrix; + attribute vec2 a_position; attribute float a_index; + varying vec2 v_texCoord; -varying float v_opacity; -varying vec4 v_color; void main(void) { mat4 offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix; - float offsetX = a_index == 0.0 || a_index == 3.0 ? -3.0 : 3.0; - float offsetY = a_index == 0.0 || a_index == 1.0 ? -3.0 : 3.0; + vec2 size = vec2(6.0); + vec2 offset = vec2(5.0, -7.0); + 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; - float u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 0.5; - float v = a_index == 0.0 || a_index == 1.0 ? 0.5 : 1.0; + vec4 texCoord = vec4(0.0, 0.5, 0.5, 1.0); + 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; v_texCoord = vec2(u, v); - v_opacity = 0.4; - v_color = vec4(0.3137254901960784, 0.0, 1.0, 1.0); -}`); - }); - it('correctly handles missing optional parameters', function() { - expect(getSymbolVertexShader( - { - rotateWithView: false, - size: 5 - })).to.eql(`precision mediump float; -uniform mat4 u_projectionMatrix; -uniform mat4 u_offsetScaleMatrix; -uniform mat4 u_offsetRotateMatrix; -attribute vec2 a_position; -attribute float a_index; -varying vec2 v_texCoord; -varying float v_opacity; -varying vec4 v_color; -void main(void) { - mat4 offsetMatrix = u_offsetScaleMatrix; - float offsetX = a_index == 0.0 || a_index == 3.0 ? -2.5 : 2.5; - float offsetY = a_index == 0.0 || a_index == 1.0 ? -2.5 : 2.5; - vec4 offsets = offsetMatrix * vec4(offsetX, offsetY, 0.0, 0.0); - gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets; - float u = a_index == 0.0 || a_index == 3.0 ? 0.0 : 1.0; - float v = a_index == 0.0 || a_index == 1.0 ? 0.0 : 1.0; - v_texCoord = vec2(u, v); - v_opacity = 1.0; - v_color = vec4(1.0, 1.0, 1.0, 1.0); }`); }); }); describe('getSymbolFragmentShader', function() { - it('generates a fixed shader', function() { - expect(getSymbolFragmentShader()).to.eql(`precision mediump float; -uniform sampler2D u_texture; + it('generates a symbol fragment shader (with varying)', function() { + const parameters = { + varyings: [{ + name: 'v_opacity', + type: 'float', + expression: formatNumber(0.4) + }, { + name: 'v_test', + type: 'vec3', + expression: 'vec3(' + formatArray([1, 2, 3]) + ')' + }], + sizeExpression: 'vec2(' + formatNumber(6) + ')', + offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', + colorExpression: 'vec4(' + formatColor([80, 0, 255]) + ', v_opacity)', + texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')', + rotateWithView: false + }; + + expect(getSymbolFragmentShader(parameters)).to.eql(`precision mediump float; + varying vec2 v_texCoord; varying float v_opacity; -varying vec4 v_color; +varying vec3 v_test; +void main(void) { + gl_FragColor = vec4(0.3137254901960784, 0.0, 1.0, v_opacity); + gl_FragColor.rgb *= gl_FragColor.a; +}`); + }); + it('generates a symbol fragment shader (with uniforms)', function() { + const parameters = { + uniforms: ['float u_myUniform', 'vec2 u_myUniform2'], + sizeExpression: 'vec2(' + formatNumber(6) + ')', + offsetExpression: 'vec2(' + formatArray([5, -7]) + ')', + colorExpression: 'vec4(' + formatColor([255, 255, 255, 1]) + ')', + texCoordExpression: 'vec4(' + formatArray([0, 0.5, 0.5, 1]) + ')' + }; + + expect(getSymbolFragmentShader(parameters)).to.eql(`precision mediump float; +uniform float u_myUniform; +uniform vec2 u_myUniform2; +varying vec2 v_texCoord; void main(void) { - if (v_opacity == 0.0) { - discard; - } - vec4 textureColor = texture2D(u_texture, v_texCoord); - gl_FragColor = v_color * textureColor; - gl_FragColor.a *= v_opacity; + gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); gl_FragColor.rgb *= gl_FragColor.a; }`); });