Создание GIF-анимаций из шума на языке Processing

Это руководство научит вас рисовать объекты из функции шума на языке Processing. Вы изучите простой способ создания зацикленных GIF-анимаций на основе функции шума.

Функция шума

В языке Processing есть функция noise(), которая при некоторых входных данных выдаёт значения от 0 до 1 (с центром в точке 0,5). Эти значения являются случайными, но они всегда повторяются при повторении входных данных и соли.

Другое свойство этой функции заключается в том, что шум непрерывен: для близких входных данных вы получаете почти одинаковые значения. Вычисление значений шума выполняется быстро, не нужно вычислять предыдущие значения. Эта функция в Processing иногда называется шумом Перлина.

С помощью небольшого фрагмента кода:

void setup(){
  size(500,500);
  background(0);
  stroke(255);
  noFill();
}
 
void draw(){
  float scale = 0.03;
 
  beginShape();
  for(int x = 0; x<width;x++){
    float y = height*noise(scale*x);
    vertex(x,y);
  }
  endShape();
}

Можно получить такой результат:

noise, Processing

Параметр scale используется потому, что без него значения изменяются слишком быстро.

Также следует помнить, что функция шума симметрична: noise(x) = noise(-x).

При каждом запуске эскиза кривая выглядит по-разному. Но можно всегда получать один и тот же результат, используя функцию noiseSeed(). Ещё обратите внимание на функцию noiseDetail(), которая изменяет параметры шума (например, его плавность).

Есть одна хитрость: чтобы нарисовать ещё одну независимую кривую, можно просто взять значения шума, далёкие от предыдущих. Например, сначала используете noise(scale*x), а затем noise(scale*x + 1000).

Эскиз выше демонстрирует пример одномерного шума, двумерный принимает 2 значения с плавающей запятой и возвращает значения от 0 до 1. Это можно представить как уровень яркости на 2D-изображении.

void setup(){
  size(500,500);
  background(0);
  stroke(255);
  noFill();
}
 
void draw(){
  float scale = 0.01;
 
  for(int x = 0; x<width;x++){
    for(int y = 0; y<height;y++){
      stroke(255*noise(scale*x,scale*y));
      point(x,y);
    }
  }
}

Результат:

noise, Processing

Единственным способом визуализации трёхмерного шума является использование времени в качестве третьего измерения.

void setup(){
  size(500,500);
  background(0);
  stroke(255);
  noFill();
  noiseDetail(5);
}
 
void draw(){
  background(0);
  float scale = 0.01;
 
  loadPixels();
  for(int x = 0; x<width;x++){
    for(int y = 0; y<height;y++){
      float col = 255*noise(scale*x,scale*y,10*scale*frameCount);
      pixels[x + width*y] = color(col);
    }
  }
  updatePixels();
 
  if(frameCount<=50){
    saveFrame("tuto###.png");
  }
}

Результат:

noise, Processing

Чтобы анимация зацикливалась, вам понадобится четырёхмерный шум. Функция шума в языке Processing ограничена 3D, поэтому знакомьтесь с openSimplex noise. Чтобы использовать её, вставьте этот код в подобную вкладку вашего Processing-эскиза.

OpenSimplexNoise noise;
 
void setup(){
  size(500,500);
  background(0);
  stroke(255);
  noFill();
 
  noise = new OpenSimplexNoise();
}
 
void draw(){
  float scale = 0.03;
 
  beginShape();
  for(int x = 0; x<width;x++){
    float ns = (float)noise.eval(scale*x,0);
    float y = map(ns,-1,1,0,height);
    vertex(x,y);
  }
  endShape();
}

Результат:

noise, Processing

Фактически здесь используется 2D шум с нулём в качестве второго входного параметра, потому что реализация openSimplex noise имеет только 2D, 3D и 4D шум.

Прежде чем использовать эту функцию для создания зацикленных анимаций, давайте нарисуем что-нибудь более интересное, отличное от шума. Используем пороговое значение для шума, чтобы нарисовать пиксели чёрным или белым:

OpenSimplexNoise noise;
  
void setup(){
  size(500,500);
  background(0);
  stroke(255);
  noFill();
  
  noise = new OpenSimplexNoise();
}
  
void draw(){
  background(0);
  float scale = 0.02;
  
  loadPixels();
  for(int x = 0; x<width;x++){
    for(int y = 0; y<height;y++){
      boolean b = (float)noise.eval(scale*x,scale*y) > 0;
      float col = b?255:0;
      pixels[x + width*y] = color(col);
    }
  }
  updatePixels();
  
  saveFrame("tuto3.jpg");
}

Результат:

noise, Processing

Оживление шума

Это хороший способ сделать идеальное зацикливание. Он будет работать везде, где используется 1D или 2D шум. Замените noise.eval(scale*x,scale*y) на noise.eval(scale*x,scale*y,radius*cos(TWO_PI*t),radius*sin(TWO_PI*t)).

Чтобы зацикливание анимации было идеальным, сделаем круг в двух новых измерениях шумового пространства. Немного проблематично реализовать графическую интерпретацию шума в 2D, но с 1D шумом это означало бы, что нужно брать значения на линии для каждого кадра и перемещать эту линию вдоль цилиндра с увеличением времени.

Давайте применим это к рисунку с пороговым значением:

OpenSimplexNoise noise;
 
void setup(){
  size(500,500);
  background(0);
  stroke(255);
  noFill();
 
  noise = new OpenSimplexNoise();
}
 
int numFrames = 75;
 
float radius = 1.0;
 
void draw(){
  float t = 1.0*frameCount/numFrames;
 
  background(0);
  float scale = 0.02;
 
  loadPixels();
  for(int x = 0; x<width;x++){
    for(int y = 0; y<height;y++){
      boolean b = (float)noise.eval(scale*x,scale*y,radius*cos(TWO_PI*t),radius*sin(TWO_PI*t)) > 0;
      float col = b?255:0;
      pixels[x + width*y] = color(col);
    }
  }
  updatePixels();
 
  println(t);
 
  if(frameCount<=numFrames){
    saveFrame("tuto2###.gif");
  }
  if(frameCount == numFrames){
    println("finished");
    stop();
  }
}

Результат:

noise, Processing

radius — это параметр, который будет контролировать, насколько сильно изменится анимация.

Можно анимировать пример кривой с помощью той же техники:

OpenSimplexNoise noise;
 
void setup(){
  size(500,500);
  background(0);
  stroke(255);
  noFill();
 
  noise = new OpenSimplexNoise();
}
 
int numFrames = 150;
 
float radius = 1.5;
 
void draw(){
  float t = 1.0*frameCount/numFrames;
 
  background(0);
  float scale = 0.02;
 
  beginShape();
  for(int x = 0; x<width;x++){
    float ns = (float)noise.eval(scale*x,radius*cos(TWO_PI*t),radius*sin(TWO_PI*t));
    float y = map(ns,-1,1,0,height);
    vertex(x,y);
  }
  endShape();
 
  println(t);
 
  if(frameCount<=numFrames){
    saveFrame("tuto2###.jpg");
  }
  if(frameCount == numFrames){
    println("finished");
    stop();
  }
}

Результат:

noise, Processing

Теперь давайте применим технику к предыдущему примеру с яркостью. Гифка будет выглядеть размытой, потому что в используемой здесь реализации openSimplex noise не так много деталей (но эта плавность отлично выглядит).

OpenSimplexNoise noise;
 
void setup(){
  size(500,500);
  background(0);
  stroke(255);
  noFill();
 
  noise = new OpenSimplexNoise();
}
 
int numFrames = 75;
 
float radius = 1.5;
 
void draw(){
  float t = 1.0*frameCount/numFrames;
 
  background(0);
  float scale = 0.02;
 
  loadPixels();
  for(int x = 0; x<width;x++){
    for(int y = 0; y<height;y++){
      float ns = (float)noise.eval(scale*x,scale*y,radius*cos(TWO_PI*t),radius*sin(TWO_PI*t));
      float col = map(ns,-1,1,0,255);
      pixels[x + width*y] = color(col);
    }
  }
  updatePixels();
 
  println(t);
 
  if(frameCount<=numFrames){
    saveFrame("tuto3###.jpg");
  }
  if(frameCount == numFrames){
    println("finished");
    stop();
  }
}

Результат:

noise, Processing

Теперь вы знаете, что каждый Processing-эскиз, использующий 1D или 2D шум, можно легко превратить в идеально зацикленную анимацию.

Давайте рассмотрим, как можно сделать несколько более сложных и интересных картинок. Начнём с этого:

noise, Processing

Белые точки генерируются случайным образом внутри круга. Каждая из них подвергается горизонтальному и вертикальному смещению на основе 2D шума. Интенсивность смещения становится равной нулю вблизи границы круга. Анимация выполняется с использованием той же техники 4D шума.

Вот код для генерации рамок GIF: noisetraj.pde

Обратите внимание, что метод drawCurve() просто рисует круг, но его можно легко изменить.

Пример:

noise, Processing

Здесь некоторые точки начинаются с решётки и рисуют свою траекторию вслед за шумовым полем. Это поле изменяется с использованием техники 4D-шума (код).

noise, Processing

Пороговое значение для 2D шума определяет, что будет отображаться: «/» или «\». Снова анимирование с 4D шумом (код).

noise, Processing

Линии представляют 2D поле на основе 2D шума. Всё то же анимирование с 4D шумом (код).

noise, Processing

Вот кое-что посложнее. В основном здесь используются пороговые кривые 1D шума, чтобы определить, каким цветом будут отрисовываться элементы: чёрным или белым. Параметры для анимации каждого столбца различны. Большой диск рисуется в центре в режиме исключения (код для чего-то похожего).

noise, Processing

В каждой строке используются независимые значения из 1D шума, анимация с трёхмерным шумом (код).

Заключение

Теперь вы знаете, как использовать функцию шума и различные её вариации для рисования собственных анимаций.

P.S.: openSimplex noise не симметрична, поэтому не возникает никаких проблем при использовании radius*cos(TWO_PI*t) или radius*sin(TWO_PI*t) в качестве входных данных. Поскольку при создании гиф-анимации используются положительные и отрицательные значения, возникнет нежелательный эффект, если шум будет симметричен. Но этого можно избежать, если применить сдвиг круга.

Перевод статьи «Drawing from noise, and then making animated loopy GIFs from there.»