From 5afd25150f49929f2b85263c1ffb42fec50424d6 Mon Sep 17 00:00:00 2001 From: Duck Date: Wed, 15 Sep 2021 09:17:04 -0700 Subject: [PATCH 1/6] Add additional extension requirements for floating point textures. --- doc/errors/index.md | 2 +- src/ol/webgl/Helper.js | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/errors/index.md b/doc/errors/index.md index 21a033cd23..37f59aa553 100644 --- a/doc/errors/index.md +++ b/doc/errors/index.md @@ -235,7 +235,7 @@ A `WebGLArrayBuffer` must either be of type `ELEMENT_ARRAY_BUFFER` or `ARRAY_BUF ### 63 -Support for the `OES_element_index_uint` WebGL extension is mandatory for WebGL layers. +Support for the `OES_element_index_uint`, `OES_texture_float`, and `OES_texture_float_linear` WebGL extensions are mandatory for WebGL layers. ### 64 diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index 25e21de657..fc580eb086 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -279,7 +279,12 @@ class WebGLHelper extends Disposable { this.currentProgram_ = null; assert(includes(getSupportedExtensions(), 'OES_element_index_uint'), 63); + assert(includes(getSupportedExtensions(), 'OES_texture_float'), 63); + assert(includes(getSupportedExtensions(), 'OES_texture_float_linear'), 63); + gl.getExtension('OES_element_index_uint'); + gl.getExtension('OES_texture_float'); + gl.getExtension("OES_texture_float_linear"); this.canvas_.addEventListener( ContextEventType.LOST, From 17394cc8be06bf36f2ebd33ce6946697ce087fe1 Mon Sep 17 00:00:00 2001 From: Duck Date: Wed, 15 Sep 2021 09:18:48 -0700 Subject: [PATCH 2/6] Ensure the texture is loaded into the correct index. The previous code would try to load subsequent textures into the 0th slot. --- src/ol/renderer/webgl/TileLayer.js | 5 ++++- src/ol/webgl/Helper.js | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/ol/renderer/webgl/TileLayer.js b/src/ol/renderer/webgl/TileLayer.js index 0540d0a69f..3565df7117 100644 --- a/src/ol/renderer/webgl/TileLayer.js +++ b/src/ol/renderer/webgl/TileLayer.js @@ -426,7 +426,10 @@ class WebGLTileLayerRenderer extends WebGLLayerRenderer { const uniformName = Uniforms.TILE_TEXTURE_PREFIX + textureIndex; gl.activeTexture(gl[textureProperty]); gl.bindTexture(gl.TEXTURE_2D, tileTexture.textures[textureIndex]); - gl.uniform1i(this.helper.getUniformLocation(uniformName), 0); + gl.uniform1i( + this.helper.getUniformLocation(uniformName), + textureIndex + ); } const alpha = diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index fc580eb086..9fdb5e0573 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -284,7 +284,7 @@ class WebGLHelper extends Disposable { gl.getExtension('OES_element_index_uint'); gl.getExtension('OES_texture_float'); - gl.getExtension("OES_texture_float_linear"); + gl.getExtension('OES_texture_float_linear'); this.canvas_.addEventListener( ContextEventType.LOST, From f2472b780128c529071f06ca611468d26261376c Mon Sep 17 00:00:00 2001 From: Duck Date: Wed, 15 Sep 2021 09:22:18 -0700 Subject: [PATCH 3/6] Allow DataTile source to include more than four bands. --- src/ol/source/DataTile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ol/source/DataTile.js b/src/ol/source/DataTile.js index cc1acc26aa..7f8e570b3c 100644 --- a/src/ol/source/DataTile.js +++ b/src/ol/source/DataTile.js @@ -26,6 +26,7 @@ import {getUid} from '../util.js'; * @property {number} [tilePixelRatio] Tile pixel ratio. * @property {boolean} [wrapX=true] Render tiles beyond the antimeridian. * @property {number} [transition] Transition time when fading in new tiles (in miliseconds). + * @property {number} [bandCount=4] Number of bands represented in the data. */ /** @@ -81,7 +82,7 @@ class DataTileSource extends TileSource { /** * @type {number} */ - this.bandCount = 4; // assume RGBA + this.bandCount = options.bandCount === undefined ? 4 : options.bandCount; // assume RGBA if undefined } /** From 05615df1a37f7d60c022886e312c9b221d3bd809 Mon Sep 17 00:00:00 2001 From: Duck Date: Wed, 15 Sep 2021 09:34:41 -0700 Subject: [PATCH 4/6] Allow TileTexture to select float or int based textures. --- src/ol/DataTile.js | 2 +- src/ol/webgl/TileTexture.js | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/ol/DataTile.js b/src/ol/DataTile.js index c63eeb5b49..8b2972b173 100644 --- a/src/ol/DataTile.js +++ b/src/ol/DataTile.js @@ -6,7 +6,7 @@ import TileState from './TileState.js'; /** * Data that can be used with a DataTile. - * @typedef {Uint8Array|Uint8ClampedArray|DataView} Data + * @typedef {Uint8Array|Uint8ClampedArray|Float32Array|DataView} Data */ /** diff --git a/src/ol/webgl/TileTexture.js b/src/ol/webgl/TileTexture.js index 794c646464..27d6589cdc 100644 --- a/src/ol/webgl/TileTexture.js +++ b/src/ol/webgl/TileTexture.js @@ -15,6 +15,7 @@ function bindAndConfigure(gl, 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.LINEAR); + gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); } /** @@ -61,6 +62,8 @@ function uploadDataTexture(gl, texture, data, size, bandCount) { } } + const texType = data instanceof Float32Array ? gl.FLOAT : gl.UNSIGNED_BYTE; + gl.texImage2D( gl.TEXTURE_2D, 0, @@ -69,7 +72,7 @@ function uploadDataTexture(gl, texture, data, size, bandCount) { size[1], 0, format, - gl.UNSIGNED_BYTE, + texType, data ); } @@ -149,8 +152,12 @@ class TileTexture extends EventTarget { } const data = tile.getData(); + const isFloat = data instanceof Float32Array; const pixelCount = this.size[0] * this.size[1]; - this.bandCount = data.byteLength / pixelCount; + // Float arrays feature four bytes per element, + // BYTES_PER_ELEMENT throws a TypeScript exception but would handle + // this better for more varied typed arrays. + this.bandCount = data.byteLength / (isFloat ? 4 : 1) / pixelCount; const textureCount = Math.ceil(this.bandCount / 4); if (textureCount === 1) { @@ -160,6 +167,8 @@ class TileTexture extends EventTarget { return; } + const DataType = isFloat ? Float32Array : Uint8Array; + const textureDataArrays = new Array(textureCount); for (let textureIndex = 0; textureIndex < textureCount; ++textureIndex) { const texture = gl.createTexture(); @@ -167,7 +176,7 @@ class TileTexture extends EventTarget { const bandCount = textureIndex < textureCount - 1 ? 4 : this.bandCount % 4; - textureDataArrays[textureIndex] = new Uint8Array(pixelCount * bandCount); + textureDataArrays[textureIndex] = new DataType(pixelCount * bandCount); } const valueCount = pixelCount * this.bandCount; From 3fc8217254dd3e66874cf9f08895c3657daf8572 Mon Sep 17 00:00:00 2001 From: Duck Date: Wed, 15 Sep 2021 15:08:02 -0700 Subject: [PATCH 5/6] Add example featuring numpytiles --- examples/numpytile.html | 20 ++++++++ examples/numpytile.js | 105 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 examples/numpytile.html create mode 100644 examples/numpytile.js diff --git a/examples/numpytile.html b/examples/numpytile.html new file mode 100644 index 0000000000..4869d7d849 --- /dev/null +++ b/examples/numpytile.html @@ -0,0 +1,20 @@ +--- +layout: example.html +title: Rendering 16-bit NumpyTiles +shortdesc: Renders a multi-byte depth source image directly using webGL. +docs: > + Use the NumpyTile format to render multibyte raster data. +tags: "numpytiles" +--- +
+ +
+
Adjust colors
+
    +
  • Min:
  • +
  • Max:
  • +
+ +
+ + diff --git a/examples/numpytile.js b/examples/numpytile.js new file mode 100644 index 0000000000..92110bb78b --- /dev/null +++ b/examples/numpytile.js @@ -0,0 +1,105 @@ +import Map from '../src/ol/Map.js'; +import TileLayer from '../src/ol/layer/WebGLTile.js'; +import View from '../src/ol/View.js'; + +import DataTileSource from '../src/ol/source/DataTile.js'; +import OsmSource from '../src/ol/source/OSM.js'; +import {fromLonLat} from '../src/ol/proj.js'; + +// 16-bit COG +// Which will be served as NumpyTiles. +const COG = + 'https://storage.googleapis.com/open-cogs/stac-examples/20201211_223832_CS2_analytic.tif'; + +function numpyTileLoader(z, x, y) { + const url = `https://api.cogeo.xyz/cog/tiles/WebMercatorQuad/${z}/${x}/${y}@1x?format=npy&url=${encodeURIComponent( + COG + )}`; + + return fetch(url) + .then((r) => r.arrayBuffer()) + .then((buffer) => NumpyLoader.fromArrayBuffer(buffer)) // eslint-disable-line no-undef + .then((numpyData) => { + // flatten the numpy data + const dataTile = new Float32Array(256 * 256 * 5); + const bandSize = 256 * 256; + for (let x = 0; x < 256; x++) { + for (let y = 0; y < 256; y++) { + const px = x + y * 256; + dataTile[px * 5 + 0] = numpyData.data[y * 256 + x]; + dataTile[px * 5 + 1] = numpyData.data[bandSize + y * 256 + x]; + dataTile[px * 5 + 2] = numpyData.data[bandSize * 2 + y * 256 + x]; + dataTile[px * 5 + 3] = numpyData.data[bandSize * 3 + y * 256 + x]; + dataTile[px * 5 + 4] = + numpyData.data[bandSize * 4 + y * 256 + x] > 0 ? 1.0 : 0; + } + } + return dataTile; + }); +} + +const interpolateBand = (bandIdx) => [ + 'interpolate', + ['linear'], + ['band', bandIdx], + ['var', 'bMin'], + 0, + ['var', 'bMax'], + 1.0, +]; + +const createNumpyStyle = (bandMin, bandMax) => ({ + color: [ + 'array', + interpolateBand(3), + interpolateBand(2), + interpolateBand(1), + ['band', 5], + ], + variables: { + 'bMin': bandMin, + 'bMax': bandMax, + }, +}); + +const numpyLayer = new TileLayer({ + style: createNumpyStyle(3000, 18000), + source: new DataTileSource({ + loader: numpyTileLoader, + bandCount: 5, + }), +}); + +const map = new Map({ + target: 'map', + layers: [ + new TileLayer({ + source: new OsmSource(), + }), + numpyLayer, + ], + view: new View({ + center: fromLonLat([172.933, 1.3567]), + zoom: 15, + }), +}); + +const configureInputs = () => { + const colorFloor = document.getElementById('color-floor'); + const colorCeil = document.getElementById('color-ceil'); + + colorFloor.addEventListener('input', (evt) => { + numpyLayer.updateStyleVariables({ + 'bMin': parseFloat(evt.target.value), + 'bMax': parseFloat(colorCeil.value), + }); + }); + + colorCeil.addEventListener('input', (evt) => { + numpyLayer.updateStyleVariables({ + 'bMin': parseFloat(colorFloor.value), + 'bMax': parseFloat(evt.target.value), + }); + }); +}; +configureInputs(); From 0783a8211f407e797cb30e85c5bd8c9e09490f6f Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Thu, 16 Sep 2021 09:44:43 -0600 Subject: [PATCH 6/6] Adjustments to NumpyTiles example --- examples/numpytile.css | 3 ++ examples/numpytile.html | 20 ++++++----- examples/numpytile.js | 75 +++++++++++++++++++++-------------------- 3 files changed, 52 insertions(+), 46 deletions(-) create mode 100644 examples/numpytile.css diff --git a/examples/numpytile.css b/examples/numpytile.css new file mode 100644 index 0000000000..a1b691a4c2 --- /dev/null +++ b/examples/numpytile.css @@ -0,0 +1,3 @@ +input[type="range"] { + vertical-align: text-bottom; +} diff --git a/examples/numpytile.html b/examples/numpytile.html index 4869d7d849..964649c04a 100644 --- a/examples/numpytile.html +++ b/examples/numpytile.html @@ -1,20 +1,22 @@ --- layout: example.html title: Rendering 16-bit NumpyTiles -shortdesc: Renders a multi-byte depth source image directly using webGL. +shortdesc: Renders a multi-byte depth source image directly using WebGL. docs: > - Use the NumpyTile format to render multibyte raster data. -tags: "numpytiles" + This example uses a ol/source/DataTile source to load multi-byte raster data in the + NumpyTile format. + The source is rendered with a ol/layer/WebGLTile layer. Adjusting the sliders above + performs a contrast stretch by adjusting the style variables set on the layer. +tags: "numpytiles, webgl" +resources: + - https://unpkg.com/@planet/ol-numpytiles@2.0.2/umd/NumpyLoader.js ---
-
Adjust colors
+
Contrast stretch
    -
  • Min:
  • -
  • Max:
  • +
  • Min
  • +
  • Max
-
- - diff --git a/examples/numpytile.js b/examples/numpytile.js index 92110bb78b..78df71634d 100644 --- a/examples/numpytile.js +++ b/examples/numpytile.js @@ -3,7 +3,6 @@ import TileLayer from '../src/ol/layer/WebGLTile.js'; import View from '../src/ol/View.js'; import DataTileSource from '../src/ol/source/DataTile.js'; -import OsmSource from '../src/ol/source/OSM.js'; import {fromLonLat} from '../src/ol/proj.js'; // 16-bit COG @@ -45,25 +44,26 @@ const interpolateBand = (bandIdx) => [ ['var', 'bMin'], 0, ['var', 'bMax'], - 1.0, + 1, ]; -const createNumpyStyle = (bandMin, bandMax) => ({ - color: [ - 'array', - interpolateBand(3), - interpolateBand(2), - interpolateBand(1), - ['band', 5], - ], - variables: { - 'bMin': bandMin, - 'bMax': bandMax, - }, -}); +const initialMin = 3000; +const initialMax = 18000; const numpyLayer = new TileLayer({ - style: createNumpyStyle(3000, 18000), + style: { + color: [ + 'array', + interpolateBand(3), + interpolateBand(2), + interpolateBand(1), + ['band', 5], + ], + variables: { + 'bMin': initialMin, + 'bMax': initialMax, + }, + }, source: new DataTileSource({ loader: numpyTileLoader, bandCount: 5, @@ -72,34 +72,35 @@ const numpyLayer = new TileLayer({ const map = new Map({ target: 'map', - layers: [ - new TileLayer({ - source: new OsmSource(), - }), - numpyLayer, - ], + layers: [numpyLayer], view: new View({ center: fromLonLat([172.933, 1.3567]), zoom: 15, }), }); -const configureInputs = () => { - const colorFloor = document.getElementById('color-floor'); - const colorCeil = document.getElementById('color-ceil'); +const inputMin = document.getElementById('input-min'); +const inputMax = document.getElementById('input-max'); +const outputMin = document.getElementById('output-min'); +const outputMax = document.getElementById('output-max'); - colorFloor.addEventListener('input', (evt) => { - numpyLayer.updateStyleVariables({ - 'bMin': parseFloat(evt.target.value), - 'bMax': parseFloat(colorCeil.value), - }); +inputMin.addEventListener('input', (evt) => { + numpyLayer.updateStyleVariables({ + 'bMin': parseFloat(evt.target.value), + 'bMax': parseFloat(inputMax.value), }); + outputMin.innerText = evt.target.value; +}); - colorCeil.addEventListener('input', (evt) => { - numpyLayer.updateStyleVariables({ - 'bMin': parseFloat(colorFloor.value), - 'bMax': parseFloat(evt.target.value), - }); +inputMax.addEventListener('input', (evt) => { + numpyLayer.updateStyleVariables({ + 'bMin': parseFloat(inputMin.value), + 'bMax': parseFloat(evt.target.value), }); -}; -configureInputs(); + outputMax.innerText = evt.target.value; +}); + +inputMin.value = initialMin; +inputMax.value = initialMax; +outputMin.innerText = initialMin; +outputMax.innerText = initialMax;