├── codelingo.yaml ├── go.mod ├── .gitignore ├── go.sum ├── mat2 ├── mat2_safe.go ├── mat2_unsafe.go ├── mat2_test.go └── mat2.go ├── float64 ├── mat2 │ ├── mat2_safe.go │ ├── mat2_unsafe.go │ ├── mat2_test.go │ └── mat2.go ├── mat3 │ ├── mat3_unsafe.go │ ├── mat3_safe.go │ └── mat3_test.go ├── mat4 │ ├── mat4_unsafe.go │ ├── mat4_safe.go │ └── mat4_test.go ├── vec2 │ ├── config.go │ ├── rect.go │ └── vec2_test.go ├── vec3 │ ├── config.go │ ├── box.go │ ├── vec3_test.go │ └── vec3.go ├── quaternion │ ├── config.go │ ├── quaternion_test.go │ └── quaternion.go ├── generic │ └── generic.go ├── vec4 │ └── vec4_test.go ├── bezier2 │ ├── bezier2_test.go │ └── bezier2.go ├── qbezier2 │ ├── qbezier2_test.go │ └── qbezier2.go ├── hermit2 │ ├── hermit2.go │ └── hermit2_test.go └── hermit3 │ ├── hermit3.go │ └── hermit3_test.go ├── mat3 ├── mat3_unsafe.go ├── mat3_safe.go └── mat3_test.go ├── mat4 ├── mat4_unsafe.go ├── mat4_safe.go └── mat4_test.go ├── vec2 ├── config.go └── rect.go ├── vec3 ├── config.go ├── box.go ├── vec3_test.go └── vec3.go ├── quaternion ├── config.go └── quaternion.go ├── .github └── workflows │ ├── gosec.yml │ └── codeql-analysis.yml ├── generic └── generic.go ├── vec4 └── vec4_test.go ├── LICENSE ├── bezier2 ├── bezier2_test.go └── bezier2.go ├── qbezier2 ├── qbezier2_test.go └── qbezier2.go ├── doc.go ├── hermit2 ├── hermit2.go └── hermit2_test.go └── hermit3 ├── hermit3.go └── hermit3_test.go /codelingo.yaml: -------------------------------------------------------------------------------- 1 | tenets: 2 | - import: codelingo/effective-go/comment-first-word-as-subject -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ungerik/go3d 2 | 3 | go 1.23 4 | 5 | require github.com/chewxy/math32 v1.11.1 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.6 2 | *.8 3 | *.o 4 | *.so 5 | *.out 6 | *.go~ 7 | *.cgo?.* 8 | _cgo_* 9 | _obj 10 | _test 11 | 12 | .DS_Store 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/chewxy/math32 v1.11.1 h1:b7PGHlp8KjylDoU8RrcEsRuGZhJuz8haxnKfuMMRqy8= 2 | github.com/chewxy/math32 v1.11.1/go.mod h1:dOB2rcuFrCn6UHrze36WSLVPKtzPMRAQvBvUwkSsLqs= 3 | -------------------------------------------------------------------------------- /mat2/mat2_safe.go: -------------------------------------------------------------------------------- 1 | //go:build netgo 2 | 3 | package mat2 4 | 5 | // Array returns the elements of the matrix as array pointer. 6 | // The data may be a copy depending on the platform implementation. 7 | func (mat *T) Array() *[4]float32 { 8 | return &[...]float32{ 9 | mat[0][0], mat[0][1], 10 | mat[1][0], mat[1][1], 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /float64/mat2/mat2_safe.go: -------------------------------------------------------------------------------- 1 | //go:build netgo 2 | 3 | package mat2 4 | 5 | // Array returns the elements of the matrix as array pointer. 6 | // The data may be a copy depending on the platform implementation. 7 | func (mat *T) Array() *[4]float64 { 8 | return &[...]float64{ 9 | mat[0][0], mat[0][1], 10 | mat[1][0], mat[1][1], 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /mat2/mat2_unsafe.go: -------------------------------------------------------------------------------- 1 | //go:build !netgo 2 | 3 | package mat2 4 | 5 | import "unsafe" 6 | 7 | // Array returns the elements of the matrix as array pointer. 8 | // The data may be a copy depending on the platform implementation. 9 | func (mat *T) Array() *[4]float32 { 10 | return (*[4]float32)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK 11 | } 12 | -------------------------------------------------------------------------------- /mat3/mat3_unsafe.go: -------------------------------------------------------------------------------- 1 | //go:build !netgo 2 | 3 | package mat3 4 | 5 | import "unsafe" 6 | 7 | // Array returns the elements of the matrix as array pointer. 8 | // The data may be a copy depending on the platform implementation. 9 | func (mat *T) Array() *[9]float32 { 10 | return (*[9]float32)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK 11 | } 12 | -------------------------------------------------------------------------------- /mat4/mat4_unsafe.go: -------------------------------------------------------------------------------- 1 | //go:build !netgo 2 | 3 | package mat4 4 | 5 | import "unsafe" 6 | 7 | // Array returns the elements of the matrix as array pointer. 8 | // The data may be a copy depending on the platform implementation. 9 | func (mat *T) Array() *[16]float32 { 10 | return (*[16]float32)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK 11 | } 12 | -------------------------------------------------------------------------------- /float64/mat2/mat2_unsafe.go: -------------------------------------------------------------------------------- 1 | //go:build !netgo 2 | 3 | package mat2 4 | 5 | import "unsafe" 6 | 7 | // Array returns the elements of the matrix as array pointer. 8 | // The data may be a copy depending on the platform implementation. 9 | func (mat *T) Array() *[4]float64 { 10 | return (*[4]float64)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK 11 | } 12 | -------------------------------------------------------------------------------- /float64/mat3/mat3_unsafe.go: -------------------------------------------------------------------------------- 1 | //go:build !netgo 2 | 3 | package mat3 4 | 5 | import "unsafe" 6 | 7 | // Array returns the elements of the matrix as array pointer. 8 | // The data may be a copy depending on the platform implementation. 9 | func (mat *T) Array() *[9]float64 { 10 | return (*[9]float64)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK 11 | } 12 | -------------------------------------------------------------------------------- /float64/mat4/mat4_unsafe.go: -------------------------------------------------------------------------------- 1 | //go:build !netgo 2 | 3 | package mat4 4 | 5 | import "unsafe" 6 | 7 | // Array returns the elements of the matrix as array pointer. 8 | // The data may be a copy depending on the platform implementation. 9 | func (mat *T) Array() *[16]float64 { 10 | return (*[16]float64)(unsafe.Pointer(mat)) //#nosec G103 -- unsafe OK 11 | } 12 | -------------------------------------------------------------------------------- /vec2/config.go: -------------------------------------------------------------------------------- 1 | package vec2 2 | 3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons. 4 | // It is used by Normalize() and other methods to determine if a vector is effectively 5 | // zero or already normalized. Can be adjusted for specific use cases. 6 | // Default: 1e-8 for float32 precision. 7 | var Epsilon float32 = 1e-8 8 | -------------------------------------------------------------------------------- /float64/vec2/config.go: -------------------------------------------------------------------------------- 1 | package vec2 2 | 3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons. 4 | // It is used by Normalize() and other methods to determine if a vector is effectively 5 | // zero or already normalized. Can be adjusted for specific use cases. 6 | // Default: 1e-14 for float64 precision. 7 | var Epsilon float64 = 1e-14 8 | -------------------------------------------------------------------------------- /vec3/config.go: -------------------------------------------------------------------------------- 1 | package vec3 2 | 3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons. 4 | // It is used by Normalize(), Normal(), and other methods to determine if a vector is 5 | // effectively zero or already normalized. Can be adjusted for specific use cases. 6 | // Default: 1e-8 for float32 precision. 7 | var Epsilon float32 = 1e-8 8 | -------------------------------------------------------------------------------- /float64/vec3/config.go: -------------------------------------------------------------------------------- 1 | package vec3 2 | 3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons. 4 | // It is used by Normalize(), Normal(), and other methods to determine if a vector is 5 | // effectively zero or already normalized. Can be adjusted for specific use cases. 6 | // Default: 1e-14 for float64 precision. 7 | var Epsilon float64 = 1e-14 8 | -------------------------------------------------------------------------------- /quaternion/config.go: -------------------------------------------------------------------------------- 1 | package quaternion 2 | 3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons. 4 | // It is used by Normalize() and Normalized() methods to determine if a quaternion is 5 | // effectively zero or already normalized. Can be adjusted for specific use cases. 6 | // Default: 1e-8 for float32 precision. 7 | var Epsilon float32 = 1e-8 8 | -------------------------------------------------------------------------------- /float64/quaternion/config.go: -------------------------------------------------------------------------------- 1 | package quaternion 2 | 3 | // Epsilon is the tolerance used for numerical stability in floating-point comparisons. 4 | // It is used by Normalize() and Normalized() methods to determine if a quaternion is 5 | // effectively zero or already normalized. Can be adjusted for specific use cases. 6 | // Default: 1e-14 for float64 precision. 7 | var Epsilon float64 = 1e-14 8 | -------------------------------------------------------------------------------- /mat3/mat3_safe.go: -------------------------------------------------------------------------------- 1 | //go:build netgo 2 | 3 | package mat3 4 | 5 | // Array returns the elements of the matrix as array pointer. 6 | // The data may be a copy depending on the platform implementation. 7 | func (mat *T) Array() *[9]float32 { 8 | return &[...]float32{ 9 | mat[0][0], mat[0][1], mat[0][2], 10 | mat[1][0], mat[1][1], mat[1][2], 11 | mat[2][0], mat[2][1], mat[2][2], 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /float64/mat3/mat3_safe.go: -------------------------------------------------------------------------------- 1 | //go:build netgo 2 | 3 | package mat3 4 | 5 | // Array returns the elements of the matrix as array pointer. 6 | // The data may be a copy depending on the platform implementation. 7 | func (mat *T) Array() *[9]float64 { 8 | return &[...]float64{ 9 | mat[0][0], mat[0][1], mat[0][2], 10 | mat[1][0], mat[1][1], mat[1][2], 11 | mat[2][0], mat[2][1], mat[2][2], 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/gosec.yml: -------------------------------------------------------------------------------- 1 | name: Run Gosec 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | tests: 11 | runs-on: ubuntu-latest 12 | env: 13 | GO111MODULE: on 14 | steps: 15 | - name: Checkout Source 16 | uses: actions/checkout@v2 17 | - name: Run Gosec Security Scanner 18 | uses: securego/gosec@master 19 | with: 20 | args: ./... 21 | -------------------------------------------------------------------------------- /mat4/mat4_safe.go: -------------------------------------------------------------------------------- 1 | //go:build netgo 2 | 3 | package mat4 4 | 5 | // Array returns the elements of the matrix as array pointer. 6 | // The data may be a copy depending on the platform implementation. 7 | func (mat *T) Array() *[16]float32 { 8 | return &[...]float32{ 9 | mat[0][0], mat[0][1], mat[0][2], mat[0][3], 10 | mat[1][0], mat[1][1], mat[1][2], mat[1][3], 11 | mat[2][0], mat[2][1], mat[2][2], mat[2][3], 12 | mat[3][0], mat[3][1], mat[3][2], mat[3][3], 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /float64/mat4/mat4_safe.go: -------------------------------------------------------------------------------- 1 | //go:build netgo 2 | 3 | package mat4 4 | 5 | // Array returns the elements of the matrix as array pointer. 6 | // The data may be a copy depending on the platform implementation. 7 | func (mat *T) Array() *[16]float64 { 8 | return &[...]float64{ 9 | mat[0][0], mat[0][1], mat[0][2], mat[0][3], 10 | mat[1][0], mat[1][1], mat[1][2], mat[1][3], 11 | mat[2][0], mat[2][1], mat[2][2], mat[2][3], 12 | mat[3][0], mat[3][1], mat[3][2], mat[3][3], 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /generic/generic.go: -------------------------------------------------------------------------------- 1 | // Package generic contains an interface T that 2 | // that all float32 vector and matrix types implement. 3 | package generic 4 | 5 | // T is an interface that all float32 vector and matrix types implement. 6 | type T interface { 7 | 8 | // Cols returns the number of columns of the vector or matrix. 9 | Cols() int 10 | 11 | // Rows returns the number of rows of the vector or matrix. 12 | Rows() int 13 | 14 | // Size returns the number elements of the vector or matrix. 15 | Size() int 16 | 17 | // Slice returns the elements of the vector or matrix as slice. 18 | Slice() []float32 19 | 20 | // Get returns one element of the vector or matrix. 21 | Get(row, col int) float32 22 | 23 | // IsZero checks if all elements of the vector or matrix are zero. 24 | IsZero() bool 25 | } 26 | -------------------------------------------------------------------------------- /float64/generic/generic.go: -------------------------------------------------------------------------------- 1 | // Package generic contains an interface T that 2 | // that all float64 vector and matrix types implement. 3 | package generic 4 | 5 | // T is an interface that all float64 vector and matrix types implement. 6 | type T interface { 7 | 8 | // Cols returns the number of columns of the vector or matrix. 9 | Cols() int 10 | 11 | // Rows returns the number of rows of the vector or matrix. 12 | Rows() int 13 | 14 | // Size returns the number elements of the vector or matrix. 15 | Size() int 16 | 17 | // Slice returns the elements of the vector or matrix as slice. 18 | Slice() []float64 19 | 20 | // Get returns one element of the vector or matrix. 21 | Get(row, col int) float64 22 | 23 | // IsZero checks if all elements of the vector or matrix are zero. 24 | IsZero() bool 25 | } 26 | -------------------------------------------------------------------------------- /vec4/vec4_test.go: -------------------------------------------------------------------------------- 1 | package vec4 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIsZeroEps(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | vec T 11 | epsilon float32 12 | want bool 13 | }{ 14 | {"exact zero", T{0, 0, 0, 0}, 0.0001, true}, 15 | {"within epsilon", T{0.00001, -0.00001, 0.00001, -0.00001}, 0.0001, true}, 16 | {"at epsilon boundary", T{0.0001, 0.0001, 0.0001, 0.0001}, 0.0001, true}, 17 | {"outside epsilon", T{0.001, 0, 0, 0}, 0.0001, false}, 18 | {"one component outside", T{0.00001, 0.001, 0.00001, 0.00001}, 0.0001, false}, 19 | {"negative outside epsilon", T{-0.001, 0, 0, 0}, 0.0001, false}, 20 | {"large values", T{1.0, 2.0, 3.0, 4.0}, 0.0001, false}, 21 | } 22 | 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | if got := tt.vec.IsZeroEps(tt.epsilon); got != tt.want { 26 | t.Errorf("IsZeroEps() = %v, want %v for vec %v with epsilon %v", got, tt.want, tt.vec, tt.epsilon) 27 | } 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /float64/vec4/vec4_test.go: -------------------------------------------------------------------------------- 1 | package vec4 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestIsZeroEps(t *testing.T) { 8 | tests := []struct { 9 | name string 10 | vec T 11 | epsilon float64 12 | want bool 13 | }{ 14 | {"exact zero", T{0, 0, 0, 0}, 0.0001, true}, 15 | {"within epsilon", T{0.00001, -0.00001, 0.00001, -0.00001}, 0.0001, true}, 16 | {"at epsilon boundary", T{0.0001, 0.0001, 0.0001, 0.0001}, 0.0001, true}, 17 | {"outside epsilon", T{0.001, 0, 0, 0}, 0.0001, false}, 18 | {"one component outside", T{0.00001, 0.001, 0.00001, 0.00001}, 0.0001, false}, 19 | {"negative outside epsilon", T{-0.001, 0, 0, 0}, 0.0001, false}, 20 | {"large values", T{1.0, 2.0, 3.0, 4.0}, 0.0001, false}, 21 | } 22 | 23 | for _, tt := range tests { 24 | t.Run(tt.name, func(t *testing.T) { 25 | if got := tt.vec.IsZeroEps(tt.epsilon); got != tt.want { 26 | t.Errorf("IsZeroEps() = %v, want %v for vec %v with epsilon %v", got, tt.want, tt.vec, tt.epsilon) 27 | } 28 | }) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012-2025 Erik Unger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /vec2/rect.go: -------------------------------------------------------------------------------- 1 | package vec2 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Rect is a coordinate system aligned rectangle defined by a Min and Max vector. 8 | type Rect struct { 9 | Min T 10 | Max T 11 | } 12 | 13 | // ParseRect parses a Rect from a string. See also String() 14 | func ParseRect(s string) (r Rect, err error) { 15 | _, err = fmt.Sscan(s, &r.Min[0], &r.Min[1], &r.Max[0], &r.Max[1]) 16 | return r, err 17 | } 18 | 19 | // String formats Rect as string. See also ParseRect(). 20 | func (rect *Rect) String() string { 21 | return rect.Min.String() + " " + rect.Max.String() 22 | } 23 | 24 | // ContainsPoint returns if a point is contained within the rectangle. 25 | func (rect *Rect) ContainsPoint(p *T) bool { 26 | return p[0] >= rect.Min[0] && p[0] <= rect.Max[0] && 27 | p[1] >= rect.Min[1] && p[1] <= rect.Max[1] 28 | } 29 | 30 | func (rect *Rect) Contains(other *Rect) bool { 31 | return other.Min[0] >= rect.Min[0] && 32 | other.Max[0] <= rect.Max[0] && 33 | other.Min[1] >= rect.Min[1] && 34 | other.Max[1] <= rect.Max[1] 35 | } 36 | 37 | func (rect *Rect) Intersects(other *Rect) bool { 38 | return other.Max[0] >= rect.Min[0] && 39 | other.Min[0] <= rect.Max[0] && 40 | other.Max[1] >= rect.Min[0] && 41 | other.Min[1] <= rect.Max[1] 42 | } 43 | 44 | // func Intersect(a, b *Rect) Rect { 45 | // panic("not implemented") 46 | // } 47 | 48 | // func Join(a, b *Rect) Rect { 49 | // panic("not implemented") 50 | // } 51 | -------------------------------------------------------------------------------- /bezier2/bezier2_test.go: -------------------------------------------------------------------------------- 1 | package bezier2 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ungerik/go3d/vec2" 7 | ) 8 | 9 | func TestPoint(t *testing.T) { 10 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 1}, vec2.T{3, 0}} 11 | if got, want := b.Point(0), (vec2.T{0, 0}); got != want { 12 | t.Errorf("cubic bezier point at t=0 failed, got %v, want %v", got, want) 13 | } 14 | if got, want := b.Point(1), (vec2.T{3, 0}); got != want { 15 | t.Errorf("cubic bezier point at t=1 failed, got %v, want %v", got, want) 16 | } 17 | if got, want := b.Point(0.5), (vec2.T{1.5, 0.75}); got != want { 18 | t.Errorf("cubic bezier point at t=0.5 failed, got %v, want %v", got, want) 19 | } 20 | if got, want := b.Point(0.25), (vec2.T{0.75, 0.5625}); got != want { 21 | t.Errorf("cubic bezier point at t=0.25 failed, got %v, want %v", got, want) 22 | } 23 | if got, want := b.Point(0.75), (vec2.T{2.25, 0.5625}); got != want { 24 | t.Errorf("cubic bezier point at t=0.75 failed, got %v, want %v", got, want) 25 | } 26 | } 27 | 28 | func TestTangent(t *testing.T) { 29 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 1}, vec2.T{3, 0}} 30 | if got, want := b.Tangent(0), (vec2.T{3, 3}); got != want { 31 | t.Errorf("cubic bezier tangent at t=0 failed, got %v, want %v", got, want) 32 | } 33 | if got, want := b.Tangent(1), (vec2.T{3, -3}); got != want { 34 | t.Errorf("cubic bezier tangent at t=1 failed, got %v, want %v", got, want) 35 | } 36 | if got, want := b.Tangent(0.5), (vec2.T{3, 0}); got != want { 37 | t.Errorf("cubic bezier tangent at t=0.5 failed, got %v, want %v", got, want) 38 | } 39 | if got, want := b.Tangent(0.25), (vec2.T{3, 1.5}); got != want { 40 | t.Errorf("cubic bezier tangent at t=0.25 failed, got %v, want %v", got, want) 41 | } 42 | if got, want := b.Tangent(0.75), (vec2.T{3, -1.5}); got != want { 43 | t.Errorf("cubic bezier tangent at t=0.75 failed, got %v, want %v", got, want) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /qbezier2/qbezier2_test.go: -------------------------------------------------------------------------------- 1 | package qbezier2 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ungerik/go3d/vec2" 7 | ) 8 | 9 | func TestPoint(t *testing.T) { 10 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 0}} 11 | if got, want := b.Point(0), (vec2.T{0, 0}); got != want { 12 | t.Errorf("quadratic bezier point at t=0 failed, got %v, want %v", got, want) 13 | } 14 | if got, want := b.Point(1), (vec2.T{2, 0}); got != want { 15 | t.Errorf("quadratic bezier point at t=1 failed, got %v, want %v", got, want) 16 | } 17 | if got, want := b.Point(0.5), (vec2.T{1.0, 0.5}); got != want { 18 | t.Errorf("quadratic bezier point at t=0.5 failed, got %v, want %v", got, want) 19 | } 20 | if got, want := b.Point(0.25), (vec2.T{0.5, 0.375}); got != want { 21 | t.Errorf("quadratic bezier point at t=0.25 failed, got %v, want %v", got, want) 22 | } 23 | if got, want := b.Point(0.75), (vec2.T{1.5, 0.375}); got != want { 24 | t.Errorf("quadratic bezier point at t=0.75 failed, got %v, want %v", got, want) 25 | } 26 | } 27 | 28 | func TestTangent(t *testing.T) { 29 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 0}} 30 | if got, want := b.Tangent(0), (vec2.T{2, 2}); got != want { 31 | t.Errorf("quadratic bezier tangent at t=0 failed, got %v, want %v", got, want) 32 | } 33 | if got, want := b.Tangent(1), (vec2.T{2, -2}); got != want { 34 | t.Errorf("quadratic bezier tangent at t=1 failed, got %v, want %v", got, want) 35 | } 36 | if got, want := b.Tangent(0.5), (vec2.T{2, 0}); got != want { 37 | t.Errorf("quadratic bezier tangent at t=0.5 failed, got %v, want %v", got, want) 38 | } 39 | if got, want := b.Tangent(0.25), (vec2.T{2, 1}); got != want { 40 | t.Errorf("quadratic bezier tangent at t=0.25 failed, got %v, want %v", got, want) 41 | } 42 | if got, want := b.Tangent(0.75), (vec2.T{2, -1}); got != want { 43 | t.Errorf("quadratic bezier tangent at t=0.75 failed, got %v, want %v", got, want) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /float64/bezier2/bezier2_test.go: -------------------------------------------------------------------------------- 1 | package bezier2 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ungerik/go3d/float64/vec2" 7 | ) 8 | 9 | func TestPoint(t *testing.T) { 10 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 1}, vec2.T{3, 0}} 11 | if got, want := b.Point(0), (vec2.T{0, 0}); got != want { 12 | t.Errorf("cubic bezier point at t=0 failed, got %v, want %v", got, want) 13 | } 14 | if got, want := b.Point(1), (vec2.T{3, 0}); got != want { 15 | t.Errorf("cubic bezier point at t=1 failed, got %v, want %v", got, want) 16 | } 17 | if got, want := b.Point(0.5), (vec2.T{1.5, 0.75}); got != want { 18 | t.Errorf("cubic bezier point at t=0.5 failed, got %v, want %v", got, want) 19 | } 20 | if got, want := b.Point(0.25), (vec2.T{0.75, 0.5625}); got != want { 21 | t.Errorf("cubic bezier point at t=0.25 failed, got %v, want %v", got, want) 22 | } 23 | if got, want := b.Point(0.75), (vec2.T{2.25, 0.5625}); got != want { 24 | t.Errorf("cubic bezier point at t=0.75 failed, got %v, want %v", got, want) 25 | } 26 | } 27 | 28 | func TestTangent(t *testing.T) { 29 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 1}, vec2.T{3, 0}} 30 | if got, want := b.Tangent(0), (vec2.T{3, 3}); got != want { 31 | t.Errorf("cubic bezier tangent at t=0 failed, got %v, want %v", got, want) 32 | } 33 | if got, want := b.Tangent(1), (vec2.T{3, -3}); got != want { 34 | t.Errorf("cubic bezier tangent at t=1 failed, got %v, want %v", got, want) 35 | } 36 | if got, want := b.Tangent(0.5), (vec2.T{3, 0}); got != want { 37 | t.Errorf("cubic bezier tangent at t=0.5 failed, got %v, want %v", got, want) 38 | } 39 | if got, want := b.Tangent(0.25), (vec2.T{3, 1.5}); got != want { 40 | t.Errorf("cubic bezier tangent at t=0.25 failed, got %v, want %v", got, want) 41 | } 42 | if got, want := b.Tangent(0.75), (vec2.T{3, -1.5}); got != want { 43 | t.Errorf("cubic bezier tangent at t=0.75 failed, got %v, want %v", got, want) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /float64/qbezier2/qbezier2_test.go: -------------------------------------------------------------------------------- 1 | package qbezier2 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ungerik/go3d/float64/vec2" 7 | ) 8 | 9 | func TestPoint(t *testing.T) { 10 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 0}} 11 | if got, want := b.Point(0), (vec2.T{0, 0}); got != want { 12 | t.Errorf("quadratic bezier point at t=0 failed, got %v, want %v", got, want) 13 | } 14 | if got, want := b.Point(1), (vec2.T{2, 0}); got != want { 15 | t.Errorf("quadratic bezier point at t=1 failed, got %v, want %v", got, want) 16 | } 17 | if got, want := b.Point(0.5), (vec2.T{1.0, 0.5}); got != want { 18 | t.Errorf("quadratic bezier point at t=0.5 failed, got %v, want %v", got, want) 19 | } 20 | if got, want := b.Point(0.25), (vec2.T{0.5, 0.375}); got != want { 21 | t.Errorf("quadratic bezier point at t=0.25 failed, got %v, want %v", got, want) 22 | } 23 | if got, want := b.Point(0.75), (vec2.T{1.5, 0.375}); got != want { 24 | t.Errorf("quadratic bezier point at t=0.75 failed, got %v, want %v", got, want) 25 | } 26 | } 27 | 28 | func TestTangent(t *testing.T) { 29 | b := T{vec2.T{0, 0}, vec2.T{1, 1}, vec2.T{2, 0}} 30 | if got, want := b.Tangent(0), (vec2.T{2, 2}); got != want { 31 | t.Errorf("quadratic bezier tangent at t=0 failed, got %v, want %v", got, want) 32 | } 33 | if got, want := b.Tangent(1), (vec2.T{2, -2}); got != want { 34 | t.Errorf("quadratic bezier tangent at t=1 failed, got %v, want %v", got, want) 35 | } 36 | if got, want := b.Tangent(0.5), (vec2.T{2, 0}); got != want { 37 | t.Errorf("quadratic bezier tangent at t=0.5 failed, got %v, want %v", got, want) 38 | } 39 | if got, want := b.Tangent(0.25), (vec2.T{2, 1}); got != want { 40 | t.Errorf("quadratic bezier tangent at t=0.25 failed, got %v, want %v", got, want) 41 | } 42 | if got, want := b.Tangent(0.75), (vec2.T{2, -1}); got != want { 43 | t.Errorf("quadratic bezier tangent at t=0.75 failed, got %v, want %v", got, want) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /float64/vec2/rect.go: -------------------------------------------------------------------------------- 1 | package vec2 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Rect is a coordinate system aligned rectangle defined by a Min and Max vector. 8 | type Rect struct { 9 | Min T 10 | Max T 11 | } 12 | 13 | // NewRect creates a Rect from two points. 14 | func NewRect(a, b *T) (rect Rect) { 15 | rect.Min = Min(a, b) 16 | rect.Max = Max(a, b) 17 | return rect 18 | } 19 | 20 | // ParseRect parses a Rect from a string. See also String() 21 | func ParseRect(s string) (r Rect, err error) { 22 | _, err = fmt.Sscan(s, &r.Min[0], &r.Min[1], &r.Max[0], &r.Max[1]) 23 | return r, err 24 | } 25 | 26 | // String formats Rect as string. See also ParseRect(). 27 | func (rect *Rect) String() string { 28 | return rect.Min.String() + " " + rect.Max.String() 29 | } 30 | 31 | // ContainsPoint returns if a point is contained within the rectangle. 32 | func (rect *Rect) ContainsPoint(p *T) bool { 33 | return p[0] >= rect.Min[0] && p[0] <= rect.Max[0] && 34 | p[1] >= rect.Min[1] && p[1] <= rect.Max[1] 35 | } 36 | 37 | // Contains returns if other Rect is contained within the rectangle. 38 | func (rect *Rect) Contains(other *Rect) bool { 39 | return rect.Min[0] <= other.Min[0] && 40 | rect.Min[1] <= other.Min[1] && 41 | rect.Max[0] >= other.Max[0] && 42 | rect.Max[1] >= other.Max[1] 43 | } 44 | 45 | // Area calculates the area of the rectangle. 46 | func (rect *Rect) Area() float64 { 47 | return (rect.Max[0] - rect.Min[0]) * (rect.Max[1] - rect.Min[1]) 48 | } 49 | 50 | // func (rect *Rect) Intersects(other *Rect) bool { 51 | // panic("not implemented") 52 | // } 53 | 54 | // func Intersect(a, b *Rect) Rect { 55 | // panic("not implemented") 56 | // } 57 | 58 | // Join enlarges this rectangle to contain also the given rectangle. 59 | func (rect *Rect) Join(other *Rect) { 60 | rect.Min = Min(&rect.Min, &other.Min) 61 | rect.Max = Max(&rect.Max, &other.Max) 62 | } 63 | 64 | // Joined returns the minimal rectangle containing both a and b. 65 | func Joined(a, b *Rect) (rect Rect) { 66 | rect.Min = Min(&a.Min, &b.Min) 67 | rect.Max = Max(&a.Max, &b.Max) 68 | return rect 69 | } 70 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package go3d is a performance oriented vector and matrix math package for 2D and 3D graphics. 3 | 4 | Every type has its own sub-package and is named T. So vec3.T is the 3D vector type. 5 | For every vector and matrix type there is a String() method and a Parse() function. 6 | Besides methods of T there are also functions in the packages, like vec3.Dot(a, b). 7 | 8 | Packages under the float64 directory are using float64 values instead of float32. 9 | 10 | Matrices are organized as arrays of columns which is also the way OpenGL expects matrices. 11 | DirectX expects "arrays of rows" matrices, use the Transpose() to convert. 12 | 13 | Methods that don't return a specific value, return a pointer to the struct to allow method call chaining. 14 | 15 | Example: 16 | 17 | a := vec3.Zero 18 | b := vec3.UnitX 19 | a.Add(&b).Scale(5) 20 | 21 | Method names in the past tense return a copy of the struct instead of a pointer to it. 22 | 23 | Example: 24 | 25 | a := vec3.UnitX 26 | b := a.Scaled(5) // a still equals vec3.UnitX 27 | 28 | Documentation: http://godoc.org/github.com/ungerik/go3d 29 | */ 30 | package go3d 31 | 32 | // Import all sub-packages for build 33 | import ( 34 | _ "github.com/ungerik/go3d/float64/bezier2" 35 | _ "github.com/ungerik/go3d/float64/generic" 36 | _ "github.com/ungerik/go3d/float64/hermit2" 37 | _ "github.com/ungerik/go3d/float64/hermit3" 38 | _ "github.com/ungerik/go3d/float64/mat2" 39 | _ "github.com/ungerik/go3d/float64/mat3" 40 | _ "github.com/ungerik/go3d/float64/mat4" 41 | _ "github.com/ungerik/go3d/float64/qbezier2" 42 | _ "github.com/ungerik/go3d/float64/quaternion" 43 | _ "github.com/ungerik/go3d/float64/vec2" 44 | _ "github.com/ungerik/go3d/float64/vec3" 45 | _ "github.com/ungerik/go3d/float64/vec4" 46 | 47 | _ "github.com/ungerik/go3d/generic" 48 | _ "github.com/ungerik/go3d/hermit2" 49 | _ "github.com/ungerik/go3d/hermit3" 50 | _ "github.com/ungerik/go3d/mat2" 51 | _ "github.com/ungerik/go3d/mat3" 52 | _ "github.com/ungerik/go3d/mat4" 53 | _ "github.com/ungerik/go3d/quaternion" 54 | _ "github.com/ungerik/go3d/vec2" 55 | _ "github.com/ungerik/go3d/vec3" 56 | _ "github.com/ungerik/go3d/vec4" 57 | ) 58 | -------------------------------------------------------------------------------- /vec3/box.go: -------------------------------------------------------------------------------- 1 | package vec3 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Box is a coordinate system aligned 3D box defined by a Min and Max vector. 8 | type Box struct { 9 | Min T 10 | Max T 11 | } 12 | 13 | var ( 14 | // MaxBox holds a box that contains the entire R3 space that can be represented as vec3 15 | MaxBox = Box{MinVal, MaxVal} 16 | ) 17 | 18 | // ParseBox parses a Box from a string. See also String() 19 | func ParseBox(s string) (r Box, err error) { 20 | _, err = fmt.Sscan(s, &r.Min[0], &r.Min[1], &r.Min[2], &r.Max[0], &r.Max[1], &r.Max[2]) 21 | return r, err 22 | } 23 | 24 | // String formats Box as string. See also ParseBox(). 25 | func (box *Box) String() string { 26 | return box.Min.String() + " " + box.Max.String() 27 | } 28 | 29 | // ContainsPoint returns if a point is contained within the box. 30 | func (box *Box) ContainsPoint(p *T) bool { 31 | return p[0] >= box.Min[0] && p[0] <= box.Max[0] && 32 | p[1] >= box.Min[1] && p[1] <= box.Max[1] && 33 | p[2] >= box.Min[2] && p[2] <= box.Max[2] 34 | } 35 | 36 | func (box *Box) Center() T { 37 | c := Add(&box.Min, &box.Max) 38 | c.Scale(0.5) 39 | return c 40 | } 41 | 42 | func (box *Box) Diagonal() T { 43 | return Sub(&box.Max, &box.Min) 44 | } 45 | 46 | // Intersects returns true if this and the given box intersect. 47 | // For an explanation of the algorithm, see 48 | // http://rbrundritt.wordpress.com/2009/10/03/determining-if-two-bounding-boxes-overlap/ 49 | func (box *Box) Intersects(other *Box) bool { 50 | d1 := box.Diagonal() 51 | d2 := other.Diagonal() 52 | sizes := Add(&d1, &d2) 53 | c1 := box.Center() 54 | c2 := other.Center() 55 | distCenters2 := Sub(&c1, &c2) 56 | distCenters2.Scale(2) 57 | distCenters2.Abs() 58 | return distCenters2[0] <= sizes[0] && distCenters2[1] <= sizes[1] && distCenters2[2] <= sizes[2] 59 | } 60 | 61 | // Join enlarges this box to contain also the given box. 62 | func (box *Box) Join(other *Box) { 63 | box.Min = Min(&box.Min, &other.Min) 64 | box.Max = Max(&box.Max, &other.Max) 65 | } 66 | 67 | // Joined returns the minimal box containing both a and b. 68 | func Joined(a, b *Box) Box { 69 | var joined Box 70 | joined.Min = Min(&a.Min, &b.Min) 71 | joined.Max = Max(&a.Max, &b.Max) 72 | return joined 73 | } 74 | -------------------------------------------------------------------------------- /float64/vec3/box.go: -------------------------------------------------------------------------------- 1 | package vec3 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Box is a coordinate system aligned 3D box defined by a Min and Max vector. 8 | type Box struct { 9 | Min T 10 | Max T 11 | } 12 | 13 | var ( 14 | // MaxBox holds a box that contains the entire R3 space that can be represented as vec3 15 | MaxBox = Box{MinVal, MaxVal} 16 | ) 17 | 18 | // ParseBox parses a Box from a string. See also String() 19 | func ParseBox(s string) (r Box, err error) { 20 | _, err = fmt.Sscan(s, &r.Min[0], &r.Min[1], &r.Min[2], &r.Max[0], &r.Max[1], &r.Max[2]) 21 | return r, err 22 | } 23 | 24 | // String formats Box as string. See also ParseBox(). 25 | func (box *Box) String() string { 26 | return box.Min.String() + " " + box.Max.String() 27 | } 28 | 29 | // ContainsPoint returns if a point is contained within the box. 30 | func (box *Box) ContainsPoint(p *T) bool { 31 | return p[0] >= box.Min[0] && p[0] <= box.Max[0] && 32 | p[1] >= box.Min[1] && p[1] <= box.Max[1] && 33 | p[2] >= box.Min[2] && p[2] <= box.Max[2] 34 | } 35 | 36 | func (box *Box) Center() T { 37 | c := Add(&box.Min, &box.Max) 38 | c.Scale(0.5) 39 | return c 40 | } 41 | 42 | func (box *Box) Diagonal() T { 43 | return Sub(&box.Max, &box.Min) 44 | } 45 | 46 | // Intersects returns true if this and the given box intersect. 47 | // For an explanation of the algorithm, see 48 | // http://rbrundritt.wordpress.com/2009/10/03/determining-if-two-bounding-boxes-overlap/ 49 | func (box *Box) Intersects(other *Box) bool { 50 | d1 := box.Diagonal() 51 | d2 := other.Diagonal() 52 | sizes := Add(&d1, &d2) 53 | c1 := box.Center() 54 | c2 := other.Center() 55 | distCenters2 := Sub(&c1, &c2) 56 | distCenters2.Scale(2) 57 | distCenters2.Abs() 58 | return distCenters2[0] <= sizes[0] && distCenters2[1] <= sizes[1] && distCenters2[2] <= sizes[2] 59 | } 60 | 61 | // Join enlarges this box to contain also the given box. 62 | func (box *Box) Join(other *Box) { 63 | box.Min = Min(&box.Min, &other.Min) 64 | box.Max = Max(&box.Max, &other.Max) 65 | } 66 | 67 | // Joined returns the minimal box containing both a and b. 68 | func Joined(a, b *Box) Box { 69 | var joined Box 70 | joined.Min = Min(&a.Min, &b.Min) 71 | joined.Max = Max(&a.Max, &b.Max) 72 | return joined 73 | } 74 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '34 8 * * 0' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'go' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /float64/qbezier2/qbezier2.go: -------------------------------------------------------------------------------- 1 | // Package qbezier2 contains functions for 2D quadratic Bezier splines. 2 | // See: http://en.wikipedia.org/wiki/B%C3%A9zier_curve 3 | package qbezier2 4 | 5 | import ( 6 | "fmt" 7 | "math" 8 | 9 | "github.com/ungerik/go3d/float64/vec2" 10 | ) 11 | 12 | // T holds the data to define a quadratic bezier spline. 13 | type T struct { 14 | P0, P1, P2 vec2.T 15 | } 16 | 17 | // Parse parses T from a string. See also String() 18 | func Parse(s string) (r T, err error) { 19 | _, err = fmt.Sscan(s, 20 | &r.P0[0], &r.P0[1], 21 | &r.P1[0], &r.P1[1], 22 | &r.P2[0], &r.P2[1], 23 | ) 24 | return r, err 25 | } 26 | 27 | // String formats T as string. See also Parse(). 28 | func (bez *T) String() string { 29 | return fmt.Sprintf("%s %s %s", 30 | bez.P0.String(), bez.P1.String(), bez.P2.String(), 31 | ) 32 | } 33 | 34 | // Point returns a point on a quadratic bezier spline at t (0,1). 35 | func (bez *T) Point(t float64) vec2.T { 36 | return Point(&bez.P0, &bez.P1, &bez.P2, t) 37 | } 38 | 39 | // Tangent returns a tangent on a quadratic bezier spline at t (0,1). 40 | func (bez *T) Tangent(t float64) vec2.T { 41 | return Tangent(&bez.P0, &bez.P1, &bez.P2, t) 42 | } 43 | 44 | // Length returns the length of a quadratic bezier spline from A.Point to t (0,1). 45 | func (bez *T) Length(t float64) float64 { 46 | return Length(&bez.P0, &bez.P1, &bez.P2, t) 47 | } 48 | 49 | // Point returns a point on a quadratic bezier spline at t (0,1). 50 | func Point(p0, p1, p2 *vec2.T, t float64) vec2.T { 51 | t1 := 1.0 - t 52 | 53 | f := t1 * t1 54 | result := p0.Scaled(f) 55 | 56 | f = 2.0 * t1 * t 57 | p1f := p1.Scaled(f) 58 | result.Add(&p1f) 59 | 60 | f = t * t 61 | p2f := p2.Scaled(f) 62 | result.Add(&p2f) 63 | 64 | return result 65 | } 66 | 67 | // Tangent returns a tangent on a quadratic bezier spline at t (0,1). 68 | func Tangent(p0, p1, p2 *vec2.T, t float64) vec2.T { 69 | t1 := 1.0 - t 70 | 71 | f := 2.0 * t1 72 | p1f := vec2.Sub(p1, p0) 73 | result := p1f.Scaled(f) 74 | 75 | f = 2.0 * t 76 | p2f := vec2.Sub(p2, p1) 77 | p2f.Scale(f) 78 | result.Add(&p2f) 79 | 80 | if result[0] == 0 && result[1] == 0 { 81 | fmt.Printf("zero tangent! p0=%v, p1=%v, p2=%v, t=%v\n", p0, p1, p2, t) 82 | panic("zero tangent of qbezier2") 83 | } 84 | 85 | return result 86 | } 87 | 88 | // Length returns the length of a quadratic bezier spline from p0 to t (0,1). 89 | // 90 | // Note that although this calculation is accurate at t=0, 0.5, and 1 due 91 | // to the nature of quadratic curves, it is an approximation for other values of t. 92 | func Length(p0, p1, p2 *vec2.T, t float64) float64 { 93 | ax := p0[0] - 2*p1[0] + p2[0] 94 | ay := p0[1] - 2*p1[1] + p2[1] 95 | bx := 2*p1[0] - 2*p0[0] 96 | by := 2*p1[1] - 2*p0[1] 97 | 98 | a := 4 * (ax*ax + ay*ay) 99 | b := 4 * (ax*bx + ay*by) 100 | c := bx*bx + by*by 101 | 102 | abc := 2 * math.Sqrt(a+b+c) 103 | a2 := math.Sqrt(a) 104 | a32 := 2 * a * a2 105 | c2 := 2 * math.Sqrt(c) 106 | ba := b / a2 107 | 108 | return t * (a32*abc + a2*b*(abc-c2) + (4*c*a-b*b)*math.Log((2*a2+ba+abc)/(ba+c2))) / (4 * a32) 109 | } 110 | -------------------------------------------------------------------------------- /qbezier2/qbezier2.go: -------------------------------------------------------------------------------- 1 | // Package qbezier2 contains a float32 type T and functions for 2D quadratic Bezier splines. 2 | // See: http://en.wikipedia.org/wiki/B%C3%A9zier_curve 3 | package qbezier2 4 | 5 | import ( 6 | "fmt" 7 | 8 | math "github.com/chewxy/math32" 9 | "github.com/ungerik/go3d/vec2" 10 | ) 11 | 12 | // T holds the data to define a quadratic bezier spline. 13 | type T struct { 14 | P0, P1, P2 vec2.T 15 | } 16 | 17 | // Parse parses T from a string. See also String() 18 | func Parse(s string) (r T, err error) { 19 | _, err = fmt.Sscan(s, 20 | &r.P0[0], &r.P0[1], 21 | &r.P1[0], &r.P1[1], 22 | &r.P2[0], &r.P2[1], 23 | ) 24 | return r, err 25 | } 26 | 27 | // String formats T as string. See also Parse(). 28 | func (bez *T) String() string { 29 | return fmt.Sprintf("%s %s %s", 30 | bez.P0.String(), bez.P1.String(), bez.P2.String(), 31 | ) 32 | } 33 | 34 | // Point returns a point on a quadratic bezier spline at t (0,1). 35 | func (bez *T) Point(t float32) vec2.T { 36 | return Point(&bez.P0, &bez.P1, &bez.P2, t) 37 | } 38 | 39 | // Tangent returns a tangent on a quadratic bezier spline at t (0,1). 40 | func (bez *T) Tangent(t float32) vec2.T { 41 | return Tangent(&bez.P0, &bez.P1, &bez.P2, t) 42 | } 43 | 44 | // Length returns the length of a quadratic bezier spline from P0 to t (0,1). 45 | func (bez *T) Length(t float32) float32 { 46 | return Length(&bez.P0, &bez.P1, &bez.P2, t) 47 | } 48 | 49 | // Point returns a point on a quadratic bezier spline at t (0,1). 50 | func Point(p0, p1, p2 *vec2.T, t float32) vec2.T { 51 | t1 := 1.0 - t 52 | 53 | f := t1 * t1 54 | result := p0.Scaled(f) 55 | 56 | f = 2.0 * t1 * t 57 | p1f := p1.Scaled(f) 58 | result.Add(&p1f) 59 | 60 | f = t * t 61 | p2f := p2.Scaled(f) 62 | result.Add(&p2f) 63 | 64 | return result 65 | } 66 | 67 | // Tangent returns a tangent on a quadratic bezier spline at t (0,1). 68 | func Tangent(p0, p1, p2 *vec2.T, t float32) vec2.T { 69 | t1 := 1.0 - t 70 | 71 | f := 2.0 * t1 72 | p1f := vec2.Sub(p1, p0) 73 | result := p1f.Scaled(f) 74 | 75 | f = 2.0 * t 76 | p2f := vec2.Sub(p2, p1) 77 | p2f.Scale(f) 78 | result.Add(&p2f) 79 | 80 | if result[0] == 0 && result[1] == 0 { 81 | fmt.Printf("zero tangent! p0=%v, p1=%v, p2=%v, t=%v\n", p0, p1, p2, t) 82 | panic("zero tangent of qbezier2") 83 | } 84 | 85 | return result 86 | } 87 | 88 | // Length returns the length of a quadratic bezier spline from p0 to t (0,1). 89 | // 90 | // Note that although this calculation is accurate at t=0, 0.5, and 1 due 91 | // to the nature of quadratic curves, it is an approximation for other values of t. 92 | func Length(p0, p1, p2 *vec2.T, t float32) float32 { 93 | ax := p0[0] - 2*p1[0] + p2[0] 94 | ay := p0[1] - 2*p1[1] + p2[1] 95 | bx := 2*p1[0] - 2*p0[0] 96 | by := 2*p1[1] - 2*p0[1] 97 | 98 | a := 4 * (ax*ax + ay*ay) 99 | b := 4 * (ax*bx + ay*by) 100 | c := bx*bx + by*by 101 | 102 | abc := 2 * math.Sqrt(a+b+c) 103 | a2 := math.Sqrt(a) 104 | a32 := 2 * a * a2 105 | c2 := 2 * math.Sqrt(c) 106 | ba := b / a2 107 | 108 | return t * (a32*abc + a2*b*(abc-c2) + (4*c*a-b*b)*math.Log((2*a2+ba+abc)/(ba+c2))) / (4 * a32) 109 | } 110 | -------------------------------------------------------------------------------- /bezier2/bezier2.go: -------------------------------------------------------------------------------- 1 | // Package bezier2 contains a float32 type T and functions for 2D cubic Bezier splines. 2 | // See: http://en.wikipedia.org/wiki/B%C3%A9zier_curve 3 | package bezier2 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/ungerik/go3d/vec2" 9 | ) 10 | 11 | // T holds the data to define a cubic bezier spline. 12 | type T struct { 13 | P0, P1, P2, P3 vec2.T 14 | } 15 | 16 | // Parse parses T from a string. See also String() 17 | func Parse(s string) (r T, err error) { 18 | _, err = fmt.Sscan(s, 19 | &r.P0[0], &r.P0[1], 20 | &r.P1[0], &r.P1[1], 21 | &r.P2[0], &r.P2[1], 22 | &r.P3[0], &r.P3[1], 23 | ) 24 | return r, err 25 | } 26 | 27 | // String formats T as string. See also Parse(). 28 | func (bez *T) String() string { 29 | return fmt.Sprintf("%s %s %s %s", 30 | bez.P0.String(), bez.P1.String(), 31 | bez.P2.String(), bez.P3.String(), 32 | ) 33 | } 34 | 35 | // Point returns a point on a cubic bezier spline at t (0,1). 36 | func (bez *T) Point(t float32) vec2.T { 37 | return Point(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t) 38 | } 39 | 40 | // Tangent returns a tangent on a cubic bezier spline at t (0,1). 41 | func (bez *T) Tangent(t float32) vec2.T { 42 | return Tangent(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t) 43 | } 44 | 45 | // Length returns the length of a cubic bezier spline from P0 to t (0,1). 46 | func (bez *T) Length(t float32) float32 { 47 | return Length(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t) 48 | } 49 | 50 | // Point returns a point on a cubic bezier spline at t (0,1). 51 | func Point(p0, p1, p2, p3 *vec2.T, t float32) vec2.T { 52 | t1 := 1.0 - t 53 | 54 | f := t1 * t1 * t1 55 | result := p0.Scaled(f) 56 | 57 | f = 3.0 * t1 * t1 * t 58 | p1f := p1.Scaled(f) 59 | result.Add(&p1f) 60 | 61 | f = 3.0 * t1 * t * t 62 | p2f := p2.Scaled(f) 63 | result.Add(&p2f) 64 | 65 | f = t * t * t 66 | p3f := p3.Scaled(f) 67 | result.Add(&p3f) 68 | 69 | return result 70 | } 71 | 72 | // Tangent returns a tangent on a cubic bezier spline at t (0,1). 73 | func Tangent(p0, p1, p2, p3 *vec2.T, t float32) vec2.T { 74 | t1 := 1.0 - t 75 | 76 | f := 3.0 * t1 * t1 77 | p1f := vec2.Sub(p1, p0) 78 | result := p1f.Scaled(f) 79 | 80 | f = 6.0 * t1 * t 81 | p2f := vec2.Sub(p2, p1) 82 | p2f.Scale(f) 83 | result.Add(&p2f) 84 | 85 | f = 3.0 * t * t 86 | p3f := vec2.Sub(p3, p2) 87 | p3f.Scale(f) 88 | result.Add(&p3f) 89 | 90 | if result[0] == 0 && result[1] == 0 { 91 | fmt.Printf("zero tangent! p0=%v, p1=%v, p2=%v, p3=%v, t=%v\n", p0, p1, p2, p3, t) 92 | panic("zero tangent of bezier2") 93 | } 94 | 95 | return result 96 | } 97 | 98 | // Length returns the length of a cubic bezier spline from p0 to t (0,1). 99 | func Length(p0, p1, p2, p3 *vec2.T, t float32) float32 { 100 | sqrT := t * t 101 | t1 := sqrT * 0.5 102 | t2 := sqrT * t * 1.0 / 3.0 103 | t3 := sqrT*sqrT + 1.0/4.0 104 | 105 | f := 2*t3 - 3*t2 + t 106 | result := p0.Scaled(f) 107 | 108 | f = t3 - 2*t2 + t1 109 | tAf := p1.Scaled(f) 110 | result.Add(&tAf) 111 | 112 | f = t3 - t2 113 | tBf := p3.Scaled(f) 114 | result.Add(&tBf) 115 | 116 | f = -2*t3 + 3*t2 117 | pBf := p2.Scaled(f) 118 | result.Add(&pBf) 119 | 120 | return result.Length() 121 | } 122 | -------------------------------------------------------------------------------- /float64/bezier2/bezier2.go: -------------------------------------------------------------------------------- 1 | // Package bezier2 contains functions for 2D cubic Bezier splines. 2 | // See: http://en.wikipedia.org/wiki/B%C3%A9zier_curve 3 | package bezier2 4 | 5 | import ( 6 | "fmt" 7 | 8 | "github.com/ungerik/go3d/float64/vec2" 9 | ) 10 | 11 | // T holds the data to define a cubic bezier spline. 12 | type T struct { 13 | P0, P1, P2, P3 vec2.T 14 | } 15 | 16 | // Parse parses T from a string. See also String() 17 | func Parse(s string) (r T, err error) { 18 | _, err = fmt.Sscan(s, 19 | &r.P0[0], &r.P0[1], 20 | &r.P1[0], &r.P1[1], 21 | &r.P2[0], &r.P2[1], 22 | &r.P3[0], &r.P3[1], 23 | ) 24 | return r, err 25 | } 26 | 27 | // String formats T as string. See also Parse(). 28 | func (bez *T) String() string { 29 | return fmt.Sprintf("%s %s %s %s", 30 | bez.P0.String(), bez.P1.String(), 31 | bez.P2.String(), bez.P3.String(), 32 | ) 33 | } 34 | 35 | // Point returns a point on a cubic bezier spline at t (0,1). 36 | func (bez *T) Point(t float64) vec2.T { 37 | return Point(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t) 38 | } 39 | 40 | // Tangent returns a tangent on a cubic bezier spline at t (0,1). 41 | func (bez *T) Tangent(t float64) vec2.T { 42 | return Tangent(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t) 43 | } 44 | 45 | // Length returns the length of a cubic bezier spline from A.Point to t (0,1). 46 | func (bez *T) Length(t float64) float64 { 47 | return Length(&bez.P0, &bez.P1, &bez.P2, &bez.P3, t) 48 | } 49 | 50 | // Point returns a point on a cubic bezier spline at t (0,1). 51 | func Point(p0, p1, p2, p3 *vec2.T, t float64) vec2.T { 52 | t1 := 1.0 - t 53 | 54 | f := t1 * t1 * t1 55 | result := p0.Scaled(f) 56 | 57 | f = 3.0 * t1 * t1 * t 58 | p1f := p1.Scaled(f) 59 | result.Add(&p1f) 60 | 61 | f = 3.0 * t1 * t * t 62 | p2f := p2.Scaled(f) 63 | result.Add(&p2f) 64 | 65 | f = t * t * t 66 | p3f := p3.Scaled(f) 67 | result.Add(&p3f) 68 | 69 | return result 70 | } 71 | 72 | // Tangent returns a tangent on a cubic bezier spline at t (0,1). 73 | func Tangent(p0, p1, p2, p3 *vec2.T, t float64) vec2.T { 74 | t1 := 1.0 - t 75 | 76 | f := 3.0 * t1 * t1 77 | p1f := vec2.Sub(p1, p0) 78 | result := p1f.Scaled(f) 79 | 80 | f = 6.0 * t1 * t 81 | p2f := vec2.Sub(p2, p1) 82 | p2f.Scale(f) 83 | result.Add(&p2f) 84 | 85 | f = 3.0 * t * t 86 | p3f := vec2.Sub(p3, p2) 87 | p3f.Scale(f) 88 | result.Add(&p3f) 89 | 90 | if result[0] == 0 && result[1] == 0 { 91 | fmt.Printf("zero tangent! p0=%v, p1=%v, p2=%v, p3=%v, t=%v\n", p0, p1, p2, p3, t) 92 | panic("zero tangent of bezier2") 93 | } 94 | 95 | return result 96 | } 97 | 98 | // Length returns the length of a cubic bezier spline from p0 to t (0,1). 99 | func Length(p0, p1, p2, p3 *vec2.T, t float64) float64 { 100 | sqrT := t * t 101 | t1 := sqrT * 0.5 102 | t2 := sqrT * t * 1.0 / 3.0 103 | t3 := sqrT*sqrT + 1.0/4.0 104 | 105 | f := 2*t3 - 3*t2 + t 106 | result := p0.Scaled(f) 107 | 108 | f = t3 - 2*t2 + t1 109 | tAf := p1.Scaled(f) 110 | result.Add(&tAf) 111 | 112 | f = t3 - t2 113 | tBf := p3.Scaled(f) 114 | result.Add(&tBf) 115 | 116 | f = -2*t3 + 3*t2 117 | pBf := p2.Scaled(f) 118 | result.Add(&pBf) 119 | 120 | return result.Length() 121 | } 122 | -------------------------------------------------------------------------------- /float64/hermit2/hermit2.go: -------------------------------------------------------------------------------- 1 | // Package hermit2 contains functions for 2D cubic hermit splines. 2 | // See: http://en.wikipedia.org/wiki/Cubic_Hermite_spline 3 | package hermit2 4 | 5 | import ( 6 | "fmt" 7 | "github.com/ungerik/go3d/float64/vec2" 8 | ) 9 | 10 | // PointTangent contains a point and a tangent at that point. 11 | // This is a helper sub-struct for T. 12 | type PointTangent struct { 13 | Point vec2.T 14 | Tangent vec2.T 15 | } 16 | 17 | // T holds the data to define a hermit spline. 18 | type T struct { 19 | A PointTangent 20 | B PointTangent 21 | } 22 | 23 | // Parse parses T from a string. See also String() 24 | func Parse(s string) (r T, err error) { 25 | _, err = fmt.Sscan(s, 26 | &r.A.Point[0], &r.A.Point[1], 27 | &r.A.Tangent[0], &r.A.Tangent[1], 28 | &r.B.Point[0], &r.B.Point[1], 29 | &r.B.Tangent[0], &r.B.Tangent[1], 30 | ) 31 | return r, err 32 | } 33 | 34 | // String formats T as string. See also Parse(). 35 | func (herm *T) String() string { 36 | return fmt.Sprintf("%s %s %s %s", 37 | herm.A.Point.String(), herm.A.Tangent.String(), 38 | herm.B.Point.String(), herm.B.Tangent.String(), 39 | ) 40 | } 41 | 42 | // Point returns a point on a hermit spline at t (0,1). 43 | func (herm *T) Point(t float64) vec2.T { 44 | return Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 45 | } 46 | 47 | // Tangent returns a tangent on a hermit spline at t (0,1). 48 | func (herm *T) Tangent(t float64) vec2.T { 49 | return Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 50 | } 51 | 52 | // Length returns the length of a hermit spline from A.Point to t (0,1). 53 | func (herm *T) Length(t float64) float64 { 54 | return Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 55 | } 56 | 57 | // Point returns a point on a hermit spline at t (0,1). 58 | func Point(pointA, tangentA, pointB, tangentB *vec2.T, t float64) vec2.T { 59 | t2 := t * t 60 | t3 := t2 * t 61 | 62 | f := 2*t3 - 3*t2 + 1 63 | result := pointA.Scaled(f) 64 | 65 | f = t3 - 2*t2 + t 66 | tAf := tangentA.Scaled(f) 67 | result.Add(&tAf) 68 | 69 | f = t3 - t2 70 | tBf := tangentB.Scaled(f) 71 | result.Add(&tBf) 72 | 73 | f = -2*t3 + 3*t2 74 | pAf := pointB.Scaled(f) 75 | result.Add(&pAf) 76 | 77 | return result 78 | } 79 | 80 | // Tangent returns a tangent on a hermit spline at t (0,1). 81 | func Tangent(pointA, tangentA, pointB, tangentB *vec2.T, t float64) vec2.T { 82 | t2 := t * t 83 | t3 := t2 * t 84 | 85 | f := 2*t3 - 3*t2 86 | result := pointA.Scaled(f) 87 | 88 | f = t3 - 2*t2 + 1 89 | tAf := tangentA.Scaled(f) 90 | result.Add(&tAf) 91 | 92 | f = t3 - t2 93 | tBf := tangentB.Scaled(f) 94 | result.Add(&tBf) 95 | 96 | f = -2*t3 + 3*t2 97 | pAf := pointB.Scaled(f) 98 | result.Add(&pAf) 99 | 100 | return result 101 | } 102 | 103 | // Length returns the length of a hermit spline from pointA to t (0,1). 104 | func Length(pointA, tangentA, pointB, tangentB *vec2.T, t float64) float64 { 105 | sqrT := t * t 106 | t1 := sqrT * 0.5 107 | t2 := sqrT * t * 1.0 / 3.0 108 | t3 := sqrT*sqrT + 1.0/4.0 109 | 110 | f := 2*t3 - 3*t2 + t 111 | result := pointA.Scaled(f) 112 | 113 | f = t3 - 2*t2 + t1 114 | tAf := tangentA.Scaled(f) 115 | result.Add(&tAf) 116 | 117 | f = t3 - t2 118 | tBf := tangentB.Scaled(f) 119 | result.Add(&tBf) 120 | 121 | f = -2*t3 + 3*t2 122 | pBf := pointB.Scaled(f) 123 | result.Add(&pBf) 124 | 125 | return result.Length() 126 | } 127 | -------------------------------------------------------------------------------- /hermit2/hermit2.go: -------------------------------------------------------------------------------- 1 | // Package hermit2 contains a float32 type T and functions for 2D cubic hermit splines. 2 | // See: http://en.wikipedia.org/wiki/Cubic_Hermite_spline 3 | package hermit2 4 | 5 | import ( 6 | "fmt" 7 | "github.com/ungerik/go3d/vec2" 8 | ) 9 | 10 | // PointTangent contains a point and a tangent at that point. 11 | // This is a helper sub-struct for T. 12 | type PointTangent struct { 13 | Point vec2.T 14 | Tangent vec2.T 15 | } 16 | 17 | // T holds the data to define a hermit spline. 18 | type T struct { 19 | A PointTangent 20 | B PointTangent 21 | } 22 | 23 | // Parse parses T from a string. See also String() 24 | func Parse(s string) (r T, err error) { 25 | _, err = fmt.Sscan(s, 26 | &r.A.Point[0], &r.A.Point[1], 27 | &r.A.Tangent[0], &r.A.Tangent[1], 28 | &r.B.Point[0], &r.B.Point[1], 29 | &r.B.Tangent[0], &r.B.Tangent[1], 30 | ) 31 | return r, err 32 | } 33 | 34 | // String formats T as string. See also Parse(). 35 | func (herm *T) String() string { 36 | return fmt.Sprintf("%s %s %s %s", 37 | herm.A.Point.String(), herm.A.Tangent.String(), 38 | herm.B.Point.String(), herm.B.Tangent.String(), 39 | ) 40 | } 41 | 42 | // Point returns a point on a hermit spline at t (0,1). 43 | func (herm *T) Point(t float32) vec2.T { 44 | return Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 45 | } 46 | 47 | // Tangent returns a tangent on a hermit spline at t (0,1). 48 | func (herm *T) Tangent(t float32) vec2.T { 49 | return Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 50 | } 51 | 52 | // Length returns the length of a hermit spline from A.Point to t (0,1). 53 | func (herm *T) Length(t float32) float32 { 54 | return Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 55 | } 56 | 57 | // Point returns a point on a hermit spline at t (0,1). 58 | func Point(pointA, tangentA, pointB, tangentB *vec2.T, t float32) vec2.T { 59 | t2 := t * t 60 | t3 := t2 * t 61 | 62 | f := 2*t3 - 3*t2 + 1 63 | result := pointA.Scaled(f) 64 | 65 | f = t3 - 2*t2 + t 66 | tAf := tangentA.Scaled(f) 67 | result.Add(&tAf) 68 | 69 | f = t3 - t2 70 | tBf := tangentB.Scaled(f) 71 | result.Add(&tBf) 72 | 73 | f = -2*t3 + 3*t2 74 | pAf := pointB.Scaled(f) 75 | result.Add(&pAf) 76 | 77 | return result 78 | } 79 | 80 | // Tangent returns a tangent on a hermit spline at t (0,1). 81 | func Tangent(pointA, tangentA, pointB, tangentB *vec2.T, t float32) vec2.T { 82 | t2 := t * t 83 | t3 := t2 * t 84 | 85 | f := 2*t3 - 3*t2 86 | result := pointA.Scaled(f) 87 | 88 | f = t3 - 2*t2 + 1 89 | tAf := tangentA.Scaled(f) 90 | result.Add(&tAf) 91 | 92 | f = t3 - t2 93 | tBf := tangentB.Scaled(f) 94 | result.Add(&tBf) 95 | 96 | f = -2*t3 + 3*t2 97 | pAf := pointB.Scaled(f) 98 | result.Add(&pAf) 99 | 100 | return result 101 | } 102 | 103 | // Length returns the length of a hermit spline from pointA to t (0,1). 104 | func Length(pointA, tangentA, pointB, tangentB *vec2.T, t float32) float32 { 105 | sqrT := t * t 106 | t1 := sqrT * 0.5 107 | t2 := sqrT * t * 1.0 / 3.0 108 | t3 := sqrT*sqrT + 1.0/4.0 109 | 110 | f := 2*t3 - 3*t2 + t 111 | result := pointA.Scaled(f) 112 | 113 | f = t3 - 2*t2 + t1 114 | tAf := tangentA.Scaled(f) 115 | result.Add(&tAf) 116 | 117 | f = t3 - t2 118 | tBf := tangentB.Scaled(f) 119 | result.Add(&tBf) 120 | 121 | f = -2*t3 + 3*t2 122 | pBf := pointB.Scaled(f) 123 | result.Add(&pBf) 124 | 125 | return result.Length() 126 | } 127 | -------------------------------------------------------------------------------- /float64/hermit3/hermit3.go: -------------------------------------------------------------------------------- 1 | // Package hermit3 contains functions for 3D cubic hermit splines. 2 | // See: http://en.wikipedia.org/wiki/Cubic_Hermite_spline 3 | package hermit 4 | 5 | import ( 6 | "fmt" 7 | "github.com/ungerik/go3d/float64/vec3" 8 | ) 9 | 10 | // PointTangent contains a point and a tangent at that point. 11 | // This is a helper sub-struct for T. 12 | type PointTangent struct { 13 | Point vec3.T 14 | Tangent vec3.T 15 | } 16 | 17 | // T holds the data to define a hermit spline. 18 | type T struct { 19 | A PointTangent 20 | B PointTangent 21 | } 22 | 23 | // Parse parses T from a string. See also String() 24 | func Parse(s string) (r T, err error) { 25 | _, err = fmt.Sscan(s, 26 | &r.A.Point[0], &r.A.Point[1], &r.A.Point[2], 27 | &r.A.Tangent[0], &r.A.Tangent[1], &r.A.Tangent[2], 28 | &r.B.Point[0], &r.B.Point[1], &r.B.Point[2], 29 | &r.B.Tangent[0], &r.B.Tangent[1], &r.B.Tangent[2], 30 | ) 31 | return r, err 32 | } 33 | 34 | // String formats T as string. See also Parse(). 35 | func (herm *T) String() string { 36 | return fmt.Sprintf("%s %s %s %s", 37 | herm.A.Point.String(), herm.A.Tangent.String(), 38 | herm.B.Point.String(), herm.B.Tangent.String(), 39 | ) 40 | } 41 | 42 | // Point returns a point on a hermit spline at t (0,1). 43 | func (herm *T) Point(t float64) vec3.T { 44 | return Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 45 | } 46 | 47 | // Tangent returns a tangent on a hermit spline at t (0,1). 48 | func (herm *T) Tangent(t float64) vec3.T { 49 | return Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 50 | } 51 | 52 | // Length returns the length of a hermit spline from A.Point to t (0,1). 53 | func (herm *T) Length(t float64) float64 { 54 | return Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 55 | } 56 | 57 | // Point returns a point on a hermit spline at t (0,1). 58 | func Point(pointA, tangentA, pointB, tangentB *vec3.T, t float64) vec3.T { 59 | t2 := t * t 60 | t3 := t2 * t 61 | 62 | f := 2*t3 - 3*t2 + 1 63 | result := pointA.Scaled(f) 64 | 65 | f = t3 - 2*t2 + t 66 | tAf := tangentA.Scaled(f) 67 | result.Add(&tAf) 68 | 69 | f = t3 - t2 70 | tBf := tangentB.Scaled(f) 71 | result.Add(&tBf) 72 | 73 | f = -2*t3 + 3*t2 74 | pAf := pointB.Scaled(f) 75 | result.Add(&pAf) 76 | 77 | return result 78 | } 79 | 80 | // Tangent returns a tangent on a hermit spline at t (0,1). 81 | func Tangent(pointA, tangentA, pointB, tangentB *vec3.T, t float64) vec3.T { 82 | t2 := t * t 83 | t3 := t2 * t 84 | 85 | f := 2*t3 - 3*t2 86 | result := pointA.Scaled(f) 87 | 88 | f = t3 - 2*t2 + 1 89 | tAf := tangentA.Scaled(f) 90 | result.Add(&tAf) 91 | 92 | f = t3 - t2 93 | tBf := tangentB.Scaled(f) 94 | result.Add(&tBf) 95 | 96 | f = -2*t3 + 3*t2 97 | pAf := pointB.Scaled(f) 98 | result.Add(&pAf) 99 | 100 | return result 101 | } 102 | 103 | // Length returns the length of a hermit spline from pointA to t (0,1). 104 | func Length(pointA, tangentA, pointB, tangentB *vec3.T, t float64) float64 { 105 | sqrT := t * t 106 | t1 := sqrT * 0.5 107 | t2 := sqrT * t * 1.0 / 3.0 108 | t3 := sqrT*sqrT + 1.0/4.0 109 | 110 | f := 2*t3 - 3*t2 + t 111 | result := pointA.Scaled(f) 112 | 113 | f = t3 - 2*t2 + t1 114 | tAf := tangentA.Scaled(f) 115 | result.Add(&tAf) 116 | 117 | f = t3 - t2 118 | tBf := tangentB.Scaled(f) 119 | result.Add(&tBf) 120 | 121 | f = -2*t3 + 3*t2 122 | pBf := pointB.Scaled(f) 123 | result.Add(&pBf) 124 | 125 | return result.Length() 126 | } 127 | -------------------------------------------------------------------------------- /hermit3/hermit3.go: -------------------------------------------------------------------------------- 1 | // Package hermit3 contains a float32 type T and functions for 3D cubic hermit splines. 2 | // See: http://en.wikipedia.org/wiki/Cubic_Hermite_spline 3 | package hermit 4 | 5 | import ( 6 | "fmt" 7 | "github.com/ungerik/go3d/vec3" 8 | ) 9 | 10 | // PointTangent contains a point and a tangent at that point. 11 | // This is a helper sub-struct for T. 12 | type PointTangent struct { 13 | Point vec3.T 14 | Tangent vec3.T 15 | } 16 | 17 | // T holds the data to define a hermit spline. 18 | type T struct { 19 | A PointTangent 20 | B PointTangent 21 | } 22 | 23 | // Parse parses T from a string. See also String() 24 | func Parse(s string) (r T, err error) { 25 | _, err = fmt.Sscan(s, 26 | &r.A.Point[0], &r.A.Point[1], &r.A.Point[2], 27 | &r.A.Tangent[0], &r.A.Tangent[1], &r.A.Tangent[2], 28 | &r.B.Point[0], &r.B.Point[1], &r.B.Point[2], 29 | &r.B.Tangent[0], &r.B.Tangent[1], &r.B.Tangent[2], 30 | ) 31 | return r, err 32 | } 33 | 34 | // String formats T as string. See also Parse(). 35 | func (herm *T) String() string { 36 | return fmt.Sprintf("%s %s %s %s", 37 | herm.A.Point.String(), herm.A.Tangent.String(), 38 | herm.B.Point.String(), herm.B.Tangent.String(), 39 | ) 40 | } 41 | 42 | // Point returns a point on a hermit spline at t (0,1). 43 | func (herm *T) Point(t float32) vec3.T { 44 | return Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 45 | } 46 | 47 | // Tangent returns a tangent on a hermit spline at t (0,1). 48 | func (herm *T) Tangent(t float32) vec3.T { 49 | return Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 50 | } 51 | 52 | // Length returns the length of a hermit spline from A.Point to t (0,1). 53 | func (herm *T) Length(t float32) float32 { 54 | return Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, t) 55 | } 56 | 57 | // Point returns a point on a hermit spline at t (0,1). 58 | func Point(pointA, tangentA, pointB, tangentB *vec3.T, t float32) vec3.T { 59 | t2 := t * t 60 | t3 := t2 * t 61 | 62 | f := 2*t3 - 3*t2 + 1 63 | result := pointA.Scaled(f) 64 | 65 | f = t3 - 2*t2 + t 66 | tAf := tangentA.Scaled(f) 67 | result.Add(&tAf) 68 | 69 | f = t3 - t2 70 | tBf := tangentB.Scaled(f) 71 | result.Add(&tBf) 72 | 73 | f = -2*t3 + 3*t2 74 | pAf := pointB.Scaled(f) 75 | result.Add(&pAf) 76 | 77 | return result 78 | } 79 | 80 | // Tangent returns a tangent on a hermit spline at t (0,1). 81 | func Tangent(pointA, tangentA, pointB, tangentB *vec3.T, t float32) vec3.T { 82 | t2 := t * t 83 | t3 := t2 * t 84 | 85 | f := 2*t3 - 3*t2 86 | result := pointA.Scaled(f) 87 | 88 | f = t3 - 2*t2 + 1 89 | tAf := tangentA.Scaled(f) 90 | result.Add(&tAf) 91 | 92 | f = t3 - t2 93 | tBf := tangentB.Scaled(f) 94 | result.Add(&tBf) 95 | 96 | f = -2*t3 + 3*t2 97 | pAf := pointB.Scaled(f) 98 | result.Add(&pAf) 99 | 100 | return result 101 | } 102 | 103 | // Length returns the length of a hermit spline from pointA to t (0,1). 104 | func Length(pointA, tangentA, pointB, tangentB *vec3.T, t float32) float32 { 105 | sqrT := t * t 106 | t1 := sqrT * 0.5 107 | t2 := sqrT * t * 1.0 / 3.0 108 | t3 := sqrT*sqrT + 1.0/4.0 109 | 110 | f := 2*t3 - 3*t2 + t 111 | result := pointA.Scaled(f) 112 | 113 | f = t3 - 2*t2 + t1 114 | tAf := tangentA.Scaled(f) 115 | result.Add(&tAf) 116 | 117 | f = t3 - t2 118 | tBf := tangentB.Scaled(f) 119 | result.Add(&tBf) 120 | 121 | f = -2*t3 + 3*t2 122 | pBf := pointB.Scaled(f) 123 | result.Add(&pBf) 124 | 125 | return result.Length() 126 | } 127 | -------------------------------------------------------------------------------- /mat2/mat2_test.go: -------------------------------------------------------------------------------- 1 | package mat2 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/ungerik/go3d/vec2" 8 | ) 9 | 10 | const EPSILON = 0.0001 11 | 12 | // Some matrices used in multiple tests. 13 | var ( 14 | invertableMatrix1 = T{vec2.T{4, -2}, vec2.T{8, -3}} 15 | invertedMatrix1 = T{vec2.T{-3.0 / 4.0, 1.0 / 2.0}, vec2.T{-2, 1}} 16 | nonInvertableMatrix1 = T{vec2.T{1, 1}, vec2.T{1, 1}} 17 | nonInvertableMatrix2 = T{vec2.T{2, 0}, vec2.T{1, 0}} 18 | 19 | row123Changed, _ = Parse("3 1 2 5") 20 | ) 21 | 22 | func TestT_Transposed(t *testing.T) { 23 | matrix := T{ 24 | vec2.T{1, 2}, 25 | vec2.T{3, 4}, 26 | } 27 | expectedMatrix := T{ 28 | vec2.T{1, 3}, 29 | vec2.T{2, 4}, 30 | } 31 | 32 | transposedMatrix := matrix.Transposed() 33 | 34 | if transposedMatrix != expectedMatrix { 35 | t.Errorf("matrix trasnposed wrong: %v --> %v", matrix, transposedMatrix) 36 | } 37 | } 38 | 39 | func TestT_Transpose(t *testing.T) { 40 | matrix := T{ 41 | vec2.T{10, 20}, 42 | vec2.T{30, 40}, 43 | } 44 | 45 | expectedMatrix := T{ 46 | vec2.T{10, 30}, 47 | vec2.T{20, 40}, 48 | } 49 | 50 | transposedMatrix := matrix 51 | transposedMatrix.Transpose() 52 | 53 | if transposedMatrix != expectedMatrix { 54 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix) 55 | } 56 | } 57 | 58 | func TestDeterminant_2(t *testing.T) { 59 | detTwo := Ident 60 | detTwo[0][0] = 2 61 | if det := detTwo.Determinant(); det != (2*1 - 0*0) { 62 | t.Errorf("Wrong determinant: %f", det) 63 | } 64 | } 65 | 66 | func TestDeterminant_3(t *testing.T) { 67 | scale2 := Ident.Scaled(2) 68 | if det := scale2.Determinant(); det != (2*2 - 0*0) { 69 | t.Errorf("Wrong determinant: %f", det) 70 | } 71 | } 72 | 73 | func TestDeterminant_4(t *testing.T) { 74 | row1changed, _ := Parse("3 0 2 2") 75 | if det := row1changed.Determinant(); det != (3*2 - 0*2) { 76 | t.Errorf("Wrong determinant: %f", det) 77 | } 78 | } 79 | 80 | func TestDeterminant_5(t *testing.T) { 81 | row12changed, _ := Parse("3 1 2 5") 82 | if det := row12changed.Determinant(); det != (3*5 - 1*2) { 83 | t.Errorf("Wrong determinant: %f", det) 84 | } 85 | } 86 | 87 | func TestDeterminant_7(t *testing.T) { 88 | randomMatrix, err := Parse("0.43685 0.81673 0.16600 0.40608") 89 | randomMatrix.Transpose() 90 | if err != nil { 91 | t.Errorf("Could not parse random matrix: %v", err) 92 | } 93 | if det := randomMatrix.Determinant(); PracticallyEquals(det, 0.0418189) { 94 | t.Errorf("Wrong determinant for random sub 3x3 matrix: %f", det) 95 | } 96 | } 97 | 98 | func PracticallyEquals(value1 float32, value2 float32) bool { 99 | return math.Abs(float64(value1-value2)) > EPSILON 100 | } 101 | 102 | func TestDeterminant_6(t *testing.T) { 103 | row123changed := row123Changed 104 | if det := row123changed.Determinant(); det != (3*5 - 2*1) { 105 | t.Errorf("Wrong determinant for 3x3 matrix: %f", det) 106 | } 107 | } 108 | 109 | func TestDeterminant_1(t *testing.T) { 110 | detId := Ident.Determinant() 111 | if detId != 1 { 112 | t.Errorf("Wrong determinant for identity matrix: %f", detId) 113 | } 114 | } 115 | 116 | func TestInvert_ok(t *testing.T) { 117 | inv := invertableMatrix1 118 | _, err := inv.Invert() 119 | 120 | if err != nil { 121 | t.Error("Inverse not computed correctly", err) 122 | } 123 | 124 | invExpected := invertedMatrix1 125 | if inv != invExpected { 126 | t.Errorf("Inverse not computed correctly: %#v", inv) 127 | } 128 | } 129 | 130 | func TestInvert_nok_1(t *testing.T) { 131 | inv := nonInvertableMatrix1 132 | _, err := inv.Inverted() 133 | if err == nil { 134 | t.Error("Inverse should not be possible", err) 135 | } 136 | } 137 | 138 | func TestInvert_nok_2(t *testing.T) { 139 | inv := nonInvertableMatrix2 140 | _, err := inv.Inverted() 141 | if err == nil { 142 | t.Error("Inverse should not be possible", err) 143 | } 144 | } 145 | 146 | func TestIsZeroEps(t *testing.T) { 147 | tests := []struct { 148 | name string 149 | mat T 150 | epsilon float32 151 | want bool 152 | }{ 153 | {"exact zero", Zero, 0.0001, true}, 154 | {"within epsilon", T{vec2.T{0.00001, -0.00001}, vec2.T{0.00001, -0.00001}}, 0.0001, true}, 155 | {"at epsilon boundary", T{vec2.T{0.0001, 0.0001}, vec2.T{0.0001, 0.0001}}, 0.0001, true}, 156 | {"outside epsilon", T{vec2.T{0.001, 0}, vec2.T{0, 0}}, 0.0001, false}, 157 | {"one element outside", T{vec2.T{0.00001, 0.00001}, vec2.T{0.001, 0.00001}}, 0.0001, false}, 158 | {"negative outside epsilon", T{vec2.T{-0.001, 0}, vec2.T{0, 0}}, 0.0001, false}, 159 | {"identity matrix", Ident, 0.0001, false}, 160 | } 161 | 162 | for _, tt := range tests { 163 | t.Run(tt.name, func(t *testing.T) { 164 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want { 165 | t.Errorf("IsZeroEps() = %v, want %v for mat %v with epsilon %v", got, tt.want, tt.mat, tt.epsilon) 166 | } 167 | }) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /float64/mat2/mat2_test.go: -------------------------------------------------------------------------------- 1 | package mat2 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/ungerik/go3d/float64/vec2" 8 | ) 9 | 10 | const EPSILON = 0.0001 11 | 12 | // Some matrices used in multiple tests. 13 | var ( 14 | invertableMatrix1 = T{vec2.T{4, -2}, vec2.T{8, -3}} 15 | invertedMatrix1 = T{vec2.T{-3.0 / 4.0, 1.0 / 2.0}, vec2.T{-2, 1}} 16 | nonInvertableMatrix1 = T{vec2.T{1, 1}, vec2.T{1, 1}} 17 | nonInvertableMatrix2 = T{vec2.T{2, 0}, vec2.T{1, 0}} 18 | 19 | row123Changed, _ = Parse("3 1 2 5") 20 | ) 21 | 22 | func TestT_Transposed(t *testing.T) { 23 | matrix := T{ 24 | vec2.T{1, 2}, 25 | vec2.T{3, 4}, 26 | } 27 | expectedMatrix := T{ 28 | vec2.T{1, 3}, 29 | vec2.T{2, 4}, 30 | } 31 | 32 | transposedMatrix := matrix.Transposed() 33 | 34 | if transposedMatrix != expectedMatrix { 35 | t.Errorf("matrix trasnposed wrong: %v --> %v", matrix, transposedMatrix) 36 | } 37 | } 38 | 39 | func TestT_Transpose(t *testing.T) { 40 | matrix := T{ 41 | vec2.T{10, 20}, 42 | vec2.T{30, 40}, 43 | } 44 | 45 | expectedMatrix := T{ 46 | vec2.T{10, 30}, 47 | vec2.T{20, 40}, 48 | } 49 | 50 | transposedMatrix := matrix 51 | transposedMatrix.Transpose() 52 | 53 | if transposedMatrix != expectedMatrix { 54 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix) 55 | } 56 | } 57 | 58 | func TestDeterminant_2(t *testing.T) { 59 | detTwo := Ident 60 | detTwo[0][0] = 2 61 | if det := detTwo.Determinant(); det != (2*1 - 0*0) { 62 | t.Errorf("Wrong determinant: %f", det) 63 | } 64 | } 65 | 66 | func TestDeterminant_3(t *testing.T) { 67 | scale2 := Ident.Scaled(2) 68 | if det := scale2.Determinant(); det != (2*2 - 0*0) { 69 | t.Errorf("Wrong determinant: %f", det) 70 | } 71 | } 72 | 73 | func TestDeterminant_4(t *testing.T) { 74 | row1changed, _ := Parse("3 0 2 2") 75 | if det := row1changed.Determinant(); det != (3*2 - 0*2) { 76 | t.Errorf("Wrong determinant: %f", det) 77 | } 78 | } 79 | 80 | func TestDeterminant_5(t *testing.T) { 81 | row12changed, _ := Parse("3 1 2 5") 82 | if det := row12changed.Determinant(); det != (3*5 - 1*2) { 83 | t.Errorf("Wrong determinant: %f", det) 84 | } 85 | } 86 | 87 | func TestDeterminant_7(t *testing.T) { 88 | randomMatrix, err := Parse("0.43685 0.81673 0.16600 0.40608") 89 | randomMatrix.Transpose() 90 | if err != nil { 91 | t.Errorf("Could not parse random matrix: %v", err) 92 | } 93 | if det := randomMatrix.Determinant(); PracticallyEquals(det, 0.0418189) { 94 | t.Errorf("Wrong determinant for random sub 3x3 matrix: %f", det) 95 | } 96 | } 97 | 98 | func PracticallyEquals(value1 float64, value2 float64) bool { 99 | return math.Abs(value1-value2) > EPSILON 100 | } 101 | 102 | func TestDeterminant_6(t *testing.T) { 103 | row123changed := row123Changed 104 | if det := row123changed.Determinant(); det != (3*5 - 2*1) { 105 | t.Errorf("Wrong determinant for 3x3 matrix: %f", det) 106 | } 107 | } 108 | 109 | func TestDeterminant_1(t *testing.T) { 110 | detId := Ident.Determinant() 111 | if detId != 1 { 112 | t.Errorf("Wrong determinant for identity matrix: %f", detId) 113 | } 114 | } 115 | 116 | func TestInvert_ok(t *testing.T) { 117 | inv := invertableMatrix1 118 | _, err := inv.Invert() 119 | 120 | if err != nil { 121 | t.Error("Inverse not computed correctly", err) 122 | } 123 | 124 | invExpected := invertedMatrix1 125 | if inv != invExpected { 126 | t.Errorf("Inverse not computed correctly: %#v", inv) 127 | } 128 | } 129 | 130 | func TestInvert_nok_1(t *testing.T) { 131 | inv := nonInvertableMatrix1 132 | _, err := inv.Inverted() 133 | if err == nil { 134 | t.Error("Inverse should not be possible", err) 135 | } 136 | } 137 | 138 | func TestInvert_nok_2(t *testing.T) { 139 | inv := nonInvertableMatrix2 140 | _, err := inv.Inverted() 141 | if err == nil { 142 | t.Error("Inverse should not be possible", err) 143 | } 144 | } 145 | 146 | func TestIsZeroEps(t *testing.T) { 147 | tests := []struct { 148 | name string 149 | mat T 150 | epsilon float64 151 | want bool 152 | }{ 153 | {"exact zero", Zero, 0.0001, true}, 154 | {"within epsilon", T{vec2.T{0.00001, -0.00001}, vec2.T{0.00001, -0.00001}}, 0.0001, true}, 155 | {"at epsilon boundary", T{vec2.T{0.0001, 0.0001}, vec2.T{0.0001, 0.0001}}, 0.0001, true}, 156 | {"outside epsilon", T{vec2.T{0.001, 0}, vec2.T{0, 0}}, 0.0001, false}, 157 | {"one element outside", T{vec2.T{0.00001, 0.00001}, vec2.T{0.001, 0.00001}}, 0.0001, false}, 158 | {"negative outside epsilon", T{vec2.T{-0.001, 0}, vec2.T{0, 0}}, 0.0001, false}, 159 | {"identity matrix", Ident, 0.0001, false}, 160 | } 161 | 162 | for _, tt := range tests { 163 | t.Run(tt.name, func(t *testing.T) { 164 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want { 165 | t.Errorf("IsZeroEps() = %v, want %v for mat %v with epsilon %v", got, tt.want, tt.mat, tt.epsilon) 166 | } 167 | }) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /float64/quaternion/quaternion_test.go: -------------------------------------------------------------------------------- 1 | package quaternion 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | 8 | "github.com/ungerik/go3d/float64/vec3" 9 | ) 10 | 11 | // RotateVec3 rotates v by the rotation represented by the quaternion. 12 | func rotateAndNormalizeVec3(quat *T, v *vec3.T) { 13 | qv := T{v[0], v[1], v[2], 0} 14 | inv := quat.Inverted() 15 | q := Mul3(quat, &qv, &inv) 16 | v[0] = q[0] 17 | v[1] = q[1] 18 | v[2] = q[2] 19 | } 20 | 21 | func TestQuaternionRotateVec3(t *testing.T) { 22 | eulerAngles := []vec3.T{ 23 | {90, 20, 21}, 24 | {-90, 0, 0}, 25 | {28, 1043, -38}, 26 | } 27 | vecs := []vec3.T{ 28 | {2, 3, 4}, 29 | {1, 3, -2}, 30 | {-6, 2, 9}, 31 | } 32 | for _, vec := range vecs { 33 | for _, eulerAngle := range eulerAngles { 34 | func() { 35 | q := FromEulerAngles(eulerAngle[1]*math.Pi/180.0, eulerAngle[0]*math.Pi/180.0, eulerAngle[2]*math.Pi/180.0) 36 | vec_r1 := vec 37 | vec_r2 := vec 38 | magSqr := vec_r1.LengthSqr() 39 | rotateAndNormalizeVec3(&q, &vec_r2) 40 | q.RotateVec3(&vec_r1) 41 | vecd := q.RotatedVec3(&vec) 42 | magSqr2 := vec_r1.LengthSqr() 43 | 44 | if !vecd.PracticallyEquals(&vec_r1, 0.000000000000001) { 45 | t.Logf("test case %v rotates %v failed - vector rotation: %+v, %+v\n", eulerAngle, vec, vecd, vec_r1) 46 | t.Fail() 47 | } 48 | 49 | angle := vec3.Angle(&vec_r1, &vec_r2) 50 | length := math.Abs(magSqr - magSqr2) 51 | 52 | if angle > 0.0000001 { 53 | t.Logf("test case %v rotates %v failed - angle difference to large\n", eulerAngle, vec) 54 | t.Logf("vectors: %+v, %+v\n", vec_r1, vec_r2) 55 | t.Logf("angle: %v\n", angle) 56 | t.Fail() 57 | } 58 | 59 | if length > 0.000000000001 { 60 | t.Logf("test case %v rotates %v failed - squared length difference to large\n", eulerAngle, vec) 61 | t.Logf("vectors: %+v %+v\n", vec_r1, vec_r2) 62 | t.Logf("squared lengths: %v, %v\n", magSqr, magSqr2) 63 | t.Fail() 64 | } 65 | }() 66 | } 67 | } 68 | } 69 | 70 | func TestToEulerAngles(t *testing.T) { 71 | specialValues := []float64{-5, -math.Pi, -2, -math.Pi / 2, 0, math.Pi / 2, 2.4, math.Pi, 3.9} 72 | for _, x := range specialValues { 73 | for _, y := range specialValues { 74 | for _, z := range specialValues { 75 | quat1 := FromEulerAngles(y, x, z) 76 | ry, rx, rz := quat1.ToEulerAngles() 77 | quat2 := FromEulerAngles(ry, rx, rz) 78 | // quat must be equivalent 79 | const e64 = 1e-14 80 | 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 81 | 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 82 | if !cond1 && !cond2 { 83 | fmt.Printf("test case %v, %v, %v failed\n", x, y, z) 84 | fmt.Printf("result is %v, %v, %v\n", rx, ry, rz) 85 | fmt.Printf("quat1 is %v\n", quat1) 86 | fmt.Printf("quat2 is %v\n", quat2) 87 | t.Fail() 88 | } 89 | } 90 | } 91 | } 92 | } 93 | 94 | func TestNormalizeEdgeCases(t *testing.T) { 95 | tests := []struct { 96 | name string 97 | quat T 98 | checkNorm bool 99 | }{ 100 | {"zero quaternion", T{0, 0, 0, 0}, false}, 101 | {"tiny quaternion (below epsilon)", T{1e-16, 1e-16, 1e-16, 1e-16}, false}, 102 | {"already normalized", T{1, 0, 0, 0}, true}, 103 | {"nearly normalized positive deviation", T{1.00000000001, 0, 0, 0}, true}, 104 | {"nearly normalized negative deviation", T{0.99999999999, 0, 0, 0}, true}, 105 | {"nearly normalized mixed", T{0.5, 0.5, 0.5, 0.5}, true}, // sqrt(4*0.25) = 1 106 | {"needs normalization", T{2, 0, 0, 0}, true}, 107 | {"needs normalization mixed", T{1, 1, 1, 1}, true}, 108 | } 109 | 110 | for _, tt := range tests { 111 | t.Run(tt.name, func(t *testing.T) { 112 | original := tt.quat 113 | originalNorm := original.Norm() 114 | result := tt.quat.Normalize() 115 | 116 | if result != &tt.quat { 117 | t.Errorf("Normalize() should return pointer to quat") 118 | } 119 | 120 | if tt.checkNorm { 121 | norm := tt.quat.Norm() 122 | // For unit quaternions, norm (squared magnitude) should be 1 123 | if math.Abs(norm-1.0) > 0.00001 { 124 | t.Errorf("After Normalize(), Norm() = %v, want 1.0 (original norm=%v)", norm, originalNorm) 125 | } 126 | } 127 | }) 128 | } 129 | } 130 | 131 | func TestNormalizedEdgeCases(t *testing.T) { 132 | tests := []struct { 133 | name string 134 | quat T 135 | checkNorm bool 136 | }{ 137 | {"zero quaternion", T{0, 0, 0, 0}, false}, 138 | {"tiny quaternion", T{1e-16, 1e-16, 1e-16, 1e-16}, false}, 139 | {"already normalized", T{1, 0, 0, 0}, true}, 140 | {"needs normalization", T{2, 0, 0, 0}, true}, 141 | {"needs normalization mixed", T{1, 1, 1, 1}, true}, 142 | } 143 | 144 | for _, tt := range tests { 145 | t.Run(tt.name, func(t *testing.T) { 146 | original := tt.quat 147 | result := tt.quat.Normalized() 148 | 149 | if tt.quat != original { 150 | t.Errorf("Normalized() modified original quaternion") 151 | } 152 | 153 | if tt.checkNorm { 154 | norm := result.Norm() 155 | if math.Abs(norm-1.0) > 0.00001 { 156 | t.Errorf("Normalized().Norm() = %v, want 1.0", norm) 157 | } 158 | } 159 | }) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /float64/mat4/mat4_test.go: -------------------------------------------------------------------------------- 1 | package mat4 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/ungerik/go3d/float64/vec3" 8 | "github.com/ungerik/go3d/float64/vec4" 9 | ) 10 | 11 | const EPSILON = 0.0000001 12 | 13 | func TestIsZeroEps(t *testing.T) { 14 | tests := []struct { 15 | name string 16 | mat T 17 | epsilon float64 18 | want bool 19 | }{ 20 | {"exact zero", Zero, 0.0001, true}, 21 | {"within epsilon", T{vec4.T{0.00001, -0.00001, 0.00001, -0.00001}, vec4.T{-0.00001, 0.00001, -0.00001, 0.00001}, vec4.T{0.00001, -0.00001, 0.00001, -0.00001}, vec4.T{-0.00001, 0.00001, -0.00001, 0.00001}}, 0.0001, true}, 22 | {"at epsilon boundary", T{vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}}, 0.0001, true}, 23 | {"outside epsilon", T{vec4.T{0.001, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}}, 0.0001, false}, 24 | {"one element outside", T{vec4.T{0.00001, 0.00001, 0.00001, 0.00001}, vec4.T{0.001, 0.00001, 0.00001, 0.00001}, vec4.T{0.00001, 0.00001, 0.00001, 0.00001}, vec4.T{0.00001, 0.00001, 0.00001, 0.00001}}, 0.0001, false}, 25 | {"negative outside epsilon", T{vec4.T{-0.001, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}}, 0.0001, false}, 26 | {"identity matrix", Ident, 0.0001, false}, 27 | } 28 | 29 | for _, tt := range tests { 30 | t.Run(tt.name, func(t *testing.T) { 31 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want { 32 | t.Errorf("IsZeroEps() = %v, want %v for mat with epsilon %v", got, tt.want, tt.epsilon) 33 | } 34 | }) 35 | } 36 | } 37 | 38 | func TestScale(t *testing.T) { 39 | m := Ident 40 | m.Scale(2.5) 41 | 42 | expected := Ident 43 | expected[0][0] = 2.5 44 | expected[1][1] = 2.5 45 | expected[2][2] = 2.5 46 | 47 | if m != expected { 48 | t.Errorf("Scale() failed: got %v, want %v", m, expected) 49 | } 50 | } 51 | 52 | func TestScaled(t *testing.T) { 53 | m := Ident 54 | result := m.Scaled(2.5) 55 | 56 | // Original should be unchanged 57 | if m != Ident { 58 | t.Errorf("Scaled() modified original matrix") 59 | } 60 | 61 | expected := Ident 62 | expected[0][0] = 2.5 63 | expected[1][1] = 2.5 64 | expected[2][2] = 2.5 65 | 66 | if result != expected { 67 | t.Errorf("Scaled() failed: got %v, want %v", result, expected) 68 | } 69 | } 70 | 71 | func TestTrace(t *testing.T) { 72 | m := Ident 73 | if trace := m.Trace(); trace != 4.0 { 74 | t.Errorf("Trace() of identity should be 4.0, got %f", trace) 75 | } 76 | 77 | m[0][0] = 2 78 | m[1][1] = 3 79 | m[2][2] = 5 80 | m[3][3] = 7 81 | if trace := m.Trace(); trace != 17.0 { 82 | t.Errorf("Trace() should be 17.0, got %f", trace) 83 | } 84 | } 85 | 86 | func TestTrace3(t *testing.T) { 87 | m := Ident 88 | if trace := m.Trace3(); trace != 3.0 { 89 | t.Errorf("Trace3() of identity should be 3.0, got %f", trace) 90 | } 91 | 92 | m[0][0] = 2 93 | m[1][1] = 3 94 | m[2][2] = 5 95 | m[3][3] = 7 // Should not be included in Trace3 96 | if trace := m.Trace3(); trace != 10.0 { 97 | t.Errorf("Trace3() should be 10.0, got %f", trace) 98 | } 99 | } 100 | 101 | func TestAssignMul(t *testing.T) { 102 | m1, _ := Parse("1 0 0 0 0 1 0 0 0 0 1 0 3 5 7 1") 103 | m2, _ := Parse("1 0 0 0 0 1 0 0 0 0 1 0 2 4 8 1") 104 | var result T 105 | result.AssignMul(&m1, &m2) 106 | expected, _ := Parse("1 0 0 0 0 1 0 0 0 0 1 0 5 9 15 1") 107 | 108 | if result != expected { 109 | t.Errorf("AssignMul() incorrect: got %v, want %v", result, expected) 110 | } 111 | } 112 | 113 | func TestMulVec4vsTransformVec4(t *testing.T) { 114 | m, _ := Parse("2 0 0 0 0 3 0 0 0 0 4 0 1 2 3 1") 115 | v := vec4.T{1, 2, 3, 1} 116 | 117 | // Method 1: MulVec4 118 | result1 := m.MulVec4(&v) 119 | 120 | // Method 2: TransformVec4 121 | result2 := v 122 | m.TransformVec4(&result2) 123 | 124 | if math.Abs(result1[0]-result2[0]) > EPSILON || 125 | math.Abs(result1[1]-result2[1]) > EPSILON || 126 | math.Abs(result1[2]-result2[2]) > EPSILON || 127 | math.Abs(result1[3]-result2[3]) > EPSILON { 128 | t.Errorf("MulVec4 and TransformVec4 differ: %v vs %v", result1, result2) 129 | } 130 | } 131 | 132 | func TestMulVec3vsTransformVec3(t *testing.T) { 133 | m, _ := Parse("2 0 0 0 0 3 0 0 0 0 4 0 1 2 3 1") 134 | v := vec3.T{1, 2, 3} 135 | 136 | // Method 1: MulVec3 137 | result1 := m.MulVec3(&v) 138 | 139 | // Method 2: TransformVec3 140 | result2 := v 141 | m.TransformVec3(&result2) 142 | 143 | if math.Abs(result1[0]-result2[0]) > EPSILON || 144 | math.Abs(result1[1]-result2[1]) > EPSILON || 145 | math.Abs(result1[2]-result2[2]) > EPSILON { 146 | t.Errorf("MulVec3 and TransformVec3 differ: %v vs %v", result1, result2) 147 | } 148 | } 149 | 150 | func TestTranspose(t *testing.T) { 151 | m, _ := Parse("1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16") 152 | original := m 153 | m.Transpose() 154 | 155 | for i := 0; i < 4; i++ { 156 | for j := 0; j < 4; j++ { 157 | if m[i][j] != original[j][i] { 158 | t.Errorf("Transpose failed at [%d][%d]: got %f, want %f", i, j, m[i][j], original[j][i]) 159 | } 160 | } 161 | } 162 | } 163 | 164 | func TestParseAndString(t *testing.T) { 165 | original := "1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16" 166 | m, err := Parse(original) 167 | if err != nil { 168 | t.Errorf("Parse() failed: %v", err) 169 | } 170 | 171 | // Parse and stringify should preserve values 172 | m2, err := Parse(m.String()) 173 | if err != nil { 174 | t.Errorf("Parse() of String() failed: %v", err) 175 | } 176 | 177 | for i := 0; i < 4; i++ { 178 | for j := 0; j < 4; j++ { 179 | if math.Abs(m[i][j]-m2[i][j]) > EPSILON { 180 | t.Errorf("Parse/String roundtrip failed at [%d][%d]", i, j) 181 | } 182 | } 183 | } 184 | } 185 | 186 | func TestIdentity(t *testing.T) { 187 | // Identity matrix should have 1s on diagonal, 0s elsewhere 188 | for i := 0; i < 4; i++ { 189 | for j := 0; j < 4; j++ { 190 | expected := 0.0 191 | if i == j { 192 | expected = 1.0 193 | } 194 | if Ident[i][j] != expected { 195 | t.Errorf("Identity matrix incorrect at [%d][%d]: got %f, want %f", i, j, Ident[i][j], expected) 196 | } 197 | } 198 | } 199 | } 200 | 201 | func TestZero(t *testing.T) { 202 | // Zero matrix should have all 0s 203 | for i := 0; i < 4; i++ { 204 | for j := 0; j < 4; j++ { 205 | if Zero[i][j] != 0 { 206 | t.Errorf("Zero matrix incorrect at [%d][%d]: got %f, want 0", i, j, Zero[i][j]) 207 | } 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /float64/hermit2/hermit2_test.go: -------------------------------------------------------------------------------- 1 | package hermit2 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/ungerik/go3d/float64/vec2" 8 | ) 9 | 10 | const EPSILON = 0.0000001 11 | 12 | func TestParseAndString(t *testing.T) { 13 | original := T{ 14 | A: PointTangent{ 15 | Point: vec2.T{1.0, 2.0}, 16 | Tangent: vec2.T{0.5, 0.5}, 17 | }, 18 | B: PointTangent{ 19 | Point: vec2.T{3.0, 4.0}, 20 | Tangent: vec2.T{-0.5, 0.5}, 21 | }, 22 | } 23 | 24 | // Convert to string and parse back 25 | str := original.String() 26 | parsed, err := Parse(str) 27 | if err != nil { 28 | t.Errorf("Parse() failed: %v", err) 29 | } 30 | 31 | // Check all components 32 | if math.Abs(parsed.A.Point[0]-original.A.Point[0]) > EPSILON || 33 | math.Abs(parsed.A.Point[1]-original.A.Point[1]) > EPSILON || 34 | math.Abs(parsed.A.Tangent[0]-original.A.Tangent[0]) > EPSILON || 35 | math.Abs(parsed.A.Tangent[1]-original.A.Tangent[1]) > EPSILON || 36 | math.Abs(parsed.B.Point[0]-original.B.Point[0]) > EPSILON || 37 | math.Abs(parsed.B.Point[1]-original.B.Point[1]) > EPSILON || 38 | math.Abs(parsed.B.Tangent[0]-original.B.Tangent[0]) > EPSILON || 39 | math.Abs(parsed.B.Tangent[1]-original.B.Tangent[1]) > EPSILON { 40 | t.Errorf("Parse/String roundtrip failed: got %v, want %v", parsed, original) 41 | } 42 | } 43 | 44 | func TestPointEndpoints(t *testing.T) { 45 | // Create a simple hermit spline from (0,0) to (1,1) with tangents pointing in the same direction 46 | herm := T{ 47 | A: PointTangent{ 48 | Point: vec2.T{0.0, 0.0}, 49 | Tangent: vec2.T{1.0, 1.0}, 50 | }, 51 | B: PointTangent{ 52 | Point: vec2.T{1.0, 1.0}, 53 | Tangent: vec2.T{1.0, 1.0}, 54 | }, 55 | } 56 | 57 | // At t=0, should return pointA 58 | p0 := herm.Point(0.0) 59 | if math.Abs(p0[0]-herm.A.Point[0]) > EPSILON || 60 | math.Abs(p0[1]-herm.A.Point[1]) > EPSILON { 61 | t.Errorf("Point(0) should equal A.Point: got %v, want %v", p0, herm.A.Point) 62 | } 63 | 64 | // At t=1, should return pointB 65 | p1 := herm.Point(1.0) 66 | if math.Abs(p1[0]-herm.B.Point[0]) > EPSILON || 67 | math.Abs(p1[1]-herm.B.Point[1]) > EPSILON { 68 | t.Errorf("Point(1) should equal B.Point: got %v, want %v", p1, herm.B.Point) 69 | } 70 | } 71 | 72 | func TestPointMidpoint(t *testing.T) { 73 | // Horizontal line from (0,0) to (2,0) with horizontal tangents 74 | herm := T{ 75 | A: PointTangent{ 76 | Point: vec2.T{0.0, 0.0}, 77 | Tangent: vec2.T{1.0, 0.0}, 78 | }, 79 | B: PointTangent{ 80 | Point: vec2.T{2.0, 0.0}, 81 | Tangent: vec2.T{1.0, 0.0}, 82 | }, 83 | } 84 | 85 | // At t=0.5, should be approximately at (1, 0) for a straight line 86 | p := herm.Point(0.5) 87 | 88 | // The y-coordinate should be exactly 0 for horizontal tangents 89 | if math.Abs(p[1]) > EPSILON { 90 | t.Errorf("Point(0.5) y-coordinate should be 0: got %v", p[1]) 91 | } 92 | 93 | // The x-coordinate should be approximately 1.0 94 | if math.Abs(p[0]-1.0) > EPSILON { 95 | t.Errorf("Point(0.5) x-coordinate should be ~1.0: got %v", p[0]) 96 | } 97 | } 98 | 99 | func TestPointFunctionMatchesMethod(t *testing.T) { 100 | herm := T{ 101 | A: PointTangent{ 102 | Point: vec2.T{0.0, 0.0}, 103 | Tangent: vec2.T{1.0, 0.5}, 104 | }, 105 | B: PointTangent{ 106 | Point: vec2.T{2.0, 1.0}, 107 | Tangent: vec2.T{1.0, -0.5}, 108 | }, 109 | } 110 | 111 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0} 112 | for _, tVal := range tests { 113 | // Method call 114 | p1 := herm.Point(tVal) 115 | 116 | // Function call 117 | p2 := Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 118 | 119 | if math.Abs(p1[0]-p2[0]) > EPSILON || 120 | math.Abs(p1[1]-p2[1]) > EPSILON { 121 | t.Errorf("Point method and function differ at t=%f: method=%v, func=%v", tVal, p1, p2) 122 | } 123 | } 124 | } 125 | 126 | func TestTangentAtZero(t *testing.T) { 127 | herm := T{ 128 | A: PointTangent{ 129 | Point: vec2.T{0.0, 0.0}, 130 | Tangent: vec2.T{1.0, 0.5}, 131 | }, 132 | B: PointTangent{ 133 | Point: vec2.T{2.0, 1.0}, 134 | Tangent: vec2.T{0.5, 1.0}, 135 | }, 136 | } 137 | 138 | // At t=0, the Tangent function returns tangentA (verified by examining the formula at t=0) 139 | tan0 := herm.Tangent(0.0) 140 | expectedTan0 := herm.A.Tangent 141 | if math.Abs(tan0[0]-expectedTan0[0]) > EPSILON || 142 | math.Abs(tan0[1]-expectedTan0[1]) > EPSILON { 143 | t.Errorf("Tangent(0) should equal A.Tangent: got %v, want %v", tan0, expectedTan0) 144 | } 145 | } 146 | 147 | func TestTangentFunctionMatchesMethod(t *testing.T) { 148 | herm := T{ 149 | A: PointTangent{ 150 | Point: vec2.T{0.0, 0.0}, 151 | Tangent: vec2.T{1.0, 0.5}, 152 | }, 153 | B: PointTangent{ 154 | Point: vec2.T{2.0, 1.0}, 155 | Tangent: vec2.T{0.5, 1.0}, 156 | }, 157 | } 158 | 159 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0} 160 | for _, tVal := range tests { 161 | // Method call 162 | tan1 := herm.Tangent(tVal) 163 | 164 | // Function call 165 | tan2 := Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 166 | 167 | if math.Abs(tan1[0]-tan2[0]) > EPSILON || 168 | math.Abs(tan1[1]-tan2[1]) > EPSILON { 169 | t.Errorf("Tangent method and function differ at t=%f: method=%v, func=%v", tVal, tan1, tan2) 170 | } 171 | } 172 | } 173 | 174 | func TestLengthValues(t *testing.T) { 175 | herm := T{ 176 | A: PointTangent{ 177 | Point: vec2.T{0.0, 0.0}, 178 | Tangent: vec2.T{1.0, 0.0}, 179 | }, 180 | B: PointTangent{ 181 | Point: vec2.T{2.0, 0.0}, 182 | Tangent: vec2.T{1.0, 0.0}, 183 | }, 184 | } 185 | 186 | // Test that Length returns a valid positive number 187 | length0 := herm.Length(0.0) 188 | if math.IsNaN(length0) || math.IsInf(length0, 0) { 189 | t.Errorf("Length(0) should be a valid number: got %f", length0) 190 | } 191 | 192 | length1 := herm.Length(1.0) 193 | if math.IsNaN(length1) || math.IsInf(length1, 0) { 194 | t.Errorf("Length(1) should be a valid number: got %f", length1) 195 | } 196 | } 197 | 198 | func TestLengthFunctionMatchesMethod(t *testing.T) { 199 | herm := T{ 200 | A: PointTangent{ 201 | Point: vec2.T{0.0, 0.0}, 202 | Tangent: vec2.T{1.0, 0.5}, 203 | }, 204 | B: PointTangent{ 205 | Point: vec2.T{2.0, 1.0}, 206 | Tangent: vec2.T{0.5, 1.0}, 207 | }, 208 | } 209 | 210 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0} 211 | for _, tVal := range tests { 212 | // Method call 213 | len1 := herm.Length(tVal) 214 | 215 | // Function call 216 | len2 := Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 217 | 218 | if math.Abs(len1-len2) > EPSILON { 219 | t.Errorf("Length method and function differ at t=%f: method=%f, func=%f", tVal, len1, len2) 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /float64/mat2/mat2.go: -------------------------------------------------------------------------------- 1 | // Package mat2 contains a 2x2 float64 matrix type T and functions. 2 | package mat2 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "math" 8 | 9 | "github.com/ungerik/go3d/float64/generic" 10 | "github.com/ungerik/go3d/float64/vec2" 11 | ) 12 | 13 | var ( 14 | // Zero holds a zero matrix. 15 | Zero = T{ 16 | vec2.T{0, 0}, 17 | vec2.T{0, 0}, 18 | } 19 | 20 | // Ident holds an ident matrix. 21 | Ident = T{ 22 | vec2.T{1, 0}, 23 | vec2.T{0, 1}, 24 | } 25 | ) 26 | 27 | // T represents a 2x2 matrix. 28 | type T [2]vec2.T 29 | 30 | // From copies a T from a generic.T implementation. 31 | func From(other generic.T) T { 32 | r := Ident 33 | cols := other.Cols() 34 | rows := other.Rows() 35 | if (cols == 3 && rows == 3) || (cols == 4 && rows == 4) { 36 | cols = 2 37 | rows = 2 38 | } else if !(cols == 2 && rows == 2) { 39 | panic("Unsupported type") 40 | } 41 | for col := 0; col < cols; col++ { 42 | for row := 0; row < rows; row++ { 43 | r[col][row] = other.Get(col, row) 44 | } 45 | } 46 | return r 47 | } 48 | 49 | // Parse parses T from a string. See also String() 50 | func Parse(s string) (r T, err error) { 51 | _, err = fmt.Sscan(s, 52 | &r[0][0], &r[0][1], 53 | &r[1][0], &r[1][1], 54 | ) 55 | return r, err 56 | } 57 | 58 | // String formats T as string. See also Parse(). 59 | func (mat *T) String() string { 60 | return fmt.Sprintf("%s %s", mat[0].String(), mat[1].String()) 61 | } 62 | 63 | // Rows returns the number of rows of the matrix. 64 | func (mat *T) Rows() int { 65 | return 2 66 | } 67 | 68 | // Cols returns the number of columns of the matrix. 69 | func (mat *T) Cols() int { 70 | return 2 71 | } 72 | 73 | // Size returns the number elements of the matrix. 74 | func (mat *T) Size() int { 75 | return 4 76 | } 77 | 78 | // Slice returns the elements of the matrix as slice. 79 | // The data may be a copy depending on the platform implementation. 80 | func (mat *T) Slice() []float64 { 81 | return mat.Array()[:] 82 | } 83 | 84 | // Get returns one element of the matrix. 85 | // Matrices are defined by (two) column vectors. 86 | // 87 | // Note that this function use the opposite reference order of rows and columns to the mathematical matrix indexing. 88 | // 89 | // A value in this matrix is referenced by where both row and column is in the range [0..1]. 90 | // This notation and range reflects the underlying representation. 91 | // 92 | // A value in a matrix A is mathematically referenced by A 93 | // where both row and column is in the range [1..2]. 94 | // (It is really the lower case 'a' followed by but this documentation syntax is somewhat limited.) 95 | // 96 | // matrixA.Get(0, 1) == matrixA[0][1] ( == A21 in mathematical indexing) 97 | func (mat *T) Get(col, row int) float64 { 98 | return mat[col][row] 99 | } 100 | 101 | // IsZero checks if all elements of the matrix are exactly zero. 102 | // Uses exact equality comparison, which may not be suitable for floating-point math results. 103 | // For tolerance-based comparison, use IsZeroEps instead. 104 | func (mat *T) IsZero() bool { 105 | return *mat == Zero 106 | } 107 | 108 | // IsZeroEps checks if all elements of the matrix are zero within the given epsilon tolerance. 109 | // This is the recommended method for comparing floating-point matrices that result from calculations. 110 | // For exact zero comparison, use IsZero instead. 111 | func (mat *T) IsZeroEps(epsilon float64) bool { 112 | return math.Abs(mat[0][0]) <= epsilon && math.Abs(mat[0][1]) <= epsilon && 113 | math.Abs(mat[1][0]) <= epsilon && math.Abs(mat[1][1]) <= epsilon 114 | } 115 | 116 | // Scale multiplies the diagonal scale elements by f returns mat. 117 | func (mat *T) Scale(f float64) *T { 118 | mat[0][0] *= f 119 | mat[1][1] *= f 120 | return mat 121 | } 122 | 123 | // Scaled returns a copy of the matrix with the diagonal scale elements multiplied by f. 124 | func (mat *T) Scaled(f float64) T { 125 | r := *mat 126 | return *r.Scale(f) 127 | } 128 | 129 | // Scaling returns the scaling diagonal of the matrix. 130 | func (mat *T) Scaling() vec2.T { 131 | return vec2.T{mat[0][0], mat[1][1]} 132 | } 133 | 134 | // SetScaling sets the scaling diagonal of the matrix. 135 | func (mat *T) SetScaling(s *vec2.T) *T { 136 | mat[0][0] = s[0] 137 | mat[1][1] = s[1] 138 | return mat 139 | } 140 | 141 | // Trace returns the trace value for the matrix. 142 | func (mat *T) Trace() float64 { 143 | return mat[0][0] + mat[1][1] 144 | } 145 | 146 | // AssignMul multiplies a and b and assigns the result to mat. 147 | func (mat *T) AssignMul(a, b *T) *T { 148 | mat[0] = a.MulVec2(&b[0]) 149 | mat[1] = a.MulVec2(&b[1]) 150 | return mat 151 | } 152 | 153 | // MulVec2 multiplies vec with mat. 154 | func (mat *T) MulVec2(vec *vec2.T) vec2.T { 155 | return vec2.T{ 156 | mat[0][0]*vec[0] + mat[1][0]*vec[1], 157 | mat[0][1]*vec[1] + mat[1][1]*vec[1], 158 | } 159 | } 160 | 161 | // TransformVec2 multiplies v with mat and saves the result in v. 162 | func (mat *T) TransformVec2(v *vec2.T) { 163 | // Use intermediate variables to not alter further computations. 164 | x := mat[0][0]*v[0] + mat[1][0]*v[1] 165 | v[1] = mat[0][1]*v[0] + mat[1][1]*v[1] 166 | v[0] = x 167 | } 168 | 169 | func (mat *T) Determinant() float64 { 170 | return mat[0][0]*mat[1][1] - mat[1][0]*mat[0][1] 171 | } 172 | 173 | // PracticallyEquals compares two matrices if they are equal with each other within a delta tolerance. 174 | func (mat *T) PracticallyEquals(matrix *T, allowedDelta float64) bool { 175 | return mat[0].PracticallyEquals(&matrix[0], allowedDelta) && 176 | mat[1].PracticallyEquals(&matrix[1], allowedDelta) 177 | } 178 | 179 | // Transpose transposes the matrix. 180 | func (mat *T) Transpose() *T { 181 | mat[0][1], mat[1][0] = mat[1][0], mat[0][1] 182 | return mat 183 | } 184 | 185 | // Transposed returns a transposed copy the matrix. 186 | func (mat *T) Transposed() T { 187 | result := *mat 188 | result.Transpose() 189 | return result 190 | } 191 | 192 | // Invert inverts the given matrix. Destructive operation. 193 | // Does not check if matrix is singular and may lead to strange results! 194 | func (mat *T) Invert() (*T, error) { 195 | determinant := mat.Determinant() 196 | if determinant == 0 { 197 | return &Zero, errors.New("can not create inverted matrix as determinant is 0") 198 | } 199 | 200 | invDet := 1.0 / determinant 201 | 202 | mat[0][0], mat[1][1] = invDet*mat[1][1], invDet*mat[0][0] 203 | mat[0][1] = -invDet * mat[0][1] 204 | mat[1][0] = -invDet * mat[1][0] 205 | 206 | return mat, nil 207 | } 208 | 209 | // Inverted inverts a copy of the given matrix. 210 | // Does not check if matrix is singular and may lead to strange results! 211 | func (mat *T) Inverted() (T, error) { 212 | result := *mat 213 | _, err := result.Invert() 214 | return result, err 215 | } 216 | -------------------------------------------------------------------------------- /mat2/mat2.go: -------------------------------------------------------------------------------- 1 | // Package mat2 contains a 2x2 float32 matrix type T and functions. 2 | package mat2 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | 8 | math "github.com/chewxy/math32" 9 | "github.com/ungerik/go3d/generic" 10 | "github.com/ungerik/go3d/vec2" 11 | ) 12 | 13 | var ( 14 | // Zero holds a zero matrix. 15 | Zero = T{ 16 | vec2.T{0, 0}, 17 | vec2.T{0, 0}, 18 | } 19 | 20 | // Ident holds an ident matrix. 21 | Ident = T{ 22 | vec2.T{1, 0}, 23 | vec2.T{0, 1}, 24 | } 25 | ) 26 | 27 | // T represents a 2x2 matrix. 28 | type T [2]vec2.T 29 | 30 | // From copies a T from a generic.T implementation. 31 | func From(other generic.T) T { 32 | r := Ident 33 | cols := other.Cols() 34 | rows := other.Rows() 35 | if (cols == 3 && rows == 3) || (cols == 4 && rows == 4) { 36 | cols = 2 37 | rows = 2 38 | } else if !(cols == 2 && rows == 2) { 39 | panic("Unsupported type") 40 | } 41 | for col := 0; col < cols; col++ { 42 | for row := 0; row < rows; row++ { 43 | r[col][row] = other.Get(col, row) 44 | } 45 | } 46 | return r 47 | } 48 | 49 | // Parse parses T from a string. See also String() 50 | func Parse(s string) (r T, err error) { 51 | _, err = fmt.Sscan(s, 52 | &r[0][0], &r[0][1], 53 | &r[1][0], &r[1][1], 54 | ) 55 | return r, err 56 | } 57 | 58 | // String formats T as string. See also Parse(). 59 | func (mat *T) String() string { 60 | return fmt.Sprintf("%s %s", mat[0].String(), mat[1].String()) 61 | } 62 | 63 | // Rows returns the number of rows of the matrix. 64 | func (mat *T) Rows() int { 65 | return 2 66 | } 67 | 68 | // Cols returns the number of columns of the matrix. 69 | func (mat *T) Cols() int { 70 | return 2 71 | } 72 | 73 | // Size returns the number elements of the matrix. 74 | func (mat *T) Size() int { 75 | return 4 76 | } 77 | 78 | // Slice returns the elements of the matrix as slice. 79 | // The data may be a copy depending on the platform implementation. 80 | func (mat *T) Slice() []float32 { 81 | return mat.Array()[:] 82 | } 83 | 84 | // Get returns one element of the matrix. 85 | // Matrices are defined by (two) column vectors. 86 | // 87 | // Note that this function use the opposite reference order of rows and columns to the mathematical matrix indexing. 88 | // 89 | // A value in this matrix is referenced by where both row and column is in the range [0..1]. 90 | // This notation and range reflects the underlying representation. 91 | // 92 | // A value in a matrix A is mathematically referenced by A 93 | // where both row and column is in the range [1..2]. 94 | // (It is really the lower case 'a' followed by but this documentation syntax is somewhat limited.) 95 | // 96 | // matrixA.Get(0, 1) == matrixA[0][1] ( == A21 in mathematical indexing) 97 | func (mat *T) Get(col, row int) float32 { 98 | return mat[col][row] 99 | } 100 | 101 | // IsZero checks if all elements of the matrix are exactly zero. 102 | // Uses exact equality comparison, which may not be suitable for floating-point math results. 103 | // For tolerance-based comparison, use IsZeroEps instead. 104 | func (mat *T) IsZero() bool { 105 | return *mat == Zero 106 | } 107 | 108 | // IsZeroEps checks if all elements of the matrix are zero within the given epsilon tolerance. 109 | // This is the recommended method for comparing floating-point matrices that result from calculations. 110 | // For exact zero comparison, use IsZero instead. 111 | func (mat *T) IsZeroEps(epsilon float32) bool { 112 | return math.Abs(mat[0][0]) <= epsilon && math.Abs(mat[0][1]) <= epsilon && 113 | math.Abs(mat[1][0]) <= epsilon && math.Abs(mat[1][1]) <= epsilon 114 | } 115 | 116 | // Scale multiplies the diagonal scale elements by f returns mat. 117 | func (mat *T) Scale(f float32) *T { 118 | mat[0][0] *= f 119 | mat[1][1] *= f 120 | return mat 121 | } 122 | 123 | // Scaled returns a copy of the matrix with the diagonal scale elements multiplied by f. 124 | func (mat *T) Scaled(f float32) T { 125 | r := *mat 126 | return *r.Scale(f) 127 | } 128 | 129 | // Scaling returns the scaling diagonal of the matrix. 130 | func (mat *T) Scaling() vec2.T { 131 | return vec2.T{mat[0][0], mat[1][1]} 132 | } 133 | 134 | // SetScaling sets the scaling diagonal of the matrix. 135 | func (mat *T) SetScaling(s *vec2.T) *T { 136 | mat[0][0] = s[0] 137 | mat[1][1] = s[1] 138 | return mat 139 | } 140 | 141 | // Trace returns the trace value for the matrix. 142 | func (mat *T) Trace() float32 { 143 | return mat[0][0] + mat[1][1] 144 | } 145 | 146 | // AssignMul multiplies a and b and assigns the result to mat. 147 | func (mat *T) AssignMul(a, b *T) *T { 148 | mat[0] = a.MulVec2(&b[0]) 149 | mat[1] = a.MulVec2(&b[1]) 150 | return mat 151 | } 152 | 153 | // MulVec2 multiplies vec with mat. 154 | func (mat *T) MulVec2(vec *vec2.T) vec2.T { 155 | return vec2.T{ 156 | mat[0][0]*vec[0] + mat[1][0]*vec[1], 157 | mat[0][1]*vec[1] + mat[1][1]*vec[1], 158 | } 159 | } 160 | 161 | // TransformVec2 multiplies v with mat and saves the result in v. 162 | func (mat *T) TransformVec2(v *vec2.T) { 163 | // Use intermediate variables to not alter further computations. 164 | x := mat[0][0]*v[0] + mat[1][0]*v[1] 165 | v[1] = mat[0][1]*v[0] + mat[1][1]*v[1] 166 | v[0] = x 167 | } 168 | 169 | func (mat *T) Determinant() float32 { 170 | return mat[0][0]*mat[1][1] - mat[1][0]*mat[0][1] 171 | } 172 | 173 | // PracticallyEquals compares two matrices if they are equal with each other within a delta tolerance. 174 | func (mat *T) PracticallyEquals(matrix *T, allowedDelta float32) bool { 175 | return mat[0].PracticallyEquals(&matrix[0], allowedDelta) && 176 | mat[1].PracticallyEquals(&matrix[1], allowedDelta) 177 | } 178 | 179 | // Transpose transposes the matrix. 180 | func (mat *T) Transpose() *T { 181 | mat[0][1], mat[1][0] = mat[1][0], mat[0][1] 182 | return mat 183 | } 184 | 185 | // Transposed returns a transposed copy the matrix. 186 | func (mat *T) Transposed() T { 187 | result := *mat 188 | result.Transpose() 189 | return result 190 | } 191 | 192 | // Invert inverts the given matrix. Destructive operation. 193 | // Does not check if matrix is singular and may lead to strange results! 194 | func (mat *T) Invert() (*T, error) { 195 | determinant := mat.Determinant() 196 | if determinant == 0 { 197 | return &Zero, errors.New("can not create inverted matrix as determinant is 0") 198 | } 199 | 200 | invDet := 1.0 / determinant 201 | 202 | mat[0][0], mat[1][1] = invDet*mat[1][1], invDet*mat[0][0] 203 | mat[0][1] = -invDet * mat[0][1] 204 | mat[1][0] = -invDet * mat[1][0] 205 | 206 | return mat, nil 207 | } 208 | 209 | // Inverted inverts a copy of the given matrix. 210 | // Does not check if matrix is singular and may lead to strange results! 211 | func (mat *T) Inverted() (T, error) { 212 | result := *mat 213 | _, err := result.Invert() 214 | return result, err 215 | } 216 | -------------------------------------------------------------------------------- /hermit2/hermit2_test.go: -------------------------------------------------------------------------------- 1 | package hermit2 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/ungerik/go3d/vec2" 8 | ) 9 | 10 | const EPSILON = 0.0001 11 | 12 | func TestParseAndString(t *testing.T) { 13 | original := T{ 14 | A: PointTangent{ 15 | Point: vec2.T{1.0, 2.0}, 16 | Tangent: vec2.T{0.5, 0.5}, 17 | }, 18 | B: PointTangent{ 19 | Point: vec2.T{3.0, 4.0}, 20 | Tangent: vec2.T{-0.5, 0.5}, 21 | }, 22 | } 23 | 24 | // Convert to string and parse back 25 | str := original.String() 26 | parsed, err := Parse(str) 27 | if err != nil { 28 | t.Errorf("Parse() failed: %v", err) 29 | } 30 | 31 | // Check all components 32 | if math.Abs(float64(parsed.A.Point[0]-original.A.Point[0])) > EPSILON || 33 | math.Abs(float64(parsed.A.Point[1]-original.A.Point[1])) > EPSILON || 34 | math.Abs(float64(parsed.A.Tangent[0]-original.A.Tangent[0])) > EPSILON || 35 | math.Abs(float64(parsed.A.Tangent[1]-original.A.Tangent[1])) > EPSILON || 36 | math.Abs(float64(parsed.B.Point[0]-original.B.Point[0])) > EPSILON || 37 | math.Abs(float64(parsed.B.Point[1]-original.B.Point[1])) > EPSILON || 38 | math.Abs(float64(parsed.B.Tangent[0]-original.B.Tangent[0])) > EPSILON || 39 | math.Abs(float64(parsed.B.Tangent[1]-original.B.Tangent[1])) > EPSILON { 40 | t.Errorf("Parse/String roundtrip failed: got %v, want %v", parsed, original) 41 | } 42 | } 43 | 44 | func TestPointEndpoints(t *testing.T) { 45 | // Create a simple hermit spline from (0,0) to (1,1) with tangents pointing in the same direction 46 | herm := T{ 47 | A: PointTangent{ 48 | Point: vec2.T{0.0, 0.0}, 49 | Tangent: vec2.T{1.0, 1.0}, 50 | }, 51 | B: PointTangent{ 52 | Point: vec2.T{1.0, 1.0}, 53 | Tangent: vec2.T{1.0, 1.0}, 54 | }, 55 | } 56 | 57 | // At t=0, should return pointA 58 | p0 := herm.Point(0.0) 59 | if math.Abs(float64(p0[0]-herm.A.Point[0])) > EPSILON || 60 | math.Abs(float64(p0[1]-herm.A.Point[1])) > EPSILON { 61 | t.Errorf("Point(0) should equal A.Point: got %v, want %v", p0, herm.A.Point) 62 | } 63 | 64 | // At t=1, should return pointB 65 | p1 := herm.Point(1.0) 66 | if math.Abs(float64(p1[0]-herm.B.Point[0])) > EPSILON || 67 | math.Abs(float64(p1[1]-herm.B.Point[1])) > EPSILON { 68 | t.Errorf("Point(1) should equal B.Point: got %v, want %v", p1, herm.B.Point) 69 | } 70 | } 71 | 72 | func TestPointMidpoint(t *testing.T) { 73 | // Horizontal line from (0,0) to (2,0) with horizontal tangents 74 | herm := T{ 75 | A: PointTangent{ 76 | Point: vec2.T{0.0, 0.0}, 77 | Tangent: vec2.T{1.0, 0.0}, 78 | }, 79 | B: PointTangent{ 80 | Point: vec2.T{2.0, 0.0}, 81 | Tangent: vec2.T{1.0, 0.0}, 82 | }, 83 | } 84 | 85 | // At t=0.5, should be approximately at (1, 0) for a straight line 86 | p := herm.Point(0.5) 87 | 88 | // The y-coordinate should be exactly 0 for horizontal tangents 89 | if math.Abs(float64(p[1])) > EPSILON { 90 | t.Errorf("Point(0.5) y-coordinate should be 0: got %v", p[1]) 91 | } 92 | 93 | // The x-coordinate should be approximately 1.0 94 | if math.Abs(float64(p[0]-1.0)) > EPSILON { 95 | t.Errorf("Point(0.5) x-coordinate should be ~1.0: got %v", p[0]) 96 | } 97 | } 98 | 99 | func TestPointFunctionMatchesMethod(t *testing.T) { 100 | herm := T{ 101 | A: PointTangent{ 102 | Point: vec2.T{0.0, 0.0}, 103 | Tangent: vec2.T{1.0, 0.5}, 104 | }, 105 | B: PointTangent{ 106 | Point: vec2.T{2.0, 1.0}, 107 | Tangent: vec2.T{1.0, -0.5}, 108 | }, 109 | } 110 | 111 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0} 112 | for _, tVal := range tests { 113 | // Method call 114 | p1 := herm.Point(tVal) 115 | 116 | // Function call 117 | p2 := Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 118 | 119 | if math.Abs(float64(p1[0]-p2[0])) > EPSILON || 120 | math.Abs(float64(p1[1]-p2[1])) > EPSILON { 121 | t.Errorf("Point method and function differ at t=%f: method=%v, func=%v", tVal, p1, p2) 122 | } 123 | } 124 | } 125 | 126 | func TestTangentAtZero(t *testing.T) { 127 | herm := T{ 128 | A: PointTangent{ 129 | Point: vec2.T{0.0, 0.0}, 130 | Tangent: vec2.T{1.0, 0.5}, 131 | }, 132 | B: PointTangent{ 133 | Point: vec2.T{2.0, 1.0}, 134 | Tangent: vec2.T{0.5, 1.0}, 135 | }, 136 | } 137 | 138 | // At t=0, the Tangent function returns tangentA (verified by examining the formula at t=0) 139 | tan0 := herm.Tangent(0.0) 140 | expectedTan0 := herm.A.Tangent 141 | if math.Abs(float64(tan0[0]-expectedTan0[0])) > EPSILON || 142 | math.Abs(float64(tan0[1]-expectedTan0[1])) > EPSILON { 143 | t.Errorf("Tangent(0) should equal A.Tangent: got %v, want %v", tan0, expectedTan0) 144 | } 145 | } 146 | 147 | func TestTangentFunctionMatchesMethod(t *testing.T) { 148 | herm := T{ 149 | A: PointTangent{ 150 | Point: vec2.T{0.0, 0.0}, 151 | Tangent: vec2.T{1.0, 0.5}, 152 | }, 153 | B: PointTangent{ 154 | Point: vec2.T{2.0, 1.0}, 155 | Tangent: vec2.T{0.5, 1.0}, 156 | }, 157 | } 158 | 159 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0} 160 | for _, tVal := range tests { 161 | // Method call 162 | tan1 := herm.Tangent(tVal) 163 | 164 | // Function call 165 | tan2 := Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 166 | 167 | if math.Abs(float64(tan1[0]-tan2[0])) > EPSILON || 168 | math.Abs(float64(tan1[1]-tan2[1])) > EPSILON { 169 | t.Errorf("Tangent method and function differ at t=%f: method=%v, func=%v", tVal, tan1, tan2) 170 | } 171 | } 172 | } 173 | 174 | func TestLengthValues(t *testing.T) { 175 | herm := T{ 176 | A: PointTangent{ 177 | Point: vec2.T{0.0, 0.0}, 178 | Tangent: vec2.T{1.0, 0.0}, 179 | }, 180 | B: PointTangent{ 181 | Point: vec2.T{2.0, 0.0}, 182 | Tangent: vec2.T{1.0, 0.0}, 183 | }, 184 | } 185 | 186 | // Test that Length returns a valid positive number 187 | length0 := herm.Length(0.0) 188 | if math.IsNaN(float64(length0)) || math.IsInf(float64(length0), 0) { 189 | t.Errorf("Length(0) should be a valid number: got %f", length0) 190 | } 191 | 192 | length1 := herm.Length(1.0) 193 | if math.IsNaN(float64(length1)) || math.IsInf(float64(length1), 0) { 194 | t.Errorf("Length(1) should be a valid number: got %f", length1) 195 | } 196 | } 197 | 198 | func TestLengthFunctionMatchesMethod(t *testing.T) { 199 | herm := T{ 200 | A: PointTangent{ 201 | Point: vec2.T{0.0, 0.0}, 202 | Tangent: vec2.T{1.0, 0.5}, 203 | }, 204 | B: PointTangent{ 205 | Point: vec2.T{2.0, 1.0}, 206 | Tangent: vec2.T{0.5, 1.0}, 207 | }, 208 | } 209 | 210 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0} 211 | for _, tVal := range tests { 212 | // Method call 213 | len1 := herm.Length(tVal) 214 | 215 | // Function call 216 | len2 := Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 217 | 218 | if math.Abs(float64(len1-len2)) > EPSILON { 219 | t.Errorf("Length method and function differ at t=%f: method=%f, func=%f", tVal, len1, len2) 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /float64/hermit3/hermit3_test.go: -------------------------------------------------------------------------------- 1 | package hermit 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/ungerik/go3d/float64/vec3" 8 | ) 9 | 10 | const EPSILON = 0.0000001 11 | 12 | func TestParseAndString(t *testing.T) { 13 | original := T{ 14 | A: PointTangent{ 15 | Point: vec3.T{1.0, 2.0, 3.0}, 16 | Tangent: vec3.T{0.5, 0.5, 0.5}, 17 | }, 18 | B: PointTangent{ 19 | Point: vec3.T{4.0, 5.0, 6.0}, 20 | Tangent: vec3.T{-0.5, 0.5, -0.5}, 21 | }, 22 | } 23 | 24 | // Convert to string and parse back 25 | str := original.String() 26 | parsed, err := Parse(str) 27 | if err != nil { 28 | t.Errorf("Parse() failed: %v", err) 29 | } 30 | 31 | // Check all components 32 | if math.Abs(parsed.A.Point[0]-original.A.Point[0]) > EPSILON || 33 | math.Abs(parsed.A.Point[1]-original.A.Point[1]) > EPSILON || 34 | math.Abs(parsed.A.Point[2]-original.A.Point[2]) > EPSILON || 35 | math.Abs(parsed.A.Tangent[0]-original.A.Tangent[0]) > EPSILON || 36 | math.Abs(parsed.A.Tangent[1]-original.A.Tangent[1]) > EPSILON || 37 | math.Abs(parsed.A.Tangent[2]-original.A.Tangent[2]) > EPSILON || 38 | math.Abs(parsed.B.Point[0]-original.B.Point[0]) > EPSILON || 39 | math.Abs(parsed.B.Point[1]-original.B.Point[1]) > EPSILON || 40 | math.Abs(parsed.B.Point[2]-original.B.Point[2]) > EPSILON || 41 | math.Abs(parsed.B.Tangent[0]-original.B.Tangent[0]) > EPSILON || 42 | math.Abs(parsed.B.Tangent[1]-original.B.Tangent[1]) > EPSILON || 43 | math.Abs(parsed.B.Tangent[2]-original.B.Tangent[2]) > EPSILON { 44 | t.Errorf("Parse/String roundtrip failed: got %v, want %v", parsed, original) 45 | } 46 | } 47 | 48 | func TestPointEndpoints(t *testing.T) { 49 | // Create a simple hermit spline from (0,0,0) to (1,1,1) with tangents pointing in the same direction 50 | herm := T{ 51 | A: PointTangent{ 52 | Point: vec3.T{0.0, 0.0, 0.0}, 53 | Tangent: vec3.T{1.0, 1.0, 1.0}, 54 | }, 55 | B: PointTangent{ 56 | Point: vec3.T{1.0, 1.0, 1.0}, 57 | Tangent: vec3.T{1.0, 1.0, 1.0}, 58 | }, 59 | } 60 | 61 | // At t=0, should return pointA 62 | p0 := herm.Point(0.0) 63 | if math.Abs(p0[0]-herm.A.Point[0]) > EPSILON || 64 | math.Abs(p0[1]-herm.A.Point[1]) > EPSILON || 65 | math.Abs(p0[2]-herm.A.Point[2]) > EPSILON { 66 | t.Errorf("Point(0) should equal A.Point: got %v, want %v", p0, herm.A.Point) 67 | } 68 | 69 | // At t=1, should return pointB 70 | p1 := herm.Point(1.0) 71 | if math.Abs(p1[0]-herm.B.Point[0]) > EPSILON || 72 | math.Abs(p1[1]-herm.B.Point[1]) > EPSILON || 73 | math.Abs(p1[2]-herm.B.Point[2]) > EPSILON { 74 | t.Errorf("Point(1) should equal B.Point: got %v, want %v", p1, herm.B.Point) 75 | } 76 | } 77 | 78 | func TestPointMidpoint(t *testing.T) { 79 | // Line along X-axis from (0,0,0) to (2,0,0) with X-aligned tangents 80 | herm := T{ 81 | A: PointTangent{ 82 | Point: vec3.T{0.0, 0.0, 0.0}, 83 | Tangent: vec3.T{1.0, 0.0, 0.0}, 84 | }, 85 | B: PointTangent{ 86 | Point: vec3.T{2.0, 0.0, 0.0}, 87 | Tangent: vec3.T{1.0, 0.0, 0.0}, 88 | }, 89 | } 90 | 91 | // At t=0.5, should be approximately at (1, 0, 0) for a straight line 92 | p := herm.Point(0.5) 93 | 94 | // The y and z coordinates should be exactly 0 95 | if math.Abs(p[1]) > EPSILON || math.Abs(p[2]) > EPSILON { 96 | t.Errorf("Point(0.5) y,z coordinates should be 0: got %v", p) 97 | } 98 | 99 | // The x-coordinate should be approximately 1.0 100 | if math.Abs(p[0]-1.0) > EPSILON { 101 | t.Errorf("Point(0.5) x-coordinate should be ~1.0: got %v", p[0]) 102 | } 103 | } 104 | 105 | func TestPointFunctionMatchesMethod(t *testing.T) { 106 | herm := T{ 107 | A: PointTangent{ 108 | Point: vec3.T{0.0, 0.0, 0.0}, 109 | Tangent: vec3.T{1.0, 0.5, 0.2}, 110 | }, 111 | B: PointTangent{ 112 | Point: vec3.T{2.0, 1.0, 0.5}, 113 | Tangent: vec3.T{1.0, -0.5, 0.3}, 114 | }, 115 | } 116 | 117 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0} 118 | for _, tVal := range tests { 119 | // Method call 120 | p1 := herm.Point(tVal) 121 | 122 | // Function call 123 | p2 := Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 124 | 125 | if math.Abs(p1[0]-p2[0]) > EPSILON || 126 | math.Abs(p1[1]-p2[1]) > EPSILON || 127 | math.Abs(p1[2]-p2[2]) > EPSILON { 128 | t.Errorf("Point method and function differ at t=%f: method=%v, func=%v", tVal, p1, p2) 129 | } 130 | } 131 | } 132 | 133 | func TestTangentAtZero(t *testing.T) { 134 | herm := T{ 135 | A: PointTangent{ 136 | Point: vec3.T{0.0, 0.0, 0.0}, 137 | Tangent: vec3.T{1.0, 0.5, 0.2}, 138 | }, 139 | B: PointTangent{ 140 | Point: vec3.T{2.0, 1.0, 0.5}, 141 | Tangent: vec3.T{0.5, 1.0, 0.3}, 142 | }, 143 | } 144 | 145 | // At t=0, the Tangent function returns tangentA (verified by examining the formula at t=0) 146 | tan0 := herm.Tangent(0.0) 147 | expectedTan0 := herm.A.Tangent 148 | if math.Abs(tan0[0]-expectedTan0[0]) > EPSILON || 149 | math.Abs(tan0[1]-expectedTan0[1]) > EPSILON || 150 | math.Abs(tan0[2]-expectedTan0[2]) > EPSILON { 151 | t.Errorf("Tangent(0) should equal A.Tangent: got %v, want %v", tan0, expectedTan0) 152 | } 153 | } 154 | 155 | func TestTangentFunctionMatchesMethod(t *testing.T) { 156 | herm := T{ 157 | A: PointTangent{ 158 | Point: vec3.T{0.0, 0.0, 0.0}, 159 | Tangent: vec3.T{1.0, 0.5, 0.2}, 160 | }, 161 | B: PointTangent{ 162 | Point: vec3.T{2.0, 1.0, 0.5}, 163 | Tangent: vec3.T{0.5, 1.0, 0.3}, 164 | }, 165 | } 166 | 167 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0} 168 | for _, tVal := range tests { 169 | // Method call 170 | tan1 := herm.Tangent(tVal) 171 | 172 | // Function call 173 | tan2 := Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 174 | 175 | if math.Abs(tan1[0]-tan2[0]) > EPSILON || 176 | math.Abs(tan1[1]-tan2[1]) > EPSILON || 177 | math.Abs(tan1[2]-tan2[2]) > EPSILON { 178 | t.Errorf("Tangent method and function differ at t=%f: method=%v, func=%v", tVal, tan1, tan2) 179 | } 180 | } 181 | } 182 | 183 | func TestLengthValues(t *testing.T) { 184 | herm := T{ 185 | A: PointTangent{ 186 | Point: vec3.T{0.0, 0.0, 0.0}, 187 | Tangent: vec3.T{1.0, 0.0, 0.0}, 188 | }, 189 | B: PointTangent{ 190 | Point: vec3.T{2.0, 0.0, 0.0}, 191 | Tangent: vec3.T{1.0, 0.0, 0.0}, 192 | }, 193 | } 194 | 195 | // Test that Length returns a valid positive number 196 | length0 := herm.Length(0.0) 197 | if math.IsNaN(length0) || math.IsInf(length0, 0) { 198 | t.Errorf("Length(0) should be a valid number: got %f", length0) 199 | } 200 | 201 | length1 := herm.Length(1.0) 202 | if math.IsNaN(length1) || math.IsInf(length1, 0) { 203 | t.Errorf("Length(1) should be a valid number: got %f", length1) 204 | } 205 | } 206 | 207 | func TestLengthFunctionMatchesMethod(t *testing.T) { 208 | herm := T{ 209 | A: PointTangent{ 210 | Point: vec3.T{0.0, 0.0, 0.0}, 211 | Tangent: vec3.T{1.0, 0.5, 0.2}, 212 | }, 213 | B: PointTangent{ 214 | Point: vec3.T{2.0, 1.0, 0.5}, 215 | Tangent: vec3.T{0.5, 1.0, 0.3}, 216 | }, 217 | } 218 | 219 | tests := []float64{0.0, 0.25, 0.5, 0.75, 1.0} 220 | for _, tVal := range tests { 221 | // Method call 222 | len1 := herm.Length(tVal) 223 | 224 | // Function call 225 | len2 := Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 226 | 227 | if math.Abs(len1-len2) > EPSILON { 228 | t.Errorf("Length method and function differ at t=%f: method=%f, func=%f", tVal, len1, len2) 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /hermit3/hermit3_test.go: -------------------------------------------------------------------------------- 1 | package hermit 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/ungerik/go3d/vec3" 8 | ) 9 | 10 | const EPSILON = 0.0001 11 | 12 | func TestParseAndString(t *testing.T) { 13 | original := T{ 14 | A: PointTangent{ 15 | Point: vec3.T{1.0, 2.0, 3.0}, 16 | Tangent: vec3.T{0.5, 0.5, 0.5}, 17 | }, 18 | B: PointTangent{ 19 | Point: vec3.T{4.0, 5.0, 6.0}, 20 | Tangent: vec3.T{-0.5, 0.5, -0.5}, 21 | }, 22 | } 23 | 24 | // Convert to string and parse back 25 | str := original.String() 26 | parsed, err := Parse(str) 27 | if err != nil { 28 | t.Errorf("Parse() failed: %v", err) 29 | } 30 | 31 | // Check all components 32 | if math.Abs(float64(parsed.A.Point[0]-original.A.Point[0])) > EPSILON || 33 | math.Abs(float64(parsed.A.Point[1]-original.A.Point[1])) > EPSILON || 34 | math.Abs(float64(parsed.A.Point[2]-original.A.Point[2])) > EPSILON || 35 | math.Abs(float64(parsed.A.Tangent[0]-original.A.Tangent[0])) > EPSILON || 36 | math.Abs(float64(parsed.A.Tangent[1]-original.A.Tangent[1])) > EPSILON || 37 | math.Abs(float64(parsed.A.Tangent[2]-original.A.Tangent[2])) > EPSILON || 38 | math.Abs(float64(parsed.B.Point[0]-original.B.Point[0])) > EPSILON || 39 | math.Abs(float64(parsed.B.Point[1]-original.B.Point[1])) > EPSILON || 40 | math.Abs(float64(parsed.B.Point[2]-original.B.Point[2])) > EPSILON || 41 | math.Abs(float64(parsed.B.Tangent[0]-original.B.Tangent[0])) > EPSILON || 42 | math.Abs(float64(parsed.B.Tangent[1]-original.B.Tangent[1])) > EPSILON || 43 | math.Abs(float64(parsed.B.Tangent[2]-original.B.Tangent[2])) > EPSILON { 44 | t.Errorf("Parse/String roundtrip failed: got %v, want %v", parsed, original) 45 | } 46 | } 47 | 48 | func TestPointEndpoints(t *testing.T) { 49 | // Create a simple hermit spline from (0,0,0) to (1,1,1) with tangents pointing in the same direction 50 | herm := T{ 51 | A: PointTangent{ 52 | Point: vec3.T{0.0, 0.0, 0.0}, 53 | Tangent: vec3.T{1.0, 1.0, 1.0}, 54 | }, 55 | B: PointTangent{ 56 | Point: vec3.T{1.0, 1.0, 1.0}, 57 | Tangent: vec3.T{1.0, 1.0, 1.0}, 58 | }, 59 | } 60 | 61 | // At t=0, should return pointA 62 | p0 := herm.Point(0.0) 63 | if math.Abs(float64(p0[0]-herm.A.Point[0])) > EPSILON || 64 | math.Abs(float64(p0[1]-herm.A.Point[1])) > EPSILON || 65 | math.Abs(float64(p0[2]-herm.A.Point[2])) > EPSILON { 66 | t.Errorf("Point(0) should equal A.Point: got %v, want %v", p0, herm.A.Point) 67 | } 68 | 69 | // At t=1, should return pointB 70 | p1 := herm.Point(1.0) 71 | if math.Abs(float64(p1[0]-herm.B.Point[0])) > EPSILON || 72 | math.Abs(float64(p1[1]-herm.B.Point[1])) > EPSILON || 73 | math.Abs(float64(p1[2]-herm.B.Point[2])) > EPSILON { 74 | t.Errorf("Point(1) should equal B.Point: got %v, want %v", p1, herm.B.Point) 75 | } 76 | } 77 | 78 | func TestPointMidpoint(t *testing.T) { 79 | // Line along X-axis from (0,0,0) to (2,0,0) with X-aligned tangents 80 | herm := T{ 81 | A: PointTangent{ 82 | Point: vec3.T{0.0, 0.0, 0.0}, 83 | Tangent: vec3.T{1.0, 0.0, 0.0}, 84 | }, 85 | B: PointTangent{ 86 | Point: vec3.T{2.0, 0.0, 0.0}, 87 | Tangent: vec3.T{1.0, 0.0, 0.0}, 88 | }, 89 | } 90 | 91 | // At t=0.5, should be approximately at (1, 0, 0) for a straight line 92 | p := herm.Point(0.5) 93 | 94 | // The y and z coordinates should be exactly 0 95 | if math.Abs(float64(p[1])) > EPSILON || math.Abs(float64(p[2])) > EPSILON { 96 | t.Errorf("Point(0.5) y,z coordinates should be 0: got %v", p) 97 | } 98 | 99 | // The x-coordinate should be approximately 1.0 100 | if math.Abs(float64(p[0]-1.0)) > EPSILON { 101 | t.Errorf("Point(0.5) x-coordinate should be ~1.0: got %v", p[0]) 102 | } 103 | } 104 | 105 | func TestPointFunctionMatchesMethod(t *testing.T) { 106 | herm := T{ 107 | A: PointTangent{ 108 | Point: vec3.T{0.0, 0.0, 0.0}, 109 | Tangent: vec3.T{1.0, 0.5, 0.2}, 110 | }, 111 | B: PointTangent{ 112 | Point: vec3.T{2.0, 1.0, 0.5}, 113 | Tangent: vec3.T{1.0, -0.5, 0.3}, 114 | }, 115 | } 116 | 117 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0} 118 | for _, tVal := range tests { 119 | // Method call 120 | p1 := herm.Point(tVal) 121 | 122 | // Function call 123 | p2 := Point(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 124 | 125 | if math.Abs(float64(p1[0]-p2[0])) > EPSILON || 126 | math.Abs(float64(p1[1]-p2[1])) > EPSILON || 127 | math.Abs(float64(p1[2]-p2[2])) > EPSILON { 128 | t.Errorf("Point method and function differ at t=%f: method=%v, func=%v", tVal, p1, p2) 129 | } 130 | } 131 | } 132 | 133 | func TestTangentAtZero(t *testing.T) { 134 | herm := T{ 135 | A: PointTangent{ 136 | Point: vec3.T{0.0, 0.0, 0.0}, 137 | Tangent: vec3.T{1.0, 0.5, 0.2}, 138 | }, 139 | B: PointTangent{ 140 | Point: vec3.T{2.0, 1.0, 0.5}, 141 | Tangent: vec3.T{0.5, 1.0, 0.3}, 142 | }, 143 | } 144 | 145 | // At t=0, the Tangent function returns tangentA (verified by examining the formula at t=0) 146 | tan0 := herm.Tangent(0.0) 147 | expectedTan0 := herm.A.Tangent 148 | if math.Abs(float64(tan0[0]-expectedTan0[0])) > EPSILON || 149 | math.Abs(float64(tan0[1]-expectedTan0[1])) > EPSILON || 150 | math.Abs(float64(tan0[2]-expectedTan0[2])) > EPSILON { 151 | t.Errorf("Tangent(0) should equal A.Tangent: got %v, want %v", tan0, expectedTan0) 152 | } 153 | } 154 | 155 | func TestTangentFunctionMatchesMethod(t *testing.T) { 156 | herm := T{ 157 | A: PointTangent{ 158 | Point: vec3.T{0.0, 0.0, 0.0}, 159 | Tangent: vec3.T{1.0, 0.5, 0.2}, 160 | }, 161 | B: PointTangent{ 162 | Point: vec3.T{2.0, 1.0, 0.5}, 163 | Tangent: vec3.T{0.5, 1.0, 0.3}, 164 | }, 165 | } 166 | 167 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0} 168 | for _, tVal := range tests { 169 | // Method call 170 | tan1 := herm.Tangent(tVal) 171 | 172 | // Function call 173 | tan2 := Tangent(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 174 | 175 | if math.Abs(float64(tan1[0]-tan2[0])) > EPSILON || 176 | math.Abs(float64(tan1[1]-tan2[1])) > EPSILON || 177 | math.Abs(float64(tan1[2]-tan2[2])) > EPSILON { 178 | t.Errorf("Tangent method and function differ at t=%f: method=%v, func=%v", tVal, tan1, tan2) 179 | } 180 | } 181 | } 182 | 183 | func TestLengthValues(t *testing.T) { 184 | herm := T{ 185 | A: PointTangent{ 186 | Point: vec3.T{0.0, 0.0, 0.0}, 187 | Tangent: vec3.T{1.0, 0.0, 0.0}, 188 | }, 189 | B: PointTangent{ 190 | Point: vec3.T{2.0, 0.0, 0.0}, 191 | Tangent: vec3.T{1.0, 0.0, 0.0}, 192 | }, 193 | } 194 | 195 | // Test that Length returns a valid positive number 196 | length0 := herm.Length(0.0) 197 | if math.IsNaN(float64(length0)) || math.IsInf(float64(length0), 0) { 198 | t.Errorf("Length(0) should be a valid number: got %f", length0) 199 | } 200 | 201 | length1 := herm.Length(1.0) 202 | if math.IsNaN(float64(length1)) || math.IsInf(float64(length1), 0) { 203 | t.Errorf("Length(1) should be a valid number: got %f", length1) 204 | } 205 | } 206 | 207 | func TestLengthFunctionMatchesMethod(t *testing.T) { 208 | herm := T{ 209 | A: PointTangent{ 210 | Point: vec3.T{0.0, 0.0, 0.0}, 211 | Tangent: vec3.T{1.0, 0.5, 0.2}, 212 | }, 213 | B: PointTangent{ 214 | Point: vec3.T{2.0, 1.0, 0.5}, 215 | Tangent: vec3.T{0.5, 1.0, 0.3}, 216 | }, 217 | } 218 | 219 | tests := []float32{0.0, 0.25, 0.5, 0.75, 1.0} 220 | for _, tVal := range tests { 221 | // Method call 222 | len1 := herm.Length(tVal) 223 | 224 | // Function call 225 | len2 := Length(&herm.A.Point, &herm.A.Tangent, &herm.B.Point, &herm.B.Tangent, tVal) 226 | 227 | if math.Abs(float64(len1-len2)) > EPSILON { 228 | t.Errorf("Length method and function differ at t=%f: method=%f, func=%f", tVal, len1, len2) 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /float64/vec3/vec3_test.go: -------------------------------------------------------------------------------- 1 | package vec3 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | ) 7 | 8 | func TestBoxIntersection(t *testing.T) { 9 | bb1 := Box{T{0, 0, 0}, T{1, 1, 1}} 10 | bb2 := Box{T{1, 1, 1}, T{2, 2, 2}} 11 | if !bb1.Intersects(&bb2) { 12 | t.Fail() 13 | } 14 | 15 | bb3 := Box{T{1, 2, 1}, T{2, 3, 2}} 16 | if bb1.Intersects(&bb3) { 17 | t.Fail() 18 | } 19 | } 20 | 21 | func TestAdd(t *testing.T) { 22 | v1 := T{1, 2, 3} 23 | v2 := T{9, 8, 7} 24 | 25 | expectedV1 := T{10, 10, 10} 26 | expectedV2 := T{9, 8, 7} 27 | expectedV3 := T{10, 10, 10} 28 | 29 | v3 := v1.Add(&v2) 30 | 31 | if v1 != expectedV1 { 32 | t.Fail() 33 | } 34 | if v2 != expectedV2 { 35 | t.Fail() 36 | } 37 | if *v3 != expectedV3 { 38 | t.Fail() 39 | } 40 | } 41 | 42 | func TestAdded(t *testing.T) { 43 | v1 := T{1, 2, 3} 44 | v2 := T{9, 8, 7} 45 | 46 | expectedV1 := T{1, 2, 3} 47 | expectedV2 := T{9, 8, 7} 48 | expectedV3 := T{10, 10, 10} 49 | 50 | v3 := v1.Added(&v2) 51 | 52 | if v1 != expectedV1 { 53 | t.Fail() 54 | } 55 | if v2 != expectedV2 { 56 | t.Fail() 57 | } 58 | if v3 != expectedV3 { 59 | t.Fail() 60 | } 61 | } 62 | 63 | func TestSub(t *testing.T) { 64 | v1 := T{1, 2, 3} 65 | v2 := T{9, 8, 7} 66 | 67 | expectedV1 := T{-8, -6, -4} 68 | expectedV2 := T{9, 8, 7} 69 | expectedV3 := T{-8, -6, -4} 70 | 71 | v3 := v1.Sub(&v2) 72 | 73 | if v1 != expectedV1 { 74 | t.Fail() 75 | } 76 | if v2 != expectedV2 { 77 | t.Fail() 78 | } 79 | if *v3 != expectedV3 { 80 | t.Fail() 81 | } 82 | } 83 | 84 | func TestSubed(t *testing.T) { 85 | v1 := T{1, 2, 3} 86 | v2 := T{9, 8, 7} 87 | 88 | expectedV1 := T{1, 2, 3} 89 | expectedV2 := T{9, 8, 7} 90 | expectedV3 := T{-8, -6, -4} 91 | 92 | v3 := v1.Subed(&v2) 93 | 94 | if v1 != expectedV1 { 95 | t.Fail() 96 | } 97 | if v2 != expectedV2 { 98 | t.Fail() 99 | } 100 | if v3 != expectedV3 { 101 | t.Fail() 102 | } 103 | } 104 | 105 | func TestMul(t *testing.T) { 106 | v1 := T{1, 2, 3} 107 | v2 := T{9, 8, 7} 108 | 109 | expectedV1 := T{9, 16, 21} 110 | expectedV2 := T{9, 8, 7} 111 | expectedV3 := T{9, 16, 21} 112 | 113 | v3 := v1.Mul(&v2) 114 | 115 | if v1 != expectedV1 { 116 | t.Fail() 117 | } 118 | if v2 != expectedV2 { 119 | t.Fail() 120 | } 121 | if *v3 != expectedV3 { 122 | t.Fail() 123 | } 124 | } 125 | 126 | func TestMuled(t *testing.T) { 127 | v1 := T{1, 2, 3} 128 | v2 := T{9, 8, 7} 129 | 130 | expectedV1 := T{1, 2, 3} 131 | expectedV2 := T{9, 8, 7} 132 | expectedV3 := T{9, 16, 21} 133 | 134 | v3 := v1.Muled(&v2) 135 | 136 | if v1 != expectedV1 { 137 | t.Fail() 138 | } 139 | if v2 != expectedV2 { 140 | t.Fail() 141 | } 142 | if v3 != expectedV3 { 143 | t.Fail() 144 | } 145 | } 146 | 147 | func TestAngle(t *testing.T) { 148 | radFor45deg := math.Pi / 4.0 149 | testSetups := []struct { 150 | a, b T 151 | expectedAngle float64 152 | name string 153 | }{ 154 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedAngle: 0 * radFor45deg, name: "0/360 degree angle, equal/parallell vectors"}, 155 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedAngle: 1 * radFor45deg, name: "45 degree angle"}, 156 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedAngle: 2 * radFor45deg, name: "90 degree angle, orthogonal vectors"}, 157 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedAngle: 3 * radFor45deg, name: "135 degree angle"}, 158 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedAngle: 4 * radFor45deg, name: "180 degree angle, inverted/anti parallell vectors"}, 159 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedAngle: (8 - 5) * radFor45deg, name: "225 degree angle"}, 160 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedAngle: (8 - 6) * radFor45deg, name: "270 degree angle, orthogonal vectors"}, 161 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedAngle: (8 - 7) * radFor45deg, name: "315 degree angle"}, 162 | } 163 | 164 | for _, testSetup := range testSetups { 165 | t.Run(testSetup.name, func(t *testing.T) { 166 | angle := Angle(&testSetup.a, &testSetup.b) 167 | 168 | if !PracticallyEquals(angle, testSetup.expectedAngle, 0.00000001) { 169 | t.Errorf("Angle expected to be %f but was %f for test \"%s\".", testSetup.expectedAngle, angle, testSetup.name) 170 | } 171 | }) 172 | } 173 | } 174 | 175 | func TestCosine(t *testing.T) { 176 | radFor45deg := math.Pi / 4.0 177 | testSetups := []struct { 178 | a, b T 179 | expectedCosine float64 180 | name string 181 | }{ 182 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedCosine: math.Cos(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, 183 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedCosine: math.Cos(1 * radFor45deg), name: "45 degree angle"}, 184 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedCosine: math.Cos(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, 185 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedCosine: math.Cos(3 * radFor45deg), name: "135 degree angle"}, 186 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedCosine: math.Cos(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, 187 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedCosine: math.Cos(5 * radFor45deg), name: "225 degree angle"}, 188 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedCosine: math.Cos(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"}, 189 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedCosine: math.Cos(7 * radFor45deg), name: "315 degree angle"}, 190 | } 191 | 192 | for _, testSetup := range testSetups { 193 | t.Run(testSetup.name, func(t *testing.T) { 194 | cos := Cosine(&testSetup.a, &testSetup.b) 195 | 196 | if !PracticallyEquals(cos, testSetup.expectedCosine, 0.00000001) { 197 | t.Errorf("Cosine expected to be %f but was %f for test \"%s\".", testSetup.expectedCosine, cos, testSetup.name) 198 | } 199 | }) 200 | } 201 | } 202 | 203 | func TestSinus(t *testing.T) { 204 | radFor45deg := math.Pi / 4.0 205 | testSetups := []struct { 206 | a, b T 207 | expectedSine float64 208 | name string 209 | }{ 210 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedSine: math.Sin(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, 211 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedSine: math.Sin(1 * radFor45deg), name: "45 degree angle"}, 212 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedSine: math.Sin(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, 213 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedSine: math.Sin(3 * radFor45deg), name: "135 degree angle"}, 214 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedSine: math.Sin(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, 215 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedSine: math.Abs(math.Sin(5 * radFor45deg)), name: "225 degree angle"}, 216 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedSine: math.Abs(math.Sin(6 * radFor45deg)), name: "270 degree angle, orthogonal vectors"}, 217 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedSine: math.Abs(math.Sin(7 * radFor45deg)), name: "315 degree angle"}, 218 | } 219 | 220 | for _, testSetup := range testSetups { 221 | t.Run(testSetup.name, func(t *testing.T) { 222 | sin := Sinus(&testSetup.a, &testSetup.b) 223 | 224 | if !PracticallyEquals(sin, testSetup.expectedSine, 0.00000001) { 225 | t.Errorf("Sine expected to be %f but was %f for test \"%s\".", testSetup.expectedSine, sin, testSetup.name) 226 | } 227 | }) 228 | } 229 | } 230 | 231 | func TestIsZeroEps(t *testing.T) { 232 | tests := []struct { 233 | name string 234 | vec T 235 | epsilon float64 236 | want bool 237 | }{ 238 | {"exact zero", T{0, 0, 0}, 0.0001, true}, 239 | {"within epsilon", T{0.00001, -0.00001, 0.00001}, 0.0001, true}, 240 | {"at epsilon boundary", T{0.0001, 0.0001, 0.0001}, 0.0001, true}, 241 | {"outside epsilon", T{0.001, 0, 0}, 0.0001, false}, 242 | {"one component outside", T{0.00001, 0.001, 0.00001}, 0.0001, false}, 243 | {"negative outside epsilon", T{-0.001, 0, 0}, 0.0001, false}, 244 | {"large values", T{1.0, 2.0, 3.0}, 0.0001, false}, 245 | } 246 | 247 | for _, tt := range tests { 248 | t.Run(tt.name, func(t *testing.T) { 249 | if got := tt.vec.IsZeroEps(tt.epsilon); got != tt.want { 250 | t.Errorf("IsZeroEps() = %v, want %v for vec %v with epsilon %v", got, tt.want, tt.vec, tt.epsilon) 251 | } 252 | }) 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /vec3/vec3_test.go: -------------------------------------------------------------------------------- 1 | package vec3 2 | 3 | import ( 4 | math "github.com/chewxy/math32" 5 | 6 | "testing" 7 | ) 8 | 9 | func TestBoxIntersection(t *testing.T) { 10 | bb1 := Box{T{0, 0, 0}, T{1, 1, 1}} 11 | bb2 := Box{T{1, 1, 1}, T{2, 2, 2}} 12 | if !bb1.Intersects(&bb2) { 13 | t.Fail() 14 | } 15 | 16 | bb3 := Box{T{1, 2, 1}, T{2, 3, 2}} 17 | if bb1.Intersects(&bb3) { 18 | t.Fail() 19 | } 20 | } 21 | 22 | func TestAdd(t *testing.T) { 23 | v1 := T{1, 2, 3} 24 | v2 := T{9, 8, 7} 25 | 26 | expectedV1 := T{10, 10, 10} 27 | expectedV2 := T{9, 8, 7} 28 | expectedV3 := T{10, 10, 10} 29 | 30 | v3 := v1.Add(&v2) 31 | 32 | if v1 != expectedV1 { 33 | t.Fail() 34 | } 35 | if v2 != expectedV2 { 36 | t.Fail() 37 | } 38 | if *v3 != expectedV3 { 39 | t.Fail() 40 | } 41 | } 42 | 43 | func TestAdded(t *testing.T) { 44 | v1 := T{1, 2, 3} 45 | v2 := T{9, 8, 7} 46 | 47 | expectedV1 := T{1, 2, 3} 48 | expectedV2 := T{9, 8, 7} 49 | expectedV3 := T{10, 10, 10} 50 | 51 | v3 := v1.Added(&v2) 52 | 53 | if v1 != expectedV1 { 54 | t.Fail() 55 | } 56 | if v2 != expectedV2 { 57 | t.Fail() 58 | } 59 | if v3 != expectedV3 { 60 | t.Fail() 61 | } 62 | } 63 | 64 | func TestSub(t *testing.T) { 65 | v1 := T{1, 2, 3} 66 | v2 := T{9, 8, 7} 67 | 68 | expectedV1 := T{-8, -6, -4} 69 | expectedV2 := T{9, 8, 7} 70 | expectedV3 := T{-8, -6, -4} 71 | 72 | v3 := v1.Sub(&v2) 73 | 74 | if v1 != expectedV1 { 75 | t.Fail() 76 | } 77 | if v2 != expectedV2 { 78 | t.Fail() 79 | } 80 | if *v3 != expectedV3 { 81 | t.Fail() 82 | } 83 | } 84 | 85 | func TestSubed(t *testing.T) { 86 | v1 := T{1, 2, 3} 87 | v2 := T{9, 8, 7} 88 | 89 | expectedV1 := T{1, 2, 3} 90 | expectedV2 := T{9, 8, 7} 91 | expectedV3 := T{-8, -6, -4} 92 | 93 | v3 := v1.Subed(&v2) 94 | 95 | if v1 != expectedV1 { 96 | t.Fail() 97 | } 98 | if v2 != expectedV2 { 99 | t.Fail() 100 | } 101 | if v3 != expectedV3 { 102 | t.Fail() 103 | } 104 | } 105 | 106 | func TestMul(t *testing.T) { 107 | v1 := T{1, 2, 3} 108 | v2 := T{9, 8, 7} 109 | 110 | expectedV1 := T{9, 16, 21} 111 | expectedV2 := T{9, 8, 7} 112 | expectedV3 := T{9, 16, 21} 113 | 114 | v3 := v1.Mul(&v2) 115 | 116 | if v1 != expectedV1 { 117 | t.Fail() 118 | } 119 | if v2 != expectedV2 { 120 | t.Fail() 121 | } 122 | if *v3 != expectedV3 { 123 | t.Fail() 124 | } 125 | } 126 | 127 | func TestMuled(t *testing.T) { 128 | v1 := T{1, 2, 3} 129 | v2 := T{9, 8, 7} 130 | 131 | expectedV1 := T{1, 2, 3} 132 | expectedV2 := T{9, 8, 7} 133 | expectedV3 := T{9, 16, 21} 134 | 135 | v3 := v1.Muled(&v2) 136 | 137 | if v1 != expectedV1 { 138 | t.Fail() 139 | } 140 | if v2 != expectedV2 { 141 | t.Fail() 142 | } 143 | if v3 != expectedV3 { 144 | t.Fail() 145 | } 146 | } 147 | 148 | func TestAngle(t *testing.T) { 149 | radFor45deg := float32(math.Pi / 4.0) 150 | testSetups := []struct { 151 | a, b T 152 | expectedAngle float32 153 | name string 154 | }{ 155 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedAngle: 0 * radFor45deg, name: "0/360 degree angle, equal/parallell vectors"}, 156 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedAngle: 1 * radFor45deg, name: "45 degree angle"}, 157 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedAngle: 2 * radFor45deg, name: "90 degree angle, orthogonal vectors"}, 158 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedAngle: 3 * radFor45deg, name: "135 degree angle"}, 159 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedAngle: 4 * radFor45deg, name: "180 degree angle, inverted/anti parallell vectors"}, 160 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedAngle: (8 - 5) * radFor45deg, name: "225 degree angle"}, 161 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedAngle: (8 - 6) * radFor45deg, name: "270 degree angle, orthogonal vectors"}, 162 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedAngle: (8 - 7) * radFor45deg, name: "315 degree angle"}, 163 | } 164 | 165 | for _, testSetup := range testSetups { 166 | t.Run(testSetup.name, func(t *testing.T) { 167 | angle := Angle(&testSetup.a, &testSetup.b) 168 | 169 | if !PracticallyEquals(angle, testSetup.expectedAngle, 0.00000001) { 170 | t.Errorf("Angle expected to be %f but was %f for test \"%s\".", testSetup.expectedAngle, angle, testSetup.name) 171 | } 172 | }) 173 | } 174 | } 175 | 176 | func TestCosine(t *testing.T) { 177 | radFor45deg := float32(math.Pi / 4.0) 178 | testSetups := []struct { 179 | a, b T 180 | expectedCosine float32 181 | name string 182 | }{ 183 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedCosine: math.Cos(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, 184 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedCosine: math.Cos(1 * radFor45deg), name: "45 degree angle"}, 185 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedCosine: math.Cos(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, 186 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedCosine: math.Cos(3 * radFor45deg), name: "135 degree angle"}, 187 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedCosine: math.Cos(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, 188 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedCosine: math.Cos(5 * radFor45deg), name: "225 degree angle"}, 189 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedCosine: math.Cos(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"}, 190 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedCosine: math.Cos(7 * radFor45deg), name: "315 degree angle"}, 191 | } 192 | 193 | for _, testSetup := range testSetups { 194 | t.Run(testSetup.name, func(t *testing.T) { 195 | cos := Cosine(&testSetup.a, &testSetup.b) 196 | 197 | if !PracticallyEquals(cos, testSetup.expectedCosine, 0.000001) { 198 | t.Errorf("Cosine expected to be %f but was %f for test \"%s\".", testSetup.expectedCosine, cos, testSetup.name) 199 | } 200 | }) 201 | } 202 | } 203 | 204 | func TestSinus(t *testing.T) { 205 | radFor45deg := float32(math.Pi / 4.0) 206 | testSetups := []struct { 207 | a, b T 208 | expectedSine float32 209 | name string 210 | }{ 211 | {a: T{1, 0, 0}, b: T{1, 0, 0}, expectedSine: math.Sin(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, 212 | {a: T{1, 0, 0}, b: T{1, 1, 0}, expectedSine: math.Sin(1 * radFor45deg), name: "45 degree angle"}, 213 | {a: T{1, 0, 0}, b: T{0, 1, 0}, expectedSine: math.Sin(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, 214 | {a: T{1, 0, 0}, b: T{-1, 1, 0}, expectedSine: math.Sin(3 * radFor45deg), name: "135 degree angle"}, 215 | {a: T{1, 0, 0}, b: T{-1, 0, 0}, expectedSine: math.Sin(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, 216 | {a: T{1, 0, 0}, b: T{-1, -1, 0}, expectedSine: math.Abs(math.Sin(5 * radFor45deg)), name: "225 degree angle"}, 217 | {a: T{1, 0, 0}, b: T{0, -1, 0}, expectedSine: math.Abs(math.Sin(6 * radFor45deg)), name: "270 degree angle, orthogonal vectors"}, 218 | {a: T{1, 0, 0}, b: T{1, -1, 0}, expectedSine: math.Abs(math.Sin(7 * radFor45deg)), name: "315 degree angle"}, 219 | } 220 | 221 | for _, testSetup := range testSetups { 222 | t.Run(testSetup.name, func(t *testing.T) { 223 | sin := Sinus(&testSetup.a, &testSetup.b) 224 | 225 | if !PracticallyEquals(sin, testSetup.expectedSine, 0.000001) { 226 | t.Errorf("Sine expected to be %f but was %f for test \"%s\".", testSetup.expectedSine, sin, testSetup.name) 227 | } 228 | }) 229 | } 230 | } 231 | 232 | func TestIsZeroEps(t *testing.T) { 233 | tests := []struct { 234 | name string 235 | vec T 236 | epsilon float32 237 | want bool 238 | }{ 239 | {"exact zero", T{0, 0, 0}, 0.0001, true}, 240 | {"within epsilon", T{0.00001, -0.00001, 0.00001}, 0.0001, true}, 241 | {"at epsilon boundary", T{0.0001, 0.0001, 0.0001}, 0.0001, true}, 242 | {"outside epsilon", T{0.001, 0, 0}, 0.0001, false}, 243 | {"one component outside", T{0.00001, 0.001, 0.00001}, 0.0001, false}, 244 | {"negative outside epsilon", T{-0.001, 0, 0}, 0.0001, false}, 245 | {"large values", T{1.0, 2.0, 3.0}, 0.0001, false}, 246 | } 247 | 248 | for _, tt := range tests { 249 | t.Run(tt.name, func(t *testing.T) { 250 | if got := tt.vec.IsZeroEps(tt.epsilon); got != tt.want { 251 | t.Errorf("IsZeroEps() = %v, want %v for vec %v with epsilon %v", got, tt.want, tt.vec, tt.epsilon) 252 | } 253 | }) 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /float64/mat3/mat3_test.go: -------------------------------------------------------------------------------- 1 | package mat3 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/ungerik/go3d/float64/mat2" 8 | "github.com/ungerik/go3d/float64/vec2" 9 | "github.com/ungerik/go3d/float64/vec3" 10 | ) 11 | 12 | const EPSILON = 0.0001 13 | 14 | // Some matrices used in multiple tests. 15 | var ( 16 | invertableMatrix1 = T{vec3.T{4, -2, 3}, vec3.T{8, -3, 5}, vec3.T{7, -2, 4}} 17 | invertedMatrix1 = T{vec3.T{-2, 2, -1}, vec3.T{3, -5, 4}, vec3.T{5, -6, 4}} 18 | nonInvertableMatrix1 = T{vec3.T{1, 1, 1}, vec3.T{1, 1, 1}, vec3.T{1, 1, 1}} 19 | nonInvertableMatrix2 = T{vec3.T{1, 1, 1}, vec3.T{1, 0, 1}, vec3.T{1, 1, 1}} 20 | 21 | testMatrix1 = T{ 22 | vec3.T{0.38016528, -0.0661157, -0.008264462}, 23 | vec3.T{-0.19834709, 0.33884296, -0.08264463}, 24 | vec3.T{0.11570247, -0.28099173, 0.21487603}, 25 | } 26 | 27 | testMatrix2 = T{ 28 | vec3.T{23, -4, -0.5}, 29 | vec3.T{-12, 20.5, -5}, 30 | vec3.T{7, -17, 13}, 31 | } 32 | 33 | row123Changed, _ = Parse("3 1 0.5 2 5 2 1 6 7") 34 | ) 35 | 36 | func Test_ColsAndRows(t *testing.T) { 37 | A := testMatrix2 38 | 39 | a11 := A.Get(0, 0) 40 | a21 := A.Get(0, 1) 41 | a31 := A.Get(0, 2) 42 | 43 | a12 := A.Get(1, 0) 44 | a22 := A.Get(1, 1) 45 | a32 := A.Get(1, 2) 46 | 47 | a13 := A.Get(2, 0) 48 | a23 := A.Get(2, 1) 49 | a33 := A.Get(2, 2) 50 | 51 | correctReference := a11 == 23 && a21 == -4 && a31 == -0.5 && 52 | a12 == -12 && a22 == 20.5 && a32 == -5 && 53 | a13 == 7 && a23 == -17 && a33 == 13 54 | 55 | if !correctReference { 56 | t.Errorf("matrix ill referenced") 57 | } 58 | } 59 | 60 | func TestT_Transposed(t *testing.T) { 61 | matrix := T{ 62 | vec3.T{1, 2, 3}, 63 | vec3.T{4, 5, 6}, 64 | vec3.T{7, 8, 9}, 65 | } 66 | expectedMatrix := T{ 67 | vec3.T{1, 4, 7}, 68 | vec3.T{2, 5, 8}, 69 | vec3.T{3, 6, 9}, 70 | } 71 | 72 | transposedMatrix := matrix.Transposed() 73 | 74 | if transposedMatrix != expectedMatrix { 75 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix) 76 | } 77 | } 78 | 79 | func TestT_Transpose(t *testing.T) { 80 | matrix := T{ 81 | vec3.T{10, 20, 30}, 82 | vec3.T{40, 50, 60}, 83 | vec3.T{70, 80, 90}, 84 | } 85 | 86 | expectedMatrix := T{ 87 | vec3.T{10, 40, 70}, 88 | vec3.T{20, 50, 80}, 89 | vec3.T{30, 60, 90}, 90 | } 91 | 92 | transposedMatrix := matrix 93 | transposedMatrix.Transpose() 94 | 95 | if transposedMatrix != expectedMatrix { 96 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix) 97 | } 98 | } 99 | 100 | func TestDeterminant_2(t *testing.T) { 101 | detTwo := Ident 102 | detTwo[0][0] = 2 103 | if det := detTwo.Determinant(); det != 2 { 104 | t.Errorf("Wrong determinant: %f", det) 105 | } 106 | } 107 | 108 | func TestDeterminant_3(t *testing.T) { 109 | scale2 := Ident.Scaled(2) 110 | if det := scale2.Determinant(); det != 2*2*2*1 { 111 | t.Errorf("Wrong determinant: %f", det) 112 | } 113 | } 114 | 115 | func TestDeterminant_4(t *testing.T) { 116 | row1changed, _ := Parse("3 0 0 2 2 0 1 0 2") 117 | if det := row1changed.Determinant(); det != 12 { 118 | t.Errorf("Wrong determinant: %f", det) 119 | } 120 | } 121 | 122 | func TestDeterminant_5(t *testing.T) { 123 | row12changed, _ := Parse("3 1 0 2 5 0 1 6 2") 124 | if det := row12changed.Determinant(); det != 26 { 125 | t.Errorf("Wrong determinant: %f", det) 126 | } 127 | } 128 | 129 | func TestDeterminant_7(t *testing.T) { 130 | randomMatrix, err := Parse("0.43685 0.81673 0.63721 0.16600 0.40608 0.53479 0.37328 0.36436 0.56356") 131 | randomMatrix.Transpose() 132 | if err != nil { 133 | t.Errorf("Could not parse random matrix: %v", err) 134 | } 135 | if det := randomMatrix.Determinant(); PracticallyEquals(det, 0.043437) { 136 | t.Errorf("Wrong determinant for random sub 3x3 matrix: %f", det) 137 | } 138 | } 139 | 140 | func PracticallyEquals(value1 float64, value2 float64) bool { 141 | return math.Abs(value1-value2) > EPSILON 142 | } 143 | 144 | func TestDeterminant_6(t *testing.T) { 145 | row123changed := row123Changed 146 | if det := row123changed.Determinant(); det != 60.500 { 147 | t.Errorf("Wrong determinant for 3x3 matrix: %f", det) 148 | } 149 | } 150 | 151 | func TestDeterminant_1(t *testing.T) { 152 | detId := Ident.Determinant() 153 | if detId != 1 { 154 | t.Errorf("Wrong determinant for identity matrix: %f", detId) 155 | } 156 | } 157 | 158 | func TestMaskedBlock(t *testing.T) { 159 | m := row123Changed 160 | blockedExpected := mat2.T{vec2.T{5, 2}, vec2.T{6, 7}} 161 | if blocked := m.maskedBlock(0, 0); *blocked != blockedExpected { 162 | t.Errorf("Did not block 0,0 correctly: %#v", blocked) 163 | } 164 | } 165 | 166 | func TestAdjugate(t *testing.T) { 167 | adj := row123Changed 168 | 169 | // Computed in octave: 170 | adjExpected := T{ 171 | vec3.T{23, -4, -0.5}, 172 | vec3.T{-12, 20.5, -5}, 173 | vec3.T{7, -17, 13}, 174 | } 175 | 176 | adj.Adjugate() 177 | 178 | if adj != adjExpected { 179 | t.Errorf("Adjugate not computed correctly: %#v", adj) 180 | } 181 | } 182 | 183 | func TestAdjugated(t *testing.T) { 184 | sqrt2 := math.Sqrt(2) 185 | A := T{ 186 | vec3.T{1, 0, -1}, 187 | vec3.T{0, sqrt2, 0}, 188 | vec3.T{1, 0, 1}, 189 | } 190 | 191 | expectedAdjugated := T{ 192 | vec3.T{1.4142135623730951, -0, 1.4142135623730951}, 193 | vec3.T{-0, 2, -0}, 194 | vec3.T{-1.4142135623730951, -0, 1.4142135623730951}, 195 | } 196 | 197 | adjA := A.Adjugated() 198 | 199 | if adjA != expectedAdjugated { 200 | t.Errorf("Adjugate not computed correctly: %v", adjA) 201 | } 202 | } 203 | 204 | func TestInvert_ok(t *testing.T) { 205 | inv := invertableMatrix1 206 | _, err := inv.Invert() 207 | 208 | if err != nil { 209 | t.Error("Inverse not computed correctly", err) 210 | } 211 | 212 | invExpected := invertedMatrix1 213 | if inv != invExpected { 214 | t.Errorf("Inverse not computed correctly: %#v", inv) 215 | } 216 | } 217 | 218 | func TestInvert_ok2(t *testing.T) { 219 | sqrt2 := math.Sqrt(2) 220 | A := T{ 221 | vec3.T{1, 0, -1}, 222 | vec3.T{0, sqrt2, 0}, 223 | vec3.T{1, 0, 1}, 224 | } 225 | 226 | expectedInverted := T{ 227 | vec3.T{0.5, 0, 0.5}, 228 | vec3.T{0, 0.7071067811865475, 0}, 229 | vec3.T{-0.5, 0, 0.5}, 230 | } 231 | 232 | invA, err := A.Inverted() 233 | if err != nil { 234 | t.Error("Inverse not computed correctly", err) 235 | } 236 | 237 | if invA != expectedInverted { 238 | t.Errorf("Inverse not computed correctly: %v", A) 239 | } 240 | } 241 | 242 | func TestInvert_nok_1(t *testing.T) { 243 | inv := nonInvertableMatrix1 244 | _, err := inv.Inverted() 245 | if err == nil { 246 | t.Error("Inverse should not be possible", err) 247 | } 248 | } 249 | 250 | func TestInvert_nok_2(t *testing.T) { 251 | inv := nonInvertableMatrix2 252 | _, err := inv.Inverted() 253 | if err == nil { 254 | t.Error("Inverse should not be possible", err) 255 | } 256 | } 257 | 258 | func BenchmarkAssignMul(b *testing.B) { 259 | m1 := testMatrix1 260 | m2 := testMatrix2 261 | var mMult T 262 | b.ResetTimer() 263 | for i := 0; i < b.N; i++ { 264 | mMult.AssignMul(&m1, &m2) 265 | } 266 | } 267 | 268 | func TestIsZeroEps(t *testing.T) { 269 | tests := []struct { 270 | name string 271 | mat T 272 | epsilon float64 273 | want bool 274 | }{ 275 | {"exact zero", Zero, 0.0001, true}, 276 | {"within epsilon", T{vec3.T{0.00001, -0.00001, 0.00001}, vec3.T{-0.00001, 0.00001, -0.00001}, vec3.T{0.00001, -0.00001, 0.00001}}, 0.0001, true}, 277 | {"at epsilon boundary", T{vec3.T{0.0001, 0.0001, 0.0001}, vec3.T{0.0001, 0.0001, 0.0001}, vec3.T{0.0001, 0.0001, 0.0001}}, 0.0001, true}, 278 | {"outside epsilon", T{vec3.T{0.001, 0, 0}, vec3.T{0, 0, 0}, vec3.T{0, 0, 0}}, 0.0001, false}, 279 | {"one element outside", T{vec3.T{0.00001, 0.00001, 0.00001}, vec3.T{0.001, 0.00001, 0.00001}, vec3.T{0.00001, 0.00001, 0.00001}}, 0.0001, false}, 280 | {"negative outside epsilon", T{vec3.T{-0.001, 0, 0}, vec3.T{0, 0, 0}, vec3.T{0, 0, 0}}, 0.0001, false}, 281 | {"identity matrix", Ident, 0.0001, false}, 282 | } 283 | 284 | for _, tt := range tests { 285 | t.Run(tt.name, func(t *testing.T) { 286 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want { 287 | t.Errorf("IsZeroEps() = %v, want %v for mat with epsilon %v", got, tt.want, tt.epsilon) 288 | } 289 | }) 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /mat3/mat3_test.go: -------------------------------------------------------------------------------- 1 | package mat3 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "github.com/ungerik/go3d/mat2" 8 | "github.com/ungerik/go3d/vec2" 9 | "github.com/ungerik/go3d/vec3" 10 | ) 11 | 12 | const EPSILON = 0.0001 13 | 14 | // Some matrices used in multiple tests. 15 | var ( 16 | invertableMatrix1 = T{vec3.T{4, -2, 3}, vec3.T{8, -3, 5}, vec3.T{7, -2, 4}} 17 | invertedMatrix1 = T{vec3.T{-2, 2, -1}, vec3.T{3, -5, 4}, vec3.T{5, -6, 4}} 18 | nonInvertableMatrix1 = T{vec3.T{1, 1, 1}, vec3.T{1, 1, 1}, vec3.T{1, 1, 1}} 19 | nonInvertableMatrix2 = T{vec3.T{1, 1, 1}, vec3.T{1, 0, 1}, vec3.T{1, 1, 1}} 20 | 21 | testMatrix1 = T{ 22 | vec3.T{0.38016528, -0.0661157, -0.008264462}, 23 | vec3.T{-0.19834709, 0.33884296, -0.08264463}, 24 | vec3.T{0.11570247, -0.28099173, 0.21487603}, 25 | } 26 | 27 | testMatrix2 = T{ 28 | vec3.T{23, -4, -0.5}, 29 | vec3.T{-12, 20.5, -5}, 30 | vec3.T{7, -17, 13}, 31 | } 32 | 33 | row123Changed, _ = Parse("3 1 0.5 2 5 2 1 6 7") 34 | ) 35 | 36 | func Test_ColsAndRows(t *testing.T) { 37 | A := testMatrix2 38 | 39 | a11 := A.Get(0, 0) 40 | a21 := A.Get(0, 1) 41 | a31 := A.Get(0, 2) 42 | 43 | a12 := A.Get(1, 0) 44 | a22 := A.Get(1, 1) 45 | a32 := A.Get(1, 2) 46 | 47 | a13 := A.Get(2, 0) 48 | a23 := A.Get(2, 1) 49 | a33 := A.Get(2, 2) 50 | 51 | correctReference := a11 == 23 && a21 == -4 && a31 == -0.5 && 52 | a12 == -12 && a22 == 20.5 && a32 == -5 && 53 | a13 == 7 && a23 == -17 && a33 == 13 54 | 55 | if !correctReference { 56 | t.Errorf("matrix ill referenced") 57 | } 58 | } 59 | 60 | func TestT_Transposed(t *testing.T) { 61 | matrix := T{ 62 | vec3.T{1, 2, 3}, 63 | vec3.T{4, 5, 6}, 64 | vec3.T{7, 8, 9}, 65 | } 66 | expectedMatrix := T{ 67 | vec3.T{1, 4, 7}, 68 | vec3.T{2, 5, 8}, 69 | vec3.T{3, 6, 9}, 70 | } 71 | 72 | transposedMatrix := matrix.Transposed() 73 | 74 | if transposedMatrix != expectedMatrix { 75 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix) 76 | } 77 | } 78 | 79 | func TestT_Transpose(t *testing.T) { 80 | matrix := T{ 81 | vec3.T{10, 20, 30}, 82 | vec3.T{40, 50, 60}, 83 | vec3.T{70, 80, 90}, 84 | } 85 | 86 | expectedMatrix := T{ 87 | vec3.T{10, 40, 70}, 88 | vec3.T{20, 50, 80}, 89 | vec3.T{30, 60, 90}, 90 | } 91 | 92 | transposedMatrix := matrix 93 | transposedMatrix.Transpose() 94 | 95 | if transposedMatrix != expectedMatrix { 96 | t.Errorf("matrix transposed wrong: %v --> %v", matrix, transposedMatrix) 97 | } 98 | } 99 | 100 | func TestDeterminant_2(t *testing.T) { 101 | detTwo := Ident 102 | detTwo[0][0] = 2 103 | if det := detTwo.Determinant(); det != 2 { 104 | t.Errorf("Wrong determinant: %f", det) 105 | } 106 | } 107 | 108 | func TestDeterminant_3(t *testing.T) { 109 | scale2 := Ident.Scaled(2) 110 | if det := scale2.Determinant(); det != 2*2*2*1 { 111 | t.Errorf("Wrong determinant: %f", det) 112 | } 113 | } 114 | 115 | func TestDeterminant_4(t *testing.T) { 116 | row1changed, _ := Parse("3 0 0 2 2 0 1 0 2") 117 | if det := row1changed.Determinant(); det != 12 { 118 | t.Errorf("Wrong determinant: %f", det) 119 | } 120 | } 121 | 122 | func TestDeterminant_5(t *testing.T) { 123 | row12changed, _ := Parse("3 1 0 2 5 0 1 6 2") 124 | if det := row12changed.Determinant(); det != 26 { 125 | t.Errorf("Wrong determinant: %f", det) 126 | } 127 | } 128 | 129 | func TestDeterminant_7(t *testing.T) { 130 | randomMatrix, err := Parse("0.43685 0.81673 0.63721 0.16600 0.40608 0.53479 0.37328 0.36436 0.56356") 131 | randomMatrix.Transpose() 132 | if err != nil { 133 | t.Errorf("Could not parse random matrix: %v", err) 134 | } 135 | if det := randomMatrix.Determinant(); PracticallyEquals(det, 0.043437) { 136 | t.Errorf("Wrong determinant for random sub 3x3 matrix: %f", det) 137 | } 138 | } 139 | 140 | func PracticallyEquals(value1 float32, value2 float32) bool { 141 | return math.Abs(float64(value1-value2)) > EPSILON 142 | } 143 | 144 | func TestDeterminant_6(t *testing.T) { 145 | row123changed := row123Changed 146 | if det := row123changed.Determinant(); det != 60.500 { 147 | t.Errorf("Wrong determinant for 3x3 matrix: %f", det) 148 | } 149 | } 150 | 151 | func TestDeterminant_1(t *testing.T) { 152 | detId := Ident.Determinant() 153 | if detId != 1 { 154 | t.Errorf("Wrong determinant for identity matrix: %f", detId) 155 | } 156 | } 157 | 158 | func TestMaskedBlock(t *testing.T) { 159 | m := row123Changed 160 | blockedExpected := mat2.T{vec2.T{5, 2}, vec2.T{6, 7}} 161 | if blocked := m.maskedBlock(0, 0); *blocked != blockedExpected { 162 | t.Errorf("Did not block 0,0 correctly: %#v", blocked) 163 | } 164 | } 165 | 166 | func TestAdjugate(t *testing.T) { 167 | adj := row123Changed 168 | 169 | // Computed in octave: 170 | adjExpected := T{ 171 | vec3.T{23, -4, -0.5}, 172 | vec3.T{-12, 20.5, -5}, 173 | vec3.T{7, -17, 13}, 174 | } 175 | 176 | adj.Adjugate() 177 | 178 | if adj != adjExpected { 179 | t.Errorf("Adjugate not computed correctly: %#v", adj) 180 | } 181 | } 182 | 183 | func TestAdjugated(t *testing.T) { 184 | sqrt2 := float32(math.Sqrt(2)) 185 | A := T{ 186 | vec3.T{1, 0, -1}, 187 | vec3.T{0, sqrt2, 0}, 188 | vec3.T{1, 0, 1}, 189 | } 190 | 191 | expectedAdjugated := T{ 192 | vec3.T{1.4142135623730951, -0, 1.4142135623730951}, 193 | vec3.T{-0, 2, -0}, 194 | vec3.T{-1.4142135623730951, -0, 1.4142135623730951}, 195 | } 196 | 197 | adjA := A.Adjugated() 198 | 199 | if adjA != expectedAdjugated { 200 | t.Errorf("Adjugate not computed correctly: %v", adjA) 201 | } 202 | } 203 | 204 | func TestInvert_ok(t *testing.T) { 205 | inv := invertableMatrix1 206 | _, err := inv.Invert() 207 | 208 | if err != nil { 209 | t.Error("Inverse not computed correctly", err) 210 | } 211 | 212 | invExpected := invertedMatrix1 213 | if inv != invExpected { 214 | t.Errorf("Inverse not computed correctly: %#v", inv) 215 | } 216 | } 217 | 218 | func TestInvert_ok2(t *testing.T) { 219 | sqrt2 := float32(math.Sqrt(2)) 220 | A := T{ 221 | vec3.T{1, 0, -1}, 222 | vec3.T{0, sqrt2, 0}, 223 | vec3.T{1, 0, 1}, 224 | } 225 | 226 | expectedInverted := T{ 227 | vec3.T{0.5, 0, 0.5}, 228 | vec3.T{0, 0.7071067811865475, 0}, 229 | vec3.T{-0.5, 0, 0.5}, 230 | } 231 | 232 | invA, err := A.Inverted() 233 | if err != nil { 234 | t.Error("Inverse not computed correctly", err) 235 | } 236 | 237 | if !invA.PracticallyEquals(&expectedInverted, EPSILON) { 238 | t.Errorf("Inverse not computed correctly: %v", A) 239 | } 240 | } 241 | 242 | func TestInvert_nok_1(t *testing.T) { 243 | inv := nonInvertableMatrix1 244 | _, err := inv.Inverted() 245 | if err == nil { 246 | t.Error("Inverse should not be possible", err) 247 | } 248 | } 249 | 250 | func TestInvert_nok_2(t *testing.T) { 251 | inv := nonInvertableMatrix2 252 | _, err := inv.Inverted() 253 | if err == nil { 254 | t.Error("Inverse should not be possible", err) 255 | } 256 | } 257 | 258 | func BenchmarkAssignMul(b *testing.B) { 259 | m1 := testMatrix1 260 | m2 := testMatrix2 261 | var mMult T 262 | b.ResetTimer() 263 | for i := 0; i < b.N; i++ { 264 | mMult.AssignMul(&m1, &m2) 265 | } 266 | } 267 | 268 | func TestIsZeroEps(t *testing.T) { 269 | tests := []struct { 270 | name string 271 | mat T 272 | epsilon float32 273 | want bool 274 | }{ 275 | {"exact zero", Zero, 0.0001, true}, 276 | {"within epsilon", T{vec3.T{0.00001, -0.00001, 0.00001}, vec3.T{-0.00001, 0.00001, -0.00001}, vec3.T{0.00001, -0.00001, 0.00001}}, 0.0001, true}, 277 | {"at epsilon boundary", T{vec3.T{0.0001, 0.0001, 0.0001}, vec3.T{0.0001, 0.0001, 0.0001}, vec3.T{0.0001, 0.0001, 0.0001}}, 0.0001, true}, 278 | {"outside epsilon", T{vec3.T{0.001, 0, 0}, vec3.T{0, 0, 0}, vec3.T{0, 0, 0}}, 0.0001, false}, 279 | {"one element outside", T{vec3.T{0.00001, 0.00001, 0.00001}, vec3.T{0.001, 0.00001, 0.00001}, vec3.T{0.00001, 0.00001, 0.00001}}, 0.0001, false}, 280 | {"negative outside epsilon", T{vec3.T{-0.001, 0, 0}, vec3.T{0, 0, 0}, vec3.T{0, 0, 0}}, 0.0001, false}, 281 | {"identity matrix", Ident, 0.0001, false}, 282 | } 283 | 284 | for _, tt := range tests { 285 | t.Run(tt.name, func(t *testing.T) { 286 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want { 287 | t.Errorf("IsZeroEps() = %v, want %v for mat with epsilon %v", got, tt.want, tt.epsilon) 288 | } 289 | }) 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /mat4/mat4_test.go: -------------------------------------------------------------------------------- 1 | package mat4 2 | 3 | import ( 4 | "testing" 5 | 6 | math "github.com/chewxy/math32" 7 | "github.com/ungerik/go3d/mat3" 8 | "github.com/ungerik/go3d/vec3" 9 | "github.com/ungerik/go3d/vec4" 10 | ) 11 | 12 | const EPSILON = 0.0001 13 | 14 | // Some matrices used in multiple tests. 15 | var ( 16 | TEST_MATRIX1 = T{ 17 | vec4.T{0.38016528, -0.0661157, -0.008264462, -0}, 18 | vec4.T{-0.19834709, 0.33884296, -0.08264463, 0}, 19 | vec4.T{0.11570247, -0.28099173, 0.21487603, -0}, 20 | vec4.T{18.958677, -33.471073, 8.066115, 0.99999994}, 21 | } 22 | 23 | TEST_MATRIX2 = T{ 24 | vec4.T{23, -4, -0.5, -0}, 25 | vec4.T{-12, 20.5, -5, 0}, 26 | vec4.T{7, -17, 13, -0}, 27 | vec4.T{1147, -2025, 488, 60.5}, 28 | } 29 | 30 | ROW_123_CHANGED, _ = Parse("3 1 0.5 0 2 5 2 0 1 6 7 0 2 100 1 1") 31 | ) 32 | 33 | func TestDeterminant(t *testing.T) { 34 | detId := Ident.Determinant() 35 | if detId != 1 { 36 | t.Errorf("Wrong determinant for identity matrix: %f", detId) 37 | } 38 | 39 | detTwo := Ident 40 | detTwo[0][0] = 2 41 | if det := detTwo.Determinant(); det != 2 { 42 | t.Errorf("Wrong determinant: %f", det) 43 | } 44 | 45 | scale2 := Ident.Scale(2) 46 | if det := scale2.Determinant(); det != 2*2*2*1 { 47 | t.Errorf("Wrong determinant: %f", det) 48 | } 49 | 50 | row1changed, _ := Parse("3 0 0 0 2 2 0 0 1 0 2 0 2 0 0 1") 51 | if det := row1changed.Determinant(); det != 12 { 52 | t.Errorf("Wrong determinant: %f", det) 53 | } 54 | 55 | row12changed, _ := Parse("3 1 0 0 2 5 0 0 1 6 2 0 2 100 0 1") 56 | if det := row12changed.Determinant(); det != 26 { 57 | t.Errorf("Wrong determinant: %f", det) 58 | } 59 | 60 | row123changed := ROW_123_CHANGED 61 | if det := row123changed.Determinant3x3(); det != 60.500 { 62 | t.Errorf("Wrong determinant for 3x3 matrix: %f", det) 63 | } 64 | if det := row123changed.Determinant(); det != 60.500 { 65 | t.Errorf("Wrong determinant: %f", det) 66 | } 67 | randomMatrix, err := Parse("0.43685 0.81673 0.63721 0.23421 0.16600 0.40608 0.53479 0.43210 0.37328 0.36436 0.56356 0.66830 0.32475 0.14294 0.42137 0.98046") 68 | randomMatrix.Transpose() //transpose for easy comparability with octave output 69 | if err != nil { 70 | t.Errorf("Could not parse random matrix: %v", err) 71 | } 72 | if det := randomMatrix.Determinant3x3(); math.Abs(det-0.043437) > EPSILON { 73 | t.Errorf("Wrong determinant for random sub 3x3 matrix: %f", det) 74 | } 75 | 76 | if det := randomMatrix.Determinant(); math.Abs(det-0.012208) > EPSILON { 77 | t.Errorf("Wrong determinant for random matrix: %f", det) 78 | } 79 | } 80 | 81 | func TestMaskedBlock(t *testing.T) { 82 | m := ROW_123_CHANGED 83 | blocked_expected := mat3.T{vec3.T{5, 2, 0}, vec3.T{6, 7, 0}, vec3.T{100, 1, 1}} 84 | if blocked := m.maskedBlock(0, 0); *blocked != blocked_expected { 85 | t.Errorf("Did not block 0,0 correctly: %#v", blocked) 86 | } 87 | } 88 | 89 | func TestAdjugate(t *testing.T) { 90 | adj := ROW_123_CHANGED 91 | adj.Adjugate() 92 | // Computed in octave: 93 | adj_expected := T{vec4.T{23, -4, -0.5, -0}, vec4.T{-12, 20.5, -5, 0}, vec4.T{7, -17, 13, -0}, vec4.T{1147, -2025, 488, 60.5}} 94 | if adj != adj_expected { 95 | t.Errorf("Adjugate not computed correctly: %#v", adj) 96 | } 97 | } 98 | 99 | func TestInvert(t *testing.T) { 100 | inv := ROW_123_CHANGED 101 | inv.Invert() 102 | // Computed in octave: 103 | inv_expected := T{vec4.T{0.38016528, -0.0661157, -0.008264462, -0}, vec4.T{-0.19834709, 0.33884296, -0.08264463, 0}, vec4.T{0.11570247, -0.28099173, 0.21487603, -0}, vec4.T{18.958677, -33.471073, 8.066115, 0.99999994}} 104 | if inv != inv_expected { 105 | t.Errorf("Inverse not computed correctly: %#v", inv) 106 | } 107 | } 108 | 109 | func TestMultSimpleMatrices(t *testing.T) { 110 | m1 := T{vec4.T{1, 0, 0, 2}, 111 | vec4.T{0, 1, 2, 0}, 112 | vec4.T{0, 2, 1, 0}, 113 | vec4.T{2, 0, 0, 1}} 114 | m2 := m1 115 | var mMult T 116 | mMult.AssignMul(&m1, &m2) 117 | t.Log(&m1) 118 | t.Log(&m2) 119 | m1.MultMatrix(&m2) 120 | if m1 != mMult { 121 | t.Errorf("Multiplication of matrices above failed, expected: \n%v \ngotten: \n%v", &mMult, &m1) 122 | } 123 | } 124 | 125 | func TestMultMatrixVsAssignMul(t *testing.T) { 126 | m1 := TEST_MATRIX1 127 | m2 := TEST_MATRIX2 128 | var mMult T 129 | mMult.AssignMul(&m1, &m2) 130 | t.Log(&m1) 131 | t.Log(&m2) 132 | m1.MultMatrix(&m2) 133 | if m1 != mMult { 134 | t.Errorf("Multiplication of matrices above failed, expected: \n%v \ngotten: \n%v", &mMult, &m1) 135 | } 136 | } 137 | 138 | func BenchmarkAssignMul(b *testing.B) { 139 | m1 := TEST_MATRIX1 140 | m2 := TEST_MATRIX2 141 | var mMult T 142 | b.ResetTimer() 143 | for i := 0; i < b.N; i++ { 144 | mMult.AssignMul(&m1, &m2) 145 | } 146 | } 147 | 148 | func BenchmarkMultMatrix(b *testing.B) { 149 | m1 := TEST_MATRIX1 150 | m2 := TEST_MATRIX2 151 | b.ResetTimer() 152 | for i := 0; i < b.N; i++ { 153 | m1.MultMatrix(&m2) 154 | } 155 | } 156 | 157 | func TestMulVec4vsTransformVec4(t *testing.T) { 158 | m1 := TEST_MATRIX1 159 | v := vec4.T{1, 1.5, 2, 2.5} 160 | v_1 := m1.MulVec4(&v) 161 | v_2 := m1.MulVec4(&v_1) 162 | 163 | m1.TransformVec4(&v) 164 | m1.TransformVec4(&v) 165 | 166 | if v_2 != v { 167 | t.Error(v_2, v) 168 | } 169 | 170 | } 171 | 172 | func BenchmarkMulVec4(b *testing.B) { 173 | m1 := TEST_MATRIX1 174 | b.ResetTimer() 175 | for i := 0; i < b.N; i++ { 176 | v := vec4.T{1, 1.5, 2, 2.5} 177 | v_1 := m1.MulVec4(&v) 178 | m1.MulVec4(&v_1) 179 | } 180 | } 181 | 182 | func BenchmarkTransformVec4(b *testing.B) { 183 | m1 := TEST_MATRIX1 184 | b.ResetTimer() 185 | for i := 0; i < b.N; i++ { 186 | v := vec4.T{1, 1.5, 2, 2.5} 187 | m1.TransformVec4(&v) 188 | m1.TransformVec4(&v) 189 | } 190 | } 191 | 192 | func (mat *T) TransformVec4_PassByValue(v vec4.T) (r vec4.T) { 193 | // Use intermediate variables to not alter further computations. 194 | x := mat[0][0]*v[0] + mat[1][0]*v[1] + mat[2][0]*v[2] + mat[3][0]*v[3] 195 | y := mat[0][1]*v[0] + mat[1][1]*v[1] + mat[2][1]*v[2] + mat[3][1]*v[3] 196 | z := mat[0][2]*v[0] + mat[1][2]*v[1] + mat[2][2]*v[2] + mat[3][2]*v[3] 197 | r[3] = mat[0][3]*v[0] + mat[1][3]*v[1] + mat[2][3]*v[2] + mat[3][3]*v[3] 198 | r[0] = x 199 | r[1] = y 200 | r[2] = z 201 | return r 202 | } 203 | 204 | func Vec4Add_PassByValue(a, b vec4.T) vec4.T { 205 | if a[3] == b[3] { 206 | return vec4.T{a[0] + b[0], a[1] + b[1], a[2] + b[2], 1} 207 | } else { 208 | a3 := a.Vec3DividedByW() 209 | b3 := b.Vec3DividedByW() 210 | return vec4.T{a3[0] + b3[0], a3[1] + b3[1], a3[2] + b3[2], 1} 211 | } 212 | } 213 | 214 | func BenchmarkMulAddVec4_PassByPointer(b *testing.B) { 215 | m1 := TEST_MATRIX1 216 | m2 := TEST_MATRIX2 217 | var v1 vec4.T 218 | b.ResetTimer() 219 | for i := 0; i < b.N; i++ { 220 | v := vec4.T{1, 1.5, 2, 2.5} 221 | m1.TransformVec4(&v) 222 | m2.TransformVec4(&v) 223 | v = vec4.Add(&v, &v1) 224 | v = vec4.Add(&v, &v1) 225 | } 226 | } 227 | 228 | // Demonstrate that 229 | func BenchmarkMulAddVec4_PassByValue(b *testing.B) { 230 | m1 := TEST_MATRIX1 231 | m2 := TEST_MATRIX2 232 | var v1 vec4.T 233 | b.ResetTimer() 234 | for i := 0; i < b.N; i++ { 235 | v := vec4.T{1, 1.5, 2, 2.5} 236 | m1.TransformVec4_PassByValue(v) 237 | m2.TransformVec4_PassByValue(v) 238 | v = Vec4Add_PassByValue(v, v1) 239 | v = Vec4Add_PassByValue(v, v1) 240 | } 241 | } 242 | 243 | func TestIsZeroEps(t *testing.T) { 244 | tests := []struct { 245 | name string 246 | mat T 247 | epsilon float32 248 | want bool 249 | }{ 250 | {"exact zero", Zero, 0.0001, true}, 251 | {"within epsilon", T{vec4.T{0.00001, -0.00001, 0.00001, -0.00001}, vec4.T{-0.00001, 0.00001, -0.00001, 0.00001}, vec4.T{0.00001, -0.00001, 0.00001, -0.00001}, vec4.T{-0.00001, 0.00001, -0.00001, 0.00001}}, 0.0001, true}, 252 | {"at epsilon boundary", T{vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}, vec4.T{0.0001, 0.0001, 0.0001, 0.0001}}, 0.0001, true}, 253 | {"outside epsilon", T{vec4.T{0.001, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}}, 0.0001, false}, 254 | {"one element outside", T{vec4.T{0.00001, 0.00001, 0.00001, 0.00001}, vec4.T{0.001, 0.00001, 0.00001, 0.00001}, vec4.T{0.00001, 0.00001, 0.00001, 0.00001}, vec4.T{0.00001, 0.00001, 0.00001, 0.00001}}, 0.0001, false}, 255 | {"negative outside epsilon", T{vec4.T{-0.001, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}, vec4.T{0, 0, 0, 0}}, 0.0001, false}, 256 | {"identity matrix", Ident, 0.0001, false}, 257 | } 258 | 259 | for _, tt := range tests { 260 | t.Run(tt.name, func(t *testing.T) { 261 | if got := tt.mat.IsZeroEps(tt.epsilon); got != tt.want { 262 | t.Errorf("IsZeroEps() = %v, want %v for mat with epsilon %v", got, tt.want, tt.epsilon) 263 | } 264 | }) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /quaternion/quaternion.go: -------------------------------------------------------------------------------- 1 | // Package quaternion contains a float32 unit-quaternion type T and functions. 2 | package quaternion 3 | 4 | import ( 5 | "fmt" 6 | 7 | math "github.com/chewxy/math32" 8 | "github.com/ungerik/go3d/vec3" 9 | "github.com/ungerik/go3d/vec4" 10 | ) 11 | 12 | var ( 13 | // Zero holds a zero quaternion. 14 | Zero = T{} 15 | 16 | // Ident holds an ident quaternion. 17 | Ident = T{0, 0, 0, 1} 18 | ) 19 | 20 | // T represents an orientation/rotation as a unit quaternion. 21 | // See http://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation 22 | type T [4]float32 23 | 24 | // FromAxisAngle returns a quaternion representing a rotation around and axis. 25 | func FromAxisAngle(axis *vec3.T, angle float32) T { 26 | angle *= 0.5 27 | sin := math.Sin(angle) 28 | q := T{axis[0] * sin, axis[1] * sin, axis[2] * sin, math.Cos(angle)} 29 | return q.Normalized() 30 | } 31 | 32 | // FromXAxisAngle returns a quaternion representing a rotation around the x axis. 33 | func FromXAxisAngle(angle float32) T { 34 | angle *= 0.5 35 | return T{math.Sin(angle), 0, 0, math.Cos(angle)} 36 | } 37 | 38 | // FromYAxisAngle returns a quaternion representing a rotation around the y axis. 39 | func FromYAxisAngle(angle float32) T { 40 | angle *= 0.5 41 | return T{0, math.Sin(angle), 0, math.Cos(angle)} 42 | } 43 | 44 | // FromZAxisAngle returns a quaternion representing a rotation around the z axis. 45 | func FromZAxisAngle(angle float32) T { 46 | angle *= 0.5 47 | return T{0, 0, math.Sin(angle), math.Cos(angle)} 48 | } 49 | 50 | // FromEulerAngles returns a quaternion representing Euler angle(in radian) rotations. 51 | func FromEulerAngles(yHead, xPitch, zRoll float32) T { 52 | qy := FromYAxisAngle(yHead) 53 | qx := FromXAxisAngle(xPitch) 54 | qz := FromZAxisAngle(zRoll) 55 | return Mul3(&qy, &qx, &qz) 56 | } 57 | 58 | // ToEulerAngles returns the euler angle(in radian) rotations of the quaternion. 59 | func (quat *T) ToEulerAngles() (yHead, xPitch, zRoll float32) { 60 | z := quat.RotatedVec3(&vec3.T{0, 0, 1}) 61 | yHead = math.Atan2(z[0], z[2]) 62 | xPitch = -math.Atan2(z[1], math.Sqrt(z[0]*z[0]+z[2]*z[2])) 63 | 64 | quatNew := FromEulerAngles(yHead, xPitch, 0) 65 | 66 | x2 := quatNew.RotatedVec3(&vec3.T{1, 0, 0}) 67 | x := quat.RotatedVec3(&vec3.T{1, 0, 0}) 68 | r := vec3.Cross(&x, &x2) 69 | sin := vec3.Dot(&r, &z) 70 | cos := vec3.Dot(&x, &x2) 71 | zRoll = -math.Atan2(sin, cos) 72 | return 73 | } 74 | 75 | // FromVec4 converts a vec4.T into a quaternion. 76 | func FromVec4(v *vec4.T) T { 77 | return T(*v) 78 | } 79 | 80 | // Vec4 converts the quaternion into a vec4.T. 81 | func (quat *T) Vec4() vec4.T { 82 | return vec4.T(*quat) 83 | } 84 | 85 | // Parse parses T from a string. See also String() 86 | func Parse(s string) (r T, err error) { 87 | _, err = fmt.Sscan(s, &r[0], &r[1], &r[2], &r[3]) 88 | return r, err 89 | } 90 | 91 | // String formats T as string. See also Parse(). 92 | func (quat *T) String() string { 93 | return fmt.Sprint(quat[0], quat[1], quat[2], quat[3]) 94 | } 95 | 96 | // AxisAngle extracts the rotation in form of an axis and a rotation angle. 97 | func (quat *T) AxisAngle() (axis vec3.T, angle float32) { 98 | cos := quat[3] 99 | sin := math.Sqrt(1 - cos*cos) 100 | angle = math.Acos(cos) 101 | 102 | var ooSin float32 103 | if math.Abs(sin) < 0.0005 { 104 | ooSin = 1 105 | } else { 106 | ooSin = 1 / sin 107 | } 108 | axis[0] = quat[0] * ooSin 109 | axis[1] = quat[1] * ooSin 110 | axis[2] = quat[2] * ooSin 111 | 112 | return axis, angle 113 | } 114 | 115 | // Norm returns the norm value of the quaternion. 116 | func (quat *T) Norm() float32 { 117 | return quat[0]*quat[0] + quat[1]*quat[1] + quat[2]*quat[2] + quat[3]*quat[3] 118 | } 119 | 120 | // Normalize normalizes to a unit quaternation. 121 | // Uses the package Epsilon variable for numerical stability: 122 | // - Quaternions with norm < Epsilon are considered zero and left unchanged 123 | // - Quaternions with norm within Epsilon of 1.0 are considered already normalized 124 | func (quat *T) Normalize() *T { 125 | norm := quat.Norm() 126 | if norm < Epsilon { 127 | // Quaternion is effectively zero 128 | return quat 129 | } 130 | if math.Abs(norm-1) < Epsilon { 131 | // Quaternion is already normalized 132 | return quat 133 | } 134 | ool := 1 / math.Sqrt(norm) 135 | quat[0] *= ool 136 | quat[1] *= ool 137 | quat[2] *= ool 138 | quat[3] *= ool 139 | return quat 140 | } 141 | 142 | // Normalized returns a copy normalized to a unit quaternation. 143 | // Uses the package Epsilon variable for numerical stability. See Normalize() for details. 144 | func (quat *T) Normalized() T { 145 | norm := quat.Norm() 146 | if norm < Epsilon { 147 | // Quaternion is effectively zero 148 | return *quat 149 | } 150 | if math.Abs(norm-1) < Epsilon { 151 | // Quaternion is already normalized 152 | return *quat 153 | } 154 | ool := 1 / math.Sqrt(norm) 155 | return T{ 156 | quat[0] * ool, 157 | quat[1] * ool, 158 | quat[2] * ool, 159 | quat[3] * ool, 160 | } 161 | } 162 | 163 | // Negate negates the quaternion. 164 | func (quat *T) Negate() *T { 165 | quat[0] = -quat[0] 166 | quat[1] = -quat[1] 167 | quat[2] = -quat[2] 168 | quat[3] = -quat[3] 169 | return quat 170 | } 171 | 172 | // Negated returns a negated copy of the quaternion. 173 | func (quat *T) Negated() T { 174 | return T{-quat[0], -quat[1], -quat[2], -quat[3]} 175 | } 176 | 177 | // Invert inverts the quaternion. 178 | func (quat *T) Invert() *T { 179 | quat[0] = -quat[0] 180 | quat[1] = -quat[1] 181 | quat[2] = -quat[2] 182 | return quat 183 | } 184 | 185 | // Inverted returns an inverted copy of the quaternion. 186 | func (quat *T) Inverted() T { 187 | return T{-quat[0], -quat[1], -quat[2], quat[3]} 188 | } 189 | 190 | // SetShortestRotation negates the quaternion if it does not represent the shortest rotation from quat to the orientation of other. 191 | // (there are two directions to rotate from the orientation of quat to the orientation of other) 192 | // See IsShortestRotation() 193 | func (quat *T) SetShortestRotation(other *T) *T { 194 | if !IsShortestRotation(quat, other) { 195 | quat.Negate() 196 | } 197 | return quat 198 | } 199 | 200 | // IsShortestRotation returns if the rotation from a to b is the shortest possible rotation. 201 | // (there are two directions to rotate from the orientation of quat to the orientation of other) 202 | // See T.SetShortestRotation 203 | func IsShortestRotation(a, b *T) bool { 204 | return Dot(a, b) >= 0 205 | } 206 | 207 | // IsUnitQuat returns if the quaternion is within tolerance of the unit quaternion. 208 | func (quat *T) IsUnitQuat(tolerance float32) bool { 209 | norm := quat.Norm() 210 | return norm >= (1.0-tolerance) && norm <= (1.0+tolerance) 211 | } 212 | 213 | // RotateVec3 rotates v by the rotation represented by the quaternion. 214 | // using the algorithm mentioned here https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion 215 | func (quat *T) RotateVec3(v *vec3.T) { 216 | u := vec3.T{quat[0], quat[1], quat[2]} 217 | s := quat[3] 218 | vt1 := u.Scaled(2 * vec3.Dot(&u, v)) 219 | vt2 := v.Scaled(s*s - vec3.Dot(&u, &u)) 220 | vt3 := vec3.Cross(&u, v) 221 | vt3 = vt3.Scaled(2 * s) 222 | v[0] = vt1[0] + vt2[0] + vt3[0] 223 | v[1] = vt1[1] + vt2[1] + vt3[1] 224 | v[2] = vt1[2] + vt2[2] + vt3[2] 225 | } 226 | 227 | // RotatedVec3 returns a rotated copy of v. 228 | // using the algorithm mentioned here https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion 229 | func (quat *T) RotatedVec3(v *vec3.T) vec3.T { 230 | u := vec3.T{quat[0], quat[1], quat[2]} 231 | s := quat[3] 232 | vt1 := u.Scaled(2 * vec3.Dot(&u, v)) 233 | vt2 := v.Scaled(s*s - vec3.Dot(&u, &u)) 234 | vt3 := vec3.Cross(&u, v) 235 | vt3 = vt3.Scaled(2 * s) 236 | return vec3.T{vt1[0] + vt2[0] + vt3[0], vt1[1] + vt2[1] + vt3[1], vt1[2] + vt2[2] + vt3[2]} 237 | } 238 | 239 | // Dot returns the dot product of two quaternions. 240 | func Dot(a, b *T) float32 { 241 | return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3] 242 | } 243 | 244 | // MulRaw multiplies two quaternions without normalizing the result. 245 | // Use this when you want to chain multiple quaternion operations and normalize once at the end: 246 | // 247 | // q := quaternion.MulRaw(&a, &b) 248 | // q = quaternion.MulRaw(&q, &c) 249 | // q.Normalize() // Normalize once at the end 250 | // 251 | // For most use cases, use Mul() instead which automatically normalizes. 252 | func MulRaw(a, b *T) T { 253 | return T{ 254 | a[3]*b[0] + a[0]*b[3] + a[1]*b[2] - a[2]*b[1], 255 | a[3]*b[1] + a[1]*b[3] + a[2]*b[0] - a[0]*b[2], 256 | a[3]*b[2] + a[2]*b[3] + a[0]*b[1] - a[1]*b[0], 257 | a[3]*b[3] - a[0]*b[0] - a[1]*b[1] - a[2]*b[2], 258 | } 259 | } 260 | 261 | // Mul multiplies two quaternions and normalizes the result. 262 | // The result is guaranteed to be a unit quaternion suitable for rotations. 263 | // For chaining multiple operations, consider using MulRaw() and normalizing once at the end. 264 | func Mul(a, b *T) T { 265 | q := MulRaw(a, b) 266 | return q.Normalized() 267 | } 268 | 269 | // Mul3 multiplies three quaternions. 270 | func Mul3(a, b, c *T) T { 271 | q := Mul(a, b) 272 | return Mul(&q, c) 273 | } 274 | 275 | // Mul4 multiplies four quaternions. 276 | func Mul4(a, b, c, d *T) T { 277 | q := Mul(a, b) 278 | q = Mul(&q, c) 279 | return Mul(&q, d) 280 | } 281 | 282 | // Slerp returns the spherical linear interpolation quaternion between a and b at t (0,1). 283 | // See http://en.wikipedia.org/wiki/Slerp 284 | func Slerp(a, b *T, t float32) T { 285 | dot := a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3] 286 | 287 | // If quaternions are very close, use linear interpolation to avoid division by zero 288 | const threshold = 0.9995 289 | if math.Abs(dot) > threshold { 290 | // Linear interpolation for close quaternions 291 | q := T{ 292 | a[0]*(1-t) + b[0]*t, 293 | a[1]*(1-t) + b[1]*t, 294 | a[2]*(1-t) + b[2]*t, 295 | a[3]*(1-t) + b[3]*t, 296 | } 297 | return q.Normalized() 298 | } 299 | 300 | d := math.Acos(dot) 301 | sinD := math.Sin(d) 302 | ooSinD := 1 / sinD 303 | 304 | t1 := math.Sin(d*(1-t)) * ooSinD 305 | t2 := math.Sin(d*t) * ooSinD 306 | 307 | q := T{ 308 | a[0]*t1 + b[0]*t2, 309 | a[1]*t1 + b[1]*t2, 310 | a[2]*t1 + b[2]*t2, 311 | a[3]*t1 + b[3]*t2, 312 | } 313 | 314 | return q.Normalized() 315 | } 316 | 317 | // Vec3Diff returns the rotation quaternion between two vectors. 318 | func Vec3Diff(a, b *vec3.T) T { 319 | dot := vec3.Dot(a, b) 320 | 321 | // Handle opposite vectors (dot product near -1) 322 | if dot < -0.999999 { 323 | // Find an orthogonal axis 324 | axis := vec3.UnitX 325 | test := vec3.Cross(a, &axis) 326 | if test.LengthSqr() < 0.01 { 327 | // a is parallel to UnitX, use UnitY instead 328 | axis = vec3.UnitY 329 | test = vec3.Cross(a, &axis) 330 | } 331 | test.Normalize() 332 | // 180 degree rotation around the orthogonal axis 333 | return T{test[0], test[1], test[2], 0} 334 | } 335 | 336 | cr := vec3.Cross(a, b) 337 | sr := math.Sqrt(2 * (1 + dot)) 338 | oosr := 1 / sr 339 | 340 | q := T{cr[0] * oosr, cr[1] * oosr, cr[2] * oosr, sr * 0.5} 341 | return q.Normalized() 342 | } 343 | -------------------------------------------------------------------------------- /float64/quaternion/quaternion.go: -------------------------------------------------------------------------------- 1 | // Package quaternion contains a float64 unit-quaternion type T and functions. 2 | package quaternion 3 | 4 | import ( 5 | "fmt" 6 | "math" 7 | 8 | "github.com/ungerik/go3d/float64/vec3" 9 | "github.com/ungerik/go3d/float64/vec4" 10 | ) 11 | 12 | var ( 13 | // Zero holds a zero quaternion. 14 | Zero = T{} 15 | 16 | // Ident holds an ident quaternion. 17 | Ident = T{0, 0, 0, 1} 18 | ) 19 | 20 | // T represents an orientation/rotation as a unit quaternion. 21 | // See http://en.wikipedia.org/wiki/Quaternions_and_spatial_rotation 22 | type T [4]float64 23 | 24 | // FromAxisAngle returns a quaternion representing a rotation around and axis. 25 | func FromAxisAngle(axis *vec3.T, angle float64) T { 26 | angle *= 0.5 27 | sin := math.Sin(angle) 28 | q := T{axis[0] * sin, axis[1] * sin, axis[2] * sin, math.Cos(angle)} 29 | return q.Normalized() 30 | } 31 | 32 | // FromXAxisAngle returns a quaternion representing a rotation around the x axis. 33 | func FromXAxisAngle(angle float64) T { 34 | angle *= 0.5 35 | return T{math.Sin(angle), 0, 0, math.Cos(angle)} 36 | } 37 | 38 | // FromYAxisAngle returns a quaternion representing a rotation around the y axis. 39 | func FromYAxisAngle(angle float64) T { 40 | angle *= 0.5 41 | return T{0, math.Sin(angle), 0, math.Cos(angle)} 42 | } 43 | 44 | // FromZAxisAngle returns a quaternion representing a rotation around the z axis. 45 | func FromZAxisAngle(angle float64) T { 46 | angle *= 0.5 47 | return T{0, 0, math.Sin(angle), math.Cos(angle)} 48 | } 49 | 50 | // FromEulerAngles returns a quaternion representing Euler angle(in radian) rotations. 51 | func FromEulerAngles(yHead, xPitch, zRoll float64) T { 52 | qy := FromYAxisAngle(yHead) 53 | qx := FromXAxisAngle(xPitch) 54 | qz := FromZAxisAngle(zRoll) 55 | return Mul3(&qy, &qx, &qz) 56 | } 57 | 58 | // ToEulerAngles returns the euler angle(in radian) rotations of the quaternion. 59 | func (quat *T) ToEulerAngles() (yHead, xPitch, zRoll float64) { 60 | z := quat.RotatedVec3(&vec3.T{0, 0, 1}) 61 | yHead = math.Atan2(z[0], z[2]) 62 | xPitch = -math.Atan2(z[1], math.Sqrt(z[0]*z[0]+z[2]*z[2])) 63 | 64 | quatNew := FromEulerAngles(yHead, xPitch, 0) 65 | 66 | x2 := quatNew.RotatedVec3(&vec3.T{1, 0, 0}) 67 | x := quat.RotatedVec3(&vec3.T{1, 0, 0}) 68 | r := vec3.Cross(&x, &x2) 69 | sin := vec3.Dot(&r, &z) 70 | cos := vec3.Dot(&x, &x2) 71 | zRoll = -math.Atan2(sin, cos) 72 | return 73 | } 74 | 75 | // FromVec4 converts a vec4.T into a quaternion. 76 | func FromVec4(v *vec4.T) T { 77 | return T(*v) 78 | } 79 | 80 | // Vec4 converts the quaternion into a vec4.T. 81 | func (quat *T) Vec4() vec4.T { 82 | return vec4.T(*quat) 83 | } 84 | 85 | // Parse parses T from a string. See also String() 86 | func Parse(s string) (r T, err error) { 87 | _, err = fmt.Sscan(s, &r[0], &r[1], &r[2], &r[3]) 88 | return r, err 89 | } 90 | 91 | // String formats T as string. See also Parse(). 92 | func (quat *T) String() string { 93 | return fmt.Sprint(quat[0], quat[1], quat[2], quat[3]) 94 | } 95 | 96 | // AxisAngle extracts the rotation from a normalized quaternion in the form of an axis and a rotation angle. 97 | func (quat *T) AxisAngle() (axis vec3.T, angle float64) { 98 | cos := quat[3] 99 | sin := math.Sqrt(1 - cos*cos) 100 | angle = math.Acos(cos) * 2 101 | 102 | var ooSin float64 103 | if math.Abs(sin) < 0.0005 { 104 | ooSin = 1 105 | } else { 106 | ooSin = 1 / sin 107 | } 108 | axis[0] = quat[0] * ooSin 109 | axis[1] = quat[1] * ooSin 110 | axis[2] = quat[2] * ooSin 111 | 112 | return axis, angle 113 | } 114 | 115 | // Norm returns the norm value of the quaternion. 116 | func (quat *T) Norm() float64 { 117 | return quat[0]*quat[0] + quat[1]*quat[1] + quat[2]*quat[2] + quat[3]*quat[3] 118 | } 119 | 120 | // Normalize normalizes to a unit quaternation. 121 | // Uses the package Epsilon variable for numerical stability: 122 | // - Quaternions with norm < Epsilon are considered zero and left unchanged 123 | // - Quaternions with norm within Epsilon of 1.0 are considered already normalized 124 | func (quat *T) Normalize() *T { 125 | norm := quat.Norm() 126 | if norm < Epsilon { 127 | // Quaternion is effectively zero 128 | return quat 129 | } 130 | if math.Abs(norm-1) < Epsilon { 131 | // Quaternion is already normalized 132 | return quat 133 | } 134 | ool := 1 / math.Sqrt(norm) 135 | quat[0] *= ool 136 | quat[1] *= ool 137 | quat[2] *= ool 138 | quat[3] *= ool 139 | return quat 140 | } 141 | 142 | // Normalized returns a copy normalized to a unit quaternation. 143 | // Uses the package Epsilon variable for numerical stability. See Normalize() for details. 144 | func (quat *T) Normalized() T { 145 | norm := quat.Norm() 146 | if norm < Epsilon { 147 | // Quaternion is effectively zero 148 | return *quat 149 | } 150 | if math.Abs(norm-1) < Epsilon { 151 | // Quaternion is already normalized 152 | return *quat 153 | } 154 | ool := 1 / math.Sqrt(norm) 155 | return T{ 156 | quat[0] * ool, 157 | quat[1] * ool, 158 | quat[2] * ool, 159 | quat[3] * ool, 160 | } 161 | } 162 | 163 | // Negate negates the quaternion. 164 | func (quat *T) Negate() *T { 165 | quat[0] = -quat[0] 166 | quat[1] = -quat[1] 167 | quat[2] = -quat[2] 168 | quat[3] = -quat[3] 169 | return quat 170 | } 171 | 172 | // Negated returns a negated copy of the quaternion. 173 | func (quat *T) Negated() T { 174 | return T{-quat[0], -quat[1], -quat[2], -quat[3]} 175 | } 176 | 177 | // Invert inverts the quaternion. 178 | func (quat *T) Invert() *T { 179 | quat[0] = -quat[0] 180 | quat[1] = -quat[1] 181 | quat[2] = -quat[2] 182 | return quat 183 | } 184 | 185 | // Inverted returns an inverted copy of the quaternion. 186 | func (quat *T) Inverted() T { 187 | return T{-quat[0], -quat[1], -quat[2], quat[3]} 188 | } 189 | 190 | // SetShortestRotation negates the quaternion if it does not represent the shortest rotation from quat to the orientation of other. 191 | // (there are two directions to rotate from the orientation of quat to the orientation of other) 192 | // See IsShortestRotation() 193 | func (quat *T) SetShortestRotation(other *T) *T { 194 | if !IsShortestRotation(quat, other) { 195 | quat.Negate() 196 | } 197 | return quat 198 | } 199 | 200 | // IsShortestRotation returns if the rotation from a to b is the shortest possible rotation. 201 | // (there are two directions to rotate from the orientation of quat to the orientation of other) 202 | // See T.SetShortestRotation 203 | func IsShortestRotation(a, b *T) bool { 204 | return Dot(a, b) >= 0 205 | } 206 | 207 | // IsUnitQuat returns if the quaternion is within tolerance of the unit quaternion. 208 | func (quat *T) IsUnitQuat(tolerance float64) bool { 209 | norm := quat.Norm() 210 | return norm >= (1.0-tolerance) && norm <= (1.0+tolerance) 211 | } 212 | 213 | // RotateVec3 rotates v by the rotation represented by the quaternion. 214 | // using the algorithm mentioned here https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion 215 | func (quat *T) RotateVec3(v *vec3.T) { 216 | u := vec3.T{quat[0], quat[1], quat[2]} 217 | s := quat[3] 218 | vt1 := u.Scaled(2 * vec3.Dot(&u, v)) 219 | vt2 := v.Scaled(s*s - vec3.Dot(&u, &u)) 220 | vt3 := vec3.Cross(&u, v) 221 | vt3 = vt3.Scaled(2 * s) 222 | v[0] = vt1[0] + vt2[0] + vt3[0] 223 | v[1] = vt1[1] + vt2[1] + vt3[1] 224 | v[2] = vt1[2] + vt2[2] + vt3[2] 225 | } 226 | 227 | // RotatedVec3 returns a rotated copy of v. 228 | // using the algorithm mentioned here https://gamedev.stackexchange.com/questions/28395/rotating-vector3-by-a-quaternion 229 | func (quat *T) RotatedVec3(v *vec3.T) vec3.T { 230 | u := vec3.T{quat[0], quat[1], quat[2]} 231 | s := quat[3] 232 | vt1 := u.Scaled(2 * vec3.Dot(&u, v)) 233 | vt2 := v.Scaled(s*s - vec3.Dot(&u, &u)) 234 | vt3 := vec3.Cross(&u, v) 235 | vt3 = vt3.Scaled(2 * s) 236 | return vec3.T{vt1[0] + vt2[0] + vt3[0], vt1[1] + vt2[1] + vt3[1], vt1[2] + vt2[2] + vt3[2]} 237 | } 238 | 239 | // Dot returns the dot product of two quaternions. 240 | func Dot(a, b *T) float64 { 241 | return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3] 242 | } 243 | 244 | // MulRaw multiplies two quaternions without normalizing the result. 245 | // Use this when you want to chain multiple quaternion operations and normalize once at the end: 246 | // 247 | // q := quaternion.MulRaw(&a, &b) 248 | // q = quaternion.MulRaw(&q, &c) 249 | // q.Normalize() // Normalize once at the end 250 | // 251 | // For most use cases, use Mul() instead which automatically normalizes. 252 | func MulRaw(a, b *T) T { 253 | return T{ 254 | a[3]*b[0] + a[0]*b[3] + a[1]*b[2] - a[2]*b[1], 255 | a[3]*b[1] + a[1]*b[3] + a[2]*b[0] - a[0]*b[2], 256 | a[3]*b[2] + a[2]*b[3] + a[0]*b[1] - a[1]*b[0], 257 | a[3]*b[3] - a[0]*b[0] - a[1]*b[1] - a[2]*b[2], 258 | } 259 | } 260 | 261 | // Mul multiplies two quaternions and normalizes the result. 262 | // The result is guaranteed to be a unit quaternion suitable for rotations. 263 | // For chaining multiple operations, consider using MulRaw() and normalizing once at the end. 264 | func Mul(a, b *T) T { 265 | q := MulRaw(a, b) 266 | return q.Normalized() 267 | } 268 | 269 | // Mul3 multiplies three quaternions. 270 | func Mul3(a, b, c *T) T { 271 | q := Mul(a, b) 272 | return Mul(&q, c) 273 | } 274 | 275 | // Mul4 multiplies four quaternions. 276 | func Mul4(a, b, c, d *T) T { 277 | q := Mul(a, b) 278 | q = Mul(&q, c) 279 | return Mul(&q, d) 280 | } 281 | 282 | // Slerp returns the spherical linear interpolation quaternion between a and b at t (0,1). 283 | // See http://en.wikipedia.org/wiki/Slerp 284 | func Slerp(a, b *T, t float64) T { 285 | dot := a[0]*b[0] + a[1]*b[1] + a[2]*b[2] + a[3]*b[3] 286 | 287 | // If quaternions are very close, use linear interpolation to avoid division by zero 288 | const threshold = 0.9995 289 | if math.Abs(dot) > threshold { 290 | // Linear interpolation for close quaternions 291 | q := T{ 292 | a[0]*(1-t) + b[0]*t, 293 | a[1]*(1-t) + b[1]*t, 294 | a[2]*(1-t) + b[2]*t, 295 | a[3]*(1-t) + b[3]*t, 296 | } 297 | return q.Normalized() 298 | } 299 | 300 | d := math.Acos(dot) 301 | sinD := math.Sin(d) 302 | ooSinD := 1 / sinD 303 | 304 | t1 := math.Sin(d*(1-t)) * ooSinD 305 | t2 := math.Sin(d*t) * ooSinD 306 | 307 | q := T{ 308 | a[0]*t1 + b[0]*t2, 309 | a[1]*t1 + b[1]*t2, 310 | a[2]*t1 + b[2]*t2, 311 | a[3]*t1 + b[3]*t2, 312 | } 313 | 314 | return q.Normalized() 315 | } 316 | 317 | // Vec3Diff returns the rotation quaternion between two vectors. 318 | func Vec3Diff(a, b *vec3.T) T { 319 | dot := vec3.Dot(a, b) 320 | 321 | // Handle opposite vectors (dot product near -1) 322 | if dot < -0.999999 { 323 | // Find an orthogonal axis 324 | axis := vec3.UnitX 325 | test := vec3.Cross(a, &axis) 326 | if test.LengthSqr() < 0.01 { 327 | // a is parallel to UnitX, use UnitY instead 328 | axis = vec3.UnitY 329 | test = vec3.Cross(a, &axis) 330 | } 331 | test.Normalize() 332 | // 180 degree rotation around the orthogonal axis 333 | return T{test[0], test[1], test[2], 0} 334 | } 335 | 336 | cr := vec3.Cross(a, b) 337 | sr := math.Sqrt(2 * (1 + dot)) 338 | oosr := 1 / sr 339 | 340 | q := T{cr[0] * oosr, cr[1] * oosr, cr[2] * oosr, sr * 0.5} 341 | return q.Normalized() 342 | } 343 | -------------------------------------------------------------------------------- /float64/vec3/vec3.go: -------------------------------------------------------------------------------- 1 | // Package vec3 contains a 3D float64 vector type T and functions. 2 | package vec3 3 | 4 | import ( 5 | "fmt" 6 | "math" 7 | 8 | "github.com/ungerik/go3d/float64/generic" 9 | ) 10 | 11 | var ( 12 | // Zero holds a zero vector. 13 | Zero = T{} 14 | 15 | // UnitX holds a vector with X set to one. 16 | UnitX = T{1, 0, 0} 17 | // UnitY holds a vector with Y set to one. 18 | UnitY = T{0, 1, 0} 19 | // UnitZ holds a vector with Z set to one. 20 | UnitZ = T{0, 0, 1} 21 | // UnitXYZ holds a vector with X, Y, Z set to one. 22 | UnitXYZ = T{1, 1, 1} 23 | 24 | // Red holds the color red. 25 | Red = T{1, 0, 0} 26 | // Green holds the color green. 27 | Green = T{0, 1, 0} 28 | // Blue holds the color blue. 29 | Blue = T{0, 0, 1} 30 | // Black holds the color black. 31 | Black = T{0, 0, 0} 32 | // White holds the color white. 33 | White = T{1, 1, 1} 34 | 35 | // MinVal holds a vector with the smallest possible component values. 36 | MinVal = T{-math.MaxFloat64, -math.MaxFloat64, -math.MaxFloat64} 37 | // MaxVal holds a vector with the highest possible component values. 38 | MaxVal = T{+math.MaxFloat64, +math.MaxFloat64, +math.MaxFloat64} 39 | ) 40 | 41 | // T represents a 3D vector. 42 | type T [3]float64 43 | 44 | // From copies a T from a generic.T implementation. 45 | func From(other generic.T) T { 46 | switch other.Size() { 47 | case 2: 48 | return T{other.Get(0, 0), other.Get(0, 1), 0} 49 | case 3, 4: 50 | return T{other.Get(0, 0), other.Get(0, 1), other.Get(0, 2)} 51 | default: 52 | panic("Unsupported type") 53 | } 54 | } 55 | 56 | // Parse parses T from a string. See also String() 57 | func Parse(s string) (r T, err error) { 58 | _, err = fmt.Sscan(s, &r[0], &r[1], &r[2]) 59 | return r, err 60 | } 61 | 62 | // String formats T as string. See also Parse(). 63 | func (vec *T) String() string { 64 | return fmt.Sprint(vec[0], vec[1], vec[2]) 65 | } 66 | 67 | // Rows returns the number of rows of the vector. 68 | func (vec *T) Rows() int { 69 | return 3 70 | } 71 | 72 | // Cols returns the number of columns of the vector. 73 | func (vec *T) Cols() int { 74 | return 1 75 | } 76 | 77 | // Size returns the number elements of the vector. 78 | func (vec *T) Size() int { 79 | return 3 80 | } 81 | 82 | // Slice returns the elements of the vector as slice. 83 | func (vec *T) Slice() []float64 { 84 | return vec[:] 85 | } 86 | 87 | // Get returns one element of the vector. 88 | func (vec *T) Get(col, row int) float64 { 89 | return vec[row] 90 | } 91 | 92 | // IsZero checks if all elements of the vector are exactly zero. 93 | // Uses exact equality comparison, which may not be suitable for floating-point math results. 94 | // For tolerance-based comparison, use IsZeroEps instead. 95 | func (vec *T) IsZero() bool { 96 | return vec[0] == 0 && vec[1] == 0 && vec[2] == 0 97 | } 98 | 99 | // IsZeroEps checks if all elements of the vector are zero within the given epsilon tolerance. 100 | // This is the recommended method for comparing floating-point vectors that result from calculations. 101 | // For exact zero comparison, use IsZero instead. 102 | func (vec *T) IsZeroEps(epsilon float64) bool { 103 | return math.Abs(vec[0]) <= epsilon && math.Abs(vec[1]) <= epsilon && math.Abs(vec[2]) <= epsilon 104 | } 105 | 106 | // Length returns the length of the vector. 107 | // See also LengthSqr and Normalize. 108 | func (vec *T) Length() float64 { 109 | return math.Sqrt(vec.LengthSqr()) 110 | } 111 | 112 | // LengthSqr returns the squared length of the vector. 113 | // See also Length and Normalize. 114 | func (vec *T) LengthSqr() float64 { 115 | return vec[0]*vec[0] + vec[1]*vec[1] + vec[2]*vec[2] 116 | } 117 | 118 | // Scale multiplies all element of the vector by f and returns vec. 119 | func (vec *T) Scale(f float64) *T { 120 | vec[0] *= f 121 | vec[1] *= f 122 | vec[2] *= f 123 | return vec 124 | } 125 | 126 | // Scaled returns a copy of vec with all elements multiplies by f. 127 | func (vec *T) Scaled(f float64) T { 128 | return T{vec[0] * f, vec[1] * f, vec[2] * f} 129 | } 130 | 131 | // PracticallyEquals compares two vectors if they are equal with each other within a delta tolerance. 132 | func (vec *T) PracticallyEquals(compareVector *T, allowedDelta float64) bool { 133 | return (math.Abs(vec[0]-compareVector[0]) <= allowedDelta) && 134 | (math.Abs(vec[1]-compareVector[1]) <= allowedDelta) && 135 | (math.Abs(vec[2]-compareVector[2]) <= allowedDelta) 136 | } 137 | 138 | // PracticallyEquals compares two values if they are equal with each other within a delta tolerance. 139 | func PracticallyEquals(v1, v2, allowedDelta float64) bool { 140 | return math.Abs(v1-v2) <= allowedDelta 141 | } 142 | 143 | // Invert inverts the vector. 144 | func (vec *T) Invert() *T { 145 | vec[0] = -vec[0] 146 | vec[1] = -vec[1] 147 | vec[2] = -vec[2] 148 | return vec 149 | } 150 | 151 | // Inverted returns an inverted copy of the vector. 152 | func (vec *T) Inverted() T { 153 | return T{-vec[0], -vec[1], -vec[2]} 154 | } 155 | 156 | // Abs sets every component of the vector to its absolute value. 157 | func (vec *T) Abs() *T { 158 | vec[0] = math.Abs(vec[0]) 159 | vec[1] = math.Abs(vec[1]) 160 | vec[2] = math.Abs(vec[2]) 161 | return vec 162 | } 163 | 164 | // Absed returns a copy of the vector containing the absolute values. 165 | func (vec *T) Absed() T { 166 | return T{math.Abs(vec[0]), math.Abs(vec[1]), math.Abs(vec[2])} 167 | } 168 | 169 | // Normalize normalizes the vector to unit length. 170 | // Uses the package Epsilon variable for numerical stability: 171 | // - Vectors with squared length < Epsilon are considered zero and left unchanged 172 | // - Vectors with squared length within Epsilon of 1.0 are considered already normalized 173 | func (vec *T) Normalize() *T { 174 | sl := vec.LengthSqr() 175 | if sl < Epsilon { 176 | // Vector is effectively zero 177 | return vec 178 | } 179 | if math.Abs(sl-1) < Epsilon { 180 | // Vector is already normalized 181 | return vec 182 | } 183 | return vec.Scale(1 / math.Sqrt(sl)) 184 | } 185 | 186 | // Normalized returns a unit length normalized copy of the vector. 187 | // Uses the package Epsilon variable for numerical stability. See Normalize() for details. 188 | func (vec *T) Normalized() T { 189 | v := *vec 190 | v.Normalize() 191 | return v 192 | } 193 | 194 | // Normal returns an orthogonal vector. 195 | // Uses the package Epsilon variable when checking if the cross product is zero, 196 | // which provides numerical stability when the input vector is parallel to UnitZ. 197 | // Falls back to UnitX when the cross product is effectively zero. 198 | func (vec *T) Normal() T { 199 | n := Cross(vec, &UnitZ) 200 | if n.IsZeroEps(Epsilon) { 201 | return UnitX 202 | } 203 | return n.Normalized() 204 | } 205 | 206 | // Add adds another vector to vec. 207 | func (vec *T) Add(v *T) *T { 208 | vec[0] += v[0] 209 | vec[1] += v[1] 210 | vec[2] += v[2] 211 | return vec 212 | } 213 | 214 | // Added adds another vector to vec and returns a copy of the result 215 | func (vec *T) Added(v *T) T { 216 | return T{vec[0] + v[0], vec[1] + v[1], vec[2] + v[2]} 217 | } 218 | 219 | // Sub subtracts another vector from vec. 220 | func (vec *T) Sub(v *T) *T { 221 | vec[0] -= v[0] 222 | vec[1] -= v[1] 223 | vec[2] -= v[2] 224 | return vec 225 | } 226 | 227 | // Subed subtracts another vector from vec and returns a copy of the result 228 | func (vec *T) Subed(v *T) T { 229 | return T{vec[0] - v[0], vec[1] - v[1], vec[2] - v[2]} 230 | } 231 | 232 | // Mul multiplies the components of the vector with the respective components of v. 233 | func (vec *T) Mul(v *T) *T { 234 | vec[0] *= v[0] 235 | vec[1] *= v[1] 236 | vec[2] *= v[2] 237 | return vec 238 | } 239 | 240 | // Muled multiplies the components of the vector with the respective components of v and returns a copy of the result 241 | func (vec *T) Muled(v *T) T { 242 | return T{vec[0] * v[0], vec[1] * v[1], vec[2] * v[2]} 243 | } 244 | 245 | // SquareDistance the distance between two vectors squared (= distance*distance) 246 | func SquareDistance(a, b *T) float64 { 247 | d := Sub(a, b) 248 | return d.LengthSqr() 249 | } 250 | 251 | // Distance the distance between two vectors 252 | func Distance(a, b *T) float64 { 253 | d := Sub(a, b) 254 | return d.Length() 255 | } 256 | 257 | // Add adds the composants of the two vectors and returns a new vector with the sum of the two vectors. 258 | func Add(a, b *T) T { 259 | return T{a[0] + b[0], a[1] + b[1], a[2] + b[2]} 260 | } 261 | 262 | // Sub returns the difference of two vectors. 263 | func Sub(a, b *T) T { 264 | return T{a[0] - b[0], a[1] - b[1], a[2] - b[2]} 265 | } 266 | 267 | // Mul returns the component wise product of two vectors. 268 | func Mul(a, b *T) T { 269 | return T{a[0] * b[0], a[1] * b[1], a[2] * b[2]} 270 | } 271 | 272 | // Dot returns the dot product of two vectors. 273 | func Dot(a, b *T) float64 { 274 | return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] 275 | } 276 | 277 | // Cross returns the cross product of two vectors. 278 | func Cross(a, b *T) T { 279 | return T{ 280 | a[1]*b[2] - a[2]*b[1], 281 | a[2]*b[0] - a[0]*b[2], 282 | a[0]*b[1] - a[1]*b[0], 283 | } 284 | } 285 | 286 | // Sinus returns the sinus value of the (shortest/smallest) angle between the two vectors a and b. 287 | // The returned sine value is in the range 0.0 ≤ value ≤ 1.0. 288 | // The angle is always considered to be in the range 0 to Pi radians and thus the sine value returned is always positive. 289 | func Sinus(a, b *T) float64 { 290 | lenSqrProduct := a.LengthSqr() * b.LengthSqr() 291 | if lenSqrProduct < 1e-20 { 292 | return 0.0 293 | } 294 | 295 | cross := Cross(a, b) 296 | v := cross.Length() / math.Sqrt(lenSqrProduct) 297 | 298 | if v > 1.0 { 299 | return 1.0 300 | } else if v < 0.0 { 301 | return 0.0 302 | } 303 | return v 304 | } 305 | 306 | // Cosine returns the cosine value of the angle between the two vectors. 307 | // The returned cosine value is in the range -1.0 ≤ value ≤ 1.0. 308 | func Cosine(a, b *T) float64 { 309 | lenSqrProduct := a.LengthSqr() * b.LengthSqr() 310 | if lenSqrProduct < 1e-20 { 311 | return 0.0 312 | } 313 | 314 | v := Dot(a, b) / math.Sqrt(lenSqrProduct) 315 | 316 | if v > 1.0 { 317 | return 1.0 318 | } else if v < -1.0 { 319 | return -1.0 320 | } 321 | return v 322 | } 323 | 324 | // Angle returns the angle value of the (shortest/smallest) angle between the two vectors a and b. 325 | // The returned value is in the range 0 ≤ angle ≤ Pi radians. 326 | func Angle(a, b *T) float64 { 327 | return math.Acos(Cosine(a, b)) 328 | } 329 | 330 | // Min returns the component wise minimum of two vectors. 331 | func Min(a, b *T) T { 332 | min := *a 333 | if b[0] < min[0] { 334 | min[0] = b[0] 335 | } 336 | if b[1] < min[1] { 337 | min[1] = b[1] 338 | } 339 | if b[2] < min[2] { 340 | min[2] = b[2] 341 | } 342 | return min 343 | } 344 | 345 | // Max returns the component wise maximum of two vectors. 346 | func Max(a, b *T) T { 347 | max := *a 348 | if b[0] > max[0] { 349 | max[0] = b[0] 350 | } 351 | if b[1] > max[1] { 352 | max[1] = b[1] 353 | } 354 | if b[2] > max[2] { 355 | max[2] = b[2] 356 | } 357 | return max 358 | } 359 | 360 | // Interpolate interpolates between a and b at t (0,1). 361 | func Interpolate(a, b *T, t float64) T { 362 | t1 := 1 - t 363 | return T{ 364 | a[0]*t1 + b[0]*t, 365 | a[1]*t1 + b[1]*t, 366 | a[2]*t1 + b[2]*t, 367 | } 368 | } 369 | 370 | // Clamp clamps the vector's components to be in the range of min to max. 371 | func (vec *T) Clamp(min, max *T) *T { 372 | for i := range vec { 373 | if vec[i] < min[i] { 374 | vec[i] = min[i] 375 | } else if vec[i] > max[i] { 376 | vec[i] = max[i] 377 | } 378 | } 379 | return vec 380 | } 381 | 382 | // Clamped returns a copy of the vector with the components clamped to be in the range of min to max. 383 | func (vec *T) Clamped(min, max *T) T { 384 | result := *vec 385 | result.Clamp(min, max) 386 | return result 387 | } 388 | 389 | // Clamp01 clamps the vector's components to be in the range of 0 to 1. 390 | func (vec *T) Clamp01() *T { 391 | return vec.Clamp(&Zero, &UnitXYZ) 392 | } 393 | 394 | // Clamped01 returns a copy of the vector with the components clamped to be in the range of 0 to 1. 395 | func (vec *T) Clamped01() T { 396 | result := *vec 397 | result.Clamp01() 398 | return result 399 | } 400 | -------------------------------------------------------------------------------- /vec3/vec3.go: -------------------------------------------------------------------------------- 1 | // Package vec3 contains a 3D float32 vector type T and functions. 2 | package vec3 3 | 4 | import ( 5 | "fmt" 6 | 7 | math "github.com/chewxy/math32" 8 | "github.com/ungerik/go3d/generic" 9 | ) 10 | 11 | var ( 12 | // Zero holds a zero vector. 13 | Zero = T{} 14 | 15 | // UnitX holds a vector with X set to one. 16 | UnitX = T{1, 0, 0} 17 | // UnitY holds a vector with Y set to one. 18 | UnitY = T{0, 1, 0} 19 | // UnitZ holds a vector with Z set to one. 20 | UnitZ = T{0, 0, 1} 21 | // UnitXYZ holds a vector with X, Y, Z set to one. 22 | UnitXYZ = T{1, 1, 1} 23 | 24 | // Red holds the color red. 25 | Red = T{1, 0, 0} 26 | // Green holds the color green. 27 | Green = T{0, 1, 0} 28 | // Blue holds the color blue. 29 | Blue = T{0, 0, 1} 30 | // Black holds the color black. 31 | Black = T{0, 0, 0} 32 | // White holds the color white. 33 | White = T{1, 1, 1} 34 | 35 | // MinVal holds a vector with the smallest possible component values. 36 | MinVal = T{-math.MaxFloat32, -math.MaxFloat32, -math.MaxFloat32} 37 | // MaxVal holds a vector with the highest possible component values. 38 | MaxVal = T{+math.MaxFloat32, +math.MaxFloat32, +math.MaxFloat32} 39 | ) 40 | 41 | // T represents a 3D vector. 42 | type T [3]float32 43 | 44 | // From copies a T from a generic.T implementation. 45 | func From(other generic.T) T { 46 | switch other.Size() { 47 | case 2: 48 | return T{other.Get(0, 0), other.Get(0, 1), 0} 49 | case 3, 4: 50 | return T{other.Get(0, 0), other.Get(0, 1), other.Get(0, 2)} 51 | default: 52 | panic("Unsupported type") 53 | } 54 | } 55 | 56 | // Parse parses T from a string. See also String() 57 | func Parse(s string) (r T, err error) { 58 | _, err = fmt.Sscan(s, &r[0], &r[1], &r[2]) 59 | return r, err 60 | } 61 | 62 | // String formats T as string. See also Parse(). 63 | func (vec *T) String() string { 64 | return fmt.Sprint(vec[0], vec[1], vec[2]) 65 | } 66 | 67 | // Rows returns the number of rows of the vector. 68 | func (vec *T) Rows() int { 69 | return 3 70 | } 71 | 72 | // Cols returns the number of columns of the vector. 73 | func (vec *T) Cols() int { 74 | return 1 75 | } 76 | 77 | // Size returns the number elements of the vector. 78 | func (vec *T) Size() int { 79 | return 3 80 | } 81 | 82 | // Slice returns the elements of the vector as slice. 83 | func (vec *T) Slice() []float32 { 84 | return vec[:] 85 | } 86 | 87 | // Get returns one element of the vector. 88 | func (vec *T) Get(col, row int) float32 { 89 | return vec[row] 90 | } 91 | 92 | // IsZero checks if all elements of the vector are exactly zero. 93 | // Uses exact equality comparison, which may not be suitable for floating-point math results. 94 | // For tolerance-based comparison, use IsZeroEps instead. 95 | func (vec *T) IsZero() bool { 96 | return vec[0] == 0 && vec[1] == 0 && vec[2] == 0 97 | } 98 | 99 | // IsZeroEps checks if all elements of the vector are zero within the given epsilon tolerance. 100 | // This is the recommended method for comparing floating-point vectors that result from calculations. 101 | // For exact zero comparison, use IsZero instead. 102 | func (vec *T) IsZeroEps(epsilon float32) bool { 103 | return math.Abs(vec[0]) <= epsilon && math.Abs(vec[1]) <= epsilon && math.Abs(vec[2]) <= epsilon 104 | } 105 | 106 | // Length returns the length of the vector. 107 | // See also LengthSqr and Normalize. 108 | func (vec *T) Length() float32 { 109 | return float32(math.Sqrt(vec.LengthSqr())) 110 | } 111 | 112 | // LengthSqr returns the squared length of the vector. 113 | // See also Length and Normalize. 114 | func (vec *T) LengthSqr() float32 { 115 | return vec[0]*vec[0] + vec[1]*vec[1] + vec[2]*vec[2] 116 | } 117 | 118 | // Scale multiplies all element of the vector by f and returns vec. 119 | func (vec *T) Scale(f float32) *T { 120 | vec[0] *= f 121 | vec[1] *= f 122 | vec[2] *= f 123 | return vec 124 | } 125 | 126 | // Scaled returns a copy of vec with all elements multiplies by f. 127 | func (vec *T) Scaled(f float32) T { 128 | return T{vec[0] * f, vec[1] * f, vec[2] * f} 129 | } 130 | 131 | // PracticallyEquals compares two vectors if they are equal with each other within a delta tolerance. 132 | func (vec *T) PracticallyEquals(compareVector *T, allowedDelta float32) bool { 133 | return (math.Abs(vec[0]-compareVector[0]) <= allowedDelta) && 134 | (math.Abs(vec[1]-compareVector[1]) <= allowedDelta) && 135 | (math.Abs(vec[2]-compareVector[2]) <= allowedDelta) 136 | } 137 | 138 | // PracticallyEquals compares two values if they are equal with each other within a delta tolerance. 139 | func PracticallyEquals(v1, v2, allowedDelta float32) bool { 140 | return math.Abs(v1-v2) <= allowedDelta 141 | } 142 | 143 | // Invert inverts the vector. 144 | func (vec *T) Invert() *T { 145 | vec[0] = -vec[0] 146 | vec[1] = -vec[1] 147 | vec[2] = -vec[2] 148 | return vec 149 | } 150 | 151 | // Inverted returns an inverted copy of the vector. 152 | func (vec *T) Inverted() T { 153 | return T{-vec[0], -vec[1], -vec[2]} 154 | } 155 | 156 | // Abs sets every component of the vector to its absolute value. 157 | func (vec *T) Abs() *T { 158 | vec[0] = math.Abs(vec[0]) 159 | vec[1] = math.Abs(vec[1]) 160 | vec[2] = math.Abs(vec[2]) 161 | return vec 162 | } 163 | 164 | // Absed returns a copy of the vector containing the absolute values. 165 | func (vec *T) Absed() T { 166 | return T{math.Abs(vec[0]), math.Abs(vec[1]), math.Abs(vec[2])} 167 | } 168 | 169 | // Normalize normalizes the vector to unit length. 170 | // Uses the package Epsilon variable for numerical stability: 171 | // - Vectors with squared length < Epsilon are considered zero and left unchanged 172 | // - Vectors with squared length within Epsilon of 1.0 are considered already normalized 173 | func (vec *T) Normalize() *T { 174 | sl := vec.LengthSqr() 175 | if sl < Epsilon { 176 | // Vector is effectively zero 177 | return vec 178 | } 179 | if math.Abs(sl-1) < Epsilon { 180 | // Vector is already normalized 181 | return vec 182 | } 183 | return vec.Scale(1 / math.Sqrt(sl)) 184 | } 185 | 186 | // Normalized returns a unit length normalized copy of the vector. 187 | // Uses the package Epsilon variable for numerical stability. See Normalize() for details. 188 | func (vec *T) Normalized() T { 189 | v := *vec 190 | v.Normalize() 191 | return v 192 | } 193 | 194 | // Normal returns an orthogonal vector. 195 | // Uses the package Epsilon variable when checking if the cross product is zero, 196 | // which provides numerical stability when the input vector is parallel to UnitZ. 197 | // Falls back to UnitX when the cross product is effectively zero. 198 | func (vec *T) Normal() T { 199 | n := Cross(vec, &UnitZ) 200 | if n.IsZeroEps(Epsilon) { 201 | return UnitX 202 | } 203 | return n.Normalized() 204 | } 205 | 206 | // Add adds another vector to vec. 207 | func (vec *T) Add(v *T) *T { 208 | vec[0] += v[0] 209 | vec[1] += v[1] 210 | vec[2] += v[2] 211 | return vec 212 | } 213 | 214 | // Added adds another vector to vec and returns a copy of the result 215 | func (vec *T) Added(v *T) T { 216 | return T{vec[0] + v[0], vec[1] + v[1], vec[2] + v[2]} 217 | } 218 | 219 | // Sub subtracts another vector from vec. 220 | func (vec *T) Sub(v *T) *T { 221 | vec[0] -= v[0] 222 | vec[1] -= v[1] 223 | vec[2] -= v[2] 224 | return vec 225 | } 226 | 227 | // Subed subtracts another vector from vec and returns a copy of the result 228 | func (vec *T) Subed(v *T) T { 229 | return T{vec[0] - v[0], vec[1] - v[1], vec[2] - v[2]} 230 | } 231 | 232 | // Mul multiplies the components of the vector with the respective components of v. 233 | func (vec *T) Mul(v *T) *T { 234 | vec[0] *= v[0] 235 | vec[1] *= v[1] 236 | vec[2] *= v[2] 237 | return vec 238 | } 239 | 240 | // Muled multiplies the components of the vector with the respective components of v and returns a copy of the result 241 | func (vec *T) Muled(v *T) T { 242 | return T{vec[0] * v[0], vec[1] * v[1], vec[2] * v[2]} 243 | } 244 | 245 | // SquareDistance the distance between two vectors squared (= distance*distance) 246 | func SquareDistance(a, b *T) float32 { 247 | d := Sub(a, b) 248 | return d.LengthSqr() 249 | } 250 | 251 | // Distance the distance between two vectors 252 | func Distance(a, b *T) float32 { 253 | d := Sub(a, b) 254 | return d.Length() 255 | } 256 | 257 | // Add adds the composants of the two vectors and returns a new vector with the sum of the two vectors. 258 | func Add(a, b *T) T { 259 | return T{a[0] + b[0], a[1] + b[1], a[2] + b[2]} 260 | } 261 | 262 | // Sub returns the difference of two vectors. 263 | func Sub(a, b *T) T { 264 | return T{a[0] - b[0], a[1] - b[1], a[2] - b[2]} 265 | } 266 | 267 | // Mul returns the component wise product of two vectors. 268 | func Mul(a, b *T) T { 269 | return T{a[0] * b[0], a[1] * b[1], a[2] * b[2]} 270 | } 271 | 272 | // Dot returns the dot product of two vectors. 273 | func Dot(a, b *T) float32 { 274 | return a[0]*b[0] + a[1]*b[1] + a[2]*b[2] 275 | } 276 | 277 | // Cross returns the cross product of two vectors. 278 | func Cross(a, b *T) T { 279 | return T{ 280 | a[1]*b[2] - a[2]*b[1], 281 | a[2]*b[0] - a[0]*b[2], 282 | a[0]*b[1] - a[1]*b[0], 283 | } 284 | } 285 | 286 | // Sinus returns the sinus value of the (shortest/smallest) angle between the two vectors a and b. 287 | // The returned sine value is in the range 0.0 ≤ value ≤ 1.0. 288 | // The angle is always considered to be in the range 0 to Pi radians and thus the sine value returned is always positive. 289 | func Sinus(a, b *T) float32 { 290 | lenSqrProduct := a.LengthSqr() * b.LengthSqr() 291 | if lenSqrProduct < 1e-10 { 292 | return 0.0 293 | } 294 | 295 | cross := Cross(a, b) 296 | v := cross.Length() / math.Sqrt(lenSqrProduct) 297 | 298 | if v > 1.0 { 299 | return 1.0 300 | } else if v < 0.0 { 301 | return 0.0 302 | } 303 | return v 304 | } 305 | 306 | // Cosine returns the cosine value of the angle between the two vectors. 307 | // The returned cosine value is in the range -1.0 ≤ value ≤ 1.0. 308 | func Cosine(a, b *T) float32 { 309 | lenSqrProduct := a.LengthSqr() * b.LengthSqr() 310 | if lenSqrProduct < 1e-10 { 311 | return 0.0 312 | } 313 | 314 | v := Dot(a, b) / math.Sqrt(lenSqrProduct) 315 | 316 | if v > 1.0 { 317 | return 1.0 318 | } else if v < -1.0 { 319 | return -1.0 320 | } 321 | return v 322 | } 323 | 324 | // Angle returns the angle value of the (shortest/smallest) angle between the two vectors a and b. 325 | // The returned value is in the range 0 ≤ angle ≤ Pi radians. 326 | func Angle(a, b *T) float32 { 327 | return math.Acos(Cosine(a, b)) 328 | } 329 | 330 | // Min returns the component wise minimum of two vectors. 331 | func Min(a, b *T) T { 332 | min := *a 333 | if b[0] < min[0] { 334 | min[0] = b[0] 335 | } 336 | if b[1] < min[1] { 337 | min[1] = b[1] 338 | } 339 | if b[2] < min[2] { 340 | min[2] = b[2] 341 | } 342 | return min 343 | } 344 | 345 | // Max returns the component wise maximum of two vectors. 346 | func Max(a, b *T) T { 347 | max := *a 348 | if b[0] > max[0] { 349 | max[0] = b[0] 350 | } 351 | if b[1] > max[1] { 352 | max[1] = b[1] 353 | } 354 | if b[2] > max[2] { 355 | max[2] = b[2] 356 | } 357 | return max 358 | } 359 | 360 | // Interpolate interpolates between a and b at t (0,1). 361 | func Interpolate(a, b *T, t float32) T { 362 | t1 := 1 - t 363 | return T{ 364 | a[0]*t1 + b[0]*t, 365 | a[1]*t1 + b[1]*t, 366 | a[2]*t1 + b[2]*t, 367 | } 368 | } 369 | 370 | // Clamp clamps the vector's components to be in the range of min to max. 371 | func (vec *T) Clamp(min, max *T) *T { 372 | for i := range vec { 373 | if vec[i] < min[i] { 374 | vec[i] = min[i] 375 | } else if vec[i] > max[i] { 376 | vec[i] = max[i] 377 | } 378 | } 379 | return vec 380 | } 381 | 382 | // Clamped returns a copy of the vector with the components clamped to be in the range of min to max. 383 | func (vec *T) Clamped(min, max *T) T { 384 | result := *vec 385 | result.Clamp(min, max) 386 | return result 387 | } 388 | 389 | // Clamp01 clamps the vector's components to be in the range of 0 to 1. 390 | func (vec *T) Clamp01() *T { 391 | return vec.Clamp(&Zero, &UnitXYZ) 392 | } 393 | 394 | // Clamped01 returns a copy of the vector with the components clamped to be in the range of 0 to 1. 395 | func (vec *T) Clamped01() T { 396 | result := *vec 397 | result.Clamp01() 398 | return result 399 | } 400 | -------------------------------------------------------------------------------- /float64/vec2/vec2_test.go: -------------------------------------------------------------------------------- 1 | package vec2 2 | 3 | import ( 4 | "math" 5 | "strconv" 6 | "testing" 7 | ) 8 | 9 | func TestAbs(t *testing.T) { 10 | v1 := T{1.5, -2.6} 11 | 12 | expectedV1 := T{1.5, 2.6} 13 | expectedV2 := T{1.5, 2.6} 14 | 15 | v2 := v1.Abs() 16 | 17 | if v1 != expectedV1 { 18 | t.Fail() 19 | } 20 | if *v2 != expectedV2 { 21 | t.Fail() 22 | } 23 | } 24 | 25 | func TestAbsed(t *testing.T) { 26 | v1 := T{1.5, -2.6} 27 | 28 | expectedV1 := T{1.5, -2.6} 29 | expectedV2 := T{1.5, 2.6} 30 | 31 | v2 := v1.Absed() 32 | 33 | if v1 != expectedV1 { 34 | t.Fail() 35 | } 36 | if v2 != expectedV2 { 37 | t.Fail() 38 | } 39 | } 40 | 41 | func TestNormal(t *testing.T) { 42 | v1 := T{1.0, 1.0} 43 | 44 | v1Length := math.Sqrt(1*1 + 1*1) 45 | expectedV1n := T{1 / v1Length, -1 / v1Length} 46 | 47 | v1n := v1.Normal() 48 | 49 | if v1n != expectedV1n { 50 | t.Fail() 51 | } 52 | } 53 | 54 | func TestNormal2(t *testing.T) { 55 | v1 := T{4, 6} 56 | 57 | v1Length := math.Sqrt(4*4 + 6*6) 58 | expectedV1n := T{6.0 / v1Length, -4 / v1Length} 59 | 60 | v1n := v1.Normal() 61 | 62 | if v1n != expectedV1n { 63 | t.Fail() 64 | } 65 | } 66 | 67 | func TestNormalCCW(t *testing.T) { 68 | v1 := T{1.0, 1.0} 69 | 70 | v1Length := math.Sqrt(1*1 + 1*1) 71 | expectedV1n := T{-1 / v1Length, 1 / v1Length} 72 | 73 | v1n := v1.NormalCCW() 74 | 75 | if v1n != expectedV1n { 76 | t.Fail() 77 | } 78 | } 79 | 80 | func TestNormalCCW2(t *testing.T) { 81 | v1 := T{4, 6} 82 | 83 | v1Length := math.Sqrt(4*4 + 6*6) 84 | expectedV1n := T{-6.0 / v1Length, 4 / v1Length} 85 | 86 | v1n := v1.NormalCCW() 87 | 88 | if v1n != expectedV1n { 89 | t.Fail() 90 | } 91 | } 92 | 93 | func TestAdd(t *testing.T) { 94 | v1 := T{1, 2} 95 | v2 := T{9, 8} 96 | 97 | expectedV1 := T{10, 10} 98 | expectedV2 := T{9, 8} 99 | expectedV3 := T{10, 10} 100 | 101 | v3 := v1.Add(&v2) 102 | 103 | if v1 != expectedV1 { 104 | t.Fail() 105 | } 106 | if v2 != expectedV2 { 107 | t.Fail() 108 | } 109 | if *v3 != expectedV3 { 110 | t.Fail() 111 | } 112 | } 113 | 114 | func TestAdded(t *testing.T) { 115 | v1 := T{1, 2} 116 | v2 := T{9, 8} 117 | 118 | expectedV1 := T{1, 2} 119 | expectedV2 := T{9, 8} 120 | expectedV3 := T{10, 10} 121 | 122 | v3 := v1.Added(&v2) 123 | 124 | if v1 != expectedV1 { 125 | t.Fail() 126 | } 127 | if v2 != expectedV2 { 128 | t.Fail() 129 | } 130 | if v3 != expectedV3 { 131 | t.Fail() 132 | } 133 | } 134 | 135 | func TestSub(t *testing.T) { 136 | v1 := T{1, 2} 137 | v2 := T{9, 8} 138 | 139 | expectedV1 := T{-8, -6} 140 | expectedV2 := T{9, 8} 141 | expectedV3 := T{-8, -6} 142 | 143 | v3 := v1.Sub(&v2) 144 | 145 | if v1 != expectedV1 { 146 | t.Fail() 147 | } 148 | if v2 != expectedV2 { 149 | t.Fail() 150 | } 151 | if *v3 != expectedV3 { 152 | t.Fail() 153 | } 154 | } 155 | 156 | func TestSubed(t *testing.T) { 157 | v1 := T{1, 2} 158 | v2 := T{9, 8} 159 | 160 | expectedV1 := T{1, 2} 161 | expectedV2 := T{9, 8} 162 | expectedV3 := T{-8, -6} 163 | 164 | v3 := v1.Subed(&v2) 165 | 166 | if v1 != expectedV1 { 167 | t.Fail() 168 | } 169 | if v2 != expectedV2 { 170 | t.Fail() 171 | } 172 | if v3 != expectedV3 { 173 | t.Fail() 174 | } 175 | } 176 | 177 | func TestMul(t *testing.T) { 178 | v1 := T{1, 2} 179 | v2 := T{9, 8} 180 | 181 | expectedV1 := T{9, 16} 182 | expectedV2 := T{9, 8} 183 | expectedV3 := T{9, 16} 184 | 185 | v3 := v1.Mul(&v2) 186 | 187 | if v1 != expectedV1 { 188 | t.Fail() 189 | } 190 | if v2 != expectedV2 { 191 | t.Fail() 192 | } 193 | if *v3 != expectedV3 { 194 | t.Fail() 195 | } 196 | } 197 | 198 | func TestMuled(t *testing.T) { 199 | v1 := T{1, 2} 200 | v2 := T{9, 8} 201 | 202 | expectedV1 := T{1, 2} 203 | expectedV2 := T{9, 8} 204 | expectedV3 := T{9, 16} 205 | 206 | v3 := v1.Muled(&v2) 207 | 208 | if v1 != expectedV1 { 209 | t.Fail() 210 | } 211 | if v2 != expectedV2 { 212 | t.Fail() 213 | } 214 | if v3 != expectedV3 { 215 | t.Fail() 216 | } 217 | } 218 | 219 | func TestAngle(t *testing.T) { 220 | radFor45deg := math.Pi / 4.0 221 | testSetups := []struct { 222 | a, b T 223 | expectedAngle float64 224 | name string 225 | }{ 226 | {a: T{1, 0}, b: T{1, 0}, expectedAngle: 0 * radFor45deg, name: "0/360 degree angle, equal/parallell vectors"}, 227 | {a: T{1, 0}, b: T{1, 1}, expectedAngle: 1 * radFor45deg, name: "45 degree angle"}, 228 | {a: T{1, 0}, b: T{0, 1}, expectedAngle: 2 * radFor45deg, name: "90 degree angle, orthogonal vectors"}, 229 | {a: T{1, 0}, b: T{-1, 1}, expectedAngle: 3 * radFor45deg, name: "135 degree angle"}, 230 | {a: T{1, 0}, b: T{-1, 0}, expectedAngle: 4 * radFor45deg, name: "180 degree angle, inverted/anti parallell vectors"}, 231 | {a: T{1, 0}, b: T{-1, -1}, expectedAngle: (8 - 5) * radFor45deg, name: "225 degree angle"}, 232 | {a: T{1, 0}, b: T{0, -1}, expectedAngle: (8 - 6) * radFor45deg, name: "270 degree angle, orthogonal vectors"}, 233 | {a: T{1, 0}, b: T{1, -1}, expectedAngle: (8 - 7) * radFor45deg, name: "315 degree angle"}, 234 | } 235 | 236 | for _, testSetup := range testSetups { 237 | t.Run(testSetup.name, func(t *testing.T) { 238 | angle := Angle(&testSetup.a, &testSetup.b) 239 | 240 | if !PracticallyEquals(angle, testSetup.expectedAngle, 0.00000001) { 241 | t.Errorf("Angle expected to be %f but was %f for test \"%s\".", testSetup.expectedAngle, angle, testSetup.name) 242 | } 243 | }) 244 | } 245 | } 246 | 247 | func TestCosine(t *testing.T) { 248 | radFor45deg := math.Pi / 4.0 249 | testSetups := []struct { 250 | a, b T 251 | expectedCosine float64 252 | name string 253 | }{ 254 | {a: T{1, 0}, b: T{1, 0}, expectedCosine: math.Cos(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, 255 | {a: T{1, 0}, b: T{1, 1}, expectedCosine: math.Cos(1 * radFor45deg), name: "45 degree angle"}, 256 | {a: T{1, 0}, b: T{0, 1}, expectedCosine: math.Cos(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, 257 | {a: T{1, 0}, b: T{-1, 1}, expectedCosine: math.Cos(3 * radFor45deg), name: "135 degree angle"}, 258 | {a: T{1, 0}, b: T{-1, 0}, expectedCosine: math.Cos(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, 259 | {a: T{1, 0}, b: T{-1, -1}, expectedCosine: math.Cos(5 * radFor45deg), name: "225 degree angle"}, 260 | {a: T{1, 0}, b: T{0, -1}, expectedCosine: math.Cos(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"}, 261 | {a: T{1, 0}, b: T{1, -1}, expectedCosine: math.Cos(7 * radFor45deg), name: "315 degree angle"}, 262 | } 263 | 264 | for _, testSetup := range testSetups { 265 | t.Run(testSetup.name, func(t *testing.T) { 266 | cos := Cosine(&testSetup.a, &testSetup.b) 267 | 268 | if !PracticallyEquals(cos, testSetup.expectedCosine, 0.00000001) { 269 | t.Errorf("Cosine expected to be %f but was %f for test \"%s\".", testSetup.expectedCosine, cos, testSetup.name) 270 | } 271 | }) 272 | } 273 | } 274 | 275 | func TestSinus(t *testing.T) { 276 | radFor45deg := math.Pi / 4.0 277 | testSetups := []struct { 278 | a, b T 279 | expectedSine float64 280 | name string 281 | }{ 282 | {a: T{1, 0}, b: T{1, 0}, expectedSine: math.Sin(0 * radFor45deg), name: "0/360 degree angle, equal/parallell vectors"}, 283 | {a: T{1, 0}, b: T{1, 1}, expectedSine: math.Sin(1 * radFor45deg), name: "45 degree angle"}, 284 | {a: T{1, 0}, b: T{0, 1}, expectedSine: math.Sin(2 * radFor45deg), name: "90 degree angle, orthogonal vectors"}, 285 | {a: T{1, 0}, b: T{-1, 1}, expectedSine: math.Sin(3 * radFor45deg), name: "135 degree angle"}, 286 | {a: T{1, 0}, b: T{-1, 0}, expectedSine: math.Sin(4 * radFor45deg), name: "180 degree angle, inverted/anti parallell vectors"}, 287 | {a: T{1, 0}, b: T{-1, -1}, expectedSine: math.Sin(5 * radFor45deg), name: "225 degree angle"}, 288 | {a: T{1, 0}, b: T{0, -1}, expectedSine: math.Sin(6 * radFor45deg), name: "270 degree angle, orthogonal vectors"}, 289 | {a: T{1, 0}, b: T{1, -1}, expectedSine: math.Sin(7 * radFor45deg), name: "315 degree angle"}, 290 | } 291 | 292 | for _, testSetup := range testSetups { 293 | t.Run(testSetup.name, func(t *testing.T) { 294 | sin := Sinus(&testSetup.a, &testSetup.b) 295 | 296 | if !PracticallyEquals(sin, testSetup.expectedSine, 0.00000001) { 297 | t.Errorf("Sine expected to be %f but was %f for test \"%s\".", testSetup.expectedSine, sin, testSetup.name) 298 | } 299 | }) 300 | } 301 | } 302 | 303 | func TestLeftRightWinding(t *testing.T) { 304 | a := T{1.0, 0.0} 305 | 306 | for angle := 0; angle <= 360; angle += 15 { 307 | rad := (math.Pi / 180.0) * float64(angle) 308 | 309 | bx := clampDecimals(math.Cos(rad), 4) 310 | by := clampDecimals(math.Sin(rad), 4) 311 | b := T{bx, by} 312 | 313 | t.Run("left winding angle "+strconv.Itoa(angle), func(t *testing.T) { 314 | lw := IsLeftWinding(&a, &b) 315 | rw := IsRightWinding(&a, &b) 316 | 317 | if angle%180 == 0 { 318 | // No winding at 0, 180 and 360 degrees 319 | if lw || rw { 320 | t.Errorf("Neither left or right winding should be true on angle %d. Left winding=%t, right winding=%t", angle, lw, rw) 321 | } 322 | } else if angle < 180 { 323 | // Left winding at 0 < angle < 180 324 | if !lw || rw { 325 | t.Errorf("Left winding should be true (not right winding) on angle %d. Left winding=%t, right winding=%t", angle, lw, rw) 326 | } 327 | } else if angle > 180 { 328 | // Right winding at 180 < angle < 360 329 | if lw || !rw { 330 | t.Errorf("Right winding should be true (not left winding) on angle %d. Left winding=%t, right winding=%t", angle, lw, rw) 331 | } 332 | } 333 | }) 334 | } 335 | } 336 | 337 | func clampDecimals(decimalValue float64, amountDecimals float64) float64 { 338 | factor := math.Pow(10, amountDecimals) 339 | return math.Round(decimalValue*factor) / factor 340 | } 341 | 342 | func TestIsZeroEps(t *testing.T) { 343 | tests := []struct { 344 | name string 345 | vec T 346 | epsilon float64 347 | want bool 348 | }{ 349 | {"exact zero", T{0, 0}, 0.0001, true}, 350 | {"within epsilon", T{0.00001, -0.00001}, 0.0001, true}, 351 | {"at epsilon boundary", T{0.0001, 0.0001}, 0.0001, true}, 352 | {"outside epsilon", T{0.001, 0}, 0.0001, false}, 353 | {"one component outside", T{0.00001, 0.001}, 0.0001, false}, 354 | {"negative outside epsilon", T{-0.001, 0}, 0.0001, false}, 355 | {"large values", T{1.0, 2.0}, 0.0001, false}, 356 | } 357 | 358 | for _, tt := range tests { 359 | t.Run(tt.name, func(t *testing.T) { 360 | if got := tt.vec.IsZeroEps(tt.epsilon); got != tt.want { 361 | t.Errorf("IsZeroEps() = %v, want %v for vec %v with epsilon %v", got, tt.want, tt.vec, tt.epsilon) 362 | } 363 | }) 364 | } 365 | } 366 | 367 | func TestNormalizeEdgeCases(t *testing.T) { 368 | tests := []struct { 369 | name string 370 | vec T 371 | checkLength bool 372 | }{ 373 | {"zero vector", T{0, 0}, false}, 374 | {"tiny vector (below epsilon)", T{1e-16, 1e-16}, false}, 375 | {"already normalized", T{1, 0}, true}, 376 | {"nearly normalized positive deviation", T{1.00000000001, 0}, true}, 377 | {"nearly normalized negative deviation", T{0.99999999999, 0}, true}, 378 | {"nearly normalized mixed", T{0.7071067811865476, 0.7071067811865475}, true}, // ~sqrt(2)/2 components 379 | {"needs normalization", T{3, 4}, true}, 380 | {"negative components", T{-3, -4}, true}, 381 | } 382 | 383 | for _, tt := range tests { 384 | t.Run(tt.name, func(t *testing.T) { 385 | original := tt.vec 386 | originalLengthSqr := original.LengthSqr() 387 | result := tt.vec.Normalize() 388 | 389 | if result != &tt.vec { 390 | t.Errorf("Normalize() should return pointer to vec") 391 | } 392 | 393 | if tt.checkLength { 394 | length := tt.vec.Length() 395 | if !PracticallyEquals(length, 1.0, 0.00001) { 396 | t.Errorf("After Normalize(), Length() = %v, want 1.0 (original lengthSqr=%v)", length, originalLengthSqr) 397 | } 398 | } 399 | }) 400 | } 401 | } 402 | 403 | func TestNormalizedEdgeCases(t *testing.T) { 404 | tests := []struct { 405 | name string 406 | vec T 407 | checkLength bool 408 | }{ 409 | {"zero vector", T{0, 0}, false}, 410 | {"tiny vector", T{1e-16, 1e-16}, false}, 411 | {"already normalized", T{1, 0}, true}, 412 | {"needs normalization", T{3, 4}, true}, 413 | } 414 | 415 | for _, tt := range tests { 416 | t.Run(tt.name, func(t *testing.T) { 417 | original := tt.vec 418 | result := tt.vec.Normalized() 419 | 420 | if tt.vec != original { 421 | t.Errorf("Normalized() modified original vector") 422 | } 423 | 424 | if tt.checkLength { 425 | length := result.Length() 426 | if !PracticallyEquals(length, 1.0, 0.0001) { 427 | t.Errorf("Normalized().Length() = %v, want 1.0", length) 428 | } 429 | } 430 | }) 431 | } 432 | } 433 | --------------------------------------------------------------------------------