From 57b4b2829e8033d6cf3f7bd48c1fe511e00b830c Mon Sep 17 00:00:00 2001 From: Mikko Pulkki Date: Mon, 27 Apr 2020 13:26:49 +0300 Subject: Refactor TransformState to use internal 3d camera --- CMakeLists.txt | 1 + src/mbgl/map/transform_state.cpp | 126 ++++++++++++++++------- src/mbgl/map/transform_state.hpp | 11 +- src/mbgl/util/camera.cpp | 215 +++++++++++++++++++++++++++++++++++++++ src/mbgl/util/camera.hpp | 42 ++++++++ 5 files changed, 358 insertions(+), 37 deletions(-) create mode 100644 src/mbgl/util/camera.cpp create mode 100644 src/mbgl/util/camera.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e7a3255240..ad989ba838 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -749,6 +749,7 @@ add_library( ${PROJECT_SOURCE_DIR}/src/mbgl/tile/vector_tile.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/vector_tile_data.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/tile/vector_tile_data.hpp + ${PROJECT_SOURCE_DIR}/src/mbgl/util/camera.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/bounding_volumes.hpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/bounding_volumes.cpp ${PROJECT_SOURCE_DIR}/src/mbgl/util/chrono.cpp diff --git a/src/mbgl/map/transform_state.cpp b/src/mbgl/map/transform_state.cpp index 0333f4860c..a61d02963a 100644 --- a/src/mbgl/map/transform_state.cpp +++ b/src/mbgl/map/transform_state.cpp @@ -9,6 +9,15 @@ #include namespace mbgl { + +namespace { +LatLng latLngFromMercator(Point mercatorCoordinate, LatLng::WrapMode wrapMode = LatLng::WrapMode::Unwrapped) { + return {util::RAD2DEG * (2 * std::atan(std::exp(M_PI - mercatorCoordinate.y * util::M2PI)) - M_PI_2), + mercatorCoordinate.x * 360.0 - 180.0, + wrapMode}; +} +} // namespace + TransformState::TransformState(ConstrainMode constrainMode_, ViewportMode viewportMode_) : bounds(LatLngBounds()), constrainMode(constrainMode_), viewportMode(viewportMode_) {} @@ -101,62 +110,52 @@ void TransformState::getProjMatrix(mat4& projMatrix, uint16_t nearZ, bool aligne // Add a bit extra to avoid precision problems when a fragment's distance is exactly `furthestDistance` const double farZ = furthestDistance * 1.01; - matrix::perspective(projMatrix, getFieldOfView(), double(size.width) / size.height, nearZ, farZ); + // Make sure the camera state is up-to-date + updateCameraState(); + + mat4 worldToCamera = camera.getWorldToCamera(scale, viewportMode == ViewportMode::FlippedY); + mat4 cameraToClip = + camera.getCameraToClipPerspective(getFieldOfView(), double(size.width) / size.height, nearZ, farZ); // Move the center of perspective to center of specified edgeInsets. // Values are in range [-1, 1] where the upper and lower range values // position viewport center to the screen edges. This is overriden // if using axonometric perspective (not in public API yet, Issue #11882). // TODO(astojilj): Issue #11882 should take edge insets into account, too. - projMatrix[8] = -offset.x * 2.0 / size.width; - projMatrix[9] = offset.y * 2.0 / size.height; - - const bool flippedY = viewportMode == ViewportMode::FlippedY; - matrix::scale(projMatrix, projMatrix, 1.0, flippedY ? 1 : -1, 1); + if (!axonometric) { + cameraToClip[8] = -offset.x * 2.0 / size.width; + cameraToClip[9] = offset.y * 2.0 / size.height; + } - matrix::translate(projMatrix, projMatrix, 0, 0, -cameraToCenterDistance); + // Apply north orientation angle + if (getNorthOrientation() != NorthOrientation::Upwards) { + matrix::rotate_z(cameraToClip, cameraToClip, -getNorthOrientationAngle()); + } - using NO = NorthOrientation; - switch (getNorthOrientation()) { - case NO::Rightwards: - matrix::rotate_y(projMatrix, projMatrix, getPitch()); - break; - case NO::Downwards: - matrix::rotate_x(projMatrix, projMatrix, -getPitch()); - break; - case NO::Leftwards: - matrix::rotate_y(projMatrix, projMatrix, -getPitch()); - break; - default: - matrix::rotate_x(projMatrix, projMatrix, getPitch()); - break; - } - - matrix::rotate_z(projMatrix, projMatrix, getBearing() + getNorthOrientationAngle()); - - const double dx = pixel_x() - size.width / 2.0f; - const double dy = pixel_y() - size.height / 2.0f; - matrix::translate(projMatrix, projMatrix, dx, dy, 0); + matrix::multiply(projMatrix, cameraToClip, worldToCamera); if (axonometric) { // mat[11] controls perspective - projMatrix[11] = 0; + projMatrix[11] = 0.0; // mat[8], mat[9] control x-skew, y-skew - projMatrix[8] = xSkew; - projMatrix[9] = ySkew; + double pixelsPerMeter = 1.0 / Projection::getMetersPerPixelAtLatitude(getLatLng().latitude(), getZoom()); + projMatrix[8] = xSkew * pixelsPerMeter; + projMatrix[9] = ySkew * pixelsPerMeter; } - matrix::scale(projMatrix, projMatrix, 1, 1, - 1.0 / Projection::getMetersPerPixelAtLatitude(getLatLng(LatLng::Unwrapped).latitude(), getZoom())); - // 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. + if (aligned) { + const double worldSize = Projection::worldSize(scale); + const double dx = x - 0.5 * worldSize; + const double dy = y - 0.5 * worldSize; + const float xShift = float(size.width % 2) / 2; const float yShift = float(size.height % 2) / 2; const double bearingCos = std::cos(bearing); @@ -168,6 +167,65 @@ void TransformState::getProjMatrix(mat4& projMatrix, uint16_t nearZ, bool aligne } } +void TransformState::updateCameraState() const { + if (!valid()) { + return; + } + + const double worldSize = Projection::worldSize(scale); + const double cameraToCenterDistance = getCameraToCenterDistance(); + + // x & y tracks the center of the map in pixels. However as rendering is done in pixel coordinates the rendering + // origo is actually in the middle of the map (0.5 * worldSize). x&y positions have to be negated because it defines + // position of the map, not the camera. Moving map 10 units left has the same effect as moving camera 10 units to the + // right. + const double dx = 0.5 * worldSize - x; + const double dy = 0.5 * worldSize - y; + + // Set camera orientation and move it to a proper distance from the map + camera.setOrientation(getPitch(), getBearing()); + + const vec3 forward = camera.forward(); + const vec3 orbitPosition = {{-forward[0] * cameraToCenterDistance, + -forward[1] * cameraToCenterDistance, + -forward[2] * cameraToCenterDistance}}; + vec3 cameraPosition = {{dx + orbitPosition[0], dy + orbitPosition[1], orbitPosition[2]}}; + + cameraPosition[0] /= worldSize; + cameraPosition[1] /= worldSize; + cameraPosition[2] /= worldSize; + + camera.setPosition(cameraPosition); +} + +void TransformState::updateStateFromCamera() { + const vec3 position = camera.getPosition(); + const vec3 forward = camera.forward(); + + const double dx = forward[0]; + const double dy = forward[1]; + const double dz = forward[2]; + assert(position[2] > 0.0 && dz < 0.0); + + // Compute bearing and pitch + double newBearing; + double newPitch; + camera.getOrientation(newPitch, newBearing); + newPitch = util::clamp(newPitch, minPitch, maxPitch); + + // Compute zoom level from the camera altitude + const double centerDistance = getCameraToCenterDistance(); + const double zoom = util::log2(centerDistance / (position[2] / std::cos(newPitch) * util::tileSize)); + const double newScale = util::clamp(std::pow(2.0, zoom), min_scale, max_scale); + + // Compute center point of the map + const double travel = -position[2] / dz; + const Point mercatorPoint = {position[0] + dx * travel, position[1] + dy * travel}; + + setLatLngZoom(latLngFromMercator(mercatorPoint), scaleZoom(newScale)); + setBearing(newBearing); + setPitch(newPitch); +} void TransformState::updateMatricesIfNeeded() const { if (!needsMatricesUpdate() || size.isEmpty()) return; diff --git a/src/mbgl/map/transform_state.hpp b/src/mbgl/map/transform_state.hpp index 32d5ef772f..aade9be098 100644 --- a/src/mbgl/map/transform_state.hpp +++ b/src/mbgl/map/transform_state.hpp @@ -1,13 +1,14 @@ #pragma once -#include #include +#include +#include +#include #include #include -#include +#include #include #include -#include #include #include @@ -248,6 +249,9 @@ private: void updateMatricesIfNeeded() const; bool needsMatricesUpdate() const { return requestMatricesUpdate; } + void updateCameraState() const; + void updateStateFromCamera(); + const mat4& getCoordMatrix() const; const mat4& getInvertedMatrix() const; @@ -276,6 +280,7 @@ private: bool axonometric = false; EdgeInsets edgeInsets; + mutable util::Camera camera; // cache values for spherical mercator math double Bc = Projection::worldSize(scale) / util::DEGREES_MAX; diff --git a/src/mbgl/util/camera.cpp b/src/mbgl/util/camera.cpp new file mode 100644 index 0000000000..f64e24ab2d --- /dev/null +++ b/src/mbgl/util/camera.cpp @@ -0,0 +1,215 @@ +#include "camera.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace mbgl { + +namespace { +double vec2Len(const vec2& v) { + return std::sqrt(v[0] * v[0] + v[1] * v[1]); +}; + +double vec2Dot(const vec2& a, const vec2& b) { + return a[0] * b[0] + a[1] * b[1]; +}; + +vec2 vec2Scale(const vec2& v, double s) { + return vec2{{v[0] * s, v[1] * s}}; +}; +} // namespace + +namespace util { + +static double mercatorXfromLng(double lng) { + return (180.0 + lng) / 360.0; +} + +static double mercatorYfromLat(double lat) { + return (180.0 - (180.0 / M_PI * std::log(std::tan(M_PI_4 + lat * M_PI / 360.0)))) / 360.0; +} + +static double latFromMercatorY(double y) { + return util::RAD2DEG * (2.0 * std::atan(std::exp(M_PI - y * util::M2PI)) - M_PI_2); +} + +static double lngFromMercatorX(double x) { + return x * 360.0 - 180.0; +} + +static double* getColumn(mat4& matrix, int col) { + assert(col >= 0 && col < 4); + return &matrix[col * 4]; +} + +static const double* getColumn(const mat4& matrix, int col) { + assert(col >= 0 && col < 4); + return &matrix[col * 4]; +} + +static vec3 toMercator(const LatLng& location, double altitudeMeters) { + const double pixelsPerMeter = 1.0 / Projection::getMetersPerPixelAtLatitude(location.latitude(), 0.0); + const double worldSize = Projection::worldSize(std::pow(2.0, 0.0)); + + return {{mercatorXfromLng(location.longitude()), + mercatorYfromLat(location.latitude()), + altitudeMeters * pixelsPerMeter / worldSize}}; +} + +static Quaternion orientationFromPitchBearing(double pitch, double bearing) { + // Both angles have to be negated to achieve CW rotation around the axis of rotation + Quaternion rotBearing = Quaternion::fromAxisAngle({{0.0, 0.0, 1.0}}, -bearing); + Quaternion rotPitch = Quaternion::fromAxisAngle({{1.0, 0.0, 0.0}}, -pitch); + + return rotBearing.multiply(rotPitch); +} + +static void updateTransform(mat4& transform, const Quaternion& orientation) { + // Construct rotation matrix from orientation + mat4 m = orientation.toRotationMatrix(); + + // Apply translation to the matrix + double* col = getColumn(m, 3); + + col[0] = getColumn(transform, 3)[0]; + col[1] = getColumn(transform, 3)[1]; + col[2] = getColumn(transform, 3)[2]; + + transform = m; +} + +static void updateTransform(mat4& transform, const vec3& position) { + getColumn(transform, 3)[0] = position[0]; + getColumn(transform, 3)[1] = position[1]; + getColumn(transform, 3)[2] = position[2]; +} + +Camera::Camera() : orientation(Quaternion::identity) { + matrix::identity(transform); +} + +vec3 Camera::getPosition() const { + const double* p = getColumn(transform, 3); + return {{p[0], p[1], p[2]}}; +} + +mat4 Camera::getCameraToWorld(double scale, bool flippedY) const { + mat4 cameraToWorld; + matrix::invert(cameraToWorld, getWorldToCamera(scale, flippedY)); + return cameraToWorld; +} + +mat4 Camera::getWorldToCamera(double scale, bool flippedY) const { + // transformation chain from world space to camera space: + // 1. Height value (z) of renderables is in meters. Scale z coordinate by pixelsPerMeter + // 2. Transform from pixel coordinates to camera space with cameraMatrix^-1 + // 3. flip Y if required + + // worldToCamera: flip * cam^-1 * zScale + // cameraToWorld: (flip * cam^-1 * zScale)^-1 => (zScale^-1 * cam * flip^-1) + const double worldSize = Projection::worldSize(scale); + const double latitude = latFromMercatorY(getColumn(transform, 3)[1]); + const double pixelsPerMeter = worldSize / (std::cos(latitude * util::DEG2RAD) * util::M2PI * util::EARTH_RADIUS_M); + + // Compute inverse of the camera matrix + mat4 result = orientation.conjugate().toRotationMatrix(); + + matrix::translate( + result, result, -transform[12] * worldSize, -transform[13] * worldSize, -transform[14] * worldSize); + + if (!flippedY) { + // Pre-multiply y + result[1] *= -1.0; + result[5] *= -1.0; + result[9] *= -1.0; + result[13] *= -1.0; + } + + // Post-multiply z + result[8] *= pixelsPerMeter; + result[9] *= pixelsPerMeter; + result[10] *= pixelsPerMeter; + result[11] *= pixelsPerMeter; + + return result; +} + +mat4 Camera::getCameraToClipPerspective(double fovy, double aspectRatio, double nearZ, double farZ) const { + mat4 projection; + matrix::perspective(projection, fovy, aspectRatio, nearZ, farZ); + return projection; +} + +vec3 Camera::forward() const { + const double* column = getColumn(transform, 2); + // The forward direction is towards the map, [0, 0, -1] + return {{-column[0], -column[1], -column[2]}}; +} + +vec3 Camera::right() const { + const double* column = getColumn(transform, 0); + return {{column[0], column[1], column[2]}}; +} + +vec3 Camera::up() const { + const double* column = getColumn(transform, 1); + // Up direction has to be flipped due to y-axis pointing towards south + return {{-column[0], -column[1], -column[2]}}; +} + +void Camera::getOrientation(double& pitch, double& bearing) const { + const vec3 f = forward(); + const vec3 r = right(); + + bearing = std::atan2(-r[1], r[0]); + pitch = std::atan2(std::sqrt(f[0] * f[0] + f[1] * f[1]), -f[2]); +} + +void Camera::setOrientation(double pitch, double bearing) { + orientation = orientationFromPitchBearing(pitch, bearing); + updateTransform(transform, orientation); +} + +void Camera::setOrientation(const Quaternion& orientation_) { + orientation = orientation_; + updateTransform(transform, orientation); +} + +void Camera::setPosition(const vec3& position) { + updateTransform(transform, position); +} + +optional Camera::orientationFromFrame(const vec3& forward, const vec3& up) { + vec3 upVector = up; + + vec2 xyForward = {{forward[0], forward[1]}}; + vec2 xyUp = {{up[0], up[1]}}; + const double epsilon = 1e-15; + + // Remove roll-component of the resulting orientation by projecting + // the up-vector to the forward vector on xy-plane + if (vec2Len(xyForward) >= epsilon) { + const vec2 xyDir = vec2Scale(xyForward, 1.0 / vec2Len(xyForward)); + + xyUp = vec2Scale(xyDir, vec2Dot(xyUp, xyDir)); + upVector[0] = xyUp[0]; + upVector[1] = xyUp[1]; + } + + const vec3 right = vec3Cross(upVector, forward); + + if (vec3Length(right) < epsilon) { + return nullopt; + } + + const double bearing = std::atan2(-right[1], right[0]); + const double pitch = std::atan2(std::sqrt(forward[0] * forward[0] + forward[1] * forward[1]), -forward[2]); + + return util::orientationFromPitchBearing(pitch, bearing); +} +} // namespace util +} // namespace mbgl \ No newline at end of file diff --git a/src/mbgl/util/camera.hpp b/src/mbgl/util/camera.hpp new file mode 100644 index 0000000000..030d7cfaac --- /dev/null +++ b/src/mbgl/util/camera.hpp @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include "quaternion.hpp" + +namespace mbgl { + +class LatLng; + +namespace util { + +class Camera { +public: + Camera(); + + vec3 getPosition() const; + mat4 getCameraToWorld(double scale, bool flippedY) const; + mat4 getWorldToCamera(double scale, bool flippedY) const; + mat4 getCameraToClipPerspective(double fovy, double aspectRatio, double nearZ, double farZ) const; + + vec3 forward() const; + vec3 right() const; + vec3 up() const; + + const Quaternion& getOrientation() const { return orientation; } + void getOrientation(double& pitch, double& bearing) const; + void setOrientation(double pitch, double bearing); + void setOrientation(const Quaternion& orientation_); + void setPosition(const vec3& position); + + // Computes orientation using forward and up vectors of the provided coordinate frame. + // Only bearing and pitch components will be used. Does not return a value if input is invalid + static optional orientationFromFrame(const vec3& forward, const vec3& up); + +private: + Quaternion orientation; + mat4 transform; // Position (mercator) and orientation of the camera +}; + +} // namespace util +} // namespace mbgl \ No newline at end of file -- cgit v1.2.1