diff --git a/CHANGELOG.md b/CHANGELOG.md index e37e34d47c..8a5773f587 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ - _...Add new stuff here..._ ### 🐞 Bug fixes +- Fix globe custom layers being supplied incorrect matrices after projection transition to mercator ([#5150](https://github.com/maplibre/maplibre-gl-js/pull/5150)) +- Fix custom 3D models disappearing during projection transition ([#5150](https://github.com/maplibre/maplibre-gl-js/pull/5150)) - _...Add new stuff here..._ ## 5.0.0-pre.9 diff --git a/src/geo/projection/globe_transform.ts b/src/geo/projection/globe_transform.ts index 5205109699..843fc3c787 100644 --- a/src/geo/projection/globe_transform.ts +++ b/src/geo/projection/globe_transform.ts @@ -1,11 +1,10 @@ -import {type mat2, mat4, type vec3, type vec4} from 'gl-matrix'; +import type {mat2, mat4, vec3, vec4} from 'gl-matrix'; import {TransformHelper} from '../transform_helper'; import {MercatorTransform} from './mercator_transform'; import {VerticalPerspectiveTransform} from './vertical_perspective_transform'; import {type LngLat, type LngLatLike,} from '../lng_lat'; -import {createMat4f32, createMat4f64, lerp, warnOnce} from '../../util/util'; -import {OverscaledTileID, type UnwrappedTileID, type CanonicalTileID} from '../../source/tile_id'; -import {EXTENT} from '../../data/extent'; +import {lerp} from '../../util/util'; +import type {OverscaledTileID, UnwrappedTileID, CanonicalTileID} from '../../source/tile_id'; import type Point from '@mapbox/point-geometry'; import type {MercatorCoordinate} from '../mercator_coordinate'; @@ -265,9 +264,17 @@ export class GlobeTransform implements ITransform { public get cameraPosition(): vec3 { return this.currentTransform.cameraPosition; } - public get nearZ(): number { return this.currentTransform.nearZ; } + // Intentionally return our helper's Z values instead of currentTransform's - they are synced in _calcMatrices. + public get nearZ(): number { return this._helper.nearZ; } + public get farZ(): number { return this._helper.farZ; } + public get autoCalculateNearFarZ(): boolean { return this._helper.autoCalculateNearFarZ; } - public get farZ(): number { return this.currentTransform.farZ; } + overrideNearFarZ(nearZ: number, farZ: number): void { + this._helper.overrideNearFarZ(nearZ, farZ); + } + clearNearFarZOverride(): void { + this._helper.clearNearFarZOverride(); + } getProjectionData(params: ProjectionDataParams): ProjectionData { const mercatorProjectionData = this._mercatorTransform.getProjectionData(params); @@ -312,8 +319,22 @@ export class GlobeTransform implements ITransform { if (!this._helper._width || !this._helper._height) { return; } - this._mercatorTransform.apply(this, true); + // VerticalPerspective reads our near/farZ values and autoCalculateNearFarZ: + // - if autoCalculateNearFarZ is true then it computes globe Z values + // - if autoCalculateNearFarZ is false then it inherits our Z values + // In either case, its Z values are consistent with out settings and we want to copy its Z values to our helper. this._verticalPerspectiveTransform.apply(this, this._globeLatitudeErrorCorrectionRadians); + this._helper._nearZ = this._verticalPerspectiveTransform.nearZ; + this._helper._farZ = this._verticalPerspectiveTransform.farZ; + + // When transitioning between globe and mercator, we need to synchronize the depth values in both transforms. + // For this reason we first update vertical perspective and then sync our Z values to its result. + // Now if globe rendering, we always want to force mercator transform to adapt our Z values. + // If not, it will either compute its own (autoCalculateNearFarZ=false) or adapt our (autoCalculateNearFarZ=true). + // In either case we want to (again) sync our Z values, this time with + this._mercatorTransform.apply(this, true, this.isGlobeRendering); + this._helper._nearZ = this._mercatorTransform.nearZ; + this._helper._farZ = this._mercatorTransform.farZ; } calculateFogMatrix(unwrappedTileID: UnwrappedTileID): mat4 { @@ -419,20 +440,15 @@ export class GlobeTransform implements ITransform { } getProjectionDataForCustomLayer(applyGlobeMatrix: boolean = true): ProjectionData { - const projectionData = this.getProjectionData({overscaledTileID: new OverscaledTileID(0, 0, 0, 0, 0), applyGlobeMatrix}); - projectionData.tileMercatorCoords = [0, 0, 1, 1]; - - // Even though we requested projection data for the mercator base tile which covers the entire mercator range, - // the shader projection machinery still expects inputs to be in tile units range [0..EXTENT]. - // Since custom layers are expected to supply mercator coordinates [0..1], we need to rescale - // the fallback projection matrix by EXTENT. - // Note that the regular projection matrices do not need to be modified, since the rescaling happens by setting - // the `u_projection_tile_mercator_coords` uniform correctly. - const fallbackMatrixScaled = createMat4f32(); - mat4.scale(fallbackMatrixScaled, projectionData.fallbackMatrix, [EXTENT, EXTENT, 1]); - - projectionData.fallbackMatrix = fallbackMatrixScaled; - return projectionData; + const mercatorData = this._mercatorTransform.getProjectionDataForCustomLayer(applyGlobeMatrix); + + if (!this.isGlobeRendering) { + return mercatorData; + } + + const globeData = this._verticalPerspectiveTransform.getProjectionDataForCustomLayer(applyGlobeMatrix); + globeData.fallbackMatrix = mercatorData.mainMatrix; + return globeData; } getFastPathSimpleProjectionMatrix(tileID: OverscaledTileID): mat4 { diff --git a/src/geo/projection/mercator_transform.ts b/src/geo/projection/mercator_transform.ts index b0770f8772..d6ce9d4044 100644 --- a/src/geo/projection/mercator_transform.ts +++ b/src/geo/projection/mercator_transform.ts @@ -193,7 +193,7 @@ export class MercatorTransform implements ITransform { return this._helper.renderWorldCopies; } get cameraToCenterDistance(): number { - return this._helper.cameraToCenterDistance; + return this._helper.cameraToCenterDistance; } setTransitionState(_value: number, _error: number): void { // Do nothing @@ -219,9 +219,6 @@ export class MercatorTransform implements ITransform { private _alignedPosMatrixCache: Map = new Map(); private _fogMatrixCacheF32: Map = new Map(); - private _nearZ; - private _farZ; - private _coveringTilesDetailsProvider; constructor(minZoom?: number, maxZoom?: number, minPitch?: number, maxPitch?: number, renderWorldCopies?: boolean) { @@ -238,16 +235,23 @@ export class MercatorTransform implements ITransform { return clone; } - public apply(that: IReadonlyTransform, constrain?: boolean): void { - this._helper.apply(that, constrain); + public apply(that: IReadonlyTransform, constrain?: boolean, forceOverrideZ?: boolean): void { + this._helper.apply(that, constrain, forceOverrideZ); } public get cameraPosition(): vec3 { return this._cameraPosition; } public get projectionMatrix(): mat4 { return this._projectionMatrix; } public get modelViewProjectionMatrix(): mat4 { return this._viewProjMatrix; } public get inverseProjectionMatrix(): mat4 { return this._invProjMatrix; } - public get nearZ(): number { return this._nearZ; } - public get farZ(): number { return this._farZ; } + public get nearZ(): number { return this._helper.nearZ; } + public get farZ(): number { return this._helper.farZ; } + public get autoCalculateNearFarZ(): boolean { return this._helper.autoCalculateNearFarZ; } + overrideNearFarZ(nearZ: number, farZ: number): void { + this._helper.overrideNearFarZ(nearZ, farZ); + } + clearNearFarZOverride(): void { + this._helper.clearNearFarZOverride(); + } public get mercatorMatrix(): mat4 { return this._mercatorMatrix; } // Not part of ITransform interface @@ -541,46 +545,50 @@ export class MercatorTransform implements ITransform { // Calculate the camera to sea-level distance in pixel in respect of terrain const limitedPitchRadians = degreesToRadians(Math.min(this.pitch, maxMercatorHorizonAngle)); const cameraToSeaLevelDistance = Math.max(this._helper.cameraToCenterDistance / 2, this._helper.cameraToCenterDistance + this._helper._elevation * this._helper._pixelPerMeter / Math.cos(limitedPitchRadians)); - // In case of negative minimum elevation (e.g. the dead see, under the sea maps) use a lower plane for calculation - const minRenderDistanceBelowCameraInMeters = 100; - const minElevation = Math.min(this.elevation, this.minElevationForCurrentTile, this.getCameraAltitude() - minRenderDistanceBelowCameraInMeters); - const cameraToLowestPointDistance = cameraToSeaLevelDistance - minElevation * this._helper._pixelPerMeter / Math.cos(limitedPitchRadians); - const lowestPlane = minElevation < 0 ? cameraToLowestPointDistance : cameraToSeaLevelDistance; - - // Find the distance from the center point [width/2 + offset.x, height/2 + offset.y] to the - // center top point [width/2 + offset.x, 0] in Z units, using the law of sines. - // 1 Z unit is equivalent to 1 horizontal px at the center of the map - // (the distance between[width/2, height/2] and [width/2 + 1, height/2]) - const groundAngle = Math.PI / 2 + this.pitchInRadians; - const zfov = degreesToRadians(this.fov) * (Math.abs(Math.cos(degreesToRadians(this.roll))) * this.height + Math.abs(Math.sin(degreesToRadians(this.roll))) * this.width) / this.height; - const fovAboveCenter = zfov * (0.5 + offset.y / this.height); - const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * lowestPlane / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, 0.01, Math.PI - 0.01)); - - // Find the distance from the center point to the horizon - const horizon = getMercatorHorizon(this); - const horizonAngle = Math.atan(horizon / this._helper.cameraToCenterDistance); - const minFovCenterToHorizonRadians = degreesToRadians(90 - maxMercatorHorizonAngle); - const fovCenterToHorizon = horizonAngle > minFovCenterToHorizonRadians ? 2 * horizonAngle * (0.5 + offset.y / (horizon * 2)) : minFovCenterToHorizonRadians; - const topHalfSurfaceDistanceHorizon = Math.sin(fovCenterToHorizon) * lowestPlane / Math.sin(clamp(Math.PI - groundAngle - fovCenterToHorizon, 0.01, Math.PI - 0.01)); - - // Calculate z distance of the farthest fragment that should be rendered. - // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` - const topHalfMinDistance = Math.min(topHalfSurfaceDistance, topHalfSurfaceDistanceHorizon); - this._farZ = (Math.cos(Math.PI / 2 - limitedPitchRadians) * topHalfMinDistance + lowestPlane) * 1.01; - - // The larger the value of nearZ is - // - the more depth precision is available for features (good) - // - clipping starts appearing sooner when the camera is close to 3d features (bad) - // - // Other values work for mapbox-gl-js but deck.gl was encountering precision issues - // when rendering custom layers. This value was experimentally chosen and - // seems to solve z-fighting issues in deck.gl while not clipping buildings too close to the camera. - this._nearZ = this._helper._height / 50; + + if (this._helper.autoCalculateNearFarZ) { + // In case of negative minimum elevation (e.g. the dead see, under the sea maps) use a lower plane for calculation + const minRenderDistanceBelowCameraInMeters = 100; + const minElevation = Math.min(this.elevation, this.minElevationForCurrentTile, this.getCameraAltitude() - minRenderDistanceBelowCameraInMeters); + const cameraToLowestPointDistance = cameraToSeaLevelDistance - minElevation * this._helper._pixelPerMeter / Math.cos(limitedPitchRadians); + const lowestPlane = minElevation < 0 ? cameraToLowestPointDistance : cameraToSeaLevelDistance; + + // Find the distance from the center point [width/2 + offset.x, height/2 + offset.y] to the + // center top point [width/2 + offset.x, 0] in Z units, using the law of sines. + // 1 Z unit is equivalent to 1 horizontal px at the center of the map + // (the distance between[width/2, height/2] and [width/2 + 1, height/2]) + const groundAngle = Math.PI / 2 + this.pitchInRadians; + const zfov = degreesToRadians(this.fov) * (Math.abs(Math.cos(degreesToRadians(this.roll))) * this.height + Math.abs(Math.sin(degreesToRadians(this.roll))) * this.width) / this.height; + const fovAboveCenter = zfov * (0.5 + offset.y / this.height); + const topHalfSurfaceDistance = Math.sin(fovAboveCenter) * lowestPlane / Math.sin(clamp(Math.PI - groundAngle - fovAboveCenter, 0.01, Math.PI - 0.01)); + + // Find the distance from the center point to the horizon + const horizon = getMercatorHorizon(this); + const horizonAngle = Math.atan(horizon / this._helper.cameraToCenterDistance); + const minFovCenterToHorizonRadians = degreesToRadians(90 - maxMercatorHorizonAngle); + const fovCenterToHorizon = horizonAngle > minFovCenterToHorizonRadians ? 2 * horizonAngle * (0.5 + offset.y / (horizon * 2)) : minFovCenterToHorizonRadians; + const topHalfSurfaceDistanceHorizon = Math.sin(fovCenterToHorizon) * lowestPlane / Math.sin(clamp(Math.PI - groundAngle - fovCenterToHorizon, 0.01, Math.PI - 0.01)); + + // Calculate z distance of the farthest fragment that should be rendered. + // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` + const topHalfMinDistance = Math.min(topHalfSurfaceDistance, topHalfSurfaceDistanceHorizon); + + this._helper._farZ = (Math.cos(Math.PI / 2 - limitedPitchRadians) * topHalfMinDistance + lowestPlane) * 1.01; + + // The larger the value of nearZ is + // - the more depth precision is available for features (good) + // - clipping starts appearing sooner when the camera is close to 3d features (bad) + // + // Other values work for mapbox-gl-js but deck.gl was encountering precision issues + // when rendering custom layers. This value was experimentally chosen and + // seems to solve z-fighting issues in deck.gl while not clipping buildings too close to the camera. + this._helper._nearZ = this._helper._height / 50; + } // matrix for conversion from location to clip space(-1 .. 1) let m: mat4; m = new Float64Array(16) as any; - mat4.perspective(m, this.fovInRadians, this._helper._width / this._helper._height, this._nearZ, this._farZ); + mat4.perspective(m, this.fovInRadians, this._helper._width / this._helper._height, this._helper._nearZ, this._helper._farZ); this._invProjMatrix = new Float64Array(16) as any as mat4; mat4.invert(this._invProjMatrix, m); @@ -622,7 +630,7 @@ export class MercatorTransform implements ITransform { // create a fog matrix, same es proj-matrix but with near clipping-plane in mapcenter // needed to calculate a correct z-value for fog calculation, because projMatrix z value is not this._fogMatrix = new Float64Array(16) as any; - mat4.perspective(this._fogMatrix, this.fovInRadians, this.width / this.height, cameraToSeaLevelDistance, this._farZ); + mat4.perspective(this._fogMatrix, this.fovInRadians, this.width / this.height, cameraToSeaLevelDistance, this._helper._farZ); this._fogMatrix[8] = -offset.x * 2 / this.width; this._fogMatrix[9] = offset.y * 2 / this.height; mat4.scale(this._fogMatrix, this._fogMatrix, [1, -1, 1]); @@ -684,9 +692,8 @@ export class MercatorTransform implements ITransform { } getCameraLngLat(): LngLat { - const cameraToCenterDistancePixels = 0.5 / Math.tan(this.fovInRadians / 2) * this.height; const pixelPerMeter = mercatorZfromAltitude(1, this.center.lat) * this.worldSize; - const cameraToCenterDistanceMeters = cameraToCenterDistancePixels / pixelPerMeter; + const cameraToCenterDistanceMeters = this._helper.cameraToCenterDistance / pixelPerMeter; const camMercator = cameraMercatorCoordinateFromCenterAndRotation(this.center, this.elevation, this.pitch, this.bearing, cameraToCenterDistanceMeters); return camMercator.toLngLat(); } @@ -802,13 +809,11 @@ export class MercatorTransform implements ITransform { const scale: vec3 = [EXTENT, EXTENT, this.worldSize / this._helper.pixelsPerMeter]; // We pass full-precision 64bit float matrices to custom layers to prevent precision loss in case the user wants to do further transformations. - const fallbackMatrixScaled = createMat4f64(); - mat4.scale(fallbackMatrixScaled, tileMatrix, scale); - + // Otherwise we get very visible precision-artifacts and twitching for objects that are bulding-scale. const projectionMatrixScaled = createMat4f64(); mat4.scale(projectionMatrixScaled, tileMatrix, scale); - projectionData.fallbackMatrix = fallbackMatrixScaled; + projectionData.fallbackMatrix = projectionMatrixScaled; projectionData.mainMatrix = projectionMatrixScaled; return projectionData; } diff --git a/src/geo/projection/projection_data.ts b/src/geo/projection/projection_data.ts index 215af751c8..43d6d33e27 100644 --- a/src/geo/projection/projection_data.ts +++ b/src/geo/projection/projection_data.ts @@ -46,6 +46,10 @@ export type ProjectionData = { fallbackMatrix: mat4; } +/** + * Parameters object for the transform's `getProjectionData` function. + * Contains the requested tile ID and more. + */ export type ProjectionDataParams = { /** * The ID of the current tile diff --git a/src/geo/projection/vertical_perspective_transform.ts b/src/geo/projection/vertical_perspective_transform.ts index 47972ceb9c..88517c2e03 100644 --- a/src/geo/projection/vertical_perspective_transform.ts +++ b/src/geo/projection/vertical_perspective_transform.ts @@ -1,14 +1,13 @@ import {type mat2, mat4, vec3, vec4} from 'gl-matrix'; import {TransformHelper} from '../transform_helper'; import {LngLat, type LngLatLike, earthRadius} from '../lng_lat'; -import {angleToRotateBetweenVectors2D, clamp, createIdentityMat4f32, createIdentityMat4f64, createMat4f32, createMat4f64, createVec3f64, createVec4f64, differenceOfAnglesDegrees, distanceOfAnglesRadians, MAX_VALID_LATITUDE, pointPlaneSignedDistance, warnOnce} from '../../util/util'; -import {UnwrappedTileID, OverscaledTileID, type CanonicalTileID} from '../../source/tile_id'; +import {angleToRotateBetweenVectors2D, clamp, createIdentityMat4f32, createIdentityMat4f64, createMat4f64, createVec3f64, createVec4f64, differenceOfAnglesDegrees, distanceOfAnglesRadians, MAX_VALID_LATITUDE, pointPlaneSignedDistance, warnOnce} from '../../util/util'; +import {OverscaledTileID, UnwrappedTileID, type CanonicalTileID} from '../../source/tile_id'; import Point from '@mapbox/point-geometry'; import {MercatorCoordinate} from '../mercator_coordinate'; import {LngLatBounds} from '../lng_lat_bounds'; import {tileCoordinatesToMercatorCoordinates} from './mercator_utils'; import {angularCoordinatesToSurfaceVector, getGlobeRadiusPixels, getZoomAdjustment, mercatorCoordinatesToAngularCoordinatesRadians, projectTileCoordinatesToSphere, sphereSurfacePointToCoordinates} from './globe_utils'; -import {EXTENT} from '../../data/extent'; import {GlobeCoveringTilesDetailsProvider} from './globe_covering_tiles_details_provider'; import {Frustum} from '../../util/primitives/frustum'; @@ -235,9 +234,6 @@ export class VerticalPerspectiveTransform implements ITransform { * Value 0 is mercator, value 1 is globe, anything between is an interpolation between the two projections. */ - private _nearZ: number; - private _farZ: number; - private _coveringTilesDetailsProvider: GlobeCoveringTilesDetailsProvider; public constructor() { @@ -280,8 +276,15 @@ export class VerticalPerspectiveTransform implements ITransform { return this._helper.cameraToCenterDistance; } - public get nearZ(): number { return this._nearZ; } - public get farZ(): number { return this._farZ; } + public get nearZ(): number { return this._helper.nearZ; } + public get farZ(): number { return this._helper.farZ; } + public get autoCalculateNearFarZ(): boolean { return this._helper.autoCalculateNearFarZ; } + overrideNearFarZ(nearZ: number, farZ: number): void { + this._helper.overrideNearFarZ(nearZ, farZ); + } + clearNearFarZOverride(): void { + this._helper.clearNearFarZOverride(); + } getProjectionData(params: ProjectionDataParams): ProjectionData { const {overscaledTileID, applyGlobeMatrix} = params; @@ -439,9 +442,11 @@ export class VerticalPerspectiveTransform implements ITransform { // Construct a completely separate matrix for globe view const globeMatrix = createMat4f64(); const globeMatrixUncorrected = createMat4f64(); - this._nearZ = 0.5; - this._farZ = this.cameraToCenterDistance + globeRadiusPixels * 2.0; // just set the far plane far enough - we will calculate our own z in the vertex shader anyway - mat4.perspective(globeMatrix, this.fovInRadians, this.width / this.height, this._nearZ, this._farZ); + if (this._helper.autoCalculateNearFarZ) { + this._helper._nearZ = 0.5; + this._helper._farZ = this.cameraToCenterDistance + globeRadiusPixels * 2.0; // just set the far plane far enough - we will calculate our own z in the vertex shader anyway + } + mat4.perspective(globeMatrix, this.fovInRadians, this.width / this.height, this._helper._nearZ, this._helper._farZ); // Apply center of perspective offset const offset = this.centerOffset; @@ -968,20 +973,9 @@ export class VerticalPerspectiveTransform implements ITransform { } getProjectionDataForCustomLayer(applyGlobeMatrix: boolean = true): ProjectionData { - const projectionData = this.getProjectionData({overscaledTileID: new OverscaledTileID(0, 0, 0, 0, 0), applyGlobeMatrix}); - projectionData.tileMercatorCoords = [0, 0, 1, 1]; - - // Even though we requested projection data for the mercator base tile which covers the entire mercator range, - // the shader projection machinery still expects inputs to be in tile units range [0..EXTENT]. - // Since custom layers are expected to supply mercator coordinates [0..1], we need to rescale - // the fallback projection matrix by EXTENT. - // Note that the regular projection matrices do not need to be modified, since the rescaling happens by setting - // the `u_projection_tile_mercator_coords` uniform correctly. - const fallbackMatrixScaled = createMat4f32(); - mat4.scale(fallbackMatrixScaled, projectionData.fallbackMatrix, [EXTENT, EXTENT, 1]); - - projectionData.fallbackMatrix = fallbackMatrixScaled; - return projectionData; + const globeData = this.getProjectionData({overscaledTileID: new OverscaledTileID(0, 0, 0, 0, 0), applyGlobeMatrix}); + globeData.tileMercatorCoords = [0, 0, 1, 1]; + return globeData; } getFastPathSimpleProjectionMatrix(_tileID: OverscaledTileID): mat4 { diff --git a/src/geo/transform_helper.ts b/src/geo/transform_helper.ts index 0b81f3cdb6..95ceecf521 100644 --- a/src/geo/transform_helper.ts +++ b/src/geo/transform_helper.ts @@ -118,6 +118,10 @@ export class TransformHelper implements ITransformGetters { _clipSpaceToPixelsMatrix: mat4; _cameraToCenterDistance: number; + _nearZ: number; + _farZ: number; + _autoCalculateNearFarZ: boolean; + constructor(callbacks: TransformHelperCallbacks, minZoom?: number, maxZoom?: number, minPitch?: number, maxPitch?: number, renderWorldCopies?: boolean) { this._callbacks = callbacks; this._tileSize = 512; // constant @@ -145,9 +149,10 @@ export class TransformHelper implements ITransformGetters { this._unmodified = true; this._edgeInsets = new EdgeInsets(); this._minElevationForCurrentTile = 0; + this._autoCalculateNearFarZ = true; } - public apply(thatI: ITransformGetters, constrain?: boolean): void { + public apply(thatI: ITransformGetters, constrain?: boolean, forceOverrideZ?: boolean): void { this._latRange = thatI.latRange; this._lngRange = thatI.lngRange; this._width = thatI.width; @@ -170,6 +175,9 @@ export class TransformHelper implements ITransformGetters { this._maxPitch = thatI.maxPitch; this._renderWorldCopies = thatI.renderWorldCopies; this._cameraToCenterDistance = thatI.cameraToCenterDistance; + this._nearZ = thatI.nearZ; + this._farZ = thatI.farZ; + this._autoCalculateNearFarZ = !forceOverrideZ && thatI.autoCalculateNearFarZ; if (constrain) { this._constrain(); } @@ -379,6 +387,20 @@ export class TransformHelper implements ITransformGetters { get cameraToCenterDistance(): number { return this._cameraToCenterDistance; } + get nearZ(): number { return this._nearZ; } + get farZ(): number { return this._farZ; } + get autoCalculateNearFarZ(): boolean { return this._autoCalculateNearFarZ; } + overrideNearFarZ(nearZ: number, farZ: number): void { + this._autoCalculateNearFarZ = false; + this._nearZ = nearZ; + this._farZ = farZ; + this._calcMatrices(); + } + clearNearFarZOverride(): void { + this._autoCalculateNearFarZ = true; + this._calcMatrices(); + } + /** * Returns if the padding params match * @@ -508,7 +530,8 @@ export class TransformHelper implements ITransformGetters { mat4.translate(m, m, [-1, -1, 0]); mat4.scale(m, m, [2 / this._width, 2 / this._height, 1]); this._pixelsToClipSpaceMatrix = m; - this._cameraToCenterDistance = 0.5 / Math.tan(this._fovInRadians / 2) * this._height; + const halfFov = this.fovInRadians / 2; + this._cameraToCenterDistance = 0.5 / Math.tan(halfFov) * this._height; } this._callbacks.calcMatrices(); } diff --git a/src/geo/transform_interface.ts b/src/geo/transform_interface.ts index 3ff5e8157a..6b990fd1c8 100644 --- a/src/geo/transform_interface.ts +++ b/src/geo/transform_interface.ts @@ -79,6 +79,10 @@ export interface ITransformGetters { * The distance from the camera to the center of the map in pixels space. */ get cameraToCenterDistance(): number; + + get nearZ(): number; + get farZ(): number; + get autoCalculateNearFarZ(): boolean; } /** @@ -144,6 +148,17 @@ interface ITransformMutators { setElevation(elevation: number): void; setMinElevationForCurrentTile(elevation: number): void; setPadding(padding: PaddingOptions): void; + /** + * Sets the overriding values to use for near and far Z instead of what the transform would normally compute. + * If set to undefined, the transform will compute its ideal values. + * Calling this will set `autoCalculateNearFarZ` to false. + */ + overrideNearFarZ(nearZ: number, farZ: number): void; + + /** + * Resets near and far Z plane override. Sets `autoCalculateNearFarZ` to true. + */ + clearNearFarZOverride(): void; /** * Sets the transform's width and height and recomputes internal matrices. @@ -240,9 +255,6 @@ export interface IReadonlyTransform extends ITransformGetters { */ get cameraPosition(): vec3; - get nearZ(): number; - get farZ(): number; - /** * Returns if the padding params match * diff --git a/src/shaders/_projection_globe.vertex.glsl b/src/shaders/_projection_globe.vertex.glsl index 010d3569e1..48ed141d34 100644 --- a/src/shaders/_projection_globe.vertex.glsl +++ b/src/shaders/_projection_globe.vertex.glsl @@ -86,30 +86,38 @@ vec4 interpolateProjection(vec2 posInTile, vec3 spherePos, float elevation) { // Z is overwritten by glDepthRange anyway - use a custom z value to clip geometry on the invisible side of the sphere. globePosition.z = globeComputeClippingZ(elevatedPos) * globePosition.w; - if (u_projection_transition < 0.999) { - vec4 flatPosition = u_projection_fallback_matrix * vec4(posInTile, elevation, 1.0); - // Only interpolate to globe's Z for the last 50% of the animation. - // (globe Z hides anything on the backfacing side of the planet) - const float z_globeness_threshold = 0.2; - vec4 result = globePosition; - result.z = mix(0.0, globePosition.z, clamp((u_projection_transition - z_globeness_threshold) / (1.0 - z_globeness_threshold), 0.0, 1.0)); - result.xyw = mix(flatPosition.xyw, globePosition.xyw, u_projection_transition); - // Gradually hide poles during transition - if ((posInTile.y < -32767.5) || (posInTile.y > 32766.5)) { - result = globePosition; - const float poles_hidden_anim_percentage = 0.02; // Only draw poles in the last 2% of the animation. - result.z = mix(globePosition.z, 100.0, pow(max((1.0 - u_projection_transition) / poles_hidden_anim_percentage, 0.0), 8.0)); - } - return result; + if (u_projection_transition > 0.999) { + // Simple case - no transition, only globe projection + return globePosition; } - return globePosition; + // Blend between globe and mercator projections. + vec4 flatPosition = u_projection_fallback_matrix * vec4(posInTile, elevation, 1.0); + // Only interpolate to globe's Z for the last 50% of the animation. + // (globe Z hides anything on the backfacing side of the planet) + const float z_globeness_threshold = 0.2; + vec4 result = globePosition; + result.z = mix(0.0, globePosition.z, clamp((u_projection_transition - z_globeness_threshold) / (1.0 - z_globeness_threshold), 0.0, 1.0)); + result.xyw = mix(flatPosition.xyw, globePosition.xyw, u_projection_transition); + // Gradually hide poles during transition + if ((posInTile.y < -32767.5) || (posInTile.y > 32766.5)) { + result = globePosition; + const float poles_hidden_anim_percentage = 0.02; // Only draw poles in the last 2% of the animation. + result.z = mix(globePosition.z, 100.0, pow(max((1.0 - u_projection_transition) / poles_hidden_anim_percentage, 0.0), 8.0)); + } + return result; } // Unlike interpolateProjection, this variant of the function preserves the Z value of the final vector. vec4 interpolateProjectionFor3D(vec2 posInTile, vec3 spherePos, float elevation) { vec3 elevatedPos = spherePos * (1.0 + elevation / GLOBE_RADIUS); vec4 globePosition = u_projection_matrix * vec4(elevatedPos, 1.0); + + if (u_projection_transition > 0.999) { + return globePosition; + } + + // Blend between globe and mercator projections. vec4 fallbackPosition = u_projection_fallback_matrix * vec4(posInTile, elevation, 1.0); return mix(fallbackPosition, globePosition, u_projection_transition); } diff --git a/test/build/min.test.ts b/test/build/min.test.ts index 7093a67199..55b6568d45 100644 --- a/test/build/min.test.ts +++ b/test/build/min.test.ts @@ -38,7 +38,7 @@ describe('test min build', () => { const decreaseQuota = 4096; // feel free to update this value after you've checked that it has changed on purpose :-) - const expectedBytes = 907777; + const expectedBytes = 907610; expect(actualBytes).toBeLessThan(expectedBytes + increaseQuota); expect(actualBytes).toBeGreaterThan(expectedBytes - decreaseQuota); diff --git a/test/examples/add-3d-model.html b/test/examples/add-3d-model.html index 727fc7625d..0dc3662817 100644 --- a/test/examples/add-3d-model.html +++ b/test/examples/add-3d-model.html @@ -135,7 +135,7 @@ // It will work regardless of current projection. // Also see the example "globe-3d-model.html". // - // const modelMatrix = map.transform.getMatrixForModel(modelOrigin, modelAltitude); + // const modelMatrix = args.getMatrixForModel(modelOrigin, modelAltitude); // const m = new THREE.Matrix4().fromArray(matrix); // const l = new THREE.Matrix4().fromArray(modelMatrix); diff --git a/test/examples/globe-3d-model.html b/test/examples/globe-3d-model.html index a9139d3c20..ef4b9b7c8f 100644 --- a/test/examples/globe-3d-model.html +++ b/test/examples/globe-3d-model.html @@ -129,7 +129,7 @@ // We can use this API to get the correct model matrix. // It will work regardless of current projection. - // See MapLibre source code, files mercator_transform.ts or globe_transform.ts, function "getCustomLayerArgs". + // See MapLibre source code, file "mercator_transform.ts" or "vertical_perspective_transform.ts". const modelMatrix = map.transform.getMatrixForModel(modelOrigin, modelAltitude); const m = new THREE.Matrix4().fromArray(args.defaultProjectionData.mainMatrix); const l = new THREE.Matrix4().fromArray(modelMatrix).scale( diff --git a/test/examples/globe-custom-tiles.html b/test/examples/globe-custom-tiles.html index 4a0f928143..b3e4702a81 100644 --- a/test/examples/globe-custom-tiles.html +++ b/test/examples/globe-custom-tiles.html @@ -222,7 +222,7 @@ const mesh = { vbo, ibo, - indexCount: meshBuffers.indices.length, + indexCount: meshBuffers.indices.byteLength / 2, }; this.meshMap.set(key, mesh); return mesh; @@ -262,7 +262,7 @@ } }; - const projectionData = map.transform.getProjectionData({overscaledTileID: tileID}); + const projectionData = map.transform.getProjectionData({overscaledTileID: tileID, applyGlobeMatrix: true}); gl.uniform4f( locations['u_projection_clipping_plane'], diff --git a/test/integration/render/tests/projection/globe/custom/tent-3d-globe-zoomed/expected.png b/test/integration/render/tests/projection/globe/custom/tent-3d-globe-zoomed/expected.png new file mode 100644 index 0000000000..34c3885fe3 Binary files /dev/null and b/test/integration/render/tests/projection/globe/custom/tent-3d-globe-zoomed/expected.png differ diff --git a/test/integration/render/tests/projection/globe/custom/tent-3d-globe-zoomed/style.json b/test/integration/render/tests/projection/globe/custom/tent-3d-globe-zoomed/style.json new file mode 100644 index 0000000000..485af7a420 --- /dev/null +++ b/test/integration/render/tests/projection/globe/custom/tent-3d-globe-zoomed/style.json @@ -0,0 +1,83 @@ +{ + "version": 8, + "metadata": { + "test": { + "width": 128, + "height": 128, + "description": "Tests that globe's custom layer works after the transition to mercator at high zooms.", + "operations": [ + [ + "addCustomLayer", + "tent-3d-globe" + ] + ] + } + }, + "sources": { + "geojson": { + "type": "geojson", + "data": { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": { + "property": 30 + }, + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [ + -10, + 0 + ], + [ + -10, + 10 + ], + [ + 10, + 10 + ], + [ + 10, + 0 + ] + ] + ] + } + } + ] + } + } + }, + "center": [ + -1.8, + -3.6 + ], + "pitch": 0, + "zoom": 12.01, + "projection": { + "type": "globe" + }, + "bearing": 0, + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "green" + } + }, + { + "id": "extrusion", + "type": "fill-extrusion", + "source": "geojson", + "paint": { + "fill-extrusion-height": 200000, + "fill-extrusion-color": "blue" + } + } + ] +} \ No newline at end of file