Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion examples/basemap-browser/src/config/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -38,6 +39,7 @@ export function buildConfig(
interleaved,
globe,
multiView,
billboard,
stressTest
});

Expand All @@ -56,6 +58,7 @@ export function buildConfig(
batched,
globe,
multiView,
billboard,
stressTest,

// Computed configuration
Expand Down
1 change: 1 addition & 0 deletions examples/basemap-browser/src/config/dimensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const DEFAULT_DIMENSIONS: Dimensions = {
batched: true,
globe: false,
multiView: false,
billboard: true,
stressTest: 'none'
};

Expand Down
13 changes: 5 additions & 8 deletions examples/basemap-browser/src/config/layers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type LayerBuildOptions = {
interleaved: boolean;
globe: boolean;
multiView: boolean;
billboard: boolean;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused globe field in LayerBuildOptions type

Low Severity

The globe field remains in the LayerBuildOptions type and is still passed by buildConfig, but the PR removed its only usage (for arcParameters) without removing the field itself. globe is never destructured or referenced in buildLayers, making it dead code that could mislead readers into thinking globe-awareness is handled in the layer building logic.

Additional Locations (1)

Fix in Cursor Fix in Web

stressTest: StressTest;
};

Expand All @@ -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]},
Expand Down Expand Up @@ -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({
Expand All @@ -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
}),
Expand All @@ -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],
Expand Down Expand Up @@ -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]
})
];
}
17 changes: 17 additions & 0 deletions examples/basemap-browser/src/control-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ function getDimensionsFromUrl(): Partial<Dimensions> {
result.multiView = params.get('multiView') === 'true';
}

if (params.has('billboard')) {
result.billboard = params.get('billboard') !== 'false';
}

const stressTest = params.get('stressTest');
if (
stressTest === 'none' ||
Expand All @@ -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);
Expand Down Expand Up @@ -262,6 +267,18 @@ export default function ControlPanel({onConfigChange}: ControlPanelProps) {
</label>
</div>

{/* Billboard Toggle */}
<div className="section">
<label>
<input
type="checkbox"
checked={dimensions.billboard}
onChange={() => updateDimension('billboard', !dimensions.billboard)}
/>
Billboard (Icons/Text)
</label>
</div>

{/* Stress Test Selection */}
<div className="section">
<div className="label">Stress Test:</div>
Expand Down
2 changes: 2 additions & 0 deletions examples/basemap-browser/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export type Dimensions = {
batched: boolean;
globe: boolean;
multiView: boolean;
billboard: boolean;
stressTest: StressTest;
};

Expand Down Expand Up @@ -68,6 +69,7 @@ export type Config = {
batched: boolean;
globe: boolean;
multiView: boolean;
billboard: boolean;
stressTest: StressTest;

// Computed configuration
Expand Down
3 changes: 0 additions & 3 deletions examples/get-started/pure-js/maplibre-globe/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion examples/website/maplibre/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,6 @@ export default function App({
timeRange,
getSourceColor: [63, 81, 181],
getTargetColor: [63, 181, 173],
parameters: {cullMode: 'none'},
...(interleaveLabels ? {beforeId: 'watername_ocean'} : {})
})
);
Expand Down
18 changes: 18 additions & 0 deletions modules/core/src/shaderlib/project/project.glsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
`;
18 changes: 18 additions & 0 deletions modules/core/src/shaderlib/project/project.wgsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,4 +302,22 @@ fn project_pixel_size_float(pixels: f32) -> f32 {
fn project_pixel_size_vec2(pixels: vec2<f32>) -> vec2<f32> {
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<f32>) -> 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;
}
`;
12 changes: 11 additions & 1 deletion modules/layers/src/icon-layer/icon-layer-vertex.glsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions modules/layers/src/icon-layer/icon-layer.wgsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<f32>(0.0)); // TODO, &geometry.position);
// DECKGL_FILTER_GL_POSITION(pos, geometry);
Expand All @@ -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<f32>(0.0, 0.0, 2.0, 1.0);
}

let uvMix = (inp.positions.xy + vec2<f32>(1.0, 1.0)) * 0.5;
outp.vTextureCoords = mix(inp.instanceIconFrames.xy, inp.instanceIconFrames.xy + iconSize, uvMix) / icon.iconsTextureDim;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
7 changes: 4 additions & 3 deletions modules/mapbox/src/deck-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
1 change: 0 additions & 1 deletion test/apps/projection/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ function App() {
<>
<DeckGL
controller
parameters={{cullMode: 'back'}}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed globe culling from standalone projection test app

Low Severity

Removing parameters={{cullMode: 'back'}} from the standalone DeckGL component causes a regression for the globe view in this test app. The app uses ScatterplotLayer, which has no shader-based globe occlusion. In overlaid mode (not interleaved), there's no shared depth buffer from the basemap, so scatter points on the back of the globe will now be visible — the only prior occlusion mechanism was the cullMode: 'back' parameter.

Fix in Cursor Fix in Web

Copy link
Copy Markdown
Collaborator

@charlieforward9 charlieforward9 Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GlobeView.tranparent.buffer.mov

If it looks like this old screen recording - I consider that a feature demo given the non interleaved config

Copy link
Copy Markdown
Collaborator Author

@chrisgervang chrisgervang Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. It looks like this example could be rewritten to use interleaved, or the cullMode config should be kept

views={opts.view}
initialViewState={opts.viewState}
layers={layers}
Expand Down
Loading