Crafter.Math/tests/Intersection.cpp

519 lines
21 KiB
C++
Raw Normal View History

2026-05-18 18:03:20 +02:00
/*
Crafter® Build
Copyright (C) 2026 Catcrafts®
Catcrafts.net
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License version 3.0 as published by the Free Software Foundation;
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include <cmath>
import Crafter.Math;
import std;
using namespace Crafter;
namespace {
constexpr float kEps = 1e-3f;
constexpr float kMaxF = std::numeric_limits<float>::max();
constexpr bool FloatEquals(float a, float b, float epsilon = kEps) {
return std::abs(a - b) < epsilon;
}
VectorF32<3, 1> Vec3(float x, float y, float z) {
alignas(16) float buf[4] = { x, y, z, 0.0f };
return VectorF32<3, 1>(buf);
}
VectorF32<4, 1> Vec4(float x, float y, float z, float w) {
alignas(16) float buf[4] = { x, y, z, w };
return VectorF32<4, 1>(buf);
}
2026-05-18 19:57:40 +02:00
// Pack Total = Packing * N vec3 records into N packed VectorF32<3, Packing>s.
// `data[i]` is the i-th sub-primitive's three components in [x, y, z] order;
// the helper places `data[batch*Packing + sub]` into the `sub`-th slot of
// `result[batch]`. Records beyond `data.size()` are left as zeros.
template <std::uint8_t Packing>
std::array<VectorF32<3, Packing>, VectorF32<3, Packing>::BatchSize>
PackVec3(std::span<const std::array<float, 3>> data) {
constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize;
std::array<VectorF32<3, Packing>, N> result;
for (std::uint8_t b = 0; b < N; ++b) {
alignas(64) float buf[VectorF32<3, Packing>::AlignmentElement] = {};
for (std::uint8_t s = 0; s < Packing; ++s) {
std::size_t idx = static_cast<std::size_t>(b) * Packing + s;
if (idx < data.size()) {
buf[s * 3 + 0] = data[idx][0];
buf[s * 3 + 1] = data[idx][1];
buf[s * 3 + 2] = data[idx][2];
}
}
result[b] = VectorF32<3, Packing>(buf);
}
return result;
2026-05-18 18:03:20 +02:00
}
2026-05-18 19:57:40 +02:00
// Same idea for vec4 records (quaternions).
template <std::uint8_t Packing>
std::array<VectorF32<4, Packing>, VectorF32<3, Packing>::BatchSize>
PackVec4MatchingVec3Batch(std::span<const std::array<float, 4>> data) {
constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize;
std::array<VectorF32<4, Packing>, N> result;
for (std::uint8_t b = 0; b < N; ++b) {
alignas(64) float buf[VectorF32<4, Packing>::AlignmentElement] = {};
for (std::uint8_t s = 0; s < Packing; ++s) {
std::size_t idx = static_cast<std::size_t>(b) * Packing + s;
if (idx < data.size()) {
buf[s * 4 + 0] = data[idx][0];
buf[s * 4 + 1] = data[idx][1];
buf[s * 4 + 2] = data[idx][2];
buf[s * 4 + 3] = data[idx][3];
}
}
result[b] = VectorF32<4, Packing>(buf);
}
return result;
}
// Pack `Total` scalars into a VectorF32<1, Total>.
template <std::uint8_t Total>
VectorF32<1, Total> PackScalars(std::span<const float> data) {
alignas(64) float buf[VectorF32<1, Total>::AlignmentElement] = {};
for (std::size_t i = 0; i < data.size() && i < Total; ++i) buf[i] = data[i];
return VectorF32<1, Total>(buf);
}
template <std::uint8_t Packing>
std::string* TestRayTriangleN() {
constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize;
constexpr std::uint8_t Total = Packing * N;
2026-05-18 18:03:20 +02:00
VectorF32<3, 1> rayOrigin = Vec3(0, 0, -5);
VectorF32<3, 1> rayDir = Vec3(0, 0, 1);
2026-05-18 19:57:40 +02:00
// Cycle of four triangle patterns, repeated to fill Total slots:
// 0: hits at z=0 (t=5)
// 1: hits at z=10 (t=15)
// 2: front-facing but off to the side - u/v rejected (miss)
// 3: parallel to the ray (miss)
constexpr std::array<std::array<float, 3>, 4> v0_pat = {{
{-1, -1, 0}, {-1, -1, 10}, { 99, -1, 0}, {-1, 2, -1}
}};
constexpr std::array<std::array<float, 3>, 4> v1_pat = {{
{ 0, 1, 0}, { 0, 1, 10}, {100, 1, 0}, { 1, 2, 1}
}};
constexpr std::array<std::array<float, 3>, 4> v2_pat = {{
{ 1, -1, 0}, { 1, -1, 10}, {101, -1, 0}, { 0, 2, 2}
}};
constexpr std::array<float, 4> expected_pat = { 5.0f, 15.0f, kMaxF, kMaxF };
2026-05-18 18:03:20 +02:00
2026-05-18 19:57:40 +02:00
std::array<std::array<float, 3>, Total> v0Data, v1Data, v2Data;
for (std::uint8_t i = 0; i < Total; ++i) {
v0Data[i] = v0_pat[i % 4];
v1Data[i] = v1_pat[i % 4];
v2Data[i] = v2_pat[i % 4];
}
auto v0 = PackVec3<Packing>(v0Data);
auto v1 = PackVec3<Packing>(v1Data);
auto v2 = PackVec3<Packing>(v2Data);
2026-05-18 18:03:20 +02:00
2026-05-18 19:57:40 +02:00
auto t = IntersectionTestRayTriangle<Packing>(rayOrigin, rayDir, v0, v1, v2);
auto stored = t.template Store<float>();
2026-05-18 18:03:20 +02:00
2026-05-18 19:57:40 +02:00
for (std::uint8_t i = 0; i < Total; ++i) {
float expected = expected_pat[i % 4];
float got = stored[i];
if (expected == kMaxF) {
if (got != kMaxF)
return new std::string(std::format(
"RayTriangle<{}> tri {}: expected miss, got {}", Packing, i, got));
} else if (!FloatEquals(got, expected)) {
return new std::string(std::format(
"RayTriangle<{}> tri {}: expected {}, got {}", Packing, i, expected, got));
}
2026-05-18 18:03:20 +02:00
}
return nullptr;
}
2026-05-18 19:57:40 +02:00
template <std::uint8_t Packing>
std::string* TestRayTriangleBackFacingN() {
constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize;
constexpr std::uint8_t Total = Packing * N;
// Same vertices as the front-facing case but wound CCW from +Z (back-facing
// for a +Z ray) - all sub-primitives should miss.
std::array<std::array<float, 3>, Total> v0Data, v1Data, v2Data;
for (std::uint8_t i = 0; i < Total; ++i) {
v0Data[i] = {-1, -1, 0};
v1Data[i] = { 1, -1, 0};
v2Data[i] = { 0, 1, 0};
}
auto v0 = PackVec3<Packing>(v0Data);
auto v1 = PackVec3<Packing>(v1Data);
auto v2 = PackVec3<Packing>(v2Data);
VectorF32<3, 1> rayOrigin = Vec3(0, 0, -5);
2026-05-18 18:03:20 +02:00
VectorF32<3, 1> rayDir = Vec3(0, 0, 1);
2026-05-18 19:57:40 +02:00
auto t = IntersectionTestRayTriangle<Packing>(rayOrigin, rayDir, v0, v1, v2);
auto stored = t.template Store<float>();
for (std::uint8_t i = 0; i < Total; ++i) {
if (stored[i] != kMaxF)
return new std::string(std::format(
"RayTriangle back-facing<{}> tri {}: expected max, got {}",
Packing, i, stored[i]));
}
2026-05-18 18:03:20 +02:00
return nullptr;
}
2026-05-18 19:57:40 +02:00
template <std::uint8_t Packing>
std::string* TestRaySphereN() {
constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize;
constexpr std::uint8_t Total = Packing * N;
VectorF32<3, 1> rayOrigin = Vec3(0, 0, -10);
VectorF32<3, 1> rayDir = Vec3(0, 0, 1);
// Cycle of four sphere patterns:
// 0: at origin, r=2, first hit z=-2, t=8
// 1: at (0,0,20), r=1, first hit z=19, t=29
// 2: off-axis at (10,10,0), r=0.5, miss
// 3: behind ray at (0,0,-50), r=1, miss
constexpr std::array<std::array<float, 3>, 4> pos_pat = {{
{ 0, 0, 0}, { 0, 0, 20}, {10, 10, 0}, { 0, 0, -50}
}};
constexpr std::array<float, 4> radii_pat = { 2.0f, 1.0f, 0.5f, 1.0f };
constexpr std::array<float, 4> expected_pat = { 8.0f, 29.0f, kMaxF, kMaxF };
std::array<std::array<float, 3>, Total> posData;
std::array<float, Total> radiiData;
for (std::uint8_t i = 0; i < Total; ++i) {
posData[i] = pos_pat[i % 4];
radiiData[i] = radii_pat[i % 4];
}
auto pos = PackVec3<Packing>(posData);
auto radii = PackScalars<Total>(radiiData);
auto t = IntersectionTestRaySphere<Packing>(rayOrigin, rayDir, pos, radii);
auto stored = t.template Store<float>();
for (std::uint8_t i = 0; i < Total; ++i) {
float expected = expected_pat[i % 4];
float got = stored[i];
if (expected == kMaxF) {
if (got != kMaxF)
return new std::string(std::format(
"RaySphere<{}> sph {}: expected miss, got {}", Packing, i, got));
} else if (!FloatEquals(got, expected)) {
return new std::string(std::format(
"RaySphere<{}> sph {}: expected {}, got {}", Packing, i, expected, got));
}
}
return nullptr;
}
template <std::uint8_t Packing>
std::string* TestRayOrientedBoxN() {
constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize;
constexpr std::uint8_t Total = Packing * N;
2026-05-18 18:03:20 +02:00
VectorF32<3, 1> rayOrigin = Vec3(0, 0, -5);
VectorF32<3, 1> rayDir = Vec3(0, 0, 1);
2026-05-18 19:57:40 +02:00
// Cycle of four AABB-as-OBB patterns (identity rotation):
// 0: at origin, full size 2, enters z=-1 -> t=4
// 1: at (0,0,10), full size 2, enters z=9 -> t=14
// 2: off-axis at (50,0,0) -> miss
// 3: behind ray at (0,0,-50) -> miss
constexpr std::array<std::array<float, 3>, 4> pos_pat = {{
{ 0, 0, 0}, { 0, 0, 10}, {50, 0, 0}, { 0, 0, -50}
}};
constexpr std::array<float, 3> size_one = { 2, 2, 2 };
constexpr std::array<float, 4> idQ = { 0, 0, 0, 1 };
constexpr std::array<float, 4> expected_pat = { 4.0f, 14.0f, kMaxF, kMaxF };
2026-05-18 18:03:20 +02:00
2026-05-18 19:57:40 +02:00
std::array<std::array<float, 3>, Total> posData;
std::array<std::array<float, 3>, Total> sizeData;
std::array<std::array<float, 4>, Total> rotData;
for (std::uint8_t i = 0; i < Total; ++i) {
posData[i] = pos_pat[i % 4];
sizeData[i] = size_one;
rotData[i] = idQ;
}
auto pos = PackVec3<Packing>(posData);
auto size = PackVec3<Packing>(sizeData);
auto rot = PackVec4MatchingVec3Batch<Packing>(rotData);
2026-05-18 18:03:20 +02:00
2026-05-18 19:57:40 +02:00
auto t = IntersectionTestRayOrientedBox<Packing>(rayOrigin, rayDir, pos, size, rot);
auto stored = t.template Store<float>();
for (std::uint8_t i = 0; i < Total; ++i) {
float expected = expected_pat[i % 4];
float got = stored[i];
if (expected == kMaxF) {
if (got != kMaxF)
return new std::string(std::format(
"RayOrientedBox<{}> box {}: expected miss, got {}", Packing, i, got));
} else if (!FloatEquals(got, expected)) {
return new std::string(std::format(
"RayOrientedBox<{}> box {}: expected {}, got {}", Packing, i, expected, got));
}
}
return nullptr;
}
// Helper: pack a homogeneous array of OBB descriptors into a PackedOBBs<Packing>.
template <std::uint8_t Packing>
PackedOBBs<Packing> PackOBBs(
std::span<const std::array<float, 3>> halfSizes,
std::span<const std::array<float, 3>> xAxes,
std::span<const std::array<float, 3>> yAxes,
std::span<const std::array<float, 3>> zAxes,
std::span<const std::array<float, 3>> origins
) {
PackedOBBs<Packing> out;
out.halfSize = PackVec3<Packing>(halfSizes);
out.xAxis = PackVec3<Packing>(xAxes);
out.yAxis = PackVec3<Packing>(yAxes);
out.zAxis = PackVec3<Packing>(zAxes);
out.origin = PackVec3<Packing>(origins);
return out;
}
// SphereOrientedBox takes a PackedOBBs (half-extents, three rotation axes,
// origin per sub-box). For axis-aligned boxes the axes are world x/y/z.
template <std::uint8_t Packing>
std::string* TestSphereOrientedBoxN() {
constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize;
constexpr std::uint8_t Total = Packing * N;
VectorF32<3, 1> sphereCenter = Vec3(0, 0, 0);
// Cycle of four box patterns (half-extent semantics, world-axis aligned):
// 0: at origin half=2, r=1 -> sphere inside -> hit
// 1: at (5,0,0) half=1, r=0.5 -> miss
// 2: at (3,0,0) half=1, r=0.5 -> miss
// 3: at origin half=0.5, r=1 -> sphere encloses box center -> hit
constexpr std::array<std::array<float, 3>, 4> size_pat = {{
{ 2, 2, 2}, { 1, 1, 1}, { 1, 1, 1}, {0.5f, 0.5f, 0.5f}
}};
constexpr std::array<std::array<float, 3>, 4> origin_pat = {{
{ 0, 0, 0}, { 5, 0, 0}, { 3, 0, 0}, { 0, 0, 0}
}};
constexpr std::array<float, 4> radii_pat = { 1.0f, 0.5f, 0.5f, 1.0f };
constexpr std::array<float, 4> expected_pat = { 0.0f, kMaxF, kMaxF, 0.0f };
constexpr std::array<float, 3> ax_x = { 1, 0, 0 };
constexpr std::array<float, 3> ax_y = { 0, 1, 0 };
constexpr std::array<float, 3> ax_z = { 0, 0, 1 };
std::array<std::array<float, 3>, Total> sizeData, originData;
std::array<std::array<float, 3>, Total> xAxesData, yAxesData, zAxesData;
std::array<float, Total> radiiData;
for (std::uint8_t i = 0; i < Total; ++i) {
sizeData[i] = size_pat[i % 4];
originData[i] = origin_pat[i % 4];
xAxesData[i] = ax_x;
yAxesData[i] = ax_y;
zAxesData[i] = ax_z;
radiiData[i] = radii_pat[i % 4];
}
auto boxes = PackOBBs<Packing>(sizeData, xAxesData, yAxesData, zAxesData, originData);
auto radii = PackScalars<Total>(radiiData);
auto t = IntersectionTestSphereOrientedBox<Packing>(sphereCenter, radii, boxes);
auto stored = t.template Store<float>();
for (std::uint8_t i = 0; i < Total; ++i) {
float expected = expected_pat[i % 4];
float got = stored[i];
if (expected == kMaxF) {
if (got != kMaxF)
return new std::string(std::format(
"SphereOrientedBox<{}> box {}: expected miss, got {}", Packing, i, got));
} else if (!FloatEquals(got, expected)) {
return new std::string(std::format(
"SphereOrientedBox<{}> box {}: expected {}, got {}", Packing, i, expected, got));
}
}
return nullptr;
}
// OBB-vs-OBB test against the new templated SAT routine. Cycles through:
// 0: identical unit boxes at (0,0,0) and (1,0,0) -> overlap on x
// 1: identical unit boxes at (0,0,0) and (10,0,0) -> far apart, miss
// 2: rotated-45° box at origin vs identity at (1,0,0). Both have half=1.
// The rotated box's projection along world-x is half=sqrt(2)≈1.414, so
// the boxes still overlap on the world-x axis.
// 3: identical unit boxes at (0,0,0) and (3,0,0) -> miss
template <std::uint8_t Packing>
std::string* TestOBBOBBN() {
constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize;
constexpr std::uint8_t Total = Packing * N;
constexpr float kRot45 = 0.70710678f; // cos(45°) = sin(45°)
constexpr std::array<std::array<float, 3>, 4> halfA_pat = {{
{ 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 }
}};
constexpr std::array<std::array<float, 3>, 4> halfB_pat = halfA_pat;
constexpr std::array<std::array<float, 3>, 4> originA_pat = {{
{ 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }
}};
constexpr std::array<std::array<float, 3>, 4> originB_pat = {{
{ 1, 0, 0 }, { 10, 0, 0 }, { 1, 0, 0 }, { 3, 0, 0 }
}};
// Box A axes: identity for patterns 0/1/3, rotated 45° around z for pattern 2.
constexpr std::array<std::array<float, 3>, 4> xAxisA_pat = {{
{ 1, 0, 0 }, { 1, 0, 0 }, { kRot45, kRot45, 0 }, { 1, 0, 0 }
}};
constexpr std::array<std::array<float, 3>, 4> yAxisA_pat = {{
{ 0, 1, 0 }, { 0, 1, 0 }, { -kRot45, kRot45, 0 }, { 0, 1, 0 }
}};
constexpr std::array<std::array<float, 3>, 4> zAxisA_pat = {{
{ 0, 0, 1 }, { 0, 0, 1 }, { 0, 0, 1 }, { 0, 0, 1 }
}};
constexpr std::array<std::array<float, 3>, 4> xAxisB_pat = {{
{ 1, 0, 0 }, { 1, 0, 0 }, { 1, 0, 0 }, { 1, 0, 0 }
}};
constexpr std::array<std::array<float, 3>, 4> yAxisB_pat = {{
{ 0, 1, 0 }, { 0, 1, 0 }, { 0, 1, 0 }, { 0, 1, 0 }
}};
constexpr std::array<std::array<float, 3>, 4> zAxisB_pat = zAxisA_pat;
constexpr std::array<float, 4> expected_pat = { 0.0f, kMaxF, 0.0f, kMaxF };
std::array<std::array<float, 3>, Total>
halfA, halfB, originA, originB,
xA, yA, zA, xB, yB, zB;
for (std::uint8_t i = 0; i < Total; ++i) {
halfA[i] = halfA_pat[i % 4];
halfB[i] = halfB_pat[i % 4];
originA[i] = originA_pat[i % 4];
originB[i] = originB_pat[i % 4];
xA[i] = xAxisA_pat[i % 4]; yA[i] = yAxisA_pat[i % 4]; zA[i] = zAxisA_pat[i % 4];
xB[i] = xAxisB_pat[i % 4]; yB[i] = yAxisB_pat[i % 4]; zB[i] = zAxisB_pat[i % 4];
}
auto a = PackOBBs<Packing>(halfA, xA, yA, zA, originA);
auto b = PackOBBs<Packing>(halfB, xB, yB, zB, originB);
auto r = IntersectionTestOrientedBoxOrientedBox<Packing>(a, b);
auto stored = r.template Store<float>();
for (std::uint8_t i = 0; i < Total; ++i) {
float expected = expected_pat[i % 4];
float got = stored[i];
if (expected == kMaxF) {
if (got != kMaxF)
return new std::string(std::format(
"OBBOBB<{}> pair {}: expected miss, got {}", Packing, i, got));
} else if (got != expected) {
return new std::string(std::format(
"OBBOBB<{}> pair {}: expected {}, got {}", Packing, i, expected, got));
}
}
2026-05-18 18:03:20 +02:00
return nullptr;
}
MatrixRowMajor<float, 4, 3, 1> MakeBoxMatrix(float tx, float ty, float tz) {
return MatrixRowMajor<float, 4, 3, 1>(
1, 0, 0, tx,
0, 1, 0, ty,
0, 0, 1, tz
);
}
std::string* TestGetOBBCorners() {
// Identity matrix - the 8 corners are exactly ±size on each axis.
VectorF32<3, 1> size = Vec3(2, 3, 4);
auto m = MakeBoxMatrix(0, 0, 0);
std::array<VectorF32<3, 1>, 8> corners = GetOBBCorners(size, m);
constexpr std::array<std::array<float, 3>, 8> expected = {{
{-2, -3, -4}, { 2, -3, -4}, {-2, 3, -4}, { 2, 3, -4},
{-2, -3, 4}, { 2, -3, 4}, {-2, 3, 4}, { 2, 3, 4},
}};
for (std::uint8_t i = 0; i < 8; ++i) {
std::array<float, 4> v = corners[i].template Store<float>();
for (std::uint8_t j = 0; j < 3; ++j) {
if (!FloatEquals(v[j], expected[i][j]))
return new std::string(std::format(
"GetOBBCorners corner {} lane {}: expected {}, got {}",
i, j, expected[i][j], v[j]));
}
}
auto m2 = MakeBoxMatrix(10, 20, 30);
std::array<VectorF32<3, 1>, 8> corners2 = GetOBBCorners(size, m2);
for (std::uint8_t i = 0; i < 8; ++i) {
std::array<float, 4> v = corners2[i].template Store<float>();
std::array<float, 3> exp = {
expected[i][0] + 10.0f,
expected[i][1] + 20.0f,
expected[i][2] + 30.0f
};
for (std::uint8_t j = 0; j < 3; ++j) {
if (!FloatEquals(v[j], exp[j]))
return new std::string(std::format(
"GetOBBCorners translated corner {} lane {}: expected {}, got {}",
i, j, exp[j], v[j]));
}
}
return nullptr;
}
2026-05-18 19:57:40 +02:00
// Top-level wrappers: exercise each refactored function at Packing=1 (always
// supported) and at its default Packing (OptimalPacking for the build target).
std::string* TestRayTriangle() { return TestRayTriangleN<1>(); }
std::string* TestRayTriangleOpt() { return TestRayTriangleN<VectorF32<3, 1>::OptimalPacking>(); }
std::string* TestRayTriangleBackFacing(){ return TestRayTriangleBackFacingN<1>(); }
std::string* TestRayTriangleBackFacingOpt() { return TestRayTriangleBackFacingN<VectorF32<3, 1>::OptimalPacking>(); }
std::string* TestRaySphere() { return TestRaySphereN<1>(); }
std::string* TestRaySphereOpt() { return TestRaySphereN<VectorF32<3, 1>::OptimalPacking>(); }
std::string* TestRayOrientedBox() { return TestRayOrientedBoxN<1>(); }
std::string* TestRayOrientedBoxOpt() {
constexpr std::uint8_t P = std::min(
VectorF32<3, 1>::OptimalPacking, VectorF32<4, 1>::OptimalPacking);
return TestRayOrientedBoxN<P>();
2026-05-18 18:03:20 +02:00
}
2026-05-18 19:57:40 +02:00
std::string* TestSphereOrientedBox() { return TestSphereOrientedBoxN<1>(); }
std::string* TestSphereOrientedBoxOpt() { return TestSphereOrientedBoxN<VectorF32<3, 1>::OptimalPacking>(); }
std::string* TestOBBOBB() { return TestOBBOBBN<1>(); }
std::string* TestOBBOBBOpt() { return TestOBBOBBN<VectorF32<3, 1>::OptimalPacking>(); }
2026-05-18 18:03:20 +02:00
} // namespace
int main() {
using Fn = std::string* (*)();
2026-05-18 19:57:40 +02:00
constexpr std::array<std::pair<const char*, Fn>, 13> tests = {{
{ "RayTriangle<1>", TestRayTriangle },
{ "RayTriangle<Opt>", TestRayTriangleOpt },
{ "RayTriangleBackFacing<1>", TestRayTriangleBackFacing },
{ "RayTriangleBackFacing<Opt>", TestRayTriangleBackFacingOpt },
{ "RaySphere<1>", TestRaySphere },
{ "RaySphere<Opt>", TestRaySphereOpt },
{ "RayOrientedBox<1>", TestRayOrientedBox },
{ "RayOrientedBox<Opt>", TestRayOrientedBoxOpt },
{ "SphereOrientedBox<1>", TestSphereOrientedBox },
{ "SphereOrientedBox<Opt>", TestSphereOrientedBoxOpt },
{ "GetOBBCorners", TestGetOBBCorners },
{ "OBBOBB<1>", TestOBBOBB },
{ "OBBOBB<Opt>", TestOBBOBBOpt },
2026-05-18 18:03:20 +02:00
}};
for (auto const& [name, fn] : tests) {
if (auto err = std::unique_ptr<std::string>(fn())) {
std::println(std::cerr, "[{}] {}", name, *err);
return 1;
}
}
return 0;
}