Эффективная система освещения на основе 2D-тайла

Каков наиболее эффективный способ освещения для тайлового движка в Java?
Будет ли это размещение черного фона позади тайлов и изменение альфа-канала тайлов?
Или добавление черного переднего плана и изменение альфа-канала этого тайла? ? Или что-то еще?

Вот пример освещения, которое мне нужно:
http://i.stack.imgur.com


person Kyranstar    schedule 30.08.2013    source источник
comment
Любой альфаер стоит дорого. Лично я, пожалуй, пойду на первое. Я думаю, что было бы проще визуализировать плитку, используя какой-то альфа-композит, так как вам все равно нужно их визуализировать...   -  person MadProgrammer    schedule 30.08.2013
comment
Как вы рендерите тайлы в данный момент?   -  person Tim B    schedule 09.12.2013
comment
@ grimrader22 надеюсь, мой ответ решит вашу проблему :)   -  person bricklore    schedule 09.12.2013


Ответы (3)


Есть много способов добиться этого. Потратьте некоторое время, прежде чем принять окончательное решение. Я кратко суммирую некоторые приемы, которые вы можете использовать, и в конце приведу код.


Жесткое освещение

Если вы хотите создать резкий световой эффект (как в примере с изображением), мне приходят в голову некоторые подходы:

пример жесткого света

Быстро и грязно (как вы предложили)

  • Используйте черный фон
  • Задайте альфа-значения плиток в соответствии с их значением темноты.

Проблема в том, что вы не можете ни сделать тайл ярче, чем он был раньше (блики), ни изменить цвет света. Оба эти аспекта обычно делают освещение в играх хорошим.

Второй набор плитки

  • Используйте второй набор (черных/цветных) плиток
  • Положите их поверх основных плиток.
  • Установите альфа-значение новых плиток в зависимости от того, насколько ярким должен быть новый цвет.

Этот подход имеет тот же эффект, что и первый, с тем преимуществом, что теперь вы можете раскрасить плитку наложения в цвет, отличный от черного, что позволяет использовать как цветные источники света, так и блики.

Пример: жесткий свет с черными плитками, пример

Несмотря на то, что это легко, проблема в том, что это действительно очень неэффективный способ. (Два отрендеренных тайла на тайл, постоянное перекрашивание, множество операций рендеринга и т. д.)


Более эффективные подходы (жесткое и/или мягкое освещение)

Глядя на ваш пример, я представляю, что свет всегда исходит от определенного источника (персонаж, факел и т. д.).

  • Для каждого типа света (большой факел, маленький факел, освещение персонажа) вы создаете изображение, которое представляет определенное поведение освещения относительно исходной плитки (световая маска). Может быть, что-то вроде этого для факела (белый — альфа):

центрированная световая маска

  • Для каждой плитки, которая является источником света, вы визуализируете это изображение в позиции источника как наложение.
  • Чтобы добавить немного светлого цвета, вы можете использовать, например. 10% непрозрачного оранжевого цвета вместо полного альфа-канала.

Результаты

результат жесткого освещения маски изображения

Добавляем мягкий свет

Мягкий свет теперь не имеет большого значения, просто используйте больше деталей в световой маске по сравнению с тайлами. Используя только 15% альфы в обычно черной области, вы можете добавить эффект плохой видимости, когда плитка не освещена:

мягкий свет

Вы даже можете легко получить более сложные формы освещения (конусы и т. д.), просто изменив изображение маски.

Несколько источников света

При комбинировании нескольких источников света такой подход приводит к проблеме: рисование двух пересекающихся друг с другом масок может компенсировать друг друга:

отмена маски

Мы хотим, чтобы они добавляли свои источники света, а не вычитали их. Как избежать проблемы:

  • Инвертировать все светлые маски (где альфа-темные области, непрозрачные - светлые)
  • Визуализируйте все эти световые маски во временное изображение, которое имеет те же размеры, что и окно просмотра.
  • Инвертируйте и визуализируйте новое изображение (как если бы это была единственная световая маска) по всему пейзажу.

Это приведет к чему-то похожему на это: result image

Код для метода инвертирования маски

Предполагая, что вы сначала визуализируете все плитки в 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);
}

Как уже отмечалось, это примитивный пример. Реализация смешивания цветов может потребовать немного больше усилий.

person bricklore    schedule 09.12.2013

Raytracing может быть самым простым подходом.

  • вы можете сохранить, какие плитки были просмотрены (используется для автокарты, используется для «запоминания вашей карты во время ослепления», может быть, для мини-карты и т. д.)
  • вы показываете только то, что видите — возможно, монстр стены или холм загораживает вам обзор, тогда трассировка лучей останавливается в этой точке
  • удаленные «светящиеся объекты» или другие источники света (факелы лавы) можно увидеть, даже если ваш собственный источник света не достигает очень далеко.
  • длина вашего луча будет использоваться для проверки количества света (затухающий свет)
  • может быть, у вас есть специальный датчик (ESP, обнаружение золота/еды), который будет использоваться для поиска объектов, которые не находятся в вашем поле зрения? трассировка лучей тоже может помочь ^^

как это делается легко?

  • проведите линию от вашего игрока до каждой точки границы вашей карты (используя алгоритм Bresehhams http://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm идите по этой линии (от вашего персонажа до конца), пока ваш обзор не будет заблокирован; в этот момент остановите поиск (или, возможно, сделайте последнюю итерацию, чтобы увидеть, что превзошел тебя)
  • для каждой точки на вашей линии установите освещение (возможно, 100% для расстояния 1, 70% для расстояния 2 и т. д.) и отметьте плитку карты как посещенную.

может не будешь ходить по всей карте, может хватит если поставить свой рейтрейс на вид 20х20? ПРИМЕЧАНИЕ: вам действительно нужно ходить по границам окна просмотра, НЕ требуется отслеживать каждую точку.

я добавляю линейный алгоритм, чтобы упростить вашу работу:

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;
}

Я сделал всю эту молниеносную штуку некоторое время назад, * 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;
        }

    }
}
person Martin Frank    schedule 12.12.2013
comment
Отличный ответ очень полезен. Что будет в блоке /* цикла */ внизу функции getLine()? - person matthewtory; 12.12.2013
comment
Кроме того, в чем смысл переменных «err» и «err2», если они не используются в операторе return или вне функции? - person matthewtory; 12.12.2013
comment
привет grimrader22, цикл 'for(;;)' является бесконечным циклом, он останавливается только с помощью оператора 'break'. честно говоря, говоря об этих двух переменных ошибок, я не могу вспомнить, зачем их использовать, но я почти уверен, что в besenham wiki это описано. - person Martin Frank; 13.12.2013

Я занимаюсь разработкой инди-игр уже около трех лет. Я бы сделал это, прежде всего, используя OpenGL, чтобы вы могли получить все преимущества графической вычислительной мощности графического процессора (надеюсь, вы уже это делаете). Предположим, мы начинаем со всех плиток в VBO, полностью освещенных. Теперь есть несколько вариантов достижения желаемого. В зависимости от того, насколько сложна ваша система освещения, вы можете выбрать другой подход.

  • Если ваш свет будет круговым вокруг игрока, независимо от того, будут ли препятствия блокировать свет в реальной жизни, вы можете выбрать алгоритм освещения, реализованный в вершинном шейдере. В вершинном шейдере вы можете вычислить расстояние от вершины до игрока и применить некоторую функцию, которая определяет, насколько яркими должны быть объекты в зависимости от вычисленного расстояния. Не используйте альфу, а просто умножьте цвет текстуры/плитки на значение освещения.

  • Если вы хотите использовать пользовательскую карту освещения (что более вероятно), я бы посоветовал добавить дополнительный атрибут вершины, определяющий яркость тайла. При необходимости обновите VBO. Здесь тот же подход: умножьте пиксель текстуры на значение света. Если вы рекурсивно заполняете свет положением игрока в качестве отправной точки, тогда вы будете обновлять VBO каждый раз, когда игрок движется.

  • Если ваша карта освещения зависит от того, где солнечный свет падает на ваш уровень, вы можете комбинировать два типа методов освещения. Создайте один атрибут вершины для яркости солнца и другой атрибут вершины для света, излучаемого световыми точками (например, факел, который держит игрок). Теперь вы можете объединить эти два значения в вершинном шейдере. Предположим, ваше солнце восходит и заходит, как день и ночь. Допустим, яркость солнца равна sun, то есть значению от 0 до 1. Это значение можно передать вершинному шейдеру как юниформу. Атрибут вершины, который представляет яркость солнца, равен s, а атрибут света, излучаемого световыми точками, — l. Затем вы можете вычислить общий свет для этой плитки следующим образом:

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

    Где flicker (также униформа вершинного шейдера) — это своего рода волнистая функция, которая представляет небольшие вариации яркости ваших световых точек.
    Такой подход делает сцену динамичной без необходимости постоянно воссоздавать VBO. Я реализовал этот подход в проекте проверки концепции. Это прекрасно работает. Посмотреть, как это выглядит, можно здесь: http://www.youtube.com/watch?v=jTcNitp_IIo. Обратите внимание, как свет факела мерцает на 0:40 видео. Это делается тем, что я объяснил здесь.

person Martijn Courteaux    schedule 11.12.2013