Skip to content

Commit 25d3364

Browse files
NathanMOlsonHarelM
authored andcommitted
Hypsometric Tint from terrain-RGB tiles (maplibre#5913)
* Basic color relief layer working, with a hardcoded colormap * use texture for color-relief colormap * add color relief benchmark * fix border rendering error and ugliness * do lookup table in shader * fix merge error * fix initialization of _featureFilter with stytle-spec update (global state addition) * remove dead code (pulling colormap from texture) * remove more dead code related to rendering color relief from texture * add unit tests for ColorReliefStyleLayer * add render tests and global opacity handling * Add a color relief example * add render test with color relief and hillshade * fix lint * fix benchmark and update changelog * updat to use latest version of style spec * add "hypsometric" to spelling list * update expectedBytes * improve function name * remove "as any", simplify colorramp creation, add comment * remove colorramp requirement length = 2^N + 1 * Remap the color-relief color ramp if the length exceeds the WebGL capabilities. * add unit test for getColorRamp() * handle empty and single-color color ramps * account for presence of other uniforms in calculation of maxLength for color ramp * return arrays in _updateColorRamp() * sinplify getColorRamp() * add (messy) type to createColorReliefLayerSpec() argument * fix colorRampLength * add color relief layer benchmarks * use defined type * add DEMData::pack() function and unit tests * version of color relief that uses textures instead of uniforms for colorramp * get the right size limit for color ramp * update expectedBytes * fix off-by-one error * use texture lookup instead of shader interpolation * function nameing and comment formating * defer color ramp creation until getColorRamp * update unit tests * remove unneeded shader #define * fix colorRampLength calculation * use first available textures * fix expectedBytes * enable color ramp to be dynamically changes using map.setPaintProperty() * reduce indentation * remove unneeded member variable colorRamp and combine _createColorRamp() and getColorRamp() * fix build * add RGBAImage::setPixel() * fix lint --------- Co-authored-by: Harel M <[email protected]>
1 parent 947cf6a commit 25d3364

34 files changed

+973
-16
lines changed

.cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
"highp",
6363
"Hira",
6464
"Hoare",
65+
"hypsometric",
6566
"ifdef",
6667
"ifdefs",
6768
"iframes",

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
### ✨ Features and improvements
44
- Add `setGlobalStateProperty()` and `getGlobalState()` to the map public API ([#5613](https://github.com/maplibre/maplibre-gl-js/pull/5613))
55
- Improve tile frustum culling for globe, leading to better performance and faster loading times. ([#5865](https://github.com/maplibre/maplibre-gl-js/pull/5865))
6+
- Add new `color-relief` layer type to render hypsometric tint from terrain-RGB tiles. ([#5742](https://github.com/maplibre/maplibre-gl-js/pull/5742))
67
- _...Add new stuff here..._
78

89
### 🐞 Bug fixes
314 KB
Loading

src/data/dem_data.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,3 +236,60 @@ describe('DEMData#getImage', () => {
236236
test('Image is correctly returned - terrarium', testGetPixels(terrariumDEM, imageData));
237237
test('Image is correctly returned - custom', testGetPixels(customDEM, imageData));
238238
});
239+
240+
describe('DEMData pack and unpack', () => {
241+
const imageData = createMockImage(4, 4);
242+
test('mapbox', () => {
243+
const dem = new DEMData('0', imageData, 'mapbox');
244+
expect(dem.unpack(123, 177, 215)).toEqual(800645.5);
245+
expect(dem.pack(800645.5)).toEqual({r: 123, g: 177, b: 215});
246+
247+
expect(dem.unpack(0, 0, 0)).toEqual(-10000);
248+
expect(dem.pack(-10000)).toEqual({r: 0, g: 0, b: 0});
249+
250+
expect(dem.unpack(1, 1, 1)).toBeCloseTo(-3420.7);
251+
expect(dem.pack(-3420.7)).toEqual({r: 1, g: 1, b: 1});
252+
253+
expect(dem.unpack(255, 255, 255)).toEqual(1667721.5);
254+
expect(dem.pack(1667721.5)).toEqual({r: 255, g: 255, b: 255});
255+
256+
expect(dem.unpack(255, 0, 255)).toEqual(1661193.5);
257+
expect(dem.pack(1661193.5)).toEqual({r: 255, g: 0, b: 255});
258+
});
259+
260+
test('terrarium', () => {
261+
const dem = new DEMData('0', imageData, 'terrarium');
262+
expect(dem.unpack(123, 177, 215)).toEqual(-1102.16015625);
263+
expect(dem.pack(-1102.16015625)).toEqual({r: 123, g: 177, b: 215});
264+
265+
expect(dem.unpack(0, 0, 0)).toEqual(-32768);
266+
expect(dem.pack(-32768)).toEqual({r: 0, g: 0, b: 0});
267+
268+
expect(dem.unpack(1, 1, 1)).toEqual(-32510.99609375);
269+
expect(dem.pack(-32510.99609375)).toEqual({r: 1, g: 1, b: 1});
270+
271+
expect(dem.unpack(255, 255, 255)).toEqual(32767.99609375);
272+
expect(dem.pack(32767.99609375)).toEqual({r: 255, g: 255, b: 255});
273+
274+
expect(dem.unpack(255, 0, 255)).toEqual(32512.99609375);
275+
expect(dem.pack(32512.99609375)).toEqual({r: 255, g: 0, b: 255});
276+
});
277+
278+
test('custom', () => {
279+
const dem = new DEMData('0', imageData, 'custom', 0.25, 64, 16384, 7000.0);
280+
expect(dem.unpack(123, 177, 215)).toEqual(3526918.75);
281+
expect(dem.pack(3526918.75)).toEqual({r: 123, g: 177, b: 215});
282+
283+
expect(dem.unpack(0, 0, 0)).toEqual(-7000);
284+
expect(dem.pack(-7000)).toEqual({r: 0, g: 0, b: 0});
285+
286+
expect(dem.unpack(1, 1, 1)).toEqual(9448.25);
287+
expect(dem.pack(9448.25)).toEqual({r: 1, g: 1, b: 1});
288+
289+
expect(dem.unpack(255, 255, 255)).toEqual(4187303.75);
290+
expect(dem.pack(4187303.75)).toEqual({r: 255, g: 255, b: 255});
291+
292+
expect(dem.unpack(255, 0, 255)).toEqual(4170983.75);
293+
expect(dem.pack(4170983.75)).toEqual({r: 255, g: 0, b: 255});
294+
});
295+
});

src/data/dem_data.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ export class DEMData {
128128
return (r * this.redFactor + g * this.greenFactor + b * this.blueFactor - this.baseShift);
129129
}
130130

131+
pack(v: number): {r: number; g: number; b: number} {
132+
return packDEMData(v, this.getUnpackVector());
133+
}
134+
131135
getPixels() {
132136
return new RGBAImage({width: this.stride, height: this.stride}, new Uint8Array(this.data.buffer));
133137
}
@@ -168,4 +172,18 @@ export class DEMData {
168172
}
169173
}
170174

175+
export function packDEMData(v: number, unpackVector: number[]): {r: number; g: number; b: number} {
176+
const redFactor = unpackVector[0];
177+
const greenFactor = unpackVector[1];
178+
const blueFactor = unpackVector[2];
179+
const baseShift = unpackVector[3];
180+
const minScale = Math.min(redFactor, greenFactor, blueFactor);
181+
const vScaled = Math.round((v + baseShift)/minScale);
182+
return {
183+
r: Math.floor(vScaled*minScale/redFactor) % 256,
184+
g: Math.floor(vScaled*minScale/greenFactor) % 256,
185+
b: Math.floor(vScaled*minScale/blueFactor) % 256
186+
};
187+
}
188+
171189
register('DEMData', DEMData);

src/render/draw_color_relief.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import {Texture} from './texture';
2+
import type {StencilMode} from '../gl/stencil_mode';
3+
import {DepthMode} from '../gl/depth_mode';
4+
import {CullFaceMode} from '../gl/cull_face_mode';
5+
import {type ColorMode} from '../gl/color_mode';
6+
import {
7+
colorReliefUniformValues
8+
} from './program/color_relief_program';
9+
10+
import type {Painter, RenderOptions} from './painter';
11+
import type {SourceCache} from '../source/source_cache';
12+
import type {ColorReliefStyleLayer} from '../style/style_layer/color_relief_style_layer';
13+
import type {OverscaledTileID} from '../source/tile_id';
14+
15+
export function drawColorRelief(painter: Painter, sourceCache: SourceCache, layer: ColorReliefStyleLayer, tileIDs: Array<OverscaledTileID>, renderOptions: RenderOptions) {
16+
if (painter.renderPass !== 'translucent') return;
17+
if (!tileIDs.length) return;
18+
19+
const {isRenderingToTexture} = renderOptions;
20+
const projection = painter.style.projection;
21+
const useSubdivision = projection.useSubdivision;
22+
23+
const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly);
24+
const colorMode = painter.colorModeForRenderPass();
25+
26+
// Globe (or any projection with subdivision) needs two-pass rendering to avoid artifacts when rendering texture tiles.
27+
// See comments in draw_raster.ts for more details.
28+
if (useSubdivision) {
29+
// Two-pass rendering
30+
const [stencilBorderless, stencilBorders, coords] = painter.stencilConfigForOverlapTwoPass(tileIDs);
31+
renderColorRelief(painter, sourceCache, layer, coords, stencilBorderless, depthMode, colorMode, false, isRenderingToTexture); // draw without borders
32+
renderColorRelief(painter, sourceCache, layer, coords, stencilBorders, depthMode, colorMode, true, isRenderingToTexture); // draw with borders
33+
} else {
34+
// Simple rendering
35+
const [stencil, coords] = painter.getStencilConfigForOverlapAndUpdateStencilID(tileIDs);
36+
renderColorRelief(painter, sourceCache, layer, coords, stencil, depthMode, colorMode, false, isRenderingToTexture);
37+
}
38+
}
39+
40+
function renderColorRelief(
41+
painter: Painter,
42+
sourceCache: SourceCache,
43+
layer: ColorReliefStyleLayer,
44+
coords: Array<OverscaledTileID>,
45+
stencilModes: {[_: number]: Readonly<StencilMode>},
46+
depthMode: Readonly<DepthMode>,
47+
colorMode: Readonly<ColorMode>,
48+
useBorder: boolean,
49+
isRenderingToTexture: boolean
50+
) {
51+
const projection = painter.style.projection;
52+
const context = painter.context;
53+
const transform = painter.transform;
54+
const gl = context.gl;
55+
const program = painter.useProgram('colorRelief');
56+
const align = !painter.options.moving;
57+
58+
let firstTile = true;
59+
60+
for (const coord of coords) {
61+
const tile = sourceCache.getTile(coord);
62+
const dem = tile.dem;
63+
if(firstTile) {
64+
const maxLength = gl.getParameter(gl.MAX_TEXTURE_SIZE);
65+
const {elevationTexture, colorTexture} = layer.getColorRampTextures(context, maxLength, dem.getUnpackVector());
66+
context.activeTexture.set(gl.TEXTURE1);
67+
elevationTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE);
68+
context.activeTexture.set(gl.TEXTURE4);
69+
colorTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
70+
firstTile = false;
71+
}
72+
73+
if (!dem || !dem.data) {
74+
continue;
75+
}
76+
77+
const textureStride = dem.stride;
78+
79+
const pixelData = dem.getPixels();
80+
context.activeTexture.set(gl.TEXTURE0);
81+
82+
context.pixelStoreUnpackPremultiplyAlpha.set(false);
83+
tile.demTexture = tile.demTexture || painter.getTileTexture(textureStride);
84+
if (tile.demTexture) {
85+
const demTexture = tile.demTexture;
86+
demTexture.update(pixelData, {premultiply: false});
87+
demTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
88+
} else {
89+
tile.demTexture = new Texture(context, pixelData, gl.RGBA, {premultiply: false});
90+
tile.demTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
91+
}
92+
93+
const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, true, 'raster');
94+
95+
const terrainData = painter.style.map.terrain?.getTerrainData(coord);
96+
97+
const projectionData = transform.getProjectionData({
98+
overscaledTileID: coord,
99+
aligned: align,
100+
applyGlobeMatrix: !isRenderingToTexture,
101+
applyTerrainMatrix: true
102+
});
103+
104+
program.draw(context, gl.TRIANGLES, depthMode, stencilModes[coord.overscaledZ], colorMode, CullFaceMode.backCCW,
105+
colorReliefUniformValues(layer, tile.dem), terrainData, projectionData, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments);
106+
}
107+
}

src/render/painter.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {drawLine} from './draw_line';
2525
import {drawFill} from './draw_fill';
2626
import {drawFillExtrusion} from './draw_fill_extrusion';
2727
import {drawHillshade} from './draw_hillshade';
28+
import {drawColorRelief} from './draw_color_relief';
2829
import {drawRaster} from './draw_raster';
2930
import {drawBackground} from './draw_background';
3031
import {drawDebug, drawDebugPadding, selectDebugSource} from './draw_debug';
@@ -56,6 +57,7 @@ import {isLineStyleLayer} from '../style/style_layer/line_style_layer';
5657
import {isFillStyleLayer} from '../style/style_layer/fill_style_layer';
5758
import {isFillExtrusionStyleLayer} from '../style/style_layer/fill_extrusion_style_layer';
5859
import {isHillshadeStyleLayer} from '../style/style_layer/hillshade_style_layer';
60+
import {isColorReliefStyleLayer} from '../style/style_layer/color_relief_style_layer';
5961
import {isRasterStyleLayer} from '../style/style_layer/raster_style_layer';
6062
import {isBackgroundStyleLayer} from '../style/style_layer/background_style_layer';
6163
import {isCustomStyleLayer} from '../style/style_layer/custom_style_layer';
@@ -671,6 +673,8 @@ export class Painter {
671673
drawFillExtrusion(painter, sourceCache, layer, coords, renderOptions);
672674
} else if (isHillshadeStyleLayer(layer)) {
673675
drawHillshade(painter, sourceCache, layer, coords, renderOptions);
676+
} else if (isColorReliefStyleLayer(layer)) {
677+
drawColorRelief(painter, sourceCache, layer, coords, renderOptions);
674678
} else if (isRasterStyleLayer(layer)) {
675679
drawRaster(painter, sourceCache, layer, coords, renderOptions);
676680
} else if (isBackgroundStyleLayer(layer)) {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
Uniform1i,
3+
Uniform1f,
4+
Uniform2f,
5+
Uniform4f
6+
} from '../uniform_binding';
7+
8+
import type {Context} from '../../gl/context';
9+
import type {UniformValues, UniformLocations} from '../uniform_binding';
10+
import type {ColorReliefStyleLayer} from '../../style/style_layer/color_relief_style_layer';
11+
import type {DEMData} from '../../data/dem_data';
12+
13+
export type ColorReliefUniformsType = {
14+
'u_image': Uniform1i;
15+
'u_unpack': Uniform4f;
16+
'u_dimension': Uniform2f;
17+
'u_elevation_stops': Uniform1i;
18+
'u_color_stops': Uniform1i;
19+
'u_opacity': Uniform1f;
20+
};
21+
22+
const colorReliefUniforms = (context: Context, locations: UniformLocations): ColorReliefUniformsType => ({
23+
'u_image': new Uniform1i(context, locations.u_image),
24+
'u_unpack': new Uniform4f(context, locations.u_unpack),
25+
'u_dimension': new Uniform2f(context, locations.u_dimension),
26+
'u_elevation_stops': new Uniform1i(context, locations.u_elevation_stops),
27+
'u_color_stops': new Uniform1i(context, locations.u_color_stops),
28+
'u_opacity': new Uniform1f(context, locations.u_opacity)
29+
});
30+
31+
const colorReliefUniformValues = (
32+
layer: ColorReliefStyleLayer,
33+
dem: DEMData
34+
): UniformValues<ColorReliefUniformsType> => {
35+
36+
return {
37+
'u_image': 0,
38+
'u_unpack': dem.getUnpackVector(),
39+
'u_dimension': [dem.stride, dem.stride],
40+
'u_elevation_stops': 1,
41+
'u_color_stops': 4,
42+
'u_opacity': layer.paint.get('color-relief-opacity')
43+
};
44+
};
45+
46+
export {
47+
colorReliefUniforms,
48+
colorReliefUniformValues,
49+
};

src/render/program/program_uniforms.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {collisionUniforms, collisionCircleUniforms} from './collision_program';
55
import {debugUniforms} from './debug_program';
66
import {heatmapUniforms, heatmapTextureUniforms} from './heatmap_program';
77
import {hillshadeUniforms, hillshadePrepareUniforms} from './hillshade_program';
8+
import {colorReliefUniforms} from './color_relief_program';
89
import {lineUniforms, lineGradientUniforms, linePatternUniforms, lineSDFUniforms} from './line_program';
910
import {rasterUniforms} from './raster_program';
1011
import {symbolIconUniforms, symbolSDFUniforms, symbolTextAndIconUniforms} from './symbol_program';
@@ -33,6 +34,7 @@ export const programUniforms = {
3334
heatmapTexture: heatmapTextureUniforms,
3435
hillshade: hillshadeUniforms,
3536
hillshadePrepare: hillshadePrepareUniforms,
37+
colorRelief: colorReliefUniforms,
3638
line: lineUniforms,
3739
lineGradient: lineGradientUniforms,
3840
linePattern: linePatternUniforms,

src/render/render_to_texture.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ const LAYERS: { [keyof in StyleLayer['type']]?: boolean } = {
1818
fill: true,
1919
line: true,
2020
raster: true,
21-
hillshade: true
21+
hillshade: true,
22+
'color-relief': true
2223
};
2324

2425
/**

0 commit comments

Comments
 (0)