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

Support max pitch > 60 #3731

Closed
lucaswoj opened this issue Dec 1, 2016 · 48 comments
Closed

Support max pitch > 60 #3731

lucaswoj opened this issue Dec 1, 2016 · 48 comments

Comments

@lucaswoj
Copy link
Contributor

lucaswoj commented Dec 1, 2016

As we improve support for custom rendering, there are legitimate cases for pitching the map beyond the hardcoded limit of 60 degrees. With the proper safeguards to prevent loading an absurd # of tiles, we could make the max pitch of the map configurable.

@mourner
Copy link
Member

mourner commented Dec 1, 2016

With the proper safeguards to prevent loading an absurd # of tiles

Yes! We've discussed some ideas about this with @ansis recently. Basically we should change the tile loading algorithm so that it loads lower zoom tiles on farther side, depending on how small they appear when projected.

@ibgreen
Copy link

ibgreen commented Dec 2, 2016

| loads lower zoom tiles on farther side, depending on how small they appear when projected.

That would be really neat. If not, just rendering blur / some sort of background color in the distance would work fine as well - at least it would unblock our use cases.

@ibgreen
Copy link

ibgreen commented Dec 2, 2016

Also, in deck.gl we are combining maps with general 3D geometry - so our main camera is a general projection matrix, which we currently have to constrain to fit with mapbox zoom and pitch limits. If we remove those constraints, this means that we could have pitch > 90 degrees!

It would certainly be OK not to render the base map in this case (we won't be underground). But we might still want to see extrusions (like buildings) from your 3D styles when looking up from street level.

Maybe this should be a separate issue?

@ansis
Copy link
Contributor

ansis commented Dec 2, 2016

besides level-of-detail tile loading we'll also need to fix:

  • label-placement (the tiled placement approach will completely fall apart)
  • antialiasing

@ibgreen
Copy link

ibgreen commented Dec 2, 2016

label-placement (the tiled placement approach will completely fall apart)

For our purposes it would be OK if the labels disappeared at high pitch levels. They probably wouldn't be very legible anyway?

@Mariusun
Copy link

Mariusun commented Sep 5, 2017

This would be really great! Any progress on this issue?

@ansis
Copy link
Contributor

ansis commented Sep 7, 2017

@Mariusun not on the main part of the issue but we are making progress one some sub-problems like improved label placement

@yuechenglei
Copy link

I am new try to use mapboxGL. It's wonderful to change the slope. But I also have the need to pitch the map more to observe the 3D
The label don't need to slope too much I think...
Hope to have progress on this issue.

@damien-murphy
Copy link

+1 This would also be useful for AR/VR Experiences for FPV and also when turning on 3D Buildings to look down streets

@1ec5
Copy link
Contributor

1ec5 commented Jan 12, 2018

Related: mapbox/mapbox-gl-native#9037.

@areknawo
Copy link

Any progress by now?

@ansis
Copy link
Contributor

ansis commented Feb 26, 2018

@areknawo we've finished some of the tasks that were blocking this, but the feature itself is not being actively worked on right now.

@areknawo
Copy link

Alright, I have found my way around, thanks for information anyway.

@Rushali
Copy link

Rushali commented Feb 26, 2018

@areknawo How are you working around this? Any pointers would be really helpful!

@areknawo
Copy link

@Rushali it's basicaly like this:

  1. Set max pitch to 89.9 - it can't be full 90.
  2. In calcMatrices divide projMatix into two - one for calculating tiles (limited to 60) and one for viewport (limited to 90).
  3. As I use it for AR experience I want camera to rotate around itself so I change its position.
  4. I didn't test it with labels because I don't need them, but I think they won't work properly.
  5. I know this code isn't clear and it isn't working well, but at least it's working.

prototypeAccessors.pitch.set = function (pitch ) {
var p = util.clamp(pitch, 0, 89.9) / 180 * Math.PI;
console.log(p)
if (this._pitch === p) { return; }
this._unmodified = false;
this._pitch = p;
this._calcMatrices();
};

Transform.prototype._calcMatrices = function _calcMatrices () {
if (!this.height) { return; }

this.cameraToCenterDistance = 0.5 / Math.tan(this._fov / 2) * this.height /20;

// Find the distance from the center point [width/2, height/2] to the
// center top point [width/2, 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])
var halfFov = this._fov / 2;
var groundAngle = Math.PI / 2 + this._pitch;
var topHalfSurfaceDistance = Math.sin(halfFov) * this.cameraToCenterDistance / Math.sin(Math.PI - groundAngle - halfFov);
var x = this.x, y = this.y;

// Calculate z distance of the farthest fragment that should be rendered.
var furthestDistance = Math.cos(Math.PI / 2 - this._pitch) * topHalfSurfaceDistance + this.cameraToCenterDistance;
// Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance`
var farZ = furthestDistance * 1.01;

// matrix for conversion from location to GL coordinates (-1 .. 1)
var m = new Float64Array(16);
mat4.perspective(m, this._fov, this.width / this.height, 1, farZ);

mat4.scale(m, m, [1, -1, 1]);
mat4.translate(m, m, [0, 5, 0]);
mat4.rotateX(m, m, this._pitch);
mat4.rotateZ(m, m, this.angle);
mat4.translate(m, m, [-x, -y, 0]);

var m2 = new Float64Array(16);
mat4.perspective(m2, this._fov, this.width / this.height, 1, farZ);

mat4.scale(m2, m2, [1, -1, 1]);
mat4.translate(m2, m2, [0, 0, -this.cameraToCenterDistance]);
if(this._pitch > 60/180*Math.PI){
mat4.rotateX(m2, m2, 60/180*Math.PI);
}
else{
    mat4.rotateX(m2, m2, this._pitch);
}
mat4.rotateZ(m2, m2, this.angle);
mat4.translate(m2, m2, [-x, -y, 0]);

// scale vertically to meters per pixel (inverse of ground resolution):
// worldSize / (circumferenceOfEarth * cos(lat * π / 180))
var verticalScale = this.worldSize / (2 * Math.PI * 6378137 * Math.abs(Math.cos(this.center.lat * (Math.PI / 180))));
mat4.scale(m, m, [1, 1, verticalScale, 1]);
mat4.scale(m2, m2, [1, 1, verticalScale, 1]);

this.projMatrix = m;
this.projPointMatrix = m2;

// Make a second projection matrix that is aligned to a pixel grid for rendering raster tiles.
// We're rounding the (floating point) x/y values to achieve to avoid rendering raster images to fractional
// coordinates. Additionally, we adjust by half a pixel in either direction in case that viewport dimension
// is an odd integer to preserve rendering to the pixel grid. We're rotating this shift based on the angle
// of the transformation so that 0°, 90°, 180°, and 270° rasters are crisp, and adjust the shift so that
// it is always <= 0.5 pixels.
var xShift = (this.width % 2) / 2, yShift = (this.height % 2) / 2,
    angleCos = Math.cos(this.angle), angleSin = Math.sin(this.angle),
    dx = x - Math.round(x) + angleCos * xShift + angleSin * yShift,
    dy = y - Math.round(y) + angleCos * yShift + angleSin * xShift;
var alignedM = new Float64Array(m);
mat4.translate(alignedM, alignedM, [ dx > 0.5 ? dx - 1 : dx, dy > 0.5 ? dy - 1 : dy, 0 ]);
this.alignedProjMatrix = alignedM;

// matrix for conversion from location to screen coordinates
m = mat4.create();
mat4.scale(m, m, [this.width / 2, -this.height / 2, 1]);
mat4.translate(m, m, [1, -1, 0]);
this.pixelMatrix = mat4.multiply(new Float64Array(16), m, this.projPointMatrix);
m2 = mat4.create();
mat4.scale(m2, m2, [this.width / 2, -this.height / 2, 1]);
mat4.translate(m2, m2, [1, -1, 0]);
this.pixelPointMatrix = mat4.multiply(new Float64Array(16), m2, this.projMatrix);

// inverse matrix for conversion from screen coordinaes to location
m = mat4.invert(new Float64Array(16), this.pixelMatrix);
if (!m) { throw new Error("failed to invert matrix"); }
this.pixelMatrixInverse = m;

m2 = mat4.invert(new Float64Array(16), this.pixelPointMatrix);
if (!m2) { throw new Error("failed to invert matrix"); }
this.pixelPointMatrixInverse = m2;

this._posMatrixCache = {};
this._alignedPosMatrixCache = {};

};

Transform.prototype.coveringTiles = function coveringTiles (
options
) {
var z = this.coveringZoomLevel(options);
var actualZ = z;

if (options.minzoom !== undefined && z < options.minzoom) { return []; }
if (options.maxzoom !== undefined && z > options.maxzoom) { z = options.maxzoom; }

var centerCoord = this.pointCoordinate(this.centerPoint, z);
var centerPoint = new Point(centerCoord.column - 0.5, centerCoord.row - 0.5);
var cornerCoords = [
    this.pointCoordinate(new Point(0, 0), z, true),
    this.pointCoordinate(new Point(this.width, 0), z, true),
    this.pointCoordinate(new Point(this.width, this.height), z, true),
    this.pointCoordinate(new Point(0, this.height), z, true)
];
return tileCover(z, cornerCoords, options.reparseOverscaled ? actualZ : z, this._renderWorldCopies)
    .sort(function (a, b) { return centerPoint.dist(a.canonical) - centerPoint.dist(b.canonical); });

};

Transform.prototype.pointCoordinate = function pointCoordinate (p , zoom , forTiles) {
if (zoom === undefined) { zoom = this.tileZoom; }

var targetZ = 0;
// since we don't know the correct projected z value for the point,
// unproject two points to get a line and then find the point on that
// line with z=0

var coord0 = [p.x, p.y, 0, 1];
var coord1 = [p.x, p.y, 1, 1];
if(forTiles){
vec4.transformMat4(coord0, coord0, this.pixelPointMatrixInverse);
vec4.transformMat4(coord1, coord1, this.pixelPointMatrixInverse);
}
else{
    vec4.transformMat4(coord0, coord0, this.pixelMatrixInverse);
    vec4.transformMat4(coord1, coord1, this.pixelMatrixInverse);  
}

var w0 = coord0[3];
var w1 = coord1[3];
var x0 = coord0[0] / w0;
var x1 = coord1[0] / w1;
var y0 = coord0[1] / w0;
var y1 = coord1[1] / w1;
var z0 = coord0[2] / w0;
var z1 = coord1[2] / w1;

var t = z0 === z1 ? 0 : (targetZ - z0) / (z1 - z0);

return new Coordinate(
    interp(x0, x1, t) / this.tileSize,
    interp(y0, y1, t) / this.tileSize,
    this.zoom)._zoomTo(zoom);

};

@kukiel
Copy link

kukiel commented May 5, 2018

Any news on that? It would be really amazing to go beyond 60 degrees limit.

@davidmichaelhogan
Copy link

Agreed ! I'm wondering where the progress is for this feature... To reiterate an earlier point, even just having some color for the distant tiles and disregarding any annotations etc would probably suffice for now!

Thank you.

@1ec5
Copy link
Contributor

1ec5 commented Jul 18, 2018

On the native side, mapbox/mapbox-gl-native#12310 would increase the maximum pitch to 67.5° unconditionally, across all zoom levels. There’s also discussion in mapbox/mapbox-gl-native#6908 about allowing the maximum pitch to vary by zoom level, since that threshold is particularly constraining at high zoom levels.

@web-wyj
Copy link

web-wyj commented Aug 2, 2018

I have tried a currently feasible method.
It's to limit high pitch(pitch>60) covering tiles loading.

the mapbox-gl version i used is mapbox-gl-dev@0.43.0.js.

  1. set max pitch
prototypeAccessors.pitch.set = function(pitch) {
    var MAX_PITCH = 85;
    var MIN_PITCH = 0;
    var p = util.clamp(pitch, MIN_PITCH, MAX_PITCH) / 180 * Math.PI;
    if (this._pitch === p) {
        return;
    }
    this._unmodified = false;
    this._pitch = p;
    this._calcMatrices();
};
  1. limit high pitch covering tiles loading
Transform.prototype.coveringTiles = function coveringTiles(
    options
) {
    var z = this.coveringZoomLevel(options);
    var actualZ = z;

    if (options.minzoom !== undefined && z < options.minzoom) {
        return [];
    }
    if (options.maxzoom !== undefined && z > options.maxzoom) {
        z = options.maxzoom;
    }

    var centerCoord = this.pointCoordinate(this.centerPoint, z);
    var centerPoint = new Point(centerCoord.column - 0.5, centerCoord.row - 0.5);

    // limit high pitch(pitch>60) covering tiles loading
    var initHeight = 0;
    var calHeight = initHeight;
    var HIGH_PITCH = 60;
    var MAX_PITCH = 85;
    if (this.pitch > HIGH_PITCH) {
        var CLIP_HEIGHT = this.height / 2 / 1.07;
        var CLIP_HEIGHT_ADJUST = 0.08;

        var pitchPercent = (this.pitch - HIGH_PITCH) / (MAX_PITCH - HIGH_PITCH);
        var zoomPercent = (this.zoom - this.minZoom) / (this.maxZoom - this.minZoom);
        var adjustAcordingZoom = 1 - zoomPercent;
        var adjustPercent = 1 + adjustAcordingZoom * CLIP_HEIGHT_ADJUST;

        calHeight = pitchPercent * CLIP_HEIGHT * adjustPercent;
    }

    var cornerCoords = [
        this.pointCoordinate(new Point(0, calHeight), z),
        this.pointCoordinate(new Point(this.width, calHeight), z),
        this.pointCoordinate(new Point(this.width, this.height), z),
        this.pointCoordinate(new Point(0, this.height), z)
    ];
    return tileCover(z, cornerCoords, options.reparseOverscaled ? actualZ : z, this._renderWorldCopies)
        .sort(function(a, b) {
            return centerPoint.dist(a.canonical) - centerPoint.dist(b.canonical);
        });
};
  1. and then you can see that.
    1. pitch == 60
      pitch60
    2. pitch == 70
      pitch70
    3. pitch == 80
      pitch80

that is a way i using now, hope that can help you.

@00hello
Copy link

00hello commented Aug 12, 2018

@web-wyj Thanks. This is just what I needed

@web-wyj
Copy link

web-wyj commented Apr 25, 2019

@web-wyj
I mean prototypeAccessors is not defined globally, Do I need to have the mapbox code locally and modify the code within?

yes

@peterbodo
Copy link

@web-wyj
I mean prototypeAccessors is not defined globally, Do I need to have the mapbox code locally and modify the code within?

yes

@web-wyj
Thanks a million. Can you be a bit more specific about where to put the snippets? File, position?
I might be able to find out with two thousand trials and errors... but :)

@web-wyj
Copy link

web-wyj commented Apr 25, 2019

@web-wyj
I mean prototypeAccessors is not defined globally, Do I need to have the mapbox code locally and modify the code within?

yes

@web-wyj
Thanks a million. Can you be a bit more specific about where to put the snippets? File, position?
I might be able to find out with two thousand trials and errors... but :)

You can search the function name 'prototypeAccessors.pitch.set' and 'Transform.prototype.coveringTiles' in mapbox-gl-dev@0.43.0.js .

@peterbodo
Copy link

@web-wyj
okjay, I feel from your answers that I am a nuisance, but it is really above my level.

  1. tried to find 43 dev, but couldn't so I downloaded this one, risking it will not work: https://github.com/mapbox/mapbox-gl-js/releases/tag/v0.43.0

  2. I have found the code parts and managed to replace in js\mapbox-gl-js-0.43.0\bench\benchmarks_generated-3d30c4b.js

  3. I realised that there are no dist/ .js and I need to build the whole stuff, which I really cannot. I have never managed to understand npm even, not talking about webpack or whatever...

  4. now after f.cking my whole morning, can anyone attach a build mapbox-gl.js with these changes?

Thanks a lot,
Peter

@web-wyj
Copy link

web-wyj commented Apr 25, 2019

@web-wyj
okjay, I feel from your answers that I am a nuisance, but it is really above my level.

  1. tried to find 43 dev, but couldn't so I downloaded this one, risking it will not work: https://github.com/mapbox/mapbox-gl-js/releases/tag/v0.43.0
  2. I have found the code parts and managed to replace in js\mapbox-gl-js-0.43.0\bench\benchmarks_generated-3d30c4b.js
  3. I realised that there are no dist/ .js and I need to build the whole stuff, which I really cannot. I have never managed to understand npm even, not talking about webpack or whatever...
  4. now after f.cking my whole morning, can anyone attach a build mapbox-gl.js with these changes?

Thanks a lot,
Peter

You misunderstood me. I'm just not good at speaking English.
You can get js and download from https://api.tiles.mapbox.com/mapbox-gl-js/v0.43.0/mapbox-gl-dev.js.
And then find 'prototypeAccessors.pitch.set' at line 13097.
And then edit it and save to use.

@peterbodo
Copy link

image

Quite cool... Certainly I need to do something with labels, which also float in the air, where otherwise tiles are not visible.

@agatheblues
Copy link

Hello everyone! Thanks for this thread! I used your snippets and it worked like a charm.

I am wondering if, maybe, you would have some clues on where to look to be able to either: completely control the camera (taking over the camera in three js so I can manipulate it the way I want, even if that means not rendering the map in specific cases), or to just be able to turn the camera so it could 'look at the sky'?

Thanks!

@andrewharvey
Copy link
Collaborator

Given this ticket is more about setting a pitch above the current limit of 60, I've renamed this ticket. Since #8834 lets you configure max pitch currently, just not beyond 60.

@danvk
Copy link

danvk commented Nov 27, 2019

FWIW @web-wyj's hack from last year no longer works because coveringTiles was refactored. #8975 seems like the best solution, but until that's merged, does anyone have a new-and-improved hack? :)

@andrewharvey
Copy link
Collaborator

andrewharvey commented Jan 17, 2020

So now that we have @mpulkki-mapbox's awesome work from #8975 merged, are there any issues with flicking the switch and increasing the maximum allowable pitch (

const defaultMaxPitch = 60;
) to something > 60?

From my testing it seems to be working great after changing the defaultMaxPitch to 85. Even setting to 90 nothing crashes!

@mourner
Copy link
Member

mourner commented Jan 17, 2020

@andrewharvey we want to do some improvements to label placement & collision behavior before we can increase the default limit — stay tuned for news from @mpulkki-mapbox on that front!

@ajaxengine
Copy link

image

Quite cool... Certainly I need to do something with labels, which also float in the air, where otherwise tiles are not visible.

this is awesome! did you solve this label placment problem now?

@julianjem
Copy link

So now that we have @mpulkki-mapbox's awesome work from #8975 merged, are there any issues with flicking the switch and increasing the maximum allowable pitch (

const defaultMaxPitch = 60;

) to something > 60?
From my testing it seems to be working great after changing the defaultMaxPitch to 85. Even setting to 90 nothing crashes!

Hi @andrewharvey so, if I need to use a little more pitch angle I just need to replace the .js on that link instead the 1.12.0 version that one's normally use? Sorry if it's a silly question but I'm totally new on this. I tried before with the other .js that someone posted but then the browser said that there was a missin .css file even when in my code there was still the link to the regular.css (the one from mapbox site) so that's why I'm not sure how to include a new .js on my code.

Thanks,

@illsio
Copy link

illsio commented Nov 5, 2020

So now that we have @mpulkki-mapbox's awesome work from #8975 merged, are there any issues with flicking the switch and increasing the maximum allowable pitch (

const defaultMaxPitch = 60;

) to something > 60?

From my testing it seems to be working great after changing the defaultMaxPitch to 85. Even setting to 90 nothing crashes!

I changed defaultMaxPitch to 85/90 in mapbox-gl-js/src/ui/map.js and it did not have any effect on the max pitch for me.
I could not get to a higher pitch by setting 85/90 when initlizing the map or by a right mouse drag pitch increase.
Am I missing anything? Do I need to include further code? I am currently using version 1.12.0.

@neon-ninja
Copy link

As a workaround, you can use TerrainLayer - that will go > 90 pitch no problem.

@illsio
Copy link

illsio commented Nov 10, 2020

As a workaround, you can use TerrainLayer - that will go > 90 pitch no problem.

It does not seem like that is an initial functionality:
https://docs.mapbox.com/vector-tiles/reference/mapbox-terrain-v2/
Just adding one and tying to pitch > 90 or set the map pitch to > 90 does not work for me.
@neon-ninja can you provide example?

@neon-ninja
Copy link

As a workaround, you can use TerrainLayer - that will go > 90 pitch no problem.

It does not seem like that is an initial functionality:
https://docs.mapbox.com/vector-tiles/reference/mapbox-terrain-v2/
Just adding one and tying to pitch > 90 or set the map pitch to > 90 does not work for me.
@neon-ninja can you provide example?

Sure - here's a codepen https://codepen.io/neonninja/pen/eYzQEQq
Hold shift or control and drag with left mouse button to tilt and go underground

@illsio
Copy link

illsio commented Nov 12, 2020

As a workaround, you can use TerrainLayer - that will go > 90 pitch no problem.

It does not seem like that is an initial functionality:
https://docs.mapbox.com/vector-tiles/reference/mapbox-terrain-v2/
Just adding one and tying to pitch > 90 or set the map pitch to > 90 does not work for me.
@neon-ninja can you provide example?

Sure - here's a codepen https://codepen.io/neonninja/pen/eYzQEQq
Hold shift or control and drag with left mouse button to tilt and go underground

Thanks a lot for the example. It seems that this is then a deckGL mapbox implementation?
I suppose adding a TerrainLayer to the classic new mapboxgl.Map wouldnt work with the pitch > 90, because that does not provide the atribute of initialViewState? Combinations then gets problematic when pitching > 90: https://codepen.io/illsio/pen/VwjqzWL

@neon-ninja
Copy link

neon-ninja commented Nov 12, 2020

As a workaround, you can use TerrainLayer - that will go > 90 pitch no problem.

It does not seem like that is an initial functionality:
https://docs.mapbox.com/vector-tiles/reference/mapbox-terrain-v2/
Just adding one and tying to pitch > 90 or set the map pitch to > 90 does not work for me.
@neon-ninja can you provide example?

Sure - here's a codepen https://codepen.io/neonninja/pen/eYzQEQq
Hold shift or control and drag with left mouse button to tilt and go underground

Thanks a lot for the example. It seems that this is then a deckGL mapbox implementation?
I suppose adding a TerrainLayer to the classic new mapboxgl.Map wouldnt work with the pitch > 90, because that does not provide the atribute of initialViewState? Combinations then gets problematic when pitching > 90: https://codepen.io/illsio/pen/VwjqzWL

I don't think TerrainLayer relies on mapbox at all, except as a tile provider. To clarify, my suggested workaround was to use TerrainLayer as your basemap, and disable the mapbox.gl basemap. I think the more imminent problem there is that you're hitting the hardcoded limit of 60. The deck.gl layer can then go beyond while the map stays stuck at 60.
image

@karimnaaji karimnaaji mentioned this issue Dec 8, 2020
@karimnaaji
Copy link
Contributor

Unlocked pitch to up to 85 degrees was added as part of the v2 release: #10160.

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

No branches or pull requests