diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e111bf277..74194b5324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) diff --git a/src/render/draw_background.ts b/src/render/draw_background.ts index 1b0182b1af..1db26599eb 100644 --- a/src/render/draw_background.ts +++ b/src/render/draw_background.ts @@ -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}); diff --git a/src/render/draw_circle.ts b/src/render/draw_circle.ts index 02b91d1129..7960ff99f7 100644 --- a/src/render/draw_circle.ts +++ b/src/render/draw_circle.ts @@ -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; diff --git a/src/render/draw_custom.ts b/src/render/draw_custom.ts index 4cd102a1cb..c34ec6b5aa 100644 --- a/src/render/draw_custom.ts +++ b/src/render/draw_custom.ts @@ -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(); @@ -40,7 +41,6 @@ export function drawCustom(painter: Painter, sourceCache: SourceCache, layer: Cu context.setDirty(); painter.setBaseState(); } - } else if (painter.renderPass === 'translucent') { painter.setCustomLayerDefaults(); @@ -48,9 +48,9 @@ export function drawCustom(painter: Painter, sourceCache: SourceCache, layer: Cu 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); diff --git a/src/render/draw_fill.ts b/src/render/draw_fill.ts index 05edc7506d..0b23b5b572 100644 --- a/src/render/draw_fill.ts +++ b/src/render/draw_fill.ts @@ -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); } @@ -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); } diff --git a/src/render/draw_fill_extrusion.ts b/src/render/draw_fill_extrusion.ts index 0b2a4b8448..02c2d8f457 100644 --- a/src/render/draw_fill_extrusion.ts +++ b/src/render/draw_fill_extrusion.ts @@ -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); @@ -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, diff --git a/src/render/draw_hillshade.ts b/src/render/draw_hillshade.ts index 1b6412ca03..6469ea153c 100644 --- a/src/render/draw_hillshade.ts +++ b/src/render/draw_hillshade.ts @@ -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') { diff --git a/src/render/draw_line.ts b/src/render/draw_line.ts index 407812e3a5..0a8b0b1d9d 100644 --- a/src/render/draw_line.ts +++ b/src/render/draw_line.ts @@ -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'); diff --git a/src/render/draw_raster.ts b/src/render/draw_raster.ts index d505935b1e..ccff8b98ff 100644 --- a/src/render/draw_raster.ts +++ b/src/render/draw_raster.ts @@ -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); diff --git a/src/render/draw_symbol.ts b/src/render/draw_symbol.ts index 6d212f67ae..550ec1b77f 100644 --- a/src/render/draw_symbol.ts +++ b/src/render/draw_symbol.ts @@ -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'); diff --git a/src/render/draw_terrain.ts b/src/render/draw_terrain.ts index 90badfaec8..ce5bcd5e02 100644 --- a/src/render/draw_terrain.ts +++ b/src/render/draw_terrain.ts @@ -74,7 +74,7 @@ function drawTerrain(painter: Painter, terrain: Terrain, tiles: Array) { 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(); diff --git a/src/render/painter.ts b/src/render/painter.ts index 96bad92874..741fb535f1 100644 --- a/src/render/painter.ts +++ b/src/render/painter.ts @@ -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; @@ -406,12 +434,16 @@ export class Painter { } } - depthModeForSublayer(n: number, mask: DepthMaskType, func?: DepthFuncType | null): Readonly { + getDepthModeForSublayer(n: number, mask: DepthMaskType, func?: DepthFuncType | null): Readonly { 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 { + 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 @@ -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 diff --git a/src/render/program/fill_extrusion_program.ts b/src/render/program/fill_extrusion_program.ts index 4974335301..7b9c79a51f 100644 --- a/src/render/program/fill_extrusion_program.ts +++ b/src/render/program/fill_extrusion_program.ts @@ -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 = { @@ -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; @@ -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 => ({ @@ -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), @@ -81,7 +77,6 @@ const fillExtrusionUniformValues = ( shouldUseVerticalGradient: boolean, opacity: number, translate: [number, number], - cameraPosGlobe: vec3 ): UniformValues => { const light = painter.style.light; const _lp = light.properties.get('position'); @@ -103,7 +98,6 @@ const fillExtrusionUniformValues = ( 'u_vertical_gradient': +shouldUseVerticalGradient, 'u_opacity': opacity, 'u_fill_translate': translate, - 'u_camera_pos_globe': cameraPosGlobe, }; }; @@ -112,12 +106,11 @@ const fillExtrusionPatternUniformValues = ( shouldUseVerticalGradient: boolean, opacity: number, translate: [number, number], - cameraPosGlobe: vec3, coord: OverscaledTileID, crossfade: CrossfadeParameters, tile: Tile ): UniformValues => { - 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 diff --git a/src/render/program/program_uniforms.ts b/src/render/program/program_uniforms.ts index 69ee5580c4..d84722ebb4 100644 --- a/src/render/program/program_uniforms.ts +++ b/src/render/program/program_uniforms.ts @@ -27,6 +27,7 @@ export const programUniforms = { collisionBox: collisionUniforms, collisionCircle: collisionCircleUniforms, debug: debugUniforms, + depth: emptyUniforms, clippingMask: emptyUniforms, heatmap: heatmapUniforms, heatmapTexture: heatmapTextureUniforms, diff --git a/src/render/render_to_texture.test.ts b/src/render/render_to_texture.test.ts index 19dfce3310..e8e00421f4 100644 --- a/src/render/render_to_texture.test.ts +++ b/src/render/render_to_texture.test.ts @@ -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'); @@ -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: () => {} diff --git a/src/shaders/depth.vertex.glsl b/src/shaders/depth.vertex.glsl new file mode 100644 index 0000000000..c5d4fb2736 --- /dev/null +++ b/src/shaders/depth.vertex.glsl @@ -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 +} diff --git a/src/shaders/fill_extrusion.fragment.glsl b/src/shaders/fill_extrusion.fragment.glsl index d9a40ec6ca..496edebfe6 100644 --- a/src/shaders/fill_extrusion.fragment.glsl +++ b/src/shaders/fill_extrusion.fragment.glsl @@ -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 } diff --git a/src/shaders/fill_extrusion.vertex.glsl b/src/shaders/fill_extrusion.vertex.glsl index c87e09d239..8726b5dfb3 100644 --- a/src/shaders/fill_extrusion.vertex.glsl +++ b/src/shaders/fill_extrusion.vertex.glsl @@ -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 @@ -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); diff --git a/src/shaders/fill_extrusion_pattern.fragment.glsl b/src/shaders/fill_extrusion_pattern.fragment.glsl index 74f3a95445..2bddd558fd 100644 --- a/src/shaders/fill_extrusion_pattern.fragment.glsl +++ b/src/shaders/fill_extrusion_pattern.fragment.glsl @@ -1,11 +1,6 @@ uniform vec2 u_texsize; uniform float u_fade; -#ifdef GLOBE - in vec3 v_sphere_pos; - uniform vec3 u_camera_pos_globe; -#endif - uniform sampler2D u_image; in vec2 v_pos_a; @@ -47,17 +42,4 @@ void main() { #ifdef OVERDRAW_INSPECTOR fragColor = vec4(1.0); #endif - - #ifdef GLOBE - // Discard fragments that are occluded by the planet - // See comment in fill_extrusion.fragment.glsl - 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); - float distance_to_planet_center_squared = dot(nearest, nearest); - if (distance_to_planet_center_squared < u_projection_transition) { - discard; - } - #endif } diff --git a/src/shaders/shaders.ts b/src/shaders/shaders.ts index b110b43850..bc7b88f2ec 100644 --- a/src/shaders/shaders.ts +++ b/src/shaders/shaders.ts @@ -21,6 +21,7 @@ import collisionCircleFrag from './collision_circle.fragment.glsl.g'; import collisionCircleVert from './collision_circle.vertex.glsl.g'; import debugFrag from './debug.fragment.glsl.g'; import debugVert from './debug.vertex.glsl.g'; +import depthVert from './depth.vertex.glsl.g'; import fillFrag from './fill.fragment.glsl.g'; import fillVert from './fill.vertex.glsl.g'; import fillOutlineFrag from './fill_outline.fragment.glsl.g'; @@ -88,6 +89,7 @@ export const shaders = { collisionBox: compile(collisionBoxFrag, collisionBoxVert), collisionCircle: compile(collisionCircleFrag, collisionCircleVert), debug: compile(debugFrag, debugVert), + depth: compile(clippingMaskFrag, depthVert), fill: compile(fillFrag, fillVert), fillOutline: compile(fillOutlineFrag, fillOutlineVert), fillOutlinePattern: compile(fillOutlinePatternFrag, fillOutlinePatternVert), @@ -109,7 +111,7 @@ export const shaders = { terrainCoords: compile(terrainCoordsFrag, terrainVertCoords), projectionErrorMeasurement: compile(projectionErrorMeasurementFrag, projectionErrorMeasurementVert), atmosphere: compile(atmosphereFrag, atmosphereVert), - sky: compile(skyFrag, skyVert) + sky: compile(skyFrag, skyVert), }; // Expand #pragmas to #ifdefs. diff --git a/test/build/min.test.ts b/test/build/min.test.ts index 47b07a7ed9..13aec8c134 100644 --- a/test/build/min.test.ts +++ b/test/build/min.test.ts @@ -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 = 886914; + const expectedBytes = 886416; expect(actualBytes).toBeLessThan(expectedBytes + increaseQuota); expect(actualBytes).toBeGreaterThan(expectedBytes - decreaseQuota); diff --git a/test/examples/globe-3d-model.html b/test/examples/globe-3d-model.html index 1009212634..5dc7714503 100644 --- a/test/examples/globe-3d-model.html +++ b/test/examples/globe-3d-model.html @@ -77,7 +77,7 @@ const customLayer = { id: '3d-model', type: 'custom', - renderingMode: '3d', + renderingMode: '3d', // The layer MUST be marked as 3D in order to get the proper depth buffer with globe depths in it. onAdd(map, gl) { this.camera = new THREE.Camera(); this.scene = new THREE.Scene(); diff --git a/test/examples/globe-custom-tiles.html b/test/examples/globe-custom-tiles.html index 09ee1266a3..d87f8bdfd2 100644 --- a/test/examples/globe-custom-tiles.html +++ b/test/examples/globe-custom-tiles.html @@ -303,7 +303,7 @@ // add the custom style layer to the map map.on('load', () => { - map.addLayer(highlightLayer); + map.addLayer(highlightLayer, 'countries-label'); }); diff --git a/test/integration/render/run_render_tests.ts b/test/integration/render/run_render_tests.ts index ec6598d816..5f459e0449 100644 --- a/test/integration/render/run_render_tests.ts +++ b/test/integration/render/run_render_tests.ts @@ -10,7 +10,7 @@ import http from 'http'; import puppeteer, {Page, Browser} from 'puppeteer'; import {CoverageReport} from 'monocart-coverage-reports'; import {localizeURLs} from '../lib/localize-urls'; -import type {Map, CanvasSource, PointLike, StyleSpecification} from '../../../dist/maplibre-gl'; +import type {Map as MaplibreMap, CanvasSource, PointLike, StyleSpecification} from '../../../dist/maplibre-gl'; import junitReportBuilder, {type TestSuite} from 'junit-report-builder'; import * as maplibreglModule from '../../../dist/maplibre-gl'; @@ -291,7 +291,7 @@ async function getImageFromStyle(styleForTest: StyleWithTestData, page: Page): P this.renderingMode = '2d'; } - onAdd(map: Map, gl: WebGL2RenderingContext) { + onAdd(map: MaplibreMap, gl: WebGL2RenderingContext) { const vertexSource = `#version 300 es in vec3 aPos; uniform mat4 u_matrix; @@ -351,7 +351,7 @@ async function getImageFromStyle(styleForTest: StyleWithTestData, page: Page): P this.renderingMode = '3d'; } - onAdd(map: Map, gl: WebGL2RenderingContext) { + onAdd(map: MaplibreMap, gl: WebGL2RenderingContext) { const vertexSource = `#version 300 es @@ -423,8 +423,149 @@ async function getImageFromStyle(styleForTest: StyleWithTestData, page: Page): P } } + class Tent3DGlobe { + id: string; + type: string; + renderingMode: string; + + vertexBuffer: WebGLBuffer; + indexBuffer: WebGLBuffer; + shaderMap: Map = new Map(); + + constructor() { + this.id = 'tent-3d-globe'; + this.type = 'custom'; + this.renderingMode = '3d'; + } + + getShader(gl, shaderDescription) { + if (this.shaderMap.has(shaderDescription.variantName)) { + return this.shaderMap.get(shaderDescription.variantName); + } + + const vertexSource = `#version 300 es + // Inject MapLibre projection code + ${shaderDescription.vertexShaderPrelude} + ${shaderDescription.define} + + in vec3 a_pos; + + void main() { + gl_Position = projectTileFor3D(a_pos.xy, a_pos.z); + }`; + + // create GLSL source for fragment shader + const fragmentSource = `#version 300 es + uniform mediump vec4 u_color; + out highp vec4 fragColor; + void main() { + fragColor = u_color; + }`; + + // create a vertex shader + const vertexShader = gl.createShader(gl.VERTEX_SHADER); + gl.shaderSource(vertexShader, vertexSource); + gl.compileShader(vertexShader); + + // create a fragment shader + const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); + gl.shaderSource(fragmentShader, fragmentSource); + gl.compileShader(fragmentShader); + + // link the two shaders into a WebGL program + const program = gl.createProgram(); + gl.attachShader(program, vertexShader); + gl.attachShader(program, fragmentShader); + gl.linkProgram(program); + + const result = { + program, + aPos: gl.getAttribLocation(program, 'a_pos'), + }; + + this.shaderMap.set(shaderDescription.variantName, result); + + return result; + } + + onAdd (map, gl) { + const x = 0.5 - 0.015; + const y = 0.5 - 0.01; + const z = 500_000; + const d = 0.01; + + const vertexArray = new Float32Array([ + x, y, 0, + x + d, y, 0, + x, y + d, z, + x + d, y + d, z, + x, y + d + d, 0, + x + d, y + d + d, 0]); + const indexArray = new Uint16Array([ + 0, 2, 1, + 1, 2, 3, + 2, 4, 3, + 3, 4, 5 + ]); + + this.vertexBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.bufferData(gl.ARRAY_BUFFER, vertexArray, gl.STATIC_DRAW); + this.indexBuffer = gl.createBuffer(); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); + gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indexArray, gl.STATIC_DRAW); + } + + render (gl, args) { + const shader = this.getShader(gl, args.shaderData); + gl.useProgram(shader.program); + gl.uniformMatrix4fv( + gl.getUniformLocation(shader.program, 'u_projection_fallback_matrix'), + false, + args.defaultProjectionData.fallbackMatrix + ); + gl.uniformMatrix4fv( + gl.getUniformLocation(shader.program, 'u_projection_matrix'), + false, + args.defaultProjectionData.mainMatrix + ); + gl.uniform4f( + gl.getUniformLocation(shader.program, 'u_projection_tile_mercator_coords'), + ...args.defaultProjectionData.tileMercatorCoords + ); + gl.uniform4f( + gl.getUniformLocation(shader.program, 'u_projection_clipping_plane'), + ...args.defaultProjectionData.clippingPlane + ); + gl.uniform1f( + gl.getUniformLocation(shader.program, 'u_projection_transition'), + args.defaultProjectionData.projectionTransition + ); + + gl.enable(gl.CULL_FACE); + gl.bindBuffer(gl.ARRAY_BUFFER, this.vertexBuffer); + gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, this.indexBuffer); + gl.enableVertexAttribArray(shader.aPos); + gl.vertexAttribPointer(shader.aPos, 3, gl.FLOAT, false, 0, 0); + for (let i = 0; i < 2; i++) { + gl.uniform4f( + gl.getUniformLocation(shader.program, 'u_color'), + i === 0 ? 1 : 0.25, 0, 0, 1 + ); + gl.cullFace(i === 0 ? gl.BACK : gl.FRONT); + gl.drawElements(gl.TRIANGLES, 12, gl.UNSIGNED_SHORT, 0); + } + } + } + const customLayerImplementations = { 'tent-3d': Tent3D, + 'tent-3d-globe': Tent3DGlobe, 'null-island': NullIsland }; @@ -458,7 +599,7 @@ async function getImageFromStyle(styleForTest: StyleWithTestData, page: Page): P * @param operations - The operations * @param callback - The callback to use when all the operations are executed */ - async function applyOperations(testData: TestData, map: Map & { _render: () => void}, idle: boolean) { + async function applyOperations(testData: TestData, map: MaplibreMap & { _render: () => void}, idle: boolean) { if (!testData.operations || testData.operations.length === 0) { return; } diff --git a/test/integration/render/tests/projection/globe/custom/tent-3d-globe-occlusion/expected-win-flaky.png b/test/integration/render/tests/projection/globe/custom/tent-3d-globe-occlusion/expected-win-flaky.png new file mode 100644 index 0000000000..8bf33fdf6a Binary files /dev/null and b/test/integration/render/tests/projection/globe/custom/tent-3d-globe-occlusion/expected-win-flaky.png differ diff --git a/test/integration/render/tests/projection/globe/custom/tent-3d-globe-occlusion/expected.png b/test/integration/render/tests/projection/globe/custom/tent-3d-globe-occlusion/expected.png new file mode 100644 index 0000000000..2f3d2a8fbb Binary files /dev/null and b/test/integration/render/tests/projection/globe/custom/tent-3d-globe-occlusion/expected.png differ diff --git a/test/integration/render/tests/projection/globe/custom/tent-3d-globe-occlusion/style.json b/test/integration/render/tests/projection/globe/custom/tent-3d-globe-occlusion/style.json new file mode 100644 index 0000000000..630872d93d --- /dev/null +++ b/test/integration/render/tests/projection/globe/custom/tent-3d-globe-occlusion/style.json @@ -0,0 +1,81 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "operations": [ + [ + "addCustomLayer", + "tent-3d-globe" + ] + ] + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 30 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -10, + 0 + ], + [ + -10, + 10 + ], + [ + 10, + 10 + ], + [ + 10, + 0 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 60, + "zoom": 3, + "center": [ + 25, + -25 + ], + "projection": { + "type": "globe" + }, + "bearing": -35, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "green" + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 200000, + "fill-extrusion-color": "blue" + } + } + ] +} \ No newline at end of file diff --git a/test/integration/render/tests/projection/globe/custom/tent-3d-globe/expected-win-flaky.png b/test/integration/render/tests/projection/globe/custom/tent-3d-globe/expected-win-flaky.png new file mode 100644 index 0000000000..5264a5ba6f Binary files /dev/null and b/test/integration/render/tests/projection/globe/custom/tent-3d-globe/expected-win-flaky.png differ diff --git a/test/integration/render/tests/projection/globe/custom/tent-3d-globe/expected.png b/test/integration/render/tests/projection/globe/custom/tent-3d-globe/expected.png new file mode 100644 index 0000000000..ceec8089b1 Binary files /dev/null and b/test/integration/render/tests/projection/globe/custom/tent-3d-globe/expected.png differ diff --git a/test/integration/render/tests/projection/globe/custom/tent-3d-globe/style.json b/test/integration/render/tests/projection/globe/custom/tent-3d-globe/style.json new file mode 100644 index 0000000000..89b57c24f8 --- /dev/null +++ b/test/integration/render/tests/projection/globe/custom/tent-3d-globe/style.json @@ -0,0 +1,77 @@ +{ + "version": 8, + "metadata": { + "test": { + "height": 256, + "operations": [ + [ + "addCustomLayer", + "tent-3d-globe" + ] + ] + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 30 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -10, + 0 + ], + [ + -10, + 10 + ], + [ + 10, + 10 + ], + [ + 10, + 0 + ] + ] + ] + } + } + ] + } + } + }, + "pitch": 60, + "zoom": 3, + "projection": { + "type": "globe" + }, + "bearing": -35, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "green" + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 200000, + "fill-extrusion-color": "blue" + } + } + ] +} \ No newline at end of file