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;
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;
}
`;
6 changes: 6 additions & 0 deletions modules/layers/src/arc-layer/arc-layer-vertex.glsl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
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'}}
views={opts.view}
initialViewState={opts.viewState}
layers={layers}
Expand Down
Loading