327 lines
13 KiB
C++
327 lines
13 KiB
C++
/// 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.hpp>
|
|
#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", []{});
|
|
} |