CZE | Plamínek v x86 assembleru.
Retro-průvodce vytvářením grafického efektu plamínku v x86 assembleru pro MS-DOS s příkladem kódu. Initicializace VGA režimu 320x200 s 256 barevnou paletou.
Úvod
Článek by měl vysvětlit, jak fungují efekty typu plamínek/oheň a případně pomoci s napsáním vlastního programu. Přiložený kód je můj vlastní krom generátoru náhodných čísel. Algoritmus je fakt úplně primitivní, kdesi na internetu lze při troše štěstí najít tinyfire na 67 bytů. Můj program vypadá trochu složitěji, ale velkou část tvoří vygenerování palety.
Inicialize
Na začátku programu je dobré inicializovat generátor náhodných čísel, přepnout do grafického módu, vymazat videopaměť (občas v ní něco je) a vhodně nastavit paletu (může být vygenerována za běhu programu nebo pevně daná jako data).
Základ algoritmu
Plamínek je generován asi takto: Máme pole (matici) uložené přímo ve videopaměti. Ve spodním řádku se vygenerují náhodná čísla. Potom se pole projede shora dolů a každý bod se spočítá z okolních například podle vzorce:
1
2
3
bod[x,y] = ( bod[x,y] + bod[x-1,y+1] + bod[x,y+1] + bod [x+1,y+1] ) / 4;
if bod[x,y]>1 then
bod[x,y]=bod[x,y] - 1
Samozřejmě lze použít i jiný vzorec, důležité je, aby aktuální bod byl průměrem několika sousedních. Krátce se počká na z5ný běh paprsku (kvůli zdržení) a cyklus se opakuje, do stisku tlačítka, ať už na klávesnici nebo (v horším případě) na přední části kompu.
Ukončení programu
Na konci programu je nutné nastavit textový mód a vymazat buffer klávesnice, jinak klávesu zachytí shell nebo správce souborů, ze kterého byl program spuštěn.
Paleta
Aby plamínek vypadal přirozeně, je zapotřebí mít vhodnou paletu. 0 musí být černá a 255 nejsvětlejší barva. Přechod by měl být plynulý. Paleta nemusí být nutně laděná do červena, docela hezky vypadá i hořící plyn nebo plamen zabarvený do zelena. Z vlastní zkušenosti však vím, že plyn se dělá trochu hůř. Na začátek doporučuji paletu černobílou a experimentovat, až bude fungovat základní algoritmus. Ve svém programu používám přechod černá -> modrá -> oranžová -> žlutá -> bílá, s tím, že bílá vyplňuje asi půlku palety a přechod černá -> modrá je velmi krátký.
Postřehy
- Dema tohoto typu obecně doporučují místo provádění výpočtu přímo ve videopaměti použít druhý buffer, který se při zpětném běhu paprsku do videopaměti zkopíruje. Přístup k videopaměti je pomalejší než u běžné RAM a u ohně přes celou obrazovku je tento rozdíl znatelný. Velkou výhodu vidím také v tom, že se snadno skryje nehezký spodek plamínku (nekopíruje se).
- Videopaměť na adrese A000:0000 má délku 65536 (64K), ale 320x200=64000 (64k). Rozdíl je 1536b. Využít toho lze zase k odstranění spodku plamene generováním náhodných číslel na 204. řádku. Pokud kód nepřesahuje 1280b a požívá se druhý buffer, není nutno pro něj alokovat paměť. (255b PSP, 1280b kód, 320x200b buffer).
- Spodek plamene mohou tvořit náhodná čísla z intervalu 0-255 a nebo pouze čísla 0 a 255. U druhého způsobu vypadá spodek plamínku škaredě, avšak u ohně širokého přes celou obrazovku je tento způsob lepší a dokonce lze generovat místo bodů krátké čárky. V opačném případě by kvůli postupnému rozmazávání vznikl nahoře spíš barevný přechod než oheň.
- Každý bod je průměrem několika okolních. Čím více bodů se zprůměruje, tím rychleji se plamínek rozmazává. Není vhodné vespod generovat čísla 0-255 a průměrovat 8 okolních bodů. Druhý extrém je generovat dvoubodové čárky, a ve smyčce pak průměrovat 4 body.
- Pokud vypadá spodek plamene škaredě, lze možné to zachránit vygenerováním palety tak, aby přibližně půlku tvořila bílá barva, jak je to v mém programu.
Programy
flame_ex.zip Ukázkové programy. Jeden je v Netwide assembleru, čtyři další pro DJGPP (až na jeden potřebují knihovnu allegro). Dva programy jsou i jako binárky. Možná chybí komentáře co je co.
Kód
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
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
[bits 16]
[org 0x0100]
[section .text]
start: ;jmp zac ;testovani plaminku
;------ Nastaveni generatoru nahodnych cisel
xor ax,ax
int 0x1A
mov [seed],dx
;------ Nastaveni vsech barev na bilou
mov di,pal ;Bila paleta
mov cx,192 ;256*3barvy/4dword
mov eax,0x3f3f3f3f
rep stosd
;------ Nastaveni pal. na cerna->modra->oranzova->zluta->bila....
; asi nejdelsi a nejhnusnejsi cast programu
lea di,[pal]
;Cerna(0,0,0) -> modra(0,0,24)
LP0: xor ax,ax ;cx=0 (viz rep stosd)
stosw
mov ax,cx
stosb
add cl,2
cmp cl,24
jne LP0
;Modra(16,8,24) -> oranzova(64,32,0)
mov dx,16 ;cx=24
LP1: mov ax,dx
stosb ;r (dx)
shr al,1
stosb ;g = b>>1 (dx)
xor ax,ax
or cl,cl
jz nemodr ;if (b!=0) b--;
dec cx
mov al,cl
nemodr: stosb ;b (cx)
inc dx
test dl,64
jz LP1
;oranzova(64,32,0) -> zluta (64,64,0)
inc di ;r = 0x3f
mov cl,32
LP2: mov al,cl
stosb ;g (cl)
mov ax,0x3f00 ;br (opacne)
stosw
add cl,2
test cl,64
jz LP2
dec di
;zluta(64,64,0)->bila(64,64,64)
xor ax,ax
mov bx,2
LP3: mov [di+bx],al ;pal[di+3*i+2]=2*i
add bl,3
add al,2
test al,64
jz LP3
;------ Nastaveni graf. modu 320x200x256c
zac: mov ax,0x0013
int 0x10
;------ Nastaveni palety
mov ax,0x1012
xor ebx,ebx ;Prvni registr
mov cl,0x00FF ;Pocet registru barev
lea dx,[pal] ;ds:dx paleta
int 0x10
;------ Nastaveni ES na VRAM
mov ax,0xA000 ;segment videopameti
mov es,ax
mov ds,ax
;------ Vymaz VRAM
mov eax,ebx ;bx=0
mov di,bx
mov cx,16000
repnz stosd
;------ Nakresleni palety (muze a nemusi byt jako poznamka)
; mov di,167*320
; mov word[di-320],0xf0f0
; mov cx,0020h
;ShowPal:push cx
; xor ax,ax
;@ShowP:stosb
; inc ax
; test ax,256
; jz @ShowP
; add di,320-256 ;proc asi?
; pop cx
; loop ShowPal
L1:
;------ update spodku ohne (pseudonahodna cisla)
mov ax,[cs:seed]
mov di,320*100+144 ;144
mov cl,0x10 ;Sirka/2
L2: add ax,0x1234 ;Pseudonahodny cislo
xor al,ah ;Outlaw triad
rol ah,1
add ax,0x4321
rol al,2
xor ah,al
;imul ax,8905h ;fakedemo
;inc ax
stosw ;do VRAM
loop L2
mov [cs:seed],ax
;------ korekce spodku (neni to moc nutne, je to hezci)
mov bx,word[320*100+144] ;Leva strana
and bx,0x7f3f
mov word[320*100+144],bx
mov bx,word[320*100+174] ;Prava
and bx,0x3f7f
mov word[320*100+174],bx
or bx,0x8080
mov word[320*100+159],bx ;Prostredek
;------ zbytek
mov cl,100 ;100 radku
mov di,144 ;zacatek radku
L3: push cx
mov cl,0x20 ;sirka plaminku
;Zprumerovani млм -> п
L4: xor bx,bx
mov ax,word[di+319] ;o jeden radek niz, dva pix
mov bl,al ;1
shr ax,8 ;mov al,ah + xor ah,ah
add bx,ax ;2
mov al,byte[di+321] ;o radek niz, posledni pix
add bx,ax ;3
mov al,byte[di] ;horni pixel
add bx,ax ;4
shr bx,2 ;/4
or bx,bx
jz dal ;cernej pixel nic
dec bx
dal: mov byte[di],bl ;vykresleni
inc di
loop L4
add di,320-0x20
pop cx
loop L3
mov ah,0Bh ;cekani na klavesu
int 0x21
or al,al ;00h-nic, 0ffh-je
jnz Konec ;opakuj, kdy neni klavesa
;syncronizace-pro zpomaleni
SyncW: mov dx,0x03DA ;port CGA
in al,dx ;sync
and al,8 ;bit 3=1 -> sync
or al,al
jz SyncW
jmp L1
Konec:
;------ Textovy mod
mov ax,0x0003
int 0x10
;------ Hlaska
mov ax,cs
mov es,ax
mov ds,ax
mov ax,0x1301
mov cx,20 ;delka str
mov bx,0x0004 ;str,atr-tm.cerv
mov dx,0x0010
lea bp,[msg00]
int 0x10
lea bp,[msg01]
mov bl,0x06 ;hneda
inc dh ;radek
int 0x10
lea bp,[msg02]
mov bl,0x0E ;zl.
inc dh
int 0x10
lea bp,[msg03]
inc bl ;bila
inc dh
int 0x10
mov ah,0x02
mov dx,0x0600 ;pozice kurzoru
int 0x10
;------ Smaz buffer klavesnice
xor ax,ax
mov ds,ax
mov dx,word[0x041c]
mov word[0x041a],dx
;------ Konec programu
int 0x20 ;Konec
;------ Data ------------------------------------------
[section .data]
msg00 db '+------------------+'
msg01 db '| Flame v1.4 |'
msg02 db '| Pavel Perina |'
msg03 db '+------------------+'
EOF db '<PP>'
[section .bss]
seed resw 1
pal resb 768
;----------------------------------------------------------
;VERZE:
;~~~~~~
; 1.0(b,e,f) puvodni verze
; 1.1 cekani na retrace (zpomaleni),
; buhvi proc prekreslovani spodniho radku
; 1.2(k) jina paleta (modra->oranz), kratsi kod
; (jine instrukce), vymaz VRAM pred spustenim
; neprekresluje se spodni radek
; 1.3 prepsano do NASM (2?.5.2000)
; 1.4 zprehledneni kodu (2.6.2000)
;
;ALGORITMUS:
;~~~~~~~~~~~
; - inicializace generatoru nahodnych cisel
; - priprav paletu
; - prepni do grafickeho modu
; - nastav paletu
; - nastav es na segment A000 (videopamet)
; - vycisti pamet
; - opakuj:
; - vygeneruj nekolik nahodnych cisel
; - (na kraji cisla zmensi)
; - projed od vrchu cely plamen a pri tom provadej:
; kdyz bod[x,y]!=0 pak:
; bod[x,y]=([x,y]+[x-1,y+1]+[x,y-1]+[x+1,y+1])/4-1
; - kdyz je zmackla klavesa pak ukonci cyklus
; - nastav textovy mod
; - (vypis hlasku)
; - smaz buffer klavesnice ([0:041a]=[0:041c])
; - ukonci program
;
;----------------------------------------------------------
;InSpIrAcE: Flamer (E. Vaughan)
Obsah níže přidán 2024-05-04
Animace
Tvorba v DOSBOXu:
1
2
3
4
5
6
keyb en
mount c c:\dos
c:
cd DEV
\TOOLS\NASM\NASM.EXE FLAME_N.ASM -o FLAME.COM
FLAME.COM
Zachytávání videa přes Ctrl+Alt+F5
Kódování videa
1
C:\apps\ffmpeg.exe -r 60 -i .\flame_000.avi -frames:v 1200 -vf "crop=160:120:80:0, scale=800:600:flags=neighbor" -c:v libsvtav1 asm_flame_av1.webm
Dodatek (2024)
Původní rogram jsem psal určitě ještě na gymnáziu v MASM/TASM, může být tak z roku 1997-99, stejně jako fotka. Jedná se o archeologickou vykopávku. Článek byl napsaný roku 2000 a bydlel na školní stránce.
Funguje v MS-DOSu, asi nejsnažší způsob jak ho spustit je v DOSBOXu. Verze napsaná v C používá DJGPP a knihovnu Allegro, což je dost exotická kombinace se kterou jsem experimentoval velmi krátce na podzim roku 2000, když mi nestačilo 64kB paměti a chtěl jsem něco dělat raytracing v chráněném režimu. Krátce na to jsem Pentium 120MHz vyměnil za Athlona XP 1700+ s Windows XP. Že existovalo DJGPP (port GCC pro DOS) jsem po pětadvaceti letech zapomněl. Nicméně kód je snadněji pochopitelný, než assembler.
Jediná úprava kódu je rámeček kolem jména, v té době o UTF-8 nikdo neslyšel a program používal nejspíš ASCII CP437.
Možná se najde ještě nějaký Tetris nebo Piškvorky v Pascalu.