Perlin 雜訊


如果你有看過〈笨學資料運算〉中的〈NumPy 與 Perlin 雜訊〉與〈Matplotlib 圖片、等值輪廓線〉,可能會想到,能不能將二維的 Perlin 雜訊作為曲面資料,搭配〈建立網格面〉的 surface 來建立曲面呢?

基本上是可以,不過…嗯…先來看看實作吧!將〈Matplotlib 圖片、等值輪廓線〉中的程式稍微修改一下:

...搭配〈建立網格面〉中的 surface

# Perlin noise 實作
def blending(t):
    return 6 * (t ** 5) - 15 * (t ** 4) + 10 * (t ** 3)

def lerp(g1, g2, t):
    return g1 + t * (g2 - g1)

def grad2(hashvalue, dx, dy):
    return [dy, dx + dy, dx, dx - dy, -dy, -dx - dy, -dx, -dx + dy][hashvalue % 8];

rand_table = np.random.randint(255, size = 256).tolist()
# 增加一個 zScale 參數
def _perlin2(x, y, zScale):
    xi = floor(x)
    yi = floor(y)

    aa = rand_table[
        (rand_table[xi % 256] + yi) % 256
    ]
    ba = rand_table[
        (rand_table[(xi + 1) % 256] + yi) % 256
    ]
    ab = rand_table[
        (rand_table[xi % 256] + yi + 1) % 256
    ]
    bb = rand_table[
        (rand_table[(xi + 1) % 256] + yi + 1) % 256
    ]

    dx = x - xi
    dy = y - yi    

    u = blending(dx)
    v = blending(dy)

    g1 = lerp(grad2(aa, dx, dy), grad2(ba, dx - 1, dy), u)
    g2 = lerp(grad2(ab, dx, dy - 1), grad2(bb, dx - 1, dy - 1), u)

    # 傳回座標,其中雜訊值是 z 
    return (x, y, lerp(g1, g2, v) * zScale)
_perlin2 = np.frompyfunc(_perlin2, 3, 1)

def perlin2(x, y, zScale):
    cx, cy = np.meshgrid(x, y)
    return _perlin2(cx, cy, zScale)

width = 25
step = 1.1
zScale = 2
thickness = 0.1

x = np.arange(0, width, step)
y = np.arange(0, width, step)

points = perlin2(x, y, zScale) 

show_object(surface(points, thickness))

因為 surface 需要一組座標,因此 perlin2 最後傳回的是座標值,其中雜訊值是 z,增加一個 zScale 是為了調整 z 的大小,來看一下執行後的畫面:

Perlin 雜訊

嗯?看來很不連續?這是因為 step 取得比較大,若要得到更細緻的起伏,必須縮小 step,不過,在 CadQuery 中建立面其實是很昂貴的,而且太多面的話,要在 CQ-editor 中顯示,CQ-editor 會跑很久甚至當掉。

CadQuery 本身採用 BREP,並不鼓勵這種自行建立多面體的做法,這跟 OpenSCAD 是相對的,多面體的做法比較適合在 OpenSCAD 中做,例如,對應的做法並將 step 縮小後,OpenSCAD 可以做出以下模型:

Perlin 雜訊

那麼 CadQuery 沒機會做出這個模型了嗎?其實還是有的,在〈建立網格面〉不是看過 Face.makeSplineApprox 嗎?你可以指定控制點,然後它會以內插法計算出必要的資料,建立起曲面,也就是說,你只要提供適當的 step,建立相對少量的資料,也能建立起接近 Perlin 雜訊建立的曲面:

from math import floor
import numpy as np

from cadquery import Workplane, Face, Vector

# Perlin noise 實作
def blending(t):
    return 6 * (t ** 5) - 15 * (t ** 4) + 10 * (t ** 3)

def lerp(g1, g2, t):
    return g1 + t * (g2 - g1)

def grad2(hashvalue, dx, dy):
    return [dy, dx + dy, dx, dx - dy, -dy, -dx - dy, -dx, -dx + dy][hashvalue % 8];

rand_table = np.random.randint(255, size = 256).tolist()
def _perlin2(x, y, zScale):
    xi = floor(x)
    yi = floor(y)

    aa = rand_table[
        (rand_table[xi % 256] + yi) % 256
    ]
    ba = rand_table[
        (rand_table[(xi + 1) % 256] + yi) % 256
    ]
    ab = rand_table[
        (rand_table[xi % 256] + yi + 1) % 256
    ]
    bb = rand_table[
        (rand_table[(xi + 1) % 256] + yi + 1) % 256
    ]

    dx = x - xi
    dy = y - yi    

    u = blending(dx)
    v = blending(dy)

    g1 = lerp(grad2(aa, dx, dy), grad2(ba, dx - 1, dy), u)
    g2 = lerp(grad2(ab, dx, dy - 1), grad2(bb, dx - 1, dy - 1), u)

    # 傳回座標的 Vector 實例,其中雜訊值是 z 
    return Vector(x, y, lerp(g1, g2, v) * zScale)
_perlin2 = np.frompyfunc(_perlin2, 3, 1)

def perlin2(x, y, zScale):
    cx, cy = np.meshgrid(x, y)
    return _perlin2(cx, cy, zScale)

width = 200
step = 20.1
zScale = 10

x = np.arange(0, width, step)
y = np.arange(0, width, step)

points = perlin2(x, y, zScale) 

show_object(Face.makeSplineApprox(points))

注意到 perlin2 傳回的資料是由 Vector 構成,這是為了配合 Face.makeSplineApprox,這次還增加了雜訊的平面範圍,來看看效果如何:

Perlin 雜訊