Introduction
This is part 2 in the walking robot series where we create a simple animated robot character using C# code in WPF.
All the source for this article is included. The project builds in Visual Studio 2010.
Let's use the WpfCube
class that we defined in the last article to create a simple environment:
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);
We call this code upon loading our Viewport3D
using the event we defined before. This will create a floor for our robot to walk upon. For this article, we are just going to define some more of the shapes we need to make our robot.
We will need a circle
class. Here is some of the code we need to construct a circle
:
private int nSides = 6;
private Point3D center;
private List<Point3D> points;
private double radiusY;
private double radiusX;
public WpfCircle(int NSides, Point3D Center, double Radius)
{
nSides = NSides;
angle = (double)360.0 / (double)nSides;
center = new Point3D(Center.X, Center.Y, Center.Z);
radiusY = Radius;
radiusX = Radius;
makeCircle();
}
public WpfCircle(int NSides, Point3D Center, double RadiusY, double RadiusX)
{
nSides = NSides;
angle = (double)360.0 / (double)nSides;
center = new Point3D(Center.X, Center.Y, Center.Z);
radiusY = RadiusY;
radiusX = RadiusX;
makeCircle();
}
private void makeCircle()
{
points = new List<Point3D>();
top = new Point3D(center.X, center.Y + radiusY, center.Z);
points.Add(top);
for (int i = 1; i < nSides; i++)
{
Point3D p = WpfUtils.RotatePointXY
(top, center, WpfUtils.radians_from_degrees(angle * i));
if (radiusX != radiusY)
{
double diff = p.X - center.X;
diff *= radiusX;
diff /= radiusY;
p = new Point3D(center.X + diff, p.Y, p.Z);
}
points.Add(p);
}
}
Only a center point and X and Y radius are needed to construct our circle. As before, we need a method to add the circle's triangles to a geometry mesh:
public void addToMesh(MeshGeometry3D mesh, bool combineVertices)
{
if (points.Count > 2)
{
List<Point3D> temp = new List<Point3D>();
foreach (Point3D p in points)
{
temp.Add(p);
}
temp.Add(points[0]);
for (int i = 1; i < temp.Count; i++)
{
WpfTriangle.addTriangleToMesh(temp[i], center, temp[i - 1],
mesh, combineVertices);
}
}
}
We provide methods to create a GeometryModel3D
from a circle
object. To illustrate a point about how WPF works, here are two versions of this method:
public GeometryModel3D createModel(Color color, bool combineVertices)
{
MeshGeometry3D mesh = new MeshGeometry3D();
addToMesh(mesh, combineVertices);
Material material = new DiffuseMaterial(
new SolidColorBrush(color));
GeometryModel3D model = new GeometryModel3D(mesh, material);
return model;
}
public GeometryModel3D createModelTwoSided(Color color, bool combineVertices)
{
MeshGeometry3D mesh = new MeshGeometry3D();
addToMesh(mesh, combineVertices);
Material material = new DiffuseMaterial(
new SolidColorBrush(color));
GeometryModel3D model = new GeometryModel3D(mesh, material);
model.BackMaterial = material;
return model;
}
One version of the createModel
method makes a two-sided model. The only difference is this line of code:
model.BackMaterial = material;
If you run the demo, you will see the two circles that are created spinning around. One of the circles is visible from both sides because it has a BackMaterial
set. The other one is only available from one side.
We are not going to use any circles for our robot, but we need the circle so that we can construct cylinder shapes that we will be using for the robot. Here are the beginnings of our cylinder
class:
private WpfCircle front;
private WpfCircle back;
private int nSides;
private double frontRadius;
private double backRadius;
private double length;
private Point3D center;
public Cylinder(Point3D Center, int NSides, double FrontRadius, double BackRadius,
double Length)
{
center = Center;
nSides = NSides;
frontRadius = FrontRadius;
backRadius = BackRadius;
length = Length;
front = new WpfCircle(nSides, center, frontRadius);
backcenter = new Point3D(center.X, center.Y, center.Z - length);
back = new WpfCircle(nSides, backcenter, backRadius);
}
As you can see, we use two circles to construct our cylinder
. Here is the method that adds the triangles to the mesh for this shape. It adds the triangles for the sides, then lets each circle
object add its own triangles to the mesh:
public void addToMesh(MeshGeometry3D mesh, bool encloseTop, bool combineVertices)
{
if (front.getPoints().Count > 2)
{
List<Point3D> frontPoints = new List<Point3D>();
foreach (Point3D p in front.getPoints())
{
frontPoints.Add(p);
}
frontPoints.Add(front.getPoints()[0]);
List<Point3D> backPoints = new List<Point3D>();
foreach (Point3D p in back.getPoints())
{
backPoints.Add(p);
}
backPoints.Add(back.getPoints()[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 (encloseTop)
{
front.addToMesh(mesh, false);
back.addToMesh(mesh, false);
}
}
}
Notice that we have an argument for combineVertices
. We talked in the first article of the robot series about how to make your model have smooth shading by combining vertices. We didn't want smooth shading for our cube, but we do want smooth shading for our cylinders. Notice that in adding the triangles to the sides of the cylinder, we use the WpfTriangle.addTriangleToMesh
method that we defined in the first article. It calls the method with the combineVertices
argument we passed in so we can specify smooth or flat shading for our triangle. Notice that we always pass in false
for combined vertices when adding our front and back circles to the mesh. We want flat shading for these because there should be a definite line between the sides and ends of the cylinder. For purposes of review, here is the triangle code from the first article that adds positions to our mesh using combined vertices for smooth shading or repeated vertices for flat shading:
public static void addPointCombined(Point3D point, MeshGeometry3D mesh, Vector3D normal)
{
bool found = false;
int i = 0;
foreach (Point3D p in mesh.Positions)
{
if (p.Equals(point))
{
found = true;
mesh.TriangleIndices.Add(i);
mesh.Positions.Add(point);
mesh.Normals.Add(normal);
break;
}
i++;
}
if (!found)
{
mesh.Positions.Add(point);
mesh.TriangleIndices.Add(mesh.TriangleIndices.Count);
mesh.Normals.Add(normal);
}
}
public static void addTriangleToMesh(Point3D p0, Point3D p1, Point3D p2,
MeshGeometry3D mesh, bool combine_vertices)
{
Vector3D normal = CalculateNormal(p0, p1, p2);
if (combine_vertices)
{
addPointCombined(p0, mesh, normal);
addPointCombined(p1, mesh, normal);
addPointCombined(p2, mesh, normal);
}
else
{
mesh.Positions.Add(p0);
mesh.Positions.Add(p1);
mesh.Positions.Add(p2);
mesh.TriangleIndices.Add(mesh.TriangleIndices.Count);
mesh.TriangleIndices.Add(mesh.TriangleIndices.Count);
mesh.TriangleIndices.Add(mesh.TriangleIndices.Count);
mesh.Normals.Add(normal);
mesh.Normals.Add(normal);
mesh.Normals.Add(normal);
}
}
You can see the effect this has by running the demo. In addition to the two spinning circles, we have two spinning cylinders that we add to our model. One has flat shading and the other one has smooth shading, so you can see the difference. Here is the cylinder creation code we use in our Loaded
event for our Viewport3D
back in the MainWindow.xaml.cs file:
Cylinder cylinder = new Cylinder(
new Point3D(0, WpfScene.sceneSize / 4, WpfScene.sceneSize / 5),
40, WpfScene.sceneSize / 8,
WpfScene.sceneSize / 8,
WpfScene.sceneSize / 6);
Cylinder cylinder2 = new Cylinder(
new Point3D(-WpfScene.sceneSize / 2, WpfScene.sceneSize / 4, 0),
40, WpfScene.sceneSize / 8,
WpfScene.sceneSize / 8,
WpfScene.sceneSize / 6);
GeometryModel3D cylinderModel = cylinder.CreateModel(Colors.AliceBlue, true, true);
GeometryModel3D cylinderModel2 = cylinder2.CreateModel(Colors.AliceBlue, true, false);
Model3DGroup groupScene = new Model3DGroup();
groupScene.Children.Add(cylinderModel);
groupScene.Children.Add(cylinderModel2);
We use the same method as before to set our circles and cylinders spinning:
public void turnModel(Point3D center, GeometryModel3D model,
double beginAngle, double endAngle, double seconds, bool forever)
{
Vector3D vector = new Vector3D(0, 1, 0);
AxisAngleRotation3D rotation = new AxisAngleRotation3D(vector, 0.0);
DoubleAnimation doubleAnimation = new DoubleAnimation
(beginAngle, endAngle, durationTS(seconds));
if (forever)
{
doubleAnimation.RepeatBehavior = RepeatBehavior.Forever;
}
doubleAnimation.BeginTime = durationTS(0.0);
RotateTransform3D rotateTransform = new RotateTransform3D(rotation, center);
model.Transform = rotateTransform;
rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, doubleAnimation);
}
So here is the entire Loaded
event for our viewport
, where we create our scene:
private void Viewport3D_Loaded(object sender, RoutedEventArgs e)
{
if (sender is Viewport3D)
{
Viewport3D viewport = (Viewport3D)sender;
Cylinder cylinder = new Cylinder(
new Point3D(0, WpfScene.sceneSize / 4, WpfScene.sceneSize / 5),
40, WpfScene.sceneSize / 8,
WpfScene.sceneSize / 8,
WpfScene.sceneSize / 6);
Cylinder cylinder2 = new Cylinder(
new Point3D(-WpfScene.sceneSize / 2, WpfScene.sceneSize / 4, 0),
40, WpfScene.sceneSize / 8,
WpfScene.sceneSize / 8,
WpfScene.sceneSize / 6);
WpfCircle circle = new WpfCircle(55,
new Point3D(-WpfScene.sceneSize / 2, WpfScene.sceneSize / 6,
WpfScene.sceneSize / 2), WpfScene.sceneSize / 15);
WpfCircle circle2 = new WpfCircle(55,
new Point3D(0, WpfScene.sceneSize / 6, WpfScene.sceneSize / 2),
WpfScene.sceneSize / 15);
GeometryModel3D cylinderModel =
cylinder.CreateModel(Colors.AliceBlue, true, true);
GeometryModel3D cylinderModel2 =
cylinder2.CreateModel(Colors.AliceBlue, true, false);
GeometryModel3D circleModel = circle.createModel(Colors.Aqua, false);
GeometryModel3D circleModel2 = circle2.createModelTwoSided(Colors.Aqua, false);
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(cylinderModel);
groupScene.Children.Add(cylinderModel2);
groupScene.Children.Add(circleModel);
groupScene.Children.Add(circleModel2);
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);
turnModel(cylinder.getCenter(), cylinderModel, 0, 360, 13, true);
turnModel(cylinder2.getCenter(), cylinderModel2, 0, 360, 13, true);
turnModel(circle.getCenter(), circleModel, 0, 360, 8, true);
turnModel(circle2.getCenter(), circleModel2, 0, 360, 8, true);
}
}
In this article, we added a couple more shapes that we need for our robot. We can now make triangles, rectangles, cubes, circles, and cylinders. We also demonstrated how to make two-sided models in WPF by using BackMaterial
and how to get smooth shading as opposed to flat shading by combining vertices.
In our next article, we will add the final shape classes we need for our robot, then we will show how to use animated transforms and storyboards to set the whole thing moving and bring the robot to life.
History
- 4th November, 2010: Initial version