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

一个用于可视化 3D 几何模型的工具(第一部分)

starIconstarIconstarIconstarIconstarIcon

5.00/5 (18投票s)

2009年10月11日

MIT

7分钟阅读

viewsIcon

77739

downloadIcon

3332

一篇关于使用 WPF 可视化 3D 几何模型的工具的文章

引言

在阅读Charles Petzold所著的《Windows 3D编程》一书时,我想我应该使用WPF 3D开发一些东西。我一直没有想法,直到有人问我是否能帮助解决一个3D几何问题:如果学生能够轻松地可视化几何模型中的点和线的定位,他们就能轻松地解决这个问题。

因此,我决定开发一个工具,允许用户:

  • 通过定义点和线来创建3D几何模型
  • 通过围绕3个轴旋转来检查模型
  • 修改模型,以及
  • 持久化模型以备后用

背景

该工具是使用Charles Petzold开发的Petzold.Media3D库开发的。使用这个库,您可以轻松地在3D空间中创建网格、线条、曲线、轴和文本。下图显示了此工具中使用的类。

WireBase类继承自ModelVisual3D,提供了在3D空间中绘制一条或多条线的基本功能。它注册了一些依赖属性,例如ColorThickness等。

Axes类派生自WireBase,在3D空间中绘制X、Y和Z轴。您可以通过Extent属性指定三个轴的范围,该属性适用于所有三个轴。原始类每单位(可以视为一英寸)绘制一个大刻度,每大刻度绘制10个小刻度。如果Extent很大,例如80英寸,那么轴上的刻度将过多,不仅使轴上的刻度和单位标签过于拥挤,而且还会影响性能。因此,我在该类中添加了两个新属性,UnitPerBigTickSmallTicksPerBigPick,您可以使用它们来指定每大刻度的英寸数,每大刻度的小刻度数。

WireLine类也派生自WireBase,是该工具的核心,用于在3D空间中绘制一条线。您可以通过指定两个端点来创建一条线。

另一个WireBase派生类WireText用于绘制3D几何模型中点的标签。

图1. 工具中使用的Petzold.Media3D类的关系

设计

该工具的设计可以分为4个部分:模型、视图、对话框和持久化机制。

我将分两部分介绍程序的設計:第一部分介紹模型、視圖和對話框,第二部分介紹持久化機制。

模型

G3DModel类表示一个3D几何模型,该模型包含G3DElement,可以是G3DPointG3DLine。您可以向模型添加/删除G3DPointG3DLine。您可以分别通过PointsLines属性检索所有G3DPointG3DLineExtent属性返回模型在X、Y或Z轴上的范围,以最大者为准。IsDirty属性指示模型自上次保存以来是否已更改。如果模型中没有点或线,IsEmpty属性将返回true

G3DModel类的CreateVisual3D()方法返回一个表示3D几何模型的ModelVisual3D,然后该模型可以被添加到Viewport3D以进行显示。

G3DElement类是G3DPointG3DLine的基类,分别表示一个点或一条线。每个G3DElement对象都有一个唯一的标识,由ID属性表示。G3DElement对象可以有一个Label,该标签将与点或线一起绘制。

G3DPoint类定义了3D空间中的一个点。类型为Point3DPosition属性指定了点的位置。

G3DLine类定义了一条线,以两个G3DPoint对象作为端点。G3DLine可以有一个颜色,默认值为Black。所有线的端点(G3DPoint对象)必须在作为线的端点使用之前,通过调用G3DModel的某个AddPoint()方法添加到模型中。

将来,我们可能会添加更多G3DElement派生类的类型,例如表示3D空间中向量的G3DVector

图2. G3DModel

以下代码显示了G3DModel类中CreateVisual3D方法的实现。

/// <summary>
/// Creates and returns a ModelVisual3D that represents the model.
/// </summary>
/// <param name="ratio">The ratio to be used to display the labels.
/// </param>
/// <returns>The ModelVisual3D that represents the model</returns>
public ModelVisual3D CreateVisual3D(double ratio)
{
    ModelVisual3D visual = new ModelVisual3D();

    AddLines(visual);
    AddPoints(visual, ratio);

    return visual;
}

/// <summary>
/// Add all lines of the model to the ModelVisual3D object.
/// </summary>
/// <param name="visual">The ModelVisual3D to which the lines are
/// added.</param>
private void AddLines(ModelVisual3D visual)
{
    foreach (var line in m_linesByName.Values)
    {
        var wl = new WireLine()
        {
            Point1 = line.StartPoint.Position,
            Point2 = line.EndPoint.Position,
            Color = line.Color,
            Thickness = LINE_THICKNESS
        };

        visual.Children.Add(wl);
    }
}

/// <summary>
/// Add points and labels to the ModelVisual3D object.
/// </summary>
/// <param name="visual">The ModelVisual3D to which the points
/// and labels are added</param>
/// <param name="ratio">The ratio to scale the labels</param>
private void AddPoints(ModelVisual3D visual, double ratio)
{
    foreach (var p in m_pointsByName.Values)
    {
        if (!IsPointUsedInLines(p))
        {
            var point = 1.0/96.0;

            var wl = new WireLine()
            {
                Point1 = p.Position,
                Point2 = p.Position + new Vector3D(point, point, point),
                Thickness = 2
            };

            visual.Children.Add(wl);
        }

        if (!string.IsNullOrEmpty(p.Label))
        {
            var wt = new WireText()
            {
                Origin = p.Position,
                Text = p.Label,
                FontSize = POINT_LABEL_SIZE * ratio,
                Thickness = 2
            };

            visual.Children.Add(wt);
        }
    }
}

/// <summary>
/// Check if the given point is used in any line.
/// </summary>
/// <param name="p">The point to be checked</param>
/// <returns>Returns true if the point is used in a line.</returns>
private bool IsPointUsedInLines(G3DPoint p)
{
    foreach (var l in m_lines)
    {
        if (l.StartPoint.ID == p.ID || l.EndPoint.ID == p.ID)
        {
            return true;
        }
    }

    return false;
}

View

视图由两个窗口组成:MainWindowControlPanelMainWindow显示G3DModel,而ControlPanel用于旋转模型和进行缩放。

MainWindow包含一个G3DViewport,该G3DViewport又包含一个ViewPort3DViewPort3D具有AmbientLightDirectionalLightPerspectiveCamera。三个RotateTransform3D对象被分配给PerspectiveCamera,以便围绕三个轴旋转相机。

图3. G3DViewport

ControlPanel有四个滑块。三个轴滑块分别用于围绕三个轴旋转相机。距离滑块通过改变相机到轴原点的距离来实现放大和缩小。

图4. ControlPanel

滑块的位置通过数据绑定绑定到Viewport3D的4个属性。轴滑块绑定到分配给相机的相应AxisAngleRotation3D对象的AngleProperty,范围从-180到180。当我们改变其中一个轴滑块的位置时,相机将围绕相应的轴旋转,从而使我们能够从不同角度查看模型。

距离滑块绑定到G3DViewportDistance属性,该属性在值改变时调整相机的Position。距离滑块的范围是从G3DViewportExtent的8倍到8分之1。

为了支持数据绑定,我们注册了一个名为DistanceProperty、类型为intDependencyProperty。当属性值改变时,会调用DistancePropertyChanged()方法,该方法会调整相机的位置,从而实现放大或缩小的效果。

public static readonly DependencyProperty DistanceProperty =
    DependencyProperty.Register("Distance",
    typeof(int),
    typeof(G3DViewport),
    new PropertyMetadata(0, DistancePropertyChanged));

public int Distance
{
    set { SetValue(DistanceProperty, value); }
    get { return (int)GetValue(DistanceProperty); }
}

protected static void DistancePropertyChanged
    (
    DependencyObject obj,
    DependencyPropertyChangedEventArgs args
    )
{
    if (obj != null)
    {
        (obj as G3DViewport).DistancePropertyChanged(args);
    }
}

protected void DistancePropertyChanged(DependencyPropertyChangedEventArgs args)
{
    var p = m_camera.Position;

    var currentDistance = Math.Sqrt(p.X * p.X + p.Y * p.Y + p.Z * p.Z);
    var ratio = Math.Sqrt(Distance / currentDistance);

    m_camera.Position = new Point3D(p.X * ratio, p.Y * ratio, p.Z * ratio);
}

ControlPanel的两个Bind()方法将DependencyObjectDependencyProperty绑定到滑块。

public void Bind(DependencyObject target, DependencyProperty property, SliderId id)
{
    if (target == null || property == null)
    {
        throw new ArgumentNullException
	("The target and property parameters should not be null");
    }

    switch (id)
    {
        case SliderId.AxisX:
            Bind(target, property, sliderX);
            break;

        case SliderId.AxisY:
            Bind(target, property, sliderY);
            break;

        case SliderId.AxisZ:
            Bind(target, property, sliderZ);
            break;

        case SliderId.Distance:
            Bind(target, property, sliderDistance);
            break;

        default:
            throw new ArgumentException("Invalid SliderId");
    }
}

private void Bind(DependencyObject target, DependencyProperty property, Slider slider)
{
    Binding binding = new Binding();
    binding.Source = slider; ;
    binding.Path = new PropertyPath("Value");
    binding.Mode = BindingMode.TwoWay;
    BindingOperations.SetBinding(target, property, binding);
}

请注意,绑定是双向的,因此属性值和滑块的位置将始终同步。

MainWindow.Window_Loaded()方法通过调用ControlPanelBind()方法将属性绑定到ControlPanel的滑块。

private void Window_Loaded(object sender, RoutedEventArgs e)
{
    …

    m_controlPanel.Bind(viewport.AxisX, 
	AxisAngleRotation3D.AngleProperty, ControlPanel.SliderId.AxisX);
    m_controlPanel.Bind(viewport.AxisY, 
	AxisAngleRotation3D.AngleProperty, ControlPanel.SliderId.AxisY);
    m_controlPanel.Bind(viewport.AxisZ, 
	AxisAngleRotation3D.AngleProperty, ControlPanel.SliderId.AxisZ);
    m_controlPanel.Bind(viewport, 
	G3DViewport.DistanceProperty, ControlPanel.SliderId.Distance);
}

对话框

有几个对话框允许您操作模型。

您可以通过AddPointDialog添加一个或多个点,方法是指定点的名称、坐标和/或标签。当您单击“添加”按钮时,该点将被添加到模型中。您可以使用同一个对话框连续添加新点,并在完成添加点后单击“关闭”按钮。

图5. 添加点对话框

您可以使用AddLineDialog向模型添加线,该对话框允许您选择线的两个端点,指定线的名称,或者选择线的颜色。该对话框列出了模型中的所有点供您选择端点。

图6. 添加线对话框

您可以使用EditModelDialog创建新模型或编辑现有模型,可以使用该对话框添加点和线,编辑值。

请注意,此对话框不是WPF窗口,而是Windows Forms Form,其中包含两个DataGridView控件。

图7. 编辑模型对话框

单元测试

单元测试使用NUnit Framework完成。我已经为一些模型类创建了单元测试,例如G3DPointG3DLineG3DModel。请参阅UnitTests项目中的文件G3DPointTester.csG3DLineTester.csG3DModelTester.cs。虽然本节篇幅很短,但这并不意味着单元测试不重要。相反,单元测试对于确保系统正常工作并使我们有勇气重构代码至关重要。

结论

在第一部分中,我们介绍了模型、视图和对话框的设计。正如您所见,使用Petzold.Media3D库绘制3D几何模型非常容易,而且我们只使用了该库的一小部分。我们可以向模型添加其他类型的视觉元素,例如曲线。

在第二部分,我将介绍该工具的持久化机制。

我在CodePlex上创建了一个项目。请从那里下载最新的代码。

历史

  • 2009年10月11日:初始帖子
© . All rights reserved.