/* 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 import Crafter.Math; import std; using namespace Crafter; namespace { constexpr float kEps = 1e-3f; constexpr float kMaxF = std::numeric_limits::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); } // 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::array, VectorF32<3, Packing>::BatchSize> PackVec3(std::span> data) { constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize; std::array, 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(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; } // Same idea for vec4 records (quaternions). template std::array, VectorF32<3, Packing>::BatchSize> PackVec4MatchingVec3Batch(std::span> data) { constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize; std::array, 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(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 VectorF32<1, Total> PackScalars(std::span 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::string* TestRayTriangleN() { constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize; constexpr std::uint8_t Total = Packing * N; VectorF32<3, 1> rayOrigin = Vec3(0, 0, -5); VectorF32<3, 1> rayDir = Vec3(0, 0, 1); // 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, 4> v0_pat = {{ {-1, -1, 0}, {-1, -1, 10}, { 99, -1, 0}, {-1, 2, -1} }}; constexpr std::array, 4> v1_pat = {{ { 0, 1, 0}, { 0, 1, 10}, {100, 1, 0}, { 1, 2, 1} }}; constexpr std::array, 4> v2_pat = {{ { 1, -1, 0}, { 1, -1, 10}, {101, -1, 0}, { 0, 2, 2} }}; constexpr std::array expected_pat = { 5.0f, 15.0f, kMaxF, kMaxF }; std::array, 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(v0Data); auto v1 = PackVec3(v1Data); auto v2 = PackVec3(v2Data); auto t = IntersectionTestRayTriangle(rayOrigin, rayDir, v0, v1, v2); auto stored = t.template Store(); 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)); } } return nullptr; } template 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, 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(v0Data); auto v1 = PackVec3(v1Data); auto v2 = PackVec3(v2Data); VectorF32<3, 1> rayOrigin = Vec3(0, 0, -5); VectorF32<3, 1> rayDir = Vec3(0, 0, 1); auto t = IntersectionTestRayTriangle(rayOrigin, rayDir, v0, v1, v2); auto stored = t.template Store(); 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])); } return nullptr; } template 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, 4> pos_pat = {{ { 0, 0, 0}, { 0, 0, 20}, {10, 10, 0}, { 0, 0, -50} }}; constexpr std::array radii_pat = { 2.0f, 1.0f, 0.5f, 1.0f }; constexpr std::array expected_pat = { 8.0f, 29.0f, kMaxF, kMaxF }; std::array, Total> posData; std::array 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(posData); auto radii = PackScalars(radiiData); auto t = IntersectionTestRaySphere(rayOrigin, rayDir, pos, radii); auto stored = t.template Store(); 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::string* TestRayOrientedBoxN() { constexpr std::uint8_t N = VectorF32<3, Packing>::BatchSize; constexpr std::uint8_t Total = Packing * N; VectorF32<3, 1> rayOrigin = Vec3(0, 0, -5); VectorF32<3, 1> rayDir = Vec3(0, 0, 1); // 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, 4> pos_pat = {{ { 0, 0, 0}, { 0, 0, 10}, {50, 0, 0}, { 0, 0, -50} }}; constexpr std::array size_one = { 2, 2, 2 }; constexpr std::array idQ = { 0, 0, 0, 1 }; constexpr std::array expected_pat = { 4.0f, 14.0f, kMaxF, kMaxF }; std::array, Total> posData; std::array, Total> sizeData; std::array, 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(posData); auto size = PackVec3(sizeData); auto rot = PackVec4MatchingVec3Batch(rotData); auto t = IntersectionTestRayOrientedBox(rayOrigin, rayDir, pos, size, rot); auto stored = t.template Store(); 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. template PackedOBBs PackOBBs( std::span> halfSizes, std::span> xAxes, std::span> yAxes, std::span> zAxes, std::span> origins ) { PackedOBBs out; out.halfSize = PackVec3(halfSizes); out.xAxis = PackVec3(xAxes); out.yAxis = PackVec3(yAxes); out.zAxis = PackVec3(zAxes); out.origin = PackVec3(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::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, 4> size_pat = {{ { 2, 2, 2}, { 1, 1, 1}, { 1, 1, 1}, {0.5f, 0.5f, 0.5f} }}; constexpr std::array, 4> origin_pat = {{ { 0, 0, 0}, { 5, 0, 0}, { 3, 0, 0}, { 0, 0, 0} }}; constexpr std::array radii_pat = { 1.0f, 0.5f, 0.5f, 1.0f }; constexpr std::array expected_pat = { 0.0f, kMaxF, kMaxF, 0.0f }; constexpr std::array ax_x = { 1, 0, 0 }; constexpr std::array ax_y = { 0, 1, 0 }; constexpr std::array ax_z = { 0, 0, 1 }; std::array, Total> sizeData, originData; std::array, Total> xAxesData, yAxesData, zAxesData; std::array 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(sizeData, xAxesData, yAxesData, zAxesData, originData); auto radii = PackScalars(radiiData); auto t = IntersectionTestSphereOrientedBox(sphereCenter, radii, boxes); auto stored = t.template Store(); 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::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, 4> halfA_pat = {{ { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 } }}; constexpr std::array, 4> halfB_pat = halfA_pat; constexpr std::array, 4> originA_pat = {{ { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 }, { 0, 0, 0 } }}; constexpr std::array, 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, 4> xAxisA_pat = {{ { 1, 0, 0 }, { 1, 0, 0 }, { kRot45, kRot45, 0 }, { 1, 0, 0 } }}; constexpr std::array, 4> yAxisA_pat = {{ { 0, 1, 0 }, { 0, 1, 0 }, { -kRot45, kRot45, 0 }, { 0, 1, 0 } }}; constexpr std::array, 4> zAxisA_pat = {{ { 0, 0, 1 }, { 0, 0, 1 }, { 0, 0, 1 }, { 0, 0, 1 } }}; constexpr std::array, 4> xAxisB_pat = {{ { 1, 0, 0 }, { 1, 0, 0 }, { 1, 0, 0 }, { 1, 0, 0 } }}; constexpr std::array, 4> yAxisB_pat = {{ { 0, 1, 0 }, { 0, 1, 0 }, { 0, 1, 0 }, { 0, 1, 0 } }}; constexpr std::array, 4> zAxisB_pat = zAxisA_pat; constexpr std::array expected_pat = { 0.0f, kMaxF, 0.0f, kMaxF }; std::array, 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(halfA, xA, yA, zA, originA); auto b = PackOBBs(halfB, xB, yB, zB, originB); auto r = IntersectionTestOrientedBoxOrientedBox(a, b); auto stored = r.template Store(); 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)); } } return nullptr; } MatrixRowMajor MakeBoxMatrix(float tx, float ty, float tz) { return MatrixRowMajor( 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, 8> corners = GetOBBCorners(size, m); constexpr std::array, 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 v = corners[i].template Store(); 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, 8> corners2 = GetOBBCorners(size, m2); for (std::uint8_t i = 0; i < 8; ++i) { std::array v = corners2[i].template Store(); std::array 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; } // 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::OptimalPacking>(); } std::string* TestRayTriangleBackFacing(){ return TestRayTriangleBackFacingN<1>(); } std::string* TestRayTriangleBackFacingOpt() { return TestRayTriangleBackFacingN::OptimalPacking>(); } std::string* TestRaySphere() { return TestRaySphereN<1>(); } std::string* TestRaySphereOpt() { return TestRaySphereN::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

(); } std::string* TestSphereOrientedBox() { return TestSphereOrientedBoxN<1>(); } std::string* TestSphereOrientedBoxOpt() { return TestSphereOrientedBoxN::OptimalPacking>(); } std::string* TestOBBOBB() { return TestOBBOBBN<1>(); } std::string* TestOBBOBBOpt() { return TestOBBOBBN::OptimalPacking>(); } } // namespace int main() { using Fn = std::string* (*)(); constexpr std::array, 13> tests = {{ { "RayTriangle<1>", TestRayTriangle }, { "RayTriangle", TestRayTriangleOpt }, { "RayTriangleBackFacing<1>", TestRayTriangleBackFacing }, { "RayTriangleBackFacing", TestRayTriangleBackFacingOpt }, { "RaySphere<1>", TestRaySphere }, { "RaySphere", TestRaySphereOpt }, { "RayOrientedBox<1>", TestRayOrientedBox }, { "RayOrientedBox", TestRayOrientedBoxOpt }, { "SphereOrientedBox<1>", TestSphereOrientedBox }, { "SphereOrientedBox", TestSphereOrientedBoxOpt }, { "GetOBBCorners", TestGetOBBCorners }, { "OBBOBB<1>", TestOBBOBB }, { "OBBOBB", TestOBBOBBOpt }, }}; for (auto const& [name, fn] : tests) { if (auto err = std::unique_ptr(fn())) { std::println(std::cerr, "[{}] {}", name, *err); return 1; } } return 0; }