Skip to content
Merged
Show file tree
Hide file tree
Changes from 54 commits
Commits
Show all changes
60 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
f889b77
add DEMData::pack() function and unit tests
NathanMOlson May 19, 2025
a2dbc6d
version of color relief that uses textures instead of uniforms for co…
NathanMOlson May 20, 2025
d9be1a8
get the right size limit for color ramp
NathanMOlson May 20, 2025
6840b70
update expectedBytes
NathanMOlson May 20, 2025
f59ead7
fix off-by-one error
NathanMOlson May 20, 2025
0f61df7
use texture lookup instead of shader interpolation
NathanMOlson May 20, 2025
0b96139
function nameing and comment formating
NathanMOlson May 20, 2025
ca69c39
defer color ramp creation until getColorRamp
NathanMOlson May 20, 2025
8e05248
Merge branch 'hypsometric2' into hypsometric3
NathanMOlson May 20, 2025
71b0e5a
update unit tests
NathanMOlson May 20, 2025
ebd061d
Merge branch 'hypsometric2' into hypsometric3
NathanMOlson May 20, 2025
a583485
remove unneeded shader #define
NathanMOlson May 20, 2025
60c1bf4
fix colorRampLength calculation
NathanMOlson May 20, 2025
dd43722
Merge branch 'main' into hypsometric2
NathanMOlson May 20, 2025
8a3513b
Merge branch 'hypsometric2' into hypsometric3
NathanMOlson May 20, 2025
b200ba1
use first available textures
NathanMOlson May 20, 2025
df1cf4c
Merge branch 'main' into hypsometric3
NathanMOlson May 20, 2025
aa7b450
fix expectedBytes
NathanMOlson May 20, 2025
1677f63
enable color ramp to be dynamically changes using map.setPaintProperty()
NathanMOlson May 21, 2025
560163a
reduce indentation
NathanMOlson May 21, 2025
c562d13
remove unneeded member variable colorRamp and combine _createColorRam…
NathanMOlson May 21, 2025
d9d8738
fix build
NathanMOlson May 21, 2025
a71d5d4
add RGBAImage::setPixel()
NathanMOlson May 21, 2025
3c1cfaf
fix lint
NathanMOlson May 21, 2025
c78c931
Merge branch 'main' into hypsometric3
HarelM May 21, 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
### ✨ Features and improvements
- Add `setGlobalStateProperty()` and `getGlobalState()` to the map public API ([#5613](https://github.com/maplibre/maplibre-gl-js/pull/5613))
- Improve tile frustum culling for globe, leading to better performance and faster loading times. ([#5865](https://github.com/maplibre/maplibre-gl-js/pull/5865))
- 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.
57 changes: 57 additions & 0 deletions src/data/dem_data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,3 +236,60 @@ describe('DEMData#getImage', () => {
test('Image is correctly returned - terrarium', testGetPixels(terrariumDEM, imageData));
test('Image is correctly returned - custom', testGetPixels(customDEM, imageData));
});

describe('DEMData pack and unpack', () => {
const imageData = createMockImage(4, 4);
test('mapbox', () => {
const dem = new DEMData('0', imageData, 'mapbox');
expect(dem.unpack(123, 177, 215)).toEqual(800645.5);
expect(dem.pack(800645.5)).toEqual({r: 123, g: 177, b: 215});

expect(dem.unpack(0, 0, 0)).toEqual(-10000);
expect(dem.pack(-10000)).toEqual({r: 0, g: 0, b: 0});

expect(dem.unpack(1, 1, 1)).toBeCloseTo(-3420.7);
expect(dem.pack(-3420.7)).toEqual({r: 1, g: 1, b: 1});

expect(dem.unpack(255, 255, 255)).toEqual(1667721.5);
expect(dem.pack(1667721.5)).toEqual({r: 255, g: 255, b: 255});

expect(dem.unpack(255, 0, 255)).toEqual(1661193.5);
expect(dem.pack(1661193.5)).toEqual({r: 255, g: 0, b: 255});
});

test('terrarium', () => {
const dem = new DEMData('0', imageData, 'terrarium');
expect(dem.unpack(123, 177, 215)).toEqual(-1102.16015625);
expect(dem.pack(-1102.16015625)).toEqual({r: 123, g: 177, b: 215});

expect(dem.unpack(0, 0, 0)).toEqual(-32768);
expect(dem.pack(-32768)).toEqual({r: 0, g: 0, b: 0});

expect(dem.unpack(1, 1, 1)).toEqual(-32510.99609375);
expect(dem.pack(-32510.99609375)).toEqual({r: 1, g: 1, b: 1});

expect(dem.unpack(255, 255, 255)).toEqual(32767.99609375);
expect(dem.pack(32767.99609375)).toEqual({r: 255, g: 255, b: 255});

expect(dem.unpack(255, 0, 255)).toEqual(32512.99609375);
expect(dem.pack(32512.99609375)).toEqual({r: 255, g: 0, b: 255});
});

test('custom', () => {
const dem = new DEMData('0', imageData, 'custom', 0.25, 64, 16384, 7000.0);
expect(dem.unpack(123, 177, 215)).toEqual(3526918.75);
expect(dem.pack(3526918.75)).toEqual({r: 123, g: 177, b: 215});

expect(dem.unpack(0, 0, 0)).toEqual(-7000);
expect(dem.pack(-7000)).toEqual({r: 0, g: 0, b: 0});

expect(dem.unpack(1, 1, 1)).toEqual(9448.25);
expect(dem.pack(9448.25)).toEqual({r: 1, g: 1, b: 1});

expect(dem.unpack(255, 255, 255)).toEqual(4187303.75);
expect(dem.pack(4187303.75)).toEqual({r: 255, g: 255, b: 255});

expect(dem.unpack(255, 0, 255)).toEqual(4170983.75);
expect(dem.pack(4170983.75)).toEqual({r: 255, g: 0, b: 255});
});
});
18 changes: 18 additions & 0 deletions src/data/dem_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,10 @@ export class DEMData {
return (r * this.redFactor + g * this.greenFactor + b * this.blueFactor - this.baseShift);
}

pack(v: number): {r: number; g: number; b: number} {
return packDEMData(v, this.getUnpackVector());
}

getPixels() {
return new RGBAImage({width: this.stride, height: this.stride}, new Uint8Array(this.data.buffer));
}
Expand Down Expand Up @@ -168,4 +172,18 @@ export class DEMData {
}
}

export function packDEMData(v: number, unpackVector: number[]): {r: number; g: number; b: number} {
const redFactor = unpackVector[0];
const greenFactor = unpackVector[1];
const blueFactor = unpackVector[2];
const baseShift = unpackVector[3];
const minScale = Math.min(redFactor, greenFactor, blueFactor);
const vScaled = Math.round((v + baseShift)/minScale);
return {
r: Math.floor(vScaled*minScale/redFactor) % 256,
g: Math.floor(vScaled*minScale/greenFactor) % 256,
b: Math.floor(vScaled*minScale/blueFactor) % 256
};
}

register('DEMData', DEMData);
107 changes: 107 additions & 0 deletions src/render/draw_color_relief.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
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 program = painter.useProgram('colorRelief');
const align = !painter.options.moving;

let firstTile = true;

for (const coord of coords) {
const tile = sourceCache.getTile(coord);
const dem = tile.dem;
if(firstTile) {
const maxLength = gl.getParameter(gl.MAX_TEXTURE_SIZE);
const {elevationTexture, colorTexture} = layer.getColorRampTextures(context, maxLength, dem.getUnpackVector());
context.activeTexture.set(gl.TEXTURE1);
elevationTexture.bind(gl.NEAREST, gl.CLAMP_TO_EDGE);
context.activeTexture.set(gl.TEXTURE4);
colorTexture.bind(gl.LINEAR, gl.CLAMP_TO_EDGE);
firstTile = false;
}

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), 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
49 changes: 49 additions & 0 deletions src/render/program/color_relief_program.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import {
Uniform1i,
Uniform1f,
Uniform2f,
Uniform4f
} 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': Uniform1i;
'u_color_stops': Uniform1i;
'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 Uniform1i(context, locations.u_elevation_stops),
'u_color_stops': new Uniform1i(context, locations.u_color_stops),
'u_opacity': new Uniform1f(context, locations.u_opacity)
});

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

return {
'u_image': 0,
'u_unpack': dem.getUnpackVector(),
'u_dimension': [dem.stride, dem.stride],
'u_elevation_stops': 1,
'u_color_stops': 4,
'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
56 changes: 56 additions & 0 deletions src/shaders/color_relief.fragment.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
uniform sampler2D u_image;
uniform vec4 u_unpack;
uniform sampler2D u_elevation_stops;
uniform sampler2D u_color_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);
}

float getElevationStop(int stop) {
// Convert encoded elevation value to meters
float x = (float(stop)+0.5)/float(textureSize(u_elevation_stops, 0)[0]);
vec4 data = texture(u_elevation_stops, vec2(x, 0)) * 255.0;
data.a = -1.0;
return dot(data, u_unpack);
}

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

int num_elevation_stops = textureSize(u_elevation_stops, 0)[0];

// Binary search
int r = (num_elevation_stops - 1);
int l = 0;
float el_l = getElevationStop(l);
float el_r = getElevationStop(r);
while(r - l > 1)
{
int m = (r + l) / 2;
float el_m = getElevationStop(m);
if(el < el_m)
{
r = m;
el_r = el_m;
}
else
{
l = m;
el_l = el_m;
}
}

float x = (float(l) + (el - el_l) / (el_r - el_l) + 0.5)/float(textureSize(u_color_stops, 0)[0]);
fragColor = u_opacity*texture(u_color_stops, vec2(x, 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;
}
}
Loading