WFTools3D:用于轻松构建3D模拟的WPF小型库





5.00/5 (25投票s)
用于Windows Presentation Foundation (WPF)的3D工具。
引言
Windows Presentation Foundation (WPF) 附带一个易于使用的 3D 框架,它本质上是 DirectX 的包装器。虽然性能不如直接使用 Direct3D 高,但对于简单场景来说已经足够了,而且玩起来也很有趣。
WFTools3D 库让使用 WPF 3D 变得更加简单和有趣。
我多年前启动这个项目是为了在为不同类型的模拟构建 3D 场景时,避免一遍又一遍地编写相同的代码。例如,在创建太阳系模拟时,设置整个场景(包括太阳和行星)不应超过几行代码。场景中对象的位置和旋转状态应该可以通过对象的简单属性访问,最后但同样重要的是,我希望能够像在飞行模拟器中环游世界一样在我的场景中飞行。我喜欢飞行模拟器!
背景
Microsoft 的 WPF 3D 图形概述以及高级操作指南主题包含了在 Windows 应用程序中嵌入 3D 图形所需的所有信息。您所需要的就是一个 Viewport3D
、一个或多个 ModelVisual3D
、一个摄像机和一些灯光。到目前为止,一切都很好。但是,直到第一个三角形出现在屏幕上,需要相当长的时间!WFTools3D
库有助于缩短这个时间,并允许在几分钟内构建出复杂的场景,例如移动的汽车和飞行的飞机。场景是交互式的,也就是说,您可以使用键盘和鼠标移动和旋转内置摄像机。此外,摄像机可以自行移动,这意味着它们可以飞行,您可以像在飞行模拟器中一样改变它们的速度以及偏航、俯仰和滚转角。
Using the Code
为了快速演示,我们将构建一个太阳-地球-月球系统的模拟。这比您也应该下载的演示项目要简单一些,但也许稍后再进行。现在,只需使用 Visual Studio 2010 或更高版本创建一个名为“Demo”的 WPF 应用程序。.NET Framework 版本需要至少为 4。如果项目已创建,请添加对 WFTools3D.dll 的引用。在 MainWindow.xaml 中,将 Grid
元素替换为 WFTools3D.Scene3D
元素,如下所示
<Window x:Class="Demo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wft3d="WFTools3D"
Title="MainWindow" WindowState="Maximized"
FocusManager.FocusedElement="{Binding ElementName=scene}">
<wft3d:Scene3D x:Name="scene"/>
</Window>
请注意,我最大化了窗口并将逻辑焦点设置到场景。现在将 MainWindow.xaml.cs 中的代码替换为以下内容
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using WFTools3D;
namespace Demo
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
scene.Models.Add(new AxisModel(10));
Sphere sun = new Sphere(32);
sun.DiffuseMaterial.Brush = Brushes.Goldenrod;
scene.Models.Add(sun);
scene.Camera.Position = new Point3D(25, -15, 8);
scene.Camera.LookAtOrigin();
}
}
}
现在够了!虽然我们的迷你太阳系尚未完成,但我们可以构建并启动应用程序。您将在黑色屏幕中央看到一个黄色球体,从中伸出红色、绿色和蓝色的线。这些线显示了我们世界的 x、y 和 z 坐标轴。X 是红色,Y 是绿色,Z 是蓝色(或者 (r,g,b) 代表 (x,y,z))。在讨论代码之前,请尝试以下操作
按下鼠标左键并移动鼠标。看起来底层的世界正在旋转。鼠标的左右移动显然使世界绕其 Z 轴旋转,而上下移动似乎使世界绕屏幕中央的水平轴旋转。实际上,真正发生的是摄像机被移动到一个新位置并指向原点。
方向键和 PgUp、PgDn 将旋转相机,但不会移动它。移动操作通过 Ctrl 加上前面的键完成。
要开始飞行,请按“W”。每次按“W”都会增加速度,“S”会降低速度,“X”会停止移动。在飞行模式下(speed != 0
),拖动鼠标左键会改变飞行方向。如果您在太空中迷失了方向,请按空格键。这会将相机转回原点。
但现在我们来谈谈代码
scene.Models.Add(new AxisModel(10));
类 Scene3D
有一个 Models
属性,用于使用 3D 对象填充场景。添加的第一个对象是坐标轴模型,这在您开始构建场景时很有帮助。坐标轴的长度将为 10,您可能会问“10 什么?码、米、像素?”答案是“这不重要。”它只是 10 个单位,我将用大小和位置适合 10x10x10 立方体的对象构建场景。任何其他数字,如 1、1000 或 42 也可以。您只需根据这个数字来调整模型(和相机)的大小和位置。
Sphere sun = new Sphere(32);
sun.DiffuseMaterial.Brush = Brushes.Goldenrod;
scene.Models.Add(sun);
这会向场景中添加一个黄色球体。类 Sphere
是一个 Primitive3D
,它是一个 Object3D
,又是一个 ModelVisual3D
,后者是 3D 场景中对象的 WPF 基类。类 Object3D
使缩放、旋转和定位对象变得容易。当然,这也可以通过底层的 ModelVisual3D
类来完成,但是 Object3D
具有 Position
、ScaleX
或 Rotation1
等属性,这些属性更方便地修改基类的 Transform
属性。
类 Primitive3D
向对象添加网格和材质。网格描述了对象的表面,由许多(有时是很多很多)三角形组成。材质(有正面和背面材质)指定了表面的外观。Material
属性(代表正面材质)实际上是一组材质。您可以通过 DiffuseMaterial
、SpecularMaterial
和 EmissiveMaterial
属性访问各个项目。如果对象是闭合的,您无需关心 BackMaterial
,否则如果对象从所有侧面看起来都一样,您可能希望将 BackMaterial
设置为 Material
。
球体是一个封闭的物体,所以我们只需要处理正面材质。我们可以通过设置其 DiffuseMaterial
的 Brush
来赋予它任何颜色。除了 SolidColorBrush
,我们还可以使用 ImageBrush
,这当然更具吸引力,并且在演示项目中已经实现。每个 Primitive3D
都有一个构造函数,它接受一个名为“divisions
”的整数值,该值决定用于创建网格的三角形数量。如果您使用“1
”而不是“32
”,球体将看起来像一个双金字塔,因为三角形的总数只有 8
。
scene.Camera.Position = new Point3D(25, -15, 8);
scene.Camera.LookAtOrigin();
这是我们六行场景设置的最后一步:我们必须将摄像机设置到某个位置,并使其看向某个点(现在是坐标系的原点)。类 Scene3D
有三个透视摄像机,scene.Camera
指的是活动摄像机。要激活另一个摄像机,请使用 scene.ActivateCamera(int index)
。
场景由一个默认照明模型照亮,该模型可通过属性 Scene3D.Lighting
访问。有两束方向光,名为 DirectionalLight1
和 DirectionalLight2
,以及一束环境光,名为 AmbientLight
。默认模型对于我的大多数场景都适用。
但地球在哪里?
的确,我们的太阳至少需要一个伴星!所以让我们为场景添加一个地球。代码与添加太阳的代码非常相似。在太阳被添加到场景之后,立即添加以下代码
Sphere earth = new Sphere(24) { Radius = 0.5, Position = new Point3D(9, 0, 0) };
earth.DiffuseMaterial.Brush = Brushes.Blue;
scene.Models.Add(earth);
唯一的真正区别在于它的半径和位置。默认位置(0,0,0)和默认半径 1 对太阳来说很好,但对于地球,我们选择了不同的值。如果您构建并运行应用程序,现在您将看到地球位于红色 x 轴的末端附近。
那月亮呢?
对于月球,我们采取一种略有不同的方法
Sphere moon = new Sphere { Radius = 0.3, Position = new Point3D(2, 0, 0) };
moon.DiffuseMaterial.Brush = Brushes.NavajoWhite;
earth.Children.Add(moon);
这里重要的是,月亮没有被添加到场景的模型中,而是添加到地球的子级中(它也是一个模型集合)。这样做的原因是,我们希望地球和月亮保持在一起。它们构成了自己的系统。每当地球被定位到新位置时,我们都希望月亮跟随地球。如果我们决定移除地球,它的月亮也应该随之消失。这正是 Children
属性的意义所在。构建并运行应用程序,您会看到现在 x 轴末端有一个月亮。
为什么它会出现在 x = 10
处?地球位于 x = 9
,月球的 x
位置是 2
。那么 10
是从何而来的?
原因是,既然月球是地球的子物体,它的坐标系就以地球为中心。所以如果我们把月球放在(0,0,0),它就会被放置在地球的正中央(因为月球比地球小,所以我们就看不到了)。为了得到月球的全局位置,我们必须把它的相对位置(2,0,0)加到它父物体的位置(9,0,0)。但那会得到(11,0,0)——而月球绝对是位于(10,0,0)!那么这是怎么回事?
那是因为我们给了地球一个 0.5 的半径!通过缩放 Object3D
,并设置球体的半径与设置其 ScaleX
、ScaleY
和 ScaleZ
为相同值一样,我们不仅缩放了对象的几何形状,还缩放了该对象的整个世界。甚至包括它的坐标系!所以地球系统中的一切都缩小了一半。这就是为什么地球系统中距离为 2,在地球的父系统(现在是全局系统)中只意味着 1。这也是为什么月球位于 x = 10
的原因。
它还在移动!
根据伽利略·伽利雷的说法,地球绕着太阳转,月亮绕着地球转。我们没有理由不相信这位智者,所以让我们添加几行代码来展示某种运动。这不是真正的运动,但无论如何看起来很有趣。在窗口构造函数的末尾,添加这两行代码
scene.TimerTicked += TimerTicked;
scene.StartTimer();
此外,为 TimerTicked
事件添加一个方法
void TimerTicked(object sender, EventArgs e)
{
angle += 2;
Object3D earth = scene.Models[2] as Object3D;
earth.Rotation1 = Math3D.RotationZ(angle);
earth.Rotation3 = Math3D.RotationZ(angle * 0.1);
}
double angle;
以上代码在场景中启动了一个 DispatcherTimer
,它每 30 毫秒触发一次。当这种情况发生时,我们的 TimerTicked()
方法会被调用,它只是设置地球的两个 Rotation
属性。Rotation1
和未使用的 Rotation2
是相对于地球中心应用的,而 Rotation3
使用父坐标系的原点作为旋转中心。所以第一次旋转使地球绕自身运动,而第二次旋转使地球绕太阳运动(速度较慢)。
既然月亮是地球的孩子,无论地球做什么,月亮都会跟着。所以如果地球绕着自身旋转,月亮也会绕着地球中心旋转。当然,月亮也会跟着地球母亲绕着太阳运行!
遨游太空!
还记得“W”、“S”和“X”键吗?用“W”加速,然后尝试跟随地球!如果太快,按“S”减速,或按“X”完全停止。你会发现飞行一个既定航线真的很难!顺便说一下,按两下“H”。第一次按键会显示摄像机,第二次按键会显示一种特殊的姿态指引仪(ADI),它有助于在飞行时保持方向。带红球的杆子始终指向原点。
摄像机以飞机的形式可视化。环顾四周,你会发现其中两架。你不会发现另一架飞机,因为它属于你当前正在查看的摄像机!飞机的机头显示了摄像机的观察方向,而其垂直安定面显示了摄像机的向上方向。你可以用键“1”、“2”和“3”切换活动摄像机。
为了完成这个演示,我们将以一种有趣的方式设置摄像机:第一个像以前一样固定,第二个绕太阳轨道运行,第三个跟随地球。MainWindow
的完整代码现在看起来像这样
using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using WFTools3D;
namespace Demo
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
scene.Models.Add(new AxisModel(10));
Sphere sun = new Sphere(32);
sun.DiffuseMaterial.Brush = Brushes.Goldenrod;
scene.Models.Add(sun);
Sphere earth = new Sphere(24) { Radius = 0.5, Position = new Point3D(9, 0, 0) };
earth.DiffuseMaterial.Brush = Brushes.Blue;
scene.Models.Add(earth);
Sphere moon = new Sphere { Radius = 0.3, Position = new Point3D(2, 0, 0) };
moon.DiffuseMaterial.Brush = Brushes.NavajoWhite;
earth.Children.Add(moon);
scene.ActivateCamera(2);
scene.Camera.Position = new Point3D(9, 0, 0.1);
scene.Camera.LookDirection = Math3D.UnitY;
scene.Camera.UpDirection = Math3D.UnitZ;
scene.Camera.Rotate(Math3D.UnitZ, -30);
scene.Camera.ChangeRoll(-12);
scene.Camera.Speed = 8;
scene.ActivateCamera(1);
scene.Camera.Position = new Point3D(0, 4, 0);
scene.Camera.LookDirection = Math3D.UnitX;
scene.Camera.UpDirection = Math3D.UnitZ;
scene.Camera.ChangeRoll(25);
scene.Camera.Speed = 8;
scene.ActivateCamera(0);
scene.Camera.Position = new Point3D(25, -15, 8);
scene.Camera.LookAtOrigin();
scene.ToggleHelperModels();
scene.TimerTicked += TimerTicked;
scene.StartTimer();
}
void TimerTicked(object sender, EventArgs e)
{
angle += 2;
Object3D earth = scene.Models[2] as Object3D;
earth.Rotation1 = Math3D.RotationZ(angle);
earth.Rotation3 = Math3D.RotationZ(angle * 0.1);
}
double angle;
}
}
您会注意到,飞机无需按下“H”键即可见。这是通过调用 scene.ToggleHelperModels()
完成的。再次按下“H”可额外显示 ADI,再次按下可移除飞机和 ADI。以下是所有键盘/鼠标命令的列表
- 1, 2, 3:激活摄像机 1, 2 或 3
- W, S:增加/减少速度
- X:速度设为 0
- T:向后转
- 空格:转向原点
- H:切换飞机和 ADI
- 鼠标滚轮:增加/减少视野
如果相机速度为 0
- LMB:绕原点旋转场景
- Ctrl+LMB:绕接触点旋转场景
- 箭头:改变视角方向
- PgUp, PgDn:改变滚转角
- Ctrl+箭头:左右、前后移动相机
- Ctrl+PgUp, PgDn:向上、向下移动相机
- Shift:增加以上所有移动步长
如果相机速度不为 0,即处于飞行模式
- LMB,箭头:改变俯仰角和滚转角
- Ctrl+LMB:改变视角方向
- A, D:标准左/右转弯
- F:与地面平行飞行
就是这样!
希望您喜欢这次旅程。如果您想看更多示例,请查看我在 github 上的仓库。项目 EquationOfTime
有一个非常真实的日地模拟,解释了日出日落时间的一个令人惊讶的事实,而项目 DoublePendulum
检查了双摆及其混沌行为。
历史
- 2016年3月19日:首次上传