Урок · Творче кодування · Початковий–середній рівень
📅 Березень 2026⏱ 40 хв читання🎓 Початковий–середній рівень⚙️ p5.js / GLSL

Генеративне мистецтво з p5.js і шейдерами GLSL

p5.js робить творче кодування доступним завдяки циклу draw() та допоміжним функціям, тоді як фрагментні шейдери GLSL відкривають візуалізацію з прискоренням на GPU, неможливу лише засобами JavaScript. Цей урок охоплює чотири техніки — від базових скетчів p5 до інтеграції шейдерів, полів потоку, плиток Трюше та експорту полотна — і пояснює математику кожної з них.

1. Основи p5.js

p5.js надає функцію setup(), що викликається один раз під час запуску, і цикл draw(), що викликається на кожному кадрі. Вбудована noise(x, y, z) повертає шум Перліна в діапазоні [0, 1]. Ось мінімальний скетч, що малює плинні кола:

// Підключення через CDN:  https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.9.0/p5.min.js
let t = 0;

function setup() {
  createCanvas(800, 800);
  background(10);
  noFill();
  strokeWeight(1.2);
}

function draw() {
  background(10, 10, 10, 8);  // напівпрозорий → сліди
  for (let i = 0; i < 200; i++) {
    const x = noise(i * 0.1, t) * width;
    const y = noise(i * 0.1, t + 100) * height;
    const r = noise(i * 0.1, t + 200) * 50 + 5;
    stroke(lerpColor(
      color(167, 139, 250),
      color(250, 200, 100),
      noise(i * 0.05, t + 300)
    ));
    circle(x, y, r * 2);
  }
  t += 0.003;
}

Ключові глобальні значення: width/height (розмір полотна), mouseX/mouseY (позиція вказівника в реальному часі), frameCount (цілочисловий лічильник кадрів), random(min, max) (рівномірний розподіл), noise(x[, y[, z]]) (плавний шум Перліна).

2. Фрагментні шейдери GLSL у p5.js

Перейдіть на createCanvas(w, h, WEBGL), щоб увімкнути режим WebGL у p5, а потім використовуйте createShader(vert, frag). Шейдер виконується на GPU для кожного пікселя — це ідеально для процедурних текстур, рейтрейсингу (ray marching) та ефектів, неможливих у Canvas2D.

const fragSrc = `
precision mediump float;
uniform vec2  u_res;
uniform float u_time;

// Матриця 2D-обертання
mat2 rot(float a) { float c=cos(a),s=sin(a); return mat2(c,-s,s,c); }

void main() {
  vec2 uv = (gl_FragCoord.xy - u_res * 0.5) / u_res.y;
  uv *= rot(u_time * 0.2);

  float d = length(uv);
  float rings = sin(d * 30.0 - u_time * 3.0) * 0.5 + 0.5;
  vec3  col   = mix(vec3(0.1, 0.0, 0.3), vec3(0.9, 0.6, 1.0), rings);

  gl_FragColor = vec4(col, 1.0);
}`;

const vertSrc = `
attribute vec3 aPosition;
void main() {
  gl_Position = vec4(aPosition * 2.0, 1.0);
}`;  // множимо на 2, щоб заповнити простір відсікання -1..1

let sh;
function setup() {
  createCanvas(800, 800, WEBGL);
  sh = createShader(vertSrc, fragSrc);
}

function draw() {
  shader(sh);
  sh.setUniform('u_res',  [width, height]);
  sh.setUniform('u_time', millis() / 1000);
  rect(-width/2, -height/2, width, height);  // малює повноекранний прямокутник
}
Попередження щодо координат: у режимі WEBGL p5 точка (0,0) — це центр полотна, а вісь Y зростає донизу. У шейдері початок координат gl_FragCoord.xy — у лівому нижньому куті. Відніміть u_res * 0.5 і поділіть на u_res.y, щоб отримати нормалізований UV-простір (–0.5…0.5).

3. Поля потоку з шумом Перліна

Поле потоку призначає вектор напрямку кожній точці у 2D-просторі. Частинки, випущені в поле, рухаються за цими напрямками, окреслюючи органічні криві. Напрямок обчислюється з градієнта функції шуму Перліна.

const COLS = 80, ROWS = 80;
const SCALE = 10;        // пікселів на комірку поля
const PARTICLES = 3000;

let field, particles;

function setup() {
  createCanvas(COLS*SCALE, ROWS*SCALE);
  field     = new Array(COLS * ROWS);
  particles = Array.from({length: PARTICLES}, () => ({
    x: random(width), y: random(height),
    vx: 0, vy: 0
  }));
  background(15);
}

function updateField(t) {
  for (let y = 0; y < ROWS; y++)
    for (let x = 0; x < COLS; x++) {
      const angle = noise(x * 0.08, y * 0.08, t) * TWO_PI * 2;
      field[y * COLS + x] = angle;
    }
}

function draw() {
  updateField(frameCount * 0.003);
  stroke(167, 139, 250, 40);
  strokeWeight(1);

  for (const p of particles) {
    const col   = floor(p.x / SCALE);
    const row   = floor(p.y / SCALE);
    const angle = field[row * COLS + col] || 0;
    p.vx = p.vx * 0.9 + cos(angle) * 2;
    p.vy = p.vy * 0.9 + sin(angle) * 2;
    const nx = p.x + p.vx, ny = p.y + p.vy;
    line(p.x, p.y, nx, ny);
    p.x = (nx + width)  % width;
    p.y = (ny + height) % height;
  }
}

4. Плитки Трюше

Себастьян Трюше (1704) виявив, що дві прості плитки — квадрат із дугами від кута до середини сторони — розміщені випадковим чином, утворюють напрочуд складні ґраткові візерунки. Кожна комірка незалежно повертається на 0°, 90°, 180° або 270°:

const T = 60;  // розмір плитки в пікселях

function setup() {
  createCanvas(720, 720);
  noFill();
  strokeWeight(2);
  stroke(200);
  background(15);
  drawTruchet();
  noLoop();
}

function drawTruchet() {
  const cols = width  / T;
  const rows = height / T;
  for (let gy = 0; gy < rows; gy++) {
    for (let gx = 0; gx < cols; gx++) {
      push();
      translate(gx * T + T/2, gy * T + T/2);
      rotate(floor(random(4)) * HALF_PI);
      // Дві чвертькругові дуги від середин сусідніх сторін
      arc(-T/2,  0,  T,  T, -HALF_PI, 0);
      arc( T/2,  0,  T,  T,  HALF_PI, PI);
      pop();
    }
  }
}

// Перегенерувати при кліку
function mousePressed() {
  background(15);
  drawTruchet();
}

Для варіації Сміта (менш регулярної) використовуйте псевдовипадкове обертання на основі хешу від координат ґратки — це дає детерміновані, але різноманітні візерунки, корисні для генерації ландшафту чи підземель.

5. Кольорові палітри за формулою

Формула косинусної палітри Іньїго Кілеса генерує плавні циклічні кольорові градієнти лише з чотирьох констант vec3 — ідеально як для p5, так і для GLSL:

// У GLSL: косинусна палітра — t це float 0..1
// vec3 palette(float t, vec3 a, vec3 b, vec3 c, vec3 d)
// col = a + b * cos(2π(c*t + d))

// Веселкова палітра: a=(0.5,0.5,0.5), b=(0.5,0.5,0.5), c=(1,1,1), d=(0,0.33,0.67)
function cosinePalette(t, a, b, c, d) {
  return {
    r: (a.r + b.r * Math.cos(TWO_PI * (c.r * t + d.r))) * 255,
    g: (a.g + b.g * Math.cos(TWO_PI * (c.g * t + d.g))) * 255,
    b: (a.b + b.b * Math.cos(TWO_PI * (c.b * t + d.b))) * 255
  };
}

// Використання у draw() p5:
const col = cosinePalette(i / PARTICLES,
  {r:.5, g:.5, b:.5}, {r:.5, g:.5, b:.5},
  {r:1,  g:1,  b:1},  {r:0,  g:.33, b:.67});
stroke(col.r, col.g, col.b, 120);

6. Збереження полотна у PNG

p5.js надає saveCanvas(), але для експорту з високою роздільною здатністю аж до 8192×8192 пікселів на звичайному екрані спершу рендерте поза екраном:

let exportMode = false;

function keyPressed() {
  if (key === 's' || key === 'S') {
    exportMode = true;
    const g = createGraphics(4096, 4096);  // експорт у 4K
    drawMySketch(g);                          // використовуйте g.noise(), g.line() тощо
    save(g, 'generative-art.png');
    g.remove();
    exportMode = false;
  }
  if (key === 'r' || key === 'R') {
    noiseSeed(Math.random() * 99999);       // нове випадкове зерно
    background(15);
  }
}
Згладжування для режиму WEBGL: p5 WEBGL на дисплеях Retina за замовчуванням рендерить із роздільною здатністю «щільність пікселів пристрою × розмір полотна». Виклик pixelDensity(2) у setup() забезпечує чітке зображення. Для збереження використовуйте pixelDensity(4), а потім скиньте його після збереження, щоб уникнути вичерпання пам'яті GPU.