deck.gl icon indicating copy to clipboard operation
deck.gl copied to clipboard

[Bug] Icon Layer on Globe Projection

Open charlieforward9 opened this issue 8 months ago • 2 comments

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

Image

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

charlieforward9 avatar Mar 29 '25 20:03 charlieforward9

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

charlieforward9 avatar Apr 08 '25 04:04 charlieforward9

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.

chrisgervang avatar Jun 03 '25 23:06 chrisgervang