deck.gl
deck.gl copied to clipboard
[Bug] Icon Layer on Globe Projection
Description
I am not able to properly visualize IconLayers on my Globe-projected DeckGL instance.
In my React-Maplibre-MapboxOverlay implementation mercator projection shows up great, but globe projection glitches out. (tested both interleaved/overlaid)
https://github.com/user-attachments/assets/0fe40c93-2514-4ea2-b09d-0e9caee5b79c
In my (codepen using script tag) reproduction, I am actually able to see the point when the globe is oriented such that the Icon extrudes in the right direction, however, in my implementation, the point disappears after any user interaction.
Additionally, in the reproduction, the IconLayer instances do not show up in the right location. Notice how the coordinates of the sample points are all within Florida, however, the IconLayer shows up at the origin
Original Comment: https://github.com/visgl/deck.gl/issues/9199#issuecomment-2674860969
Flavors
- [x] Script tag
- [x] React
- [ ] Python/Jupyter notebook
- [x] MapboxOverlay
- [ ] GoogleMapsOverlay
- [ ] CARTO
- [ ] ArcGIS
Expected Behavior
I would expect the IconLayer to behave as it normally does in the "mercator" projection.
Steps to Reproduce
https://gist.github.com/charlieforward9/7637c8598caa1537ac965ed1603dcdbe
Environment
- Framework version: ^9.1.0
- Browser: Chrome
- OS: MacOS
Logs
No response
I managed to work around this by turning off depthTest and filter my dataset with this method based on the camera position:
// Helper function to determine if a feature is visible from a camera's pov on globe projection
function isFeatureVisibleOnGlobe(
cameraLat: number,
cameraLon: number,
featureLat: number,
featureLon: number,
zoom: number,
): boolean {
const toRad = (deg: number) => (deg * Math.PI) / 180;
// Convert lat/lon to radians
const camLatRad = toRad(cameraLat);
const camLonRad = toRad(cameraLon);
const featLatRad = toRad(featureLat);
const featLonRad = toRad(featureLon);
// Convert to unit vectors
const toVec3 = (lat: number, lon: number): [number, number, number] => {
return [
Math.cos(lat) * Math.cos(lon),
Math.sin(lat),
Math.cos(lat) * Math.sin(lon),
];
};
const camVec = toVec3(camLatRad, camLonRad);
const featVec = toVec3(featLatRad, featLonRad);
// Compute dot product
const dot =
camVec[0] * featVec[0] + camVec[1] * featVec[1] + camVec[2] * featVec[2];
// Convert zoom level to a tighter FOV threshold
const fovDegrees = zoomToFOV(zoom); // field of view in degrees
const halfFovCos = Math.cos(toRad(fovDegrees / 2));
// If the angle between the vectors is within the cone, dot ≥ cos(halfFOV)
return dot >= halfFovCos;
}
// Tune this mapping to match your renderer behavior
function zoomToFOV(zoom: number): number {
const clamped = Math.max(Math.min(zoom, 20), 1);
// At zoom 1 → full hemisphere (≈130° FOV), at zoom 20 → tight 0° FOV
return 130 * (1 - (clamped - 1) / 19); // Range: 130 → 0 degrees
}
//assumes data is a geojson feature collection with Point geometries
new IconClusterLayer<
GeoJSON.Feature<GeoJSON.MultiPoint, GeoJSON.GeoJsonProperties>
>({
data: data.features
.flatMap((feature) =>
feature.geometry.coordinates.map((coordinate) => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: coordinate,
},
properties: feature.properties,
})),
)
.filter((feature) => {
const [lng, lat] = feature.geometry.coordinates;
return isFeatureVisibleOnGlobe(
vs.longitude,
vs.latitude,
lng,
lat,
vs.zoom,
);
}),
...
parameters: {
depthTest: false,
},
}),
https://github.com/user-attachments/assets/f2b21504-c6ac-40d8-8f46-675b02edc6a1
Additionally, in the reproduction, the IconLayer instances do not show up in the right location. Notice how the coordinates of the sample points are all within Florida, however, the IconLayer shows up at the origin
This sub-issue is fixed with this accessor: getPosition: d => d.geometry.coordinates,
The main issue seems to be that the layers are projected onto a 3D sphere, and the icon's z is anchored on that sphere. So, as a consequence, when the globe is tilted/pulled towards the camera, the icon is blocked by the tiles that are "in front" of the anchor.
I spent some time today trying a variety of parameters to solve this without filtering based on camera position.. I wasn't successful. I think the problem is a general issue with billboard-style rendering combined with how the anchor point on the globe contributes to the depth buffer. If the icon were either wrapped around the sphere like the tile meshes are, then maybe this wouldn't be an issue.
For illustration purposes, setting a ridiculous z (`getPosition: d => [0,0, 10000000],) will prevent occlusion.
@charlieforward9 the video and workaround you shared looks great and even fades the icon's opacity as it approaches the edge (is that code not included in your snippet?). I'm not sure what the generic solution for this is, but doing a camera-space test like you're doing does seem like a reliable method.