如何在Minecraft风格的游戏中实现具有遮挡的基于体素的照明?


13

我正在使用C#和XNA。我当前的照明算法是一种递归方法。但是,这样做很昂贵,以至于每5秒计算一个8x128x8块。

  • 还有其他照明方法会产生可变暗度阴影吗?
  • 还是递归方法好,也许我只是做错了?

似乎递归的东西从根本上来说是昂贵的(每个块被迫遍历约25k块)。我当时正在考虑使用一种类似于光线跟踪的方法,但是我不知道这将如何工作。我尝试的另一件事是将光源存储在一个列表中,并为每个块获取到每个光源的距离,并使用该光源将其照亮到正确的水平,但随后照明将穿过墙壁。

我当前的递归代码如下。在清除并重新添加阳光和手电筒之后,从不具有零光照水平的块中的任何位置调用此方法。

world.get___at是一个可以在该块之外获取块的函数(该块位于块类内部)。Location是我自己的结构,类似于Vector3,但是使用整数而不是浮点值。light[,,]是该块的光照图。

    private void recursiveLight(int x, int y, int z, byte lightLevel)
    {
        Location loc = new Location(x + chunkx * 8, y, z + chunky * 8);
        if (world.getBlockAt(loc).BlockData.isSolid)
            return;
        lightLevel--;
        if (world.getLightAt(loc) >= lightLevel || lightLevel <= 0)
            return;
        if (y < 0 || y > 127 || x < -8 || x > 16 || z < -8 || z > 16)
            return;
        if (x >= 0 && x < 8 && z >= 0 && z < 8)
            light[x, y, z] = lightLevel;

        recursiveLight(x + 1, y, z, lightLevel);
        recursiveLight(x - 1, y, z, lightLevel);
        recursiveLight(x, y + 1, z, lightLevel);
        recursiveLight(x, y - 1, z, lightLevel);
        recursiveLight(x, y, z + 1, lightLevel);
        recursiveLight(x, y, z - 1, lightLevel);
    }

1
东西是可怕的错误,如果你每块做2000000块-尤其是因为有实际只有8,192块的8 * 128 * 8块。您正在经历每个块〜244次的操作是什么?(可能是255吗?)
doppelgreener

1
我的数学错了。抱歉:P。变化中。但是,为什么要遍历那么多的原因是,您必须从每个块中“冒泡”,直到达到比设置高的亮度。这意味着每个块在达到实际亮度之前可能会被覆盖5-10次。8x8x128x5 =很多

2
您如何存储体素?这对于减少遍历时间很重要。
Samaursa,2011年

1
您可以发布照明算法吗?(您问您是否做得不好,我们不知道)
doppelgreener 2011年

我将它们存储在“块”数组中,一个块由一个用于材料的枚举以及一个供将来使用的元数据字节组成。

Answers:


6
  1. 每个光源都有一个精确的(浮点)位置,并且边界球体由标量光源半径值定义LR
  2. 每个体素在其中心都有一个精确的(浮点)位置,您可以根据其在网格中的位置轻松地计算出该位置。
  3. 仅运行一次8192 |VP - LP| < LR个体素中的每个体素,然后通过检查来查看每个体素是否落在N个光源的球面边界体积中,其中VP是相对于原点的体素位置矢量,并且LP是相对于原点的光源位置矢量。对于发现当前体素半径的每个光源,将其照明因子增加到距光源中心的距离|VP - LP|。如果对向量进行归一化,然后得到其大小,则范围为0.0-> 1.0。体素可以达到的最大亮度为1.0。

运行时间是O(s^3 * n),其中s体素区域的边长(128)n是光源的数量。如果您的光源是静态的,那没有问题。如果您的光源是实时移动的,则您可以仅对增量进行操作,而不必在每次更新时重新计算整个shebang。

您甚至可以将每个灯光影响的体素存储为该灯光中的参考。这样,当灯光移动或被破坏时,您可以只浏览该列表,相应地调整灯光值,而不必再次遍历整个立方网格。


如果我正确理解了他的算法,他将通过允许光线到达较远的地方来尝试进行某种伪辐射,即使这意味着光线必须“绕过”某些角落。或者,换句话说,根据到原点的最短路径。所以-不太符合您目前的建议。
Martin Sojka

感谢@MartinSojka的详细信息。是的,听起来更像是智能补水。对于全局照明的任何尝试,即使进行了明智的优化,成本也往往很高。因此,最好先在2D模式下尝试解决这些问题,如果它们的成本甚至高昂,请知道在3D模式下您将面临一定的挑战。
工程师

4

Minecraft本身不以这种方式进行日光照射。

您只需从上到下填充阳光,每一层都将衰减来自上一层中相邻体素的光。非常快-单遍,无列表,无数据结构,无递归。

您必须在以后的过程中添加手电筒和其他非泛光灯。

还有许多其他方法可以做到这一点,包括定向光传播等,但是它们显然较慢,并且鉴于这些惩罚,您必须弄清楚是否要投资于其他现实主义。


等待,那么Minecraft到底是如何做到的?我听不清楚您在说什么...“每一层都在衰减的情况下从上一层的相邻体素收集光”是什么意思?

2
从最顶层(恒定高度的切片)开始。充满阳光。然后转到下面的层,那里的每个体素都从上一层(在其上)中最接近的体素获得照明。将零光置于固体体素中。您可以通过两种方法来确定“内核”,即上述体素的贡献权重,Minecraft使用找到的最大值,但如果传播不是直截了当的,则将其减小1。这是横向衰减,因此体素的垂直列不受阻碍将获得完整的阳光传播,并且拐角处的光会弯曲。
比约恩·韦森

1
请注意,此方法决不是基于任何实际物理原理的:)主要问题在于,您实际上是在尝试近似非定向光(大气散射)并通过简单的启发式反射光能传递。看起来不错。
比约恩·韦森

3
悬在其上的“嘴唇”呢?光线如何从那里升起?光如何向上传播?当您仅从上到下进行操作时,就不能再向上返回以填充突出部分。还要电筒/其他光源。你会怎么做?(它们只能

1
@Felheart:我看了好一阵子了,但本质上有一个最低的环境光水平,通常足以应付悬垂的下方,因此它们并不完全是黑色的。当我自己执行此操作时,确实从下到上添加了第二遍,但是与环境方法相比,我没有真正看到任何重大的美学改进。火炬/点光源必须单独处理-如果您将火炬放在墙的中间并进行一些实验,我认为您会看到MC中使用的传播模式。在测试中,我将它们传播到一个单独的光场中,然后添加。
比约恩·韦森

3

如果您知道了,有人说回答您自己的问题,是的。找出一种方法。

我正在做的是:首先,创建一个3d的“已更改的块”布尔数组,该数组覆盖在块上。然后,填充阳光,手电筒等(只需点亮其上的块,尚无洪水填充)。如果您更改了某些内容,请在该位置点击“更改的块”为true。同样,将每个实体块(因此无需计算照明)更改为“已更改”。

现在是沉重的工作:遍历16个遍(对于每个轻量级),整个块,如果其“已更改”,则继续。然后获取周围的块的光照水平。获得最高的照明水平。如果该光水平等于当前通过的光水平,则将您所在的块设置为当前水平,并将该位置的“已更改”设置为true。继续。

我知道这种情况很复杂,我试图解释自己的最佳状态。但是重要的事实是,它有效且快速。


2

我建议一种将您的多遍解决方案与原始递归方法结合起来的算法,并且该算法最有可能比任何一种算法都快。

您将需要16个块列表(或任何种类的集合),每个照明级别一个。(实际上,有多种方法可以优化此算法以使用较少的列表,但是这种方法最容易描述。)

首先,清除列表,并将所有模块的照明水平设置为零,然后像在当前解决方案中一样初始化光源。在那之后(或之中),将具有非零光照水平的所有块添加到相应的列表中。

现在,浏览光照级别为16的块的列表。如果相邻的任何块的光照级别小于15,请将其光照级别设置为15,然后将其添加到适当的列表中。(如果它们已经在另一个列表中,则可以将其从列表中删除,但是即使您没有,也不会造成任何伤害。)

然后以亮度降序对所有其他列表重复相同的操作。如果您发现列表中的某个块已经具有比该列表中的块更高的光照水平,则可以假定该块已经被处理,甚至不必费心检查其邻居。(然后,再次检查邻居可能会更快-这取决于这种情况发生的频率。您可能应该同时尝试两种方法,看看哪种方法更快。)

您可能会注意到,我还没有指定列表的存储方式。实际上,只要插入给定的块并提取任意块都是快速的操作,几乎任何合理的实现都应该这样做。链表应该起作用,但是可变长度数组的任何中途实现也应如此。只要使用最适合您的东西。


附录:如果您的大多数照明灯不经常移动(墙壁也不移动),则对于每个照明块来说,存储甚至更快的指针可以确定光源的亮度(或指向其中一个)。他们,如果有几个并列)。这样,您几乎可以避免全局照明更新:如果添加了新光源(或现有光源已变亮),则只需对其周围的块进行一次递归照明遍历,而如果移除了(或变暗),则只需要更新指向它的那些块即可。

您甚至可以通过以下方式处理墙更改:移除墙后,只需在该街区开始新的递归照明遍历;如果添加了一个,则对所有指向与新墙块相同的光源的块进行照明重新计算。

(如果同时发生多个照明更改(例如,移动了一个照明,这算作移除和添加),则应使用上述算法将这些更新合并为一个更新。基本上,将所有照明级别归零指向已移除光源的模块,将其周围的所有点亮的模块以及任何新光源(或归零区域中的现有光源)添加到适当的列表,然后按上述方式运行更新。)

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.