diff --git a/examples/basemap-browser/src/config/build-config.ts b/examples/basemap-browser/src/config/build-config.ts index daf53680860..80d2312bcb0 100644 --- a/examples/basemap-browser/src/config/build-config.ts +++ b/examples/basemap-browser/src/config/build-config.ts @@ -24,7 +24,8 @@ export function buildConfig( dimensions: Dimensions, onViewStateChange?: ViewStateChangeCallback ): Config { - const {basemap, framework, interleaved, batched, globe, multiView, stressTest} = dimensions; + const {basemap, framework, interleaved, batched, globe, multiView, billboard, stressTest} = + dimensions; // Validate dimensions (warnings only) const validation = validateDimensions(dimensions); @@ -38,6 +39,7 @@ export function buildConfig( interleaved, globe, multiView, + billboard, stressTest }); @@ -56,6 +58,7 @@ export function buildConfig( batched, globe, multiView, + billboard, stressTest, // Computed configuration diff --git a/examples/basemap-browser/src/config/dimensions.ts b/examples/basemap-browser/src/config/dimensions.ts index af4de2af558..bf6d560b1eb 100644 --- a/examples/basemap-browser/src/config/dimensions.ts +++ b/examples/basemap-browser/src/config/dimensions.ts @@ -14,6 +14,7 @@ export const DEFAULT_DIMENSIONS: Dimensions = { batched: true, globe: false, multiView: false, + billboard: true, stressTest: 'none' }; diff --git a/examples/basemap-browser/src/config/layers.ts b/examples/basemap-browser/src/config/layers.ts index ad16b7b2b9e..a93a81419fe 100644 --- a/examples/basemap-browser/src/config/layers.ts +++ b/examples/basemap-browser/src/config/layers.ts @@ -27,6 +27,7 @@ type LayerBuildOptions = { interleaved: boolean; globe: boolean; multiView: boolean; + billboard: boolean; stressTest: StressTest; }; @@ -35,13 +36,10 @@ type LayerBuildOptions = { * Single source of truth for layer configuration. */ export function buildLayers(options: LayerBuildOptions): Layer[] { - const {basemap, interleaved, globe, multiView, stressTest} = options; + const {basemap, interleaved, multiView, billboard, stressTest} = options; const interleavedProps = getInterleavedProps(basemap, interleaved); - // Arc layer needs cullMode: 'none' for globe projection - const arcParameters = globe ? {cullMode: 'none' as const} : undefined; - // Sample city data for IconLayer and TextLayer const cities = [ {name: 'London', coordinates: [-0.1276, 51.5074]}, @@ -74,7 +72,6 @@ export function buildLayers(options: LayerBuildOptions): Layer[] { getSourceColor: [0, 128, 200], getTargetColor: [200, 0, 80], getWidth: 1, - parameters: arcParameters, ...interleavedProps }), new IconLayer({ @@ -86,6 +83,7 @@ export function buildLayers(options: LayerBuildOptions): Layer[] { getPosition: (d: any) => d.coordinates, getSize: 40, getColor: [0, 140, 255], + billboard, pickable: true, ...interleavedProps }), @@ -100,6 +98,7 @@ export function buildLayers(options: LayerBuildOptions): Layer[] { getTextAnchor: 'middle', getAlignmentBaseline: 'top', getPixelOffset: [0, 20], + billboard, background: true, getBackgroundColor: [0, 0, 0, 180], backgroundPadding: [4, 2], @@ -150,9 +149,7 @@ export function buildLayers(options: LayerBuildOptions): Layer[] { getAlignmentBaseline: 'center', background: true, getBackgroundColor: [0, 0, 0, 200], - backgroundPadding: [6, 3], - // Disable culling for globe projection - parameters: {cullMode: 'none'} + backgroundPadding: [6, 3] }) ]; } diff --git a/examples/basemap-browser/src/control-panel.tsx b/examples/basemap-browser/src/control-panel.tsx index 7a2fc6051f3..ce9894f09b0 100644 --- a/examples/basemap-browser/src/control-panel.tsx +++ b/examples/basemap-browser/src/control-panel.tsx @@ -56,6 +56,10 @@ function getDimensionsFromUrl(): Partial { result.multiView = params.get('multiView') === 'true'; } + if (params.has('billboard')) { + result.billboard = params.get('billboard') !== 'false'; + } + const stressTest = params.get('stressTest'); if ( stressTest === 'none' || @@ -80,6 +84,7 @@ function setUrlFromDimensions(dimensions: Dimensions) { params.set('batched', String(dimensions.batched)); params.set('globe', String(dimensions.globe)); params.set('multiView', String(dimensions.multiView)); + params.set('billboard', String(dimensions.billboard)); params.set('stressTest', dimensions.stressTest); const newUrl = `${window.location.pathname}?${params.toString()}`; window.history.replaceState({}, '', newUrl); @@ -262,6 +267,18 @@ export default function ControlPanel({onConfigChange}: ControlPanelProps) { + {/* Billboard Toggle */} +
+ +
+ {/* Stress Test Selection */}
Stress Test:
diff --git a/examples/basemap-browser/src/types.ts b/examples/basemap-browser/src/types.ts index 1a8dfee13bd..b86b1c1923f 100644 --- a/examples/basemap-browser/src/types.ts +++ b/examples/basemap-browser/src/types.ts @@ -39,6 +39,7 @@ export type Dimensions = { batched: boolean; globe: boolean; multiView: boolean; + billboard: boolean; stressTest: StressTest; }; @@ -68,6 +69,7 @@ export type Config = { batched: boolean; globe: boolean; multiView: boolean; + billboard: boolean; stressTest: StressTest; // Computed configuration diff --git a/examples/get-started/pure-js/maplibre-globe/app.js b/examples/get-started/pure-js/maplibre-globe/app.js index d6cc3aff225..99babf394de 100644 --- a/examples/get-started/pure-js/maplibre-globe/app.js +++ b/examples/get-started/pure-js/maplibre-globe/app.js @@ -41,9 +41,6 @@ const deckOverlay = new DeckOverlay({ new ArcLayer({ id: 'arcs', data: AIR_PORTS, - parameters: { - cullMode: 'none' - }, dataTransform: d => d.features.filter(f => f.properties.scalerank < 4), // Styles getSourcePosition: f => [-0.4531566, 51.4709959], // London diff --git a/examples/website/maplibre/app.tsx b/examples/website/maplibre/app.tsx index 719ed874466..fb368e48bbb 100644 --- a/examples/website/maplibre/app.tsx +++ b/examples/website/maplibre/app.tsx @@ -92,7 +92,6 @@ export default function App({ timeRange, getSourceColor: [63, 81, 181], getTargetColor: [63, 181, 173], - parameters: {cullMode: 'none'}, ...(interleaveLabels ? {beforeId: 'watername_ocean'} : {}) }) ); diff --git a/modules/core/src/shaderlib/project/project.glsl.ts b/modules/core/src/shaderlib/project/project.glsl.ts index 37acfeba2f8..559b6a0338d 100644 --- a/modules/core/src/shaderlib/project/project.glsl.ts +++ b/modules/core/src/shaderlib/project/project.glsl.ts @@ -277,4 +277,22 @@ float project_pixel_size(float pixels) { vec2 project_pixel_size(vec2 pixels) { return pixels / project.scale; } + +// +// Globe occlusion - check if a position is on the back of the globe (occluded from view). +// Returns true if occluded, false if visible. +// +bool project_globe_is_occluded(vec3 commonPosition) { + if (project.projectionMode == PROJECTION_MODE_GLOBE) { + // In globe projection, positions are on a sphere centered at origin. + // A point is visible if it faces the camera. + // The surface normal at any point is the normalized position vector. + // The point is visible if dot(normal, viewDirection) > 0 + vec3 normal = normalize(commonPosition); + vec3 viewDir = normalize(project.cameraPosition - commonPosition); + float visibility = dot(normal, viewDir); + return visibility <= 0.0; + } + return false; +} `; diff --git a/modules/core/src/shaderlib/project/project.wgsl.ts b/modules/core/src/shaderlib/project/project.wgsl.ts index 27955576f4d..56e87bff08c 100644 --- a/modules/core/src/shaderlib/project/project.wgsl.ts +++ b/modules/core/src/shaderlib/project/project.wgsl.ts @@ -302,4 +302,22 @@ fn project_pixel_size_float(pixels: f32) -> f32 { fn project_pixel_size_vec2(pixels: vec2) -> vec2 { return pixels / project.scale; } + +// +// Globe occlusion - check if a position is on the back of the globe (occluded from view). +// Returns true if occluded, false if visible. +// +fn project_globe_is_occluded(commonPosition: vec3) -> bool { + if (project.projectionMode == PROJECTION_MODE_GLOBE) { + // In globe projection, positions are on a sphere centered at origin. + // A point is visible if it faces the camera. + // The surface normal at any point is the normalized position vector. + // The point is visible if dot(normal, viewDirection) > 0 + let normal = normalize(commonPosition); + let viewDir = normalize(project.cameraPosition - commonPosition); + let visibility = dot(normal, viewDir); + return visibility <= 0.0; + } + return false; +} `; diff --git a/modules/layers/src/arc-layer/arc-layer-vertex.glsl.ts b/modules/layers/src/arc-layer/arc-layer-vertex.glsl.ts index 6d1fcd4872e..334c1878dac 100644 --- a/modules/layers/src/arc-layer/arc-layer-vertex.glsl.ts +++ b/modules/layers/src/arc-layer/arc-layer-vertex.glsl.ts @@ -224,6 +224,12 @@ void main(void) { arc.widthMinPixels, arc.widthMaxPixels ); + // Hide arc segments that are occluded by the globe (on the back side) + // Set width to 0 instead of clipping to avoid artifacts at segment boundaries + if (project_globe_is_occluded(geometry.position.xyz)) { + widthPixels = 0.0; + } + // extrude vec3 offset = vec3( getExtrusionOffset((next.xy - curr.xy) * indexDir, segmentSide, widthPixels), diff --git a/modules/layers/src/icon-layer/icon-layer-vertex.glsl.ts b/modules/layers/src/icon-layer/icon-layer-vertex.glsl.ts index 3653346754e..78453a1ed2c 100644 --- a/modules/layers/src/icon-layer/icon-layer-vertex.glsl.ts +++ b/modules/layers/src/icon-layer/icon-layer-vertex.glsl.ts @@ -57,6 +57,9 @@ void main(void) { pixelOffset += instancePixelOffset; pixelOffset.y *= -1.0; + // Calculate common position for globe occlusion check (anchor position without offset) + vec3 commonPosition = project_position(instancePositions, instancePositions64Low); + if (icon.billboard) { gl_Position = project_position_to_clipspace(instancePositions, instancePositions64Low, vec3(0.0), geometry.position); DECKGL_FILTER_GL_POSITION(gl_Position, geometry); @@ -66,10 +69,17 @@ void main(void) { } else { vec3 offset_common = vec3(project_pixel_size(pixelOffset), 0.0); DECKGL_FILTER_SIZE(offset_common, geometry); - gl_Position = project_position_to_clipspace(instancePositions, instancePositions64Low, offset_common, geometry.position); + gl_Position = project_position_to_clipspace(instancePositions, instancePositions64Low, offset_common, geometry.position); DECKGL_FILTER_GL_POSITION(gl_Position, geometry); } + // Hide icons/text that are occluded by the globe (on the back side) + // Use anchor position (without pixel offset) for consistent occlusion behavior + if (project_globe_is_occluded(commonPosition)) { + // Move to clip space position that will be clipped + gl_Position = vec4(0.0, 0.0, 2.0, 1.0); + } + vTextureCoords = mix( instanceIconFrames.xy, instanceIconFrames.xy + iconSize, diff --git a/modules/layers/src/icon-layer/icon-layer.wgsl.ts b/modules/layers/src/icon-layer/icon-layer.wgsl.ts index 1ba09cbd0fb..ee47961dedf 100644 --- a/modules/layers/src/icon-layer/icon-layer.wgsl.ts +++ b/modules/layers/src/icon-layer/icon-layer.wgsl.ts @@ -78,6 +78,9 @@ fn vertexMain(inp: Attributes) -> Varyings { pixelOffset = pixelOffset + inp.instancePixelOffset; pixelOffset.y = pixelOffset.y * -1.0; + // Calculate common position for globe occlusion check + let commonPosition = project_position_vec3_f64(inp.instancePositions, inp.instancePositions64Low); + if (icon.billboard != 0) { var pos = project_position_to_clipspace(inp.instancePositions, inp.instancePositions64Low, vec3(0.0)); // TODO, &geometry.position); // DECKGL_FILTER_GL_POSITION(pos, geometry); @@ -95,6 +98,12 @@ fn vertexMain(inp: Attributes) -> Varyings { outp.position = pos; } + // Hide icons/text that are occluded by the globe (on the back side) + if (project_globe_is_occluded(commonPosition)) { + // Move to clip space position that will be clipped + outp.position = vec4(0.0, 0.0, 2.0, 1.0); + } + let uvMix = (inp.positions.xy + vec2(1.0, 1.0)) * 0.5; outp.vTextureCoords = mix(inp.instanceIconFrames.xy, inp.instanceIconFrames.xy + iconSize, uvMix) / icon.iconsTextureDim; diff --git a/modules/layers/src/text-layer/text-background-layer/text-background-layer-vertex.glsl.ts b/modules/layers/src/text-layer/text-background-layer/text-background-layer-vertex.glsl.ts index 1a821c8801b..51cf994e053 100644 --- a/modules/layers/src/text-layer/text-background-layer/text-background-layer-vertex.glsl.ts +++ b/modules/layers/src/text-layer/text-background-layer/text-background-layer-vertex.glsl.ts @@ -55,6 +55,9 @@ void main(void) { pixelOffset += instancePixelOffsets; pixelOffset.y *= -1.0; + // Calculate common position for globe occlusion check (anchor position without offset) + vec3 commonPosition = project_position(instancePositions, instancePositions64Low); + if (textBackground.billboard) { gl_Position = project_position_to_clipspace(instancePositions, instancePositions64Low, vec3(0.0), geometry.position); DECKGL_FILTER_GL_POSITION(gl_Position, geometry); @@ -68,6 +71,13 @@ void main(void) { DECKGL_FILTER_GL_POSITION(gl_Position, geometry); } + // Hide text backgrounds that are occluded by the globe (on the back side) + // Use anchor position (without pixel offset) for consistent occlusion behavior + if (project_globe_is_occluded(commonPosition)) { + // Move to clip space position that will be clipped + gl_Position = vec4(0.0, 0.0, 2.0, 1.0); + } + // Apply opacity to instance color, or return instance picking color vFillColor = vec4(instanceFillColors.rgb, instanceFillColors.a * layer.opacity); DECKGL_FILTER_COLOR(vFillColor, geometry); diff --git a/modules/mapbox/src/deck-utils.ts b/modules/mapbox/src/deck-utils.ts index 7ad9d017989..0c19b5ae805 100644 --- a/modules/mapbox/src/deck-utils.ts +++ b/modules/mapbox/src/deck-utils.ts @@ -111,9 +111,10 @@ export function getDefaultParameters(map: Map, interleaved: boolean): Parameters blendAlphaOperation: 'add' } : {}; - if (getProjection(map) === 'globe') { - result.cullMode = 'back'; - } + // Note: Globe occlusion (hiding geometry on the back of the globe) is handled + // in the shader via project_globe_get_occlusion() rather than GPU back-face culling. + // Back-face culling doesn't work correctly for billboard geometry (IconLayer, TextLayer) + // which always faces the camera. return result; } diff --git a/test/apps/projection/app.tsx b/test/apps/projection/app.tsx index 9d14e66b292..b81d80f12a7 100644 --- a/test/apps/projection/app.tsx +++ b/test/apps/projection/app.tsx @@ -144,7 +144,6 @@ function App() { <>