前言
如果你已經玩過 p5.js 一陣子,做了不少 2D 的生成式藝術,某天你可能會想:「如果這些東西能在 3D 空間中呈現呢?」
p5.js 確實有 WEBGL 模式,可以做基本的 3D 渲染。但當你想要更精細的光影、自訂 shader、後處理效果,或是需要處理上萬個 3D 物件時,p5.js 就開始力不從心了。
這時候就輪到 Three.js 登場了。
Three.js 是 JavaScript 世界裡最成熟的 3D 圖形函式庫。它封裝了 WebGL 的底層 API,讓你可以用相對友善的方式建立 3D 場景、控制攝影機、設定材質和光源。
這篇文章會帶你從 p5.js 的角度出發,理解 Three.js 的核心概念,然後一步步做出 3D 生成式藝術。如果你能跟著做完,你會發現一個全新的創作維度在你面前展開。
Three.js vs p5.js WEBGL:該選哪個?
p5.js WEBGL 的優缺點
優點:
- API 熟悉,學習曲線平緩
- 跟 p5.js 的 2D 功能無縫整合
draw()loop 的概念不變
缺點:
- 效能有限(大量物件時明顯掉幀)
- Shader 支援有限
- 缺少進階功能(陰影、後處理、粒子系統)
- 材質和光源的控制粒度粗
Three.js 的優缺點
優點:
- 效能優異(底層最佳化做得好)
- 完整的材質系統(PBR、自訂 shader)
- 豐富的幾何體和生成器
- 後處理效果(Bloom、DOF、SSAO)
- 巨大的社群和生態系
缺點:
- 學習曲線較陡
- API 比較囉唆(boilerplate 多)
- 沒有
draw()loop 的簡潔抽象
我的建議
你的需求 建議
快速原型 / 2D 為主偶爾 3D → p5.js WEBGL
簡單的 3D 幾何展示 → p5.js WEBGL
需要精細的光影和材質 → Three.js
需要處理大量 3D 物件 → Three.js
想做 VR/AR 體驗 → Three.js
想深入學習 3D 圖形程式 → Three.js
Three.js 的核心概念
Scene, Camera, Renderer — 三位一體
每一個 Three.js 程式都從三個東西開始:
import * as THREE from 'three';
// 1. Scene(場景):放東西的空間
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
// 2. Camera(攝影機):決定你從哪個角度看
const camera = new THREE.PerspectiveCamera(
75, // 視角(FOV)
window.innerWidth / window.innerHeight, // 長寬比
0.1, // 近裁剪面
1000 // 遠裁剪面
);
camera.position.z = 5;
// 3. Renderer(渲染器):把場景畫到螢幕上
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
document.body.appendChild(renderer.domElement);
用 p5.js 的概念來類比:
| Three.js | p5.js |
|———|——-|
| Scene | Canvas(畫布) |
| Camera | 沒有直接對應(WEBGL 模式有預設 camera) |
| Renderer | p5.js 的渲染引擎(你看不到) |
| Mesh | box(), sphere() 等 3D 形狀 |
| Material | fill(), stroke() 的 3D 版 |
Mesh = Geometry + Material
Three.js 的 3D 物件由兩個部分組成:
// Geometry:定義形狀
const geometry = new THREE.BoxGeometry(1, 1, 1);
// Material:定義外觀
const material = new THREE.MeshStandardMaterial({
color: 0xff4040,
metalness: 0.3,
roughness: 0.7,
});
// Mesh:組合在一起
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
動畫循環
p5.js 有 draw(),Three.js 用 requestAnimationFrame:
function animate() {
requestAnimationFrame(animate);
// 更新物件
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
// 渲染
renderer.render(scene, camera);
}
animate();
完整的 Hello World
import * as THREE from 'three';
// 場景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x111111);
// 攝影機
const camera = new THREE.PerspectiveCamera(
75, window.innerWidth / window.innerHeight, 0.1, 1000
);
camera.position.z = 5;
// 渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 光源
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xffffff, 1, 100);
pointLight.position.set(5, 5, 5);
scene.add(pointLight);
// 物件
const geometry = new THREE.IcosahedronGeometry(1, 1);
const material = new THREE.MeshStandardMaterial({
color: 0xff4040,
wireframe: false,
flatShading: true,
});
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
// 動畫
function animate() {
requestAnimationFrame(animate);
mesh.rotation.x += 0.005;
mesh.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
// 視窗大小改變時調整
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
3D 幾何生成:生成式藝術的核心
參數化幾何體
Three.js 內建很多幾何體,但生成式藝術的重點是自己生成幾何:
// 用數學公式生成 3D 點陣
function generateParametricPoints(count) {
const points = [];
for (let i = 0; i < count; i++) {
const t = (i / count) Math.PI 20;
const r = 1 + Math.sin(t 0.3) 0.5;
// 螺旋線
const x = Math.cos(t) * r;
const y = t * 0.05 - 2;
const z = Math.sin(t) * r;
points.push(new THREE.Vector3(x, y, z));
}
return points;
}
// 用這些點建立幾何體
const points = generateParametricPoints(5000);
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const material = new THREE.PointsMaterial({
color: 0xff6060,
size: 0.02,
transparent: true,
opacity: 0.8,
});
const pointCloud = new THREE.Points(geometry, material);
scene.add(pointCloud);
噪音驅動的地形
用 Perlin noise 生成 3D 地形是經典的生成式藝術題材:
// 需要引入 simplex-noise 函式庫
import { createNoise3D } from 'simplex-noise';
const noise3D = createNoise3D();
function generateTerrain(width, height, segments) {
const geometry = new THREE.PlaneGeometry(width, height, segments, segments);
const positions = geometry.attributes.position;
const time = Date.now() * 0.001;
for (let i = 0; i < positions.count; i++) {
const x = positions.getX(i);
const y = positions.getY(i);
// 用 noise 生成高度
const elevation = noise3D(x 0.3, y 0.3, time 0.2) 1.5
+ noise3D(x 0.8, y 0.8, time 0.1) 0.5;
positions.setZ(i, elevation);
}
positions.needsUpdate = true;
geometry.computeVertexNormals();
return geometry;
}
// 建立地形 mesh
const terrainGeometry = generateTerrain(10, 10, 100);
const terrainMaterial = new THREE.MeshStandardMaterial({
color: 0x4488ff,
wireframe: true,
side: THREE.DoubleSide,
});
const terrain = new THREE.Mesh(terrainGeometry, terrainMaterial);
terrain.rotation.x = -Math.PI / 2;
scene.add(terrain);
Instanced Mesh:大量物件的效能秘器
如果你要在場景中放上千個相同形狀的物件,用 InstancedMesh 比建立上千個 Mesh 快得多:
const COUNT = 10000;
const geometry = new THREE.IcosahedronGeometry(0.1, 0);
const material = new THREE.MeshStandardMaterial({
color: 0xffffff,
flatShading: true,
});
const instancedMesh = new THREE.InstancedMesh(geometry, material, COUNT);
const dummy = new THREE.Object3D();
const color = new THREE.Color();
for (let i = 0; i < COUNT; i++) {
// 位置
const theta = Math.random() Math.PI 2;
const phi = Math.acos(Math.random() * 2 - 1);
const r = 2 + Math.random() * 3;
dummy.position.set(
r Math.sin(phi) Math.cos(theta),
r Math.sin(phi) Math.sin(theta),
r * Math.cos(phi)
);
// 隨機旋轉
dummy.rotation.set(
Math.random() * Math.PI,
Math.random() * Math.PI,
Math.random() * Math.PI
);
// 隨機大小
const scale = 0.5 + Math.random() * 1.5;
dummy.scale.set(scale, scale, scale);
dummy.updateMatrix();
instancedMesh.setMatrixAt(i, dummy.matrix);
// 隨機顏色
color.setHSL(Math.random() * 0.1 + 0.5, 0.8, 0.6);
instancedMesh.setColorAt(i, color);
}
scene.add(instancedMesh);
這段程式碼在場景中放了 10,000 個多面體,但效能遠比建立 10,000 個獨立 Mesh 好,因為 GPU 只需要一次 draw call。
Shader Material:自訂著色器
Shader 是 3D 生成式藝術的靈魂。Three.js 的 ShaderMaterial 讓你完全掌控每個頂點和像素的渲染方式。
基本結構
const shaderMaterial = new THREE.ShaderMaterial({
uniforms: {
u_time: { value: 0 },
u_resolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) },
u_color1: { value: new THREE.Color(0xff4040) },
u_color2: { value: new THREE.Color(0x4040ff) },
},
vertexShader:
varying vec2 vUv;
varying vec3 vPosition;
uniform float u_time;
void main() {
vUv = uv;
vPosition = position;
// 頂點位移:讓表面波動
vec3 pos = position;
float displacement = sin(pos.x <em> 5.0 + u_time) </em>
sin(pos.y <em> 5.0 + u_time) </em> 0.2;
pos += normal * displacement;
gl_Position = projectionMatrix <em> modelViewMatrix </em> vec4(pos, 1.0);
}
,
fragmentShader:
varying vec2 vUv;
varying vec3 vPosition;
uniform float u_time;
uniform vec3 u_color1;
uniform vec3 u_color2;
void main() {
// 根據 UV 座標混合兩個顏色
float pattern = sin(vUv.x <em> 10.0 + u_time) </em>
sin(vUv.y <em> 10.0 - u_time </em> 0.5);
pattern = smoothstep(-0.2, 0.2, pattern);
vec3 color = mix(u_color1, u_color2, pattern);
// 加入一點光澤效果
float fresnel = pow(1.0 - abs(dot(normalize(vPosition), vec3(0.0, 0.0, 1.0))), 2.0);
color += fresnel * 0.3;
gl_FragColor = vec4(color, 1.0);
}
,
side: THREE.DoubleSide,
});
// 用在一個球上
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(2, 64, 64),
shaderMaterial
);
scene.add(sphere);
// 在動畫循環中更新 uniform
function animate() {
requestAnimationFrame(animate);
shaderMaterial.uniforms.u_time.value = performance.now() * 0.001;
renderer.render(scene, camera);
}
從 p5.js shader 遷移
如果你已經會寫 p5.js 的 shader,遷移到 Three.js 主要的差異是:
// p5.js shader 的 varying
// attribute vec3 aPosition; → Three.js 自動提供為 position
// attribute vec2 aTexCoord; → Three.js 自動提供為 uv
// p5.js 的 uniform
// uniform mat4 uModelViewMatrix; → Three.js 提供 modelViewMatrix
// uniform mat4 uProjectionMatrix; → Three.js 提供 projectionMatrix
後處理效果
Three.js 的後處理(Post-processing)可以在渲染完 3D 場景後,再對整張畫面做效果處理。這是讓作品「看起來很厲害」的秘密武器。
import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js';
import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js';
import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';
// 建立後處理管線
const composer = new EffectComposer(renderer);
// 第一步:正常渲染場景
const renderPass = new RenderPass(scene, camera);
composer.addPass(renderPass);
// 第二步:Bloom 效果(光暈)
const bloomPass = new UnrealBloomPass(
new THREE.Vector2(window.innerWidth, window.innerHeight),
1.5, // 強度
0.4, // 半徑
0.85 // 門檻
);
composer.addPass(bloomPass);
// 在動畫循環中用 composer 替代 renderer
function animate() {
requestAnimationFrame(animate);
// 更新場景...
composer.render(); // 替代 renderer.render(scene, camera)
}
常用的後處理效果
| 效果 | 用途 |
|——|——|
| UnrealBloomPass | 光暈,讓亮的地方發光 |
| BokehPass | 景深模糊(DOF) |
| FilmPass | 底片顆粒和掃描線 |
| GlitchPass | 故障藝術效果 |
| SSAOPass | 環境光遮蔽,增加立體感 |
Bloom 效果特別適合生成式藝術——它能讓幾何圖形看起來像是在發光,非常有科幻感。
完整範例:3D 粒子螺旋
讓我們把以上的知識整合,做一個完整的 3D 生成式藝術作品:
import * as THREE from 'three'; import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; import { EffectComposer } from 'three/examples/jsm/postprocessing/EffectComposer.js'; import { RenderPass } from 'three/examples/jsm/postprocessing/RenderPass.js'; import { UnrealBloomPass } from 'three/examples/jsm/postprocessing/UnrealBloomPass.js';, fragmentShader:// === 基礎設定 === const scene = new THREE.Scene(); scene.background = new THREE.Color(0x050510); scene.fog = new THREE.FogExp2(0x050510, 0.08);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 100); camera.position.set(0, 2, 8);
const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(window.innerWidth, window.innerHeight); renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); document.body.appendChild(renderer.domElement);
// 軌道控制器 const controls = new OrbitControls(camera, renderer.domElement); controls.enableDamping = true; controls.autoRotate = true; controls.autoRotateSpeed = 0.5;
// === 粒子螺旋 === const PARTICLE_COUNT = 50000; const positions = new Float32Array(PARTICLE_COUNT * 3); const colors = new Float32Array(PARTICLE_COUNT * 3); const sizes = new Float32Array(PARTICLE_COUNT);
const color = new THREE.Color();
for (let i = 0; i < PARTICLE_COUNT; i++) { // 雙螺旋結構 const arm = i % 2; // 兩條螺旋臂 const t = (i / PARTICLE_COUNT) Math.PI 10; const r = t 0.15 + Math.random() 0.3;
const armOffset = arm * Math.PI; const spread = 0.3 + t * 0.02;
positions[i 3] = Math.cos(t + armOffset) r + (Math.random() - 0.5) * spread; positions[i 3 + 1] = (Math.random() - 0.5) spread * 0.5; positions[i 3 + 2] = Math.sin(t + armOffset) r + (Math.random() - 0.5) * spread;
// 顏色:沿著螺旋從暖色到冷色 const hue = (t / (Math.PI 10)) 0.3 + (arm * 0.5); color.setHSL(hue, 0.8, 0.6); colors[i * 3] = color.r; colors[i * 3 + 1] = color.g; colors[i * 3 + 2] = color.b;
sizes[i] = Math.random() * 3 + 1; }
const particleGeometry = new THREE.BufferGeometry(); particleGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); particleGeometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); particleGeometry.setAttribute('size', new THREE.BufferAttribute(sizes, 1));
const particleMaterial = new THREE.ShaderMaterial({ uniforms: { u_time: { value: 0 }, u_pointSize: { value: 3.0 }, }, vertexShader:
attribute float size; varying vec3 vColor; uniform float u_time; uniform float u_pointSize;void main() { vColor = color;
vec3 pos = position; // 緩慢旋轉 float angle = u_time * 0.1; float cosA = cos(angle); float sinA = sin(angle); pos.xz = mat2(cosA, -sinA, sinA, cosA) * pos.xz;
// 上下浮動 pos.y += sin(length(pos.xz) <em> 2.0 - u_time) </em> 0.1;
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); gl_PointSize = size <em> u_pointSize </em> (300.0 / -mvPosition.z); gl_Position = projectionMatrix * mvPosition; }
varying vec3 vColor;, vertexColors: true, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, });void main() { // 圓形粒子 float dist = length(gl_PointCoord - vec2(0.5)); if (dist > 0.5) discard;
// 柔和的邊緣 float alpha = 1.0 - smoothstep(0.3, 0.5, dist);
gl_FragColor = vec4(vColor, alpha); }
const particles = new THREE.Points(particleGeometry, particleMaterial); scene.add(particles);
// === 中心發光球 === const coreGeometry = new THREE.IcosahedronGeometry(0.3, 3); const coreMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8, }); const core = new THREE.Mesh(coreGeometry, coreMaterial); scene.add(core);
// === 後處理 === const composer = new EffectComposer(renderer); composer.addPass(new RenderPass(scene, camera));
const bloomPass = new UnrealBloomPass( new THREE.Vector2(window.innerWidth, window.innerHeight), 1.2, 0.4, 0.2 ); composer.addPass(bloomPass);
// === 動畫循環 === const clock = new THREE.Clock();
function animate() { requestAnimationFrame(animate);
const time = clock.getElapsedTime(); particleMaterial.uniforms.u_time.value = time;
// 中心球脈動 const pulse = 1 + Math.sin(time 2) 0.1; core.scale.set(pulse, pulse, pulse);
controls.update(); composer.render(); }
animate();
// === 視窗調整 === window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); composer.setSize(window.innerWidth, window.innerHeight); });
這個作品會產生一個旋轉的雙螺旋粒子結構,中心有一個脈動的發光球,加上 Bloom 後處理讓整個場景有夢幻的光暈效果。
學習路線
從 p5.js 到 Three.js 的遷移路線
第 1 週:理解 Scene/Camera/Renderer 的概念
做出旋轉的方塊(Hello World)
第 2 週:Materials 和 Lights
試不同的材質和光源組合
第 3 週:自訂 Geometry
用數學公式生成 3D 形狀
第 4 週:Shader Material
把 p5.js 的 shader 遷移過來
第 5 週:後處理和優化
Bloom、DOF、InstancedMesh
第 6 週:做一個完整的作品
發布到自己的網站
推薦資源
- Three.js Journey(threejs-journey.com):最完整的 Three.js 課程(付費但值得)
- Three.js 官方範例:上百個可以直接看 code 的範例
- Shadertoy:shader 的靈感寶庫,很多可以移植到 Three.js
小結
Three.js 是 p5.js 使用者進入 3D 生成式藝術的自然選擇。它的學習曲線比較陡,但回報也更大——你可以做出真正令人驚嘆的 3D 視覺效果。
對我來說,從 p5.js 轉到 Three.js 的過程,就像是從平面設計轉到雕塑——多了一個維度,表現力就多了一個數量級。
最重要的建議是:不要試圖一次學完所有東西。先用最基本的功能做出一個作品,然後每次加一個新技巧。享受每一個「哇,這樣也行?」的瞬間。
延伸閱讀
- Three.js 官方文件
- Three.js Journey — 最推薦的 Three.js 教學
- Three.js Examples — 官方範例大全
- Shadertoy — Shader 靈感和技術參考
- The Book of Shaders — Shader 入門必讀
- Bruno Simon 的作品集 — Three.js 的創意應用範例