Lode 的计算机图形学教程

等离子体

目录

简介

等离子体特效是 demo 中常用的一种波动动画效果。生成动态等离子体有很多方法,这里只讨论一种:将其作为正弦函数和其他函数的叠加。通过调整参数、尝试不同函数以及试验颜色,你最终会找到一个漂亮的效果。

Future Cube 制作的 demo《Second Reality》中的一个效果就是等离子体,渲染在旋转立方体的侧面上:



截图也展示了如何在 DOSBox 模拟器中运行无法在 Windows 上运行的老式 DOS demo :)

一些函数

在制作真正的等离子体之前,先来研究一些可以用于生成等离子体的函数。

正弦函数生成的值始终在 -1 到 +1 之间,绘制出来是这样的:



不过,我们以另一种方式来使用它,即将像素的颜色值设置为其 x 坐标的正弦值:



白色部分是正弦曲线的波峰,黑色部分是波谷。

用以下代码即可轻松实现:


int main(int argc, char *argv[])
{
  screen(256, 256, 0, "Plasma");

  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    int color = int(128.0 + (128.0 * sin(x / 8.0)));
    pset(x, y, ColorRGB(color, color, color));
  }
  redraw();
  sleep();
  return(0);
}

代码遍历每个像素,用正弦函数计算像素颜色并绘制。像素颜色值必须在 0 到 256 之间,由于正弦函数的结果始终在 -1 到 1 之间,我们将其乘以 128 再加 128,转换为 0 到 256 的范围。x 除以 8 是为了使正弦波更大。若除以更小的值(如 2),则得到不同大小的正弦波:



若改用 "sin((x + y) / 8.0)",则得到倾斜的正弦波:



另一个非常适合等离子体的函数是像素到某点距离的正弦值。

像素到点 (0, 0) 的距离为:sqrt(x * x + y * y)。像素到屏幕中心的距离为:sqrt((x - w / 2) * (x - w / 2) + (y - h / 2) * (y - h / 2))。对其取正弦,得到如下效果:



生成此图的代码如下:

int main(int argc, char *argv[])
{
  screen(256, 256, 0, "Plasma");

  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    int color = int(128.0 + (128.0 * sin(sqrt((x - w / 2.0) * (x - w / 2.0) + (y - h / 2.0) * (y - h / 2.0)) / 8.0)));
    pset(x, y, ColorRGB(color, color, color));
  }
  redraw();
  sleep();
  return(0);
}

将许多这样的函数叠加(各有不同的大小、中心点等),并使用比这里单调的灰度更好的调色板,就可以生成等离子体。先热个身,这里已经是一个真正的等离子体了,它是 2 个正弦函数的叠加:



虽然看起来还不够漂亮,但它确实是等离子体。它是 x 方向正弦和 y 方向正弦的叠加。代码比前面的例子难不了多少,但为了清晰起见,两个正弦的叠加被拆分成了几行。最后结果除以 2,使值回到 0-255 范围:

int main(int argc, char *argv[])
{
  screen(256, 256, 0, "Plasma");

  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    int color = int
    (
        128.0 + (128.0 * sin(x / 8.0))
      + 128.0 + (128.0 * sin(y / 8.0))
    ) / 2;
    pset(x, y, ColorRGB(color, color, color));
  }
  redraw();
  sleep();
  return(0);
}

基于调色板循环的等离子体

同时获得漂亮颜色和动画效果的方法是使用调色板(计算出的 "color" 参数作为从调色板中选取颜色的索引),每帧将调色板偏移几个值,使绘制的等离子体的所有颜色循环旋转。在调色板颜色模式下,显卡有自动旋转调色板的功能,因此只需绘制一次等离子体,然后每帧改变调色板即可。但我们这里使用 RGB 模式,所以需要模拟这个功能。

由于颜色会循环旋转,调色板不应有任何不连续性,从最后一种颜色回到第一种颜色时也不行。

这个调色板有很多不连续性,不能用于等离子体:



这个调色板有一处不连续:从右到左(从黄色跳回蓝色),也不能使用:



下面这个可以使用,它用 HSVtoRGB 函数通过循环遍历色相来生成(色相本身是循环的,参见光与颜色教程)。



用这种方式制作等离子体特效:生成调色板,生成包含每个像素正弦函数计算结果的等离子体缓冲区,生成后用调色板中的正确颜色绘制每个像素,并每帧偏移调色板。

以下是生成调色板和等离子体的代码部分。

plasma 数组包含等离子体缓冲区的值,palette 数组是调色板,buffer 数组用于在屏幕上绘制像素。

生成的等离子体与前面给出的黑白等离子体相同,但加上动画后已经看起来有些有趣了。


#define screenWidth 256
#define screenHeight 256

// Y-coordinate first because we use horizontal scanlines
Uint32 plasma[screenHeight][screenWidth];
Uint32 buffer[screenHeight][screenWidth];
Uint32 palette[256];

int main(int argc, char *argv[])
{
  screen(screenWidth, screenHeight, 0, "Plasma");

  //generate the palette
  ColorRGB colorRGB;
  for(int x = 0; x < 256; x++)
  {
    //use HSVtoRGB to vary the Hue of the color through the palette
    colorRGB = HSVtoRGB(ColorHSV(x, 255, 255));
    palette[x] = RGBtoINT(colorRGB);
  }

  //generate the plasma once
  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    //the plasma buffer is a sum of sines
    int color = int
    (
        128.0 + (128.0 * sin(x / 16.0))
      + 128.0 + (128.0 * sin(y / 16.0))
    ) / 2;
    plasma[y][x] = color;
  }


一切生成完毕后,主循环开始。此后不再需要太多数学计算。


  int paletteShift;

  //start the animation loop, it rotates the palette
  while(!done())
  {
    //the parameter to shift the palette varies with time
    paletteShift = int(getTime() / 10.0);

    //draw every pixel again, with the shifted palette color
    for(int y = 0; y < h; y++)
    for(int x = 0; x < w; x++)
    {
      buffer[y][x] = palette[(plasma[y][x] + paletteShift) % 256];
    }

    //make everything visible
    drawBuffer(buffer[0]);
    redraw();
  }

  return(0);
}

结果如下图所示,但颜色每帧都会变化:



下图展示了调色板中单个颜色值随时间在等离子体中的移动过程(图中仅显示 9 步)。它用与上面相同的代码生成,但调色板除了 3 个相邻颜色外其余全为白色(用 3 个而不是 1 个是为了更易观察):



这还是一个简单的等离子体,是时候尝试一些更复杂的了,它们是更多函数的叠加,包括使用距离公式的那个。

在代码中,将生成等离子体的部分替换为以下内容:这次是 4 个正弦的叠加,其中一个使用到左上角的距离公式:

    int color = int
    (
        128.0 + (128.0 * sin(x / 16.0))
      + 128.0 + (128.0 * sin(y / 8.0))
      + 128.0 + (128.0 * sin((x + y) / 16.0))
      + 128.0 + (128.0 * sin(sqrt(double(x * x + y * y)) / 8.0))
    ) / 4;
    plasma[y][x] = color;

以下是结果的两帧:



还有另一个可以尝试的函数:

    int color = int
    (
        128.0 + (128.0 * sin(x / 16.0))
      + 128.0 + (128.0 * sin(y / 32.0))
      + 128.0 + (128.0 * sin(sqrt(double((x - w / 2.0)* (x - w / 2.0) + (y - h / 2.0) * (y - h / 2.0))) / 8.0))
      + 128.0 + (128.0 * sin(sqrt(double(x * x + y * y)) / 8.0))
    ) / 4;
    plasma[y][x] = color; 



如你所见,可以不断调整参数直到效果满意。

最后,让我们换一个不同的调色板,因为目前使用的调色板可能有点太……"花哨"了。

将生成调色板的代码改为:


  //generate the palette
  ColorRGB colorRGB;
  for(int x = 0; x < 256; x++)
  {
    colorRGB.r = int(128.0 + 128 * sin(3.1415 * x / 32.0));
    colorRGB.g = int(128.0 + 128 * sin(3.1415 * x / 64.0));
    colorRGB.b = int(128.0 + 128 * sin(3.1415 * x / 128.0));
    palette[x] = RGBtoINT(colorRGB);
  } 

这将生成以下调色板:



该调色板可以平铺(左右两端都是蓝灰色),要确保调色板可平铺,需要在正弦函数内使用 pi 乘以 x 再除以 2 的幂次(前提是颜色数量本身是 2 的幂次)。

这是与上面相同的等离子体,但使用了新的调色板:



以下调色板能产生相当漂亮的效果(当然这可能是个人偏好 ;)):

  //generate the palette
  ColorRGB colorRGB;
  for(int x = 0; x < 256; x++)
  {
    colorRGB.r = int(128.0 + 128 * sin(3.1415 * x / 16.0));
    colorRGB.g = int(128.0 + 128 * sin(3.1415 * x / 128.0));
    colorRGB.b = 0;
    palette[x] = RGBtoINT(colorRGB);
  }  

调色板:



等离子体:



RGB 等离子体

除了使用调色板并循环遍历,还可以每帧重新计算正弦函数,并通过将时间引入这些函数来在每帧实际改变正弦波形。可以分别生成 R、G 和 B 通道。这样每帧每个像素都要计算大量正弦,速度不会很快。但值得一提,因为这样等离子体的实际形状会随时间变化。

这里用宏定义了"距离"公式,dist(a, b, c, d) 计算点 (a, b) 和点 (c, d) 之间的距离。

每帧,对每个像素计算 4 个不同距离的正弦之和。然后由此值计算出颜色值,用于创建像素绘制命令的 R、G、B 参数。由于颜色的计算方式(取一个小值的整数部分再乘以 32),颜色会存在不同的离散值,使效果看起来有点像不同的细胞。

#define dist(a, b, c, d) sqrt(double((a - c) * (a - c) + (b - d) * (b - d)))

int main(int argc, char *argv[])
{
  screen(256, 256, 0, "Plasma");
  double time;
  while(!done())
  {
    time = getTime() / 50.0;
    for(int y = 0; y < h; y++)
    for(int x = 0; x < w; x++)
    {
      double value = sin(dist(x + time, y, 128.0, 128.0) / 8.0)
             + sin(dist(x, y, 64.0, 64.0) / 8.0)
             + sin(dist(x, y + time / 7, 192.0, 64) / 7.0)
             + sin(dist(x, y, 192.0, 100.0) / 8.0);
      int color = int((4 + value)) * 32;
      pset(x, y, ColorRGB(color, color * 2, 255 - color));
    }
    redraw();
  }
  return(0);
}



你也可以为每个颜色通道分别计算等离子体等,可能性无穷无尽,但这始终比调色板循环慢,尤其是使用这些教程中未经优化的代码时。


最后编辑:2004 年

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