3D transformations are similar to their two-dimensional counterparts, although as you'd expect, they apply to three-dimensional objects instead of two-dimensional objects, so there are a few differences. This section explains how 3D transformations work, but to really get to know them, you need to build some 3D applications. See my article WPF Wonders: 3D Drawing to get started.
WPF provides three basic types of 3D transformations that perform translation, scaling, and rotation:
- The TranslateTransform3D class lets you translate an object in 3D space. Its OffsetX, OffsetY, and OffsetZ properties determine how far the object is offset in each dimension. This is pretty simple and similar to the 2D version.
- The ScaleTransform3D class lets you scale an object in 3D space. Its ScaleX, ScaleY, and ScaleZ properties determine how much the object is scaled in the X, Y, and Z directions. Again no real surprises here.
- A RotateTransform3D object lets you specify a rotation in three dimensions. In two dimensions, you rotate an object around a point. In three dimensions, you rotate an object around a line or axis. The object's Rotation property specifies the rotation. This value should be a Rotation3D object. Rotation3D is an abstract class so you must use one of its derived classes: AxisAngleRotation3D or QuaternionRotation3D. I'm going to ignore the second of these and describe only AxisAngleRotation3D because it's reasonably intuitive.
An AxisAngleRotation3D object represents a rotation around an axis by a specified angle. Its Angle property determines the angle of rotation in degrees and its Axis property determines the vector around which to rotate.
For example, the following XAML snippet rotates an object 30 degrees around the X axis.
<AxisAngleRotation3D Axis="1,0,0" Angle="30"/>
(Yes, it seems somewhat roundabout to create a RotateTransform3D object and set its Rotation property to an AxisAngleRotation3D object but that's the way it works.)
The axis is specified as a vector so its coordinates are relative. The value 1,0,0 points in the direction of the X-axis but doesn't indicate where the axis starts. In other words, it could specify the X-axis passing through the origin, or a line parallel to the X-axis that passes through the points (0, 1, 0) and (1, 1, 0).
By default, the axis passes through the origin, but you can change that by setting the RotateTransform3D object's CenterX, CenterY, and CenterZ properties to specify a point through which the axis of rotation should pass.
Using 3D Transformations
So now that you understand what the 3D transformations do, what good are they? It turns out they're quite useful for building scenes (and making robots).
For example, building a bunch of rectangular blocks scattered around various places is straightforward but rather tedious. If you mess up the points' coordinates or the order in which the points are used to make triangles, you can end up with a mess—or worse, with some sides oriented improperly, so parts of the block disappear when viewed from various angles.
In contrast, it's relatively easy to build a single cube that's one unit wide centered at the origin. With that cube in hand, you can copy and paste it and then apply transformations to move the copy into position to make up your blocks.
Transformations are also quite handy for building robots. The sample program RobotArm shown in Figure 5 displays a robot arm that you can control by using the program's sliders. The sliders inside the drawing area let you rotate the entire model and zoom in and out.
Each piece of the robot is built from a single cube centered at the origin that has been scaled, translated, and rotated into position.
This arm has a red base that can rotate around the vertical Y-axis. The base ends in a shoulder that can rotate so the green upper arm moves up and down. The elbow also rotates to move the blue forearm. The red hand at the end of the forearm can rotate side-to-side and twist around the axis of the forearm. Finally, the hand's two green fingers can move together and apart. (In a more elaborate application, the fingers could grab things so the arm could pick them up.)
The tricky part of building this kind of robot is figuring out what transformations to use to put the various pieces in the right positions. What series of transformations do you need to use to get the forearm or wrist positioned correctly?
Although this seems like a daunting problem, it's actually fairly simple if you think about it properly.
The first step is to think of the robot as being in some initial position. In this case, the arm's initial position is pointing straight up when all of its joints are rotated by 0 degrees.
Now think about what happens when you rotate the base around the Y-axis. Rotating the base makes the entire arm from that point on rotate. The upper arm, forearm, hand, and fingers all rotate because they are all connected. If you rotate the base, everything attached to it also rotates.
Next, consider what happens if you rotate the shoulder downward. The upper arm moves downward and so do the forearm, hand, and fingers that are attached to it. Again, all of the pieces after the shoulder move because they are all attached.
To keep these related pieces together, you can create a Model3DGroup object containing the related pieces. Then, when you transform the group, all the pieces transform as well.
So here's how RobotArm works. It starts with a Model3DGroup that holds everything, including the lights and the ground. It doesn't have a transformation because the ground doesn't move.
The top-level group contains a group for the base. That group holds the red base itself, built by stretching a cube vertically. The group's transformation rotates the whole group, which so far contains just the base.
The base group also contains another group that represents the upper arm. This group holds the upper arm, again built by stretching a cube. This time the upper arm's transformation also translates it so it sits on top of the base. The group uses a transformation to represent the shoulder's rotation. It moves the upper arm up or down.
The upper arm group contains another group that represents the lower arm. This group contains the lower arm itself and another group representing the hand.
By now you should begin to see the pattern. Each group contains a piece of the arm plus another group that holds everything farther down the chain. Transforming any group moves the entire group, so the whole thing moves as a unit.
I recommend that you download the RobotArm program and see the code for details, but the code contains two little tricks that are worth mentioning.
First, the program lets you control the arm's joints by moving sliders. Each slider is named and the groups' transformations refer to those names to get their values.
For example, the following code shows the base group's transformation. It uses a single RotateTransform3D object to rotate the base and everything attached to it around the Y-axis. The transform's Angle property is bound to the sliBase slider's Value property.
The other sliders control transformations in similar ways.
The second trick I wanted to mention deals with the fingers. The following code shows how the program builds its left finger.
<ScaleTransform3D ScaleX="1.5" ScaleY="3" ScaleZ="0.5"/>
The first transformation takes a cube centered at the origin and moves it so it sits on the XY plane. The second transformation stretches the cube to have the finger's shape. The third transformation moves the finger to its position on top of the hand after all the other transformations higher up the arm's hierarchy are set to their initial values.
The final transformation moves the finger according to the value provided by the sliHand slider. All this follows the previous techniques fairly closely, using transformations to build the basic finger and then binding a final transformation to a slider to move the finger.
The tricky part comes when you try to build the right finger. I wanted the same slider to move both the left and right fingers closer together or farther apart. For that to work, when the left finger moves left, the right finger must move right. If the sliHand slider has value V and the left finger is translated distance V in the Z direction, the right finger needs to move distance - V.
Unfortunately, WPF doesn't allow you to use any calculations in XAML code, so you cannot multiply the value V by -1. You could handle this in code but it would be nice to have an all-XAML solution.
But there is a workaround. Here's the trick: Rather than multiplying the value by -1, you can use a scale transformation to scale it by a factor of -1. That essentially flips the sign of the offset and solves the problem.
The following code shows the right finger's transformations. They mirror the left finger's transformations, but scaled by a factor of -1 in the Z direction. Now when the left finger moves left, the right finger moves right.
<ScaleTransform3D ScaleX="1.5" ScaleY="3" ScaleZ="0.5"/>