Чуть-чуть страстей по теням, или как я провел выходные.
Как известно, отрисовка теней в двойке и в тройке заметно различается.
Видно, что троечные тени - серые, а двоечные - цветные, но как это выразить формально?
Можно предположить, что тройка использует альфа-смешивание (обычную полупрозрачность), а двойка - изменение яркости/насыщенности, но лучше бы знать точно.
Для подробностей вспомним техническую реализацию.
Тройка сделана уже в труколоре (шестнадцатибитный цвет, RGB565), но еще для машин прошлого века: если заглянуть под капот, то можно увидеть, что для смешивания используется альфа-прозрачность, но реализованная на битовых масках, потому-то в тройке и очень дискретные шаги, дискретнее чем в двойке (за исключением терры инкогниты, где остались четыре цвета тени, правда, реализованные на костылях типа наложения тени на тень).
Код
case 1u: //полутень
case 2u:
v70 = v67;
do
{
LOWORD(v69) = *(_WORD *)v58; //берем пиксель из скринбуфера
v58 += 2;
LOWORD(v62) = mask_0011100111100111[0] & ((_WORD)v69 >> 2); //это такое деление компонентов цвета на четыре
LOWORD(v69) = (_WORD)v69 >> 1;
v69 &= *(_DWORD *)&mask_0111101111101111; //а эти две строки - на два
v62 += v69; //складываем, получаем три четверти исходного пикселя, четверть черного мрака бесконечности
--v70;
*(_WORD *)(v58 - 2) = v62;
}
while ( v70 ); //цикл разжатия RLE, неважно
v62 = v89;
break;
case 3u:
case 4u: //тень
v71 = v67;
do
{
LOWORD(v68) = *(_WORD *)v58;
v58 += 2;
LOWORD(v68) = (_WORD)v68 >> 1;
v68 &= *(_DWORD *)&mask_0111101111101111;
--v71;
*(_WORD *)(v58 - 2) = v68;
}
while ( v71 );
break;
default:
v58 += 2 * v67;
break;
}
Надо проверять, конечно, но навскидку я не очень уверен в том, что это прям до бита корректно. Впрочем, за пятнадцать лет никто не докопался, и ладно.
Ну там с нюансами, конечно, где-то в бою, кстати, и реальная полупрозрачность есть, но в данном случае неважно.
И вообще неважно: даже если вставить честный расчет альфы, то, чуть-чуть поигравшись с коэффициентами, становится ясно, что двойкоподобного результата не достичь все равно, и тройкоалгоритм становится интересным только с точки зрения "а как бы его ловчее отпилить".
В двойке же все интереснее.
Вспоминая, что двойка вообще-то насквозь палитренная, и
цвета там идут однотонными блоками, от светлого к темному, можно сразу заподозрить, что затенение реализуется просто сдвигом по палитре. Окей, пусть так, но на сколько именно сдвигается - учитывая, что блоки разной длины?
Поизучав HEROES2W.C, недолго и безрезультатно, я решил обратиться к эмпирическому методу. Орзи добавил в двойку объект, состоящий из 256 квадратиков со всеми цветами палитры и четыре затенялки, и путем нехитрых манипуляций в редакторе я получил такую вот картинку:

Замечательно, гипотеза насчет сдвига по палитре подтверждена. Можно даже заметить пару тонких деталей — например, насчет колорциклинга в двойке: лава или оперение феникса под тенью не только продолжают анимироваться, но и не темнеют (типа, самосветятся), а вот вода или хвост джинна - замирают и теряют оттенки — но яснее с этого не стало: сдвиги разнообразны.
Ада в котел подбавляет еще и тот приятный факт, что даже если мы выведем формулу для преобразования двоечного цвета, от которой процессор не будет рыдать слезами при ветвистой арифметике на каждом затененном пикселе, то моду для _тройки_ это не очень поможет.
Мы не очень палитренные пуристы (например, где-то в лодах валяется ксинская накладнуха молодой травы с отсутствующими в палитре ярко-зелеными оттенками); что еще более печально, до недавнего времени таковым совсем не был дефтул, и у дефов могут быть самые разнообразные палитры, отличающиеся от канонной по мелочи, но часто. Местами есть даже наколдованный из ниоткуда дитеринг, угу.
То есть, нам нужно преобразовывать не только двоечный индекс в палитре в другой индекс в палитре, а в принципе, любой 16-битный цвет в затененный. В двух вариантах, а в перспективе - в четырех. Причем делать это с малыми накладными расходами: насколько я помню свои когдатошние эксперименты с альфойдлл, даже современные процессоры (особенно _современные_ процессоры) не в восторге от нескольких флоатов на пиксель.
Ладно, давайте вернемся к гипотезе насчет HSV-коррекции.
Я разобрал вот эту вот картинку на таблицу 256 x (5*RGB): строка - индекс, пятнадцать колонок для красного-синего-зеленого в пяти вариантах.
Потом добавил еще пятнадцать колонок для HSV.
Посмотрел. Плюнул. Свернул.
Да, действительно, h сохраняется - по крайней мере, в общих чертах, а вот s и v изменяются довольно бодро, и, что печально, нелинейно. Что еще более печально, по-разному на разных тоновых диапазонах. Восхитительно, гипотетическая функция становится все развесистее и развесистее.
Тем временем мне пришла в голову замечательная мысль насчет накладных расходов: а давайте заменим вычисления - выборкой!
В конце концов, чем набор _всех_ шестнадцатибитных цветов не обычная палитра, изменения в которой можно предвычислить? Да, мы докинем затрат на память, но в 2k17 ли о ней беспокоиться? Расходы памяти на таблицы - 4*65536*2 = 524288 байт = всего-то полмегабайта, а накладные на пиксель - одна операция выборки (
ладно, еще у нас будет дрочиться процессорный кэш, но яжбыдлокодер-шарпятник, мне слов-то таких знать не положено).
Это хорошая идея, и это снимает ограничения по изощренности преобразований, но сами эти преобразования еще надо придумать.
А какое у нас контрольное решение для случаев, когда надо думать? Правильно, "поручить подчиненному". Вот машина железная, пусть она и думает. Иными словами, я решил натравить на двоечную палитру нейросетку и заставить выдать её расширенную версию теней для всей троечной палитры.
Вообще, нейросети - не единственная обучаемая моделька, которая существует в природе, и даже не единственная, о которой я помнил. Есть, например, еще более черномагические генетические алгоритмы (в схем-теорему я могу только верить), но они работают на дискретных данных типа графов и деревьев, а цвет, все же, диапазон. Или "скучные" неэвристические методы типа регрессионного/ковариационного анализа, но все равно я даже ради неследования моде не буду долбаться вокруг переформулирования классической задачи из лаб по простеньким перцепетронным нейронкам "есть три входа, есть три выхода, есть функциональная зависимость между ними, нам нужно аппроксимировать результат поточнее, а вот вид функции не интересен" для чего-то еще. Хотя, конечно, нейросети сейчас хуже вейпа.
На этой подготовительной работе пятница окончательно закончилась вместе с моим сознанием.
С утра я нашел у себя старый софт со времен соответствующего вузовского предмета, Deductor Lite, академверсию. Штука довольно интуитивная, в отличие от нарытого в интернетах прочего говна, но в академверсии обладающая рядом серьезных ограничений.
Подготовив тестовый набор - тупо текстовик с шестью колонками: RGB исходные, RGB затененные и двумястами строками (двоечная палитра без спецслучаев), я загрузил его, скормил нейросетке (трехслойная, девять нейронов в скрытом слое), успешно обучил (десять тысяч раундов - две минуты), и стал пытаться заставить выплюнуть данные о том, а как бы нарисовали двойкодизайнеры тень для 65536 цветов троечного труколора.
Режим "what-if" есть, поодиночке вроде гоняет, вот кнопка "загрузить из Excel".. "ЭТОТ ФУНКЦИОНАЛ НЕДОСТУПЕН В АКАДЕМИЧЕСКОЙ ВЕРСИИ".
Вас когда-нибудь били веслом по голове?(с)
За ночь софт по нейронкам в интернетах не стал дешевле и лучше, и даже виденный вчера на гитхабе простенький набор шарповых классов куда-то продевался. Но я все равно попытался, впрочем, безуспешно.
Ничего, где наша не пропадала. Deductor дает выгружать обработанные исходные данные, и умеет делить их на обучающую выборку и контрольные примеры, причем в том числе и по ключу исходных данных.
Таблица распухает на два столбца — и на шестьдесят пять тысяч строк вида "шестнадцатибитное значение цвета - выконвертированное R - выконвертированное G - выконвертированное B - 0 - 0 - 0 - true"
Типа, так

Вновь скармливаю таблицу дедуктору, запускаю обучение и матюгаюсь. Каждый раунд эта сволочь, негуманная к нищим программистам, не только обучается на выборке, но еще и рассчитывает контрольную группу. А контрольная группа, напомню, это у нас 65536 строк. А нейронку я сделал с 18 нейронами в промежуточном слое, потому что так меньше пляшет отклонение. Ну и вот этот неметафорический миллион сигмоидов считается понятно сколько.
А чо делать. Будем считать. Главное, что не руками и не головой, запустил да пошел чай пить.
О десяти тысячах раундов речь не идет, да и бессмысленно это, с 18 нейронами оно учится довольно быстро. Полторы-две тысячи, ~30-45 минут расчета. Я уже писал, что мне _определенно_ надо что-то решать с машиной.
Опять же, радости добавил эпизод с косяком в исходных данных.
Смотрю я на второй пачке, что ошибка падает слишком медленно. Поигрался с количеством нейронов, думал, мало ли, переобучение случилось, ничего не добился, оставил считать. Экспортирую, и смотрю, что эксель покосорезил строчки, сунув кусок рассчитываемых примеров в обучающую выборку. Эх. Чая я, в общем, обпился.
Зато, собрав по данным нейросетки палитры, я увидел пять замечательных рисунков.

первая - оригинальные цвета, номер в битмапе соответствует взятому как little-endian RGB565 значению цвета.
Между прочим, про эндианы я сообразил только сейчас, надо перепроверить, все ли верно.четыре других - наложение тени, поверх тж брошены пиксели эталонной выборки и четыре оригинальных цвета лавы - я решил сохранить эффект свечения затененного лавового ландшафта.
Быстрая проверка в игре, буквально кодом уровня
Код
int __stdcall Umbrae(LoHook* h, HookContext* c)
{
unsigned short cl = *(unsigned short*)(c->edi); //цвет пикселя
c->ecx = umbrae[cl&0xFFFF]; //вот эти вот красивые массивы
return EXEC_DEFAULT;
}
/*(это неправильно, потому что там компилятор наоптимизировал декремент указателя и взятие данных из памяти так, что на самом деле при таком подходе берутся данные на одно машслово раньше)*/
//завтра, всё завтра
показывает что

допустим, работает с натяжками
а еще в процессе я убил два часа о совершенно идиотскую ошибку, посчитав себя умнее sizeof и написав fread(umbrae, 2, 65535, stream );, из-за чего некоторые пиксели затененного снега становились черными.