Implement Frustum Unit Tests (Currently failing)

This commit is contained in:
2024-07-10 14:17:34 -04:00
parent 6684c64ab7
commit cbfbc6acf0

View File

@@ -0,0 +1,327 @@
/// Josh's 3D Math Library
/// A C++20 Library for 3D Math, Computer Graphics, and Scientific Computing.
/// Developed and Maintained by Josh O'Leary @ Redacted Software.
/// Special Thanks to William Tomasine II and Maxine Hayes.
/// (c) 2024 Redacted Software
/// This work is dedicated to the public domain.
/// @file FrustumTests.hpp
/// @desc Unit tests for the Frustum class.
/// @edit 2024-07-07
#pragma once
#include <jtest/jtest.hpp>
#include <J3ML/Geometry/Frustum.h>
#include <J3ML/Geometry/PBVolume.hpp>
Frustum GenIdFrustum(FrustumType t, FrustumHandedness h, FrustumProjectiveSpace p) {
Frustum f;
f.SetKind(p, h);
f.SetViewPlaneDistances(1.f, 100.f);
f.SetFrame({0,0,0}, {0,0,-1}, {0,1,0});
if (t == FrustumType::Perspective)
f.SetPerspective(Math::Pi / 2.f, Math::Pi / 2.f);
else
f.SetOrthographic(100.f, 100.f);
return f;
}
#define FOR_EACH_FRUSTUM_CONVENTION(f) \
for (int ii = 0; ii < 8; ++ii) { \
switch (ii) { \
case 0 : f = GenIdFrustum(FrustumType::Perspective, FrustumHandedness::Left, FrustumProjectiveSpace::GL); break; \
case 1 : f = GenIdFrustum(FrustumType::Perspective, FrustumHandedness::Right, FrustumProjectiveSpace::GL); break; \
case 2 : f = GenIdFrustum(FrustumType::Perspective, FrustumHandedness::Left, FrustumProjectiveSpace::D3D); break; \
case 3 : f = GenIdFrustum(FrustumType::Perspective, FrustumHandedness::Right, FrustumProjectiveSpace::D3D); break; \
case 4 : f = GenIdFrustum(FrustumType::Orthographic, FrustumHandedness::Left, FrustumProjectiveSpace::GL); break; \
case 5 : f = GenIdFrustum(FrustumType::Orthographic, FrustumHandedness::Right, FrustumProjectiveSpace::GL); break; \
case 6 : f = GenIdFrustum(FrustumType::Orthographic, FrustumHandedness::Left, FrustumProjectiveSpace::D3D); break; \
case 7 : f = GenIdFrustum(FrustumType::Orthographic, FrustumHandedness::Right, FrustumProjectiveSpace::D3D); break; \
} \
void FrustumTests() {
using namespace jtest;
using namespace J3ML::Geometry;
using namespace J3ML::Math;
RNG rng;
TEST("Frustum::AspectRatio", [] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
check(EqualAbs(f.AspectRatio(), 1.f));
};
});
TEST("Frustum::WorldRight", [] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
if (f.Handedness() == FrustumHandedness::Right) {
check(f.WorldRight().Equals({1, 0, 0}));
} else { // In the test func, all cameras lock down to -Z, so left-handed cameras need to point their right towards -X then.
check(f.WorldRight().Equals({-1, 0, 0}));
}
}
});
TEST("Frustum::Chirality", [] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
check(f.WorldMatrix().Determinant() > 0.f);
check(f.ViewMatrix().Determinant() > 0.f);
if (f.Handedness() == FrustumHandedness::Left)
check(f.ProjectionMatrix().Determinant4() > 0.f); // left-handed view -> projection space transform does not change handedness.
else
check(f.ProjectionMatrix().Determinant4() < 0.f); // but right-handed transform should.
}
});
TEST("Frustum::Planes", [] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
check(f.NearPlane().Normal.Equals({0,0,1}));
check(Math::EqualAbs(f.NearPlane().distance, -1.f));
check(f.FarPlane().Normal.Equals({0,0,-1}));
check(Math::EqualAbs(f.FarPlane().distance, 100.f));
for (int i = 0; i < 9; ++i) {
Vector3 pt;
if (i == 8)
pt = f.CenterPoint();
else
pt = f.CornerPoint(i);
check(f.NearPlane().SignedDistance(pt) < 1e-3f);
check(f.FarPlane().SignedDistance(pt) < 1e-3f);
check(f.LeftPlane().SignedDistance(pt) < 1e-3f);
check(f.RightPlane().SignedDistance(pt) < 1e-3f);
check(f.TopPlane().SignedDistance(pt) < 1e-3f);
check(f.BottomPlane().SignedDistance(pt) < 1e-3f);
check(f.Contains(pt));
}
}
});
TEST("Frustum::Corners", [] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
// Corner points are returned in XYZ order: 0: ---, 1: --+, 2: -+-, 3: -++, 4: +--, 5: +-+, 6: ++-, 7: +++
if (f.Type() == FrustumType::Perspective && f.Handedness() == FrustumHandedness::Left) {
check(f.CornerPoint(0).Equals({1.f, -1.f, -1.f}));
check(f.CornerPoint(1).Equals({100.f, -100.f, -100.f}));
check(f.CornerPoint(2).Equals({1.f, 1, -1.f}));
check(f.CornerPoint(3).Equals({100.f, 100.f, 100.f}));
check(f.CornerPoint(4).Equals({-1.f, -1.f, -1.f}));
check(f.CornerPoint(5).Equals({-100.f, -100.f, -100.f}));
check(f.CornerPoint(6).Equals({-1.f, 1.f, -1.f}));
check(f.CornerPoint(7).Equals({-100.f, 100.f, -100.f}));
} else if (f.Type() == FrustumType::Perspective && f.Handedness() == FrustumHandedness::Right) {
check(f.CornerPoint(0).Equals({-1.f, -1.f, -1.f}));
check(f.CornerPoint(1).Equals({-100.f, -100.f, -100.f}));
check(f.CornerPoint(2).Equals({-1.f, 1.f, -1.f}));
check(f.CornerPoint(3).Equals({-100.f, 100.f, -100.f}));
check(f.CornerPoint(4).Equals({1.f, -1.f, -1.f}));
check(f.CornerPoint(5).Equals({100.f, -100.f, -100.f}));
check(f.CornerPoint(6).Equals({1.f, 1.f, -1.f}));
check(f.CornerPoint(7).Equals({100.f, 100.f, -100.f}));
} else if (f.Type() == FrustumType::Orthographic && f.Handedness() == FrustumHandedness::Left) {
check(f.CornerPoint(0).Equals({50.f, -50.f, -1.f}));
check(f.CornerPoint(1).Equals({50.f, -50.f, -100.f}));
check(f.CornerPoint(2).Equals({50.f, 50.f, -1.f}));
check(f.CornerPoint(3).Equals({50.f, 50.f, -100.f}));
check(f.CornerPoint(4).Equals({-50.f, -50.f, -1.f}));
check(f.CornerPoint(5).Equals({-50.f, -50.f, -100.f}));
check(f.CornerPoint(6).Equals({-50.f, 50.f, -1.f}));
check(f.CornerPoint(7).Equals({-50.f, 50.f, -100.f}));
} else if (f.Type() == FrustumType::Orthographic && f.Handedness() == FrustumHandedness::Right) {
check(f.CornerPoint(0).Equals({-50.f, -50.f, -1.f}));
check(f.CornerPoint(1).Equals({-50.f, -50.f, -100.f}));
check(f.CornerPoint(2).Equals({-50.f, 50.f, -1.f}));
check(f.CornerPoint(3).Equals({-50.f, 50.f, -100.f}));
check(f.CornerPoint(4).Equals({50.f, -50.f, -1.f}));
check(f.CornerPoint(5).Equals({50.f, -50.f, -100.f}));
check(f.CornerPoint(6).Equals({50.f, 50.f, -1.f}));
check(f.CornerPoint(7).Equals({50.f, 50.f, -100.f}));
}
}
});
TEST("Frustum::ProjectUnprojectSymmetry", [&rng] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
for (int i = 0; i < 10; ++i) {
// Orient and locate the Frustum randomly
Matrix3x3 rot = Matrix3x3::RandomRotation(rng);
f.Transform(rot);
f.SetPos(f.Pos() + Vector3::RandomDir(rng, rng.Float(1.f, 100.f)));
for (int j = 0; j < 100; ++j) {
Vector2 pt = Vector2::RandomBox(rng, -1.f, 1.f);
Vector3 pos = f.NearPlanePos(pt);
Vector3 pt2 = f.Project(pos);
check(pt.Equals(pt2.XY()));
pos = f.FarPlanePos(pt);
pt2 = f.Project(pos);
check(pt.Equals(pt2.XY()));
pos = f.PointInside(pt.x, pt.y, rng.Float());
pt2 = f.Project(pos);
}
}
}
});
TEST("Frustum::PlaneNormalsAreCorrect", [] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
auto pb = f.ToPBVolume();
Vector3 corners[8];
Plane planes[6];
f.GetCornerPoints(corners);
f.GetPlanes(planes);
for (int i = 0; i < 8; ++i)
check(pb.Contains(corners[i]));
for(int i = 0; i < 6; ++i)
for(int j = 0; j < 8; ++j)
check(planes[i].SignedDistance(corners[j]) <= 0.f);
}
});
TEST("Frustum::IsConvex", [] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
Polyhedron p = f.ToPolyhedron();
for(int i = 0; i < 6; ++i)
{
Plane p1 = f.GetPlane(i);
Plane p2 = p.FacePlane(i);
check(p1.Equals(p2));
}
check(p.EulerFormulaHolds());
check(p.IsClosed());
check(p.IsConvex());
check(!p.IsNull());
}
});
TEST("Plane::ProjectToNegativeHalf", [] {
Plane p(Vector3(0,1,0), 50.f);
Vector3 neg = Vector3(0,-100.f, 0);
Vector3 pos = Vector3(0, 100.f, 0);
check(neg.Equals(p.ProjectToNegativeHalf(neg)));
check(!neg.Equals(p.ProjectToPositiveHalf(neg)));
check(pos.Equals(p.ProjectToPositiveHalf(pos)));
check(!pos.Equals(p.ProjectToNegativeHalf(pos)));
});
TEST("Frustum::Contains", [] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
for(int i = 0; i < 8; ++i)
{
Vector3 corner = f.CornerPoint(i);
Vector3 closestPoint = f.ClosestPoint(corner);
float distance = f.Distance(corner);
if (!f.Contains(corner) || distance > 1e-4f)
//LOGE("Closest point to %s: %s", corner.ToString().c_str(), closestPoint.ToString().c_str());
check(f.Contains(corner));
check(distance < 10.f);
}
check(f.Contains(f.CenterPoint()));
}
});
TEST("Frustum::ContainsCorners", [&rng] {
constexpr float SCALE = 1e2f;
Vector3 pt = Vector3::RandomBox(rng, Vector3::FromScalar(-SCALE), Vector3::FromScalar(SCALE));
Frustum b;// = RandomFrustumContainingPoint(rng, pt);
for(int i = 0; i < 9; ++i)
{
Vector3 point = (i == 8) ? b.CenterPoint() : b.CornerPoint(i);
check(b.NearPlane().SignedDistance(point) < 1e-3f);
check(b.FarPlane().SignedDistance(point) < 1e-3f);
check(b.LeftPlane().SignedDistance(point) < 1e-3f);
check(b.RightPlane().SignedDistance(point) < 1e-3f);
check(b.TopPlane().SignedDistance(point) < 1e-3f);
check(b.BottomPlane().SignedDistance(point) < 1e-3f);
check(b.Contains(point));
}
});
TEST("Frustum::Matrices", [] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
if (f.Handedness() == FrustumHandedness::Right) {
Matrix4x4 wm = f.WorldMatrix();
check(wm.IsIdentity());
Matrix4x4 vm = f.ViewMatrix();
check(vm.IsIdentity());
} else {
Matrix4x4 wm = f.WorldMatrix() * Matrix4x4::RotateY(Math::Pi);
check(wm.IsIdentity());
Matrix4x4 vm = f.ViewMatrix() * Matrix4x4::RotateY(Math::Pi);
check(vm.IsIdentity());
}
}
});
TEST("Frustum::Projection", [] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
const float nearD = (f.ProjectiveSpace() == FrustumProjectiveSpace::D3D) ? 0.f : -1.f;
// Corner points are returned in XYZ order: 0: ---, 1: --+, 2: -+-, 3: -++, 4: +--, 5: +-+, 6: ++-, 7: +++
check(f.Project(f.CornerPoint(0)).Equals({-1,-1, nearD}));
check(f.Project(f.CornerPoint(1)).Equals({-1,-1, 1}));
check(f.Project(f.CornerPoint(2)).Equals({-1,1, nearD}));
check(f.Project(f.CornerPoint(3)).Equals({-1,1, 1}));
check(f.Project(f.CornerPoint(4)).Equals({1,-1, nearD}));
check(f.Project(f.CornerPoint(5)).Equals({1,-1, 1}));
check(f.Project(f.CornerPoint(6)).Equals({1,1, nearD}));
check(f.Project(f.CornerPoint(7)).Equals({1,1, 1}));
}
});
TEST("Frustum::UnProject", [] {
Frustum f;
FOR_EACH_FRUSTUM_CONVENTION(f)
if (f.Type() == FrustumType::Perspective) {
Ray r = f.UnProject(0, 0);
check(r.Origin.Equals(f.Pos()));
check(r.Origin.Equals({0,0,0}));
check(r.Direction.Equals({0,0,-1}));
r = f.UnProject(-1, -1);
check(r.Origin.Equals(f.Pos()));
check(r.Origin.Equals({0,0,0}));
}
}
});
TEST("Frustum::UnProjectFromNearPlane", []{});
TEST("Frustum::UnProjectFromNearPlane", []{});
}