From 2e35ecce9c010381ca85e94b7445673fc19efe4b Mon Sep 17 00:00:00 2001 From: CHM <56677134+chocola-mint@users.noreply.github.com> Date: Wed, 8 Nov 2023 15:38:31 +0800 Subject: [PATCH] Add tests for Camera3D --- tests/scene/test_camera_3d.h | 370 +++++++++++++++++++++++++++++++++++ tests/test_main.cpp | 1 + 2 files changed, 371 insertions(+) create mode 100644 tests/scene/test_camera_3d.h diff --git a/tests/scene/test_camera_3d.h b/tests/scene/test_camera_3d.h new file mode 100644 index 00000000000..169486d1082 --- /dev/null +++ b/tests/scene/test_camera_3d.h @@ -0,0 +1,370 @@ +/**************************************************************************/ +/* test_camera_3d.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_CAMERA_3D_H +#define TEST_CAMERA_3D_H + +#include "scene/3d/camera_3d.h" +#include "scene/main/viewport.h" +#include "scene/main/window.h" + +#include "tests/test_macros.h" + +// Constants. +#define SQRT3 (1.7320508f) + +TEST_CASE("[SceneTree][Camera3D] Getters and setters") { + Camera3D *test_camera = memnew(Camera3D); + + SUBCASE("Cull mask") { + constexpr int cull_mask = (1 << 5) | (1 << 7) | (1 << 9); + constexpr int set_enable_layer = 3; + constexpr int set_disable_layer = 5; + test_camera->set_cull_mask(cull_mask); + CHECK(test_camera->get_cull_mask() == cull_mask); + test_camera->set_cull_mask_value(set_enable_layer, true); + CHECK(test_camera->get_cull_mask_value(set_enable_layer)); + test_camera->set_cull_mask_value(set_disable_layer, false); + CHECK_FALSE(test_camera->get_cull_mask_value(set_disable_layer)); + } + + SUBCASE("Attributes") { + Ref attributes = memnew(CameraAttributes); + test_camera->set_attributes(attributes); + CHECK(test_camera->get_attributes() == attributes); + Ref physical_attributes = memnew(CameraAttributesPhysical); + test_camera->set_attributes(physical_attributes); + CHECK(test_camera->get_attributes() == physical_attributes); + } + + SUBCASE("Camera frustum properties") { + constexpr float near = 0.2f; + constexpr float far = 995.0f; + constexpr float fov = 120.0f; + constexpr float size = 7.0f; + constexpr float h_offset = 1.1f; + constexpr float v_offset = -1.6f; + const Vector2 frustum_offset(5, 7); + test_camera->set_near(near); + CHECK(test_camera->get_near() == near); + test_camera->set_far(far); + CHECK(test_camera->get_far() == far); + test_camera->set_fov(fov); + CHECK(test_camera->get_fov() == fov); + test_camera->set_size(size); + CHECK(test_camera->get_size() == size); + test_camera->set_h_offset(h_offset); + CHECK(test_camera->get_h_offset() == h_offset); + test_camera->set_v_offset(v_offset); + CHECK(test_camera->get_v_offset() == v_offset); + test_camera->set_frustum_offset(frustum_offset); + CHECK(test_camera->get_frustum_offset() == frustum_offset); + test_camera->set_keep_aspect_mode(Camera3D::KeepAspect::KEEP_HEIGHT); + CHECK(test_camera->get_keep_aspect_mode() == Camera3D::KeepAspect::KEEP_HEIGHT); + test_camera->set_keep_aspect_mode(Camera3D::KeepAspect::KEEP_WIDTH); + CHECK(test_camera->get_keep_aspect_mode() == Camera3D::KeepAspect::KEEP_WIDTH); + } + + SUBCASE("Projection mode") { + test_camera->set_projection(Camera3D::ProjectionType::PROJECTION_ORTHOGONAL); + CHECK(test_camera->get_projection() == Camera3D::ProjectionType::PROJECTION_ORTHOGONAL); + test_camera->set_projection(Camera3D::ProjectionType::PROJECTION_PERSPECTIVE); + CHECK(test_camera->get_projection() == Camera3D::ProjectionType::PROJECTION_PERSPECTIVE); + } + + SUBCASE("Helper setters") { + constexpr float fov = 90.0f, size = 6.0f; + constexpr float near1 = 0.1f, near2 = 0.5f; + constexpr float far1 = 1001.0f, far2 = 1005.0f; + test_camera->set_perspective(fov, near1, far1); + CHECK(test_camera->get_projection() == Camera3D::ProjectionType::PROJECTION_PERSPECTIVE); + CHECK(test_camera->get_near() == near1); + CHECK(test_camera->get_far() == far1); + CHECK(test_camera->get_fov() == fov); + test_camera->set_orthogonal(size, near2, far2); + CHECK(test_camera->get_projection() == Camera3D::ProjectionType::PROJECTION_ORTHOGONAL); + CHECK(test_camera->get_near() == near2); + CHECK(test_camera->get_far() == far2); + CHECK(test_camera->get_size() == size); + } + + SUBCASE("Doppler tracking") { + test_camera->set_doppler_tracking(Camera3D::DopplerTracking::DOPPLER_TRACKING_IDLE_STEP); + CHECK(test_camera->get_doppler_tracking() == Camera3D::DopplerTracking::DOPPLER_TRACKING_IDLE_STEP); + test_camera->set_doppler_tracking(Camera3D::DopplerTracking::DOPPLER_TRACKING_PHYSICS_STEP); + CHECK(test_camera->get_doppler_tracking() == Camera3D::DopplerTracking::DOPPLER_TRACKING_PHYSICS_STEP); + test_camera->set_doppler_tracking(Camera3D::DopplerTracking::DOPPLER_TRACKING_DISABLED); + CHECK(test_camera->get_doppler_tracking() == Camera3D::DopplerTracking::DOPPLER_TRACKING_DISABLED); + } + + memdelete(test_camera); +} + +TEST_CASE("[SceneTree][Camera3D] Position queries") { + // Cameras need a viewport to know how to compute their frustums, so we make a fake one here. + Camera3D *test_camera = memnew(Camera3D); + SubViewport *mock_viewport = memnew(SubViewport); + // 4:2. + mock_viewport->set_size(Vector2(400, 200)); + SceneTree::get_singleton()->get_root()->add_child(mock_viewport); + mock_viewport->add_child(test_camera); + test_camera->set_keep_aspect_mode(Camera3D::KeepAspect::KEEP_WIDTH); + REQUIRE_MESSAGE(test_camera->is_current(), "Camera3D should be made current upon entering tree."); + + SUBCASE("Orthogonal projection") { + test_camera->set_projection(Camera3D::ProjectionType::PROJECTION_ORTHOGONAL); + // The orthogonal case is simpler, so we test a more random position + rotation combination here. + // For the other cases we'll use zero translation and rotation instead. + test_camera->set_global_position(Vector3(1, 2, 3)); + test_camera->look_at(Vector3(-4, 5, 1)); + // Width = 5, Aspect Ratio = 400 / 200 = 2, so Height is 2.5. + test_camera->set_orthogonal(5.0f, 0.5f, 1000.0f); + const Basis basis = test_camera->get_global_basis(); + // Subtract near so offset starts from the near plane. + const Vector3 offset1 = basis.xform(Vector3(-1.5f, 3.5f, 0.2f - test_camera->get_near())); + const Vector3 offset2 = basis.xform(Vector3(2.0f, -0.5f, -0.6f - test_camera->get_near())); + const Vector3 offset3 = basis.xform(Vector3(-3.0f, 1.0f, -0.6f - test_camera->get_near())); + const Vector3 offset4 = basis.xform(Vector3(-2.0f, 1.5f, -0.6f - test_camera->get_near())); + const Vector3 offset5 = basis.xform(Vector3(0, 0, 10000.0f - test_camera->get_near())); + + SUBCASE("is_position_behind") { + CHECK(test_camera->is_position_behind(test_camera->get_global_position() + offset1)); + CHECK_FALSE(test_camera->is_position_behind(test_camera->get_global_position() + offset2)); + + SUBCASE("h/v offset should have no effect on the result of is_position_behind") { + test_camera->set_h_offset(-11.0f); + test_camera->set_v_offset(22.1f); + CHECK(test_camera->is_position_behind(test_camera->get_global_position() + offset1)); + test_camera->set_h_offset(4.7f); + test_camera->set_v_offset(-3.0f); + CHECK_FALSE(test_camera->is_position_behind(test_camera->get_global_position() + offset2)); + } + // Reset h/v offsets. + test_camera->set_h_offset(0); + test_camera->set_v_offset(0); + } + + SUBCASE("is_position_in_frustum") { + // If the point is behind the near plane, it is outside the camera frustum. + // So offset1 is not in frustum. + CHECK_FALSE(test_camera->is_position_in_frustum(test_camera->get_global_position() + offset1)); + // If |right| > 5 / 2 or |up| > 2.5 / 2, the point is outside the camera frustum. + // So offset2 is in frustum and offset3 and offset4 are not. + CHECK(test_camera->is_position_in_frustum(test_camera->get_global_position() + offset2)); + CHECK_FALSE(test_camera->is_position_in_frustum(test_camera->get_global_position() + offset3)); + CHECK_FALSE(test_camera->is_position_in_frustum(test_camera->get_global_position() + offset4)); + // offset5 is beyond the far plane, so it is not in frustum. + CHECK_FALSE(test_camera->is_position_in_frustum(test_camera->get_global_position() + offset5)); + } + } + + SUBCASE("Perspective projection") { + test_camera->set_projection(Camera3D::ProjectionType::PROJECTION_PERSPECTIVE); + // Camera at origin, looking at +Z. + test_camera->set_global_position(Vector3(0, 0, 0)); + test_camera->set_global_rotation(Vector3(0, 0, 0)); + // Keep width, so horizontal fov = 120. + // Since the near plane distance is 1, + // with trig we know the near plane's width is 2 * sqrt(3), so its height is sqrt(3). + test_camera->set_perspective(120.0f, 1.0f, 1000.0f); + + SUBCASE("is_position_behind") { + CHECK_FALSE(test_camera->is_position_behind(Vector3(0, 0, -1.5f))); + CHECK(test_camera->is_position_behind(Vector3(2, 0, -0.2f))); + } + + SUBCASE("is_position_in_frustum") { + CHECK(test_camera->is_position_in_frustum(Vector3(-1.3f, 0, -1.1f))); + CHECK_FALSE(test_camera->is_position_in_frustum(Vector3(2, 0, -1.1f))); + CHECK(test_camera->is_position_in_frustum(Vector3(1, 0.5f, -1.1f))); + CHECK_FALSE(test_camera->is_position_in_frustum(Vector3(1, 1, -1.1f))); + CHECK(test_camera->is_position_in_frustum(Vector3(0, 0, -1.5f))); + CHECK_FALSE(test_camera->is_position_in_frustum(Vector3(0, 0, -0.5f))); + } + } + + memdelete(test_camera); + memdelete(mock_viewport); +} + +TEST_CASE("[SceneTree][Camera3D] Project/Unproject position") { + // Cameras need a viewport to know how to compute their frustums, so we make a fake one here. + Camera3D *test_camera = memnew(Camera3D); + SubViewport *mock_viewport = memnew(SubViewport); + // 4:2. + mock_viewport->set_size(Vector2(400, 200)); + SceneTree::get_singleton()->get_root()->add_child(mock_viewport); + mock_viewport->add_child(test_camera); + test_camera->set_global_position(Vector3(0, 0, 0)); + test_camera->set_global_rotation(Vector3(0, 0, 0)); + test_camera->set_keep_aspect_mode(Camera3D::KeepAspect::KEEP_HEIGHT); + + SUBCASE("project_position") { + SUBCASE("Orthogonal projection") { + test_camera->set_orthogonal(5.0f, 0.5f, 1000.0f); + // Center. + CHECK(test_camera->project_position(Vector2(200, 100), 0.5f).is_equal_approx(Vector3(0, 0, -0.5f))); + // Top left. + CHECK(test_camera->project_position(Vector2(0, 0), 1.5f).is_equal_approx(Vector3(-5.0f, 2.5f, -1.5f))); + // Bottom right. + CHECK(test_camera->project_position(Vector2(400, 200), 5.0f).is_equal_approx(Vector3(5.0f, -2.5f, -5.0f))); + } + + SUBCASE("Perspective projection") { + test_camera->set_perspective(120.0f, 0.5f, 1000.0f); + // Center. + CHECK(test_camera->project_position(Vector2(200, 100), 0.5f).is_equal_approx(Vector3(0, 0, -0.5f))); + CHECK(test_camera->project_position(Vector2(200, 100), 100.0f).is_equal_approx(Vector3(0, 0, -100.0f))); + // 3/4th way to Top left. + CHECK(test_camera->project_position(Vector2(100, 50), 0.5f).is_equal_approx(Vector3(-SQRT3 * 0.5f, SQRT3 * 0.25f, -0.5f))); + CHECK(test_camera->project_position(Vector2(100, 50), 1.0f).is_equal_approx(Vector3(-SQRT3, SQRT3 * 0.5f, -1.0f))); + // 3/4th way to Bottom right. + CHECK(test_camera->project_position(Vector2(300, 150), 0.5f).is_equal_approx(Vector3(SQRT3 * 0.5f, -SQRT3 * 0.25f, -0.5f))); + CHECK(test_camera->project_position(Vector2(300, 150), 1.0f).is_equal_approx(Vector3(SQRT3, -SQRT3 * 0.5f, -1.0f))); + } + } + + // Uses cases that are the inverse of the above sub-case. + SUBCASE("unproject_position") { + SUBCASE("Orthogonal projection") { + test_camera->set_orthogonal(5.0f, 0.5f, 1000.0f); + // Center + CHECK(test_camera->unproject_position(Vector3(0, 0, -0.5f)).is_equal_approx(Vector2(200, 100))); + // Top left + CHECK(test_camera->unproject_position(Vector3(-5.0f, 2.5f, -1.5f)).is_equal_approx(Vector2(0, 0))); + // Bottom right + CHECK(test_camera->unproject_position(Vector3(5.0f, -2.5f, -5.0f)).is_equal_approx(Vector2(400, 200))); + } + + SUBCASE("Perspective projection") { + test_camera->set_perspective(120.0f, 0.5f, 1000.0f); + // Center. + CHECK(test_camera->unproject_position(Vector3(0, 0, -0.5f)).is_equal_approx(Vector2(200, 100))); + CHECK(test_camera->unproject_position(Vector3(0, 0, -100.0f)).is_equal_approx(Vector2(200, 100))); + // 3/4th way to Top left. + WARN(test_camera->unproject_position(Vector3(-SQRT3 * 0.5f, SQRT3 * 0.25f, -0.5f)).is_equal_approx(Vector2(100, 50))); + WARN(test_camera->unproject_position(Vector3(-SQRT3, SQRT3 * 0.5f, -1.0f)).is_equal_approx(Vector2(100, 50))); + // 3/4th way to Bottom right. + CHECK(test_camera->unproject_position(Vector3(SQRT3 * 0.5f, -SQRT3 * 0.25f, -0.5f)).is_equal_approx(Vector2(300, 150))); + CHECK(test_camera->unproject_position(Vector3(SQRT3, -SQRT3 * 0.5f, -1.0f)).is_equal_approx(Vector2(300, 150))); + } + } + + memdelete(test_camera); + memdelete(mock_viewport); +} + +TEST_CASE("[SceneTree][Camera3D] Project ray") { + // Cameras need a viewport to know how to compute their frustums, so we make a fake one here. + Camera3D *test_camera = memnew(Camera3D); + SubViewport *mock_viewport = memnew(SubViewport); + // 4:2. + mock_viewport->set_size(Vector2(400, 200)); + SceneTree::get_singleton()->get_root()->add_child(mock_viewport); + mock_viewport->add_child(test_camera); + test_camera->set_global_position(Vector3(0, 0, 0)); + test_camera->set_global_rotation(Vector3(0, 0, 0)); + test_camera->set_keep_aspect_mode(Camera3D::KeepAspect::KEEP_HEIGHT); + + SUBCASE("project_ray_origin") { + SUBCASE("Orthogonal projection") { + test_camera->set_orthogonal(5.0f, 0.5f, 1000.0f); + // Center. + CHECK(test_camera->project_ray_origin(Vector2(200, 100)).is_equal_approx(Vector3(0, 0, -0.5f))); + // Top left. + CHECK(test_camera->project_ray_origin(Vector2(0, 0)).is_equal_approx(Vector3(-5.0f, 2.5f, -0.5f))); + // Bottom right. + CHECK(test_camera->project_ray_origin(Vector2(400, 200)).is_equal_approx(Vector3(5.0f, -2.5f, -0.5f))); + } + + SUBCASE("Perspective projection") { + test_camera->set_perspective(120.0f, 0.5f, 1000.0f); + // Center. + CHECK(test_camera->project_ray_origin(Vector2(200, 100)).is_equal_approx(Vector3(0, 0, 0))); + // Top left. + CHECK(test_camera->project_ray_origin(Vector2(0, 0)).is_equal_approx(Vector3(0, 0, 0))); + // Bottom right. + CHECK(test_camera->project_ray_origin(Vector2(400, 200)).is_equal_approx(Vector3(0, 0, 0))); + } + } + + SUBCASE("project_ray_normal") { + SUBCASE("Orthogonal projection") { + test_camera->set_orthogonal(5.0f, 0.5f, 1000.0f); + // Center. + CHECK(test_camera->project_ray_normal(Vector2(200, 100)).is_equal_approx(Vector3(0, 0, -1))); + // Top left. + CHECK(test_camera->project_ray_normal(Vector2(0, 0)).is_equal_approx(Vector3(0, 0, -1))); + // Bottom right. + CHECK(test_camera->project_ray_normal(Vector2(400, 200)).is_equal_approx(Vector3(0, 0, -1))); + } + + SUBCASE("Perspective projection") { + test_camera->set_perspective(120.0f, 0.5f, 1000.0f); + // Center. + CHECK(test_camera->project_ray_normal(Vector2(200, 100)).is_equal_approx(Vector3(0, 0, -1))); + // Top left. + CHECK(test_camera->project_ray_normal(Vector2(0, 0)).is_equal_approx(Vector3(-SQRT3, SQRT3 / 2, -0.5f).normalized())); + // Bottom right. + CHECK(test_camera->project_ray_normal(Vector2(400, 200)).is_equal_approx(Vector3(SQRT3, -SQRT3 / 2, -0.5f).normalized())); + } + } + + SUBCASE("project_local_ray_normal") { + test_camera->set_rotation_degrees(Vector3(60, 60, 60)); + + SUBCASE("Orthogonal projection") { + test_camera->set_orthogonal(5.0f, 0.5f, 1000.0f); + // Center. + CHECK(test_camera->project_local_ray_normal(Vector2(200, 100)).is_equal_approx(Vector3(0, 0, -1))); + // Top left. + CHECK(test_camera->project_local_ray_normal(Vector2(0, 0)).is_equal_approx(Vector3(0, 0, -1))); + // Bottom right. + CHECK(test_camera->project_local_ray_normal(Vector2(400, 200)).is_equal_approx(Vector3(0, 0, -1))); + } + + SUBCASE("Perspective projection") { + test_camera->set_perspective(120.0f, 0.5f, 1000.0f); + // Center. + CHECK(test_camera->project_local_ray_normal(Vector2(200, 100)).is_equal_approx(Vector3(0, 0, -1))); + // Top left. + CHECK(test_camera->project_local_ray_normal(Vector2(0, 0)).is_equal_approx(Vector3(-SQRT3, SQRT3 / 2, -0.5f).normalized())); + // Bottom right. + CHECK(test_camera->project_local_ray_normal(Vector2(400, 200)).is_equal_approx(Vector3(SQRT3, -SQRT3 / 2, -0.5f).normalized())); + } + } + + memdelete(test_camera); + memdelete(mock_viewport); +} + +#undef SQRT3 + +#endif // TEST_CAMERA_3D_H diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 8c120f6d3af..5187ebd00f7 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -94,6 +94,7 @@ #include "tests/scene/test_arraymesh.h" #include "tests/scene/test_audio_stream_wav.h" #include "tests/scene/test_bit_map.h" +#include "tests/scene/test_camera_3d.h" #include "tests/scene/test_code_edit.h" #include "tests/scene/test_color_picker.h" #include "tests/scene/test_control.h"