add ray intersection checks

This commit is contained in:
ohyzha
2024-10-29 12:41:07 +02:00
parent 5ece0acc77
commit ced45f7b72
4 changed files with 512 additions and 0 deletions

View File

@@ -14,6 +14,7 @@
#include <glm/gtx/matrix_decompose.hpp> #include <glm/gtx/matrix_decompose.hpp>
#include <glm/gtx/quaternion.hpp> #include <glm/gtx/quaternion.hpp>
#include <glm/gtx/transform.hpp> #include <glm/gtx/transform.hpp>
#include <glm/gtx/intersect.hpp>
#include <glm/gtx/io.hpp> #include <glm/gtx/io.hpp>
#include <numbers> #include <numbers>

View File

@@ -0,0 +1,240 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
#include "Ray.hpp"
#include <vector>
namespace
{
int SolveQuadraticEquation(float a, float b, float c, float& x0, float& x1)
{
float discr = b * b - 4 * a * c;
if (discr < 0)
{
return 0;
}
if (discr == 0)
{
x0 = x1 = (-b) / (2 * a);
return 1;
}
float q = (b > 0) ? -0.5 * (b + std::sqrt(discr)) : -0.5 * (b - std::sqrt(discr));
x0 = q / a;
x1 = c / q;
if (x0 > x1)
{
std::swap(x0, x1);
}
return 2;
}
};
namespace OpenVulkano::Scene
{
using namespace Math::Utils;
std::optional<RayHit> Ray::IntersectSphere(const Math::Vector3f& center, float radius) const
{
RayHit hitRes;
if (intersectRaySphere(m_origin, m_dir, center, radius, hitRes.point, hitRes.normal))
{
hitRes.distance = distance(m_origin, hitRes.point);
return hitRes;
}
return {};
}
std::optional<RayHit> Ray::IntersectTriangle(const Math::Vector3f& v0, const Math::Vector3f& v1,
const Math::Vector3f& v2) const
{
RayHit hitRes;
vec2 baryPos;
if (intersectRayTriangle(m_origin, m_dir, v0, v1, v2, baryPos, hitRes.distance) && hitRes.distance >= 0)
{
hitRes.point = (1.f - baryPos.x - baryPos.y) * v0 + baryPos.x * v1 + baryPos.y * v2;
// calculate like cross product or leave empty ? what if current triangle is smoothly shaded ?
hitRes.normal = Math::Vector3f(0);
return hitRes;
}
return {};
}
std::optional<RayHit> Ray::IntersectQuad(const Math::Vector3f& v0, const Math::Vector3f& v1,
const Math::Vector3f& v2, const Math::Vector3f& v3) const
{
if (auto hitRes = IntersectTriangle(v0, v1, v2))
{
return hitRes;
}
if (auto hitRes = IntersectTriangle(v0, v2, v3))
{
return hitRes;
}
return {};
}
std::optional<RayHit> Ray::IntersectAABB(const Math::AABB& bbox) const
{
RayHit h1, h2;
const int intersections = this->IntersectAABB(bbox, h1, h2);
switch (intersections)
{
case 1:
return h1;
case 2:
return (h1.distance < h2.distance) ? h1 : h2;
}
return {};
}
int Ray::IntersectAABB(const Math::AABB& bbox, RayHit& p1, RayHit& p2) const
{
const auto bMax = bbox.max;
const auto bMin = bbox.min;
float tmin = (bMin.x - m_origin.x) / m_dir.x;
float tmax = (bMax.x - m_origin.x) / m_dir.x;
if (tmin > tmax)
{
std::swap(tmin, tmax);
}
float tymin = (bMin.y - m_origin.y) / m_dir.y;
float tymax = (bMax.y - m_origin.y) / m_dir.y;
if (tymin > tymax)
{
std::swap(tymin, tymax);
}
if ((tmin > tymax) || (tymin > tmax))
{
return 0;
}
if (tymin > tmin)
{
tmin = tymin;
}
if (tymax < tmax)
{
tmax = tymax;
}
float tzmin = (bMin.z - m_origin.z) / m_dir.z;
float tzmax = (bMax.z - m_origin.z) / m_dir.z;
if (tzmin > tzmax)
{
std::swap(tzmin, tzmax);
}
if ((tmin > tzmax) || (tzmin > tmax))
{
return 0;
}
if (tzmin > tmin)
{
tmin = tzmin;
}
if (tzmax < tmax)
{
tmax = tzmax;
}
int intersections = 2;
if (tmin < 0)
{
if (tmax < 0)
{
return 0;
}
intersections--;
tmin = tmax;
}
p1.point = m_origin + tmin * m_dir;
p2.point = m_origin + tmax * m_dir;
p1.distance = distance(m_origin, p1.point);
p2.distance = distance(m_origin, p2.point);
p1.normal = p2.normal = Math::Vector3f(0);
return intersections;
}
std::optional<RayHit> Ray::IntersectPlane(const Math::Vector3f& pOrigin, const Math::Vector3f& pNorm) const
{
RayHit hit;
Math::Vector3f norm = normalize(pNorm);
if (intersectRayPlane(m_origin, m_dir, pOrigin, pNorm, hit.distance))
{
hit.point = m_origin + m_dir * hit.distance;
hit.normal = norm;
return hit;
}
return {};
}
int Ray::IntersectSphere(const Math::Vector3f& center, float radius, RayHit& p1, RayHit& p2) const
{
const Math::Vector3f L = m_origin - center;
const float a = dot(m_dir, m_dir); // equals to length^2
const float b = 2 * dot(m_dir, L);
const float c = dot(L, L) - radius * radius;
float x1, x2;
int roots = ::SolveQuadraticEquation(a, b, c, x1, x2);
if (roots == 0)
{
return 0;
}
if (x1 > x2)
{
std::swap(x1, x2);
}
if (roots == 1)
{
// ray intersects sphere behind the origin
if (x1 < 0)
{
return 0;
}
p1.point = m_origin + x1 * m_dir;
p1.distance = distance(m_origin, p1.point);
p1.normal = normalize(p1.point - center);
p2 = p1;
}
else if (roots == 2)
{
// ray intersects sphere behind the origin
if (x1 < 0 && x2 < 0)
{
return 0;
}
if (x1 >= 0 && x2 >= 0)
{
p1.point = m_origin + x1 * m_dir;
p1.distance = distance(m_origin, p1.point);
p1.normal = normalize(p1.point - center);
p2.point = m_origin + x2 * m_dir;
p2.distance = distance(m_origin, p2.point);
p2.normal = normalize(p2.point - center);
}
else
{
--roots;
if (x1 < 0)
{
x1 = x2;
}
p1.point = m_origin + x1 * m_dir;
p1.distance = distance(m_origin, p1.point);
p1.normal = normalize(p1.point - center);
p2 = p1;
}
}
return roots;
}
}

View File

@@ -0,0 +1,55 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
#pragma once
#include "Math/Math.hpp"
#include "Math/AABB.hpp"
#include <optional>
namespace OpenVulkano::Scene
{
struct RayHit
{
Math::Vector3f point;
Math::Vector3f normal;
float distance;
bool operator==(const RayHit& other) const
{
return point == other.point && normal == other.normal && distance == other.distance;
}
bool operator!=(const RayHit& other) const { return !((*this) == other); }
};
class Ray
{
public:
Ray() : m_origin(0), m_dir(0) {}
Ray(const Math::Vector3f& origin, const Math::Vector3f& dir)
: m_origin(origin), m_dir(Math::Utils::normalize(dir))
{
}
void SetPosition(const Math::Vector3f& origin) { m_origin = origin; }
void SetDir(const Math::Vector3f& dir) { m_dir = dir; }
std::optional<RayHit> IntersectSphere(const Math::Vector3f& center, float radius) const;
int IntersectSphere(const Math::Vector3f& center, float radius, RayHit& p1, RayHit& p2) const;
std::optional<RayHit> IntersectTriangle(const Math::Vector3f& v0, const Math::Vector3f& v1,
const Math::Vector3f& v2) const;
std::optional<RayHit> IntersectQuad(const Math::Vector3f& v0, const Math::Vector3f& v1,
const Math::Vector3f& v2, const Math::Vector3f& v3) const;
std::optional<RayHit> IntersectAABB(const Math::AABB& bbox) const;
int IntersectAABB(const Math::AABB& bbox, RayHit& p1, RayHit& p2) const;
std::optional<RayHit> IntersectPlane(const Math::Vector3f& pOrigin, const Math::Vector3f& pNorm) const;
[[nodiscard]] Math::Vector3f& GetOrigin() { return m_origin; }
[[nodiscard]] Math::Vector3f& GetDir() { return m_dir; }
[[nodiscard]] const Math::Vector3f& GetOrigin() const { return m_origin; }
[[nodiscard]] const Math::Vector3f& GetDir() const { return m_dir; }
private:
Math::Vector3f m_origin;
Math::Vector3f m_dir;
};
}

216
tests/RayTests.cpp Normal file
View File

@@ -0,0 +1,216 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
#include <catch2/catch_all.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "Math/Math.hpp"
#include "Scene/Ray.hpp"
#include "Scene/GeometryFactory.hpp"
using namespace OpenVulkano::Math;
using namespace OpenVulkano::Math::Utils;
using namespace OpenVulkano::Scene;
namespace
{
void CompareVec3Approx(const Vector3f& f, const Vector3f& s)
{
REQUIRE_THAT(f.x, Catch::Matchers::WithinRel(s.x));
REQUIRE_THAT(f.x, Catch::Matchers::WithinRel(s.x));
REQUIRE_THAT(f.x, Catch::Matchers::WithinRel(s.x));
}
};
TEST_CASE("RaySphereIntersection")
{
auto sphere = GeometryFactory::MakeSphere(1, 32, 16);
RayHit h1, h2;
// 2 intersections
{
Ray ray(Vector3f(0, 0, -5), Vector3f(0, 0, 1));
REQUIRE(ray.IntersectSphere(Vector3f(0), 1, h1, h2) == 2);
REQUIRE((h1.point == Vector3f(0, 0, -1) && h2.point == Vector3f(0, 0, 1)));
REQUIRE((h1.normal == Vector3f(0, 0, -1) && h2.normal == Vector3f(0, 0, 1)));
REQUIRE(h1.distance < h2.distance);
REQUIRE((h1.distance == distance(ray.GetOrigin(), h1.point) && h2.distance == distance(ray.GetOrigin(), h2.point)));
// this returns just closest point
if (auto opt = ray.IntersectSphere(Vector3f(0), 1))
{
REQUIRE(opt.value() == h1);
}
}
// 1 intersection
{
Ray ray(Vector3f(1, 0, -1), Vector3f(0, 0, 1));
REQUIRE(ray.IntersectSphere(Vector3f(0), 1, h1, h2) == 1);
REQUIRE(h1 == h2);
REQUIRE(h1.point == Vector3f(1, 0, 0));
REQUIRE(ray.IntersectSphere(Vector3f(0), 1).value() == h1);
}
// 0 intersections
{
Ray ray(Vector3f(2, 0, -1), Vector3f(0, 0, 1));
REQUIRE(ray.IntersectSphere(Vector3f(0), 1, h1, h2) == 0);
REQUIRE(!ray.IntersectSphere(Vector3f(0), 1).has_value());
}
// ray is inside sphere
{
Ray ray(Vector3f(0, 0, 0), Vector3f(0.5, 0.5, 1));
REQUIRE(ray.IntersectSphere(Vector3f(0), 1, h1, h2) == 1);
REQUIRE(h1 == h2);
::CompareVec3Approx(h1.normal, h1.point);
const auto& value = ray.IntersectSphere(Vector3f(0), 1).value();
REQUIRE(value.distance == h1.distance);
::CompareVec3Approx(value.normal, h1.normal);
::CompareVec3Approx(value.point, h1.point);
}
// ray intersects sphere behind the origin
{
Ray ray(Vector3f(0, 0, 3), Vector3f(0, 0, 1));
REQUIRE(ray.IntersectSphere(Vector3f(0), 1, h1, h2) == 0);
REQUIRE(!ray.IntersectSphere(Vector3f(0), 1).has_value());
}
}
TEST_CASE("RayTriangleIntersection")
{
auto tri = GeometryFactory::MakeTriangle(Vector3f(0), Vector3f(3, 0, 0), Vector3f(1.5, 2, 0));
std::optional<RayHit> hit;
// intersects
{
Ray ray(Vector3f(1.5, 2, -5), Vector3f(0, 0, 1));
hit = ray.IntersectTriangle(tri.vertices[0].position, tri.vertices[1].position, tri.vertices[2].position);
REQUIRE(hit.has_value());
REQUIRE(hit->distance == distance(ray.GetOrigin(), hit->point));
REQUIRE(hit->point == Vector3f(1.5, 2, 0));
}
{
Ray ray(Vector3f(1.5, 1, -1), Vector3f(0, 0, 1));
hit = ray.IntersectTriangle(tri.vertices[0].position, tri.vertices[1].position, tri.vertices[2].position);
REQUIRE(hit.has_value());
REQUIRE(hit->distance == distance(ray.GetOrigin(), hit->point));
REQUIRE(hit->point == Vector3f(1.5, 1, 0));
}
// no intersections
{
Ray ray(Vector3f(5, 0, 0), Vector3f(0, 0, 1));
hit = ray.IntersectTriangle(tri.vertices[0].position, tri.vertices[1].position, tri.vertices[2].position);
REQUIRE(!hit.has_value());
}
{
Ray ray(Vector3f(1.5, 1, 0.5), Vector3f(0, 0, 1));
hit = ray.IntersectTriangle(tri.vertices[0].position, tri.vertices[1].position, tri.vertices[2].position);
REQUIRE(!hit.has_value());
}
}
TEST_CASE("RayQuadIntersection")
{
auto cube = GeometryFactory::MakeCube();
std::optional<RayHit> hit;
// intersects
{
Ray ray(Vector3f(0), Vector3f(0, 0, 1));
// front face
hit = ray.IntersectQuad(cube.vertices[0].position, cube.vertices[1].position, cube.vertices[2].position,
cube.vertices[3].position);
REQUIRE(hit.has_value());
REQUIRE(hit->point == Vector3f(0, 0, 0.5));
}
// no intersections
{
Ray ray(Vector3f(0), Vector3f(0, 0, -1));
hit = ray.IntersectQuad(cube.vertices[0].position, cube.vertices[1].position, cube.vertices[2].position,
cube.vertices[3].position);
REQUIRE(!hit.has_value());
}
{
Ray ray(Vector3f(1, 1, 0), Vector3f(0, 0, 1));
hit = ray.IntersectQuad(cube.vertices[0].position, cube.vertices[1].position, cube.vertices[2].position,
cube.vertices[3].position);
REQUIRE(!hit.has_value());
}
}
TEST_CASE("RayAABBIntersection")
{
auto sphere = GeometryFactory::MakeSphere(1, 32, 16);
sphere.CalculateAABB();
std::optional<RayHit> hit;
RayHit h1, h2;
// intersects
{
Ray ray(Vector3f(0, 0, -2), Vector3f(0, 0, 1));
REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 2);
REQUIRE(h1.distance < h2.distance);
REQUIRE(h1.point == Vector3f(0, 0, -1));
REQUIRE(h2.point == Vector3f(0, 0, 1));
auto p = ray.IntersectAABB(sphere.aabb);
REQUIRE(p->point == h1.point);
REQUIRE(p->distance == h1.distance);
}
{
Ray ray(Vector3f(0, 0, 1), Vector3f(0, 0, 1));
REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 1);
REQUIRE(h1.distance == h2.distance);
CompareVec3Approx(h1.point, h2.point);
REQUIRE(h1.point == Vector3f(0, 0, 1));
REQUIRE(ray.IntersectAABB(sphere.aabb)->distance == h1.distance);
}
{
// inside sphere
Ray ray(Vector3f(0), Vector3f(0.3));
REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 1);
REQUIRE(h1 == h2);
auto val = ray.IntersectAABB(sphere.aabb);
REQUIRE(val.has_value());
REQUIRE_THAT(val->distance, Catch::Matchers::WithinRel(h1.distance));
CompareVec3Approx(val->point, h1.point);
}
{
Ray ray(Vector3f(2, -0.5, 1.5), Vector3f(-2, 0.5, -1.5));
REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 2);
REQUIRE(h1.distance < h2.distance);
auto val = ray.IntersectAABB(sphere.aabb);
REQUIRE(val.has_value());
REQUIRE(val->distance == h1.distance);
CompareVec3Approx(val->point, h1.point);
}
// no intersections
{
Ray ray(Vector3f(3, 0, 1), Vector3f(0, 0, 1));
REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 0);
REQUIRE(!ray.IntersectAABB(sphere.aabb).has_value());
}
{
Ray ray(Vector3f(0, 0, 1.1), Vector3f(0, 0, 1));
REQUIRE(ray.IntersectAABB(sphere.aabb, h1, h2) == 0);
REQUIRE(!ray.IntersectAABB(sphere.aabb).has_value());
}
}
TEST_CASE("RayPlaneIntersection")
{
Vector3f pOrigin = Vector3f(0);
Vector3f pNorm = Vector3f(0, 1, 0);
{
Ray ray(Vector3f(2, -2, 2), Vector3f(0, 1, 0));
auto hit = ray.IntersectPlane(pOrigin, pNorm);
REQUIRE(hit.has_value());
REQUIRE(hit->normal == pNorm);
REQUIRE(hit->distance == 2.f);
REQUIRE(hit->point == Vector3f(2, 0, 2));
}
{
Ray ray(Vector3f(2, -2, 2), Vector3f(1, 0, 0));
REQUIRE(!ray.IntersectPlane(pOrigin, pNorm));
}
{
Ray ray(Vector3f(0), Vector3f(1, 0, 0));
REQUIRE(!ray.IntersectPlane(pOrigin, pNorm));
}
}