使用 OpenGL 和 C# 的 3D 拼图






4.80/5 (6投票s)
使用 OpenGL 和 C# 进行 3D 编程,创建交互式拼图
引言
在重新学习 3D 图形编程时,我考虑了我的几个选择
- C++ 和 DirectX
- C# 和托管 DirectX
- WPF
- C# 和 OpenGL
(这里有一个很好的 其他选项 列表。) 我放弃了 C++ 和 DirectX 选项,因为我更喜欢 C# 而不是 C++。托管 DirectX 已不再受微软支持,所以我也没有选择那个选项。我最初尝试使用 WPF,发现使用 XAML 和 C# 有点太难了,所以我选择了 C# 和 OpenGL 选项。本项目使用了 Tao OpenGL 框架。使用 Tao OpenGL 框架的关键是传递一个句柄(C 和 C++ 术语中的指针)给设备上下文。
handleDvcCtx = (uint)pnlViewPort.Handle;
OpenGLControl.OpenGLInit(ref handleDvcCtx, pnlViewPort.Width, pnlViewPort.Height, ref error);
背景
我的目标是构建一个基础的“游戏”,玩家可以在 3D 世界中操作一定数量的游戏块,比如乐高®积木或俄罗斯方块积木。玩家应该能够独立地移动和旋转它们,并移动相机来查看它们。对于这个项目,所有的旋转都是围绕立方体的局部轴旋转 90 度(尽管我很乐意接受关于如何围绕世界轴以及局部轴旋转的建议)。我决定做一个 3D 拼图,由上面的截图所示的 8 个立方体组成。(这些立方体——游戏块——可以通过下面描述的按键操作来操作)。
背景
在 3D 编程中,旋转和平移涉及矩阵运算,包括矩阵乘法,而矩阵乘法通常是不可交换的。我提供了一个 Matrix
类,其中包含一些矩阵运算方法和许多关于矩阵算术的链接。OpenGL 使用矩阵堆栈的概念,这在代码中得到了使用。对于更新版本的 OpenGL,有 矩阵堆栈的替代方案,本项目目前并未采用。
立方体
为了绘制带纹理的立方体,我修改了 C++ 的 GLUT 立方体,将其转换为一个名为 Cube
的 C# 类,并添加了对 glTexCoord2f 的调用。我还使用了 Game Programming Wiki 的 LoadTexture() 方法从文件中加载图片。下面是如何使用 Cube
类创建一个立方体。另请参阅 GamePiece.cs 中的 Create()
(图像文件必须位于 bin\Debug 或 bin\Release 目录)
Cube cube = new Cube(new string[]
{ "face_1c.bmp", "face_1e.bmp", "face_1a.bmp",
"face_1f.bmp", "face_1d.bmp", "face_1b.bmp" });
本项目使用 Visual Studio 2010 的“允许不安全代码”选项进行编译,因为需要使用 fixed
关键字将指针传递给 glVertex3fv
。
fixed (float* face0 = &verts[faces[ndx, 0], 0])
{
float s = tex[(4 * ndx), 0];
float t = tex[(4 * ndx), 1];
Gl.glTexCoord2f(s, t);
Gl.glVertex3fv(face0);
}
另一个使用指针的地方是调用 SwapBuffers
,这时你需要 System.Runtime.InteropServices
命名空间。
运行程序
为了保持项目体积小,我将使用的图片放在了一个单独的可选下载文件中。程序默认使用上面截图中的图片,但你也可以在 app.config 文件中指定自己的图片。我包含了一个示例 app.config 来展示如何实现这一点。将图片(*.bmp 或 *.png)放在项目的 Images 目录中。一个生成后步骤会将它们复制到 \bin\Debug 和 \bin\Release。如果你指定的文件不存在,Visual Studio 输出窗口会显示一条警告信息。
下载并构建程序后,你可以从菜单栏中选择“帮助”来显示用于操作游戏块的按键说明。一旦你学会了按键操作,稍加练习你就可以快速地将游戏的拼图块移动和旋转到你想要的位置。拼图最初是组装好的。“随机化”菜单项会随机移动和旋转拼图块。还有一个可以启动的计时器,你可以用来查看解决拼图需要多长时间。
当按下 'X'、'Y' 或 'Z' 键围绕立方体的局部 x、y 和 z 轴旋转立方体时,有必要跟踪旋转的顺序,因为如前面所述,矩阵乘法,因此旋转,是不可交换的。我选择的策略是创建一个 GamePiece
类来表示每个游戏块(它们本质上是 Cube
),以及一个 GameUniverse
类,在其中构建一个游戏块列表。每个 GamePiece
都有一个当前旋转矩阵 private float[] currentRotMatrix
,该矩阵被初始化为单位矩阵。
然后,当按下 'X'、'Y' 或 'Z' 键来旋转游戏块时,会构造一个临时旋转矩阵 rotMatrix
,然后将 currentRotMatrix
与其相乘。然后用 currentRotMatrix
乘以当前模型视图矩阵。即使每次旋转都是 90 度,我也通过执行九次单独的 10 度旋转来实现了一个简单的动画,这样立方体就会平滑地旋转而不是瞬间移动。使用的暂停时间是 AnimationPauseMS
,可以根据机器的速度进行调整。(我使用的是一台相当普通的双核处理器机器,对动画效果感到非常满意。)
同样,平移也保存在一个名为 currentTranMatrix
的矩阵中。在 ApplyTransformations()
中。你可以看到当前模型视图矩阵与平移矩阵相乘,然后乘以 currentRotMatrix
。你可能想尝试颠倒乘法顺序,以证明矩阵乘法是不可交换的。对于游戏块的平移,'S' 键(代表 South)将块沿正 X 方向移动,'N' 键(代表 North)沿负 X 方向移动,'U' 键(代表 Up)沿正 Y 方向移动,'D' 键(代表 Down)沿负 Y 方向移动,'W' 键(代表 West)沿正 Z 方向移动,'E' 键(代表 East)沿负 Z 方向移动。
每个块的位置和旋转状态,以及相机设置(即“状态”),都可以在退出时保存在应用程序设置中,并在启动时恢复;有关详细信息,请参阅 SaveValues()
和 RetrieveValues()
。或者,状态也可以使用菜单栏的“文件”选项保存在文本文件中,并通过“打开”读取文件来恢复。
增强功能
这个简单程序的增强功能多种多样,仅受想象力限制。这里是我突然想到的一些想法:
- 使用鼠标选择、旋转和移动游戏块
- 能够将两个或多个块组合为一个单元进行平移或旋转
- 将所有平移和旋转保存在一个列表中,以便实现“撤销”功能
- 一种方式向玩家指示她已成功解开拼图
- “难度级别”选项(例如,更多立方体,误导性图像,多种解决方案)
结论
我不是 OpenGL 专家,因此代码的某些部分可能效率不高。我还在尝试找到一种方法,在按下 X、Y 和 Z 键时让立方体围绕世界轴旋转。(此版本目前围绕立方体的局部轴旋转。)欢迎 OpenGL 大师的评论。
历史
- 版本 1.0 2015/01/18