Skip to content

[Bug] OrbitController pan diverges exponentially when a offset TerrainExtension layer is picked as the pan pivot #10334

@marcomuser

Description

@marcomuser

Description

When an OrbitView with dragMode: 'pan' is used together with TerrainExtension, panning works correctly when clicking on the terrain layer directly, but causes runaway (exponentially growing) camera movement when the click lands on a layer that uses TerrainExtension with terrainDrawMode: 'offset', e.g. an EditableGeoJsonLayer over a point-cloud layer.

Root cause

OrbitController._unproject3D calls deck.pickObject({ unproject3D: true }) to determine the 3D world position that will be used as the pan pivot (startPanPosition in OrbitState). The depth for the unproject3D coordinate is resolved in DeckPicker._getDepthLayers:

// packages/core/src/lib/deck-picker.ts
_getDepthLayers(pickInfo, pickableLayers, unproject3D) {
  if (!unproject3D || !this.depthFBO) return [];

  const { pickedLayer } = pickInfo;
  const isDraped = pickedLayer?.state?.terrainDrawMode === 'drape';

  if (pickedLayer && !isDraped) {
    return [pickedLayer];             // <-- re-renders pickedLayer for depth
  }
  // For draped layers or no pick: use terrain layers
  return pickableLayers.filter(l => l.props.operation.includes('terrain'));
}

When the picked layer uses terrainDrawMode: 'offset', isDraped is false, so [pickedLayer] is returned. The picker re-renders that layer in a dedicated depth pass to extract the Z value. However, in this depth pass the terrain heightmap texture (needed to apply the terrain offset in the vertex shader) is not reliably available, so the geometry is evaluated at its raw data Z (= 0). The resulting startPanPosition is [x, y, 0] instead of [x, y, terrain_z].

Workaround

Subclass OrbitController to ignore non-terrain picks. This fixes it in my setup.

class TerrainAwareOrbitController extends OrbitController {
  protected _unproject3D = (pos: number[]): number[] | null => {
    if (!this.pickPosition) return null;
    const { x, y } = this.props as { x: number; y: number };
    const pickResult = this.pickPosition(x + pos[0], y + pos[1]);
    // Only accept picks from layers explicitly marked pickable:'3d'
    // (the terrain layer); fall back to viewport.unproject at target depth.
    if (
      pickResult?.coordinate &&
      (pickResult as any).layer?.props.pickable === '3d'
    ) {
      return pickResult.coordinate;
    }
    return null;
  };
}

class TerrainOrbitView extends OrbitView {
  get ControllerType() { return TerrainAwareOrbitController; }
}

Flavors

  • Script tag
  • React
  • Python/Jupyter notebook
  • MapboxOverlay
  • GoogleMapsOverlay
  • CARTO
  • ArcGIS

Expected Behavior

Panning should feel the same regardless of which pickable layer the pointer lands on. The 3D pan pivot should always resolve to the terrain surface.

Steps to Reproduce

  • OrbitView with controller: { dragMode: 'pan' }
  • PointCloudLayer with operation: 'terrain+draw' and pickable: '3d' — defines the terrain surface
  • A second layer (e.g. EditableGeoJsonLayer) with extensions: [new TerrainExtension()] and terrainDrawMode: 'offset' rendered on top

Environment

  • Framework version: 9.3.0
  • Browser: Chrome
  • OS: Mac

Logs

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions