前言
大自然是最好的設計師。如果你仔細觀察長頸鹿的皮膚花紋、乾裂的泥土、蜻蜓的翅脈、龜殼的紋路,你會發現它們有一個共同的數學結構——Voronoi 圖。
Voronoi 圖(又稱 Voronoi diagram 或 Thiessen polygon)是一種根據一組種子點劃分平面的方法。每一個種子點會「佔領」距離它最近的所有空間,形成一個多邊形區域。這種結構在自然界中反覆出現,因為它是許多自然過程(如細胞生長、結晶、乾裂)的最優解。
把 Voronoi 紋理用在雷雕上,你可以做出極具自然感的鏤空設計——燈罩、裝飾品、手機殼背板、書擋⋯⋯可能性無窮。這篇文章會帶你從 Voronoi 的數學原理走到雷雕實作。
Voronoi 圖的數學基礎
定義
給定平面上的一組點集 P = {p1, p2, …, pn},每個點 pi 的 Voronoi 區域定義為:
V(pi) = { x ∈ R² | d(x, pi) ≤ d(x, pj), ∀j ≠ i }
白話來說,就是平面上離 pi 最近的所有點的集合。這些區域的邊界就是 Voronoi 圖的邊(edge),邊的交點稱為 Voronoi 頂點。
Voronoi 與 Delaunay 的對偶關係
Voronoi 圖有一個「雙胞胎」——Delaunay 三角剖分。它們互為對偶:
- Voronoi 的邊垂直平分 Delaunay 的邊
- Voronoi 的頂點是 Delaunay 三角形的外接圓圓心
- Delaunay 的頂點就是 Voronoi 的種子點
這個對偶關係在實作中很有用:很多程式庫是先算 Delaunay 三角剖分,再從中推導 Voronoi 圖。
種子點分佈的影響
Voronoi 圖的「長相」完全取決於種子點的分佈:
- 完全隨機分佈:區域大小不均,有些很大有些很小,看起來最「自然」
- 均勻抖動(jittered grid):在格子點的基礎上加入隨機擾動,區域大小較均勻
- 泊松盤取樣(Poisson disk sampling):保證任意兩點之間有最小距離,效果最好
對於雷雕應用,泊松盤取樣通常是最佳選擇——它兼顧了隨機感和結構穩定性。
生成 Voronoi 紋理
使用 Python + scipy
import numpy as np
from scipy.spatial import Voronoi
import svgwrite
def generate_voronoi_svg(filename, width=100, height=100,
num_points=50, margin=10, seed=42):
"""生成 Voronoi 紋理的 SVG 檔案"""
np.random.seed(seed)
# 生成種子點(在邊界外也撒點,避免邊緣區域太大)
points_inner = np.random.rand(num_points, 2) * [width, height]
# 鏡像點(處理邊界)
points_mirror = []
for p in points_inner:
points_mirror.append([-p[0], p[1]])
points_mirror.append([2*width - p[0], p[1]])
points_mirror.append([p[0], -p[1]])
points_mirror.append([p[0], 2*height - p[1]])
all_points = np.vstack([points_inner, points_mirror])
# 計算 Voronoi 圖
vor = Voronoi(all_points)
# 建立 SVG
dwg = svgwrite.Drawing(filename,
size=(f'{width}mm', f'{height}mm'),
viewBox=f'0 0 {width} {height}')
# 繪製 Voronoi 邊
for ridge_vertices in vor.ridge_vertices:
if -1 in ridge_vertices:
continue # 跳過無限邊
v0 = vor.vertices[ridge_vertices[0]]
v1 = vor.vertices[ridge_vertices[1]]
# 只繪製在畫布內的邊
if (0 <= v0[0] <= width and 0 <= v0[1] <= height and
0 <= v1[0] <= width and 0 <= v1[1] <= height):
dwg.add(dwg.line(
(v0[0], v0[1]), (v1[0], v1[1]),
stroke='black', stroke_width=0.2
))
# 外框(切割線)
dwg.add(dwg.rect((0, 0), (width, height),
fill='none', stroke='red', stroke_width=0.2))
dwg.save()
print(f"Voronoi SVG saved to {filename}")
generate_voronoi_svg('voronoi_pattern.svg', num_points=80)
使用 p5.js 互動式設計
如果你想更直覺地調整 Voronoi 圖案,p5.js 搭配 d3-delaunay 是個好選擇:
// 需要引入 d3-delaunay 函式庫
// <script src="https://cdn.jsdelivr.net/npm/d3-delaunay@6"></script>
let points = [];
let voronoi;
function setup() {
createCanvas(800, 600);
// 泊松盤取樣生成種子點
points = poissonDiskSampling(800, 600, 30, 30);
updateVoronoi();
}
function draw() {
background(30);
// 繪製 Voronoi 邊
stroke(200);
strokeWeight(1);
noFill();
const delaunay = d3.Delaunay.from(points);
const v = delaunay.voronoi([0, 0, width, height]);
for (let i = 0; i < points.length; i++) {
const cell = v.cellPolygon(i);
if (cell) {
beginShape();
for (const [x, y] of cell) {
vertex(x, y);
}
endShape(CLOSE);
}
}
}
function poissonDiskSampling(w, h, radius, k) {
// 簡化版泊松盤取樣
let samples = [];
let active = [];
// 第一個點
let first = [random(w), random(h)];
samples.push(first);
active.push(first);
while (active.length > 0) {
let idx = floor(random(active.length));
let point = active[idx];
let found = false;
for (let n = 0; n < k; n++) {
let angle = random(TWO_PI);
let r = random(radius, 2 * radius);
let candidate = [
point[0] + r * cos(angle),
point[1] + r * sin(angle)
];
if (candidate[0] < 0 || candidate[0] >= w ||
candidate[1] < 0 || candidate[1] >= h) continue;
let tooClose = false;
for (let s of samples) {
if (dist(candidate[0], candidate[1], s[0], s[1]) < radius) {
tooClose = true;
break;
}
}
if (!tooClose) {
samples.push(candidate);
active.push(candidate);
found = true;
break;
}
}
if (!found) {
active.splice(idx, 1);
}
}
return samples;
}
鏤空設計的關鍵考量
鏤空強度
把 Voronoi 的每個區域都鏤空,結構就垮了。你需要保留 Voronoi 的邊作為「骨架」,並且確保骨架夠寬。
邊寬計算原則:
最小邊寬 ≥ 材料厚度 × 0.5(壓克力)
最小邊寬 ≥ 材料厚度 × 0.8(木材)
最小邊寬 ≥ 2mm(一般經驗值)
在 SVG 中,Voronoi 邊是零寬度的線。要轉換成有寬度的骨架,需要對每個 Voronoi 區域做內縮(inset/offset):
from shapely.geometry import Polygon
from shapely.ops import unary_union
def create_voronoi_skeleton(vor, inset_distance=1.0):
"""將 Voronoi 區域內縮,產生鏤空骨架"""
shrunk_regions = []
for region_idx in vor.point_region:
region = vor.regions[region_idx]
if -1 in region or len(region) == 0:
continue
vertices = [vor.vertices[i] for i in region]
poly = Polygon(vertices)
if poly.is_valid:
shrunk = poly.buffer(-inset_distance)
if not shrunk.is_empty:
shrunk_regions.append(shrunk)
# 鏤空區域 = 內縮後的多邊形
# 骨架 = 外框 - 鏤空區域
return shrunk_regions
結構穩定性分析
鏤空比例太高會讓作品脆弱。一些經驗法則:
- 鏤空率 < 60%:結構穩固,適合功能性零件
- 鏤空率 60-75%:需要小心處理,適合裝飾品
- 鏤空率 > 75%:很脆弱,只適合框架保護良好的場景
你可以用 Voronoi 種子點的密度來控制鏤空率。種子點越多,每個區域越小,骨架越密,結構越強。
邊緣處理
Voronoi 鏤空設計需要特別注意邊緣。如果鏤空區域碰到外輪廓邊緣,那個位置就會特別脆弱。解決方法:
- 加寬邊框:在外輪廓內留一圈 3-5mm 的實心邊框
- 種子點避開邊緣:讓邊緣的 Voronoi 區域較小(不鏤空或只淺雕)
- 圓角處理:鏤空區域的尖角加上圓角,減少應力集中
實作範例
Voronoi 燈罩
這是 Voronoi 鏤空最經典的應用。步驟:
- 設計燈罩的展開圖(圓柱展開為矩形)
- 在矩形區域內生成 Voronoi 圖案
- 每個 Voronoi 區域內縮 1.5mm 作為鏤空
- 加上頂部和底部的固定邊框
- 加上接合用的齒槽(tab and slot)
- 雷切 3mm 樺木合板
- 彎曲成圓柱形(木材需要先泡水或用蒸氣軟化)
Voronoi 手機殼背板
較小的作品,適合練手:
def phone_case_voronoi(filename, w=75, h=150):
"""手機殼大小的 Voronoi 鏤空設計"""
np.random.seed(123)
# 泊松盤取樣,最小距離 8mm
points = poisson_disk_sampling(w, h, min_dist=8)
# ... 生成 Voronoi 並內縮 ...
# 加入相機孔(圓形區域不鏤空)
camera_hole_center = (w 0.5, h 0.15)
camera_hole_radius = 8
# 過濾掉相機孔範圍內的鏤空區域
filtered_regions = [r for r in shrunk_regions
if not r.intersects(camera_circle)]
Voronoi 書擋
用 5mm 壓克力切割,Voronoi 鏤空加上 L 型底座:
- 正面板:Voronoi 鏤空設計
- 底座:L 型,用箱指接合連接正面板
- 建議鏤空率控制在 50% 以下,因為書擋需要承受書本的重量
進階技巧
加權 Voronoi
普通 Voronoi 中每個種子點的「影響力」相同。加權 Voronoi(weighted Voronoi 或 power diagram)允許每個點有不同的權重,產生大小更有變化的區域。
# 加權 Voronoi 的距離函數
# d_weighted(x, pi) = d(x, pi)² - wi
# 其中 wi 是點 pi 的權重
# 權重越大的點,佔領的區域越大
密度漸變
讓 Voronoi 的密度在空間中漸變,可以產生從密到疏(或反過來)的漸層效果:
def density_gradient_points(w, h, num_points, gradient_func):
"""根據密度梯度函數生成種子點"""
points = []
while len(points) < num_points:
x, y = np.random.rand() w, np.random.rand() h
density = gradient_func(x, y, w, h)
if np.random.rand() < density:
points.append([x, y])
return np.array(points)
# 從左到右密度遞減
gradient = lambda x, y, w, h: 1.0 - 0.8 * (x / w)
points = density_gradient_points(100, 100, 200, gradient)
這種漸變效果在裝飾設計中非常吸引人——視覺上有「消散」或「聚集」的動態感。
小結
Voronoi 紋理之所以在設計中如此受歡迎,是因為它完美地平衡了秩序與隨機。它有數學上的規律性(每條邊都是兩點的中垂線),又因為種子點的隨機分佈而呈現自然的不規則感。
如果你是第一次嘗試 Voronoi 雷雕,建議從杯墊這種小尺寸的作品開始。用 3mm 合板,50 個左右的種子點,內縮 1.5mm,就能得到一個漂亮又有強度的鏤空杯墊。
延伸閱讀:
- The Algorithmic Beauty of Plants — 自然界中的 Voronoi 結構
- Nervous System 的作品 — 將 Voronoi 應用到極致的設計工作室
- Inkscape 的 Voronoi 擴充套件 — 不寫程式也能生成 Voronoi
- Processing / p5.js 的 Voronoi 函式庫 — 互動式探索