Skip to content

Conversation

@bigmistqke
Copy link

@bigmistqke bigmistqke commented Oct 17, 2025

Fixes #6584

map.transform.getCameraAltitude() returns NaN for both globe and vertical-perspective projection due to pixelsPerMeter being undefined.

This PR initializes this._helper._pixelPerMeter just as MercatorTransform does it, I am keeping this PR in draft because I am not super confident this is the correct solution.

Launch Checklist

  • Confirm your changes do not include backports from Mapbox projects (unless with compliant license) - if you are not sure about this, please ask!
  • Briefly describe the changes in this PR.
  • Link to related issues.
  • Include before/after visuals or gifs if this PR includes visual changes.
  • Write tests for all new functionality.
  • Document any changes to public APIs.
  • Post benchmark scores.
  • Add an entry to CHANGELOG.md under the ## main section.

@HarelM
Copy link
Collaborator

HarelM commented Oct 17, 2025

Thanks for taking the time to open this PR!
I'm not sure this is the right calculation either.
It might be better than NaN though.
Regardless, globe transform should mostly be a wrapper around either vertical perspective or mercator, so in most cases you would want to use the logic of one of them and not calculate something "new".

@bigmistqke
Copy link
Author

bigmistqke commented Oct 18, 2025

Thanks for taking the time to open this PR!

You are very welcome!

I'm not sure this is the right calculation either.

Do you maybe know who has expertise in the vertical perspective transform and the globe transform that we could ping for looking at this? It's a bit much to sink my teeth in 😅

Regardless, globe transform should mostly be a wrapper around either vertical perspective or mercator, so in most cases you would want to use the logic of one of them and not calculate something "new".

Good point, I agree.

@HarelM
Copy link
Collaborator

HarelM commented Oct 18, 2025

@kubapelc probably knows best.

@kubapelc
Copy link
Contributor

Hi, it has been some time since I last delved into transforms in MapLibre, but I think your fix is correct.

this._mercatorTransform.apply(this, true, this.isGlobeRendering);
this._helper._nearZ = this._mercatorTransform.nearZ;
this._helper._farZ = this._mercatorTransform.farZ;
this._helper._pixelPerMeter = mercatorZfromAltitude(1, this.center.lat) * this.worldSize;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Globe transform should mostly be a wrapper around mercator and vertical presense.
So I would expect this call to be something like:
this._helper._pixelPerMeter = this.currentTransform.pixelPerMeter
Or something similar, can you please check if this is possible?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I tend to think that placing this logic inside the calcMatrices of the helper will solve this issue as the code is the same for both vertical perspective and mercator transforms.
I'm guessing this it's also the right place since the helper is "responsible" for this variable and should update it as needed.

Copy link
Author

Choose a reason for hiding this comment

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

I moved the calculation to the TransformHelper as you suggested.

There is a slight functional difference with my change: MercatorTransform used to recalculate this._helper._pixelPerMeter when this.helper._width is possibly nullable (or 0), but now it is only re-calculated when both _width and _height are defined.

@pcace
Copy link

pcace commented Nov 12, 2025

Hi there, is there any plan when this fix can make it into maplibre?
thanks a lot for updates on this issue

@HarelM
Copy link
Collaborator

HarelM commented Nov 12, 2025

I'm not sure, the status of this PR is draft, I'll be happy to review it again after the fixes are made and the status changes to ready for review.
Feel free to take it from here if you need it sooner rather than later.

@bigmistqke
Copy link
Author

Hi @HarelM

Apologies for the inactivity. I will pick it up today!

…lcMatrices

NOTE: MercatorTransform used to recalculate this._helper._pixelPerMeter when `this.helper._width` is possibly nullable (or 0), but now it is only re-calculated when both _width and _height are defined.
@bigmistqke bigmistqke marked this pull request as ready for review November 13, 2025 12:15
@bigmistqke
Copy link
Author

My fix breaks the same test locally as on CI: Applies options.opacityWhenCovered when marker is covered by globe with terrain disabled or enabled. It also breaks the same test at commit 23a0ac1, from before I moved the _pixelsPerMeter calculation to TransformHelper.

@HarelM
Copy link
Collaborator

HarelM commented Nov 13, 2025

I'm guessing this needs to be addressed then, shouldn't it?

Example stepping through failing test `Applies options.opacityWhenCovered when marker is covered by globe with terrain disabled or enabled`
@bigmistqke
Copy link
Author

bigmistqke commented Nov 14, 2025

I'm guessing this needs to be addressed then, shouldn't it?

I agree!

I have been looking at it, trying to conclude if the failing test was a false positive. I recreated the test as an example (and pushed it 👉 0c41471) that I could step through so I could get a bit more of a visual understanding.

Before fix

Screen.Recording.2025-11-14.at.09.57.50.mov

In Marker._updateOpacity they make use of map.transform.pixelsPerMeter (but only when terrain is enabled). As pixelsPerMeter is then undefined, the resulting computation becomes NaN.

After fix

When we add our fix, we get the following result:

Screen.Recording.2025-11-14.at.10.05.34.mov

pixelsPerMeter is not undefined anymore (but instead is 0.00002075931143493277 on zoom level 0).

This results in the following calculation for the failing expectation:

// 674396.1640481714
const metersToCenter = -this._offset.y / map.transform.pixelsPerMeter; 

// 0
const elevationToCenter =
    Math.sin((map.getPitch() * Math.PI) / 180) * metersToCenter; 
 
// 0.9
const terrainDistanceCenter = map.terrain.depthAtPoint(
    new Point(this._pos.x, this._pos.y - this._offset.y)
);

// 0.9998595004323604
const markerDistanceCenter = map.transform.lngLatToCameraDepth(
    this._lngLat,
    elevation + elevationToCenter
);

// true = 0.9998595004323604 > 0.006
const centerIsInvisible = markerDistanceCenter - terrainDistanceCenter > forgiveness;

@bigmistqke
Copy link
Author

The test makes use of a mock for Terrain, just as the tests (see createTerrain):

map.terrain = {
    pointCoordinate: () => null,
    getElevationForLngLatZoom: () => 1000,
    getMinTileElevationForLngLatZoom: () => 0,
    getFramebuffer: () => ({}),
    getCoordsTexture: () => ({}),
    depthAtPoint: () => .9,
    tileManager: {
        update: () => {},
        getRenderableTiles: () => [],
        anyTilesAfterTime: () => false
    }
}

My assumption would be is that if I mock getElevationForLngLatZoom and depthAtPoint to return 0 I should get the same results as if it would be occluding with no terrain (and the test should succeed), but this is not the case:

Screen.Recording.2025-11-14.at.10.29.30.mov

@HarelM
Copy link
Collaborator

HarelM commented Nov 14, 2025

I think the question is around what this test is testing and if this PR didn't broke it:
I.e. if markers are properly "hidden" when behind the terrain.

@bigmistqke
Copy link
Author

I think the question is around what this test is testing and if this PR didn't broke it:
I.e. if markers are properly "hidden" when behind the terrain.

I think what I found out is that the terrain occlusion of the markers in globe transform was broken before and that the succeeding test was a false positive. But I don't think my fix is correct either, as I would expect terrain with 0 elevation to return the same results as a globe without terrain.

@HarelM
Copy link
Collaborator

HarelM commented Nov 14, 2025

That's interesting, good thing we have these tests 😀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

map.transform.getCameraAltitude() returns NaN in globe projection

4 participants