使用 OpenGL 和 C# 的 3D 太阳系






4.91/5 (53投票s)
用 OpenGL 和 C# 编写的太阳系演示
引言
这是一个使用 OpenGL 和 C# 实现的 3D 太阳系。我尽量保持简单,因为这个演示仅用于教育目的。它包含了太阳、行星、我们的月球、行星的轨道以及一些星星。它使用 Visual Studio 2008 编写,并已成功升级到 Visual Studio 2010。对于这个演示,我使用了 TAO 命名空间,它是 OpenGL DLL 和 .NET Framework 之间的互操作。我还使用了 Shadowengine
,这是一个由我开发的微型图形框架,用于免去硬编码加载纹理、初始化图形上下文等繁琐工作。
从 3D 程序员的视角看太阳系
那么,太阳系包含什么呢?行星、太阳、卫星、宇宙、背景上的星星等等。作为一名 3D 程序员,您应该考虑如何将这些实体转化为编程环境。例如,宇宙是黑色的。拥有一个黑色的背景就能解决这个问题。OpenGL 已经有了 `Gl.glClearColor(0, 0, 0, 1); // 红 绿 蓝 Alpha` 函数,可以将背景颜色设置为黑色。至于星星,它们只是明亮的点,然后您可以使用 OpenGL 的图元来绘制点。如果您懒得一个一个地放置星星,可以使用随机函数生成大量星星,只需确保它们不会落在太阳系内部。行星就是带纹理的球体;它们也有轨道和自身的轴向自转,您需要使用变量来跟踪并随时间更新它们。如果您不想在 3D Max 中制作球体,可以使用 OpenGL quadrics,它定义了一组基本的三角形状,并为它们定义了纹理坐标。卫星与行星相同,唯一的区别是它们的旋转轴位于行星上而不是太阳上。
Using the Code
项目中的引用包括 ShadowEngine
和 TAO.OpenGL
。我想指出的是,我并没有像 XNA、GLUT 等那样在独立的窗口中创建图形上下文。我的图形上下文是在一个普通的 .NET WinForm 中创建的。这非常方便,因为您可以将 3D 内容绘制到任何窗口中,并将其与 2D 组件混合。稍后您会看到,您可以将 3D 内容绘制到几乎任何 2D 组件中。OpenGL 的初始化函数只需要一个有效的组件句柄即可开始绘制 3D 内容。
以下是项目类列表
Camera.cs
这是一个经典的 FPS(第一人称射击)相机。关于 FPS 如何工作的解释超出了本文的范围。它们的工作方式如下:
- 鼠标光标会居中在屏幕中间。
- 当用户移动鼠标时,会从起始点计算出 Delta X 和 Delta Y。
- 这些 Delta X 和 Delta Y 会被转换为角度,从而决定相机的旋转方式。
- 当您希望向前或向后移动时,相机将沿着当前角度指向的方向移动。
- 您可以查看 `camera` 类中的 `public void Update(int pressedButton)` 方法,以获得更好的理解。
MainForm.cs
此类名称不言自明,它是项目的主窗体,也是唯一的窗体。它包含了纹理加载的调用、3D 上下文初始化、3D 内容的绘制等。它还处理用户的键盘和鼠标输入。由于 3D 内容需要至少 30 帧/秒的速率进行绘制,所以我使用了一个定时器并将所有绘制代码都放在了里面。一个值得关注的点是,我在一个 Panel 上启动了 3D 上下文,这样我就可以将 Panel 放在窗体中任何我想要的位置。以下是项目中的 3D 初始化代码:
hdc = (uint)pnlViewPort.Handle;
string error = "";
OpenGLControl.OpenGLInit(ref hdc, pnlViewPort.Width, pnlViewPort.Height, ref error);
以下是将纹理加载到 OpenGL 内存中的代码:
ContentManager.SetTextureList("texturas\\");
ContentManager.LoadTextures();
我的小型引擎负责加载该文件夹中的所有纹理,支持的纹理格式为 TGA、JPG 和 BMP。即使纹理不是 NPOT(非 2 的幂次方),也能正确加载。
这是绘制所有场景的代码:
private void tmrPaint_Tick(object sender, EventArgs e)
{
// clears OpenGL
Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT);
//updates the camera
solarSystem.Camara.Update(moving);
//draws the scene
solarSystem.DrawsScene();
//swaps buffers
Winapi.SwapBuffers(hdc);
//finish drawing operations
Gl.glFlush();
}
Planet.cs
一个行星包含以下变量:
- 职位
- 纹理
- 轨道(距离太阳的当前距离)
- 当前自转角度
- 当前轨道旋转角度
- 当前轨道速度
我使用 OpenGL quadrics 来绘制行星的球体。Quadrics 是 OpenGL 预定义的形状,有助于小型绘图任务。例如,Quadrics 附带纹理坐标,因此我无需使用 3D 编辑器(如 3D Max)即可正确地为每个行星应用纹理。在每一帧中,行星会根据其轨道速度在其轨道上移动。还有一个名为 `hasMoon` 的 `bool` 变量,用于指定是否为此行星绘制月球。我只有我们的月球,但如果您想绘制火星的卫星火卫一和火卫二,可以使用这段代码。行星类中的另一个有趣的函数是用于绘制其轨道的函数。首先,我使用正弦函数生成点,然后使用 `GL_LINE_STRIP` 将它们连接起来。以下是代码:
public void DrawOrbit()
{
Gl.glBegin(Gl.GL_LINE_STRIP);
for (int i = 0; i < 361; i++)
{
Gl.glVertex3f(p.x * (float)Math.Sin(i * Math.PI / 180),
0, p.x * (float)Math.Cos(i * Math.PI / 180));
}
Gl.glEnd();
}
请注意,行星的轨道几乎总是椭圆形的。这里绘制的是一个圆形的轨道。行星类中用于维护行星绕自身轴旋转和绕太阳旋转的两个角度变量。
Satellite.cs
卫星包含行星的所有内容。唯一的区别是它的旋转中心不是太阳,而是它所在的行星。因此,每次绘制时,它都必须接收其所在行星的位置。您可以在其 `draw` 函数中注意到这一点。
SolarSystem.cs
这是包含行星、恒星和卫星列表的类。它只负责创建和绘制它们。行星被保存在一个列表中,当我在主窗体中调用 `DrawScene()` 时,它会进行一个 `foreach` 循环,调用行星的 `Draw` 方法。
Star.cs
这是绘制恒星的类。恒星是单个 `GL_POINTS`,以随机位置生成。这是生成它们的函数:
public void CreateStars(int amount)
{
Random r = new Random();
int count = 0;
while (count != amount)
{
Position p = default(Position);
p.x = (r.Next(110)) * (float)Math.Pow(-1, r.Next());
p.z = (r.Next(110)) * (float)Math.Pow(-1, r.Next());
p.y = (r.Next(110)) * (float)Math.Pow(-1, r.Next());
if (Math.Pow(Math.Pow(p.x, 2) + Math.Pow(p.y, 2) + Math.Pow(p.z, 2), 1 / 3f) > 15)
{
stars.Add(p);
count++;
}
}
}
这段代码的作用是生成一个随机点,计算其到太阳的距离,如果距离小于预设值,则丢弃该点。在这种情况下,预设值为太阳系半径的两倍。此操作将重复进行,直到达到所需的星星数量。
Sun.cs
`sun` 类是最简单的。它类似于行星类,只是它没有轨道。它只有绕自身轴的旋转。太阳绘制在 OpenGL 3D 坐标 (0,0,0) 处。
关注点
在这个演示中,您将学习 OpenGL 旋转的工作原理以及如何围绕任意轴旋转网格。此外,您还将使用主要的 OpenGL 图元:点、线和三角形。好了,以上就是此项目中涉及的所有类。我希望它对您有所帮助,并鼓励开发人员开始进行 3D 编程。欢迎随意修改代码并提出您想问的任何问题。
历史
- 2013 年 4 月 17 日:演示的第一个版本