Skip to content

Commit

Permalink
Add Experimental Layer: TerrainLayer (#3984)
Browse files Browse the repository at this point in the history
* Adding experimental TerrainLayer
* Adding experimental terrain-layer example.

Co-authored-by: Xiaoji Chen <Pessimistress@users.noreply.github.com>
  • Loading branch information
chrisgervang and Pessimistress authored Feb 5, 2020
1 parent 93c78f3 commit af5e949
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 1 deletion.
18 changes: 18 additions & 0 deletions examples/experimental/terrain/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>deck.gl Example</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {margin: 0; font-family: sans-serif; width: 100vw; height: 100vh; overflow: hidden; background: #111;}
</style>
</head>
<body>
<div id="app"></div>
</body>
<script type="text/javascript" src="app.js"></script>
<script type="text/javascript">
App.renderToDOM(document.getElementById('app'));
</script>
</html>
25 changes: 25 additions & 0 deletions examples/experimental/terrain/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "terrain",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"start-local": "webpack-dev-server --env.local --progress --hot --open",
"start": "webpack-dev-server --progress --hot --open"
},
"dependencies": {
"@loaders.gl/images": "2.0.2",
"@mapbox/martini": "^0.1.0",
"deck.gl": "^8.1.0-alpha.1",
"react": "^16.3.0",
"react-dom": "^16.3.0",
"react-map-gl": "^5.0.0"
},
"devDependencies": {
"@babel/core": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.0.0",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.1"
}
}
77 changes: 77 additions & 0 deletions examples/experimental/terrain/src/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/* eslint-disable max-statements */
import React, {PureComponent} from 'react';
import {render} from 'react-dom';
import DeckGL from '@deck.gl/react';
import {WebMercatorViewport, COORDINATE_SYSTEM} from '@deck.gl/core';
import {load} from '@loaders.gl/core';
import {TileLayer} from '@deck.gl/geo-layers';

import TerrainLayer from './terrain-layer/terrain-layer';

// Set your mapbox token here
const MAPBOX_TOKEN = process.env.MapboxAccessToken; // eslint-disable-line

const INITIAL_VIEW_STATE = {
latitude: 46.24,
longitude: -122.18,
zoom: 11.5,
bearing: 140,
pitch: 60
};

// Constants
// const STREET = 'https://c.tile.openstreetmap.org';
// const SECTIONAL = 'https://wms.chartbundle.com/tms/1.0.0/sec';
const TERRAIN_RGB = 'https://api.mapbox.com/v4/mapbox.terrain-rgb';
const SATELLITE = 'https://api.mapbox.com/v4/mapbox.satellite';

const getTerrainData = ({x, y, z}) => {
const terrainTile = `${TERRAIN_RGB}/${z}/${x}/${y}.pngraw?access_token=${MAPBOX_TOKEN}`;
// Some tiles over the ocean may not exist
// eslint-disable-next-line handle-callback-err
return load(terrainTile).catch(err => null);
};

const getSurfaceImage = ({x, y, z}) => {
return `${SATELLITE}/${z}/${x}/${y}@2x.png?access_token=${MAPBOX_TOKEN}`;
};

export default class App extends PureComponent {
render() {
const layer = new TileLayer({
id: 'loader',
minZoom: 0,
maxZoom: 23,
maxCacheSize: 100,
getTileData: getTerrainData,
renderSubLayers: props => {
const {bbox, z} = props.tile;

const viewport = new WebMercatorViewport({
longitude: (bbox.west + bbox.east) / 2,
latitude: (bbox.north + bbox.south) / 2,
zoom: z
});
const bottomLeft = viewport.projectFlat([bbox.west, bbox.south]);
const topRight = viewport.projectFlat([bbox.east, bbox.north]);

return new TerrainLayer({
id: props.id,
coordinateSystem: COORDINATE_SYSTEM.CARTESIAN,
bounds: [bottomLeft[0], bottomLeft[1], topRight[0], topRight[1]],
surfaceImage: getSurfaceImage(props.tile),
terrainImage: props.data,
// https://docs.mapbox.com/help/troubleshooting/access-elevation-data/#mapbox-terrain-rgb
// Note - the elevation rendered by this example is greatly exagerated!
getElevation: (r, g, b) => (r * 65536 + g * 256 + b) / 10 - 10000
});
}
});

return <DeckGL initialViewState={INITIAL_VIEW_STATE} controller={true} layers={[layer]} />;
}
}

export function renderToDOM(container) {
render(<App />, container);
}
121 changes: 121 additions & 0 deletions examples/experimental/terrain/src/terrain-layer/terrain-layer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {CompositeLayer} from '@deck.gl/core';
import Martini from '@mapbox/martini';
import {getImageData, getImageSize} from '@loaders.gl/images';
import {SimpleMeshLayer} from '@deck.gl/mesh-layers';

const defaultProps = {
// Image that encodes height data
terrainImage: {type: 'object', value: null, async: true},
// Image to use as texture
surfaceImage: {type: 'object', value: null, async: true},
// Martini error tolerance in meters, smaller number -> more detailed mesh
meshMaxError: {type: 'number', value: 4.0},
// Bounding box of the terrain image, [minX, minY, maxX, maxY] in world coordinates
bounds: {type: 'object', value: [0, 0, 1, 1]},
// Color to use if surfaceImage is unavailable
color: {type: 'color', value: [255, 255, 255]},
// Function to decode height data, from (r, g, b) to height in meters
getElevation: {type: 'accessor', value: (r, g, b) => r}
};

function getTerrain(imageData, tileSize, getElevation) {
const gridSize = tileSize + 1;
// From Martini demo
// https://observablehq.com/@mourner/martin-real-time-rtin-terrain-mesh
const terrain = new Float32Array(gridSize * gridSize);
// decode terrain values
for (let i = 0, y = 0; y < tileSize; y++) {
for (let x = 0; x < tileSize; x++, i++) {
const k = i * 4;
const r = imageData[k + 0];
const g = imageData[k + 1];
const b = imageData[k + 2];
terrain[i + y] = getElevation(r, g, b);
}
}
// backfill bottom border
for (let i = gridSize * (gridSize - 1), x = 0; x < gridSize - 1; x++, i++) {
terrain[i] = terrain[i - gridSize];
}
// backfill right border
for (let i = gridSize - 1, y = 0; y < gridSize; y++, i += gridSize) {
terrain[i] = terrain[i - 1];
}
return terrain;
}

function getMeshAttributes(vertices, terrain, tileSize, bounds) {
const gridSize = tileSize + 1;
const numOfVerticies = vertices.length / 2;
// vec3. x, y in pixels, z in meters
const positions = new Float32Array(numOfVerticies * 3);
// vec2. 1 to 1 relationship with position. represents the uv on the texture image. 0,0 to 1,1.
const texCoords = new Float32Array(numOfVerticies * 2);

const [minX, minY, maxX, maxY] = bounds;
const xScale = (maxX - minX) / tileSize;
const yScale = (maxY - minY) / tileSize;

for (let i = 0; i < numOfVerticies; i++) {
const x = vertices[i * 2];
const y = vertices[i * 2 + 1];
const pixelIdx = y * gridSize + x;

positions[3 * i + 0] = x * xScale + minX;
positions[3 * i + 1] = -y * yScale + maxY;
positions[3 * i + 2] = terrain[pixelIdx];

texCoords[2 * i + 0] = x / tileSize;
texCoords[2 * i + 1] = y / tileSize;
}

return {
positions: {value: positions, size: 3},
texCoords: {value: texCoords, size: 2}
// normals: [], - optional, but creates the high poly look with lighting
};
}

function getMartiniTileMesh(terrainImage, getElevation, meshMaxError, bounds) {
if (terrainImage === null) {
return null;
}
const data = getImageData(terrainImage);
const size = getImageSize(terrainImage);

const tileSize = size.width;
const gridSize = tileSize + 1;

const terrain = getTerrain(data, tileSize, getElevation);

const martini = new Martini(gridSize);
const tile = martini.createTile(terrain);
const {vertices, triangles} = tile.getMesh(meshMaxError);

return {
indices: triangles,
attributes: getMeshAttributes(vertices, terrain, tileSize, bounds)
};
}

export default class TerrainLayer extends CompositeLayer {
renderLayers() {
const {bounds, color, getElevation, meshMaxError, terrainImage, surfaceImage} = this.props;

return new SimpleMeshLayer(
this.getSubLayerProps({
id: 'terrain'
}),
{
data: [1],
mesh: getMartiniTileMesh(terrainImage, getElevation, meshMaxError, bounds),
texture: surfaceImage,
getPosition: d => [0, 0, 0],
getColor: d => color
}
);
}
}

TerrainLayer.layerName = 'TerrainLayer';
TerrainLayer.defaultProps = defaultProps;
39 changes: 39 additions & 0 deletions examples/experimental/terrain/webpack.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// NOTE: To use this example standalone (e.g. outside of deck.gl repo)
// delete the local development overrides at the bottom of this file

// avoid destructuring for older Node version support
const resolve = require('path').resolve;
const webpack = require('webpack');

const CONFIG = {
mode: 'development',

entry: {
app: resolve('./src/app.js')
},

output: {
library: 'App'
},

module: {
rules: [
{
// Transpile ES6 to ES5 with babel
// Remove if your app does not use JSX or you don't need to support old browsers
test: /\.js$/,
loader: 'babel-loader',
exclude: [/node_modules/],
options: {
presets: ['@babel/preset-react']
}
}
]
},

// Optional: Enables reading mapbox token from environment variable
plugins: [new webpack.EnvironmentPlugin(['MapboxAccessToken'])]
};

// This line enables bundling against src in this repo rather than installed module
module.exports = env => (env ? require('../../webpack.config.local')(CONFIG)(env) : CONFIG);
2 changes: 1 addition & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1636,7 +1636,7 @@
dependencies:
"@loaders.gl/core" "2.0.2"

"@luma.gl/constants@8.0.3", "@luma.gl/constants@^8.0.3", "@luma.gl/constants@^8.0.0":
"@luma.gl/constants@8.0.3", "@luma.gl/constants@^8.0.0", "@luma.gl/constants@^8.0.3":
version "8.0.3"
resolved "https://registry.yarnpkg.com/@luma.gl/constants/-/constants-8.0.3.tgz#5418bc121f2d2b477e94df1170702f7563ee1b91"
integrity sha512-Ps89hhaNGyseXDECnSNWbDARP3n1WOttoUnci2BL3O6Od7IIzkuV4HeoI3wsH5Zyyf9/3UBFaFwDnfnfK7E0TA==
Expand Down

0 comments on commit af5e949

Please sign in to comment.