diff --git a/Engine/cpp/Runtime/Core/CMakeLists.txt b/Engine/cpp/Runtime/Core/CMakeLists.txt index cf2a4391..4478a693 100644 --- a/Engine/cpp/Runtime/Core/CMakeLists.txt +++ b/Engine/cpp/Runtime/Core/CMakeLists.txt @@ -1,12 +1,13 @@ -add_modules_library(Definitions) -add_modules_library(Math) -add_modules_library(IO) -add_modules_library(Memory) -add_modules_library(Logging) -add_modules_library(Containers) +# PIC: these are linked into the shared Runtime library (via Core), so their objects +# must be position-independent - a non-PIC static (e.g. Quaternion::identity) otherwise +# fails to relocate when making the shared object. +add_modules_library(Definitions PIC) +add_modules_library(Math PIC) +add_modules_library(IO PIC) +add_modules_library(Memory PIC) +add_modules_library(Logging PIC) -target_link_libraries(Math PUBLIC Definitions bx) +target_link_libraries(Math PUBLIC Definitions) target_link_libraries(IO PUBLIC Definitions stb_image) target_link_libraries(Memory PUBLIC Definitions Math) target_link_libraries(Logging PUBLIC Definitions) -target_link_libraries(Containers PUBLIC Definitions Memory Math) diff --git a/Engine/cpp/Runtime/Core/Math/Functions.cppm b/Engine/cpp/Runtime/Core/Math/Functions.cppm index 6a411f06..52969c71 100644 --- a/Engine/cpp/Runtime/Core/Math/Functions.cppm +++ b/Engine/cpp/Runtime/Core/Math/Functions.cppm @@ -1,5 +1,6 @@ module; +#include #include #include #include @@ -114,6 +115,21 @@ export namespace draco::math { return static_cast(std::pow(x, y)); } + // Trigonometry (radians) and square root. + [[nodiscard]] inline f32 sin(f32 x) noexcept { return std::sin(x); } + [[nodiscard]] inline f32 cos(f32 x) noexcept { return std::cos(x); } + [[nodiscard]] inline f32 tan(f32 x) noexcept { return std::tan(x); } + [[nodiscard]] inline f32 acos(f32 x) noexcept { return std::acos(x); } + [[nodiscard]] inline f32 sqrt(f32 x) noexcept { return std::sqrt(x); } + + // Approximate comparisons against an epsilon (defaults to CMP_EPSILON). + [[nodiscard]] constexpr bool nearlyEqual(f32 a, f32 b, f32 epsilon = CMP_EPSILON) noexcept { + return abs(a - b) <= epsilon; + } + [[nodiscard]] constexpr bool nearlyZero(f32 v, f32 epsilon = CMP_EPSILON) noexcept { + return abs(v) <= epsilon; + } + template constexpr T lerp(T from, T to, T weight) noexcept { return std::lerp(from, to, weight); @@ -202,4 +218,4 @@ export namespace draco::math { T d = (control_1 - start) * T{3.} * omt2 + (control_2 - control_1) * T{6.} * omt * t + (end - control_2) * T{3.} * t2; return d; } -} +} \ No newline at end of file diff --git a/Engine/cpp/Runtime/Core/Math/Math.cppm b/Engine/cpp/Runtime/Core/Math/Math.cppm index bf983a28..275bf225 100644 --- a/Engine/cpp/Runtime/Core/Math/Math.cppm +++ b/Engine/cpp/Runtime/Core/Math/Math.cppm @@ -4,4 +4,6 @@ export import core.defs; export import core.math.constants; export import core.math.functions; export import core.math.types; +export import core.math.matrix4; +export import core.math.quaternion; export import core.math.transform; diff --git a/Engine/cpp/Runtime/Core/Math/Math.test.cpp b/Engine/cpp/Runtime/Core/Math/Math.test.cpp index 4b300766..291a6226 100644 --- a/Engine/cpp/Runtime/Core/Math/Math.test.cpp +++ b/Engine/cpp/Runtime/Core/Math/Math.test.cpp @@ -1,4 +1,5 @@ #include +#include import core.math; @@ -449,28 +450,16 @@ TEST_SUITE("vector2") { ); } - TEST_CASE("approx_eq") { + TEST_CASE("nearlyEqual") { using math::Vector2; - using math::approx_eq; + using math::nearlyEqual; using math::CMP_EPSILON; static constexpr Vector2 v{1.0f, 2.0f}; - static constexpr Vector2 offset = Vector2::xAxis(CMP_EPSILON); - BASIC_R_SUBCASE("distance < epsilon", - ( approx_eq(v, v + offset * 0.5f) ), - ( true ) - ); - - BASIC_R_SUBCASE("distance == epsilon", - ( approx_eq(v, v + offset) ), - ( true ) - ); - - BASIC_R_SUBCASE("distance > epsilon", - ( approx_eq(v, v + offset * 2.0f) ), - ( false ) - ); + CHECK(nearlyEqual(v, v)); + CHECK(nearlyEqual(v, v + Vector2{0.5f * CMP_EPSILON, 0.0f})); // within epsilon + CHECK_FALSE(nearlyEqual(v, v + Vector2{2.0f * CMP_EPSILON, 0.0f})); // beyond epsilon } } @@ -904,28 +893,16 @@ TEST_SUITE("vector3") { ); } - TEST_CASE("approx_eq") { + TEST_CASE("nearlyEqual") { using math::Vector3; - using math::approx_eq; + using math::nearlyEqual; using math::CMP_EPSILON; static constexpr Vector3 v{1.0f, 2.0f, 3.0f}; - static constexpr Vector3 offset = Vector3::xAxis(CMP_EPSILON); - - BASIC_R_SUBCASE("distance < epsilon", - ( approx_eq(v, v + offset * 0.5f) ), - ( true ) - ); - - BASIC_R_SUBCASE("distance == epsilon", - ( approx_eq(v, v + offset) ), - ( true ) - ); - BASIC_R_SUBCASE("distance > epsilon", - ( approx_eq(v, v + offset * 2.0f) ), - ( false ) - ); + CHECK(nearlyEqual(v, v)); + CHECK(nearlyEqual(v, v + Vector3{0.0f, 0.0f, 0.5f * CMP_EPSILON})); // within epsilon + CHECK_FALSE(nearlyEqual(v, v + Vector3{0.0f, 0.0f, 2.0f * CMP_EPSILON})); // beyond epsilon (z lane) } TEST_CASE("cross") { @@ -1395,43 +1372,241 @@ TEST_SUITE("vector4") { ); } - TEST_CASE("approx_eq") { + TEST_CASE("nearlyEqual") { using math::Vector4; - using math::approx_eq; + using math::nearlyEqual; using math::CMP_EPSILON; static constexpr Vector4 v{1.0f, 2.0f, 3.0f, 4.0f}; - static constexpr Vector4 offset = Vector4::xAxis(CMP_EPSILON); - - BASIC_R_SUBCASE("distance < epsilon", - ( approx_eq(v, v + offset * 0.5f) ), - ( true ) - ); - BASIC_R_SUBCASE("distance == epsilon", - ( approx_eq(v, v + offset) ), - ( true ) - ); - - BASIC_R_SUBCASE("distance > epsilon", - ( approx_eq(v, v + offset * 2.0f) ), - ( false ) - ); + CHECK(nearlyEqual(v, v)); + CHECK(nearlyEqual(v, v + Vector4{0.0f, 0.0f, 0.0f, 0.5f * CMP_EPSILON})); // within epsilon + CHECK_FALSE(nearlyEqual(v, v + Vector4{0.0f, 0.0f, 0.0f, 2.0f * CMP_EPSILON})); // beyond epsilon (w lane) } TEST_CASE("Transform") { using math::Transform; static constexpr Transform transform; - STATIC_REQUIRE(transform.position[0] == 0.f); - STATIC_REQUIRE(transform.position[1] == 0.f); - STATIC_REQUIRE(transform.position[2] == 0.f); - STATIC_REQUIRE(transform.rotation[0] == 0.f); - STATIC_REQUIRE(transform.rotation[1] == 0.f); - STATIC_REQUIRE(transform.rotation[2] == 0.f); - STATIC_REQUIRE(transform.scale[0] == 1.f); - STATIC_REQUIRE(transform.scale[1] == 1.f); - STATIC_REQUIRE(transform.scale[2] == 1.f); + // Default: position 0, rotation identity, scale 1. + STATIC_REQUIRE(transform.position.x == 0.f); + STATIC_REQUIRE(transform.position.y == 0.f); + STATIC_REQUIRE(transform.position.z == 0.f); + STATIC_REQUIRE(transform.rotation.x == 0.f); + STATIC_REQUIRE(transform.rotation.y == 0.f); + STATIC_REQUIRE(transform.rotation.z == 0.f); + STATIC_REQUIRE(transform.rotation.w == 1.f); + STATIC_REQUIRE(transform.scale.x == 1.f); + STATIC_REQUIRE(transform.scale.y == 1.f); + STATIC_REQUIRE(transform.scale.z == 1.f); + } +} + +TEST_SUITE("matrix4") { + TEST_CASE("identity and multiply") { + using namespace draco::math; + const Matrix4 id = Matrix4::identity(); + const Matrix4 t = Matrix4::translation(Vector3{ 1.0f, 2.0f, 3.0f }); + + CHECK(nearlyEqual(id * t, t)); + CHECK(nearlyEqual(t * id, t)); + + static_assert(Matrix4::identity()[0, 0] == 1.0f); + static_assert(Matrix4::identity()[0, 1] == 0.0f); + } + + TEST_CASE("translation lives in the last row (row vectors)") { + using namespace draco::math; + const Matrix4 t = Matrix4::translation(Vector3{ 10.0f, 20.0f, 30.0f }); + CHECK(t.m[3][0] == 10.0f); + CHECK(t.m[3][1] == 20.0f); + CHECK(t.m[3][2] == 30.0f); + + const Vector3 p = transformPoint(Vector3{ 1.0f, 1.0f, 1.0f }, t); + CHECK(nearlyEqual(p, Vector3{ 11.0f, 21.0f, 31.0f })); + + // Directions ignore translation. + CHECK(nearlyEqual(transformDirection(Vector3{ 1.0f, 0.0f, 0.0f }, t), Vector3{ 1.0f, 0.0f, 0.0f })); + } + + TEST_CASE("rotationZ(90) maps +X to +Y (row vectors)") { + using namespace draco::math; + const Matrix4 rz = Matrix4::rotationZ(degToRad(90.0f)); + CHECK(nearlyEqual(transformDirection(Vector3::xAxis(), rz), Vector3::yAxis())); + + // rotationY(90) maps +Z to +X. + const Matrix4 ry = Matrix4::rotationY(degToRad(90.0f)); + CHECK(nearlyEqual(transformDirection(Vector3::zAxis(), ry), Vector3::xAxis())); + } + + TEST_CASE("2D affine helpers (transformPoint2D, operator==)") { + using namespace draco::math; + CHECK(Matrix4::identity() == Matrix4::identity()); + CHECK_FALSE(Matrix4::translation(Vector3{ 1, 0, 0 }) == Matrix4::identity()); + + const Matrix4 t = Matrix4::translation(Vector3{ 5.0f, 7.0f, 0.0f }); + CHECK(nearlyEqual(transformPoint2D(Vector2{ 1.0f, 2.0f }, t), Vector2{ 6.0f, 9.0f })); + + // Row vectors, left-to-right: v * (S * T). + const Matrix4 st = Matrix4::scale(Vector3{ 2.0f, 3.0f, 1.0f }) * Matrix4::translation(Vector3{ 1.0f, 1.0f, 0.0f }); + CHECK(nearlyEqual(transformPoint2D(Vector2{ 1.0f, 1.0f }, st), Vector2{ 3.0f, 4.0f })); + + const Matrix4 rz = Matrix4::rotationZ(degToRad(90.0f)); + CHECK(nearlyEqual(transformPoint2D(Vector2{ 1.0f, 0.0f }, rz), Vector2{ 0.0f, 1.0f })); + } + + TEST_CASE("composition reads left-to-right (scale then translate)") { + using namespace draco::math; + const Matrix4 st = Matrix4::scale(Vector3{ 2.0f, 2.0f, 2.0f }) * Matrix4::translation(Vector3{ 1.0f, 0.0f, 0.0f }); + const Vector3 p = transformPoint(Vector3{ 1.0f, 1.0f, 1.0f }, st); + CHECK(nearlyEqual(p, Vector3{ 3.0f, 2.0f, 2.0f })); + } + + TEST_CASE("perspective has the expected projective structure") { + using namespace draco::math; + const Matrix4 proj = Matrix4::perspectiveFovRH(degToRad(90.0f), 1.0f, 1.0f, 100.0f); + CHECK(proj.m[2][3] == -1.0f); // w' = -z (RH) + CHECK(nearlyEqual(proj.m[0][0], 1.0f)); // xScale = 1/tan(45) at aspect 1 + + // Depth mapping: the near plane (z = -zNear) maps to NDC depth 0, the far plane + // (z = -zFar) to NDC depth 1 - exercising the near/far-derived coefficients. + const auto ndcDepth = [&](f32 z) { + const Vector4 clip = Vector4{ 0.0f, 0.0f, z, 1.0f } * proj; + return clip.z / clip.w; + }; + CHECK(nearlyEqual(ndcDepth(-1.0f), 0.0f, 1.0e-4f)); + CHECK(nearlyEqual(ndcDepth(-100.0f), 1.0f, 1.0e-4f)); + } + + TEST_CASE("determinant") { + using namespace draco::math; + CHECK(nearlyEqual(determinant(Matrix4::identity()), 1.0f)); + CHECK(nearlyEqual(determinant(Matrix4::scale(Vector3{ 2.0f, 3.0f, 4.0f })), 24.0f)); + } + + TEST_CASE("inverse undoes the transform") { + using namespace draco::math; + Transform xform; + xform.scale = Vector3{ 2.0f, 0.5f, 3.0f }; + xform.rotation = Quaternion::fromAxisAngle(normalize(Vector3{ 1.0f, 2.0f, 3.0f }), degToRad(50.0f)); + xform.position = Vector3{ 5.0f, -2.0f, 1.0f }; + + const Matrix4 m = xform.toMatrix(); + const Matrix4 inv = inverse(m); + + CHECK(nearlyEqual(m * inv, Matrix4::identity(), 1.0e-3f)); + CHECK(nearlyEqual(inv * m, Matrix4::identity(), 1.0e-3f)); + + // Point transformed then inverse-transformed returns to itself. + const Vector3 p{ 3.0f, 4.0f, 5.0f }; + const Vector3 roundTrip = transformPoint(transformPoint(p, m), inv); + CHECK(nearlyEqual(roundTrip, p, 1.0e-3f)); + + // Singular matrix: tryInverse reports the failure explicitly (no silent identity). + CHECK_FALSE(tryInverse(Matrix4::scale(Vector3::zero())).has_value()); + + // A small-but-invertible scale must NOT be flagged singular by the scale-aware + // threshold (an absolute-epsilon determinant check would wrongly reject it). + const Vector3 tinyScale{ 1.0e-3f, 1.0e-3f, 1.0e-3f }; + const auto tiny = tryInverse(Matrix4::scale(tinyScale)); + REQUIRE(tiny.has_value()); + CHECK(nearlyEqual(Matrix4::scale(tinyScale) * *tiny, Matrix4::identity(), 1.0e-2f)); + } +} + +TEST_SUITE("quaternion") { + TEST_CASE("rotates vectors and agrees with its matrix") { + using namespace draco::math; + const Quaternion q = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f)); + + // 90 deg about Z maps +X to +Y. + CHECK(nearlyEqual(rotateVector(q, Vector3::xAxis()), Vector3::yAxis())); + + // Quaternion rotation and its matrix agree. + const Matrix4 r = rotationMatrix(q); + CHECK(nearlyEqual(rotateVector(q, Vector3::xAxis()), transformDirection(Vector3::xAxis(), r))); + CHECK(nearlyEqual(rotateVector(q, Vector3{ 0.3f, -0.5f, 0.8f }), + transformDirection(Vector3{ 0.3f, -0.5f, 0.8f }, r))); + + // Identity does nothing. + CHECK(nearlyEqual(rotateVector(Quaternion::identity(), Vector3{ 1.0f, 2.0f, 3.0f }), Vector3{ 1.0f, 2.0f, 3.0f })); + + // Composition: two 45-deg rotations == one 90-deg. + const Quaternion half = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(45.0f)); + CHECK(nearlyEqual(rotateVector(half * half, Vector3::xAxis()), Vector3::yAxis())); + + // Composition is non-commutative: distinct-axis rotations depend on order, and + // q1 * q2 applies q2 first, then q1 (so it equals the sequential rotation). + const Quaternion qx = Quaternion::fromAxisAngle(Vector3::xAxis(), degToRad(90.0f)); + const Quaternion qy = Quaternion::fromAxisAngle(Vector3::yAxis(), degToRad(90.0f)); + const Vector3 zv{ 0.0f, 0.0f, 1.0f }; + CHECK(nearlyEqual(rotateVector(qx * qy, zv), rotateVector(qx, rotateVector(qy, zv)))); + CHECK_FALSE(nearlyEqual(rotateVector(qx * qy, zv), rotateVector(qy * qx, zv))); + + // A zero-length axis has no direction to rotate about: fall back to identity + // rather than divide by zero and produce a NaN quaternion. + CHECK(nearlyEqual(Quaternion::fromAxisAngle(Vector3::zero(), degToRad(90.0f)), Quaternion::identity())); + + // q and -q are the same rotation, so nearlyEqual treats them as equal. + CHECK(nearlyEqual(q, Quaternion{ -q.x, -q.y, -q.z, -q.w })); + } + + TEST_CASE("slerp endpoints and midpoint") { + using namespace draco::math; + const Quaternion a = Quaternion::identity(); + const Quaternion b = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f)); + + CHECK(nearlyEqual(slerp(a, b, 0.0f), a)); + CHECK(nearlyEqual(slerp(a, b, 1.0f), b)); + + // Halfway 0..90 deg about Z is 45 deg: +X -> (cos45, sin45, 0). + const Quaternion mid = slerp(a, b, 0.5f); + const Vector3 rotated = rotateVector(mid, Vector3::xAxis()); + const f32 c = cos(degToRad(45.0f)); + CHECK(nearlyEqual(rotated, Vector3{ c, c, 0.0f }, 1.0e-4f)); + + // Antipodal endpoint: -b is the same rotation as b, so slerp's shortest-path sign + // correction gives a finite midpoint representing that same halfway rotation. + const Quaternion negB = Quaternion{ -b.x, -b.y, -b.z, -b.w }; + const Quaternion midAnti = slerp(a, negB, 0.5f); + CHECK(isFinite(midAnti.x)); + CHECK(nearlyEqual(rotateVector(midAnti, Vector3::xAxis()), Vector3{ c, c, 0.0f }, 1.0e-4f)); + } +} +TEST_SUITE("transform") { + TEST_CASE("composes scale, rotation, translation") { + using namespace draco::math; + Transform xform; + xform.scale = Vector3{ 2.0f, 2.0f, 2.0f }; + xform.rotation = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f)); + xform.position = Vector3{ 5.0f, 0.0f, 0.0f }; + + const Matrix4 m = xform.toMatrix(); + + // (1,0,0) -> scale*2 -> (2,0,0) -> rot90Z -> (0,2,0) -> +translate -> (5,2,0) + const Vector3 p = transformPoint(Vector3::xAxis(), m); + CHECK(nearlyEqual(p, Vector3{ 5.0f, 2.0f, 0.0f })); + + // Identity transform is a no-op. + Transform id; + CHECK(nearlyEqual(transformPoint(Vector3{ 7.0f, 8.0f, 9.0f }, id.toMatrix()), + Vector3{ 7.0f, 8.0f, 9.0f })); + } + + TEST_CASE("lerp interpolates position, scale, and rotation") { + using namespace draco::math; + Transform a; + Transform b; + b.position = Vector3{ 10.0f, 0.0f, 0.0f }; + b.scale = Vector3{ 3.0f, 3.0f, 3.0f }; + b.rotation = Quaternion::fromAxisAngle(Vector3::zAxis(), degToRad(90.0f)); + + const Transform mid = Transform::lerp(a, b, 0.5f); + CHECK(nearlyEqual(mid.position, Vector3{ 5.0f, 0.0f, 0.0f })); + CHECK(nearlyEqual(mid.scale, Vector3{ 2.0f, 2.0f, 2.0f })); + // Rotation slerped to 45 deg about Z: +X -> (cos45, sin45, 0). + const f32 c = cos(degToRad(45.0f)); + CHECK(nearlyEqual(rotateVector(mid.rotation, Vector3::xAxis()), Vector3{ c, c, 0.0f }, 1.0e-4f)); } } \ No newline at end of file diff --git a/Engine/cpp/Runtime/Core/Math/Matrix4.cpp b/Engine/cpp/Runtime/Core/Math/Matrix4.cpp new file mode 100644 index 00000000..724ebb5b --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Matrix4.cpp @@ -0,0 +1,72 @@ +// Matrix4 implementation: the 4x4 inverse routines. Kept out of the module interface +// so Matrix4.cppm carries no // - only these bodies need them. +module; + +#include +#include +#include +#include + +module core.math.matrix4; + +import core.stdtypes; +import core.math.constants; + +namespace draco::math +{ + std::optional tryInverse(const Matrix4& mat) noexcept + { + const f32* m = &mat.m[0][0]; + f32 inv[16]; + + inv[0] = m[5]*m[10]*m[15] - m[5]*m[11]*m[14] - m[9]*m[6]*m[15] + m[9]*m[7]*m[14] + m[13]*m[6]*m[11] - m[13]*m[7]*m[10]; + inv[4] = -m[4]*m[10]*m[15] + m[4]*m[11]*m[14] + m[8]*m[6]*m[15] - m[8]*m[7]*m[14] - m[12]*m[6]*m[11] + m[12]*m[7]*m[10]; + inv[8] = m[4]*m[9]*m[15] - m[4]*m[11]*m[13] - m[8]*m[5]*m[15] + m[8]*m[7]*m[13] + m[12]*m[5]*m[11] - m[12]*m[7]*m[9]; + inv[12] = -m[4]*m[9]*m[14] + m[4]*m[10]*m[13] + m[8]*m[5]*m[14] - m[8]*m[6]*m[13] - m[12]*m[5]*m[10] + m[12]*m[6]*m[9]; + inv[1] = -m[1]*m[10]*m[15] + m[1]*m[11]*m[14] + m[9]*m[2]*m[15] - m[9]*m[3]*m[14] - m[13]*m[2]*m[11] + m[13]*m[3]*m[10]; + inv[5] = m[0]*m[10]*m[15] - m[0]*m[11]*m[14] - m[8]*m[2]*m[15] + m[8]*m[3]*m[14] + m[12]*m[2]*m[11] - m[12]*m[3]*m[10]; + inv[9] = -m[0]*m[9]*m[15] + m[0]*m[11]*m[13] + m[8]*m[1]*m[15] - m[8]*m[3]*m[13] - m[12]*m[1]*m[11] + m[12]*m[3]*m[9]; + inv[13] = m[0]*m[9]*m[14] - m[0]*m[10]*m[13] - m[8]*m[1]*m[14] + m[8]*m[2]*m[13] + m[12]*m[1]*m[10] - m[12]*m[2]*m[9]; + inv[2] = m[1]*m[6]*m[15] - m[1]*m[7]*m[14] - m[5]*m[2]*m[15] + m[5]*m[3]*m[14] + m[13]*m[2]*m[7] - m[13]*m[3]*m[6]; + inv[6] = -m[0]*m[6]*m[15] + m[0]*m[7]*m[14] + m[4]*m[2]*m[15] - m[4]*m[3]*m[14] - m[12]*m[2]*m[7] + m[12]*m[3]*m[6]; + inv[10] = m[0]*m[5]*m[15] - m[0]*m[7]*m[13] - m[4]*m[1]*m[15] + m[4]*m[3]*m[13] + m[12]*m[1]*m[7] - m[12]*m[3]*m[5]; + inv[14] = -m[0]*m[5]*m[14] + m[0]*m[6]*m[13] + m[4]*m[1]*m[14] - m[4]*m[2]*m[13] - m[12]*m[1]*m[6] + m[12]*m[2]*m[5]; + inv[3] = -m[1]*m[6]*m[11] + m[1]*m[7]*m[10] + m[5]*m[2]*m[11] - m[5]*m[3]*m[10] - m[9]*m[2]*m[7] + m[9]*m[3]*m[6]; + inv[7] = m[0]*m[6]*m[11] - m[0]*m[7]*m[10] - m[4]*m[2]*m[11] + m[4]*m[3]*m[10] + m[8]*m[2]*m[7] - m[8]*m[3]*m[6]; + inv[11] = -m[0]*m[5]*m[11] + m[0]*m[7]*m[9] + m[4]*m[1]*m[11] - m[4]*m[3]*m[9] - m[8]*m[1]*m[7] + m[8]*m[3]*m[5]; + inv[15] = m[0]*m[5]*m[10] - m[0]*m[6]*m[9] - m[4]*m[1]*m[10] + m[4]*m[2]*m[9] + m[8]*m[1]*m[6] - m[8]*m[2]*m[5]; + + const f32 det = m[0]*inv[0] + m[1]*inv[4] + m[2]*inv[8] + m[3]*inv[12]; + + // Scale-aware singularity: by Hadamard's inequality |det| <= product of the + // row norms, so reject relative to that product, not an absolute epsilon. + const auto rowNorm = [&](usize r) { + const f32* row = m + r * 4; + return std::sqrt(row[0]*row[0] + row[1]*row[1] + row[2]*row[2] + row[3]*row[3]); + }; + const f32 normProduct = rowNorm(0) * rowNorm(1) * rowNorm(2) * rowNorm(3); + if (std::abs(det) <= CMP_EPSILON * normProduct) + { + return std::nullopt; + } + + const f32 invDet = 1.0f / det; + Matrix4 result{}; + f32* out = &result.m[0][0]; + for (usize i = 0; i < 16; ++i) + { + out[i] = inv[i] * invDet; + } + return result; + } + + Matrix4 inverse(const Matrix4& mat) noexcept + { + if (const std::optional inv = tryInverse(mat)) { return *inv; } + assert(false && "Matrix4 inverse(): singular matrix; use tryInverse() to handle it"); + Matrix4 result{}; + f32* out = &result.m[0][0]; + for (usize i = 0; i < 16; ++i) { out[i] = std::numeric_limits::quiet_NaN(); } + return result; + } +} diff --git a/Engine/cpp/Runtime/Core/Math/Matrix4.cppm b/Engine/cpp/Runtime/Core/Math/Matrix4.cppm new file mode 100644 index 00000000..b78bee1e --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Matrix4.cppm @@ -0,0 +1,242 @@ +module; + +#include // tryInverse() return type; the rest of the impl lives in Matrix4.cpp + +export module core.math.matrix4; + +import core.stdtypes; +import core.math.types; +import core.math.functions; +import core.math.constants; + +// Matrix4: 4x4 row-major matrix - transforms, projections (perspective/ortho/ +// lookAt RH), multiply, transpose/determinant/inverse, point/direction xform. +// +// Conventions: row-major storage m[row][col]; row vectors (v' = v * M); +// composition left-to-right; right-handed projections, NDC depth [0,1]; +// translation in the last row. + +export namespace draco::math +{ + // ======================================================================= + // Matrix4 - 4x4, row-major, row-vector convention. + // ======================================================================= + struct [[nodiscard]] Matrix4 + { + f32 m[4][4]; + + // Raw row-major float pointer (16 contiguous floats), e.g. for GPU upload. + [[nodiscard]] const f32* data() const noexcept { return &m[0][0]; } + [[nodiscard]] f32* data() noexcept { return &m[0][0]; } + + [[nodiscard]] constexpr f32 operator[](usize row, usize col) const noexcept + { + return m[row][col]; + } + [[nodiscard]] constexpr f32& operator[](usize row, usize col) noexcept + { + return m[row][col]; + } + + [[nodiscard]] static constexpr Matrix4 identity() noexcept + { + return Matrix4{ { { 1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } + + [[nodiscard]] static constexpr Matrix4 translation(Vector3 t) noexcept + { + return Matrix4{ { { 1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f, 0.0f }, + { t.x, t.y, t.z, 1.0f } } }; + } + + [[nodiscard]] static constexpr Matrix4 scale(Vector3 s) noexcept + { + return Matrix4{ { { s.x, 0.0f, 0.0f, 0.0f }, + { 0.0f, s.y, 0.0f, 0.0f }, + { 0.0f, 0.0f, s.z, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } + + [[nodiscard]] static Matrix4 rotationX(f32 radians) noexcept + { + const f32 c = cos(radians); + const f32 s = sin(radians); + return Matrix4{ { { 1.0f, 0.0f, 0.0f, 0.0f }, + { 0.0f, c, s, 0.0f }, + { 0.0f, -s, c, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } + + [[nodiscard]] static Matrix4 rotationY(f32 radians) noexcept + { + const f32 c = cos(radians); + const f32 s = sin(radians); + return Matrix4{ { { c, 0.0f, -s, 0.0f }, + { 0.0f, 1.0f, 0.0f, 0.0f }, + { s, 0.0f, c, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } + + [[nodiscard]] static Matrix4 rotationZ(f32 radians) noexcept + { + const f32 c = cos(radians); + const f32 s = sin(radians); + return Matrix4{ { { c, s, 0.0f, 0.0f }, + { -s, c, 0.0f, 0.0f }, + { 0.0f, 0.0f, 1.0f, 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } + + // Right-handed perspective, NDC z in [0, 1]. + [[nodiscard]] static Matrix4 perspectiveFovRH(f32 fovYRadians, f32 aspect, f32 zNear, f32 zFar) noexcept + { + const f32 yScale = 1.0f / tan(fovYRadians * 0.5f); + const f32 xScale = yScale / aspect; + const f32 zRange = zFar / (zNear - zFar); + return Matrix4{ { { xScale, 0.0f, 0.0f, 0.0f }, + { 0.0f, yScale, 0.0f, 0.0f }, + { 0.0f, 0.0f, zRange, -1.0f }, + { 0.0f, 0.0f, zNear * zRange, 0.0f } } }; + } + + [[nodiscard]] static Matrix4 orthographicRH(f32 width, f32 height, f32 zNear, f32 zFar) noexcept + { + const f32 zRange = 1.0f / (zNear - zFar); + return Matrix4{ { { 2.0f / width, 0.0f, 0.0f, 0.0f }, + { 0.0f, 2.0f / height, 0.0f, 0.0f }, + { 0.0f, 0.0f, zRange, 0.0f }, + { 0.0f, 0.0f, zNear * zRange, 1.0f } } }; + } + + [[nodiscard]] static Matrix4 lookAtRH(Vector3 eye, Vector3 target, Vector3 up) noexcept + { + const Vector3 zAxis = normalize(eye - target); // camera looks down -z + const Vector3 xAxis = normalize(cross(up, zAxis)); + const Vector3 yAxis = cross(zAxis, xAxis); + return Matrix4{ { { xAxis.x, yAxis.x, zAxis.x, 0.0f }, + { xAxis.y, yAxis.y, zAxis.y, 0.0f }, + { xAxis.z, yAxis.z, zAxis.z, 0.0f }, + { -dot(xAxis, eye), -dot(yAxis, eye), -dot(zAxis, eye), 1.0f } } }; + } + }; + + [[nodiscard]] constexpr Matrix4 operator*(const Matrix4& a, const Matrix4& b) noexcept + { + Matrix4 result{}; + for (usize row = 0; row < 4; ++row) + { + for (usize col = 0; col < 4; ++col) + { + f32 sum = 0.0f; + for (usize k = 0; k < 4; ++k) + { + sum += a.m[row][k] * b.m[k][col]; + } + result.m[row][col] = sum; + } + } + return result; + } + + // Row-vector transform: v' = v * M. + [[nodiscard]] constexpr Vector4 operator*(Vector4 v, const Matrix4& m) noexcept + { + return { v.x * m.m[0][0] + v.y * m.m[1][0] + v.z * m.m[2][0] + v.w * m.m[3][0], + v.x * m.m[0][1] + v.y * m.m[1][1] + v.z * m.m[2][1] + v.w * m.m[3][1], + v.x * m.m[0][2] + v.y * m.m[1][2] + v.z * m.m[2][2] + v.w * m.m[3][2], + v.x * m.m[0][3] + v.y * m.m[1][3] + v.z * m.m[2][3] + v.w * m.m[3][3] }; + } + + [[nodiscard]] constexpr Matrix4 transpose(const Matrix4& a) noexcept + { + Matrix4 result{}; + for (usize row = 0; row < 4; ++row) + { + for (usize col = 0; col < 4; ++col) + { + result.m[row][col] = a.m[col][row]; + } + } + return result; + } + + // Transforms a position (implicit w = 1, translation applied). + [[nodiscard]] constexpr Vector3 transformPoint(Vector3 p, const Matrix4& m) noexcept + { + return { p.x * m.m[0][0] + p.y * m.m[1][0] + p.z * m.m[2][0] + m.m[3][0], + p.x * m.m[0][1] + p.y * m.m[1][1] + p.z * m.m[2][1] + m.m[3][1], + p.x * m.m[0][2] + p.y * m.m[1][2] + p.z * m.m[2][2] + m.m[3][2] }; + } + + // Transforms a direction (implicit w = 0, translation ignored). + [[nodiscard]] constexpr Vector3 transformDirection(Vector3 d, const Matrix4& m) noexcept + { + return { d.x * m.m[0][0] + d.y * m.m[1][0] + d.z * m.m[2][0], + d.x * m.m[0][1] + d.y * m.m[1][1] + d.z * m.m[2][1], + d.x * m.m[0][2] + d.y * m.m[1][2] + d.z * m.m[2][2] }; + } + + // Transforms a 2D position (implicit z = 0, w = 1, translation applied; + // result projected back to 2D). For 2D affine transforms stored in a Matrix4. + [[nodiscard]] constexpr Vector2 transformPoint2D(Vector2 p, const Matrix4& m) noexcept + { + return { p.x * m.m[0][0] + p.y * m.m[1][0] + m.m[3][0], + p.x * m.m[0][1] + p.y * m.m[1][1] + m.m[3][1] }; + } + + // Exact element-wise equality (e.g. for an identity fast-path). + [[nodiscard]] constexpr bool operator==(const Matrix4& a, const Matrix4& b) noexcept + { + for (usize row = 0; row < 4; ++row) + for (usize col = 0; col < 4; ++col) + if (a.m[row][col] != b.m[row][col]) { return false; } + return true; + } + + [[nodiscard]] constexpr bool nearlyEqual(const Matrix4& a, const Matrix4& b, f32 epsilon = CMP_EPSILON) noexcept + { + for (usize row = 0; row < 4; ++row) + { + for (usize col = 0; col < 4; ++col) + { + if (!nearlyEqual(a.m[row][col], b.m[row][col], epsilon)) { return false; } + } + } + return true; + } + + [[nodiscard]] inline f32 determinant(const Matrix4& mat) noexcept + { + const f32* m = &mat.m[0][0]; + const f32 s0 = m[0] * m[5] - m[1] * m[4]; + const f32 s1 = m[0] * m[6] - m[2] * m[4]; + const f32 s2 = m[0] * m[7] - m[3] * m[4]; + const f32 s3 = m[1] * m[6] - m[2] * m[5]; + const f32 s4 = m[1] * m[7] - m[3] * m[5]; + const f32 s5 = m[2] * m[7] - m[3] * m[6]; + const f32 c5 = m[10] * m[15] - m[11] * m[14]; + const f32 c4 = m[9] * m[15] - m[11] * m[13]; + const f32 c3 = m[9] * m[14] - m[10] * m[13]; + const f32 c2 = m[8] * m[15] - m[11] * m[12]; + const f32 c1 = m[8] * m[14] - m[10] * m[12]; + const f32 c0 = m[8] * m[13] - m[9] * m[12]; + return s0 * c5 - s1 * c4 + s2 * c3 + s3 * c2 - s4 * c1 + s5 * c0; + } + + // Full 4x4 inverse (adjugate / determinant). Returns nullopt for a singular + // matrix, judged with a scale-aware (Hadamard row-norm) threshold so a small- + // but-invertible matrix (e.g. a 1e-3 uniform scale) is not wrongly rejected. + // (Implementation in Matrix4.cpp.) + [[nodiscard]] std::optional tryInverse(const Matrix4& mat) noexcept; + + // Convenience inverse for matrices known to be invertible. On a singular matrix it + // does NOT silently return identity (which would hide the failure) - it asserts in + // debug and returns a not-a-number matrix so the error propagates visibly. Prefer + // tryInverse() whenever a matrix may be singular. (Implementation in Matrix4.cpp.) + [[nodiscard]] Matrix4 inverse(const Matrix4& mat) noexcept; +} diff --git a/Engine/cpp/Runtime/Core/Math/Quaternion.cppm b/Engine/cpp/Runtime/Core/Math/Quaternion.cppm new file mode 100644 index 00000000..cc12c4ef --- /dev/null +++ b/Engine/cpp/Runtime/Core/Math/Quaternion.cppm @@ -0,0 +1,133 @@ +export module core.math.quaternion; + +import core.stdtypes; +import core.math.types; +import core.math.matrix4; +import core.math.functions; +import core.math.constants; + +// Quaternion: unit quaternion rotation - fromAxisAngle, Hamilton product, +// conjugate/dot/normalize/slerp, rotateVector, and rotationMatrix (-> Matrix4). +// Row-vector convention (v' = v * M), matching Matrix4. + +export namespace draco::math +{ + // ======================================================================= + // Quaternion - unit quaternion rotation (x, y, z, w). + // ======================================================================= + struct [[nodiscard]] Quaternion + { + f32 x = 0.0f; + f32 y = 0.0f; + f32 z = 0.0f; + f32 w = 1.0f; + + constexpr Quaternion() noexcept = default; + constexpr Quaternion(f32 inX, f32 inY, f32 inZ, f32 inW) noexcept : x(inX), y(inY), z(inZ), w(inW) {} + + [[nodiscard]] static Quaternion fromAxisAngle(Vector3 axis, f32 radians) noexcept + { + // A zero-length axis is not a rotation; normalize() would divide by zero, + // so fall back to identity rather than produce a NaN/degenerate quaternion. + if (nearlyZero(dot(axis, axis))) + { + return Quaternion{ 0.0f, 0.0f, 0.0f, 1.0f }; + } + const f32 half = radians * 0.5f; + const f32 s = sin(half); + const Vector3 a = normalize(axis); + return Quaternion{ a.x * s, a.y * s, a.z * s, cos(half) }; + } + + [[nodiscard]] static constexpr Quaternion identity() noexcept { return { 0.0f, 0.0f, 0.0f, 1.0f }; } + }; + + // Hamilton product: applies `b` then `a` to a vector. + [[nodiscard]] constexpr Quaternion operator*(Quaternion a, Quaternion b) noexcept + { + return { a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y, + a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x, + a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w, + a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z }; + } + + [[nodiscard]] constexpr Quaternion conjugate(Quaternion q) noexcept { return { -q.x, -q.y, -q.z, q.w }; } + [[nodiscard]] constexpr f32 dot(Quaternion a, Quaternion b) noexcept { return a.x * b.x + a.y * b.y + a.z * b.z + a.w * b.w; } + + // General inverse (= conjugate / |q|^2). For unit quaternions this equals the conjugate. + [[nodiscard]] inline Quaternion inverse(Quaternion q) noexcept + { + const f32 lengthSq = dot(q, q); + if (lengthSq <= CMP_EPSILON * CMP_EPSILON) { return Quaternion::identity(); } + const f32 inv = 1.0f / lengthSq; + return { -q.x * inv, -q.y * inv, -q.z * inv, q.w * inv }; + } + + [[nodiscard]] inline Quaternion normalize(Quaternion q) noexcept + { + const f32 lengthSq = dot(q, q); + if (lengthSq <= CMP_EPSILON * CMP_EPSILON) { return Quaternion::identity(); } + const f32 inv = 1.0f / sqrt(lengthSq); + return { q.x * inv, q.y * inv, q.z * inv, q.w * inv }; + } + + [[nodiscard]] constexpr Vector3 rotateVector(Quaternion q, Vector3 v) noexcept + { + const Vector3 u{ q.x, q.y, q.z }; + const f32 s = q.w; + return u * (2.0f * dot(u, v)) + v * (s * s - dot(u, u)) + cross(u, v) * (2.0f * s); + } + + [[nodiscard]] inline bool nearlyEqual(Quaternion a, Quaternion b, f32 epsilon = CMP_EPSILON) noexcept + { + const bool same = nearlyEqual(a.x, b.x, epsilon) && nearlyEqual(a.y, b.y, epsilon) + && nearlyEqual(a.z, b.z, epsilon) && nearlyEqual(a.w, b.w, epsilon); + // q and -q represent the same rotation (and slerp/normalize may flip the sign), + // so accept the antipodal quaternion as equal too. + const bool antipodal = nearlyEqual(a.x, -b.x, epsilon) && nearlyEqual(a.y, -b.y, epsilon) + && nearlyEqual(a.z, -b.z, epsilon) && nearlyEqual(a.w, -b.w, epsilon); + return same || antipodal; + } + + // Spherical linear interpolation along the shortest arc; result is unit. + [[nodiscard]] inline Quaternion slerp(Quaternion a, Quaternion b, f32 t) noexcept + { + f32 cosTheta = dot(a, b); + if (cosTheta < 0.0f) // shortest path + { + b = Quaternion{ -b.x, -b.y, -b.z, -b.w }; + cosTheta = -cosTheta; + } + + if (cosTheta > 0.9995f) // nearly parallel - lerp + normalize + { + return normalize(Quaternion{ a.x + (b.x - a.x) * t, + a.y + (b.y - a.y) * t, + a.z + (b.z - a.z) * t, + a.w + (b.w - a.w) * t }); + } + + const f32 theta0 = acos(cosTheta); + const f32 theta = theta0 * t; + const f32 sinTheta = sin(theta); + const f32 sinTheta0 = sin(theta0); + const f32 s1 = sinTheta / sinTheta0; + const f32 s0 = cos(theta) - cosTheta * s1; + return Quaternion{ a.x * s0 + b.x * s1, + a.y * s0 + b.y * s1, + a.z * s0 + b.z * s1, + a.w * s0 + b.w * s1 }; + } + + // Rotation matrix for a unit quaternion (row-vector convention). + [[nodiscard]] constexpr Matrix4 rotationMatrix(Quaternion q) noexcept + { + const f32 xx = q.x * q.x, yy = q.y * q.y, zz = q.z * q.z; + const f32 xy = q.x * q.y, xz = q.x * q.z, yz = q.y * q.z; + const f32 wx = q.w * q.x, wy = q.w * q.y, wz = q.w * q.z; + return Matrix4{ { { 1.0f - 2.0f * (yy + zz), 2.0f * (xy + wz), 2.0f * (xz - wy), 0.0f }, + { 2.0f * (xy - wz), 1.0f - 2.0f * (xx + zz), 2.0f * (yz + wx), 0.0f }, + { 2.0f * (xz + wy), 2.0f * (yz - wx), 1.0f - 2.0f * (xx + yy), 0.0f }, + { 0.0f, 0.0f, 0.0f, 1.0f } } }; + } +} diff --git a/Engine/cpp/Runtime/Core/Math/Transform.cpp b/Engine/cpp/Runtime/Core/Math/Transform.cpp deleted file mode 100644 index 5e1029b4..00000000 --- a/Engine/cpp/Runtime/Core/Math/Transform.cpp +++ /dev/null @@ -1,38 +0,0 @@ -module; - -#include - -#include - -module core.math.transform; - -import core.stdtypes; - -namespace draco::math -{ - void Transform::toMatrix(f32 out[16]) const - { - f32 translation[16]; - f32 rx[16]; - f32 ry[16]; - f32 rz[16]; - f32 scale[16]; - f32 temp[16]; - - bx::mtxIdentity(out); - - bx::mtxScale(scale, scale[0], scale[1], scale[2]); - - bx::mtxRotateX(rx, rotation[0]); - bx::mtxRotateY(ry, rotation[1]); - bx::mtxRotateZ(rz, rotation[2]); - - bx::mtxTranslate(translation, position[0], position[1], position[2]); - - // scale * rotation * translation - bx::mtxMul(temp, scale, rx); - bx::mtxMul(temp, temp, ry); - bx::mtxMul(temp, temp, rz); - bx::mtxMul(out, temp, translation); - } -} diff --git a/Engine/cpp/Runtime/Core/Math/Transform.cppm b/Engine/cpp/Runtime/Core/Math/Transform.cppm index 176cbe7c..efd37166 100644 --- a/Engine/cpp/Runtime/Core/Math/Transform.cppm +++ b/Engine/cpp/Runtime/Core/Math/Transform.cppm @@ -1,70 +1,50 @@ -module; - export module core.math.transform; -import core.math.types; + import core.stdtypes; +import core.math.types; +import core.math.matrix4; +import core.math.quaternion; export namespace draco::math { - struct Transform + // ======================================================================= + // Transform - position / rotation / scale, composed as S * R * T. + // ======================================================================= + struct [[nodiscard]] Transform { - f32 position[3] = { 0.0f, 0.0f, 0.0f }; - f32 rotation[3] = { 0.0f, 0.0f, 0.0f }; // Euler (radians) - f32 scale[3] = { 1.0f, 1.0f, 1.0f }; + Vector3 position{ 0.0f, 0.0f, 0.0f }; + Quaternion rotation{ 0.0f, 0.0f, 0.0f, 1.0f }; + Vector3 scale{ 1.0f, 1.0f, 1.0f }; - constexpr void setPosition(f32 x, f32 y, f32 z) noexcept { - position[0] = x; - position[1] = y; - position[2] = z; - } - - constexpr void setPosition(const Vector3& v) noexcept { - position[0] = v.x; - position[1] = v.y; - position[2] = v.z; - } - - [[nodiscard]] constexpr Vector3 getPosition() const noexcept { - return Vector3{position[0], position[1], position[2]}; - } + // Identity transform (position 0, rotation identity, scale 1) - the + // default-constructed value. + static constexpr Transform identity() noexcept { return {}; } - constexpr void setRotation(const f32 x, const f32 y, const f32 z) noexcept + Matrix4 toMatrix() const noexcept { - rotation[0] = x; - rotation[1] = y; - rotation[2] = z; + Matrix4 result = Matrix4::scale(scale) * rotationMatrix(rotation); + result.m[3][0] = position.x; + result.m[3][1] = position.y; + result.m[3][2] = position.z; + return result; } - constexpr void setRotation(const Vector3& v) noexcept + // Fills a caller-provided 16-float buffer (row-major, matching Matrix4::data()). + void toMatrix(f32 out[16]) const noexcept { - rotation[0] = v.x; - rotation[1] = v.y; - rotation[2] = v.z; + const Matrix4 m = toMatrix(); + const f32* src = m.data(); + for (usize i = 0; i < 16; ++i) { out[i] = src[i]; } } - [[nodiscard]] constexpr Vector3 getRotation() const noexcept { - return Vector3{rotation[0], rotation[1], rotation[2]}; - } - - constexpr void setScale(const f32 x, const f32 y, const f32 z) noexcept - { - scale[0] = x; - scale[1] = y; - scale[2] = z; - } - - constexpr void setScale(const Vector3& v) noexcept + // Component-wise interpolation: position/scale lerp, rotation slerp. + static Transform lerp(const Transform& a, const Transform& b, f32 t) noexcept { - scale[0] = v.x; - scale[1] = v.y; - scale[2] = v.z; - } - - [[nodiscard]] constexpr Vector3 getScale() const noexcept { - return Vector3{scale[0], scale[1], scale[2]}; + return Transform{ + draco::math::lerp(a.position, b.position, t), + slerp(a.rotation, b.rotation, t), + draco::math::lerp(a.scale, b.scale, t), + }; } - - // Compute column-major matrix from transform - void toMatrix(f32 out[16]) const; }; } diff --git a/Engine/cpp/Runtime/Core/Math/TypesCommon.cppm b/Engine/cpp/Runtime/Core/Math/TypesCommon.cppm index c9df0996..f8f8dc64 100644 --- a/Engine/cpp/Runtime/Core/Math/TypesCommon.cppm +++ b/Engine/cpp/Runtime/Core/Math/TypesCommon.cppm @@ -63,6 +63,10 @@ export namespace draco::math { [[nodiscard]] static Vector3 spherical(f32 azimuth, f32 inclination, f32 radius = 1.0f) noexcept; [[nodiscard]] static Vector3 cylindrical(f32 angle, f32 radius = 1.0f, f32 height = 0.0f) noexcept; + // constant vectors (defined in Vector3.cppm) + [[nodiscard]] static constexpr Vector3 zero() noexcept; + [[nodiscard]] static constexpr Vector3 one() noexcept; + // element access [[nodiscard]] constexpr f32& operator[](i32 i) noexcept; [[nodiscard]] constexpr const f32& operator[](i32 i) const noexcept; diff --git a/Engine/cpp/Runtime/Core/Math/Vector2.cppm b/Engine/cpp/Runtime/Core/Math/Vector2.cppm index cc1ccaa3..795625a9 100644 --- a/Engine/cpp/Runtime/Core/Math/Vector2.cppm +++ b/Engine/cpp/Runtime/Core/Math/Vector2.cppm @@ -413,9 +413,9 @@ export namespace draco::math { }; } - // Returns true if the vectors are approximately equal - [[nodiscard]] constexpr bool approx_eq(const Vector2& a, const Vector2& b) noexcept { - return distance_sq(a, b) < CMP_EPSILON2; + // Component-wise approximate equality against an epsilon. + [[nodiscard]] constexpr bool nearlyEqual(const Vector2& a, const Vector2& b, f32 epsilon = CMP_EPSILON) noexcept { + return nearlyEqual(a.x, b.x, epsilon) && nearlyEqual(a.y, b.y, epsilon); } } diff --git a/Engine/cpp/Runtime/Core/Math/Vector3.cppm b/Engine/cpp/Runtime/Core/Math/Vector3.cppm index b7bc2cef..ca1011c2 100644 --- a/Engine/cpp/Runtime/Core/Math/Vector3.cppm +++ b/Engine/cpp/Runtime/Core/Math/Vector3.cppm @@ -441,15 +441,19 @@ export namespace draco::math { }; } - // Returns true if the vectors are approximately equal - [[nodiscard]] constexpr bool approx_eq(const Vector3& a, const Vector3& b) noexcept { - return distance_sq(a, b) < CMP_EPSILON2; + // Component-wise approximate equality against an epsilon. + [[nodiscard]] constexpr bool nearlyEqual(const Vector3& a, const Vector3& b, f32 epsilon = CMP_EPSILON) noexcept { + return nearlyEqual(a.x, b.x, epsilon) && nearlyEqual(a.y, b.y, epsilon) && nearlyEqual(a.z, b.z, epsilon); } // Returns cross product [[nodiscard]] constexpr Vector3 cross(const Vector3& a, const Vector3& b) noexcept { return { a.y*b.z - a.z*b.y, a.z*b.x - a.x*b.z, a.x*b.y - a.y*b.x }; } + + // constant vectors + constexpr Vector3 Vector3::zero() noexcept { return { 0.0f, 0.0f, 0.0f }; } + constexpr Vector3 Vector3::one() noexcept { return { 1.0f, 1.0f, 1.0f }; } } export namespace std { diff --git a/Engine/cpp/Runtime/Core/Math/Vector4.cppm b/Engine/cpp/Runtime/Core/Math/Vector4.cppm index c4b1fe41..9c55fea7 100644 --- a/Engine/cpp/Runtime/Core/Math/Vector4.cppm +++ b/Engine/cpp/Runtime/Core/Math/Vector4.cppm @@ -493,9 +493,10 @@ export namespace draco::math { }; } - // Returns true if the vectors are approximately equal - [[nodiscard]] constexpr bool approx_eq(const Vector4& a, const Vector4& b) noexcept { - return distance_sq(a, b) < CMP_EPSILON2; + // Component-wise approximate equality against an epsilon. + [[nodiscard]] constexpr bool nearlyEqual(const Vector4& a, const Vector4& b, f32 epsilon = CMP_EPSILON) noexcept { + return nearlyEqual(a.x, b.x, epsilon) && nearlyEqual(a.y, b.y, epsilon) + && nearlyEqual(a.z, b.z, epsilon) && nearlyEqual(a.w, b.w, epsilon); } } // namespace draco::math diff --git a/Samples/cpp/Render/RenderSample.cpp b/Samples/cpp/Render/RenderSample.cpp index 73431fa1..152b1367 100644 --- a/Samples/cpp/Render/RenderSample.cpp +++ b/Samples/cpp/Render/RenderSample.cpp @@ -28,6 +28,8 @@ namespace int main(int, char*[]) { + using namespace draco::math; + auto shell = createShell(WindowSettings{ .title = u8"Draconic Engine Rendering Sample", .width = 1280, @@ -126,7 +128,7 @@ int main(int, char*[]) draco::scene::Scene scene; - static constexpr draco::math::Transform tr; + static constexpr Transform tr; scene.renderables.push_back({cube_mesh, tr, mat}); scene.renderables.push_back({plane_mesh, tr, mat}); @@ -134,13 +136,13 @@ int main(int, char*[]) scene.renderables.push_back({cylinder_mesh, tr, mat}); scene.renderables.push_back({capsule_mesh, tr, mat}); - scene.renderables[0].transform.setPosition(-12.0f, 0.0f, 0.0f); - scene.renderables[1].transform.setPosition(-6.0f, 0.0f, 0.0f); - scene.renderables[2].transform.setPosition(0.0f, 0.0f, 0.0f); - scene.renderables[3].transform.setPosition(6.0f, 0.0f, 0.0f); - scene.renderables[4].transform.setPosition(12.0f, 0.0f, 0.0f); + scene.renderables[0].transform.position = Vector3{ -12.0f, 0.0f, 0.0f }; + scene.renderables[1].transform.position = Vector3{ -6.0f, 0.0f, 0.0f }; + scene.renderables[2].transform.position = Vector3{ 0.0f, 0.0f, 0.0f }; + scene.renderables[3].transform.position = Vector3{ 6.0f, 0.0f, 0.0f }; + scene.renderables[4].transform.position = Vector3{ 12.0f, 0.0f, 0.0f }; - scene.renderables[1].transform.setRotation(-bx::kPiHalf, 0.0f, 0.0f); + scene.renderables[1].transform.rotation = Quaternion::fromAxisAngle(Vector3::xAxis(), -bx::kPiHalf); using clock = std::chrono::steady_clock; const auto startTime = clock::now();