-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Experimental Layer: TerrainLayer (#3984)
* Adding experimental TerrainLayer * Adding experimental terrain-layer example. Co-authored-by: Xiaoji Chen <Pessimistress@users.noreply.github.com>
- Loading branch information
1 parent
93c78f3
commit af5e949
Showing
6 changed files
with
281 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
121
examples/experimental/terrain/src/terrain-layer/terrain-layer.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters