Генеративне мистецтво з 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); // малює повноекранний прямокутник
}
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);
}
}
pixelDensity(2)
у setup() забезпечує чітке зображення. Для збереження
використовуйте pixelDensity(4), а потім скиньте його після
збереження, щоб уникнути вичерпання пам'яті GPU.