Browse Source

Simplify and fix `Projection`'s getter functions

Florent Guiocheau 8 months ago
parent
commit
1d416fc2ba
3 changed files with 234 additions and 102 deletions
  1. 45 100
      core/math/projection.cpp
  2. 1 1
      doc/classes/Projection.xml
  3. 188 1
      tests/core/math/test_projection.h

+ 45 - 100
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) {

+ 1 - 1
doc/classes/Projection.xml

@@ -206,7 +206,7 @@
 			<return type="int" />
 			<param index="0" name="for_pixel_width" type="int" />
 			<description>
-				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.
 			</description>
 		</method>
 		<method name="get_projection_plane" qualifiers="const">

+ 188 - 1
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