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

Как и обещал ранее, в этой заметке покажу как реализовать красивый эффект пламени на ассемблере под 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
71
72
        repeat 64           ; Чёрный -> Красный
        db %-1,0,0          ; от 0,0,0 до 63,0,0
        end repeat

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

db 0,0,0
db 1,0,0
db 2,0,0
…

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

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

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

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

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

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
org 100h

;------------------------------------------------------------------------------
; FIRE.ASM

mov ax,0013h    ; Выбираем режим 320x200 256 цветов
int 10h         ; Видеосервис: Установка видеорежима

push 0A000h     ; Отправляем в стэк адрес сегмента видеопамяти
pop es          ; Извлекаем адрес сегмента видеопамяти в ES
xor di,di       ; Обнуляем смещение от начала сегмента видеопамяти

call palset     ; Записываем цвета палитры
call dpal       ; Вызываем отрисовку палитры

mainloop:       ; Метка главного цикла

call dline      ; Вызываем отрисовку линии точек случайных цветов внизу экрана
call updscr     ; Вызываем вычисление цвета точек на экране

mov ah,1        ; Выбираем режим ожидания ввода AH = 01h
int 16h         ; Сервис клавиатуры: проверяем нажатие на клавишу

jz  mainloop    ; Если нажатия не было возвращаемся на метку mainloop

xor ah,ah       ; Выбираем режим ожидания ввода AH = 00h
int 16h         ; Сервис клавиатуры: пауза до нажатия на клавишу

mov ax,0003h    ; Выбираем текстовый режим 80x25 16 цветов
int 10h         ; Видеосервис: Установка видеорежима

int 20h         ; Выходим в DOS

;------------------------------------------------------------------------------
; Функция записи цветов в таблицу палитры

palset:

mov dx,03c8h    ; В DX будет храниться адрес порта ЦАП для управления палитрой
xor ax,ax       ; Обнуляем аккумулятор
out dx,al       ; Записываем в порт 03c8h индекс 0-го элемента палитры
inc dx          ; Увеличивает адрес порта на 1

mov cx,256      ; Устанавливаем счётчик цикла по всем 256 цветам палитры
mov bx,pal      ; В BX будет адрес начала сгенерированных данных палитры

ps_l1:

mov al,[bx]     ; Берём содержимое байта из BX
out dx,al       ; Записываем его в порт 03c9h это будет R
inc bx          ; Смещаем указатель адреса данных палитры на 1

mov al,[bx]     ; Берём содержимое байта из BX
out dx,al       ; Записываем его в порт 03c9h это будет G
inc bx          ; Смещаем указатель адреса данных палитры на 1

mov al,[bx]     ; Берём содержимое байта из BX
out dx,al       ; Записываем его в порт 03c9h это будет B
inc bx          ; Смещаем указатель адреса данных палитры на 1

loop ps_l1      ; Повторяем пока не запишутся все цвета

ret             ; Возврат

;------------------------------------------------------------------------------
; Палитра

pal:

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

repeat 64       ; Красный -> Жёлтый
db 63,%-1,0     ; от 63,0,0 до 63,63,0
end repeat

repeat 64       ; Жёлтый -> Белый
db 63,63,%-1    ; от 63,63,0 до 63,63,63
end repeat

repeat 64       ; Белый
db 63,63,63     ; 63,63,63
end repeat

;------------------------------------------------------------------------------
; Функция отрисовки палитры на экране

dpal:

xor ax,ax       ; Обнуляем текущий индекс цвета палитры
xor dx,dx       ; Обнуляем индекс цвета палитры для строки

mov cx,8        ; Цикл для 8 строк

dp_l4:

push cx         ; Отправляем значение счётчика dp_l4 в стэк
mov cx,25       ; Цикл для 25 линий

dp_l3:

push cx         ; Отправляем значение счётчика dp_l3 в стэк
mov cx, 32      ; Цикл для 32 цветов

dp_l2:

push cx         ; Отправляем значение счётчика dp_l2 в стэк
mov cx,10       ; Цикл для 10 точек

dp_l1:

stosb           ; Записываем байт в память
loop dp_l1      ; Повторяем 10 раз

pop cx          ; Восстанавливаем значение счётчика dp_l2 из стэка
inc al          ; Увеличиваем индекс цвета на один
loop dp_l2      ; Повторяем 32 раза

pop cx          ; Восстанавливаем значение счётчика dp_l3 из стэка
mov ax,dx       ; Устанавливаем текущее значение индекса цвета для строки
loop dp_l3      ; Повторяем 25 раз

pop cx          ; Восстанавливаем значение счётчика dp_l4 из стэка
add al,32       ; Увеличиваем текущий индекс цвета на 32
mov dx,ax       ; Сохраняем начальный индекс цвета палитры для строки
loop dp_l4      ; Повторяем 8 раз

ret             ; Возврат

;------------------------------------------------------------------------------
; Функция генерации случайного числа

rand:

mov ax,[seed]   ; Сохраняем в аккумулятор начальное значение семячка
mov dx,8405h    ; Заносим в DX «магическое» число
mul dx          ; Умножаем начальное значение в AX на «магическое» число в DX
mov [seed],ax   ; Сохраняем младшие 16 бит результата умножения как новое семячко
mov ax,dx       ; Возвращаем старшие 16 бит результата от умножения

ret             ; Возврат

seed: dw 3749h  ; Начальное значение

;------------------------------------------------------------------------------
; Функция отрисовки полоски точек случайных цветов

dline:

mov di,320*199  ; Заносим в смещение первую точку последнего ряда экрана
mov cx,320      ; Цикл для 320 точек

randpix:

call rand       ; Получаем случайное число в AX
stosb           ; Записываем содержимое регистра AL в байт по адресу ES:DI
loop randpix    ; Повторяем для каждой точки строки

ret             ; Возврат

;------------------------------------------------------------------------------
; Функция вычисления цвета точек на экране

updscr:

mov cx,320*199  ; Цикл для 320×199 точек, это весь экран кроме последнего ряда
xor di,di       ; Обнуляем смещение

updpix:

push di         ; Запоминаем текущее смещение

xor ax,ax       ; Обнуляем аккумулятор
xor dx,dx       ; Обнуляем регистр данных

add di,320      ; Увеличиваем смещение на строку вниз
dec di          ; и уменьшаем на пиксель влево, ниже и левее
mov dl,[es:di]  ; Сохраняем байт в младшую часть регистра DL
add ax,dx       ; Складываем с аккумулятором

inc di          ; Увеличиваем смещение на пиксель вправо, точно под точкой
mov dl,[es:di]  ; Сохраняем байт в младшую часть регистра DL
add ax,dx       ; Складываем с аккумулятором

inc di          ; Увеличиваем смещение на пиксель вправо, ниже и правее
mov dl,[es:di]  ; Сохраняем байт в младшую часть регистра DL
add ax,dx       ; Складываем с аккумулятором

add di,319      ; Увеличиваем смещение на строку вниз и на пиксель влево
mov dl,[es:di]  ; Сохраняем байт в младшую часть регистра DL
add ax,dx       ; Складываем с аккумулятором

sar ax,2        ; Арифметический сдвиг вправо, знаковое деление на четыре
jz  next
dec al          ; Дополнительное уменьшение яркости

next:

pop di          ; Восстанавливаем текущее смещение
stosb

loop updpix

ret             ; Возврат

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

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