Тонировка пикселей в Java - нужен более быстрый метод - PullRequest
0 голосов
/ 05 декабря 2018

Я делаю псевдо-3D игру в стиле дум.Мир отображается пиксель за пикселем в буферизованное изображение, которое впоследствии отображается на JPanel.Я хочу сохранить этот подход, чтобы освещение отдельных пикселей было легче.

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

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

Это моя функция для тонирования пикселей:

//Change the color of the pixel using its brightness.
public static int tintABGRPixel(int pixelColor, Color tintColor) {
    //Calculate the luminance. The decimal values are pre-determined.
    double lum = ((pixelColor>>16 & 0xff) * 0.2126 +
                 (pixelColor>>8 & 0xff) * 0.7152 +
                 (pixelColor & 0xff) * 0.0722) / 255;

    //Calculate the new tinted color of the pixel and return it.
    return ((pixelColor>>24 & 0xff) << 24) |
           ((int)(tintColor.getBlue()*lum) & 0xff) |
           (((int)(tintColor.getGreen()*lum) & 0xff) << 8) |
           (((int)(tintColor.getRed()*lum) & 0xff) << 16);
}

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

Она содержит только простые операции, но эта функция вызывается до миллиона раз за кадрв худшем случае.Узким местом является расчет в операторе возврата.

Существует ли более эффективный способ расчета нового цвета?Было бы лучше, если бы я изменил свой подход?

Спасибо

Ответы [ 3 ]

0 голосов
/ 05 декабря 2018

Чтобы повысить производительность, вам придется избавляться от таких объектов, как Color, во время манипуляции с изображениями, даже если вы знаете, что метод должен вызываться миллион раз (image.width * image.height раз), тогда лучше встроить этот метод,В общем случае JVM, вероятно, встроит сам этот метод, но вы не должны рисковать.

Вы можете использовать PixelGrabber , чтобы получить все пиксели в массив.Вот общее использование

final int[] pixels = new int[width * height];
final PixelGrabber pixelgrabber = new PixelGrabber(image, 0, 0, width, height, pixels, 0, 0);

for(int i = 0; i < height; i++) {
    for(int j = 0; j < width; j++) {
        int p = pixels[i * width + j]; // same as image.getRGB(j, i);

        int alpha = ( ( p >> 24) & 0xff );
        int red = ( ( p >> 16) & 0xff );
        int green = ( ( p >> 8) & 0xff );
        int blue = ( p  & 0xff );

        //do something i.e. apply luminance
    }
}

Выше приведен пример итерации индексов строк и столбцов, однако в вашем случае вложенный цикл не требуется.Это должно значительно повысить производительность.

Вероятно, это можно распараллелить, также легко используя потоки Java 8, однако будьте осторожны перед использованием потоков при работе с изображениями, поскольку потоки намного медленнее, чем простые старые циклы.

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

0 голосов
/ 05 декабря 2018

Выполняйте работу в параллельном режиме

Потоки не обязательно являются единственным способом распараллеливания кода, в процессорах часто есть наборы инструкций, такие как SIMD, которые позволяют вам вычислять одну и ту же арифметику для нескольких чисел одновременно.Графические процессоры берут эту идею и работают с ней, позволяя вам запускать одну и ту же функцию параллельно от сотен до тысяч номеров.Я не знаю, как сделать это в Java, но я уверен, что с помощью некоторого поиска в Google можно найти метод, который работает.

Алгоритм - Делать меньше работы

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

Возможные варианты:

  • Уменьшите окно / разрешение игры.
  • Работайте с другим представлением.Вы выполняете много операций, которые легче выполнять, когда пиксели - это HSV вместо RGB?Затем конвертируйте в RGB только тогда, когда вы собираетесь визуализировать пиксель.
  • Используйте ограниченное количество цветов для каждого пикселя.Таким образом, вы можете заранее определить возможные оттенки, и это всего лишь поиск, в отличие от вызова функции.
  • Оттенки как можно меньше.Может быть, есть какой-то пользовательский интерфейс, который тонирован и не должен быть.Возможно световые эффекты только путешествуют.
    • В крайнем случае, сделайте тонированное по умолчанию.Если тонирование пикселей выполняется так много, то, возможно, «отрисовка» происходит гораздо реже, и вы можете добиться более высокой производительности, делая это.

Производительность - (микро-) оптимизация кода

Если вы можете согласиться на «приблизительный оттенок» , этот ответ SO дает приближение для яркости (яркости) пикселя, который должен быть дешевле для вычисления.(Формула из ссылки: Y = 0,33 R + 0,5 G + 0,16 B, что можно записать как Y = (R + R + B + G + G + G) / 6.)

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

С этого момента мы будем считать, что функция, представленная в вопросе, занимает больше всего времени.Давайте посмотрим, на что он тратит свое время.У меня нет остальной части вашего кода, поэтому я не могу его сравнить, но я могу скомпилировать его и посмотреть на полученный байт-код.Используя javap для класса, содержащего функцию, я получаю следующее (байт-код обрезан там, где есть повторы).

public static int tintABGRPixel(int, Color);
    Code:
       0: iload_0
       1: bipush        16
       3: ishr
       4: sipush        255
       7: iand
       8: i2d
       9: ldc2_w        #2                  // double 0.2126d
      12: dmul
      13: iload_0
      ...
      37: dadd
      38: ldc2_w        #8                  // double 255.0d
      41: ddiv
      42: dstore_2
      43: iload_0
      44: bipush        24
      46: ishr
      47: sipush        255
      50: iand
      51: bipush        24
      53: ishl
      54: aload_1
      55: pop
      56: invokestatic  #10                 // Method Color.getBlue:()I
      59: i2d
      60: dload_2
      61: dmul
      62: d2i
      63: sipush        255
      66: iand
      67: ior
      68: aload_1
      69: pop
      ...
      102: ireturn

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

Общий метод, чтобы увидеть, внесло ли изменение улучшение, состоит в измерении кода до и после.С этим знанием вы можете решить, стоит ли вносить изменения.Как только производительность станет достаточно хорошей, остановитесь.

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

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

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

Некоторое время я смотрел на байт-код, и все, что я заметил, это то, что в строках 8 - 42 много операций с плавающей запятой.Этот раздел кода отрабатывает lum (яркость).Кроме этого, больше ничего не выделяется, поэтому давайте перепишем код с нашей первой эвристической идеей.Если вам не нужны объяснения, я предоставлю окончательный код в конце.

Давайте просто рассмотрим, каким будет синий цвет (который мы обозначим B) к концу функции.,Изменения будут применяться и к красному и к зеленому цветам, но мы будем их кратко исключать.

double lum = ((pixelColor>>16 & 0xff) * 0.2126 +
             (pixelColor>>8 & 0xff) * 0.7152 +
             (pixelColor & 0xff) * 0.0722) / 255;
...
... | ((int)(tintColor.getBlue()*lum) & 0xff) | ...

Это можно переписать как int x = (pixelColor >> 16 & 0xff), y = (pixelColor>> 8 & 0xff), z = (pixelColor & 0xff);double a = 0,2126, b = 0,7152, c = 0,0722;двойной свет = (a x + b y + c * z) / 255;int B = (int) (tintColor.getBlue () * lum) & 0xff;

Мы не хотим делать так много операций с плавающей запятой, поэтому давайте сделаем некоторый факторинг.Идея состоит в том, что 0.2126 можно записать как 2126 / 10000.

int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z;
double temp = (double)(tintColor.getBlue() * top) / 10000 / 255;
int B = (int)temp & 0xff;

Так что теперь мы делаем три умножения целых чисел (imul) вместо трех dmuls.Стоимость - одно дополнительное плавающее деление, которое само по себе, вероятно, не стоило бы того.Но мы можем решить эту проблему, объединив два последовательных деления.Мы также можем настроить код для еще одной оптимизации, переместив приведение и деление на одну строку.

int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z);
int temp = (int)((double)(tintColor.getBlue()*top) / 2550000);
int B = temp & 0xff;

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

int x = (pixelColor>>16 & 0xff), y = (pixelColor>>8 & 0xff), z = (pixelColor & 0xff);
int a = 2126, b = 7152, c = 722;
int top = a*x + b*y + c*z;
int Btemp = (int)(( * top * 1766117501L) >> 52);
int B = temp & 0xff;

где магические числа равны двум, которые были замаскированы, когда я скомпилировал версию кода на c ++ с помощью clang.Я не могу объяснить, как создать это волшебство, но оно работает, насколько я тестировал, с парой значений для x, y, z и tintColor.getBlue ().При тестировании я предполагал, что все значения находятся в диапазоне от 0 до 256, и я попробовал только пару примеров.

Окончательный код приведен ниже.Имейте в виду, что это не очень хорошо проверено и может иметь пропущенные края, так что дайте мне знать, если есть какие-либо ошибки.Надеюсь, это достаточно быстро.

public static int tintABGRPixel(int pixelColor, Color tintColor) {
    //Calculate the luminance. The decimal values are pre-determined.
    int x = pixelColor>>16 & 0xff, y = pixelColor>>8 & 0xff, z = pixelColor & 0xff;
    int top = 2126*x + 7252*y + 722*z;
    int Btemp = (int)((tintColor.getBlue() * top * 1766117501L) >> 52);
    int Gtemp = (int)((tintColor.getGreen() * top * 1766117501L) >> 52);
    int Rtemp = (int)((tintColor.getRed() * top * 1766117501L) >> 52);

    //Calculate the new tinted color of the pixel and return it.
    return ((pixelColor>>24 & 0xff) << 24) | Btemp & 0xff | (Gtemp & 0xff) << 8 | (Rtemp & 0xff) << 16;
}
0 голосов
/ 05 декабря 2018

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

((pixelColor>>24 & 0xff) << 24)

Я не знаю, сэкономит ли это вам кучу времени, но я думаю, что на данный момент это почти все, что вы моглисделайте это с точки зрения микрооптимизации.

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

Если ни одна из вышеперечисленных идей не сработает, я думаю, вам, возможно, придется попробовать перенести вычисления цвета на карту GPU.Это все математика голого металла, которая должна происходить миллионы раз, и именно это делают видеокарты лучше всего.К сожалению, это глубокая тема с большим количеством образования, которое должно произойти, чтобы выбрать лучший вариант.Вот некоторые интересные вещи для исследования:

Я знаю, что некоторые из них - это огромные фреймворки, которые вы не просили.Но они могут содержать другие относительно неизвестные библиотеки, которые вы можете использовать, чтобы отправить эти математические вычисления в графический процессор.Аннотация @Parrallel выглядела так, как будто она может быть наиболее полезной или привязкой JavaCL.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...