From 5dc2b69a46a81bb09d54faef4c9195e32086060d Mon Sep 17 00:00:00 2001 From: Christian Andersson Date: Mon, 4 Apr 2022 22:08:21 +0200 Subject: [PATCH 1/4] Breaking change(!) cross vec2 (float32 float64) now return a scalar value, not a new vector. Cross-product in 2D is not well-defined but it is changed to the 2D version stated at https://mathworld.wolfram.com/CrossProduct.html . Add Sinus and Cosine for angle between vectors (vec2 vec3 float64 float32) Changed implementation for Angle (vec2 vec3 float32 float64) to utilize Cosine implementation (with math.Acos). Changed implementation for left and right winding (vec2 float32 float64) Duplicated test file for quaternion (float32 float64) --- float64/quaternion/quaternion_test.go | 91 ++++++++++++++++++ float64/vec2/vec2.go | 86 +++++++++++------ float64/vec2/vec2_test.go | 124 +++++++++++++++++++++++++ float64/vec3/vec3.go | 45 +++++++-- float64/vec3/vec3_test.go | 85 +++++++++++++++++ quaternion/quaternion_test.go | 104 ++++++--------------- vec2/vec2.go | 86 +++++++++++------ vec2/vec2_test.go | 129 +++++++++++++++++++++++++- vec3/vec3.go | 45 +++++++-- vec3/vec3_test.go | 86 +++++++++++++++++ 10 files changed, 725 insertions(+), 156 deletions(-) create mode 100644 float64/quaternion/quaternion_test.go diff --git a/float64/quaternion/quaternion_test.go b/float64/quaternion/quaternion_test.go new file mode 100644 index 0000000..3081e0c --- /dev/null +++ b/float64/quaternion/quaternion_test.go @@ -0,0 +1,91 @@ +package quaternion + +import ( + "fmt" + "math" + "testing" + + "github.com/ungerik/go3d/float64/vec3" +) + +// RotateVec3 rotates v by the rotation represented by the quaternion. +func rotateAndNormalizeVec3(quat *T, v *vec3.T) { + qv := T{v[0], v[1], v[2], 0} + inv := quat.Inverted() + q := Mul3(quat, &qv, &inv) + v[0] = q[0] + v[1] = q[1] + v[2] = q[2] +} + +func TestQuaternionRotateVec3(t *testing.T) { + eularAngles := []vec3.T{ + {90, 20, 21}, + {-90, 0, 0}, + {28, 1043, -38}, + } + vecs := []vec3.T{ + {2, 3, 4}, + {1, 3, -2}, + {-6, 2, 9}, + } + for _, vec := range vecs { + for _, eularAngle := range eularAngles { + func() { + q := FromEulerAngles(eularAngle[1]*math.Pi/180.0, eularAngle[0]*math.Pi/180.0, eularAngle[2]*math.Pi/180.0) + vec_r1 := vec + vec_r2 := vec + magSqr := vec_r1.LengthSqr() + rotateAndNormalizeVec3(&q, &vec_r2) + q.RotateVec3(&vec_r1) + vecd := q.RotatedVec3(&vec) + magSqr2 := vec_r1.LengthSqr() + + if vecd != vec_r1 { + t.Fail() + } + + angle := vec3.Angle(&vec_r1, &vec_r2) + length := math.Abs(magSqr - magSqr2) + + if angle > 0.0000001 { + fmt.Printf("test case %v rotates %v failed - angle difference to large\n", eularAngle, vec) + fmt.Println("vectors:", vec_r1, vec_r2) + fmt.Println("angle:", angle) + t.Fail() + } + + if length > 0.000000000001 { + fmt.Printf("test case %v rotates %v failed - squared length difference to large\n", eularAngle, vec) + fmt.Println("vectors:", vec_r1, vec_r2) + fmt.Println("squared lengths:", magSqr, magSqr2) + t.Fail() + } + }() + } + } +} + +func TestToEulerAngles(t *testing.T) { + specialValues := []float64{-5, -math.Pi, -2, -math.Pi / 2, 0, math.Pi / 2, 2.4, math.Pi, 3.9} + for _, x := range specialValues { + for _, y := range specialValues { + for _, z := range specialValues { + quat1 := FromEulerAngles(y, x, z) + ry, rx, rz := quat1.ToEulerAngles() + quat2 := FromEulerAngles(ry, rx, rz) + // quat must be equivalent + const e64 = 1e-14 + cond1 := math.Abs(quat1[0]-quat2[0]) < e64 && math.Abs(quat1[1]-quat2[1]) < e64 && math.Abs(quat1[2]-quat2[2]) < e64 && math.Abs(quat1[3]-quat2[3]) < e64 + cond2 := math.Abs(quat1[0]+quat2[0]) < e64 && math.Abs(quat1[1]+quat2[1]) < e64 && math.Abs(quat1[2]+quat2[2]) < e64 && math.Abs(quat1[3]+quat2[3]) < e64 + if !cond1 && !cond2 { + fmt.Printf("test case %v, %v, %v failed\n", x, y, z) + fmt.Printf("result is %v, %v, %v\n", rx, ry, rz) + fmt.Printf("quat1 is %v\n", quat1) + fmt.Printf("quat2 is %v\n", quat2) + t.Fail() + } + } + } + } +} diff --git a/float64/vec2/vec2.go b/float64/vec2/vec2.go index e294c6e..4f8c906 100644 --- a/float64/vec2/vec2.go +++ b/float64/vec2/vec2.go @@ -104,6 +104,11 @@ func (vec *T) PracticallyEquals(compareVector *T, allowedDelta float64) bool { (math.Abs(vec[1]-compareVector[1]) <= allowedDelta) } +// PracticallyEquals compares two values if they are equal with each other within a delta tolerance. +func PracticallyEquals(v1, v2, allowedDelta float64) bool { + return math.Abs(v1-v2) <= allowedDelta +} + // Invert inverts the vector. func (vec *T) Invert() *T { vec[0] = -vec[0] @@ -134,7 +139,7 @@ func (vec *T) Normalize() *T { if sl == 0 || sl == 1 { return vec } - return vec.Scale(1 / math.Sqrt(sl)) + return vec.Scale(1.0 / math.Sqrt(sl)) } // Normalized returns a unit length normalized copy of the vector. @@ -144,16 +149,18 @@ func (vec *T) Normalized() T { return v } -// Normal returns an orthogonal vector. +// Normal returns a new normalized orthogonal vector. // The normal is orthogonal clockwise to the vector. +// See also function Rotate90DegRight. func (vec *T) Normal() T { n := *vec n[0], n[1] = n[1], -n[0] return *n.Normalize() } -// Normal returns an orthogonal vector. -// The normal is orthogonal counter clockwise to the vector. +// NormalCCW returns a new normalized orthogonal vector. +// The normal is orthogonal counterclockwise to the vector. +// See also function Rotate90DegLeft. func (vec *T) NormalCCW() T { n := *vec n[0], n[1] = -n[1], n[0] @@ -218,21 +225,46 @@ func (vec *T) RotateAroundPoint(point *T, angle float64) *T { } // Rotate90DegLeft rotates the vector 90 degrees left (counter-clockwise). +// See also function NormalCCW. func (vec *T) Rotate90DegLeft() *T { - temp := vec[0] - vec[0] = -vec[1] - vec[1] = temp + vec[0], vec[1] = -vec[1], vec[0] return vec } // Rotate90DegRight rotates the vector 90 degrees right (clockwise). +// See also function Normal. func (vec *T) Rotate90DegRight() *T { - temp := vec[0] - vec[0] = vec[1] - vec[1] = -temp + vec[0], vec[1] = vec[1], -vec[0] return vec } +// Sinus returns the sinus value of the (shortest/smallest) angle between the two vectors a and b. +// The returned sine value is in the range -1.0 ≤ value ≤ 1.0. +// The angle is always considered to be in the range 0 to Pi radians and thus the sine value returned is always positive. +func Sinus(a, b *T) float64 { + v := Cross(a, b) / math.Sqrt(a.LengthSqr()*b.LengthSqr()) + + if v > 1.0 { + return 1.0 + } else if v < -1.0 { + return -1.0 + } + return v +} + +// Cosine returns the cosine value of the angle between the two vectors. +// The returned cosine value is in the range -1.0 ≤ value ≤ 1.0. +func Cosine(a, b *T) float64 { + v := Dot(a, b) / math.Sqrt(a.LengthSqr()*b.LengthSqr()) + + if v > 1.0 { + return 1.0 + } else if v < -1.0 { + return -1.0 + } + return v +} + // Angle returns the counter-clockwise angle of the vector from the x axis. func (vec *T) Angle() float64 { return math.Atan2(vec[1], vec[0]) @@ -258,36 +290,30 @@ func Dot(a, b *T) float64 { return a[0]*b[0] + a[1]*b[1] } -// Cross returns the cross product of two vectors. -func Cross(a, b *T) T { - return T{ - a[1]*b[0] - a[0]*b[1], - a[0]*b[1] - a[1]*b[0], - } +// Cross returns the "cross product" of two vectors. +// In 2D space it is a scalar value. +// It is the same as the determinant value of the 2D matrix constructed by the two vectors. +// Cross product in 2D is not well-defined but this is the implementation stated at https://mathworld.wolfram.com/CrossProduct.html . +func Cross(a, b *T) float64 { + return a[0]*b[1] - a[1]*b[0] } -// Angle returns the angle between two vectors. +// Angle returns the angle value of the (shortest/smallest) angle between the two vectors a and b. +// The returned value is in the range 0 ≤ angle ≤ Pi radians. func Angle(a, b *T) float64 { - v := Dot(a, b) / (a.Length() * b.Length()) - // prevent NaN - if v > 1. { - v = v - 2 - } else if v < -1. { - v = v + 2 - } - return math.Acos(v) + return math.Acos(Cosine(a, b)) } // IsLeftWinding returns if the angle from a to b is left winding. +// Two parallell or anti parallell vectors will give a false result. func IsLeftWinding(a, b *T) bool { - ab := b.Rotated(-a.Angle()) - return ab.Angle() > 0 + return Cross(a, b) > 0 // It's really the sign changing part of the Sinus(a, b) function } // IsRightWinding returns if the angle from a to b is right winding. +// Two parallell or anti parallell vectors will give a false result. func IsRightWinding(a, b *T) bool { - ab := b.Rotated(-a.Angle()) - return ab.Angle() < 0 + return Cross(a, b) < 0 // It's really the sign changing part of the Sinus(a, b) function } // Min returns the component wise minimum of two vectors. @@ -316,7 +342,7 @@ func Max(a, b *T) T { // Interpolate interpolates between a and b at t (0,1). func Interpolate(a, b *T, t float64) T { - t1 := 1 - t + t1 := 1.0 - t return T{ a[0]*t1 + b[0]*t, a[1]*t1 + b[1]*t, diff --git a/float64/vec2/vec2_test.go b/float64/vec2/vec2_test.go index d8fa340..3ea5b21 100644 --- a/float64/vec2/vec2_test.go +++ b/float64/vec2/vec2_test.go @@ -2,6 +2,7 @@ package vec2 import ( "math" + "strconv" "testing" ) @@ -214,3 +215,126 @@ func TestMuled(t *testing.T) { t.Fail() } } + +func TestAngle(t *testing.T) { + radFor45deg := math.Pi / 4.0 + testSetups := []struct { + a, b T + expectedAngle float64 + name string + }{ + {a: T{1, 0}, b: T{1, 0}, expectedAngle: 0 * radFor45deg, name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0}, b: T{1, 1}, expectedAngle: 1 * radFor45deg, name: "45 degree angle"}, + {a: T{1, 0}, b: T{0, 1}, expectedAngle: 2 * radFor45deg, name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{-1, 1}, expectedAngle: 3 * radFor45deg, name: "135 degree angle"}, + {a: T{1, 0}, b: T{-1, 0}, expectedAngle: 4 * radFor45deg, name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0}, b: T{-1, -1}, expectedAngle: (8 - 5) * radFor45deg, name: "225 degree angle"}, + {a: T{1, 0}, b: T{0, -1}, expectedAngle: (8 - 6) * radFor45deg, name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{1, -1}, expectedAngle: (8 - 7) * radFor45deg, name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + angle := Angle(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(angle, testSetup.expectedAngle, 0.00000001) { + t.Errorf("Angle expected to be %f but was %f for test \"%s\".", testSetup.expectedAngle, angle, testSetup.name) + } + }) + } +} + +func TestCosine(t *testing.T) { + radFor45deg := math.Pi / 4.0 + testSetups := []struct { + a, b T + expectedCosine float64 + name string + }{ + {a: T{1, 0}, b: T{1, 0}, expectedCosine: math.Cos(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0}, b: T{1, 1}, expectedCosine: math.Cos(1 * radFor45deg), name: "45 degree angle"}, + {a: T{1, 0}, b: T{0, 1}, expectedCosine: math.Cos(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{-1, 1}, expectedCosine: math.Cos(3 * radFor45deg), name: "135 degree angle"}, + {a: T{1, 0}, b: T{-1, 0}, expectedCosine: math.Cos(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0}, b: T{-1, -1}, expectedCosine: math.Cos(5 * radFor45deg), name: "225 degree angle"}, + {a: T{1, 0}, b: T{0, -1}, expectedCosine: math.Cos(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{1, -1}, expectedCosine: math.Cos(7 * radFor45deg), name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + cos := Cosine(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(cos, testSetup.expectedCosine, 0.00000001) { + t.Errorf("Cosine expected to be %f but was %f for test \"%s\".", testSetup.expectedCosine, cos, testSetup.name) + } + }) + } +} + +func TestSinus(t *testing.T) { + radFor45deg := math.Pi / 4.0 + testSetups := []struct { + a, b T + expectedSine float64 + name string + }{ + {a: T{1, 0}, b: T{1, 0}, expectedSine: math.Sin(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0}, b: T{1, 1}, expectedSine: math.Sin(1 * radFor45deg), name: "45 degree angle"}, + {a: T{1, 0}, b: T{0, 1}, expectedSine: math.Sin(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{-1, 1}, expectedSine: math.Sin(3 * radFor45deg), name: "135 degree angle"}, + {a: T{1, 0}, b: T{-1, 0}, expectedSine: math.Sin(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0}, b: T{-1, -1}, expectedSine: math.Sin(5 * radFor45deg), name: "225 degree angle"}, + {a: T{1, 0}, b: T{0, -1}, expectedSine: math.Sin(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{1, -1}, expectedSine: math.Sin(7 * radFor45deg), name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + sin := Sinus(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(sin, testSetup.expectedSine, 0.00000001) { + t.Errorf("Sine expected to be %f but was %f for test \"%s\".", testSetup.expectedSine, sin, testSetup.name) + } + }) + } +} + +func TestLeftRightWinding(t *testing.T) { + a := T{1.0, 0.0} + + for angle := 0; angle <= 360; angle += 15 { + rad := (math.Pi / 180.0) * float64(angle) + + bx := clampDecimals(math.Cos(rad), 4) + by := clampDecimals(math.Sin(rad), 4) + b := T{bx, by} + + t.Run("left winding angle "+strconv.Itoa(angle), func(t *testing.T) { + lw := IsLeftWinding(&a, &b) + rw := IsRightWinding(&a, &b) + + if angle%180 == 0 { + // No winding at 0, 180 and 360 degrees + if lw || rw { + t.Errorf("Neither left or right winding should be true on angle %d. Left winding=%t, right winding=%t", angle, lw, rw) + } + } else if angle < 180 { + // Left winding at 0 < angle < 180 + if !lw || rw { + t.Errorf("Left winding should be true (not right winding) on angle %d. Left winding=%t, right winding=%t", angle, lw, rw) + } + } else if angle > 180 { + // Right winding at 180 < angle < 360 + if lw || !rw { + t.Errorf("Right winding should be true (not left winding) on angle %d. Left winding=%t, right winding=%t", angle, lw, rw) + } + } + }) + } +} + +func clampDecimals(decimalValue float64, amountDecimals float64) float64 { + factor := math.Pow(10, amountDecimals) + return math.Round(decimalValue*factor) / factor +} diff --git a/float64/vec3/vec3.go b/float64/vec3/vec3.go index edd8456..32df455 100644 --- a/float64/vec3/vec3.go +++ b/float64/vec3/vec3.go @@ -126,6 +126,11 @@ func (vec *T) PracticallyEquals(compareVector *T, allowedDelta float64) bool { (math.Abs(vec[2]-compareVector[2]) <= allowedDelta) } +// PracticallyEquals compares two values if they are equal with each other within a delta tolerance. +func PracticallyEquals(v1, v2, allowedDelta float64) bool { + return math.Abs(v1-v2) <= allowedDelta +} + // Invert inverts the vector. func (vec *T) Invert() *T { vec[0] = -vec[0] @@ -257,16 +262,38 @@ func Cross(a, b *T) T { } } -// Angle returns the angle between two vectors. -func Angle(a, b *T) float64 { - v := Dot(a, b) / (a.Length() * b.Length()) - // prevent NaN - if v > 1. { - return 0 - } else if v < -1. { - return math.Pi +// Sinus returns the sinus value of the (shortest/smallest) angle between the two vectors a and b. +// The returned sine value is in the range 0.0 ≤ value ≤ 1.0. +// The angle is always considered to be in the range 0 to Pi radians and thus the sine value returned is always positive. +func Sinus(a, b *T) float64 { + cross := Cross(a, b) + v := cross.Length() / math.Sqrt(a.LengthSqr()*b.LengthSqr()) + + if v > 1.0 { + return 1.0 + } else if v < 0.0 { + return 0.0 } - return math.Acos(v) + return v +} + +// Cosine returns the cosine value of the angle between the two vectors. +// The returned cosine value is in the range -1.0 ≤ value ≤ 1.0. +func Cosine(a, b *T) float64 { + v := Dot(a, b) / math.Sqrt(a.LengthSqr()*b.LengthSqr()) + + if v > 1.0 { + return 1.0 + } else if v < -1.0 { + return -1.0 + } + return v +} + +// Angle returns the angle value of the (shortest/smallest) angle between the two vectors a and b. +// The returned value is in the range 0 ≤ angle ≤ Pi radians. +func Angle(a, b *T) float64 { + return math.Acos(Cosine(a, b)) } // Min returns the component wise minimum of two vectors. diff --git a/float64/vec3/vec3_test.go b/float64/vec3/vec3_test.go index 97deae7..1d07bca 100644 --- a/float64/vec3/vec3_test.go +++ b/float64/vec3/vec3_test.go @@ -1,6 +1,7 @@ package vec3 import ( + "math" "testing" ) @@ -142,3 +143,87 @@ func TestMuled(t *testing.T) { t.Fail() } } + +func TestAngle(t *testing.T) { + radFor45deg := math.Pi / 4.0 + testSetups := []struct { + a, b T + expectedAngle float64 + name string + }{ + {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedAngle: 0 * radFor45deg, name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedAngle: 1 * radFor45deg, name: "45 degree angle"}, + {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedAngle: 2 * radFor45deg, name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedAngle: 3 * radFor45deg, name: "135 degree angle"}, + {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedAngle: 4 * radFor45deg, name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedAngle: (8 - 5) * radFor45deg, name: "225 degree angle"}, + {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedAngle: (8 - 6) * radFor45deg, name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedAngle: (8 - 7) * radFor45deg, name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + angle := Angle(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(angle, testSetup.expectedAngle, 0.00000001) { + t.Errorf("Angle expected to be %f but was %f for test \"%s\".", testSetup.expectedAngle, angle, testSetup.name) + } + }) + } +} + +func TestCosine(t *testing.T) { + radFor45deg := math.Pi / 4.0 + testSetups := []struct { + a, b T + expectedCosine float64 + name string + }{ + {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedCosine: math.Cos(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedCosine: math.Cos(1 * radFor45deg), name: "45 degree angle"}, + {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedCosine: math.Cos(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedCosine: math.Cos(3 * radFor45deg), name: "135 degree angle"}, + {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedCosine: math.Cos(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedCosine: math.Cos(5 * radFor45deg), name: "225 degree angle"}, + {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedCosine: math.Cos(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedCosine: math.Cos(7 * radFor45deg), name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + cos := Cosine(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(cos, testSetup.expectedCosine, 0.00000001) { + t.Errorf("Cosine expected to be %f but was %f for test \"%s\".", testSetup.expectedCosine, cos, testSetup.name) + } + }) + } +} + +func TestSinus(t *testing.T) { + radFor45deg := math.Pi / 4.0 + testSetups := []struct { + a, b T + expectedSine float64 + name string + }{ + {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedSine: math.Sin(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedSine: math.Sin(1 * radFor45deg), name: "45 degree angle"}, + {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedSine: math.Sin(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedSine: math.Sin(3 * radFor45deg), name: "135 degree angle"}, + {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedSine: math.Sin(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedSine: math.Abs(math.Sin(5 * radFor45deg)), name: "225 degree angle"}, + {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedSine: math.Abs(math.Sin(6 * radFor45deg)), name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedSine: math.Abs(math.Sin(7 * radFor45deg)), name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + sin := Sinus(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(sin, testSetup.expectedSine, 0.00000001) { + t.Errorf("Sine expected to be %f but was %f for test \"%s\".", testSetup.expectedSine, sin, testSetup.name) + } + }) + } +} diff --git a/quaternion/quaternion_test.go b/quaternion/quaternion_test.go index 4566d6a..c69ba60 100644 --- a/quaternion/quaternion_test.go +++ b/quaternion/quaternion_test.go @@ -2,17 +2,14 @@ package quaternion import ( "fmt" - "math" "testing" - quaternion64 "github.com/ungerik/go3d/float64/quaternion" - vec364 "github.com/ungerik/go3d/float64/vec3" - "github.com/ungerik/go3d/fmath" + math "github.com/ungerik/go3d/fmath" "github.com/ungerik/go3d/vec3" ) // RotateVec3 rotates v by the rotation represented by the quaternion. -func rotateAndNormalizeVec332(quat *T, v *vec3.T) { +func rotateAndNormalizeVec3(quat *T, v *vec3.T) { qv := T{v[0], v[1], v[2], 0} inv := quat.Inverted() q := Mul3(quat, &qv, &inv) @@ -21,23 +18,13 @@ func rotateAndNormalizeVec332(quat *T, v *vec3.T) { v[2] = q[2] } -// RotateVec3 rotates v by the rotation represented by the quaternion. -func rotateAndNormalizeVec364(quat *quaternion64.T, v *vec364.T) { - qv := quaternion64.T{v[0], v[1], v[2], 0} - inv := quat.Inverted() - q := quaternion64.Mul3(quat, &qv, &inv) - v[0] = q[0] - v[1] = q[1] - v[2] = q[2] -} - func TestQuaternionRotateVec3(t *testing.T) { - eularAngles := []vec364.T{ + eularAngles := []vec3.T{ {90, 20, 21}, {-90, 0, 0}, {28, 1043, -38}, } - vecs := []vec364.T{ + vecs := []vec3.T{ {2, 3, 4}, {1, 3, -2}, {-6, 2, 9}, @@ -45,53 +32,33 @@ func TestQuaternionRotateVec3(t *testing.T) { for _, vec := range vecs { for _, eularAngle := range eularAngles { func() { - q := quaternion64.FromEulerAngles(eularAngle[1]*math.Pi/180, eularAngle[0]*math.Pi/180, eularAngle[2]*math.Pi/180) + q := FromEulerAngles(eularAngle[1]*math.Pi/180.0, eularAngle[0]*math.Pi/180.0, eularAngle[2]*math.Pi/180.0) vec_r1 := vec vec_r2 := vec magSqr := vec_r1.LengthSqr() - rotateAndNormalizeVec364(&q, &vec_r2) + rotateAndNormalizeVec3(&q, &vec_r2) q.RotateVec3(&vec_r1) vecd := q.RotatedVec3(&vec) magSqr2 := vec_r1.LengthSqr() + if vecd != vec_r1 { t.Fail() } - if vec364.Angle(&vec_r1, &vec_r2) > 0.00000001 { - fmt.Printf("test case %v rotates %v failed\n", eularAngle, vec) - fmt.Println(vec_r1, vec_r2) - fmt.Println(vec364.Angle(&vec_r1, &vec_r2)) - t.Fail() - } - if math.Abs(float64(magSqr-magSqr2)) > 0.000000000001 { - fmt.Printf("test case %v rotates %v failed\n", eularAngle, vec) - fmt.Println(vec_r1, vec_r2) - fmt.Println(magSqr, magSqr2) - t.Fail() - } - }() - func() { - q := FromEulerAngles(float32(eularAngle[1]*math.Pi/180), float32(eularAngle[0]*math.Pi/180), float32(eularAngle[2]*math.Pi/180)) - vec32 := vec3.T{float32(vec[0]), float32(vec[1]), float32(vec[2])} - vec_r1 := vec32 - vec_r2 := vec32 - magSqr := vec_r1.LengthSqr() - rotateAndNormalizeVec332(&q, &vec_r2) - q.RotateVec3(&vec_r1) - vecd := q.RotatedVec3(&vec32) - magSqr2 := vec_r1.LengthSqr() - if vecd != vec_r1 { - t.Fail() - } - if vec3.Angle(&vec_r1, &vec_r2) > 0.001 { - fmt.Printf("test case %v rotates %v failed\n", eularAngle, vec) - fmt.Println(vec_r1, vec_r2) - fmt.Println(vec3.Angle(&vec_r1, &vec_r2)) + + angle := vec3.Angle(&vec_r1, &vec_r2) + length := math.Abs(magSqr - magSqr2) + + if angle > 0.001 { + fmt.Printf("test case %v rotates %v failed - angle difference to large\n", eularAngle, vec) + fmt.Println("vectors:", vec_r1, vec_r2) + fmt.Println("angle:", angle) t.Fail() } - if math.Abs(float64(magSqr-magSqr2)) > 0.0001 { - fmt.Printf("test case %v rotates %v failed\n", eularAngle, vec) - fmt.Println(vec_r1, vec_r2) - fmt.Println(magSqr, magSqr2) + + if length > 0.0001 { + fmt.Printf("test case %v rotates %v failed - squared length difference to large\n", eularAngle, vec) + fmt.Println("vectors:", vec_r1, vec_r2) + fmt.Println("squared lengths:", magSqr, magSqr2) t.Fail() } }() @@ -100,41 +67,24 @@ func TestQuaternionRotateVec3(t *testing.T) { } func TestToEulerAngles(t *testing.T) { - specialValues := []float64{-5, -math.Pi, -2, -math.Pi / 2, 0, math.Pi / 2, 2.4, math.Pi, 3.9} + specialValues := []float32{-5, -math.Pi, -2, -math.Pi / 2, 0, math.Pi / 2, 2.4, math.Pi, 3.9} for _, x := range specialValues { for _, y := range specialValues { for _, z := range specialValues { - quat1 := quaternion64.FromEulerAngles(y, x, z) + quat1 := FromEulerAngles(y, x, z) ry, rx, rz := quat1.ToEulerAngles() - quat2 := quaternion64.FromEulerAngles(ry, rx, rz) + quat2 := FromEulerAngles(ry, rx, rz) // quat must be equivalent - const e64 = 1e-14 - cond1 := math.Abs(quat1[0]-quat2[0]) < e64 && math.Abs(quat1[1]-quat2[1]) < e64 && math.Abs(quat1[2]-quat2[2]) < e64 && math.Abs(quat1[3]-quat2[3]) < e64 - cond2 := math.Abs(quat1[0]+quat2[0]) < e64 && math.Abs(quat1[1]+quat2[1]) < e64 && math.Abs(quat1[2]+quat2[2]) < e64 && math.Abs(quat1[3]+quat2[3]) < e64 + const e32 = 1e-6 + cond1 := math.Abs(quat1[0]-quat2[0]) < e32 && math.Abs(quat1[1]-quat2[1]) < e32 && math.Abs(quat1[2]-quat2[2]) < e32 && math.Abs(quat1[3]-quat2[3]) < e32 + cond2 := math.Abs(quat1[0]+quat2[0]) < e32 && math.Abs(quat1[1]+quat2[1]) < e32 && math.Abs(quat1[2]+quat2[2]) < e32 && math.Abs(quat1[3]+quat2[3]) < e32 if !cond1 && !cond2 { fmt.Printf("test case %v, %v, %v failed\n", x, y, z) - fmt.Printf("result is %v, %v, % v\n", rx, ry, rz) + fmt.Printf("result is %v, %v, %v\n", rx, ry, rz) fmt.Printf("quat1 is %v\n", quat1) fmt.Printf("quat2 is %v\n", quat2) t.Fail() } - x32 := float32(x) - y32 := float32(y) - z32 := float32(z) - quat132 := FromEulerAngles(x32, y32, z32) - ry32, rx32, rz32 := quat132.ToEulerAngles() - quat232 := FromEulerAngles(ry32, rx32, rz32) - // quat must be equivalent - const e32 = 1e-6 - cond1 = fmath.Abs(quat132[0]-quat232[0]) < e32 && fmath.Abs(quat132[1]-quat232[1]) < e32 && fmath.Abs(quat132[2]-quat232[2]) < e32 && fmath.Abs(quat132[3]-quat232[3]) < e32 - cond2 = fmath.Abs(quat132[0]+quat232[0]) < e32 && fmath.Abs(quat132[1]+quat232[1]) < e32 && fmath.Abs(quat132[2]+quat232[2]) < e32 && fmath.Abs(quat132[3]+quat232[3]) < e32 - if !cond1 && !cond2 { - fmt.Printf("test case %v, %v, %v failed\n", x32, y32, z32) - fmt.Printf("result is %v, %v, % v\n", rx32, ry32, rz32) - fmt.Printf("quat1 is %v\n", quat132) - fmt.Printf("quat2 is %v\n", quat232) - t.Fail() - } } } } diff --git a/vec2/vec2.go b/vec2/vec2.go index bd3291f..8def202 100644 --- a/vec2/vec2.go +++ b/vec2/vec2.go @@ -104,6 +104,11 @@ func (vec *T) PracticallyEquals(compareVector *T, allowedDelta float32) bool { (math.Abs(vec[1]-compareVector[1]) <= allowedDelta) } +// PracticallyEquals compares two values if they are equal with each other within a delta tolerance. +func PracticallyEquals(v1, v2, allowedDelta float32) bool { + return math.Abs(v1-v2) <= allowedDelta +} + // Invert inverts the vector. func (vec *T) Invert() *T { vec[0] = -vec[0] @@ -134,7 +139,7 @@ func (vec *T) Normalize() *T { if sl == 0 || sl == 1 { return vec } - return vec.Scale(1 / math.Sqrt(sl)) + return vec.Scale(1.0 / math.Sqrt(sl)) } // Normalized returns a unit length normalized copy of the vector. @@ -144,16 +149,18 @@ func (vec *T) Normalized() T { return v } -// Normal returns an orthogonal vector. +// Normal returns a new normalized orthogonal vector. // The normal is orthogonal clockwise to the vector. +// See also function Rotate90DegRight. func (vec *T) Normal() T { n := *vec n[0], n[1] = n[1], -n[0] return *n.Normalize() } -// Normal returns an orthogonal vector. -// The normal is orthogonal counter clockwise to the vector. +// NormalCCW returns a new normalized orthogonal vector. +// The normal is orthogonal counterclockwise to the vector. +// See also function Rotate90DegLeft. func (vec *T) NormalCCW() T { n := *vec n[0], n[1] = -n[1], n[0] @@ -218,21 +225,46 @@ func (vec *T) RotateAroundPoint(point *T, angle float32) *T { } // Rotate90DegLeft rotates the vector 90 degrees left (counter-clockwise). +// See also function NormalCCW. func (vec *T) Rotate90DegLeft() *T { - temp := vec[0] - vec[0] = -vec[1] - vec[1] = temp + vec[0], vec[1] = -vec[1], vec[0] return vec } // Rotate90DegRight rotates the vector 90 degrees right (clockwise). +// See also function Normal. func (vec *T) Rotate90DegRight() *T { - temp := vec[0] - vec[0] = vec[1] - vec[1] = -temp + vec[0], vec[1] = vec[1], -vec[0] return vec } +// Sinus returns the sinus value of the (shortest/smallest) angle between the two vectors a and b. +// The returned sine value is in the range -1.0 ≤ value ≤ 1.0. +// The angle is always considered to be in the range 0 to Pi radians and thus the sine value returned is always positive. +func Sinus(a, b *T) float32 { + v := Cross(a, b) / math.Sqrt(a.LengthSqr()*b.LengthSqr()) + + if v > 1.0 { + return 1.0 + } else if v < -1.0 { + return -1.0 + } + return v +} + +// Cosine returns the cosine value of the angle between the two vectors. +// The returned cosine value is in the range -1.0 ≤ value ≤ 1.0. +func Cosine(a, b *T) float32 { + v := Dot(a, b) / math.Sqrt(a.LengthSqr()*b.LengthSqr()) + + if v > 1.0 { + return 1.0 + } else if v < -1.0 { + return -1.0 + } + return v +} + // Angle returns the counter-clockwise angle of the vector from the x axis. func (vec *T) Angle() float32 { return math.Atan2(vec[1], vec[0]) @@ -258,36 +290,30 @@ func Dot(a, b *T) float32 { return a[0]*b[0] + a[1]*b[1] } -// Cross returns the cross product of two vectors. -func Cross(a, b *T) T { - return T{ - a[1]*b[0] - a[0]*b[1], - a[0]*b[1] - a[1]*b[0], - } +// Cross returns the "cross product" of two vectors. +// In 2D space it is a scalar value. +// It is the same as the determinant value of the 2D matrix constructed by the two vectors. +// Cross product in 2D is not well-defined but this is the implementation stated at https://mathworld.wolfram.com/CrossProduct.html . +func Cross(a, b *T) float32 { + return a[0]*b[1] - a[1]*b[0] } -// Angle returns the angle between two vectors. +// Angle returns the angle value of the (shortest/smallest) angle between the two vectors a and b. +// The returned value is in the range 0 ≤ angle ≤ Pi radians. func Angle(a, b *T) float32 { - v := Dot(a, b) / (a.Length() * b.Length()) - // prevent NaN - if v > 1. { - v = v - 2 - } else if v < -1. { - v = v + 2 - } - return math.Acos(v) + return math.Acos(Cosine(a, b)) } // IsLeftWinding returns if the angle from a to b is left winding. +// Two parallell or anti parallell vectors will give a false result. func IsLeftWinding(a, b *T) bool { - ab := b.Rotated(-a.Angle()) - return ab.Angle() > 0 + return Cross(a, b) > 0 // It's really the sign changing part of the Sinus(a, b) function } // IsRightWinding returns if the angle from a to b is right winding. +// Two parallell or anti parallell vectors will give a false result. func IsRightWinding(a, b *T) bool { - ab := b.Rotated(-a.Angle()) - return ab.Angle() < 0 + return Cross(a, b) < 0 // It's really the sign changing part of the Sinus(a, b) function } // Min returns the component wise minimum of two vectors. @@ -316,7 +342,7 @@ func Max(a, b *T) T { // Interpolate interpolates between a and b at t (0,1). func Interpolate(a, b *T, t float32) T { - t1 := 1 - t + t1 := 1.0 - t return T{ a[0]*t1 + b[0]*t, a[1]*t1 + b[1]*t, diff --git a/vec2/vec2_test.go b/vec2/vec2_test.go index 3c26581..c3408a5 100644 --- a/vec2/vec2_test.go +++ b/vec2/vec2_test.go @@ -1,8 +1,12 @@ package vec2 import ( - "math" + "strconv" "testing" + + math64 "math" + + math "github.com/ungerik/go3d/fmath" ) func TestAbs(t *testing.T) { @@ -214,3 +218,126 @@ func TestMuled(t *testing.T) { t.Fail() } } + +func TestAngle(t *testing.T) { + radFor45deg := float32(math.Pi / 4.0) + testSetups := []struct { + a, b T + expectedAngle float32 + name string + }{ + {a: T{1, 0}, b: T{1, 0}, expectedAngle: 0 * radFor45deg, name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0}, b: T{1, 1}, expectedAngle: 1 * radFor45deg, name: "45 degree angle"}, + {a: T{1, 0}, b: T{0, 1}, expectedAngle: 2 * radFor45deg, name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{-1, 1}, expectedAngle: 3 * radFor45deg, name: "135 degree angle"}, + {a: T{1, 0}, b: T{-1, 0}, expectedAngle: 4 * radFor45deg, name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0}, b: T{-1, -1}, expectedAngle: (8 - 5) * radFor45deg, name: "225 degree angle"}, + {a: T{1, 0}, b: T{0, -1}, expectedAngle: (8 - 6) * radFor45deg, name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{1, -1}, expectedAngle: (8 - 7) * radFor45deg, name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + angle := Angle(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(angle, testSetup.expectedAngle, 0.00000001) { + t.Errorf("Angle expected to be %f but was %f for test \"%s\".", testSetup.expectedAngle, angle, testSetup.name) + } + }) + } +} + +func TestCosine(t *testing.T) { + radFor45deg := float32(math.Pi / 4.0) + testSetups := []struct { + a, b T + expectedCosine float32 + name string + }{ + {a: T{1, 0}, b: T{1, 0}, expectedCosine: math.Cos(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0}, b: T{1, 1}, expectedCosine: math.Cos(1 * radFor45deg), name: "45 degree angle"}, + {a: T{1, 0}, b: T{0, 1}, expectedCosine: math.Cos(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{-1, 1}, expectedCosine: math.Cos(3 * radFor45deg), name: "135 degree angle"}, + {a: T{1, 0}, b: T{-1, 0}, expectedCosine: math.Cos(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0}, b: T{-1, -1}, expectedCosine: math.Cos(5 * radFor45deg), name: "225 degree angle"}, + {a: T{1, 0}, b: T{0, -1}, expectedCosine: math.Cos(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{1, -1}, expectedCosine: math.Cos(7 * radFor45deg), name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + cos := Cosine(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(cos, testSetup.expectedCosine, 0.000001) { + t.Errorf("Cosine expected to be %f but was %f for test \"%s\".", testSetup.expectedCosine, cos, testSetup.name) + } + }) + } +} + +func TestSinus(t *testing.T) { + radFor45deg := float32(math.Pi / 4.0) + testSetups := []struct { + a, b T + expectedSine float32 + name string + }{ + {a: T{1, 0}, b: T{1, 0}, expectedSine: math.Sin(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0}, b: T{1, 1}, expectedSine: math.Sin(1 * radFor45deg), name: "45 degree angle"}, + {a: T{1, 0}, b: T{0, 1}, expectedSine: math.Sin(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{-1, 1}, expectedSine: math.Sin(3 * radFor45deg), name: "135 degree angle"}, + {a: T{1, 0}, b: T{-1, 0}, expectedSine: math.Sin(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0}, b: T{-1, -1}, expectedSine: math.Sin(5 * radFor45deg), name: "225 degree angle"}, + {a: T{1, 0}, b: T{0, -1}, expectedSine: math.Sin(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0}, b: T{1, -1}, expectedSine: math.Sin(7 * radFor45deg), name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + sin := Sinus(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(sin, testSetup.expectedSine, 0.000001) { + t.Errorf("Sine expected to be %f but was %f for test \"%s\".", testSetup.expectedSine, sin, testSetup.name) + } + }) + } +} + +func TestLeftRightWinding(t *testing.T) { + a := T{1.0, 0.0} + + for angle := 0; angle <= 360; angle += 15 { + rad := (math64.Pi / 180.0) * float64(angle) + + bx := float32(clampDecimals(math64.Cos(rad), 4)) + by := float32(clampDecimals(math64.Sin(rad), 4)) + b := T{bx, by} + + t.Run("left winding angle "+strconv.Itoa(angle), func(t *testing.T) { + lw := IsLeftWinding(&a, &b) + rw := IsRightWinding(&a, &b) + + if angle%180 == 0 { + // No winding at 0, 180 and 360 degrees + if lw || rw { + t.Errorf("Neither left or right winding should be true on angle %d. Left winding=%t, right winding=%t", angle, lw, rw) + } + } else if angle < 180 { + // Left winding at 0 < angle < 180 + if !lw || rw { + t.Errorf("Left winding should be true (not right winding) on angle %d. Left winding=%t, right winding=%t", angle, lw, rw) + } + } else if angle > 180 { + // Right winding at 180 < angle < 360 + if lw || !rw { + t.Errorf("Right winding should be true (not left winding) on angle %d. Left winding=%t, right winding=%t", angle, lw, rw) + } + } + }) + } +} + +func clampDecimals(decimalValue float64, amountDecimals float64) float64 { + factor := math64.Pow(10, amountDecimals) + return math64.Round(decimalValue*factor) / factor +} diff --git a/vec3/vec3.go b/vec3/vec3.go index 577503a..52fd030 100644 --- a/vec3/vec3.go +++ b/vec3/vec3.go @@ -126,6 +126,11 @@ func (vec *T) PracticallyEquals(compareVector *T, allowedDelta float32) bool { (math.Abs(vec[2]-compareVector[2]) <= allowedDelta) } +// PracticallyEquals compares two values if they are equal with each other within a delta tolerance. +func PracticallyEquals(v1, v2, allowedDelta float32) bool { + return math.Abs(v1-v2) <= allowedDelta +} + // Invert inverts the vector. func (vec *T) Invert() *T { vec[0] = -vec[0] @@ -257,16 +262,38 @@ func Cross(a, b *T) T { } } -// Angle returns the angle between two vectors. -func Angle(a, b *T) float32 { - v := Dot(a, b) / (a.Length() * b.Length()) - // prevent NaN - if v > 1. { - return 0 - } else if v < -1. { - return math.Pi +// Sinus returns the sinus value of the (shortest/smallest) angle between the two vectors a and b. +// The returned sine value is in the range 0.0 ≤ value ≤ 1.0. +// The angle is always considered to be in the range 0 to Pi radians and thus the sine value returned is always positive. +func Sinus(a, b *T) float32 { + cross := Cross(a, b) + v := cross.Length() / math.Sqrt(a.LengthSqr()*b.LengthSqr()) + + if v > 1.0 { + return 1.0 + } else if v < 0.0 { + return 0.0 } - return math.Acos(v) + return v +} + +// Cosine returns the cosine value of the angle between the two vectors. +// The returned cosine value is in the range -1.0 ≤ value ≤ 1.0. +func Cosine(a, b *T) float32 { + v := Dot(a, b) / math.Sqrt(a.LengthSqr()*b.LengthSqr()) + + if v > 1.0 { + return 1.0 + } else if v < -1.0 { + return -1.0 + } + return v +} + +// Angle returns the angle value of the (shortest/smallest) angle between the two vectors a and b. +// The returned value is in the range 0 ≤ angle ≤ Pi radians. +func Angle(a, b *T) float32 { + return math.Acos(Cosine(a, b)) } // Min returns the component wise minimum of two vectors. diff --git a/vec3/vec3_test.go b/vec3/vec3_test.go index 97deae7..3a62d2a 100644 --- a/vec3/vec3_test.go +++ b/vec3/vec3_test.go @@ -1,6 +1,8 @@ package vec3 import ( + math "github.com/ungerik/go3d/fmath" + "testing" ) @@ -142,3 +144,87 @@ func TestMuled(t *testing.T) { t.Fail() } } + +func TestAngle(t *testing.T) { + radFor45deg := float32(math.Pi / 4.0) + testSetups := []struct { + a, b T + expectedAngle float32 + name string + }{ + {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedAngle: 0 * radFor45deg, name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedAngle: 1 * radFor45deg, name: "45 degree angle"}, + {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedAngle: 2 * radFor45deg, name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedAngle: 3 * radFor45deg, name: "135 degree angle"}, + {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedAngle: 4 * radFor45deg, name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedAngle: (8 - 5) * radFor45deg, name: "225 degree angle"}, + {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedAngle: (8 - 6) * radFor45deg, name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedAngle: (8 - 7) * radFor45deg, name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + angle := Angle(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(angle, testSetup.expectedAngle, 0.00000001) { + t.Errorf("Angle expected to be %f but was %f for test \"%s\".", testSetup.expectedAngle, angle, testSetup.name) + } + }) + } +} + +func TestCosine(t *testing.T) { + radFor45deg := float32(math.Pi / 4.0) + testSetups := []struct { + a, b T + expectedCosine float32 + name string + }{ + {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedCosine: math.Cos(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedCosine: math.Cos(1 * radFor45deg), name: "45 degree angle"}, + {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedCosine: math.Cos(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedCosine: math.Cos(3 * radFor45deg), name: "135 degree angle"}, + {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedCosine: math.Cos(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedCosine: math.Cos(5 * radFor45deg), name: "225 degree angle"}, + {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedCosine: math.Cos(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedCosine: math.Cos(7 * radFor45deg), name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + cos := Cosine(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(cos, testSetup.expectedCosine, 0.000001) { + t.Errorf("Cosine expected to be %f but was %f for test \"%s\".", testSetup.expectedCosine, cos, testSetup.name) + } + }) + } +} + +func TestSinus(t *testing.T) { + radFor45deg := float32(math.Pi / 4.0) + testSetups := []struct { + a, b T + expectedSine float32 + name string + }{ + {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedSine: math.Sin(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, + {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedSine: math.Sin(1 * radFor45deg), name: "45 degree angle"}, + {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedSine: math.Sin(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedSine: math.Sin(3 * radFor45deg), name: "135 degree angle"}, + {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedSine: math.Sin(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, + {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedSine: math.Abs(math.Sin(5 * radFor45deg)), name: "225 degree angle"}, + {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedSine: math.Abs(math.Sin(6 * radFor45deg)), name: "270 degree angle, orthogonal vectors"}, + {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedSine: math.Abs(math.Sin(7 * radFor45deg)), name: "315 degree angle"}, + } + + for _, testSetup := range testSetups { + t.Run(testSetup.name, func(t *testing.T) { + sin := Sinus(&testSetup.a, &testSetup.b) + + if !PracticallyEquals(sin, testSetup.expectedSine, 0.000001) { + t.Errorf("Sine expected to be %f but was %f for test \"%s\".", testSetup.expectedSine, sin, testSetup.name) + } + }) + } +} From 6c578d616bacc1d848b800e2cef4176ffd7e7d8c Mon Sep 17 00:00:00 2001 From: Erik Unger <617459+ungerik@users.noreply.github.com> Date: Wed, 1 May 2024 11:37:52 +0200 Subject: [PATCH 2/4] doc fix --- float64/vec3/vec3.go | 2 +- float64/vec4/vec4.go | 2 +- vec3/vec3.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/float64/vec3/vec3.go b/float64/vec3/vec3.go index 32df455..ad8e9b1 100644 --- a/float64/vec3/vec3.go +++ b/float64/vec3/vec3.go @@ -25,7 +25,7 @@ var ( Red = T{1, 0, 0} // Green holds the color green. Green = T{0, 1, 0} - // Blue holds the color black. + // Blue holds the color blue. Blue = T{0, 0, 1} // Black holds the color black. Black = T{0, 0, 0} diff --git a/float64/vec4/vec4.go b/float64/vec4/vec4.go index 2e9b4e0..0d114ff 100644 --- a/float64/vec4/vec4.go +++ b/float64/vec4/vec4.go @@ -28,7 +28,7 @@ var ( Red = T{1, 0, 0, 1} // Green holds the color green. Green = T{0, 1, 0, 1} - // Black holds the color black. + // Blue holds the color blue. Blue = T{0, 0, 1, 1} // Black holds the color black. Black = T{0, 0, 0, 1} diff --git a/vec3/vec3.go b/vec3/vec3.go index 52fd030..4c59159 100644 --- a/vec3/vec3.go +++ b/vec3/vec3.go @@ -25,7 +25,7 @@ var ( Red = T{1, 0, 0} // Green holds the color green. Green = T{0, 1, 0} - // Blue holds the color black. + // Blue holds the color blue. Blue = T{0, 0, 1} // Black holds the color black. Black = T{0, 0, 0} From f69a36fa71ff1a2a31b5c155cae77e6f14f75f9d Mon Sep 17 00:00:00 2001 From: Erik Unger Date: Wed, 1 May 2024 11:58:52 +0200 Subject: [PATCH 3/4] rename fmath.Sqrtf to Sqrt and fix build for non amd64 --- fmath/README | 4 ++-- fmath/doc.go | 5 +++-- fmath/{sqrtf.go => sqrt.go} | 5 +++-- fmath/{sqrtf_amd64.s => sqrt_amd64.s} | 4 ++-- fmath/{sqrtf_decl.go => sqrt_decl.go} | 6 +++--- fmath/sqrt_test.go | 24 ++++++++++++++++++++++++ fmath/sqrtf_test.go | 24 ------------------------ 7 files changed, 37 insertions(+), 35 deletions(-) rename fmath/{sqrtf.go => sqrt.go} (88%) rename fmath/{sqrtf_amd64.s => sqrt_amd64.s} (78%) rename fmath/{sqrtf_decl.go => sqrt_decl.go} (79%) create mode 100644 fmath/sqrt_test.go delete mode 100644 fmath/sqrtf_test.go diff --git a/fmath/README b/fmath/README index 4b58f5d..4c6f3e5 100644 --- a/fmath/README +++ b/fmath/README @@ -1,8 +1,8 @@ 32-bit floating point math library for GO. This library provides float32 counterparts for Go's float64 math functions. -E.g.: Sqrtf(x float32) float32. +E.g.: Sqrt(x float32) float32. -The implementation partially uses assembly code (see, e.g., sqrtf_amd64.s) for fast computation. However, when no assembly implementation exists yet a generic implementation is used which uses Go's float64 math functions underneath. +The implementation partially uses assembly code (see, e.g., sqrt_amd64.s) for fast computation. However, when no assembly implementation exists yet a generic implementation is used which uses Go's float64 math functions underneath. Note: the assembly code is not goinstallable on all platforms. Therefore, goinstall will compile the portable implementation. If you manually execute "make install", you will get the faster implementation. diff --git a/fmath/doc.go b/fmath/doc.go index 95027ea..89fe200 100644 --- a/fmath/doc.go +++ b/fmath/doc.go @@ -5,9 +5,10 @@ // float32 math package. // // This library provides float32 counterparts for Go's float64 math functions. E.g.: -// Sqrtf(x float32) float32. // -// The implementation partially uses assembly code (see, e.g., sqrtf_amd64.s) for fast computation. However, when no assembly implementation exists yet a generic implementation is used which uses Go's float64 math functions underneath. +// Sqrt(x float32) float32. +// +// The implementation partially uses assembly code (see, e.g., sqrt_amd64.s) for fast computation. However, when no assembly implementation exists yet a generic implementation is used which uses Go's float64 math functions underneath. // // Note: the assembly code is not goinstallable on all platforms. Therefore, goinstall will compile the portable implementation. If you manually execute "make install", you will get the faster implementation. package fmath diff --git a/fmath/sqrtf.go b/fmath/sqrt.go similarity index 88% rename from fmath/sqrtf.go rename to fmath/sqrt.go index 68396da..b2b8255 100644 --- a/fmath/sqrtf.go +++ b/fmath/sqrt.go @@ -1,3 +1,5 @@ +//go:build !amd64 + // Copyright 2011 Arne Vansteenkiste. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. @@ -5,12 +7,11 @@ // This file provides a trivial implementation based on // Go's float64 math library. It may be overridden by an // assembly implementation when available for the platform. - package fmath import "math" -// float32 version of math.Sqrt +// Sqrt is a float32 version of math.Sqrt func Sqrt(x float32) float32 { return float32(math.Sqrt(float64(x))) } diff --git a/fmath/sqrtf_amd64.s b/fmath/sqrt_amd64.s similarity index 78% rename from fmath/sqrtf_amd64.s rename to fmath/sqrt_amd64.s index bb66b18..220a5fc 100644 --- a/fmath/sqrtf_amd64.s +++ b/fmath/sqrt_amd64.s @@ -2,8 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -// func Sqrtf(x float32) float32 -TEXT ·Sqrtf+0(SB),$0-16 +// func Sqrt(x float32) float32 +TEXT ·Sqrt+0(SB),$0-16 SQRTSS x+0(FP), X0 MOVSS X0,r+8(FP) RET diff --git a/fmath/sqrtf_decl.go b/fmath/sqrt_decl.go similarity index 79% rename from fmath/sqrtf_decl.go rename to fmath/sqrt_decl.go index e4dd6d6..b0fcd60 100644 --- a/fmath/sqrtf_decl.go +++ b/fmath/sqrt_decl.go @@ -1,8 +1,8 @@ -// +build amd64 +//go:build amd64 + // Copyright 2011 Arne Vansteenkiste. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. - package fmath -func Sqrtf(x float32) float32 +func Sqrt(x float32) float32 diff --git a/fmath/sqrt_test.go b/fmath/sqrt_test.go new file mode 100644 index 0000000..41286e1 --- /dev/null +++ b/fmath/sqrt_test.go @@ -0,0 +1,24 @@ +package fmath + +import ( + "testing" +) + +func TestSqrt(t *testing.T) { + f := Sqrt(2) + if f != 1.41421356237 { + t.Fatal("Sqrt(2):", f) + } +} + +func BenchmarkSqrtAssembly(b *testing.B) { + for i := 0; i < b.N; i++ { + Sqrt(2) + } +} + +func BenchmarkSqrtGo(b *testing.B) { + for i := 0; i < b.N; i++ { + Sqrt(2) + } +} diff --git a/fmath/sqrtf_test.go b/fmath/sqrtf_test.go deleted file mode 100644 index 25a0e35..0000000 --- a/fmath/sqrtf_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package fmath - -import ( - "testing" -) - -func TestSqrtf(t *testing.T) { - f := Sqrtf(2) - if f != 1.41421356237 { - t.Fatal("Sqrtf(2):", f) - } -} - -func BenchmarkSqrtfAssembly(b *testing.B) { - for i := 0; i < b.N; i++ { - Sqrtf(2) - } -} - -func BenchmarkSqrtfGo(b *testing.B) { - for i := 0; i < b.N; i++ { - Sqrt(2) - } -} \ No newline at end of file From 94677f08315bc68380d4e979aa1cbaaeb0326b41 Mon Sep 17 00:00:00 2001 From: christian Date: Thu, 2 May 2024 09:30:24 +0200 Subject: [PATCH 4/4] fix quaternion tests. They were failing due to precision errors in float values. --- float64/quaternion/quaternion_test.go | 15 ++++++++------- quaternion/quaternion_test.go | 15 ++++++++------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/float64/quaternion/quaternion_test.go b/float64/quaternion/quaternion_test.go index 3081e0c..3bd3c8d 100644 --- a/float64/quaternion/quaternion_test.go +++ b/float64/quaternion/quaternion_test.go @@ -41,7 +41,8 @@ func TestQuaternionRotateVec3(t *testing.T) { vecd := q.RotatedVec3(&vec) magSqr2 := vec_r1.LengthSqr() - if vecd != vec_r1 { + if !vecd.PracticallyEquals(&vec_r1, 0.000000000000001) { + t.Logf("test case %v rotates %v failed - vector rotation: %+v, %+v\n", eularAngle, vec, vecd, vec_r1) t.Fail() } @@ -49,16 +50,16 @@ func TestQuaternionRotateVec3(t *testing.T) { length := math.Abs(magSqr - magSqr2) if angle > 0.0000001 { - fmt.Printf("test case %v rotates %v failed - angle difference to large\n", eularAngle, vec) - fmt.Println("vectors:", vec_r1, vec_r2) - fmt.Println("angle:", angle) + t.Logf("test case %v rotates %v failed - angle difference to large\n", eularAngle, vec) + t.Logf("vectors: %+v, %+v\n", vec_r1, vec_r2) + t.Logf("angle: %v\n", angle) t.Fail() } if length > 0.000000000001 { - fmt.Printf("test case %v rotates %v failed - squared length difference to large\n", eularAngle, vec) - fmt.Println("vectors:", vec_r1, vec_r2) - fmt.Println("squared lengths:", magSqr, magSqr2) + t.Logf("test case %v rotates %v failed - squared length difference to large\n", eularAngle, vec) + t.Logf("vectors: %+v %+v\n", vec_r1, vec_r2) + t.Logf("squared lengths: %v, %v\n", magSqr, magSqr2) t.Fail() } }() diff --git a/quaternion/quaternion_test.go b/quaternion/quaternion_test.go index c69ba60..28d1f89 100644 --- a/quaternion/quaternion_test.go +++ b/quaternion/quaternion_test.go @@ -41,7 +41,8 @@ func TestQuaternionRotateVec3(t *testing.T) { vecd := q.RotatedVec3(&vec) magSqr2 := vec_r1.LengthSqr() - if vecd != vec_r1 { + if !vecd.PracticallyEquals(&vec_r1, 0.000001) { + t.Logf("test case %v rotates %v failed - vector rotation: %+v, %+v\n", eularAngle, vec, vecd, vec_r1) t.Fail() } @@ -49,16 +50,16 @@ func TestQuaternionRotateVec3(t *testing.T) { length := math.Abs(magSqr - magSqr2) if angle > 0.001 { - fmt.Printf("test case %v rotates %v failed - angle difference to large\n", eularAngle, vec) - fmt.Println("vectors:", vec_r1, vec_r2) - fmt.Println("angle:", angle) + t.Logf("test case %v rotates %v failed - angle difference to large\n", eularAngle, vec) + t.Logf("vectors: %+v, %+v\n", vec_r1, vec_r2) + t.Logf("angle: %v\n", angle) t.Fail() } if length > 0.0001 { - fmt.Printf("test case %v rotates %v failed - squared length difference to large\n", eularAngle, vec) - fmt.Println("vectors:", vec_r1, vec_r2) - fmt.Println("squared lengths:", magSqr, magSqr2) + t.Logf("test case %v rotates %v failed - squared length difference to large\n", eularAngle, vec) + t.Logf("vectors: %+v %+v\n", vec_r1, vec_r2) + t.Logf("squared lengths: %v, %v\n", magSqr, magSqr2) t.Fail() } }()