Как было реализовано пламя в Doom на Playstation

в 5:03, , рубрики: DOOM, PlayStation 1, psx, Демосцена, Игры и игровые приставки, обратная разработка, портирование, разработка игр, реверс-инжиниринг
Как было реализовано пламя в Doom на Playstation - 1

Целая глава моей книги Game Engine Black Book: DOOM посвящена консольным портам DOOM и сложностям, с которыми сталкивались их разработчики. Можно долго рассказывать о полном провале на 3DO, о сложностях на Saturn из-за аффинного наложения текстур и о потрясающем «реверс-инжиниринге с нуля», выполненном Рэнди Линденом для Super Nintendo.

Изначально двинувшись в направлении, ведущем к катастрофе[1], разработчики порта под Playstation 1 (PSX) в дальнейшем сменить курс и создать порт, завоевавший успех у критиков и рынка. Final DOOM был первым истинным портом, сравнимым с PC-версией. Цветовые сектора с альфа-смешением не только усовершенствовали визуальное качество, но и улучшили геймплей благодаря индикации ключа нужного цвета. Также благодаря эффектам реверберации Audio Processing Unit консоли PSX был улучшен звук.

Команда разработчиков выполнила настолько качественную работу, что у неё осталось ещё немного свободных циклов ЦП, которые они решили использовать для генерации анимированного огня в интро и геймплее. Меня это настолько привело в благоговейный трепет, что я решил разобраться, как был реализован эффект. Когда первые поиски не дали ответа, я приготовился уже сдувать пыль с книги по MIPS для взлома исполняемого файла, но Сэмюэль Вильяреал вовремя ответил в Twitter, что он уже выполнил обратную разработку версии для Nintendo 64[2]. Мне достаточно было просто немного её подчистить, упростить и оптимизировать.

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

Базовая идея


В своей основе эффект огня использует простую карту высот. Массив размером с экран заполняется 37 значениями в интервале от 0 до 36. Каждое значение связывается с цветом от белого до чёрного, и захватывает по дороге между ними жёлтый, оранжевый и красный. Идея заключается в моделировании температуры частицы пламени, которая поднимается вверх и постепенно охлаждается.

Как было реализовано пламя в Doom на Playstation - 2

Буфер кадра инициализируется полностью чёрным (заполненным нулями) с единственной белой строкой белых пикселей внизу (36), которая является «источником» пламени.

Как было реализовано пламя в Doom на Playstation - 3

При каждом обновлении экрана «тепло» поднимается вверх. Для каждого пикселя в буфере кадра вычисляется новое значение. Каждый пиксель обновляется с учётом значения, расположенного непосредственно под ним. В коде нижний левый угол это нулевой индекс массива, а верхний правый угол имеет индекс FIRE_HEIGHT * FIRE_WIDTH — 1.

function doFire() {
    for(x=0 ; x < FIRE_WIDTH; x++) {
        for (y = 1; y < FIRE_HEIGHT; y++) {
            spreadFire(y * FIRE_WIDTH + x);
        }
    }
 }

 function spreadFire(src) {
    firePixels[src - FIRE_WIDTH] = firePixels[src] - 1;
 }

Заметьте, что строка 0 никогда не обновляется (итерация по y начинается не с 0, а с 1). Эта заполненная нулями строка является «генератором» огня. Простая версия с линейным охлаждением (-=1) даёт нам скучные равномерные выходные данные.

Как было реализовано пламя в Doom на Playstation - 4

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

 function spreadFire(src) {
    var rand = Math.round(Math.random() * 3.0) & 3;
    firePixels[src - FIRE_WIDTH ] = pixel - (rand & 1);
 }

Как было реализовано пламя в Doom на Playstation - 5
Так уже лучше. Чтобы усовершенствовать иллюзию, можно случайным образом распространять не только вверх, но также влево и вправо.

 function spreadFire(src) {
    var rand = Math.round(Math.random() * 3.0) & 3;
    var dst = src - rand + 1;
    firePixels[dst - FIRE_WIDTH ] = firePixels[src] - (rand & 1);
 }

[Прим. пер.: Youtube ужасно пережимает видео, лучше смотреть демо на Javascript в оригинале статьи.]

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

Полный исходный код


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

Справочные материалы


[1] Источник: Полная история подробно рассказана в книге Game Engine Black Book: DOOM

[2] Источник: пост в Twitter за 25 марта 2018 года

Автор: PatientZero

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js