探索WPF 3D Dome Creator的构建之旅

最近,在进行一个LEGO项目时,需要构建一个由LEGO砖块组成的穹顶。尝试了几个LEGO CAD应用程序后,仍然难以构建穹顶。然后,偶然发现了一个名为Arthur Gugick的人开发的LEGO穹顶创建器应用程序。这个应用程序是用VB6开发的,存在一些bug,比如一旦最小化,表单就会被清除。这个应用程序提供了穹顶的高度和每个螺柱的位置,这也是WPF 3D Dome Creator的工作原理。然而,它是完全2D的,无法直观地看到穹顶的外观。因此,联系了Arthur,询问是否可以提供源代码。他非常乐意提供,将其作为WPF 3D Dome Creator的基础。

LDrawPartLib是为类似于MLCad的LEGO CAD应用程序开发的库,目前仍在开发中,但库已经完成。LDrawPartLib使用LDraw零件库进行零件几何建模。LDraw是LEGO CAD应用程序的开源标准。他们有一个非常活跃的社区,并定期更新库中的新零件。在这里讨论LDrawPartLib可能超出了本文的范围。然而,如果对LDrawPartLib如何解析和创建3D LEGO零件感兴趣,将不得不查看代码,代码中有相当多的注释和LDraw的文档。当然,如果有非常具体的问题,可以随时联系。

关于穹顶创建器,使用这个应用程序作为学习WPF、3D和著名的MVVM的方式。这个应用程序是对MVVM和WPF实现的理解。对评论和改进这个应用程序的方式持开放态度。由于这个应用程序只使用一个零件,所有所需的文件都放在了“Parts”文件夹中。如果想使用LDraw零件库,需要从这里下载,并将路径放在App.Config中。另外,ldconfig.ldr,作为LDraw的一部分,需要放在应用程序路径中,因为它包含了零件颜色的所有详细信息。

应用程序本身非常简单易用。选择想要构建的穹顶类型,设置各种参数,如穹顶的直径和高度,高度只在穹顶类型不是球形时才考虑。可以通过点击颜色调色板来改变穹顶的颜色。

首先,它使用了WPF 3D。使用WPF 3D是因为想知道它的能力。这是一个很棒的3D框架,但严重怀疑这将用于开发游戏。有许多缺点,首先,框架没有内置支持创建基本图形,如立方体、球体等,其次,正如痛苦地学到的,在一个3D画布上画线可能是一个真正的痛苦。然而,后来发现如何使用3DTools和Charles Petzold的库来画线。他也写了一本关于WPF 3D的优秀书籍,名为《Windows 3D编程》。前者在实现中有bug,更多信息请参见这里,所以没有使用3DTools的线条实现。然而,3DTools的TrackBall实现是极好的。第二个有一些优秀的特性,但随着3D场景中线条零件数量的增加,性能显著下降。如果想看到线条的零件,取消注释以下行:

public class Dome3DViewModel : DomeViewModel { ... public void CalculateDomeValues() { ... int y = (currentHeight - noOfPlates) * (int)ScaleHeight; for (int i = 0; i < noOfPlates; i++, y += (int)ScaleHeight) { Point3D pt = new Point3D(x * ScaleWidth, y, z * ScaleWidth); PartColors color = (PartColors)Enum.Parse(typeof(PartColors), MainWindowVM.ColorChooserVM.CurrentColor.Name); Part3D part = new Part3D(PLATE_PART_CODE, PLATE_PART_CODE, color) { Position = pt }; // part.ShowLines = true; Viewport.Children.Add(part); } ... } }

关于WPF 3D有一个极好的教程在这里。

其次,正如之前提到的,Arthur的程序给出了在特定位置的LEGO板的高度。可以从“地面”插入确切数量的板,但这将创建一个实心穹顶。因此,决定只添加“可见”的板。这是通过找到其在网格中的八个邻居中哪一个有最低的高度,然后只添加最低和当前高度之间的差值来实现的。为了实现这一点,使用了以下代码:

List neighbours = new List(); ... // Fill all the values from the surrounding neighbours. FillNeighbours(diameter, z, drCurrent, neighbours); if (drPrev != null) FillNeighbours(dt.Rows.Count, z, drPrev, neighbours); if (drNext != null) FillNeighbours(dt.Rows.Count, z, drNext, neighbours); // Gets the lowest values on the top. neighbours.Sort(); // no of viewable plates. if (drNext == null || drPrev == null || z == 0 || z == (diameter - 1)) noOfPlates = currentHeight; // if dome edges then show all the plates. else if (currentHeight == neighbours[0] && currentHeight > 0) noOfPlates = 1; // If lowest neighbours is of the current height // then add only one plate. else noOfPlates = currentHeight - neighbours[0];

第三,想从其他视图模型中访问一个视图模型。为了实现这一点,创建了一个名为ApplicationViewModel的基类。

public abstract class ApplicationViewModel : ViewModelBase { #region ctor protected ApplicationViewModel(MainWindowViewModel mainWindowModel) { MainWindowVM = mainWindowModel; } #endregion #region Public Properties public MainWindowViewModel MainWindowVM { get; set; } #endregion }

这个类的构造函数接受MainWindowViewModel作为参数。所有需要在应用程序中相互通信的视图模型都从ApplicationViewModel派生。

public class ParametersViewModel : ApplicationViewModel { #region ctor public ParametersViewModel(MainWindowViewModel mainWindowModel) : base(mainWindowModel) { DomeDiameter = 12; DomeHeight = 16; } #endregion ... }

MainWindowViewModel为这些模型中的每一个都有属性。

public class MainWindowViewModel : ViewModelBase { #region ctor public MainWindowViewModel() { ParametersVM = new ParametersViewModel(this); Dome2DVM = new Dome2DViewModel(this); Dome3DVM = new Dome3DViewModel(this); ColorChooserVM = new ColorChooserViewModel(this); } #endregion #region Public Properties public ParametersViewModel ParametersVM { get; set; } public Dome2DViewModel Dome2DVM { get; set; } public Dome3DViewModel Dome3DVM { get; set; } public ColorChooserViewModel ColorChooserVM { get; set; } #endregion }

作为一个学习者,很想知道对这种应用程序开发方式的看法。如果批评是建设性的,那就太好了。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485