Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Globe custom layer fixes #5150

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
64b3fdd
Simplify custom layer ProjectionData code in mercator transform
kubapelc Nov 28, 2024
0a99eae
Fix globe tiles example
kubapelc Nov 28, 2024
589c701
Fix globe projection shader
kubapelc Dec 3, 2024
ea41f44
Add custom layer 3D model after globe->mercator transition render test
kubapelc Dec 2, 2024
bc20299
Fix custom layer 3D models not rendering when globe transitions to me…
kubapelc Dec 3, 2024
3360e43
Move camera to center distance to helper
HarelM Dec 1, 2024
2f0cea3
Synchronize globe+mercator near and far Z to prevent 3D model disappe…
kubapelc Dec 3, 2024
541599a
Expose getProjectionData and getMatrixForModel for custom layers, don…
kubapelc Dec 3, 2024
4368c65
VerticalPerspectiveTransform should have a valid getProjectionDataFor…
kubapelc Dec 3, 2024
2e0614d
Add changelog entry
kubapelc Dec 3, 2024
3837d33
Fix missing docs
kubapelc Dec 3, 2024
17130ae
Fix docs again
kubapelc Dec 3, 2024
67019af
Merge remote-tracking branch 'upstream/projection-expression' into ku…
kubapelc Dec 11, 2024
19ffe34
Review feedback
kubapelc Dec 11, 2024
3590c9c
Move nearZfarZoverride to transform helper
kubapelc Dec 11, 2024
10f4e15
Update build size
kubapelc Dec 11, 2024
51878c1
Merge remote-tracking branch 'upstream/projection-expression' into ku…
kubapelc Dec 11, 2024
907b31c
Review feedback
kubapelc Dec 11, 2024
23027d8
Change near/far Z override API
kubapelc Dec 11, 2024
f1ea168
Custom layers use internal API
kubapelc Dec 11, 2024
f92f447
Fix globe custom tiles example
kubapelc Dec 11, 2024
6e66417
Update build size
kubapelc Dec 11, 2024
48dee24
Fix typo
kubapelc Dec 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 37 additions & 21 deletions src/geo/projection/globe_transform.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -265,9 +264,17 @@

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);

Check warning on line 273 in src/geo/projection/globe_transform.ts

View check run for this annotation

Codecov / codecov/patch

src/geo/projection/globe_transform.ts#L273

Added line #L273 was not covered by tests
}
clearNearFarZOverride(): void {
this._helper.clearNearFarZOverride();

Check warning on line 276 in src/geo/projection/globe_transform.ts

View check run for this annotation

Codecov / codecov/patch

src/geo/projection/globe_transform.ts#L276

Added line #L276 was not covered by tests
}

getProjectionData(params: ProjectionDataParams): ProjectionData {
const mercatorProjectionData = this._mercatorTransform.getProjectionData(params);
Expand Down Expand Up @@ -312,8 +319,22 @@
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 {
Expand Down Expand Up @@ -419,20 +440,15 @@
}

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 {
Expand Down
107 changes: 56 additions & 51 deletions src/geo/projection/mercator_transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@
return this._helper.renderWorldCopies;
}
get cameraToCenterDistance(): number {
return this._helper.cameraToCenterDistance;
return this._helper.cameraToCenterDistance;
}
setTransitionState(_value: number, _error: number): void {
// Do nothing
Expand All @@ -219,9 +219,6 @@
private _alignedPosMatrixCache: Map<string, {f64: mat4; f32: mat4}> = new Map();
private _fogMatrixCacheF32: Map<string, mat4> = new Map();

private _nearZ;
private _farZ;

private _coveringTilesDetailsProvider;

constructor(minZoom?: number, maxZoom?: number, minPitch?: number, maxPitch?: number, renderWorldCopies?: boolean) {
Expand All @@ -238,16 +235,23 @@
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; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move these methods to be with the other helper methods in the file

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);

Check warning on line 250 in src/geo/projection/mercator_transform.ts

View check run for this annotation

Codecov / codecov/patch

src/geo/projection/mercator_transform.ts#L250

Added line #L250 was not covered by tests
}
clearNearFarZOverride(): void {
this._helper.clearNearFarZOverride();

Check warning on line 253 in src/geo/projection/mercator_transform.ts

View check run for this annotation

Codecov / codecov/patch

src/geo/projection/mercator_transform.ts#L253

Added line #L253 was not covered by tests
}

public get mercatorMatrix(): mat4 { return this._mercatorMatrix; } // Not part of ITransform interface

Expand Down Expand Up @@ -541,46 +545,50 @@
// 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was under the impression other variables are used later on, since this is not the case I would consider moving this to an internal method, it will be a lot more readable.

// 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);

Expand Down Expand Up @@ -622,7 +630,7 @@
// 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]);
Expand Down Expand Up @@ -684,9 +692,8 @@
}

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();
}
Expand Down Expand Up @@ -802,13 +809,11 @@
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;
}
Expand Down
4 changes: 4 additions & 0 deletions src/geo/projection/projection_data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading