前言

如果你已經玩過 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';

// === 基礎設定 === 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; } , fragmentShader: varying vec3 vColor;

void main() { // 圓形粒子 float dist = length(gl_PointCoord - vec2(0.5)); if (dist &gt; 0.5) discard;

// 柔和的邊緣 float alpha = 1.0 - smoothstep(0.3, 0.5, dist);

gl_FragColor = vec4(vColor, alpha); } , vertexColors: true, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, });

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 的過程,就像是從平面設計轉到雕塑——多了一個維度,表現力就多了一個數量級。

最重要的建議是:不要試圖一次學完所有東西。先用最基本的功能做出一個作品,然後每次加一個新技巧。享受每一個「哇,這樣也行?」的瞬間。

延伸閱讀