Почти все игровые ретроконсоли генерируют цвета в каком-нибудь из вариантов RGB-кодирования.

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

Самым распространённым сегодня видом дисплеев являются ЖК-панели (LCD). Они известны тем, что имеют очень плохие уровни чёрного. Различия между TN, PVA и IPS не слишком на это влияют.

Отдельные фанаты играют на ЭЛТ-мониторах, и всё большую популярность набирают OLED-экраны, особенно на телефонах и планшетах. Но в этой статье мы в основном будем рассматривать ЖК-экраны, хотя данная методика важна для дисплеев любого типа.

Точность цвета


Первая важная деталь: большинство компьютеров работает в режиме 24-битного цвета, обеспечивающем 8-битную детализацию цветов для каналов красного, зелёного и синего. Но в большинстве старых игровых систем цвета не задаются с такой точностью.

Например, Sega Genesis кодирует 9-битные цвета, что даёт по 3 бита на канал.

Самым наивным решением было бы поместить 3 бита в самые старшие 3 бита вывода, а младшие 5 бит оставить пустыми, но при этом белый цвет становится немного серым.

Пример:

000 000 000 -> 000'00000 000'00000 000'00000
111 111 111 -> 111'00000 111'00000 111'00000



Если же заполнить их единицами, то слишком светлым становится чёрный.

Пример:

000 000 000 -> 000'11111 000'11111 000'11111
111 111 111 -> 111'11111 111'11111 111'11111



Решение заключается в том, чтобы повторять исходные биты, чтобы они заполнили все выходные биты.

Пример:

000 -> 000 000 00...
010 -> 010 010 01...
011 -> 011 011 01...
111 -> 111 111 11...

В виде кода:

uint8 red = r << 5 | r << 2 | r >> 1
//rrr00000 | 000rrr00 | 000000rr -> rrrrrrrr

Эмуляция экрана


Игровые ретросистемы не были предназначены для работы на современных ЖК-мониторах компьютеров. Обычно домашние консоли были рассчитаны на ЭЛТ-экраны, а в портативных консолях применялись гораздо более старые и менее точные ЖК-панели.

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

Мониторы PC


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

Эмуляция ЭЛТ: Super Nintendo


Основное различие между ЭЛТ-экранами и ЖК-мониторами компьютеров заключается значительно сниженных уровнях чёрного, что можно только немного компенсировать при помощи кривой гамма-коррекции:

//SNES colors are in RGB555 format, so there are 32 levels for each channel
static const uint8 gammaRamp[32] = {
  0x00, 0x01, 0x03, 0x06, 0x0a, 0x0f, 0x15, 0x1c,
  0x24, 0x2d, 0x37, 0x42, 0x4e, 0x5b, 0x69, 0x78,
  0x88, 0x90, 0x98, 0xa0, 0xa8, 0xb0, 0xb8, 0xc0,
  0xc8, 0xd0, 0xd8, 0xe0, 0xe8, 0xf0, 0xf8, 0xff,
};

Эта таблица позаимствована у Overload of Super Sleuth / Kindred. Она затеняет нижнюю половину цветовой палитры, оставляя верхнюю часть неизменной.

Это оказывает потрясающее воздействие на изображение при эмуляции: сверху показан оригинал, снизу — изображение с применённой гамма-коррекцией:





Эмуляция ЖК: Game Boy Advance


Game Boy Advance имел один из худших ЖК-экранов с совершенно блеклыми цветами. Хитрые разработчики поняли, что значительно преувеличив цвета, можно получить на реальном оборудовании более приятные результаты.

Разумеется, если использовать эти цвета на стандартном ЖК-мониторе, то результат окажется пёстрым кошмаром. К счастью, мы можем компенсировать и это, создав достаточно естественные цвета:

double lcdGamma = 4.0, outGamma = 2.2;
double lb = pow(B / 31.0, lcdGamma);
double lg = pow(G / 31.0, lcdGamma);
double lr = pow(R / 31.0, lcdGamma);
r = pow((  0 * lb +  50 * lg + 255 * lr) / 255, 1 / outGamma) * (0xffff * 255 / 280);
g = pow(( 30 * lb + 230 * lg +  10 * lr) / 255, 1 / outGamma) * (0xffff * 255 / 280);
b = pow((220 * lb +  10 * lg +  50 * lr) / 255, 1 / outGamma) * (0xffff * 255 / 280);

Этот фрагмент кода написан Talarubi.

Намного более разительный контраст по сравнению с ЭЛТ — сверху оригинал, снизу версия с цветокоррекцией:





Эмуляция ЖК: Game Boy Color


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

В эмуляторах Game Boy Color достаточно популярен такой алгоритм:

R = (r * 26 + g *  4 + b *  2);
G = (         g * 24 + b *  8);
B = (r *  6 + g *  4 + b * 22);
R = min(960, R) >> 2;
G = min(960, G) >> 2;
B = min(960, B) >> 2;

К сожалению, я не знаю, кто его написал алгоритм. Если вы знаете, то сообщите мне, чтобы я мог указать авторство!

Как и раньше, оригинал слева, версия с цветокоррекцией — справа:



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

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

В заключение


Существует ещё много систем, которым пока не хватает хороших фильтров эмуляции цветов.
Их очень трудно настраивать. Из самых важных примеров можно указать WonderSwan и Neo Geo Pocket, у которых на момент написания статьи не было хороших фильтров аппроксимации цветов.

С портативными консолями всё ещё сложнее, потому что в них часто отсутствует задняя подсветка (а иногда и передняя подсветка!) и есть способы изменения контраста, благодаря чему нет какого-то истинного значения «цвета» для конкретного значения RGB.

Особо интересным пограничным случаем является WonderSwan Color, в котором есть программно устанавливаемый флаг для повышения контраста выводимого изображения.
Пока мы не знаем, как достоверно эмулировать такое поведение, и непонятно, сможем ли вообще.

Эмуляция цветов — это область, требующая больше внимания, поэтому если вы специалист в математике и анализе цветов, то ваша помощь очень бы пригодилась сцене эмуляции!

Комментарии (15)


  1. GCU
    17.09.2019 13:58

    Решение заключается в том, чтобы повторять исходные биты, чтобы они заполнили все выходные биты.

    uint8 red = r << 5 | r << 2 | r >> 1

    Как-то заумно для r*255/7


    1. Fragster
      17.09.2019 14:39
      -1

      Зато сииииильно быстрее.


      1. GCU
        17.09.2019 16:05

        Возможно, но не факт.
        LUT, как написал DimPal ниже — может оказаться ещё быстрее.
        Тем более что 3 бита*3 канала — это всего 512 записей и не нужно возиться с каналами отдельно.


  1. DimPal
    17.09.2019 15:24
    +1

    А чем LUT (lookup table) не устроила?


    1. khim
      18.09.2019 02:53

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

      Не забывайте, что задержка даже в L1 на современных процессорах — это 3-4 такта. А вычисления описанные можно сразу для трёх каналов делать.

      Если же вы сделаете одну таблицу сразу на три канала — то сожрёте сразу изрядный кусок L1.


      1. DimPal
        18.09.2019 13:03

        Так то да… Но и вы не забывайте что эта LUT нужна для перелопачивания экранного буфера, вероятность что кэш будет прогреваться по максимуму более чем высока.


        1. khim
          18.09.2019 19:23

          Зависит от того, как эмулятор устроен. Если хранить картинку в пересчитанном виде (чтобы её было удобнее выводить), то на одну операцию с цветом будет приходиться масса других операций.


  1. DimPal
    18.09.2019 13:08
    +1

    Кстати а GPU для такой задачи никак нельзя задействовать?


    1. GCU
      18.09.2019 13:46
      -1

      Конечно можно :), преобразования цветов прекрасно работают на GPU (во фрагментном шейдере OpenGL например).

      Но для современных устройств эмуляция 5 МГц процессора с выводом даже 320х240 пикселей 30 кадров в секунду не особо проблема производительности.

      Более накладные в вычислительном плане и лучше подходящие для GPU операции, на мой взгляд, в статье не описаны:

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


      1. mistergrim
        18.09.2019 17:16

        30 кадров в секунду
        Это на современных консолях 30, а раньше было 60.

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


        1. GCU
          18.09.2019 17:36

          Увы, статья не относится к _точной_ эмуляции, поскольку на выходе консолей не было пикселей, а был ТВ сигнал.

          Старые консоли подключались к телевизорам через PAL или NTSC, и частота кадров также телевизионная. Честных 60 кадров там точно не было :).


          1. mistergrim
            18.09.2019 17:38

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


            1. GCU
              18.09.2019 18:06

              Ну я так понял что статья о «точной» эмуляции вывода картинки только.

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

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


              1. mistergrim
                18.09.2019 18:22

                Извините, но, например ru.wikipedia.org/wiki/Super_Nintendo_Entertainment_System#Технические_спецификации
                Как мне кажется, тут отнюдь не только ТВ-сигнал.


                1. GCU
                  18.09.2019 23:42

                  Я не имел ввиду "только ТВ сигнал", эмулятор не ограничен одной графикой и состоит из множества систем, точная симуляция которых довольно сложная. Точность эмуляции зависит от системы, не везде на практике нужно cycle accurate. ТВ сигнал является выходом для приставки и само отображение его на экране не является частью приставки как таковой. Я имел ввиду что ответственность эмулятора приставки за вывод графики на этом заканчивается.