Skip to content

Commit

Permalink
Globe custom layers: draw globe into depth buffer (#4838)
Browse files Browse the repository at this point in the history
* Add new "3d" pass for drawing layers with actual depth buffer, prerender globe into depth buffer if needed

* Adapt fill extrusion for new "3d" pass

* Fix tests

* Revert fill extrusion changes

* Update build size

* Add changelog entry

* Update test/examples/globe-3d-model.html

Co-authored-by: Harel M <[email protected]>

* Rename depthModeFor3D and depthModeForSublayer

* Use already existing mechanism for drawing 3D, simplify fill-extrusion

* Add tent-3d-globe custom layer to render tests

* Fix globe custom tiles example error + fix layer order

* Fix tent-3d-globe custom layer, add test that covers it

* Add globe custom layer occlusion render test

* Update build size

* Remove globe tent 3d test file from examples

* Add more expected render test images for tent-3d-globe custom layer

* Move globe custom layer tests to projection/globe

* Update build size

---------

Co-authored-by: Harel M <[email protected]>
  • Loading branch information
kubapelc and HarelM authored Oct 21, 2024
1 parent 92b86d5 commit 6c99b6d
Show file tree
Hide file tree
Showing 30 changed files with 383 additions and 99 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
### 🐞 Bug fixes

- Fix text not being hidden behind the globe when overlap mode was set to `always` ([#4802](https://github.com/maplibre/maplibre-gl-js/issues/4802))
- Fix 3D models in custom layers not being properly occluded by the globe ([#4817](https://github.com/maplibre/maplibre-gl-js/issues/4817))
- Fix a single white frame being displayed when the map internally transitions from mercator to globe projection ([#4816](https://github.com/maplibre/maplibre-gl-js/issues/4816))
- Fix loading of RTL plugin version 0.3.0 ([#4860](https://github.com/maplibre/maplibre-gl-js/pull/4860))

Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export function drawBackground(painter: Painter, sourceCache: SourceCache, layer
if (painter.renderPass !== pass) return;

const stencilMode = StencilMode.disabled;
const depthMode = painter.depthModeForSublayer(0, pass === 'opaque' ? DepthMode.ReadWrite : DepthMode.ReadOnly);
const depthMode = painter.getDepthModeForSublayer(0, pass === 'opaque' ? DepthMode.ReadWrite : DepthMode.ReadOnly);
const colorMode = painter.colorModeForRenderPass();
const program = painter.useProgram(image ? 'backgroundPattern' : 'background');
const tileIDs = coords ? coords : transform.coveringTiles({tileSize, terrain: painter.style.map.terrain});
Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_circle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export function drawCircles(painter: Painter, sourceCache: SourceCache, layer: C
const gl = context.gl;
const transform = painter.transform;

const depthMode = painter.depthModeForSublayer(0, DepthMode.ReadOnly);
const depthMode = painter.getDepthModeForSublayer(0, DepthMode.ReadOnly);
// Turn off stencil testing to allow circles to be drawn across boundaries,
// so that large circles are not clipped to tiles
const stencilMode = StencilMode.disabled;
Expand Down
10 changes: 5 additions & 5 deletions src/render/draw_custom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ export function drawCustom(painter: Painter, sourceCache: SourceCache, layer: Cu
defaultProjectionData: projectionData,
};

if (painter.renderPass === 'offscreen') {
const renderingMode = implementation.renderingMode ? implementation.renderingMode : '2d';

if (painter.renderPass === 'offscreen') {
const prerender = implementation.prerender;
if (prerender) {
painter.setCustomLayerDefaults();
Expand All @@ -40,17 +41,16 @@ export function drawCustom(painter: Painter, sourceCache: SourceCache, layer: Cu
context.setDirty();
painter.setBaseState();
}

} else if (painter.renderPass === 'translucent') {

painter.setCustomLayerDefaults();

context.setColorMode(painter.colorModeForRenderPass());
context.setStencilMode(StencilMode.disabled);

const depthMode = implementation.renderingMode === '3d' ?
new DepthMode(painter.context.gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D) :
painter.depthModeForSublayer(0, DepthMode.ReadOnly);
const depthMode = renderingMode === '3d' ?
painter.getDepthModeFor3D() :
painter.getDepthModeForSublayer(0, DepthMode.ReadOnly);

context.setDepthMode(depthMode);

Expand Down
4 changes: 2 additions & 2 deletions src/render/draw_fill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function drawFill(painter: Painter, sourceCache: SourceCache, layer: Fill

// Draw fill
if (painter.renderPass === pass) {
const depthMode = painter.depthModeForSublayer(
const depthMode = painter.getDepthModeForSublayer(
1, painter.renderPass === 'opaque' ? DepthMode.ReadWrite : DepthMode.ReadOnly);
drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode, false);
}
Expand All @@ -52,7 +52,7 @@ export function drawFill(painter: Painter, sourceCache: SourceCache, layer: Fill
// or stroke color is translucent. If we wouldn't clip to outside
// the current shape, some pixels from the outline stroke overlapped
// the (non-antialiased) fill.
const depthMode = painter.depthModeForSublayer(
const depthMode = painter.getDepthModeForSublayer(
layer.getPaintProperty('fill-outline-color') ? 2 : 0, DepthMode.ReadOnly);
drawFillTiles(painter, sourceCache, layer, coords, depthMode, colorMode, true);
}
Expand Down
5 changes: 2 additions & 3 deletions src/render/draw_fill_extrusion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ function drawExtrusionTiles(
const opacity = layer.paint.get('fill-extrusion-opacity');
const constantPattern = patternProperty.constantOr(null);
const transform = painter.transform;
const globeCameraPosition = transform.cameraPosition;

for (const coord of coords) {
const tile = source.getTile(coord);
Expand Down Expand Up @@ -92,8 +91,8 @@ function drawExtrusionTiles(

const shouldUseVerticalGradient = layer.paint.get('fill-extrusion-vertical-gradient');
const uniformValues = image ?
fillExtrusionPatternUniformValues(painter, shouldUseVerticalGradient, opacity, translate, globeCameraPosition, coord, crossfade, tile) :
fillExtrusionUniformValues(painter, shouldUseVerticalGradient, opacity, translate, globeCameraPosition);
fillExtrusionPatternUniformValues(painter, shouldUseVerticalGradient, opacity, translate, coord, crossfade, tile) :
fillExtrusionUniformValues(painter, shouldUseVerticalGradient, opacity, translate);

program.draw(context, context.gl.TRIANGLES, depthMode, stencilMode, colorMode, CullFaceMode.backCCW,
uniformValues, terrainData, projectionData, layer.id, bucket.layoutVertexBuffer, bucket.indexBuffer,
Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_hillshade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function drawHillshade(painter: Painter, sourceCache: SourceCache, layer:
const projection = painter.style.projection;
const useSubdivision = projection.useSubdivision;

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

if (painter.renderPass === 'offscreen') {
Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export function drawLine(painter: Painter, sourceCache: SourceCache, layer: Line
const width = layer.paint.get('line-width');
if (opacity.constantOr(1) === 0 || width.constantOr(1) === 0) return;

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

const dasharray = layer.paint.get('line-dasharray');
Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_raster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ function drawTiles(
for (const coord of coords) {
// Set the lower zoom level to sublayer 0, and higher zoom levels to higher sublayers
// Use gl.LESS to prevent double drawing in areas where tiles overlap.
const depthMode = painter.depthModeForSublayer(coord.overscaledZ - minTileZ,
const depthMode = painter.getDepthModeForSublayer(coord.overscaledZ - minTileZ,
layer.paint.get('raster-opacity') === 1 ? DepthMode.ReadWrite : DepthMode.ReadOnly, gl.LESS);

const tile = sourceCache.getTile(coord);
Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_symbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ function drawLayerSymbols(
const hasSortKey = !layer.layout.get('symbol-sort-key').isConstant();
let sortFeaturesByKey = false;

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

const hasVariablePlacement = layer._unevaluatedLayout.hasValue('text-variable-anchor') || layer._unevaluatedLayout.hasValue('text-variable-anchor-offset');

Expand Down
2 changes: 1 addition & 1 deletion src/render/draw_terrain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function drawTerrain(painter: Painter, terrain: Terrain, tiles: Array<Tile>) {
const gl = context.gl;
const tr = painter.transform;
const colorMode = painter.colorModeForRenderPass();
const depthMode = new DepthMode(gl.LEQUAL, DepthMode.ReadWrite, painter.depthRangeFor3D);
const depthMode = painter.getDepthModeFor3D();
const program = painter.useProgram('terrain');
const mesh = terrain.getTerrainMesh();

Expand Down
45 changes: 44 additions & 1 deletion src/render/painter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,34 @@ export class Painter {
}
}

/**
* Fills the depth buffer with the geometry of all supplied tiles.
* Does not change the color buffer or the stencil buffer.
*/
_renderTilesDepthBuffer() {
const context = this.context;
const gl = context.gl;
const projection = this.style.projection;
const transform = this.transform;

const program = this.useProgram('depth');
const depthMode = this.getDepthModeFor3D();
const tileIDs = transform.coveringTiles({tileSize: transform.tileSize});

// tiles are usually supplied in ascending order of z, then y, then x
for (const tileID of tileIDs) {
const terrainData = this.style.map.terrain && this.style.map.terrain.getTerrainData(tileID);
const mesh = projection.getMeshFromTileID(this.context, tileID.canonical, true, true, 'raster');

const projectionData = transform.getProjectionData(tileID);

program.draw(context, gl.TRIANGLES, depthMode, StencilMode.disabled,
ColorMode.disabled, CullFaceMode.backCCW, null,
terrainData, projectionData, '$clipping', mesh.vertexBuffer,
mesh.indexBuffer, mesh.segments);
}
}

stencilModeFor3D(): StencilMode {
this.currentStencilSource = undefined;

Expand Down Expand Up @@ -406,12 +434,16 @@ export class Painter {
}
}

depthModeForSublayer(n: number, mask: DepthMaskType, func?: DepthFuncType | null): Readonly<DepthMode> {
getDepthModeForSublayer(n: number, mask: DepthMaskType, func?: DepthFuncType | null): Readonly<DepthMode> {
if (!this.opaquePassEnabledForLayer()) return DepthMode.disabled;
const depth = 1 - ((1 + this.currentLayer) * this.numSublayers + n) * this.depthEpsilon;
return new DepthMode(func || this.context.gl.LEQUAL, mask, [depth, depth]);
}

getDepthModeFor3D(): Readonly<DepthMode> {
return new DepthMode(this.context.gl.LEQUAL, DepthMode.ReadWrite, this.depthRangeFor3D);
}

/*
* The opaque pass and 3D layers both use the depth buffer.
* Layers drawn above 3D layers need to be drawn using the
Expand Down Expand Up @@ -525,12 +557,23 @@ export class Painter {
// Draw all other layers bottom-to-top.
this.renderPass = 'translucent';

let globeDepthRendered = false;

for (this.currentLayer = 0; this.currentLayer < layerIds.length; this.currentLayer++) {
const layer = this.style._layers[layerIds[this.currentLayer]];
const sourceCache = sourceCaches[layer.source];

if (this.renderToTexture && this.renderToTexture.renderLayer(layer)) continue;

if (!this.opaquePassEnabledForLayer() && !globeDepthRendered) {
globeDepthRendered = true;
// Render the globe sphere into the depth buffer - but only if globe is enabled and terrain is disabled.
// There should be no need for explicitly writing tile depths when terrain is enabled.
if (this.style.projection.name === 'globe' && !this.style.map.terrain) {
this._renderTilesDepthBuffer();
}
}

// For symbol layers in the translucent pass, we add extra tiles to the renderable set
// for cross-tile symbol fading. Symbol layers don't use tile clipping, so no need to render
// separate clipping masks
Expand Down
9 changes: 1 addition & 8 deletions src/render/program/fill_extrusion_program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export type FillExtrusionUniformsType = {
'u_vertical_gradient': Uniform1f;
'u_opacity': Uniform1f;
'u_fill_translate': Uniform2f;
'u_camera_pos_globe': Uniform3f;
};

export type FillExtrusionPatternUniformsType = {
Expand All @@ -36,7 +35,6 @@ export type FillExtrusionPatternUniformsType = {
'u_vertical_gradient': Uniform1f;
'u_opacity': Uniform1f;
'u_fill_translate': Uniform2f;
'u_camera_pos_globe': Uniform3f;
// pattern uniforms:
'u_texsize': Uniform2f;
'u_image': Uniform1i;
Expand All @@ -54,7 +52,6 @@ const fillExtrusionUniforms = (context: Context, locations: UniformLocations): F
'u_vertical_gradient': new Uniform1f(context, locations.u_vertical_gradient),
'u_opacity': new Uniform1f(context, locations.u_opacity),
'u_fill_translate': new Uniform2f(context, locations.u_fill_translate),
'u_camera_pos_globe': new Uniform3f(context, locations.u_camera_pos_globe)
});

const fillExtrusionPatternUniforms = (context: Context, locations: UniformLocations): FillExtrusionPatternUniformsType => ({
Expand All @@ -66,7 +63,6 @@ const fillExtrusionPatternUniforms = (context: Context, locations: UniformLocati
'u_height_factor': new Uniform1f(context, locations.u_height_factor),
'u_opacity': new Uniform1f(context, locations.u_opacity),
'u_fill_translate': new Uniform2f(context, locations.u_fill_translate),
'u_camera_pos_globe': new Uniform3f(context, locations.u_camera_pos_globe),
// pattern uniforms
'u_image': new Uniform1i(context, locations.u_image),
'u_texsize': new Uniform2f(context, locations.u_texsize),
Expand All @@ -81,7 +77,6 @@ const fillExtrusionUniformValues = (
shouldUseVerticalGradient: boolean,
opacity: number,
translate: [number, number],
cameraPosGlobe: vec3
): UniformValues<FillExtrusionUniformsType> => {
const light = painter.style.light;
const _lp = light.properties.get('position');
Expand All @@ -103,7 +98,6 @@ const fillExtrusionUniformValues = (
'u_vertical_gradient': +shouldUseVerticalGradient,
'u_opacity': opacity,
'u_fill_translate': translate,
'u_camera_pos_globe': cameraPosGlobe,
};
};

Expand All @@ -112,12 +106,11 @@ const fillExtrusionPatternUniformValues = (
shouldUseVerticalGradient: boolean,
opacity: number,
translate: [number, number],
cameraPosGlobe: vec3,
coord: OverscaledTileID,
crossfade: CrossfadeParameters,
tile: Tile
): UniformValues<FillExtrusionPatternUniformsType> => {
return extend(fillExtrusionUniformValues(painter, shouldUseVerticalGradient, opacity, translate, cameraPosGlobe),
return extend(fillExtrusionUniformValues(painter, shouldUseVerticalGradient, opacity, translate),
patternUniformValues(crossfade, painter, tile),
{
'u_height_factor': -Math.pow(2, coord.overscaledZ) / tile.tileSize / 8
Expand Down
1 change: 1 addition & 0 deletions src/render/program/program_uniforms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const programUniforms = {
collisionBox: collisionUniforms,
collisionCircle: collisionCircleUniforms,
debug: debugUniforms,
depth: emptyUniforms,
clippingMask: emptyUniforms,
heatmap: heatmapUniforms,
heatmapTexture: heatmapTextureUniforms,
Expand Down
2 changes: 2 additions & 0 deletions src/render/render_to_texture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {FillStyleLayer} from '../style/style_layer/fill_style_layer';
import {RasterStyleLayer} from '../style/style_layer/raster_style_layer';
import {HillshadeStyleLayer} from '../style/style_layer/hillshade_style_layer';
import {BackgroundStyleLayer} from '../style/style_layer/background_style_layer';
import {DepthMode} from '../gl/depth_mode';

describe('render to texture', () => {
const gl = document.createElement('canvas').getContext('webgl');
Expand Down Expand Up @@ -66,6 +67,7 @@ describe('render to texture', () => {
context: new Context(gl),
transform: {zoom: 10, calculatePosMatrix: () => {}, getProjectionData(_a) {}, calculateFogMatrix: () => {}},
colorModeForRenderPass: () => ColorMode.alphaBlended,
getDepthModeFor3D: () => DepthMode.disabled,
useProgram: () => { return {draw: () => { layersDrawn++; }}; },
_renderTileClippingMasks: () => {},
renderLayer: () => {}
Expand Down
9 changes: 9 additions & 0 deletions src/shaders/depth.vertex.glsl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
in vec2 a_pos;

void main() {
#ifdef GLOBE
gl_Position = projectTileFor3D(a_pos, 0.0);
#else
gl_Position = u_projection_matrix * vec4(a_pos, 0.0, 1.0);
#endif
}
41 changes: 0 additions & 41 deletions src/shaders/fill_extrusion.fragment.glsl
Original file line number Diff line number Diff line change
@@ -1,50 +1,9 @@
in vec4 v_color;

#ifdef GLOBE
in vec3 v_sphere_pos;
uniform vec3 u_camera_pos_globe;
uniform highp float u_projection_transition;
#endif

void main() {
fragColor = v_color;

#ifdef OVERDRAW_INSPECTOR
fragColor = vec4(1.0);
#endif

#ifdef GLOBE
// We want extruded geometry to be occluded by the planet.
// This would be trivial in any traditional 3D renderer with Z-buffer,
// but not in MapLibre, since Z-buffer is used to mask certain layers
// and optimize overdraw.
// One solution would be to draw the planet into Z-buffer just before
// rendering fill-extrusion layers, but what if another layer
// is drawn after that which makes use of this Z-buffer mask?
// We can't just trash the mask with out own Z values.
// So instead, the "Z-test" against the planet is done here,
// in the pixel shader.
// Luckily the planet is (assumed to be) a perfect sphere,
// so the ray-planet intersection test is quite simple.
// We discard any fragments that are occluded by the planet.

// Get nearest point along the ray from fragment to camera.
// Remember that planet center is at 0,0,0.
// Also clamp t to not consider intersections that happened behind the ray origin.
vec3 toPlanetCenter = -v_sphere_pos;
vec3 toCameraNormalized = normalize(u_camera_pos_globe - v_sphere_pos);
float t = dot(toPlanetCenter, toCameraNormalized);
vec3 nearest = v_sphere_pos + toCameraNormalized * max(t, 0.0);

// We want to remove planet occlusion during the animated transition out of globe view.
// Thus we animate the "radius" of the planet sphere used in ray-sphere collision.
// Radius of 1.0 is equal to full size planet (since we raycast against a unit sphere).
// Note that unsquared globeness is intentionally compared to squared distance from planet center,
// (because `dot(nearest, nearest)` returns the squared length of the vector `nearest`)
// effectively using sqrt(globeness) as the planet radius. This is done to make the animation look better.
float distance_to_planet_center_squared = dot(nearest, nearest);
if (distance_to_planet_center_squared < u_projection_transition) {
discard; // Ray intersected the planet.
}
#endif
}
6 changes: 0 additions & 6 deletions src/shaders/fill_extrusion.vertex.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ in vec4 a_normal_ed;

out vec4 v_color;

#ifdef GLOBE
out vec3 v_sphere_pos;
#endif

#pragma mapbox: define highp float base
#pragma mapbox: define highp float height

Expand Down Expand Up @@ -54,8 +50,6 @@ void main() {

#ifdef GLOBE
vec3 spherePos = projectToSphere(posInTile);
vec3 elevatedPos = spherePos * (1.0 + elevation / GLOBE_RADIUS);
v_sphere_pos = elevatedPos;
gl_Position = interpolateProjectionFor3D(posInTile, spherePos, elevation);
#else
gl_Position = u_projection_matrix * vec4(posInTile, elevation, 1.0);
Expand Down
Loading

0 comments on commit 6c99b6d

Please sign in to comment.