Support expressions for band arguments

This commit is contained in:
Tim Schaub
2021-12-03 17:23:55 -07:00
parent 4ed1226411
commit f5803ad6ca
9 changed files with 254 additions and 44 deletions

6
examples/cog-stretch.css Normal file
View File

@@ -0,0 +1,6 @@
.controls {
display: grid;
grid-template-columns: auto auto 1fr;
align-items: baseline;
gap: 0 1em;
}

44
examples/cog-stretch.html Normal file
View File

@@ -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"
---
<div id="map" class="map"></div>
<div class="controls">
<label for="red">Red channel</label>
<select id="red">
<option value="1" selected>visible red</option>
<option value="2">visible green</option>
<option value="3">visible blue</option>
<option value="4">near infrared</option>
</select>
<label>max
<input type="range" id="redMax" value="3000" min="2000" max="5000">
</label>
<label for="green">Green channel</label>
<select id="green">
<option value="1">visible red</option>
<option value="2" selected>visible green</option>
<option value="3">visible blue</option>
<option value="4">near infrared</option>
</select>
<label>max
<input type="range" id="greenMax" value="3000" min="2000" max="5000">
</label>
<label for="blue">Blue channel</label>
<select id="blue">
<option value="1">visible red</option>
<option value="2">visible green</option>
<option value="3" selected>visible blue</option>
<option value="4">near infrared</option>
</select>
<label>max
<input type="range" id="blueMax" value="3000" min="2000" max="5000">
</label>
</div>

62
examples/cog-stretch.js Normal file
View File

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

View File

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

View File

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

View File

@@ -184,6 +184,7 @@ export function isTypeUnique(valueType) {
* @property {Array<string>} variables List of variables used in the expression; contains **unprefixed names**
* @property {Array<string>} attributes List of attributes used in the expression; contains **unprefixed names**
* @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 {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})`;
},
};

View File

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

View File

@@ -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({

View File

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