summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMikko Pulkki <mikko.pulkki@mapbox.com>2020-04-27 13:32:03 +0300
committerMikko Pulkki <55925868+mpulkki-mapbox@users.noreply.github.com>2020-05-02 17:07:02 +0300
commit3a2db7cbf5b60b0bf02e2e32d00325ae4ae7a16c (patch)
treefc8f9c34cfe3fcf6e8f361dca258807e8df0f7b5
parent7b4a126cadbfacccb2de507d6d7cfaf18abf4b30 (diff)
downloadqtlocation-mapboxgl-3a2db7cbf5b60b0bf02e2e32d00325ae4ae7a16c.tar.gz
Add unit tests for the free camera
-rw-r--r--test/CMakeLists.txt1
-rw-r--r--test/map/transform.test.cpp245
-rw-r--r--test/util/camera.test.cpp259
3 files changed, 503 insertions, 2 deletions
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index 5bef6883c3..2ead99e4d8 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -82,6 +82,7 @@ add_library(
${PROJECT_SOURCE_DIR}/test/tile/vector_tile.test.cpp
${PROJECT_SOURCE_DIR}/test/util/async_task.test.cpp
${PROJECT_SOURCE_DIR}/test/util/bounding_volumes.test.cpp
+ ${PROJECT_SOURCE_DIR}/test/util/camera.test.cpp
${PROJECT_SOURCE_DIR}/test/util/dtoa.test.cpp
${PROJECT_SOURCE_DIR}/test/util/geo.test.cpp
${PROJECT_SOURCE_DIR}/test/util/grid_index.test.cpp
diff --git a/test/map/transform.test.cpp b/test/map/transform.test.cpp
index 41bf31a032..f14099d2fb 100644
--- a/test/map/transform.test.cpp
+++ b/test/map/transform.test.cpp
@@ -1,9 +1,10 @@
#include <mbgl/test/util.hpp>
+#include <gmock/gmock.h>
+#include <cmath>
#include <mbgl/map/transform.hpp>
#include <mbgl/util/geo.hpp>
-
-#include <cmath>
+#include <mbgl/util/quaternion.hpp>
using namespace mbgl;
@@ -870,3 +871,243 @@ TEST(Transform, MinMaxPitch) {
transform.setMinPitch(60);
ASSERT_DOUBLE_EQ(45 * util::DEG2RAD, transform.getState().getMinPitch());
}
+
+static const double abs_double_error = 1e-5;
+
+MATCHER_P(Vec3NearEquals, vec, "") {
+ return std::fabs(vec[0] - arg[0]) <= abs_double_error && std::fabs(vec[1] - arg[1]) <= abs_double_error &&
+ std::fabs(vec[2] - arg[2]) <= abs_double_error;
+}
+
+TEST(Transform, FreeCameraOptionsInvalidSize) {
+ Transform transform;
+ FreeCameraOptions options;
+
+ options.orientation = vec4{{1.0, 1.0, 1.0, 1.0}};
+ options.position = vec3{{0.1, 0.2, 0.3}};
+ transform.setFreeCameraOptions(options);
+
+ const auto updatedOrientation = transform.getFreeCameraOptions().orientation.value();
+ const auto updatedPosition = transform.getFreeCameraOptions().position.value();
+
+ EXPECT_DOUBLE_EQ(0.0, updatedOrientation[0]);
+ EXPECT_DOUBLE_EQ(0.0, updatedOrientation[1]);
+ EXPECT_DOUBLE_EQ(0.0, updatedOrientation[2]);
+ EXPECT_DOUBLE_EQ(1.0, updatedOrientation[3]);
+
+ EXPECT_THAT(updatedPosition, Vec3NearEquals(vec3{{0.0, 0.0, 0.0}}));
+}
+
+TEST(Transform, FreeCameraOptionsNanInput) {
+ Transform transform;
+ transform.resize({100, 100});
+ FreeCameraOptions options;
+
+ options.position = vec3{{0.5, 0.5, 0.25}};
+ transform.setFreeCameraOptions(options);
+
+ options.position = vec3{{0.0, 0.0, NAN}};
+ transform.setFreeCameraOptions(options);
+ EXPECT_EQ((vec3{{0.5, 0.5, 0.25}}), transform.getFreeCameraOptions().position.value());
+
+ // Only the invalid parameter should be discarded
+ options.position = vec3{{0.3, 0.1, 0.2}};
+ options.orientation = vec4{{NAN, 0.0, NAN, 0.0}};
+ transform.setFreeCameraOptions(options);
+ EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals(vec3{{0.3, 0.1, 0.2}}));
+ EXPECT_EQ(Quaternion::identity.m, transform.getFreeCameraOptions().orientation.value());
+}
+
+TEST(Transform, FreeCameraOptionsInvalidZ) {
+ Transform transform;
+ transform.resize({100, 100});
+ FreeCameraOptions options;
+
+ // Invalid z-value (<= 0.0 || > 1) should be clamped to respect both min&max zoom values
+ options.position = vec3{{0.1, 0.1, 0.0}};
+ transform.setFreeCameraOptions(options);
+ EXPECT_DOUBLE_EQ(transform.getState().getMaxZoom(), transform.getState().getZoom());
+ EXPECT_GT(transform.getFreeCameraOptions().position.value()[2], 0.0);
+
+ options.position = vec3{{0.5, 0.2, 123.456}};
+ transform.setFreeCameraOptions(options);
+ EXPECT_DOUBLE_EQ(transform.getState().getMinZoom(), transform.getState().getZoom());
+ EXPECT_LE(transform.getFreeCameraOptions().position.value()[2], 1.0);
+}
+
+TEST(Transform, FreeCameraOptionsInvalidOrientation) {
+ // Invalid orientations that cannot be clamped into a valid range
+ Transform transform;
+ transform.resize({100, 100});
+
+ FreeCameraOptions options;
+ options.orientation = vec4{{0.0, 0.0, 0.0, 0.0}};
+ transform.setFreeCameraOptions(options);
+ EXPECT_EQ(Quaternion::identity.m, transform.getFreeCameraOptions().orientation);
+
+ // Gimbal lock. Both forward and up vectors are on xy-plane
+ options.orientation = Quaternion::fromAxisAngle(vec3{{0.0, 1.0, 0.0}}, M_PI_2).m;
+ transform.setFreeCameraOptions(options);
+ EXPECT_EQ(Quaternion::identity.m, transform.getFreeCameraOptions().orientation);
+
+ // Camera is upside down
+ options.orientation = Quaternion::fromAxisAngle(vec3{{1.0, 0.0, 0.0}}, M_PI_2 + M_PI_4).m;
+ transform.setFreeCameraOptions(options);
+ EXPECT_EQ(Quaternion::identity.m, transform.getFreeCameraOptions().orientation);
+}
+
+TEST(Transform, FreeCameraOptionsSetOrientation) {
+ Transform transform;
+ transform.resize({100, 100});
+ FreeCameraOptions options;
+
+ options.orientation = Quaternion::identity.m;
+ transform.setFreeCameraOptions(options);
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getBearing());
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getPitch());
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getX());
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getY());
+
+ options.orientation = Quaternion::fromAxisAngle(vec3{{1.0, 0.0, 0.0}}, -60.0 * util::DEG2RAD).m;
+ transform.setFreeCameraOptions(options);
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getBearing());
+ EXPECT_DOUBLE_EQ(60.0 * util::DEG2RAD, transform.getState().getPitch());
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getX());
+ EXPECT_DOUBLE_EQ(206.0, transform.getState().getY());
+
+ options.orientation = Quaternion::fromAxisAngle(vec3{{0.0, 0.0, 1.0}}, 56.0 * util::DEG2RAD).m;
+ transform.setFreeCameraOptions(options);
+ EXPECT_DOUBLE_EQ(-56.0 * util::DEG2RAD, transform.getState().getBearing());
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getPitch());
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getX());
+ EXPECT_NEAR(152.192378, transform.getState().getY(), 1e-6);
+
+ options.orientation = Quaternion::fromEulerAngles(0.0, 0.0, -179.0 * util::DEG2RAD)
+ .multiply(Quaternion::fromEulerAngles(-30.0 * util::DEG2RAD, 0.0, 0.0))
+ .m;
+ transform.setFreeCameraOptions(options);
+ EXPECT_DOUBLE_EQ(179.0 * util::DEG2RAD, transform.getState().getBearing());
+ EXPECT_DOUBLE_EQ(30.0 * util::DEG2RAD, transform.getState().getPitch());
+ EXPECT_NEAR(1.308930, transform.getState().getX(), 1e-6);
+ EXPECT_NEAR(56.813889, transform.getState().getY(), 1e-6);
+}
+
+static std::tuple<vec3, vec3, vec3> rotatedFrame(const std::array<double, 4>& quaternion) {
+ Quaternion q(quaternion);
+ return std::make_tuple(
+ q.transform({{1.0, 0.0, 0.0}}), q.transform({{0.0, -1.0, 0.0}}), q.transform({{0.0, 0.0, -1.0}}));
+}
+
+TEST(Transform, FreeCameraOptionsClampPitch) {
+ Transform transform;
+ transform.resize({100, 100});
+ FreeCameraOptions options;
+ vec3 right, up, forward;
+
+ options.orientation = Quaternion::fromAxisAngle(vec3{{1.0, 0.0, 0.0}}, -85.0 * util::DEG2RAD).m;
+ transform.setFreeCameraOptions(options);
+ EXPECT_DOUBLE_EQ(util::PITCH_MAX, transform.getState().getPitch());
+ std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value());
+ EXPECT_THAT(right, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+ EXPECT_THAT(up, Vec3NearEquals(vec3{{0, -0.5, 0.866025}}));
+ EXPECT_THAT(forward, Vec3NearEquals(vec3{{0, -0.866025, -0.5}}));
+}
+
+TEST(Transform, FreeCameraOptionsClampToBounds) {
+ Transform transform;
+ transform.resize({100, 100});
+ transform.setConstrainMode(ConstrainMode::WidthAndHeight);
+ transform.jumpTo(CameraOptions().withZoom(8.56));
+ FreeCameraOptions options;
+
+ // Place camera to an arbitrary position looking away from the map
+ options.position = vec3{{-100.0, -10000.0, 1000.0}};
+ options.orientation = Quaternion::fromEulerAngles(-45.0 * util::DEG2RAD, 0.0, 0.0).m;
+ transform.setFreeCameraOptions(options);
+
+ // Map center should be clamped to width/2 pixels away from map borders
+ EXPECT_DOUBLE_EQ(206.0, transform.getState().getX());
+ EXPECT_DOUBLE_EQ(206.0, transform.getState().getY());
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getBearing());
+ EXPECT_DOUBLE_EQ(45.0 * util::DEG2RAD, transform.getState().getPitch());
+
+ vec3 right, up, forward;
+ std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value());
+ EXPECT_THAT(transform.getFreeCameraOptions().position.value(),
+ Vec3NearEquals(vec3{{0.0976562, 0.304816, 0.20716}}));
+ EXPECT_THAT(right, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+ EXPECT_THAT(up, Vec3NearEquals(vec3{{0, -0.707107, 0.707107}}));
+ EXPECT_THAT(forward, Vec3NearEquals(vec3{{0, -0.707107, -0.707107}}));
+}
+
+TEST(Transform, FreeCameraOptionsInvalidState) {
+ Transform transform;
+
+ // Invalid size
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getX());
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getY());
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getBearing());
+ EXPECT_DOUBLE_EQ(0.0, transform.getState().getPitch());
+
+ const auto options = transform.getFreeCameraOptions();
+ EXPECT_THAT(options.position.value(), Vec3NearEquals(vec3{{0.0, 0.0, 0.0}}));
+}
+
+TEST(Transform, FreeCameraOptionsOrientationRoll) {
+ Transform transform;
+ FreeCameraOptions options;
+ transform.resize({100, 100});
+
+ const auto orientationWithoutRoll = Quaternion::fromEulerAngles(-M_PI_4, 0.0, 0.0);
+ const auto orientationWithRoll = orientationWithoutRoll.multiply(Quaternion::fromEulerAngles(0.0, 0.0, M_PI_4));
+
+ options.orientation = orientationWithRoll.m;
+ transform.setFreeCameraOptions(options);
+ options = transform.getFreeCameraOptions();
+
+ EXPECT_NEAR(options.orientation.value()[0], orientationWithoutRoll.x, 1e-9);
+ EXPECT_NEAR(options.orientation.value()[1], orientationWithoutRoll.y, 1e-9);
+ EXPECT_NEAR(options.orientation.value()[2], orientationWithoutRoll.z, 1e-9);
+ EXPECT_NEAR(options.orientation.value()[3], orientationWithoutRoll.w, 1e-9);
+
+ EXPECT_NEAR(45.0 * util::DEG2RAD, transform.getState().getPitch(), 1e-9);
+ EXPECT_NEAR(0.0, transform.getState().getBearing(), 1e-9);
+ EXPECT_NEAR(0.0, transform.getState().getX(), 1e-9);
+ EXPECT_NEAR(150.0, transform.getState().getY(), 1e-9);
+}
+
+TEST(Transform, FreeCameraOptionsStateSynchronization) {
+ Transform transform;
+ transform.resize({100, 100});
+ vec3 right, up, forward;
+
+ transform.jumpTo(CameraOptions().withPitch(0.0).withBearing(0.0));
+ std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value());
+ EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals(vec3{{0.5, 0.5, 0.29296875}}));
+ EXPECT_THAT(right, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+ EXPECT_THAT(up, Vec3NearEquals(vec3{{0.0, -1.0, 0.0}}));
+ EXPECT_THAT(forward, Vec3NearEquals(vec3{{0.0, 0.0, -1.0}}));
+
+ transform.jumpTo(CameraOptions().withCenter(LatLng{60.1699, 24.9384}));
+ EXPECT_THAT(transform.getFreeCameraOptions().position.value(),
+ Vec3NearEquals(vec3{{0.569273, 0.289453, 0.292969}}));
+
+ transform.jumpTo(CameraOptions().withPitch(20.0).withBearing(77.0).withCenter(LatLng{-20.0, 20.0}));
+ EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals(vec3{{0.457922, 0.57926, 0.275301}}));
+
+ // Invalid pitch
+ transform.jumpTo(CameraOptions().withPitch(-10.0).withBearing(0.0));
+ std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value());
+ EXPECT_THAT(transform.getFreeCameraOptions().position.value(),
+ Vec3NearEquals(vec3{{0.555556, 0.556719, 0.292969}}));
+ EXPECT_THAT(right, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+ EXPECT_THAT(up, Vec3NearEquals(vec3{{0.0, -1.0, 0.0}}));
+ EXPECT_THAT(forward, Vec3NearEquals(vec3{{0.0, 0.0, -1.0}}));
+
+ transform.jumpTo(CameraOptions().withPitch(85.0).withBearing(0.0).withCenter(LatLng{-80.0, 0.0}));
+ std::tie(right, up, forward) = rotatedFrame(transform.getFreeCameraOptions().orientation.value());
+ EXPECT_THAT(transform.getFreeCameraOptions().position.value(), Vec3NearEquals(vec3{{0.5, 1.14146, 0.146484}}));
+ EXPECT_THAT(right, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+ EXPECT_THAT(up, Vec3NearEquals(vec3{{0, -0.5, 0.866025}}));
+ EXPECT_THAT(forward, Vec3NearEquals(vec3{{0, -0.866025, -0.5}}));
+} \ No newline at end of file
diff --git a/test/util/camera.test.cpp b/test/util/camera.test.cpp
new file mode 100644
index 0000000000..ceed437a39
--- /dev/null
+++ b/test/util/camera.test.cpp
@@ -0,0 +1,259 @@
+#include <mbgl/test/util.hpp>
+
+#include <mbgl/map/camera.hpp>
+#include <mbgl/util/mat3.hpp>
+#include <mbgl/util/quaternion.hpp>
+
+#include <gmock/gmock.h>
+
+using namespace mbgl;
+
+static const double abs_double_error = 1e-7;
+
+MATCHER_P(Vec3NearEquals, vec, "") {
+ return std::fabs(vec[0] - arg[0]) <= abs_double_error && std::fabs(vec[1] - arg[1]) <= abs_double_error &&
+ std::fabs(vec[2] - arg[2]) <= abs_double_error;
+}
+
+TEST(FreeCameraOptions, SetLocation) {
+ FreeCameraOptions options;
+
+ options.setLocation({{0.0, 0.0}, util::EARTH_RADIUS_M * M_PI});
+ ASSERT_TRUE(options.position);
+ ASSERT_THAT(options.position.value(), Vec3NearEquals(vec3{{0.5, 0.5, 0.5}}));
+
+ options.setLocation({{25.0, -180.0}, 1000.0});
+ ASSERT_TRUE(options.position);
+ ASSERT_THAT(options.position.value(), Vec3NearEquals(vec3{{0.0, 0.4282409625, 0.000027532812465}}));
+
+ options.setLocation(
+ {{util::LATITUDE_MAX, 0.0}, util::EARTH_RADIUS_M * M_PI * std::cos(util::LATITUDE_MAX * util::DEG2RAD)});
+ ASSERT_TRUE(options.position);
+ ASSERT_THAT(options.position.value(), Vec3NearEquals(vec3{{0.5, 0.0, 0.5}}));
+
+ options.setLocation(
+ {{-util::LATITUDE_MAX, 0.0}, util::EARTH_RADIUS_M * M_PI * std::cos(-util::LATITUDE_MAX * util::DEG2RAD)});
+ ASSERT_TRUE(options.position);
+ ASSERT_THAT(options.position.value(), Vec3NearEquals(vec3{{0.5, 1.0, 0.5}}));
+}
+
+TEST(FreeCameraOptions, SetLocationNegativeAltitude) {
+ FreeCameraOptions options;
+ options.setLocation({{0.0, 0.0}, -100.0});
+ ASSERT_TRUE(options.position);
+ ASSERT_DOUBLE_EQ(options.position.value()[2], -100.0 / util::M2PI / util::EARTH_RADIUS_M);
+}
+
+TEST(FreeCameraOptions, SetLocationUnwrappedLocation) {
+ FreeCameraOptions options;
+
+ options.setLocation({{0.0, -540.0}, 0.0});
+ ASSERT_TRUE(options.position);
+ ASSERT_THAT(options.position.value(), Vec3NearEquals(vec3{{-1.0, 0.5, 0.0}}));
+}
+
+TEST(FreeCameraOptions, GetLocation) {
+ FreeCameraOptions options;
+ LatLngAltitude latLngAltitude;
+
+ options.position = vec3{{0.5, 0.5, 0.5}};
+ ASSERT_TRUE(options.getLocation());
+ latLngAltitude = options.getLocation().value();
+ ASSERT_DOUBLE_EQ(latLngAltitude.location.latitude(), 0.0);
+ ASSERT_DOUBLE_EQ(latLngAltitude.location.longitude(), 0.0);
+ ASSERT_DOUBLE_EQ(latLngAltitude.altitude, 20037508.342789244);
+
+ options.position = vec3{{0.0, 0.4282409625, 0.000027532812465}};
+ ASSERT_TRUE(options.getLocation());
+ latLngAltitude = options.getLocation().value();
+ ASSERT_NEAR(latLngAltitude.location.latitude(), 25.0, 1e-7);
+ ASSERT_NEAR(latLngAltitude.location.longitude(), -180.0, 1e-7);
+ ASSERT_NEAR(latLngAltitude.altitude, 1000.0, 1e-7);
+
+ options.position = vec3{{0.5, 0.0, 0.5}};
+ ASSERT_TRUE(options.getLocation());
+ latLngAltitude = options.getLocation().value();
+ ASSERT_NEAR(latLngAltitude.location.latitude(), util::LATITUDE_MAX, 1e-7);
+ ASSERT_NEAR(latLngAltitude.location.longitude(), 0.0, 1e-7);
+ ASSERT_NEAR(latLngAltitude.altitude, 1728570.489074, 1e-6);
+}
+
+TEST(FreeCameraOptions, GetLocationInvalidPosition) {
+ FreeCameraOptions options;
+ // Mercator position not set. Should return nothing
+ ASSERT_FALSE(options.getLocation());
+
+ // Invalid latitude
+ options.position = vec3{{0.0, -0.1, 0.0}};
+ ASSERT_FALSE(options.getLocation());
+ options.position = vec3{{0.0, 2.0, 0.0}};
+ ASSERT_FALSE(options.getLocation());
+}
+
+TEST(FreeCameraOptions, GetLocationNegativeAltitude) {
+ FreeCameraOptions options;
+ options.position = vec3{{0.5, 0.5, -0.25}};
+ const auto latLngAltitude = options.getLocation();
+ ASSERT_TRUE(latLngAltitude);
+
+ ASSERT_DOUBLE_EQ(latLngAltitude->altitude, -0.5 * util::EARTH_RADIUS_M * M_PI);
+}
+
+TEST(FreeCameraOptions, GetLocationUnwrappedPosition) {
+ FreeCameraOptions options;
+ options.position = vec3{{1.25, 0.5, 0.0}};
+ const auto latLngAltitude = options.getLocation();
+ ASSERT_TRUE(latLngAltitude);
+ ASSERT_DOUBLE_EQ(latLngAltitude->location.longitude(), 270.0);
+ ASSERT_DOUBLE_EQ(latLngAltitude->location.latitude(), 0.0);
+ ASSERT_DOUBLE_EQ(latLngAltitude->altitude, 0.0);
+}
+
+static std::tuple<vec3, vec3, vec3> rotatedFrame(const std::array<double, 4>& quaternion) {
+ Quaternion q(quaternion);
+ return std::make_tuple(
+ q.transform({{1.0, 0.0, 0.0}}), q.transform({{0.0, -1.0, 0.0}}), q.transform({{0.0, 0.0, -1.0}}));
+}
+
+TEST(FreeCameraOptions, LookAtPoint) {
+ FreeCameraOptions options;
+ vec3 right, up, forward;
+ const double cosPi4 = 1.0 / std::sqrt(2.0);
+
+ // Pitch: 45, bearing: 0
+ options.position = vec3{{0.5, 0.5, 0.5}};
+ options.lookAtPoint({util::LATITUDE_MAX, 0.0});
+ ASSERT_TRUE(options.orientation);
+ std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
+
+ ASSERT_THAT(right, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+ ASSERT_THAT(up, Vec3NearEquals(vec3{{0.0, -cosPi4, cosPi4}}));
+ ASSERT_THAT(forward, Vec3NearEquals(vec3{{0.0, -cosPi4, -cosPi4}}));
+
+ // Look directly to east
+ options.position = vec3{{0.5, 0.5, 0.0}};
+ options.lookAtPoint({0.0, 30.0});
+ ASSERT_TRUE(options.orientation);
+ std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
+
+ ASSERT_THAT(right, Vec3NearEquals(vec3{{0.0, 1.0, 0.0}}));
+ ASSERT_THAT(up, Vec3NearEquals(vec3{{0.0, 0.0, 1.0}}));
+ ASSERT_THAT(forward, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+
+ // Pitch: 0, bearing: 0
+ options.setLocation({{60.1699, 24.9384}, 100.0});
+ options.lookAtPoint({60.1699, 24.9384}, vec3{{0.0, -1.0, 0.0}});
+ ASSERT_TRUE(options.orientation);
+ std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
+
+ ASSERT_THAT(right, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+ ASSERT_THAT(up, Vec3NearEquals(vec3{{0.0, -1.0, 0.0}}));
+ ASSERT_THAT(forward, Vec3NearEquals(vec3{{0.0, 0.0, -1.0}}));
+
+ // Pitch: 0, bearing: 45
+ options.setLocation({{60.1699, 24.9384}, 100.0});
+ options.lookAtPoint({60.1699, 24.9384}, vec3{{-1.0, -1.0, 0.0}});
+ ASSERT_TRUE(options.orientation);
+ std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
+
+ ASSERT_THAT(right, Vec3NearEquals(vec3{{cosPi4, -cosPi4, 0.0}}));
+ ASSERT_THAT(up, Vec3NearEquals(vec3{{-cosPi4, -cosPi4, 0.0}}));
+ ASSERT_THAT(forward, Vec3NearEquals(vec3{{0.0, 0.0, -1.0}}));
+
+ // Looking south, up vector almost same as forward vector
+ options.setLocation({{37.7749, 122.4194}, 0.0});
+ options.lookAtPoint({37.5, 122.4194}, vec3{{0.0, 1.0, 0.00001}});
+ ASSERT_TRUE(options.orientation);
+ std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
+
+ ASSERT_THAT(right, Vec3NearEquals(vec3{{-1.0, 0.0, 0.0}}));
+ ASSERT_THAT(up, Vec3NearEquals(vec3{{0.0, 0.0, 1.0}}));
+ ASSERT_THAT(forward, Vec3NearEquals(vec3{{0.0, 1.0, 0.0}}));
+
+ // Orientation with roll-component
+ options.setLocation({{-33.8688, 151.2093}, 0.0});
+ options.lookAtPoint({-33.8688, 160.0}, vec3{{0.0, -1.0, 0.1}});
+ ASSERT_TRUE(options.orientation);
+ std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
+
+ ASSERT_THAT(right, Vec3NearEquals(vec3{{0.0, 1.0, 0.0}}));
+ ASSERT_THAT(up, Vec3NearEquals(vec3{{0.0, 0.0, 1.0}}));
+ ASSERT_THAT(forward, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+
+ // Up vector pointing downwards
+ options.position = vec3{{0.5, 0.5, 0.5}};
+ options.lookAtPoint({util::LATITUDE_MAX, 0.0}, vec3{{0.0, 0.0, -0.5}});
+ ASSERT_TRUE(options.orientation);
+ std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
+
+ ASSERT_THAT(right, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+ ASSERT_THAT(up, Vec3NearEquals(vec3{{0.0, -cosPi4, cosPi4}}));
+ ASSERT_THAT(forward, Vec3NearEquals(vec3{{0.0, -cosPi4, -cosPi4}}));
+}
+
+TEST(FreeCameraOptions, LookAtPointInvalidInput) {
+ FreeCameraOptions options;
+
+ // Position not set
+ options.orientation = vec4{{0.0, 0.0, 0.0, 0.0}};
+ options.lookAtPoint({0.0, 0.0});
+ ASSERT_FALSE(options.orientation);
+
+ // Target same as position
+ options.orientation = vec4{{0.0, 0.0, 0.0, 0.0}};
+ options.position = vec3{{0.5, 0.5, 0.0}};
+ options.lookAtPoint({0.0, 0.0});
+ ASSERT_FALSE(options.orientation);
+
+ // Camera looking directly down without an explicit up vector
+ options.orientation = vec4{{0.0, 0.0, 0.0, 0.0}};
+ options.position = vec3{{0.5, 0.5, 0.5}};
+ options.lookAtPoint({0.0, 0.0});
+ ASSERT_FALSE(options.orientation);
+
+ // Zero up vector
+ options.orientation = vec4{{0.0, 0.0, 0.0, 0.0}};
+ options.lookAtPoint({0.0, 0.0}, vec3{{0.0, 0.0, 0.0}});
+ ASSERT_FALSE(options.orientation);
+
+ // Up vector same as direction
+ options.orientation = vec4{{0.0, 0.0, 0.0, 0.0}};
+ options.lookAtPoint({0.0, 0.0}, vec3{{0.0, 0.0, -1.0}});
+ ASSERT_FALSE(options.orientation);
+}
+
+TEST(FreeCameraOptions, SetPitchBearing) {
+ FreeCameraOptions options;
+ vec3 right, up, forward;
+
+ options.setPitchBearing(0.0, 0.0);
+ ASSERT_TRUE(options.orientation);
+ std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
+ ASSERT_THAT(right, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+ ASSERT_THAT(up, Vec3NearEquals(vec3{{0.0, -1.0, 0.0}}));
+ ASSERT_THAT(forward, Vec3NearEquals(vec3{{0.0, 0.0, -1.0}}));
+
+ options.setPitchBearing(0.0, 180.0);
+ ASSERT_TRUE(options.orientation);
+ std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
+ ASSERT_THAT(right, Vec3NearEquals(vec3{{-1.0, 0.0, 0.0}}));
+ ASSERT_THAT(up, Vec3NearEquals(vec3{{0.0, 1.0, 0.0}}));
+ ASSERT_THAT(forward, Vec3NearEquals(vec3{{0.0, 0.0, -1.0}}));
+
+ const double cos60 = std::cos(60.0 * util::DEG2RAD);
+ const double sin60 = std::sin(60.0 * util::DEG2RAD);
+
+ options.setPitchBearing(60.0, 0.0);
+ ASSERT_TRUE(options.orientation);
+ std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
+ ASSERT_THAT(right, Vec3NearEquals(vec3{{1.0, 0.0, 0.0}}));
+ ASSERT_THAT(up, Vec3NearEquals(vec3{{0.0, -cos60, sin60}}));
+ ASSERT_THAT(forward, Vec3NearEquals(vec3{{0.0, -sin60, -cos60}}));
+
+ options.setPitchBearing(60.0, -450);
+ ASSERT_TRUE(options.orientation);
+ std::tie(right, up, forward) = rotatedFrame(options.orientation.value());
+ ASSERT_THAT(right, Vec3NearEquals(vec3{{0.0, 1.0, 0.0}}));
+ ASSERT_THAT(up, Vec3NearEquals(vec3{{cos60, 0.0, sin60}}));
+ ASSERT_THAT(forward, Vec3NearEquals(vec3{{sin60, 0.0, -cos60}}));
+} \ No newline at end of file