Заметки за 2020 год

Создание эффекта огня на ассемблере

Как и обещал ранее, в этой заметке покажу как реализовать красивый эффект пламени на ассемблере под DOS в графическом режиме VGA.

Эффект пламени впервые появился на заре демосцены. В те далёкие времена каждый уважающий себя программист отжигал направо и налево.

Для повторения данного подвига нам потребуется:

  1. создать палитру с градиентом от чёрного через красный и жёлтый к белому;
  2. сгенерировать в самой нижней строке экрана точки со случайным цветом из этой палитры;
  3. обновлять цвет каждой точки экрана, пробегая по адресному пространству и вычисляя значение путём усреднения цветов смежных точек.
  4. повторить с пункта 2.

В качестве каркаса программы я буду использовать код для отрисовки палитры из заметки Использование VGA графики на ассемблере x86. Он ещё пригодится, чтобы проверить корректность полученных цветов.

К слову о палитре.

В наши дни видеокарты с лёгкостью отображают миллионы цветов. А цвет точки на экране задаётся комбинацией значений каналов красного, зелёного и синего. При этом каждый канал может иметь значение от 0 до 255 (восемь бит). Отсюда легко подсчитать количество одновременно отображаемых на экране цветов: (2^8)^3 = 256^3 = 16 777 216.

Три байта на точку - жуткое расточительство. В VGA точка представлена одним байтом, который соответствует номеру цвета в палитре. То есть, всего возможно не более 256 цветов. В добавок в VGA палитре каждый канал цвета имеет значение от 0 до 63. Однако эти ограничения не мешали создавать красочные игры и демо.

Цвета для палитры я сгенерировал при помощи оператора повтора блока инструкций fasm: repeat [число повторов] [инструкции] end repeat.

70        repeat 64           ; Чёрный -> Красный
71        db %-1,0,0          ; от 0,0,0 до 63,0,0
72        end repeat

Например так генерируется градиент от чёрного к красному в 64 цветах. Цвет задаётся тремя байтами db (R),(G),(B). Символ % содержит в себе текущее значение итерации повтора. Соответственно инструкции будут записаны следующим образом:

1 - db 0,0,0
2 - db 1,0,0
3 - db 2,0,0
4 - и так далее.

Сгенерированная палитра

И поскольку наша палитра для огня является по сути градиентом, то и значения точек можно рассматривать и вычислять как яркость.

Вычисление цвета точки будет происходить по алгоритму изображённому ниже.

Алгоритм вычисления цвета точки

То есть для получения значения каждого пикселя мы вычисляем сумму яркости трёх пикселей под ним и одного пикселя на две строки ниже, а затем делим эту сумму на четыре. Реализация этого алгоритма в подпрограмме updscr на 165 строке.

Полный листинг программы с комментариями.

  1org 100h
  2
  3;------------------------------------------------------------------------------
  4; FIRE.ASM
  5
  6mov ax,0013h    ; Выбираем режим 320x200 256 цветов
  7int 10h         ; Видеосервис: Установка видеорежима
  8
  9push 0A000h     ; Отправляем в стэк адрес сегмента видеопамяти
 10pop es          ; Извлекаем адрес сегмента видеопамяти в ES
 11xor di,di       ; Обнуляем смещение от начала сегмента видеопамяти
 12
 13call palset     ; Записываем цвета палитры
 14call dpal       ; Вызываем отрисовку палитры
 15
 16mainloop:       ; Метка главного цикла
 17
 18call dline      ; Вызываем отрисовку линии точек случайных цветов внизу экрана
 19call updscr     ; Вызываем вычисление цвета точек на экране
 20
 21mov ah,1        ; Выбираем режим ожидания ввода AH = 01h
 22int 16h         ; Сервис клавиатуры: проверяем нажатие на клавишу
 23
 24jz  mainloop    ; Если нажатия не было возвращаемся на метку mainloop
 25
 26xor ah,ah       ; Выбираем режим ожидания ввода AH = 00h
 27int 16h         ; Сервис клавиатуры: пауза до нажатия на клавишу
 28
 29mov ax,0003h    ; Выбираем текстовый режим 80x25 16 цветов
 30int 10h         ; Видеосервис: Установка видеорежима
 31
 32int 20h         ; Выходим в DOS
 33
 34;------------------------------------------------------------------------------
 35; Функция записи цветов в таблицу палитры
 36
 37palset:
 38
 39mov dx,03c8h    ; В DX будет храниться адрес порта ЦАП для управления палитрой
 40xor ax,ax       ; Обнуляем аккумулятор
 41out dx,al       ; Записываем в порт 03c8h индекс 0-го элемента палитры
 42inc dx          ; Увеличивает адрес порта на 1
 43
 44mov cx,256      ; Устанавливаем счётчик цикла по всем 256 цветам палитры
 45mov bx,pal      ; В BX будет адрес начала сгенерированных данных палитры
 46
 47ps_l1:
 48
 49mov al,[bx]     ; Берём содержимое байта из BX
 50out dx,al       ; Записываем его в порт 03c9h это будет R
 51inc bx          ; Смещаем указатель адреса данных палитры на 1
 52
 53mov al,[bx]     ; Берём содержимое байта из BX
 54out dx,al       ; Записываем его в порт 03c9h это будет G
 55inc bx          ; Смещаем указатель адреса данных палитры на 1
 56
 57mov al,[bx]     ; Берём содержимое байта из BX
 58out dx,al       ; Записываем его в порт 03c9h это будет B
 59inc bx          ; Смещаем указатель адреса данных палитры на 1
 60
 61loop ps_l1      ; Повторяем пока не запишутся все цвета
 62
 63ret             ; Возврат
 64
 65;------------------------------------------------------------------------------
 66; Палитра
 67
 68pal:
 69
 70repeat 64       ; Чёрный -> Красный
 71db %-1,0,0      ; от 0,0,0 до 63,0,0
 72end repeat
 73
 74repeat 64       ; Красный -> Жёлтый
 75db 63,%-1,0     ; от 63,0,0 до 63,63,0
 76end repeat
 77
 78repeat 64       ; Жёлтый -> Белый
 79db 63,63,%-1    ; от 63,63,0 до 63,63,63
 80end repeat
 81
 82repeat 64       ; Белый
 83db 63,63,63     ; 63,63,63
 84end repeat
 85
 86;------------------------------------------------------------------------------
 87; Функция отрисовки палитры на экране
 88
 89dpal:
 90
 91xor ax,ax       ; Обнуляем текущий индекс цвета палитры
 92xor dx,dx       ; Обнуляем индекс цвета палитры для строки
 93
 94mov cx,8        ; Цикл для 8 строк
 95
 96dp_l4:
 97
 98push cx         ; Отправляем значение счётчика dp_l4 в стэк
 99mov cx,25       ; Цикл для 25 линий
100
101dp_l3:
102
103push cx         ; Отправляем значение счётчика dp_l3 в стэк
104mov cx, 32      ; Цикл для 32 цветов
105
106dp_l2:
107
108push cx         ; Отправляем значение счётчика dp_l2 в стэк
109mov cx,10       ; Цикл для 10 точек
110
111dp_l1:
112
113stosb           ; Записываем байт в память
114loop dp_l1      ; Повторяем 10 раз
115
116pop cx          ; Восстанавливаем значение счётчика dp_l2 из стэка
117inc al          ; Увеличиваем индекс цвета на один
118loop dp_l2      ; Повторяем 32 раза
119
120pop cx          ; Восстанавливаем значение счётчика dp_l3 из стэка
121mov ax,dx       ; Устанавливаем текущее значение индекса цвета для строки
122loop dp_l3      ; Повторяем 25 раз
123
124pop cx          ; Восстанавливаем значение счётчика dp_l4 из стэка
125add al,32       ; Увеличиваем текущий индекс цвета на 32
126mov dx,ax       ; Сохраняем начальный индекс цвета палитры для строки
127loop dp_l4      ; Повторяем 8 раз
128
129ret             ; Возврат
130
131;------------------------------------------------------------------------------
132; Функция генерации случайного числа
133
134rand:
135
136mov ax,[seed]   ; Сохраняем в аккумулятор начальное значение семячка
137mov dx,8405h    ; Заносим в DX «магическое» число
138mul dx          ; Умножаем начальное значение в AX на «магическое» число в DX
139mov [seed],ax   ; Сохраняем младшие 16 бит результата умножения как новое семячко
140mov ax,dx       ; Возвращаем старшие 16 бит результата от умножения
141
142ret             ; Возврат
143
144seed: dw 3749h  ; Начальное значение
145
146;------------------------------------------------------------------------------
147; Функция отрисовки полоски точек случайных цветов
148
149dline:
150
151mov di,320*199  ; Заносим в смещение первую точку последнего ряда экрана
152mov cx,320      ; Цикл для 320 точек
153
154randpix:
155
156call rand       ; Получаем случайное число в AX
157stosb           ; Записываем содержимое регистра AL в байт по адресу ES:DI
158loop randpix    ; Повторяем для каждой точки строки
159
160ret             ; Возврат
161
162;------------------------------------------------------------------------------
163; Функция вычисления цвета точек на экране
164
165updscr:
166
167mov cx,320*199  ; Цикл для 320×199 точек, это весь экран кроме последнего ряда
168xor di,di       ; Обнуляем смещение
169
170updpix:
171
172push di         ; Запоминаем текущее смещение
173
174xor ax,ax       ; Обнуляем аккумулятор
175xor dx,dx       ; Обнуляем регистр данных
176
177add di,320      ; Увеличиваем смещение на строку вниз
178dec di          ; и уменьшаем на пиксель влево, ниже и левее
179mov dl,[es:di]  ; Сохраняем байт в младшую часть регистра DL
180add ax,dx       ; Складываем с аккумулятором
181
182inc di          ; Увеличиваем смещение на пиксель вправо, точно под точкой
183mov dl,[es:di]  ; Сохраняем байт в младшую часть регистра DL
184add ax,dx       ; Складываем с аккумулятором
185
186inc di          ; Увеличиваем смещение на пиксель вправо, ниже и правее
187mov dl,[es:di]  ; Сохраняем байт в младшую часть регистра DL
188add ax,dx       ; Складываем с аккумулятором
189
190add di,319      ; Увеличиваем смещение на строку вниз и на пиксель влево
191mov dl,[es:di]  ; Сохраняем байт в младшую часть регистра DL
192add ax,dx       ; Складываем с аккумулятором
193
194sar ax,2        ; Арифметический сдвиг вправо, знаковое деление на четыре
195jz  next
196dec al          ; Дополнительное уменьшение яркости
197
198next:
199
200pop di          ; Восстанавливаем текущее смещение
201stosb
202
203loop updpix
204
205ret             ; Возврат

После компиляции и запуска можем насладиться огоньком.

А если убрать из строки номер 196 инструкцию dec al можно получить вот такой вид огня!