**Raz**
Registered: Aug 2003 Posts: 8 |
**Release id #185647 : Gumbo Revised**
Dear all
I have previously written about how the heat-bobs works:
Release id #179166 : Gumbo
And (indirectly) about how the raster scroller works:
No More Grey Dots
(Look for the note I wrote - I'll condense it and post it here instead, when I find the time).
And I thought it would now be in place to write a bit about how the Fire Effect in Gumbo works (especially since it is featured a bit more prominently in Gumbo Revised).
**Note:** This piece is intentionally written to be more accessible to *all levels of coders* - so it contains quite a bit of introductory explanation.
__ABOUT THE FIRE EFFECT__
In many ways, the fire effect is not rocket science (and it's certainly not the most optimized effect in the demo), but it does have a couple of tricks up its sleeve that I think makes it look more interesting:
1) The side-to-side wave pattern
2) Its non-linear transformation of heat-values to colors (ECM based)
The core mechanics of the fire effect is based on the following simple principle for propagating the fire:
cell[x,y] = ((cell[x,y]+cell[x-1,y+1]+cell[x,y+1]+cell[x+1,y+1])-cooling[x,y]) / 4
(Coordinate system as a typical C64 char screen)
In essence, the new value of a given cell is calculated as 1) the mean of itself + the three cells below and 2) a local cooling factor (think blobs of hotter/colder areas) is subtracted to give a more organic feel to the flames. This blends the heat of the cells, cools it a bit and moves the heat up one line.
The loop runs from top to bottom, and new flames are seeded by heating the line below the visible matrix with semi-random values (cooler towards the sides, and one effect have a small cool zone in the middle). If no heating is applied the fire will die out by itself, as the happens when the demo cycles between effects.
Looking at the simple equation above it's quite natural to work with 6-bit heat values, as adding four of those will never overflow.
Ignoring the cooling-zones, an implementation could look like this:
clc // Just once - we'll never overflow
lda matrix+(xpos)+(ypos*40)
adc matrix+(xpos-1)+((ypos+1)*40)
adc matrix+(xpos+0)+((ypos+1)*40)
adc matrix+(xpos+1)+((ypos+1)*40)
lsr
lsr
sta matrix+(xpos)+(ypos*40)
(roll out code for all matrix cells)
Next we'd want to address the cooling zones, but we do not want to deal with the hassle of handling underflows, and setting/clearing carry. This is simply handled by generating a number of page-long tables, that both takes care of dividing by four and subtracting a value (and handling underflows). I use 4 such tables in the demo:
subtab0: sub0 followed by div4
subtab1: sub1 followed by div4
subtab2: sub2 followed by div4
subtab3: sub3 followed by div4
The first 16 bytes of each table look like this:
subtab0: $00,$00,$00,$00,$01,$01,$01,$01,$02,$02,$02,$02,$03,$03,$03,$03 ...
subtab1: $00,$00,$00,$00,$00,$01,$01,$01,$01,$02,$02,$02,$02,$03,$03,$03 ...
subtab2: $00,$00,$00,$00,$00,$00,$01,$01,$01,$01,$02,$02,$02,$02,$03,$03 ...
subtab3: $00,$00,$00,$00,$00,$00,$00,$01,$01,$01,$01,$02,$02,$02,$02,$03 ...
In the code that rolls out the speedcode, it's easy to look up the values in the cooling map (between 0 and 3) and select the right table to use. That allow us to have code that looks something like this:
lda matrix+(xpos)+(ypos*40)
adc matrix+(xpos-1)+((ypos+1)*40)
adc matrix+(xpos+0)+((ypos+1)*40)
adc matrix+(xpos+1)+((ypos+1)*40)
tay
lda subtabXX,y // Will point to subtab0,subtab1,subtab2 or subtab3
sta matrix+(xpos)+(ypos*40)
This could more or less have been it - wrap it up, and make a charset that goes from 00-63 and call it a day, but I decided to do a slightly more complicated (and somewhat slower) version, I though looked more interesting:
__Wave pattern:__
Frankly, this is a total hack of the core algorithm, but I think it looks pretty neat. The idea is to dynamically vary which of the cells below gets evaluated in the calculating the mean on a line-by-line basis.
This is what the normal case looks like:
x
xxx
Shifting to the right (uppercase X = double sampled):
x
Xx
Shifting to the left (uppercase X = double sampled):
x
xX
The code now looks something like this:
ldx addpatt+linenum // Once per line (x <- 0 or 1)
...
lda matrix+(xpos)+(ypos*40)
adc matrix+(xpos-1)+((ypos+1)*40),x
adc matrix+(xpos+0)+((ypos+1)*40)
adc matrix+(xpos+0)+((ypos+1)*40),x
tay
lda subtabXX,y // Will point to subtab0,subtab1,subtab2 or subtab3
sta matrix+(xpos)+(ypos*40)
Note: Yes, I could have saved an ADC (was kept in since the code can also be generated to address the general case - I have a flag that will swap between modes, when the code is rolled out).
The actual wave-pattern looks like this:
addpatt:
.byte 0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1,0,0,0,0,1,1,1,1
And the pattern is then gradually cycled to the right, giving an upward motion of the waves.
__Colors:__
I use a high-res (single color) dithered charset between 00-63, which allows the final 2 bits to be used to select ECM colors. My original plan was to have a simple linear transformation of heat-values into a color ramp covering a lot of colors:
// Color ramps
// bk : 07
// ec1: 08
// ec2: 02
// ec3: 0b
// CHAR COL ; BK COL ; bit patt ; direction
// -----------------------------------------
// 01 ; 07 ; 00 ; 00-3f
// 0a ; 07 ; 00 ; 3f-00
// 0a ; 08 ; 01 ; 00-3f
// 04 ; 08 ; 01 ; 3f-00
// 04 ; 02 ; 10 ; 00-3f
// 09 ; 02 ; 10 ; 3f-00
// 09 ; 0b ; 11 ; 00-3f
// 00 ; 0b ; 11 ; 3f-00
However, that simply turned out not to look that great. The result looked waaaaay too blocky, and had a "flat" feeling to it. It was also clear from my experimentation, that while gradual transition between colors looked good in the hot part of the flames, it needed to transition much faster in the "almost cold" parts in order to give the flames a well-defined outline. Also, the first few steps for the "hot" colors were rarely used, as the heat-seed needs to fluctuate quite a bit for the effect to be lively.
In the end, I used a ramp with fewer colors (char-col and background, ec1, ec2), and the following system for mapping colors:
Color-mapping (after 4*ADC, before division, hence 256 steps):
$00-$1f: [fixed color] black
$20-$3f: [step size=2] black -> red
$40-$5f: [step size=2] red -> orange
$60-$7f: [step size=2] orange -> yellow
$80-$bf: [step size=1] yellow -> white
$c0-$ff: [fixed color] white
(Example - the red/yellow color ramp)
The final code looks something like this:
lda matrix+(xpos)+(ypos*40)
adc matrix+(xpos-1)+((ypos+1)*40),x
adc matrix+(xpos+0)+((ypos+1)*40)
adc matrix+(xpos+0)+((ypos+1)*40),x
tay
lda subtabXX,y // Will point to subtab0,subtab1,subtab2 or subtab3
sta matrix+(xpos)+(ypos*40)
lda charcodes,y
sta screen+(xpos)+(ypos*40)
lda colcodes,y
sta $d800+(xpos)+(ypos*40)
__Final remarks__
The final bits of the effects was essentially down to starting the fire with a slow ramp-up of the heat (really improved the effect a lot when it builds up), ending each loop with cutting the heat-generation for the "burn out" effect + add some funky colors (in the revised version of Gumbo) to rotate through to make it more interesting to watch on repeat =)
-Raz, Feb 7th 2020 |