Files
aego/core/mathx/math3d_test.go
sanspie 17cb5e4291 Stage 2-5: assets, scene/ECS, editor, 3D, and Aego Script
Stage 2: render pipeline and UI toolkit over gpu/platform.
Stage 3: schema registry, asset pipeline + runtime loader, scene graph and ECS.
Stage 4: editor — docking, inspector, scene/game views, play mode, asset browser, console, profiler; 4.5 adds the 3D foundation (mixed 2D/3D spaces, meshes, depth).
Stage 5: Aego Script — compiler (lexer/parser/types/resolver/checker/asm/compiler), bytecode with verifier and register VM, instance runtime, stdlib, native bindings, and editor/CLI integration (aego run/edit/check/test/dump).
2026-10-08 13:17:28 +03:00

229 lines
7.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mathx
import "testing"
const eps3 = 1e-4
func TestQuatAxisAngleRotate(t *testing.T) {
q := QuatAxisAngle(Vec3{Z: 1}, Pi/2)
if got := q.Rotate(Vec3{X: 1}); !got.NearlyEqual(Vec3{Y: 1}, eps3) {
t.Fatalf("rotate = %v", got)
}
if got := q.Inverse().Rotate(Vec3{Y: 1}); !got.NearlyEqual(Vec3{X: 1}, eps3) {
t.Fatalf("inverse rotate = %v", got)
}
}
func TestQuatEulerRoundTrip(t *testing.T) {
cases := [][3]float32{{0, 0, 0}, {0.3, -0.4, 1.2}, {-1, 0.2, -2.5}, {2, 1, -0.5}}
for _, c := range cases {
x, y, z := QuatEuler(c[0], c[1], c[2]).Euler()
if !NearlyEqual(x, c[0], eps3) || !NearlyEqual(y, c[1], eps3) || !NearlyEqual(z, c[2], eps3) {
t.Fatalf("%v -> %v %v %v", c, x, y, z)
}
}
q := QuatEuler(0.5, Pi/2, 0.3)
x, y, z := q.Euler()
if !QuatEuler(x, y, z).NearlySame(q, eps3) {
t.Fatal("gimbal lock round trip changed the rotation")
}
}
func TestQuatMat4MatchesRotate(t *testing.T) {
q := QuatEuler(0.4, -0.7, 1.1)
v := Vec3{X: 1, Y: -2, Z: 0.5}
if !q.Mat4().TransformDir(v).NearlyEqual(q.Rotate(v), eps3) {
t.Fatal("matrix and quaternion rotate differently")
}
}
func TestQuatSlerp(t *testing.T) {
a := QuatIdentity()
b := QuatAxisAngle(Vec3{Z: 1}, Pi/2)
if !a.Slerp(b, 0.5).NearlySame(QuatAxisAngle(Vec3{Z: 1}, Pi/4), eps3) {
t.Fatal("slerp midpoint wrong")
}
}
func TestQuatLookAt(t *testing.T) {
f := Vec3{X: 1, Y: 0.5, Z: -1}
q := QuatLookAt(f, Vec3{Y: 1})
if !q.Forward().NearlyEqual(f.Normalized(), eps3) {
t.Fatalf("forward = %v", q.Forward())
}
if q.Up().Y <= 0 {
t.Fatal("up flipped")
}
if !QuatLookAt(Vec3{Z: -1}, Vec3{Y: 1}).NearlySame(QuatIdentity(), eps3) {
t.Fatal("looking down -Z must be identity")
}
QuatLookAt(Vec3{Y: 1}, Vec3{Y: 1})
}
func TestMat4InverseAndDecompose(t *testing.T) {
pos := Vec3{X: 1, Y: 2, Z: 3}
rot := QuatEuler(0.3, 0.5, -0.2)
scale := Vec3{X: 2, Y: 3, Z: 0.5}
m := Mat4TRS(pos, rot, scale)
p, q, s, ok := m.Decompose()
if !ok || !p.NearlyEqual(pos, eps3) || !s.NearlyEqual(scale, eps3) || !q.NearlySame(rot, eps3) {
t.Fatalf("decompose = %v %v %v", p, q, s)
}
flipped := Mat4TRS(pos, rot, Vec3{X: -2, Y: 3, Z: 1})
p, q, s, ok = flipped.Decompose()
if !ok || s.X >= 0 || !Mat4TRS(p, q, s).NearlyEqual(flipped, eps3) {
t.Fatal("negative scale does not rebuild the same matrix")
}
if _, ok := (Mat4{}).Inverse(); ok {
t.Fatal("singular matrix inverted")
}
if _, ok := Mat4TRS(pos, rot, Vec3{X: 1, Y: 0, Z: 1}).InverseAffine(); ok {
t.Fatal("zero scale inverted")
}
}
func TestInverseVariants(t *testing.T) {
trs := Mat4TRS(Vec3{X: -4, Y: 1.5, Z: 7}, QuatEuler(0.7, -1.1, 0.4), Vec3{X: 3, Y: 0.25, Z: 1.7})
full, ok := trs.Inverse()
if !ok || !trs.Mul(full).NearlyEqual(Mat4Identity(), 1e-5) {
t.Fatal("Inverse failed on a non-uniform TRS")
}
aff, ok := trs.InverseAffine()
if !ok || !trs.Mul(aff).NearlyEqual(Mat4Identity(), 1e-5) {
t.Fatal("InverseAffine failed on a non-uniform TRS")
}
if !aff.NearlyEqual(full, 1e-5) {
t.Fatal("affine and full inverse disagree")
}
vp := Mat4Perspective(Deg2Rad(60), 1.5, 0.1, 100).Mul(Mat4LookAt(Vec3{X: 3, Y: 2, Z: 5}, Vec3{}, Vec3{Y: 1}))
inv, ok := vp.Inverse()
if !ok || !vp.Mul(inv).NearlyEqual(Mat4Identity(), 1e-4) {
t.Fatal("Inverse failed on Perspective×LookAt")
}
}
func TestLookAtDegenerateUp(t *testing.T) {
for _, target := range []Vec3{{Y: -3}, {Y: 3}, {X: 1e-7, Y: 3}} {
m := Mat4LookAt(Vec3{}, target, Vec3{Y: 1})
for i, v := range m {
if v != v {
t.Fatalf("NaN at %d looking at %v", i, target)
}
}
if got := m.TransformPoint(target); !got.NearlyEqual(Vec3{Z: -target.Len()}, 1e-3) {
t.Fatalf("target in view = %v", got)
}
}
}
func TestPerspectiveDegenerate(t *testing.T) {
if Mat4Perspective(Deg2Rad(60), 1, 5, 5) != Mat4Identity() {
t.Fatal("near == far must yield identity")
}
if Mat4Perspective(0, 1, 0.1, 100) != Mat4Identity() {
t.Fatal("zero fov must yield identity")
}
m := Mat4Perspective(Deg2Rad(60), 0, 0.1, 100)
if m[0] != m[5] {
t.Fatal("zero aspect must behave as 1")
}
}
func TestTransformPointHomogeneous(t *testing.T) {
p := Mat4Perspective(Deg2Rad(90), 1, 1, 10)
got := p.TransformPoint(Vec3{X: 2, Z: -2})
if !NearlyEqual(got.X, 1, eps3) {
t.Fatalf("perspective divide missing: %v", got)
}
if got := Mat4Translate(Vec3{X: 1}).TransformPoint(Vec3{Y: 2}); got != (Vec3{X: 1, Y: 2}) {
t.Fatalf("affine point = %v", got)
}
}
func TestPerspectiveAndLookAt(t *testing.T) {
p := Mat4Perspective(Deg2Rad(60), 1.5, 0.1, 100)
near := p.MulVec4(Vec4{Z: -0.1, W: 1})
far := p.MulVec4(Vec4{Z: -100, W: 1})
if !NearlyEqual(near.Z/near.W, -1, 1e-3) || !NearlyEqual(far.Z/far.W, 1, 1e-3) {
t.Fatalf("near %v far %v", near.Z/near.W, far.Z/far.W)
}
eye := Vec3{Z: 5}
v := Mat4LookAt(eye, Vec3{}, Vec3{Y: 1})
if got := v.TransformPoint(Vec3{}); !got.NearlyEqual(Vec3{Z: -5}, eps3) {
t.Fatalf("origin in view = %v", got)
}
if got := v.TransformPoint(Vec3{X: 1}); !got.NearlyEqual(Vec3{X: 1, Z: -5}, eps3) {
t.Fatalf("+x in view = %v", got)
}
target := Vec3{X: 2, Y: 1, Z: -3}
cam := Mat4TRS(eye, QuatLookAt(target.Sub(eye), Vec3{Y: 1}), Vec3{X: 1, Y: 1, Z: 1})
inv, _ := cam.Inverse()
if !inv.NearlyEqual(Mat4LookAt(eye, target, Vec3{Y: 1}), eps3) {
t.Fatal("LookAt differs from inverse camera TRS")
}
}
func TestMat4LiftComposes(t *testing.T) {
a := Mat3TRS(Vec2{X: 3, Y: 4}, 0.5, Vec2{X: 2, Y: 1})
b := Mat3TRS(Vec2{X: -1, Y: 2}, -0.3, Vec2{X: 1, Y: 1})
if !Mat4Lift(a).Mul(Mat4Lift(b)).NearlyEqual(Mat4Lift(a.Mul(b)), eps3) {
t.Fatal("Lift is not a homomorphism")
}
p := Vec2{X: 1.5, Y: -2}
w := a.Apply(p)
got := Mat4Lift(a).TransformPoint(Vec3{X: p.X, Y: -p.Y})
if !got.NearlyEqual(Vec3{X: w.X, Y: -w.Y}, eps3) {
t.Fatalf("lift = %v, want %v", got, w)
}
}
func TestAABB3Transformed(t *testing.T) {
b := AABB3{Min: Vec3{X: -0.5, Y: -0.5, Z: -0.5}, Max: Vec3{X: 0.5, Y: 0.5, Z: 0.5}}
r := b.Transformed(Mat4TRS(Vec3{X: 10}, QuatAxisAngle(Vec3{Z: 1}, Pi/4), Vec3{X: 1, Y: 1, Z: 1}))
h := Sqrt(2) / 2
if !r.Extents().NearlyEqual(Vec3{X: h, Y: h, Z: 0.5}, eps3) || !r.Center().NearlyEqual(Vec3{X: 10}, eps3) {
t.Fatalf("transformed = %+v", r)
}
}
func TestRayAABB(t *testing.T) {
b := AABB3{Min: Vec3{X: -0.5, Y: -0.5, Z: -0.5}, Max: Vec3{X: 0.5, Y: 0.5, Z: 0.5}}
if d, ok := (Ray3{Origin: Vec3{Z: 5}, Dir: Vec3{Z: -1}}).IntersectAABB(b); !ok || !NearlyEqual(d, 4.5, eps3) {
t.Fatalf("hit = %v %v", d, ok)
}
if _, ok := (Ray3{Origin: Vec3{X: 3, Z: 5}, Dir: Vec3{Z: -1}}).IntersectAABB(b); ok {
t.Fatal("miss reported as hit")
}
if _, ok := (Ray3{Origin: Vec3{Z: 5}, Dir: Vec3{Z: 1}}).IntersectAABB(b); ok {
t.Fatal("box behind the ray reported as hit")
}
if d, ok := (Ray3{Dir: Vec3{X: 1}}).IntersectAABB(b); !ok || d != 0 {
t.Fatal("origin inside must hit at 0")
}
pl := PlaneFromPointNormal(Vec3{}, Vec3{Y: 1})
if d, ok := (Ray3{Origin: Vec3{Y: 2}, Dir: Vec3{Y: -1}}).IntersectPlane(pl); !ok || !NearlyEqual(d, 2, eps3) {
t.Fatalf("plane hit = %v %v", d, ok)
}
}
func TestFrustumContainsAABB(t *testing.T) {
proj := Mat4Perspective(Deg2Rad(60), 1, 0.1, 100)
view := Mat4LookAt(Vec3{Z: 5}, Vec3{}, Vec3{Y: 1})
f := FrustumFromMat4(proj.Mul(view))
box := func(c Vec3) AABB3 {
return AABB3{Min: c.Sub(Vec3{X: 0.5, Y: 0.5, Z: 0.5}), Max: c.Add(Vec3{X: 0.5, Y: 0.5, Z: 0.5})}
}
if !f.ContainsAABB(box(Vec3{})) {
t.Fatal("box in front of the camera culled")
}
if f.ContainsAABB(box(Vec3{Z: 7})) {
t.Fatal("box behind the camera visible")
}
if f.ContainsAABB(box(Vec3{X: 50})) {
t.Fatal("box far to the side visible")
}
if f.ContainsAABB(box(Vec3{Z: -200})) {
t.Fatal("box past the far plane visible")
}
}