WPF 中的行走机器人系列 -- 第一部分:三角形、矩形和立方体





5.00/5 (12投票s)
关于如何使用 C# 代码在 WPF 中制作动画 3D 机器人的系列的第一部分

引言
是的,这是 WPF 中的又一个旋转立方体示例。但这也是我系列文章的第一部分,该系列将介绍如何使用 C# 代码在 WPF 中制作一个动画行走机器人。此项目中使用的 XAML 非常少,因为我们正在使用 C# 代码对所有内容进行建模。在第一部分中,我们将介绍一些用于在 WPF 中构建形状的基本可重用类,并设置一个简单的动画场景。在未来的章节中,我们将讨论其他形状、背面材质、平滑着色和平面着色以及故事板。我们将使用的用于制作立方体的类将被组合起来制作我们的机器人,一个具有自己动作的简单角色。系列文章的每个章节都包含所有源代码。
我们首先在 Visual Studio 2010 中启动一个新项目。选择 WPF Application 作为项目类型。您需要将此 XAML 添加到 MainWindow
<ContentControl Name="contentControl2">
<Viewport3D ClipToBounds="True" Width="Auto" Height="Auto">
</Viewport3D>
</ContentControl>
您需要在 MainWindow.xaml.cs 中添加几个 using
语句
using System.Windows.Media.Animation;
using System.Windows.Media.Media3D;
我们需要一些基本类来完成立方体建模工作。您知道 WPF 中的 3D 几何模型由三角形网格构成。因此,我们需要一个 C# 类来构建一个三角形。
向您的项目添加一个类并开始添加一些变量。显然,我们需要 3 个点和一个构造函数
private Point3D p1;
private Point3D p2;
private Point3D p3;
public WpfTriangle(Point3D P1, Point3D P2, Point3D P3)
{
p1 = P1;
p2 = P2;
p3 = P3;
}
我们需要一些代码将我们的点添加到 GeometryMesh3D
,以便它们可以用于模型。由于我们将把来自不同形状类的 triangle
s 组合在一起以创建一个单独的模型,因此我们将传入网格变量并将我们的三角形点添加到其中
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);
}
}
我们将此函数设为 static
,因为我们通常只将 triangle
作为一个更大的形状的一部分来添加。我们很少会单独构建一个 triangle
模型。我们将使用数百个 triangle
s,所以我们不想仅仅为了将一个 triangle
添加到网格而实例化一个 triangle
类。
您会注意到我们包含了一个名为 combine_vertices
的参数。这非常重要,因为 WPF 中的平面着色和光滑着色之间存在差异。对于我们的立方体模型,我们希望使用平面着色。如果您为三角形网格组合顶点,它将导致使用 Gouraud
方法进行平滑着色。稍后,我们将展示这如何适用于圆柱体等形状,我们可能希望使用平滑着色。现在,我们将为我们的三角形类添加组合顶点的功能,即使它不会用于立方体示例
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);
}
}
当组合三角形网格中的点时,每个唯一的点 (Position
) 在网格中只列出一次,即使它可能属于多个三角形。单独的列表 (TriangleIndices
) 用于存储构成每个 triangle
的每个点的索引。还需要第三个列表为每个三角形提供法线。这对于计算光源与每个 triangle
表面之间的入射角是必需的。WPF 就是这样进行平面着色的。它使用 Gouraud 插值来平滑着色,但前提是您组合了相同的点。如果您单独列出每个点,则结果将是平面着色。这正是我们想要的立方体。
法线使用简单的叉积计算。WPF 为此提供了一个内置的 Vector 成员函数。这是我们计算 triangle
法线的成员函数
public static Vector3D CalculateNormal(Point3D P0, Point3D P1, Point3D P2)
{
Vector3D v0 = new Vector3D(P1.X - P0.X, P1.Y - P0.Y, P1.Z - P0.Z);
Vector3D v1 = new Vector3D(P2.X - P1.X, P2.Y - P1.Y, P2.Z - P1.Z);
return Vector3D.CrossProduct(v0, v1);
}
这样,我们就可以在构建网格时填充 Normals 列表。
我们添加了另一个成员函数来构建一个 triangle GeometryModel3D
,以防万一我们想这样做。此项目不需要它,但我还是将其包含在内。它将颜色作为参数,并使用该纯色的 DiffuseMaterial
构建模型
public static GeometryModel3D CreateTriangleModel
(Point3D P0, Point3D P1, Point3D P2, Color color)
{
MeshGeometry3D mesh = new MeshGeometry3D();
addTriangleToMesh(P0, P1, P2, mesh);
Material material = new DiffuseMaterial(new SolidColorBrush(color));
GeometryModel3D model = new GeometryModel3D(mesh, material);
return model;
}
现在,由于我们要创建一个立方体,我们需要一个 rectangle
类来使用立方体的每个侧面。方便的是,一个 rectangle
可以由两个 triangle
s 组成。我们只需要 4 个点来指定我们的 rectangle
private Point3D p0;
private Point3D p1;
private Point3D p2;
private Point3D p3;
public WpfRectangle(Point3D P0, Point3D P1, Point3D P2, Point3D P3)
{
p0 = P0;
p1 = P1;
p2 = P2;
p3 = P3;
}
与 triangle
一样,我们希望将 rectangle
添加到我们正在构建的 MeshGeometry3D
中。用于此目的的成员函数看起来相似,并且建立在我们为三角形创建的基础上
public static void addRectangleToMesh
(Point3D p0, Point3D p1, Point3D p2, Point3D p3, MeshGeometry3D mesh)
{
WpfTriangle.addTriangleToMesh(p0, p1, p2, mesh);
WpfTriangle.addTriangleToMesh(p2, p3, p0, mesh);
}
与 triangle
一样,我们并不总是希望单独实例化一个 rectangle
,因此我们将此成员函数设为 static
并在每次调用时传递点。调用 WpfTriangle.addTriangleToMesh
时的点顺序将确保每个 triangle
具有相同的缠绕方向。这很重要,因为默认情况下,WPF 中的形状只有一面,从另一面看是不可见的。这由每个 triangle
中点的缠绕方向或顺序决定,因此保持模型中所有三角形的一致性很重要。
当有一个 rectangle
实例并想将其添加到网格而不指定点时,我们添加了另一个成员函数,让它使用其成员点
public void addToMesh(MeshGeometry3D mesh)
{
WpfTriangle.addTriangleToMesh(p0, p1, p2, mesh);
WpfTriangle.addTriangleToMesh(p2, p3, p0, mesh);
}
triangle
和 rectangle
类的完整代码可在本文的 zip 文件中找到。让我们继续处理立方体。仅需要 4 个成员变量即可完全描述我们的立方体
private Point3D origin;
private double width;
private double height;
private double depth;
public WpfCube(Point3D P0, double w, double h, double d)
{
width = w;
height = h;
depth = d;
origin = P0;
}
我们提供了一个将立方体添加到网格的成员函数。为了方便起见,它是一个 static
函数,它将构建一个立方体然后将其添加到网格中。它通过为每个侧面构建一个 rectangle
然后将它们添加到网格中来实现
public static void addCubeToMesh(Point3D p0, double w, double h, double d,
MeshGeometry3D mesh)
{
WpfCube cube = new WpfCube(p0, w, h, d);
double maxDimension = Math.Max(d, Math.Max(w, h));
WpfRectangle front = cube.Front();
WpfRectangle back = cube.Back();
WpfRectangle right = cube.Right();
WpfRectangle left = cube.Left();
WpfRectangle top = cube.Top();
WpfRectangle bottom = cube.Bottom();
front.addToMesh(mesh);
back.addToMesh(mesh);
right.addToMesh(mesh);
left.addToMesh(mesh);
top.addToMesh(mesh);
bottom.addToMesh(mesh);
}
我们还提供了一个用于立方体实例的版本。它通过调用 static
版本来完成工作
public GeometryModel3D CreateModel(Color color)
{
return CreateCubeModel(origin, width, height, depth, color);
}
立方体具有可以调用的成员函数,以根据其宽度、高度、深度和原点来制作每个组件矩形
public WpfRectangle Front()
{
WpfRectangle r = new WpfRectangle(origin, width, height, 0);
return r;
}
public WpfRectangle Back()
{
WpfRectangle r = new WpfRectangle(new Point3D
(origin.X + width, origin.Y, origin.Z + depth), -width, height, 0);
return r;
}
public WpfRectangle Left()
{
WpfRectangle r = new WpfRectangle(new Point3D(origin.X, origin.Y, origin.Z + depth),
0, height, -depth);
return r;
}
public WpfRectangle Right()
{
WpfRectangle r = new WpfRectangle(new Point3D(origin.X + width, origin.Y, origin.Z),
0, height, depth);
return r;
}
public WpfRectangle Top()
{
WpfRectangle r = new WpfRectangle(origin, width, 0, depth);
return r;
}
public WpfRectangle Bottom()
{
WpfRectangle r = new WpfRectangle
(new Point3D(origin.X + width, origin.Y - height, origin.Z),
-width, 0, depth);
return r;
}
这 6 个成员函数在将立方体添加到我们的 MeshGeometry3D
时被调用。立方体的完整代码可以在此示例的 zip 文件中找到。
这些是我们所需的所有类。我们现在只需要在我们的 MainWindow.xaml.cs 中添加一些代码来创建我们的立方体并创建一个场景。
private double sceneSize = 10;
Point3D lookat = new Point3D(0, 0, 0);
上面的变量定义了场景坐标的任意大小和摄像机的观察点。
右键单击您在设计器视图中添加的 Viewport3D
并添加一个 Loaded
事件。请参阅注释以了解其作用的解释
private void Viewport3D_Loaded(object sender, RoutedEventArgs e)
{
if (sender is Viewport3D)
{
Viewport3D viewport = (Viewport3D)sender;
// create a cube with dimensions as some fraction of the scene size
WpfCube cube = new WpfCube(new System.Windows.Media.Media3D.Point3D
(0, 0, 0), sceneSize / 6, sceneSize / 6, sceneSize / 6);
// construct our geometry model from the cube object
GeometryModel3D cubeModel = cube.CreateModel(Colors.Aquamarine);
// create a model group to hold our model
Model3DGroup groupScene = new Model3DGroup();
// add our cube to the model group
groupScene.Children.Add(cubeModel);
// add a directional light
groupScene.Children.Add(positionLight
(new Point3D(-sceneSize, sceneSize / 2, 0.0)));
// add ambient lighting
groupScene.Children.Add(new AmbientLight(Colors.Gray));
// add a camera
viewport.Camera = camera();
// create a visual model that we can add to our viewport
ModelVisual3D visual = new ModelVisual3D();
// populate the visual with the geometry model we made
visual.Content = groupScene;
// add the visual to our viewport
viewport.Children.Add(visual);
// animate the model
turnModel(cube.center(), cubeModel, 0, 360, 3, true);
}
}
我们需要一些帮助函数在我们的窗口中来完成我们上面列出的一些工作。这些函数创建了我们的灯光和摄像机。它们还利用了我们之前定义的 sceneSize
变量,因此如果您想更改坐标基准,所有内容仍然可以协同工作
public PerspectiveCamera camera()
{
PerspectiveCamera perspectiveCamera = new PerspectiveCamera();
perspectiveCamera.Position = new Point3D(-sceneSize, sceneSize / 2, 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;
}
public DirectionalLight positionLight(Point3D position)
{
DirectionalLight directionalLight = new DirectionalLight();
directionalLight.Color = Colors.Gray;
directionalLight.Direction = new Point3D(0, 0, 0) - position;
return directionalLight;
}
这是动画化我们立方体的辅助函数。您可以阅读注释以了解其作用
public void turnModel(Point3D center, GeometryModel3D model,
double beginAngle, double endAngle, double seconds, bool forever)
{
// vectors serve as 2 axes to turn our model
Vector3D vector = new Vector3D(0, 1, 0);
Vector3D vector2 = new Vector3D(1, 0, 0);
// create rotations to use. We can set a 0.0 degrees
// for our rotations since we are going to animate them
AxisAngleRotation3D rotation = new AxisAngleRotation3D(vector, 0.0);
AxisAngleRotation3D rotation2 = new AxisAngleRotation3D(vector2, 0.0);
// create double animations to animate each of our rotations
DoubleAnimation doubleAnimation =
new DoubleAnimation(beginAngle, endAngle, durationTS(seconds));
DoubleAnimation doubleAnimation2 =
new DoubleAnimation(beginAngle, endAngle, durationTS(seconds));
// set the repeat behavior and duration for our animations
if (forever)
{
doubleAnimation.RepeatBehavior = RepeatBehavior.Forever;
doubleAnimation2.RepeatBehavior = RepeatBehavior.Forever;
}
doubleAnimation.BeginTime = durationTS(0.0);
doubleAnimation2.BeginTime = durationTS(0.0);
// create 2 rotate transforms to apply to our model.
// Each needs a rotation and a center point
RotateTransform3D rotateTransform = new RotateTransform3D(rotation, center);
RotateTransform3D rotateTransform2 = new RotateTransform3D(rotation2, center);
// create a transform group to hold our 2 transforms
Transform3DGroup transformGroup = new Transform3DGroup();
transformGroup.Children.Add(rotateTransform);
transformGroup.Children.Add(rotateTransform2);
// set our model transform to the transform group
model.Transform = transformGroup;
// begin the animations -- specify a target object and
// property for each animation -- in this case,
// the targets are the two rotations we created and
// we are animating the angle property for each one
rotation.BeginAnimation(AxisAngleRotation3D.AngleProperty, doubleAnimation);
rotation2.BeginAnimation(AxisAngleRotation3D.AngleProperty, doubleAnimation2);
}
我们只需要另外几个辅助函数,可以帮助我们以秒为单位指定动画持续时间,并将它们转换为 WPF 可以使用的时间跨度
private int durationM(double seconds)
{
int milliseconds = (int)(seconds * 1000);
return milliseconds;
}
public TimeSpan durationTS(double seconds)
{
TimeSpan ts = new TimeSpan(0, 0, 0, 0, durationM(seconds));
return ts;
}
这就是我们简单的旋转立方体。我们创建的三角形和其他类将有助于构建我们带有行走机器人角色的简单场景。动画概念稍后在使机器人的手臂和腿部移动时也很有用。在本系列的下一篇文章中,我们将添加一个 cylinder
类并演示平面着色和光滑着色之间的区别。
历史
- 2010 年 11 月 1 日:初始版本