好久之前,看到了水傳輸速率效的演算法,最近又看到了火焰特效的演算法,兩者之間有很多的共同之處,我就將一篇關於火焰特效的文章翻譯了一下,一共大家參閱,如有不足之處,請見量!由於本文作者寫得很幽默,本人的水平有限,故有些地方還是採用的原文。
火焰特效
著 Shaun Patterson 譯 beauli
火焰效果也許是我在所有特效中最喜歡的特效之一。當你設計出一個非常有效火焰常式,這是非常值得的。
在本文中,我將向你展示如何在DirectX 5.0下製作一個非常美麗的火焰效果。我想只要你認真的學習之後,你將能夠用任何語言來進行設計。
首先,在本文中我將不講述怎樣建立DirectDraw Surface或者在代碼中添加任何有趣的實際代碼(譯註:即本文僅是火焰特效的演算法理論介紹)。我猜想你能在Sweet.Oblivion或GPMega上找到。然而,我將向你展示如何建立一個Palette,做一個簡單的迴圈,然後畫出火焰來。火焰實際上是一種比較簡單的特效,對它編程有一點娛樂的味道。
不幸的事,我不懂彙編,所以它並不是在整個世界上最快的火焰常式。但是它卻不可置信的在我的P2上快速的運行,而且在我的P75上表現得也不錯哦。
好了,下面轉入正題。首先你想讓你的代碼能真正的運行,你必須要建立一個Palette。我是這樣做的:
在底部依次分配為黑色(在火焰效果的頂部),紅色,橙色和黃色。
我僅僅用了一些迴圈就建立起了屬於我們自己的256種顏色。現在我們需要一個“容器”來盛放我們的調色盤資訊。
LPDIRECTDRAWPALETTE lpDDPal; // the palette object
PALETTEENTRY mypal[256]; // stores palette stuff
我想這是非常恰當的注釋。現在,我們需要載入我們的資訊。首先我們載入我們的RGB資訊到每一個mypal的數組條目中。(為什麼我使用迴圈)
index = 0;
for (index=95;index<200;index++)
{
mypal[index].peRed = index+70;
mypal[index].peGreen = index+30;
mypal[index].peBlue = rand()%10;
}
for (index = 1; index < 35; index++)
{
mypal[index].peRed = index+25;
mypal[index].peGreen = rand()%10;
mypal[index].peBlue = rand()%10;
}
for (index = 35; index < 55; index++)
{
mypal[index].peRed = index+25;
mypal[index].peGreen = index-25;
mypal[index].peBlue = rand()%10;
}
for (index = 55; index < 95; index++)
{
mypal[index].peRed = index+75;
mypal[index].peGreen = index;
mypal[index].peBlue = rand()%5;
}
for(index = 200; index < 255; index++)
{
mypal[index].peRed = index;
mypal[index].peGreen = index-rand()%25;
mypal[index].peBlue = rand()%5;
}
這是我發現的最好的結構。雖然我花了不到10分鐘時間。
所有的一切基本上是為了將RGB值裝載入屬於我們的每一個調色盤數組的條目中。mypal結構中的peRed成員是紅色的值,peGreen是綠色的值,以此類推。
現在我們已經接近調色盤對象(lpDDPal)和首頁面(primary surface)的值。我們只需要使用下面的語句就可以使用調色盤。
lpDD->CreatePalette(DDPCAPS_8BIT | DDPCAPS_ALLOW256, mypal, &lpDDPal, NULL);
lpDDSPrimary->SetPalette(lpDDPal);
lpDD——DirectDraw對象
lpDDSPrimary——Primary Surface
With me so far? No? Hmm, not good. Go read another tutorial then! Geez! People these days...
Still holding your breath? Well breathe. The rest is a breeze.
dun dun dun dunnnn
火焰演算法
首先,為了能夠使我們能進行繪畫,我製作了一個小程式來鎖住背景頁面。
unsigned char *Lock_Back_Buffer (void)
{
DDSURFACEDESC ddsd;
HRESULT ret;
ddsd.dwSize = sizeof(ddsd);
ret = DDERR_WASSTILLDRAWING;
while (ret == DDERR_WASSTILLDRAWING)
ret = lpDDSBack->Lock(NULL, &ddsd, 0, NULL);
return (ret == DD_OK ? (unsigned char *)ddsd.lpSurface : NULL);
}
這個函數返回一個指向螢幕結構的unsigned char指標,因此我們需要建立一個螢幕結構。
unsigned char *double_buffer = NULL;
接下來,我們給它分配一些記憶體空間
double_buffer = (unsigned char *)malloc(307200);
Oh yeah。我是工作在640x480x8bit 的分辨力下的。
接下來,我們需要另一個unsigned char指標數組來儲存我們的火焰像素。
unsigned char *fire_buffer = NULL;
fire_buffer = (unsigned char *)malloc(307200);
現在可不要忘了在結束時顯式的釋放這些空間。
free(double_buffer); free(fire_buffer);
Ok..whew.現在開始製作火焰部分,在螢幕的底部(最後一行)繪一些隨機點,這些隨即點的顏色可以任意給定為0或者255。如下所作:
for(x = 1; x < 637; x+=rand()%3)
{
if(rand()%2)
fire_buffer[(480*640) + x] = 255;
else
fire_buffer[(480*640) + x] = 0;
}
以上是我所喜歡的做法,當然你也可以不使用隨機點。
好了,這就是我製作火焰的方法,選擇一個像素的背景像素然後用像素的數量去除。現在你也許在想——WHAT?!好了,很簡單。你是不是有一個點?如果你熟悉像素的繪製,你一定也知道在1d數組中尋找一個指標數組中的點的方法——Y position * ScreenWidth + X position。還糊塗嗎?Good。這表示你已經開始思考了=)
就像打字機一樣的思考它吧。
tap tap tap tap tap tap CA CHING - next line
tap tap tap tap tap tap CA CHING - next line
tap tap tap tap tap tap CA CHING - next line
etc...
在這個例子中螢幕只有6個tap寬。好了,說你想要畫的像素在第二行第三列。2*6=?12。
12+3=?15。whoa!一口氣算完。
現在開始計數1,2,3。。。15。等一下,我們現在是在第三行!!開始計數是從0開始然後1, 2 ,3 的對嗎?所以2*6=12,12+3=15(譯註:原文為so 1*6 is 6. Then 6 + 3 is 9.恐為筆誤)。現在開始轉入正題。
哎!接下來該怎麼做呢?對了,使用迴圈。需要通過迴圈我們要繪製火焰的整個螢幕。開始進入迴圈,首先是Y然後X。你會馬上就能明白的。在X的迴圈中,就像我以前所說的,平均周圍的像素來獲得新像素。不要忘記調色盤(Palette)。假如我們的像素點隨機值為255,就是黃色。如果是0,就是黑色。接下來,255+0=?255。255/2=?128。whoa!!我們在調色盤中獲得了桔紅色地區的點。這是所有火焰的最基本的點。在火焰上升時,對顏色進行平均你能得到一個又一個新的顏色。這時候如果你思考得越多,你就會變得越明白。開始黃色,變成橙色,紅色,最後當它達到一個非常紅的時候變成黑色消失。當火焰上升時,它將會變成很cool的形狀。你將能夠在例子中看到。
偽碼:
for y = 1, y to screenheight, increment y
for x = 1, x to screenwidth, increment x
find our offset - what pixel we are going to start
averaging around - (Y*ScreenHeight)+x
add up the surrounding pixels - all eight of them
divide that total by 8 - hard concept there =)
now if that value is not 0 - not black
we decrement it - subtract 1 =)
end
Draw to our back buffer
end
好了,接下來是C++代碼
int x,y,fireoffset;
//calculate the bottom line pixels
for(x = 1; x < 637; x+=rand()%3)
{
if(rand()%2)
fire_buffer[(480*640) + X] = 255;
else
fire_buffer[(480*640) + X] = 0;
}
}
//CALCULATE THE SURROUNDING PIXELS
for(y = 1; y < 480; ++y)
{
for(x = 1; x <640; ++x)
{
fireoffset = (y*640) + x;
firevalue = ((fire_buffer[fireoffset-640] +
fire_buffer[fireoffset+640] +
fire_buffer[fireoffset+1] +
fire_buffer[fireoffset-1] +
fire_buffer[fireoffset-641] +
fire_buffer[fireoffset-639] +
fire_buffer[fireoffset+641] +
fire_buffer[fireoffset+639]) / 8); // this can be optimized by a
// look up table as I'll show you later
if(firevalue != 0) // is it black?
{
--firevalue; // Nope. Beam me down Scotty.
fire_buffer[fireoffset-640] = firevalue; // Plot that new color
// on the above pixel
// remember the typewriter analogy
}
}
}
double_buffer = Lock_Back_Buffer(); // Remember this function? Good
memcpy(double_buffer, fire_buffer, (640*480)); // Copy fire buffer to the screen
lpddsback->Unlock(NULL); // Unlock! Important! you have no idear!
好了,也許已開始你可能認為很不好理解,但是,實際上只要你好好地想一下,這是非常簡單的。
我們可以發現這個像素點,上面我們討論過的——Y*640+X,這就是我們的fireoffset——這個點是我們用來平均的中心點。
現在fireoffset-640=?位於這個點正上方的點是嗎?fireoffset-639=?位於這個點上方但是稍微偏右的點。所以是右上方的點。Fireoffset+640=?正下方的點。明白了嗎??
我說過它是很簡單的不是嗎?Ha!是你自己你不相信我。我想也許已開始可能有點迷糊。但是你好好地想一下之後,你就會一下子明白過來——像我一樣。(譯註:為了協助理解,可以參考)