diff --git a/src/ol/layer/WebGLTile.js b/src/ol/layer/WebGLTile.js index 9e404bc266..1409a69ef7 100644 --- a/src/ol/layer/WebGLTile.js +++ b/src/ol/layer/WebGLTile.js @@ -8,6 +8,7 @@ import WebGLTileLayerRenderer, { Uniforms, } from '../renderer/webgl/TileLayer.js'; import { + PALETTE_TEXTURE_ARRAY, ValueTypes, expressionToGlsl, getStringNumberEquivalent, @@ -77,6 +78,7 @@ import {assign} from '../obj.js'; * @property {string} vertexShader The vertex shader. * @property {string} fragmentShader The fragment shader. * @property {Object} uniforms Uniform definitions. + * @property {Array} paletteTextures Palette textures. */ /** @@ -209,6 +211,12 @@ function parseStyle(style, bandCount) { `uniform sampler2D ${Uniforms.TILE_TEXTURE_ARRAY}[${textureCount}];` ); + if (context.paletteTextures) { + uniformDeclarations.push( + `uniform sampler2D ${PALETTE_TEXTURE_ARRAY}[${context.paletteTextures.length}];` + ); + } + const functionDefintions = Object.keys(context.functions).map(function ( name ) { @@ -253,6 +261,7 @@ function parseStyle(style, bandCount) { vertexShader: vertexShader, fragmentShader: fragmentShader, uniforms: uniforms, + paletteTextures: context.paletteTextures, }; } @@ -327,6 +336,7 @@ class WebGLTileLayer extends BaseTileLayer { fragmentShader: parsedStyle.fragmentShader, uniforms: parsedStyle.uniforms, cacheSize: this.cacheSize_, + paletteTextures: parsedStyle.paletteTextures, }); } @@ -344,6 +354,7 @@ class WebGLTileLayer extends BaseTileLayer { vertexShader: parsedStyle.vertexShader, fragmentShader: parsedStyle.fragmentShader, uniforms: parsedStyle.uniforms, + paletteTextures: parsedStyle.paletteTextures, }); this.changed(); } diff --git a/src/ol/renderer/webgl/TileLayer.js b/src/ol/renderer/webgl/TileLayer.js index 47c39fcc28..c23f77270e 100644 --- a/src/ol/renderer/webgl/TileLayer.js +++ b/src/ol/renderer/webgl/TileLayer.js @@ -114,6 +114,7 @@ function getRenderExtent(frameState, extent) { * @property {string} fragmentShader Fragment shader source. * @property {Object} [uniforms] Additional uniforms * made available to shaders. + * @property {Array} [paletteTextures] Palette textures. * @property {number} [cacheSize=512] The texture cache size. */ @@ -211,6 +212,12 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer { * @private */ this.tileTextureCache_ = new LRUCache(cacheSize); + + /** + * @type {Array} + * @private + */ + this.paletteTextures_ = options.paletteTextures || []; } /** @@ -222,6 +229,7 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer { }); this.vertexShader_ = options.vertexShader; this.fragmentShader_ = options.fragmentShader; + this.paletteTextures_ = options.paletteTextures || []; if (this.helper) { this.program_ = this.helper.getProgram( @@ -510,19 +518,33 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer { this.helper.bindBuffer(this.indices_); this.helper.enableAttributes(attributeDescriptions); - for ( - let textureIndex = 0; - textureIndex < tileTexture.textures.length; - ++textureIndex - ) { - const textureProperty = 'TEXTURE' + textureIndex; - const uniformName = `${Uniforms.TILE_TEXTURE_ARRAY}[${textureIndex}]`; + let textureSlot = 0; + while (textureSlot < tileTexture.textures.length) { + const textureProperty = 'TEXTURE' + textureSlot; + const uniformName = `${Uniforms.TILE_TEXTURE_ARRAY}[${textureSlot}]`; gl.activeTexture(gl[textureProperty]); - gl.bindTexture(gl.TEXTURE_2D, tileTexture.textures[textureIndex]); + gl.bindTexture(gl.TEXTURE_2D, tileTexture.textures[textureSlot]); gl.uniform1i( this.helper.getUniformLocation(uniformName), - textureIndex + textureSlot ); + ++textureSlot; + } + + for ( + let paletteIndex = 0; + paletteIndex < this.paletteTextures_.length; + ++paletteIndex + ) { + const paletteTexture = this.paletteTextures_[paletteIndex]; + gl.activeTexture(gl['TEXTURE' + textureSlot]); + const texture = paletteTexture.getTexture(gl); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.uniform1i( + this.helper.getUniformLocation(paletteTexture.name), + textureSlot + ); + ++textureSlot; } const alpha = diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js index 89ae372f6f..2fef211625 100644 --- a/src/ol/style/expressions.js +++ b/src/ol/style/expressions.js @@ -3,8 +3,9 @@ * @module ol/style/expressions */ +import PaletteTexture from '../webgl/PaletteTexture.js'; import {Uniforms} from '../renderer/webgl/TileLayer.js'; -import {asArray, isStringColor} from '../color.js'; +import {asArray, fromString, isStringColor} from '../color.js'; import {log2} from '../math.js'; /** @@ -77,6 +78,11 @@ import {log2} from '../math.js'; * * `['color', red, green, blue, alpha]` creates a `color` value from `number` values; the `alpha` parameter is * optional; if not specified, it will be set to 1. * Note: `red`, `green` and `blue` components must be values between 0 and 255; `alpha` between 0 and 1. + * * `['palette', index, colors]` picks a `color` value from an array of colors using the given index; the `index` + * expression must evaluate to a number; the items in the `colors` array must be strings with hex colors + * (e.g. `'#86A136'`), colors using the rgba[a] functional notation (e.g. `'rgb(134, 161, 54)'` or `'rgba(134, 161, 54, 1)'`), + * named colors (e.g. `'red'`), or array literals with 3 ([r, g, b]) or 4 ([r, g, b, a]) values (with r, g, and b + * in the 0-255 range and a in the 0-1 range). * * Values can either be literals or another operator, as they will be evaluated recursively. * Literal values can be of the following types: @@ -186,6 +192,7 @@ export function isTypeUnique(valueType) { * @property {Object} stringLiteralsMap This object maps all encountered string values to a number * @property {Object} functions Lookup of functions used by the style. * @property {number} [bandCount] Number of bands per pixel. + * @property {Array} [paletteTextures] List of palettes used by the style. */ /** @@ -418,6 +425,65 @@ Operators['var'] = { }, }; +export const PALETTE_TEXTURE_ARRAY = 'u_paletteTextures'; + +// ['palette', index, colors] +Operators['palette'] = { + getReturnType: function (args) { + return ValueTypes.COLOR; + }, + toGlsl: function (context, args) { + assertArgsCount(args, 2); + assertNumber(args[0]); + const index = expressionToGlsl(context, args[0]); + const colors = args[1]; + if (!Array.isArray(colors)) { + throw new Error('The second argument of palette must be an array'); + } + const numColors = colors.length; + const palette = new Uint8Array(numColors * 4); + for (let i = 0; i < numColors; i++) { + const candidate = colors[i]; + /** + * @type {import('../color.js').Color} + */ + let color; + if (typeof candidate === 'string') { + color = fromString(candidate); + } else { + if (!Array.isArray(candidate)) { + throw new Error( + 'The second argument of palette must be an array of strings or colors' + ); + } + const length = candidate.length; + if (length === 4) { + color = candidate; + } else { + if (length !== 3) { + throw new Error( + `Expected palette color to have 3 or 4 values, got ${length}` + ); + } + color = [candidate[0], candidate[1], candidate[2], 1]; + } + } + const offset = i * 4; + palette[offset] = color[0]; + palette[offset + 1] = color[1]; + palette[offset + 2] = color[2]; + palette[offset + 3] = color[3] * 255; + } + if (!context.paletteTextures) { + context.paletteTextures = []; + } + const paletteName = `${PALETTE_TEXTURE_ARRAY}[${context.paletteTextures.length}]`; + const paletteTexture = new PaletteTexture(paletteName, palette); + context.paletteTextures.push(paletteTexture); + return `texture2D(${paletteName}, vec2((${index} + 0.5) / ${numColors}.0, 0.5))`; + }, +}; + const GET_BAND_VALUE_FUNC = 'getBandValue'; Operators['band'] = { diff --git a/src/ol/webgl/PaletteTexture.js b/src/ol/webgl/PaletteTexture.js new file mode 100644 index 0000000000..bbfbaeb59a --- /dev/null +++ b/src/ol/webgl/PaletteTexture.js @@ -0,0 +1,50 @@ +/** + * @module ol/webgl/PaletteTexture + */ + +class PaletteTexture { + /** + * @param {string} name The name of the texture. + * @param {Uint8Array} data The texture data. + */ + constructor(name, data) { + this.name = name; + this.data = data; + + /** + * @type {WebGLTexture} + * @private + */ + this.texture_ = null; + } + + /** + * @param {WebGLRenderingContext} gl Rendering context. + * @return {WebGLTexture} The texture. + */ + getTexture(gl) { + if (!this.texture_) { + const texture = gl.createTexture(); + gl.bindTexture(gl.TEXTURE_2D, texture); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); + gl.texImage2D( + gl.TEXTURE_2D, + 0, + gl.RGBA, + this.data.length / 4, + 1, + 0, + gl.RGBA, + gl.UNSIGNED_BYTE, + this.data + ); + this.texture_ = texture; + } + return this.texture_; + } +} + +export default PaletteTexture; diff --git a/test/rendering/cases/webgl-palette/expected.png b/test/rendering/cases/webgl-palette/expected.png new file mode 100644 index 0000000000..d5017ade58 Binary files /dev/null and b/test/rendering/cases/webgl-palette/expected.png differ diff --git a/test/rendering/cases/webgl-palette/main.js b/test/rendering/cases/webgl-palette/main.js new file mode 100644 index 0000000000..2f82c01bf6 --- /dev/null +++ b/test/rendering/cases/webgl-palette/main.js @@ -0,0 +1,62 @@ +import DataTile from '../../../../src/ol/source/DataTile.js'; +import Map from '../../../../src/ol/Map.js'; +import TileLayer from '../../../../src/ol/layer/WebGLTile.js'; +import View from '../../../../src/ol/View.js'; + +const size = 256; + +const colors = [ + 'palegoldenrod', + 'palegreen', + 'paleturquoise', + 'palevioletred', + 'papayawhip', + 'peachpuff', + 'peru', + 'pink', + 'plum', + 'powderblue', + 'rosybrown', + 'royalblue', + 'saddlebrown', + 'salmon', + 'sandybrown', + 'seagreen', + 'seashell', + 'sienna', + 'skyblue', +]; + +new Map({ + target: 'map', + layers: [ + new TileLayer({ + source: new DataTile({ + loader: function () { + const data = new Float32Array(size * size); + const numColors = colors.length; + const numBlocks = 8; + const blockSize = size / numBlocks; + for (let row = 0; row < size; ++row) { + const r = Math.floor(row / blockSize); + for (let col = 0; col < size; ++col) { + const c = Math.floor(col / blockSize); + const colorIndex = (r * numBlocks + c) % numColors; + data[row * size + col] = colorIndex; + } + } + return data; + }, + }), + style: { + color: ['palette', ['band', 1], colors], + }, + }), + ], + view: new View({ + center: [0, 0], + zoom: 0, + }), +}); + +render({message: 'Renders colors from a palette'});