记一次手搓简单的Minecraft

暑假快结束了,项目也做完了,最近闲的没事干,偶然想起下学期选了一个《游戏中的计算机图形学》这个课,想着做点什么来玩玩,兴许能当大作业呢,于是就有了本文

代码已开源 https://github.com/jeanhua/Minecraft

如何做

上篇文章学了怎么用OpenGL渲染一个彩色三角形,那么做一个简单的Minecraft应该没啥问题了,刚好mc的是一个像素方块游戏。

投影

建模软件和游戏的模型都是由面组成,确定一个面至少需要3个顶点,所以三角形便形成了一个面。那么正方形也很简单,两个三角形便可以拼成一个正方形。然后六个正方形就可以拼成一个正方体。

有了正方体,mc中的基础便完成了

下一步,我们需要把3D世界中的点投影到我们的2D屏幕上,这需要经过如下变换:
$$
V_{clip} = M_{projection} ⋅ M_{view} ⋅ M_{model} ⋅ V_{local}
$$
关于这几个矩阵的概念,我们引用learnopengl网站的原文:

世界空间

如果我们将我们所有的物体导入到程序当中,它们有可能会全挤在世界的原点(0, 0, 0)上,这并不是我们想要的结果。我们想为每一个物体定义一个位置,从而能在更大的世界当中放置它们。世界空间中的坐标正如其名:是指顶点相对于(游戏)世界的坐标。如果你希望将物体分散在世界上摆放(特别是非常真实的那样),这就是你希望物体变换到的空间。物体的坐标将会从局部变换到世界空间;该变换是由模型矩阵(Model Matrix)实现的。

模型矩阵是一种变换矩阵,它能通过对物体进行位移、缩放、旋转来将它置于它本应该在的位置或朝向。你可以将它想像为变换一个房子,你需要先将它缩小(它在局部空间中太大了),并将其位移至郊区的一个小镇,然后在y轴上往左旋转一点以搭配附近的房子。你也可以把上一节将箱子到处摆放在场景中用的那个矩阵大致看作一个模型矩阵;我们将箱子的局部坐标变换到场景/世界中的不同位置。

观察空间

观察空间经常被人们称之OpenGL的摄像机(Camera)(所以有时也称为摄像机空间(Camera Space)或视觉空间(Eye Space))。观察空间是将世界空间坐标转化为用户视野前方的坐标而产生的结果。因此观察空间就是从摄像机的视角所观察到的空间。而这通常是由一系列的位移和旋转的组合来完成,平移/旋转场景从而使得特定的对象被变换到摄像机的前方。这些组合在一起的变换通常存储在一个观察矩阵(View Matrix)里,它被用来将世界坐标变换到观察空间。在下一节中我们将深入讨论如何创建一个这样的观察矩阵来模拟一个摄像机。

裁剪空间

在一个顶点着色器运行的最后,OpenGL期望所有的坐标都能落在一个特定的范围内,且任何在这个范围之外的点都应该被裁剪掉(Clipped)。被裁剪掉的坐标就会被忽略,所以剩下的坐标就将变为屏幕上可见的片段。这也就是裁剪空间(Clip Space)名字的由来。

透视投影

如果你曾经体验过实际生活给你带来的景象,你就会注意到离你越远的东西看起来更小。这个奇怪的效果称之为透视(Perspective)。

经过如上操作,我们成功将一个3D方块在我们的屏幕上渲染起来

scubox

地形

接下来就是渲染地形了

我们知道,mc的地形渲染是按照区块来渲染的,一个区块的大小是16x16x256 (后面增加了,这里不考虑),那么我们如何将我们的地形渲染出来呢,肯定不能一个一个坐标填上去,那得写到猴年马月。

这里我们使用柏林噪声算法,生成高低连续的地形

Perlin噪声Perlin noise,又称为柏林噪声)指由Ken Perlin发明的自然噪声生成算法,具有在函数上的连续性,并可在多次调用时给出一致的数值。 在电子游戏领域中可以透过使用Perlin噪声生成具连续性的地形;或是在艺术领域中使用Perlin噪声生成图样。由于Perlin本人的失误,Perlin噪声这个名词现在被同时用于指代两种有一定联系的的噪声生成算法。这两种算法都广泛地应用于计算机图形学,因此人们对这两种算法的称呼存在一定误解。Simplex噪声分形噪声都曾在严肃学术论文中被单独的称作Perlin噪声

地形

这样我们就完成了一个简单的mc

细节

上面只是表面工作,总所周知,代码里面最复杂的往往是暗处的细节,接下来我们来研究一下编写这个游戏中遇到的各种问题

性能

重中之重,必然是性能问题,我们来估算一下,假如我们一次要渲染21x21个区块,每个区块大小是16x16x256个方块,每个方块是六个面,每个面是两个三角形,也就是6个顶点,每个顶点是一个位置坐标的3维向量+2维UV坐标+3维法线向量,每个元数据是一个浮点数,那么我们需要:

21x21x16x16x256x6x2x3x(3+2+3) = 8323596288个浮点数

每个浮点数是4字节,我们一共需要 31752MB ,也就是 31GB 的数据,这显然是不现实的,我们没有那么大的内存,更没有那么大的显存

那么我们就要使用面剔除来去掉多余的面了,比如地形内部的面,我们不需要渲染,因为看不到,还有背面也不需要渲染,这两者我们分别来看

  1. 内部的面:也就是没有和空气接触的面,我们直接跳过,只判断与空气接触的面,我们才记录顶点数据

  2. 背面:对于一个封闭的物体,背面,也就是物体内部的面,我们也不需要渲染,那么就要剔除,那么我们怎么判断物体的外面还是内面呢,这就需要我们线性代数中学到的了,我们把一个三角形顶点按照逆时针连成向量,根据右手定则即可区分

做完这些之后,我们的世界就是这个样子,我们进入地里面观察:

inside1

inside2

可以看到只有表面渲染了

linemode

现在我们的面数量就大大减少了,只需要200~300MB即可

渲染

接下来是渲染部分

我们做的地图肯定是需要无限生成

那么就需要判断,靠近摄像机的区块生成和远离摄像机的区块的销毁问题

销毁没啥好讲的,但是生成区块的部分,如果我们每帧都进行计算生成区块,那么我们在区块生成时将会很卡,这里我的解决方案是使用线程池来生成区块,然后放在缓冲队列里面,每帧取出一个区块渲染,这就让我们的游戏无感生成区块了


剩下的还有方块销毁和放置问题,需要使用射线检测来判断哪个面上放置或者销毁,还有光线,这里不再细究,感兴趣可以去网上探索

最终成果

logo

logo2

LOVE

river


记一次手搓简单的Minecraft
https://www.blog.jeanhua.cn/2025/08/24/9c9ca101175f/
作者
jeanhua
发布于
2025年8月24日
许可协议