Lode 的计算机图形学教程

火焰特效

目录

返回目录

简介

这是一篇关于经典火焰特效以及如何为其创建火焰调色板的简短教程。也许日后会扩展为用粒子或其他更好看的方式来制作火焰。

经典火焰特效

这种火焰特效在 Java 演示程序、老式 demo 等场景中被广泛使用。它非常容易编程,而且能产生非常漂亮的火焰效果。

首先需要创建一个调色板,通常包含 256 种颜色,但由于这里是模拟实现,也可以使用其他数量。使用 HSLtoRGB 函数可以轻松创建合适的调色板:选择从红色到黄色的色相范围,饱和度保持为 255,亮度从 0 到 255,使得偏红的颜色同时也更暗。调色板是为配合 drawBuffer 函数使用而创建的,因此 RGB 颜色以 BGR 顺序存储在单个整数值中。

为了存储火焰数据,需要一个与屏幕大小相同的缓冲区(此处称为 fire[][])。在使用调色板的图形模式下,该缓冲区可以就是屏幕本身,但这里使用 RGB 模式并模拟调色板。缓冲区初始时所有像素值为 0。然后在最底行给每个像素赋随机值,并在每帧都更新这些随机值。每帧中,根据下方两行像素来计算当前行的每个像素:每个像素的值等于其正下方 1 个像素、左下方 1 个像素、右下方 1 个像素,以及正下方第 2 行像素的总和,再除以一个略大于 4 的值,使火焰向上扩散时逐渐熄灭。除数越大,火焰能上升的高度越低。例如,使用 *16, /65(即除以 4.0625)时,火焰比使用 *4, /17(即除以 4.25)时上升得更高。若除以 4,火焰将永远上升;若除以 5,火焰会消散得太快。

下图展示了计算中涉及哪些像素:带十字的像素是当前像素,4 个绿色像素是用于计算当前像素值的像素。



计算应从上到下进行。若顺序错误,部分像素会在依赖它们的像素计算之前就被重新计算。每一新帧只能依赖上一帧的值。屏幕顶部的 y 坐标为 0,底部的 y 坐标为 h-1。

屏幕两侧的像素没有左邻居或右邻居,为解决此问题,可以将特效做成循环的,使最右侧像素以最左侧像素为邻居,反之亦然。为此,可以对邻居像素的 x 坐标对屏幕宽度取模。

这里的特效在高分辨率下速度不是很快,但通过 waitFrame 函数设置了每秒最大帧数,以防在未来更快的计算机上运行得过快。

由于有了火焰缓冲区,可以使用 drawBuffer 函数代替逐像素绘制,速度快得多。不过绘制的缓冲区与 fire 数组不同,因为后者的值为 0 到 256,会产生错误的颜色。因此需要创建一个新缓冲区,将从调色板获取的 RGB 颜色存入其中,然后绘制这个缓冲区(此处也命名为"buffer")。

将以下代码放入 main.cpp 文件中,代码中的注释会进一步说明:

#include <cmath>
#include <string>
#include <vector>
#include <iostream>

#include "quickcg.h"
using namespace QuickCG;

//define the width and height of the screen and the buffers
const int screenWidth = 640;
const int screenHeight = 128;

// Y-coordinate first because we use horizontal scanlines
Uint32 fire[screenHeight][screenWidth];  //this buffer will contain the fire
Uint32 buffer[screenHeight][screenWidth];  //this is the buffer to be drawn to the screen
Uint32 palette[256]; //this will contain the color palette

int main(int argc, char *argv[])
{
  //set up the screen
  screen(screenWidth, screenHeight, 0, "fire");

  //declarations
  ColorRGB color; //used during palette generation
  float time = getTime(), oldTime; //the time of this and the previous frame, for timing

  //make sure the fire buffer is zero in the beginning
  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  fire[y][x] = 0;

  //generate the palette
  for(int x = 0; x < 256; x++)
  {
    //HSLtoRGB is used to generate colors:
    //Hue goes from 0 to 85: red to yellow
    //Saturation is always the maximum: 255
    //Lightness is 0..255 for x=0..128, and 255 for x=128..255
    color = HSLtoRGB(ColorHSL(x / 3, 255, std::min(255, x * 2)));
    //set the palette to the calculated RGB value
    palette[x] = RGBtoINT(color);
  }

  //start the loop (one frame per loop)
  while(!done())
  {
    //timing: set to maximum 50 milliseconds per frame = 20 frames per second
    oldTime = time;
    waitFrame(oldTime, 0.05);
    time = getTime();

    //randomize the bottom row of the fire buffer
    for(int x = 0; x < w; x++) fire[h - 1][x] = abs(32768 + rand()) % 256;
    //do the fire calculations for every pixel, from top to bottom
    for(int y = 0; y < h - 1; y++)
    for(int x = 0; x < w; x++)
    {
      fire[y][x] =
        ((fire[(y + 1) % h][(x - 1 + w) % w]
        + fire[(y + 1) % h][(x) % w]
        + fire[(y + 1) % h][(x + 1) % w]
        + fire[(y + 2) % h][(x) % w])
        * 32) / 129;
    }

    //set the drawing buffer to the fire buffer, using the palette colors
    for(int y = 0; y < h; y++)
    for(int x = 0; x < w; x++)
    {
      buffer[y][x] = palette[fire[y][x]];
    }

    //draw the buffer and redraw the screen
    drawBuffer(buffer[0]);
    redraw();
  }
}

运行后,一开始屏幕全黑,火焰每帧上升两个像素,直到达到最大高度。之后由于每帧底部都会随机化,火焰会持续动画:



如果不在每帧随机化火焰缓冲区底部,而只在开始时随机化一次,那么火焰也不会动画,一旦上升到最大高度就会永远静止:



通过修改参数可以调整火焰效果,例如将计算改为以下内容后的效果:


      fire[y][x] =
        ((fire[(y + 1) % h][(x - 1 + w) % w]
        + fire[(y + 2) % h][(x) % w]
        + fire[(y + 1) % h][(x + 1) % w]
        + fire[(y + 3) % h][(x) % w])
        * 64) / 257;





除了随机化最底行像素,也可以随机化物体边缘,例如制作燃烧的文字等。


最后编辑:2007 年 8 月 20 日

版权所有 (c) 2004-2007 Lode Vandevenne,保留所有权利。