Заметки за 2020 год
Создание эффекта огня на ассемблере
Как и обещал ранее, в этой заметке покажу как реализовать красивый эффект пламени на ассемблере под DOS в графическом режиме VGA.
Эффект пламени впервые появился на заре демосцены. В те далёкие времена каждый уважающий себя программист отжигал направо и налево.
Для повторения данного подвига нам потребуется:
- создать палитру с градиентом от чёрного через красный и жёлтый к белому;
- сгенерировать в самой нижней строке экрана точки со случайным цветом из этой палитры;
- обновлять цвет каждой точки экрана, пробегая по адресному пространству и вычисляя значение путём усреднения цветов смежных точек.
- повторить с пункта 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 можно получить вот такой вид огня!