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

Soma Cube WPF

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2019年2月20日

CPOL

16分钟阅读

viewsIcon

8980

downloadIcon

252

用于操作 Soma Cube 零件的 WPF 3D 图形程序

SomaCubeWPF

引言

SomaCubeWPF 是一个演示 3D 图形技术的 WPF 程序。它可以让你操作著名的 Soma Cube 拼图块。代码与我的 Orbital Mechanics WPF 程序类似。如果你不熟悉 WPF 和 3D 图形,你可能想看看那个项目中“3D Graphics and WPF”的部分。这两个项目之间的一个区别是 SomaCubeWPF 使用了两个视口和两个摄像头,如下文所述。拥有两个摄像头可以让你从不同角度查看 Soma Cube 块,从而更容易操作它们。

背景

Soma Cube 是一个由七个块组成的拼图。其中六个块由 4 个小立方体组成(这六个块被称为“四联立方体”,因为 *tetra* 是希腊语中“四”的意思),一个块由 3 个小立方体组成(这个块是“三联立方体”),总共有 27 个小立方体。这七个块可以组合成一个更大的立方体,或者以无数种方式排列。上面的图片显示了七个块被排列成 Soma 发烧友称为“狗”的形状。仅仅是将这七个块组合成一个更大的立方体,就有 200 多种解决方案!我将更大的立方体和“狗”的解决方案包含在 *SettingsFiles* 目录下的设置文件 *cube.txt* 和 *dog.txt* 中。*Settings* 文件将在下面讨论

在深入研究代码之前,让我们先熟悉一下这七个块。虽然命名法起初可能看起来有点笨拙,但稍加练习就会变得非常熟悉。我遵循这里的命名惯例,每个块都有一个名称和一个编号。

  1. L 三联立方体,#1

    T Tetra Cube

  2. L 四联立方体,#2

    T Tetra Cube

  3. T 四联立方体,#3

    T Tetra Cube

  4. S 四联立方体,#4

    S Tetra Cube

  5. 左螺旋四联立方体,#5

    Right Screw Tetra Cube

  6. 右螺旋四联立方体,#6

    T Tetra Cube

  7. 分支四联立方体,#7

    T Tetra Cube

组成块的每个小立方体的每个面都标有一个*面标签*。例如,3 号块,“T 四联立方体”有 18 个面,由小写字母 **a** 到 **r** 表示。所以 T 四联立方体的面是 3a、3b、3c,依此类推,直到 3r。这些面标签在你在*平移*或*旋转*块时有助于保持块的朝向。你可以通过按 **L** 键或使用视图菜单来切换面标签的显示。

组成块的每个小立方体也有一个标签;*立方体标签*是块的编号和一个大写字母。因此,S 四联立方体——由 4 个小立方体组成——的立方体标签是 4A、4B、4C 和 4D,如上所示。立方体标签用于指示块何时*与其他块相交*。你可以通过按 **C** 键或使用视图菜单来切换立方体标签的显示。你可以同时显示面标签和立方体标签,但通常情况下,只显示其中一个会更清晰。在这些图像中,显示了 T 四联立方体和右螺旋四联立方体的面标签;对于其他块,显示的是立方体标签。

Using the Code

下载并解压项目,然后用 Visual Studio 打开 *SomaCubeWPF.sln*。它只包含一个项目。在运行程序之前,你首先需要在 *app.config* 中设置你的机器上图像文件的位置,例如

<setting name="ImageFileLocation" serializeAs="String">
	<value>C:\MyProjects\SomaCubeWPF\Images\</value>
</setting>

修改并保存 *app.config* 后,从 Visual Studio 主菜单使用“**生成**”->“**重新生成解决方案**”来构建解决方案。使用 **Ctrl-F5** “不调试启动”,然后会显示一个 WPF 窗体,如下图所示。WPF 窗体右侧是主(较大的)视口。左侧从上到下包括:

  1. 菜单栏
  2. 两个摄像机的相机设置
  3. 为七个块中的每一个块提供一行。每个块的行显示它是否被选中,以及它是否与其他块相交(或碰撞)。你还可以通过单击“**显示矩阵**”按钮来显示平移矩阵,并使用“**清除**”标签下的“**旋转**”和“**平移**”按钮来清除块的旋转和/或平移。
  4. “**手电筒**”控件
  5. 次要(较小)视口

选择和移动 Soma Cube 块

选择

要移动一个块,你必须先*选择*它。选定的块将显示为选定颜色,而所有其他块将显示为未选定颜色。(有关如何设置选定和未选定颜色的信息,请参阅配置颜色。)在窗体的左侧,选定块(或块)的名称将具有青色背景,而未选定块的名称将具有灰色背景。你可以通过以下三种方式之一来选择一个块:

  1. 使用 **Spacebar** 选择下一个块。例如,如果当前选择的是 5 号块,按下空格键将取消选择 5 号块并选择 6 号块。**Shift-Spacebar** 将取消选择当前块并选择前一个块。
  2. 使用数字小键盘输入块(1-7)的编号。
  3. 你可以用鼠标在主视口中左键单击一个块。

你还可以通过在使用技术 2 和 3 时按住 **Ctrl** 键来选择*多个*块。例如,如果当前没有选择任何块,要选择 1、3 和 6 号块,请按数字小键盘上的 **1**,然后按 **Ctrl-3** 和 **Ctrl-6**。如果当前已选择一个块,按 **Ctrl** 键和选定的块编号将取消选择该块。如果你选择多个块,你可以将它们作为一个整体移动,使用*平移*键(平移)。

 

旋转

旋转选定块的按键是:

  1. 使用 **X**、**Y**、**Z** 分别绕其 X、Y 和 Z 轴*旋转*单个选定块 90 度。(90 度在 *app.config* 文件中指定为 SomaPieceRotateIncrement下面“杂项参数”中将进行描述。)只能旋转一个块;如果你尝试旋转多个选定块,将显示一个对话框,消息为:“Rotating multiple pieces not allowed (select exactly one piece)”。如果未选择任何块,并且你使用了 **X**、**Y**、**Z**,将显示一个对话框,消息为:“You must select at least one piece to rotate or translate (no pieces currently selected)”。如果未选择任何块,并且你使用了 6 个平移键(如下所述)中的任何一个,也会收到此消息。
  2. **Shift** **X**、**Y**、**Z** 将使选定块绕其*负* X、Y 和 Z 轴旋转 90 度。

旋转是使用 WPF 的 Rotation3DAnimation 类进行动画处理的,这带来了我认为非常酷的效果。*动画速度*可以通过杂项参数进行设置。你可能希望使用比默认动画速度更高或更低的值,具体取决于你的机器速度。将动画速度设置为零将禁用动画。

转换

将这 7 个块想象成处于一个世界中,你可以在该世界中向四个基本方向以及向上和向下移动选定的块。因此,有 6 个平移键用于平移选定的块。使用这六个方向,向南、向上或向西移动块对应于沿全局 +X、+Y、+Z 轴的移动。同样,向北、向下或向东移动块对应于沿全局 -X、-Y、-Z 轴的移动。有两种平移方式:*粗略*平移和*精细*平移。它们分别在杂项参数中指定为 SomaPieceTranslateIncrementCoarseSomaPieceTranslateIncrementFine

  1. 要以世界坐标*平移*选定块,请使用 **N**、**S**、**E**、**W**、**U** 和 **D** 键。它们分别是 North(北)、South(南)、East(东)、West(西)、Up(上)和 Down(下)的首字母。默认的 SomaPieceTranslateIncrementCoarse4.0,因此按下 **S** 键 2 次会将选定的块(或块)沿 +X 方向(南)平移 8 个单位。
  2. 使用 **Shift** 和 6 个平移键中的一个,可以将选定块向相反方向平移。例如,按下 **Shift-W** 会将块向*东*移动。
  3. 要通过精细平移来平移选定块,请使用 **Ctrl** 和 6 个平移键中的一个。默认的 SomaPieceTranslateIncrementFine0.5,因此按下 **Ctrl-U** 键 3 次会将选定块向上(即沿 +Z 方向)平移 1.5 个单位。
  4. 如果选择了多个块,它们将全部以相同的量进行平移。如果你想将一组块作为一个整体移动,这一点很有用。

平移是使用 WPF 的 DoubleAnimation 类进行动画处理的。与块旋转动画一样,*动画速度*可以通过杂项参数进行设置。将动画速度设置为零将禁用动画。

有关在 *app.config* 中设置 SomaPieceRotateIncrementSomaPieceTranslateIncrementCoarseSomaPieceTranslateIncrementFine 的示例,请参阅杂项参数

撤销和清除

  1. 每个都有一个变换矩阵列表。当你旋转和移动块时,当前的变换矩阵会被添加到块列表的末尾。按下 **Ctrl-Z** 会通过删除列表中的最后一项,然后使用更新列表中的最后一个变换矩阵作为当前变换矩阵来撤销上一次移动(旋转或平移)。有关详细信息,请参阅 *SomaPiece.cs* 中的 #region undo
  2. 要清除某个块的所有旋转,请单击“**清除旋转**”按钮。要清除所有平移,请单击“**清除平移**”按钮。这些操作不会清除旋转和平移列表,因此按下 **Ctrl-Z** 仍然可以撤销上一次移动。

练习

稍加练习,你就会发现你可以轻松精确地移动选定的块。在下面的 4 张图片中,我使用键盘按照以下步骤对选定的块(左螺旋四联立方体)进行定向:

  1. 使用菜单栏的“**文件**”->“**打开...**”打开 *SettingsFiles* 目录下的设置文件(下面介绍)*practice.txt*。请注意,左螺旋四联立方体(5 号块)是唯一选定的块。
  2. 通过按一次 **X** 键,将左螺旋四联立方体(5 号块)绕其 X 轴旋转 90°。
  3. 通过按一次 **N** 键,沿其 -X 轴(北)平移它 2 个单位。
  4. 通过按一次 **E** 键,沿其 -Z 轴(东)平移它 2 个单位。

请注意,你可以按 **CTRL-Z** 3 次来撤销这 3 次移动。

1277823/Practice.png

菜单栏有四个项目:**文件**、**视图**、**相机**和**帮助**。

文件

在“**文件**”下,有四个选项:

  1. “**打开...**”:允许你打开以前保存的设置文件。打开设置文件将清除你之前的所有旋转和移动!上面的图片显示了打开 *SettingsFiles* 目录中的设置文件 *dog.txt* 的结果。
  2. “**另存为...**”:允许你将设置保存到文本文件中。你可以将设置文件保存到你喜欢的任何目录。
  3. “**重置**”:将块设置为 `Properties.Settings.Default` 中的设置。此操作将清除你之前的所有旋转和移动!你还可以使用 **Esc** 键进行重置。
  4. “**退出**”:退出程序。

视图

使用菜单栏上的“**视图**”,你可以控制以下项的绘制:

  1. 选定块(或多个选定块)的 X、Y、Z 轴。这些是 X、Y 和 Z 旋转发生所在的轴。
  2. 背景,一个带有纹理的盒子,显示了北、东、南、西四个方向以及上和下。
  3. 立方体标签,用于显示相交情况
  4. 块上的面标签,用于帮助定向块。
  5. 全局 X、Y 和 Z 轴(以原点为中心)。
  6. “**手电筒**”,显示方向光。背景和手电筒的代码取自我的Clipping Plane in WPF 3D项目。
  7. 随机化,用于随机排列块。请注意,即使你随机化了块,你仍然可以使用 **CTRL-Z** 来撤销块的随机移动。

相机

为了更容易看到所有块,SomaCubeWPF 有两个视口,因此有两个相机,其中一个相机在任何时候都是*活动的*。活动视口由活动视口周围的青色边框指示,并且活动相机(“**相机 1**”或“**相机 2**”)在窗体的左上角有一个青色标签。

使用菜单栏上的“**相机**”,你可以:

  • 选择活动*相机*,可以是**相机 1** 或**相机 2**。
  • 打开“相机设置”对话框,使用六个相机设置(倾斜(Φ)、旋转(ϴ)、缩放距离(R)、X、Y 和 Z)来设置活动相机的位置。
  • 将相机移动到沿正或负 X、Y 或 Z 轴指向。(分别看向南、上、西、北、东、下。)你也可以使用两个连续的按键来实现这一点:**QS**、**QU**、**QW**、**QN**、**QE** 和 **QD**。它们分别是“快速南”、“快速上”、“快速西”、“快速北”、“快速东”和“快速下”的首字母缩写。

活动相机可以通过使用左、右、上、下、加号和减号键来移动:

  • 向上/向下箭头:按 phi (Φ) 度倾斜相机
  • 向右/向左箭头:按 theta (ϴ) 度旋转相机
  • 加号/减号:按 R 个单位缩放相机(内/外)

你还可以平移相机:

  • Ctrl+左箭头:沿 -X 轴平移相机
  • Ctrl+右箭头:沿 +X 轴平移相机
  • Shift+向上箭头:沿 +Y 轴平移相机
  • Shift+向下箭头:沿 -Y 轴平移相机
  • Shift+左箭头:沿 -Z 轴平移相机
  • Shift+右箭头:沿 +Z 轴平移相机

六个相机设置(phi、theta、R、X、Y 和 Z)显示在窗体的左上方。这六个设置的计算在 Camera 类中完成,该类使用了 Media3D PerspectiveCamera 类。你可以通过单击活动相机的“**重置相机**”按钮将活动相机重置为默认设置。

帮助

菜单栏上的“**帮助**”会显示一条消息,描述用于定向相机和移动块的按键。

Soma Piece 类

SomaPiece 类是 SomaCubeWPF 的核心。下面是一个显示一些主要字段的摘录。

	public class SomaPiece
	{
		public Cube3D[] Cubes { get; set; }
		public Matrix3D cumulativeRotationAndTranslation;
		public List previousRotationAndTranslationList { get; set; }
		public readonly Point3D[] Points;
		protected Model3DGroup ModelGroup { get; set; }
		protected MeshGeometry3D Mesh { get; set; }
		public GeometryModel3D PieceModel { get; set; }
	}

我努力使用描述性的字段名称,但为了清晰起见,这里列出了这些字段的含义:

  1. Cube3D[] Cubes 用于确定块的*相交*情况。
  2. Matrix3D cumulativeRotationAndTranslation 是描述块状态的 4x4 变换矩阵。它包含了块经历的所有旋转和平移。
  3. previousRotationAndTranslationList 是发生过的每次旋转和平移的列表。当你按下 **CTRL-Z** 时,它用于撤销移动。
  4. 七个块中的每一个都有一个点数组,Point3D[] MyPoints。它们在每个块的构造函数中被复制到 Point3D[] Points 数组中。Model3DGroup ModelGroupMeshGeometry3D Meshpublic GeometryModel3D PieceModel 用于从 Point3D[] Points 数组表示的三角形构建 Soma 块,正如“3D Graphics and WPF”中所述

块的相交

为了确定块是否相交(这显然只可能发生在*软件* Soma 块中),我使用了一个非常简单的方案:组成块的每个小立方体内部有一个稍微小一点的 public class Cube3D 类型立方体,它不会被绘制出来,但有自己的点数组 Point3D[] Points。在 GetIntersectingCubes() 方法中,会检查选定块的每个 Cube3D 是否位于另一个块的 Cube3D 内部。如果是,则这两个块发生相交。

例如,在下面侧面图的左侧,L 三联立方体(1 号块)和 T 四联立方体(3 号块)没有相交(这就是为什么 Cube3D“稍微小一点”——如果它们与包含它的那个小立方体完全相同大小,那么 CubesIntersect() 方法会认为这两个块相交,但实际上它们并没有相交)。当我选择 L 三联立方体并使用 **CTRL-E** 将其向东(右侧图像)平移 0.5 个单位时,CubesIntersect() 方法(使用了 C# 强大的 LINQ 功能)确定 L 三联立方体的 1B 立方体与 T 四联立方体的 3D 立方体相交。(请注意,在我将 L 三联立方体向西移动后,我使用了多选来同时选择* L 三联立方体和 T 四联立方体,以清楚地显示立方体标签。)

1277823/Intersection8.png

在窗体的左上角(下面显示了其中一部分),“**相交**”下方的文本框中的消息列出了该块与哪些块相交,以及相交的小立方体的标签。在此示例中,有一个相交,所以只有一条消息。对于 L 三联立方体(1 号块),消息是“T Tetra Cube (1B, 3D)”。这意味着 L 三联立方体和 T 四联立方体在 L 三联立方体的 1B 小立方体和 T 四联立方体的 3D 小立方体处相交,如上图所示。由于块的相交方式几乎有无限多种,因此“相交”文本框是一个多行文本框,带有垂直滚动条,每行一条消息。移动 Soma 块的目标是使相交数为零。

1277823/Intersection9.png

设置文件

你可以通过菜单栏的“**文件**”->“**另存为...**”将你的设置保存到文本文件中。保存的设置包括:

  • 当前视图(轴开/关、背景开/关、立方体标签开/关、面标签开/关、全局轴开/关,以及手电筒可见/不可见)
  • 七个块中每个块的变换矩阵,即其累积旋转和平移
  • 哪个块或哪些块被选中
  • 两个相机的相机设置(CameraTheta, CameraPhi, CameraR, CameraX, CameraY, 和 CameraZ),以及活动相机
  • 手电筒的位置和颜色

当你在稍后启动 SomaCubeWPF 时,你可以通过“**文件**”->“**打开...**”并指定文本文件的名称来从该文件恢复你的设置。打开设置文件将清除你之前的所有旋转和移动!

配置文件

在 *app.config* 中有几个设置可以用来定制 SomaCubeWPF。如前所述,在运行程序之前,你首先必须在你的机器上设置*图像*文件的位置,例如:

<setting name="ImageFileLocation" serializeAs="String">
	<value>C:\MyProjects\SomaCubeWPF\Images\</value>
</setting>

这些设置分为以下几类:

  1. 视图设置
  2. 相机设置
  3. 块变换矩阵参数
  4. 颜色
  5. 杂项参数

配置文件 - 视图设置

<setting name="AxesOn" serializeAs="String">
	<value>True</value>
</setting>
<setting name="BackgroundOn" serializeAs="String">
	<value>True</value>
</setting>
<setting name="FaceLabels" serializeAs="String">
	<value>True</value>
</setting>
<setting name="FlashlightsVisible" serializeAs="String">
	<value>False</value>
</setting>
<setting name="GlobalAxesOn" serializeAs="String">
	<value>False</value>
</setting>

配置文件 - 相机

两个相机的参数可以按如下方式设置:

<setting name="Camera1R" serializeAs="String">
	<value>15</value>
</setting>
<setting name="Camera1Phi" serializeAs="String">
	<value>10</value>
</setting>
<setting name="Camera1Theta" serializeAs="String">
	<value>20</value>
</setting>
<setting name="Camera1X" serializeAs="String">
	<value>1</value>
</setting>
<setting name="Camera1Y" serializeAs="String">
	<value>2</value>
</setting>
<setting name="Camera1Z" serializeAs="String">
	<value>3</value>
</setting>
<setting name="Camera2R" serializeAs="String">
	<value>47</value>
</setting>
<setting name="Camera2Phi" serializeAs="String">
	<value>10</value>
</setting>
<setting name="Camera2Theta" serializeAs="String">
	<value>230</value>
</setting>
<setting name="Camera2X" serializeAs="String">
	<value>9</value>
</setting>
<setting name="Camera2Y" serializeAs="String">
	<value>10</value>
</setting>
<setting name="Camera2Z" serializeAs="String">
	<value>11</value>
</setting>
<setting name="Camera1Selected" serializeAs="String">
	<value>True</value>
</setting>

配置文件 - 块变换矩阵

在我的 Clipping Plane Part 3 项目中,我描述了 3D 变换矩阵以及如何围绕轴旋转模型和沿轴平移模型。本项目使用相同的技术。

每个块的 4x4 变换矩阵可以如下设置。这仅适用于高级用户,请谨慎操作,因为不正确的设置可能会导致块渲染不正确!

<setting name="LTriCubeMatrix" serializeAs="String">
	<value>1,0,0,0,0,1,0,0,0,0,1,0,-1.5,1,3,1</value>
</setting>
<setting name="LTetraCubeMatrix" serializeAs="String">
	<value>-1,0,0,0,0,0,-1,0,0,-1,0,0,-0.5,-2,2,1</value>
</setting>
<setting name="TTetraCubeMatrix" serializeAs="String">
	<value>-1,0,0,0,0,0,1,0,0,1,0,0,-0.5,-2,0,1</value>
</setting>
<setting name="STetraCubeMatrix" serializeAs="String">
	<value>0,-1,0,0,-1,0,0,0,0,0,-1,0,-1.5,0,1,1</value>
</setting>
<setting name="LeftScrewTetraCubeMatrix" serializeAs="String">
	<value>0,0,1,0,0,1,0,0,-1,0,0,0,1.5,1,3,1</value>
</setting>
<setting name="RightScrewTetraCubeMatrix" serializeAs="String">
	<value>1,0,0,0,0,1,0,0,0,0,1,0,-2.5,1,-1,1</value>
</setting>
<setting name="BranchTetraCubeMatrix" serializeAs="String">
	<value>-1,0,0,0,0,-1,0,0,0,0,1,0,1.5,1,-1,1</value>
</setting>

配置文件 - 颜色

块被选中时的颜色,以及未选中时的颜色可以按如下方式设置:

<setting name="LTriCubeColorSelected" serializeAs="String">
	<value>#FFFF0080</value>
</setting>
<setting name="LTetraCubeColorSelected" serializeAs="String">
	<value>#FFFF6684</value>
</setting>
<setting name="TTetraCubeColorSelected" serializeAs="String">
	<value>#FFFFFF00</value>
</setting>
<setting name="STetraCubeColorSelected" serializeAs="String">
	<value>#FF8080FF</value>
</setting>
<setting name="LeftScrewTetraCubeColorSelected" serializeAs="String">
	<value>#FF0000FF</value>
</setting>
<setting name="RightScrewTetraCubeColorSelected" serializeAs="String">
	<value>#FF8CBFFF</value>
</setting>
<setting name="BranchTetraCubeColorSelected" serializeAs="String">
	<value>#FF00FFFF</value>
</setting>
<setting name="BackgroundColor" serializeAs="String">
	<value>#FFDCDCDC</value>
</setting>
<setting name="LTriCubeColorNotSelected" serializeAs="String">
	<value>#88FFC4E8</value>
</setting>
<setting name="LTetraCubeColorNotSelected" serializeAs="String">
	<value>#88FFCCCC</value>
</setting>
<setting name="TTetraCubeColorNotSelected" serializeAs="String">
	<value>#88DDDD00</value>
</setting>
<setting name="STetraCubeColorNotSelected" serializeAs="String">
	<value>#88C0C0FF</value>
</setting>
<setting name="LeftScrewTetraCubeColorNotSelected" serializeAs="String">
	<value>#88EEEEFF</value>
</setting>
<setting name="RightScrewTetraCubeColorNotSelected" serializeAs="String">
	<value>#88ABBFFF</value>
</setting>
<setting name="BranchTetraCubeColorNotSelected" serializeAs="String">
	<value>#88DDFFFF</value>
</setting>

配置文件 - 杂项

不属于其他类别的参数可以按如下方式设置:

<setting name="AnimationSpeed" serializeAs="String">
	<value>1.5</value>
</setting>
<setting name="SelectedPieces" serializeAs="String">
	<value>0%1%2%3%4%5%6:1</value>
</setting>
<setting name="SomaPieceTranslateIncrementCoarse" serializeAs="String">
	<value>2</value>
</setting>
<setting name="SomaPieceTranslateIncrementFine" serializeAs="String">
	<value>0.5</value>
</setting>
<setting name="SomaPieceRotateIncrement" serializeAs="String">
	<value>90</value>
</setting>
<setting name="BackgroundColor" serializeAs="String">
	<value>#FFDCDCDC</value>
</setting>

关注点

在未来的版本中,我计划演示一个解决 Soma Cube 的算法。

历史

  • 2019 年 2 月 17 日:版本 1.0
© . All rights reserved.