Introduction
This series of articles contains advice about how to model and animate shapes to create a simple character in WPF using C# code. There is a minimum of XAML involved in the example. Instead, C# classes are included which create the basic shape classes, and the model is constructed by building upon those shapes.
In the first two articles, we talked about basic shapes like triangles, rectangles, circles and cylinders. We also talked about back materials and about flat shading and smooth shading. We also covered some simple rotation transforms and animated rotations.
In this final article, we will add some other shapes we need to make our robot, and show how the kind of simple rotation transforms we saw before can be used to animate the arms and legs of our robot. We will also cover storyboards and emissive materials.
Storyboarding will be used to make our robot walk along a predetermined path. The storyboard and animations are done in C# rather than in XAML. We will create a very simple character using triangle meshes, animations, materials, and storyboards, and set him in motion using the same environment, cameras and lights that we covered in the first part.
All the code for the robot is contained in the download for the article. I will only include snippets in the article.
This example was created with Visual Studio 2010 and the 4.0 framework. All the source for the example is included in the zip file. You can run the demo to see the walking robot scene.
To make our robot follow human proportions, we need some slightly more sophisticated shapes. We could make him out of cubes or cylinders, but to get a little more graceful appearance we will introduce some shapes that can skew to the left or right as well as taper from bottom to top and front to back. The first new class we will add is a polygon class.
All it needs to exist is a list of points:
public List<point3d> points;
public WpfPolygon(List<point3d> Points)
{
points = new List<point3d>();
foreach (Point3D p in Points)
{
Point3D p1 = clonePoint(p);
points.Add(p1);
}
}
The polygon class is used to support our wedge class so the fronts and backs of wedges don't have to be square.
The entire code for the polygon class is in the zip files, but I just want to go over the part that adds the polygon to a mesh by dividing it into triangles:
public void addToMesh(MeshGeometry3D mesh, List<point3d> pointList)
{
if (pointList.Count == 3)
{
WpfTriangle.addTriangleToMesh(pointList[0], pointList[1], pointList[2], mesh);
}
else if (pointList.Count == 4)
{
WpfTriangle.addTriangleToMesh(pointList[0], pointList[1], pointList[2], mesh);
WpfTriangle.addTriangleToMesh(pointList[0], pointList[2], pointList[3], mesh);
}
else if (pointList.Count > 4)
{
Point3D center = GetCenter();
List<point3d> temp = new List<point3d>();
foreach (Point3D p in pointList)
{
temp.Add(p);
}
temp.Add(pointList[0]);
for (int i = 1; i < temp.Count; i++)
{
WpfTriangle.addTriangleToMesh(temp[i], center, temp[i - 1], mesh);
}
}
}
This is a very simple way to triangulate a polygon. If the polygon is a triangle, or is 4 points and therefore consists of 2 triangles, the job is easy. For the general case where it has more than 4 points, we have to find the center and then make the triangles by combining the 2 points from each side with the center point. Of course the problem is that this will only work if the polygon is completely convex. If the polygon has any concave parts to it, this method will not always work. In a future article, I will show how to use the Ear Clipping method to triangulate a polygon. But for this project, our simple triangulation method will suffice.
The wedge class we will add allows us to define shapes as a portion of a cube. For our robot, we will go with an Android type that simulates a human being. Human body proportions are remarkably consistent. If you Google "human body proportions", you can easily find images online that explain the human body proportions. In order to follow those proportions fairly closely, the wedge class allows us to deform a cube by offsetting all sides of the top and bottom faces independently. This produces front and side faces that are not square, and so we use polygons to define those parts of the wedge.
The wedge class is included in the zip file for the project. Here is the constructor:
public WpfWedge(WpfCube containingCube,
double TopWidth,
double BottomWidth,
double TopDepth,
double BottomDepth,
double Height,
double TopOffSet, double XAlignmentTop, double XAlignmentBottom,
double ZAlignmentTop, double ZAlignmentBottom
)
{
cube = new WpfCube(containingCube);
front = new WpfPolygon();
back = new WpfPolygon();
topWidthPercent = TopWidth;
bottomWidthPercent = BottomWidth;
topDepthPercent = TopDepth;
bottomDepthPercent = BottomDepth;
heightPercent = Height;
topOffSetPercent = TopOffSet;
xAlignmentTopPercent = XAlignmentTop;
xAlignmentBottomPercent = XAlignmentBottom;
zAlignmentTopPercent = ZAlignmentTop;
zAlignmentBottomPercent = ZAlignmentBottom;
TopWidth *= cube.width;
BottomWidth *= cube.width;
TopDepth *= cube.depth;
BottomDepth *= cube.depth;
Height *= cube.height;
TopOffSet *= cube.height;
XAlignmentTop *= cube.width;
XAlignmentBottom *= cube.width;
ZAlignmentTop *= cube.depth;
ZAlignmentBottom *= cube.depth;
topWidth = TopWidth;
bottomWidth = BottomWidth;
topDepth = TopDepth;
bottomDepth = BottomDepth;
height = Height;
topOffSet = TopOffSet;
xAlignmentTop = XAlignmentTop;
xAlignmentBottom = XAlignmentBottom;
zAlignmentTop = ZAlignmentTop;
zAlignmentBottom = ZAlignmentBottom;
topCenter = cube.centerTop();
topCenter.Y -= TopOffSet;
topCenter.Z += ZAlignmentTop;
topFront = cube.centerTop();
topFront.Y -= TopOffSet;
topFront.Z += ZAlignmentTop;
topFront.Z += topDepth / 2;
}
Notice that the wedge is defined in terms of a containing cube and then the width and offset variations are described in terms of fractions of that cube size. In our example, the defining is the cube that contains the entire human body, its width, height and depth. This makes it easy for us to translate the relative sizes of the body parts from a body proportions image and then create a wedge for each one part.
Some of our body parts like arms and hands are obviously mirror images of the other one, so to make the job easier, a method was added to create the mirror image of a wedge:
public WpfWedge mirrorX()
{
WpfWedge wedge2 = new WpfWedge(cube,
topWidthPercent,
bottomWidthPercent,
topDepthPercent,
bottomDepthPercent,
heightPercent,
topOffSetPercent,
-xAlignmentTopPercent,
-xAlignmentBottomPercent,
zAlignmentTopPercent,
zAlignmentBottomPercent);
return wedge2;
}
To actually realize our wedge as a triangle mesh, we need a method to first create the wedge from our class members, and then another method to add that wedge to the mesh as a series of triangles:
public void makeWedge()
{
double xpad = (cube.width - topWidth) / 2;
double zpad = (cube.depth - topDepth) / 2;
Point3D p1 = cubeOffset(
xpad + xAlignmentTop,
topOffSet,
(cube.depth - zpad) + zAlignmentTop
);
front.addPoint(p1);
Point3D p2 = cubeOffset(
(cube.width - xpad) + xAlignmentTop,
topOffSet,
(cube.depth - zpad) + zAlignmentTop
);
front.addPoint(p2);
xpad = (cube.width - bottomWidth) / 2;
zpad = (cube.depth - bottomDepth) / 2;
Point3D p3 = cubeOffset(
(cube.width - xpad) + xAlignmentBottom,
topOffSet + height,
(cube.depth - zpad) + zAlignmentBottom
);
front.addPoint(p3);
Point3D p4 = cubeOffset(
xpad + xAlignmentBottom,
topOffSet + height,
(cube.depth - zpad) + zAlignmentBottom
);
front.addPoint(p4);
xpad = (cube.width - topWidth) / 2;
zpad = (cube.depth - topDepth) / 2;
Point3D p5 = cubeOffset(
xpad + xAlignmentTop,
topOffSet,
zpad + zAlignmentTop
);
back.addPoint(p5);
Point3D p6 = cubeOffset(
(cube.width - xpad) + xAlignmentTop,
topOffSet,
zpad + zAlignmentTop
);
back.addPoint(p6);
xpad = (cube.width - bottomWidth) / 2;
zpad = (cube.depth - bottomDepth) / 2;
Point3D p7 = cubeOffset(
(cube.width - xpad) + xAlignmentBottom,
topOffSet + height,
zpad + zAlignmentBottom
);
back.addPoint(p7);
Point3D p8 = cubeOffset(
xpad + xAlignmentBottom,
topOffSet + height,
zpad + zAlignmentBottom
);
back.addPoint(p8);
}
public void addToMesh(MeshGeometry3D mesh, bool combineVertices)
{
if (front.points.Count > 2)
{
front.reversePoints();
back.reversePoints();
List<point3d> frontPoints = new List<point3d>();
foreach (Point3D p in front.points)
{
frontPoints.Add(p);
}
frontPoints.Add(front.points[0]);
List<point3d> backPoints = new List<point3d>();
foreach (Point3D p in back.points)
{
backPoints.Add(p);
}
backPoints.Add(back.points[0]);
for (int i = 1; i < frontPoints.Count; i++)
{
WpfTriangle.addTriangleToMesh(frontPoints[i - 1],
backPoints[i - 1], frontPoints[i], mesh, combineVertices);
WpfTriangle.addTriangleToMesh(frontPoints[i],
backPoints[i - 1], backPoints[i], mesh, combineVertices);
}
}
back.reversePoints();
front.addToMesh(mesh);
back.addToMesh(mesh);
}
We construct our wedge as two polygons. We add the sides by stitching together the points of both polygons to make triangles, and then let the front and back polygons add themselves to the mesh. Notice that in a couple of spots, we have to reverse the order of points in the polygons. This sort of thing is sometimes necessary because of winding direction in WPF 3D meshes. Remember that 3D WPF objects only have one side. So when constructing a solid object, it is important that the winding direction of each face is pointing toward the outside of the object. This will also ensure that the shading for the object is done properly.
We could make our entire robot out of wedges. Instead I decided to use wedges only for the hands and to use tubes for the other parts so as to provide a more rounded appearance. A tube is a convenient shape that is similar to a cylinder, but can have an arbitrary number of circles instead of just 2 that a cylinder has. It can also taper in different directions like a wedge can. I arranged things so that a tube can be constructed by first defining a wedge, and then letting the wedge create a tube of the same dimensions. Here is the wedge class method that makes a tube:
public WpfTube makeTube(int NSides, bool addBottom, bool addTop)
{
Point3D p = cube.centerTop();
p.Y -= topOffSet;
p.X += xAlignmentTop;
p.Z += zAlignmentTop;
WpfCircle top = new WpfCircle(NSides, p, topDepth / 2, topWidth / 2);
top.RotateZY(p, WpfUtils.radians_from_degrees(90));
Point3D p2 = cube.centerTop();
p2.Y -= topOffSet + height;
p2.X += xAlignmentBottom;
p2.Z += zAlignmentBottom;
WpfCircle bottom = new WpfCircle(NSides, p2, bottomDepth / 2, bottomWidth / 2);
bottom.RotateZY(p2, WpfUtils.radians_from_degrees(90));
WpfTube tube = new WpfTube(NSides);
if (addTop)
{
tube.closeTop = true;
}
tube.addCircle(top);
tube.addCircle(bottom);
if (addBottom)
{
tube.closeBottom = true;
}
return tube;
}
The code for the tube class is also included in the project for this article. Here is the code that adds a tube to a mesh:
public void addToMesh(MeshGeometry3D mesh, bool combineVertices)
{
for (int c = 1; c < circles.Count; c++)
{
WpfCircle front = circles[c - 1];
WpfCircle back = circles[c];
if (front.points.Count > 2)
{
List<point3d> frontPoints = new List<point3d>();
foreach (Point3D p in front.points)
{
frontPoints.Add(p);
}
frontPoints.Add(front.points[0]);
List<point3d> backPoints = new List<point3d>();
foreach (Point3D p in back.points)
{
backPoints.Add(p);
}
backPoints.Add(back.points[0]);
for (int i = 1; i < frontPoints.Count; i++)
{
WpfTriangle.addTriangleToMesh(frontPoints[i - 1],
backPoints[i - 1], frontPoints[i], mesh, combineVertices);
WpfTriangle.addTriangleToMesh(frontPoints[i],
backPoints[i - 1], backPoints[i], mesh, combineVertices);
}
}
}
if (closeTop)
{
circles[0].addToMesh(mesh, false);
}
if (closeBottom)
{
circles[circles.Count - 1].addToMesh(mesh, false);
}
}
We also want to make a change to our cylinder class from the earlier articles. We will use cylinders for the robot eyes. We want the eyes to look like glowing lights, so we don't want any shading on the eyes. Instead, we want to use an emissive material to construct the mesh for the eyes. So we added this method to the cylinder class:
public GeometryModel3D CreateModelEmissive(Color color)
{
MeshGeometry3D mesh = new MeshGeometry3D();
addToMesh(mesh, true, false);
Material materialBack = new DiffuseMaterial
(new SolidColorBrush(System.Windows.Media.Colors.Black));
Material material = new EmissiveMaterial(new SolidColorBrush(color));
MaterialGroup materialGroup = new MaterialGroup();
materialGroup.Children.Add(materialBack);
materialGroup.Children.Add(material);
GeometryModel3D model = new GeometryModel3D(mesh, materialGroup);
return model;
}
Notice that we created two materials for the emissive cylinder - a diffuse material in black and then an emissive material in the desired color. We combine these into a material group and then use that as the material for our model. This is necessary because of a glitch or oddity in how DirectX / WPF rasterization works. To see what happens without this hack, you can edit the project to try using only the emissive material for the eyes. It results in the eyes not being visible all the time when they should. To get the model to display correctly, the black material has to be used underneath the emissive material.
This completes the shape classes we need for our robot. The robot itself is defined as a separate class. The code is included. Here are the member variables and constructor:
private RotateTransform3D rotateTransform;
private TranslateTransform3D translateTransform;
private Color color = Colors.Silver;
private Point3D hipJoint;
private Point3D kneeJoint;
private Point3D shoulderJointLeft;
private Point3D shoulderJointRight;
private Point3D elbowJoint;
private GeometryModel3D thighModelLeft;
private GeometryModel3D thighModelRight;
private GeometryModel3D legModelLeft;
private GeometryModel3D legModelRight;
private GeometryModel3D footModelLeft;
private GeometryModel3D footModelRight;
private GeometryModel3D bootModelLeft;
private GeometryModel3D bootModelRight;
private GeometryModel3D armLeftModel;
private GeometryModel3D armRightModel;
private GeometryModel3D foreArmLeftModel;
private GeometryModel3D foreArmRightModel;
private GeometryModel3D handLeftModel;
private GeometryModel3D handRightModel;
private GeometryModel3D gauntletLeftModel;
private GeometryModel3D gauntletRightModel;
private Point3D origin;
private double width = 0;
private double height = 0;
private double depth = 0;
private Model3DGroup robotModelGroup;
private Point3D eyePointLeft = new Point3D(0, 0, 0);
private Point3D eyePointRight = new Point3D(0, 0, 0);
public WpfRobotBody()
{
getDimensions();
origin = getModelPlace();
robotModelGroup = createModel(true);
addMotionTransforms();
workLegs();
workArms();
}
Notice that our robot is made of several different models combined into a model group. It has to be done this way so that we can animate the pieces of each leg and arm separately and still animate the entire robot as a unit. Also you will notice that we define several reference points:
private Point3D hipJoint;
private Point3D kneeJoint;
private Point3D shoulderJointLeft;
private Point3D shoulderJointRight;
private Point3D elbowJoint;
As we are constructing our robot model according to our defined proportions, these reference points are populated which will allow use to define rotation transforms to animate our arms and legs.
Here is the method that the constructor calls to create the robot model group. The entire robot is made of tubes, except for the hands which are wedges, and the eyes are cylinders. We call the special method we added earlier to create emissive material cylinders for the eyes:
public Model3DGroup createModel(bool combineVertices)
{
int tuberes = 16;
Model3DGroup modelGroup = new Model3DGroup();
getDimensions();
WpfCube cube = new WpfCube(origin, width, height, depth);
double headHeightPercentage = cube.height / 8; double headWidthPercentage =
(headHeightPercentage * 2) / 3;
headHeightPercentage /= cube.height; headWidthPercentage /= cube.width;
double footOffset = headHeightPercentage * 7.75;
double torsoHeight = headHeightPercentage * 2.55;
double torsoOffset = headHeightPercentage * 1.5;
WpfWedge head = new WpfWedge(cube,
headWidthPercentage, headWidthPercentage * 0.85, 0.75, 0.67, headHeightPercentage, 0, 0, 0, 0, 0 );
WpfTube headTube = head.makeTube(tuberes, true, true);
double eyeDiameter = getHeadHeight() / 10;
double eyeLength = getHeadHeight() / 25;
eyePointLeft = clonePoint(origin);
eyePointLeft.X += width / 2;
eyePointLeft.Z += depth * 0.9;
eyePointLeft.Y -= getHeadHeight() / 2;
eyePointLeft.X -= (getHeadHeight() / 6);
eyePointRight = clonePoint(origin);
eyePointRight.X += width / 2;
eyePointRight.Z += depth * 0.9;
eyePointRight.Y -= getHeadHeight() / 2;
eyePointRight.X += (getHeadHeight() / 6);
Cylinder leftEye = new Cylinder
(eyePointLeft, 12, eyeDiameter, eyeDiameter, eyeLength);
modelGroup.Children.Add(leftEye.CreateModelEmissive(Colors.Red));
Cylinder rightEye = new Cylinder
(eyePointRight, 12, eyeDiameter, eyeDiameter, eyeLength);
modelGroup.Children.Add(rightEye.CreateModelEmissive(Colors.Red));
WpfWedge neck = new WpfWedge(cube,
headWidthPercentage * 0.7, headWidthPercentage * 0.7, 0.45, 0.45, headHeightPercentage, headHeightPercentage, 0, 0, -0.1, -0.1 );
WpfTube neckTube = neck.makeTube(tuberes, false, false);
WpfWedge shoulder = new WpfWedge(cube,
headWidthPercentage * 0.7, 1, 0.46, 0.8, headHeightPercentage * 0.23, torsoOffset -
(headHeightPercentage * 0.22), 0, 0, -0.1, 0 );
WpfTube shoulderTube = shoulder.makeTube(tuberes, true, false);
WpfWedge torso = new WpfWedge(cube,
headWidthPercentage * 2.2, headWidthPercentage * 1.85, 0.8, 0.6, torsoHeight, torsoOffset, 0, 0, 0, 0 );
WpfTube torsoTube = torso.makeTube(tuberes, false, true);
WpfWedge thighLeft = new WpfWedge(cube,
headWidthPercentage * 0.96, headWidthPercentage * 0.57, 0.55, 0.5, headHeightPercentage * 2.1, headHeightPercentage * 4, headWidthPercentage *
0.44, headWidthPercentage * 0.45, 0, 0 );
WpfWedge thighRight = thighLeft.mirrorX();
WpfTube thighRightTube = thighRight.makeTube(tuberes, false, false);
WpfTube thighLeftTube = thighLeft.makeTube(tuberes, false, false);
hipJoint = thighLeft.topCenter;
WpfWedge legLeft = new WpfWedge(cube,
headWidthPercentage * 0.57, headWidthPercentage * 0.47, 0.5, 0.45, headHeightPercentage * 1.76, headHeightPercentage * 6, headWidthPercentage *
0.45, headWidthPercentage * 0.4, 0, 0.0 );
WpfWedge legRight = legLeft.mirrorX();
WpfTube legRightTube = legRight.makeTube(tuberes, false, true);
WpfTube legLeftTube = legLeft.makeTube(tuberes, false, true);
kneeJoint = legLeft.topFront;
WpfWedge footLeft = new WpfWedge(cube,
headWidthPercentage * 0.4, headWidthPercentage * 0.42, 0.35, 0.95, headHeightPercentage / 3, footOffset, headWidthPercentage *
0.4, headWidthPercentage * 0.4, 0.0, 0.3 );
WpfWedge footRight = footLeft.mirrorX();
WpfTube footRightTube = footRight.makeTube(tuberes, false, false);
WpfTube footLeftTube = footLeft.makeTube(tuberes, false, false);
ankleJoint = footLeft.topCenter;
WpfWedge armLeft = new WpfWedge(cube,
headWidthPercentage / 2, headWidthPercentage / 2, 0.5, 0.46, headHeightPercentage * 1.5, headHeightPercentage * 1.5, 0.4, 0.44, 0, 0 );
WpfWedge armRight = armLeft.mirrorX();
WpfTube armRightTube = armRight.makeTube(tuberes, false, true);
WpfTube armLeftTube = armLeft.makeTube(tuberes, false, true);
shoulderJointRight = armRight.topCenter;
shoulderJointLeft = armLeft.topCenter;
WpfWedge foreArmLeft = new WpfWedge(cube,
headWidthPercentage / 2, headWidthPercentage / 4, 0.46, 0.26, headHeightPercentage * 1.3, headHeightPercentage * 3, 0.44, 0.43, 0, 0 );
WpfWedge foreArmRight = foreArmLeft.mirrorX();
WpfTube foreArmRightTube = foreArmRight.makeTube(tuberes, true, true);
WpfTube foreArmLeftTube = foreArmLeft.makeTube(tuberes, true, true);
elbowJoint = foreArmRight.topCenter;
WpfWedge handLeft = new WpfWedge(cube,
headWidthPercentage * 0.2, headWidthPercentage * 0.16, 0.23, 0.21, headHeightPercentage * 0.7, headHeightPercentage * 4.3, 0.43, 0.38, 0, 0 );
handLeft.makeWedge();
WpfWedge handRight = handLeft.mirrorX();
handRight.makeWedge();
modelGroup.Children.Add(headTube.CreateModel(color, combineVertices));
modelGroup.Children.Add(neckTube.CreateModel(color, combineVertices));
modelGroup.Children.Add(shoulderTube.CreateModel(color, combineVertices));
modelGroup.Children.Add(torsoTube.CreateModel(color, combineVertices));
legModelLeft = legLeftTube.CreateModel(color, combineVertices);
legModelRight = legRightTube.CreateModel(color, combineVertices);
modelGroup.Children.Add(legModelLeft);
modelGroup.Children.Add(legModelRight);
thighModelLeft = thighLeftTube.CreateModel(color, combineVertices);
thighModelRight = thighRightTube.CreateModel(color, combineVertices);
modelGroup.Children.Add(thighModelLeft);
modelGroup.Children.Add(thighModelRight);
footModelLeft = footLeftTube.CreateModel(color, combineVertices);
footModelRight = footRightTube.CreateModel(color, combineVertices);
modelGroup.Children.Add(footModelLeft);
modelGroup.Children.Add(footModelRight);
armLeftModel = armLeftTube.CreateModel(color, combineVertices);
armRightModel = armRightTube.CreateModel(color, combineVertices);
modelGroup.Children.Add(armLeftModel);
modelGroup.Children.Add(armRightModel);
foreArmLeftModel = foreArmLeftTube.CreateModel(color, combineVertices);
foreArmRightModel = foreArmRightTube.CreateModel(color, combineVertices);
modelGroup.Children.Add(foreArmRightModel);
modelGroup.Children.Add(foreArmLeftModel);
handLeftModel = handLeft.CreateModel(color, false);
handRightModel = handRight.CreateModel(color, false);
modelGroup.Children.Add(handLeftModel);
modelGroup.Children.Add(handRightModel);
if (hasBelt)
{
double beltPercent = 0.1;
WpfWedge belt = new WpfWedge(cube,
headWidthPercentage * 1.96, headWidthPercentage * 1.96, 0.68, 0.68, beltPercent, (torsoOffset + torsoHeight) -
beltPercent, 0, 0, 0, 0 );
WpfTube beltTube = belt.makeTube(tuberes, false, true);
modelGroup.Children.Add(beltTube.CreateModel(color, combineVertices));
}
if (hasBoots)
{
double bootPercent = 0.15;
WpfWedge bootLeft = new WpfWedge(cube,
headWidthPercentage * 0.7, headWidthPercentage * 0.5, 0.57, 0.5, bootPercent, footOffset - bootPercent, headWidthPercentage *
0.5, headWidthPercentage * 0.4, 0, 0 );
WpfWedge bootRight = bootLeft.mirrorX();
WpfTube bootRightTube = bootRight.makeTube(tuberes, false, true);
WpfTube bootLeftTube = bootLeft.makeTube(tuberes, false, true);
bootModelLeft = bootLeftTube.CreateModel(color, combineVertices);
bootModelRight = bootRightTube.CreateModel(color, combineVertices);
modelGroup.Children.Add(bootModelRight);
modelGroup.Children.Add(bootModelLeft);
}
if (hasGauntlets)
{
WpfWedge gauntletLeft = new WpfWedge(cube,
headWidthPercentage / 2, headWidthPercentage / 2.5, 0.55, 0.32, headHeightPercentage * 0.9, headHeightPercentage * 3.4, 0.43, 0.43, 0, 0 );
WpfWedge gauntletRight = gauntletLeft.mirrorX();
WpfTube gauntletRightTube = gauntletRight.makeTube(tuberes, false, true);
WpfTube gauntletLeftTube = gauntletLeft.makeTube(tuberes, false, true);
gauntletLeftModel = gauntletLeftTube.CreateModel(color, combineVertices);
gauntletRightModel = gauntletRightTube.CreateModel(color, combineVertices);
modelGroup.Children.Add(gauntletRightModel);
modelGroup.Children.Add(gauntletLeftModel);
}
return modelGroup;
}
All the hardcoded numbers in the robot are percentages of the whole containing cube that defines his size. This means that if you change the size of the robot and his containing cube, you don't have to change any of these numbers -- they are based on the human body proportions that we learned from artist images on Google. So you can use this same class to make a robot of any size. We defined our robot size as some arbitrary fraction of the width of our scene as a whole.
After making the robot model, the constructor calls some methods to animate the arms and legs. Here is the one that animates the legs:
public void workLegs()
{
double stepSeconds = 0.4;
if (legModelLeft != null && legModelRight != null)
{
double hipDegree = 10;
double kneeDegree = -9;
Transform3DGroup leftKneeTransformGroup = new Transform3DGroup();
Transform3DGroup rightKneeTransformGroup = new Transform3DGroup();
AxisAngleRotation3D axisHipRotationLeft =
new AxisAngleRotation3D(new Vector3D(1, 0, 0), 0.0);
RotateTransform3D rotateHipTransformLeft =
new RotateTransform3D(axisHipRotationLeft, hipJoint);
AxisAngleRotation3D axisHipRotationRight =
new AxisAngleRotation3D(new Vector3D(1, 0, 0), 0.0);
RotateTransform3D rotateHipTransformRight =
new RotateTransform3D(axisHipRotationRight, hipJoint);
AxisAngleRotation3D axisKneeRotationLeft =
new AxisAngleRotation3D(new Vector3D(1, 0, 0), 0.0);
RotateTransform3D rotateKneeTransformLeft =
new RotateTransform3D(axisKneeRotationLeft, kneeJoint);
AxisAngleRotation3D axisKneeRotationRight =
new AxisAngleRotation3D(new Vector3D(1, 0, 0), 0.0);
RotateTransform3D rotateKneeTransformRight =
new RotateTransform3D(axisKneeRotationRight, kneeJoint);
leftKneeTransformGroup.Children.Add(rotateKneeTransformLeft);
rightKneeTransformGroup.Children.Add(rotateKneeTransformRight);
leftKneeTransformGroup.Children.Add(rotateHipTransformLeft);
rightKneeTransformGroup.Children.Add(rotateHipTransformRight);
thighModelLeft.Transform = rotateHipTransformLeft;
thighModelRight.Transform = rotateHipTransformRight;
footModelRight.Transform = rightKneeTransformGroup;
footModelLeft.Transform = leftKneeTransformGroup;
legModelRight.Transform = rightKneeTransformGroup;
legModelLeft.Transform = leftKneeTransformGroup;
if (bootModelLeft != null && bootModelRight != null)
{
bootModelRight.Transform = rightKneeTransformGroup;
bootModelLeft.Transform = leftKneeTransformGroup;
}
DoubleAnimation legAnimationRight =
new DoubleAnimation(-hipDegree, hipDegree, durationTS(stepSeconds));
DoubleAnimation legAnimationLeft =
new DoubleAnimation(hipDegree, -hipDegree, durationTS(stepSeconds));
legAnimationLeft.RepeatBehavior = RepeatBehavior.Forever;
legAnimationRight.RepeatBehavior = RepeatBehavior.Forever;
DoubleAnimation kneeAnimationRight =
new DoubleAnimation(-kneeDegree, 0, durationTS(stepSeconds));
DoubleAnimation kneeAnimationLeft =
new DoubleAnimation(0, -kneeDegree, durationTS(stepSeconds));
kneeAnimationLeft.RepeatBehavior = RepeatBehavior.Forever;
kneeAnimationRight.RepeatBehavior = RepeatBehavior.Forever;
kneeAnimationLeft.AutoReverse = true;
kneeAnimationRight.AutoReverse = true;
legAnimationLeft.AutoReverse = true;
legAnimationRight.AutoReverse = true;
kneeAnimationLeft.BeginTime = durationTS(0.0);
kneeAnimationRight.BeginTime = durationTS(0.0);
legAnimationLeft.BeginTime = durationTS(0.0);
legAnimationRight.BeginTime = durationTS(0.0);
axisHipRotationLeft.BeginAnimation
(AxisAngleRotation3D.AngleProperty, legAnimationLeft);
axisHipRotationRight.BeginAnimation
(AxisAngleRotation3D.AngleProperty, legAnimationRight);
axisKneeRotationLeft.BeginAnimation
(AxisAngleRotation3D.AngleProperty, kneeAnimationLeft);
axisKneeRotationRight.BeginAnimation
(AxisAngleRotation3D.AngleProperty, kneeAnimationRight);
}
}
A few things to point out here. First, the leg animation is done by creating axis angle rotations, creating transforms from those rotations, applying transforms to models within our robot, and then animating the rotation angles. The auto reverse property is used to make the leg move back and forth. It is easier to see what is going on if you just break out the code for one hip:
AxisAngleRotation3D axisHipRotationLeft =
new AxisAngleRotation3D(new Vector3D(1, 0, 0), 0.0);
RotateTransform3D rotateHipTransformLeft =
new RotateTransform3D(axisHipRotationLeft, hipJoint);
thighModelLeft.Transform = rotateHipTransformLeft;
axisHipRotationLeft.BeginAnimation(AxisAngleRotation3D.AngleProperty, legAnimationLeft);
DoubleAnimation legAnimationLeft =
new DoubleAnimation(hipDegree, -hipDegree, durationTS(stepSeconds));
axisHipRotationLeft.BeginAnimation(AxisAngleRotation3D.AngleProperty, legAnimationLeft);a
legAnimationLeft.RepeatBehavior = RepeatBehavior.Forever;
legAnimationLeft.AutoReverse = true;
The animation for the lower leg is a little bit more complex because we have to move the lower leg relative to the upper leg, but we also have to move it along with the leg as a whole. So for the lower legs, we have to combine two transforms into a transform group. Here is how it works for one lower leg:
Transform3DGroup leftKneeTransformGroup = new Transform3DGroup();
AxisAngleRotation3D axisKneeRotationLeft =
new AxisAngleRotation3D(new Vector3D(1, 0, 0), 0.0);
RotateTransform3D rotateKneeTransformLeft =
new RotateTransform3D(axisKneeRotationLeft, kneeJoint);
leftKneeTransformGroup.Children.Add(rotateKneeTransformLeft);
leftKneeTransformGroup.Children.Add(rotateHipTransformLeft);
footModelLeft.Transform = leftKneeTransformGroup;
legModelLeft.Transform = leftKneeTransformGroup;
DoubleAnimation kneeAnimationLeft =
new DoubleAnimation(0, -kneeDegree, durationTS(stepSeconds));
kneeAnimationLeft.RepeatBehavior = RepeatBehavior.Forever;
kneeAnimationLeft.AutoReverse = true;
axisKneeRotationLeft.BeginAnimation(AxisAngleRotation3D.AngleProperty, kneeAnimationLeft);
The method to animate the arms works exactly the same way:
private void workArms()
{
double seconds = 0.4;
if (armLeftModel != null && armRightModel != null)
{
double elbowDegree = -16;
double shoulderDegreeH = 14;
Transform3DGroup leftHandGroup = new Transform3DGroup();
Transform3DGroup rightHandGroup = new Transform3DGroup();
Transform3DGroup leftShoulderGroup = new Transform3DGroup();
Transform3DGroup rightShoulderGroup = new Transform3DGroup();
armRightModel.Transform = rightShoulderGroup;
armLeftModel.Transform = leftShoulderGroup;
foreArmRightModel.Transform = rightHandGroup;
foreArmLeftModel.Transform = leftHandGroup;
handRightModel.Transform = rightHandGroup;
handLeftModel.Transform = leftHandGroup;
if (gauntletLeftModel != null && gauntletRightModel != null)
{
gauntletLeftModel.Transform = leftHandGroup;
gauntletRightModel.Transform = rightHandGroup;
}
AxisAngleRotation3D axisElbowRotationLeft =
new AxisAngleRotation3D(new Vector3D(1, 0, 0), elbowDegree);
RotateTransform3D rotateElbowTransformLeft =
new RotateTransform3D(axisElbowRotationLeft, elbowJoint);
AxisAngleRotation3D axisShoulderRotationLeft =
new AxisAngleRotation3D(new Vector3D(1, 0, 0), shoulderDegreeH);
RotateTransform3D rotateShoulderTransformLeft =
new RotateTransform3D(axisShoulderRotationLeft, shoulderJointLeft);
AxisAngleRotation3D axisElbowRotationRight =
new AxisAngleRotation3D(new Vector3D(1, 0, 0), elbowDegree);
RotateTransform3D rotateElbowTransformRight =
new RotateTransform3D(axisElbowRotationRight, elbowJoint);
AxisAngleRotation3D axisShoulderRotationRight =
new AxisAngleRotation3D(new Vector3D(1, 0, 0), shoulderDegreeH);
RotateTransform3D rotateShoulderTransformRight =
new RotateTransform3D(axisShoulderRotationRight, shoulderJointLeft);
leftHandGroup.Children.Add(rotateElbowTransformLeft);
rightHandGroup.Children.Add(rotateElbowTransformRight);
leftHandGroup.Children.Add(rotateShoulderTransformLeft);
rightHandGroup.Children.Add(rotateShoulderTransformRight);
leftShoulderGroup.Children.Add(rotateShoulderTransformLeft);
rightShoulderGroup.Children.Add(rotateShoulderTransformRight);
DoubleAnimation shoulderAnimationRight =
new DoubleAnimation(shoulderDegreeH, -shoulderDegreeH, durationTS(seconds));
DoubleAnimation shoulderAnimationLeft =
new DoubleAnimation(-shoulderDegreeH, shoulderDegreeH, durationTS(seconds));
shoulderAnimationLeft.RepeatBehavior = RepeatBehavior.Forever;
shoulderAnimationRight.RepeatBehavior = RepeatBehavior.Forever;
DoubleAnimation elbowAnimationRight =
new DoubleAnimation(elbowDegree, 0, durationTS(seconds));
DoubleAnimation elbowAnimationLeft =
new DoubleAnimation(0, elbowDegree, durationTS(seconds));
elbowAnimationLeft.RepeatBehavior = RepeatBehavior.Forever;
elbowAnimationRight.RepeatBehavior = RepeatBehavior.Forever;
elbowAnimationLeft.AutoReverse = true;
elbowAnimationRight.AutoReverse = true;
shoulderAnimationLeft.AutoReverse = true;
shoulderAnimationRight.AutoReverse = true;
elbowAnimationLeft.BeginTime = durationTS(0.0);
elbowAnimationRight.BeginTime = durationTS(0.0);
shoulderAnimationLeft.BeginTime = durationTS(0.0);
shoulderAnimationRight.BeginTime = durationTS(0.0);
axisShoulderRotationLeft.BeginAnimation
(AxisAngleRotation3D.AngleProperty, shoulderAnimationLeft);
axisElbowRotationLeft.BeginAnimation
(AxisAngleRotation3D.AngleProperty, elbowAnimationLeft);
axisShoulderRotationRight.BeginAnimation
(AxisAngleRotation3D.AngleProperty, shoulderAnimationRight);
axisElbowRotationRight.BeginAnimation
(AxisAngleRotation3D.AngleProperty, elbowAnimationRight);
}
}
Now it's time to add the robot to our scene along with the floor we created in the last article, and get him walking around. We can do that in the Viewport3D_Loaded
event back in our MainWindow.xaml.cs module. Here is what the event looks like now:
private void Viewport3D_Loaded(object sender, RoutedEventArgs e)
{
if (sender is Viewport3D)
{
viewport = (Viewport3D)sender;
robot = new WpfRobotBody();
double floorThickness = WpfScene.sceneSize / 100;
GeometryModel3D floorModel = WpfCube.CreateCubeModel(
new Point3D(-WpfScene.sceneSize / 2,
-floorThickness,
-WpfScene.sceneSize / 2),
WpfScene.sceneSize, floorThickness, WpfScene.sceneSize, Colors.Tan);
Model3DGroup groupScene = new Model3DGroup();
groupScene.Children.Add(floorModel);
groupScene.Children.Add(robot.getModelGroup());
groupScene.Children.Add(leftLight());
groupScene.Children.Add(new AmbientLight(Colors.Gray));
viewport.Camera = camera();
ModelVisual3D visual = new ModelVisual3D();
visual.Content = groupScene;
viewport.Children.Add(visual);
storyboardRobot();
}
}
We have added a method to create a storyboard for our robot. We want to combine 4 movements and repeat them over and over. The robot will walk along one edge of the floor, turn and walk along the next edge, until he gets back to where he began. These four separate animations can be sequenced using a storyboard. Back in the robot class, we already added two transforms to the robot model group that we will animate in order to move him around:
private RotateTransform3D rotateTransform;
private TranslateTransform3D translateTransform;
addTransform(robotModelGroup, translateTransform);
public void addTransform(Model3DGroup model, Transform3D transform)
{
Transform3DGroup group = new Transform3DGroup();
if (model.Transform != null && model.Transform != Transform3D.Identity)
{
if (model.Transform is Transform3D)
{
group.Children.Add(model.Transform);
}
else if (model.Transform is Transform3DGroup)
{
Transform3DGroup g = (Transform3DGroup)(model.Transform);
foreach (Transform3D t in g.Children)
{
group.Children.Add(t);
}
}
}
group.Children.Add(transform);
model.Transform = group;
}
So here is the code that creates our storyboard:
private void storyboardRobot()
{
double turnDuration = 0.7;
double totalDuration = 0.0;
double walkDuration = 3.4;
NameScope.SetNameScope(this, new NameScope());
storyBoard = new Storyboard();
Vector3D vector = new Vector3D(0, 1, 0);
AxisAngleRotation3D rotation = new AxisAngleRotation3D(vector, 0.0);
robot.getRotateTransform().Rotation = rotation;
DoubleAnimation doubleAnimationTurn1 =
new DoubleAnimation(0.0, 90.0, durationTS(turnDuration));
DoubleAnimation doubleAnimationTurn2 =
new DoubleAnimation(90.0, 180.0, durationTS(turnDuration));
DoubleAnimation doubleAnimationTurn3 =
new DoubleAnimation(180.0, 270.0, durationTS(turnDuration));
DoubleAnimation doubleAnimationTurn4 =
new DoubleAnimation(270.0, 360.0, durationTS(turnDuration));
RegisterName("TurnRotation", rotation);
RegisterName("MoveTransform", robot.getTranslateTransform());
storyBoard.Children.Add(doubleAnimationTurn1);
storyBoard.Children.Add(doubleAnimationTurn2);
storyBoard.Children.Add(doubleAnimationTurn3);
storyBoard.Children.Add(doubleAnimationTurn4);
Storyboard.SetTargetName(doubleAnimationTurn1, "TurnRotation");
Storyboard.SetTargetProperty(doubleAnimationTurn1,
new PropertyPath(AxisAngleRotation3D.AngleProperty));
Storyboard.SetTargetName(doubleAnimationTurn2, "TurnRotation");
Storyboard.SetTargetProperty(doubleAnimationTurn2,
new PropertyPath(AxisAngleRotation3D.AngleProperty));
Storyboard.SetTargetName(doubleAnimationTurn3, "TurnRotation");
Storyboard.SetTargetProperty(doubleAnimationTurn3,
new PropertyPath(AxisAngleRotation3D.AngleProperty));
Storyboard.SetTargetName(doubleAnimationTurn4, "TurnRotation");
Storyboard.SetTargetProperty(doubleAnimationTurn4,
new PropertyPath(AxisAngleRotation3D.AngleProperty));
double offset = WpfScene.sceneSize * 0.45;
DoubleAnimation doubleAnimationX1 =
new DoubleAnimation(-offset, -offset, durationTS(walkDuration));
DoubleAnimation doubleAnimationZ1 =
new DoubleAnimation(-offset, offset, durationTS(walkDuration));
Storyboard.SetTargetName(doubleAnimationX1, "MoveTransform");
Storyboard.SetTargetProperty(doubleAnimationX1,
new PropertyPath(TranslateTransform3D.OffsetXProperty));
Storyboard.SetTargetName(doubleAnimationZ1, "MoveTransform");
Storyboard.SetTargetProperty(doubleAnimationZ1,
new PropertyPath(TranslateTransform3D.OffsetZProperty));
storyBoard.Children.Add(doubleAnimationX1);
storyBoard.Children.Add(doubleAnimationZ1);
DoubleAnimation doubleAnimationX2 =
new DoubleAnimation(-offset, offset, durationTS(walkDuration));
DoubleAnimation doubleAnimationZ2 =
new DoubleAnimation(offset, offset, durationTS(walkDuration));
Storyboard.SetTargetName(doubleAnimationX2, "MoveTransform");
Storyboard.SetTargetProperty(doubleAnimationX2,
new PropertyPath(TranslateTransform3D.OffsetXProperty));
Storyboard.SetTargetName(doubleAnimationZ2, "MoveTransform");
Storyboard.SetTargetProperty(doubleAnimationZ2,
new PropertyPath(TranslateTransform3D.OffsetZProperty));
storyBoard.Children.Add(doubleAnimationX2);
storyBoard.Children.Add(doubleAnimationZ2);
DoubleAnimation doubleAnimationX3 =
new DoubleAnimation(offset, offset, durationTS(walkDuration));
DoubleAnimation doubleAnimationZ3 =
new DoubleAnimation(offset, -offset, durationTS(walkDuration));
Storyboard.SetTargetName(doubleAnimationX3, "MoveTransform");
Storyboard.SetTargetProperty(doubleAnimationX3,
new PropertyPath(TranslateTransform3D.OffsetXProperty));
Storyboard.SetTargetName(doubleAnimationZ3, "MoveTransform");
Storyboard.SetTargetProperty(doubleAnimationZ3,
new PropertyPath(TranslateTransform3D.OffsetZProperty));
storyBoard.Children.Add(doubleAnimationX3);
storyBoard.Children.Add(doubleAnimationZ3);
DoubleAnimation doubleAnimationX4 =
new DoubleAnimation(offset, -offset, durationTS(walkDuration));
DoubleAnimation doubleAnimationZ4 =
new DoubleAnimation(-offset, -offset, durationTS(walkDuration));
Storyboard.SetTargetName(doubleAnimationX4, "MoveTransform");
Storyboard.SetTargetProperty(doubleAnimationX4,
new PropertyPath(TranslateTransform3D.OffsetXProperty));
Storyboard.SetTargetName(doubleAnimationZ4, "MoveTransform");
Storyboard.SetTargetProperty(doubleAnimationZ4,
new PropertyPath(TranslateTransform3D.OffsetZProperty));
storyBoard.Children.Add(doubleAnimationX4);
storyBoard.Children.Add(doubleAnimationZ4);
doubleAnimationX1.BeginTime = durationTS(totalDuration);
doubleAnimationZ1.BeginTime = durationTS(totalDuration);
totalDuration += walkDuration;
doubleAnimationTurn1.BeginTime = durationTS(totalDuration);
totalDuration += turnDuration;
doubleAnimationX2.BeginTime = durationTS(totalDuration);
doubleAnimationZ2.BeginTime = durationTS(totalDuration);
totalDuration += walkDuration;
doubleAnimationTurn2.BeginTime = durationTS(totalDuration);
totalDuration += turnDuration;
doubleAnimationX3.BeginTime = durationTS(totalDuration);
doubleAnimationZ3.BeginTime = durationTS(totalDuration);
totalDuration += walkDuration;
doubleAnimationTurn3.BeginTime = durationTS(totalDuration);
totalDuration += turnDuration;
doubleAnimationX4.BeginTime = durationTS(totalDuration);
doubleAnimationZ4.BeginTime = durationTS(totalDuration);
totalDuration += walkDuration;
doubleAnimationTurn4.BeginTime = durationTS(totalDuration);
totalDuration += turnDuration;
storyBoard.RepeatBehavior = RepeatBehavior.Forever;
storyBoard.Begin(this);
}
There is a trick to setting up storyboards in C# code. You have to set the target for each animation. In our arm and leg movements for the robot, it was easy to associate the animation with its target object and property by using the BeginAnimation
call:
axisHipRotationLeft.BeginAnimation(AxisAngleRotation3D.AngleProperty, legAnimationLeft);
This single call to BeginAnimation
is enough to establish that the legAnimationLeft
is going to operate upon the AxisAngleRotation3D.AngleProperty
property of the axisHipRotationLeft
. But we can't call BeginAnimation
in this case because we are adding all of our animations to a storyboard and we want to give them all different start times so they will happen in a particular sequence. We start the whole thing going by making the following calls:
storyBoard.RepeatBehavior = RepeatBehavior.Forever;
storyBoard.Begin(this);
So in order to set the targets for each animation, we have to establish a name scope, register names for each target object we want to animate, and then call SetTargetName
and SetTargetProperty
for each one. Here is how it works for one of our animations:
NameScope.SetNameScope(this, new NameScope()); RegisterName("TurnRotation", rotation);
Storyboard.SetTargetName(doubleAnimationTurn1, "TurnRotation");
Storyboard.SetTargetProperty(doubleAnimationTurn1,
new PropertyPath(AxisAngleRotation3D.AngleProperty));
So that's how to storyboard our robot. Only one last wrinkle for this animation demo. We want to add a keystroke to change to an alternate camera angle. So add a Keyup
event to our window and change the camera()
function like this:
private void Window_KeyUp(object sender, KeyEventArgs e)
{
overheadCamera = !overheadCamera;
viewport.Camera = camera();
}
public PerspectiveCamera camera()
{
PerspectiveCamera perspectiveCamera = new PerspectiveCamera();
if (overheadCamera)
{
perspectiveCamera.Position = new Point3D(0,
WpfScene.sceneSize * 2, WpfScene.sceneSize / 50);
}
else
{
perspectiveCamera.Position =
new Point3D(-WpfScene.sceneSize, WpfScene.sceneSize / 2, WpfScene.sceneSize);
}
perspectiveCamera.LookDirection = new Vector3D
(lookat.X - perspectiveCamera.Position.X,
lookat.Y - perspectiveCamera.Position.Y,
lookat.Z - perspectiveCamera.Position.Z);
perspectiveCamera.FieldOfView = 60;
return perspectiveCamera;
}
History
- 7th November, 2010: Initial version