├── .gitignore ├── .jshintrc ├── bin └── npm-postinstall ├── .babelrc ├── .editorconfig ├── index.html ├── LICENSE.md ├── lib ├── shaders │ ├── elevationGradientVert.glsl │ └── elevationGradientFrag.glsl ├── renderContourLabels.js ├── ElevationGradientImageryProvider.js └── TileRenderer.js ├── README.md ├── package.json ├── webpack.config.js └── app.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true, 3 | "unused": true, 4 | "predef": [ "require", "console", "module", "alert" ] 5 | } -------------------------------------------------------------------------------- /bin/npm-postinstall: -------------------------------------------------------------------------------- 1 | var exec = require('child_process').exec; 2 | 3 | exec('mkdir -p dist && cp -r node_modules/cesium/Build/Cesium/* dist/.'); 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "glslify", 4 | "transform-object-rest-spread", 5 | "transform-class-properties" 6 | ], 7 | "presets": ["es2015", "airbnb"] 8 | } 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | cesium-elevation-gradient 6 | 7 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2019 Big Silver 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /lib/shaders/elevationGradientVert.glsl: -------------------------------------------------------------------------------- 1 | attribute vec2 a_position; 2 | attribute vec2 a_texCoord; 3 | 4 | uniform vec2 u_resolution; 5 | 6 | varying vec2 v_texCoord; 7 | 8 | void main() { 9 | // convert the rectangle from pixels to 0.0 to 1.0 10 | vec2 zeroToOne = a_position / u_resolution; 11 | 12 | // convert from 0->1 to 0->2 13 | vec2 zeroToTwo = zeroToOne * 2.0; 14 | 15 | // convert from 0->2 to -1->+1 (clipspace) 16 | vec2 clipSpace = zeroToTwo - 1.0; 17 | 18 | gl_Position = vec4(clipSpace * vec2(1, -1), 0, 1); 19 | 20 | // pass the texCoord to the fragment shader 21 | // The GPU will interpolate this value between points. 22 | v_texCoord = a_texCoord; 23 | } 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cesium-gradient 2 | ========================= 3 | 4 | 5 | An elevation visualiser for [Cesium](https://cesiumjs.org/) acting as an imagery provider. Elevation samples from a terrain provider are passed to a 2D WebGL renderer. The renderer then applies a combination of the following algorithms: 6 | 7 | * Colour ramp 8 | * Hillshade 9 | * Contour lines 10 | 11 | 12 | Run the test app with a local server 13 | ------------------------------------ 14 | ``` 15 | npm install 16 | npm start 17 | ``` 18 | Then browse to [http://localhost:8080](http://localhost:8080) 19 | 20 | Using in your app 21 | ----------------- 22 | This code uses GLSL shaders. It is currently set up to load them using [shader-loader for webpack](https://github.com/makio64/shader-loader). If you happen to be using webpack on your project then you should be able to... 23 | 24 | * install shader-loader: 25 | ``` 26 | npm install shader-loader --save-dev 27 | ``` 28 | * set it up in your webpack.config.js: 29 | ``` 30 | module: { 31 | loaders: [{ 32 | test: /\.(glsl|vs|fs)$/, 33 | loaders: ['shader'] 34 | }] 35 | } 36 | ``` 37 | * import (or require()) into your app 38 | ``` 39 | import ElevationGradientImageryProvider from '/lib/ElevationGradientImageryProvider' 40 | ``` 41 | 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cesium-gradient", 3 | "version": "1.0.0", 4 | "description": "Elevation gradient plugin for Cesium", 5 | "main": "lib/ElevationGradientImageryProvider.js", 6 | "scripts": { 7 | "deploy": "webpack -p && gh-pages -d dist", 8 | "start": "webpack-dev-server --config webpack.config.js", 9 | "postinstall": "node bin/npm-postinstall" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/big-silver/cesium-gradient.git" 14 | }, 15 | "author": "Big Silver", 16 | "license": "Apache-2.0", 17 | "bugs": { 18 | "url": "https://github.com/big-silver/cesium-gradient/issues" 19 | }, 20 | "homepage": "https://github.com/big-silver/cesium-gradient#readme", 21 | "devDependencies": { 22 | "babel-core": "^6.24.1", 23 | "babel-loader": "^7.0.0", 24 | "babel-plugin-glslify": "^1.0.2", 25 | "babel-plugin-transform-class-properties": "^6.24.1", 26 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 27 | "babel-preset-airbnb": "^2.4.0", 28 | "babel-preset-es2015": "^6.24.1", 29 | "babel-preset-es2015-webpack": "^6.4.3", 30 | "cesium": "1.43.0", 31 | "copy-webpack-plugin": "^4.0.1", 32 | "css-loader": "^0.28.7", 33 | "file-loader": "^0.11.1", 34 | "gh-pages": "^0.11.0", 35 | "html-webpack-plugin": "^2.30.1", 36 | "shader-loader": "^1.1.4", 37 | "strip-pragma-loader": "^1.0.0", 38 | "style-loader": "^0.18.2", 39 | "uglifyjs-webpack-plugin": "^1.0.0-beta.3", 40 | "url-loader": "^0.6.2", 41 | "webpack": "^3.5.6", 42 | "webpack-dev-server": "^2.9.1" 43 | }, 44 | "peerDependencies": { 45 | "cesium": ">=1.22" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const webpack = require('webpack'); 4 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | 7 | // The path to the cesium source code 8 | const cesiumSource = 'node_modules/cesium'; 9 | const cesiumWorkers = '../cesium/Build/Cesium/Workers'; 10 | 11 | module.exports = [{ 12 | context: __dirname, 13 | entry: { 14 | app: './app.js' 15 | }, 16 | output: { 17 | filename: '[name].js', 18 | path: path.resolve(__dirname, 'dist'), 19 | 20 | // Needed by Cesium for multiline strings 21 | sourcePrefix: '' 22 | }, 23 | amd: { 24 | // Enable webpack-friendly use of require in cesium 25 | toUrlUndefined: true 26 | }, 27 | node: { 28 | // Resolve node module use of fs 29 | fs: "empty" 30 | }, 31 | resolve: { 32 | alias: { 33 | // Cesium module name 34 | cesium: path.resolve(__dirname, cesiumSource) 35 | } 36 | }, 37 | module: { 38 | rules: [{ 39 | test: /\.jsx?$/, 40 | use: { 41 | loader: 'babel-loader' 42 | }, 43 | exclude: /node_modules/, 44 | include: __dirname 45 | },{ 46 | test: /\.css$/, 47 | use: ['style-loader', 'css-loader'] 48 | }, { 49 | test: /\.(png|gif|jpg|jpeg|svg|xml|json)$/, 50 | use: ['url-loader'] 51 | }, { 52 | test: /\.(glsl|vs|fs)$/, 53 | use: { 54 | loader: 'shader-loader' 55 | } 56 | }] 57 | }, 58 | plugins: [ 59 | new HtmlWebpackPlugin({ 60 | template: 'index.html' 61 | }), 62 | // Copy Cesium Assets, Widgets, and Workers to a static directory 63 | new CopyWebpackPlugin([{from: path.join(cesiumSource, '../cesium/Build/Cesium/Workers'), to: 'Workers'}]), 64 | new CopyWebpackPlugin([{from: path.join(cesiumSource, '../cesium/Source/Assets'), to: 'Assets'}]), 65 | new CopyWebpackPlugin([{from: path.join(cesiumSource, '../cesium/Source/Widgets'), to: 'Widgets'}]), 66 | new webpack.DefinePlugin({ 67 | // Define relative base path in cesium for loading assets 68 | CESIUM_BASE_URL: JSON.stringify('') 69 | }), 70 | // Split cesium into a seperate bundle 71 | new webpack.optimize.CommonsChunkPlugin({ 72 | name: 'cesium', 73 | minChunks: function (module) { 74 | return module.context && module.context.indexOf('cesium') !== -1; 75 | } 76 | }) 77 | ], 78 | 79 | // development server options 80 | devServer: { 81 | contentBase: path.join(__dirname, "dist") 82 | } 83 | }]; -------------------------------------------------------------------------------- /lib/renderContourLabels.js: -------------------------------------------------------------------------------- 1 | import { min, max, sortBy } from 'lodash' 2 | 3 | const approxMedian = values => sortBy(values)[Math.floor(values.length / 2)] 4 | 5 | const OUTLINE_COLOR = 'rgba(0,0,0,0.5)' 6 | const TEXT_COLOR = 'white' 7 | const ANGLE_OFFSET = Math.PI * 0.5 8 | const DEBUG_MODE = false 9 | const BORDER = 0.25 10 | const DELTA_SIZE = 0.2 11 | 12 | const indexToGridLocation = (index, gridSize) => ( 13 | { 14 | x: index % gridSize, 15 | y: Math.floor(index / gridSize), 16 | } 17 | ) 18 | 19 | const gridLocationToIndex = (gridLocation, gridSize) => gridLocation.x + gridLocation.y * gridSize 20 | 21 | const calculateGradientAngle = (values, gridSize, gridLocation) => { 22 | const valueAtDelta = (dx, dy) => { 23 | const deltaGridLocation = { 24 | x: gridLocation.x + dx, 25 | y: gridLocation.y + dy, 26 | } 27 | return values[gridLocationToIndex(deltaGridLocation, gridSize)] 28 | } 29 | 30 | const deltaSize = Math.floor(gridSize * DELTA_SIZE) 31 | 32 | const dx = valueAtDelta(deltaSize, 0) - valueAtDelta(-deltaSize, 0) 33 | const dy = valueAtDelta(0, deltaSize) - valueAtDelta(0, -deltaSize) 34 | 35 | return Math.atan2(dy, dx) + ANGLE_OFFSET 36 | } 37 | 38 | export default function renderContourLabels({ 39 | canvas, 40 | values, 41 | maskSamples, 42 | majorContour, 43 | minorContour, 44 | fontSize, 45 | formatLabel, 46 | shouldRenderContourLabel, 47 | textColor, 48 | textOutlineColor, 49 | }) { 50 | const { width } = canvas 51 | 52 | const median = approxMedian(values) 53 | const gridSize = Math.sqrt(values.length) 54 | 55 | const candidateContour = Math.round(approxMedian(values) / majorContour) * majorContour 56 | 57 | const isNearGridCentre = ({ x, y }) => (x > gridSize * BORDER) && 58 | (x < gridSize * (1 - BORDER)) && 59 | (y > gridSize * BORDER) && 60 | (y < gridSize * (1 - BORDER)) 61 | 62 | const candidateValues = values.map((value, i) => ({ value, index: i })).filter((valueWithIndex) => { 63 | const gridLocation = indexToGridLocation(valueWithIndex.index, gridSize) 64 | return isNearGridCentre(gridLocation) 65 | }) 66 | 67 | const valuesOnly = candidateValues.map(valueWithIndex => valueWithIndex.value) 68 | 69 | const minValue = min(valuesOnly) 70 | const maxValue = max(valuesOnly) 71 | 72 | if (candidateContour < minValue || candidateContour > maxValue || !shouldRenderContourLabel(candidateContour)) { return canvas } 73 | 74 | const bestValueWithIndex = sortBy(candidateValues, valueWithIndex => Math.abs(valueWithIndex.value - candidateContour))[0] 75 | 76 | if (Math.abs(bestValueWithIndex.value - candidateContour) > minorContour * 0.5) { return canvas } 77 | 78 | const gridLocation = indexToGridLocation(bestValueWithIndex.index, gridSize) 79 | 80 | const x = gridLocation.x * width / gridSize 81 | const y = gridLocation.y * width / gridSize 82 | 83 | const maskSize = Math.sqrt(maskSamples.length) 84 | const maskGridLocation = { 85 | x: Math.round(gridLocation.x * maskSize / gridSize), 86 | y: Math.round(gridLocation.y * maskSize / gridSize), 87 | } 88 | const maskValue = maskSamples[gridLocationToIndex(maskGridLocation, maskSize)] 89 | if (maskValue < 0.9) { return canvas } 90 | 91 | const gradientAngle = calculateGradientAngle(values, gridSize, gridLocation) 92 | 93 | const context = canvas.getContext('2d') 94 | 95 | if (DEBUG_MODE) { 96 | context.strokeStyle = textOutlineColor.toCssColorString() 97 | context.lineWidth = 1 98 | context.strokeRect(1, 1, width - 1, width - 1) 99 | } 100 | 101 | const label = formatLabel(candidateContour) 102 | context.font = `bold ${fontSize}px Arial` 103 | context.textAlign = 'center' 104 | context.textBaseline = 'middle' 105 | 106 | context.translate(x, y) 107 | context.rotate(gradientAngle) 108 | 109 | context.lineWidth = 3 110 | context.strokeStyle = textOutlineColor.toCssColorString() 111 | context.strokeText(label, 0, 0) 112 | 113 | context.fillStyle = textColor.toCssColorString() 114 | context.fillText(label, 0, 0) 115 | 116 | if (DEBUG_MODE) { 117 | context.beginPath() 118 | context.arc(0, 0, 3, 0, 2 * Math.PI, false) 119 | context.fillStyle = 'red' 120 | context.fill() 121 | } 122 | 123 | return canvas 124 | } 125 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | import 'cesium/Source/Widgets/widgets.css'; 2 | 3 | import Cesium from 'cesium/Source/Cesium' 4 | import ElevationGradient from './lib/ElevationGradientImageryProvider' 5 | const { 6 | Cartesian3, 7 | CesiumMath, 8 | CesiumTerrainProvider, 9 | Ellipsoid, 10 | Matrix4, 11 | Rectangle, 12 | ScreenSpaceEventHandler, 13 | ScreenSpaceEventType, 14 | Viewer, 15 | buildModuleUrl, 16 | sampleTerrainMostDetailed, 17 | } = Cesium 18 | 19 | const { WGS84 } = Ellipsoid 20 | 21 | buildModuleUrl.setBaseUrl('./'); 22 | 23 | const viewer = new Viewer('cesiumContainer'); 24 | 25 | const setUpTerrain = (viewer) => { 26 | const cesiumTerrainProviderMeshes = new CesiumTerrainProvider({ 27 | url: 'https://assets.agi.com/stk-terrain/world', 28 | requestWaterMask: false, 29 | requestVertexNormals: true 30 | }); 31 | 32 | viewer.terrainProvider = cesiumTerrainProviderMeshes; 33 | } 34 | 35 | const setUpElevationGradient = (viewer) => { 36 | const terrainProvider = viewer.terrainProvider; 37 | const scene = viewer.scene; 38 | 39 | const valueSampler = (positions, level) => ( 40 | sampleTerrainMostDetailed(terrainProvider, positions).then( 41 | (sampledPositions) => sampledPositions.map(position => position.height) 42 | ) 43 | ) 44 | formatContourLabel: value => `${value.toFixed(2)} m` 45 | 46 | const imageryLayer = scene.imageryLayers.addImageryProvider(new ElevationGradient({ 47 | valueSampler, 48 | readyPromise: terrainProvider.readyPromise, 49 | majorContour: 25, 50 | minorContour: 5, 51 | gradient: [ 52 | { 53 | "color": { 54 | "red": 0, 55 | "green": 0, 56 | "blue": 0, 57 | "alpha": 0 58 | }, 59 | "value": 600 60 | }, 61 | { 62 | "color": { 63 | "red": 0, 64 | "green": 0, 65 | "blue": 1, 66 | "alpha": 0.5 67 | }, 68 | "value": 600 69 | }, 70 | { 71 | "color": { 72 | "red": 1, 73 | "green": 0, 74 | "blue": 0, 75 | "alpha": 0.5 76 | }, 77 | "value": 1000 78 | }, 79 | { 80 | "color": { 81 | "red": 0, 82 | "green": 0, 83 | "blue": 0, 84 | "alpha": 0 85 | }, 86 | "value": 1000 87 | } 88 | ] 89 | })); 90 | 91 | // You can control overall layer opacity here... 92 | imageryLayer.alpha = 1.0; 93 | } 94 | 95 | const initCameraLocation = (viewer) => { 96 | const target = Cartesian3.fromDegrees(130.7359, -25.2990); 97 | const offset = new Cartesian3(1500, 1500, 3000); 98 | viewer.camera.lookAt(target, offset); 99 | viewer.camera.lookAtTransform(Matrix4.IDENTITY); 100 | } 101 | 102 | const setUpMouseInfo = (viewer) => { 103 | const scene = viewer.scene; 104 | const globe = scene.globe; 105 | 106 | const entity = viewer.entities.add({ 107 | label: { 108 | font: '14px sans-serif', 109 | show: false 110 | } 111 | }); 112 | 113 | const handler = new ScreenSpaceEventHandler(scene.canvas); 114 | handler.setInputAction(movement => { 115 | 116 | const ray = viewer.camera.getPickRay(movement.endPosition); 117 | 118 | const cartesian = globe.pick(ray, scene); 119 | if (cartesian) { 120 | const cartographic = WGS84.cartesianToCartographic(cartesian); 121 | const longitudeString = CesiumMath.toDegrees(cartographic.longitude).toFixed(4); 122 | const latitudeString = CesiumMath.toDegrees(cartographic.latitude).toFixed(4); 123 | const heightString = cartographic.height.toFixed(2); 124 | 125 | entity.position = cartesian; 126 | entity.label.show = true; 127 | entity.label.text = `(${longitudeString}, ${latitudeString}, ${heightString})`; 128 | } else { 129 | entity.label.show = false; 130 | } 131 | }, ScreenSpaceEventType.MOUSE_MOVE); 132 | } 133 | 134 | setUpTerrain(viewer); 135 | setUpElevationGradient(viewer); 136 | initCameraLocation(viewer); 137 | setUpMouseInfo(viewer); 138 | -------------------------------------------------------------------------------- /lib/shaders/elevationGradientFrag.glsl: -------------------------------------------------------------------------------- 1 | precision mediump float; 2 | 3 | // our texture 4 | uniform sampler2D u_image; 5 | uniform sampler2D u_mask; 6 | uniform vec2 u_textureSize; 7 | uniform vec2 u_tileDimension; 8 | uniform float u_zFactor; 9 | uniform float u_zenith; 10 | uniform float u_azimuth; 11 | uniform float u_majorContour; 12 | uniform float u_minorContour; 13 | uniform float u_hillshadeAmount; 14 | uniform float u_gradientAmount; 15 | uniform float u_contourAmount; 16 | uniform float u_useSlope; 17 | uniform vec4 u_contourColor; 18 | 19 | // external GRADIENT_STOP_COUNT 20 | 21 | uniform vec4 u_gradientColors[GRADIENT_STOP_COUNT]; 22 | uniform float u_gradientValues[GRADIENT_STOP_COUNT]; 23 | 24 | varying vec2 v_texCoord; 25 | 26 | uniform vec2 u_tileElevationRange; 27 | 28 | #define M_PI 3.1415926535897932384626433832795 29 | #define CONTOUR_MAJOR_OPACITY 1.0 30 | #define CONTOUR_MINOR_OPACITY 0.3 31 | 32 | vec3 light = vec3(255., 231., 177.) / vec3(255.); 33 | vec3 shade = vec3(3., 152., 255.) / vec3(255.); 34 | 35 | vec2 cellsize = u_tileDimension / u_textureSize; 36 | 37 | float colourToElevation(vec4 col){ 38 | float range = u_tileElevationRange.y - u_tileElevationRange.x; 39 | return mix(u_tileElevationRange.x, u_tileElevationRange.y, col.r) + range * col.g / 255.; 40 | } 41 | 42 | float getElevation(vec2 coord){ 43 | vec4 col = texture2D(u_image, coord); 44 | return colourToElevation(col); 45 | } 46 | 47 | float calcSlope(float a, float b, float c, float d, float e, float f, float g, float h, float i) { 48 | float dzdx = ((c + 2.0 * f + i) - (a + 2.0 * d + g)) / (8.0 * cellsize.x); 49 | float dzdy = ((g + 2.0 * h + i) - (a + 2.0 * b + c)) / (8.0 * cellsize.y); 50 | return atan(sqrt(dzdx * dzdx + dzdy * dzdy)) * 180. / M_PI; 51 | } 52 | 53 | float calcHillshade(float a, float b, float c, float d, float e, float f, float g, float h, float i){ 54 | // http://edndoc.esri.com/arcobjects/9.2/net/shared/geoprocessing/spatial_analyst_tools/how_hillshade_works.htm 55 | 56 | float dzdx = ((c + 2.0 * f + i) - (a + 2.0 * d + g)) / (8.0 * cellsize.x); 57 | float dzdy = ((g + 2.0 * h + i) - (a + 2.0 * b + c)) / (8.0 * cellsize.y); 58 | float slope = atan(u_zFactor * sqrt(dzdx * dzdx + dzdy * dzdy)); 59 | 60 | float aspect = atan(dzdy, -dzdx); 61 | 62 | if(aspect < 0.0){ 63 | aspect = aspect + 2.0 * M_PI; 64 | } 65 | 66 | float hillshade = ((cos(u_zenith) * cos(slope)) + (sin(u_zenith) * sin(slope) * cos(u_azimuth - aspect))); 67 | return clamp(hillshade, 0., 1.); 68 | } 69 | 70 | float linstep(float edge0, float edge1, float x){ 71 | return clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); 72 | } 73 | 74 | float detectEdge(float x, float a, float b){ 75 | return float(x > min(a,b) && x < max(a,b)); 76 | } 77 | 78 | float calcDistance(float x, float a, float b, float c, float d, float e){ 79 | float s0 = (c - b) * 0.5; 80 | float s1 = (e - d) * 0.5; 81 | float s = (abs(s0) + abs(s1)) * 0.5; 82 | return abs((x-a) / s); 83 | } 84 | 85 | float calcContour(float minor, float major, float a, float b, float c, float d, float e, float f, float g, float h, float i){ 86 | 87 | float x = floor(e * (1.0 / minor) + 0.5) * minor; // nearest contour 88 | 89 | float isMajor = float(mod(x, major) < 0.01); 90 | float isMinor = (1.0 - isMajor); 91 | 92 | // a b c 93 | // d e f 94 | // g h i 95 | 96 | float dist = calcDistance(x, e, d, f, b, h); 97 | float result = linstep(2.0, 0.5, dist); 98 | 99 | result *= CONTOUR_MAJOR_OPACITY * isMajor + CONTOUR_MINOR_OPACITY * isMinor; 100 | 101 | return clamp(result, 0., 1.); 102 | } 103 | 104 | vec3 applyTint(float hillshade) { 105 | return mix(shade, light, hillshade) * hillshade * 1.2; 106 | } 107 | 108 | vec3 applyGamma(vec3 col){ 109 | return clamp(pow(col, vec3(0.8)), vec3(0.), vec3(1.)); 110 | } 111 | 112 | // Useful for debugging 113 | float rand(vec2 co){ 114 | return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453); 115 | } 116 | 117 | vec4 calcGradientColour(float e){ 118 | if(e <= u_gradientValues[0]){ 119 | return u_gradientColors[0]; 120 | } 121 | 122 | for(int i = 1; i < GRADIENT_STOP_COUNT; ++i){ 123 | if(e <= u_gradientValues[i]){ 124 | float a = (e - u_gradientValues[i-1]) / (u_gradientValues[i] - u_gradientValues[i-1]); 125 | return mix(u_gradientColors[i-1], u_gradientColors[i], a); 126 | } 127 | } 128 | 129 | return u_gradientColors[GRADIENT_STOP_COUNT-1]; 130 | } 131 | 132 | void main() { 133 | vec2 onePixel = vec2(1.0, 1.0) / u_textureSize; 134 | 135 | vec4 maskColour = texture2D(u_mask, v_texCoord); 136 | float maskValue = maskColour.a; 137 | 138 | float a = getElevation(v_texCoord + onePixel * vec2(-1.0, -1.0)); 139 | float b = getElevation(v_texCoord + onePixel * vec2( 0.0, -1.0)); 140 | float c = getElevation(v_texCoord + onePixel * vec2( 1.0, -1.0)); 141 | float d = getElevation(v_texCoord + onePixel * vec2(-1.0, 0.0)); 142 | float e = getElevation(v_texCoord + onePixel * vec2( 0.0, 0.0)); 143 | float f = getElevation(v_texCoord + onePixel * vec2( 1.0, 0.0)); 144 | float g = getElevation(v_texCoord + onePixel * vec2(-1.0, 1.0)); 145 | float h = getElevation(v_texCoord + onePixel * vec2( 0.0, 1.0)); 146 | float i = getElevation(v_texCoord + onePixel * vec2( 1.0, 1.0)); 147 | 148 | float hillshade = mix(1., calcHillshade(a, b, c, d, e, f, g, h, i), u_hillshadeAmount); 149 | 150 | vec3 colourHillshade = applyTint(hillshade); 151 | 152 | float slope = calcSlope(a, b, c, d, e, f, g, h, i); 153 | 154 | vec4 gradientColor = calcGradientColour(u_useSlope > 0.5 ? slope : e); 155 | float contourAmount = gradientColor.a > CONTOUR_OPACITY_THRESHOLD ? u_contourAmount : 0.; 156 | 157 | float contour = contourAmount * calcContour(u_minorContour, u_majorContour, a, b, c, d, e, f, g, h, i); 158 | 159 | vec4 litColour = gradientColor * vec4(colourHillshade, 1.0) * u_gradientAmount; 160 | 161 | vec4 unmaskedColour = mix(litColour, u_contourColor, contour); 162 | vec4 clampedColour = clamp(unmaskedColour, vec4(0.0), vec4(unmaskedColour.a)); 163 | 164 | gl_FragColor = mix(vec4(0.0, 0.0, 0.0, 0.0), clampedColour, maskValue); 165 | } 166 | -------------------------------------------------------------------------------- /lib/ElevationGradientImageryProvider.js: -------------------------------------------------------------------------------- 1 | import Cartographic from 'cesium/Source/Core/Cartographic' 2 | import Color from 'cesium/Source/Core/Color' 3 | import Credit from 'cesium/Source/Core/Credit' 4 | import EllipsoidGeodesic from 'cesium/Source/Core/EllipsoidGeodesic' 5 | import Event from 'cesium/Source/Core/Event' // jshint ignore:line 6 | import WebMercatorTilingScheme from 'cesium/Source/Core/WebMercatorTilingScheme' 7 | import Rectangle from 'cesium/Source/Core/Rectangle' 8 | import when from 'cesium/Source/ThirdParty/when' 9 | import TileRenderer from './TileRenderer' 10 | import renderContourLabels from './renderContourLabels' 11 | 12 | const DEFAULT_TILE_SIZE = 256 13 | const DEFAULT_TERRAIN_TILE_SIZE = 65 14 | const MINIMUM_TILE_LEVEL = 13 15 | const MAJOR_CONTOUR = 10 16 | const MINOR_CONTOUR = 1 17 | const CREDIT = '© Propeller Aerobotics' 18 | const FONT_SIZE = 16 19 | const CONTOUR_OPACITY_THRESHOLD = 0.05 20 | 21 | const mix = (x, y, a) => (x * (1.0 - a) + y * a) 22 | 23 | const mixColors = (x, y, a) => new Color( 24 | mix(x.red, y.red, a), 25 | mix(x.green, y.green, a), 26 | mix(x.blue, y.blue, a), 27 | mix(x.alpha, y.alpha, a), 28 | ) 29 | 30 | function calcGradientColour(gradientStops, z) { 31 | if (z <= gradientStops[0].value) { 32 | return gradientStops[0].color 33 | } 34 | 35 | for (let i = 1; i < gradientStops.length; ++i) { 36 | if (z <= gradientStops[i].value) { 37 | const a = (z - gradientStops[i - 1].value) / (gradientStops[i].value - gradientStops[i - 1].value) 38 | return mixColors(gradientStops[i - 1].color, gradientStops[i].color, a) 39 | } 40 | } 41 | 42 | return gradientStops[gradientStops.length - 1].color 43 | } 44 | 45 | class ElevationGradientImageryProvider { 46 | constructor({ 47 | valueSampler, 48 | maskSampler, 49 | tilingScheme, 50 | ellipsoid, 51 | providerCache, 52 | contourColor = Color.WHITE, 53 | textOutlineColor = Color.BLACK.withAlpha(0.5), 54 | tileSize = DEFAULT_TILE_SIZE, 55 | gridSize = DEFAULT_TERRAIN_TILE_SIZE, 56 | minimumTileLevel = MINIMUM_TILE_LEVEL, 57 | contourAmount = 1, 58 | gradientAmount = 1, 59 | majorContour = MAJOR_CONTOUR, 60 | minorContour = MINOR_CONTOUR, 61 | credit = CREDIT, 62 | extent, 63 | gradient, 64 | fontSize = FONT_SIZE, 65 | hillshadeAmount = 1, 66 | formatContourLabel = value => `${value} m`, 67 | useSlope = 0, 68 | readyPromise = when.resolve(), 69 | linearUnitFactor = 1, 70 | }) { 71 | this.valueSampler = valueSampler 72 | this.maskSampler = maskSampler 73 | this.cache = providerCache 74 | 75 | this.tilingScheme = tilingScheme || new WebMercatorTilingScheme({ ellipsoid }) 76 | this.contourColor = contourColor 77 | this.textOutlineColor = textOutlineColor 78 | this.errorEvent = new Event() 79 | 80 | // Render resolution 81 | this.tileSize = tileSize 82 | this.maskSize = tileSize 83 | this.gridSize = gridSize 84 | 85 | this.fontSize = fontSize 86 | 87 | this.minimumTileLevel = minimumTileLevel 88 | 89 | this.gradientAmount = gradientAmount 90 | this.contourAmount = contourAmount 91 | this.majorContour = majorContour 92 | this.minorContour = minorContour 93 | this.formatContourLabel = formatContourLabel 94 | this.gradientStops = gradient 95 | this.useSlope = useSlope 96 | this.linearUnitFactor = linearUnitFactor 97 | 98 | this.credit = typeof (credit === 'string') ? new Credit(credit) : credit 99 | 100 | this.tileRenderer = new TileRenderer({ 101 | width: this.tileSize, 102 | height: this.tileSize, 103 | gradientStops: gradient, 104 | gradientAmount, 105 | hillshadeAmount, 106 | contourAmount, 107 | majorContour, 108 | minorContour, 109 | contourOpacityThreshold: CONTOUR_OPACITY_THRESHOLD, 110 | useSlope, 111 | contourColor, 112 | }) 113 | 114 | this.blankCanvasPromise = when.resolve(makeBlankCanvas(this.tileSize)) 115 | 116 | this.readyPromise = readyPromise 117 | this.extent = extent 118 | this._ready = false 119 | this.readyPromise.then(() => { 120 | this._ready = true 121 | }) 122 | } 123 | 124 | getTileCredits() { 125 | return this.credit 126 | } 127 | 128 | requestImage = (x, y, tileLevel) => { 129 | const { gridSize, maskSize, minorContour, majorContour, fontSize, formatContourLabel, contourAmount, useSlope, linearUnitFactor } = this 130 | 131 | const getGradientAlpha = z => calcGradientColour(this.gradientStops, z).alpha 132 | const shouldRenderContourLabel = (z) => { 133 | if (useSlope) { return true } 134 | const alpha = getGradientAlpha(z) 135 | return alpha > CONTOUR_OPACITY_THRESHOLD 136 | } 137 | 138 | if (tileLevel < this.minimumTileLevel) { 139 | return this.blankCanvasPromise 140 | } 141 | 142 | const rectangle = this.tilingScheme.tileXYToRectangle(x, y, tileLevel) 143 | 144 | if (this.extent && !Rectangle.intersection(rectangle, this.extent)) { 145 | return this.blankCanvasPromise 146 | } 147 | 148 | const tileDimension = getRectangleGeodesicSize(rectangle, linearUnitFactor) 149 | 150 | const handleRequest = ([maskSamples, valueSamples]) => { 151 | const canvas = this.tileRenderer.render( 152 | valueSamples, 153 | maskSamples, 154 | gridSize, 155 | maskSize, 156 | tileDimension, 157 | ) 158 | 159 | return contourAmount > 0.01 ? renderContourLabels({ 160 | canvas, 161 | values: valueSamples, 162 | maskSamples, 163 | majorContour, 164 | minorContour, 165 | fontSize, 166 | formatLabel: formatContourLabel, 167 | shouldRenderContourLabel, 168 | textColor: this.contourColor, 169 | textOutlineColor: this.textOutlineColor, 170 | }) : canvas 171 | } 172 | 173 | const cacheKey = `${x}:${y}:${tileLevel}` 174 | 175 | const cacheRequest = (result) => { 176 | if (this.cache) { 177 | this.cache.set(cacheKey, result) 178 | } 179 | return result 180 | } 181 | 182 | const response = this.cache && this.cache.has(cacheKey) ? this.cache.get(cacheKey) : null 183 | 184 | if (response) { 185 | return when.resolve(handleRequest(response)) 186 | } 187 | 188 | const valueSampleLocations = rectangleToCartographicGrid(rectangle, gridSize, gridSize) 189 | const valuePromise = when(this.valueSampler(valueSampleLocations, tileLevel)) 190 | 191 | let maskPromise 192 | if (this.maskSampler) { 193 | const maskSampleLocations = rectangleToCartographicGrid(rectangle, maskSize, maskSize) 194 | maskPromise = when(this.maskSampler(maskSampleLocations, tileLevel)) 195 | } else { 196 | maskPromise = when.resolve(Array(maskSize * maskSize).fill(1)) 197 | } 198 | 199 | return when.all([maskPromise, valuePromise]) 200 | .then(cacheRequest) 201 | .then(handleRequest) 202 | .otherwise(() => this.blankCanvasPromise) 203 | } 204 | 205 | pickFeatures() { 206 | return undefined 207 | } 208 | 209 | get tileWidth() { 210 | return this.tileSize 211 | } 212 | 213 | get tileHeight() { 214 | return this.tileSize 215 | } 216 | 217 | get maximumLevel() { 218 | return undefined 219 | } 220 | 221 | get minimumLevel() { 222 | return undefined 223 | } 224 | 225 | get rectangle() { 226 | return this.tilingScheme.rectangle 227 | } 228 | 229 | get tileDiscardPolicy() { 230 | return undefined 231 | } 232 | 233 | get ready() { 234 | return this._ready 235 | } 236 | 237 | get hasAlphaChannel() { 238 | return true 239 | } 240 | } 241 | 242 | const rectangleToCartographicGrid = (rectangle, divisionsX, divisionsY) => { 243 | const n = divisionsX * divisionsY 244 | const result = new Array(n) 245 | for (let i = 0; i < n; ++i) { 246 | const x = i % divisionsX 247 | const y = Math.floor(i / divisionsX) 248 | 249 | const nx = x / (divisionsX - 1) 250 | const ny = 1.0 - y / (divisionsY - 1) 251 | 252 | const longitude = (1.0 - nx) * rectangle.west + nx * rectangle.east 253 | const latitude = (1.0 - ny) * rectangle.south + ny * rectangle.north 254 | 255 | result[i] = new Cartographic(longitude, latitude) 256 | } 257 | return result 258 | } 259 | 260 | const getRectangleGeodesicSize = (r, linearUnitFactor) => { 261 | const northEast = Rectangle.northeast(r) 262 | const northWest = Rectangle.northwest(r) 263 | const southWest = Rectangle.southwest(r) 264 | 265 | const widthGeodesic = new EllipsoidGeodesic(northWest, northEast) 266 | const heightGeodesic = new EllipsoidGeodesic(southWest, northWest) 267 | 268 | return { 269 | x: widthGeodesic.surfaceDistance / linearUnitFactor, 270 | y: heightGeodesic.surfaceDistance / linearUnitFactor, 271 | } 272 | } 273 | 274 | const heightsFromTileGeo = (tileGeo) => { 275 | const heightOffset = tileGeo._structure.heightOffset 276 | const heightScale = tileGeo._structure.heightScale 277 | 278 | const result = new Array(tileGeo._buffer.length) 279 | tileGeo._buffer.forEach((e, i) => { 280 | result[i] = e * heightScale + heightOffset 281 | }) 282 | 283 | return result 284 | } 285 | 286 | const makeBlankCanvas = (size) => { 287 | const canvas = document.createElement('canvas') 288 | canvas.width = size 289 | canvas.height = size 290 | return canvas 291 | } 292 | 293 | export default ElevationGradientImageryProvider 294 | -------------------------------------------------------------------------------- /lib/TileRenderer.js: -------------------------------------------------------------------------------- 1 | import { max, min } from 'lodash' 2 | import elevationGradientVert from './shaders/elevationGradientVert.glsl' 3 | import elevationGradientFrag from './shaders/elevationGradientFrag.glsl' 4 | 5 | const Z_FACTOR = 0.75 6 | const ZENITH = 0.7857142857 7 | const AZIMUTH = 2.3571428571 8 | const TO_BYTE = 255 9 | 10 | const canvases = {} 11 | 12 | const getCanvasAndWebGL = (width, height) => { 13 | const key = JSON.stringify({ width, height }) 14 | if (!canvases[key]) { 15 | const canvas = document.createElement('canvas') 16 | canvas.width = width 17 | canvas.height = height 18 | const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl') 19 | canvases[key] = { 20 | canvas, 21 | gl, 22 | } 23 | } 24 | return canvases[key] 25 | } 26 | 27 | class TileRenderer { 28 | constructor({ width, height, gradientStops, gradientAmount, hillshadeAmount, contourAmount, majorContour, minorContour, contourOpacityThreshold, useSlope, contourColor }) { 29 | const { canvas, gl } = getCanvasAndWebGL(width, height) 30 | this.canvasElement = canvas 31 | this.gl = gl 32 | if (!this.gl) { 33 | throw Error('Failed to get WebGL context') 34 | } 35 | 36 | this.gradientStops = gradientStops 37 | 38 | const modifiedFragmentShader = elevationGradientFrag 39 | .replace(/GRADIENT_STOP_COUNT/g, this.gradientStops.length.toString()) 40 | .replace(/CONTOUR_OPACITY_THRESHOLD/g, contourOpacityThreshold.toString()) 41 | 42 | this.program = createProgram(this.gl, elevationGradientVert, modifiedFragmentShader) 43 | this.hillshadeAmount = hillshadeAmount 44 | this.gradientAmount = gradientAmount 45 | this.contourAmount = contourAmount 46 | this.majorContour = majorContour 47 | this.minorContour = minorContour 48 | this.useSlope = useSlope 49 | this.contourColor = contourColor 50 | } 51 | 52 | render( 53 | heights, 54 | maskSamples, 55 | gridDim, 56 | maskDim, 57 | tileDimension, 58 | ) { 59 | const { gl, program, canvasElement, gradientStops, majorContour, minorContour, gradientAmount, contourAmount, contourColor } = this 60 | 61 | const maskBuffer = new ArrayBuffer(maskSamples.length) 62 | const mask = new Uint8Array(maskBuffer) 63 | maskSamples.forEach((maskSample, i) => { 64 | mask[i] = maskSample * TO_BYTE 65 | }) 66 | 67 | const elevationBuffer = new ArrayBuffer(heights.length * 4) 68 | const elevations = new Uint8Array(elevationBuffer) 69 | 70 | const minElevation = min(heights) 71 | const maxElevation = max(heights) 72 | const deltaElevation = maxElevation - minElevation 73 | 74 | heights.forEach((elevation, i) => { 75 | const normalizedElevation = deltaElevation < 0.001 ? 0 : (elevation - minElevation) / (maxElevation - minElevation) 76 | const value = normalizedElevation * TO_BYTE 77 | 78 | // Note: this is incorrect but reduces visual artefacts 79 | const floorValue = Math.floor(value) 80 | const frac = value - floorValue 81 | const fracValue = frac * TO_BYTE 82 | 83 | elevations[i * 4] = value 84 | elevations[i * 4 + 1] = fracValue 85 | elevations[i * 4 + 2] = value + 0.5 86 | elevations[i * 4 + 3] = value + 0.75 87 | }) 88 | 89 | // setup GLSL program 90 | gl.useProgram(program) 91 | 92 | // look up where the vertex data needs to go. 93 | const positionLocation = gl.getAttribLocation(program, 'a_position') 94 | const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord') 95 | 96 | // offset by half a pixel 97 | const textureCoordinateOffset = 0.5 / (gridDim - 1) 98 | const minUV = textureCoordinateOffset 99 | const maxUV = 1.0 - textureCoordinateOffset 100 | 101 | // provide texture coordinates for the rectangle. 102 | const texCoordBuffer = gl.createBuffer() 103 | gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer) 104 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 105 | minUV, minUV, 106 | maxUV, minUV, 107 | minUV, maxUV, 108 | minUV, maxUV, 109 | maxUV, minUV, 110 | maxUV, maxUV, 111 | ]), gl.STATIC_DRAW) 112 | gl.enableVertexAttribArray(texCoordLocation) 113 | gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0) 114 | 115 | // Create a texture. 116 | const elevationTexture = gl.createTexture() 117 | gl.bindTexture(gl.TEXTURE_2D, elevationTexture) 118 | 119 | // Set the parameters so we can render any size image. 120 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 121 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 122 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) 123 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) 124 | 125 | // Upload the image into the texture. 126 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gridDim, gridDim, 0, gl.RGBA, gl.UNSIGNED_BYTE, elevations) 127 | 128 | // Create a texture. 129 | const maskTexture = gl.createTexture() 130 | gl.bindTexture(gl.TEXTURE_2D, maskTexture) 131 | 132 | // Set the parameters so we can render any size image. 133 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE) 134 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE) 135 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR) 136 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR) 137 | 138 | // Upload the image into the texture. 139 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, maskDim, maskDim, 0, gl.ALPHA, gl.UNSIGNED_BYTE, mask) 140 | 141 | // Set appropriate uniform 142 | const setUniformF = (...args) => { 143 | const uniformType = args.length - 1 144 | const location = gl.getUniformLocation(program, args[0]) 145 | switch (uniformType) { 146 | case 1: 147 | gl.uniform1f(location, args[1]) 148 | break 149 | case 2: 150 | gl.uniform2f(location, args[1], args[2]) 151 | break 152 | case 3: 153 | gl.uniform3f(location, args[1], args[2], args[3]) 154 | break 155 | case 4: 156 | gl.uniform4f(location, args[1], args[2], args[3], args[4]) 157 | break 158 | default: 159 | throw new Error('unsupported uniform') 160 | } 161 | } 162 | 163 | setUniformF('u_resolution', canvasElement.width, canvasElement.height) 164 | setUniformF('u_tileElevationRange', minElevation, maxElevation) 165 | setUniformF('u_textureSize', canvasElement.width, canvasElement.height) 166 | setUniformF('u_tileDimension', tileDimension.x, tileDimension.y) 167 | setUniformF('u_zFactor', Z_FACTOR) 168 | setUniformF('u_zenith', ZENITH) 169 | setUniformF('u_azimuth', AZIMUTH) 170 | setUniformF('u_majorContour', majorContour) 171 | setUniformF('u_minorContour', minorContour) 172 | setUniformF('u_hillshadeAmount', this.hillshadeAmount) 173 | setUniformF('u_gradientAmount', gradientAmount) 174 | setUniformF('u_contourAmount', contourAmount) 175 | setUniformF('u_useSlope', this.useSlope) 176 | setUniformF('u_contourColor', contourColor.red, contourColor.green, contourColor.blue, contourColor.alpha) 177 | 178 | const gradientColors = [] 179 | gradientStops.forEach(({ color: { red, green, blue, alpha } }) => { 180 | gradientColors.push(red * alpha) 181 | gradientColors.push(green * alpha) 182 | gradientColors.push(blue * alpha) 183 | gradientColors.push(alpha) 184 | }) 185 | 186 | const gradientValues = gradientStops.map(({ value }) => value) 187 | 188 | const gradientColorLocation = gl.getUniformLocation(program, 'u_gradientColors') 189 | gl.uniform4fv(gradientColorLocation, new Float32Array(gradientColors)) 190 | const gradientHeightLocation = gl.getUniformLocation(program, 'u_gradientValues') 191 | gl.uniform1fv(gradientHeightLocation, new Float32Array(gradientValues)) 192 | 193 | // Create a buffer for the position of the rectangle corners. 194 | const buffer = gl.createBuffer() 195 | gl.bindBuffer(gl.ARRAY_BUFFER, buffer) 196 | gl.enableVertexAttribArray(positionLocation) 197 | gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0) 198 | 199 | // lookup the sampler locations. 200 | const u_image0Location = gl.getUniformLocation(program, 'u_image') 201 | const u_image1Location = gl.getUniformLocation(program, 'u_mask') 202 | 203 | // set which texture units to render with. 204 | gl.uniform1i(u_image0Location, 0) // texture unit 0 205 | gl.uniform1i(u_image1Location, 1) // texture unit 1 206 | 207 | gl.activeTexture(gl.TEXTURE0) 208 | gl.bindTexture(gl.TEXTURE_2D, elevationTexture) 209 | gl.activeTexture(gl.TEXTURE1) 210 | gl.bindTexture(gl.TEXTURE_2D, maskTexture) 211 | 212 | // Set a rectangle the same size as the image. 213 | setRectangle(gl, 0, 0, canvasElement.width, canvasElement.height) 214 | 215 | // Draw the rectangle. 216 | gl.drawArrays(gl.TRIANGLES, 0, 6) 217 | 218 | return cloneCanvas(canvasElement) 219 | } 220 | } 221 | 222 | const cloneCanvas = (oldCanvas) => { 223 | const newCanvas = document.createElement('canvas') 224 | newCanvas.width = oldCanvas.width 225 | newCanvas.height = oldCanvas.height 226 | 227 | const context = newCanvas.getContext('2d') 228 | context.drawImage(oldCanvas, 0, 0) 229 | 230 | return newCanvas 231 | } 232 | 233 | const detectShaderError = (gl, shader) => { 234 | const compiled = gl.getShaderParameter(shader, gl.COMPILE_STATUS) 235 | if (!compiled) { 236 | const lastError = gl.getShaderInfoLog(shader) 237 | console.error(`*** Error compiling shader '${shader}':${lastError}`) 238 | } 239 | } 240 | 241 | const createProgram = (gl, vertShaderSource, fragShaderSource) => { 242 | const program = gl.createProgram() 243 | 244 | const vertShader = gl.createShader(gl.VERTEX_SHADER) 245 | gl.shaderSource(vertShader, vertShaderSource) 246 | gl.compileShader(vertShader) 247 | detectShaderError(gl, vertShader) 248 | gl.attachShader(program, vertShader) 249 | 250 | const fragShader = gl.createShader(gl.FRAGMENT_SHADER) 251 | gl.shaderSource(fragShader, fragShaderSource) 252 | gl.compileShader(fragShader) 253 | detectShaderError(gl, fragShader) 254 | gl.attachShader(program, fragShader) 255 | 256 | gl.linkProgram(program) 257 | const linked = gl.getProgramParameter(program, gl.LINK_STATUS) 258 | if (!linked) { 259 | // something went wrong with the link 260 | const lastError = gl.getProgramInfoLog(program) 261 | console.error(`Error in program linking:${lastError}`) 262 | 263 | gl.deleteProgram(program) 264 | return null 265 | } 266 | return program 267 | } 268 | 269 | const setRectangle = (gl, x, y, width, height) => { 270 | const x1 = x 271 | const x2 = x + width 272 | const y1 = y 273 | const y2 = y + height 274 | gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ 275 | x1, y1, 276 | x2, y1, 277 | x1, y2, 278 | x1, y2, 279 | x2, y1, 280 | x2, y2, 281 | ]), gl.STATIC_DRAW) 282 | } 283 | 284 | export default TileRenderer 285 | --------------------------------------------------------------------------------