如何优化Minecraft风格的体素世界?


76

我发现即使使用四核和多肉的显卡,《我的世界》奇妙的大世界也极难导航。

我认为Minecraft的运行缓慢来自:

  • Java,因为空间分区和内存管理在本机C ++中更快。
  • 弱世界分区。

两种假设我都可能错了。但是,这让我开始思考管理大型体素世界的最佳方法。由于这是一个真实的3D世界,其中块可以在世界的任何部分存在,它基本上是一个大的3D阵列[x][y][z],其中在世界的每个块具有类型(即BlockType.Empty = 0BlockType.Dirt = 1等)

我认为要使这种世界表现良好,您需要:

  • 使用某种树(oct / kd / bsp)将所有多维数据集拆分出来;似乎oct / kd会是更好的选择,因为您可以仅在每个多维数据集级别而不是每个三角形级别进行分区。
  • 使用某种算法来确定当前可以看到哪些块,因为距离用户最近的块可能会混淆后面的块,从而使其变得毫无意义。
  • 保持块对象本身轻巧,因此可以快速将其从树中添加和删除。

我想对此没有正确的答案,但是我很想看到人们对此问题的看法。 在大型体素世界中,您将如何提高性能?



2
那你到底在问什么?您是在寻求管理大世界的好方法,还是对您的特定方法的反馈,或者在管理大世界的主题上提出意见?
doppelgreener

1
到目前为止,这是好东西,更多的是关于最常见的这类方法。我并没有在收到有关我的方法的反馈后就明确提出建议,因为我所建议的只是我在逻辑上期望发生的事情。我只是真正想要有关此主题的更多信息,经过几次搜索后并没有得到太多建议。我想我的问题不仅在于渲染性能,还在于如何管理如此大量的数据……例如对区域进行分块处理
SomeXnaChump 2011年

2
因此,请清楚并在您的帖子中添加一个问题,以便我们知道我们要回答的问题。;)
doppelgreener

3
您“极慢的导航”是什么意思?游戏生成新地形时,肯定会有一些放慢的速度,但是在那之后,Minecraft往往会很好地处理地形。
thedaian

Answers:


107

体素引擎岩石

体素引擎草

关于Java vs C ++,我都用两种语言编写了一个体素引擎(上面显示的C ++版本)。自2004年以来(当它们不流行时),我还一直在编写体素引擎。:)我可以毫不犹豫地说C ++的性能要好得多(但也更难编写代码)。它很少涉及计算速度,而更多地涉及内存管理。放下手,当您分配/取消分配与体素世界中的数据一样多的数据时,C(++)是要击败的语言。 然而,您应该考虑自己的目标。如果性能是您的最高优先级,请使用C ++。如果您只想编写一款性能无懈可击的游戏,那么Java绝对可以接受(Minecraft证明)。有很多琐碎/边缘的情况,但是总的来说,您可以期望Java的运行速度比编写良好的C ++慢约1.75-2.0倍。您可以在这里看到我的引擎的优化不佳的旧版本(编辑:此处为较新版本)。虽然块生成看起来可能很慢,但是请记住,它是按体积生成3D voronoi图,并使用蛮力方法计算CPU上的表面法线,光照,AO和阴影。我尝试了各种技术,使用各种缓存和实例化技术可以使块生成速度提高约100倍。

要回答其余问题,您可以采取许多措施来提高性能。

  1. 正在缓存。只要有可能,就应该计算一次数据。例如,我将灯光烘烤到场景中。它可以使用动态照明(在屏幕空间中作为后期处理),但是在照明中进行烘焙意味着我不必传递三角形的法线,这意味着...
  2. 将尽可能少的数据传递到视频卡。人们往往会忘记的一件事是,您传递给GPU的数据越多,花费的时间就越多。我传递单一颜色和顶点位置。如果我想进行昼/夜循环,则可以进行颜色分级,或者可以随着太阳逐渐变化来重新计算场景。

  3. 由于将数据传递到GPU的成本如此之高,因此可以在某些方面以更快的速度用软件编写引擎。软件的优势在于它可以执行各种在GPU上根本不可能实现的数据操作/内存访问。

  4. 批量播放。如果您使用的是GPU,则性能可能会因传递的每个顶点阵列的大小而异。因此,请尝试使用块的大小(如果使用块)。我发现64x64x64块效果很好。无论如何,请保持块状立方体(无直角棱镜)。这将使编码和各种操作(如转换)更加容易,并且在某些情况下还可以提高性能。如果只为每个维度的长度存储一个值,请记住这就是在计算过程中要交换的少两个寄存器。

  5. 考虑显示列表(对于OpenGL)。即使它们是“旧”方式,也可以更快。您必须将显示列表烘焙到一个变量中...如果您实时调用显示列表创建操作,那将非常慢。显示列表如何更快?它仅更新状态和每个顶点属性。这意味着我最多可以传递六个面,然后传递一种颜色(相对于体素每个顶点的颜色)。如果您使用的是GL_QUADS和立方体素,则每个体素最多可以节省20个字节(160位)!(15个字节,不带alpha字母,尽管通常情况下您希望保持4个字节对齐)

  6. 我使用一种蛮力方法来渲染“块”或数据页面,这是一种常见的技术。与八叉树不同,它更易于/更快地读取/处理数据,尽管对内存的友好性要差得多(但是,如今,您可以以200-300美元的价格获得64 GB的内存)...这并不是普通用户所拥有的。显然,您无法为整个世界分配一个巨大的数组(假设每个体素使用32位int,则1024x1024x1024的体素集为4 GB的内存)。因此,您可以根据它们与查看器的接近程度来分配/取消分配许多小数组。您还可以分配数据,获取必要的显示列表,然后转储数据以节省内存。我认为理想的组合可能是使用八叉树和数组的混合方法-在进行世界的程序生成,照明等操作时,将数据存储在数组中,

  7. 渲染近到远...修剪的像素节省了时间。如果gpu没有通过深度缓冲区测试,它将抛出一个像素。

  8. 仅在视口中渲染块/页面(不言自明)。即使GPU知道如何在视口之外剪辑多面体,传递这些数据仍然需要时间。我不知道最有效的结构是什么(可耻的是,我从未写过BSP树),但是即使是基于每个块的简单raycast也会提高性能,并且显然针对视锥进行测试会省时间。

  9. 显而易见的信息,但对于新手:删除不在表面上的每个多边形-即,如果一个体素由六个面组成,则删除从未渲染过的面(正在接触另一个体素)。

  10. 作为您在编程中所做的一切的一般规则:缓存局部性!如果您可以将内容保留在本地缓存中(即使时间很短,也会有很大的不同。这意味着保持数据一致(在相同的内存区域中),而不必将内存区域切换到太频繁地进行处理。 ,理想情况下,每个线程只能处理一个块,并将该内存专用于该线程,这不仅适用于CPU缓存,还可以考虑这样的缓存层次结构(从最慢到最快):网络(云/数据库/等) ->硬盘驱动器(如果没有,请获取一个SSD),内存(如果没有,请获取三通道或更大的RAM),CPU高速缓存,寄存器,请尝试保持数据畅通后者,而不是将其交换得比您需要的更多。

  11. 穿线。做吧 Voxel世界非常适合线程化,因为每个部分(大部分)都可以独立于其他部分进行计算...当我编写程序化世界时,我在程序世界中几乎看到了将近四倍的改进(在4核,8线程Core i7上)。线程例程。

  12. 不要使用char / byte数据类型。或短裤。您的普通消费者将拥有现代的AMD或Intel处理器(也许您也会)。这些处理器没有8位寄存器。他们通过将字节放入32位插槽中,然后将其转换回(可能是)到内存中来计算字节。您的编译器可能会执行各种伏都教义,但使用32或64位数字将为您提供最可预测(最快)的结果。同样,“ bool”值不占用1位。编译器通常会将完整的32位用于布尔值。对数据进行某些类型的压缩可能很诱人。例如,如果8个体素都是相同的类型/颜色,则可以将它们存储为一个数字(2 ^ 8 = 256个组合)。但是,您必须考虑其后果-可能会节省大量内存,但即使在减压时间短的情况下,也可能会影响性能,因为即使少的额外时间也会与您的世界大小成正比。想象一下计算射线广播;对于射线投射的每一步,您都必须运行解压缩算法(除非您想出一种明智的方法,可以在一步射线中概括8个体素的计算)。

  13. 正如何塞·查韦斯(Jose Chavez)所提到的那样,轻量化设计模式可能会很有用。就像您使用位图来表示2D游戏中的图块一样,您可以使用多种3D图块(或块)类型构建世界。缺点是纹理的重复,但是您可以通过使用适合在一起的方差纹理来改善此效果。根据经验,您想尽可能地利用实例化。

  14. 输出几何图形时,请避免在着色器中进行顶点和像素处理。在体素引擎中,不可避免地会有许多三角形,因此即使是简单的像素着色器也可以大大减少渲染时间。最好将其渲染到缓冲区,然后再将像素着色器作为后期处理。如果您不能这样做,请尝试在顶点着色器中进行计算。其他计算应尽可能纳入顶点数据中。如果必须重新渲染所有几何图形(例如阴影贴图或环境贴图),则额外的遍将变得非常昂贵。有时最好放弃动态场景,而使用更丰富的细节。如果您的游戏具有可修改的场景(即可破坏的地形),那么您可以随时在场景被破坏时重新计算场景。重新编译并不昂贵,应该花费一秒钟的时间。

  15. 放松循环并保持阵列平坦!不要这样做:

    for (i = 0; i < chunkLength; i++) {
     for (j = 0; j < chunkLength; j++) {
      for (k = 0; k < chunkLength; k++) {
       MyData[i][j][k] = newVal;
      }
     }
    }
    //Instead, do this:
    for (i = 0; i < chunkLengthCubed; i++) {
     //figure out x, y, z index of chunk using modulus and div operators on i
     //myData should have chunkLengthCubed number of indices, obviously
     myData[i] = newVal;
    }

    编辑:通过更广泛的测试,我发现这可能是错误的。使用最适合您的方案的案例。通常,数组应该是平面的,但根据情况,使用多索引循环通常会更快

编辑2:使用多索引循环时,最好以int依次循环z,y,x顺序,而不是相反。您的编译器可能对此进行了优化,但是如果这样做,我会感到惊讶。这样可以最大程度地提高内存访问和局部性的效率。

for (k < 0; k < volumePitch; k++) {
    for (j = 0; j < volumePitch; j++) {
        for (i = 0; i < volumePitch; i++) {
            myIndex = k*volumePitch*volumePitch + j*volumePitch + i;
        }
    }
}
  1. 有时您必须做出假设,概括和牺牲。您可以做的最好的假设是,假设您的大部分世界都是完全静态的,并且每隔几千帧才更改一次。对于世界上的动画部分,这些可以单独通过。还要假设您的大部分世界都是完全不透明的。可以在单独的通道中渲染透明对象。假设纹理仅每x个单位变化一次,或者对象只能以x增量放置。假定一个固定的世界大小...就像一个无限世界一样诱人,它可能导致无法预测的系统需求。例如,为了简化上面岩石中的voronoi模式生成,我假设每个voronoi中心点都位于一个均匀的网格中,并且有微小的偏移(换句话说,隐含的几何哈希)。假设一个没有包裹的世界(有边缘)。这可以简化包装坐标系统带来的许多复杂性,而对用户体验的成本却最小。

您可以在我的网站上阅读有关我的实现的更多信息


9
+1。包括顶部图片在内的良好手法可以激发阅读文章的兴趣。既然我已经读完这篇文章,我可以说不需要它们了,这是值得的。;)
George Duckett 2012年

谢谢-正如他们所说,一张图片值得一千个字。:)除了使我的文字墙不那么令人畏惧之外,我还想让读者了解使用所描述的技术可以以合理的速率渲染多少个体素。
Gavan Woolery 2012年

14
我仍然希望SE允许喜欢的特定答案。
joltmode 2012年

2
@PatrickMoriarty#15是一个很常见的把戏。假设您的编译器没有进行此优化(它可能会展开循环,但可能不会压缩多维数组)。您希望将所有数据保留在相同的连续内存空间中以进行缓存。多维数组可以(潜在地)分配在许多空间中,因为它是一个指针数组。至于展开循环,请考虑编译后的代码是什么样的。为了使寄存器和缓存的交换最少,您希望产生最少的vars /指令。您认为哪个可以编译得更多?
Gavan Woolery 2012年

2
尽管这里的某些观点是不错的,尤其是在缓存,线程化和最小化GPU传输方面,但其中的一些观点非常不准确。5:始终使用VBO / VAO代替显示列表。6:更多的RAM仅需要更多的带宽。导致12:与EXACT相反的情况适用于现代内存,为此,保存的每个字节都会增加将更多数据装入缓存的机会。14:在Minecraft中,顶点多于像素(所有那些远方的立方体),因此将计算移至像素着色器,而不是从像素着色器转移,最好使用延迟着色。

7

Minecraft可以在很多方面提高效率。例如,《我的世界》会加载约16x16瓦片的整个垂直支柱并进行渲染。我觉得不必要地发送和渲染许多图块效率很低。但是我不认为选择语言是一个重要的选择。

Java可以相当快,但是对于这种面向数据的东西,C ++确实具有很大的优势,并且访问数组和在字节内工作的开销明显更少。另一方面,在Java中跨所有平台执行线程要容易得多。除非您打算使用OpenMP或OpenCL,否则在C ++中不会发现这种便利。

我理想的系统应该是稍微复杂一些的层次结构。

拼贴是一个单位,可能约为4个字节,以保留诸如材料类型和照明等信息。

将是一个32x32x32的图块块。

  1. 如果整个侧面都是实心块,则将为六个侧面中的每个侧面设置标志。这将允许渲染器遮挡该片段之后的片段。Minecraft当前似乎没有执行遮挡测试。但是有人提到可以使用硬件遮挡剔除,这可能成本很高,但比在低端卡上渲染大量多边形要好。
  2. 仅在活动(玩家,NPC,水物理,树木生长等)期间将片段加载到内存中。否则,它们将直接从磁盘压缩后直接发送到客户端。

扇区将是一个16x16x8的分段块。

  1. 扇区将跟踪每个垂直列的最高细分,以便可以快速确定高于该最高细分的细分。
  2. 它还会跟踪被遮挡的底部线段,以便可以快速抓取需要从表面渲染的每个线段。
  3. 部门还将跟踪下一次需要更新每个部分的时间(水物理,树木生长等)。这样,在每个部门中进行加载就足以维持世界的生命,并且仅在足够长的段中进行加载即可完成任务。
  4. 将跟踪所有实体相对于该部门的职位。这样可以防止Minecraft远离地图中心旅行时出现浮点错误。

世界将是无限的部门地图。

  1. 世界将负责管理部门及其下一个更新。
  2. 世界将抢先向潜在玩家发送细分。Minecraft被动地发送客户端请求的片段,这会引起延迟。

我通常喜欢这个主意,但是您将如何在内部绘制世界各个领域的地图?
Clashsoft

虽然阵列将是“分段中的图块”和“扇形中的分段”的最佳解决方案,但“世界中的扇形”将需要一些不同的东西以允许无限的地图尺寸。我的建议是使用哈希表(伪Dictionary <Vector2i,Sector>),将XY坐标用于哈希。然后,世界可以简单地在给定坐标处查找一个扇区。
乔什·布朗

6

即使在我的2核系统中,Minecraft的运行速度也相当快。尽管这里存在一些服务器延迟,但Java似乎并不是一个限制因素。本地游戏似乎做得更好,所以我要假设那里的效率低下。

关于您的问题,Notch(《我的世界》的作者)已就该技术发表了一些篇幅。尤其是,世界存储在“块”中(您有时会看到这些块,尤其是当由于世界尚未填充而丢失时)。因此,第一个优化是确定是否可以看到块。

如您所料,在一个块内,应用程序必须根据是否被其他块遮挡来决定是否可以看到一个块。

同样要注意,由于被遮盖(即,另一块遮住了脸部)或摄像机指向的方向(如果摄像机朝北,则可以),可以将某些FACES块视为看不见。看不到任何方块的北面!)

通用技术还包括不保留单独的块对象,而是保留一个“块”块类型,每个块对象具有一个原型块,以及一些最小的数据集来描述如何自定义该块。例如,没有任何定制的花岗岩块(据我所知),但是水有数据可以告诉人们沿着每个侧面有多深,人们可以从中计算出其流动方向。

您是否要优化渲染速度,数据大小或其他内容尚不清楚。澄清会有帮助。


4
“块”通常被称为块。
Marco Marco 2012年

好收成(+1); 答案已更新。(本来是为记忆做的,却忘记了正确的词。)
Olie 2012年

您提到的低效率也称为“网络”,即使在相同的端点进行通信的情况下,它也绝不会以相同的方式重复两次。
Edwin Buck

4

这只是一些一般性的信息和建议,我可以作为一个经验丰富的Minecraft Modder来提供(至少可以为您提供一些指导)。

Minecraft速度缓慢的原因与很多可疑的低级设计决策有很大关系-例如,每次通过定位引用一个块时,游戏都会使用约7个if语句验证坐标,以确保其不会超出范围。此外,无法绕过“块”(游戏用的16x16x256块单位)然后直接引用其中的块,以绕过缓存查找和erm愚蠢的验证问题(现在,每个块引用也涉及在我的mod中,我创建了一种直接获取和更改块阵列的方法,从而将大量的地牢生成从无法玩耍的笨拙提升到了惊人的快速。

编辑:删除了声明在不同范围内声明变量会导致性能提高的说法,实际上似乎并非如此。我相信当时我将此结果与我正在尝试的其他东西进行了混合(具体来说,通过合并为双精度来消除爆炸相关代码中的双精度和浮点之间的强制转换...可以理解,这产生了巨大的影响!)

另外,尽管这不是我花费大量时间的领域,但《我的世界》中的大部分性能瓶颈都与渲染有关(大约75%的游戏时间专用于我的系统)。显然,您关心的不是要在多人游戏中支持更多玩家(服务器什么都不渲染),但这在每个人的机器都可以玩的程度上很重要。

因此,无论您选择哪种语言,都应尽量与实现/底层细节保持紧密联系,因为在这样的项目中,即使是一个细节也可能会带来不同(C ++中的一个例子是“编译器可以静态内联函数吗?指针?”是的,它可以!在我正在从事的一个项目中产生了令人难以置信的变化,因为我的代码更少,并且具有内联的优势。)

我真的不喜欢这个答案,因为它使高级设计变得困难,但是如果要考虑性能,这是一个痛苦的事实。希望对您有所帮助!

另外,Gavin的回答涵盖了一些我不想重申的细节(还有更多!他显然比我更了解该主题),并且我在很大程度上同意他的观点。我将不得不试验他关于处理器和较短的变量大小的评论,我从未听说过-我想向自己证明这是真的!


2

问题是首先考虑如何加载数据。如果您在需要时将地图数据流式传输到内存中,则可以渲染的内容显然受到了自然限制,这已经是渲染性能的提升。

然后,由您决定如何处理这些数据。为了提高GFX的性能,您可以使用Clipping裁剪隐藏的对象,太小而看不见的对象等。

如果您只是在寻找图形性能技术,我相信您可以在网上找到很多东西。


1

值得一看的是Flyweight设计模式。我相信这里的大多数答案都以某种方式引用了这种设计模式。

虽然我不知道Minecraft用来最小化每种块类型内存的确切方法,但这是在游戏中使用的一种可能途径。想法是只有一个对象(如原型对象)可以保存有关所有块的信息。唯一的区别是每个块的位置。

但是,甚至位置也可以最小化:如果您知道一块土地属于一种类型,为什么不使用一组位置数据将那块土地的尺寸存储为一个巨型块呢?

显然,唯一知道的方法是开始实现自己的实现,并进行一些内存测试以提高性能。让我们知道怎么回事!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.