前言

到目前為止,我們的所有作品都是在二維平面上的。但 p5.js 其實有一個強大的 3D 模式——只要在 createCanvas() 加上 WEBGL 參數,你就進入了一個全新的三維世界。

3D 模式打開了許多二維做不到的可能性:旋轉的幾何體、光影效果、深度感、透視投影。你可以用它來做 3D 的生成式藝術、互動雕塑、資料視覺化,甚至簡單的 3D 場景。

說實話,我一開始對 p5.js 的 3D 功能抱著「玩具」的心態——畢竟要做 3D,大家第一個想到的是 Three.js 或 Unity。但實際用過之後,我發現 p5.js 的 WEBGL 模式在「快速原型」和「藝術表達」方面有獨特的優勢:它的 API 保持了一貫的簡潔風格,讓你專注在創意而不是技術細節。

這篇文章會帶你進入 p5.js 的 3D 世界:基本幾何體、相機控制、光源設定、材質應用。


createCanvas WEBGL

進入 3D 模式只需要一個參數:

function setup() {
  createCanvas(800, 600, WEBGL);
}

function draw() { background(30, 30, 40);

// 畫一個旋轉的方塊 rotateX(frameCount * 0.01); rotateY(frameCount * 0.013);

fill(255, 100, 100); stroke(255); box(150); }

WEBGL 模式有幾個重要的差異:

  1. 座標原點在畫布中央(不是左上角)
  2. Y 軸方向朝下(跟 2D 模式一樣)
  3. Z 軸指向你(朝螢幕外面)
  4. 有深度緩衝(遠的物體會被近的擋住)
-Y (上)
      |
      |
      +------ +X (右)
     /
    /
  +Z (朝你)

3D 基本幾何體

p5.js 提供了幾個內建的 3D 幾何體:

function draw() {
  background(30, 30, 40);
  lights();  // 先加個基本光源

// 方塊 push(); translate(-250, -100, 0); rotateY(frameCount * 0.02); fill(255, 100, 100); box(100); // box(寬, 高, 深) 或 box(大小) pop();

// 球體 push(); translate(0, -100, 0); rotateY(frameCount * 0.02); fill(100, 255, 100); noStroke(); sphere(60); // sphere(半徑, 細分X, 細分Y) pop();

// 圓柱 push(); translate(250, -100, 0); rotateX(frameCount * 0.02); fill(100, 100, 255); cylinder(40, 120); // cylinder(半徑, 高度) pop();

// 圓錐 push(); translate(-250, 150, 0); rotateX(frameCount * 0.02); fill(255, 200, 100); cone(50, 120); // cone(半徑, 高度) pop();

// 圓環 push(); translate(0, 150, 0); rotateX(frameCount * 0.02); fill(200, 100, 255); torus(60, 20); // torus(外半徑, 管半徑) pop();

// 平面 push(); translate(250, 150, 0); rotateX(-0.5); fill(100, 200, 200); plane(120, 120); // plane(寬, 高) pop(); }

自訂幾何體

beginShape()vertex() 建立自訂的 3D 形狀:

function draw() {
  background(30, 30, 40);
  lights();
  rotateX(frameCount * 0.01);
  rotateY(frameCount * 0.015);

// 金字塔 fill(255, 180, 50); stroke(255); strokeWeight(1);

let s = 100; let h = 150;

// 四個側面 beginShape(TRIANGLES); // 前面 vertex(0, -h/2, 0); vertex(-s/2, h/2, s/2); vertex(s/2, h/2, s/2);

// 右面 vertex(0, -h/2, 0); vertex(s/2, h/2, s/2); vertex(s/2, h/2, -s/2);

// 後面 vertex(0, -h/2, 0); vertex(s/2, h/2, -s/2); vertex(-s/2, h/2, -s/2);

// 左面 vertex(0, -h/2, 0); vertex(-s/2, h/2, -s/2); vertex(-s/2, h/2, s/2); endShape();

// 底面 beginShape(); vertex(-s/2, h/2, s/2); vertex(s/2, h/2, s/2); vertex(s/2, h/2, -s/2); vertex(-s/2, h/2, -s/2); endShape(CLOSE); }


Camera 控制

在 3D 場景中,「從哪裡看」跟「看什麼」一樣重要。

基本相機

function draw() {
  background(30, 30, 40);

// camera(眼睛x, 眼睛y, 眼睛z, 看向x, 看向y, 看向z, 上方x, 上方y, 上方z) camera( 200 sin(frameCount 0.01), // 眼睛 x:繞圈 -150, // 眼睛 y:在物體上方 200 cos(frameCount 0.01), // 眼睛 z:繞圈 0, 0, 0, // 看向原點 0, 1, 0 // 上方是 Y 正方向 );

lights();

// 地面 push(); translate(0, 50, 0); rotateX(HALF_PI); fill(50, 50, 60); noStroke(); plane(400, 400); pop();

// 物體 fill(255, 100, 100); noStroke(); box(80); }

orbitControl 互動相機

p5.js 有個很方便的函數,讓你用滑鼠旋轉、縮放、平移相機:

function draw() {
  background(30, 30, 40);
  orbitControl();  // 就這一行!
  lights();

// 左鍵拖曳:旋轉 // 右鍵拖曳:平移 // 滾輪:縮放

fill(255, 100, 100); box(100);

push(); translate(200, 0, 0); fill(100, 255, 100); sphere(50); pop(); }

透視 vs 正交投影

function draw() {
  background(30, 30, 40);

// 透視投影(預設)— 有近大遠小效果 perspective(PI / 3, width / height, 0.1, 5000);

// 正交投影 — 無透視效果,像技術製圖 // ortho(-width/2, width/2, -height/2, height/2, 0.1, 5000);

orbitControl(); lights();

// 排列多個方塊,觀察透視差異 for (let z = -500; z <= 500; z += 200) { push(); translate(0, 0, z); fill(map(z, -500, 500, 100, 255), 100, 100); box(80); pop(); } }


光源設定

光源是 3D 場景的靈魂。沒有光,所有物體都是平的色塊。p5.js 提供了幾種光源類型:

環境光(Ambient Light)

均勻地照亮所有表面,不產生陰影或高光:

ambientLight(80, 80, 80);  // 淡灰色的環境光

方向光(Directional Light)

平行光,像遠處的太陽:

// directionalLight(顏色, 方向)
directionalLight(255, 255, 255, 0.5, 1, -0.5);
// 從右上方照下來

點光源(Point Light)

從一個點向四面八方發光,像燈泡:

// pointLight(顏色, 位置)
pointLight(255, 200, 100, 200, -200, 200);

聚光燈(Spot Light)

錐形光束,像舞台燈:

// spotLight(顏色, 位置, 方向, 角度, 衰減)
let spotX = 200  sin(frameCount  0.02);
spotLight(
  255, 255, 255,          // 白光
  spotX, -300, 200,       // 位置
  0, 1, 0,                // 方向(朝下)
  PI / 6,                 // 錐角
  5                       // 衰減
);

綜合光源範例

function draw() {
  background(20, 20, 30);
  orbitControl();

// 微弱的環境光 ambientLight(30, 30, 40);

// 暖色的點光源(跟著時間移動) let lx = 200 sin(frameCount 0.02); let lz = 200 cos(frameCount 0.02); pointLight(255, 180, 100, lx, -150, lz);

// 冷色的方向光 directionalLight(100, 150, 255, -0.5, 0.5, -1);

// 用小球標記點光源位置 push(); translate(lx, -150, lz); fill(255, 180, 100); noStroke(); emissiveMaterial(255, 180, 100); sphere(10); pop();

// 地面 push(); translate(0, 100, 0); rotateX(HALF_PI); fill(80, 80, 90); noStroke(); plane(600, 600); pop();

// 物體們 noStroke();

push(); translate(0, 0, 0); fill(255, 100, 100); sphere(50); pop();

push(); translate(-150, 50, -50); fill(100, 255, 100); box(70); pop();

push(); translate(150, 50, 50); fill(100, 100, 255); torus(40, 15); pop(); }


材質(Material)

材質決定了物體表面如何回應光線。

基本材質

// 正常材質 — 會回應光源
fill(255, 100, 100);
normalMaterial();  // 依法線方向著色(除錯用)

環境材質

ambientMaterial(255, 100, 100);
// 只對 ambientLight 有反應

自發光材質

emissiveMaterial(255, 100, 0);
// 自己發光,不受光源影響

鏡面材質

specularMaterial(255);
shininess(50);  // 高光的銳利度
// 會產生高光反射

材質展示範例

function draw() {
  background(20);
  orbitControl();

ambientLight(50); pointLight(255, 255, 255, 200, -200, 200); directionalLight(200, 200, 255, -1, 1, -1);

// 環境材質 push(); translate(-200, 0, 0); ambientMaterial(255, 100, 100); sphere(50); pop();

// 自發光材質 push(); translate(-70, 0, 0); emissiveMaterial(100, 255, 100); sphere(50); pop();

// 鏡面材質(低光澤) push(); translate(70, 0, 0); specularMaterial(100, 100, 255); shininess(10); sphere(50); pop();

// 鏡面材質(高光澤) push(); translate(200, 0, 0); specularMaterial(255, 255, 255); shininess(100); sphere(50); pop(); }


3D 生成式藝術範例

把之前學的概念應用到 3D 中。以下是一個用 noise 驅動的 3D 地形:

let cols = 50;
let rows = 50;
let scl = 15;
let terrain = [];
let flying = 0;

function setup() { createCanvas(800, 600, WEBGL);

// 初始化地形陣列 for (let y = 0; y < rows; y++) { terrain[y] = []; for (let x = 0; x < cols; x++) { terrain[y][x] = 0; } } }

function draw() { background(20, 20, 30);

// 生成地形高度 flying -= 0.03; let yoff = flying; for (let y = 0; y < rows; y++) { let xoff = 0; for (let x = 0; x < cols; x++) { terrain[y][x] = map(noise(xoff, yoff), 0, 1, -80, 80); xoff += 0.15; } yoff += 0.15; }

// 相機設定 rotateX(PI / 3); translate(-cols scl / 2, -rows scl / 2, 0);

// 光源 ambientLight(30, 30, 50); directionalLight(200, 200, 255, 0.5, 0.5, -1); pointLight(255, 150, 100, cols scl / 2, rows scl / 2, 100);

// 繪製地形網格 for (let y = 0; y < rows - 1; y++) { beginShape(TRIANGLE_STRIP); for (let x = 0; x < cols; x++) { let h1 = terrain[y][x]; let h2 = terrain[y + 1][x];

// 高度 → 顏色 let c1 = getTerrainColor(h1); let c2 = getTerrainColor(h2);

fill(c1); vertex(x scl, y scl, h1);

fill(c2); vertex(x scl, (y + 1) scl, h2); } endShape(); } }

function getTerrainColor(h) { if (h < -30) { return color(30, 50, 120); // 深水 } else if (h < -10) { return color(50, 80, 160); // 淺水 } else if (h < 10) { return color(80, 160, 80); // 草地 } else if (h < 40) { return color(120, 100, 70); // 山坡 } else { return color(220, 220, 230); // 雪頂 } }

3D 粒子雲

let particles = [];

function setup() { createCanvas(800, 600, WEBGL);

for (let i = 0; i < 2000; i++) { particles.push({ pos: p5.Vector.random3D().mult(random(50, 250)), size: random(1, 4), speed: random(0.001, 0.005), offset: random(TWO_PI) }); } }

function draw() { background(15, 15, 25); orbitControl();

// 微弱的環境光 ambientLight(30); pointLight(255, 200, 150, 0, 0, 0);

// 中央發光球 push(); emissiveMaterial(255, 150, 50); noStroke(); sphere(20); pop();

// 粒子 for (let p of particles) { let angle = frameCount * p.speed + p.offset;

// 繞原點旋轉 let x = p.pos.x cos(angle) - p.pos.z sin(angle); let z = p.pos.x sin(angle) + p.pos.z cos(angle); let y = p.pos.y + sin(frameCount 0.01 + p.offset) 10;

let d = dist(0, 0, 0, x, y, z); let brightness = map(d, 50, 250, 255, 50);

push(); translate(x, y, z); emissiveMaterial(brightness, brightness 0.7, brightness 0.4); noStroke(); sphere(p.size); pop(); } }


效能注意事項

3D 模式比 2D 吃效能得多。幾個建議:

  1. 減少幾何體數量:sphere 和 torus 的 detail 參數可以降低
  2. noStroke():描邊在 3D 中很耗效能
  3. 限制光源數量:每個光源都會增加 shader 的計算量
  4. 善用 push/pop:避免不必要的矩陣運算
// 降低球體精細度
sphere(50, 12, 8);  // 預設是 (50, 24, 16)

// 批量繪製時關閉描邊 noStroke();


小結

p5.js 的 WEBGL 模式為你打開了三維創作的大門。雖然它不像 Three.js 那樣功能完整,但它的簡潔 API 讓你可以快速原型、快速實驗。

這篇文章我們學了:

  1. WEBGL 模式的啟用與座標系統
  2. 六種內建 3D 幾何體
  3. 相機控制(手動設定與 orbitControl)
  4. 四種光源類型及其特性
  5. 材質系統(環境、自發光、鏡面)
  6. 3D 地形生成與粒子雲範例

3D 的世界很深,這篇只是入門。但有了這些基礎,你已經可以開始做很多有趣的 3D 創意專案了。

延伸閱讀

  • p5.js 官方 3D 教學頁面 — WEBGL 的完整 API 文件
  • Daniel Shiffman, Coding Train: “WEBGL in p5.js” 系列教學
  • The Book of Shaders — 如果你想深入 shader 程式設計
  • Three.js — 當 p5.js 的 3D 功能不夠用時的進階選擇
  • p5.js createShader() 文件 — 自訂 GLSL shader