Skip to content

Commit

Permalink
Kubapelc/globe pr hillshade (#3979)
Browse files Browse the repository at this point in the history
* Import background layer changes from main vector globe branch

* Import hillshade layer changes from main vector globe branch

* Subdivision: explicit types

* Fix single-pixel seams in the oceans

* Add render test for background pattern on globe

* Refactor drawBackground

* Refactor drawHillshade

* Update build size

* Update globe background-pattern render test with results from CI

* Hillshade: refactor prepareHillshade

* Add a render test for fill layer seams fix
  • Loading branch information
kubapelc authored Apr 12, 2024
1 parent bf4a5b5 commit 66b2262
Show file tree
Hide file tree
Showing 28 changed files with 230 additions and 66 deletions.
9 changes: 5 additions & 4 deletions src/geo/projection/globe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const granularitySettingsGlobe: SubdivisionGranularitySetting = new SubdivisionG
// Always keep at least some subdivision on raster tiles, etc,
// otherwise they will be visibly warped at high zooms (before mercator transition).
// This si not needed on fill, because fill geometry tends to already be
// highly tesselated and granular at high zooms.
// highly tessellated and granular at high zooms.
tile: new SubdivisionGranularityExpression(128, 16),
});

Expand Down Expand Up @@ -441,13 +441,14 @@ export class GlobeProjection implements Projection {
}

public getMeshFromTileID(context: Context, canonical: CanonicalTileID, hasBorder: boolean): Mesh {
const granularity = granularitySettingsGlobe.tile.getGranularityForZoomLevel(canonical.z);
// Stencil granularity must match fill granularity
const granularity = granularitySettingsGlobe.fill.getGranularityForZoomLevel(canonical.z);
const north = (canonical.y === 0);
const south = (canonical.y === (1 << canonical.z) - 1);
return this.getMesh(context, granularity, hasBorder, north, south);
return this._getMesh(context, granularity, hasBorder, north, south);
}

public getMesh(context: Context, granularity: number, hasBorder: boolean, hasNorthEdge: boolean, hasSouthEdge: boolean): Mesh {
private _getMesh(context: Context, granularity: number, hasBorder: boolean, hasNorthEdge: boolean, hasSouthEdge: boolean): Mesh {
const key = this._getMeshKey(granularity, hasBorder, hasNorthEdge, hasSouthEdge);

if (key in this._tileMeshCache) {
Expand Down
23 changes: 19 additions & 4 deletions src/render/draw_background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export function drawBackground(painter: Painter, sourceCache: SourceCache, layer

const context = painter.context;
const gl = context.gl;
const projection = painter.style.map.projection;
const transform = painter.transform;
const tileSize = transform.tileSize;
const image = layer.paint.get('background-pattern');
Expand All @@ -39,15 +40,29 @@ export function drawBackground(painter: Painter, sourceCache: SourceCache, layer
}

const crossfade = layer.getCrossfadeParameters();

for (const tileID of tileIDs) {
const matrix = coords ? tileID.posMatrix : painter.transform.calculatePosMatrix(tileID.toUnwrapped());
const projectionData = projection.getProjectionData(tileID.canonical, matrix);

const uniformValues = image ?
backgroundPatternUniformValues(matrix, opacity, painter, image, {tileID, tileSize}, crossfade) :
backgroundUniformValues(matrix, opacity, color);
backgroundPatternUniformValues(opacity, painter, image, {tileID, tileSize}, crossfade) :
backgroundUniformValues(opacity, color);
const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(tileID);

// For globe rendering, background uses tile meshes *without* borders and no stencil clipping.
// This works assuming the tileIDs list contains only tiles of the same zoom level.
// This seems to always be the case for background layers, but I'm leaving this comment
// here in case this assumption is false in the future.

// In case background starts having tiny holes at tile boundaries, switch to meshes with borders
// and also enable stencil clipping. Make sure to render a proper tile clipping mask into stencil
// first though, as that doesn't seem to happen for background layers as of writing this.

const useMeshWithBorders = false;
const mesh = projection.getMeshFromTileID(context, tileID.canonical, useMeshWithBorders);
program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
uniformValues, terrainData, null, layer.id, painter.tileExtentBuffer,
painter.quadTriangleIndexBuffer, painter.tileExtentSegments);
uniformValues, terrainData, projectionData, layer.id,
mesh.vertexBuffer, mesh.indexBuffer, mesh.segments);
}
}
88 changes: 59 additions & 29 deletions src/render/draw_hillshade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import {StencilMode} from '../gl/stencil_mode';
import {DepthMode} from '../gl/depth_mode';
import {CullFaceMode} from '../gl/cull_face_mode';
import {ColorMode} from '../gl/color_mode';
import {Tile} from '../source/tile';
import {
hillshadeUniformValues,
hillshadeUniformPrepareValues
Expand All @@ -18,64 +17,95 @@ export function drawHillshade(painter: Painter, sourceCache: SourceCache, layer:
if (painter.renderPass !== 'offscreen' && painter.renderPass !== 'translucent') return;

const context = painter.context;
const projection = painter.style.map.projection;
const useSubdivision = projection.useSubdivision;

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

const [stencilModes, coords] = painter.renderPass === 'translucent' ?
painter.stencilConfigForOverlap(tileIDs) : [{}, tileIDs];

for (const coord of coords) {
const tile = sourceCache.getTile(coord);
if (typeof tile.needsHillshadePrepare !== 'undefined' && tile.needsHillshadePrepare && painter.renderPass === 'offscreen') {
prepareHillshade(painter, tile, layer, depthMode, StencilMode.disabled, colorMode);
} else if (painter.renderPass === 'translucent') {
renderHillshade(painter, coord, tile, layer, depthMode, stencilModes[coord.overscaledZ], colorMode);
if (painter.renderPass === 'offscreen') {
// Prepare tiles
prepareHillshade(painter, sourceCache, tileIDs, layer, depthMode, StencilMode.disabled, colorMode);
context.viewport.set([0, 0, painter.width, painter.height]);
} else if (painter.renderPass === 'translucent') {
// 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);
renderHillshade(painter, sourceCache, layer, coords, stencilBorderless, depthMode, colorMode, false); // draw without borders
renderHillshade(painter, sourceCache, layer, coords, stencilBorders, depthMode, colorMode, true); // draw with borders
} else {
// Simple rendering
const [stencil, coords] = painter.stencilConfigForOverlap(tileIDs);
renderHillshade(painter, sourceCache, layer, coords, stencil, depthMode, colorMode, false);
}
}

context.viewport.set([0, 0, painter.width, painter.height]);
}

function renderHillshade(
painter: Painter,
coord: OverscaledTileID,
tile: Tile,
sourceCache: SourceCache,
layer: HillshadeStyleLayer,
coords: Array<OverscaledTileID>,
stencilModes: {[_: number]: Readonly<StencilMode>},
depthMode: Readonly<DepthMode>,
stencilMode: Readonly<StencilMode>,
colorMode: Readonly<ColorMode>) {
colorMode: Readonly<ColorMode>,
useBorder: boolean
) {
const projection = painter.style.map.projection;
const context = painter.context;
const gl = context.gl;
const fbo = tile.fbo;
if (!fbo) return;

const program = painter.useProgram('hillshade');
const terrainData = painter.style.map.terrain && painter.style.map.terrain.getTerrainData(coord);
const align = !painter.options.moving;

for (const coord of coords) {
const tile = sourceCache.getTile(coord);
const fbo = tile.fbo;
if (!fbo) {
continue;
}
const mesh = projection.getMeshFromTileID(context, coord.canonical, useBorder);

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

context.activeTexture.set(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, fbo.colorAttachment.get());
context.activeTexture.set(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, fbo.colorAttachment.get());

const terrainCoord = terrainData ? coord : null;
program.draw(context, gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.disabled,
hillshadeUniformValues(painter, tile, layer, terrainCoord), terrainData, null, layer.id, painter.rasterBoundsBuffer,
painter.quadTriangleIndexBuffer, painter.rasterBoundsSegments);
const posMatrix = terrainData ? coord.posMatrix : painter.transform.calculatePosMatrix(tile.tileID.toUnwrapped(), align);
const projectionData = painter.style.map.projection.getProjectionData(coord.canonical, posMatrix);

program.draw(context, gl.TRIANGLES, depthMode, stencilModes[coord.overscaledZ], colorMode, CullFaceMode.disabled,
hillshadeUniformValues(painter, tile, layer), terrainData, projectionData, layer.id, mesh.vertexBuffer, mesh.indexBuffer, mesh.segments);
}
}

// hillshade rendering is done in two steps. the prepare step first calculates the slope of the terrain in the x and y
// directions for each pixel, and saves those values to a framebuffer texture in the r and g channels.
function prepareHillshade(
painter: Painter,
tile: Tile,
sourceCache: SourceCache,
tileIDs: Array<OverscaledTileID>,
layer: HillshadeStyleLayer,
depthMode: Readonly<DepthMode>,
stencilMode: Readonly<StencilMode>,
colorMode: Readonly<ColorMode>) {

const context = painter.context;
const gl = context.gl;
const dem = tile.dem;
if (dem && dem.data) {

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

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

if (!tile.needsHillshadePrepare) {
continue;
}

const tileSize = dem.dim;
const textureStride = dem.stride;

Expand Down
13 changes: 2 additions & 11 deletions src/render/program/background_program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import {
Uniform1i,
Uniform1f,
Uniform2f,
UniformColor,
UniformMatrix4f
UniformColor
} from '../uniform_binding';
import {extend} from '../../util/util';

Expand All @@ -15,16 +14,13 @@ import type {Color, ResolvedImage} from '@maplibre/maplibre-gl-style-spec';
import type {CrossFaded} from '../../style/properties';
import type {CrossfadeParameters} from '../../style/evaluation_parameters';
import type {OverscaledTileID} from '../../source/tile_id';
import {mat4} from 'gl-matrix';

export type BackgroundUniformsType = {
'u_matrix': UniformMatrix4f;
'u_opacity': Uniform1f;
'u_color': UniformColor;
};

export type BackgroundPatternUniformsType = {
'u_matrix': UniformMatrix4f;
'u_opacity': Uniform1f;
// pattern uniforms:
'u_image': Uniform1i;
Expand All @@ -44,13 +40,11 @@ export type BackgroundPatternUniformsType = {
};

const backgroundUniforms = (context: Context, locations: UniformLocations): BackgroundUniformsType => ({
'u_matrix': new UniformMatrix4f(context, locations.u_matrix),
'u_opacity': new Uniform1f(context, locations.u_opacity),
'u_color': new UniformColor(context, locations.u_color)
});

const backgroundPatternUniforms = (context: Context, locations: UniformLocations): BackgroundPatternUniformsType => ({
'u_matrix': new UniformMatrix4f(context, locations.u_matrix),
'u_opacity': new Uniform1f(context, locations.u_opacity),
'u_image': new Uniform1i(context, locations.u_image),
'u_pattern_tl_a': new Uniform2f(context, locations.u_pattern_tl_a),
Expand All @@ -68,14 +62,12 @@ const backgroundPatternUniforms = (context: Context, locations: UniformLocations
'u_tile_units_to_pixels': new Uniform1f(context, locations.u_tile_units_to_pixels)
});

const backgroundUniformValues = (matrix: mat4, opacity: number, color: Color): UniformValues<BackgroundUniformsType> => ({
'u_matrix': matrix,
const backgroundUniformValues = (opacity: number, color: Color): UniformValues<BackgroundUniformsType> => ({
'u_opacity': opacity,
'u_color': color
});

const backgroundPatternUniformValues = (
matrix: mat4,
opacity: number,
painter: Painter,
image: CrossFaded<ResolvedImage>,
Expand All @@ -87,7 +79,6 @@ const backgroundPatternUniformValues = (
): UniformValues<BackgroundPatternUniformsType> => extend(
bgPatternUniformValues(image, crossfade, painter, tile),
{
'u_matrix': matrix,
'u_opacity': opacity
}
);
Expand Down
5 changes: 0 additions & 5 deletions src/render/program/hillshade_program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import type {DEMData} from '../../data/dem_data';
import type {OverscaledTileID} from '../../source/tile_id';

export type HillshadeUniformsType = {
'u_matrix': UniformMatrix4f;
'u_image': Uniform1i;
'u_latrange': Uniform2f;
'u_light': Uniform2f;
Expand All @@ -38,7 +37,6 @@ export type HillshadePrepareUniformsType = {
};

const hillshadeUniforms = (context: Context, locations: UniformLocations): HillshadeUniformsType => ({
'u_matrix': new UniformMatrix4f(context, locations.u_matrix),
'u_image': new Uniform1i(context, locations.u_image),
'u_latrange': new Uniform2f(context, locations.u_latrange),
'u_light': new Uniform2f(context, locations.u_light),
Expand All @@ -59,7 +57,6 @@ const hillshadeUniformValues = (
painter: Painter,
tile: Tile,
layer: HillshadeStyleLayer,
coord: OverscaledTileID
): UniformValues<HillshadeUniformsType> => {
const shadow = layer.paint.get('hillshade-shadow-color');
const highlight = layer.paint.get('hillshade-highlight-color');
Expand All @@ -70,9 +67,7 @@ const hillshadeUniformValues = (
if (layer.paint.get('hillshade-illumination-anchor') === 'viewport') {
azimuthal -= painter.transform.angle;
}
const align = !painter.options.moving;
return {
'u_matrix': coord ? coord.posMatrix : painter.transform.calculatePosMatrix(tile.tileID.toUnwrapped(), align),
'u_image': 0,
'u_latrange': getTileLatRange(painter, tile.tileID),
'u_light': [layer.paint.get('hillshade-exaggeration'), azimuthal],
Expand Down
8 changes: 4 additions & 4 deletions src/render/subdivision.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,15 +419,15 @@ class Subdivider {
* Generates an outline for a given polygon, returns a list of arrays of line indices.
*/
private _generateOutline(polygon: Array<Array<Point>>): Array<Array<number>> {
const subdividedLines = [];
const subdividedLines: Array<Array<number>> = [];
for (const ring of polygon) {
const line = subdivideVertexLine(ring, this._granularity, true);
const pathIndices = this._pointArrayToIndices(line);
// Points returned by subdivideVertexLine are "path" waypoints,
// for example with indices 0 1 2 3 0.
// We need list of individual line segments for rendering,
// for example 0, 1, 1, 2, 2, 3, 3, 0.
const lineIndices = [];
const lineIndices: Array<number> = [];
for (let i = 1; i < pathIndices.length; i++) {
lineIndices.push(pathIndices[i - 1]);
lineIndices.push(pathIndices[i]);
Expand Down Expand Up @@ -590,7 +590,7 @@ class Subdivider {
this._initializeVertices(flattened);

// Subdivide triangles
let subdividedTriangles;
let subdividedTriangles: Array<number>;
try {
// At this point this._finalVertices is just flattened polygon points
const earcutResult = earcut(flattened, holeIndices);
Expand All @@ -601,7 +601,7 @@ class Subdivider {
}

// Subdivide lines
let subdividedLines = [];
let subdividedLines: Array<Array<number>> = [];
if (generateOutlineLines) {
subdividedLines = this._generateOutline(polygon);
}
Expand Down
4 changes: 1 addition & 3 deletions src/shaders/background.vertex.glsl
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
in vec2 a_pos;

uniform mat4 u_matrix;

void main() {
gl_Position = u_matrix * vec4(a_pos, 0, 1);
gl_Position = projectTile(a_pos);
}
3 changes: 1 addition & 2 deletions src/shaders/background_pattern.vertex.glsl
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
uniform mat4 u_matrix;
uniform vec2 u_pattern_size_a;
uniform vec2 u_pattern_size_b;
uniform vec2 u_pixel_coord_upper;
Expand All @@ -12,7 +11,7 @@ out vec2 v_pos_a;
out vec2 v_pos_b;

void main() {
gl_Position = u_matrix * vec4(a_pos, 0, 1);
gl_Position = projectTile(a_pos);

v_pos_a = get_pattern_pos(u_pixel_coord_upper, u_pixel_coord_lower, u_scale_a * u_pattern_size_a, u_tile_units_to_pixels, a_pos);
v_pos_b = get_pattern_pos(u_pixel_coord_upper, u_pixel_coord_lower, u_scale_b * u_pattern_size_b, u_tile_units_to_pixels, a_pos);
Expand Down
13 changes: 10 additions & 3 deletions src/shaders/hillshade.vertex.glsl
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
uniform mat4 u_matrix;

in vec2 a_pos;
in vec2 a_texture_pos;

out vec2 v_pos;

void main() {
gl_Position = u_matrix * vec4(a_pos, 0, 1);
v_pos = a_texture_pos / 8192.0;
gl_Position = projectTile(a_pos);
v_pos = a_pos / 8192.0;
// 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;
}
}
2 changes: 1 addition & 1 deletion test/build/min.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe('test min build', () => {
const decreaseQuota = 4096;

// feel free to update this value after you've checked that it has changed on purpose :-)
const expectedBytes = 809636;
const expectedBytes = 809907;

expect(actualBytes - expectedBytes).toBeLessThan(increaseQuota);
expect(expectedBytes - actualBytes).toBeLessThan(decreaseQuota);
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 66b2262

Please sign in to comment.