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

Обложка поста

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

Это руководство научит вас рисовать объекты из функции шума на языке 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) в качестве входных данных. Поскольку при создании гиф-анимации используются положительные и отрицательные значения, возникнет нежелательный эффект, если шум будет симметричен. Но этого можно избежать, если применить сдвиг круга.

Не смешно? А здесь смешно: @ithumor