-
Notifications
You must be signed in to change notification settings - Fork 2.2k
feat(extensions): TerrainExtension GlobeView support #10251
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
0a45963
59cd841
34d4813
b3d4ecf
92f63bb
444afe7
5a6627e
20765c0
408bc0a
abeed1a
7dd49ce
95b48f3
9352f89
6f4e2b2
0d3cbe2
5aafcb7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,7 +3,14 @@ | |
| // Copyright (c) vis.gl contributors | ||
|
|
||
| import {Device, Framebuffer} from '@luma.gl/core'; | ||
| import {joinLayerBounds, getRenderBounds, makeViewport, Bounds} from '../utils/projection-utils'; | ||
| import { | ||
| joinLayerBounds, | ||
| getRenderBounds, | ||
| makeViewport, | ||
| getMercatorReferenceViewport, | ||
| lngLatToMercatorCommon, | ||
| Bounds | ||
| } from '../utils/projection-utils'; | ||
| import {createRenderTarget} from './utils'; | ||
|
|
||
| import type {Viewport, Layer} from '@deck.gl/core'; | ||
|
|
@@ -72,18 +79,31 @@ export class HeightMapBuilder { | |
| ); | ||
|
|
||
| if (layersChanged) { | ||
| // Recalculate cached bounds | ||
| // Recalculate cached bounds. | ||
| // Use a Mercator reference viewport so layer bounds live in ABSOLUTE | ||
| // Mercator common space — same rationale as terrain-cover.ts. On | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we add mercator common space as a supported coordinateSystem? |
||
| // GlobeView, `viewport.projectPosition` would return 3D sphere cartesian | ||
| // coords that can't be compared against screen-space render bounds. | ||
| this.layers = layers; | ||
| this.layersBounds = layers.map(layer => layer.getBounds()); | ||
| this.layersBoundsCommon = joinLayerBounds(layers, viewport); | ||
| this.layersBoundsCommon = joinLayerBounds(layers, getMercatorReferenceViewport(viewport)); | ||
| } | ||
|
|
||
| const viewportChanged = !this.lastViewport || !viewport.equals(this.lastViewport); | ||
|
|
||
| if (!this.layersBoundsCommon) { | ||
| this.renderViewport = null; | ||
| } else if (layersChanged || viewportChanged) { | ||
| const bounds = getRenderBounds(this.layersBoundsCommon, viewport); | ||
| // getRenderBounds intersects layer bounds with viewport bounds. On globe, | ||
| // viewport bounds project to sphere cartesian and won't intersect the | ||
| // Mercator layer bounds meaningfully — use the full layer bounds instead. | ||
| const isGlobe = Boolean( | ||
| (viewport as {resolution?: number}).resolution && | ||
| (viewport as {resolution?: number}).resolution! > 0 | ||
| ); | ||
| const bounds = isGlobe | ||
| ? this.layersBoundsCommon | ||
| : getRenderBounds(this.layersBoundsCommon, viewport); | ||
| if (bounds[2] <= bounds[0] || bounds[3] <= bounds[1]) { | ||
| this.renderViewport = null; | ||
| return false; | ||
|
|
@@ -96,19 +116,25 @@ export class HeightMapBuilder { | |
| const pixelWidth = (bounds[2] - bounds[0]) * scale; | ||
| const pixelHeight = (bounds[3] - bounds[1]) * scale; | ||
|
|
||
| // Center for the render viewport must be expressed in Mercator common so | ||
| // makeViewport (which unprojects through a WebMercatorViewport for | ||
| // geospatial inputs) gets a valid lng/lat back. `viewport.center` on | ||
| // GlobeView is 3D sphere cartesian and would unproject bogusly. | ||
| const centerMerc = viewport.isGeospatial | ||
| ? lngLatToMercatorCommon([ | ||
| (viewport as {longitude?: number}).longitude ?? 0, | ||
| (viewport as {latitude?: number}).latitude ?? 0 | ||
| ]) | ||
| : [viewport.center[0], viewport.center[1]]; | ||
|
|
||
| this.renderViewport = | ||
| pixelWidth > 0 || pixelHeight > 0 | ||
| ? makeViewport({ | ||
| // It's not important whether the geometry is visible in this viewport, because | ||
| // vertices will not use the standard project_to_clipspace in the DRAW_TO_HEIGHT_MAP shader | ||
| // However the viewport must have the same center and zoom as the screen viewport | ||
| // So that projection uniforms used for calculating z are the same | ||
| bounds: [ | ||
| viewport.center[0] - 1, | ||
| viewport.center[1] - 1, | ||
| viewport.center[0] + 1, | ||
| viewport.center[1] + 1 | ||
| ], | ||
| bounds: [centerMerc[0] - 1, centerMerc[1] - 1, centerMerc[0] + 1, centerMerc[1] + 1], | ||
| zoom: viewport.zoom, | ||
| width: Math.min(pixelWidth, MAP_MAX_SIZE), | ||
| height: Math.min(pixelHeight, MAP_MAX_SIZE), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,7 +5,7 @@ | |
| /* eslint-disable camelcase */ | ||
|
|
||
| import type {ShaderModule} from '@luma.gl/shadertools'; | ||
| import {project, ProjectProps, ProjectUniforms} from '@deck.gl/core'; | ||
| import {project, ProjectProps} from '@deck.gl/core'; | ||
|
|
||
| import type {Texture} from '@luma.gl/core'; | ||
| import type {Bounds} from '../utils/projection-utils'; | ||
|
|
@@ -67,10 +67,16 @@ uniform sampler2D terrain_map; | |
| export const terrainModule = { | ||
| name: 'terrain', | ||
| dependencies: [project], | ||
| // eslint-disable-next-line prefer-template | ||
| vs: uniformBlock + /* glsl */ 'out vec3 commonPos;', | ||
| // eslint-disable-next-line prefer-template | ||
| fs: uniformBlock + /* glsl */ 'in vec3 commonPos;', | ||
| vs: `${uniformBlock} | ||
| out vec3 commonPos; | ||
| // Fragment position in ABSOLUTE Mercator common space, regardless of the live | ||
| // viewport's projection mode. Computed here (not in FS) because the project | ||
| // module's helpers (project_mercator_, PROJECTION_MODE_*) are only declared | ||
| // in the vertex shader. Mercator is log-nonlinear in lat, but terrain meshes | ||
| // are fine enough that varying-interpolation error is negligible. | ||
| out vec2 terrainMercPos; | ||
| `, | ||
| fs: `${uniformBlock}in vec3 commonPos;\nin vec2 terrainMercPos;`, | ||
| inject: { | ||
| 'vs:#main-start': /* glsl */ ` | ||
| if (terrain.mode == TERRAIN_MODE_SKIP) { | ||
|
|
@@ -80,16 +86,33 @@ if (terrain.mode == TERRAIN_MODE_SKIP) { | |
| `, | ||
| 'vs:DECKGL_FILTER_GL_POSITION': /* glsl */ ` | ||
| commonPos = geometry.position.xyz; | ||
| if (project.projectionMode == PROJECTION_MODE_GLOBE) { | ||
| // Unproject globe cartesian (see project_globe_) back to lng/lat, then | ||
| // forward-project through project_mercator_. Elevation scales the sphere | ||
| // radius uniformly, so angular components recover cleanly. | ||
| vec3 cp = commonPos; | ||
| float D = length(cp); | ||
| float lat = degrees(asin(clamp(cp.z / D, -1.0, 1.0))); | ||
| float lng = degrees(atan(cp.x, -cp.y)); | ||
|
charlieforward9 marked this conversation as resolved.
Outdated
|
||
| terrainMercPos = project_mercator_(vec2(lng, lat)); | ||
| } else { | ||
| // Web Mercator modes: commonPos.xy is mercator-common minus commonOrigin. | ||
| terrainMercPos = commonPos.xy + project.commonOrigin.xy; | ||
| } | ||
| if (terrain.mode == TERRAIN_MODE_WRITE_HEIGHT_MAP) { | ||
| vec2 texCoords = (commonPos.xy - terrain.bounds.xy) / terrain.bounds.zw; | ||
| // Height-map bounds are in ABSOLUTE Mercator common (so the FBO is reusable | ||
| // across MapView / GlobeView). Use the mercator xy computed above. | ||
| vec2 texCoords = (terrainMercPos - terrain.bounds.xy) / terrain.bounds.zw; | ||
| position = vec4(texCoords * 2.0 - 1.0, 0.0, 1.0); | ||
| commonPos.z += project.commonOrigin.z; | ||
| } | ||
| if (terrain.mode == TERRAIN_MODE_USE_HEIGHT_MAP) { | ||
| vec3 anchor = geometry.worldPosition; | ||
| anchor.z = 0.0; | ||
| vec3 anchorCommon = project_position(anchor); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. OK, so if I understand correctly you're basically forcing if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR) {A natural question that comes to mind is that if we are going to hardcode a projection, then why Mercator? Sure it works well for
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps related, I have been feeling that if we extend layers to take a "deformation" when generating geometry, such as a deformation UV grid for BitmapLayer than we could generalize that to support any projection. |
||
| vec2 texCoords = (anchorCommon.xy - terrain.bounds.xy) / terrain.bounds.zw; | ||
| // worldPosition.xy is lng/lat for geospatial instance-position layers | ||
| // (IconLayer, TextLayer, etc.) — project directly through mercator so the | ||
| // UV matches the absolute-mercator bounds used by WRITE_HEIGHT_MAP, on | ||
| // both MapView and GlobeView. | ||
| vec2 anchorMerc = project_mercator_(geometry.worldPosition.xy); | ||
| vec2 texCoords = (anchorMerc - terrain.bounds.xy) / terrain.bounds.zw; | ||
| if (texCoords.x >= 0.0 && texCoords.y >= 0.0 && texCoords.x <= 1.0 && texCoords.y <= 1.0) { | ||
| float terrainZ = texture(terrain_map, texCoords).r; | ||
| geometry.position.z += terrainZ; | ||
|
|
@@ -105,7 +128,7 @@ if (terrain.mode == TERRAIN_MODE_WRITE_HEIGHT_MAP) { | |
| `, | ||
| 'fs:DECKGL_FILTER_COLOR': /* glsl */ ` | ||
| if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_USE_COVER_ONLY)) { | ||
| vec2 texCoords = (commonPos.xy - terrain.bounds.xy) / terrain.bounds.zw; | ||
| vec2 texCoords = (terrainMercPos - terrain.bounds.xy) / terrain.bounds.zw; | ||
| vec4 pixel = texture(terrain_map, texCoords); | ||
| if (terrain.mode == TERRAIN_MODE_USE_COVER_ONLY) { | ||
| color = pixel; | ||
|
|
@@ -129,8 +152,9 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US | |
| useTerrainHeightMap, | ||
| terrainSkipRender | ||
| } = opts; | ||
| const projectUniforms = project.getUniforms(opts.project) as ProjectUniforms; | ||
| const {commonOrigin} = projectUniforms; | ||
| // All modes now pack bounds in absolute Mercator common; shader samples | ||
| // against absolute xy computed per-fragment, so we no longer need the | ||
| // project module's commonOrigin here. | ||
|
|
||
| let mode: number = terrainSkipRender ? TERRAIN_MODE.SKIP : TERRAIN_MODE.NONE; | ||
| // height map if case USE_HEIGHT_MAP, terrain cover if USE_COVER, otherwise empty | ||
|
|
@@ -165,18 +189,16 @@ if ((terrain.mode == TERRAIN_MODE_USE_COVER) || (terrain.mode == TERRAIN_MODE_US | |
| } | ||
| } | ||
|
|
||
| // All bounds live in ABSOLUTE Mercator common space so the FBOs can be | ||
| // shared across MapView and GlobeView (see terrain-cover.ts and | ||
| // height-map-builder.ts). No commonOrigin subtract. | ||
| /* eslint-disable camelcase */ | ||
| return { | ||
| mode, | ||
| terrain_map: sampler, | ||
| // Convert bounds to the common space, as [minX, minY, width, height] | ||
| // Pack bounds as [minX, minY, width, height] | ||
| bounds: bounds | ||
| ? [ | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I worry this will introduce precision issues |
||
| bounds[0] - commonOrigin[0], | ||
| bounds[1] - commonOrigin[1], | ||
| bounds[2] - bounds[0], | ||
| bounds[3] - bounds[1] | ||
| ] | ||
| ? [bounds[0], bounds[1], bounds[2] - bounds[0], bounds[3] - bounds[1]] | ||
| : [0, 0, 0, 0] | ||
| }; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,14 @@ import {Framebuffer} from '@luma.gl/core'; | |
| import type {Layer, Viewport} from '@deck.gl/core'; | ||
|
|
||
| import {createRenderTarget} from './utils'; | ||
| import {joinLayerBounds, makeViewport, getRenderBounds, Bounds} from '../utils/projection-utils'; | ||
| import { | ||
| getMercatorReferenceViewport, | ||
| joinLayerBounds, | ||
| lngLatToMercatorCommon, | ||
| makeViewport, | ||
| getRenderBounds, | ||
| Bounds | ||
| } from '../utils/projection-utils'; | ||
|
|
||
| type TileHeader = { | ||
| boundingBox: [min: number[], max: number[]]; | ||
|
|
@@ -113,13 +120,18 @@ export class TerrainCover { | |
| const targetLayer = this.targetLayer; | ||
| let shouldRedraw = false; | ||
|
|
||
| // Bounds are computed in ABSOLUTE Mercator common space — NOT the live | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we try to use a cartesian space? Seems like a closer match to the spherical coordinates |
||
| // viewport's common space. The terrain cover FBO is rendered via a | ||
| // WebMercatorViewport regardless of the screen viewport, so UVs must also | ||
| // live in Mercator. This is what lets the same cover texture be sampled | ||
| // from MapView and GlobeView. | ||
| if (this.tile && 'boundingBox' in this.tile) { | ||
| if (!this.targetBounds) { | ||
| shouldRedraw = true; | ||
| this.targetBounds = this.tile.boundingBox; | ||
|
|
||
| const bottomLeftCommon = viewport.projectPosition(this.targetBounds[0]); | ||
| const topRightCommon = viewport.projectPosition(this.targetBounds[1]); | ||
| const bottomLeftCommon = lngLatToMercatorCommon(this.targetBounds[0]); | ||
| const topRightCommon = lngLatToMercatorCommon(this.targetBounds[1]); | ||
| this.targetBoundsCommon = [ | ||
| bottomLeftCommon[0], | ||
| bottomLeftCommon[1], | ||
|
|
@@ -131,7 +143,14 @@ export class TerrainCover { | |
| // console.log('bounds changed', this.bounds, '>>', newBounds); | ||
| shouldRedraw = true; | ||
| this.targetBounds = targetLayer.getBounds(); | ||
| this.targetBoundsCommon = joinLayerBounds([targetLayer], viewport); | ||
| // Non-tile terrain layer: project layer bounds through the Mercator | ||
| // reference so the cover is projection-invariant. joinLayerBounds uses | ||
| // layer.projectPosition() internally, which honors the layer's | ||
| // coordinateSystem (LNGLAT / CARTESIAN / METER_OFFSETS). | ||
| this.targetBoundsCommon = joinLayerBounds( | ||
| [targetLayer], | ||
| getMercatorReferenceViewport(viewport) | ||
| ); | ||
| } | ||
|
|
||
| if (!this.targetBoundsCommon) { | ||
|
|
@@ -146,7 +165,15 @@ export class TerrainCover { | |
| } else { | ||
| const oldZoom = this.renderViewport?.zoom; | ||
| shouldRedraw = shouldRedraw || newZoom !== oldZoom; | ||
| const newBounds = getRenderBounds(this.targetBoundsCommon, viewport); | ||
| // getRenderBounds intersects layer bounds (Mercator) with viewport bounds | ||
| // derived via viewport.projectPosition. On GlobeView that yields sphere | ||
| // cartesian coords, which would corrupt the intersection. Fall back to | ||
| // full layer bounds on non-Mercator geospatial viewports — resolution | ||
| // is reduced but output stays correct. | ||
| const isGlobe = Boolean(viewport.resolution && viewport.resolution > 0); | ||
| const newBounds = isGlobe | ||
| ? this.targetBoundsCommon | ||
| : getRenderBounds(this.targetBoundsCommon, viewport); | ||
| const oldBounds = this.bounds; | ||
| shouldRedraw = shouldRedraw || !oldBounds || newBounds.some((x, i) => x !== oldBounds[i]); | ||
| this.bounds = newBounds; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.