From 93b2c904eaa85e65fd8108f889c2ea7cb49656e4 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Sun, 19 Apr 2026 23:54:41 -0400 Subject: [PATCH 1/6] fix(core): project METER_OFFSETS coordinates on GlobeView Tile3DLayer (and any other layer using COORDINATE_SYSTEM.METER_OFFSETS) rendered as flat "flaps" sticking out of the sphere on GlobeView because project_position only handled COORDINATE_SYSTEM_LNGLAT in the PROJECTION_MODE_GLOBE branch. METER_OFFSETS positions fell through to the linear project_offset_ path, which treats the meter offsets as raw common-space units rather than as vectors in the ENU tangent frame anchored at the coordinate origin. Attach the tangent plane to the sphere: build the east/north/up basis at the origin in globe common space, scale the meter offsets by GLOBE_RADIUS / EARTH_RADIUS, and add them to the origin position on the globe surface. Each tile now lands at its correct geographic location oriented with the surface. Also update the google-3d-tiles example with a Map/Globe toggle so the fix can be exercised end-to-end against the Google Photorealistic 3D Tiles dataset. --- examples/website/google-3d-tiles/app.jsx | 43 ++++++++++++++++--- .../src/shaderlib/project/project.glsl.ts | 23 ++++++++++ 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/examples/website/google-3d-tiles/app.jsx b/examples/website/google-3d-tiles/app.jsx index 307d1cc7c2e..ae785151d61 100644 --- a/examples/website/google-3d-tiles/app.jsx +++ b/examples/website/google-3d-tiles/app.jsx @@ -2,11 +2,11 @@ // SPDX-License-Identifier: MIT // Copyright (c) vis.gl contributors -import React, {useState} from 'react'; +import React, {useState, useMemo} from 'react'; import {scaleLinear} from 'd3-scale'; import {createRoot} from 'react-dom/client'; import {DeckGL} from '@deck.gl/react'; -import {TerrainController} from '@deck.gl/core'; +import {MapView, _GlobeView as GlobeView, TerrainController} from '@deck.gl/core'; import {GeoJsonLayer} from '@deck.gl/layers'; import {Tile3DLayer} from '@deck.gl/geo-layers'; import {DataFilterExtension, _TerrainExtension as TerrainExtension} from '@deck.gl/extensions'; @@ -28,10 +28,10 @@ const INITIAL_VIEW_STATE = { latitude: 50.089, longitude: 14.42, zoom: 16, - minZoom: 14, - maxZoom: 18, - bearing: 90, - pitch: 60 + minZoom: 0, + maxZoom: 24, + bearing: 0, + pitch: 0 }; const BUILDING_DATA = @@ -50,6 +50,8 @@ function getTooltip({object}) { export default function App({data = TILESET_URL, distance = 0, opacity = 0.2}) { const [credits, setCredits] = useState(''); + const [useGlobe, setUseGlobe] = useState(false); + const onTraversalComplete = selectedTiles => { const uniqueCredits = new Set(); selectedTiles.forEach(tile => { @@ -90,15 +92,42 @@ export default function App({data = TILESET_URL, distance = 0, opacity = 0.2}) { }) ]; + const view = useMemo( + () => + useGlobe + ? new GlobeView({id: 'view', controller: true}) + : new MapView({ + id: 'view', + controller: {type: TerrainController, touchRotate: true, inertia: 500} + }), + [useGlobe] + ); + return (
+
diff --git a/modules/core/src/shaderlib/project/project.glsl.ts b/modules/core/src/shaderlib/project/project.glsl.ts index a06f6080a5f..ddf8de3fe0e 100644 --- a/modules/core/src/shaderlib/project/project.glsl.ts +++ b/modules/core/src/shaderlib/project/project.glsl.ts @@ -208,6 +208,29 @@ vec4 project_position(vec4 position, vec3 position64Low) { position_world.w ); } + if (project.coordinateSystem == COORDINATE_SYSTEM_METER_OFFSETS) { + // position_world is meters in the ENU tangent frame at coordinateOrigin. + // Attach the tangent plane to the sphere so each tile lands at its + // geographic location oriented with the surface, rather than floating + // as a flat patch in globe common space. + float lambda = radians(project.coordinateOrigin.x); + float phi = radians(project.coordinateOrigin.y); + float cosPhi = cos(phi); + float sinPhi = sin(phi); + float cosLambda = cos(lambda); + float sinLambda = sin(lambda); + vec3 east = vec3(cosLambda, sinLambda, 0.0); + vec3 north = vec3(-sinLambda * sinPhi, cosLambda * sinPhi, cosPhi); + vec3 up = vec3(sinLambda * cosPhi, -cosLambda * cosPhi, sinPhi); + float D = (project.coordinateOrigin.z / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; + float metersToCommon = GLOBE_RADIUS / EARTH_RADIUS; + vec3 offsetCommon = ( + east * position_world.x + + north * position_world.y + + up * position_world.z + ) * metersToCommon; + return vec4(up * D + offsetCommon, position_world.w); + } } if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET) { if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { From e3556f0265b8a61f20d9dde27d258b2597913111 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Mon, 20 Apr 2026 00:01:06 -0400 Subject: [PATCH 2/6] fix(core): tighten GlobeView far plane to horizon distance GlobeView.farZ was computed as (altitude + GLOBE_RADIUS*2*scale/height), which pushes the far plane roughly a full globe-diameter beyond the camera. At close zoom this produces a far/near ratio on the order of 40,000:1, which destroys depth-buffer precision and causes z-fighting on 3D features like Tile3DLayer meshes (buildings, terrain surfaces). Replace with the horizon-distance formula sqrt(h^2 + 2*R*h) - the distance from the camera at altitude h to the tangent horizon on a sphere of radius R. Earth's curvature occludes everything beyond that anyway. Keep a 2x multiplier so tall features (mountains, buildings) peeking above the horizon stay in view. At zoom=16 this drops far/near from ~40,000 to ~500, an ~80x boost in depth precision that eliminates visible z-fighting on 3D tile meshes. At zoom=0 the new bound is slightly tighter than before (1.79 vs 2.14), which is fine - the back half of the globe is occluded anyway. --- modules/core/src/viewports/globe-viewport.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/modules/core/src/viewports/globe-viewport.ts b/modules/core/src/viewports/globe-viewport.ts index 578d49530ce..7d0c6d00825 100644 --- a/modules/core/src/viewports/globe-viewport.ts +++ b/modules/core/src/viewports/globe-viewport.ts @@ -101,7 +101,15 @@ export default class GlobeViewport extends Viewport { // https://github.com/maplibre/maplibre-gl-js/blob/f8ab4b48d59ab8fe7b068b102538793bbdd4c848/src/geo/projection/globe_transform.ts#L575-L577 const scale = Math.pow(2, zoom - zoomAdjust(latitude)); const nearZ = opts.nearZ ?? nearZMultiplier; - const farZ = opts.farZ ?? (altitude + (GLOBE_RADIUS * 2 * scale) / height) * farZMultiplier; + // Far plane = distance from camera to horizon (beyond that, earth's + // curvature occludes everything). Using the full globe-diameter formula + // would push far past anything drawable and wreck depth-buffer precision + // at close zoom, causing z-fighting on 3D features like Tile3DLayer + // meshes. Multiply by 2 to keep headroom for tall features (mountains, + // buildings) peeking above the horizon. + const globeRadiusView = (GLOBE_RADIUS * scale) / height; + const horizonDistance = Math.sqrt(altitude * altitude + 2 * altitude * globeRadiusView); + const farZ = opts.farZ ?? horizonDistance * 2 * farZMultiplier; // Calculate view matrix const viewMatrix = new Matrix4().lookAt({eye: [0, -altitude, 0], up: [0, 0, 1]}); From 42a25d270ac1f4de8fa42a3622e47e32abf8e655 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Mon, 20 Apr 2026 17:32:55 -0400 Subject: [PATCH 3/6] refactor: enhance viewport uniform calculations and coordinate system handling --- .../src/shaderlib/project/project.glsl.ts | 618 +++++++-------- .../src/shaderlib/project/project.wgsl.ts | 635 +++++++-------- .../shaderlib/project/viewport-uniforms.ts | 746 +++++++++--------- 3 files changed, 1009 insertions(+), 990 deletions(-) diff --git a/modules/core/src/shaderlib/project/project.glsl.ts b/modules/core/src/shaderlib/project/project.glsl.ts index ddf8de3fe0e..a63bb375b33 100644 --- a/modules/core/src/shaderlib/project/project.glsl.ts +++ b/modules/core/src/shaderlib/project/project.glsl.ts @@ -1,315 +1,303 @@ -// deck.gl -// SPDX-License-Identifier: MIT -// Copyright (c) vis.gl contributors - -import {PROJECTION_MODE, UNIT} from '../../lib/constants'; -import {getShaderCoordinateSystem} from './viewport-uniforms'; - -const SHADER_COORDINATE_SYSTEMS = [ - 'default', - 'lnglat', - 'meter-offsets', - 'lnglat-offsets', - 'cartesian' -] as const; - -const COORDINATE_SYSTEM_GLSL_CONSTANTS = SHADER_COORDINATE_SYSTEMS.map( - coordinateSystem => - `const int COORDINATE_SYSTEM_${coordinateSystem.toUpperCase().replaceAll('-', '_')} = ${getShaderCoordinateSystem(coordinateSystem)};` -).join(''); -const PROJECTION_MODE_GLSL_CONSTANTS = Object.keys(PROJECTION_MODE) - .map(key => `const int PROJECTION_MODE_${key} = ${PROJECTION_MODE[key]};`) - .join(''); -const UNIT_GLSL_CONSTANTS = Object.keys(UNIT) - .map(key => `const int UNIT_${key.toUpperCase()} = ${UNIT[key]};`) - .join(''); - -export const projectGLSL = /* glsl */ `\ -${COORDINATE_SYSTEM_GLSL_CONSTANTS} -${PROJECTION_MODE_GLSL_CONSTANTS} -${UNIT_GLSL_CONSTANTS} - -layout(std140) uniform projectUniforms { - bool wrapLongitude; - int coordinateSystem; - vec3 commonUnitsPerMeter; - int projectionMode; - float scale; - vec3 commonUnitsPerWorldUnit; - vec3 commonUnitsPerWorldUnit2; - vec4 center; - mat4 modelMatrix; - mat4 viewProjectionMatrix; - vec2 viewportSize; - float devicePixelRatio; - float focalDistance; - vec3 cameraPosition; - vec3 coordinateOrigin; - vec3 commonOrigin; - bool pseudoMeters; -} project; - - -const float TILE_SIZE = 512.0; -const float PI = 3.1415926536; -const float WORLD_SCALE = TILE_SIZE / (PI * 2.0); -const vec3 ZERO_64_LOW = vec3(0.0); -const float EARTH_RADIUS = 6370972.0; // meters -const float GLOBE_RADIUS = 256.0; - -// returns an adjustment factor for uCommonUnitsPerMeter -float project_size_at_latitude(float lat) { - float y = clamp(lat, -89.9, 89.9); - return 1.0 / cos(radians(y)); -} - -float project_size() { - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR && - project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT && - project.pseudoMeters == false) { - - // uCommonUnitsPerMeter in low-zoom Web Mercator is non-linear - // Adjust by 1 / cos(latitude) - // If geometry.position (vertex in common space) is populated, use it - // Otherwise use geometry.worldPosition (anchor in world space) - - if (geometry.position.w == 0.0) { - return project_size_at_latitude(geometry.worldPosition.y); - } - - // latitude from common y: 2.0 * (atan(exp(y / TILE_SIZE * 2.0 * PI - PI)) - PI / 4.0) - // Taylor series of 1 / cos(latitude) - // Max error < 0.003 - - float y = geometry.position.y / TILE_SIZE * 2.0 - 1.0; - float y2 = y * y; - float y4 = y2 * y2; - float y6 = y4 * y2; - return 1.0 + 4.9348 * y2 + 4.0587 * y4 + 1.5642 * y6; - } - return 1.0; -} - -float project_size_at_latitude(float meters, float lat) { - return meters * project.commonUnitsPerMeter.z * project_size_at_latitude(lat); -} - -// -// Scaling offsets - scales meters to "world distance" -// Note the scalar version of project_size is for scaling the z component only -// -float project_size(float meters) { - // For scatter relevant - return meters * project.commonUnitsPerMeter.z * project_size(); -} - -vec2 project_size(vec2 meters) { - return meters * project.commonUnitsPerMeter.xy * project_size(); -} - -vec3 project_size(vec3 meters) { - return meters * project.commonUnitsPerMeter * project_size(); -} - -vec4 project_size(vec4 meters) { - return vec4(meters.xyz * project.commonUnitsPerMeter, meters.w); -} - -// Get rotation matrix that aligns the z axis with the given up vector -// Find 3 unit vectors ux, uy, uz that are perpendicular to each other and uz == up -mat3 project_get_orientation_matrix(vec3 up) { - vec3 uz = normalize(up); - // Tangent on XY plane - vec3 ux = abs(uz.z) == 1.0 ? vec3(1.0, 0.0, 0.0) : normalize(vec3(uz.y, -uz.x, 0)); - vec3 uy = cross(uz, ux); - return mat3(ux, uy, uz); -} - -bool project_needs_rotation(vec3 commonPosition, out mat3 transform) { - if (project.projectionMode == PROJECTION_MODE_GLOBE) { - transform = project_get_orientation_matrix(commonPosition); - return true; - } - return false; -} - -// -// Projecting normal - transform deltas from current coordinate system to -// normals in the worldspace -// -vec3 project_normal(vec3 vector) { - // Apply model matrix - vec4 normal_modelspace = project.modelMatrix * vec4(vector, 0.0); - vec3 n = normalize(normal_modelspace.xyz * project.commonUnitsPerMeter); - mat3 rotation; - if (project_needs_rotation(geometry.position.xyz, rotation)) { - n = rotation * n; - } - return n; -} - -vec4 project_offset_(vec4 offset) { - float dy = offset.y; - vec3 commonUnitsPerWorldUnit = project.commonUnitsPerWorldUnit + project.commonUnitsPerWorldUnit2 * dy; - return vec4(offset.xyz * commonUnitsPerWorldUnit, offset.w); -} - -// -// Projecting positions - non-linear projection: lnglats => unit tile [0-1, 0-1] -// -vec2 project_mercator_(vec2 lnglat) { - float x = lnglat.x; - if (project.wrapLongitude) { - x = mod(x + 180., 360.0) - 180.; - } - float y = clamp(lnglat.y, -89.9, 89.9); - return vec2( - radians(x) + PI, - PI + log(tan_fp32(PI * 0.25 + radians(y) * 0.5)) - ) * WORLD_SCALE; -} - -vec3 project_globe_(vec3 lnglatz) { - float lambda = radians(lnglatz.x); - float phi = radians(lnglatz.y); - float cosPhi = cos(phi); - float D = (lnglatz.z / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; - - return vec3( - sin(lambda) * cosPhi, - -cos(lambda) * cosPhi, - sin(phi) - ) * D; -} - -// -// Projects positions (defined by project.coordinateSystem) to common space (defined by project.projectionMode) -// -vec4 project_position(vec4 position, vec3 position64Low) { - vec4 position_world = project.modelMatrix * position; - - // Work around for a Mac+NVIDIA bug https://github.com/visgl/deck.gl/issues/4145 - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - return vec4( - project_mercator_(position_world.xy), - project_size_at_latitude(position_world.z, position_world.y), - position_world.w - ); - } - if (project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN) { - position_world.xyz += project.coordinateOrigin; - } - } - if (project.projectionMode == PROJECTION_MODE_GLOBE) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - return vec4( - project_globe_(position_world.xyz), - position_world.w - ); - } - if (project.coordinateSystem == COORDINATE_SYSTEM_METER_OFFSETS) { - // position_world is meters in the ENU tangent frame at coordinateOrigin. - // Attach the tangent plane to the sphere so each tile lands at its - // geographic location oriented with the surface, rather than floating - // as a flat patch in globe common space. - float lambda = radians(project.coordinateOrigin.x); - float phi = radians(project.coordinateOrigin.y); - float cosPhi = cos(phi); - float sinPhi = sin(phi); - float cosLambda = cos(lambda); - float sinLambda = sin(lambda); - vec3 east = vec3(cosLambda, sinLambda, 0.0); - vec3 north = vec3(-sinLambda * sinPhi, cosLambda * sinPhi, cosPhi); - vec3 up = vec3(sinLambda * cosPhi, -cosLambda * cosPhi, sinPhi); - float D = (project.coordinateOrigin.z / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; - float metersToCommon = GLOBE_RADIUS / EARTH_RADIUS; - vec3 offsetCommon = ( - east * position_world.x + - north * position_world.y + - up * position_world.z - ) * metersToCommon; - return vec4(up * D + offsetCommon, position_world.w); - } - } - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - if (abs(position_world.y - project.coordinateOrigin.y) > 0.25) { - // Too far from the projection center for offset mode to be accurate - // Only use high parts - return vec4( - project_mercator_(position_world.xy) - project.commonOrigin.xy, - project_size(position_world.z), - position_world.w - ); - } - } - } - if (project.projectionMode == PROJECTION_MODE_IDENTITY || - (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET && - (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT || - project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN))) { - // Subtract high part of 64 bit value. Convert remainder to float32, preserving precision. - position_world.xyz -= project.coordinateOrigin; - } - - // Translation is already added to the high parts - return project_offset_(position_world) + project_offset_(project.modelMatrix * vec4(position64Low, 0.0)); -} - -vec4 project_position(vec4 position) { - return project_position(position, ZERO_64_LOW); -} - -vec3 project_position(vec3 position, vec3 position64Low) { - vec4 projected_position = project_position(vec4(position, 1.0), position64Low); - return projected_position.xyz; -} - -vec3 project_position(vec3 position) { - vec4 projected_position = project_position(vec4(position, 1.0), ZERO_64_LOW); - return projected_position.xyz; -} - -vec2 project_position(vec2 position) { - vec4 projected_position = project_position(vec4(position, 0.0, 1.0), ZERO_64_LOW); - return projected_position.xy; -} - -vec4 project_common_position_to_clipspace(vec4 position, mat4 viewProjectionMatrix, vec4 center) { - return viewProjectionMatrix * position + center; -} - -// -// Projects from common space coordinates to clip space. -// Uses project.viewProjectionMatrix -// -vec4 project_common_position_to_clipspace(vec4 position) { - return project_common_position_to_clipspace(position, project.viewProjectionMatrix, project.center); -} - -// Returns a clip space offset that corresponds to a given number of screen pixels -vec2 project_pixel_size_to_clipspace(vec2 pixels) { - vec2 offset = pixels / project.viewportSize * project.devicePixelRatio * 2.0; - return offset * project.focalDistance; -} - -float project_size_to_pixel(float meters) { - return project_size(meters) * project.scale; -} -vec2 project_size_to_pixel(vec2 meters) { - return project_size(meters) * project.scale; -} -float project_size_to_pixel(float size, int unit) { - if (unit == UNIT_METERS) return project_size_to_pixel(size); - if (unit == UNIT_COMMON) return size * project.scale; - // UNIT_PIXELS - return size; -} -float project_pixel_size(float pixels) { - return pixels / project.scale; -} -vec2 project_pixel_size(vec2 pixels) { - return pixels / project.scale; -} -`; +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {PROJECTION_MODE, UNIT} from '../../lib/constants'; +import {getShaderCoordinateSystem} from './viewport-uniforms'; + +const SHADER_COORDINATE_SYSTEMS = [ + 'default', + 'lnglat', + 'meter-offsets', + 'lnglat-offsets', + 'cartesian' +] as const; + +const COORDINATE_SYSTEM_GLSL_CONSTANTS = SHADER_COORDINATE_SYSTEMS.map( + coordinateSystem => + `const int COORDINATE_SYSTEM_${coordinateSystem.toUpperCase().replaceAll('-', '_')} = ${getShaderCoordinateSystem(coordinateSystem)};` +).join(''); +const PROJECTION_MODE_GLSL_CONSTANTS = Object.keys(PROJECTION_MODE) + .map(key => `const int PROJECTION_MODE_${key} = ${PROJECTION_MODE[key]};`) + .join(''); +const UNIT_GLSL_CONSTANTS = Object.keys(UNIT) + .map(key => `const int UNIT_${key.toUpperCase()} = ${UNIT[key]};`) + .join(''); + +export const projectGLSL = /* glsl */ `\ +${COORDINATE_SYSTEM_GLSL_CONSTANTS} +${PROJECTION_MODE_GLSL_CONSTANTS} +${UNIT_GLSL_CONSTANTS} + +layout(std140) uniform projectUniforms { + bool wrapLongitude; + int coordinateSystem; + vec3 commonUnitsPerMeter; + int projectionMode; + float scale; + vec3 commonUnitsPerWorldUnit; + vec3 commonUnitsPerWorldUnit2; + vec4 center; + mat4 modelMatrix; + mat4 viewProjectionMatrix; + vec2 viewportSize; + float devicePixelRatio; + float focalDistance; + vec3 cameraPosition; + vec3 coordinateOrigin; + vec3 commonOrigin; + bool pseudoMeters; +} project; + + +const float TILE_SIZE = 512.0; +const float PI = 3.1415926536; +const float WORLD_SCALE = TILE_SIZE / (PI * 2.0); +const vec3 ZERO_64_LOW = vec3(0.0); +const float EARTH_RADIUS = 6370972.0; // meters +const float GLOBE_RADIUS = 256.0; + +// returns an adjustment factor for uCommonUnitsPerMeter +float project_size_at_latitude(float lat) { + float y = clamp(lat, -89.9, 89.9); + return 1.0 / cos(radians(y)); +} + +float project_size() { + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR && + project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT && + project.pseudoMeters == false) { + + // uCommonUnitsPerMeter in low-zoom Web Mercator is non-linear + // Adjust by 1 / cos(latitude) + // If geometry.position (vertex in common space) is populated, use it + // Otherwise use geometry.worldPosition (anchor in world space) + + if (geometry.position.w == 0.0) { + return project_size_at_latitude(geometry.worldPosition.y); + } + + // latitude from common y: 2.0 * (atan(exp(y / TILE_SIZE * 2.0 * PI - PI)) - PI / 4.0) + // Taylor series of 1 / cos(latitude) + // Max error < 0.003 + + float y = geometry.position.y / TILE_SIZE * 2.0 - 1.0; + float y2 = y * y; + float y4 = y2 * y2; + float y6 = y4 * y2; + return 1.0 + 4.9348 * y2 + 4.0587 * y4 + 1.5642 * y6; + } + return 1.0; +} + +float project_size_at_latitude(float meters, float lat) { + return meters * project.commonUnitsPerMeter.z * project_size_at_latitude(lat); +} + +// +// Scaling offsets - scales meters to "world distance" +// Note the scalar version of project_size is for scaling the z component only +// +float project_size(float meters) { + // For scatter relevant + return meters * project.commonUnitsPerMeter.z * project_size(); +} + +vec2 project_size(vec2 meters) { + return meters * project.commonUnitsPerMeter.xy * project_size(); +} + +vec3 project_size(vec3 meters) { + return meters * project.commonUnitsPerMeter * project_size(); +} + +vec4 project_size(vec4 meters) { + return vec4(meters.xyz * project.commonUnitsPerMeter, meters.w); +} + +// Get rotation matrix that aligns the z axis with the given up vector +// Find 3 unit vectors ux, uy, uz that are perpendicular to each other and uz == up +mat3 project_get_orientation_matrix(vec3 up) { + vec3 uz = normalize(up); + // Tangent on XY plane + vec3 ux = abs(uz.z) == 1.0 ? vec3(1.0, 0.0, 0.0) : normalize(vec3(uz.y, -uz.x, 0)); + vec3 uy = cross(uz, ux); + return mat3(ux, uy, uz); +} + +bool project_needs_rotation(vec3 commonPosition, out mat3 transform) { + if (project.projectionMode == PROJECTION_MODE_GLOBE) { + transform = project_get_orientation_matrix(commonPosition); + return true; + } + return false; +} + +// +// Projecting normal - transform deltas from current coordinate system to +// normals in the worldspace +// +vec3 project_normal(vec3 vector) { + // Apply model matrix + vec4 normal_modelspace = project.modelMatrix * vec4(vector, 0.0); + vec3 n = normalize(normal_modelspace.xyz * project.commonUnitsPerMeter); + mat3 rotation; + if (project_needs_rotation(geometry.position.xyz, rotation)) { + n = rotation * n; + } + return n; +} + +vec4 project_offset_(vec4 offset) { + float dy = offset.y; + vec3 commonUnitsPerWorldUnit = project.commonUnitsPerWorldUnit + project.commonUnitsPerWorldUnit2 * dy; + return vec4(offset.xyz * commonUnitsPerWorldUnit, offset.w); +} + +// +// Projecting positions - non-linear projection: lnglats => unit tile [0-1, 0-1] +// +vec2 project_mercator_(vec2 lnglat) { + float x = lnglat.x; + if (project.wrapLongitude) { + x = mod(x + 180., 360.0) - 180.; + } + float y = clamp(lnglat.y, -89.9, 89.9); + return vec2( + radians(x) + PI, + PI + log(tan_fp32(PI * 0.25 + radians(y) * 0.5)) + ) * WORLD_SCALE; +} + +vec3 project_globe_(vec3 lnglatz) { + float lambda = radians(lnglatz.x); + float phi = radians(lnglatz.y); + float cosPhi = cos(phi); + float D = (lnglatz.z / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; + + return vec3( + sin(lambda) * cosPhi, + -cos(lambda) * cosPhi, + sin(phi) + ) * D; +} + +// +// Projects positions (defined by project.coordinateSystem) to common space (defined by project.projectionMode) +// +vec4 project_position(vec4 position, vec3 position64Low) { + vec4 position_world = project.modelMatrix * position; + + // Work around for a Mac+NVIDIA bug https://github.com/visgl/deck.gl/issues/4145 + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + return vec4( + project_mercator_(position_world.xy), + project_size_at_latitude(position_world.z, position_world.y), + position_world.w + ); + } + if (project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN) { + position_world.xyz += project.coordinateOrigin; + } + } + if (project.projectionMode == PROJECTION_MODE_GLOBE) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + return vec4( + project_globe_(position_world.xyz), + position_world.w + ); + } + if (project.coordinateSystem == COORDINATE_SYSTEM_METER_OFFSETS) { + // position_world is meters in the ENU tangent frame at coordinateOrigin. + // commonOrigin (precomputed CPU-side) is the globe-space position of + // that origin; project_get_orientation_matrix derives the ENU frame from + // that direction so the tangent plane attaches to the sphere and tiles + // land at their geographic location oriented with the surface. + mat3 enuMatrix = project_get_orientation_matrix(project.commonOrigin); + float metersToCommon = GLOBE_RADIUS / EARTH_RADIUS; + vec3 offsetCommon = (enuMatrix * vec3(-position_world.xy, position_world.z)) * metersToCommon; + return vec4(project.commonOrigin + offsetCommon, position_world.w); + } + } + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + if (abs(position_world.y - project.coordinateOrigin.y) > 0.25) { + // Too far from the projection center for offset mode to be accurate + // Only use high parts + return vec4( + project_mercator_(position_world.xy) - project.commonOrigin.xy, + project_size(position_world.z), + position_world.w + ); + } + } + } + if (project.projectionMode == PROJECTION_MODE_IDENTITY || + (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET && + (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT || + project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN))) { + // Subtract high part of 64 bit value. Convert remainder to float32, preserving precision. + position_world.xyz -= project.coordinateOrigin; + } + + // Translation is already added to the high parts + return project_offset_(position_world) + project_offset_(project.modelMatrix * vec4(position64Low, 0.0)); +} + +vec4 project_position(vec4 position) { + return project_position(position, ZERO_64_LOW); +} + +vec3 project_position(vec3 position, vec3 position64Low) { + vec4 projected_position = project_position(vec4(position, 1.0), position64Low); + return projected_position.xyz; +} + +vec3 project_position(vec3 position) { + vec4 projected_position = project_position(vec4(position, 1.0), ZERO_64_LOW); + return projected_position.xyz; +} + +vec2 project_position(vec2 position) { + vec4 projected_position = project_position(vec4(position, 0.0, 1.0), ZERO_64_LOW); + return projected_position.xy; +} + +vec4 project_common_position_to_clipspace(vec4 position, mat4 viewProjectionMatrix, vec4 center) { + return viewProjectionMatrix * position + center; +} + +// +// Projects from common space coordinates to clip space. +// Uses project.viewProjectionMatrix +// +vec4 project_common_position_to_clipspace(vec4 position) { + return project_common_position_to_clipspace(position, project.viewProjectionMatrix, project.center); +} + +// Returns a clip space offset that corresponds to a given number of screen pixels +vec2 project_pixel_size_to_clipspace(vec2 pixels) { + vec2 offset = pixels / project.viewportSize * project.devicePixelRatio * 2.0; + return offset * project.focalDistance; +} + +float project_size_to_pixel(float meters) { + return project_size(meters) * project.scale; +} +vec2 project_size_to_pixel(vec2 meters) { + return project_size(meters) * project.scale; +} +float project_size_to_pixel(float size, int unit) { + if (unit == UNIT_METERS) return project_size_to_pixel(size); + if (unit == UNIT_COMMON) return size * project.scale; + // UNIT_PIXELS + return size; +} +float project_pixel_size(float pixels) { + return pixels / project.scale; +} +vec2 project_pixel_size(vec2 pixels) { + return pixels / project.scale; +} +`; diff --git a/modules/core/src/shaderlib/project/project.wgsl.ts b/modules/core/src/shaderlib/project/project.wgsl.ts index c14a7c6da45..e41c6ad43b0 100644 --- a/modules/core/src/shaderlib/project/project.wgsl.ts +++ b/modules/core/src/shaderlib/project/project.wgsl.ts @@ -1,312 +1,323 @@ -// deck.gl -// SPDX-License-Identifier: MIT -// Copyright (c) vis.gl contributors - -import {PROJECTION_MODE, UNIT} from '../../lib/constants'; -import {getShaderCoordinateSystem} from './viewport-uniforms'; - -const SHADER_COORDINATE_SYSTEMS = [ - 'default', - 'lnglat', - 'meter-offsets', - 'lnglat-offsets', - 'cartesian' -] as const; - -const COORDINATE_SYSTEM_WGSL_CONSTANTS = SHADER_COORDINATE_SYSTEMS.map( - coordinateSystem => - `const COORDINATE_SYSTEM_${coordinateSystem.toUpperCase().replaceAll('-', '_')}: i32 = ${getShaderCoordinateSystem(coordinateSystem)};` -).join(''); -const PROJECTION_MODE_WGSL_CONSTANTS = Object.keys(PROJECTION_MODE) - .map(key => `const PROJECTION_MODE_${key}: i32 = ${PROJECTION_MODE[key]};`) - .join(''); -const UNIT_WGSL_CONSTANTS = Object.keys(UNIT) - .map(key => `const UNIT_${key.toUpperCase()}: i32 = ${UNIT[key]};`) - .join(''); - -export const projectWGSLHeader = /* wgsl */ `\ -${COORDINATE_SYSTEM_WGSL_CONSTANTS} -${PROJECTION_MODE_WGSL_CONSTANTS} -${UNIT_WGSL_CONSTANTS} - -const TILE_SIZE: f32 = 512.0; -const PI: f32 = 3.1415926536; -const WORLD_SCALE: f32 = TILE_SIZE / (PI * 2.0); -const ZERO_64_LOW: vec3 = vec3(0.0, 0.0, 0.0); -const EARTH_RADIUS: f32 = 6370972.0; // meters -const GLOBE_RADIUS: f32 = 256.0; - -// ----------------------------------------------------------------------------- -// Uniform block (converted from GLSL uniform block) -// ----------------------------------------------------------------------------- -struct ProjectUniforms { - wrapLongitude: i32, - coordinateSystem: i32, - commonUnitsPerMeter: vec3, - projectionMode: i32, - scale: f32, - commonUnitsPerWorldUnit: vec3, - commonUnitsPerWorldUnit2: vec3, - center: vec4, - modelMatrix: mat4x4, - viewProjectionMatrix: mat4x4, - viewportSize: vec2, - devicePixelRatio: f32, - focalDistance: f32, - cameraPosition: vec3, - coordinateOrigin: vec3, - commonOrigin: vec3, - pseudoMeters: i32, -}; - -@group(0) @binding(auto) -var project: ProjectUniforms; - -// ----------------------------------------------------------------------------- -// Geometry data shared across the project helpers. -// The active layer shader is responsible for populating this private module -// state before calling the project functions below. -// ----------------------------------------------------------------------------- - -// Structure to carry additional geometry data used by deck.gl filters. -struct Geometry { - worldPosition: vec3, - worldPositionAlt: vec3, - position: vec4, - normal: vec3, - uv: vec2, - pickingColor: vec3, -}; - -var geometry: Geometry; -`; - -export const projectWGSL = /* wgsl */ `\ -${projectWGSLHeader} - -// ----------------------------------------------------------------------------- -// Functions -// ----------------------------------------------------------------------------- - -// Returns an adjustment factor for commonUnitsPerMeter -fn _project_size_at_latitude(lat: f32) -> f32 { - let y = clamp(lat, -89.9, 89.9); - return 1.0 / cos(radians(y)); -} - -// Overloaded version: scales a value in meters at a given latitude. -fn _project_size_at_latitude_m(meters: f32, lat: f32) -> f32 { - return meters * project.commonUnitsPerMeter.z * _project_size_at_latitude(lat); -} - -// Computes a non-linear scale factor based on geometry. -// (Note: This function relies on "geometry" being provided.) -fn project_size() -> f32 { - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR && - project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT && - project.pseudoMeters == 0) { - if (geometry.position.w == 0.0) { - return _project_size_at_latitude(geometry.worldPosition.y); - } - let y: f32 = geometry.position.y / TILE_SIZE * 2.0 - 1.0; - let y2 = y * y; - let y4 = y2 * y2; - let y6 = y4 * y2; - return 1.0 + 4.9348 * y2 + 4.0587 * y4 + 1.5642 * y6; - } - return 1.0; -} - -// Overloads to scale offsets (meters to world units) -fn project_size_float(meters: f32) -> f32 { - return meters * project.commonUnitsPerMeter.z * project_size(); -} - -fn project_size_vec2(meters: vec2) -> vec2 { - return meters * project.commonUnitsPerMeter.xy * project_size(); -} - -fn project_size_vec3(meters: vec3) -> vec3 { - return meters * project.commonUnitsPerMeter * project_size(); -} - -fn project_size_vec4(meters: vec4) -> vec4 { - return vec4(meters.xyz * project.commonUnitsPerMeter, meters.w); -} - -// Returns a rotation matrix aligning the z‑axis with the given up vector. -fn project_get_orientation_matrix(up: vec3) -> mat3x3 { - let uz = normalize(up); - let ux = select( - vec3(1.0, 0.0, 0.0), - normalize(vec3(uz.y, -uz.x, 0.0)), - abs(uz.z) == 1.0 - ); - let uy = cross(uz, ux); - return mat3x3(ux, uy, uz); -} - -// Since WGSL does not support "out" parameters, we return a struct. -struct RotationResult { - needsRotation: bool, - transform: mat3x3, -}; - -fn project_needs_rotation(commonPosition: vec3) -> RotationResult { - if (project.projectionMode == PROJECTION_MODE_GLOBE) { - return RotationResult(true, project_get_orientation_matrix(commonPosition)); - } else { - return RotationResult(false, mat3x3()); // identity alternative if needed - }; -} - -// Projects a normal vector from the current coordinate system to world space. -fn project_normal(vector: vec3) -> vec3 { - let normal_modelspace = project.modelMatrix * vec4(vector, 0.0); - var n = normalize(normal_modelspace.xyz * project.commonUnitsPerMeter); - let rotResult = project_needs_rotation(geometry.position.xyz); - if (rotResult.needsRotation) { - n = rotResult.transform * n; - } - return n; -} - -// Applies a scale offset based on y-offset (dy) -fn project_offset_(offset: vec4) -> vec4 { - let dy: f32 = offset.y; - let commonUnitsPerWorldUnit = project.commonUnitsPerWorldUnit + project.commonUnitsPerWorldUnit2 * dy; - return vec4(offset.xyz * commonUnitsPerWorldUnit, offset.w); -} - -// Projects lng/lat coordinates to a unit tile [0,1] -fn project_mercator_(lnglat: vec2) -> vec2 { - var x = lnglat.x; - if (project.wrapLongitude != 0) { - x = ((x + 180.0) % 360.0) - 180.0; - } - let y = clamp(lnglat.y, -89.9, 89.9); - return vec2( - radians(x) + PI, - PI + log(tan(PI * 0.25 + radians(y) * 0.5)) - ) * WORLD_SCALE; -} - -// Projects lng/lat/z coordinates for a globe projection. -fn project_globe_(lnglatz: vec3) -> vec3 { - let lambda = radians(lnglatz.x); - let phi = radians(lnglatz.y); - let cosPhi = cos(phi); - let D = (lnglatz.z / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; - return vec3( - sin(lambda) * cosPhi, - -cos(lambda) * cosPhi, - sin(phi) - ) * D; -} - -// Projects positions (with an optional 64-bit low part) from the input -// coordinate system to the common space. -fn project_position_vec4_f64(position: vec4, position64Low: vec3) -> vec4 { - var position_world = project.modelMatrix * position; - - // Work around for a Mac+NVIDIA bug: - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - return vec4( - project_mercator_(position_world.xy), - _project_size_at_latitude_m(position_world.z, position_world.y), - position_world.w - ); - } - if (project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN) { - position_world = vec4f(position_world.xyz + project.coordinateOrigin, position_world.w); - } - } - if (project.projectionMode == PROJECTION_MODE_GLOBE) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - return vec4( - project_globe_(position_world.xyz), - position_world.w - ); - } - } - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - if (abs(position_world.y - project.coordinateOrigin.y) > 0.25) { - return vec4( - project_mercator_(position_world.xy) - project.commonOrigin.xy, - project_size_float(position_world.z), - position_world.w - ); - } - } - } - if (project.projectionMode == PROJECTION_MODE_IDENTITY || - (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET && - (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT || - project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN))) { - position_world = vec4f(position_world.xyz - project.coordinateOrigin, position_world.w); - } - - return project_offset_(position_world) + - project_offset_(project.modelMatrix * vec4(position64Low, 0.0)); -} - -// Overloaded versions for different input types. -fn project_position_vec4_f32(position: vec4) -> vec4 { - return project_position_vec4_f64(position, ZERO_64_LOW); -} - -fn project_position_vec3_f64(position: vec3, position64Low: vec3) -> vec3 { - let projected_position = project_position_vec4_f64(vec4(position, 1.0), position64Low); - return projected_position.xyz; -} - -fn project_position_vec3_f32(position: vec3) -> vec3 { - let projected_position = project_position_vec4_f64(vec4(position, 1.0), ZERO_64_LOW); - return projected_position.xyz; -} - -fn project_position_vec2_f32(position: vec2) -> vec2 { - let projected_position = project_position_vec4_f64(vec4(position, 0.0, 1.0), ZERO_64_LOW); - return projected_position.xy; -} - -// Transforms a common space position to clip space. -fn project_common_position_to_clipspace_with_projection(position: vec4, viewProjectionMatrix: mat4x4, center: vec4) -> vec4 { - return viewProjectionMatrix * position + center; -} - -// Uses the project viewProjectionMatrix and center. -fn project_common_position_to_clipspace(position: vec4) -> vec4 { - return project_common_position_to_clipspace_with_projection(position, project.viewProjectionMatrix, project.center); -} - -// Returns a clip space offset corresponding to a given number of screen pixels. -fn project_pixel_size_to_clipspace(pixels: vec2) -> vec2 { - let offset = pixels / project.viewportSize * project.devicePixelRatio * 2.0; - return offset * project.focalDistance; -} - -fn project_meter_size_to_pixel(meters: f32) -> f32 { - return project_size_float(meters) * project.scale; -} - -fn project_unit_size_to_pixel(size: f32, unit: i32) -> f32 { - if (unit == UNIT_METERS) { - return project_meter_size_to_pixel(size); - } else if (unit == UNIT_COMMON) { - return size * project.scale; - } - // UNIT_PIXELS: no scaling applied. - return size; -} - -fn project_pixel_size_float(pixels: f32) -> f32 { - return pixels / project.scale; -} - -fn project_pixel_size_vec2(pixels: vec2) -> vec2 { - return pixels / project.scale; -} -`; +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {PROJECTION_MODE, UNIT} from '../../lib/constants'; +import {getShaderCoordinateSystem} from './viewport-uniforms'; + +const SHADER_COORDINATE_SYSTEMS = [ + 'default', + 'lnglat', + 'meter-offsets', + 'lnglat-offsets', + 'cartesian' +] as const; + +const COORDINATE_SYSTEM_WGSL_CONSTANTS = SHADER_COORDINATE_SYSTEMS.map( + coordinateSystem => + `const COORDINATE_SYSTEM_${coordinateSystem.toUpperCase().replaceAll('-', '_')}: i32 = ${getShaderCoordinateSystem(coordinateSystem)};` +).join(''); +const PROJECTION_MODE_WGSL_CONSTANTS = Object.keys(PROJECTION_MODE) + .map(key => `const PROJECTION_MODE_${key}: i32 = ${PROJECTION_MODE[key]};`) + .join(''); +const UNIT_WGSL_CONSTANTS = Object.keys(UNIT) + .map(key => `const UNIT_${key.toUpperCase()}: i32 = ${UNIT[key]};`) + .join(''); + +export const projectWGSLHeader = /* wgsl */ `\ +${COORDINATE_SYSTEM_WGSL_CONSTANTS} +${PROJECTION_MODE_WGSL_CONSTANTS} +${UNIT_WGSL_CONSTANTS} + +const TILE_SIZE: f32 = 512.0; +const PI: f32 = 3.1415926536; +const WORLD_SCALE: f32 = TILE_SIZE / (PI * 2.0); +const ZERO_64_LOW: vec3 = vec3(0.0, 0.0, 0.0); +const EARTH_RADIUS: f32 = 6370972.0; // meters +const GLOBE_RADIUS: f32 = 256.0; + +// ----------------------------------------------------------------------------- +// Uniform block (converted from GLSL uniform block) +// ----------------------------------------------------------------------------- +struct ProjectUniforms { + wrapLongitude: i32, + coordinateSystem: i32, + commonUnitsPerMeter: vec3, + projectionMode: i32, + scale: f32, + commonUnitsPerWorldUnit: vec3, + commonUnitsPerWorldUnit2: vec3, + center: vec4, + modelMatrix: mat4x4, + viewProjectionMatrix: mat4x4, + viewportSize: vec2, + devicePixelRatio: f32, + focalDistance: f32, + cameraPosition: vec3, + coordinateOrigin: vec3, + commonOrigin: vec3, + pseudoMeters: i32, +}; + +@group(0) @binding(auto) +var project: ProjectUniforms; + +// ----------------------------------------------------------------------------- +// Geometry data shared across the project helpers. +// The active layer shader is responsible for populating this private module +// state before calling the project functions below. +// ----------------------------------------------------------------------------- + +// Structure to carry additional geometry data used by deck.gl filters. +struct Geometry { + worldPosition: vec3, + worldPositionAlt: vec3, + position: vec4, + normal: vec3, + uv: vec2, + pickingColor: vec3, +}; + +var geometry: Geometry; +`; + +export const projectWGSL = /* wgsl */ `\ +${projectWGSLHeader} + +// ----------------------------------------------------------------------------- +// Functions +// ----------------------------------------------------------------------------- + +// Returns an adjustment factor for commonUnitsPerMeter +fn _project_size_at_latitude(lat: f32) -> f32 { + let y = clamp(lat, -89.9, 89.9); + return 1.0 / cos(radians(y)); +} + +// Overloaded version: scales a value in meters at a given latitude. +fn _project_size_at_latitude_m(meters: f32, lat: f32) -> f32 { + return meters * project.commonUnitsPerMeter.z * _project_size_at_latitude(lat); +} + +// Computes a non-linear scale factor based on geometry. +// (Note: This function relies on "geometry" being provided.) +fn project_size() -> f32 { + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR && + project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT && + project.pseudoMeters == 0) { + if (geometry.position.w == 0.0) { + return _project_size_at_latitude(geometry.worldPosition.y); + } + let y: f32 = geometry.position.y / TILE_SIZE * 2.0 - 1.0; + let y2 = y * y; + let y4 = y2 * y2; + let y6 = y4 * y2; + return 1.0 + 4.9348 * y2 + 4.0587 * y4 + 1.5642 * y6; + } + return 1.0; +} + +// Overloads to scale offsets (meters to world units) +fn project_size_float(meters: f32) -> f32 { + return meters * project.commonUnitsPerMeter.z * project_size(); +} + +fn project_size_vec2(meters: vec2) -> vec2 { + return meters * project.commonUnitsPerMeter.xy * project_size(); +} + +fn project_size_vec3(meters: vec3) -> vec3 { + return meters * project.commonUnitsPerMeter * project_size(); +} + +fn project_size_vec4(meters: vec4) -> vec4 { + return vec4(meters.xyz * project.commonUnitsPerMeter, meters.w); +} + +// Returns a rotation matrix aligning the z‑axis with the given up vector. +fn project_get_orientation_matrix(up: vec3) -> mat3x3 { + let uz = normalize(up); + let ux = select( + vec3(1.0, 0.0, 0.0), + normalize(vec3(uz.y, -uz.x, 0.0)), + abs(uz.z) == 1.0 + ); + let uy = cross(uz, ux); + return mat3x3(ux, uy, uz); +} + +// Since WGSL does not support "out" parameters, we return a struct. +struct RotationResult { + needsRotation: bool, + transform: mat3x3, +}; + +fn project_needs_rotation(commonPosition: vec3) -> RotationResult { + if (project.projectionMode == PROJECTION_MODE_GLOBE) { + return RotationResult(true, project_get_orientation_matrix(commonPosition)); + } else { + return RotationResult(false, mat3x3()); // identity alternative if needed + }; +} + +// Projects a normal vector from the current coordinate system to world space. +fn project_normal(vector: vec3) -> vec3 { + let normal_modelspace = project.modelMatrix * vec4(vector, 0.0); + var n = normalize(normal_modelspace.xyz * project.commonUnitsPerMeter); + let rotResult = project_needs_rotation(geometry.position.xyz); + if (rotResult.needsRotation) { + n = rotResult.transform * n; + } + return n; +} + +// Applies a scale offset based on y-offset (dy) +fn project_offset_(offset: vec4) -> vec4 { + let dy: f32 = offset.y; + let commonUnitsPerWorldUnit = project.commonUnitsPerWorldUnit + project.commonUnitsPerWorldUnit2 * dy; + return vec4(offset.xyz * commonUnitsPerWorldUnit, offset.w); +} + +// Projects lng/lat coordinates to a unit tile [0,1] +fn project_mercator_(lnglat: vec2) -> vec2 { + var x = lnglat.x; + if (project.wrapLongitude != 0) { + x = ((x + 180.0) % 360.0) - 180.0; + } + let y = clamp(lnglat.y, -89.9, 89.9); + return vec2( + radians(x) + PI, + PI + log(tan(PI * 0.25 + radians(y) * 0.5)) + ) * WORLD_SCALE; +} + +// Projects lng/lat/z coordinates for a globe projection. +fn project_globe_(lnglatz: vec3) -> vec3 { + let lambda = radians(lnglatz.x); + let phi = radians(lnglatz.y); + let cosPhi = cos(phi); + let D = (lnglatz.z / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; + return vec3( + sin(lambda) * cosPhi, + -cos(lambda) * cosPhi, + sin(phi) + ) * D; +} + +// Projects positions (with an optional 64-bit low part) from the input +// coordinate system to the common space. +fn project_position_vec4_f64(position: vec4, position64Low: vec3) -> vec4 { + var position_world = project.modelMatrix * position; + + // Work around for a Mac+NVIDIA bug: + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + return vec4( + project_mercator_(position_world.xy), + _project_size_at_latitude_m(position_world.z, position_world.y), + position_world.w + ); + } + if (project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN) { + position_world = vec4f(position_world.xyz + project.coordinateOrigin, position_world.w); + } + } + if (project.projectionMode == PROJECTION_MODE_GLOBE) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + return vec4( + project_globe_(position_world.xyz), + position_world.w + ); + } + if (project.coordinateSystem == COORDINATE_SYSTEM_METER_OFFSETS) { + // position_world is meters in the ENU tangent frame at coordinateOrigin. + // commonOrigin (precomputed CPU-side) is the globe-space position of + // that origin; project_get_orientation_matrix derives the ENU frame from + // that direction so the tangent plane attaches to the sphere and tiles + // land at their geographic location oriented with the surface. + let enuMatrix = project_get_orientation_matrix(project.commonOrigin); + let metersToCommon = GLOBE_RADIUS / EARTH_RADIUS; + let offsetCommon = (enuMatrix * vec3(-position_world.x, -position_world.y, position_world.z)) * metersToCommon; + return vec4(project.commonOrigin + offsetCommon, position_world.w); + } + } + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + if (abs(position_world.y - project.coordinateOrigin.y) > 0.25) { + return vec4( + project_mercator_(position_world.xy) - project.commonOrigin.xy, + project_size_float(position_world.z), + position_world.w + ); + } + } + } + if (project.projectionMode == PROJECTION_MODE_IDENTITY || + (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET && + (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT || + project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN))) { + position_world = vec4f(position_world.xyz - project.coordinateOrigin, position_world.w); + } + + return project_offset_(position_world) + + project_offset_(project.modelMatrix * vec4(position64Low, 0.0)); +} + +// Overloaded versions for different input types. +fn project_position_vec4_f32(position: vec4) -> vec4 { + return project_position_vec4_f64(position, ZERO_64_LOW); +} + +fn project_position_vec3_f64(position: vec3, position64Low: vec3) -> vec3 { + let projected_position = project_position_vec4_f64(vec4(position, 1.0), position64Low); + return projected_position.xyz; +} + +fn project_position_vec3_f32(position: vec3) -> vec3 { + let projected_position = project_position_vec4_f64(vec4(position, 1.0), ZERO_64_LOW); + return projected_position.xyz; +} + +fn project_position_vec2_f32(position: vec2) -> vec2 { + let projected_position = project_position_vec4_f64(vec4(position, 0.0, 1.0), ZERO_64_LOW); + return projected_position.xy; +} + +// Transforms a common space position to clip space. +fn project_common_position_to_clipspace_with_projection(position: vec4, viewProjectionMatrix: mat4x4, center: vec4) -> vec4 { + return viewProjectionMatrix * position + center; +} + +// Uses the project viewProjectionMatrix and center. +fn project_common_position_to_clipspace(position: vec4) -> vec4 { + return project_common_position_to_clipspace_with_projection(position, project.viewProjectionMatrix, project.center); +} + +// Returns a clip space offset corresponding to a given number of screen pixels. +fn project_pixel_size_to_clipspace(pixels: vec2) -> vec2 { + let offset = pixels / project.viewportSize * project.devicePixelRatio * 2.0; + return offset * project.focalDistance; +} + +fn project_meter_size_to_pixel(meters: f32) -> f32 { + return project_size_float(meters) * project.scale; +} + +fn project_unit_size_to_pixel(size: f32, unit: i32) -> f32 { + if (unit == UNIT_METERS) { + return project_meter_size_to_pixel(size); + } else if (unit == UNIT_COMMON) { + return size * project.scale; + } + // UNIT_PIXELS: no scaling applied. + return size; +} + +fn project_pixel_size_float(pixels: f32) -> f32 { + return pixels / project.scale; +} + +fn project_pixel_size_vec2(pixels: vec2) -> vec2 { + return pixels / project.scale; +} +`; diff --git a/modules/core/src/shaderlib/project/viewport-uniforms.ts b/modules/core/src/shaderlib/project/viewport-uniforms.ts index 07ca79251ea..55ba3a838b0 100644 --- a/modules/core/src/shaderlib/project/viewport-uniforms.ts +++ b/modules/core/src/shaderlib/project/viewport-uniforms.ts @@ -1,363 +1,383 @@ -// deck.gl -// SPDX-License-Identifier: MIT -// Copyright (c) vis.gl contributors - -/* eslint-disable complexity, camelcase */ - -import {mat4, Matrix4Like, vec4} from '@math.gl/core'; - -import {PROJECTION_MODE} from '../../lib/constants'; - -import memoize from '../../utils/memoize'; - -import type Viewport from '../../viewports/viewport'; -import type {CoordinateSystem} from '../../lib/constants'; - -type Vec3 = [number, number, number]; -type Vec4 = [number, number, number, number]; - -// To quickly set a vector to zero -const ZERO_VECTOR: Vec4 = [0, 0, 0, 0]; -// 4x4 matrix that drops 4th component of vector -const VECTOR_TO_POINT_MATRIX: Matrix4Like = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]; -const IDENTITY_MATRIX: Matrix4Like = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; -const DEFAULT_PIXELS_PER_UNIT2: Vec3 = [0, 0, 0]; -const DEFAULT_COORDINATE_ORIGIN: Vec3 = [0, 0, 0]; - -/** Coordinate system constants */ -const COORDINATE_SYSTEM_NUMBERS = { - default: -1, - cartesian: 0, - lnglat: 1, - 'meter-offsets': 2, - 'lnglat-offsets': 3 -} as const satisfies Record; - -export function getShaderCoordinateSystem(coordinateSystem: CoordinateSystem) { - const shaderCoordinateSystem = COORDINATE_SYSTEM_NUMBERS[coordinateSystem]; - if (shaderCoordinateSystem === undefined) { - throw new Error(`Invalid coordinateSystem: ${coordinateSystem}`); - } - return shaderCoordinateSystem; -} - -const getMemoizedViewportUniforms = memoize(calculateViewportUniforms); - -export function getOffsetOrigin( - viewport: Viewport, - coordinateSystem: CoordinateSystem, - coordinateOrigin: Vec3 = DEFAULT_COORDINATE_ORIGIN -): { - geospatialOrigin: Vec3 | null; - shaderCoordinateOrigin: Vec3; - offsetMode: boolean; -} { - if (coordinateOrigin.length < 3) { - coordinateOrigin = [coordinateOrigin[0], coordinateOrigin[1], 0]; - } - - let shaderCoordinateOrigin = coordinateOrigin; - let geospatialOrigin: Vec3 | null; - let offsetMode = true; - - if (coordinateSystem === 'lnglat-offsets' || coordinateSystem === 'meter-offsets') { - geospatialOrigin = coordinateOrigin; - } else { - geospatialOrigin = viewport.isGeospatial - ? // @ts-expect-error longitude and latitude are not defined on the base Viewport, but is expected on geospatial viewports - [Math.fround(viewport.longitude), Math.fround(viewport.latitude), 0] - : null; - } - - switch (viewport.projectionMode) { - case PROJECTION_MODE.WEB_MERCATOR: - if (coordinateSystem === 'lnglat' || coordinateSystem === 'cartesian') { - geospatialOrigin = [0, 0, 0]; - offsetMode = false; - } - break; - - case PROJECTION_MODE.WEB_MERCATOR_AUTO_OFFSET: - if (coordinateSystem === 'lnglat') { - // viewport center in world space - // @ts-expect-error when using LNGLAT coordinates, we expect the viewport to be geospatial, in which case geospatialOrigin is defined - shaderCoordinateOrigin = geospatialOrigin; - } else if (coordinateSystem === 'cartesian') { - // viewport center in common space - shaderCoordinateOrigin = [ - Math.fround(viewport.center[0]), - Math.fround(viewport.center[1]), - 0 - ]; - // Geospatial origin (wgs84) must match shaderCoordinateOrigin (common) - geospatialOrigin = viewport.unprojectPosition(shaderCoordinateOrigin); - shaderCoordinateOrigin[0] -= coordinateOrigin[0]; - shaderCoordinateOrigin[1] -= coordinateOrigin[1]; - shaderCoordinateOrigin[2] -= coordinateOrigin[2]; - } - break; - - case PROJECTION_MODE.IDENTITY: - shaderCoordinateOrigin = viewport.position.map(Math.fround) as Vec3; - shaderCoordinateOrigin[2] = shaderCoordinateOrigin[2] || 0; - break; - - case PROJECTION_MODE.GLOBE: - offsetMode = false; - geospatialOrigin = null; - break; - - default: - // Unknown projection mode - offsetMode = false; - } - - return {geospatialOrigin, shaderCoordinateOrigin, offsetMode}; -} - -// The code that utilizes Matrix4 does the same calculation as their mat4 counterparts, -// has lower performance but provides error checking. -function calculateMatrixAndOffset( - viewport: Viewport, - coordinateSystem: CoordinateSystem, - coordinateOrigin: Vec3 -): { - viewMatrix: Matrix4Like; - viewProjectionMatrix: Matrix4Like; - projectionCenter: Vec4; - originCommon: Vec4; - cameraPosCommon: Vec3; - shaderCoordinateOrigin: Vec3; - geospatialOrigin: Vec3 | null; -} { - const {viewMatrixUncentered, projectionMatrix} = viewport; - let {viewMatrix, viewProjectionMatrix} = viewport; - - let projectionCenter = ZERO_VECTOR; - let originCommon: Vec4 = ZERO_VECTOR; - let cameraPosCommon: Vec3 = viewport.cameraPosition as Vec3; - const {geospatialOrigin, shaderCoordinateOrigin, offsetMode} = getOffsetOrigin( - viewport, - coordinateSystem, - coordinateOrigin - ); - - if (offsetMode) { - // Calculate transformed projectionCenter (using 64 bit precision JS) - // This is the key to offset mode precision - // (avoids doing this addition in 32 bit precision in GLSL) - // @ts-expect-error the 4th component is assigned below - originCommon = viewport.projectPosition(geospatialOrigin || shaderCoordinateOrigin); - - cameraPosCommon = [ - cameraPosCommon[0] - originCommon[0], - cameraPosCommon[1] - originCommon[1], - cameraPosCommon[2] - originCommon[2] - ]; - - originCommon[3] = 1; - - // projectionCenter = new Matrix4(viewProjectionMatrix) - // .transformVector([positionPixels[0], positionPixels[1], 0.0, 1.0]); - projectionCenter = vec4.transformMat4([], originCommon, viewProjectionMatrix); - - // Always apply uncentered projection matrix if available (shader adds center) - viewMatrix = viewMatrixUncentered || viewMatrix; - - // Zero out 4th coordinate ("after" model matrix) - avoids further translations - // viewMatrix = new Matrix4(viewMatrixUncentered || viewMatrix) - // .multiplyRight(VECTOR_TO_POINT_MATRIX); - viewProjectionMatrix = mat4.multiply([], projectionMatrix, viewMatrix); - viewProjectionMatrix = mat4.multiply([], viewProjectionMatrix, VECTOR_TO_POINT_MATRIX); - } - - return { - viewMatrix: viewMatrix as Matrix4Like, - viewProjectionMatrix: viewProjectionMatrix as Matrix4Like, - projectionCenter, - originCommon, - cameraPosCommon, - shaderCoordinateOrigin, - geospatialOrigin - }; -} - -export type ProjectUniforms = { - coordinateSystem: number; - projectionMode: number; - coordinateOrigin: Vec3; - commonOrigin: Vec3; - center: Vec4; - // Backward compatibility - // TODO: remove in v9 - pseudoMeters: boolean; - - // Screen size - viewportSize: [number, number]; - devicePixelRatio: number; - - focalDistance: number; - commonUnitsPerMeter: Vec3; - commonUnitsPerWorldUnit: Vec3; - commonUnitsPerWorldUnit2: Vec3; - /** 2^zoom */ - scale: number; - wrapLongitude: boolean; - - viewProjectionMatrix: Matrix4Like; - modelMatrix: Matrix4Like; - - // This is for lighting calculations - cameraPosition: Vec3; -}; - -export type ProjectProps = { - viewport: Viewport; - devicePixelRatio?: number; - modelMatrix?: Matrix4Like | null; - coordinateSystem?: CoordinateSystem; - coordinateOrigin?: Vec3; - autoWrapLongitude?: boolean; -}; - -/** - * Returns uniforms for shaders based on current projection - * includes: projection matrix suitable for shaders - * - * TODO - Ensure this works with any viewport, not just WebMercatorViewports - * - * @param {WebMercatorViewport} viewport - - * @return {Float32Array} - 4x4 projection matrix that can be used in shaders - */ -export function getUniformsFromViewport({ - viewport, - devicePixelRatio = 1, - modelMatrix = null, - // Match Layer.defaultProps - coordinateSystem = 'default', - coordinateOrigin = DEFAULT_COORDINATE_ORIGIN, - autoWrapLongitude = false -}: ProjectProps): ProjectUniforms { - if (coordinateSystem === 'default') { - coordinateSystem = viewport.isGeospatial ? 'lnglat' : 'cartesian'; - } - - const uniforms = getMemoizedViewportUniforms({ - viewport, - devicePixelRatio, - coordinateSystem, - coordinateOrigin - }); - - uniforms.wrapLongitude = autoWrapLongitude; - uniforms.modelMatrix = modelMatrix || IDENTITY_MATRIX; - - return uniforms; -} - -function calculateViewportUniforms({ - viewport, - devicePixelRatio, - coordinateSystem, - coordinateOrigin -}: { - viewport: Viewport; - devicePixelRatio: number; - coordinateSystem: CoordinateSystem; - coordinateOrigin: Vec3; -}): ProjectUniforms { - const { - projectionCenter, - viewProjectionMatrix, - originCommon, - cameraPosCommon, - shaderCoordinateOrigin, - geospatialOrigin - } = calculateMatrixAndOffset(viewport, coordinateSystem, coordinateOrigin); - - // Calculate projection pixels per unit - const distanceScales = viewport.getDistanceScales(); - - const viewportSize: [number, number] = [ - viewport.width * devicePixelRatio, - viewport.height * devicePixelRatio - ]; - - // Distance at which screen pixels are projected. - // Used to scale sizes in clipspace to match screen pixels. - // When using Viewport class's default projection matrix, this yields 1 for orthographic - // and `viewport.focalDistance` for perspective views - const focalDistance = - vec4.transformMat4([], [0, 0, -viewport.focalDistance, 1], viewport.projectionMatrix)[3] || 1; - - const uniforms: ProjectUniforms = { - // Projection mode values - coordinateSystem: getShaderCoordinateSystem(coordinateSystem), - projectionMode: viewport.projectionMode, - coordinateOrigin: shaderCoordinateOrigin, - commonOrigin: originCommon.slice(0, 3) as Vec3, - center: projectionCenter, - - // Backward compatibility - // TODO: remove in v9 - // @ts-expect-error _pseudoMeters is only defined on WebMercator viewport - pseudoMeters: Boolean(viewport._pseudoMeters), - - // Screen size - viewportSize, - devicePixelRatio, - - focalDistance, - commonUnitsPerMeter: distanceScales.unitsPerMeter as Vec3, - commonUnitsPerWorldUnit: distanceScales.unitsPerMeter as Vec3, - commonUnitsPerWorldUnit2: DEFAULT_PIXELS_PER_UNIT2, - scale: viewport.scale, // This is the mercator scale (2 ** zoom) - wrapLongitude: false, - - viewProjectionMatrix, - modelMatrix: IDENTITY_MATRIX, - - // This is for lighting calculations - cameraPosition: cameraPosCommon - }; - - if (geospatialOrigin) { - // Get high-precision DistanceScales from geospatial viewport - // TODO: stricter types in Viewport classes - const distanceScalesAtOrigin = viewport.getDistanceScales(geospatialOrigin) as { - unitsPerMeter: Vec3; - metersPerUnit: Vec3; - unitsPerMeter2: Vec3; - unitsPerDegree: Vec3; - degreesPerUnit: Vec3; - unitsPerDegree2: Vec3; - }; - switch (coordinateSystem) { - case 'meter-offsets': - uniforms.commonUnitsPerWorldUnit = distanceScalesAtOrigin.unitsPerMeter; - uniforms.commonUnitsPerWorldUnit2 = distanceScalesAtOrigin.unitsPerMeter2; - break; - - case 'lnglat': - case 'lnglat-offsets': - // @ts-expect-error _pseudoMeters only exists on WebMercatorView - if (!viewport._pseudoMeters) { - uniforms.commonUnitsPerMeter = distanceScalesAtOrigin.unitsPerMeter; - } - uniforms.commonUnitsPerWorldUnit = distanceScalesAtOrigin.unitsPerDegree; - uniforms.commonUnitsPerWorldUnit2 = distanceScalesAtOrigin.unitsPerDegree2; - break; - - // a.k.a "preprojected" positions - case 'cartesian': - uniforms.commonUnitsPerWorldUnit = [1, 1, distanceScalesAtOrigin.unitsPerMeter[2]]; - uniforms.commonUnitsPerWorldUnit2 = [0, 0, distanceScalesAtOrigin.unitsPerMeter2[2]]; - break; - - default: - break; - } - } - - return uniforms; -} +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +/* eslint-disable complexity, camelcase */ + +import {mat4, Matrix4Like, vec4} from '@math.gl/core'; + +import {PROJECTION_MODE} from '../../lib/constants'; + +import memoize from '../../utils/memoize'; + +import type Viewport from '../../viewports/viewport'; +import type {CoordinateSystem} from '../../lib/constants'; + +type Vec3 = [number, number, number]; +type Vec4 = [number, number, number, number]; + +// To quickly set a vector to zero +const ZERO_VECTOR: Vec4 = [0, 0, 0, 0]; +// 4x4 matrix that drops 4th component of vector +const VECTOR_TO_POINT_MATRIX: Matrix4Like = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]; +const IDENTITY_MATRIX: Matrix4Like = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; +const DEFAULT_PIXELS_PER_UNIT2: Vec3 = [0, 0, 0]; +const DEFAULT_COORDINATE_ORIGIN: Vec3 = [0, 0, 0]; + +/** Coordinate system constants */ +const COORDINATE_SYSTEM_NUMBERS = { + default: -1, + cartesian: 0, + lnglat: 1, + 'meter-offsets': 2, + 'lnglat-offsets': 3 +} as const satisfies Record; + +export function getShaderCoordinateSystem(coordinateSystem: CoordinateSystem) { + const shaderCoordinateSystem = COORDINATE_SYSTEM_NUMBERS[coordinateSystem]; + if (shaderCoordinateSystem === undefined) { + throw new Error(`Invalid coordinateSystem: ${coordinateSystem}`); + } + return shaderCoordinateSystem; +} + +const getMemoizedViewportUniforms = memoize(calculateViewportUniforms); + +export function getOffsetOrigin( + viewport: Viewport, + coordinateSystem: CoordinateSystem, + coordinateOrigin: Vec3 = DEFAULT_COORDINATE_ORIGIN +): { + geospatialOrigin: Vec3 | null; + shaderCoordinateOrigin: Vec3; + offsetMode: boolean; +} { + if (coordinateOrigin.length < 3) { + coordinateOrigin = [coordinateOrigin[0], coordinateOrigin[1], 0]; + } + + let shaderCoordinateOrigin = coordinateOrigin; + let geospatialOrigin: Vec3 | null; + let offsetMode = true; + + if (coordinateSystem === 'lnglat-offsets' || coordinateSystem === 'meter-offsets') { + geospatialOrigin = coordinateOrigin; + } else { + geospatialOrigin = viewport.isGeospatial + ? // @ts-expect-error longitude and latitude are not defined on the base Viewport, but is expected on geospatial viewports + [Math.fround(viewport.longitude), Math.fround(viewport.latitude), 0] + : null; + } + + switch (viewport.projectionMode) { + case PROJECTION_MODE.WEB_MERCATOR: + if (coordinateSystem === 'lnglat' || coordinateSystem === 'cartesian') { + geospatialOrigin = [0, 0, 0]; + offsetMode = false; + } + break; + + case PROJECTION_MODE.WEB_MERCATOR_AUTO_OFFSET: + if (coordinateSystem === 'lnglat') { + // viewport center in world space + // @ts-expect-error when using LNGLAT coordinates, we expect the viewport to be geospatial, in which case geospatialOrigin is defined + shaderCoordinateOrigin = geospatialOrigin; + } else if (coordinateSystem === 'cartesian') { + // viewport center in common space + shaderCoordinateOrigin = [ + Math.fround(viewport.center[0]), + Math.fround(viewport.center[1]), + 0 + ]; + // Geospatial origin (wgs84) must match shaderCoordinateOrigin (common) + geospatialOrigin = viewport.unprojectPosition(shaderCoordinateOrigin); + shaderCoordinateOrigin[0] -= coordinateOrigin[0]; + shaderCoordinateOrigin[1] -= coordinateOrigin[1]; + shaderCoordinateOrigin[2] -= coordinateOrigin[2]; + } + break; + + case PROJECTION_MODE.IDENTITY: + shaderCoordinateOrigin = viewport.position.map(Math.fround) as Vec3; + shaderCoordinateOrigin[2] = shaderCoordinateOrigin[2] || 0; + break; + + case PROJECTION_MODE.GLOBE: + offsetMode = false; + geospatialOrigin = null; + break; + + default: + // Unknown projection mode + offsetMode = false; + } + + return {geospatialOrigin, shaderCoordinateOrigin, offsetMode}; +} + +// The code that utilizes Matrix4 does the same calculation as their mat4 counterparts, +// has lower performance but provides error checking. +function calculateMatrixAndOffset( + viewport: Viewport, + coordinateSystem: CoordinateSystem, + coordinateOrigin: Vec3 +): { + viewMatrix: Matrix4Like; + viewProjectionMatrix: Matrix4Like; + projectionCenter: Vec4; + originCommon: Vec4; + cameraPosCommon: Vec3; + shaderCoordinateOrigin: Vec3; + geospatialOrigin: Vec3 | null; +} { + const {viewMatrixUncentered, projectionMatrix} = viewport; + let {viewMatrix, viewProjectionMatrix} = viewport; + + let projectionCenter = ZERO_VECTOR; + let originCommon: Vec4 = ZERO_VECTOR; + let cameraPosCommon: Vec3 = viewport.cameraPosition as Vec3; + const {geospatialOrigin, shaderCoordinateOrigin, offsetMode} = getOffsetOrigin( + viewport, + coordinateSystem, + coordinateOrigin + ); + + if (offsetMode) { + // Calculate transformed projectionCenter (using 64 bit precision JS) + // This is the key to offset mode precision + // (avoids doing this addition in 32 bit precision in GLSL) + // @ts-expect-error the 4th component is assigned below + originCommon = viewport.projectPosition(geospatialOrigin || shaderCoordinateOrigin); + + cameraPosCommon = [ + cameraPosCommon[0] - originCommon[0], + cameraPosCommon[1] - originCommon[1], + cameraPosCommon[2] - originCommon[2] + ]; + + originCommon[3] = 1; + + // projectionCenter = new Matrix4(viewProjectionMatrix) + // .transformVector([positionPixels[0], positionPixels[1], 0.0, 1.0]); + projectionCenter = vec4.transformMat4([], originCommon, viewProjectionMatrix); + + // Always apply uncentered projection matrix if available (shader adds center) + viewMatrix = viewMatrixUncentered || viewMatrix; + + // Zero out 4th coordinate ("after" model matrix) - avoids further translations + // viewMatrix = new Matrix4(viewMatrixUncentered || viewMatrix) + // .multiplyRight(VECTOR_TO_POINT_MATRIX); + viewProjectionMatrix = mat4.multiply([], projectionMatrix, viewMatrix); + viewProjectionMatrix = mat4.multiply([], viewProjectionMatrix, VECTOR_TO_POINT_MATRIX); + } + + return { + viewMatrix: viewMatrix as Matrix4Like, + viewProjectionMatrix: viewProjectionMatrix as Matrix4Like, + projectionCenter, + originCommon, + cameraPosCommon, + shaderCoordinateOrigin, + geospatialOrigin + }; +} + +export type ProjectUniforms = { + coordinateSystem: number; + projectionMode: number; + coordinateOrigin: Vec3; + commonOrigin: Vec3; + center: Vec4; + // Backward compatibility + // TODO: remove in v9 + pseudoMeters: boolean; + + // Screen size + viewportSize: [number, number]; + devicePixelRatio: number; + + focalDistance: number; + commonUnitsPerMeter: Vec3; + commonUnitsPerWorldUnit: Vec3; + commonUnitsPerWorldUnit2: Vec3; + /** 2^zoom */ + scale: number; + wrapLongitude: boolean; + + viewProjectionMatrix: Matrix4Like; + modelMatrix: Matrix4Like; + + // This is for lighting calculations + cameraPosition: Vec3; +}; + +export type ProjectProps = { + viewport: Viewport; + devicePixelRatio?: number; + modelMatrix?: Matrix4Like | null; + coordinateSystem?: CoordinateSystem; + coordinateOrigin?: Vec3; + autoWrapLongitude?: boolean; +}; + +/** + * Returns uniforms for shaders based on current projection + * includes: projection matrix suitable for shaders + * + * TODO - Ensure this works with any viewport, not just WebMercatorViewports + * + * @param {WebMercatorViewport} viewport - + * @return {Float32Array} - 4x4 projection matrix that can be used in shaders + */ +export function getUniformsFromViewport({ + viewport, + devicePixelRatio = 1, + modelMatrix = null, + // Match Layer.defaultProps + coordinateSystem = 'default', + coordinateOrigin = DEFAULT_COORDINATE_ORIGIN, + autoWrapLongitude = false +}: ProjectProps): ProjectUniforms { + if (coordinateSystem === 'default') { + coordinateSystem = viewport.isGeospatial ? 'lnglat' : 'cartesian'; + } + + const uniforms = getMemoizedViewportUniforms({ + viewport, + devicePixelRatio, + coordinateSystem, + coordinateOrigin + }); + + uniforms.wrapLongitude = autoWrapLongitude; + uniforms.modelMatrix = modelMatrix || IDENTITY_MATRIX; + + return uniforms; +} + +function calculateViewportUniforms({ + viewport, + devicePixelRatio, + coordinateSystem, + coordinateOrigin +}: { + viewport: Viewport; + devicePixelRatio: number; + coordinateSystem: CoordinateSystem; + coordinateOrigin: Vec3; +}): ProjectUniforms { + const { + projectionCenter, + viewProjectionMatrix, + originCommon, + cameraPosCommon, + shaderCoordinateOrigin, + geospatialOrigin + } = calculateMatrixAndOffset(viewport, coordinateSystem, coordinateOrigin); + + // Calculate projection pixels per unit + const distanceScales = viewport.getDistanceScales(); + + const viewportSize: [number, number] = [ + viewport.width * devicePixelRatio, + viewport.height * devicePixelRatio + ]; + + // Distance at which screen pixels are projected. + // Used to scale sizes in clipspace to match screen pixels. + // When using Viewport class's default projection matrix, this yields 1 for orthographic + // and `viewport.focalDistance` for perspective views + const focalDistance = + vec4.transformMat4([], [0, 0, -viewport.focalDistance, 1], viewport.projectionMatrix)[3] || 1; + + const uniforms: ProjectUniforms = { + // Projection mode values + coordinateSystem: getShaderCoordinateSystem(coordinateSystem), + projectionMode: viewport.projectionMode, + coordinateOrigin: shaderCoordinateOrigin, + commonOrigin: originCommon.slice(0, 3) as Vec3, + center: projectionCenter, + + // Backward compatibility + // TODO: remove in v9 + // @ts-expect-error _pseudoMeters is only defined on WebMercator viewport + pseudoMeters: Boolean(viewport._pseudoMeters), + + // Screen size + viewportSize, + devicePixelRatio, + + focalDistance, + commonUnitsPerMeter: distanceScales.unitsPerMeter as Vec3, + commonUnitsPerWorldUnit: distanceScales.unitsPerMeter as Vec3, + commonUnitsPerWorldUnit2: DEFAULT_PIXELS_PER_UNIT2, + scale: viewport.scale, // This is the mercator scale (2 ** zoom) + wrapLongitude: false, + + viewProjectionMatrix, + modelMatrix: IDENTITY_MATRIX, + + // This is for lighting calculations + cameraPosition: cameraPosCommon + }; + + if (geospatialOrigin) { + // Get high-precision DistanceScales from geospatial viewport + // TODO: stricter types in Viewport classes + const distanceScalesAtOrigin = viewport.getDistanceScales(geospatialOrigin) as { + unitsPerMeter: Vec3; + metersPerUnit: Vec3; + unitsPerMeter2: Vec3; + unitsPerDegree: Vec3; + degreesPerUnit: Vec3; + unitsPerDegree2: Vec3; + }; + switch (coordinateSystem) { + case 'meter-offsets': + uniforms.commonUnitsPerWorldUnit = distanceScalesAtOrigin.unitsPerMeter; + uniforms.commonUnitsPerWorldUnit2 = distanceScalesAtOrigin.unitsPerMeter2; + break; + + case 'lnglat': + case 'lnglat-offsets': + // @ts-expect-error _pseudoMeters only exists on WebMercatorView + if (!viewport._pseudoMeters) { + uniforms.commonUnitsPerMeter = distanceScalesAtOrigin.unitsPerMeter; + } + uniforms.commonUnitsPerWorldUnit = distanceScalesAtOrigin.unitsPerDegree; + uniforms.commonUnitsPerWorldUnit2 = distanceScalesAtOrigin.unitsPerDegree2; + break; + + // a.k.a "preprojected" positions + case 'cartesian': + uniforms.commonUnitsPerWorldUnit = [1, 1, distanceScalesAtOrigin.unitsPerMeter[2]]; + uniforms.commonUnitsPerWorldUnit2 = [0, 0, distanceScalesAtOrigin.unitsPerMeter2[2]]; + break; + + default: + break; + } + } + + // For GLOBE + METER_OFFSETS, precompute the globe-space position of + // coordinateOrigin into commonOrigin. The shader derives the ENU frame from + // this direction via project_get_orientation_matrix, avoiding per-vertex trig. + if ( + viewport.projectionMode === PROJECTION_MODE.GLOBE && + coordinateSystem === 'meter-offsets' + ) { + const EARTH_RADIUS = 6370972; + const GLOBE_RADIUS = 256; + const lambda = (coordinateOrigin[0] * Math.PI) / 180; + const phi = (coordinateOrigin[1] * Math.PI) / 180; + const cosPhi = Math.cos(phi); + const D = ((coordinateOrigin[2] || 0) / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; + uniforms.commonOrigin = [ + Math.sin(lambda) * cosPhi * D, + -Math.cos(lambda) * cosPhi * D, + Math.sin(phi) * D + ]; + } + + return uniforms; +} From c57abbd05881ae1fae4874312f24d25112741fd5 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Wed, 22 Apr 2026 10:04:50 -0400 Subject: [PATCH 4/6] chore: format globe projection shader changes --- .../src/shaderlib/project/project.glsl.ts | 606 +++++++------- .../src/shaderlib/project/project.wgsl.ts | 646 +++++++-------- .../shaderlib/project/viewport-uniforms.ts | 763 +++++++++--------- 3 files changed, 1006 insertions(+), 1009 deletions(-) diff --git a/modules/core/src/shaderlib/project/project.glsl.ts b/modules/core/src/shaderlib/project/project.glsl.ts index a63bb375b33..cc13f0eb4a4 100644 --- a/modules/core/src/shaderlib/project/project.glsl.ts +++ b/modules/core/src/shaderlib/project/project.glsl.ts @@ -1,303 +1,303 @@ -// deck.gl -// SPDX-License-Identifier: MIT -// Copyright (c) vis.gl contributors - -import {PROJECTION_MODE, UNIT} from '../../lib/constants'; -import {getShaderCoordinateSystem} from './viewport-uniforms'; - -const SHADER_COORDINATE_SYSTEMS = [ - 'default', - 'lnglat', - 'meter-offsets', - 'lnglat-offsets', - 'cartesian' -] as const; - -const COORDINATE_SYSTEM_GLSL_CONSTANTS = SHADER_COORDINATE_SYSTEMS.map( - coordinateSystem => - `const int COORDINATE_SYSTEM_${coordinateSystem.toUpperCase().replaceAll('-', '_')} = ${getShaderCoordinateSystem(coordinateSystem)};` -).join(''); -const PROJECTION_MODE_GLSL_CONSTANTS = Object.keys(PROJECTION_MODE) - .map(key => `const int PROJECTION_MODE_${key} = ${PROJECTION_MODE[key]};`) - .join(''); -const UNIT_GLSL_CONSTANTS = Object.keys(UNIT) - .map(key => `const int UNIT_${key.toUpperCase()} = ${UNIT[key]};`) - .join(''); - -export const projectGLSL = /* glsl */ `\ -${COORDINATE_SYSTEM_GLSL_CONSTANTS} -${PROJECTION_MODE_GLSL_CONSTANTS} -${UNIT_GLSL_CONSTANTS} - -layout(std140) uniform projectUniforms { - bool wrapLongitude; - int coordinateSystem; - vec3 commonUnitsPerMeter; - int projectionMode; - float scale; - vec3 commonUnitsPerWorldUnit; - vec3 commonUnitsPerWorldUnit2; - vec4 center; - mat4 modelMatrix; - mat4 viewProjectionMatrix; - vec2 viewportSize; - float devicePixelRatio; - float focalDistance; - vec3 cameraPosition; - vec3 coordinateOrigin; - vec3 commonOrigin; - bool pseudoMeters; -} project; - - -const float TILE_SIZE = 512.0; -const float PI = 3.1415926536; -const float WORLD_SCALE = TILE_SIZE / (PI * 2.0); -const vec3 ZERO_64_LOW = vec3(0.0); -const float EARTH_RADIUS = 6370972.0; // meters -const float GLOBE_RADIUS = 256.0; - -// returns an adjustment factor for uCommonUnitsPerMeter -float project_size_at_latitude(float lat) { - float y = clamp(lat, -89.9, 89.9); - return 1.0 / cos(radians(y)); -} - -float project_size() { - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR && - project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT && - project.pseudoMeters == false) { - - // uCommonUnitsPerMeter in low-zoom Web Mercator is non-linear - // Adjust by 1 / cos(latitude) - // If geometry.position (vertex in common space) is populated, use it - // Otherwise use geometry.worldPosition (anchor in world space) - - if (geometry.position.w == 0.0) { - return project_size_at_latitude(geometry.worldPosition.y); - } - - // latitude from common y: 2.0 * (atan(exp(y / TILE_SIZE * 2.0 * PI - PI)) - PI / 4.0) - // Taylor series of 1 / cos(latitude) - // Max error < 0.003 - - float y = geometry.position.y / TILE_SIZE * 2.0 - 1.0; - float y2 = y * y; - float y4 = y2 * y2; - float y6 = y4 * y2; - return 1.0 + 4.9348 * y2 + 4.0587 * y4 + 1.5642 * y6; - } - return 1.0; -} - -float project_size_at_latitude(float meters, float lat) { - return meters * project.commonUnitsPerMeter.z * project_size_at_latitude(lat); -} - -// -// Scaling offsets - scales meters to "world distance" -// Note the scalar version of project_size is for scaling the z component only -// -float project_size(float meters) { - // For scatter relevant - return meters * project.commonUnitsPerMeter.z * project_size(); -} - -vec2 project_size(vec2 meters) { - return meters * project.commonUnitsPerMeter.xy * project_size(); -} - -vec3 project_size(vec3 meters) { - return meters * project.commonUnitsPerMeter * project_size(); -} - -vec4 project_size(vec4 meters) { - return vec4(meters.xyz * project.commonUnitsPerMeter, meters.w); -} - -// Get rotation matrix that aligns the z axis with the given up vector -// Find 3 unit vectors ux, uy, uz that are perpendicular to each other and uz == up -mat3 project_get_orientation_matrix(vec3 up) { - vec3 uz = normalize(up); - // Tangent on XY plane - vec3 ux = abs(uz.z) == 1.0 ? vec3(1.0, 0.0, 0.0) : normalize(vec3(uz.y, -uz.x, 0)); - vec3 uy = cross(uz, ux); - return mat3(ux, uy, uz); -} - -bool project_needs_rotation(vec3 commonPosition, out mat3 transform) { - if (project.projectionMode == PROJECTION_MODE_GLOBE) { - transform = project_get_orientation_matrix(commonPosition); - return true; - } - return false; -} - -// -// Projecting normal - transform deltas from current coordinate system to -// normals in the worldspace -// -vec3 project_normal(vec3 vector) { - // Apply model matrix - vec4 normal_modelspace = project.modelMatrix * vec4(vector, 0.0); - vec3 n = normalize(normal_modelspace.xyz * project.commonUnitsPerMeter); - mat3 rotation; - if (project_needs_rotation(geometry.position.xyz, rotation)) { - n = rotation * n; - } - return n; -} - -vec4 project_offset_(vec4 offset) { - float dy = offset.y; - vec3 commonUnitsPerWorldUnit = project.commonUnitsPerWorldUnit + project.commonUnitsPerWorldUnit2 * dy; - return vec4(offset.xyz * commonUnitsPerWorldUnit, offset.w); -} - -// -// Projecting positions - non-linear projection: lnglats => unit tile [0-1, 0-1] -// -vec2 project_mercator_(vec2 lnglat) { - float x = lnglat.x; - if (project.wrapLongitude) { - x = mod(x + 180., 360.0) - 180.; - } - float y = clamp(lnglat.y, -89.9, 89.9); - return vec2( - radians(x) + PI, - PI + log(tan_fp32(PI * 0.25 + radians(y) * 0.5)) - ) * WORLD_SCALE; -} - -vec3 project_globe_(vec3 lnglatz) { - float lambda = radians(lnglatz.x); - float phi = radians(lnglatz.y); - float cosPhi = cos(phi); - float D = (lnglatz.z / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; - - return vec3( - sin(lambda) * cosPhi, - -cos(lambda) * cosPhi, - sin(phi) - ) * D; -} - -// -// Projects positions (defined by project.coordinateSystem) to common space (defined by project.projectionMode) -// -vec4 project_position(vec4 position, vec3 position64Low) { - vec4 position_world = project.modelMatrix * position; - - // Work around for a Mac+NVIDIA bug https://github.com/visgl/deck.gl/issues/4145 - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - return vec4( - project_mercator_(position_world.xy), - project_size_at_latitude(position_world.z, position_world.y), - position_world.w - ); - } - if (project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN) { - position_world.xyz += project.coordinateOrigin; - } - } - if (project.projectionMode == PROJECTION_MODE_GLOBE) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - return vec4( - project_globe_(position_world.xyz), - position_world.w - ); - } - if (project.coordinateSystem == COORDINATE_SYSTEM_METER_OFFSETS) { - // position_world is meters in the ENU tangent frame at coordinateOrigin. - // commonOrigin (precomputed CPU-side) is the globe-space position of - // that origin; project_get_orientation_matrix derives the ENU frame from - // that direction so the tangent plane attaches to the sphere and tiles - // land at their geographic location oriented with the surface. - mat3 enuMatrix = project_get_orientation_matrix(project.commonOrigin); - float metersToCommon = GLOBE_RADIUS / EARTH_RADIUS; - vec3 offsetCommon = (enuMatrix * vec3(-position_world.xy, position_world.z)) * metersToCommon; - return vec4(project.commonOrigin + offsetCommon, position_world.w); - } - } - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - if (abs(position_world.y - project.coordinateOrigin.y) > 0.25) { - // Too far from the projection center for offset mode to be accurate - // Only use high parts - return vec4( - project_mercator_(position_world.xy) - project.commonOrigin.xy, - project_size(position_world.z), - position_world.w - ); - } - } - } - if (project.projectionMode == PROJECTION_MODE_IDENTITY || - (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET && - (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT || - project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN))) { - // Subtract high part of 64 bit value. Convert remainder to float32, preserving precision. - position_world.xyz -= project.coordinateOrigin; - } - - // Translation is already added to the high parts - return project_offset_(position_world) + project_offset_(project.modelMatrix * vec4(position64Low, 0.0)); -} - -vec4 project_position(vec4 position) { - return project_position(position, ZERO_64_LOW); -} - -vec3 project_position(vec3 position, vec3 position64Low) { - vec4 projected_position = project_position(vec4(position, 1.0), position64Low); - return projected_position.xyz; -} - -vec3 project_position(vec3 position) { - vec4 projected_position = project_position(vec4(position, 1.0), ZERO_64_LOW); - return projected_position.xyz; -} - -vec2 project_position(vec2 position) { - vec4 projected_position = project_position(vec4(position, 0.0, 1.0), ZERO_64_LOW); - return projected_position.xy; -} - -vec4 project_common_position_to_clipspace(vec4 position, mat4 viewProjectionMatrix, vec4 center) { - return viewProjectionMatrix * position + center; -} - -// -// Projects from common space coordinates to clip space. -// Uses project.viewProjectionMatrix -// -vec4 project_common_position_to_clipspace(vec4 position) { - return project_common_position_to_clipspace(position, project.viewProjectionMatrix, project.center); -} - -// Returns a clip space offset that corresponds to a given number of screen pixels -vec2 project_pixel_size_to_clipspace(vec2 pixels) { - vec2 offset = pixels / project.viewportSize * project.devicePixelRatio * 2.0; - return offset * project.focalDistance; -} - -float project_size_to_pixel(float meters) { - return project_size(meters) * project.scale; -} -vec2 project_size_to_pixel(vec2 meters) { - return project_size(meters) * project.scale; -} -float project_size_to_pixel(float size, int unit) { - if (unit == UNIT_METERS) return project_size_to_pixel(size); - if (unit == UNIT_COMMON) return size * project.scale; - // UNIT_PIXELS - return size; -} -float project_pixel_size(float pixels) { - return pixels / project.scale; -} -vec2 project_pixel_size(vec2 pixels) { - return pixels / project.scale; -} -`; +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {PROJECTION_MODE, UNIT} from '../../lib/constants'; +import {getShaderCoordinateSystem} from './viewport-uniforms'; + +const SHADER_COORDINATE_SYSTEMS = [ + 'default', + 'lnglat', + 'meter-offsets', + 'lnglat-offsets', + 'cartesian' +] as const; + +const COORDINATE_SYSTEM_GLSL_CONSTANTS = SHADER_COORDINATE_SYSTEMS.map( + coordinateSystem => + `const int COORDINATE_SYSTEM_${coordinateSystem.toUpperCase().replaceAll('-', '_')} = ${getShaderCoordinateSystem(coordinateSystem)};` +).join(''); +const PROJECTION_MODE_GLSL_CONSTANTS = Object.keys(PROJECTION_MODE) + .map(key => `const int PROJECTION_MODE_${key} = ${PROJECTION_MODE[key]};`) + .join(''); +const UNIT_GLSL_CONSTANTS = Object.keys(UNIT) + .map(key => `const int UNIT_${key.toUpperCase()} = ${UNIT[key]};`) + .join(''); + +export const projectGLSL = /* glsl */ `\ +${COORDINATE_SYSTEM_GLSL_CONSTANTS} +${PROJECTION_MODE_GLSL_CONSTANTS} +${UNIT_GLSL_CONSTANTS} + +layout(std140) uniform projectUniforms { + bool wrapLongitude; + int coordinateSystem; + vec3 commonUnitsPerMeter; + int projectionMode; + float scale; + vec3 commonUnitsPerWorldUnit; + vec3 commonUnitsPerWorldUnit2; + vec4 center; + mat4 modelMatrix; + mat4 viewProjectionMatrix; + vec2 viewportSize; + float devicePixelRatio; + float focalDistance; + vec3 cameraPosition; + vec3 coordinateOrigin; + vec3 commonOrigin; + bool pseudoMeters; +} project; + + +const float TILE_SIZE = 512.0; +const float PI = 3.1415926536; +const float WORLD_SCALE = TILE_SIZE / (PI * 2.0); +const vec3 ZERO_64_LOW = vec3(0.0); +const float EARTH_RADIUS = 6370972.0; // meters +const float GLOBE_RADIUS = 256.0; + +// returns an adjustment factor for uCommonUnitsPerMeter +float project_size_at_latitude(float lat) { + float y = clamp(lat, -89.9, 89.9); + return 1.0 / cos(radians(y)); +} + +float project_size() { + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR && + project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT && + project.pseudoMeters == false) { + + // uCommonUnitsPerMeter in low-zoom Web Mercator is non-linear + // Adjust by 1 / cos(latitude) + // If geometry.position (vertex in common space) is populated, use it + // Otherwise use geometry.worldPosition (anchor in world space) + + if (geometry.position.w == 0.0) { + return project_size_at_latitude(geometry.worldPosition.y); + } + + // latitude from common y: 2.0 * (atan(exp(y / TILE_SIZE * 2.0 * PI - PI)) - PI / 4.0) + // Taylor series of 1 / cos(latitude) + // Max error < 0.003 + + float y = geometry.position.y / TILE_SIZE * 2.0 - 1.0; + float y2 = y * y; + float y4 = y2 * y2; + float y6 = y4 * y2; + return 1.0 + 4.9348 * y2 + 4.0587 * y4 + 1.5642 * y6; + } + return 1.0; +} + +float project_size_at_latitude(float meters, float lat) { + return meters * project.commonUnitsPerMeter.z * project_size_at_latitude(lat); +} + +// +// Scaling offsets - scales meters to "world distance" +// Note the scalar version of project_size is for scaling the z component only +// +float project_size(float meters) { + // For scatter relevant + return meters * project.commonUnitsPerMeter.z * project_size(); +} + +vec2 project_size(vec2 meters) { + return meters * project.commonUnitsPerMeter.xy * project_size(); +} + +vec3 project_size(vec3 meters) { + return meters * project.commonUnitsPerMeter * project_size(); +} + +vec4 project_size(vec4 meters) { + return vec4(meters.xyz * project.commonUnitsPerMeter, meters.w); +} + +// Get rotation matrix that aligns the z axis with the given up vector +// Find 3 unit vectors ux, uy, uz that are perpendicular to each other and uz == up +mat3 project_get_orientation_matrix(vec3 up) { + vec3 uz = normalize(up); + // Tangent on XY plane + vec3 ux = abs(uz.z) == 1.0 ? vec3(1.0, 0.0, 0.0) : normalize(vec3(uz.y, -uz.x, 0)); + vec3 uy = cross(uz, ux); + return mat3(ux, uy, uz); +} + +bool project_needs_rotation(vec3 commonPosition, out mat3 transform) { + if (project.projectionMode == PROJECTION_MODE_GLOBE) { + transform = project_get_orientation_matrix(commonPosition); + return true; + } + return false; +} + +// +// Projecting normal - transform deltas from current coordinate system to +// normals in the worldspace +// +vec3 project_normal(vec3 vector) { + // Apply model matrix + vec4 normal_modelspace = project.modelMatrix * vec4(vector, 0.0); + vec3 n = normalize(normal_modelspace.xyz * project.commonUnitsPerMeter); + mat3 rotation; + if (project_needs_rotation(geometry.position.xyz, rotation)) { + n = rotation * n; + } + return n; +} + +vec4 project_offset_(vec4 offset) { + float dy = offset.y; + vec3 commonUnitsPerWorldUnit = project.commonUnitsPerWorldUnit + project.commonUnitsPerWorldUnit2 * dy; + return vec4(offset.xyz * commonUnitsPerWorldUnit, offset.w); +} + +// +// Projecting positions - non-linear projection: lnglats => unit tile [0-1, 0-1] +// +vec2 project_mercator_(vec2 lnglat) { + float x = lnglat.x; + if (project.wrapLongitude) { + x = mod(x + 180., 360.0) - 180.; + } + float y = clamp(lnglat.y, -89.9, 89.9); + return vec2( + radians(x) + PI, + PI + log(tan_fp32(PI * 0.25 + radians(y) * 0.5)) + ) * WORLD_SCALE; +} + +vec3 project_globe_(vec3 lnglatz) { + float lambda = radians(lnglatz.x); + float phi = radians(lnglatz.y); + float cosPhi = cos(phi); + float D = (lnglatz.z / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; + + return vec3( + sin(lambda) * cosPhi, + -cos(lambda) * cosPhi, + sin(phi) + ) * D; +} + +// +// Projects positions (defined by project.coordinateSystem) to common space (defined by project.projectionMode) +// +vec4 project_position(vec4 position, vec3 position64Low) { + vec4 position_world = project.modelMatrix * position; + + // Work around for a Mac+NVIDIA bug https://github.com/visgl/deck.gl/issues/4145 + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + return vec4( + project_mercator_(position_world.xy), + project_size_at_latitude(position_world.z, position_world.y), + position_world.w + ); + } + if (project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN) { + position_world.xyz += project.coordinateOrigin; + } + } + if (project.projectionMode == PROJECTION_MODE_GLOBE) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + return vec4( + project_globe_(position_world.xyz), + position_world.w + ); + } + if (project.coordinateSystem == COORDINATE_SYSTEM_METER_OFFSETS) { + // position_world is meters in the ENU tangent frame at coordinateOrigin. + // commonOrigin (precomputed CPU-side) is the globe-space position of + // that origin; project_get_orientation_matrix derives the ENU frame from + // that direction so the tangent plane attaches to the sphere and tiles + // land at their geographic location oriented with the surface. + mat3 enuMatrix = project_get_orientation_matrix(project.commonOrigin); + float metersToCommon = GLOBE_RADIUS / EARTH_RADIUS; + vec3 offsetCommon = (enuMatrix * vec3(-position_world.xy, position_world.z)) * metersToCommon; + return vec4(project.commonOrigin + offsetCommon, position_world.w); + } + } + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + if (abs(position_world.y - project.coordinateOrigin.y) > 0.25) { + // Too far from the projection center for offset mode to be accurate + // Only use high parts + return vec4( + project_mercator_(position_world.xy) - project.commonOrigin.xy, + project_size(position_world.z), + position_world.w + ); + } + } + } + if (project.projectionMode == PROJECTION_MODE_IDENTITY || + (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET && + (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT || + project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN))) { + // Subtract high part of 64 bit value. Convert remainder to float32, preserving precision. + position_world.xyz -= project.coordinateOrigin; + } + + // Translation is already added to the high parts + return project_offset_(position_world) + project_offset_(project.modelMatrix * vec4(position64Low, 0.0)); +} + +vec4 project_position(vec4 position) { + return project_position(position, ZERO_64_LOW); +} + +vec3 project_position(vec3 position, vec3 position64Low) { + vec4 projected_position = project_position(vec4(position, 1.0), position64Low); + return projected_position.xyz; +} + +vec3 project_position(vec3 position) { + vec4 projected_position = project_position(vec4(position, 1.0), ZERO_64_LOW); + return projected_position.xyz; +} + +vec2 project_position(vec2 position) { + vec4 projected_position = project_position(vec4(position, 0.0, 1.0), ZERO_64_LOW); + return projected_position.xy; +} + +vec4 project_common_position_to_clipspace(vec4 position, mat4 viewProjectionMatrix, vec4 center) { + return viewProjectionMatrix * position + center; +} + +// +// Projects from common space coordinates to clip space. +// Uses project.viewProjectionMatrix +// +vec4 project_common_position_to_clipspace(vec4 position) { + return project_common_position_to_clipspace(position, project.viewProjectionMatrix, project.center); +} + +// Returns a clip space offset that corresponds to a given number of screen pixels +vec2 project_pixel_size_to_clipspace(vec2 pixels) { + vec2 offset = pixels / project.viewportSize * project.devicePixelRatio * 2.0; + return offset * project.focalDistance; +} + +float project_size_to_pixel(float meters) { + return project_size(meters) * project.scale; +} +vec2 project_size_to_pixel(vec2 meters) { + return project_size(meters) * project.scale; +} +float project_size_to_pixel(float size, int unit) { + if (unit == UNIT_METERS) return project_size_to_pixel(size); + if (unit == UNIT_COMMON) return size * project.scale; + // UNIT_PIXELS + return size; +} +float project_pixel_size(float pixels) { + return pixels / project.scale; +} +vec2 project_pixel_size(vec2 pixels) { + return pixels / project.scale; +} +`; diff --git a/modules/core/src/shaderlib/project/project.wgsl.ts b/modules/core/src/shaderlib/project/project.wgsl.ts index e41c6ad43b0..e4291ded391 100644 --- a/modules/core/src/shaderlib/project/project.wgsl.ts +++ b/modules/core/src/shaderlib/project/project.wgsl.ts @@ -1,323 +1,323 @@ -// deck.gl -// SPDX-License-Identifier: MIT -// Copyright (c) vis.gl contributors - -import {PROJECTION_MODE, UNIT} from '../../lib/constants'; -import {getShaderCoordinateSystem} from './viewport-uniforms'; - -const SHADER_COORDINATE_SYSTEMS = [ - 'default', - 'lnglat', - 'meter-offsets', - 'lnglat-offsets', - 'cartesian' -] as const; - -const COORDINATE_SYSTEM_WGSL_CONSTANTS = SHADER_COORDINATE_SYSTEMS.map( - coordinateSystem => - `const COORDINATE_SYSTEM_${coordinateSystem.toUpperCase().replaceAll('-', '_')}: i32 = ${getShaderCoordinateSystem(coordinateSystem)};` -).join(''); -const PROJECTION_MODE_WGSL_CONSTANTS = Object.keys(PROJECTION_MODE) - .map(key => `const PROJECTION_MODE_${key}: i32 = ${PROJECTION_MODE[key]};`) - .join(''); -const UNIT_WGSL_CONSTANTS = Object.keys(UNIT) - .map(key => `const UNIT_${key.toUpperCase()}: i32 = ${UNIT[key]};`) - .join(''); - -export const projectWGSLHeader = /* wgsl */ `\ -${COORDINATE_SYSTEM_WGSL_CONSTANTS} -${PROJECTION_MODE_WGSL_CONSTANTS} -${UNIT_WGSL_CONSTANTS} - -const TILE_SIZE: f32 = 512.0; -const PI: f32 = 3.1415926536; -const WORLD_SCALE: f32 = TILE_SIZE / (PI * 2.0); -const ZERO_64_LOW: vec3 = vec3(0.0, 0.0, 0.0); -const EARTH_RADIUS: f32 = 6370972.0; // meters -const GLOBE_RADIUS: f32 = 256.0; - -// ----------------------------------------------------------------------------- -// Uniform block (converted from GLSL uniform block) -// ----------------------------------------------------------------------------- -struct ProjectUniforms { - wrapLongitude: i32, - coordinateSystem: i32, - commonUnitsPerMeter: vec3, - projectionMode: i32, - scale: f32, - commonUnitsPerWorldUnit: vec3, - commonUnitsPerWorldUnit2: vec3, - center: vec4, - modelMatrix: mat4x4, - viewProjectionMatrix: mat4x4, - viewportSize: vec2, - devicePixelRatio: f32, - focalDistance: f32, - cameraPosition: vec3, - coordinateOrigin: vec3, - commonOrigin: vec3, - pseudoMeters: i32, -}; - -@group(0) @binding(auto) -var project: ProjectUniforms; - -// ----------------------------------------------------------------------------- -// Geometry data shared across the project helpers. -// The active layer shader is responsible for populating this private module -// state before calling the project functions below. -// ----------------------------------------------------------------------------- - -// Structure to carry additional geometry data used by deck.gl filters. -struct Geometry { - worldPosition: vec3, - worldPositionAlt: vec3, - position: vec4, - normal: vec3, - uv: vec2, - pickingColor: vec3, -}; - -var geometry: Geometry; -`; - -export const projectWGSL = /* wgsl */ `\ -${projectWGSLHeader} - -// ----------------------------------------------------------------------------- -// Functions -// ----------------------------------------------------------------------------- - -// Returns an adjustment factor for commonUnitsPerMeter -fn _project_size_at_latitude(lat: f32) -> f32 { - let y = clamp(lat, -89.9, 89.9); - return 1.0 / cos(radians(y)); -} - -// Overloaded version: scales a value in meters at a given latitude. -fn _project_size_at_latitude_m(meters: f32, lat: f32) -> f32 { - return meters * project.commonUnitsPerMeter.z * _project_size_at_latitude(lat); -} - -// Computes a non-linear scale factor based on geometry. -// (Note: This function relies on "geometry" being provided.) -fn project_size() -> f32 { - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR && - project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT && - project.pseudoMeters == 0) { - if (geometry.position.w == 0.0) { - return _project_size_at_latitude(geometry.worldPosition.y); - } - let y: f32 = geometry.position.y / TILE_SIZE * 2.0 - 1.0; - let y2 = y * y; - let y4 = y2 * y2; - let y6 = y4 * y2; - return 1.0 + 4.9348 * y2 + 4.0587 * y4 + 1.5642 * y6; - } - return 1.0; -} - -// Overloads to scale offsets (meters to world units) -fn project_size_float(meters: f32) -> f32 { - return meters * project.commonUnitsPerMeter.z * project_size(); -} - -fn project_size_vec2(meters: vec2) -> vec2 { - return meters * project.commonUnitsPerMeter.xy * project_size(); -} - -fn project_size_vec3(meters: vec3) -> vec3 { - return meters * project.commonUnitsPerMeter * project_size(); -} - -fn project_size_vec4(meters: vec4) -> vec4 { - return vec4(meters.xyz * project.commonUnitsPerMeter, meters.w); -} - -// Returns a rotation matrix aligning the z‑axis with the given up vector. -fn project_get_orientation_matrix(up: vec3) -> mat3x3 { - let uz = normalize(up); - let ux = select( - vec3(1.0, 0.0, 0.0), - normalize(vec3(uz.y, -uz.x, 0.0)), - abs(uz.z) == 1.0 - ); - let uy = cross(uz, ux); - return mat3x3(ux, uy, uz); -} - -// Since WGSL does not support "out" parameters, we return a struct. -struct RotationResult { - needsRotation: bool, - transform: mat3x3, -}; - -fn project_needs_rotation(commonPosition: vec3) -> RotationResult { - if (project.projectionMode == PROJECTION_MODE_GLOBE) { - return RotationResult(true, project_get_orientation_matrix(commonPosition)); - } else { - return RotationResult(false, mat3x3()); // identity alternative if needed - }; -} - -// Projects a normal vector from the current coordinate system to world space. -fn project_normal(vector: vec3) -> vec3 { - let normal_modelspace = project.modelMatrix * vec4(vector, 0.0); - var n = normalize(normal_modelspace.xyz * project.commonUnitsPerMeter); - let rotResult = project_needs_rotation(geometry.position.xyz); - if (rotResult.needsRotation) { - n = rotResult.transform * n; - } - return n; -} - -// Applies a scale offset based on y-offset (dy) -fn project_offset_(offset: vec4) -> vec4 { - let dy: f32 = offset.y; - let commonUnitsPerWorldUnit = project.commonUnitsPerWorldUnit + project.commonUnitsPerWorldUnit2 * dy; - return vec4(offset.xyz * commonUnitsPerWorldUnit, offset.w); -} - -// Projects lng/lat coordinates to a unit tile [0,1] -fn project_mercator_(lnglat: vec2) -> vec2 { - var x = lnglat.x; - if (project.wrapLongitude != 0) { - x = ((x + 180.0) % 360.0) - 180.0; - } - let y = clamp(lnglat.y, -89.9, 89.9); - return vec2( - radians(x) + PI, - PI + log(tan(PI * 0.25 + radians(y) * 0.5)) - ) * WORLD_SCALE; -} - -// Projects lng/lat/z coordinates for a globe projection. -fn project_globe_(lnglatz: vec3) -> vec3 { - let lambda = radians(lnglatz.x); - let phi = radians(lnglatz.y); - let cosPhi = cos(phi); - let D = (lnglatz.z / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; - return vec3( - sin(lambda) * cosPhi, - -cos(lambda) * cosPhi, - sin(phi) - ) * D; -} - -// Projects positions (with an optional 64-bit low part) from the input -// coordinate system to the common space. -fn project_position_vec4_f64(position: vec4, position64Low: vec3) -> vec4 { - var position_world = project.modelMatrix * position; - - // Work around for a Mac+NVIDIA bug: - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - return vec4( - project_mercator_(position_world.xy), - _project_size_at_latitude_m(position_world.z, position_world.y), - position_world.w - ); - } - if (project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN) { - position_world = vec4f(position_world.xyz + project.coordinateOrigin, position_world.w); - } - } - if (project.projectionMode == PROJECTION_MODE_GLOBE) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - return vec4( - project_globe_(position_world.xyz), - position_world.w - ); - } - if (project.coordinateSystem == COORDINATE_SYSTEM_METER_OFFSETS) { - // position_world is meters in the ENU tangent frame at coordinateOrigin. - // commonOrigin (precomputed CPU-side) is the globe-space position of - // that origin; project_get_orientation_matrix derives the ENU frame from - // that direction so the tangent plane attaches to the sphere and tiles - // land at their geographic location oriented with the surface. - let enuMatrix = project_get_orientation_matrix(project.commonOrigin); - let metersToCommon = GLOBE_RADIUS / EARTH_RADIUS; - let offsetCommon = (enuMatrix * vec3(-position_world.x, -position_world.y, position_world.z)) * metersToCommon; - return vec4(project.commonOrigin + offsetCommon, position_world.w); - } - } - if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET) { - if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { - if (abs(position_world.y - project.coordinateOrigin.y) > 0.25) { - return vec4( - project_mercator_(position_world.xy) - project.commonOrigin.xy, - project_size_float(position_world.z), - position_world.w - ); - } - } - } - if (project.projectionMode == PROJECTION_MODE_IDENTITY || - (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET && - (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT || - project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN))) { - position_world = vec4f(position_world.xyz - project.coordinateOrigin, position_world.w); - } - - return project_offset_(position_world) + - project_offset_(project.modelMatrix * vec4(position64Low, 0.0)); -} - -// Overloaded versions for different input types. -fn project_position_vec4_f32(position: vec4) -> vec4 { - return project_position_vec4_f64(position, ZERO_64_LOW); -} - -fn project_position_vec3_f64(position: vec3, position64Low: vec3) -> vec3 { - let projected_position = project_position_vec4_f64(vec4(position, 1.0), position64Low); - return projected_position.xyz; -} - -fn project_position_vec3_f32(position: vec3) -> vec3 { - let projected_position = project_position_vec4_f64(vec4(position, 1.0), ZERO_64_LOW); - return projected_position.xyz; -} - -fn project_position_vec2_f32(position: vec2) -> vec2 { - let projected_position = project_position_vec4_f64(vec4(position, 0.0, 1.0), ZERO_64_LOW); - return projected_position.xy; -} - -// Transforms a common space position to clip space. -fn project_common_position_to_clipspace_with_projection(position: vec4, viewProjectionMatrix: mat4x4, center: vec4) -> vec4 { - return viewProjectionMatrix * position + center; -} - -// Uses the project viewProjectionMatrix and center. -fn project_common_position_to_clipspace(position: vec4) -> vec4 { - return project_common_position_to_clipspace_with_projection(position, project.viewProjectionMatrix, project.center); -} - -// Returns a clip space offset corresponding to a given number of screen pixels. -fn project_pixel_size_to_clipspace(pixels: vec2) -> vec2 { - let offset = pixels / project.viewportSize * project.devicePixelRatio * 2.0; - return offset * project.focalDistance; -} - -fn project_meter_size_to_pixel(meters: f32) -> f32 { - return project_size_float(meters) * project.scale; -} - -fn project_unit_size_to_pixel(size: f32, unit: i32) -> f32 { - if (unit == UNIT_METERS) { - return project_meter_size_to_pixel(size); - } else if (unit == UNIT_COMMON) { - return size * project.scale; - } - // UNIT_PIXELS: no scaling applied. - return size; -} - -fn project_pixel_size_float(pixels: f32) -> f32 { - return pixels / project.scale; -} - -fn project_pixel_size_vec2(pixels: vec2) -> vec2 { - return pixels / project.scale; -} -`; +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +import {PROJECTION_MODE, UNIT} from '../../lib/constants'; +import {getShaderCoordinateSystem} from './viewport-uniforms'; + +const SHADER_COORDINATE_SYSTEMS = [ + 'default', + 'lnglat', + 'meter-offsets', + 'lnglat-offsets', + 'cartesian' +] as const; + +const COORDINATE_SYSTEM_WGSL_CONSTANTS = SHADER_COORDINATE_SYSTEMS.map( + coordinateSystem => + `const COORDINATE_SYSTEM_${coordinateSystem.toUpperCase().replaceAll('-', '_')}: i32 = ${getShaderCoordinateSystem(coordinateSystem)};` +).join(''); +const PROJECTION_MODE_WGSL_CONSTANTS = Object.keys(PROJECTION_MODE) + .map(key => `const PROJECTION_MODE_${key}: i32 = ${PROJECTION_MODE[key]};`) + .join(''); +const UNIT_WGSL_CONSTANTS = Object.keys(UNIT) + .map(key => `const UNIT_${key.toUpperCase()}: i32 = ${UNIT[key]};`) + .join(''); + +export const projectWGSLHeader = /* wgsl */ `\ +${COORDINATE_SYSTEM_WGSL_CONSTANTS} +${PROJECTION_MODE_WGSL_CONSTANTS} +${UNIT_WGSL_CONSTANTS} + +const TILE_SIZE: f32 = 512.0; +const PI: f32 = 3.1415926536; +const WORLD_SCALE: f32 = TILE_SIZE / (PI * 2.0); +const ZERO_64_LOW: vec3 = vec3(0.0, 0.0, 0.0); +const EARTH_RADIUS: f32 = 6370972.0; // meters +const GLOBE_RADIUS: f32 = 256.0; + +// ----------------------------------------------------------------------------- +// Uniform block (converted from GLSL uniform block) +// ----------------------------------------------------------------------------- +struct ProjectUniforms { + wrapLongitude: i32, + coordinateSystem: i32, + commonUnitsPerMeter: vec3, + projectionMode: i32, + scale: f32, + commonUnitsPerWorldUnit: vec3, + commonUnitsPerWorldUnit2: vec3, + center: vec4, + modelMatrix: mat4x4, + viewProjectionMatrix: mat4x4, + viewportSize: vec2, + devicePixelRatio: f32, + focalDistance: f32, + cameraPosition: vec3, + coordinateOrigin: vec3, + commonOrigin: vec3, + pseudoMeters: i32, +}; + +@group(0) @binding(auto) +var project: ProjectUniforms; + +// ----------------------------------------------------------------------------- +// Geometry data shared across the project helpers. +// The active layer shader is responsible for populating this private module +// state before calling the project functions below. +// ----------------------------------------------------------------------------- + +// Structure to carry additional geometry data used by deck.gl filters. +struct Geometry { + worldPosition: vec3, + worldPositionAlt: vec3, + position: vec4, + normal: vec3, + uv: vec2, + pickingColor: vec3, +}; + +var geometry: Geometry; +`; + +export const projectWGSL = /* wgsl */ `\ +${projectWGSLHeader} + +// ----------------------------------------------------------------------------- +// Functions +// ----------------------------------------------------------------------------- + +// Returns an adjustment factor for commonUnitsPerMeter +fn _project_size_at_latitude(lat: f32) -> f32 { + let y = clamp(lat, -89.9, 89.9); + return 1.0 / cos(radians(y)); +} + +// Overloaded version: scales a value in meters at a given latitude. +fn _project_size_at_latitude_m(meters: f32, lat: f32) -> f32 { + return meters * project.commonUnitsPerMeter.z * _project_size_at_latitude(lat); +} + +// Computes a non-linear scale factor based on geometry. +// (Note: This function relies on "geometry" being provided.) +fn project_size() -> f32 { + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR && + project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT && + project.pseudoMeters == 0) { + if (geometry.position.w == 0.0) { + return _project_size_at_latitude(geometry.worldPosition.y); + } + let y: f32 = geometry.position.y / TILE_SIZE * 2.0 - 1.0; + let y2 = y * y; + let y4 = y2 * y2; + let y6 = y4 * y2; + return 1.0 + 4.9348 * y2 + 4.0587 * y4 + 1.5642 * y6; + } + return 1.0; +} + +// Overloads to scale offsets (meters to world units) +fn project_size_float(meters: f32) -> f32 { + return meters * project.commonUnitsPerMeter.z * project_size(); +} + +fn project_size_vec2(meters: vec2) -> vec2 { + return meters * project.commonUnitsPerMeter.xy * project_size(); +} + +fn project_size_vec3(meters: vec3) -> vec3 { + return meters * project.commonUnitsPerMeter * project_size(); +} + +fn project_size_vec4(meters: vec4) -> vec4 { + return vec4(meters.xyz * project.commonUnitsPerMeter, meters.w); +} + +// Returns a rotation matrix aligning the z‑axis with the given up vector. +fn project_get_orientation_matrix(up: vec3) -> mat3x3 { + let uz = normalize(up); + let ux = select( + vec3(1.0, 0.0, 0.0), + normalize(vec3(uz.y, -uz.x, 0.0)), + abs(uz.z) == 1.0 + ); + let uy = cross(uz, ux); + return mat3x3(ux, uy, uz); +} + +// Since WGSL does not support "out" parameters, we return a struct. +struct RotationResult { + needsRotation: bool, + transform: mat3x3, +}; + +fn project_needs_rotation(commonPosition: vec3) -> RotationResult { + if (project.projectionMode == PROJECTION_MODE_GLOBE) { + return RotationResult(true, project_get_orientation_matrix(commonPosition)); + } else { + return RotationResult(false, mat3x3()); // identity alternative if needed + }; +} + +// Projects a normal vector from the current coordinate system to world space. +fn project_normal(vector: vec3) -> vec3 { + let normal_modelspace = project.modelMatrix * vec4(vector, 0.0); + var n = normalize(normal_modelspace.xyz * project.commonUnitsPerMeter); + let rotResult = project_needs_rotation(geometry.position.xyz); + if (rotResult.needsRotation) { + n = rotResult.transform * n; + } + return n; +} + +// Applies a scale offset based on y-offset (dy) +fn project_offset_(offset: vec4) -> vec4 { + let dy: f32 = offset.y; + let commonUnitsPerWorldUnit = project.commonUnitsPerWorldUnit + project.commonUnitsPerWorldUnit2 * dy; + return vec4(offset.xyz * commonUnitsPerWorldUnit, offset.w); +} + +// Projects lng/lat coordinates to a unit tile [0,1] +fn project_mercator_(lnglat: vec2) -> vec2 { + var x = lnglat.x; + if (project.wrapLongitude != 0) { + x = ((x + 180.0) % 360.0) - 180.0; + } + let y = clamp(lnglat.y, -89.9, 89.9); + return vec2( + radians(x) + PI, + PI + log(tan(PI * 0.25 + radians(y) * 0.5)) + ) * WORLD_SCALE; +} + +// Projects lng/lat/z coordinates for a globe projection. +fn project_globe_(lnglatz: vec3) -> vec3 { + let lambda = radians(lnglatz.x); + let phi = radians(lnglatz.y); + let cosPhi = cos(phi); + let D = (lnglatz.z / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; + return vec3( + sin(lambda) * cosPhi, + -cos(lambda) * cosPhi, + sin(phi) + ) * D; +} + +// Projects positions (with an optional 64-bit low part) from the input +// coordinate system to the common space. +fn project_position_vec4_f64(position: vec4, position64Low: vec3) -> vec4 { + var position_world = project.modelMatrix * position; + + // Work around for a Mac+NVIDIA bug: + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + return vec4( + project_mercator_(position_world.xy), + _project_size_at_latitude_m(position_world.z, position_world.y), + position_world.w + ); + } + if (project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN) { + position_world = vec4f(position_world.xyz + project.coordinateOrigin, position_world.w); + } + } + if (project.projectionMode == PROJECTION_MODE_GLOBE) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + return vec4( + project_globe_(position_world.xyz), + position_world.w + ); + } + if (project.coordinateSystem == COORDINATE_SYSTEM_METER_OFFSETS) { + // position_world is meters in the ENU tangent frame at coordinateOrigin. + // commonOrigin (precomputed CPU-side) is the globe-space position of + // that origin; project_get_orientation_matrix derives the ENU frame from + // that direction so the tangent plane attaches to the sphere and tiles + // land at their geographic location oriented with the surface. + let enuMatrix = project_get_orientation_matrix(project.commonOrigin); + let metersToCommon = GLOBE_RADIUS / EARTH_RADIUS; + let offsetCommon = (enuMatrix * vec3(-position_world.x, -position_world.y, position_world.z)) * metersToCommon; + return vec4(project.commonOrigin + offsetCommon, position_world.w); + } + } + if (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET) { + if (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT) { + if (abs(position_world.y - project.coordinateOrigin.y) > 0.25) { + return vec4( + project_mercator_(position_world.xy) - project.commonOrigin.xy, + project_size_float(position_world.z), + position_world.w + ); + } + } + } + if (project.projectionMode == PROJECTION_MODE_IDENTITY || + (project.projectionMode == PROJECTION_MODE_WEB_MERCATOR_AUTO_OFFSET && + (project.coordinateSystem == COORDINATE_SYSTEM_LNGLAT || + project.coordinateSystem == COORDINATE_SYSTEM_CARTESIAN))) { + position_world = vec4f(position_world.xyz - project.coordinateOrigin, position_world.w); + } + + return project_offset_(position_world) + + project_offset_(project.modelMatrix * vec4(position64Low, 0.0)); +} + +// Overloaded versions for different input types. +fn project_position_vec4_f32(position: vec4) -> vec4 { + return project_position_vec4_f64(position, ZERO_64_LOW); +} + +fn project_position_vec3_f64(position: vec3, position64Low: vec3) -> vec3 { + let projected_position = project_position_vec4_f64(vec4(position, 1.0), position64Low); + return projected_position.xyz; +} + +fn project_position_vec3_f32(position: vec3) -> vec3 { + let projected_position = project_position_vec4_f64(vec4(position, 1.0), ZERO_64_LOW); + return projected_position.xyz; +} + +fn project_position_vec2_f32(position: vec2) -> vec2 { + let projected_position = project_position_vec4_f64(vec4(position, 0.0, 1.0), ZERO_64_LOW); + return projected_position.xy; +} + +// Transforms a common space position to clip space. +fn project_common_position_to_clipspace_with_projection(position: vec4, viewProjectionMatrix: mat4x4, center: vec4) -> vec4 { + return viewProjectionMatrix * position + center; +} + +// Uses the project viewProjectionMatrix and center. +fn project_common_position_to_clipspace(position: vec4) -> vec4 { + return project_common_position_to_clipspace_with_projection(position, project.viewProjectionMatrix, project.center); +} + +// Returns a clip space offset corresponding to a given number of screen pixels. +fn project_pixel_size_to_clipspace(pixels: vec2) -> vec2 { + let offset = pixels / project.viewportSize * project.devicePixelRatio * 2.0; + return offset * project.focalDistance; +} + +fn project_meter_size_to_pixel(meters: f32) -> f32 { + return project_size_float(meters) * project.scale; +} + +fn project_unit_size_to_pixel(size: f32, unit: i32) -> f32 { + if (unit == UNIT_METERS) { + return project_meter_size_to_pixel(size); + } else if (unit == UNIT_COMMON) { + return size * project.scale; + } + // UNIT_PIXELS: no scaling applied. + return size; +} + +fn project_pixel_size_float(pixels: f32) -> f32 { + return pixels / project.scale; +} + +fn project_pixel_size_vec2(pixels: vec2) -> vec2 { + return pixels / project.scale; +} +`; diff --git a/modules/core/src/shaderlib/project/viewport-uniforms.ts b/modules/core/src/shaderlib/project/viewport-uniforms.ts index 55ba3a838b0..34a94ad6666 100644 --- a/modules/core/src/shaderlib/project/viewport-uniforms.ts +++ b/modules/core/src/shaderlib/project/viewport-uniforms.ts @@ -1,383 +1,380 @@ -// deck.gl -// SPDX-License-Identifier: MIT -// Copyright (c) vis.gl contributors - -/* eslint-disable complexity, camelcase */ - -import {mat4, Matrix4Like, vec4} from '@math.gl/core'; - -import {PROJECTION_MODE} from '../../lib/constants'; - -import memoize from '../../utils/memoize'; - -import type Viewport from '../../viewports/viewport'; -import type {CoordinateSystem} from '../../lib/constants'; - -type Vec3 = [number, number, number]; -type Vec4 = [number, number, number, number]; - -// To quickly set a vector to zero -const ZERO_VECTOR: Vec4 = [0, 0, 0, 0]; -// 4x4 matrix that drops 4th component of vector -const VECTOR_TO_POINT_MATRIX: Matrix4Like = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]; -const IDENTITY_MATRIX: Matrix4Like = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; -const DEFAULT_PIXELS_PER_UNIT2: Vec3 = [0, 0, 0]; -const DEFAULT_COORDINATE_ORIGIN: Vec3 = [0, 0, 0]; - -/** Coordinate system constants */ -const COORDINATE_SYSTEM_NUMBERS = { - default: -1, - cartesian: 0, - lnglat: 1, - 'meter-offsets': 2, - 'lnglat-offsets': 3 -} as const satisfies Record; - -export function getShaderCoordinateSystem(coordinateSystem: CoordinateSystem) { - const shaderCoordinateSystem = COORDINATE_SYSTEM_NUMBERS[coordinateSystem]; - if (shaderCoordinateSystem === undefined) { - throw new Error(`Invalid coordinateSystem: ${coordinateSystem}`); - } - return shaderCoordinateSystem; -} - -const getMemoizedViewportUniforms = memoize(calculateViewportUniforms); - -export function getOffsetOrigin( - viewport: Viewport, - coordinateSystem: CoordinateSystem, - coordinateOrigin: Vec3 = DEFAULT_COORDINATE_ORIGIN -): { - geospatialOrigin: Vec3 | null; - shaderCoordinateOrigin: Vec3; - offsetMode: boolean; -} { - if (coordinateOrigin.length < 3) { - coordinateOrigin = [coordinateOrigin[0], coordinateOrigin[1], 0]; - } - - let shaderCoordinateOrigin = coordinateOrigin; - let geospatialOrigin: Vec3 | null; - let offsetMode = true; - - if (coordinateSystem === 'lnglat-offsets' || coordinateSystem === 'meter-offsets') { - geospatialOrigin = coordinateOrigin; - } else { - geospatialOrigin = viewport.isGeospatial - ? // @ts-expect-error longitude and latitude are not defined on the base Viewport, but is expected on geospatial viewports - [Math.fround(viewport.longitude), Math.fround(viewport.latitude), 0] - : null; - } - - switch (viewport.projectionMode) { - case PROJECTION_MODE.WEB_MERCATOR: - if (coordinateSystem === 'lnglat' || coordinateSystem === 'cartesian') { - geospatialOrigin = [0, 0, 0]; - offsetMode = false; - } - break; - - case PROJECTION_MODE.WEB_MERCATOR_AUTO_OFFSET: - if (coordinateSystem === 'lnglat') { - // viewport center in world space - // @ts-expect-error when using LNGLAT coordinates, we expect the viewport to be geospatial, in which case geospatialOrigin is defined - shaderCoordinateOrigin = geospatialOrigin; - } else if (coordinateSystem === 'cartesian') { - // viewport center in common space - shaderCoordinateOrigin = [ - Math.fround(viewport.center[0]), - Math.fround(viewport.center[1]), - 0 - ]; - // Geospatial origin (wgs84) must match shaderCoordinateOrigin (common) - geospatialOrigin = viewport.unprojectPosition(shaderCoordinateOrigin); - shaderCoordinateOrigin[0] -= coordinateOrigin[0]; - shaderCoordinateOrigin[1] -= coordinateOrigin[1]; - shaderCoordinateOrigin[2] -= coordinateOrigin[2]; - } - break; - - case PROJECTION_MODE.IDENTITY: - shaderCoordinateOrigin = viewport.position.map(Math.fround) as Vec3; - shaderCoordinateOrigin[2] = shaderCoordinateOrigin[2] || 0; - break; - - case PROJECTION_MODE.GLOBE: - offsetMode = false; - geospatialOrigin = null; - break; - - default: - // Unknown projection mode - offsetMode = false; - } - - return {geospatialOrigin, shaderCoordinateOrigin, offsetMode}; -} - -// The code that utilizes Matrix4 does the same calculation as their mat4 counterparts, -// has lower performance but provides error checking. -function calculateMatrixAndOffset( - viewport: Viewport, - coordinateSystem: CoordinateSystem, - coordinateOrigin: Vec3 -): { - viewMatrix: Matrix4Like; - viewProjectionMatrix: Matrix4Like; - projectionCenter: Vec4; - originCommon: Vec4; - cameraPosCommon: Vec3; - shaderCoordinateOrigin: Vec3; - geospatialOrigin: Vec3 | null; -} { - const {viewMatrixUncentered, projectionMatrix} = viewport; - let {viewMatrix, viewProjectionMatrix} = viewport; - - let projectionCenter = ZERO_VECTOR; - let originCommon: Vec4 = ZERO_VECTOR; - let cameraPosCommon: Vec3 = viewport.cameraPosition as Vec3; - const {geospatialOrigin, shaderCoordinateOrigin, offsetMode} = getOffsetOrigin( - viewport, - coordinateSystem, - coordinateOrigin - ); - - if (offsetMode) { - // Calculate transformed projectionCenter (using 64 bit precision JS) - // This is the key to offset mode precision - // (avoids doing this addition in 32 bit precision in GLSL) - // @ts-expect-error the 4th component is assigned below - originCommon = viewport.projectPosition(geospatialOrigin || shaderCoordinateOrigin); - - cameraPosCommon = [ - cameraPosCommon[0] - originCommon[0], - cameraPosCommon[1] - originCommon[1], - cameraPosCommon[2] - originCommon[2] - ]; - - originCommon[3] = 1; - - // projectionCenter = new Matrix4(viewProjectionMatrix) - // .transformVector([positionPixels[0], positionPixels[1], 0.0, 1.0]); - projectionCenter = vec4.transformMat4([], originCommon, viewProjectionMatrix); - - // Always apply uncentered projection matrix if available (shader adds center) - viewMatrix = viewMatrixUncentered || viewMatrix; - - // Zero out 4th coordinate ("after" model matrix) - avoids further translations - // viewMatrix = new Matrix4(viewMatrixUncentered || viewMatrix) - // .multiplyRight(VECTOR_TO_POINT_MATRIX); - viewProjectionMatrix = mat4.multiply([], projectionMatrix, viewMatrix); - viewProjectionMatrix = mat4.multiply([], viewProjectionMatrix, VECTOR_TO_POINT_MATRIX); - } - - return { - viewMatrix: viewMatrix as Matrix4Like, - viewProjectionMatrix: viewProjectionMatrix as Matrix4Like, - projectionCenter, - originCommon, - cameraPosCommon, - shaderCoordinateOrigin, - geospatialOrigin - }; -} - -export type ProjectUniforms = { - coordinateSystem: number; - projectionMode: number; - coordinateOrigin: Vec3; - commonOrigin: Vec3; - center: Vec4; - // Backward compatibility - // TODO: remove in v9 - pseudoMeters: boolean; - - // Screen size - viewportSize: [number, number]; - devicePixelRatio: number; - - focalDistance: number; - commonUnitsPerMeter: Vec3; - commonUnitsPerWorldUnit: Vec3; - commonUnitsPerWorldUnit2: Vec3; - /** 2^zoom */ - scale: number; - wrapLongitude: boolean; - - viewProjectionMatrix: Matrix4Like; - modelMatrix: Matrix4Like; - - // This is for lighting calculations - cameraPosition: Vec3; -}; - -export type ProjectProps = { - viewport: Viewport; - devicePixelRatio?: number; - modelMatrix?: Matrix4Like | null; - coordinateSystem?: CoordinateSystem; - coordinateOrigin?: Vec3; - autoWrapLongitude?: boolean; -}; - -/** - * Returns uniforms for shaders based on current projection - * includes: projection matrix suitable for shaders - * - * TODO - Ensure this works with any viewport, not just WebMercatorViewports - * - * @param {WebMercatorViewport} viewport - - * @return {Float32Array} - 4x4 projection matrix that can be used in shaders - */ -export function getUniformsFromViewport({ - viewport, - devicePixelRatio = 1, - modelMatrix = null, - // Match Layer.defaultProps - coordinateSystem = 'default', - coordinateOrigin = DEFAULT_COORDINATE_ORIGIN, - autoWrapLongitude = false -}: ProjectProps): ProjectUniforms { - if (coordinateSystem === 'default') { - coordinateSystem = viewport.isGeospatial ? 'lnglat' : 'cartesian'; - } - - const uniforms = getMemoizedViewportUniforms({ - viewport, - devicePixelRatio, - coordinateSystem, - coordinateOrigin - }); - - uniforms.wrapLongitude = autoWrapLongitude; - uniforms.modelMatrix = modelMatrix || IDENTITY_MATRIX; - - return uniforms; -} - -function calculateViewportUniforms({ - viewport, - devicePixelRatio, - coordinateSystem, - coordinateOrigin -}: { - viewport: Viewport; - devicePixelRatio: number; - coordinateSystem: CoordinateSystem; - coordinateOrigin: Vec3; -}): ProjectUniforms { - const { - projectionCenter, - viewProjectionMatrix, - originCommon, - cameraPosCommon, - shaderCoordinateOrigin, - geospatialOrigin - } = calculateMatrixAndOffset(viewport, coordinateSystem, coordinateOrigin); - - // Calculate projection pixels per unit - const distanceScales = viewport.getDistanceScales(); - - const viewportSize: [number, number] = [ - viewport.width * devicePixelRatio, - viewport.height * devicePixelRatio - ]; - - // Distance at which screen pixels are projected. - // Used to scale sizes in clipspace to match screen pixels. - // When using Viewport class's default projection matrix, this yields 1 for orthographic - // and `viewport.focalDistance` for perspective views - const focalDistance = - vec4.transformMat4([], [0, 0, -viewport.focalDistance, 1], viewport.projectionMatrix)[3] || 1; - - const uniforms: ProjectUniforms = { - // Projection mode values - coordinateSystem: getShaderCoordinateSystem(coordinateSystem), - projectionMode: viewport.projectionMode, - coordinateOrigin: shaderCoordinateOrigin, - commonOrigin: originCommon.slice(0, 3) as Vec3, - center: projectionCenter, - - // Backward compatibility - // TODO: remove in v9 - // @ts-expect-error _pseudoMeters is only defined on WebMercator viewport - pseudoMeters: Boolean(viewport._pseudoMeters), - - // Screen size - viewportSize, - devicePixelRatio, - - focalDistance, - commonUnitsPerMeter: distanceScales.unitsPerMeter as Vec3, - commonUnitsPerWorldUnit: distanceScales.unitsPerMeter as Vec3, - commonUnitsPerWorldUnit2: DEFAULT_PIXELS_PER_UNIT2, - scale: viewport.scale, // This is the mercator scale (2 ** zoom) - wrapLongitude: false, - - viewProjectionMatrix, - modelMatrix: IDENTITY_MATRIX, - - // This is for lighting calculations - cameraPosition: cameraPosCommon - }; - - if (geospatialOrigin) { - // Get high-precision DistanceScales from geospatial viewport - // TODO: stricter types in Viewport classes - const distanceScalesAtOrigin = viewport.getDistanceScales(geospatialOrigin) as { - unitsPerMeter: Vec3; - metersPerUnit: Vec3; - unitsPerMeter2: Vec3; - unitsPerDegree: Vec3; - degreesPerUnit: Vec3; - unitsPerDegree2: Vec3; - }; - switch (coordinateSystem) { - case 'meter-offsets': - uniforms.commonUnitsPerWorldUnit = distanceScalesAtOrigin.unitsPerMeter; - uniforms.commonUnitsPerWorldUnit2 = distanceScalesAtOrigin.unitsPerMeter2; - break; - - case 'lnglat': - case 'lnglat-offsets': - // @ts-expect-error _pseudoMeters only exists on WebMercatorView - if (!viewport._pseudoMeters) { - uniforms.commonUnitsPerMeter = distanceScalesAtOrigin.unitsPerMeter; - } - uniforms.commonUnitsPerWorldUnit = distanceScalesAtOrigin.unitsPerDegree; - uniforms.commonUnitsPerWorldUnit2 = distanceScalesAtOrigin.unitsPerDegree2; - break; - - // a.k.a "preprojected" positions - case 'cartesian': - uniforms.commonUnitsPerWorldUnit = [1, 1, distanceScalesAtOrigin.unitsPerMeter[2]]; - uniforms.commonUnitsPerWorldUnit2 = [0, 0, distanceScalesAtOrigin.unitsPerMeter2[2]]; - break; - - default: - break; - } - } - - // For GLOBE + METER_OFFSETS, precompute the globe-space position of - // coordinateOrigin into commonOrigin. The shader derives the ENU frame from - // this direction via project_get_orientation_matrix, avoiding per-vertex trig. - if ( - viewport.projectionMode === PROJECTION_MODE.GLOBE && - coordinateSystem === 'meter-offsets' - ) { - const EARTH_RADIUS = 6370972; - const GLOBE_RADIUS = 256; - const lambda = (coordinateOrigin[0] * Math.PI) / 180; - const phi = (coordinateOrigin[1] * Math.PI) / 180; - const cosPhi = Math.cos(phi); - const D = ((coordinateOrigin[2] || 0) / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; - uniforms.commonOrigin = [ - Math.sin(lambda) * cosPhi * D, - -Math.cos(lambda) * cosPhi * D, - Math.sin(phi) * D - ]; - } - - return uniforms; -} +// deck.gl +// SPDX-License-Identifier: MIT +// Copyright (c) vis.gl contributors + +/* eslint-disable complexity, camelcase */ + +import {mat4, Matrix4Like, vec4} from '@math.gl/core'; + +import {PROJECTION_MODE} from '../../lib/constants'; + +import memoize from '../../utils/memoize'; + +import type Viewport from '../../viewports/viewport'; +import type {CoordinateSystem} from '../../lib/constants'; + +type Vec3 = [number, number, number]; +type Vec4 = [number, number, number, number]; + +// To quickly set a vector to zero +const ZERO_VECTOR: Vec4 = [0, 0, 0, 0]; +// 4x4 matrix that drops 4th component of vector +const VECTOR_TO_POINT_MATRIX: Matrix4Like = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0]; +const IDENTITY_MATRIX: Matrix4Like = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; +const DEFAULT_PIXELS_PER_UNIT2: Vec3 = [0, 0, 0]; +const DEFAULT_COORDINATE_ORIGIN: Vec3 = [0, 0, 0]; + +/** Coordinate system constants */ +const COORDINATE_SYSTEM_NUMBERS = { + default: -1, + cartesian: 0, + lnglat: 1, + 'meter-offsets': 2, + 'lnglat-offsets': 3 +} as const satisfies Record; + +export function getShaderCoordinateSystem(coordinateSystem: CoordinateSystem) { + const shaderCoordinateSystem = COORDINATE_SYSTEM_NUMBERS[coordinateSystem]; + if (shaderCoordinateSystem === undefined) { + throw new Error(`Invalid coordinateSystem: ${coordinateSystem}`); + } + return shaderCoordinateSystem; +} + +const getMemoizedViewportUniforms = memoize(calculateViewportUniforms); + +export function getOffsetOrigin( + viewport: Viewport, + coordinateSystem: CoordinateSystem, + coordinateOrigin: Vec3 = DEFAULT_COORDINATE_ORIGIN +): { + geospatialOrigin: Vec3 | null; + shaderCoordinateOrigin: Vec3; + offsetMode: boolean; +} { + if (coordinateOrigin.length < 3) { + coordinateOrigin = [coordinateOrigin[0], coordinateOrigin[1], 0]; + } + + let shaderCoordinateOrigin = coordinateOrigin; + let geospatialOrigin: Vec3 | null; + let offsetMode = true; + + if (coordinateSystem === 'lnglat-offsets' || coordinateSystem === 'meter-offsets') { + geospatialOrigin = coordinateOrigin; + } else { + geospatialOrigin = viewport.isGeospatial + ? // @ts-expect-error longitude and latitude are not defined on the base Viewport, but is expected on geospatial viewports + [Math.fround(viewport.longitude), Math.fround(viewport.latitude), 0] + : null; + } + + switch (viewport.projectionMode) { + case PROJECTION_MODE.WEB_MERCATOR: + if (coordinateSystem === 'lnglat' || coordinateSystem === 'cartesian') { + geospatialOrigin = [0, 0, 0]; + offsetMode = false; + } + break; + + case PROJECTION_MODE.WEB_MERCATOR_AUTO_OFFSET: + if (coordinateSystem === 'lnglat') { + // viewport center in world space + // @ts-expect-error when using LNGLAT coordinates, we expect the viewport to be geospatial, in which case geospatialOrigin is defined + shaderCoordinateOrigin = geospatialOrigin; + } else if (coordinateSystem === 'cartesian') { + // viewport center in common space + shaderCoordinateOrigin = [ + Math.fround(viewport.center[0]), + Math.fround(viewport.center[1]), + 0 + ]; + // Geospatial origin (wgs84) must match shaderCoordinateOrigin (common) + geospatialOrigin = viewport.unprojectPosition(shaderCoordinateOrigin); + shaderCoordinateOrigin[0] -= coordinateOrigin[0]; + shaderCoordinateOrigin[1] -= coordinateOrigin[1]; + shaderCoordinateOrigin[2] -= coordinateOrigin[2]; + } + break; + + case PROJECTION_MODE.IDENTITY: + shaderCoordinateOrigin = viewport.position.map(Math.fround) as Vec3; + shaderCoordinateOrigin[2] = shaderCoordinateOrigin[2] || 0; + break; + + case PROJECTION_MODE.GLOBE: + offsetMode = false; + geospatialOrigin = null; + break; + + default: + // Unknown projection mode + offsetMode = false; + } + + return {geospatialOrigin, shaderCoordinateOrigin, offsetMode}; +} + +// The code that utilizes Matrix4 does the same calculation as their mat4 counterparts, +// has lower performance but provides error checking. +function calculateMatrixAndOffset( + viewport: Viewport, + coordinateSystem: CoordinateSystem, + coordinateOrigin: Vec3 +): { + viewMatrix: Matrix4Like; + viewProjectionMatrix: Matrix4Like; + projectionCenter: Vec4; + originCommon: Vec4; + cameraPosCommon: Vec3; + shaderCoordinateOrigin: Vec3; + geospatialOrigin: Vec3 | null; +} { + const {viewMatrixUncentered, projectionMatrix} = viewport; + let {viewMatrix, viewProjectionMatrix} = viewport; + + let projectionCenter = ZERO_VECTOR; + let originCommon: Vec4 = ZERO_VECTOR; + let cameraPosCommon: Vec3 = viewport.cameraPosition as Vec3; + const {geospatialOrigin, shaderCoordinateOrigin, offsetMode} = getOffsetOrigin( + viewport, + coordinateSystem, + coordinateOrigin + ); + + if (offsetMode) { + // Calculate transformed projectionCenter (using 64 bit precision JS) + // This is the key to offset mode precision + // (avoids doing this addition in 32 bit precision in GLSL) + // @ts-expect-error the 4th component is assigned below + originCommon = viewport.projectPosition(geospatialOrigin || shaderCoordinateOrigin); + + cameraPosCommon = [ + cameraPosCommon[0] - originCommon[0], + cameraPosCommon[1] - originCommon[1], + cameraPosCommon[2] - originCommon[2] + ]; + + originCommon[3] = 1; + + // projectionCenter = new Matrix4(viewProjectionMatrix) + // .transformVector([positionPixels[0], positionPixels[1], 0.0, 1.0]); + projectionCenter = vec4.transformMat4([], originCommon, viewProjectionMatrix); + + // Always apply uncentered projection matrix if available (shader adds center) + viewMatrix = viewMatrixUncentered || viewMatrix; + + // Zero out 4th coordinate ("after" model matrix) - avoids further translations + // viewMatrix = new Matrix4(viewMatrixUncentered || viewMatrix) + // .multiplyRight(VECTOR_TO_POINT_MATRIX); + viewProjectionMatrix = mat4.multiply([], projectionMatrix, viewMatrix); + viewProjectionMatrix = mat4.multiply([], viewProjectionMatrix, VECTOR_TO_POINT_MATRIX); + } + + return { + viewMatrix: viewMatrix as Matrix4Like, + viewProjectionMatrix: viewProjectionMatrix as Matrix4Like, + projectionCenter, + originCommon, + cameraPosCommon, + shaderCoordinateOrigin, + geospatialOrigin + }; +} + +export type ProjectUniforms = { + coordinateSystem: number; + projectionMode: number; + coordinateOrigin: Vec3; + commonOrigin: Vec3; + center: Vec4; + // Backward compatibility + // TODO: remove in v9 + pseudoMeters: boolean; + + // Screen size + viewportSize: [number, number]; + devicePixelRatio: number; + + focalDistance: number; + commonUnitsPerMeter: Vec3; + commonUnitsPerWorldUnit: Vec3; + commonUnitsPerWorldUnit2: Vec3; + /** 2^zoom */ + scale: number; + wrapLongitude: boolean; + + viewProjectionMatrix: Matrix4Like; + modelMatrix: Matrix4Like; + + // This is for lighting calculations + cameraPosition: Vec3; +}; + +export type ProjectProps = { + viewport: Viewport; + devicePixelRatio?: number; + modelMatrix?: Matrix4Like | null; + coordinateSystem?: CoordinateSystem; + coordinateOrigin?: Vec3; + autoWrapLongitude?: boolean; +}; + +/** + * Returns uniforms for shaders based on current projection + * includes: projection matrix suitable for shaders + * + * TODO - Ensure this works with any viewport, not just WebMercatorViewports + * + * @param {WebMercatorViewport} viewport - + * @return {Float32Array} - 4x4 projection matrix that can be used in shaders + */ +export function getUniformsFromViewport({ + viewport, + devicePixelRatio = 1, + modelMatrix = null, + // Match Layer.defaultProps + coordinateSystem = 'default', + coordinateOrigin = DEFAULT_COORDINATE_ORIGIN, + autoWrapLongitude = false +}: ProjectProps): ProjectUniforms { + if (coordinateSystem === 'default') { + coordinateSystem = viewport.isGeospatial ? 'lnglat' : 'cartesian'; + } + + const uniforms = getMemoizedViewportUniforms({ + viewport, + devicePixelRatio, + coordinateSystem, + coordinateOrigin + }); + + uniforms.wrapLongitude = autoWrapLongitude; + uniforms.modelMatrix = modelMatrix || IDENTITY_MATRIX; + + return uniforms; +} + +function calculateViewportUniforms({ + viewport, + devicePixelRatio, + coordinateSystem, + coordinateOrigin +}: { + viewport: Viewport; + devicePixelRatio: number; + coordinateSystem: CoordinateSystem; + coordinateOrigin: Vec3; +}): ProjectUniforms { + const { + projectionCenter, + viewProjectionMatrix, + originCommon, + cameraPosCommon, + shaderCoordinateOrigin, + geospatialOrigin + } = calculateMatrixAndOffset(viewport, coordinateSystem, coordinateOrigin); + + // Calculate projection pixels per unit + const distanceScales = viewport.getDistanceScales(); + + const viewportSize: [number, number] = [ + viewport.width * devicePixelRatio, + viewport.height * devicePixelRatio + ]; + + // Distance at which screen pixels are projected. + // Used to scale sizes in clipspace to match screen pixels. + // When using Viewport class's default projection matrix, this yields 1 for orthographic + // and `viewport.focalDistance` for perspective views + const focalDistance = + vec4.transformMat4([], [0, 0, -viewport.focalDistance, 1], viewport.projectionMatrix)[3] || 1; + + const uniforms: ProjectUniforms = { + // Projection mode values + coordinateSystem: getShaderCoordinateSystem(coordinateSystem), + projectionMode: viewport.projectionMode, + coordinateOrigin: shaderCoordinateOrigin, + commonOrigin: originCommon.slice(0, 3) as Vec3, + center: projectionCenter, + + // Backward compatibility + // TODO: remove in v9 + // @ts-expect-error _pseudoMeters is only defined on WebMercator viewport + pseudoMeters: Boolean(viewport._pseudoMeters), + + // Screen size + viewportSize, + devicePixelRatio, + + focalDistance, + commonUnitsPerMeter: distanceScales.unitsPerMeter as Vec3, + commonUnitsPerWorldUnit: distanceScales.unitsPerMeter as Vec3, + commonUnitsPerWorldUnit2: DEFAULT_PIXELS_PER_UNIT2, + scale: viewport.scale, // This is the mercator scale (2 ** zoom) + wrapLongitude: false, + + viewProjectionMatrix, + modelMatrix: IDENTITY_MATRIX, + + // This is for lighting calculations + cameraPosition: cameraPosCommon + }; + + if (geospatialOrigin) { + // Get high-precision DistanceScales from geospatial viewport + // TODO: stricter types in Viewport classes + const distanceScalesAtOrigin = viewport.getDistanceScales(geospatialOrigin) as { + unitsPerMeter: Vec3; + metersPerUnit: Vec3; + unitsPerMeter2: Vec3; + unitsPerDegree: Vec3; + degreesPerUnit: Vec3; + unitsPerDegree2: Vec3; + }; + switch (coordinateSystem) { + case 'meter-offsets': + uniforms.commonUnitsPerWorldUnit = distanceScalesAtOrigin.unitsPerMeter; + uniforms.commonUnitsPerWorldUnit2 = distanceScalesAtOrigin.unitsPerMeter2; + break; + + case 'lnglat': + case 'lnglat-offsets': + // @ts-expect-error _pseudoMeters only exists on WebMercatorView + if (!viewport._pseudoMeters) { + uniforms.commonUnitsPerMeter = distanceScalesAtOrigin.unitsPerMeter; + } + uniforms.commonUnitsPerWorldUnit = distanceScalesAtOrigin.unitsPerDegree; + uniforms.commonUnitsPerWorldUnit2 = distanceScalesAtOrigin.unitsPerDegree2; + break; + + // a.k.a "preprojected" positions + case 'cartesian': + uniforms.commonUnitsPerWorldUnit = [1, 1, distanceScalesAtOrigin.unitsPerMeter[2]]; + uniforms.commonUnitsPerWorldUnit2 = [0, 0, distanceScalesAtOrigin.unitsPerMeter2[2]]; + break; + + default: + break; + } + } + + // For GLOBE + METER_OFFSETS, precompute the globe-space position of + // coordinateOrigin into commonOrigin. The shader derives the ENU frame from + // this direction via project_get_orientation_matrix, avoiding per-vertex trig. + if (viewport.projectionMode === PROJECTION_MODE.GLOBE && coordinateSystem === 'meter-offsets') { + const EARTH_RADIUS = 6370972; + const GLOBE_RADIUS = 256; + const lambda = (coordinateOrigin[0] * Math.PI) / 180; + const phi = (coordinateOrigin[1] * Math.PI) / 180; + const cosPhi = Math.cos(phi); + const D = ((coordinateOrigin[2] || 0) / EARTH_RADIUS + 1.0) * GLOBE_RADIUS; + uniforms.commonOrigin = [ + Math.sin(lambda) * cosPhi * D, + -Math.cos(lambda) * cosPhi * D, + Math.sin(phi) * D + ]; + } + + return uniforms; +} From 1be2d266b3c938e7b41760c62b901cf2c28eaf58 Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Mon, 27 Apr 2026 21:21:04 -0400 Subject: [PATCH 5/6] fix(core): keep GlobeView on GlobeViewport --- modules/core/src/views/globe-view.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/modules/core/src/views/globe-view.ts b/modules/core/src/views/globe-view.ts index 148d1eb2f1c..ed810b981e4 100644 --- a/modules/core/src/views/globe-view.ts +++ b/modules/core/src/views/globe-view.ts @@ -4,7 +4,6 @@ import View, {CommonViewState, CommonViewProps} from './view'; import GlobeViewport from '../viewports/globe-viewport'; -import WebMercatorViewport from '../viewports/web-mercator-viewport'; import GlobeController from '../controllers/globe-controller'; export type GlobeViewState = { @@ -42,8 +41,8 @@ export default class GlobeView extends View { super(props); } - getViewportType(viewState: GlobeViewState) { - return viewState.zoom > 12 ? WebMercatorViewport : GlobeViewport; + getViewportType() { + return GlobeViewport; } get ControllerType() { From db043abfb16428296688ac3afa86bdf3317be9be Mon Sep 17 00:00:00 2001 From: charlieforward9 Date: Tue, 12 May 2026 12:03:16 -0400 Subject: [PATCH 6/6] chore: address tile globe review feedback --- modules/core/src/shaderlib/project/project.glsl.ts | 5 ----- modules/core/src/shaderlib/project/project.wgsl.ts | 5 ----- modules/core/src/views/globe-view.ts | 5 +++-- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/modules/core/src/shaderlib/project/project.glsl.ts b/modules/core/src/shaderlib/project/project.glsl.ts index cc13f0eb4a4..93139446cd0 100644 --- a/modules/core/src/shaderlib/project/project.glsl.ts +++ b/modules/core/src/shaderlib/project/project.glsl.ts @@ -209,11 +209,6 @@ vec4 project_position(vec4 position, vec3 position64Low) { ); } if (project.coordinateSystem == COORDINATE_SYSTEM_METER_OFFSETS) { - // position_world is meters in the ENU tangent frame at coordinateOrigin. - // commonOrigin (precomputed CPU-side) is the globe-space position of - // that origin; project_get_orientation_matrix derives the ENU frame from - // that direction so the tangent plane attaches to the sphere and tiles - // land at their geographic location oriented with the surface. mat3 enuMatrix = project_get_orientation_matrix(project.commonOrigin); float metersToCommon = GLOBE_RADIUS / EARTH_RADIUS; vec3 offsetCommon = (enuMatrix * vec3(-position_world.xy, position_world.z)) * metersToCommon; diff --git a/modules/core/src/shaderlib/project/project.wgsl.ts b/modules/core/src/shaderlib/project/project.wgsl.ts index e4291ded391..db22f291b34 100644 --- a/modules/core/src/shaderlib/project/project.wgsl.ts +++ b/modules/core/src/shaderlib/project/project.wgsl.ts @@ -230,11 +230,6 @@ fn project_position_vec4_f64(position: vec4, position64Low: vec3) -> v ); } if (project.coordinateSystem == COORDINATE_SYSTEM_METER_OFFSETS) { - // position_world is meters in the ENU tangent frame at coordinateOrigin. - // commonOrigin (precomputed CPU-side) is the globe-space position of - // that origin; project_get_orientation_matrix derives the ENU frame from - // that direction so the tangent plane attaches to the sphere and tiles - // land at their geographic location oriented with the surface. let enuMatrix = project_get_orientation_matrix(project.commonOrigin); let metersToCommon = GLOBE_RADIUS / EARTH_RADIUS; let offsetCommon = (enuMatrix * vec3(-position_world.x, -position_world.y, position_world.z)) * metersToCommon; diff --git a/modules/core/src/views/globe-view.ts b/modules/core/src/views/globe-view.ts index fad3bbb9825..b4b4bf08171 100644 --- a/modules/core/src/views/globe-view.ts +++ b/modules/core/src/views/globe-view.ts @@ -4,6 +4,7 @@ import View, {CommonViewState, CommonViewProps} from './view'; import GlobeViewport from '../viewports/globe-viewport'; +import WebMercatorViewport from '../viewports/web-mercator-viewport'; import GlobeController from '../controllers/globe-controller'; import type {Parameters} from '@luma.gl/core'; @@ -52,8 +53,8 @@ export default class GlobeView extends View { }); } - getViewportType() { - return GlobeViewport; + getViewportType(viewState: GlobeViewState) { + return viewState.zoom > 12 ? WebMercatorViewport : GlobeViewport; } get ControllerType() {