diff --git a/examples/cog-stretch.css b/examples/cog-stretch.css
new file mode 100644
index 0000000000..fc35d9aac7
--- /dev/null
+++ b/examples/cog-stretch.css
@@ -0,0 +1,6 @@
+.controls {
+ display: grid;
+ grid-template-columns: auto auto 1fr;
+ align-items: baseline;
+ gap: 0 1em;
+}
diff --git a/examples/cog-stretch.html b/examples/cog-stretch.html
new file mode 100644
index 0000000000..11434912ed
--- /dev/null
+++ b/examples/cog-stretch.html
@@ -0,0 +1,44 @@
+---
+layout: example.html
+title: Band Constrast Stretch
+shortdesc: Choosing bands and applying constrast stretch
+docs: >
+ This example uses the `layer.updateStyleVariables()` method to update the rendering
+ of a GeoTIFF based on user selected bands and contrast stretch parameters.
+tags: "cog, webgl, style"
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/cog-stretch.js b/examples/cog-stretch.js
new file mode 100644
index 0000000000..3e7acdb8a2
--- /dev/null
+++ b/examples/cog-stretch.js
@@ -0,0 +1,62 @@
+import GeoTIFF from '../src/ol/source/GeoTIFF.js';
+import Map from '../src/ol/Map.js';
+import TileLayer from '../src/ol/layer/WebGLTile.js';
+import View from '../src/ol/View.js';
+
+const channels = ['red', 'green', 'blue'];
+for (const channel of channels) {
+ const selector = document.getElementById(channel);
+ selector.addEventListener('change', update);
+
+ const input = document.getElementById(`${channel}Max`);
+ input.addEventListener('input', update);
+}
+
+function getVariables() {
+ const variables = {};
+ for (const channel of channels) {
+ const selector = document.getElementById(channel);
+ variables[channel] = parseInt(selector.value, 10);
+
+ const inputId = `${channel}Max`;
+ const input = document.getElementById(inputId);
+ variables[inputId] = parseInt(input.value, 10);
+ }
+ return variables;
+}
+
+const layer = new TileLayer({
+ style: {
+ variables: getVariables(),
+ color: [
+ 'array',
+ ['/', ['band', ['var', 'red']], ['var', 'redMax']],
+ ['/', ['band', ['var', 'green']], ['var', 'greenMax']],
+ ['/', ['band', ['var', 'blue']], ['var', 'blueMax']],
+ 1,
+ ],
+ },
+ source: new GeoTIFF({
+ normalize: false,
+ sources: [
+ {
+ url: 'https://s2downloads.eox.at/demo/EOxCloudless/2020/rgbnir/s2cloudless2020-16bits_sinlge-file_z0-4.tif',
+ },
+ ],
+ }),
+});
+
+function update() {
+ layer.updateStyleVariables(getVariables());
+}
+
+const map = new Map({
+ target: 'map',
+ layers: [layer],
+ view: new View({
+ projection: 'EPSG:4326',
+ center: [0, 0],
+ zoom: 2,
+ maxZoom: 6,
+ }),
+});
diff --git a/src/ol/layer/WebGLTile.js b/src/ol/layer/WebGLTile.js
index dc34479ce9..d801be00a6 100644
--- a/src/ol/layer/WebGLTile.js
+++ b/src/ol/layer/WebGLTile.js
@@ -105,6 +105,7 @@ function parseStyle(style, bandCount) {
variables: [],
attributes: [],
stringLiteralsMap: {},
+ functions: {},
bandCount: bandCount,
};
@@ -203,14 +204,15 @@ function parseStyle(style, bandCount) {
});
const textureCount = Math.ceil(bandCount / 4);
- const colorAssignments = new Array(textureCount);
- for (let textureIndex = 0; textureIndex < textureCount; ++textureIndex) {
- const uniformName = Uniforms.TILE_TEXTURE_PREFIX + textureIndex;
- uniformDeclarations.push(`uniform sampler2D ${uniformName};`);
- colorAssignments[
- textureIndex
- ] = `vec4 color${textureIndex} = texture2D(${uniformName}, v_textureCoord);`;
- }
+ uniformDeclarations.push(
+ `uniform sampler2D ${Uniforms.TILE_TEXTURE_ARRAY}[${textureCount}];`
+ );
+
+ const functionDefintions = Object.keys(context.functions).map(function (
+ name
+ ) {
+ return context.functions[name];
+ });
const fragmentShader = `
#ifdef GL_FRAGMENT_PRECISION_HIGH
@@ -228,10 +230,12 @@ function parseStyle(style, bandCount) {
${uniformDeclarations.join('\n')}
- void main() {
- ${colorAssignments.join('\n')}
+ ${functionDefintions.join('\n')}
- vec4 color = color0;
+ void main() {
+ vec4 color = texture2D(${
+ Uniforms.TILE_TEXTURE_ARRAY
+ }[0], v_textureCoord);
${pipeline.join('\n')}
diff --git a/src/ol/renderer/webgl/TileLayer.js b/src/ol/renderer/webgl/TileLayer.js
index 68c0917031..b94618be2f 100644
--- a/src/ol/renderer/webgl/TileLayer.js
+++ b/src/ol/renderer/webgl/TileLayer.js
@@ -31,7 +31,7 @@ import {numberSafeCompareFunction} from '../../array.js';
import {toSize} from '../../size.js';
export const Uniforms = {
- TILE_TEXTURE_PREFIX: 'u_tileTexture',
+ TILE_TEXTURE_ARRAY: 'u_tileTextures',
TILE_TRANSFORM: 'u_tileTransform',
TRANSITION_ALPHA: 'u_transitionAlpha',
DEPTH: 'u_depth',
@@ -516,7 +516,7 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer {
++textureIndex
) {
const textureProperty = 'TEXTURE' + textureIndex;
- const uniformName = Uniforms.TILE_TEXTURE_PREFIX + textureIndex;
+ const uniformName = `${Uniforms.TILE_TEXTURE_ARRAY}[${textureIndex}]`;
gl.activeTexture(gl[textureProperty]);
gl.bindTexture(gl.TEXTURE_2D, tileTexture.textures[textureIndex]);
gl.uniform1i(
diff --git a/src/ol/style/expressions.js b/src/ol/style/expressions.js
index f34da179cd..89ae372f6f 100644
--- a/src/ol/style/expressions.js
+++ b/src/ol/style/expressions.js
@@ -184,6 +184,7 @@ export function isTypeUnique(valueType) {
* @property {Array} variables List of variables used in the expression; contains **unprefixed names**
* @property {Array} attributes List of attributes used in the expression; contains **unprefixed names**
* @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.
*/
@@ -417,6 +418,8 @@ Operators['var'] = {
},
};
+const GET_BAND_VALUE_FUNC = 'getBandValue';
+
Operators['band'] = {
getReturnType: function (args) {
return ValueTypes.NUMBER;
@@ -425,32 +428,38 @@ Operators['band'] = {
assertArgsMinCount(args, 1);
assertArgsMaxCount(args, 3);
const band = args[0];
- if (typeof band !== 'number') {
- throw new Error('Band index must be a number');
- }
- const zeroBasedBand = band - 1;
- const colorIndex = Math.floor(zeroBasedBand / 4);
- let bandIndex = zeroBasedBand % 4;
- if (band === context.bandCount && bandIndex === 1) {
- // LUMINANCE_ALPHA - band 1 assigned to rgb and band 2 assigned to alpha
- bandIndex = 3;
- }
- if (args.length === 1) {
- return `color${colorIndex}[${bandIndex}]`;
- } else {
- const xOffset = args[1];
- const yOffset = args[2] || 0;
- assertNumber(xOffset);
- assertNumber(yOffset);
- const uniformName = Uniforms.TILE_TEXTURE_PREFIX + colorIndex;
- return `texture2D(${uniformName}, v_textureCoord + vec2(${expressionToGlsl(
- context,
- xOffset
- )} / ${Uniforms.TEXTURE_PIXEL_WIDTH}, ${expressionToGlsl(
- context,
- yOffset
- )} / ${Uniforms.TEXTURE_PIXEL_HEIGHT}))[${bandIndex}]`;
+
+ if (!(GET_BAND_VALUE_FUNC in context.functions)) {
+ let ifBlocks = '';
+ const bandCount = context.bandCount || 1;
+ for (let i = 0; i < bandCount; i++) {
+ const colorIndex = Math.floor(i / 4);
+ let bandIndex = i % 4;
+ if (bandIndex === bandCount - 1 && bandIndex === 1) {
+ // LUMINANCE_ALPHA - band 1 assigned to rgb and band 2 assigned to alpha
+ bandIndex = 3;
+ }
+ const textureName = `${Uniforms.TILE_TEXTURE_ARRAY}[${colorIndex}]`;
+ ifBlocks += `
+ if (band == ${i + 1}.0) {
+ return texture2D(${textureName}, v_textureCoord + vec2(dx, dy))[${bandIndex}];
+ }
+ `;
+ }
+
+ context.functions[GET_BAND_VALUE_FUNC] = `
+ float getBandValue(float band, float xOffset, float yOffset) {
+ float dx = xOffset / ${Uniforms.TEXTURE_PIXEL_WIDTH};
+ float dy = yOffset / ${Uniforms.TEXTURE_PIXEL_HEIGHT};
+ ${ifBlocks}
+ }
+ `;
}
+
+ const bandExpression = expressionToGlsl(context, band);
+ const xOffsetExpression = expressionToGlsl(context, args[1] || 0);
+ const yOffsetExpression = expressionToGlsl(context, args[2] || 0);
+ return `${GET_BAND_VALUE_FUNC}(${bandExpression}, ${xOffsetExpression}, ${yOffsetExpression})`;
},
};
diff --git a/src/ol/webgl/ShaderBuilder.js b/src/ol/webgl/ShaderBuilder.js
index 281da512b6..5c9ae52b65 100644
--- a/src/ol/webgl/ShaderBuilder.js
+++ b/src/ol/webgl/ShaderBuilder.js
@@ -441,6 +441,7 @@ export function parseLiteralStyle(style) {
variables: [],
attributes: [],
stringLiteralsMap: {},
+ functions: {},
};
const parsedSize = expressionToGlsl(
vertContext,
@@ -471,6 +472,7 @@ export function parseLiteralStyle(style) {
variables: vertContext.variables,
attributes: [],
stringLiteralsMap: vertContext.stringLiteralsMap,
+ functions: {},
};
const parsedColor = expressionToGlsl(fragContext, color, ValueTypes.COLOR);
const parsedOpacity = expressionToGlsl(
diff --git a/test/browser/spec/ol/layer/WebGLTile.test.js b/test/browser/spec/ol/layer/WebGLTile.test.js
index 04662ab1ab..d4b14695f8 100644
--- a/test/browser/spec/ol/layer/WebGLTile.test.js
+++ b/test/browser/spec/ol/layer/WebGLTile.test.js
@@ -96,10 +96,9 @@ describe('ol/layer/WebGLTile', function () {
uniform float u_var_r;
uniform float u_var_g;
uniform float u_var_b;
- uniform sampler2D u_tileTexture0;
+ uniform sampler2D u_tileTextures[1];
void main() {
- vec4 color0 = texture2D(u_tileTexture0, v_textureCoord);
- vec4 color = color0;
+ vec4 color = texture2D(u_tileTextures[0], v_textureCoord);
color = vec4(u_var_r / 255.0, u_var_g / 255.0, u_var_b / 255.0, 1.0);
if (color.a == 0.0) {
discard;
@@ -125,6 +124,82 @@ describe('ol/layer/WebGLTile', function () {
);
});
+ it('adds a getBandValue function to the fragment shaders', function () {
+ const max = 3000;
+ function normalize(value) {
+ return ['/', value, max];
+ }
+
+ const red = normalize(['band', 1]);
+ const green = normalize(['band', 2]);
+ const nir = normalize(['band', 4]);
+
+ layer.setStyle({
+ color: ['array', nir, red, green, 1],
+ });
+
+ const compileShaderSpy = sinon.spy(WebGLHelper.prototype, 'compileShader');
+ const renderer = layer.createRenderer();
+ const viewState = map.getView().getState();
+ const size = map.getSize();
+ const frameState = {
+ viewState: viewState,
+ extent: getForViewAndSize(
+ viewState.center,
+ viewState.resolution,
+ viewState.rotation,
+ size
+ ),
+ layerStatesArray: map.getLayerGroup().getLayerStatesArray(),
+ layerIndex: 0,
+ };
+ renderer.prepareFrame(frameState);
+ compileShaderSpy.restore();
+ expect(compileShaderSpy.callCount).to.be(2);
+ expect(compileShaderSpy.getCall(0).args[0].replace(/[ \n]+/g, ' ')).to.be(
+ `
+ #ifdef GL_FRAGMENT_PRECISION_HIGH
+ precision highp float;
+ #else
+ precision mediump float;
+ #endif varying vec2 v_textureCoord;
+ uniform float u_transitionAlpha;
+ uniform float u_texturePixelWidth;
+ uniform float u_texturePixelHeight;
+ uniform float u_resolution;
+ uniform float u_zoom;
+ uniform sampler2D u_tileTextures[1];
+
+ float getBandValue(float band, float xOffset, float yOffset) {
+ float dx = xOffset / u_texturePixelWidth;
+ float dy = yOffset / u_texturePixelHeight;
+ if (band == 1.0) {
+ return texture2D(u_tileTextures[0], v_textureCoord + vec2(dx, dy))[0];
+ }
+ if (band == 2.0) {
+ return texture2D(u_tileTextures[0], v_textureCoord + vec2(dx, dy))[1];
+ }
+ if (band == 3.0) {
+ return texture2D(u_tileTextures[0], v_textureCoord + vec2(dx, dy))[2];
+ }
+ if (band == 4.0) {
+ return texture2D(u_tileTextures[0], v_textureCoord + vec2(dx, dy))[3];
+ }
+ }
+
+ void main() {
+ vec4 color = texture2D(u_tileTextures[0], v_textureCoord);
+ color = vec4((getBandValue(4.0, 0.0, 0.0) / 3000.0), (getBandValue(1.0, 0.0, 0.0) / 3000.0), (getBandValue(2.0, 0.0, 0.0) / 3000.0), 1.0);
+ if (color.a == 0.0) {
+ discard;
+ }
+ gl_FragColor = color;
+ gl_FragColor.rgb *= gl_FragColor.a;
+ gl_FragColor *= u_transitionAlpha;
+ }`.replace(/[ \n]+/g, ' ')
+ );
+ });
+
describe('updateStyleVariables()', function () {
it('updates style variables', function (done) {
layer.updateStyleVariables({
diff --git a/test/browser/spec/ol/style/expressions.test.js b/test/browser/spec/ol/style/expressions.test.js
index d2e6980b0c..1afa4c260a 100644
--- a/test/browser/spec/ol/style/expressions.test.js
+++ b/test/browser/spec/ol/style/expressions.test.js
@@ -10,7 +10,7 @@ import {
uniformNameForVariable,
} from '../../../../../src/ol/style/expressions.js';
-describe('ol.style.expressions', function () {
+describe('ol/style/expressions', function () {
describe('numberToGlsl', function () {
it('does a simple transform when a fraction is present', function () {
expect(numberToGlsl(1.3456)).to.eql('1.3456');
@@ -70,6 +70,7 @@ describe('ol.style.expressions', function () {
beforeEach(function () {
context = {
stringLiteralsMap: {},
+ functions: {},
};
});
@@ -207,6 +208,7 @@ describe('ol.style.expressions', function () {
variables: [],
attributes: [],
stringLiteralsMap: {},
+ functions: {},
};
});
@@ -289,9 +291,11 @@ describe('ol.style.expressions', function () {
expect(
expressionToGlsl(context, ['color', ['get', 'attr4'], 1, 2, 0.5])
).to.eql('vec4(a_attr4 / 255.0, 1.0 / 255.0, 2.0 / 255.0, 0.5)');
- expect(expressionToGlsl(context, ['band', 1])).to.eql('color0[0]');
+ expect(expressionToGlsl(context, ['band', 1])).to.eql(
+ 'getBandValue(1.0, 0.0, 0.0)'
+ );
expect(expressionToGlsl(context, ['band', 1, -1, 2])).to.eql(
- 'texture2D(u_tileTexture0, v_textureCoord + vec2(-1.0 / u_texturePixelWidth, 2.0 / u_texturePixelHeight))[0]'
+ 'getBandValue(1.0, -1.0, 2.0)'
);
});
@@ -464,6 +468,7 @@ describe('ol.style.expressions', function () {
variables: [],
attributes: [],
stringLiteralsMap: {},
+ functions: {},
};
});
@@ -608,6 +613,7 @@ describe('ol.style.expressions', function () {
variables: [],
attributes: [],
stringLiteralsMap: {},
+ functions: {},
};
});
@@ -821,6 +827,7 @@ describe('ol.style.expressions', function () {
variables: [],
attributes: [],
stringLiteralsMap: {},
+ functions: {},
};
});
@@ -1067,6 +1074,7 @@ describe('ol.style.expressions', function () {
variables: [],
attributes: [],
stringLiteralsMap: {},
+ functions: {},
};
});