Урок · Творче кодування · Початковий–середній рівень
📅 Березень 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.

Часті запитання

Чого я навчуся в цьому уроці?

Створюйте генеративне мистецтво за допомогою p5.js для творчого кодування та фрагментних шейдерів GLSL для візуалізації на GPU. Поля потоку, шум Перліна, плитки Трюше та збереження експорту полотна.

Які теми розглядаються в цьому уроці?

Цей урок охоплює такі теми: Основи p5.js, Фрагментні шейдери GLSL у p5, Поля потоку з шумом Перліна, Плитки Трюше, Кольорові палітри за формулою, Збереження полотна у PNG.

Які інструменти й технології використовуються?

Цей урок використовує: p5.js, GLSL.

Скільки часу займає цей урок?

Цей урок займає приблизно 40 хв.

Які попередні знання потрібні?

Це урок рівня «Початковий–середній рівень» — окрема попередня підготовка, крім базового JavaScript, не потрібна.