Skip to content

feat: Initial Globe view support#563

Merged
kylebarron merged 15 commits into
mainfrom
kyle/globe-view-may2026
May 22, 2026
Merged

feat: Initial Globe view support#563
kylebarron merged 15 commits into
mainfrom
kyle/globe-view-may2026

Conversation

@kylebarron
Copy link
Copy Markdown
Member

@kylebarron kylebarron commented May 21, 2026

Screen.Recording.2026-05-22.at.12.02.20.PM.mov

Written by Claude on behalf of @kylebarron

What I am changing

Adds prototype globe-view support to deck.gl-raster. RasterTileLayer-based
layers (COG/Zarr) can now render on a 3D globe — deck.gl _GlobeView, or
MapLibre projection: 'globe' with deck.gl interleaved — not just the flat Web
Mercator map. Includes a new cog-globe example.

Deliberately scoped as a prototype: it renders real data end-to-end and
exercises the full pipeline, with the harder accuracy work called out and
deferred (see Limitations). Design + deferred work:
dev-docs/specs/2026-05-21-globe-view-design.md.

How I did it

  • Globe tile selection. Implemented the globe computeBoundingVolume
    (was assert(false)): sample reference points (REF_POINTS_11), reproject to
    WGS84, and project onto the sphere via viewport.projectPosition for the
    frustum-culling oriented bounding box; keep a Web-Mercator-world AABB for the
    dataset pre-filter and the LOD latitude. The bounding-volume cache is rebuilt
    when the viewport projection mode switches (globe↔mercator).
  • lng/lat-direct rendering. Globe renders in coordinateSystem: 'lnglat'
    (mesh vertices in WGS84 via projectTo4326) — no manual common-space mapping.
    Collapsed SimpleMeshLayer's composeModelMatrix branch in the
    MeshTextureLayer vertex shader to a single direct-projection path (the old
    branch projected lng/lat degrees as if they were meters).
  • Z-fighting with the basemap. The raster mesh is coplanar with MapLibre's
    globe sphere in the shared interleaved depth buffer. A depth bias doesn't help
    with maplibre's globe depth encoding; instead depthCompare: 'always' +
    back-face culling (cullMode: 'back' for this grid's winding). See
    visgl/deck.gl#9592.
  • LOD fix. The meters-per-pixel latitude must come from the mercator-world
    bounds, not the 3D OBB center (globe-common → worldToLngLat returns ~-89°
    near the Mercator singularity → meters/px far too small → the traversal loads
    the finest tiles across the whole globe when zoomed out).
  • Anti-faceting scaffold (throwaway). Globe builds a uniform per-tile grid
    instead of the adaptive Delatin mesh, whose reprojection-error metric is blind
    to sphere curvature (an EPSG:4326 source yields 2 triangles that chord through
    the sphere).
  • Merged main, reconciling with fix: traverse tiles across world copies (#517) #518 (world copies); world-copy passes are
    gated on viewport.subViewports, so they don't run on a globe.
  • Documented the globe path in dev-docs/coordinate-systems.md.

How you can test it

pnpm install
pnpm --filter @developmentseed/deck.gl-raster build
pnpm --filter @developmentseed/deck.gl-geotiff build
pnpm --filter deck.gl-cog-globe-example dev

Confirm: the basemap renders as a 3D globe, the COG drapes onto the sphere and
lines up with the basemap (source selector to try several COGs), no z-fighting,
back-of-globe tiles are occluded, and LOD tracks zoom (coarse when zoomed out,
finer when zoomed in).

Unit tests: pnpm --filter @developmentseed/deck.gl-raster test — globe
bounding volume, cache rebuild on projection switch, and a globe LOD regression
test.

Limitations (deferred — tracked as follow-up issues)

Related Issues

kylebarron and others added 2 commits May 21, 2026 12:39
Scopes a working globe-view prototype (globe tile-selection bounding
volume, lng/lat-direct render path via a shader unification, cog-globe
and zarr-globe examples over MapLibre globe, throwaway anti-faceting
scaffold) while deliberately carving out spherical-reprojection
correctness as a follow-up design.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the assert(false) globe stub in computeBoundingVolume with an
oriented bounding box built from WGS84 reference points projected onto
the globe sphere via viewport.projectPosition. Makes the bounding-volume
cache key projection-aware so globe and mercator volumes don't collide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread packages/deck.gl-raster/src/raster-tileset/bounding-volume-cache.ts Outdated
Comment thread packages/deck.gl-raster/src/raster-tileset/bounding-volume-cache.ts Outdated
Comment thread packages/deck.gl-raster/src/raster-tileset/raster-tile-traversal.ts
- Drop the projection-namespaced cache key; instead RasterTileset2D
  clears the BoundingVolumeCache when the viewport projection mode
  switches (globe<->mercator). Simpler z/x/y key; adds a clear() method.
- Always use REF_POINTS_11 for the globe bounding volume. Our descriptor
  z is an internal overview index, not web-mercator zoom, so upstream's
  zoom-keyed point count doesn't transfer; a tile never spans more than
  the whole world, so 11 points always suffice and the cache makes the
  per-tile cost one-time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread packages/deck.gl-raster/src/raster-tileset/raster-tileset-2d.ts
Comment thread packages/deck.gl-raster/src/raster-tileset/bounding-volume-cache.ts
kylebarron and others added 3 commits May 21, 2026 14:09
Follow the repo convention of aliasing deck.gl's underscore-prefixed
exports (cf. _Tileset2D as Tileset2D).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapse SimpleMeshLayer's composeModelMatrix branch to one
direct-projection path. MeshTextureLayer always draws one identity mesh
at the origin, so projecting the mesh vertex directly is correct for
both cartesian (Web Mercator) and lnglat (GlobeView). Fixes the globe
path, which previously hit the meters-offset branch and ran
project_size on lng/lat degrees. Behavior-preserving for mercator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Collapse SimpleMeshLayer's composeModelMatrix branch to one
direct-projection path. MeshTextureLayer always draws one identity mesh
at the origin, so projecting the mesh vertex directly is correct for
both cartesian (Web Mercator) and lnglat (GlobeView). Fixes the globe
path, which previously hit the meters-offset branch and ran
project_size on lng/lat degrees. Behavior-preserving for mercator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kylebarron kylebarron force-pushed the kyle/globe-view-may2026 branch from 037f178 to 0f3691b Compare May 21, 2026 18:54
kylebarron and others added 7 commits May 21, 2026 14:57
In globe mode, build a uniform per-tile grid instead of the adaptive
Delatin mesh, whose reprojection-error metric is blind to sphere
curvature and facets EPSG:4326 sources at low zoom. Stopgap only; to be
removed when sphere-aware reprojection lands. Globe is detected via
viewport.resolution (null-safe so direct _generateMesh calls in tests
fall back to the reprojector path).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standalone example rendering an EPSG:4326 COG on MapLibre's globe
projection (projection="globe") with deck.gl interleaved. Exercises the
globe tile-selection, lng/lat render path, and uniform-grid scaffold
end-to-end. fitBounds frames the data wherever the COG sits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In globe mode the raster mesh is coplanar with MapLibre's basemap sphere
and z-fights in the shared interleaved depth buffer. A depth bias does
not help with maplibre's globe depth encoding; instead skip depth
comparison (depthCompare: "always") and occlude the far hemisphere with
back-face culling (cullMode: "back" for this grid's winding). Approach
per visgl/deck.gl#9592.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirror cog-basic's source list + selector (and debug controls) so every
COG source can be tested on the globe. fitBounds reframes on each switch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The LOD meters-per-pixel read the latitude from the 3D oriented bounding
box center, which on a globe is in globe common space (y far outside the
Web Mercator world range), so worldToLngLat returned ~-89deg and
cos(lat) made meters-per-pixel 10-270x too small. devicePixelsPerSource
was then always >>1, so the traversal always recursed to the finest
level -- loading the finest tiles across the whole globe when zoomed out.

Drive the latitude from commonSpaceBounds instead, which is in Web
Mercator world space in both the mercator and globe paths. Adds a
regression test asserting LOD tracks zoom on a globe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Globe view (prototype)" section: lnglat-direct rendering, the
two-coordinate-space tile selection, the LOD latitude gotcha (+ limb
foreshortening caveat), the depthCompare/cullMode z-fighting recipe, and
the throwaway anti-faceting grid. Links the globe-view spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kylebarron kylebarron changed the title Kyle/globe view may2026 feat: Globe view May 22, 2026
@github-actions github-actions Bot added the feat label May 22, 2026
Reconciles the globe-view work with #518 ("traverse tiles across world
copies"), which refactored raster-tile-traversal.ts. The globe additions
(globe bounding volume, REF_POINTS_11, lnglat LOD via
getMetersPerPixelAtCommonSpaceBounds) layer onto main's worldOffset
structure; update() now also destructures commonSpaceBounds for the LOD
latitude. World-copy passes are gated on viewport.subViewports (>1), so
they never run on a globe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread examples/cog-globe/README.md
Comment thread packages/deck.gl-raster/src/raster-layer.ts Outdated
Per review: the depthCompare/cullMode recipe for z-fighting against the
basemap depends on the compositing context (MapLibre interleaved globe
handedness vs a standalone _GlobeView), so it's an app decision, not a
library one. RasterLayer no longer injects it; deck.gl forwards the
`parameters` prop from COGLayer down to the mesh via getSubLayerProps,
and the cog-globe example sets it. Also simplify the cog-globe README.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kylebarron kylebarron marked this pull request as ready for review May 22, 2026 16:01
@kylebarron kylebarron merged commit e538d97 into main May 22, 2026
5 checks passed
@kylebarron kylebarron deleted the kyle/globe-view-may2026 branch May 22, 2026 16:04
@kylebarron kylebarron self-assigned this May 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant