Skip to content

Hypsometric Tint from terrain-RGB tiles #5742

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 40 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
e8a5bba
Basic color relief layer working, with a hardcoded colormap
NathanMOlson Apr 10, 2025
57b9b1b
use texture for color-relief colormap
NathanMOlson Apr 11, 2025
b729d3e
add color relief benchmark
NathanMOlson Apr 11, 2025
3fe4707
fix border rendering error and ugliness
NathanMOlson Apr 11, 2025
bf5e974
do lookup table in shader
NathanMOlson Apr 13, 2025
002cc7b
Merge branch 'main' into hypsometric2
NathanMOlson Apr 24, 2025
df7af32
Merge branch 'main' into hypsometric2
NathanMOlson May 14, 2025
23ff672
fix merge error
NathanMOlson May 14, 2025
f0c048d
fix initialization of _featureFilter with stytle-spec update (global …
NathanMOlson May 14, 2025
700c675
remove dead code (pulling colormap from texture)
NathanMOlson May 14, 2025
98ac2b9
remove more dead code related to rendering color relief from texture
NathanMOlson May 14, 2025
b83cf42
add unit tests for ColorReliefStyleLayer
NathanMOlson May 14, 2025
8b1bc49
add render tests and global opacity handling
NathanMOlson May 14, 2025
06bedd4
Add a color relief example
NathanMOlson May 15, 2025
c15de0a
add render test with color relief and hillshade
NathanMOlson May 15, 2025
57f03a3
Merge branch 'main' into hypsometric2
NathanMOlson May 15, 2025
ab7edd3
fix lint
NathanMOlson May 15, 2025
f3acf20
fix benchmark and update changelog
NathanMOlson May 15, 2025
77d2c1e
Merge branch 'main' into hypsometric2
NathanMOlson May 16, 2025
8883d0d
updat to use latest version of style spec
NathanMOlson May 17, 2025
5f9dd70
add "hypsometric" to spelling list
NathanMOlson May 17, 2025
d9c3cfd
update expectedBytes
NathanMOlson May 17, 2025
82d8fe9
improve function name
NathanMOlson May 19, 2025
bb11461
remove "as any", simplify colorramp creation, add comment
NathanMOlson May 19, 2025
e22d1b6
remove colorramp requirement length = 2^N + 1
NathanMOlson May 19, 2025
b085c1d
Remap the color-relief color ramp if the length exceeds the WebGL cap…
NathanMOlson May 19, 2025
2774b1f
add unit test for getColorRamp()
NathanMOlson May 19, 2025
b98e027
handle empty and single-color color ramps
NathanMOlson May 19, 2025
0a629b4
account for presence of other uniforms in calculation of maxLength fo…
NathanMOlson May 19, 2025
9512079
return arrays in _updateColorRamp()
NathanMOlson May 19, 2025
3740a55
sinplify getColorRamp()
NathanMOlson May 19, 2025
ae5cc80
add (messy) type to createColorReliefLayerSpec() argument
NathanMOlson May 19, 2025
a861180
fix colorRampLength
NathanMOlson May 19, 2025
8bcd98f
add color relief layer benchmarks
NathanMOlson May 19, 2025
7db8b75
use defined type
NathanMOlson May 19, 2025
0b96139
function nameing and comment formating
NathanMOlson May 20, 2025
ca69c39
defer color ramp creation until getColorRamp
NathanMOlson May 20, 2025
71b0e5a
update unit tests
NathanMOlson May 20, 2025
60c1bf4
fix colorRampLength calculation
NathanMOlson May 20, 2025
dd43722
Merge branch 'main' into hypsometric2
NathanMOlson May 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"highp",
"Hira",
"Hoare",
"hypsometric",
"ifdef",
"ifdefs",
"iframes",
Expand Down
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## main

### ✨ Features and improvements

- Add new `color-relief` layer type to render hypsometric tint from terrain-RGB tiles. ([#5742](https://github.com/maplibre/maplibre-gl-js/pull/5742))
- _...Add new stuff here..._

### 🐞 Bug fixes
Expand Down
Binary file added docs/assets/examples/color-relief.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
99 changes: 99 additions & 0 deletions src/render/draw_color_relief.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import {Texture} from './texture';
import type {StencilMode} from '../gl/stencil_mode';
import {DepthMode} from '../gl/depth_mode';
import {CullFaceMode} from '../gl/cull_face_mode';
import {type ColorMode} from '../gl/color_mode';
import {
colorReliefUniformValues
} from './program/color_relief_program';

import type {Painter, RenderOptions} from './painter';
import type {SourceCache} from '../source/source_cache';
import type {ColorReliefStyleLayer} from '../style/style_layer/color_relief_style_layer';
import type {OverscaledTileID} from '../source/tile_id';

export function drawColorRelief(painter: Painter, sourceCache: SourceCache, layer: ColorReliefStyleLayer, tileIDs: Array<OverscaledTileID>, renderOptions: RenderOptions) {
if (painter.renderPass !== 'translucent') return;
if (!tileIDs.length) return;

const {isRenderingToTexture} = renderOptions;
const projection = painter.style.projection;
const useSubdivision = projection.useSubdivision;

const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly);
const colorMode = painter.colorModeForRenderPass();

// Globe (or any projection with subdivision) needs two-pass rendering to avoid artifacts when rendering texture tiles.
// See comments in draw_raster.ts for more details.
if (useSubdivision) {
// Two-pass rendering
const [stencilBorderless, stencilBorders, coords] = painter.stencilConfigForOverlapTwoPass(tileIDs);
renderColorRelief(painter, sourceCache, layer, coords, stencilBorderless, depthMode, colorMode, false, isRenderingToTexture); // draw without borders
renderColorRelief(painter, sourceCache, layer, coords, stencilBorders, depthMode, colorMode, true, isRenderingToTexture); // draw with borders
} else {
// Simple rendering
const [stencil, coords] = painter.getStencilConfigForOverlapAndUpdateStencilID(tileIDs);
renderColorRelief(painter, sourceCache, layer, coords, stencil, depthMode, colorMode, false, isRenderingToTexture);
}
}

function renderColorRelief(
painter: Painter,
sourceCache: SourceCache,
layer: ColorReliefStyleLayer,
coords: Array<OverscaledTileID>,
stencilModes: {[_: number]: Readonly<StencilMode>},
depthMode: Readonly<DepthMode>,
colorMode: Readonly<ColorMode>,
useBorder: boolean,
isRenderingToTexture: boolean
) {
const projection = painter.style.projection;
const context = painter.context;
const transform = painter.transform;
const gl = context.gl;
const maxLength = Math.floor((gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS) - 3) / 2);
const colorRampLength = layer.getColorRamp(maxLength).colorStops.length;
const defines = [`#define NUM_ELEVATION_STOPS ${colorRampLength}`];
const program = painter.useProgram('colorRelief', null, false, defines);
const align = !painter.options.moving;

for (const coord of coords) {
const tile = sourceCache.getTile(coord);
const dem = tile.dem;

if (!dem || !dem.data) {
continue;
}

const textureStride = dem.stride;

const pixelData = dem.getPixels();
context.activeTexture.set(gl.TEXTURE0);

context.pixelStoreUnpackPremultiplyAlpha.set(false);
tile.demTexture = tile.demTexture || painter.getTileTexture(textureStride);
if (tile.demTexture) {
const demTexture = tile.demTexture;
demTexture.update(pixelData, {premultiply: false});
demTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
} else {
tile.demTexture = new Texture(context, pixelData, gl.RGBA, {premultiply: false});
tile.demTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
}

const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder, true, 'raster');

const terrainData = painter.style.map.terrain?.getTerrainData(coord);

const projectionData = transform.getProjectionData({
overscaledTileID: coord,
aligned: align,
applyGlobeMatrix: !isRenderingToTexture,
applyTerrainMatrix: true
});

program.draw(context, gl.TRIANGLES, depthMode, stencilModes[coord.overscaledZ], colorMode, CullFaceMode.backCCW,
colorReliefUniformValues(layer, tile.dem, maxLength), terrainData, projectionData, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments);
}
}
4 changes: 4 additions & 0 deletions src/render/painter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {drawLine} from './draw_line';
import {drawFill} from './draw_fill';
import {drawFillExtrusion} from './draw_fill_extrusion';
import {drawHillshade} from './draw_hillshade';
import {drawColorRelief} from './draw_color_relief';
import {drawRaster} from './draw_raster';
import {drawBackground} from './draw_background';
import {drawDebug, drawDebugPadding, selectDebugSource} from './draw_debug';
Expand Down Expand Up @@ -56,6 +57,7 @@ import {isLineStyleLayer} from '../style/style_layer/line_style_layer';
import {isFillStyleLayer} from '../style/style_layer/fill_style_layer';
import {isFillExtrusionStyleLayer} from '../style/style_layer/fill_extrusion_style_layer';
import {isHillshadeStyleLayer} from '../style/style_layer/hillshade_style_layer';
import {isColorReliefStyleLayer} from '../style/style_layer/color_relief_style_layer';
import {isRasterStyleLayer} from '../style/style_layer/raster_style_layer';
import {isBackgroundStyleLayer} from '../style/style_layer/background_style_layer';
import {isCustomStyleLayer} from '../style/style_layer/custom_style_layer';
Expand Down Expand Up @@ -671,6 +673,8 @@ export class Painter {
drawFillExtrusion(painter, sourceCache, layer, coords, renderOptions);
} else if (isHillshadeStyleLayer(layer)) {
drawHillshade(painter, sourceCache, layer, coords, renderOptions);
} else if (isColorReliefStyleLayer(layer)) {
drawColorRelief(painter, sourceCache, layer, coords, renderOptions);
} else if (isRasterStyleLayer(layer)) {
drawRaster(painter, sourceCache, layer, coords, renderOptions);
} else if (isBackgroundStyleLayer(layer)) {
Expand Down
53 changes: 53 additions & 0 deletions src/render/program/color_relief_program.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
Uniform1i,
Uniform1f,
Uniform2f,
Uniform4f,
UniformFloatArray,
UniformColorArray
} from '../uniform_binding';

import type {Context} from '../../gl/context';
import type {UniformValues, UniformLocations} from '../uniform_binding';
import type {ColorReliefStyleLayer} from '../../style/style_layer/color_relief_style_layer';
import type {DEMData} from '../../data/dem_data';

export type ColorReliefUniformsType = {
'u_image': Uniform1i;
'u_unpack': Uniform4f;
'u_dimension': Uniform2f;
'u_elevation_stops': UniformFloatArray;
'u_color_stops': UniformColorArray;
'u_opacity': Uniform1f;
};

const colorReliefUniforms = (context: Context, locations: UniformLocations): ColorReliefUniformsType => ({
'u_image': new Uniform1i(context, locations.u_image),
'u_unpack': new Uniform4f(context, locations.u_unpack),
'u_dimension': new Uniform2f(context, locations.u_dimension),
'u_elevation_stops': new UniformFloatArray(context, locations.u_elevation_stops),
'u_color_stops': new UniformColorArray(context, locations.u_color_stops),
'u_opacity': new Uniform1f(context, locations.u_opacity)
});

const colorReliefUniformValues = (
layer: ColorReliefStyleLayer,
dem: DEMData,
maxLength: number
): UniformValues<ColorReliefUniformsType> => {

const colorRamp = layer.getColorRamp(maxLength);
return {
'u_image': 0,
'u_unpack': dem.getUnpackVector(),
'u_dimension': [dem.stride, dem.stride],
'u_elevation_stops': colorRamp.elevationStops,
'u_color_stops': colorRamp.colorStops,
'u_opacity': layer.paint.get('color-relief-opacity')
};
};

export {
colorReliefUniforms,
colorReliefUniformValues,
};
2 changes: 2 additions & 0 deletions src/render/program/program_uniforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {collisionUniforms, collisionCircleUniforms} from './collision_program';
import {debugUniforms} from './debug_program';
import {heatmapUniforms, heatmapTextureUniforms} from './heatmap_program';
import {hillshadeUniforms, hillshadePrepareUniforms} from './hillshade_program';
import {colorReliefUniforms} from './color_relief_program';
import {lineUniforms, lineGradientUniforms, linePatternUniforms, lineSDFUniforms} from './line_program';
import {rasterUniforms} from './raster_program';
import {symbolIconUniforms, symbolSDFUniforms, symbolTextAndIconUniforms} from './symbol_program';
Expand Down Expand Up @@ -33,6 +34,7 @@ export const programUniforms = {
heatmapTexture: heatmapTextureUniforms,
hillshade: hillshadeUniforms,
hillshadePrepare: hillshadePrepareUniforms,
colorRelief: colorReliefUniforms,
line: lineUniforms,
lineGradient: lineGradientUniforms,
linePattern: linePatternUniforms,
Expand Down
3 changes: 2 additions & 1 deletion src/render/render_to_texture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ const LAYERS: { [keyof in StyleLayer['type']]?: boolean } = {
fill: true,
line: true,
raster: true,
hillshade: true
hillshade: true,
'color-relief': true
};

/**
Expand Down
41 changes: 41 additions & 0 deletions src/shaders/color_relief.fragment.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
uniform sampler2D u_image;
uniform vec4 u_unpack;
uniform float u_elevation_stops[NUM_ELEVATION_STOPS];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any thoughts on defining an upper limit on the number of permissible stops? I suspect performance would be a problem before hitting the uniform limit, but it is technically possible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The minimum required value for GL_MAX_FRAGMENT_UNIFORM_VECTORS is 16, so it does seem wise to add an upper limit.

16 is too low to be a generic limit, so I'm thinking of adding a check against GL_MAX_FRAGMENT_UNIFORM_VECTORS, and if the color ramp is too long, re-sample it and print a warning "Color relief layer may not render properly". This warning would only show up on low-end systems, so might not be evident to a developer who doesn't have access to such a system.

Also, the current implementation requires the colorramp length to be 1 + a power of 2, (e.g 2, 3, 5, 9, 17, 33, 65, ...). I'll see if I can rework the binary search to avoid this requirement (which would make the actual limit 9 on a low end system GL_MAX_FRAGMENT_UNIFORM_VECTORS=16).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like the existing implementation didn't require the colorramp length to be 2^N + 1, so I've removed that requirement.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've updated to remap the color ramp if the length exceeds GL_MAX_FRAGMENT_UNIFORM_VECTORS. Note this will change the appearance of non-smooth colorramps when it happens.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the limit is indeed rather low for WebGL. For WebGL 2, following the specification should defer to ES 3.0 which specifies at least 224 vectors. In practice, both WebGL and WebGL 2 report 1024 on my desktop machines (laptops) and 256 on Snapdragon 8 Gen 2 and Snapdragon 865 Android devices. It stands to reason devices which only support the original WebGL minimum are rather dated by this point but I've been (unpleasantly) surprised before by strange hardware configurations.

It may be possible to win back some uniform space for the low end by packing the colors into a 1D RGBA atlas and sending the stops as pairs of (elevation, color ramp U coordinate), trading uniform space for some extra texture samples.

uniform vec4 u_color_stops[NUM_ELEVATION_STOPS];
uniform float u_opacity;

in vec2 v_pos;

float getElevation(vec2 coord) {
// Convert encoded elevation value to meters
vec4 data = texture(u_image, coord) * 255.0;
data.a = -1.0;
return dot(data, u_unpack);
}

void main() {
float el = getElevation(v_pos);

// Binary search
int r = (NUM_ELEVATION_STOPS - 1);
int l = 0;
while(r - l > 1)
{
int m = (r + l) / 2;
if(el < u_elevation_stops[m])
{
r = m;
}
else
{
l = m;
}
}
fragColor = u_opacity*mix(u_color_stops[l],
u_color_stops[r],
clamp((el - u_elevation_stops[l])/(u_elevation_stops[r]-u_elevation_stops[l]), 0.0, 1.0));

#ifdef OVERDRAW_INSPECTOR
fragColor = vec4(1.0);
#endif
}
20 changes: 20 additions & 0 deletions src/shaders/color_relief.vertex.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
uniform vec2 u_dimension;

in vec2 a_pos;

out vec2 v_pos;

void main() {
gl_Position = projectTile(a_pos, a_pos);
highp vec2 epsilon = 1.0 / u_dimension;
float scale = (u_dimension.x - 2.0) / u_dimension.x;
v_pos = (a_pos / 8192.0) * scale + epsilon;
// North pole
if (a_pos.y < -32767.5) {
v_pos.y = 0.0;
}
// South pole
if (a_pos.y > 32766.5) {
v_pos.y = 1.0;
}
}
3 changes: 3 additions & 0 deletions src/shaders/shaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import collisionBoxFrag from './collision_box.fragment.glsl.g';
import collisionBoxVert from './collision_box.vertex.glsl.g';
import collisionCircleFrag from './collision_circle.fragment.glsl.g';
import collisionCircleVert from './collision_circle.vertex.glsl.g';
import colorReliefFrag from './color_relief.fragment.glsl.g';
import colorReliefVert from './color_relief.vertex.glsl.g';
import debugFrag from './debug.fragment.glsl.g';
import debugVert from './debug.vertex.glsl.g';
import depthVert from './depth.vertex.glsl.g';
Expand Down Expand Up @@ -87,6 +89,7 @@ export const shaders = {
heatmapTexture: prepare(heatmapTextureFrag, heatmapTextureVert),
collisionBox: prepare(collisionBoxFrag, collisionBoxVert),
collisionCircle: prepare(collisionCircleFrag, collisionCircleVert),
colorRelief: prepare(colorReliefFrag, colorReliefVert),
debug: prepare(debugFrag, debugVert),
depth: prepare(clippingMaskFrag, depthVert),
fill: prepare(fillFrag, fillVert),
Expand Down
3 changes: 3 additions & 0 deletions src/style/create_style_layer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {CircleStyleLayer} from './style_layer/circle_style_layer';
import {HeatmapStyleLayer} from './style_layer/heatmap_style_layer';
import {HillshadeStyleLayer} from './style_layer/hillshade_style_layer';
import {ColorReliefStyleLayer} from './style_layer/color_relief_style_layer';
import {FillStyleLayer} from './style_layer/fill_style_layer';
import {FillExtrusionStyleLayer} from './style_layer/fill_extrusion_style_layer';
import {LineStyleLayer} from './style_layer/line_style_layer';
Expand All @@ -20,6 +21,8 @@ export function createStyleLayer(layer: LayerSpecification | CustomLayerInterfac
return new BackgroundStyleLayer(layer);
case 'circle':
return new CircleStyleLayer(layer);
case 'color-relief':
return new ColorReliefStyleLayer(layer);
case 'fill':
return new FillStyleLayer(layer);
case 'fill-extrusion':
Expand Down
Loading