CHAOS

  • 一个流场
  • 噪声线条



随机与噪声非常适合绘制看似杂乱无章又具有规律性的图形。


多现场输出ProcessBuilder流 processing流场_二维

一个流场

流场是用柏林噪声实现的,说到柏林噪声,就不得不放出这样的可视化图:

多现场输出ProcessBuilder流 processing流场_二维_02


相较于其他噪声,柏林噪声能够产生连续平滑的随机数,所以适用于生成地形或模拟其他自然中的随机现象。

调用processing中的noise函数可以获得一个一维、二维或三维的随机数值。二维的柏林噪声在x、y方向上都随机且连续,所以可以获得像上图的纹理,由于模拟大理石或生成地形。

因为我们希望我们的流场能够有随机效果,同时流场每个分量之间又应该是连续的,所以采用柏林噪声。

  • 第一步,将屏幕划分为resolution大小的网格,以便每个网格中可以使用柏林噪声获得随机值。
Flowfield(int r) {
    rows = ceil(width / resolution);
    cols = ceil(height / resolution);
    field = new float[cols][rows];
    init();
    zoff = 0;
  };
  • 第二步,遍历网格的每一个单元,使用两个变量xoff与yoff来记录在二维柏林噪声上的偏移量,输入连续的变量值,得到的噪声值也连续。

由于柏林噪声的返回结果是一个(0,1)的小数,而在流场中,我们希望得到每个单元格中随机的方向,所以需要一个(0, 2π)的值,所以使用map函数将之映射到(0,2π)。

field[i][j] = map(noise(xoff, yoff),0, 1, 0, TWO_PI);

流场绘制时计算每个单元格的中心点

float x = (j + 0.5) * resolution;
        float y = (i + 0.5) * resolution;

根据随机得到的角度确定直线的两端进行绘制。

line(x - cos(field[i][j]) * resolution / 2, y - sin(field[i][j]) * resolution / 2, x + cos(field[i][j]) * resolution / 2, y + sin(field[i][j]) * resolution / 2);

改变xoff与yoff每次变化的大小可以改变获得的噪声值,差值越大,噪声值越不连续。

void init() {
    float xoff = 0;
    for(int i = 0; i < cols; i++) {
      float yoff = 0;
      for(int j = 0; j < rows; j++) {
        field[i][j] = map(noise(xoff, yoff),0, 1, 0, TWO_PI);
        
        yoff += 0.2;
      }
      xoff += 0.2;
    }
  }

此时我们可以得到一个静态的流场,可以看到它的每个方向都是随机的,但由于柏林噪声的特性,整体又很连续。

多现场输出ProcessBuilder流 processing流场_二维_03

  • 第三步,使用第三分量zoff使流场动起来。
    noise(xoff, yoff)能够得到一个二维的噪声,其在x、y方向上连续且随机,但由于柏林噪声是一个伪随机数,因此当x、y输入的值一样时,得到的结果也是一样的。
    一种方法是每次改变xoff与yoff的值,如使其初始值改变。
float xoff = xstart;
    float yoff = ystart;
xstart += 0.2;
    ystart += 0.2;

但这样得到的结果,我们可以明显发现原来的流场在进行平移,视觉效果非常不好。

多现场输出ProcessBuilder流 processing流场_i++_04


正确方法是使用三维的柏林噪声,添加zoff方向作为时间上的变化量,才可以得到正确的连续变化的二维柏林噪声图。

void update() {
    float xoff = 0;
    for(int i = 0; i < cols; i++) {
      float yoff = 0;
      for(int j = 0; j < rows; j++) {
        field[i][j] = map(noise(xoff, yoff, zoff),0, 1, 0, TWO_PI);
        
        float x = (j + 0.5) * resolution;
        float y = (i + 0.5) * resolution;
        stroke(200);
        line(x - cos(field[i][j]) * resolution / 2, y - sin(field[i][j]) * resolution / 2, x + cos(field[i][j]) * resolution / 2, y + sin(field[i][j]) * resolution / 2);
        
        yoff += 0.2;
      }
      xoff += 0.2;
    }
    zoff += 0.2;
  };

多现场输出ProcessBuilder流 processing流场_i++_05


写到这里,原本想要实现鼠标交互,随机扩散的效果,但苦于还没想好流场内部方向与扩散效果边缘方向统一的规则,而且好像比较适合使用流体来模拟,所以暂且搁置,重新写了一个比较简单的交互。

(但这个不难实现,先把坑挖着,等有空继续)

噪声线条

由流场得到灵感,实现噪声线条比较简单,不需要一个二维数组来记录网格中每一个单元格的方向,因为竖直的线条中每个的x坐标都是相等的,而y坐标之间的间隔也相等,只需要记录当前是第几根线条计算便可以得到。
依然与流场中思路一样,对于y方向上每一根线条,每次偏移量xoff递增,将得到的噪声值映射到(0,2π),根据噪声值绘制旋转后的线段。

void updateDir(PVector mouse) {
    float xoff = 0;
    for(int i = 0; i < num; i++) {
      xoff += 0.1;
      float theta = map(noise(xoff, yoff),0, 1, 0, TWO_PI);
      stroke(255);
      float l = lineLength * map(abs(sin(theta)),0, 1, 0.3, 1) ;
      line(points[i].x - cos(theta) * l, points[i].y - sin(theta) * l, points[i].x + cos(theta) * l, points[i].y + sin(theta) * l);
    }
    yoff += 0.1;
  }

我们可以得到这样一根不断连续扭动的线条。

多现场输出ProcessBuilder流 processing流场_随机数_06


由于想要一个鼠标的交互,我们希望当鼠标滑动时,线条可以被拨动。

方法是每次判断鼠标与各个点之间的距离(这里可以优化,因为线条是竖直的,所以当鼠标在x轴距离线条x轴足够远时,我们便不用考虑其影响),当她们之间的距离小于150时,改变这个点的位置,使之向鼠标位置移动。

移动的距离由它与鼠标之间的距离决定,距离越远,移动距离越小。

PVector p = new PVector(pos, (i + 0.5) * interval);
      float distance = dist(mouse.x, mouse.y, p.x, p.y);
      if(distance < 150) {
        if((mouse.x - points[i].x) > 0){
          points[i].x += min((mouse.x - points[i].x) * (1 - distance / 150), 40 * (1 - distance / 150));
        }
        else {
          points[i].x += max((mouse.x - points[i].x) * (1 - distance / 150), -40 * (1 - distance / 150));
        }

多现场输出ProcessBuilder流 processing流场_i++_07

然后我们发现,当鼠标移走后,发生了形变的线条没有自动恢复成直线,而是一直保持原来的弯曲状态。这显然不是我们想要的效果。

因此使线条不在鼠标吸引范围内时自动恢复。自动恢复时我们不希望它一帧内突变,所以使它慢慢回去,每帧恢复1/5。

else if(abs(points[i].x - pos) > 1) {
        points[i].x += (pos - points[i].x) / 5;
      }
for(int i = 0; i < num; i++) {
      xoff += 0.1;
      PVector p = new PVector(pos, (i + 0.5) * interval);
      float distance = dist(mouse.x, mouse.y, p.x, p.y);
      if(distance < 150) {
        if((mouse.x - points[i].x) > 0){
          points[i].x += min((mouse.x - points[i].x) * (1 - distance / 150), 40 * (1 - distance / 150));
        }
        else {
          points[i].x += max((mouse.x - points[i].x) * (1 - distance / 150), -40 * (1 - distance / 150));
        }
      }

这样,我们得到了正确又平滑的效果。

多现场输出ProcessBuilder流 processing流场_i++_08

但这样太单调了,我们再使之每根线条的粗细发生变化,而非使用同一长度:

float l = lineLength * map(abs(sin(theta)),0, 1, 0.3, 1) ;

并且使它不再是一根直线,而也用柏林噪声发生随机扭动:

points[i].x += sin(theta) * 8;

多现场输出ProcessBuilder流 processing流场_二维_09


最后,一根线条太孤单了,再多几条吧。

Flowfield field;
noiseLine[] noiseline;
int lineNum = 4;

void setup() {
  size(800, 600);
  background(0);

  noiseline = new noiseLine[lineNum];
  int interval = width / (lineNum + 1);
  for(int i = 0; i < lineNum; i++) {
    noiseline[i] = new noiseLine(40, 10, interval * (i + 1) - 10, i * 5);
  }
}
void draw() {
  if(frameCount % 10 == 0) {
    fill(0, 200);
    rect(0, 0, width, height);
    for(noiseLine nl: noiseline) {
      nl.updateDir(new PVector(mouseX, mouseY));
    }
  }
}

多现场输出ProcessBuilder流 processing流场_i++_10