Skip to content

Commit

Permalink
Raster colorization via "raster-color" (internal-455)
Browse files Browse the repository at this point in the history
* Initial implementation of `raster-color`

* Clean up example and change order of operations in shader

* Move scale factors to a better location

* Clean up raster color configuration

* Formatting

* Build out demo page

* Fix test

* Move uniforms inside ifdef

* Update docs and minor code tweak for clarity

* Apply lint fixes

* Retrigger CI

* Partial linter, flow, and style spec fixes

* Fix flow type errors

* Apply PR feedback

* Remove rreusser tilesets from debug page

* Code review fixes

---------

Co-authored-by: Ricky Reusser <[email protected]>
  • Loading branch information
2 people authored and mourner committed Aug 7, 2023
1 parent 596f310 commit 817fe63
Show file tree
Hide file tree
Showing 15 changed files with 555 additions and 12 deletions.
280 changes: 280 additions & 0 deletions debug/raster-color.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
<!DOCTYPE html>
<html>
<head>
<title>Mapbox GL JS debug page</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="../dist/mapbox-gl.css" />
<style>
body { margin: 0; padding: 0; }
html, body { height: 100%; }
#map { height: calc(100% - 40px); }
#controls {
position: fixed;
z-index: 1;
top: 5px;
left: 5px;
}
#gradientbg, #gradient {
position: fixed;
bottom: 0;
width: 100%;
height: 40px;
}
#gradientbg {
z-index: 1;
background: repeating-conic-gradient(#ccc 0% 25%, white 0% 50%) 50% / 20px 20px;
}
#gradient {
transition: all 0.3s ease-in-out;
}
</style>
</head>

<body>
<div id="map"></div>
<div id="controls"></div>
<div id="gradientbg"><div id="gradient"></div></div>

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
<script type="text/javascript" src="https://unpkg.com/[email protected]/dist/d3.min.js"></script>
<script src="../dist/mapbox-gl-dev.js"></script>
<script src="../debug/access_token_generated.js"></script>
<script>

/*global d3*/

const presets = {
Satellite: {
tileset: "mapbox.satellite",
rasterColor: "Cubehelix",
rasterColorMix: "luminosity",
rasterColorRange: "[0, 1]"
},
Terrain: {
tileset: "mapbox.terrain-rgb",
rasterColor: "Hypsometric",
rasterColorMix: "decode terrain-rgb",
rasterColorRange: "[0, 8848]",
forcePngraw: true
}
};

function GUIParams () {
this.preset = "Satellite";
this.opacity = 1;
Object.assign(this, presets[this.preset]);
}

const guiParams = new GUIParams();

const mixes = {
luminosity: [ 0.2126, 0.7152, 0.0722, 0 ],
average: [1 / 3, 1 / 3, 1 / 3, 0],
red: [1, 0, 0, 0],
green: [0, 1, 0, 0],
blue: [0, 0, 1, 0],
"decode terrain-rgb": [
256 * 256 * 256 * 0.1,
256 * 256 * 0.1,
256 * 0.1,
-10000
],
"decode population log-density": [
256 * 256 * 256 * 8.348993603394451e-07,
256 * 256 * 8.348993603394451e-07,
256 * 8.348993603394451e-07,
-9
],
};

const ranges = {
"[0, 1]": [0, 1],
"[0, 8848]": [0, 8848],
"[-3, 4]": [-3, 4],
};

const tilesets = {
"mapbox.terrain-rgb": "mapbox.terrain-rgb",
"mapbox.satellite": "mapbox.satellite"
};

const quantize = (interpolator, opac = x => 1, n = 100) => d3.quantize(interpolator, n).map((c, i) => {
const col = d3.rgb(c);
const t = i / (n - 1);
return [ t, `rgba(${col.r},${col.g},${col.b},${col.opacity * opac(t)})` ];
});

const colorScales = {
Viridis: quantize(d3.interpolateViridis),
Greys: quantize(x => d3.interpolateGreys(1 - x)),
Turbo: quantize(d3.interpolateTurbo),
Inferno: quantize(d3.interpolateInferno),
Magma: quantize(d3.interpolateMagma),
Plasma: quantize(d3.interpolatePlasma),
Cividis: quantize(d3.interpolateCividis),
Cool: quantize(d3.interpolateCool),
Warm: quantize(d3.interpolateWarm),
Cubehelix: quantize(d3.interpolateCubehelixDefault),
RdPu: quantize(x => d3.interpolateRdPu(1 - x)),
YlGnBu: quantize(x => d3.interpolateYlGnBu(1 - x)),
Rainbow: quantize(d3.interpolateRainbow),
Sinebow: quantize(d3.interpolateSinebow),
RdBu: quantize(d3.interpolateRdBu),
PopDensity: quantize(d3.interpolateMagma, t => 3 * t * t - 2 * t * t * t),
Hypsometric: [
[ 0.004, "rgb(48, 167, 228)" ],
[ 0.005, "rgb(57, 143, 83)" ],
[ 0.031, "rgb(116, 166, 129)" ],
[ 0.055, "rgb(178, 205, 174)" ],
[ 0.076, "rgb(188, 195, 169)" ],
[ 0.108, "rgb(221, 207, 153)" ],
[ 0.153, "rgb(211, 174, 114)" ],
[ 0.205, "rgb(207, 155, 103)" ],
[ 0.277, "rgb(179, 120, 85)" ],
[ 0.375, "rgb(227, 210, 197)" ],
[ 0.660, "rgb(255, 255, 255)" ]
],
'Transparent white': [
[ 0.0, "rgba(255, 255, 255, 0)" ],
[ 1.0, "rgba(255, 255, 255, 1)" ],
],
Radar: [
[ 0.0000, "rgba(35,23,27,0)" ],
[ 0.0714, "rgba(68, 188, 116, 0.1)" ],
[ 0.1428, "rgba(30, 183, 66, 0.3)" ],
[ 0.2142, "rgba(45, 193, 81, 0.5)" ],
[ 0.2857, "rgba(38, 208, 43, 0.7)" ],
[ 0.3571, "rgba(58, 237, 55, 0.9)" ],
[ 0.4285, "rgb(143, 252, 95)" ],
[ 0.5000, "rgb(190, 251, 81)" ],
[ 0.5714, "rgb(200, 235, 26)" ],
[ 0.6428, "rgb(245,199,43)" ],
[ 0.7142, "rgb(255,155,33)" ],
[ 0.7857, "rgb(251,105,25)" ],
[ 0.8571, "rgb(214,57,15)" ],
[ 0.9285, "rgb(168,22,4)" ],
[ 1.0000, "rgb(183, 44, 204)" ]
]
};

function setStyle (tileset) {
map.setStyle({
version: 8,
sources: {
mapbox: {
type: "raster",
url: "mapbox://mapbox.satellite",
tileSize: 256
},
raster: {
type: "raster",
tiles: [ `https://api.mapbox.com/v4/${tilesets[tileset]}/{z}/{x}/{y}@2x.webp` ],
maxzoom: 14,
tileSize: 256
}
},
layers: [{
"id": "background",
"type": "background",
"paint": {
"background-color": "rgb(4,7,14)"
}
}, {
"id": "satellite",
"type": "raster",
"source": "mapbox",
"source-layer": "mapbox_satellite_full",
"paint": {
"raster-saturation": -0.3,
"raster-brightness-max": 0.5,
}
}, {
"type": "raster",
"source": "raster",
"id": "raster",
"paint": {
"raster-opacity": guiParams.opacity
}
}]
});
}

var map = window.map = new mapboxgl.Map({
container: "map",
style: {version: 8, layers: [], sources: {}},
hash: true,
transformRequest (url, type) {
if (type !== "Tile") return;
const preset = presets[guiParams.preset];
if (preset.forbid2x) url = url.replace('@2x', '');
if (preset.forcePngraw) url = url.replace('.webp', '.pngraw');
return {url};
}
});

map.once("load", function () {
var gui = window.gui = new dat.GUI(); // eslint-disable-line

const preset = gui.add(guiParams, "preset", Object.keys(presets));
const tileset = gui.add(guiParams, "tileset", Object.keys(tilesets));
const scale = gui.add(guiParams, "rasterColor", Object.keys(colorScales));
const mix = gui.add(guiParams, "rasterColorMix", Object.keys(mixes));
const range = gui.add(guiParams, "rasterColorRange", Object.keys(ranges));
const opacity = gui.add(guiParams, "opacity", 0, 1);

function setColorScale(scale) {
const range = ranges[guiParams.rasterColorRange];
const cs = colorScales[scale];
map.setPaintProperty("raster", "raster-color", [
"interpolate",
["linear"],
["raster-value"],
...cs.map(([pos, col]) => [
range[0] + (range[1] - range[0]) * pos,
col
]).flat(),
]);
document.getElementById("gradient").style.background = `linear-gradient(90deg, ${cs.map(([pos, col]) => `${col} ${(pos * 100).toFixed(1)}%`).join(",")})`;
document.getElementById("gradient").style.opacity = guiParams.opacity;
}

function setColorMix(mix) {
map.setPaintProperty("raster", "raster-color-mix", mixes[mix]);
}

function setColorRange(range) {
map.setPaintProperty("raster", "raster-color-range", ranges[range]);
setColorScale(guiParams.rasterColor);
}

function setTileset(range) {
setStyle(guiParams.tileset);
setColorMix(guiParams.rasterColorMix);
setColorScale(guiParams.rasterColor);
setColorRange(guiParams.rasterColorRange);
}

function setPreset(presetName) {
const preset = presets[presetName];
Object.assign(guiParams, preset);
setTileset(guiParams.tileset);
}

mix.onChange(setColorMix).listen();
scale.onChange(setColorScale).listen();
range.onChange(setColorRange).listen();
tileset.onChange(setTileset).listen();
preset.onChange(setPreset);

opacity.onChange(opac => {
map.setPaintProperty("raster", "raster-opacity", opac);
document.getElementById("gradient").style.opacity = opac;
});

setTileset(guiParams.tileset);
});

</script>
</body>
</html>
29 changes: 27 additions & 2 deletions src/render/draw_raster.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import ImageSource from '../source/image_source.js';
import StencilMode from '../gl/stencil_mode.js';
import DepthMode from '../gl/depth_mode.js';
import CullFaceMode from '../gl/cull_face_mode.js';
import Texture from './texture.js';
import {rasterUniformValues} from './program/raster_program.js';

import type Context from '../gl/context.js';
import type Painter from './painter.js';
import type SourceCache from '../source/source_cache.js';
import type RasterStyleLayer from '../style/style_layer/raster_style_layer.js';
Expand All @@ -14,6 +16,8 @@ import rasterFade from './raster_fade.js';

export default drawRaster;

const RASTER_COLOR_TEXTURE_UNIT = 2;

function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterStyleLayer, tileIDs: Array<OverscaledTileID>, variableOffsets: any, isInitialLoad: boolean) {
if (painter.renderPass !== 'translucent') return;
if (layer.paint.get('raster-opacity') === 0) return;
Expand All @@ -22,7 +26,10 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
const context = painter.context;
const gl = context.gl;
const source = sourceCache.getSource();
const program = painter.useProgram('raster');

const rasterColor = configureRasterColor(layer, context, gl);

const program = painter.useProgram('raster', null, rasterColor.defines);

const colorMode = painter.colorModeForRenderPass();

Expand Down Expand Up @@ -85,7 +92,7 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
}

const perspectiveTransform = source instanceof ImageSource ? source.perspectiveTransform : [0, 0];
const uniformValues = rasterUniformValues(projMatrix, parentTL || [0, 0], parentScaleBy || 1, fade, layer, perspectiveTransform);
const uniformValues = rasterUniformValues(projMatrix, parentTL || [0, 0], parentScaleBy || 1, fade, layer, perspectiveTransform, RASTER_COLOR_TEXTURE_UNIT, rasterColor.mix || [0, 0, 0, 0], rasterColor.range || [0, 0]);

painter.uploadCommonUniforms(context, program, unwrappedTileID);

Expand All @@ -106,3 +113,21 @@ function drawRaster(painter: Painter, sourceCache: SourceCache, layer: RasterSty
painter.resetStencilClippingMasks();
}

function configureRasterColor (layer: RasterStyleLayer, context: Context, gl: WebGLRenderingContext) {
const defines = [];
let mix;
let range;

if (layer.paint.get('raster-color')) {
defines.push('RASTER_COLOR');
mix = layer.paint.get('raster-color-mix');
range = layer.paint.get('raster-color-range');

// Allocate a texture if not allocated
context.activeTexture.set(gl.TEXTURE2);
let tex = layer.colorRampTexture;
if (!tex) tex = layer.colorRampTexture = new Texture(context, layer.colorRamp, gl.RGBA);
tex.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
}
return {mix, range, defines};
}
3 changes: 2 additions & 1 deletion src/render/program/program_uniforms.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// @flow

import type {CircleDefinesType} from './circle_program.js';
import type {RasterDefinesType} from './raster_program.js';
import type {SymbolDefinesType} from './symbol_program.js';
import type {LineDefinesType} from './line_program.js';
import {fillExtrusionUniforms, fillExtrusionPatternUniforms} from './fill_extrusion_program.js';
Expand All @@ -23,7 +24,7 @@ import type {HeatmapDefinesType} from './heatmap_program.js';
import type {DebugDefinesType} from './debug_program.js';
import type {GlobeDefinesType} from '../../terrain/globe_raster_program.js';

export type DynamicDefinesType = CircleDefinesType | SymbolDefinesType | LineDefinesType | HeatmapDefinesType | DebugDefinesType | GlobeDefinesType;
export type DynamicDefinesType = CircleDefinesType | SymbolDefinesType | LineDefinesType | HeatmapDefinesType | DebugDefinesType | GlobeDefinesType | RasterDefinesType;

export const programUniforms = {
fillExtrusion: fillExtrusionUniforms,
Expand Down
Loading

0 comments on commit 817fe63

Please sign in to comment.