Matrix Math is Hard?

As obvious as this may be (or for those of you who roll your eyes at this sentiment), it’s certainly been a learning experience for me. Let’s have a quick recap on what I’ve learnt and what I’ve determined to be important, at least for my learning.

Coordinates

2D runs X,Y – This should be familiar enough – if not, then this post really isn’t a good starting point.

image

3D runs X,Y,Z – Notice in this example I’m referring to Z being in the negative direction when considering an object to move forward.  You can do it the other way if you want but XNA is by default a right handed (Z comes towards you).

image

In SunBurn this looks like:

image

The direction the arrows are pointing indicate which way is the positive axis. Green is Y, Red is X and Blue is Z.

Vectors

3D Coordinates are represented using Vectors, specifically Vector3. Vector 3 stores the X,Y,Z coordinates of an object as floats. This vector is referred to as the Translation (worth getting used to, however it’s just a way of saying location in 3D space). From this Vector3, all we can determine is the objects location in the world space (the scene).

The other Vector3 we will use consistently is a representation of scale. In a similar way it stores a representation of X,Y,Z vectors as floats specifically to control the scaling of the object in the 3d space.

Quaternions

Are numbers that extend complex numbers (seriously, a complex number can be extended to be even more complicated). Quaternion’s are used to store a representation of rotation in 3D space and luckily, so far, I haven’t seen much of a need to actually know much more about them than that & have still been able to use them in the process of object rotation (which is actually quite awesome).

Matrices

Using this information in XNA requires this data to be setup and stored in a Matrix. Vector3’s and Quaternion’s can be converted easily in to Matrices using the Matrix helper methods that exist; depending on the type of Matrix we are trying to create, we need to use a different helper method.

A Translation is Matrix is quite easily defined as:
Matrix translation = Matrix.CreateTranslation(new Vector3(X,Y,Z));

Scale Matrices are similarly defined by:
Matrix scaleM = Matrix.CreateScale(new Vector3(X,Y,Z));

Where a rotation Matrix is required (across all axis (Pitch, Roll & Yaw)) a Quaternion is used:
Matrix rotation = Matrix.CreateFromQuaternion(aQuaternion);

If you don’t have a Quaternion that you want to use then Matrix’s representing the various axis then helper methods exist for this as well:
Matrix rotationX = Matrix.CreateRotationX(angleX);
Matrix rotationY = Matrix.CreateRotationY(angleY);
Matrix rotationZ = Matrix.CreateRotationZ(angleZ);

Welcome to the World

Scale, Rotation & Translation information are really important; the next step is bringing them together so that we actually have something useful and using this information to influence an outcome in the 3D space is where the concept of the world space comes in.

An Object has it’s own perspective of the world; if I am looking in a direction, to me that direction is forward – if you are standing next to me and looking in the same direction then you would probably agree. However, if you were standing at the opposite end of the field looking at me, your definition of forward would be different to mine. The World Space provides a reference from the outside perspective, regardless of what direction either of us are looking in.

The World is expressed in terms of a Matrix and contains the information required to represent an objects Scale, Rotation and Translation in the world.

Scale * Rotation * Translation

When we update the World Matrix for an object, we need to do it in the order of Scale * Rotation * Translation so that the final Matrix is defined correctly. If you multiple the Matrices out of order, you get weird and unexpected results.

Taking an example of our good old SpinZ Component:

[Serializable]
public class Component_SpinZAxisSlow : BaseComponentAutoSerialization<ISceneEntity>
{
    public override void OnUpdate(GameTime gameTime)
    {
        // Calculate the elapsed rotation based on the time. float angle = (float)gameTime.ElapsedGameTime.TotalSeconds * 0.3f;
        Matrix rotation = Matrix.CreateRotationZ(angle);

        // Apply the rotation to the parent object (owner of the component). ParentObject.World = rotation * ParentObject.World;
    }
}

 

Here we have a rotation being applied each update to an object where the component has been assigned to. Notice that the ParentObject.World Matrix is updated by the order of the Rotation * ParentObject.World – here ParentObject.World is already storing the World Matrix for the object when this method is called; it already has information about the Objects scale, rotation and translation.

If we also want to move the object at the same time we would need to include a change to the Translation for the object. A good way to pull apart an existing objects Matrix to find out it’s scale, rotation and translation is the Matrix.Decompose method:

// Decompose World Matrix (Child) Quaternion rotationQ = new Quaternion();
Vector3 translation = new Vector3();
Vector3 scale = new Vector3();
ParentObject.World.Decompose(out scale, out rotationQ, out translation);

This splits the Matrix up in to its relevant parts that we can then adjust before putting back together.

Defy Gravity

Moving the object upwards (without using physics, aka teleportation) can be achieved by specifying an offset and applying it to the object on each update loop.

[Serializable]
public class Component_DefyGravity : BaseComponentAutoSerialization<ISceneEntity>
{
    public override void OnUpdate(GameTime gameTime)
    {
        // Decompose World Matrix (Child) Quaternion _rotationQ = new Quaternion();
        Vector3 _translation = new Vector3();
        Vector3 _scale = new Vector3();
        ParentObject.World.Decompose(out _scale, out _rotationQ, out _translation);

        // You can just specify the float values directly (purposes of demo) Matrix mOffset = Matrix.CreateTranslation(new Vector3(0f, 0.1f, 0f));

        // Recreate the current Rotation Matrix Matrix mRotation = Matrix.CreateFromQuaternion(_rotationQ);

        // Recreate the current Scale Matrix Matrix mScale = Matrix.CreateScale(_scale);

        // Recreate the current Translation Matrix Matrix mTranslation = Matrix.CreateTranslation(_translation);

        // Multiple the Matrices and assign the result to the parent object (owner of the component). ParentObject.World = mScale * (mRotation * (mTranslation * mOffset));
    }
}

 

Creating the offset Matrix through the process of Matrix.CreateTranslation allows us to specify which element of the 3D world space we want to move the object in. For this example we are increasing the Y axis by 0.1 each update loop:
Matrix
mOffset = Matrix.CreateTranslation(new Vector3(0f, 0.1f, 0f));

This offset translation is multiplied by the current translation of the object to work out the new translation for the object and making sure that we multiple all of the other required Matrices in the correct order (S*R*T) then we get the expected result. Copy and past the component above, load up SunBurn and find an Object (make sure the object can receive updates) and don’t worry about actually turning on gravity for that object and then assign the component to it. It will just drift up, up and away.

Scenarios moving the object along any axis is exactly the same, just change the float for the corresponding axis that you want to move the object along. This is the basic changes required to move an object Forward, Backwards, Strafe Left & Strafe Right. To add in rotation follows the same approach.

Do it in Style

// Create a rotation Matrix that will be used to gently rotate the object // each update Matrix mRotateZ = Matrix.CreateRotationZ(0.005f);

// Multiple the Matrices and assign the result to the parent object (owner of the component). ParentObject.World = mScale * ((mRotation * mRotateZ) * (mTranslation * mOffset));

Here we are adding in a new Matrix for the Z rotation, based on a very small float (so that it’s a slow rotation, remember this code gets called every time the update method is called, so 30-60 FPS, depending on your settings.)

Once the new rotation Matrix is defined, we need to then just change the calculation of the world Matrix to take in to account the Rotation offset, this is done by multiplying the new Rotation Matrix by the Rotation Matrix that represents the current rotation of the Object.

Living it Large

Scaling can be achieved following the same principle. A new Matrix to create an offset amount for the scale and then ensure that it’s multiplied in the right order for the World Matrix.

// Create an offset scale Matrix Matrix mScaleOffset = Matrix.CreateScale(1f, 1f, 1.0005f);

// Multiple the Matrices and assign the result to the parent object (owner of the component). ParentObject.World = (mScale * mScaleOffset) * ((mRotation * mRotateZ) * (mTranslation * mOffset));

Obligatory Video

Click for Full Screen on YouTube

 

Enough?

To start with, absolutely. That’s really enough to get your objects scaling, rotating and moving. It starts to get a bit more fun when you try to take an offset from some out side influence (say another object) but that’s a topic for another post.