Skip to content

Commit abe4b3c

Browse files
committed
fix(core): add shader-based globe occlusion for IconLayer, TextLayer, and ArcLayer
Fixes #9777 and #9592. The previous approach of setting cullMode: 'back' globally for globe projections had two issues: 1. Billboard geometry (IconLayer, TextLayer) doesn't have proper back faces, causing icons and text to be incorrectly culled and disappear 2. The coordinate system handedness could cause culling to work in unexpected ways This change implements shader-based globe occlusion that works for all geometry types: - Added project_globe_get_occlusion() and project_globe_is_occluded() functions to the projection shader module (both GLSL and WGSL) - IconLayer, TextLayer (via MultiIconLayer), and ArcLayer now automatically hide geometry that is on the back side of the globe - Removed the global cullMode: 'back' parameter from getDefaultParameters() in deck-utils.ts - Updated examples to remove manual cullMode: 'none' workarounds The solution is transparent to users - no API changes or special configuration needed. https://claude.ai/code/session_01P6canyQPTd6nckN66pauLK
1 parent 6149b4c commit abe4b3c

File tree

10 files changed

+77
-16
lines changed

10 files changed

+77
-16
lines changed

examples/basemap-browser/src/config/layers.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ export function buildLayers(options: LayerBuildOptions): Layer[] {
3939

4040
const interleavedProps = getInterleavedProps(basemap, interleaved);
4141

42-
// Arc layer needs cullMode: 'none' for globe projection
43-
const arcParameters = globe ? {cullMode: 'none' as const} : undefined;
44-
4542
// Sample city data for IconLayer and TextLayer
4643
const cities = [
4744
{name: 'London', coordinates: [-0.1276, 51.5074]},
@@ -74,7 +71,6 @@ export function buildLayers(options: LayerBuildOptions): Layer[] {
7471
getSourceColor: [0, 128, 200],
7572
getTargetColor: [200, 0, 80],
7673
getWidth: 1,
77-
parameters: arcParameters,
7874
...interleavedProps
7975
}),
8076
new IconLayer({
@@ -150,9 +146,7 @@ export function buildLayers(options: LayerBuildOptions): Layer[] {
150146
getAlignmentBaseline: 'center',
151147
background: true,
152148
getBackgroundColor: [0, 0, 0, 200],
153-
backgroundPadding: [6, 3],
154-
// Disable culling for globe projection
155-
parameters: {cullMode: 'none'}
149+
backgroundPadding: [6, 3]
156150
})
157151
];
158152
}

examples/get-started/pure-js/maplibre-globe/app.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,6 @@ const deckOverlay = new DeckOverlay({
4141
new ArcLayer({
4242
id: 'arcs',
4343
data: AIR_PORTS,
44-
parameters: {
45-
cullMode: 'none'
46-
},
4744
dataTransform: d => d.features.filter(f => f.properties.scalerank < 4),
4845
// Styles
4946
getSourcePosition: f => [-0.4531566, 51.4709959], // London

examples/website/maplibre/app.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@ export default function App({
9292
timeRange,
9393
getSourceColor: [63, 81, 181],
9494
getTargetColor: [63, 181, 173],
95-
parameters: {cullMode: 'none'},
9695
...(interleaveLabels ? {beforeId: 'watername_ocean'} : {})
9796
})
9897
);

modules/core/src/shaderlib/project/project.glsl.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,4 +277,29 @@ float project_pixel_size(float pixels) {
277277
vec2 project_pixel_size(vec2 pixels) {
278278
return pixels / project.scale;
279279
}
280+
281+
//
282+
// Globe occlusion - returns a value indicating whether a position is occluded by the globe.
283+
// Returns 0.0 if the position is visible, 1.0 if fully occluded.
284+
// Can be used to discard fragments or fade out geometry on the back of the globe.
285+
//
286+
float project_globe_get_occlusion(vec3 commonPosition) {
287+
if (project.projectionMode == PROJECTION_MODE_GLOBE) {
288+
// In globe projection, positions are on a sphere centered at origin.
289+
// A point is visible if it faces the camera.
290+
// The surface normal at any point is the normalized position vector.
291+
// The point is visible if dot(normal, viewDirection) > 0
292+
vec3 normal = normalize(commonPosition);
293+
vec3 viewDir = normalize(project.cameraPosition - commonPosition);
294+
float visibility = dot(normal, viewDir);
295+
// Return 0.0 if visible (visibility > 0), 1.0 if occluded (visibility <= 0)
296+
return visibility > 0.0 ? 0.0 : 1.0;
297+
}
298+
return 0.0;
299+
}
300+
301+
// Helper function to check if position is on the back of the globe
302+
bool project_globe_is_occluded(vec3 commonPosition) {
303+
return project_globe_get_occlusion(commonPosition) > 0.5;
304+
}
280305
`;

modules/core/src/shaderlib/project/project.wgsl.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,4 +302,29 @@ fn project_pixel_size_float(pixels: f32) -> f32 {
302302
fn project_pixel_size_vec2(pixels: vec2<f32>) -> vec2<f32> {
303303
return pixels / project.scale;
304304
}
305+
306+
//
307+
// Globe occlusion - returns a value indicating whether a position is occluded by the globe.
308+
// Returns 0.0 if the position is visible, 1.0 if fully occluded.
309+
// Can be used to discard fragments or fade out geometry on the back of the globe.
310+
//
311+
fn project_globe_get_occlusion(commonPosition: vec3<f32>) -> f32 {
312+
if (project.projectionMode == PROJECTION_MODE_GLOBE) {
313+
// In globe projection, positions are on a sphere centered at origin.
314+
// A point is visible if it faces the camera.
315+
// The surface normal at any point is the normalized position vector.
316+
// The point is visible if dot(normal, viewDirection) > 0
317+
let normal = normalize(commonPosition);
318+
let viewDir = normalize(project.cameraPosition - commonPosition);
319+
let visibility = dot(normal, viewDir);
320+
// Return 0.0 if visible (visibility > 0), 1.0 if occluded (visibility <= 0)
321+
return select(1.0, 0.0, visibility > 0.0);
322+
}
323+
return 0.0;
324+
}
325+
326+
// Helper function to check if position is on the back of the globe
327+
fn project_globe_is_occluded(commonPosition: vec3<f32>) -> bool {
328+
return project_globe_get_occlusion(commonPosition) > 0.5;
329+
}
305330
`;

modules/layers/src/arc-layer/arc-layer-vertex.glsl.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,12 @@ void main(void) {
232232
DECKGL_FILTER_GL_POSITION(curr, geometry);
233233
gl_Position = curr + vec4(project_pixel_size_to_clipspace(offset.xy), 0.0, 0.0);
234234
235+
// Hide arc segments that are occluded by the globe (on the back side)
236+
if (project_globe_is_occluded(geometry.position.xyz)) {
237+
// Move to clip space position that will be clipped
238+
gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
239+
}
240+
235241
vec4 color = mix(instanceSourceColors, instanceTargetColors, segmentRatio);
236242
vColor = vec4(color.rgb, color.a * layer.opacity);
237243
DECKGL_FILTER_COLOR(vColor, geometry);

modules/layers/src/icon-layer/icon-layer-vertex.glsl.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,16 @@ void main(void) {
6666
} else {
6767
vec3 offset_common = vec3(project_pixel_size(pixelOffset), 0.0);
6868
DECKGL_FILTER_SIZE(offset_common, geometry);
69-
gl_Position = project_position_to_clipspace(instancePositions, instancePositions64Low, offset_common, geometry.position);
69+
gl_Position = project_position_to_clipspace(instancePositions, instancePositions64Low, offset_common, geometry.position);
7070
DECKGL_FILTER_GL_POSITION(gl_Position, geometry);
7171
}
7272
73+
// Hide icons/text that are occluded by the globe (on the back side)
74+
if (project_globe_is_occluded(geometry.position.xyz)) {
75+
// Move to clip space position that will be clipped
76+
gl_Position = vec4(0.0, 0.0, 2.0, 1.0);
77+
}
78+
7379
vTextureCoords = mix(
7480
instanceIconFrames.xy,
7581
instanceIconFrames.xy + iconSize,

modules/layers/src/icon-layer/icon-layer.wgsl.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ fn vertexMain(inp: Attributes) -> Varyings {
7878
pixelOffset = pixelOffset + inp.instancePixelOffset;
7979
pixelOffset.y = pixelOffset.y * -1.0;
8080
81+
// Calculate common position for globe occlusion check
82+
let commonPosition = project_position_vec3_f64(inp.instancePositions, inp.instancePositions64Low);
83+
8184
if (icon.billboard != 0) {
8285
var pos = project_position_to_clipspace(inp.instancePositions, inp.instancePositions64Low, vec3<f32>(0.0)); // TODO, &geometry.position);
8386
// DECKGL_FILTER_GL_POSITION(pos, geometry);
@@ -95,6 +98,12 @@ fn vertexMain(inp: Attributes) -> Varyings {
9598
outp.position = pos;
9699
}
97100
101+
// Hide icons/text that are occluded by the globe (on the back side)
102+
if (project_globe_is_occluded(commonPosition)) {
103+
// Move to clip space position that will be clipped
104+
outp.position = vec4<f32>(0.0, 0.0, 2.0, 1.0);
105+
}
106+
98107
let uvMix = (inp.positions.xy + vec2<f32>(1.0, 1.0)) * 0.5;
99108
outp.vTextureCoords = mix(inp.instanceIconFrames.xy, inp.instanceIconFrames.xy + iconSize, uvMix) / icon.iconsTextureDim;
100109

modules/mapbox/src/deck-utils.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,10 @@ export function getDefaultParameters(map: Map, interleaved: boolean): Parameters
111111
blendAlphaOperation: 'add'
112112
}
113113
: {};
114-
if (getProjection(map) === 'globe') {
115-
result.cullMode = 'back';
116-
}
114+
// Note: Globe occlusion (hiding geometry on the back of the globe) is handled
115+
// in the shader via project_globe_get_occlusion() rather than GPU back-face culling.
116+
// Back-face culling doesn't work correctly for billboard geometry (IconLayer, TextLayer)
117+
// which always faces the camera.
117118
return result;
118119
}
119120

test/apps/projection/app.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,6 @@ function App() {
144144
<>
145145
<DeckGL
146146
controller
147-
parameters={{cullMode: 'back'}}
148147
views={opts.view}
149148
initialViewState={opts.viewState}
150149
layers={layers}

0 commit comments

Comments
 (0)