From 3a2db7cbf5b60b0bf02e2e32d00325ae4ae7a16c Mon Sep 17 00:00:00 2001 From: Mikko Pulkki Date: Mon, 27 Apr 2020 13:32:03 +0300 Subject: Add unit tests for the free camera --- test/CMakeLists.txt | 1 + test/map/transform.test.cpp | 245 ++++++++++++++++++++++++++++++++++++++++- test/util/camera.test.cpp | 259 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 503 insertions(+), 2 deletions(-) create mode 100644 test/util/camera.test.cpp 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 +#include +#include #include #include - -#include +#include 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 rotatedFrame(const std::array& 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 + +#include +#include +#include + +#include + +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 rotatedFrame(const std::array& 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 -- cgit v1.2.1