5.5 KiB
Transforms
In code that works with linear algebra, it is common to use matrices as transformations, i.e. functions that take in a vector and output a new vector. These matrices can often be divided into categories based on the operations they perform, such as a rotation matrix or an affine transformation matrix.
The Transform
wrapper exists to encode these differing properties on the type level, as well as to provide extra utilities for working with matrices in this fashion.
Types of Transforms
Transform
has the following type signature:
Transform : (ty : TransType) -> (n : Nat) -> (a : Type) -> Type
A Transform ty n a
is a wrapper over an HMatrix' n a
. The ty
parameter is the transform type, which dictates what properties the transform has. These eight options are currently available:
Affine Types:
TAffine
TIsometry
(rotation + reflection + translation)TRigid
(rotation + translation)TTranslation
Linear Types:
TLinear
TOrthonormal
(rotation + reflection)TRotation
TTrivial
(always theidentity
)
The capital T at the beginning of each of these names identifies it as a TransType
value. To make working with transforms smoother, NumIdr provides synonyms for transforms of each type. For example, Isometry n a
is a synonym for Transform TIsometry n a
.
Linear and Affine
Transform types are divided into linear and affine types. Linear transforms must preserve the origin point, whereas affine transforms do not have this restriction.
Linear and affine transform types are in a one-to-one correspondence: a linear transform can be converted to and from an affine transform by adding or removing a translation component.
Linear <-> Affine
Orthonormal <-> Isometry
Rotation <-> Rigid
Trivial <-> Translation
The setTranslation
and linearize
functions perform these conversions.
For simplicity, both categories of transform are wrappers over homogeneous matrices, even though linear transforms could be represented by non-homogeneous matrices.
Transform Type Casting
Some transform types can be cast into other types. For example, a Rotation
can be cast into an Orthonormal
, as all rotation matrices are orthonormal.
rot : Rotation 3 Double
cast rot : Orthonormal 3 Double
In the diagram from the previous section, lower types can be cast into types higher up. Each linear type (on the left) can also be cast into the corresponding affine type (on the right).
Constructing Transforms
There are multiple ways to construct transforms, either by wrapping a matrix or directly through constructor functions.
For each transform type, fromHMatrix
can be used to test if a homogeneous matrix satisfies the right properties, and converts it into a transform if it does. The Rotation
, Orthonormal
and Linear
types also have fromMatrix
for non-homogeneous matrices.
Note
The
fromHMatrix
andfromMatrix
constructors use exact equality comparisons when testing matrices, which can be an issue if your element type isDouble
or a similar inexact number type. To remedy this, NumIdr provides aWithEpsilon
named implementation that defines equality approximately.fromHMatrix @{WithEpsilon 1.0e-6} mat
There are also direct constructors for transforms, which are often more convenient as they do not have the possibility of failing. There are too many of these constructors to exhaustively list here, so I encourage you to look through the functions in the Data.NumIdr.Transform.*
modules to see what is available.
Multiplication with Transforms
Like most objects in NumIdr, transforms multiply with the generalized multiplication operator (*.)
, and identity
and inverse
can also be used with transforms. There is no tryInverse
function, as all transforms are required to be invertible.
Transforms of any types can be multiplied. When two transforms of different types are multiplied, the resulting transform type is determined by taking the most specific type that both original types can be cast to. For example, an Orthonormal
transform multiplied by a Translation
returns an Isometry
.
The Point Type
Transforms behave differently from regular matrices when applied to vectors. When an affine transform is applied in this way, it is first linearized, so that vectors only have linear transforms applied to them. This is not a bug!
In order to properly apply affine transforms, the Point
type must be used, which is a wrapper around the Vector
type that supports these transforms. A point can be constructed with the point
function, which is used exactly the same as the vector
constructor.
point [4, 3, 6]
Points support most basic operations that vectors do, including indexing operations and standard library methods. However, a point cannot be added to another point. Instead, a vector must be added to a point:
(+.) : Vector n a -> Point n a -> Point n a
(.+) : Point n a -> Vector n a -> Point n a
(-.) : Point n a -> Point n a -> Vector n a
To remember the distinction between the two addition operators, the dot is always on the side of the point, not the vector.
This separation between points and vectors is intended to make working with affine transformations more convenient, as it mirrors the separation between points and vectors in affine algebra. These may feel like arbitrary restrictions, but you might be surprised by how convenient they are to work with!