diff --git a/core/math/projection.cpp b/core/math/projection.cpp index 20638826a6e..17904df7ba6 100644 --- a/core/math/projection.cpp +++ b/core/math/projection.cpp @@ -402,82 +402,35 @@ void Projection::set_frustum(real_t p_size, real_t p_aspect, Vector2 p_offset, r } real_t Projection::get_z_far() const { - const real_t *matrix = (const real_t *)columns; - Plane new_plane = Plane(matrix[3] - matrix[2], - matrix[7] - matrix[6], - matrix[11] - matrix[10], - matrix[15] - matrix[14]); - - new_plane.normalize(); - - return new_plane.d; + // NOTE: This assumes z-facing near and far planes, i.e. that : + // - the matrix is a projection across z-axis (i.e. is invertible and columns[0][1], [0][3], [1][0] and [1][3] == 0) + // - near and far planes are z-facing (i.e. columns[0][2] and [1][2] == 0) + return (columns[3][3] - columns[3][2]) / (columns[2][3] - columns[2][2]); } real_t Projection::get_z_near() const { - const real_t *matrix = (const real_t *)columns; - Plane new_plane = Plane(matrix[3] + matrix[2], - matrix[7] + matrix[6], - matrix[11] + matrix[10], - -matrix[15] - matrix[14]); - - new_plane.normalize(); - return new_plane.d; + // NOTE: This assumes z-facing near and far planes, i.e. that : + // - the matrix is a projection across z-axis (i.e. is invertible and columns[0][1], [0][3], [1][0] and [1][3] == 0) + // - near and far planes are z-facing (i.e. columns[0][2] and [1][2] == 0) + return (columns[3][3] + columns[3][2]) / (columns[2][3] + columns[2][2]); } Vector2 Projection::get_viewport_half_extents() const { - const real_t *matrix = (const real_t *)columns; - ///////--- Near Plane ---/////// - Plane near_plane = Plane(matrix[3] + matrix[2], - matrix[7] + matrix[6], - matrix[11] + matrix[10], - -matrix[15] - matrix[14]); - near_plane.normalize(); - - ///////--- Right Plane ---/////// - Plane right_plane = Plane(matrix[3] - matrix[0], - matrix[7] - matrix[4], - matrix[11] - matrix[8], - -matrix[15] + matrix[12]); - right_plane.normalize(); - - Plane top_plane = Plane(matrix[3] - matrix[1], - matrix[7] - matrix[5], - matrix[11] - matrix[9], - -matrix[15] + matrix[13]); - top_plane.normalize(); - - Vector3 res; - near_plane.intersect_3(right_plane, top_plane, &res); - - return Vector2(res.x, res.y); + // NOTE: This assumes a symmetrical frustum, i.e. that : + // - the matrix is a projection across z-axis (i.e. is invertible and columns[0][1], [0][3], [1][0] and [1][3] == 0) + // - the projection plane is rectangular (i.e. columns[0][2] and [1][2] == 0 if columns[2][3] != 0) + // - there is no offset / skew (i.e. columns[2][0] == columns[2][1] == 0) + real_t w = -get_z_near() * columns[2][3] + columns[3][3]; + return Vector2(w / columns[0][0], w / columns[1][1]); } Vector2 Projection::get_far_plane_half_extents() const { - const real_t *matrix = (const real_t *)columns; - ///////--- Far Plane ---/////// - Plane far_plane = Plane(matrix[3] - matrix[2], - matrix[7] - matrix[6], - matrix[11] - matrix[10], - -matrix[15] + matrix[14]); - far_plane.normalize(); - - ///////--- Right Plane ---/////// - Plane right_plane = Plane(matrix[3] - matrix[0], - matrix[7] - matrix[4], - matrix[11] - matrix[8], - -matrix[15] + matrix[12]); - right_plane.normalize(); - - Plane top_plane = Plane(matrix[3] - matrix[1], - matrix[7] - matrix[5], - matrix[11] - matrix[9], - -matrix[15] + matrix[13]); - top_plane.normalize(); - - Vector3 res; - far_plane.intersect_3(right_plane, top_plane, &res); - - return Vector2(res.x, res.y); + // NOTE: This assumes a symmetrical frustum, i.e. that : + // - the matrix is a projection across z-axis (i.e. is invertible and columns[0][1], [0][3], [1][0] and [1][3] == 0) + // - the projection plane is rectangular (i.e. columns[0][2] and [1][2] == 0 if columns[2][3] != 0) + // - there is no offset / skew (i.e. columns[2][0] == columns[2][1] == 0) + real_t w = -get_z_far() * columns[2][3] + columns[3][3]; + return Vector2(w / columns[0][0], w / columns[1][1]); } bool Projection::get_endpoints(const Transform3D &p_transform, Vector3 *p_8points) const { @@ -919,53 +872,45 @@ Projection::operator String() const { } real_t Projection::get_aspect() const { - Vector2 vp_he = get_viewport_half_extents(); - return vp_he.x / vp_he.y; + // NOTE: This assumes a rectangular projection plane, i.e. that : + // - the matrix is a projection across z-axis (i.e. is invertible and columns[0][1], [0][3], [1][0] and [1][3] == 0) + // - the projection plane is rectangular (i.e. columns[0][2] and [1][2] == 0 if columns[2][3] != 0) + return columns[1][1] / columns[0][0]; } int Projection::get_pixels_per_meter(int p_for_pixel_width) const { - Vector3 result = xform(Vector3(1, 0, -1)); - - return int((result.x * 0.5 + 0.5) * p_for_pixel_width); + // NOTE: This assumes a rectangular projection plane, i.e. that : + // - the matrix is a projection across z-axis (i.e. is invertible and columns[0][1], [0][3], [1][0] and [1][3] == 0) + // - the projection plane is rectangular (i.e. columns[0][2] and [1][2] == 0 if columns[2][3] != 0) + real_t width = 2 * (-get_z_near() * columns[2][3] + columns[3][3]) / columns[0][0]; + return p_for_pixel_width / width; // Note : return type should be real_t (kept as int for compatibility for now). } bool Projection::is_orthogonal() const { - return columns[3][3] == 1.0; + // NOTE: This assumes that the matrix is a projection across z-axis + // i.e. is invertible and columns[0][1], [0][3], [1][0] and [1][3] == 0 + return columns[2][3] == 0.0; } real_t Projection::get_fov() const { - const real_t *matrix = (const real_t *)columns; - - Plane right_plane = Plane(matrix[3] - matrix[0], - matrix[7] - matrix[4], - matrix[11] - matrix[8], - -matrix[15] + matrix[12]); - right_plane.normalize(); - - if ((matrix[8] == 0) && (matrix[9] == 0)) { - return Math::rad_to_deg(Math::acos(Math::abs(right_plane.normal.x))) * 2.0; + // NOTE: This assumes a rectangular projection plane, i.e. that : + // - the matrix is a projection across z-axis (i.e. is invertible and columns[0][1], [0][3], [1][0] and [1][3] == 0) + // - the projection plane is rectangular (i.e. columns[0][2] and [1][2] == 0 if columns[2][3] != 0) + if (columns[2][0] == 0) { + return Math::rad_to_deg(2 * Math::atan2(1, columns[0][0])); } else { - // our frustum is asymmetrical need to calculate the left planes angle separately.. - Plane left_plane = Plane(matrix[3] + matrix[0], - matrix[7] + matrix[4], - matrix[11] + matrix[8], - matrix[15] + matrix[12]); - left_plane.normalize(); - - return Math::rad_to_deg(Math::acos(Math::abs(left_plane.normal.x))) + Math::rad_to_deg(Math::acos(Math::abs(right_plane.normal.x))); + // The frustum is asymmetrical so we need to calculate the left and right angles separately. + real_t right = Math::atan2(columns[2][0] + 1, columns[0][0]); + real_t left = Math::atan2(columns[2][0] - 1, columns[0][0]); + return Math::rad_to_deg(right - left); } } real_t Projection::get_lod_multiplier() const { - if (is_orthogonal()) { - return get_viewport_half_extents().x; - } else { - const real_t zn = get_z_near(); - const real_t width = get_viewport_half_extents().x * 2.0f; - return 1.0f / (zn / width); - } - - // Usage is lod_size / (lod_distance * multiplier) < threshold + // NOTE: This assumes a rectangular projection plane, i.e. that : + // - the matrix is a projection across z-axis (i.e. is invertible and columns[0][1], [0][3], [1][0] and [1][3] == 0) + // - the projection plane is rectangular (i.e. columns[0][2] and [1][2] == 0 if columns[2][3] != 0) + return 2 / columns[0][0]; } void Projection::make_scale(const Vector3 &p_scale) { diff --git a/doc/classes/Projection.xml b/doc/classes/Projection.xml index d32fdc9bb8f..fd9dd7446b6 100644 --- a/doc/classes/Projection.xml +++ b/doc/classes/Projection.xml @@ -207,7 +207,7 @@ - Returns the number of pixels with the given pixel width displayed per meter, after this [Projection] is applied. + Returns [param for_pixel_width] divided by the viewport's width measured in meters on the near plane, after this [Projection] is applied. diff --git a/tests/core/math/test_projection.h b/tests/core/math/test_projection.h index 02bee245a7d..575b34a5f85 100644 --- a/tests/core/math/test_projection.h +++ b/tests/core/math/test_projection.h @@ -449,6 +449,78 @@ TEST_CASE("[Projection] Perspective values extraction") { CHECK(zfar == doctest::Approx(50)); CHECK(aspect == doctest::Approx(0.5)); CHECK(fov == doctest::Approx(90)); + + persp.set_perspective(38, 1.3, 0.2, 8, false); + + znear = persp.get_z_near(); + zfar = persp.get_z_far(); + aspect = persp.get_aspect(); + fov = persp.get_fov(); + + CHECK(znear == doctest::Approx(0.2)); + CHECK(zfar == doctest::Approx(8)); + CHECK(aspect == doctest::Approx(1.3)); + CHECK(fov == doctest::Approx(Projection::get_fovy(38, 1.3))); + + persp.set_perspective(47, 2.5, 0.9, 14, true); + + znear = persp.get_z_near(); + zfar = persp.get_z_far(); + aspect = persp.get_aspect(); + fov = persp.get_fov(); + + CHECK(znear == doctest::Approx(0.9)); + CHECK(zfar == doctest::Approx(14)); + CHECK(aspect == doctest::Approx(2.5)); + CHECK(fov == doctest::Approx(47)); +} + +TEST_CASE("[Projection] Frustum values extraction") { + Projection frustum = Projection::create_frustum_aspect(1.0, 4.0 / 3.0, Vector2(0.5, -0.25), 0.5, 50, true); + + double znear = frustum.get_z_near(); + double zfar = frustum.get_z_far(); + double aspect = frustum.get_aspect(); + double fov = frustum.get_fov(); + + CHECK(znear == doctest::Approx(0.5)); + CHECK(zfar == doctest::Approx(50)); + CHECK(aspect == doctest::Approx(4.0 / 3.0)); + CHECK(fov == doctest::Approx(Math::rad_to_deg(Math::atan(2.0)))); + + frustum.set_frustum(2.0, 1.5, Vector2(-0.5, 2), 2, 12, false); + + znear = frustum.get_z_near(); + zfar = frustum.get_z_far(); + aspect = frustum.get_aspect(); + fov = frustum.get_fov(); + + CHECK(znear == doctest::Approx(2)); + CHECK(zfar == doctest::Approx(12)); + CHECK(aspect == doctest::Approx(1.5)); + CHECK(fov == doctest::Approx(Math::rad_to_deg(Math::atan(1.0) + Math::atan(0.5)))); +} + +TEST_CASE("[Projection] Orthographic values extraction") { + Projection ortho = Projection::create_orthogonal(-2, 3, -0.5, 1.5, 1.2, 15); + + double znear = ortho.get_z_near(); + double zfar = ortho.get_z_far(); + double aspect = ortho.get_aspect(); + + CHECK(znear == doctest::Approx(1.2)); + CHECK(zfar == doctest::Approx(15)); + CHECK(aspect == doctest::Approx(2.5)); + + ortho.set_orthogonal(-7, 2, 2.5, 5.5, 0.5, 6); + + znear = ortho.get_z_near(); + zfar = ortho.get_z_far(); + aspect = ortho.get_aspect(); + + CHECK(znear == doctest::Approx(0.5)); + CHECK(zfar == doctest::Approx(6)); + CHECK(aspect == doctest::Approx(3)); } TEST_CASE("[Projection] Orthographic check") { @@ -487,16 +559,48 @@ TEST_CASE("[Projection] Planes extraction") { CHECK(plane_array[Projection::PLANE_BOTTOM].normalized().is_equal_approx(planes[Projection::PLANE_BOTTOM].normalized())); } -TEST_CASE("[Projection] Half extents") { +TEST_CASE("[Projection] Perspective Half extents") { + constexpr real_t sqrt3 = 1.7320508; Projection persp = Projection::create_perspective(90, 1, 1, 40, false); Vector2 ne = persp.get_viewport_half_extents(); Vector2 fe = persp.get_far_plane_half_extents(); CHECK(ne.is_equal_approx(Vector2(1, 1) * 1)); CHECK(fe.is_equal_approx(Vector2(1, 1) * 40)); + + persp.set_perspective(120, sqrt3, 0.8, 10, true); + ne = persp.get_viewport_half_extents(); + fe = persp.get_far_plane_half_extents(); + + CHECK(ne.is_equal_approx(Vector2(sqrt3, 1.0) * 0.8)); + CHECK(fe.is_equal_approx(Vector2(sqrt3, 1.0) * 10)); + + persp.set_perspective(60, 1.2, 0.5, 15, false); + ne = persp.get_viewport_half_extents(); + fe = persp.get_far_plane_half_extents(); + + CHECK(ne.is_equal_approx(Vector2(sqrt3 / 3 * 1.2, sqrt3 / 3) * 0.5)); + CHECK(fe.is_equal_approx(Vector2(sqrt3 / 3 * 1.2, sqrt3 / 3) * 15)); +} + +TEST_CASE("[Projection] Orthographic Half extents") { + Projection ortho = Projection::create_orthogonal(-3, 3, -1.5, 1.5, 1.2, 15); + Vector2 ne = ortho.get_viewport_half_extents(); + Vector2 fe = ortho.get_far_plane_half_extents(); + + CHECK(ne.is_equal_approx(Vector2(3, 1.5))); + CHECK(fe.is_equal_approx(Vector2(3, 1.5))); + + ortho.set_orthogonal(-7, 7, -2.5, 2.5, 0.5, 6); + ne = ortho.get_viewport_half_extents(); + fe = ortho.get_far_plane_half_extents(); + + CHECK(ne.is_equal_approx(Vector2(7, 2.5))); + CHECK(fe.is_equal_approx(Vector2(7, 2.5))); } TEST_CASE("[Projection] Endpoints") { + constexpr real_t sqrt3 = 1.7320508; Projection persp = Projection::create_perspective(90, 1, 1, 40, false); Vector3 ep[8]; persp.get_endpoints(Transform3D(), ep); @@ -509,8 +613,91 @@ TEST_CASE("[Projection] Endpoints") { CHECK(ep[5].is_equal_approx(Vector3(-1, -1, -1) * 1)); CHECK(ep[6].is_equal_approx(Vector3(1, 1, -1) * 1)); CHECK(ep[7].is_equal_approx(Vector3(1, -1, -1) * 1)); + + persp.set_perspective(120, sqrt3, 0.8, 10, true); + persp.get_endpoints(Transform3D(), ep); + + CHECK(ep[0].is_equal_approx(Vector3(-sqrt3, 1, -1) * 10)); + CHECK(ep[1].is_equal_approx(Vector3(-sqrt3, -1, -1) * 10)); + CHECK(ep[2].is_equal_approx(Vector3(sqrt3, 1, -1) * 10)); + CHECK(ep[3].is_equal_approx(Vector3(sqrt3, -1, -1) * 10)); + CHECK(ep[4].is_equal_approx(Vector3(-sqrt3, 1, -1) * 0.8)); + CHECK(ep[5].is_equal_approx(Vector3(-sqrt3, -1, -1) * 0.8)); + CHECK(ep[6].is_equal_approx(Vector3(sqrt3, 1, -1) * 0.8)); + CHECK(ep[7].is_equal_approx(Vector3(sqrt3, -1, -1) * 0.8)); + + persp.set_perspective(60, 1.2, 0.5, 15, false); + persp.get_endpoints(Transform3D(), ep); + + CHECK(ep[0].is_equal_approx(Vector3(-sqrt3 / 3 * 1.2, sqrt3 / 3, -1) * 15)); + CHECK(ep[1].is_equal_approx(Vector3(-sqrt3 / 3 * 1.2, -sqrt3 / 3, -1) * 15)); + CHECK(ep[2].is_equal_approx(Vector3(sqrt3 / 3 * 1.2, sqrt3 / 3, -1) * 15)); + CHECK(ep[3].is_equal_approx(Vector3(sqrt3 / 3 * 1.2, -sqrt3 / 3, -1) * 15)); + CHECK(ep[4].is_equal_approx(Vector3(-sqrt3 / 3 * 1.2, sqrt3 / 3, -1) * 0.5)); + CHECK(ep[5].is_equal_approx(Vector3(-sqrt3 / 3 * 1.2, -sqrt3 / 3, -1) * 0.5)); + CHECK(ep[6].is_equal_approx(Vector3(sqrt3 / 3 * 1.2, sqrt3 / 3, -1) * 0.5)); + CHECK(ep[7].is_equal_approx(Vector3(sqrt3 / 3 * 1.2, -sqrt3 / 3, -1) * 0.5)); } +TEST_CASE("[Projection] LOD multiplier") { + constexpr real_t sqrt3 = 1.7320508; + Projection proj; + real_t multiplier; + + proj.set_perspective(60, 1, 1, 40, false); + multiplier = proj.get_lod_multiplier(); + CHECK(multiplier == doctest::Approx(2 * sqrt3 / 3)); + + proj.set_perspective(120, 1.5, 0.5, 20, false); + multiplier = proj.get_lod_multiplier(); + CHECK(multiplier == doctest::Approx(3 * sqrt3)); + + proj.set_orthogonal(15, 20, 10, 12, 5, 15); + multiplier = proj.get_lod_multiplier(); + CHECK(multiplier == doctest::Approx(5)); + + proj.set_orthogonal(-5, 15, -8, 10, 1.5, 10); + multiplier = proj.get_lod_multiplier(); + CHECK(multiplier == doctest::Approx(20)); + + proj.set_frustum(1.0, 4.0 / 3.0, Vector2(0.5, -0.25), 0.5, 50, false); + multiplier = proj.get_lod_multiplier(); + CHECK(multiplier == doctest::Approx(8.0 / 3.0)); + + proj.set_frustum(2.0, 1.2, Vector2(-0.1, 0.8), 1, 10, true); + multiplier = proj.get_lod_multiplier(); + CHECK(multiplier == doctest::Approx(2)); +} + +TEST_CASE("[Projection] Pixels per meter") { + constexpr real_t sqrt3 = 1.7320508; + Projection proj; + int ppm; + + proj.set_perspective(60, 1, 1, 40, false); + ppm = proj.get_pixels_per_meter(1024); + CHECK(ppm == int(1536.0f / sqrt3)); + + proj.set_perspective(120, 1.5, 0.5, 20, false); + ppm = proj.get_pixels_per_meter(1200); + CHECK(ppm == int(800.0f / sqrt3)); + + proj.set_orthogonal(15, 20, 10, 12, 5, 15); + ppm = proj.get_pixels_per_meter(500); + CHECK(ppm == 100); + + proj.set_orthogonal(-5, 15, -8, 10, 1.5, 10); + ppm = proj.get_pixels_per_meter(640); + CHECK(ppm == 32); + + proj.set_frustum(1.0, 4.0 / 3.0, Vector2(0.5, -0.25), 0.5, 50, false); + ppm = proj.get_pixels_per_meter(2048); + CHECK(ppm == 1536); + + proj.set_frustum(2.0, 1.2, Vector2(-0.1, 0.8), 1, 10, true); + ppm = proj.get_pixels_per_meter(800); + CHECK(ppm == 400); +} } //namespace TestProjection #endif // TEST_PROJECTION_H