diff --git a/src/data/dem_data.js b/src/data/dem_data.js index 59aee0ad0dd..d5ddf2b4c31 100644 --- a/src/data/dem_data.js +++ b/src/data/dem_data.js @@ -16,56 +16,53 @@ import { register } from '../util/web_worker_transfer'; export default class DEMData { uid: string; - data: Int32Array; + data: Uint32Array; stride: number; dim: number; + encoding: "mapbox" | "terrarium"; + // RGBAImage data has uniform 1px padding on all sides: square tile edge size defines stride + // and dim is calculated as stride - 2. constructor(uid: string, data: RGBAImage, encoding: "mapbox" | "terrarium") { this.uid = uid; if (data.height !== data.width) throw new RangeError('DEM tiles must be square'); if (encoding && encoding !== "mapbox" && encoding !== "terrarium") return warnOnce( `"${encoding}" is not a valid encoding type. Valid types include "mapbox" and "terrarium".` ); - const dim = this.dim = data.height; - this.stride = this.dim + 2; - this.data = new Int32Array(this.stride * this.stride); - - const pixels = data.data; - const unpack = encoding === "terrarium" ? this._unpackTerrarium : this._unpackMapbox; - for (let y = 0; y < dim; y++) { - for (let x = 0; x < dim; x++) { - const i = y * dim + x; - const j = i * 4; - this.set(x, y, unpack(pixels[j], pixels[j + 1], pixels[j + 2])); - } - } + this.stride = data.height; + const dim = this.dim = data.height - 2; + this.data = new Uint32Array(data.data.buffer); + this.encoding = encoding || 'mapbox'; // in order to avoid flashing seams between tiles, here we are initially populating a 1px border of pixels around the image // with the data of the nearest pixel from the image. this data is eventually replaced when the tile's neighboring // tiles are loaded and the accurate data can be backfilled using DEMData#backfillBorder for (let x = 0; x < dim; x++) { // left vertical border - this.set(-1, x, this.get(0, x)); + this.data[this._idx(-1, x)] = this.data[this._idx(0, x)]; // right vertical border - this.set(dim, x, this.get(dim - 1, x)); + this.data[this._idx(dim, x)] = this.data[this._idx(dim - 1, x)]; // left horizontal border - this.set(x, -1, this.get(x, 0)); + this.data[this._idx(x, -1)] = this.data[this._idx(x, 0)]; // right horizontal border - this.set(x, dim, this.get(x, dim - 1)); + this.data[this._idx(x, dim)] = this.data[this._idx(x, dim - 1)]; } // corners - this.set(-1, -1, this.get(0, 0)); - this.set(dim, -1, this.get(dim - 1, 0)); - this.set(-1, dim, this.get(0, dim - 1)); - this.set(dim, dim, this.get(dim - 1, dim - 1)); + this.data[this._idx(-1, -1)] = this.data[this._idx(0, 0)]; + this.data[this._idx(dim, -1)] = this.data[this._idx(dim - 1, 0)]; + this.data[this._idx(-1, dim)] = this.data[this._idx(0, dim - 1)]; + this.data[this._idx(dim, dim)] = this.data[this._idx(dim - 1, dim - 1)]; } - set(x: number, y: number, value: number) { - this.data[this._idx(x, y)] = value + 65536; + get(x: number, y: number) { + const pixels = new Uint8Array(this.data.buffer); + const index = this._idx(x, y) * 4; + const unpack = this.encoding === "terrarium" ? this._unpackTerrarium : this._unpackMapbox; + return unpack(pixels[index], pixels[index + 1], pixels[index + 2]); } - get(x: number, y: number) { - return this.data[this._idx(x, y)] - 65536; + getUnpackVector() { + return this.encoding === "terrarium" ? [256.0, 1.0, 1.0 / 256.0, 32768.0] : [6553.6, 25.6, 0.1, 10000.0]; } _idx(x: number, y: number) { @@ -119,7 +116,7 @@ export default class DEMData { const oy = -dy * this.dim; for (let y = yMin; y < yMax; y++) { for (let x = xMin; x < xMax; x++) { - this.set(x, y, borderTile.get(x + ox, y + oy)); + this.data[this._idx(x, y)] = borderTile.data[this._idx(x + ox, y + oy)]; } } } diff --git a/src/render/draw_hillshade.js b/src/render/draw_hillshade.js index 81426847fc1..4d6dc647582 100644 --- a/src/render/draw_hillshade.js +++ b/src/render/draw_hillshade.js @@ -12,6 +12,7 @@ import { import type Painter from './painter'; import type SourceCache from '../source/source_cache'; import type HillshadeStyleLayer from '../style/style_layer/hillshade_style_layer'; +import type DEMData from '../data/dem_data'; import type {OverscaledTileID} from '../source/tile_id'; export default drawHillshade; @@ -68,16 +69,6 @@ function renderHillshade(painter, tile, layer, depthMode, stencilMode, colorMode function prepareHillshade(painter, tile, layer, sourceMaxZoom, depthMode, stencilMode, colorMode) { const context = painter.context; const gl = context.gl; - // decode rgba levels by using integer overflow to convert each Uint32Array element -> 4 Uint8Array elements. - // ex. - // Uint32: - // base 10 - 67308 - // base 2 - 0000 0000 0000 0001 0000 0110 1110 1100 - // - // Uint8: - // base 10 - 0, 1, 6, 236 (this order is reversed in the resulting array via the overflow. - // first 8 bits represent 236, so the r component of the texture pixel will be 236 etc.) - // base 2 - 0000 0000, 0000 0001, 0000 0110, 1110 1100 if (tile.dem && tile.dem.data) { const tileSize = tile.dem.dim; const textureStride = tile.dem.stride; @@ -85,9 +76,6 @@ function prepareHillshade(painter, tile, layer, sourceMaxZoom, depthMode, stenci const pixelData = tile.dem.getPixels(); context.activeTexture.set(gl.TEXTURE1); - // if UNPACK_PREMULTIPLY_ALPHA_WEBGL is set to true prior to drawHillshade being called - // tiles will appear blank, because as you can see above the alpha value for these textures - // is always 0 context.pixelStoreUnpackPremultiplyAlpha.set(false); tile.demTexture = tile.demTexture || painter.getTileTexture(textureStride); if (tile.demTexture) { @@ -116,7 +104,7 @@ function prepareHillshade(painter, tile, layer, sourceMaxZoom, depthMode, stenci painter.useProgram('hillshadePrepare').draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled, - hillshadeUniformPrepareValues(tile, sourceMaxZoom), + hillshadeUniformPrepareValues(((tile: any): {dem: DEMData, tileID: OverscaledTileID}), sourceMaxZoom), layer.id, painter.rasterBoundsBuffer, painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments); diff --git a/src/render/program/hillshade_program.js b/src/render/program/hillshade_program.js index 72223704b11..c4c42cb32ca 100644 --- a/src/render/program/hillshade_program.js +++ b/src/render/program/hillshade_program.js @@ -1,6 +1,5 @@ // @flow -import assert from 'assert'; import { mat4 } from 'gl-matrix'; import { @@ -8,7 +7,8 @@ import { Uniform1f, Uniform2f, UniformColor, - UniformMatrix4f + UniformMatrix4f, + Uniform4f } from '../uniform_binding'; import EXTENT from '../../data/extent'; import MercatorCoordinate from '../../geo/mercator_coordinate'; @@ -36,7 +36,8 @@ export type HillshadePrepareUniformsType = {| 'u_image': Uniform1i, 'u_dimension': Uniform2f, 'u_zoom': Uniform1f, - 'u_maxzoom': Uniform1f + 'u_maxzoom': Uniform1f, + 'u_unpack': Uniform4f |}; const hillshadeUniforms = (context: Context, locations: UniformLocations): HillshadeUniformsType => ({ @@ -54,7 +55,8 @@ const hillshadePrepareUniforms = (context: Context, locations: UniformLocations) 'u_image': new Uniform1i(context, locations.u_image), 'u_dimension': new Uniform2f(context, locations.u_dimension), 'u_zoom': new Uniform1f(context, locations.u_zoom), - 'u_maxzoom': new Uniform1f(context, locations.u_maxzoom) + 'u_maxzoom': new Uniform1f(context, locations.u_maxzoom), + 'u_unpack': new Uniform4f(context, locations.u_unpack) }); const hillshadeUniformValues = ( @@ -84,10 +86,10 @@ const hillshadeUniformValues = ( }; const hillshadeUniformPrepareValues = ( - tile: {dem: ?DEMData, tileID: OverscaledTileID}, maxzoom: number + tile: {dem: DEMData, tileID: OverscaledTileID}, maxzoom: number ): UniformValues => { - assert(tile.dem); - const stride = ((tile.dem: any): DEMData).stride; + + const stride = tile.dem.stride; const matrix = mat4.create(); // Flip rendering at y axis. mat4.ortho(matrix, 0, EXTENT, -EXTENT, 0, 0, 1); @@ -98,7 +100,8 @@ const hillshadeUniformPrepareValues = ( 'u_image': 1, 'u_dimension': [stride, stride], 'u_zoom': tile.tileID.overscaledZ, - 'u_maxzoom': maxzoom + 'u_maxzoom': maxzoom, + 'u_unpack': tile.dem.getUnpackVector() }; }; diff --git a/src/shaders/hillshade_prepare.fragment.glsl b/src/shaders/hillshade_prepare.fragment.glsl index 9c9866c34f5..27de56d6532 100644 --- a/src/shaders/hillshade_prepare.fragment.glsl +++ b/src/shaders/hillshade_prepare.fragment.glsl @@ -7,11 +7,13 @@ varying vec2 v_pos; uniform vec2 u_dimension; uniform float u_zoom; uniform float u_maxzoom; +uniform vec4 u_unpack; float getElevation(vec2 coord, float bias) { // Convert encoded elevation value to meters vec4 data = texture2D(u_image, coord) * 255.0; - return (data.r + data.g * 256.0 + data.b * 256.0 * 256.0) / 4.0; + data.a = -1.0; + return dot(data, u_unpack) / 4.0; } void main() { diff --git a/src/source/raster_dem_tile_source.js b/src/source/raster_dem_tile_source.js index 1937a103d6e..57e2ff61ebf 100644 --- a/src/source/raster_dem_tile_source.js +++ b/src/source/raster_dem_tile_source.js @@ -54,8 +54,7 @@ class RasterDEMTileSource extends RasterTileSource implements Source { if (this.map._refreshExpiredTiles) tile.setExpiryData(img); delete (img: any).cacheControl; delete (img: any).expires; - - const rawImageData = browser.getImageData(img); + const rawImageData = browser.getImageData(img, 1); const params = { uid: tile.uid, coord: tile.tileID, diff --git a/src/util/browser.js b/src/util/browser.js index 6192bc6fada..773d7a3e08f 100755 --- a/src/util/browser.js +++ b/src/util/browser.js @@ -36,7 +36,7 @@ const exported = { return { cancel: () => cancel(frame) }; }, - getImageData(img: CanvasImageSource): ImageData { + getImageData(img: CanvasImageSource, padding?: number = 0): ImageData { const canvas = window.document.createElement('canvas'); const context = canvas.getContext('2d'); if (!context) { @@ -45,7 +45,7 @@ const exported = { canvas.width = img.width; canvas.height = img.height; context.drawImage(img, 0, 0, img.width, img.height); - return context.getImageData(0, 0, img.width, img.height); + return context.getImageData(-padding, -padding, img.width + 2 * padding, img.height + 2 * padding); }, resolveURL(path: string) { diff --git a/test/ajax_stubs.js b/test/ajax_stubs.js index 70d61fa1169..fa235faa86d 100644 --- a/test/ajax_stubs.js +++ b/test/ajax_stubs.js @@ -94,8 +94,15 @@ export const getImage = function({ url }, callback) { }); }; -browser.getImageData = function({width, height, data}) { - return {width, height, data: new Uint8Array(data)}; +browser.getImageData = function({width, height, data}, padding = 0) { + const source = new Uint8Array(data); + const dest = new Uint8Array((2 * padding + width) * (2 * padding + height) * 4); + + const offset = (2 * padding + width) * padding + padding; + for (let i = 0; i < height; i++) { + dest.set(source.slice(i * width * 4, (i + 1) * width * 4), 4 * (offset + (width + 2 * padding) * i)); + } + return {width: width + 2 * padding, height: height + 2 * padding, data: dest}; }; // Hack: since node doesn't have any good video codec modules, just grab a png with diff --git a/test/unit/data/dem_data.test.js b/test/unit/data/dem_data.test.js index 41d85d65393..5b412183b7d 100644 --- a/test/unit/data/dem_data.test.js +++ b/test/unit/data/dem_data.test.js @@ -4,6 +4,9 @@ import { RGBAImage } from '../../../src/util/image'; import { serialize, deserialize } from '../../../src/util/web_worker_transfer'; function createMockImage(height, width) { + // RGBAImage passed to constructor has uniform 1px padding on all sides. + height += 2; + width += 2; const pixels = new Uint8Array(height * width * 4); for (let i = 0; i < pixels.length; i++) { pixels[i] = (i + 1) % 4 === 0 ? 1 : Math.floor(Math.random() * 256); @@ -15,9 +18,8 @@ test('DEMData', (t) => { t.test('constructor', (t) => { const dem = new DEMData(0, {width: 4, height: 4, data: new Uint8ClampedArray(4 * 4 * 4)}); t.equal(dem.uid, 0); - t.equal(dem.dim, 4); - t.equal(dem.stride, 6); - t.true(dem.data instanceof Int32Array); + t.equal(dem.dim, 2); + t.equal(dem.stride, 4); t.end(); }); @@ -139,6 +141,7 @@ test('DEMData#backfillBorder', (t) => { dim: 4, stride: 6, data: dem0.data, + encoding: 'mapbox' }, 'serializes DEM'); const transferrables = [];