Lode 的计算机图形学教程

使用随机噪声生成纹理

目录

返回索引

简介

在自然界中,万物都呈现出随机的外观,而数学公式通常无法生成随机效果,除非你善加利用。随机噪声(例如由 Ken Perlin 发明的 Perlin 噪声)利用随机数来生成具有自然感的纹理。

平滑噪声

作为随机噪声的来源,我们需要一个随机值数组,称为 noise[x][y]。由于我们的目标是生成 2D 纹理,因此使用二维数组。函数 generateNoise 将用噪声填充该数组,而主函数负责将噪声数组显示到屏幕上。噪声本身由 <cstdlib> 头文件中的 rand() 函数生成,该函数返回 0 到 32768 之间的随机整数(如头文件中所定义)。通过除以 32768.0 将其归一化为 0 到 1 之间的随机实数(请确保使用浮点除法)。

#define noiseWidth 128
#define noiseHeight 128

double noise[noiseHeight][noiseWidth]; //the noise array

void generateNoise();

int main(int argc, char *argv[])
{
  screen(noiseWidth, noiseHeight, 0, "Random Noise");
  generateNoise();

  ColorRGB color;

  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    color.r = color.g = color.b = Uint8(256 * noise[x][y]);
    pset(x, y, color);
  }

  redraw();
  sleep();
  return 0;
}

void generateNoise()
{
  for (int y = 0; y < noiseHeight; y++)
  for (int x = 0; x < noiseWidth; x++)
  {
    noise[y][x] = (rand() % 32768) / 32768.0;
  }
}

以下是它生成的噪声:



然而,这种噪声看起来不够自然,尤其是放大之后。在主函数的像素循环中,将访问噪声数组所用的 x 和 y 除以 8 来进行放大,你会得到一种块状效果:

    color.r = color.g = color.b = Uint8(256 * noise[y / 8][x / 8]);
    pset(x, y, color);

放大时,我们希望得到更平滑的效果。为此,可以使用线性插值。目前噪声是一个数组,其内容只能通过离散的整数索引访问。通过对小数部分使用双线性插值,可以使其更加平滑。为此,引入了一个新函数 smoothNoise:

double smoothNoise(double x, double y)
{
   //get fractional part of x and y
   double fractX = x - int(x);
   double fractY = y - int(y);

   //wrap around
   int x1 = (int(x) + noiseWidth) % noiseWidth;
   int y1 = (int(y) + noiseHeight) % noiseHeight;

   //neighbor values
   int x2 = (x1 + noiseWidth - 1) % noiseWidth;
   int y2 = (y1 + noiseHeight - 1) % noiseHeight;

   //smooth the noise with bilinear interpolation
   double value = 0.0;
   value += fractX     * fractY     * noise[y1][x1];
   value += (1 - fractX) * fractY     * noise[y1][x2];
   value += fractX     * (1 - fractY) * noise[y2][x1];
   value += (1 - fractX) * (1 - fractY) * noise[y2][x2];

   return value;
}

返回值是数组中 4 个相邻像素的加权平均值。在主函数中,现在用它代替直接访问噪声数组,并对除法使用实数:

    color.r = color.g = color.b = Uint8(256 * smoothNoise(x / 8.0, y / 8.0));
    pset(x, y, color);

这仍然是放大 8 倍后的结果,但现在加入了双线性插值。如果不放大,将无法看出插值效果:



这对随机噪声相当有用,平滑方法或许还可以更好,双线性插值常被 3D 显卡用于游戏中纹理的平滑处理,是一种廉价而快速的技术。

我们将这张图像称为"噪声纹理"。

湍流

湍流能够从平滑噪声中创造出具有自然感的特征。其技巧在于将多个不同缩放比例的噪声纹理叠加在一起。在山脉中可以找到这一原理在自然界中的体现:有非常大的特征(主山体),它们是深度放大的噪声。



然后叠加在山体上的是较小的特征:多个山顶、坡度的变化……



然后,在更小的尺度上,山上还有岩石。



更小的一层是沙粒。所有这些层叠加在一起,就形成了具有自然感的山脉。

在 2D 中,这是通过将不同尺寸的平滑噪声叠加来实现的。



这里缩放因子从 16 开始,每次除以 2。持续这样做直到缩放因子为 1。山脉示例中较小的特征不仅在宽度上更小,在高度上也更小。为了在 2D 纹理中实现这一点,将缩放比例较小的图像变暗,这样叠加时其影响就会较小:



将这 5 张图像叠加,再将结果除以 5 取平均值,就得到了湍流纹理:



以下是一个能自动对单个像素完成上述所有操作的函数。参数 "size" 是初始缩放因子,在上面的示例中为 16。返回值经过归一化,使其成为 0 到 255 之间的数值。

double turbulence(double x, double y, double size)
{
  double value = 0.0, initialSize = size;

  while(size >= 1)
  {
    value += smoothNoise(x / size, y / size) * size;
    size /= 2.0;
  }

  return(128.0 * value / initialSize);
}

要使用湍流函数,将遍历每个像素的循环中的那小段代码替换为以下内容:

    color.r = color.g = color.b = Uint8(turbulence(x, y, 64));
    pset(x, y, color);

这里的 size 设为 64,结果如下所示:



如果将初始 size 设为 256,结果会更大也更平滑:



以下是初始 size 仅为 8 时的效果:



由于使用了双线性滤波平滑函数,这里的纹理存在一些明显的水平和垂直线条。Photoshop 中的云彩滤镜会生成与上面类似的纹理,但使用了更优质的平滑函数。不过,更优质的平滑函数超出了本文的讨论范围。

如果完全不使用平滑函数,效果如下:


云朵

要生成带云朵的天空,可以使用上面的湍流纹理,但将黑白调色板替换为蓝白调色板。为此可以使用 HSLtoRGB 函数,将色调设为蓝色(169 或 240°),明度范围设为 192 到 255 以使其足够白。以下是实现此效果的新主函数:

#define noiseWidth 320
#define noiseHeight 240

double noise[noiseHeight][noiseWidth]; //the noise array

void generateNoise();
double smoothNoise(double x, double y);
double turbulence(double x, double y, double size);

int main(int argc, char *argv[])
{
  screen(noiseWidth, noiseHeight, 0, "Random Noise");
  generateNoise();

  Uint8 L;
  ColorRGB color;

  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    L = 192 + Uint8(turbulence(x, y, 64)) / 4;
    color = HSLtoRGB(ColorHSL(169, 255, L));

    pset(x, y, color);
  }

  redraw();
  sleep();
  return 0;
}



大理石

可以使用随机噪声来创建看起来像大理石的纹理。为此,以正弦图案作为基础,正弦图案如下所示:



正弦纹理通过给位置 (x, y) 处的像素赋予颜色值 255 * sin(x + y) 来生成。可以通过将 x 和 y 乘以系数来改变角度和周期(即线条数量)。正弦图案具有深色和亮色线条,通过在正弦中加入湍流项来对这些线条施加湍流,就能得到类似大理石纹脉的效果:

int main(int argc, char *argv[])
{
  screen(noiseWidth, noiseHeight, 0, "Marble");
  generateNoise();

  ColorRGB color;

  //xPeriod and yPeriod together define the angle of the lines
  //xPeriod and yPeriod both 0 ==> it becomes a normal clouds or turbulence pattern
  double xPeriod = 5.0; //defines repetition of marble lines in x direction
  double yPeriod = 10.0; //defines repetition of marble lines in y direction
  //turbPower = 0 ==> it becomes a normal sine pattern
  double turbPower = 5.0; //makes twists
  double turbSize = 32.0; //initial size of the turbulence

  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    double xyValue = x * xPeriod / noiseWidth + y * yPeriod / noiseHeight + turbPower * turbulence(x, y, turbSize) / 256.0;
    double sineValue = 256 * fabs(sin(xyValue * 3.14159));
    color.r = color.g = color.b = Uint8(sineValue);
    pset(x, y, color);
  }

  redraw();
  sleep();
  return 0;
}

值 "xyValue" 是 x 乘以一个系数、y 乘以一个系数以及湍流乘以一个系数的总和。xPeriod、yPeriod 和 turbPower 是可以调整以获得不同纹理的参数。将湍流除以 256 是为了将其转换为 0 到 1 之间的值,因为湍流函数的返回值范围是 0 到 255。上述参数值给出如下结果:



减小 turbPower 会使扭曲减少,例如将其设为 1.0,你会得到:



现在可以更清楚地看到正弦图案的使用方式,深色和亮色线条只有轻微的扭曲,仍然呈现出一种自然感。

增大湍流函数的初始 size 会使扭曲变大(从而更加微妙,类似于减小 turbPower),而较小的初始 size 则会产生更小但更剧烈的扭曲。这里 turbPower 再次设为 5.0,turbSize 分别设为 128.0 和 16.0:



改变 x 和 y 的周期可以增减黑色线条的数量,例如这里通过将 xPeriod 设为 0、yPeriod 设为 1,使线条变宽且角度为 0°,这样只会有一条水平黑线。turbSize 设为 32,turbPower 仅设为 1,以便更清楚地看到线条的方向:



以下是相同参数,但 turbPower 恢复为 5,可以看到足够大的湍流是如何完全掩盖只有一条黑线这一事实的:



你也可以通过为 R、G、B 使用不同的值来改变大理石的颜色,例如使其呈现略带红色或黄色的效果:

    double xyValue = x * xPeriod / noiseWidth + y * yPeriod / noiseHeight + turbPower * turbulence(x, y, turbSize) / 256.0;
    double sineValue = 226 * fabs(sin(xyValue * 3.14159));
    color.r = Uint8(30 + sineValue);
    color.g = Uint8(10 + sineValue);
    color.b = Uint8(sineValue);
    pset(x, y, color);



通过调整这些参数,你可以得到完全不同的大理石或石材图案。

木纹

通过向以下数学函数中添加湍流,可以创建出具有自然感的木纹年轮:



要得到上面的图案,取 x 和 y 到中心距离的正弦值,即位置 x, y 处像素的颜色值为 256 * sin(sqrt(x*x + y*y))。在正弦中加入湍流项,就能得到具有自然感的木纹。

R、G、B 的值由结果计算得出,使木纹呈现棕色外观:

int main(int argc, char *argv[])
{
  screen(noiseWidth, noiseHeight, 0, "Wood");
  generateNoise();

  ColorRGB color;

  double xyPeriod = 12.0; //number of rings
  double turbPower = 0.1; //makes twists
  double turbSize = 32.0; //initial size of the turbulence

  for(int y = 0; y < h; y++)
  for(int x = 0; x < w; x++)
  {
    double xValue = (x - noiseWidth / 2) / double(noiseWidth);
    double yValue = (y - noiseHeight / 2) / double(noiseHeight);
    double distValue = sqrt(xValue * xValue + yValue * yValue) + turbPower * turbulence(x, y, turbSize) / 256.0;
    double sineValue = 128.0 * fabs(sin(2 * xyPeriod * distValue * 3.14159));
    color.r = Uint8(80 + sineValue);
    color.g = Uint8(30 + sineValue);
    color.b = 30;
    pset(x, y, color);
  }

  redraw();
  sleep();
  return 0;
}

这里年轮应当清晰可见,因此与大理石不同,turbPower 应当较小。



以下是年轮更多时的结果:xyPeriod 设为 25。



这里木纹再次有 12 个年轮,但湍流更多:turbPower 设为 0.2:



如果将 turbPower 设得太高,年轮将不再可见,你会得到更像大理石图案的效果。这里设为 0.5:



由此可见,通过添加噪声,可以将数学 2D 函数转变为具有自然感的纹理。你可以在更多函数上尝试这一方法,例如以下是数学图案 sin(x) + sin(y):



用以下代码为其添加一些噪声:

    double xValue = (x - noiseWidth / 2) / double(noiseWidth) + turbPower * turbulence(x, y, turbSize) / 256.0;
    double yValue = (y - noiseHeight / 2) / double(noiseHeight) + turbPower * turbulence(h - y, w - x, turbSize) / 256.0;
    double sineValue = 22.0 * fabs(sin(xyPeriod * xValue * 3.1415) + sin(xyPeriod * yValue * 3.1415));
    color = HSVtoRGB(ColorHSV(Uint8(sineValue), 255, 255));
    pset(x, y, color);

你会得到:



2D 随机噪声还可用于地形高度图、物理模拟等……

3D 随机噪声


随机噪声可以扩展到任意维度。扩展到 3D 需要添加一个 z 分量,除了宽度和高度之外,噪声数组现在还需要一个深度维度。

#define noiseWidth 192
#define noiseHeight 192
#define noiseDepth 64

double noise[noiseDepth][noiseHeight][noiseWidth]; //the noise array

generateNoise 函数现在需要填充三维数组,因此增加了一个额外的循环:

void generateNoise()
{
  for(int z = 0; z < noiseDepth; z++)
  for(int y = 0; y < noiseHeight; y++)
  for(int x = 0; x < noiseWidth; x++)
  {
    noise[z][y][x] = (rand() % 32768) / 32768.0;
  }
}

平滑函数现在需要在 x、y 和 z 方向上进行插值,因此有 8 个项而不是只有 4 个:

double smoothNoise(double x, double y, double z)
{
  //get fractional part of x and y
  double fractX = x - int(x);
  double fractY = y - int(y);
  double fractZ = z - int(z);

  //wrap around
  int x1 = (int(x) + noiseWidth) % noiseWidth;
  int y1 = (int(y) + noiseHeight) % noiseHeight;
  int z1 = (int(z) + noiseDepth) % noiseDepth;

  //neighbor values
  int x2 = (x1 + noiseWidth - 1) % noiseWidth;
  int y2 = (y1 + noiseHeight - 1) % noiseHeight;
  int z2 = (z1 + noiseDepth - 1) % noiseDepth;

  //smooth the noise with bilinear interpolation
  double value = 0.0;
  value += fractX     * fractY     * fractZ     * noise[z1][y1][x1];
  value += fractX     * (1 - fractY) * fractZ     * noise[z1][y2][x1];
  value += (1 - fractX) * fractY     * fractZ     * noise[z1][y1][x2];
  value += (1 - fractX) * (1 - fractY) * fractZ     * noise[z1][y2][x2];

  value += fractX     * fractY     * (1 - fractZ) * noise[z2][y1][x1];
  value += fractX     * (1 - fractY) * (1 - fractZ) * noise[z2][y2][x1];
  value += (1 - fractX) * fractY     * (1 - fractZ) * noise[z2][y1][x2];
  value += (1 - fractX) * (1 - fractY) * (1 - fractZ) * noise[z2][y2][x2];

  return value;
}

湍流函数很容易扩展,只需在调用 smoothNoise 函数时加入 z / size:

double turbulence(double x, double y, double z, double size)
{
  double value = 0.0, initialSize = size;

  while(size >= 1)
  {
    value += smoothNoise(x / size, y / size, z / size) * size;
    size /= 2.0;
  }

  return(128.0 * value / initialSize);
}

这里展示的主函数将使用 3D 随机噪声来生成随时间动态变化的云朵,仿佛云朵在平滑地形成和变化。

int main(int argc, char *argv[])
{
  screen(noiseWidth, noiseHeight, 0, "3D Random Noise");
  generateNoise();

  Uint8 L;
  ColorRGB color;
  double t;

  while(!done())
  {
    for(int y = 0; y < h; y++)
    for(int x = 0; x < w; x++)
    {
      L = 192 + Uint8(turbulence(x, y, t, 32)) / 4;
      color = HSLtoRGB(ColorHSL(169, 255, L));

      pset(x, y, color);

    }
    t = getTicks() / 40.0;
    redraw();
  }
  return 0;
}

截图当然无法展示动画效果:



3D 随机噪声可用于 2D 纹理动画、3D 纹理(例如,3D 纹理可用于岩石,当你移除部分岩石或射掉一块来查看内部时,如果使用 2D 纹理,移除部分后同一张 2D 纹理会再次被绘制出来)、3D 星球纹理、3D 体积雾等……


最后编辑:2004 年

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