65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (25投票s)

2016年3月21日

GPL3

11分钟阅读

viewsIcon

41785

downloadIcon

2370

用于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 具有 PositionScaleXRotation1 等属性,这些属性更方便地修改基类的 Transform 属性。

Primitive3D 向对象添加网格和材质。网格描述了对象的表面,由许多(有时是很多很多)三角形组成。材质(有正面和背面材质)指定了表面的外观。Material 属性(代表正面材质)实际上是一组材质。您可以通过 DiffuseMaterialSpecularMaterialEmissiveMaterial 属性访问各个项目。如果对象是闭合的,您无需关心 BackMaterial,否则如果对象从所有侧面看起来都一样,您可能希望将 BackMaterial 设置为 Material

球体是一个封闭的物体,所以我们只需要处理正面材质。我们可以通过设置其 DiffuseMaterialBrush 来赋予它任何颜色。除了 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 访问。有两束方向光,名为 DirectionalLight1DirectionalLight2,以及一束环境光,名为 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,并设置球体的半径与设置其 ScaleXScaleYScaleZ 为相同值一样,我们不仅缩放了对象的几何形状,还缩放了该对象的整个世界。甚至包括它的坐标系!所以地球系统中的一切都缩小了一半。这就是为什么地球系统中距离为 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日:首次上传
© . All rights reserved.