Skip to content

Transforms

g$ edited this page Jun 14, 2018 · 18 revisions

This page is a general introduction to graphics programming, as it applies to UWP XAML.

For an excellent overview, Wikipedia never fails.

The Matrix

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.

The 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 Reloaded

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)!

Breakdown

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

XAML Transforms

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 own RenderTransform, all the way up.

Animating XAML Transforms

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.

Rendering

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!

Projection

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!

View

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.

Model (World)

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 and OffsetY 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!

Additional Transforms

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.

Going in Reverse

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!

Example: DC to WC

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

Example: P Matrix Inversion

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.