Найти в Дзене
ZDG

Эта программа – просто огонь

Другие материалы: вода как в Quake, движение сквозь звёзды

Раскопал старую симуляцию огня.

Лучше смотреть в движении. Если хотите – проматывайте сразу вниз. А я буду объяснять, как это делается. Пока что на JavaScript. Версия на Python будет позже, когда уточню графические возможности. Но не стоит ждать, лучше пишите сами, так как вся информация есть.

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

Шаг 1. Буфер

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

Размеры буфера должны совпадать с размером области экрана, где вы хотите нарисовать огонь. В данном случае это весь экран, то есть весь холст (canvas), который выделен в браузере для рисования. Его размер 480 * 320 пикселов.

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

В качестве буфера я взял массив типа Uint8Array в JavaScript. Это массив, состоящий из байтов. Размер буфера 120*80, и благодаря типу Uint8Array я могу быть уверен, что выделил ровно 9600 байт.

Шаг 2. Затравочные пикселы

Каждый байт буфера – это один пиксел. Он не имеет цвета. Точнее, он может иметь цвет, но тогда вместо одного байта надо будет хранить три (красный, зеленый, синий). Я же храню не цвет, а только яркость. 0 – самый тёмный, 255 – самый яркий. Цвет я буду генерировать на основании яркости.

Чтобы создать затравочные пикселы, нужно в самой нижней строке буфера в случайном порядке расставить несколько пикселов с яркостью 255 и несколько пикселов с яркостью 0. Тёмные пикселы нужно ставить для того, чтобы строка через некоторое время не заполнилась яркими пикселами целиком.

Буфер, как я говорил, прямоугольный, но это лишь визуальная форма. В памяти он расположен одним непрерывным куском. Каждый байт в нём имеет не координаты x, y, а адрес. Адрес вычисляется проcто: если ширина буфера это W, то каждые W байт это 1 строка. Первая строка (y = 0) начинается по адресу 0, вторая строка (y = 1) начинается по адресу W, третья (y = 2) по адресу W * 2, и т.д.

Шаг 3. Сглаживание

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

От того, как делается сглаживание, зависят многие параметры огня: будет он похож на огонь или на что-то другое, насколько высоко он будет подниматься, какие клубки будет образовывать. Здесь большое поле для экспериментов. Я остановился на следующем варианте: сложить текущий пиксел с его тремя соседями снизу. Итого получается сумма из 4-х пикселов, которую удобно делить на 4 путем быстрого битового сдвига.

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

После того, как буфер обработан, первый и второй буфер меняются местами. Сами буферы не копируются, копируются только указатели.

Шаг 4. Перенос на экран

Я использую JavaScript-объект ImageData, который имитирует прямой доступ к видеопамяти. Это тоже массив Uint8Array, в котором каждые 4 байта – один пиксел. Чтобы установить цвет пиксела, нужно задать эти 4 байта (красный, зеленый, синий и прозрачность). Прозрачность для всех пикселов ImageData я задал заранее и больше не трогаю, так как она мне не понадобится.

Чтобы установить цвет каждого пиксела, я читаю буфер байт за байтом и получаю яркость. Эту яркость надо перевести в цвет. Так как огонь – желтых, красных и оранжевых оттенков, я использую красный компонент как основу. Красный = яркость. Зеленый = половина яркости. Синий = 0 (не использую его).

Кроме того, чтобы создать именно эффект огня, нужно определенным образом создать резкую границу между уровнями яркости. Иначе вся симуляция будет больше похожа на монотонный дым. Для этого я дополнительно проверяю уровень красного цвета: если он больше 200, то я присваиваю ему 255, и также увеличиваю и зеленый и синий компонент, чтобы цвет был максимально яркий. Число 200 я выбрал эмпирически. Здесь тоже можно экспериментировать.

Вместо таких вычислений можно использовать палитру: массив из 256 элементов, где каждый элемент это 3 байта r,g,b. Чтение элемента из палитры производится в соответствии с яркостью 0..255. Палитра заполняется цветами заранее, и они вовсе не должны соответствовать яркости. Вы можете задавать любые цвета и получать какие-то совершенно безумные эффекты.

Теперь нужно эти компоненты цвета записать в ImageData. Зная, на какой позиции в буфере я нахожусь, и зная, что буфер по сторонам в 4 раза меньше холста, и зная, что в ImageData каждая позиция пиксела занимает 4 байта, я вычисляю корректный адрес внутри ImageData и записываю в него rgb-значения.

Кроме того, так как буфер меньше в 4 раза, в ImageData пишется не 1 пиксел, а квадрат 4*4 пиксела, чтобы заполнить недостающие данные.

После того, как все значения перенесены из буфера в ImageData, содержимое ImageData копируется на холст. И цикл повторяется, начиная с расстановки затравочных пикселов.

Как это работает

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

Комментарии я написал в коде. Работающую программу и её исходный текст можно посмотреть онлайн. А также исправить что-нибудь и сразу проверить, как оно будет выглядеть. Исправлять можно: количество ярких и тёмных затравочных точек, метод сглаживания, порог перехода яркости, сами цвета (например, сделать синий огонь или разноцветный огонь).