mirror of https://github.com/liamg/aminal.git
175 lines
5.7 KiB
Go
175 lines
5.7 KiB
Go
package matstack
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/go-gl/mathgl/mgl32"
|
|
)
|
|
|
|
// A transform stack is a linear fully-persistent data structure of matrix multiplications
|
|
// Each push to a TransformStack multiplies the current top of the stack with thew new matrix
|
|
// and appends it to the top. Each pop undoes the previous multiplication.
|
|
//
|
|
// This allows arbitrary unwinding of transformations, at the cost of a lot of memory. A notable feature
|
|
// is the reseed and rebase, which allow invertible transformations to be rewritten as if a different transform
|
|
// had been made in the middle.
|
|
type TransformStack []mgl32.Mat4
|
|
|
|
// Returns a matrix stack where the top element is the identity.
|
|
func NewTransformStack() *TransformStack {
|
|
ms := make(TransformStack, 1)
|
|
ms[0] = mgl32.Ident4()
|
|
|
|
return &ms
|
|
}
|
|
|
|
// Multiplies the current top matrix by m, and pushes the result
|
|
// on the stack.
|
|
func (ms *TransformStack) Push(m mgl32.Mat4) {
|
|
prev := (*ms)[len(*ms)-1]
|
|
(*ms) = append(*ms, prev.Mul4(m))
|
|
}
|
|
|
|
// Pops the current matrix off the top of the stack and returns it.
|
|
// If the matrix stack only has one element left, this will return an error.
|
|
func (ms *TransformStack) Pop() (mgl32.Mat4, error) {
|
|
if len(*ms) == 1 {
|
|
return mgl32.Mat4{}, errors.New("attempt to pop last element of the stack; Matrix Stack must have at least one element")
|
|
}
|
|
|
|
retVal := (*ms)[len(*ms)-1]
|
|
|
|
(*ms) = (*ms)[:len(*ms)-1]
|
|
|
|
return retVal, nil
|
|
}
|
|
|
|
// Returns the value of the current top element of the stack, without
|
|
// removing it.
|
|
func (ms *TransformStack) Peek() mgl32.Mat4 {
|
|
return (*ms)[len(*ms)-1]
|
|
}
|
|
|
|
// Returns the size of the matrix stack. This value will never be less
|
|
// than 1.
|
|
func (ms *TransformStack) Len() int {
|
|
return len(*ms)
|
|
}
|
|
|
|
// This cuts down the matrix as if Pop had been called n times. If n would
|
|
// bring the matrix down below 1 element, this does nothing and returns an error.
|
|
func (ms *TransformStack) Unwind(n int) error {
|
|
if n > len(*ms)-1 {
|
|
return errors.New("Cannot unwind a matrix to below 1 value")
|
|
}
|
|
|
|
(*ms) = (*ms)[:len(*ms)-n]
|
|
return nil
|
|
}
|
|
|
|
// Copy will create a new "branch" of the current matrix stack,
|
|
// the copy will contain all elements of the current stack in a new stack. Changes to
|
|
// one will never affect the other.
|
|
func (ms *TransformStack) Copy() *TransformStack {
|
|
v := append(TransformStack{}, (*ms)...)
|
|
return &v
|
|
}
|
|
|
|
// Reseed is tricky. It attempts to seed an arbitrary point in the matrix and replay all transformations
|
|
// as if that point in the push had been the argument "change" instead of the original value.
|
|
// The matrix stack does NOT keep track of arguments so this is done via consecutive inverses.
|
|
// If the inverse of element i can be found, we can calculate the transformation that was given at point i+1.
|
|
// This transformation can then be multiplied by the NEW matrix at point i to complete the "what if".
|
|
// If no such inverse can be found at any given point along the rebase, it will be aborted, and the original
|
|
// stack will NOT be visibly affected. The error returned will be of type NoInverseError.
|
|
//
|
|
// If n is out of bounds (n <= 0 || n >= len(*ms)), a generic error from the errors package will be returned.
|
|
//
|
|
// If you have the old transformations retained, it is recommended
|
|
// that you use Unwind followed by Push(change) and then further calling Push for each transformation. Rebase is
|
|
// imprecise by nature, and sometimes impossible. It's also expensive due to the inverse calculation at each point.
|
|
func (ms *TransformStack) Reseed(n int, change mgl32.Mat4) error {
|
|
if n >= len(*ms) || n <= 0 {
|
|
return errors.New("Cannot rebase at the given point on the stack, it is out of bounds.")
|
|
}
|
|
|
|
return ms.reseed(n, change)
|
|
}
|
|
|
|
// Operates like reseed with no bounds checking; allows us to overwrite
|
|
// the leading identity matrix with Rebase.
|
|
func (ms *TransformStack) reseed(n int, change mgl32.Mat4) error {
|
|
backup := []mgl32.Mat4((*ms)[n:])
|
|
backup = append([]mgl32.Mat4{}, backup...) // copy into new slice
|
|
|
|
curr := (*ms)[n]
|
|
(*ms)[n] = (*ms)[n-1].Mul4(change)
|
|
|
|
for i := n + 1; i < len(*ms); i++ {
|
|
inv := curr.Inv()
|
|
|
|
blank := mgl32.Mat4{}
|
|
if inv == blank {
|
|
ms.undoRebase(n, backup)
|
|
return NoInverseError{Loc: i - 1, Mat: curr}
|
|
}
|
|
|
|
ghost := inv.Mul4((*ms)[i])
|
|
|
|
curr = (*ms)[i]
|
|
(*ms)[i] = (*ms)[i-1].Mul4(ghost)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (ms *TransformStack) undoRebase(n int, prev []mgl32.Mat4) {
|
|
for i := n; i < len(*ms); i++ {
|
|
(*ms)[i] = prev[i-n]
|
|
}
|
|
}
|
|
|
|
// Rebase replays the current matrix stack as if the transformation that occurred at index "from"
|
|
// in ms had instead started at the top of m.
|
|
//
|
|
// This returns a brand new stack containing all of m followed by all transformations
|
|
// at from and after on ms as if they has been done on m instead.
|
|
func Rebase(ms *TransformStack, from int, m *TransformStack) (*TransformStack, error) {
|
|
if from <= 0 || from >= len(*ms) {
|
|
return nil, errors.New("Cannot rebase, index out of range")
|
|
}
|
|
|
|
// Shift tmp so that the element immediately
|
|
// preceding our target is the "top" element of the list.
|
|
tmp := ms.Copy()
|
|
if from == 1 {
|
|
(*tmp) = append(*tmp, mgl32.Mat4{})
|
|
}
|
|
copy((*tmp)[1:], (*tmp)[from-1:])
|
|
if from-2 > 0 {
|
|
(*tmp) = (*tmp)[:len(*tmp)-(from-2)]
|
|
}
|
|
|
|
err := tmp.Reseed(1, m.Peek())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
(*tmp) = append(*m, (*tmp)[2:]...)
|
|
|
|
return tmp, nil
|
|
}
|
|
|
|
// A NoInverseError is returned on rebase when an inverse cannot be found along the chain,
|
|
// due to a transformation projecting the matrix into a singularity. The values include the matrix
|
|
// no inverse can be found for, and the location of that matrix.
|
|
type NoInverseError struct {
|
|
Mat mgl32.Mat4
|
|
Loc int
|
|
}
|
|
|
|
func (nie NoInverseError) Error() string {
|
|
return fmt.Sprintf("cannot find inverse of matrix %v at location %d in matrix stack, aborting rebase/reseed", nie.Mat, nie.Loc)
|
|
}
|