前言

大自然是最好的設計師。如果你仔細觀察長頸鹿的皮膚花紋、乾裂的泥土、蜻蜓的翅脈、龜殼的紋路,你會發現它們有一個共同的數學結構——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 鏤空設計需要特別注意邊緣。如果鏤空區域碰到外輪廓邊緣,那個位置就會特別脆弱。解決方法:

  1. 加寬邊框:在外輪廓內留一圈 3-5mm 的實心邊框
  2. 種子點避開邊緣:讓邊緣的 Voronoi 區域較小(不鏤空或只淺雕)
  3. 圓角處理:鏤空區域的尖角加上圓角,減少應力集中

實作範例

Voronoi 燈罩

這是 Voronoi 鏤空最經典的應用。步驟:

  1. 設計燈罩的展開圖(圓柱展開為矩形)
  2. 在矩形區域內生成 Voronoi 圖案
  3. 每個 Voronoi 區域內縮 1.5mm 作為鏤空
  4. 加上頂部和底部的固定邊框
  5. 加上接合用的齒槽(tab and slot)
  6. 雷切 3mm 樺木合板
  7. 彎曲成圓柱形(木材需要先泡水或用蒸氣軟化)

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 函式庫 — 互動式探索