Click here to Skip to main content
Click here to Skip to main content

Managed DirectX Tutorials: Part 4 - The Transformation Pipeline

, 24 May 2006 CPOL
Rate this:
Please Sign up or sign in to vote.
Set up a robust framework for creating manipulatable 3D objects
Sample Image - MDXTut5.jpg

The Transformation Pipeline

In our last tutorial, we created a triangle by specifying in screen coordinates where we wanted to put it.
This method, whilst simple is obviously not very useful for creating rich, moving 3D environments.
Instead, most DirectX applications use Transformed coordinates. This means that the coordinates of a model are manipulated from their original position before being rendered, so take this basic example for a man running:

Man.Position Y += Speeds.Running * Time_Elapsed;

This basic assignment multiplies the running speed by the amount of time elapsed since the last update.

Theory

Whilst useful, the transformation pipeline can often be hard to understand.
When you create a model (either a triangle like in the last tutorial, or a complex 3D model) in transformed coordinates, everything is defined relative to the model itself. Take this example:

first

This shows a triangle with its coordinates defined relatively. The center point is defined as coordinate (0,0), and the three vertices being based off that.
The huge advantage of this system is that these coordinates can easily be scaled, translated and rotated across a screen.
The transformation pipeline transforms these relative coordinates into world space coordinates, i.e., how the primitive will be positioned in the full world, and finally view space coordinates, i.e., how the primitive will look on a screen.
Take the example of a crate in a game. This crate’s coordinates are relative to itself, like our triangle.

Now, someone places this box in a game world. The box is scaled by 3 to make it bigger, and to be moved into the game world it has to be translated into a new position.
To accomplish this, DirectX uses a mathematical structure called Matrices to perform manipulations on an object. It is beyond the scope of this article to fully explain Matrices, but if you are feeling lucky, http://en.wikipedia.org/wiki/Matrix_(mathematics), and when you’ve read it you can come back here!
A lower-level understanding of Matrices will help you program them, but we will go into this when we really start to use them in the next tutorial.

Basically, a matrix is a grid of numbers we can perform mathematical operations on to dynamically change the position of a primitive in DirectX.
Naturally, to do this we will need to re-write and add parts to our engine. The first thing to do is add two new objects to our cObject.cs file. These are cPosObject and cPosTriangle. As you can guess, these are the positioned versions of our original classes.

NOTE: In DirectX, we use the word “Transformed’ to describe vertices which have already been declared in screen-space coordinates (i.e. our last tutorial) and positioned to describe those which have been declared in object space (relative) coordinates.

The new cObject class:

class cPosObject   
{    
   public CustomVertex.PositionColored[] itsVerts;
   public Matrix itsTransform;
   public bool isActive;
}

As you can see, this is very similar to our previous class, with the exception that our CustomVertex has been changed to a PositionColored as opposed to TransformedColored. This is explained in the note above.

The second difference is that we have added a Matrix object to our class. This is used when we render the triangle. It is how we translate, scale and rotate the object before rendering it (the transformation from Object space to World space coordinates).

We will assign this Matrix object in the constructor of our new cPosTriangle class. The class will have two methods and no members. The methods will be its constructor and the render method. The ability to render itself will be very useful when dealing with large objects. You will be able to see an example of this in the tutorial.

Its members will be inherited from our new cPosObject class.

public cPosTriangle(bool ac, Vector3 Translation, 
	Vector3 Scale, Vector3 Rotation,Color pColor)
{
   itsVerts = new CustomVertex.PositionColored[3];
   Matrix T = Matrix.Translation(Translation);
   Matrix S = Matrix.Scaling(Scale);
   Matrix Rx = Matrix.RotationX(Rotation.X);
   Matrix Ry = Matrix.RotationY(Rotation.Y);
   Matrix Rz = Matrix.RotationZ(Rotation.Z);
   itsTransform  = Rx * Ry * Rz * T * S;
   isActive =  ac;
   itsVerts[0]  = new CustomVertex.PositionColored
		(new Vector3(0,  0, 0), pColor.ToArgb());
   itsVerts[1]  = new CustomVertex.PositionColored
		(new Vector3(2, 0, 0), pColor.ToArgb());
   itsVerts[2]  = new CustomVertex.PositionColored
		(new Vector3(1, 2, 0), pColor.ToArgb());
}

Ok, calm down and move the mouse away from the large X at the top right of your screen.
It isn't that bad – it's basically the same method we have used previously, but we are assigning matrices for Rotation, Translation and Scale based on a set of XYZ coordinates. We make one matrix for each manipulation point (Translation, Scaling and then each rotation side) and then call a method to construct the matrix into a certain manipulation based on one of our parameters.

We then multiply all of these matrices together to come up with a final matrix which we can use to tell the rendering pipeline how to manipulate our primitive.

Because we now have a better system, you no longer need to input a set of vertices into the method, instead we can define the points as Vector3s and our scaling matrix can make any triangle from them.

This method can be easily overloaded to suit your needs – maybe you want to input vertices, maybe you don’t need rotation and maybe you want a global scale modifier for each side, etc.

And finally, our Render() method:

public void Render(Device device)
  {
  device.Transform.World = itsTransform; 
  device.DrawUserPrimitives(PrimitiveType.TriangleList, 1, itsVerts);
  }

The only change here is the assignment of itsTransform to device.Transform.World.

This sets how the device will render all primitives until it is re-assigned.
When we get into dealing with larger numbers of primitives, we can call this once for multiple primitives, but for now it will suffice.
We then draw the user primitives and by setting the world transform to itsTransform, itsVerts is automatically manipulated into the world space coordinates we want.

The next step is to change our OnPaint event. We need to call some methods in the rendering loop to set how we look at the scene.
We need to set the camera and view. We will explain in detail how this works here:

device.Transform.View =  
  Matrix.LookAtLH(new Vector3(0, -40, 300), 
	new Vector3(0, -5, 0), new Vector3(0, 1, 0));

This method defines the properties of our view point, or camera.
In a first-person game, this would be the eye of the character we are playing, for instance.

The Matrix.LookAtLH is defined as follows:

Matrix.LookAtLH(Camera Position, Camera Target, Up )

The first parameter is fairly self explanatory, a coordinate for the position of the camera.

The second coordinate is also pretty obvious, the target (i.e. where the camera is looking at). This is used with the cameras position to create a vector-line from the camera to its target so DX can calculate where the camera is looking.

The final parameter is not so obvious, but luckily it's almost always the same, this defines which way is upwards. As usual, this is the Y axis.
To put it into application, in a First Person Shooter game, the camera target will be wherever the crosshair (usually an aiming device / scope for the gun in the center of the screen).

As the player moves the mouse, the camera moves along with the crosshair. Although these are not directly linked, it’s a good way to see how the parameters would be used in a real-life application (well, not real life as such, but..)

The camera position would be the player moving around the screen.

Now feast your eyes upon my latest diagram. Although not usually of a high standard, this one should be even more interesting as my laptop (which I am currently using) does not have any image editing software for which the trial has not run out except for the world-(in?)famous Microsoft Paint.

second

Ok, here you see a bad guy, representing the camera target (what the player is looking at) and three possible camera positions (where the player is standing).

As you can see as the player moves, he/she has to turn accordingly to keep the bad-guy within his/her view range. As such, the vector between them changes.
This displays the relationship between the camera position / target in a visual application of how it may be used in a game (which is what most people reading this article will be interested in doing at some point).

The last stage in setting up a position-coordinate based system is to change the Projection. This is performed in a similar way to setting up the camera stage.
Projection defines things such as the camera’s viewing “cone” (anyone who has played Metal Gear Solid will understand what I mean). This is technically referred to as a viewing "Frustrum".
We make a call to this method:

Matrix.PerspectiveFovLH(Field of View,Aspect Ratio, Near   Plane, Far Plane);

The first parameter is our “viewing cone” as discussed earlier. This will usually be PI / 4 (quarter-circle) to mimic the human eye’s vision.
The second parameter is our Aspect Ratio. Usually this will be our height divided by width as with televisions, etc.
The last two parameters define the far and near planes. This means the closest plane it can see at, and the furthest.

Sounds like we need another diagram…

third

The red area denotes the area which will be rendered. Any objects before our near plane, after our far plane or outside of our viewing cone will be clipped (ignored).

The actual calls to set our camera and projection in the OnPaint method are as follows:

device.Transform.View = Matrix.LookAtLH(new Vector3(0, -40,  300), 
			new Vector3(0,  -5, 0), new Vector3(0,  1, 0));
device.Transform.Projection = Matrix.PerspectiveFovLH((float)Math.PI / 4,  
				Width / Height, 1f, 500f);

Feel free to play around with these variables after you have read the tutorial to see the different reactions you get by changing them.
Now that we finally have our framework set up, we can call it in our Main() method.

To show the ease and robustness of our new engine, we will render four differently coloured triangles of the same size, in a line from each other.
To do this, switch back to our GameWindow class, and change the public cTriangle Triangle to an array of new cPosTriangles. Next add an int called NumTriangles and initialise it to 0. This will keep track of how many triangles in our array we have initialised.

Then in the Main() method, add the following (replacing all code in the using statement):

As you can see, besides the color parameter, the only thing that changes is the Translation vector. This increases globally by one each time.
Only one more thing to do... and then you will be that next step closer to Doom IV…
Switch back to your GameWindow class, and in between the calls to BeginScene() and EndScene(), replace the code with the following:

foreach (cTriangle Triangle in Triangles)
    if (Triangle != null)
        Triangle.Render(device);

As you can see, this loops through every triangle in the array, tests whether or not they are null, and if not it calls its Render() method.
You can clearly see the results when you compile and run the program.

I encourage you to play with the camera and triangle variables – playing with scaling, rotating, translating, camera targets, etc.
In the next tutorial, we will really bring matrixes to use when we create a control system for moving and looking around our world.

Feedback

I am always open for answering questions, whether through MSN (jamespraveen@aol.com), email (james@magclan.cwhnetworks.com) or through the message board attached to this article.
If you have a problem with any of my samples / my topics in general, please feel free to ask me.

History

  • 21/02/06: First submitted article 4 to CodeProject
  • 22/02/06: Re-formatted code syntax

Previous Articles

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

James Gupta
Web Developer
United States United States
I live in England, UK in a small town which only just got broadband...
 
In my spare time, I run a small website design, hosting and maintenance business at www.jamesgupta.com

Comments and Discussions

 
GeneralYou don't have to be old to be wise :-). Judas priest PinmemberEpsilone33-May-07 10:00 
GeneralKudos James... PinmemberVorlin26-Dec-06 22:06 
QuestionParts 1-4 ? PinmemberW. Kleinschmit21-Feb-06 20:55 
AnswerRe: Parts 1-4 ? PinmemberJames Gupta22-Feb-06 6:39 
GeneralFormatting guidelines PinstaffNishant Sivakumar21-Feb-06 7:20 
GeneralRe: Formatting guidelines PinmemberJames Gupta21-Feb-06 8:58 
GeneralRe: Formatting guidelines PinstaffNishant Sivakumar21-Feb-06 9:38 
GeneralRe: Formatting guidelines PinmemberDaniel Turini21-Feb-06 12:36 
GeneralRe: Formatting guidelines PinstaffNishant Sivakumar21-Feb-06 13:13 
Daniel Turini wrote:
Come on, give the guy a break. I wish I could write such an article when I was 13...

 
Hey Daniel,
 
Just because he is 14 doesn't mean we can't help him improve, right? And he is 14, not 5, so he should be old enough to get this right. Smile | :)
 
Regards,
Nish
Nish’s thoughts on MFC, C++/CLI and .NET (my blog)
The Ultimate Grid - The #1 MFC grid out there!

GeneralRe: Formatting guidelines PinmemberJames Gupta22-Feb-06 6:43 
GeneralTouche! Pinmemberfwsouthern22-Feb-06 7:18 
GeneralRe: Formatting guidelines PinstaffNishant Sivakumar23-Feb-06 1:01 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web03 | 2.8.141015.1 | Last Updated 24 May 2006
Article Copyright 2006 by James Gupta
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid