Почти все игровые ретроконсоли генерируют цвета в каком-нибудь из вариантов 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)
DimPal
17.09.2019 15:24+1А чем LUT (lookup table) не устроила?
khim
18.09.2019 02:53Это может показаться странным, но даже такую функцию на современных процессорах может оказаться быстрее посчитать.
Не забывайте, что задержка даже в L1 на современных процессорах — это 3-4 такта. А вычисления описанные можно сразу для трёх каналов делать.
Если же вы сделаете одну таблицу сразу на три канала — то сожрёте сразу изрядный кусок L1.DimPal
18.09.2019 13:03Так то да… Но и вы не забывайте что эта LUT нужна для перелопачивания экранного буфера, вероятность что кэш будет прогреваться по максимуму более чем высока.
khim
18.09.2019 19:23Зависит от того, как эмулятор устроен. Если хранить картинку в пересчитанном виде (чтобы её было удобнее выводить), то на одну операцию с цветом будет приходиться масса других операций.
DimPal
18.09.2019 13:08+1Кстати а GPU для такой задачи никак нельзя задействовать?
GCU
18.09.2019 13:46-1Конечно можно :), преобразования цветов прекрасно работают на GPU (во фрагментном шейдере OpenGL например).
Но для современных устройств эмуляция 5 МГц процессора с выводом даже 320х240 пикселей 30 кадров в секунду не особо проблема производительности.
Более накладные в вычислительном плане и лучше подходящие для GPU операции, на мой взгляд, в статье не описаны:
В этой статье мы не будем рассматривать артефакты экранов, такие как кривизна экрана, строки развёртки, хроматическая аберрация, межкадровое смешение, апертурные решётки и т.д.
mistergrim
18.09.2019 17:1630 кадров в секунду
Это на современных консолях 30, а раньше было 60.
Также, если мы делаем не просто эмуляцию, а _точную_ эмуляцию, требования к производительности вырастают на порядок.GCU
18.09.2019 17:36Увы, статья не относится к _точной_ эмуляции, поскольку на выходе консолей не было пикселей, а был ТВ сигнал.
Старые консоли подключались к телевизорам через PAL или NTSC, и частота кадров также телевизионная. Честных 60 кадров там точно не было :).mistergrim
18.09.2019 17:38Речь не о точной эмуляции вывода картинки, а об эмуляции всего железа, так называемой cycle accurate.
GCU
18.09.2019 18:06Ну я так понял что статья о «точной» эмуляции вывода картинки только.
По сути эмуляция приставки на ТВ сигнале и заканчивается, дальше уже идёт эмуляция дисплея/телевизора, что вообще отдельная тема.
Эмуляция всего железа cycle accurate для игрушек с приставки на мой взгляд особо и не нужна, те же эмуляторы PS1 заметно лучше оригинала в плане графики.mistergrim
18.09.2019 18:22Извините, но, например ru.wikipedia.org/wiki/Super_Nintendo_Entertainment_System#Технические_спецификации
Как мне кажется, тут отнюдь не только ТВ-сигнал.GCU
18.09.2019 23:42Я не имел ввиду "только ТВ сигнал", эмулятор не ограничен одной графикой и состоит из множества систем, точная симуляция которых довольно сложная. Точность эмуляции зависит от системы, не везде на практике нужно cycle accurate. ТВ сигнал является выходом для приставки и само отображение его на экране не является частью приставки как таковой. Я имел ввиду что ответственность эмулятора приставки за вывод графики на этом заканчивается.
GCU
Как-то заумно для r*255/7
Fragster
Зато сииииильно быстрее.
GCU
Возможно, но не факт.
LUT, как написал DimPal ниже — может оказаться ещё быстрее.
Тем более что 3 бита*3 канала — это всего 512 записей и не нужно возиться с каналами отдельно.