-
Notifications
You must be signed in to change notification settings - Fork 1
Transforms
This page is a general introduction to graphics programming, as it applies to UWP XAML.
For an excellent overview, Wikipedia never fails.
Let's start with this thing, which is a struct
not a class
. This has subtle semantics in C#. Simply treat them as immutable things (technically they are mutable) that are "copied" and equate by member-wise comparison, not reference.
For example, the code in the if
will never execute:
if(SomeMatrix == null) {
// matrix was not initialized
}
This fails precisely because Matrix
is not a reference type (a class
).
Instead you need some syntax that may be unfamiliar to those inexperienced with C# value types:
if(SomeMatrix == default(Matrix)) {
// matrix was not initialized
}
The default
keyword produces the "default" value for all types, which for Matrix
is 0,0,0,0,0,0
and not null
(the default for reference types).
We'll return to Matrix
, after a brief detour into linear algebra.
In the "graphics" realm, the math uses square matrices for transforms, and column vectors for coordinates. This coordinate system is extended by one column of a specific format, and also in the coordinate.
- In 2 dimensions the extension is (0 0 1) and the coordinate (x y 1).
- In 3 dimensions the extension is (0 0 0 1) and the coordinate (x y z 1).
- The "homogeneous" coordinate component is called
w
and can be other values (e.g. perspective transform).
This whole thing creates affine transformations and homogeneous coordinates amenable to the linear algebra of geometric transformations.
The Matrix
is an "abbreviated" version of the 2D affine transform matrix. It only stores the first two columns of the 3-by-3 matrix, because the third column is always (0 0 1). And even cooler: this holds true for multiplication and inversion; the third column magically cancels out to (0 0 1)!
There are two "general" components to the transformation:
- the scale/rotation component (upper 2x2) called
M11,M12,M21,M22
- the translation component (lower 1x2) called
OffsetX,OffsetY
The translation component is actually the reason for the extra column!
The scale components are M11
and M22
. When there's no rotation M12
and M21
are both 0
.
When there is rotation counter-clockwise:
-
M11
=cos(theta)
*scaleX
-
M12
=sin(theta)
*scaleY
-
M21
=-sin(theta)
*scaleX
-
M22
=cos(theta)
*scaleY
When there is rotation clockwise:
-
M11
=cos(theta)
*scaleX
-
M12
=-sin(theta)
*scaleY
-
M21
=sin(theta)
*scaleX
-
M22
=cos(theta)
*scaleY
The XAML subsystem has tons of support for transforms, e.g. TranslateTransform
etc. These are great when you are on the UI thread, because they require the UI thread or you get Exception
.
In spite of this, we still implemented Matrix
multiplication and inversion (it's easy) for use in background tasks and test cases.
Remember all of this operates within a UI object (usually a
Canvas
) with its ownRenderTransform
, all the way up.
In spite of all the amazing Matrix
support, you cannot animate MatrixTransform
! Now this might sound like a bummer, but remember that these transforms compose so a rotate/scale/translate individually create the same transform as the one of them multiplied together.
If you are animating, it is best to keep "key" components of a transform pipeline expressed by
ScaleTransform
et al, so you can animate those.
Those familiar with graphics programming know about the linear algebra equation for rendering: MVP
where each letter is a 3x3 affine transformation matrix:
-
M
is for model -
V
is for view -
P
is for projection
We don't (currently) use the
V
matrix; assume it's the identity matrix.
Take your coordinates in a row (1x3) vector and multiply, and you get the final "device" coordinate as a row vector.
Important: remember that matrix multiplication is not commutative!
Note: it also works using column vector but the
Matrix
is transposed!
Let's start at the "end" of the pipeline. This matrix encodes the size and position of the final area to render into. It's easy to set up:
- M11: width
- M22: height
- OffsetX: left
- OffsetY: top
The output of this matrix is going to be "device" coordinates. Recall that by default, XAML coordinates has origin in the upper-left, and the y-axis goes "down" as the value increases.
So now, what is the input to this matrix? It clearly has to be in different units, or this wouldn't be a transform! The input unit is this yummy thing called normalized device coordinates or NDC. This is where everything is normalized to the interval [0,1].
So to feed this matrix:
- (0,0) maps to (left, top)
- (1,1) maps to (left+width, top+height)
Pretty simple!
There is often an intermediate layer for "windowing" over the world coordinate space, called the view. We have conveniently set this to the identity matrix which means it can be left out of calculations.
When processing the data in a chart, we are in world coordinates and specifically within some bounding box of this space, as determined by the axis extents.
How do we set up this matrix? The input is, well, world coordinates.
What about the output? We know from the previous section, that we need NDC to feed the P
matrix, so what does this mean? To output NDC, the M
matrix must form a basis, which means that the vector components for each axis must have length equal one.
- M11 = 1/xaxis.Range
- M22 = -1/yaxis.Range
- OffsetX = -xaxis.Minimum/xaxis.Range
- OffsetY = yaxis.Maximum/yaxis.Range
Of special note:
-
Normalize everything by dividing by the "length" of the axis (the
Range
property), including the offsets. -
Invert the y-axis, by multiplying
M22
andOffsetY
by-1
.- Also known as the y-up vector, e.g. OpenGL.
- Account for the "start" of each axis in the offsets.
So to feed this matrix:
- (xaxis.Min, yaxis.Max) maps to (0, 0)
- (xaxis.Max, yaxis.Min) maps to (1, 1)
All set for the P
matrix!
This is now the basis for additional "local transforms" like markers etc. that can draw in their own coordinate systems. These additional transforms go on the "front" of the matrix "pipeline": Mk
* M
* P
is the equation, Mk
being a transform for the marker.
Just remember these must also be normalized vectors because everything to the "left" of P
is NDC.
So how do we go "backwards" through these transformations? Is it even possible? For example, taking some DC and turning them into world coordinates, to determine where a click occurred. This in turn can be used to select an "object".
We can indeed run "reverse" transforms by using matrix inversion, and running those coordinates through that matrix.
An inverse matrix is what is multiplied by the "source" matrix, to get an identity matrix. It in effect "cancels out" the source matrix.
Only non-singular matrices (non-zero determinant) can be inverted!
Take our "combined" MVP matrix, and invert it. The XAML Transform
classes have the Inverse
property to conveniently provide this, or you can use MatrixSupport.Invert(Matrix)
in our package.
DC * Inv(MVP) = WC
The following test case demonstrates how to "round trip" coordinates through just the P
matrix, which as you recall, takes in NDC and outputs DC:
[TestMethod]
public void Matrix_Inverse_Projection() {
var point = Projection.Transform(Origin);
Assert.AreEqual(Bounds.Left, point.X, "X failed");
Assert.AreEqual(Bounds.Top, point.Y, "Y failed");
var invproj = MatrixSupport.Invert(Projection);
TestContext.WriteLine($"proj {Projection} inv {invproj}");
var ppoint = invproj.Transform(point);
Assert.AreEqual(Origin.X, ppoint.X, "X failed");
Assert.AreEqual(Origin.Y, ppoint.Y, "Y failed");
}
As you can see the matrix inverse of P
takes in DC and outputs NDC and these values agree.
©2017-22 eScape Technology LLC This project is Apache licensed!