343 lines
11 KiB
C++
343 lines
11 KiB
C++
|
|
/*
|
||
|
|
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-4f;
|
||
|
|
|
||
|
|
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);
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* CheckVec3(VectorF32<3, 1> got, float x, float y, float z, std::string_view label) {
|
||
|
|
std::array<float, 4> v = got.template Store<float>();
|
||
|
|
if (!FloatEquals(v[0], x) || !FloatEquals(v[1], y) || !FloatEquals(v[2], z))
|
||
|
|
return new std::string(std::format(
|
||
|
|
"{}: expected ({},{},{}), got ({},{},{})",
|
||
|
|
label, x, y, z, v[0], v[1], v[2]));
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* CheckMat43(MatrixRowMajor<float, 4, 3, 1> const& m, std::array<float, 12> expected, std::string_view label) {
|
||
|
|
std::array<float, 12> got = m.Store();
|
||
|
|
for (std::uint8_t i = 0; i < 12; ++i) {
|
||
|
|
if (!FloatEquals(got[i], expected[i]))
|
||
|
|
return new std::string(std::format(
|
||
|
|
"{} element {}: expected {}, got {}", label, i, expected[i], got[i]));
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* CheckMat44(MatrixRowMajor<float, 4, 4, 1> const& m, std::array<float, 16> expected, std::string_view label) {
|
||
|
|
std::array<float, 16> got = m.Store();
|
||
|
|
for (std::uint8_t i = 0; i < 16; ++i) {
|
||
|
|
if (!FloatEquals(got[i], expected[i]))
|
||
|
|
return new std::string(std::format(
|
||
|
|
"{} element {}: expected {}, got {}", label, i, expected[i], got[i]));
|
||
|
|
}
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------- Identity / Store roundtrip ----------
|
||
|
|
|
||
|
|
std::string* TestIdentity43() {
|
||
|
|
auto m = MatrixRowMajor<float, 4, 3, 1>::Identity();
|
||
|
|
return CheckMat43(m, {
|
||
|
|
1, 0, 0, 0,
|
||
|
|
0, 1, 0, 0,
|
||
|
|
0, 0, 1, 0,
|
||
|
|
}, "Identity 4x3");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* TestIdentity44() {
|
||
|
|
auto m = MatrixRowMajor<float, 4, 4, 1>::Identity();
|
||
|
|
return CheckMat44(m, {
|
||
|
|
1, 0, 0, 0,
|
||
|
|
0, 1, 0, 0,
|
||
|
|
0, 0, 1, 0,
|
||
|
|
0, 0, 0, 1,
|
||
|
|
}, "Identity 4x4");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* TestStoreRoundtrip() {
|
||
|
|
MatrixRowMajor<float, 4, 4, 1> m(
|
||
|
|
1, 2, 3, 4,
|
||
|
|
5, 6, 7, 8,
|
||
|
|
9, 10, 11, 12,
|
||
|
|
13, 14, 15, 16
|
||
|
|
);
|
||
|
|
return CheckMat44(m, {
|
||
|
|
1, 2, 3, 4,
|
||
|
|
5, 6, 7, 8,
|
||
|
|
9, 10, 11, 12,
|
||
|
|
13, 14, 15, 16,
|
||
|
|
}, "Store roundtrip 4x4");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------- Factory matrices ----------
|
||
|
|
|
||
|
|
std::string* TestTranslation() {
|
||
|
|
auto m = MatrixRowMajor<float, 4, 3, 1>::Translation(10, 20, 30);
|
||
|
|
if (auto e = CheckMat43(m, {
|
||
|
|
1, 0, 0, 10,
|
||
|
|
0, 1, 0, 20,
|
||
|
|
0, 0, 1, 30,
|
||
|
|
}, "Translation 4x3")) return e;
|
||
|
|
|
||
|
|
auto m4 = MatrixRowMajor<float, 4, 4, 1>::Translation(10, 20, 30);
|
||
|
|
if (auto e = CheckMat44(m4, {
|
||
|
|
1, 0, 0, 0,
|
||
|
|
0, 1, 0, 0,
|
||
|
|
0, 0, 1, 0,
|
||
|
|
10, 20, 30, 1,
|
||
|
|
}, "Translation 4x4")) return e;
|
||
|
|
return nullptr;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* TestScaling() {
|
||
|
|
auto m = MatrixRowMajor<float, 4, 3, 1>::Scaling(2, 3, 4);
|
||
|
|
if (auto e = CheckMat43(m, {
|
||
|
|
2, 0, 0, 0,
|
||
|
|
0, 3, 0, 0,
|
||
|
|
0, 0, 4, 0,
|
||
|
|
}, "Scaling 4x3")) return e;
|
||
|
|
|
||
|
|
auto m4 = MatrixRowMajor<float, 4, 4, 1>::Scaling(2, 3, 4);
|
||
|
|
return CheckMat44(m4, {
|
||
|
|
2, 0, 0, 0,
|
||
|
|
0, 3, 0, 0,
|
||
|
|
0, 0, 4, 0,
|
||
|
|
0, 0, 0, 1,
|
||
|
|
}, "Scaling 4x4");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------- Matrix * Vector ----------
|
||
|
|
|
||
|
|
std::string* TestMatVecIdentity() {
|
||
|
|
auto m = MatrixRowMajor<float, 4, 3, 1>::Identity();
|
||
|
|
VectorF32<3, 1> v = Vec3(7, -2, 3);
|
||
|
|
return CheckVec3(m * v, 7, -2, 3, "I * v");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* TestMatVecTranslation() {
|
||
|
|
auto m = MatrixRowMajor<float, 4, 3, 1>::Translation(1, 2, 3);
|
||
|
|
// Affine multiply with implicit w=1 -> result = v + (1,2,3).
|
||
|
|
VectorF32<3, 1> v = Vec3(10, 20, 30);
|
||
|
|
return CheckVec3(m * v, 11, 22, 33, "Translation * v");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* TestMatVecScaling() {
|
||
|
|
auto m = MatrixRowMajor<float, 4, 3, 1>::Scaling(2, 3, 4);
|
||
|
|
VectorF32<3, 1> v = Vec3(1, 1, 1);
|
||
|
|
return CheckVec3(m * v, 2, 3, 4, "Scaling * v");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* TestMatVecCombined() {
|
||
|
|
// Scale then translate: full matrix is [diag(s) | t].
|
||
|
|
MatrixRowMajor<float, 4, 3, 1> m(
|
||
|
|
2, 0, 0, 10,
|
||
|
|
0, 3, 0, 20,
|
||
|
|
0, 0, 4, 30
|
||
|
|
);
|
||
|
|
VectorF32<3, 1> v = Vec3(1, 1, 1);
|
||
|
|
return CheckVec3(m * v, 12, 23, 34, "Scale+Translate * v");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* TestTransformNormal() {
|
||
|
|
// TransformNormal ignores the translation column.
|
||
|
|
MatrixRowMajor<float, 4, 3, 1> m(
|
||
|
|
2, 0, 0, 100,
|
||
|
|
0, 3, 0, 200,
|
||
|
|
0, 0, 4, 300
|
||
|
|
);
|
||
|
|
VectorF32<3, 1> v = Vec3(1, 1, 1);
|
||
|
|
return CheckVec3(m.TransformNormal(v), 2, 3, 4, "TransformNormal");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------- Matrix * Matrix ----------
|
||
|
|
|
||
|
|
std::string* TestMatMulIdentityLeft44() {
|
||
|
|
MatrixRowMajor<float, 4, 4, 1> a = MatrixRowMajor<float, 4, 4, 1>::Identity();
|
||
|
|
MatrixRowMajor<float, 4, 4, 1> b(
|
||
|
|
1, 2, 3, 4,
|
||
|
|
5, 6, 7, 8,
|
||
|
|
9, 10, 11, 12,
|
||
|
|
13, 14, 15, 16
|
||
|
|
);
|
||
|
|
auto r = a * b;
|
||
|
|
return CheckMat44(r, {
|
||
|
|
1, 2, 3, 4,
|
||
|
|
5, 6, 7, 8,
|
||
|
|
9, 10, 11, 12,
|
||
|
|
13, 14, 15, 16,
|
||
|
|
}, "I * M");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* TestMatMulIdentityRight44() {
|
||
|
|
MatrixRowMajor<float, 4, 4, 1> a(
|
||
|
|
1, 2, 3, 4,
|
||
|
|
5, 6, 7, 8,
|
||
|
|
9, 10, 11, 12,
|
||
|
|
13, 14, 15, 16
|
||
|
|
);
|
||
|
|
MatrixRowMajor<float, 4, 4, 1> b = MatrixRowMajor<float, 4, 4, 1>::Identity();
|
||
|
|
auto r = a * b;
|
||
|
|
return CheckMat44(r, {
|
||
|
|
1, 2, 3, 4,
|
||
|
|
5, 6, 7, 8,
|
||
|
|
9, 10, 11, 12,
|
||
|
|
13, 14, 15, 16,
|
||
|
|
}, "M * I");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* TestMatMul44() {
|
||
|
|
// Hand-computed: standard row-major (B*A)[i][j] = sum_k B[i][k]*A[k][j].
|
||
|
|
MatrixRowMajor<float, 4, 4, 1> a(
|
||
|
|
1, 2, 0, 0,
|
||
|
|
0, 1, 3, 0,
|
||
|
|
4, 0, 1, 0,
|
||
|
|
0, 0, 0, 1
|
||
|
|
);
|
||
|
|
MatrixRowMajor<float, 4, 4, 1> b(
|
||
|
|
1, 0, 2, 0,
|
||
|
|
0, 1, 0, 3,
|
||
|
|
0, 0, 1, 0,
|
||
|
|
0, 0, 0, 1
|
||
|
|
);
|
||
|
|
auto r = a * b;
|
||
|
|
// result.m[i][j] = sum_k b.m[i][k] * a.m[k][j]
|
||
|
|
// row 0: [1*1+0*0+2*4+0*0, 1*2+0*1+2*0+0*0, 1*0+0*3+2*1+0*0, 1*0+0*0+2*0+0*1]
|
||
|
|
// = [9, 2, 2, 0]
|
||
|
|
// row 1: [0*1+1*0+0*4+3*0, 0*2+1*1+0*0+3*0, 0*0+1*3+0*1+3*0, 0*0+1*0+0*0+3*1]
|
||
|
|
// = [0, 1, 3, 3]
|
||
|
|
// row 2: [0*1+0*0+1*4+0*0, 0*2+0*1+1*0+0*0, 0*0+0*3+1*1+0*0, 0*0+0*0+1*0+0*1]
|
||
|
|
// = [4, 0, 1, 0]
|
||
|
|
// row 3: [0*1+0*0+0*4+1*0, 0*2+0*1+0*0+1*0, 0*0+0*3+0*1+1*0, 0*0+0*0+0*0+1*1]
|
||
|
|
// = [0, 0, 0, 1]
|
||
|
|
return CheckMat44(r, {
|
||
|
|
9, 2, 2, 0,
|
||
|
|
0, 1, 3, 3,
|
||
|
|
4, 0, 1, 0,
|
||
|
|
0, 0, 0, 1,
|
||
|
|
}, "Mat * Mat 4x4");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* TestMatMulTranslationCompose() {
|
||
|
|
// Translation composition: T(a) * T(b) acting on a vector translates by a+b
|
||
|
|
// (and the resulting matrix has the summed translation in its last column).
|
||
|
|
auto t1 = MatrixRowMajor<float, 4, 3, 1>::Translation(1, 2, 3);
|
||
|
|
auto t2 = MatrixRowMajor<float, 4, 3, 1>::Translation(10, 20, 30);
|
||
|
|
auto r = t1 * t2;
|
||
|
|
if (auto e = CheckMat43(r, {
|
||
|
|
1, 0, 0, 11,
|
||
|
|
0, 1, 0, 22,
|
||
|
|
0, 0, 1, 33,
|
||
|
|
}, "T(1,2,3) * T(10,20,30)")) return e;
|
||
|
|
|
||
|
|
// Sanity check: applying the composed matrix to origin yields (11,22,33).
|
||
|
|
return CheckVec3(r * Vec3(0, 0, 0), 11, 22, 33, "Composed translation * origin");
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string* TestMatMulScaleThenTranslate() {
|
||
|
|
// The matrix product follows the engine's row-vector / post-multiply
|
||
|
|
// convention: a.operator*(b) yields the matrix b composed with a so that
|
||
|
|
// applying the result is equivalent to "first apply this, then apply b"
|
||
|
|
// when read left-to-right with a row vector (v * (a*b) = (v*a) * b).
|
||
|
|
//
|
||
|
|
// For column-vector intuition that means s.operator*(t) builds T·S, i.e.
|
||
|
|
// "scale, then translate"; applying it to (1,1,1) yields s*v + t.
|
||
|
|
auto s = MatrixRowMajor<float, 4, 3, 1>::Scaling(2, 3, 4);
|
||
|
|
auto t = MatrixRowMajor<float, 4, 3, 1>::Translation(10, 20, 30);
|
||
|
|
auto r = s * t;
|
||
|
|
return CheckVec3(r * Vec3(1, 1, 1), 12, 23, 34, "scale-then-translate * (1,1,1)");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------- LookTo / LookAt ----------
|
||
|
|
|
||
|
|
std::string* TestLookToBasis() {
|
||
|
|
// Camera at +Z looking back toward origin. eyeDirection should point from
|
||
|
|
// focus to eye, i.e. +Z, so the resulting basis has R2 = +Z.
|
||
|
|
VectorF32<3, 1> eye = Vec3(0, 0, 5);
|
||
|
|
VectorF32<3, 1> dir = Vec3(0, 0, 1); // already from origin toward eye
|
||
|
|
VectorF32<3, 1> up = Vec3(0, 1, 0);
|
||
|
|
auto m = MatrixRowMajor<float, 4, 4, 1>::LookTo(eye, dir, up);
|
||
|
|
// R2 normalized = (0,0,1). R0 = up x R2 = (0,1,0) x (0,0,1) = (1,0,0).
|
||
|
|
// R1 = R2 x R0 = (0,0,1) x (1,0,0) = (0,1,0).
|
||
|
|
// The view matrix stores basis columns in the rotation block, with the
|
||
|
|
// translation column = -basis dot eye. So m.row[3] = (D0, D1, D2, 1).
|
||
|
|
// D0 = R0 . -eye = -(0*0+0*0+5*0) = 0
|
||
|
|
// D1 = R1 . -eye = -(0*0+1*0+0*5) = 0
|
||
|
|
// D2 = R2 . -eye = -(0*0+0*0+1*5) = -5
|
||
|
|
return CheckMat44(m, {
|
||
|
|
1, 0, 0, 0,
|
||
|
|
0, 1, 0, 0,
|
||
|
|
0, 0, 1, 0,
|
||
|
|
0, 0, -5, 1,
|
||
|
|
}, "LookTo basis");
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---------- Test harness ----------
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
using Fn = std::string* (*)();
|
||
|
|
constexpr std::array<std::pair<const char*, Fn>, 15> tests = {{
|
||
|
|
{ "Identity43", TestIdentity43 },
|
||
|
|
{ "Identity44", TestIdentity44 },
|
||
|
|
{ "StoreRoundtrip", TestStoreRoundtrip },
|
||
|
|
{ "Translation", TestTranslation },
|
||
|
|
{ "Scaling", TestScaling },
|
||
|
|
{ "MatVecIdentity", TestMatVecIdentity },
|
||
|
|
{ "MatVecTranslation", TestMatVecTranslation },
|
||
|
|
{ "MatVecScaling", TestMatVecScaling },
|
||
|
|
{ "MatVecCombined", TestMatVecCombined },
|
||
|
|
{ "TransformNormal", TestTransformNormal },
|
||
|
|
{ "MatMulIdentityLeft", TestMatMulIdentityLeft44 },
|
||
|
|
{ "MatMulIdentityRight", TestMatMulIdentityRight44 },
|
||
|
|
{ "MatMul44", TestMatMul44 },
|
||
|
|
{ "MatMulTranslation", TestMatMulTranslationCompose },
|
||
|
|
{ "MatMulScaleTranslate",TestMatMulScaleThenTranslate },
|
||
|
|
}};
|
||
|
|
|
||
|
|
for (auto const& [name, fn] : tests) {
|
||
|
|
if (auto err = std::unique_ptr<std::string>(fn())) {
|
||
|
|
std::println(std::cerr, "[{}] {}", name, *err);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// LookTo is tested separately - its expected result depends on the
|
||
|
|
// internal basis convention and a single failure here is informative
|
||
|
|
// enough to keep in the matrix suite.
|
||
|
|
if (auto err = std::unique_ptr<std::string>(TestLookToBasis())) {
|
||
|
|
std::println(std::cerr, "[LookTo] {}", *err);
|
||
|
|
return 1;
|
||
|
|
}
|
||
|
|
return 0;
|
||
|
|
}
|