好久之前,看到了水波特效的算法,最近又看到了火焰特效的算法,两者之间有很多的共同之处,我就将一篇关于火焰特效的文章翻译了一下,一共大家参阅,如有不足之处,请见量!由于本文作者写得很幽默,本人的水平有限,故有些地方还是采用的原文。
火焰特效
著 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!是你自己你不相信我。我想也许已开始可能有点迷糊。但是你好好地想一下之后,你就会一下子明白过来——像我一样。(译注:为了帮助理解,可以参考下图)