Support paletted rendering in WebGL

This commit is contained in:
Tim Schaub
2021-12-20 18:25:49 -07:00
parent 0c11a7514d
commit 59186f2afd
6 changed files with 221 additions and 10 deletions

View File

@@ -8,6 +8,7 @@ import WebGLTileLayerRenderer, {
Uniforms, Uniforms,
} from '../renderer/webgl/TileLayer.js'; } from '../renderer/webgl/TileLayer.js';
import { import {
PALETTE_TEXTURE_ARRAY,
ValueTypes, ValueTypes,
expressionToGlsl, expressionToGlsl,
getStringNumberEquivalent, getStringNumberEquivalent,
@@ -77,6 +78,7 @@ import {assign} from '../obj.js';
* @property {string} vertexShader The vertex shader. * @property {string} vertexShader The vertex shader.
* @property {string} fragmentShader The fragment shader. * @property {string} fragmentShader The fragment shader.
* @property {Object<string,import("../webgl/Helper.js").UniformValue>} uniforms Uniform definitions. * @property {Object<string,import("../webgl/Helper.js").UniformValue>} uniforms Uniform definitions.
* @property {Array<import("../webgl/PaletteTexture.js").default>} paletteTextures Palette textures.
*/ */
/** /**
@@ -209,6 +211,12 @@ function parseStyle(style, bandCount) {
`uniform sampler2D ${Uniforms.TILE_TEXTURE_ARRAY}[${textureCount}];` `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 ( const functionDefintions = Object.keys(context.functions).map(function (
name name
) { ) {
@@ -253,6 +261,7 @@ function parseStyle(style, bandCount) {
vertexShader: vertexShader, vertexShader: vertexShader,
fragmentShader: fragmentShader, fragmentShader: fragmentShader,
uniforms: uniforms, uniforms: uniforms,
paletteTextures: context.paletteTextures,
}; };
} }
@@ -327,6 +336,7 @@ class WebGLTileLayer extends BaseTileLayer {
fragmentShader: parsedStyle.fragmentShader, fragmentShader: parsedStyle.fragmentShader,
uniforms: parsedStyle.uniforms, uniforms: parsedStyle.uniforms,
cacheSize: this.cacheSize_, cacheSize: this.cacheSize_,
paletteTextures: parsedStyle.paletteTextures,
}); });
} }
@@ -344,6 +354,7 @@ class WebGLTileLayer extends BaseTileLayer {
vertexShader: parsedStyle.vertexShader, vertexShader: parsedStyle.vertexShader,
fragmentShader: parsedStyle.fragmentShader, fragmentShader: parsedStyle.fragmentShader,
uniforms: parsedStyle.uniforms, uniforms: parsedStyle.uniforms,
paletteTextures: parsedStyle.paletteTextures,
}); });
this.changed(); this.changed();
} }

View File

@@ -114,6 +114,7 @@ function getRenderExtent(frameState, extent) {
* @property {string} fragmentShader Fragment shader source. * @property {string} fragmentShader Fragment shader source.
* @property {Object<string, import("../../webgl/Helper").UniformValue>} [uniforms] Additional uniforms * @property {Object<string, import("../../webgl/Helper").UniformValue>} [uniforms] Additional uniforms
* made available to shaders. * made available to shaders.
* @property {Array<import("../../webgl/PaletteTexture.js").default>} [paletteTextures] Palette textures.
* @property {number} [cacheSize=512] The texture cache size. * @property {number} [cacheSize=512] The texture cache size.
*/ */
@@ -211,6 +212,12 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
* @private * @private
*/ */
this.tileTextureCache_ = new LRUCache(cacheSize); this.tileTextureCache_ = new LRUCache(cacheSize);
/**
* @type {Array<import("../../webgl/PaletteTexture.js").default>}
* @private
*/
this.paletteTextures_ = options.paletteTextures || [];
} }
/** /**
@@ -222,6 +229,7 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
}); });
this.vertexShader_ = options.vertexShader; this.vertexShader_ = options.vertexShader;
this.fragmentShader_ = options.fragmentShader; this.fragmentShader_ = options.fragmentShader;
this.paletteTextures_ = options.paletteTextures || [];
if (this.helper) { if (this.helper) {
this.program_ = this.helper.getProgram( this.program_ = this.helper.getProgram(
@@ -510,19 +518,33 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
this.helper.bindBuffer(this.indices_); this.helper.bindBuffer(this.indices_);
this.helper.enableAttributes(attributeDescriptions); this.helper.enableAttributes(attributeDescriptions);
for ( let textureSlot = 0;
let textureIndex = 0; while (textureSlot < tileTexture.textures.length) {
textureIndex < tileTexture.textures.length; const textureProperty = 'TEXTURE' + textureSlot;
++textureIndex const uniformName = `${Uniforms.TILE_TEXTURE_ARRAY}[${textureSlot}]`;
) {
const textureProperty = 'TEXTURE' + textureIndex;
const uniformName = `${Uniforms.TILE_TEXTURE_ARRAY}[${textureIndex}]`;
gl.activeTexture(gl[textureProperty]); gl.activeTexture(gl[textureProperty]);
gl.bindTexture(gl.TEXTURE_2D, tileTexture.textures[textureIndex]); gl.bindTexture(gl.TEXTURE_2D, tileTexture.textures[textureSlot]);
gl.uniform1i( gl.uniform1i(
this.helper.getUniformLocation(uniformName), 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 = const alpha =

View File

@@ -3,8 +3,9 @@
* @module ol/style/expressions * @module ol/style/expressions
*/ */
import PaletteTexture from '../webgl/PaletteTexture.js';
import {Uniforms} from '../renderer/webgl/TileLayer.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'; 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 * * `['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. * 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. * 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. * Values can either be literals or another operator, as they will be evaluated recursively.
* Literal values can be of the following types: * Literal values can be of the following types:
@@ -186,6 +192,7 @@ export function isTypeUnique(valueType) {
* @property {Object<string, number>} stringLiteralsMap This object maps all encountered string values to a number * @property {Object<string, number>} stringLiteralsMap This object maps all encountered string values to a number
* @property {Object<string, string>} functions Lookup of functions used by the style. * @property {Object<string, string>} functions Lookup of functions used by the style.
* @property {number} [bandCount] Number of bands per pixel. * @property {number} [bandCount] Number of bands per pixel.
* @property {Array<PaletteTexture>} [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'; const GET_BAND_VALUE_FUNC = 'getBandValue';
Operators['band'] = { Operators['band'] = {

View File

@@ -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;

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -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'});