10

在 Java 中为基于瓦片的引擎进行照明的最有效方法是什么?
是否会在瓷砖后面放置黑色背景并更改瓷砖的阿尔法?
或者把一个黑色的前景和改变它的阿尔法?还是别的什么?

这是我想要的那种照明的一个例子:
http://i.stack.imgur.com/F5Lzo.png

4

3 回答 3

25

有很多方法可以实现这一目标。在做出最终决定之前花点时间。我将简要总结一些您可以选择使用的技术,并在最后提供一些代码。


硬照明

如果您想创建硬边照明效果(例如您的示例图像),我会想到一些方法:

强光示例

又快又脏(正如你所建议的)

  • 使用黑色背景
  • 根据它们的度值设置图块的 alpha值

一个问题是,您既不能使瓷砖比以前更亮(高光),也不能改变灯光的颜色。这两个方面通常会使游戏中的照明看起来不错。

第二组瓷砖

  • 使用第二组(黑色/彩色)瓷砖
  • 把这些放在主要的瓷砖上
  • 根据新颜色的强度设置新瓷砖的 alpha 值。

这种方法与第一种方法具有相同的效果,但优点是您现在可以用黑色以外的另一种颜色为覆盖图块着色,这允许彩色灯光和高光。

例子: 带黑色瓷砖的硬光示例

尽管这很容易,但问题是,这确实是一种非常低效的方法。(每个图块两个渲染图块,不断重新着色,许多渲染操作等)


更有效的方法(硬照明和/或软照明)

在查看您的示例时,我想光线总是来自特定的源图块(角色、手电筒等)

  • 对于每种类型的灯光(大手电筒、小手电筒、角色照明),您都可以创建一个图像来表示相对于源图块(灯光遮罩)的特定照明行为。对于手电筒来说可能是这样的(白色是阿尔法):

居中的光罩

  • 对于作为光源的每个图块,您将在源位置处渲染此图像作为叠加层。
  • 要添加一点浅色,您可以使用例如 10% 不透明的橙色而不是完整的 alpha。

结果

图像遮罩硬光结果

添加柔光

柔光现在没什么大不了的,与瓷砖相比,只需在光蒙版中使用更多细节。通过在通常为黑色的区域中仅使用 15% 的 alpha,您可以在瓷砖未点亮时添加低视力效果:

柔光

您甚至可以通过更改遮罩图像轻松实现更复杂的照明形式(锥体等)。

多个光源

当组合多个光源时,这种方法会导致一个问题:绘制两个相互交叉的蒙版,可能会相互抵消:

面具取消

我们想要的是他们添加灯光而不是减去它们。避免问题:

  • 反转所有光蒙版(alpha 为暗区,不透明为亮区)
  • 将所有这些光蒙版渲染为与视口尺寸相同的临时图像
  • 在整个风景上反转并渲染新图像(好像它是唯一的光罩)。

这将导致类似于以下内容: 结果图像

掩码反转方法的代码

假设您首先渲染所有图块BufferedImage,我将提供一些类似于最后显示的方法的指导代码(仅支持灰度)。

可以像这样组合用于例如手电筒和播放器的多个光罩:

public BufferedImage combineMasks(BufferedImage[] images)
{
    // create the new image, canvas size is the max. of all image sizes
    int w, h;

    for (BufferedImage img : images)
    {
        w = img.getWidth() > w ? img.getWidth() : w;
        h = img.getHeight() > h ? img.getHeight() : h;
    }

    BufferedImage combined = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);

    // paint all images, preserving the alpha channels
    Graphics g = combined.getGraphics();

    for (BufferedImage img : images)
        g.drawImage(img, 0, 0, null);

    return combined;
}

使用此方法创建和应用最终蒙版:

public void applyGrayscaleMaskToAlpha(BufferedImage image, BufferedImage mask)
{
    int width = image.getWidth();
    int height = image.getHeight();

    int[] imagePixels = image.getRGB(0, 0, width, height, null, 0, width);
    int[] maskPixels = mask.getRGB(0, 0, width, height, null, 0, width);

    for (int i = 0; i < imagePixels.length; i++)
    {
        int color = imagePixels[i] & 0x00ffffff; // Mask preexisting alpha

        // get alpha from color int
        // be careful, an alpha mask works the other way round, so we have to subtract this from 255
        int alpha = (maskPixels[i] >> 24) & 0xff;
        imagePixels[i] = color | alpha;
    }

    image.setRGB(0, 0, width, height, imagePixels, 0, width);
}

如前所述,这是一个原始示例。实现颜色混合可能需要更多的工作。

于 2013-12-09T19:16:57.323 回答
11

光线追踪可能是最简单的方法。

  • 您可以存储已看到的图块(用于自动映射,用于“在失明时记住您的地图”,可能用于小地图等)
  • 你只显示你看到的东西——可能是一堵墙或一座山的怪物挡住了你的视线,然后光线追踪在那个点停止
  • 即使您自己的光源距离不远,也可以看到远处的“发光物体”或其他光源(火炬熔岩)。
  • 您的光线长度将用于检查光量(衰落光)
  • 也许您有一个特殊的传感器(ESP,黄金/食物检测),可用于查找不在您视野中的物体?光线追踪也可能有帮助^^

这怎么做容易?

  • 从你的玩家到地图边界的每个点画一条线(使用 Bresehhams 算法http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm 沿着这条线(从你的角色到最后)走到你的视野被阻止;此时停止您的搜索(或者可能进行最后一次最终迭代以查看最重要的内容)
  • 为您线上的每个点设置闪电(距离 1 可能为 100%,距离 2 为 70%,依此类推)并将您的地图图块标记为已访问

也许您不会沿着整个地图走,如果您将光线跟踪设置为 20x20 视图就足够了?注意:你真的必须沿着视口的边界走,它不需要跟踪每个点。

我正在添加行算法以简化您的工作:

public static ArrayList<Point> getLine(Point start, Point target) {
    ArrayList<Point> ret = new ArrayList<Point>();
    int x0 =  start.x;
    int y0 =  start.y;

    int x1 = target.x;
    int y1 = target.y;

    int sx = 0;
    int sy = 0;

    int dx =  Math.abs(x1-x0);
    sx = x0<x1 ? 1 : -1;
    int dy = -1*Math.abs(y1-y0);
    sy = y0<y1 ? 1 : -1; 
    int err = dx+dy, e2; /* error value e_xy */

    for(;;){  /* loop */
        ret.add( new Point(x0,y0) );
        if (x0==x1 && y0==y1) break;
        e2 = 2*err;
        if (e2 >= dy) { err += dy; x0 += sx; } /* e_xy+e_x > 0 */
        if (e2 <= dx) { err += dx; y0 += sy; } /* e_xy+e_y < 0 */
    }

    return ret;
}

前段时间我做了这整个闪电般的事情,a * pathfindin随时问更多问题

附录:也许我可以简单地添加一些用于光线追踪的小算法^^

要获取南北边界点,只需使用以下代码段:

for (int x = 0; x <map.WIDTH; x++){
    Point northBorderPoint = new Point(x,0);
    Point southBorderPoint = new Point(x,map.HEIGHT);

    rayTrace( getLine(player.getPos(), northBorderPoint), player.getLightRadius()) );
    rayTrace( getLine(player.getPos(), southBorderPoint, player.getLightRadius()) );
}

光线跟踪是这样工作的:

private static void rayTrace(ArrayList<Point> line, WorldMap map, int radius) {

    //int radius = radius from light source     
    for (Point p: line){

        boolean doContinue = true;
        float d = distance(line.get(0), p);

        //caclulate light linear 100%...0% 
        float amountLight = (radius - d) / radius;  
        if (amountLight < 0 ){
            amountLight = 0;
        }

        map.setLight( p, amountLight );

        if ( ! map.isViewBlocked(p) ){ //can be blockeb dy wall, or monster  
            doContinue = false;
            break;
        }

    }
}
于 2013-12-12T09:49:12.797 回答
2

我已经从事独立游戏开发大约三年了。我这样做的方法首先是使用 OpenGL,这样您就可以获得 GPU 图形计算能力的所有好处(希望您已经这样做了)。假设我们从 VBO 中的所有图块开始,完全点亮。现在,有几种选择可以实现您想要的。根据您的照明系统的复杂程度,您可以选择不同的方法。

  • 如果您的灯光将围绕玩家呈圆形,无论障碍物是否会在现实生活中阻挡灯光,您都可以选择在顶点着色器中实现的照明算法。在顶点着色器中,您可以计算顶点到玩家的距离,并应用一些函数来定义物体在计算距离的函数中应该有多亮。不要使用 alpha,而只需将纹理/平铺的颜色乘以光照值。

  • 如果您想使用自定义光照贴图(这更有可能),我建议添加一个额外的顶点属性来指定图块的亮度。如果需要,更新 V​​BO。此处采用相同的方法:将纹理的像素乘以光照值。如果您以玩家位置为起点递归地填充光线,那么您将在每次玩家移动时更新 VBO。

  • 如果您的光照贴图取决于阳光照射到关卡的位置,您可以结合两种光照技术。为太阳亮度创建一个顶点属性,为光点发出的光创建另一个顶点属性(如玩家手持的火炬)。现在您可以在顶点着色器中组合这两个值。假设你的太阳像昼夜模式一样升起落下。假设太阳亮度是sun,它是一个介于 0 和 1 之间的值。这个值可以作为统一值传递给顶点着色器。代表太阳亮度的顶点属性是s,由光点发出的光的属性是l。然后你可以像这样计算该图块的总光:

    tileBrightness = max(s * sun, l + flicker);
    

    其中flicker(也是顶点着色器制服)是某种挥动函数,它代表了光点亮度的微小变化。
    这种方法使场景动态,而无需连续重新创建 VBO。我在一个概念验证项目中实现了这种方法。它工作得很好。您可以在这里查看它的外观:http ://www.youtube.com/watch?v=jTcNitp_IIo 。请注意视频中手电筒在 0:40 时是如何闪烁的。这是由我在这里解释的。

于 2013-12-11T23:04:09.670 回答