Skip to content

Commit

Permalink
add raster-dem source type and hillshade layer type (#5286)
Browse files Browse the repository at this point in the history
* add raster-dem source type and hillshade layer type

* add hillshade shaders

fix shader

* implement raster-dem source loading

* render hillshading to map

use new output format of browser.getImageData, flow

* flow changes 🌊

abandon flow for raster dem worker

lint n flow

make eslint and flow temporarily happy

* add render tests

add unit tests

update style-spec tests

clean

* use raster clipping masks for raster-dem sources

* use RangeError instead of assertions

* add dem_data tests

dem_data test

dem_data types

* scale slope calculations by pixel latitude

* use Texture and RenderTexture classes

* remove shader debug code

* use separate renderpass to render terrain prepare textures

* flow happy for raster-dem worker sources

* oops

* more raster_dem_tile tests

* dont hardcode tilesize and test dem serialize/deserialize

* remove unnecessary light uniform component, update shader

* tweak zoom-based hillshade exaggeration

* fix debug page for safari/firefox

* use segmented raster mask vertex buffers

* follow new linting rule

* initially populate tile borders with existing data to avoid border flashes as data backfills

* only create one texture/rendertexture per tile

* fix dem test

* save and reuse tile textures AND fix missing tiles 😂

* use highp precision to fix android rendering

* clean up

* 📝 add explanatory comments

* fix test borked in rebase

* fix tile state assignment

* refactor based on review comments

* ensure DEMData Levels are always square, use a single Level instead of an array

* remove hardcoded tilesize

* use dim instead of data dimensions

* update shaders+spec based on nicki's recs

* fix rebase 🤦‍♀️

* use new Color class output of getPaintValue and updated source schema

* check for accurate backfilled data, clarify loop

* use new Property interface, fix style-spec error

* try LAB color space

* use layer-specific lighting for hillshade

* use new WebWorkerTransfer

update tests for WebWorkerTransfer

* clean up and remove unused code

* Add HillshadeLayer benchmark

* use gl state tracking

* address final review comments

* add HillshadeStyleLayer to TypedStyleLayer
  • Loading branch information
mollymerp authored Dec 2, 2017
1 parent 98d8fce commit 722ae7c
Show file tree
Hide file tree
Showing 56 changed files with 1,876 additions and 21 deletions.
21 changes: 21 additions & 0 deletions bench/benchmarks/layers.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,26 @@ class LayerHeatmap extends LayerBenchmark {
}
}

class LayerHillshade extends LayerBenchmark {
constructor() {
super();

this.layerStyle = Object.assign({}, style, {
sources: {
'terrain-rgb': {
'type': 'raster-dem',
'url': 'mapbox://mapbox.terrain-rgb'
}
},
layers: generateLayers({
'id': 'layer',
'type': 'hillshade',
'source': 'terrain-rgb',
})
});
}
}

class LayerLine extends LayerBenchmark {
constructor() {
super();
Expand Down Expand Up @@ -202,6 +222,7 @@ module.exports = [
LayerFill,
LayerFillExtrusion,
LayerHeatmap,
LayerHillshade,
LayerLine,
LayerRaster,
LayerSymbol
Expand Down
77 changes: 77 additions & 0 deletions debug/terrain-rendering.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<!DOCTYPE html>
<html>
<head>
<title>Mapbox GL JS debug page</title>
<meta charset='utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel='stylesheet' href='/dist/mapbox-gl.css' />
<style>
body { margin: 0; padding: 0; }
html, body, #map { height: 100%; }
</style>
</head>

<body>
<div id='map'></div>

<script src='/dist/mapbox-gl-dev.js'></script>
<script src='/debug/access_token_generated.js'></script>
<script>
var hillshadeStyle = {
"version": 8,
"light": {
"position": [1.15, 335, 60]
},
"sources": {
"streets": {
"url": 'mapbox://mapbox.mapbox-streets-v7',
"type": 'vector'
},
"rasterTerrain": {
"type": "raster-dem",
"url": "mapbox://mapbox.terrain-rgb"
}
},
"layers": [
{
"id": "background", "type": "background",
"paint": {
"background-color": "#fff"
}
},
{
"id":"water",
"type": "fill",
"source":"streets",
"source-layer":"water",
"paint": {
"fill-color":"#7bcdff"
}
},
{
"id": "terrain",
"source": "rasterTerrain",
"type": "hillshade"
},
{
"id":"water2",
"type": "line",
"source":"streets",
"source-layer":"water",
"paint": {
"line-color":"#4a99ca",
"line-width": 1
}
},
]
};
var map = window.map = new mapboxgl.Map({
container: 'map',
zoom: 12,
center: [-112.9487, 36.2628],
style: hillshadeStyle,
hash: true
});
</script>
</body>
</html>
25 changes: 24 additions & 1 deletion flow-typed/style-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ declare type VectorSourceSpecification = {
}

declare type RasterSourceSpecification = {
"type": "raster",
"type": "raster" | "raster-dem",
"url"?: string,
"tiles"?: Array<string>,
"bounds"?: [number, number, number, number],
Expand Down Expand Up @@ -345,6 +345,28 @@ declare type RasterLayerSpecification = {|
|}
|}

declare type HillshadeLayerSpecification = {|
"id": string,
"type": "hillshade",
"metadata"?: mixed,
"source": string,
"source-layer"?: string,
"minzoom"?: number,
"maxzoom"?: number,
"filter"?: FilterSpecification,
"layout"?: {|
"visibility"?: "visible" | "none"
|},
"paint"?: {|
"hillshade-illumination-direction"?: PropertyValueSpecification<number>,
"hillshade-illumination-anchor"?: PropertyValueSpecification<"map" | "viewport">,
"hillshade-exaggeration"?: PropertyValueSpecification<number>,
"hillshade-shadow-color"?: PropertyValueSpecification<ColorSpecification>,
"hillshade-highlight-color"?: PropertyValueSpecification<ColorSpecification>,
"hillshade-accent-color"?: PropertyValueSpecification<ColorSpecification>
|}
|}

declare type BackgroundLayerSpecification = {|
"id": string,
"type": "background",
Expand All @@ -369,5 +391,6 @@ declare type LayerSpecification =
| HeatmapLayerSpecification
| FillExtrusionLayerSpecification
| RasterLayerSpecification
| HillshadeLayerSpecification
| BackgroundLayerSpecification;

155 changes: 155 additions & 0 deletions src/data/dem_data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// @flow
const {RGBAImage} = require('../util/image');
const util = require('../util/util');
const {register} = require('../util/web_worker_transfer');

export type SerializedDEMData = {
uid: string,
scale: number,
dim: number,
level: ArrayBuffer
};

class Level {
dim: number;
border: number;
stride: number;
data: Int32Array;

constructor(dim: number, border: number, data: ?Int32Array) {
if (dim <= 0) throw new RangeError('Level must have positive dimension');
this.dim = dim;
this.border = border;
this.stride = this.dim + 2 * this.border;
this.data = data || new Int32Array((this.dim + 2 * this.border) * (this.dim + 2 * this.border));
}

set(x: number, y: number, value: number) {
this.data[this._idx(x, y)] = value + 65536;
}

get(x: number, y: number) {
return this.data[this._idx(x, y)] - 65536;
}

_idx(x: number, y: number) {
if (x < -this.border || x >= this.dim + this.border || y < -this.border || y >= this.dim + this.border) throw new RangeError('out of range source coordinates for DEM data');
return (y + this.border) * this.stride + (x + this.border);
}
}

register(Level);

// DEMData is a data structure for decoding, backfilling, and storing elevation data for processing in the hillshade shaders
// data can be populated either from a pngraw image tile or from serliazed data sent back from a worker. When data is initially
// loaded from a image tile, we decode the pixel values using the mapbox terrain-rgb tileset decoding formula, but we store the
// elevation data in a Level as an Int32 value. we add 65536 (2^16) to eliminate negative values and enable the use of
// integer overflow when creating the texture used in the hillshadePrepare step.

// DEMData also handles the backfilling of data from a tile's neighboring tiles. This is necessary because we use a pixel's 8
// surrounding pixel values to compute the slope at that pixel, and we cannot accurately calculate the slope at pixels on a
// tile's edge without backfilling from neighboring tiles.

class DEMData {
uid: string;
scale: number;
level: Level;
loaded: boolean;

constructor(uid: string, scale: ?number, data: ?Level) {
this.uid = uid;
this.scale = scale || 1;
// if no data is provided, use a temporary empty level to satisfy flow
this.level = data || new Level(256, 512);
this.loaded = !!data;
}

loadFromImage(data: RGBAImage) {
if (data.height !== data.width) throw new RangeError('DEM tiles must be square');

// Build level 0
const level = this.level = new Level(data.width, data.width / 2);
const pixels = data.data;

// unpack
for (let y = 0; y < level.dim; y++) {
for (let x = 0; x < level.dim; x++) {
const i = y * level.dim + x;
const j = i * 4;
// decoding per https://blog.mapbox.com/global-elevation-data-6689f1d0ba65
level.set(x, y, this.scale * ((pixels[j] * 256 * 256 + pixels[j + 1] * 256.0 + pixels[j + 2]) / 10.0 - 10000.0));
}
}

// 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 < level.dim; x++) {
// left vertical border
level.set(-1, x, level.get(0, x));
// right vertical border
level.set(level.dim, x, level.get(level.dim - 1, x));
// left horizontal border
level.set(x, -1, level.get(x, 0));
// right horizontal border
level.set(x, level.dim, level.get(x, level.dim - 1));
}
// corners
level.set(-1, -1, level.get(0, 0));
level.set(level.dim, -1, level.get(level.dim - 1, 0));
level.set(-1, level.dim, level.get(0, level.dim - 1));
level.set(level.dim, level.dim, level.get(level.dim - 1, level.dim - 1));
this.loaded = true;
}

getPixels() {
return RGBAImage.create({width: this.level.dim + 2 * this.level.border, height: this.level.dim + 2 * this.level.border}, new Uint8Array(this.level.data.buffer));
}

backfillBorder(borderTile: DEMData, dx: number, dy: number) {
const t = this.level;
const o = borderTile.level;

if (t.dim !== o.dim) throw new Error('level mismatch (dem dimension)');

let _xMin = dx * t.dim,
_xMax = dx * t.dim + t.dim,
_yMin = dy * t.dim,
_yMax = dy * t.dim + t.dim;

switch (dx) {
case -1:
_xMin = _xMax - 1;
break;
case 1:
_xMax = _xMin + 1;
break;
}

switch (dy) {
case -1:
_yMin = _yMax - 1;
break;
case 1:
_yMax = _yMin + 1;
break;
}

const xMin = util.clamp(_xMin, -t.border, t.dim + t.border);
const xMax = util.clamp(_xMax, -t.border, t.dim + t.border);
const yMin = util.clamp(_yMin, -t.border, t.dim + t.border);
const yMax = util.clamp(_yMax, -t.border, t.dim + t.border);

const ox = -dx * t.dim;
const oy = -dy * t.dim;
for (let y = yMin; y < yMax; y++) {
for (let x = xMin; x < xMax; x++) {
t.set(x, y, o.get(x + ox, y + oy));
}
}
}
}

register(DEMData);
module.exports = {DEMData, Level};

Loading

0 comments on commit 722ae7c

Please sign in to comment.