Ретро-рендер мечты
2 поста
2 поста
Как-то ковыряясь в очередной самопальной браузерной игре, я задумался о том, где она хранит свои переменные. Очень уж хотелось бабосиков себе в ней намутить. Да, подобные приколы умерли вместе с Artmoney и многие считают, что такие выкрутасы в однопользовательских логальных играх - удел неудачников. Но, пытливый ум он иногда просто бесит :(
Ну в самом деле, если на экране написано, что у меня 12345 золотых, то где-то эти 12345 должны лежать. В обычной игре под виндовс я бы на этом месте запустил старый добрый Cheat Engine или ArtMoney, нашёл значение за несколько проходов и успокоился, но тут всё происходило в браузере, а значит стало интересно.
Игра оказалась собрана на WebAssembly. Я с ним раньше особо близко не общался, поэтому некоторое время ушло просто на то, чтобы понять, куда смотреть. Оказалось, что всё не так страшно: память WASM линейная, а со стороны Javascript её часто можно увидеть в виде знакомых массивов HEAP8, HEAP16, HEAP32, HEAPF32 и прочих. По сути, разные представления одних и тех же байтов.
Ну а дальше задача уже выглядит совсем привычно: берём HEAP, ищем в нём нужное значение, запоминаем индексы, значение в игре меняется - ищем новое только среди найденных ранее. Обычный Cheat Engine, только вместо памяти процесса у нас массив Javascript. Я написал для этого три маленьких сниппета в консоль. Первый проходил по HEAP и сохранял найденные индексы в localStorage, второй проверял только сохранённые позиции и отбрасывал лишнее, третий показывал, что в итоге осталось. На этом, собственно, всё и должно было закончиться.
Но не закончилось :)
В первой игре всё работало прекрасно, а в следующей нужных HEAP32 и компании я просто не нашёл. Хотя понятно, что память у работающего WASM куда-то исчезнуть не могла.
После некоторого ковыряния выяснилась интересная особенность. Если остановить выполнение игры в отладчике, нужный HEAP обнаруживался в локальных переменных одного из call frames. Пока игра стоит на паузе - вот он, можно спокойно читать. Продолжаем выполнение - привычной ссылки на него уже нет. И вот тут мои три прекрасных сниппета превратились в довольно унылое занятие. Остановить игру в debugger, найти нужный frame, выполнить первый код, снять с паузы, что-нибудь изменить в игре, снова остановить, опять найти HEAP, выполнить следующий код...
Причём в некоторых играх всё это хозяйство ещё и живёт не в основной странице, а во фреймах или вообще в воркерах. Работало, но уже начинало напоминать ручной труд там, где его быть совершенно не должно. Поскольку Chrome умеет всё это делать через Debugger Protocol, следующим логичным шагом было переложить работу на расширение, дабы оно само подключалось к выбранной вкладке, ставило выполнение на паузу, перебирало доступные targets и call frames и искало там HEAPы. Так появилась кнопка Pause & Hunt HEAP и вместе с ней первая более-менее удобная версия того, что я назвал WASM Memory Scanner.
Если вы когда-нибудь пользовались Cheat Engine или ArtMoney, объяснять особо нечего. Выбираем вкладку с игрой, жмём Pause & Hunt HEAP, после чего расширение пытается найти доступную память WASM.
Дальше вводим значение и делаем First Scan, отпускаем игру через Resume, меняем искомое значение и после следующего Pause & Hunt делаем Next Scan. Повторяем, пока из тысяч результатов не останется что-нибудь вменяемое.
Причём довольно быстро выяснилось, что одного поиска точного значения мало. Поэтому появились Changed/Unchanged, Increased/Decreased, поиск диапазона, изменение на заданную величину и Unknown initial value. Последний полезен, например, когда есть полоска здоровья, но самого числа мы не знаем: запоминаем начальное состояние, получаем урон и оставляем уменьшившиеся значения. Лечимся - оставляем увеличившиеся.
Тут тоже ничего нового, ровно так memory scanners работали последние лет двадцать. Найденные адреса можно складывать в отдельный список, смотреть их текущее значение, записывать новое и при необходимости включать Freeze. Заодно пришлось сделать индикатор прогресса, потому что первый проход по большому HEAP может занять заметное время, а смотреть несколько минут на неподвижное окно и гадать, работает оно или уже всё, оказалось не очень увлекательно.
Вот здесь самое интересное. Сам факт того, что мы нашли в памяти игры число и успешно его поменяли, ещё совершенно не означает, что мы получили бесконечные деньги, бессмертие и власть над миром. В полностью клиентских и одиночных WASM-играх результат может быть именно таким - значение действительно является частью игровой логики и его изменение сразу на что-то влияет.
С такими играми экспериментировать интереснее всего. В серверных всё сильно зависит от архитектуры. Можно найти локальное количество денег, заменить 100 на 999999 и даже увидеть новый результат на WASM Memory Scanner , а при попытке что-нибудь купить сервер вполне резонно сообщит, что денег у нас как было 100, так и осталось.
Мы поменяли представление на клиенте, а настоящее состояние хранится совсем в другом месте. По той же причине я не вижу особого смысла лезть с этим в нормальные сетевые игры. Помимо возможного нарушения правил конкретной игры, там просто значительно меньше интересного: сервер всё равно главный.
А вот старые и новые однопользовательские браузерные игры, всякие Unity WebGL/WASM проекты, эмуляторы, небольшие игры на Emscripten и вообще WASM, в который хочется посмотреть из любопытства - совсем другое дело. Хотя последнее всё-таки с оговоркой: техническая возможность залезть программе в память не означает автоматически разрешение делать это где угодно. Для экспериментов лучше выбирать свои проекты, локальные игры или то, где подобные вещи не запрещены.
Самое забавное во всей этой истории, что писать расширение я вообще не собирался. Мне нужны были три маленьких Javascript-сниппета, чтобы один раз проверить одну идею. Потом понадобилось автоматически находить HEAP. Потом выяснилось, что его надо выковыривать из остановленных call frames. Потом нашлись workers. Потом захотелось нормального поиска, прогресса, изменения значений, Freeze и интерфейса, которым можно пользоваться не держа рядом инструкцию.
Ну и в какой-то момент стало очевидно, что сниппеты несколько вышли из-под контроля :) Сейчас это уже вполне самостоятельное расширение для Chrome/Chromium. Всё работает локально, никакого сервера нет, исходники открыты, интерфейс на русском и английском.
В репозитории лежит само расширение, инструкция по установке и немного более подробное описание того, что оно делает.
Ссылка на репу: https://github.com/rikovmike/chrome-wasm-memory-scanner
Если кто-нибудь попробует его на какой-нибудь необычной WASM-игре или движке - интересно будет посмотреть на результаты. Особенно если HEAP там не найдётся вообще...
По сути, инструмент может быть полезен в дебаггинге своих игровых разработок, проверке безопасности и своих античитерских наработок, если ваш мегапроект настолько крут, что его нужно от этого защищать:)
UDP: В 99% случаев достаточно сканировать HEAP32, а на остальные забить. В 64, U32, U64 , да и порой в 16 значения просто отзеркалены из HEAP32. Можно не тратить на них время. Но... 1% все же существует :)
Простите, но не удержался, чтобы не вставить классику :)
Что то я подзабил на продолжение банкета с ретро-рендерингом и его упрощением, а ведь работы еще достаточно много. Штош, исправляюсь.
Как обычно пара вступительных слов: для ЛЛ ссылка на гитхаб в конце и мой дисклеймер - я ни разу не программист шейдеров, и зачастую понятия не имею, что делаю и зачем, но методом проб и ошибок это начинает работать.
В предыдущей статье мы реализовали довольно неплохой эффект старого рендера из ранних 90х. Однако, ощущения от него будут неполными, если мы не введем другие характерные для того времени ограничения. Нет-нет, 10 фпс и имитацию слабых компьютеров вводить не будем, но постараемся ввести один значимый для ретро-рендера эффект. Возможно он не является частью общей парадигмы, особенно в области рендеринга на РС, но добавит особую атмосферу. Речь идет, конечно же, об эффекте дрожания вершин, характерный для Playstation 1.
Эффект «дрожания вершин» в PlayStation 1, был следствием ограниченной точности расчетов, использовавшихся на этом железе. Поскольку PlayStation 1 использовала фиксированную точку для вычисления координат вершин, это приводило к визуальным артефактам, когда позиции вершин «дрожали» при перемещении камеры или объектов. Мы, как и в прошлый раз, попробуем воссоздать данный эффект только с помощью шейдера в Unity, без использования дополнительных скриптов на C#.
Основная идея простая — нужно будет обойти все доступные шейдеру вершины и округлить их позиции относительно проекционных координат .
Напишем заготовку для шейдера, в которой укажем будущие параметры для материалы и объявим структуры и переменные.
Здесь мы задаём два параметра для будущего материала: _MainTex — текстура и _JitterAmount — слайдер для точной настройки силы эффекта дрожания.
Еще нам, конечно же, понадобятся две структуры:
appdata — данные вершин, передаваемые в вершинный шейдер из Unity
v2f — данные, передаваемые из вершинного шейдера в пиксельный шейдер и, в конечном итоге, в графический конвейер
Добавим функцию округления позиций вершин. Мы применяем округление только к clipPos.xy, то есть к координатам, которые уже находятся в проекционных координатах (после всех трансформаций объекта). Это даст эффект «дрожания», как на PlayStation 1, когда камера или объект движется. Параметр clipPos будет передаваться в функцию, как и параметр jitterAmount, который позволяет контролировать, насколько сильным будет эффект дрожания.
Округление в clip space означает, что каждая вершина будет «привязана» к определенным фиксированным позициям на экране. При движении камеры или объекта из-за этого квантования вершины будут перескакивать с одного положения на другое, создавая желаемый эффект «дрожания».
Пишем «вершинный» блок шейдера.
Здесь мы сначала получаем clipPos функцией UnityObjectToClipPos для нашей вершины. Затем получаем новую позицию вершины через вызов написанной ранее функции JitterVertex, которой передаем полученный clipPos и наш параметр _JitterAmount из инспектора.
Возвращаем новые данные вершины. UV-координаты оставляем без изменений.
Параметр _JitterAmount нужно держать в пределах от 0.01 до 0.1, этого достаточно для заметного эффекта. Однако, никто не запрещает эксперименты — все ваших руках.
Осталось только добавить фрагментную (пиксельную) часть, в которой мы просто отдаем текстуру, как есть.
Полный код шейдера лучше посмотреть в репозитории. Выкладывать картинку сюда было бы издевательством )
Шейдер использует стандартную текстуру, которую можно задать в Unity через инспектор. Текстура рендерится через стандартную функцию tex2D, которая отображает текстуру по UV-координатам, переданным из вершинного шейдера. С помощью бегунка _JitterAmount плавно регулируем эффект дрожания. В зависимости от проекта, свойства самой текстуры настраиваются индивидуально, но стилистически, конечно же, желательно выключить фильтрацию и использовать текстуры с небольшим разрешением, вроде 64х64 или 128х128.
По итогу, мы имеем простой, но эффективный шейдер для добавления эффекта дрожания, которые совместно с шейдером олдскульного рендеринга из прошлой статьи даст нам интересный ламповый эффект ретро-гейминга. Однако это далеко не все.
В следующих статьях мы попытаемся дополнить его необходимыми расширениям, добавив эффекты прозрачности через альфаканал текстуры (или отдельной альфа-текстуры), с учетом особенностей прозрачности ретрорендеров, эффекты искажения текстур и прочие интересные элементы, вроде имитации афинных искажений, которых все еще не хватает в нашем новом шейдере.
Ссылка на github: https://github.com/rikovmike/YetAnotherVertexJitter
2009 год стал для меня незабываемым и ламповым временем, полным ярких моментов и простых радостей. Мы часто собирались с друзьями, вечеринки проходили в домах, где царила атмосфера искренности и веселья. Общение происходило вживую, ведь социальные сети только начинали набирать популярность, и MySpace был одним из любимых мест для обмена музыкой и новостями. В это время мы открывали для себя множество новых групп и направлений, обсуждая их звучание с неподдельным восторгом.
Ностальгия по тем вечерам вызывает у меня тепло в душе: мы вместе смотрели фильмы на DVD, смеялись над комедиями и восхищались фантастикой. Музыка звучала из колонок, создавая особую атмосферу, а угощения были простыми, но ароматными. Часто мы прогуливались по городу, наслаждаясь свежим воздухом и откровенными разговорами. 2009 год стал символом беззаботности и дружбы, когда всё было просто и искренне. Несмотря на скорость изменений в мире, именно этот год оставил в сердце светлые воспоминания и ощущение тепла, а так же полной уверенности, что я напизжу этот текст сегодня.
Самая странная вечеринка в честь Дня рождения, на которой я когда-либо бывал, проходила в заброшенном доме на окраине города. Именинник решил отметить свой день с тематикой "Мистический вечер". Все гости пришли в необычных костюмах: я увидел людей в нарядах вампиров, привидений и даже в костюмах существ из старинных легенд.
Сам дом был украшен странными предметами: паутиной, свечами и загадочными артефактами. Атмосфера была насыщена чем-то необычным, и легкий холодок пробегал по спине. В процессе вечеринки кто-то предложил сыграть в "духовный сеанс", что вызвало бурю эмоций. Мы сели в круг, держась за руки, и попытались вызвать "духа" с помощью старинной Вуду-куклы.
На столе были экзотические закуски, такие как черные резинки и зеленое желе, что добавляло странностей в угощение. Тортик оказался с вкусом лаврового листа, что удивило всех присутствующих.
Вечеринка закончилась неожиданным дождем из искусственного снега, который словно бы спустился с потолка, создавая волшебное, но в то же время беспокойное настроение. Это была одна из самых запоминающихся и странных вечеринок, о которых я когда либо так качественно пиздел.
Вы все просто невероятные!
И ты, и ты, и ты, и вот ты, и даже ты!
Первые 3 дня телефон разряжался быстрее обычного от постоянного потока уведомлений, будто я знаменитость, которой пишут круглые сутки поклонники :) Классные и смешные суммы, вроде 49.50 и приятные сообщения вдогонку. Лучшие дни за последнее время! Спасибо вам всем огромное. Вы все - брестейкинг!
Итак, за 6 дней прилетело потрясающе много!
Сбер 1 - 24 537,74
Тинькоф - 9 136,54
ВТБ - 661,4
Всего 34 335,68
На момент написания поста продолжают прилетать, поэтому итоговая сумма будет чуток побольше. Сейчас предотпускная пора и работы на работе втрое больше - аврал за авралом, задача сделать максимум перед отпускным затишьем, поэтому потратить это все не успел, но на желанное, о чем писал в группе, уже отложено. Коту будет куплен новый точильный домик, псу диванчик, обоим естественно вкусноты. Детей ждет в отпуске сюрприз :)
Как и обещал, продолжу традицию - 10% от собранного будет отправлено победителю 41й фабрики, а именно 3 433,57 рублей.
Всем участникам желаю удачи, сам я совершенно не ждал выигрыша. Меня вон даже искали всю пятницу, думали, что я не существую. С первого раза и не понял, почему мне пишут незнакомые люди, чуть не заблочил за спам. А потом кааак понял :):)
Еще раз спасибо всем! Спасибо Фабрике! Спасибо @TheLaughtingMan! Спасибо родной Пикабу!