From 5955610356cf405b7cb698a899b05f305021d41e Mon Sep 17 00:00:00 2001 From: Aleksandar Stojiljkovic Date: Tue, 23 Jul 2019 17:19:52 +0300 Subject: [PATCH 1/2] [core] Limit pitch based on edge insets. Fix max Z calculation in getProjMatrix. Patch partly fixes #15163 in a way that it doesn't allow loading tens of thousands of tiles and attempt to show area above horizon: Limit pitch based on edge insets. It is not too bad - current limit of 60 degrees stays active until center of perspective is moved towards the bottom, to 84% of screen height. The plan is to split removal of 60 degrees limit to follow up patch. Fix max Z calculation in getProjMatrix. TransformState::getProjMatrix calculation of farZ was complex with possibility to lead to negative z values. Replacing it with simpler, precise calculation: furthestDistance = cameraToCenterDistance / (1 - tanFovAboveCenter * std::tan(getPitch())); TransformState::getProjMatrix calculation of farZ was an aproximation. Replacing it with simpler, but precise calculation. Related to: #15163 --- include/mbgl/util/projection.hpp | 2 +- src/mbgl/map/transform.cpp | 26 ++++++++++++++++++------ src/mbgl/map/transform.hpp | 3 +++ src/mbgl/map/transform_state.cpp | 20 +++++++++--------- test/map/transform.test.cpp | 3 +++ test/util/tile_cover.test.cpp | 35 ++++++++++++++++++++++++++++++++ 6 files changed, 72 insertions(+), 17 deletions(-) diff --git a/include/mbgl/util/projection.hpp b/include/mbgl/util/projection.hpp index 9619a380b42..f0b9115eb07 100644 --- a/include/mbgl/util/projection.hpp +++ b/include/mbgl/util/projection.hpp @@ -98,7 +98,7 @@ class Projection { return Point { util::LONGITUDE_MAX + latLng.longitude(), util::LONGITUDE_MAX - util::RAD2DEG * std::log(std::tan(M_PI / 4 + latitude * M_PI / util::DEGREES_MAX)) - } * worldSize / util::DEGREES_MAX; + } * (worldSize / util::DEGREES_MAX); } }; diff --git a/src/mbgl/map/transform.cpp b/src/mbgl/map/transform.cpp index a51d4dd4ff2..0651cfd2844 100644 --- a/src/mbgl/map/transform.cpp +++ b/src/mbgl/map/transform.cpp @@ -138,9 +138,6 @@ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& anim if (bearing != startBearing) { state.bearing = util::wrap(util::interpolate(startBearing, bearing, t), -M_PI, M_PI); } - if (pitch != startPitch) { - state.pitch = util::interpolate(startPitch, pitch, t); - } if (padding != startEdgeInsets) { // Interpolate edge insets state.edgeInsets = { @@ -150,6 +147,10 @@ void Transform::easeTo(const CameraOptions& camera, const AnimationOptions& anim util::interpolate(startEdgeInsets.right(), padding.right(), t) }; } + auto maxPitch = getMaxPitchForEdgeInsets(state.edgeInsets); + if (pitch != startPitch || maxPitch < startPitch) { + state.pitch = std::min(maxPitch, util::interpolate(startPitch, pitch, t)); + } }, duration); } @@ -302,9 +303,6 @@ void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &anima if (bearing != startBearing) { state.bearing = util::wrap(util::interpolate(startBearing, bearing, k), -M_PI, M_PI); } - if (pitch != startPitch) { - state.pitch = util::interpolate(startPitch, pitch, k); - } if (padding != startEdgeInsets) { // Interpolate edge insets state.edgeInsets = { @@ -314,6 +312,10 @@ void Transform::flyTo(const CameraOptions &camera, const AnimationOptions &anima util::interpolate(startEdgeInsets.right(), padding.right(), us) }; } + auto maxPitch = getMaxPitchForEdgeInsets(state.edgeInsets); + if (pitch != startPitch || maxPitch < startPitch) { + state.pitch = std::min(maxPitch, util::interpolate(startPitch, pitch, k)); + } }, duration); } @@ -576,4 +578,16 @@ LatLng Transform::screenCoordinateToLatLng(const ScreenCoordinate& point, LatLng return state.screenCoordinateToLatLng(flippedPoint, wrapMode); } +double Transform::getMaxPitchForEdgeInsets(const EdgeInsets &insets) const +{ + double centerOffsetY = 0.5 * (insets.top() - insets.bottom()); // See TransformState::getCenterOffset. + + const auto height = state.size.height; + assert(height); + // See TransformState::fov description: fov = 2 * arctan((height / 2) / (height * 1.5)). + const double fovAboveCenter = std::atan((0.666666 + 0.02) * (0.5 + centerOffsetY / height)); + return M_PI * 0.5 - fovAboveCenter; // 0.02 added ^^^^ to prevent parallel ground to viewport clipping plane. + // e.g. Maximum pitch of 60 degrees is when perspective center's offset from the top is 84% of screen height. +} + } // namespace mbgl diff --git a/src/mbgl/map/transform.hpp b/src/mbgl/map/transform.hpp index 0c018a6e487..75dfeff6459 100644 --- a/src/mbgl/map/transform.hpp +++ b/src/mbgl/map/transform.hpp @@ -115,6 +115,9 @@ class Transform : private util::noncopyable { std::function, const Duration&); + // We don't want to show horizon: limit max pitch based on edge insets. + double getMaxPitchForEdgeInsets(const EdgeInsets &insets) const; + TimePoint transitionStart; Duration transitionDuration; std::function transitionFrameFn; diff --git a/src/mbgl/map/transform_state.cpp b/src/mbgl/map/transform_state.cpp index 92c02d0bc76..77309a2a55b 100644 --- a/src/mbgl/map/transform_state.cpp +++ b/src/mbgl/map/transform_state.cpp @@ -36,18 +36,18 @@ void TransformState::getProjMatrix(mat4& projMatrix, uint16_t nearZ, bool aligne const double cameraToCenterDistance = getCameraToCenterDistance(); auto offset = getCenterOffset(); - // Find the distance from the viewport center point - // [width/2 + offset.x, height/2 + offset.y] to the top edge, to point - // [width/2 + offset.x, 0] in Z units, using the law of sines. + // Find the Z distance from the viewport center point + // [width/2 + offset.x, height/2 + offset.y] to the top edge; to point + // [width/2 + offset.x, 0] in Z units. // 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 double fovAboveCenter = getFieldOfView() * (0.5 + offset.y / size.height); - const double groundAngle = M_PI / 2.0 + getPitch(); - const double aboveCenterSurfaceDistance = std::sin(fovAboveCenter) * cameraToCenterDistance / std::sin(M_PI - groundAngle - fovAboveCenter); - - + // See https://github.com/mapbox/mapbox-gl-native/pull/15195 for details. + // See TransformState::fov description: fov = 2 * arctan((height / 2) / (height * 1.5)). + const double tanFovAboveCenter = (size.height * 0.5 + offset.y) / (size.height * 1.5); + const double tanMultiple = tanFovAboveCenter * std::tan(getPitch()); + assert(tanMultiple < 1); // Calculate z distance of the farthest fragment that should be rendered. - const double furthestDistance = std::cos(M_PI / 2 - getPitch()) * aboveCenterSurfaceDistance + cameraToCenterDistance; + const double furthestDistance = cameraToCenterDistance / (1 - tanMultiple); // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` const double farZ = furthestDistance * 1.01; @@ -64,7 +64,7 @@ void TransformState::getProjMatrix(mat4& projMatrix, uint16_t nearZ, bool aligne const bool flippedY = viewportMode == ViewportMode::FlippedY; matrix::scale(projMatrix, projMatrix, 1.0, flippedY ? 1 : -1, 1); - matrix::translate(projMatrix, projMatrix, 0, 0, -getCameraToCenterDistance()); + matrix::translate(projMatrix, projMatrix, 0, 0, -cameraToCenterDistance); using NO = NorthOrientation; switch (getNorthOrientation()) { diff --git a/test/map/transform.test.cpp b/test/map/transform.test.cpp index 3d37312b170..84fdb06b211 100644 --- a/test/map/transform.test.cpp +++ b/test/map/transform.test.cpp @@ -9,6 +9,7 @@ using namespace mbgl; TEST(Transform, InvalidZoom) { Transform transform; + transform.resize({1, 1}); ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); @@ -56,6 +57,7 @@ TEST(Transform, InvalidZoom) { TEST(Transform, InvalidBearing) { Transform transform; + transform.resize({1, 1}); ASSERT_DOUBLE_EQ(0, transform.getLatLng().latitude()); ASSERT_DOUBLE_EQ(0, transform.getLatLng().longitude()); @@ -78,6 +80,7 @@ TEST(Transform, InvalidBearing) { TEST(Transform, IntegerZoom) { Transform transform; + transform.resize({1, 1}); auto checkIntegerZoom = [&transform](uint8_t zoomInt, double zoom) { transform.jumpTo(CameraOptions().withZoom(zoom)); diff --git a/test/util/tile_cover.test.cpp b/test/util/tile_cover.test.cpp index 3fc7681520d..e35e6e2e994 100644 --- a/test/util/tile_cover.test.cpp +++ b/test/util/tile_cover.test.cpp @@ -43,6 +43,41 @@ TEST(TileCover, Pitch) { util::tileCover(transform.getState(), 2)); } +TEST(TileCover, PitchOverAllowedByContentInsets) { + Transform transform; + transform.resize({ 512, 512 }); + + transform.jumpTo(CameraOptions().withCenter(LatLng { 0.1, -0.1 }).withPadding(EdgeInsets { 376, 0, 0, 0 }) + .withZoom(8.0).withBearing(45.0).withPitch(60.0)); + // Top padding of 376 leads to capped pitch. See Transform::getMaxPitchForEdgeInsets. + EXPECT_LE(transform.getPitch() + 0.001, util::DEG2RAD * 60); + + EXPECT_EQ((std::vector{ + { 3, 4, 3 }, { 3, 3, 3 }, { 3, 4, 4 }, { 3, 3, 4 }, { 3, 4, 2 }, { 3, 5, 3 }, { 3, 5, 2 } + }), + util::tileCover(transform.getState(), 3)); +} + +TEST(TileCover, PitchWithLargerResultSet) { + Transform transform; + transform.resize({ 1024, 768 }); + + // The values used here triggered the regression with left and right edge + // selection in tile_cover.cpp scanSpans. + transform.jumpTo(CameraOptions().withCenter(LatLng { 0.1, -0.1 }).withPadding(EdgeInsets { 400, 0, 0, 0 }) + .withZoom(5).withBearing(-142.2630000003529176).withPitch(60.0)); + + auto cover = util::tileCover(transform.getState(), 5); + // Returned vector has above 100 elements, we check first 16 as there is a + // plan to return lower LOD for distant tiles. + EXPECT_EQ((std::vector { + { 5, 15, 16 }, { 5, 15, 17 }, { 5, 14, 16 }, { 5, 14, 17 }, + { 5, 16, 16 }, { 5, 16, 17 }, { 5, 15, 15 }, { 5, 14, 15 }, + { 5, 15, 18 }, { 5, 14, 18 }, { 5, 16, 15 }, { 5, 13, 16 }, + { 5, 13, 17 }, { 5, 16, 18 }, { 5, 13, 18 }, { 5, 15, 19 } + }), (std::vector { cover.begin(), cover.begin() + 16}) ); +} + TEST(TileCover, WorldZ1) { EXPECT_EQ((std::vector{ { 1, 0, 0 }, { 1, 0, 1 }, { 1, 1, 0 }, { 1, 1, 1 }, From 94903d5cbb0fbbe1ddc090a62e5bf5b90094ad8f Mon Sep 17 00:00:00 2001 From: Aleksandar Stojiljkovic Date: Wed, 24 Jul 2019 18:08:11 +0300 Subject: [PATCH 2/2] util::tileCover optimization: three scans and no duplicates handling. TileCover: Replaced 4 scanSpans by 3. As the split lines (triangle, trapezoid, triangle) is horizontal, there is no need to handle duplicates. Benchmarks (release build) on MacBookPro11,2 (Mid 2014) with Intel(R) Core(TM) i7-4770HQ CPU @ 2.20GHz compared against src/mbgl/util/tile_cover.cpp from master and from the patch: ``` master | patch --------------------- TileCoverPitchedViewport 72000ns | 50300ns TileCoverBounds 1620ns | 1400ns ``` TileCoverPitchedViewport modified to have pitch capped by targe top inset, returning 1124 tiles at zoom level 8. TileCover.PitchOverAllowedByContentInsets test verifies pitch capping by large top inset. Expectation was calculated using previous tileCover algorithm implementation. Related to: #15163 --- benchmark/util/tilecover.benchmark.cpp | 3 +- src/mbgl/util/tile_cover.cpp | 108 ++++++++++++++++--------- 2 files changed, 73 insertions(+), 38 deletions(-) diff --git a/benchmark/util/tilecover.benchmark.cpp b/benchmark/util/tilecover.benchmark.cpp index e966875a655..cd9da548557 100644 --- a/benchmark/util/tilecover.benchmark.cpp +++ b/benchmark/util/tilecover.benchmark.cpp @@ -22,7 +22,8 @@ static void TileCoverPitchedViewport(benchmark::State& state) { Transform transform; transform.resize({ 512, 512 }); // slightly offset center so that tile order is better defined - transform.jumpTo(CameraOptions().withCenter(LatLng { 0.1, -0.1 }).withZoom(8.0).withBearing(5.0).withPitch(40.0)); + transform.jumpTo(CameraOptions().withCenter(LatLng { 0.1, -0.1 }).withPadding(EdgeInsets { 376, 0, 0, 0 }) + .withZoom(8.0).withBearing(5.0).withPitch(60.0)); std::size_t length = 0; while (state.KeepRunning()) { diff --git a/src/mbgl/util/tile_cover.cpp b/src/mbgl/util/tile_cover.cpp index 5189b79f267..ab4ec0afccb 100644 --- a/src/mbgl/util/tile_cover.cpp +++ b/src/mbgl/util/tile_cover.cpp @@ -39,15 +39,15 @@ static void scanSpans(edge e0, edge e1, int32_t ymin, int32_t ymax, ScanLine sca double y1 = ::fmin(ymax, std::ceil(e1.y1)); // sort edges by x-coordinate - if ((e0.x0 == e1.x0 && e0.y0 == e1.y0) ? - (e0.x0 + e1.dy / e0.dy * e0.dx < e1.x1) : - (e0.x1 - e1.dy / e0.dy * e0.dx < e1.x0)) { + double m0 = e0.dx / e0.dy; + double m1 = e1.dx / e1.dy; + double ySort = e0.y0 == e1.y0 ? std::min(e0.y1, e1.y1) : std::max(e0.y0, e1.y0); + if (e0.x0 - (e0.y0 - ySort) * m0 < e1.x0 - (e1.y0 - ySort) * m1) { std::swap(e0, e1); + std::swap(m0, m1); } // scan lines! - double m0 = e0.dx / e0.dy; - double m1 = e1.dx / e1.dy; double d0 = e0.dx > 0; // use y + 1 to compute x0 double d1 = e1.dx < 0; // use y + 1 to compute x1 for (int32_t y = y0; y < y1; y++) { @@ -57,22 +57,6 @@ static void scanSpans(edge e0, edge e1, int32_t ymin, int32_t ymax, ScanLine sca } } -// scan-line conversion -static void scanTriangle(const Point& a, const Point& b, const Point& c, int32_t ymin, int32_t ymax, ScanLine& scanLine) { - edge ab = edge(a, b); - edge bc = edge(b, c); - edge ca = edge(c, a); - - // sort edges by y-length - if (ab.dy > bc.dy) { std::swap(ab, bc); } - if (ab.dy > ca.dy) { std::swap(ab, ca); } - if (bc.dy > ca.dy) { std::swap(bc, ca); } - - // scan span! scan span! - if (ab.dy) scanSpans(ca, ab, ymin, ymax, scanLine); - if (bc.dy) scanSpans(ca, bc, ymin, ymax, scanLine); -} - } // namespace namespace util { @@ -85,7 +69,7 @@ std::vector tileCover(const Point& tl, const Point& bl, const Point& c, int32_t z) { - const int32_t tiles = 1 << z; + const int32_t tiles = (1 << z) + 1; struct ID { int32_t x, y; @@ -96,30 +80,80 @@ std::vector tileCover(const Point& tl, auto scanLine = [&](int32_t x0, int32_t x1, int32_t y) { int32_t x; - if (y >= 0 && y <= tiles) { - for (x = x0; x < x1; ++x) { - const auto dx = x + 0.5 - c.x, dy = y + 0.5 - c.y; - t.emplace_back(ID{ x, y, dx * dx + dy * dy }); - } + for (x = x0; x < x1; ++x) { + const auto dx = x + 0.5 - c.x, dy = y + 0.5 - c.y; + t.emplace_back(ID { x, y, dx * dx + dy * dy }); } }; - // Divide the screen up in two triangles and scan each of them: - // \---+ - // | \ | - // +---\. - scanTriangle(tl, tr, br, 0, tiles, scanLine); - scanTriangle(br, bl, tl, 0, tiles, scanLine); + std::vector> bounds = {tl, tr, br, bl}; + while (bounds[0].y > min(min(bounds[1].y, bounds[2].y), bounds[3].y)) { + std::rotate(bounds.begin(), bounds.begin() + 1, bounds.end()); + } + /* + Keeping the clockwise winding order (abcd), we rotated convex quadrilateral + angles in such way that angle a (bounds[0]) is on top): + a + / \ + / b + / | + / c + / .... + / .. + d + This is an example: we handle also cases where d.y < c.y, d.y < b.y etc. + Split the scan to tree steps: + a + / \ (1) + / b + ----------------- + / | (2) + / c + ----------------- + / .... + / .. (3) + d + */ + edge ab = edge(bounds[0], bounds[1]); + edge ad = edge(bounds[0], bounds[3]); + + // Scan (1). + int32_t ymin = std::floor(bounds[0].y); + if (bounds[3].y < bounds[1].y) { std::swap(ab, ad); } + int32_t ymax = std::ceil(ab.y1); + if (ab.dy) { + scanSpans(ad, ab, std::max(0, ymin), std::min(tiles, ymax), scanLine); + ymin = ymax; + } + + // Scan (2). + // yCutLower is c or d, whichever is with lower y value. + float yCutLower = min(bounds[2].y, ad.y1); + ymax = std::ceil(yCutLower); + + // bc is edge opposite of ad. + edge bc = bounds[3].y < bounds[1].y ? edge(bounds[3], bounds[2]) : edge(bounds[1], bounds[2]); + if (bc.dy) { + scanSpans(ad, bc, std::max(0, ymin), std::min(tiles, ymax), scanLine); + ymin = ymax; + } else { + ymin = std::floor(yCutLower); + } + + // Scan (3) - the triangle at the bottom. + if (ad.y1 < bc.y1) { std::swap(ad, bc); } + ymax = std::ceil(ad.y1); + bc = edge({ bc.x1, bc.y1 }, { ad.x1, ad.y1 }); + if (bc.dy) { scanSpans(ad, bc, std::max(0, ymin), std::min(tiles, ymax), scanLine); } // Sort first by distance, then by x/y. std::sort(t.begin(), t.end(), [](const ID& a, const ID& b) { return std::tie(a.sqDist, a.x, a.y) < std::tie(b.sqDist, b.x, b.y); }); - // Erase duplicate tile IDs (they typically occur at the common side of both triangles). - t.erase(std::unique(t.begin(), t.end(), [](const ID& a, const ID& b) { - return a.x == b.x && a.y == b.y; - }), t.end()); + assert(t.end() == std::unique(t.begin(), t.end(), [](const ID& a, const ID& b) { + return a.x == b.x && a.y == b.y; + })); // no duplicates. std::vector result; for (const auto& id : t) {