Repository navigation
fix(core, layers): hide icons, text and billboard points behind the globe - #10750
chrisgervang wants to merge 10 commits into
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3973c50. Configure here.
|
| In the MapView, it is often sufficient to provide a solid background color where there is no geometry. In the GlobeView, the user can "see through" to the other side of the earth. There are two ways to fix this: | ||
| In the MapView, it is often sufficient to provide a solid background color where there is no geometry. In the GlobeView, the user can "see through" to the other side of the earth. | ||
|
|
||
| Camera-facing geometry is handled by the layers themselves: `IconLayer`, `TextLayer` and billboard `ScatterplotLayer` hide objects whose anchor is behind the globe, and keep the curve of the globe from clipping an object whose anchor is visible. Positions with altitude stay visible past the horizon of the ground below them. Objects that are not billboards lie on the surface and are culled with it. |
There was a problem hiding this comment.
This explains itself in whats-new and feels redundant.
If you prefer to keep it, it feels better to put beneath the suggested code that still requires occlusive workarounds.
My thought process is copyable examples/workarounds precede written explanations of behavior.
There was a problem hiding this comment.
Agreed. Claude writes too much documentation so I removed it
| } | ||
|
|
||
| // Screen-space radius, in pixels, that a billboard is assumed to cover around its anchor | ||
| const GLOBE_BILLBOARD_FOOTPRINT_PIXELS: f32 = 64.0; |
There was a problem hiding this comment.
just for more clarity - 64 was chosen as an assumed extent for most use cases? Whats the cost of making it larger?
There was a problem hiding this comment.
Yeah, 64 was picked to match the default icon size, which is a common size to use.
The larger the value, the more the sprite is lifted away from the surface - I had claude generate a visual comparison for us in the other thread
| // DECKGL_FILTER_SIZE(offset, geometry); | ||
| let clipOffset = project_pixel_size_to_clipspace(offset.xy); | ||
| pos = vec4<f32>(pos.x + clipOffset.x, pos.y + clipOffset.y, pos.z, pos.w); | ||
| // Hide icons whose anchor is behind the globe; culling handles non-billboard icons |
There was a problem hiding this comment.
maybe a feature to stack on this is incremental occlusion like you mentioned in my original issue.
Would that be handled in here?
There was a problem hiding this comment.
Fading the icons as they approach the edge would be nice. I'd look into it in a follow up once the globe changes that are up are all landed and proven stable
There was a problem hiding this comment.
@copilot you alive? Draft a PR for this to remind me to get back to this sometime!
There was a problem hiding this comment.
Proposed follow-up PR:
Title: feat(layers): fade billboard sprites near the globe horizon
Body: Add optional horizon fade for billboard IconLayer, TextLayer, and ScatterplotLayer on GlobeView, so sprites transition out smoothly as they approach globe occlusion instead of disappearing at a hard cutoff. Build it on top of the new globe occlusion helpers, keep the current default behavior unchanged, and cover it with render tests plus docs.
This comment has been minimized.
This comment has been minimized.
|
The billboard paragraph in Generated by Claude Code |
befac28 to
323a809
Compare
The vertex shaders flip y, so the strips wound clockwise and GlobeView's default cullMode: 'back' culled every icon and text background. Emit the top row first so the quads are front-facing in any view. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FyzWaF5TmQPmGjh3Zr3XL9
Add project_globe_is_occluded, a ray-sphere test between the camera and the vertex that is exact at any altitude, and project_globe_billboard_clipspace, which gives a billboard the depth of the nearest globe surface point so the curve of the globe never clips a sprite whose anchor is visible. IconLayer, TextLayer (glyphs and backgrounds) and ScatterplotLayer use them: objects behind the globe are hidden, objects at the horizon are drawn whole over an opaque surface, and raised objects stay visible past the ground horizon. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FyzWaF5TmQPmGjh3Zr3XL9
…nchors only project_globe_billboard_clipspace moved every billboard to the distance of the globe point below the camera, which put all sprites at roughly the same depth and in front of any 3D geometry. It now shifts a sprite toward the camera only by the amount the surface rises within a 64 px footprint around its anchor, so raised sprites and other geometry keep their order and the shift vanishes where the surface is flat on screen. The occlusion test now runs only in billboard mode, on the anchor. Non-billboard marks lie on the surface and are culled with it; testing their offset vertices tore marks straddling the horizon across the viewport. TextBackgroundLayer's clip-rect path assigns the same corners in the flipped order of the default path so the reversed strip stays front-facing with a content box. Render cases: globe-occlusion-text-content-box and globe-occlusion-marks-surface added, WebGPU enabled for the suite, altitude and text goldens regenerated. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FyzWaF5TmQPmGjh3Zr3XL9
… remarks Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FyzWaF5TmQPmGjh3Zr3XL9
323a809 to
1bfa480
Compare
Halves its runtime. MapboxOverlay on MapLibre 5 shares the same layer code.
…spec The spec created a map and an overlay per case, 16 WebGL contexts in a few seconds. Chrome drops the oldest live context once 16 exist, and in the headless suite the oldest is the shared test device, so every GPU test after this file failed with a null link error. One map and one overlay per interleaved mode now serve all cases, with a fresh label id per case so the reused Deck initializes each label instead of updating it, and the render wait keys on the label's isLoaded and its pixel. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FyzWaF5TmQPmGjh3Zr3XL9
Both overlays now run on a single map, and the non-interleaved overlay releases its WebGL context when it is removed. The headless suite runs within one or two contexts of the browser's limit of 16, past which the browser evicts the oldest context, the shared test device. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FyzWaF5TmQPmGjh3Zr3XL9
ibgreen
left a comment
There was a problem hiding this comment.
Approving the general globe rendering improvements, with the depth-ordering concern recorded inline for follow-up. Correcting quad winding and testing camera-to-anchor intersection with the sphere are basemap-independent fixes and are useful for other globe integrations, including a future Tangram integration.
Review assisted by Codex. Local headless tests could not start because of sandbox server restrictions and incompatible installed dependency exports; the current GitHub test workflow passed. The numerical depth reproduction in the inline comment is not a rendered regression test.
| if (shift <= 0.0) { | ||
| return clipPosition; | ||
| } | ||
| vec4 shiftedClip = project_common_position_to_clipspace(vec4(commonPosition - direction * shift, 1.0)); |
There was a problem hiding this comment.
The depth lift does not generally preserve ordering against other 3D geometry, despite the guarantee in the helper comment and project.md. Only billboards receive this adjustment; terrain, meshes and extrusions retain their original depths. Numerically evaluating this shader formula for a standalone GlobeView centered at [0, 0], zoom 12, pitch 60°, height 512 and default altitude 1.5 gives a shift of about 0.07249 common units, equivalent to approximately 1.8 km toward the camera, at the center anchor. Consequently, foreground geometry closer than the original anchor can lose its occlusion of the billboard, including at high zoom. This is a numerical reproduction rather than a rendered test. Please add a rendered regression with a billboard behind foreground 3D geometry and either revise the depth strategy or explicitly document and expose the depth-lift policy as a controllable trade-off. Keeping text glyphs and backgrounds at the same depth is sensible, but does not preserve ordering against the rest of the scene. Apply any corresponding changes to the WGSL helper too.
There was a problem hiding this comment.
Confirmed your number. Evaluating the helper's formula against GlobeViewport I get 1767 m at the center anchor for a standalone GlobeView at lng/lat 0, zoom 12, pitch 60 (0.0710 common units), and 0 m at pitch 0, 614 m at pitch 30, 110 m at zoom 16 with pitch 60.
The guarantee in the comment and project.md was wrong and is corrected in GLSL, WGSL and the docs. What holds is narrower: the shift depends on the anchor direction alone, so sprites anchored at the same place keep their order whatever their altitude and a label's glyphs stay at the depth of their background. Against the rest of the scene it is zero only where the surface faces the camera; where the surface is foreshortened it grows with the grazing angle, and 3D geometry in front of the anchor by less than the shift no longer hides the billboard. The docs now give the magnitude, the oversized-sprite limit, and that the 64 px radius is a shader constant.
New render case globe-occlusion-billboard-depth records the behavior: an opaque wall 4 km tall standing 1 km in front of three columns of icons 1 km, 3 km and 10 km behind it, at zoom 12 with pitch 60. The flanking columns show all three icons; behind the wall only the icon 1 km back draws through, while the ones 3 km and 10 km back stay hidden. It runs on WebGL and WebGPU.
I did look at revising the depth strategy rather than documenting it. Subtracting the tangent plane at the anchor's surface point leaves a shift that is less than or equal to zero everywhere, so the clipping this helper fixes is the grazing-angle effect and not curvature as such; there is no curvature-only term to keep. The remaining alternative is per-vertex depth, which would put TextLayer glyphs behind their own background, since the background and the glyphs are separate layers with different extents. So the radius stays a constant here, and I'll offer the configurable depth-lift policy as a follow-up.
In 424c5ea.
Generated by Claude Code
There was a problem hiding this comment.
Pictures of the new case, since the trade-off is easier to see than to read. Same scene both times: zoom 12, pitch 60°, icons 1 km / 3 km / 10 km behind an opaque wall, three columns (one hidden by the wall, two beside it). The control flattens the wall to 1 m so you can tell which icons are being depth-tested away rather than dropped.
| Control: wall flattened | globe-occlusion-billboard-depth golden |
|---|---|
![]() |
![]() |
All nine draw in the control. With the wall standing, the orange icon 1 km behind it draws through anyway, because the wall is nearer to the anchor than the ~1.8 km lift is long; the blue icons 3 km and 10 km behind it stay hidden, so the lift is bounded rather than unconditional. That is exactly the cost you measured, now pinned by a golden.
Generated by Claude Code
The helper comments and `project.md` claimed the shift kept depth order against the rest of the scene. It only keeps order among sprites anchored at the same place: the shift is the depth the globe surface spans across the 64 px footprint, which grows with the grazing angle and reaches about 1.8 km at zoom 12 with pitch 60, so nearer 3D geometry stops hiding a billboard. State that in GLSL, WGSL and `project.md`, and add `globe-occlusion-billboard-depth`, a wall in front of icons 1 km, 3 km and 10 km behind it.



For #9592, #9777 and #9554. Alternative to #9975.
Background
GlobeViewenablescullMode: 'back', but back-face culling only correlates with the far side for geometry that follows the sphere. Camera-facing quads wind the same way on both hemispheres, so culling removes them either everywhere or nowhere.IconLayerandTextLayerquads wind clockwise after the y flip in their shaders and are culled everywhere, so nothing renders on a standaloneGlobeView; billboardScatterplotLayeris never culled and shows through the globe. #9975 testeddot(normal, viewDir)at the anchor, which is only correct at sea level and cut off raised geometry near the horizon, and it left visible sprites clipped by the globe surface in interleaved mode.cullMode: 'none'workaroundSolidPolygonLayerearthScatterplotLayerTextLayerwith backgrounds (blank before, like icons)TextLayerwith a fixed-heightgetContentBox(blank before)ScatterplotLayerandIconLayeron the surfaceStandalone
GlobeViewat zoom 0.9 over lng 0, lat 10, default parameters. In the surface row, note the bottom row of icons on the limb: cut by the globe before, whole after. In the altitude row, the raised icons reach further down than the sea-level ones because a raised anchor is visible past the horizon of the ground below it, and they draw over the sea-level icons where the two overlap.Change List
IconLayerandTextBackgroundLayer: emit the quad's top row first so the strip is front-facing after the y flip;MultiIconLayerinherits the geometry. The background's clip-rect path assigns the same corners in the flipped order so a content box keeps the strip front-facingprojectshader module (GLSL and WGSL): addproject_globe_is_occluded(commonPosition), a ray-sphere test between the camera and the anchor that is exact at any altitude, andproject_globe_billboard_clipspace(clipPosition, commonPosition), which shifts a billboard toward the camera by the amount the globe surface rises within a 64 px footprint around its anchor. The shift depends on the anchor direction only, so sprites anchored at the same place keep their depth order whatever their altitude and a label's glyphs stay at the depth of their background. It is zero where the surface faces the camera and grows with the grazing angle, about 1.8 km at zoom 12 with pitch 60°, so 3D geometry in front of an anchor by less than that no longer hides the billboard;project.mddocuments the trade-off. Both are identity in flat projectionsIconLayer,MultiIconLayer,TextBackgroundLayer,ScatterplotLayer(GLSL and WGSL): in billboard mode, hide objects whose anchor is behind the globe and take the globe-safe depth beforeDECKGL_FILTER_GL_POSITION, so collision-priority depth still wins in the collision pass. Non-billboard marks lie on the surface and are left to cullingglobe-occlusion.spec.tswith the seven cases above, plusglobe-occlusion-billboard-depth, which records the depth shift against an extruded wall at zoom 12 with pitch 60°, on WebGL and WebGPU; existing icon, text and scatterplot goldens unchangedmaplibre/globe-icons-text.spec.tschecks thatIconLayerandTextLayerlabels, backgrounds and picking survive on MapLibre 6 under mercator and globe, overlaid and interleaved (adapted from fix(layers): keep IconLayer and TextLayer visible on globe projections #10822)project.mdfor the two helpers,docs/whats-new.md;globe-view.mdOcclusion remarks note that back-face culling is the defaultRan the new spec and the icon, text and scatterplot render specs headless on WebGL,
tscfor core and layers, and lint. WebGPU runs on CI only.TODO
Note
Medium Risk
Changes core projection shaders and multiple layer vertex paths, including depth-buffer behavior near the horizon; visual regressions are possible in GlobeView and interleaved basemap setups despite new tests.
Overview
GlobeView billboard rendering for
IconLayer,TextLayer, and billboardScatterplotLayeris fixed by adding globe-specific helpers to theprojectshader module (GLSL and WGSL):project_globe_is_occludedhides anchors behind the sphere via a camera–position ray test (including altitude), andproject_globe_billboard_clipspacenudges depth toward the camera so limb sprites are not clipped by the curved globe surface.Billboard vertex shaders call those helpers and discard far-side anchors; quad winding is reordered in icon and text-background geometry so strips stay front-facing under
GlobeView's default back-face culling. Docs cover the new APIs, defaultcullMode, and what's new; render and MapLibre tests exercise occlusion, horizon clipping, depth-shift trade-offs, and mercator/globe parity.Reviewed by Cursor Bugbot for commit 1f82ced. Bugbot is set up for automated code reviews on this repo. Configure here.