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: {}, }; });