前言
如果你曾看過那些粒子像河流一樣蜿蜒流動的生成式藝術作品,八成背後就是一個流場(Flow Field)在驅動。流場是我最喜歡的創意程式設計主題之一——它的概念簡潔優雅,效果卻可以無限豐富。
流場的核心想法很簡單:在畫布上佈滿一個看不見的向量場,每個位置都有一個「方向」,然後讓粒子跟著這些方向移動。當我們用 Perlin noise 來生成這些方向時,流場就會展現出一種有機、流暢、如同自然界中的水流或氣流般的美感。
我還記得第一次看到 Tyler Hobbs 的《Fidenza》系列作品時的震撼——那些色彩斑斕的線條在畫布上流淌,形成了某種介於混沌與秩序之間的紋理。而這一切的基礎,就是流場。
這篇文章會帶你理解向量場的概念、用 Perlin noise 生成流場、讓粒子沿著流場移動,並且調校出漂亮的視覺效果。
向量場概念
向量場就是一個函數,給定空間中的每一個點,回傳一個向量。在二維的情況下,每個 (x, y) 座標都對應一個方向和大小。
想像一張氣象圖上的風場:每個位置都有一個小箭頭,表示該處風的方向和強度。我們的流場就是這樣的東西。
在實作上,我們把畫布切割成一個網格,每個格子儲存一個角度值:
let cols, rows;
let scl = 20; // 每個格子的大小
let flowField = [];
function setup() {
createCanvas(800, 600);
cols = floor(width / scl);
rows = floor(height / scl);
flowField = new Array(cols * rows);
}
scl 是解析度——值越小,流場越精細,但計算量也越大。20 像素通常是個不錯的起點。
用 Perlin Noise 生成角度
Perlin noise 是流場的靈魂。它是一種連續的偽隨機函數,相鄰的輸入會產生相鄰的輸出。這意味著流場的方向變化是平滑的,不會突然跳變。
let zoff = 0; // 第三維度,用於動態變化
function generateFlowField() {
let yoff = 0;
for (let y = 0; y < rows; y++) {
let xoff = 0;
for (let x = 0; x < cols; x++) {
let index = x + y * cols;
// noise 回傳 0~1 的值,乘以 TWO_PI 映射到完整角度
let angle = noise(xoff, yoff, zoff) TWO_PI 2;
flowField[index] = p5.Vector.fromAngle(angle);
flowField[index].setMag(1); // 可調整流場的「推力」
xoff += 0.1; // noise 的 x 步進
}
yoff += 0.1; // noise 的 y 步進
}
zoff += 0.003; // 時間步進,讓流場緩慢變化
}
這裡有幾個關鍵的參數需要理解:
xoff和yoff的步進值(0.1):決定 noise 的「縮放」。值越小,流場越平滑、大尺度的旋渦;值越大,流場越混亂、細碎的紋理。zoff的步進值(0.003):決定流場隨時間變化的速度。設成 0 則流場是靜態的。- 角度的映射範圍:
noise <em> TWO_PI </em> 2讓角度可以覆蓋完整的旋轉範圍。乘以不同的倍數會產生不同的圖案特性。
視覺化流場本身
在開發階段,把流場畫出來有助於理解和除錯:
function drawFlowField() {
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
let index = x + y * cols;
let v = flowField[index];
push();
translate(x scl + scl / 2, y scl + scl / 2);
rotate(v.heading());
stroke(100, 150);
strokeWeight(1);
line(0, 0, scl * 0.6, 0);
// 小箭頭
line(scl 0.6, 0, scl 0.4, -3);
line(scl 0.6, 0, scl 0.4, 3);
pop();
}
}
}
粒子沿流場移動
粒子的設計跟上一篇的粒子系統類似,但這次粒子的行為不是由重力主導,而是由流場決定:
class FlowParticle {
constructor() {
this.pos = createVector(random(width), random(height));
this.vel = createVector(0, 0);
this.acc = createVector(0, 0);
this.maxSpeed = 3;
this.prevPos = this.pos.copy();
}
follow(flowField) {
// 根據位置找到所在格子的向量
let x = floor(this.pos.x / scl);
let y = floor(this.pos.y / scl);
let index = constrain(x + y * cols, 0, flowField.length - 1);
let force = flowField[index];
this.applyForce(force);
}
applyForce(force) {
this.acc.add(force);
}
update() {
this.vel.add(this.acc);
this.vel.limit(this.maxSpeed);
this.prevPos = this.pos.copy();
this.pos.add(this.vel);
this.acc.mult(0);
}
display() {
stroke(255, 10);
strokeWeight(1);
line(this.prevPos.x, this.prevPos.y, this.pos.x, this.pos.y);
}
edges() {
// 環形邊界:從一邊消失,另一邊出現
if (this.pos.x > width) { this.pos.x = 0; this.prevPos = this.pos.copy(); }
if (this.pos.x < 0) { this.pos.x = width; this.prevPos = this.pos.copy(); }
if (this.pos.y > height) { this.pos.y = 0; this.prevPos = this.pos.copy(); }
if (this.pos.y < 0) { this.pos.y = height; this.prevPos = this.pos.copy(); }
}
}
注意幾個設計決策:
- 畫線而非畫點:用
line(prevPos, pos)取代ellipse,讓粒子的軌跡形成連續的線條 - 環形邊界:粒子飛出畫布後從另一側回來,保持畫面的粒子密度
prevPos重置:環形跳轉時重置前一個位置,避免畫出跨越整個畫布的線條
完整流場範例
把所有元素組合在一起:
let particles = [];
let flowField = [];
let cols, rows;
let scl = 20;
let zoff = 0;
const NUM_PARTICLES = 3000;
function setup() {
createCanvas(800, 600);
cols = floor(width / scl);
rows = floor(height / scl);
for (let i = 0; i < NUM_PARTICLES; i++) {
particles.push(new FlowParticle());
}
background(20, 20, 30);
}
function draw() {
// 不清除背景,讓軌跡累積
// 但加一層半透明背景來緩慢淡化舊軌跡
// background(20, 20, 30, 5); // 取消註解可啟用淡化
generateFlowField();
for (let p of particles) {
p.follow(flowField);
p.update();
p.edges();
p.display();
}
}
function generateFlowField() {
let yoff = 0;
for (let y = 0; y < rows; y++) {
let xoff = 0;
for (let x = 0; x < cols; x++) {
let index = x + y * cols;
let angle = noise(xoff, yoff, zoff) TWO_PI 2;
flowField[index] = p5.Vector.fromAngle(angle);
flowField[index].setMag(0.5);
xoff += 0.08;
}
yoff += 0.08;
}
zoff += 0.002;
}
如果不清除背景,粒子的軌跡會慢慢覆蓋整個畫布,最終形成一幅完整的流場畫作。這正是許多生成式藝術作品的做法。
視覺效果調校
流場的魅力在於,改變幾個參數就能得到完全不同的視覺風格。
調整 noise 的尺度
// 大尺度、平滑的流場
xoff += 0.03; // 小步進 → 大旋渦
// 小尺度、混沌的流場
xoff += 0.3; // 大步進 → 碎裂紋理
加入顏色
根據粒子的速度或位置來著色:
display() {
let speed = this.vel.mag();
let hue = map(speed, 0, this.maxSpeed, 180, 360);
colorMode(HSB, 360, 100, 100, 100);
stroke(hue, 80, 90, 5);
strokeWeight(1);
line(this.prevPos.x, this.prevPos.y, this.pos.x, this.pos.y);
colorMode(RGB);
}
粗細變化
根據 noise 值調整線條粗細:
follow(flowField) {
let x = floor(this.pos.x / scl);
let y = floor(this.pos.y / scl);
let index = constrain(x + y * cols, 0, flowField.length - 1);
let force = flowField[index];
this.applyForce(force);
// 額外的 noise 控制線條粗細
this.weight = map(noise(this.pos.x 0.01, this.pos.y 0.01), 0, 1, 0.5, 3);
}
display() {
stroke(255, 8);
strokeWeight(this.weight);
line(this.prevPos.x, this.prevPos.y, this.pos.x, this.pos.y);
}
多層流場
可以用不同的 noise 種子疊加多層流場,製造更豐富的視覺層次:
function generateFlowField() {
let yoff = 0;
for (let y = 0; y < rows; y++) {
let xoff = 0;
for (let x = 0; x < cols; x++) {
let index = x + y * cols;
// 兩層 noise 混合
let angle1 = noise(xoff, yoff, zoff) TWO_PI 2;
let angle2 = noise(xoff + 100, yoff + 100, zoff 0.5) TWO_PI;
let angle = lerp(angle1, angle2, 0.3);
flowField[index] = p5.Vector.fromAngle(angle);
flowField[index].setMag(0.5);
xoff += 0.08;
}
yoff += 0.08;
}
zoff += 0.002;
}
進階技巧:用影像驅動流場
你甚至可以用一張圖片的亮度來影響流場的方向:
let img;
function preload() {
img = loadImage('portrait.jpg');
}
function generateImageFlowField() {
img.loadPixels();
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
let index = x + y * cols;
// 取得對應像素的亮度
let px = floor(map(x, 0, cols, 0, img.width));
let py = floor(map(y, 0, rows, 0, img.height));
let c = img.get(px, py);
let bright = brightness(c);
// 亮度映射到角度
let angle = map(bright, 0, 100, 0, TWO_PI);
angle += noise(x 0.05, y 0.05) PI 0.5; // 加入一點隨機性
flowField[index] = p5.Vector.fromAngle(angle);
flowField[index].setMag(map(bright, 0, 100, 0.2, 1.5));
}
}
}
這個技巧可以讓粒子在亮區和暗區有不同的行為,最終在畫布上重新「畫」出原始影像的輪廓。
小結
流場是生成式藝術中最具表現力的工具之一。它的核心概念很簡單——向量場加粒子——但透過調整 noise 參數、色彩方案、線條風格,你可以創造出千變萬化的作品。
今天我們學了:
- 向量場的資料結構設計
- 用 Perlin noise 生成平滑的角度場
- 粒子跟隨流場的移動邏輯
- 各種視覺效果的調校技巧
- 用影像驅動流場的進階手法
我建議你花一個下午的時間,把這個範例跑起來,然後瘋狂調參數。流場就是那種——每次改一個數字,都會帶來驚喜的東西。
延伸閱讀
- Tyler Hobbs 的部落格文章《Flow Fields》 — 深入探討流場在藝術創作中的應用
- Daniel Shiffman, Coding Train: “Coding Challenge #24: Perlin Noise Flow Field”
- 《Generative Design》一書中的向量場章節
- p5.js noise() 文件:理解 Perlin noise 的維度與參數