如何提高Voxel / Minecraft类型游戏的渲染速度?


35

我正在编写自己的Minecraft副本(也用Java编写)。现在效果很好。在40米的可视距离下,我可以轻松地在MacBook Pro 8,1上达到60 FPS。(英特尔i5 +英特尔高清显卡3000)。但是,如果将可视距离设为70米,则只能达到15-25 FPS。在真实的《我的世界》中,我可以毫无问题地将视距扩大到256m。所以我的问题是我应该怎么做才能使我的游戏变得更好?

我实现的优化:

  • 仅将本地块保留在内存中(取决于玩家的观看距离)
  • 视锥剔除(首先在块上,然后在块上)
  • 只画出真正可见的方块面
  • 对包含可见块的每个块使用列表。可见的块将自己添加到此列表中。如果它们不可见,则会自动从此列表中删除。通过构建或销毁相邻的块,块变得(不可见)。
  • 每个包含更新块的块使用列表。与可见阻止列表相同的机制。
  • new在游戏循环内几乎不使用任何语句。(我的游戏运行大约20秒钟,直到调用了垃圾回收器)
  • 我目前正在使用OpenGL呼叫列表。(glNewList()glEndList()glCallList())一类块的每个侧面。

目前,我什至没有使用任何照明系统。我已经听说过VBO。但是我不知道到底是什么。但是,我将对其进行一些研究。他们会提高性能吗?在实施VBO之前,我想尝试使用glCallLists()并传递呼叫清单列表。而是使用了千次glCallList()。(我想尝试一下,因为我认为真正的MineCraft不使用VBO。对吗?)

还有其他提高性能的技巧吗?

VisualVM配置文件向我展示了这一点(仅33帧配置文件,可视距离为70米):

在此处输入图片说明

用40米(246帧)进行分析:

在此处输入图片说明

注意:我正在同步许多方法和代码块,因为我正在另一个线程中生成块。我认为在游戏循环中进行大量操作时,获取对象的锁是性能问题(当然,我所说的是只有游戏循环而没有新块生成的时间)。这是正确的吗?

编辑:删除一些synchronised块和其他一些小的改进之后。性能已经好得多了。这是我对70米的新分析结果:

在此处输入图片说明

我认为这很明显selectVisibleBlocks是这里的问题。

提前致谢!
马丁

更新资料:经过一些额外的改进(例如,使用for循环代替每个循环,在循环外缓冲变量,等等...),我现在可以很好地查看距离60。

我想我将尽快实施VBO。

PS:所有源代码都可以在GitHub上找到:https
//github.com/mcourteaux/CraftMania


2
您能否给我们提供40m的资料,以便我们能看出哪些比另一个更快?
詹姆斯

也许说得太具体了,但是如果您考虑的话,只是问如何加速3D游戏的技术,听起来很有趣。但是标题可能会吓死人。
Gustavo Maciel

@Gtoknu:您对标题的建议是什么?
马丁

5
取决于您问的是谁,有些人会说Minecraft的速度也不是那么快。
thedaian 2012年

我认为诸如“哪些技术可以加快3D游戏的速度”之类的东西应该会好得多。想一想,但不要使用“ best”一词,也不要尝试与其他游戏进行比较。我们无法确切说出它们在某些游戏中的用途。
Gustavo Maciel

Answers:


15

您提到在各个块上执行平截头体剔除-尝试将其扔掉。大多数渲染块应完全可见或完全不可见。

当在给定的块中修改块时,Minecraft仅重建显示列表/顶点缓冲区(我不知道它使用的是什么),我也是如此。如果每当视图更改时都在修改显示列表,则无法获得显示列表的好处。

另外,您似乎正在使用世界高度块。请注意,我的世界采用立方体 16×16×16块为它的显示列表,不像加载和保存。如果这样做,则没有必要再进行平截头体剔除单个块的理由了。

(注意:我尚未检查Minecraft的代码。所有这些信息要么是传闻,要么是我在玩游戏时观察Minecraft渲染的结论。)


更一般的建议:

请记住,渲染在两个处理器上执行:CPU和GPU。当帧速率不足时,一个或另一个就是限制资源 -您的程序受CPU约束或受GPU约束(假设它没有交换或调度问题)。

如果您的程序在100%CPU上运行(并且没有无限制的其他任务要完成),则您的CPU做太多的工作。您应该尝试简化其任务(例如,减少剔除),以换取GPU做更多的事情。鉴于您的描述,我强烈怀疑这是您的问题。

另一方面,如果GPU是极限(不幸的是,通常没有方便的0%-100%负载监视器),那么您应该考虑如何发送较少的数据,或要求其填充较少的像素。


2
很好的参考,您在维基上提到的对此的研究对我很有帮助!+1
Gustavo Maciel '04年

@OP:仅渲染可见的面孔(不渲染图块)。病理性但单调的16x16x16块将具有近800个可见面,而所包含的块将具有24,000个可见面。完成此操作后,Kevin的答案将包含接下来最重要的改进。
AndrewS

@KevinReid有些程序可以帮助性能调试。例如,AMD GPU PerfStudio会告诉您是否绑定了CPU或GPU,以及在GPU上绑定了哪个组件(纹理,片段,顶点等),而且我敢肯定Nvidia也有类似的东西。
akaltar

3

什么叫Vec3f.set呢?如果您要构建要从头开始渲染的每帧图像,那么绝对是您开始加速它的地方。我不是OpenGL用户,我对Minecraft的渲染方式也不是很了解,但是似乎您使用的数学函数现在正在杀死您(只是看看您花在他们身上的时间和次数)他们被召唤-一千次削减导致死亡。

理想情况下,将您的世界分割开来,以便您可以将东西组合在一起以进行渲染,构建顶点缓冲区对象并在多个框架之间重复使用它们。仅当VBO代表的世界以某种方式更改(例如用户对其进行编辑)时,才需要对其进行修改。然后,您可以为要显示的内容创建/销毁VBO,因为它在可见范围内以降低内存消耗,您只会在创建VBO时就大获全胜,而不是每一帧。

如果您的个人资料中的“调用”计数正确,则说明您多次调用很多东西。(向Vec3f.set发出1000万次呼叫...哎呀!)


我用这种方法处理很多东西。它只是为向量设置了三个值。这比每次分配一个新对象要好得多。
马丁

2

我的描述(根据我自己的实验)适用于:

对于体素渲染,更有效的方法是:预制的VBO或几何着色器?

Minecraft和您的代码可能使用固定功能管道;我自己为GLSL所做的努力,但是要点通常适用,我觉得:

(从内存中)我制作了一个平截头体,它比屏幕平截头体大了一半。然后,我测试了每个块的中心点(Minecraft有16 * 16 * 128个块)。

每个面中的面在元素数组VBO中具有跨度(来自块的许多面共享相同的VBO,直到“满”;像这样思考malloc;尽可能在相同的VBO中具有相同纹理的面)和北方的顶点索引脸,南脸等是相邻而不是混合在一起。绘制时,我glDrawRangeElements要对北面进行均匀的法线投影和标准化处理。然后我做南面等等,所以法线不在任何VBO中。对于每个块,我只需要发出可见的面孔-例如,只有屏幕中央的那些面孔才需要绘制左右两侧;GL_CULL_FACE在应用程序级别这很简单。

iirc的最大提速是多边形化每个块时剔除内部表面。

同样重要的是纹理图集管理和按纹理对面进行排序,并将具有相同纹理的面与其他块中的面放在同一vbo中。您要避免过多的纹理更改,并按纹理对面进行排序,以此类推,以最大程度地减少的跨度glDrawRangeElements。将相邻的平铺面合并成更大的矩形也很重要。我在上面引用的其他答案中谈论合并。

显然,您只对那些曾经可见的块进行了多边形处理,您可能会丢弃了很长一段时间不可见的那些块,然后对所编辑的块重新进行了多边形处理(与渲染它们相比,这种情况很少见)。


我喜欢您的视锥细胞优化的想法。但是,您不是在解释中混淆了术语“块”和“块”吗?
马丁

可能是。一大块是英文的块。
2012年

1

您所有的比较(BlockDistanceComparator)来自哪里?如果来自排序函数,是否可以将其替换为基数排序(渐近地更快,并且不是基于比较的)?

查看您的时间安排,即使排序本身没有那么糟糕,您的relativeToOrigin函数也会被每个compare函数调用两次;所有这些数据应该计算一次。排序辅助结构应该更快,例如

struct DistanceIndexPair
{
    float m_distanceSquaredFromOrigin;
    int m_index;
};

然后用伪代码

// for i = 0..numBlocks
//     distanceIndexPairs[i].m_distanceSquaredFromOrigin = ...;
///    distanceIndexPairs[i].m_index = i;
// sort distanceIndexPairs
// for i = 0..numBlocks
//    sortedBlock[i] = unsortedBlocks[ distanceIndexPairs.m_index ]

抱歉,如果那不是有效的Java结构(自从本科毕业以来我还没有接触过Java),但希望您能理解。


我觉得这很有趣。Java没有结构。好吧,在Java世界中有一个类似的东西,但是它与数据库有关,根本不是一回事。他们可以与公众成员一起创建最终课程,我想这是可行的。
Theraot

1

是的,使用VBO和CULL Face,但这几乎适用于所有游戏。您想要做的就是仅在播放器可见的情况下渲染立方体,并且如果块以特定方式触摸(例如,由于位于地下而无法看到的块),则添加块的顶点并制作它几乎就像一个“更大的块”,或者在您的情况下是一块。这称为贪婪网格划分,它可以大大提高性能。我正在开发一个游戏(基于Voxel),它使用贪婪的网格划分算法。

而不是像这样渲染所有内容:

渲染

它像这样渲染:

render2

不利的一面是,您必须在初始世界构建的每个块上进行更多的计算,或者如果玩家移除/添加一个块。

几乎所有类型的体素引擎都需要此才能获得良好的性能。

它的作用是检查块面是否正在接触另一个块面,如果是,则:仅渲染为一个(或零个)块面。当您真正快速地渲染块时,这是一个昂贵的尝试。

public void greedyMesh(int p, BlockData[][][] blockData){
        boolean[][][][] mask = new boolean[blockData.length][blockData[0].length][blockData[0][0].length][6];

    for(int side=0; side<6; side++){
        for(int x=0; x<blockData.length; x++){
            for(int y=0; y<blockData[0].length; y++){
                for(int z=0; z<blockData[0][0].length; z++){
                    if(data[x][y][z] > Material.AIR && !mask[x][y][z][side] && blockData[x][y][z].faces[side]){
                        if(side == 0 || side == 1){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=y; i<blockData[0].length; i++){
                                if(i == y){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[x][i][j][side] && blockData[x][i][j].id == blockData[x][y][z].id && blockData[x][i][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[x][i][z+j][side] || blockData[x][i][z+j].id != blockData[x][y][z].id || !blockData[x][i][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x][y+i][z+j][side] = true;
                                }
                            }

                            if(side == 0)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+1, y, z), new VoxelVector3i(x+1, y+height, z+width), new VoxelVector3i(1, 0, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z+width), new VoxelVector3i(x, y+height, z), new VoxelVector3i(-1, 0, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 2 || side == 3){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=z; j<blockData[0][0].length; j++){
                                        if(!mask[i][y][j][side] && blockData[i][y][j].id == blockData[x][y][z].id && blockData[i][y][j].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y][z+j][side] || blockData[i][y][z+j].id != blockData[x][y][z].id || !blockData[i][y][z+j].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y][z+j][side] = true;
                                }
                            }

                            if(side == 2)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y+1, z+width), new VoxelVector3i(x+height, y+1, z), new VoxelVector3i(0, 1, 0), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+width), new VoxelVector3i(x, y, z), new VoxelVector3i(0, -1, 0), Material.getColor(data[x][y][z])));
                        }else if(side == 4 || side == 5){
                            int width = 0;
                            int height = 0;
                            loop:
                            for(int i=x; i<blockData.length; i++){
                                if(i == x){
                                    for(int j=y; j<blockData[0].length; j++){
                                        if(!mask[i][j][z][side] && blockData[i][j][z].id == blockData[x][y][z].id && blockData[i][j][z].faces[side]){
                                            width++;
                                        }else{
                                            break;
                                        }
                                    }
                                }else{
                                    for(int j=0; j<width; j++){
                                        if(mask[i][y+j][z][side] || blockData[i][y+j][z].id != blockData[x][y][z].id || !blockData[i][y+j][z].faces[side]){
                                            break loop;
                                        }
                                    }
                                }
                                height++;
                            }
                            for(int i=0; i<height; i++){
                                for(int j=0; j<width; j++){
                                    mask[x+i][y+j][z][side] = true;
                                }
                            }

                            if(side == 4)
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x+height, y, z+1), new VoxelVector3i(x, y+width, z+1), new VoxelVector3i(0, 0, 1), Material.getColor(data[x][y][z])));
                            else
                                meshes.get(p).add(new Mesh(new VoxelVector3i(x, y, z), new VoxelVector3i(x+height, y+width, z), new VoxelVector3i(0, 0, -1), Material.getColor(data[x][y][z])));
                        }
                    }
                }
            }
        }
    }
}

1
值得吗?LOD系统似乎更合适。
MichaelHouse

0

您的代码似乎淹没在对象和函数调用中。测量数字,似乎没有任何内联发生。

您可以尝试找到不同的Java环境,或者只是弄乱现有环境的设置,但是一种简单而又简单的方式来编写代码(虽然速度不快,但是速度要慢得多)至少在Vec3f内部可以停止编码OOO *。使每个方法都自成一体,不要仅仅为了执行一些琐碎的任务而调用其他任何方法。

编辑:尽管到处都有开销,但似乎在渲染之前对块进行排序是最糟糕的性能。那真的有必要吗?如果是这样,您可能应该通过循环开始,计算每个块到原点的距离,然后按此排序。

*过度面向对象


是的,您可以节省内存,但会浪费CPU!所以OOO在实时游戏中不太好。
Gustavo Maciel 2012年

一旦开始分析(而不仅仅是采样),JVM通常不会进行的任何内联操作都会消失。这有点像量子理论,无法在不改变结果的情况下进行测量:p
Michael

@Gtoknu并非普遍如此,在OOO的某个级别上,函数调用开始比内联代码占用更多的内存。我想说问题代码中有很大一部分是在内存的收支平衡点附近。
aaaaaaaaaaaaaa

0

您也可以尝试将Math运算分解为按位运算符。如果有128 / 16,请尝试按位运算符:128 << 4。这将对您的问题有很大帮助。不要试图使事情全速运行。使游戏以60左右的速率更新,甚至将其分解为其他事物,但是您将不得不销毁或放置体素,或者必须创建待办事项列表,这会降低fps。您可以为实体执行大约20的更新率。大约有10个用于世界更新和/或生成。

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.