├── .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 |
--------------------------------------------------------------------------------