Skip to content

[Bug] Icon Layer on Globe Projection #9554

Open

Description

@charlieforward9

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)

Screen.Recording.2025-03-29.at.4.14.08.PM.mov

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

Image

Original Comment: #9199 (comment)

Flavors

  • Script tag
  • React
  • Python/Jupyter notebook
  • 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

Activity

  1. charlieforward9 commented on Apr 8, 2025

    @charlieforward9
    MemberAuthor

    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,
      },
    }),
    Globe.Spin.mp4
  2. chrisgervang commented on Jun 3, 2025

    @chrisgervang
    Collaborator

    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.

  3. tmrdlt commented on Feb 3, 2026

    @tmrdlt

    @charlieforward9 Do you know, if are there any updates regarding globe projection support for the IconLayer in deck.gl? If not, can you recomment any alternative layer I could use without the need for the camera-space test?

  4. chrisgervang commented on Feb 3, 2026

    @chrisgervang
    Collaborator

    Hey @tmrdlt, thanks for resurfacing this issue. I've drafted a fix in #9975. I'll test out the gist shared here

  5. chrisgervang commented on Apr 1, 2026

    @chrisgervang
    Collaborator

    I've removed this from the 9.3 roadmap, and think it's worth revisiting. I've linked this issue with the other two related issues. I'm not convinced the fix proposed in #9975 looks better than the current workaround. The important requirements include:

    In interleaved and non-interleaved mode:

    1. the globe shouldn't clip foreground billboard layers
    2. the globe should trigger automatic occlusion on billboard layers when they're beyond the horizon

    The PR satisfied 2, but not 1.

  6. charlieforward9 commented on Apr 1, 2026

    @charlieforward9
    MemberAuthor

    @chrisgervang i appreciate you looking into this in more detail and stepping back to try different approaches.

    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.

    The original workaround I built was to do layer-specific filtering based on the dot product of the data coordinate and viewstate with an additional filter on the opacity field to "transition fade".

    This is a GlobeView specific workaround. I wonder if we could
    try to land something like this as an extension or a GlobeView patch with a combination of our approaches?

    Just trying to keep this conversation moving forward

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions