Quaternion Rotation: The Blocks and Plasticine Version

Posted on October 20, 2008 by Chris at 12:41 pm

As you know, I’m currently working on Mini-Game: Dwarf vs Zombie. As I’m working on the next section, I’ve come across some code that uses Quaternions to perform the rotation. In the past, I’ve kind of glossed over how they work, because they somehow seem beond my understanding, copied what I can from the available code and moved on. This time, however, I am determined to know exactly how they work, and why we use them as opposed to other rotation methods, such as rotation matrices.

I thought you said this was “Blocks and Plasticine” ?!!!

Before you go too far in, I have made a few assumptions about your maths skills. Firstly, I would expect that you know how to add, subtract and multiply. Secondly, you may want to know basic vector and matrix oprations like cross products and dot products.

If you can’t be bothered with any of that, I’ve included some code and samples at the end to speed you along.

What is a Quaternion?

A quaternion is a mathematical entity that can be used for a variety of operations. Most of their usefulness has been superseded by other areas of maths, but they still provide some significant performance boosts in the calculation of 3d rotations.

Quaternions are commonly expressed as a scalar (w) plus a 3 dimensional vector V, Sometimes, you will also see the notation [w,V].  They are also often just stored as a 4 dimensional vector <w,x,y,z>.

q = w + V
q = [w, V]
q = <w,x,y,z>

Supposing that these are the values, we could create a structure to hold these values like the one below. I’ve also included a few methods that we will cover later.

struct Quaternion {
public:
     float w,x,y,z;

public:
     // constructor
     Quaternion(float qw, float qx, float qy, float qz);

     // FromAxisAngle - creates a quaternion to do a rotation,
     //     from an <x,y,z> axis and an angle in degrees
     static Quaternion FromAxisAngle(float angle, float ax, float ay, float az); 

     // GetConjugate - returns the conjugate of the current quaternion
     Quaternion GetConjugate();

     // Multiply - Multiplies two quaternions togeather
     Quaternion Multiply(Quaternion q2);

     // RotatePoint - Rotates the given point by the current quaternion
     Quaternion RotatePoint(float x, float y, float z);

     // Slerp - Performs Spherical Linear IntERPolation
     Quaternion Slerp(Quaternion q2, float time);
};

A Quaternion for Rotation

The most common use for Quaternions that we find lately is to perform a short cut for rotation about an arbitraty axis.

To perform a rotation, we need a few initial values.

First, we need the <x,y,z> axis that we will be rotating around. This will be a standard vector, which we will call “Axis” in the calculations below.

Next, we will need the angle of rotation. For simplicity, I’ve called this “angle” in the calculations.

We also need to use the point that we will be rotating. This is another <x,y,z> vector, that is convinently named “Point”.

Now we have the basic values, we need to create the following quaternions.

q = [cos(angle/2), Axis * (sin(angle/2))]
q' = [cos(angle/2), -(Axis * (sin(angle/2)))]
P = [0, Point]

The (q) value defined above represents the rotation,

The (q’) value is known as the conjugate, which is simply calculated using one of the formulas below.

q' = [q.w, -q.V]
q' = [q.w, -q.x, -q.y, -q.z]

The (P) value here is simply the point that we want to rotate expressed as a quaternion. In this case, we use the Point vector without modification, and specify zero for the w value.

To make things a little easier, you could add a couple of functions to the Quaternion struct above.

Quaternion::Quaternion(float qw, float qx, float qy, float qz){
     w = qw; x = qx; y = qy; z = qz;
}

Quaternion Quaternion::FromAxisAngle(float angle, float ax, float ay, float az){
     float sinangle, cosangle;
     float length;

     // we need to normalize the axis values so that they are a unit vector
     length = (float) sqrt(ax*ax+ay*ay+az*az);
     ax /= length;
     ay /= length;
     az /= length;

     // precalculate the sin and cos of the angle so we dont need to keep using the sin function
     // NOTE: sin uses radians, and we want degrees as angles, so the formula is
     //     angle = angle * (PI / 180);
     //     sinangle = sin(angle/2);
     // which is the same as saying
     //     sinangle = sin(angle * PI/360);
     sinangle = (float) sin(angle * PI / 360.0);
     cosangle = (float) cos(angle * PI / 360.0);

     return Quaternion(cosangle, ax * sinangle, ay * sinangle, az * sinangle);
}

Quaternion Quaternion::GetConjugate(){
     return Quaternion(w,-x,-y,-z);
}

Now we have our preliminary quaternions defined, we can use the following formulas to calculate the new rotated point.

R = q * P * q'
Result = <R.x, R.y, R.z>

Multiplying the three quaternions q, P, and q’ will give us a third quaternion (R), which we can then extract the the Result vector from it’s x,y,z values. Multiplying two quaternions togeather requires a special formula, which we will cover next.

Quaternion Multiplication

As you can see in the example above, quaternions are multiplied togeather to give us the result that we need. Just as there is a special way of multiplying matrices togeather, there is also a way of multiplying quaternions with each other.

If you know matrix multiplication the formula below will give you the correct multiplication. If you let w1 = q1.w, and V1 = q1.V, and likewise for w2, and V2.

q1 * q2 = w1 * w2 - DotProduct(V1, V2) + w1 * V2 + w1 * V1 + CrossProduct(V1, V2)

If you don’t know matrix multiplication, the formula below is for you. I’ve written it as it would appear in code, so you should just be able to copy and paste :).

Quaternion Quaternion::Multiply(Quaternion q2){
     Quaternion Result;
     float w1, x1, y1, z1;
     float w2, x2, y2, z2;

     w1 = w; x1 = x; y1 = y; z1 = z;
     w2 = q2.w; x2 = q2.x; y2 = q2.y; z2 = q2.z;

     Result.w = w1 * w2 - x1 * x2 - y1 * y2 - z1 * z2;
     Result.x = w1 * x2 + x1 * w2 + y1 * z2 - z1 * y2;
     Result.y = w1 * y2 - x1 * z2 + y1 * w2 + z1 * x2;
     Result.z = w1 * z2 + x1 * y2 - y1 * x2 + z1 * w2;

     return Result;
}

So, now that we have multiplication defined, we are on easy street. We just plug in that nifty “q * P * q’” thingame to get our result… but what if we could be even lazier? How ’bout some code that makes you not need to remember that little rule?

Quaternion Quaternion::RotatePoint(float x, float y, float z){
     Quaternion p(0,x,y,z);
     Quaternion cq = GetConjugate();
     Quaternion result;

     // now for the magic (q * P * q')
     result = Multiply(p).Multiply(cq);
     return result;
}

It’s polite to Slerp your Interpolation

Interpolation is just a nifty word for “moving from one point to another at small intervals”. It is used to make the animation that you see between two keyframes as a smooth transition from one to the next.

To make an angle move smoothly from one quaternion to another, we use a method called Spherical Linear Interpolation (SLERP). The formula for Slerp is given by

q(time) = ((sin(angle * (1 - time))/(sin(angle))) * q1
          + ((sin(angle * time)) / sin(angle)) * q2

where
0.0 <= time <= 1.0  and
(q1 dot q2) >= 0
angle = cos-1(q1 dot q2)

NOTE q === -q in regards to angles, so we can simply change the sign of
     either q1 or q2 to make the rule fit.

Fortunately, I’ve come prepared with some code that you can use to do the task.

Quaternion Quaternion::Slerp(Quaternion q2, float time){
     float dotprod;
     float angle;
     double sinangle;
     float a, b;
     Quaternion result;
     float w1, x1, y1, z1;
     float w2, x2, y2, z2;

     w1 = w; x1 = x; y1 = y; z1 = z;
     w2 = q2.w; x2 = q2.x; y2 = q2.y; z2 = q2.z;

     // get the dot product, and ensure it's >= 0
     // to ensure the shortest path (< 180) is chosen
     dotprod = w1*w2 + x1*x2 + y1*y2 + z1*z2;
     if(dotprod < 0.0f){
          w1 = -w1; x1 = -x1; y1 = -y1; z1 = -z1;
          dotprod = w1*w2 + x1*x2 + y1*y2 + z1*z2;
     }

     // get the angle
     angle = (float) acos(dotprod);
     if(angle == 0){
          return Quaternion(w1,x1,y1,z1);
     }
     sinangle = sqrt(1 - (dotprod * dotprod));

     // calculate the multipliers
     a = (float)(sin(angle * (1.0f - time)) / sinangle);
     b = (float)(sin(angle * time) / sinangle);

     // create the result quaternion
     return Quaternion(a*w1+b*w2, a*x1+b*x2, a*y1+b*y2, a*z1+b*z2);
}

No Really, I Want my Blocks and Plasticine!!!!

Ok, so you’re not interested in all the theory, here’s the basics.

  • Quaternions are used to rotate a point around an axis
  • Download and use my source code [c++][java] (there’s your blocks.. of code)
  • If you are interested in how it all works, read the stuff above. It doesn’t get any easier, but it can be a whole lot harder.
  • Take a look at how to use the code below, and play with the samples (malleable, just like plasticine)

Example: Rotating a point <1,0,0> around the z-axis by 45º.

void main(){
     Quaternion angleQuaternion;
     Quaternion result;

     angleQuaternion = Quaternion::FromAxisAngle(45.0f, 0.0f, 0.0f, 1.0f);
     result = angleQuaternion.RotatePoint(1.0f,0.0f,0.0f);
     cout << "(" << result.x << ", " << result.y << ", " << result.z << ")" << endl;
}

Sample: Rotating point <1,0,0> around the z-axis

>

Example: Using SLERP to rotate a point from 45º to 135º around the z-axis.

void main(){
     Quaternion q1, q2, result;

     q1 = Quaternion::FromAxisAngle(45.0f, 0.0f, 0.0f, 1.0f);
     q2 = Quaternion::FromAxisAngle(135.0f, 0.0f, 0.0f, 1.0f);

     for(int i = 0; i < 10; i++){
          result = q1.Slerp(q2, 0.1f * i);
          cout << "(" << result.x << ", " << result.y << ", " << result.z << ")" << endl;
     }
}

Sample: SLERP around the z-axis


Why Use Quaternion Rotation?

The reasons for using quaternions in your rotations kind of play out like an add for a stocktake sale.

  • Less Storage Space - A quaternion holds 4 values, while a 3×3 matrix holds 9 values. “That’s a saving of more than 50%.. what a bargain!”
  • Less Operations - Each time you multiply two quaternions together, you need to perform 16 operations, while a 3×3 matrix will require 27, and a 4×4 will need 64… “that’s up to 75% off!!!”
  • Easier to Interpolate - Making a smooth transition between two quaternions is much easier than doing the same between two matrices. “open 24 hours.. hurry in while stocks last”
Filed under: Articles, Game Development Tags: , , ,

A Network Protocol for Movement (Part 3)

Posted on March 14, 2008 by Chris at 5:00 pm

A quick recap before we begin. In Part 1, I described a network protocol for moving objects around in 3d space. In Part 2, I wrote a server application that recieved connection requests and mob updates, and forwarded mob positions on to all other clients.

In this entry I’ll be writing about the Client Application that goes with the movement server. The application will basically be a 2d ships flying around type application, which looks like the screenshot to the right. The red blob on the screen is the Mob that is controlled by this client, while the green blobs are controlled by other clients. The blobs are supposed to look like turtles, but you can see my mspaint skills at work here :)

The server application is basically the same one that we used last time, with one or two bug fixes. As with the last entry, you can download the acompanying code to play with. I’ve also added the compiled binaries if you would prefer to just play with them.

Warning: Math Content

The only problem with working with 3d (even if you are displaying it in 2d), is that you need to know a little extra math to get things working properly. If you know how all this math works, congratulations. If not, here are some helpful hints and links that will assist you in reading my ugly math ridden code.

A Vector is basically a 3 dimentional {X,Y,Z} coordinate that can determine an objects position in 3d space, or be used for a variety of other porpoises (yes, they can be used for dolphins! who said they cant?). In fact we are also using Unit Vectors (vectors with a magnitude/length of 1 unit), to determine where Up, Right and Forward are.

The Cross Product of two vectors can be used to determine a third mutually orthogonal (right angled) vector. You will notice that we are only sending the Up and Right vectors using our movement protocol. To create our position matrix, we need to calculate a Forward or LookAt vector, which is simply done using the cross product of our Up and Right vectors.

The Dot Product of two vectors can be used to detrmine the cosine of the angle between them. This is how we figure out which way our little space ships are pointing. Given that our coordinate system is a right-handed y-up, z-facing one, we perform the dot product of {0,0,-1} and the Forward Vector. Finally, if the Forward.X value is less than 0, we make an adjustment, realising that the angle we are looking at is a reflex angle (greater than 180).

We use a Matrix to store the current Up, Right, Forward, and Location vectors. We can then use Matrix Multiplication against another matrix (such as a Translation, or Rotation Matrix) to move the object around the 3d space.

To create our position matrix can be made up using our Right (R), Up (U), Forward (F), and Position (P) vectors in the following 4×4 array

[R.X, R.Y, R.Z, 0.0]

[U.X, U.Y, U.Z, 0.0]

[F.X, F.Y, F.Z, 0.0]

[P.X, P.Y, P.Z, 1.0]

Note: I did kind of come up with the positioning matrix by myself from what I knew of 3d math, and how matrices worked.. so if I’ve got it wrong please do post a comment, so I can beat myself with a large stick.

Show Me the Coding!!!

Ok, now we have done the obligatory foray into the math behind the workings of the er.. game.. thingame (which is incedently the hardest part), we can now look at the client side networking code, which is actually what this entry is all about.

The general process that the client performs is as follows

  • Connect to the server using a LOC_Connect message
  • Recieve a LOC_AcceptConnection message, and store the given MobID for future use
  • Every 100ms or so update the location, orientation, and velocity of the current MobID using the LOC_UpdateMobLocation message
  • Every so often check for new LOC_MobLocation messages and update the relevant mob from the list
  • Attempt to update the screen to maintain a 30fps frame rate

Connecting to the Server

Connecting to thw server is done in two parts. First, the client will send a LOC_Connect message to the server, using the code below:

public void Connect(String address, int port)
{
   MemoryStream stream = new MemoryStream((int)LOC_ConnectMessage.MessageSize);
   BinaryWriter writer = new BinaryWriter(stream);

   if (m_Connection != null) return;

   // connect to the server
   m_Connection = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
   m_Connection.NoDelay = true; // disable nagle algorithm
   m_Connection.Connect(address, port);

   // send the connect message
   Random r = new Random();
   writer.Write((Byte)ProtocolCommand.LOC_Connect);
   // just send a random session id, the server doesnt use this right now
   writer.Write((int)r.Next());
   m_Connection.Send(stream.GetBuffer());
}

Note: On line 1 I am specifying the size of the stream to create, in the MemoryStream constructor (new MemoryStream((int)LOC_ConnectMessage.MessageSize);). If you don’t do this, the buffer size that you end up with when you pull it out using the GetBuffer method will be too large, which will screw up both the client and server communications. This is also one of the bugs that were fixed in the server app.

Next, the client will recieve the LOC_AcceptConnection message and store the MobID for use in future location update messages.

if (m_Connection.Available >= (LOC_ConnectMessage.MessageSize))
{
   buffer = new byte[LOC_ConnectMessage.MessageSize];
   m_Connection.Receive(buffer);

   reader = new BinaryReader(new MemoryStream(buffer));
   reader.ReadByte();
   m_MyMobId = reader.ReadInt32();

   m_NextMessage = 0x00;
   bProcessed = true;
   Connected(this, new EventArgs());
}

After the read loop (see next section) determins that the next message being recieved is a LOC_AcceptConnection message, the available data is checked, and the AcceptConnection message is pulled off the socket. We then read a single byte (with which we do nothing, because it is the actual command), and store the MobID in m_MyMobId. After this, the Connect event is fired so that other parts of the program (such as the main form) can react accordingly.

The Read Loop

The read loop of the client is slightly different to the server’s one. This is actually more due to the fact that I found a part of the Socket.Recieve method that allowed me to peek (look at, without consuming) the incoming data.

if (m_Connection.Poll(100, SelectMode.SelectRead))
{
   bProcessed = true;
   bNextMessage = 0; // no message
   // keep looping until no more messages can be processed
   while (bProcessed)
   {
      bProcessed = false;

      if (m_Connection.Available > 0)
      {
         buffer = new byte[1];

         // peek at the invcoming data using SocketFlags.Peek
         m_Connection.Receive(buffer, 0, 1, SocketFlags.Peek);
         bNextMessage = buffer[0];
      }

      // check which message is being recieved and react accordinly
      switch ((ProtocolCommand)bNextMessage)
      {
         case ProtocolCommand.LOC_AcceptConnection:
            ...
         case ProtocolCommand.LOC_MobLocation:
            ...
      }
   }
}

The code above can be found in the MovementClient.Iterate method. Each time the Iterate method is called (about once every 500ms), this code checks if there is any data available on the socked using the Socket.Poll method, then attempts to peek at the first byte that’s available. The first byte should always be either a LOC_AcceptConnection message, or a LOC_MobLocation message.

Updating the Locations

There are two parts to the locations being updated. One is sending updates for your mob to the server, and the other is recieving the updates for all the other mobs.

The MovementClient.SendMobData method takes care of sending the updates to the server. It’s a simple package and send sort of function, similar to what you saw befor with the Connect method, but bigger.

public void SendMobData(MobData data)
{
   MemoryStream stream = new MemoryStream((int)LOC_UpdateMobLocationMessage.MessageSize);
   BinaryWriter writer = new BinaryWriter(stream);

   writer.Write(Convert.ToByte(ProtocolCommand.LOC_UpdateMobLocation));
   writer.Write(data.MobId);
   writer.Write(data.Location.X);
   writer.Write(data.Location.Y);
   writer.Write(data.Location.Z);
   writer.Write(data.UpVector.X);
   writer.Write(data.UpVector.Y);
   writer.Write(data.UpVector.Z);
   writer.Write(data.RightVector.X);
   writer.Write(data.RightVector.Y);
   writer.Write(data.RightVector.Z);
   writer.Write(data.Velocity.X);
   writer.Write(data.Velocity.Y);
   writer.Write(data.Velocity.Z);

   try
   {
      m_Connection.Send(stream.GetBuffer());
   }
   catch (Exception) { ;}
}

You have probably noticed that the Socket.Send function is enclosed in a try/catch routine. This is mainly because I’ve not bothered about good programming, and making sure that I actually have a good connection prior to sending. I’ve made it so that it just fails silently, not bothering me or anyone else in the process. This is actually a problem with my coding style, where I indend on getting back to it and fixing it, but in the mean time, I want it to work without bothering me :)… then it never bothers me again.. then something screws up.. then it takes many days to find where the problem is… when I can be bothered I’ll write some lines for my sins.. “I will use caution when programming. I will use caution when programming. I will use caution when programming. I will use caution when programming.”

The next bit handles incoming location updates from the other clients. You may have noticed the case ProtocolCommand.LOC_MobLocation in the read loop section. This is where we handle the incoming location updates. Again, it is a simple unpack / alert routine that you are seeing.

if (m_Connection.Available >= (LOC_UpdateMobLocationMessage.MessageSize))
{
   buffer = new byte[LOC_UpdateMobLocationMessage.MessageSize];
   m_Connection.Receive(buffer);

   reader = new BinaryReader(new MemoryStream(buffer));

   message = new LOC_UpdateMobLocationMessage();
   message.Command = reader.ReadByte();
   message.MobId = reader.ReadInt32();
   message.Location.X = reader.ReadDouble();
   message.Location.Y = reader.ReadDouble();
   message.Location.Z = reader.ReadDouble();
   message.UpVector.X = reader.ReadDouble();
   message.UpVector.Y = reader.ReadDouble();
   message.UpVector.Z = reader.ReadDouble();
   message.RightVector.X = reader.ReadDouble();
   message.RightVector.Y = reader.ReadDouble();
   message.RightVector.Z = reader.ReadDouble();
   message.Velocity.X = reader.ReadDouble();
   message.Velocity.Y = reader.ReadDouble();
   message.Velocity.Z = reader.ReadDouble();

   bNextMessage = 0;
   bProcessed = true;
   UpdateMobLocation(message);
}

Similar to the accept connection routine, it simply pulls the appropriate amount of data out of the socket using the Recieve method, then reads the information in the correct order, just like if we stored the data in a binary file. Finally the UpdateMobLocation event is fired so that the main form can keep track of the unit in question.

It’s probably worth having a look at the code from the main form in this circumstance, as some of the logic is also performed here. Below you will find the function that is called when the MovementClient.UpdateMobLocation event is fired.

void OnUpdateLocation(LOC_UpdateMobLocationMessage message)
{
   MobileUnit mob = null;
   Vector3d vUp, vRight, vLoc, vVel; 

   // only make changes if the mob is not the current client
   if (message.MobId != m_Client.MobId)
   {
      if (m_UnitList.ContainsKey(message.MobId))
      {
         mob = m_UnitList[message.MobId];
      }
      else
      {
         mob = new MobileUnit();
         m_UnitList.Add(message.MobId, mob);
      }

      // get the new vector sets
      vUp = new Vector3d(message.UpVector.X, message.UpVector.Y, message.UpVector.Z);
      vRight = new Vector3d(message.RightVector.X, message.RightVector.Y, message.RightVector.Z);
      vLoc = new Vector3d(message.Location.X, message.Location.Y, message.Location.Z);
      vVel = new Vector3d(message.Velocity.X, message.Velocity.Y, message.Velocity.Z);

      mob.UpdateMatrix(vUp, vRight, vLoc, vVel);
   }
}

Basically, I’m storing all of the mobs that aren’t the current client in a Dictionary object. This way I can pull a MobileUnit object, given a single key. The reason we dont want to store the current client’s info in this list, is that each client is actually the authority on where their own mobs are, so we would not want to overwrite this information with older or inaccurate data.

Please Stop! My Eyes Hurt!

So, we’ve come to the end of my little foray into the dephts of network programming. What’s next? you may ask. What could possibly make this code better?

There are a good number of things that have been left un-done, mainly due to laziness on my own part. Here’s a list of what could be done in the future, and you may notice that I’ve copied stuff from my last post (muy perezoso! wtf?! spanish?)

  • Handle Client disconnects better, rather than just ignoring them
  • Send a message to the clients when someone disconnects, so that they know to remove a mob (LOC_Disconnect)
  • Perform more checks on the api calls to ensure that there are no errors, such as socket already in use
  • Allow users to specify the port that they want to run the server on
  • Allow users to specify the port that they want to connect to the server on (from the client)
  • Add a Rotation vector to the LOC_MobLocation and LOC_UpdateMobLocation messages, so that we can track turning smoothly
  • Re-write the server so that is better, stronger, faster

Source Code: Download

Binaries: Download