前言
本文將介紹如何使用極座標參數方程和上一篇文章提到的距離場SDF來繪製有趣的圖案。
説到曲線和幾何圖形的繪製,我們知道圖形系統默認支持的是通過直角座標繪製,但是有些曲線呢,不太容易使用直角座標系來表示,卻可以很方便地使用極座標來表示,這個時候我們可以選擇通過極座標和直角座標的相互轉換,來實現圖形的繪製。
下面我就用玫瑰線、花瓣線等曲線作為例子來進行演示。
在開始演示之前,我先簡單介紹下極座標和參數方程。
- 極座標是使用相對極點的距離,以及與X軸正向的夾角,使用這一對值來表示平面上點的座標。
- 參數方程表示的是點的座標分別和相關參數的關係,所以通常會對應一個方程組,比如説二維平面上的點,會使用兩個參數方程來表示。
我這裏只是簡單介紹一下,可能説的並不是很準確。我們也可以用圓作為例子,圓的標準方程大家都知道,假設圓心在原點,在直角座標系下,圓的公式就是x的平方加上y的平方等於半徑的平方,圓的參數方程是x等於半徑乘以與X軸夾角的餘弦值,y等於半徑乘以與X軸夾角的正弦值。
$$ \begin{cases} x = r * cosθ \\ y = r * sinθ \end{cases} $$
但在極座標系下,圓的參數方程就變成了r等於一個常量,θ等於與X軸的夾角。這基本上可以算是最簡單的極座標參數方程組了。
$$ \begin{cases} r = r \\ θ = t \end{cases} $$
那麼基本的知識瞭解後,我們就可以開始使用極座標參數方程來繪製圖形和曲線了。
具體實現
現在就來演示通過曲線的極座標參數方程來繪製曲線。
Canvas
首先先來看Canvas2D的例子,在Canvas中我們可以通過lineTo方法繪製足夠多的線段將它們連在一起,來模擬曲線,所以我們可以通過參數方程獲取足夠多的點,將它們連接起來,這樣就能最終完成曲線的繪製。
因此我們先來定義一個高階函數parametric用於創建方程組,接收三個參數xFunc、yFunc和rFunc。xFunc和yFunc表示點的一對座標值各自分別對應的參數方程,rFunc表示座標映射函數。
export default function parametric(xFunc, yFunc, rFunc) {
return function(start, end, seg = 100, ...args) {
const points = [];
for (let i = 0; i <= seg; i ++) {
const p = i / seg;
// const t = start * (1 - p) + end * p;
const t = start + (end - start) * i / seg;
// console.log(t);
const x = xFunc(t, ...args);
const y = yFunc(t, ...args);
if (rFunc) points.push(rFunc(x, y));
else points.push([x, y]);
}
return {
draw: draw.bind(null, points),
points
}
}
}
這個高階函數的返回值也是一個函數,在這個匿名函數應該不難理解,我簡單説一下,它接收多個參數,必選的三個是座標相關參數 t 的上下限start和end,以及要收集的點的數量seg,最終返回一個對象,這個對象帶有一個draw方法,通過調用這個draw方法就可以完成曲線的繪製。
下面我們就通過parametric函數構造不同的曲線方程組。來看一個玫瑰線:
// 玫瑰線
const rose = parametric(
(t, a, k) => a * Math.cos(k * t), // r
t => t, // θ
fromPolar,
);
這裏fromPolar是一種座標映射函數,作用是將極座標轉換為直角座標,這樣我們才能在Canvas2d中使用座標值。現在我們就可以通過調用rose函數,來獲取曲線上的點並繪製曲線了。
rose(0, Math.PI, 100, 200, 5).draw(ctx, {strokeStyle: 'blue'}); // 玫瑰線
通過傳入不同的a,我們可以改變曲線的大小,而不同的k,可以改變葉子的數目;這樣就能構造出不同的圖案。
所以我們也可以應用不同的曲線方程,繪製出各種曲線。
WebGL
那麼除了Canvas2d,我們當然也可以在WebGL中應用曲線的參數方程實現圖形的繪製。接下來我就演示幾個在shader中應用參數方程的例子,並結合上篇文章中提到的距離場,完成圖形的繪製。
首先還是玫瑰線:
void main() {
vec2 st = vUv - vec2(0.5);
st = polar(st);
float d = 0.5 * cos(st.y * 3.0) - st.x;
gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0);
gl_FragColor.a = 1.0;
}
在這段Shader代碼中,這裏的0.5就是剛剛在Canvas例子中的a,而3.0就是Canvas例子中的k,所以這裏葉子的數目就是3。那為什麼這裏的d要這樣定義呢?這是因為玫瑰線上的所有點都滿足 0 = a * cos(k * θ) - r 這個等式,也就是説玫瑰線上的點對應的d都是0,並且玫瑰線形成的圖形內部的點對應的d都大於0,而外部的點對應的d都小於0,因此這樣我們就可以得到一個內部填充為白色的玫瑰線圖形。
我們對玫瑰線的參數和公式做些微調,就能得到不同的圖形,比如添加一個取絕對值的操作,並且給角度乘以一個常量0.5。
void main() {
float u_k = 3.0;
vec2 st = vUv - vec2(0.5);
st = polar(st);
float d = 0.5 * abs(cos(st.y * u_k * 0.5)) - st.x;
gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0);
gl_FragColor.a = 1.0;
}
可以看出這個圖形有些類似花瓣的形狀。
當我們設置不同的u_k值時,也能得到不同的圖案,比如當u_k設置為1.3時,圖形看上去就像一個橫放的蘋果。
void main() {
float u_k = 1.3; // 橫放的蘋果
vec2 st = vUv - vec2(0.5);
st = polar(st);
float d = 0.5 * abs(cos(st.y * u_k * 0.5)) - st.x;
gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0);
gl_FragColor.a = 1.0;
}
繼續微調參數方程,我們還能畫出類似橫放的葫蘆圖案:
void main() {
float u_k = 1.7; // 橫放的葫蘆
float u_scale = 0.5;
float u_offset = 0.2;
vec2 st = vUv - vec2(0.5);
st = polar(st);
float d = u_scale * 0.5 * abs(cos(st.y * u_k * 0.5)) - st.x + u_offset;
gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0);
gl_FragColor.a = 1.0;
}
相信小夥伴們通過結合三角函數、abs、smoothstep等等這些函數,還能繪製出更多有趣的圖案,大家可以動手自己嘗試一下。
結合隨機數
接下來,我們結合一個隨機數函數,來實現一個類似剪紙的圖案。
在這個例子中,我們會使用一個生成隨機數的函數叫做random,從代碼中我們可以看出random實際上生成的是一個偽隨機數,因為紋理座標是確定的,所以根據紋理座標生成的隨機數也是確定的。
float random(vec2 st) {
return fract(
sin(
dot(st.xy, vec2(12.9898, 78.233))
) *
43758.5453123
);
}
這個函數是我學的課程上給的一個函數,應該是一個經驗公式。那麼接下來我們就開始來實現這個剪紙圖案。
首先,我們利用實現網格的方式,將畫布變為10 x 10的網格,此時得到的st變量就是片元對應的紋理座標乘以10,接着我們分別獲取到st的整數部分和小數部分。
整數部分ipos將用於生成隨機數,而小數部分就用於繪製各類圖案。
vec2 st = vUv * 10.0;
vec2 ipos = floor(st); // integer
vec2 fpos = fract(st); // fraction
float r = random(ipos);
接着我們就可以按照隨機數在不同區間繪製不同的圖案,我這裏就用上期學的着色器幾何造型裏的不同距離公式和上面的幾個參數方程來實現不同圖案的繪製。
void main() {
vec2 st = vUv * 10.0;
vec2 ipos = floor(st); // integer
vec2 fpos = fract(st); // fraction
float r = random(ipos);
float d = 0.0;
if(r < 0.14) { // 四邊形
fpos = fpos - vec2(0.5);
float d = polygon_distance2(
fpos,
4,
vec2(0.0, 0.4)
);
gl_FragColor.rgb = smoothstep(0.01, 0.0, d) * vec3(1.0, 0.0, 0.0);
gl_FragColor.a = smoothstep(0.01, 0.0, d);
} else if (r < 0.28) { // 四片花瓣
float u_k = 4.0;
fpos = fpos - vec2(0.5);
fpos = polar(fpos);
d = 0.5 * abs(cos(fpos.y * u_k * 0.5)) - fpos.x;
gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0, 0.0, 0.0);
gl_FragColor.a = smoothstep(-0.01, 0.01, d);
} else if (r < 0.42) { // 蘋果
float u_k = 1.3;
fpos = fpos - vec2(0.5, 0.7);
fpos = polar(fpos);
fpos.y += 3.14 / 2.0;
// atan 的返回值是:從第一到第二象限為 0~PI,從第三到第四象限為 -PI~0
// 旋轉極座標後要保證函數定義域的一致性
if (fpos.y > 3.14) fpos.y -= 3.14 * 2.0;
float d = 0.5 * abs(cos(fpos.y * u_k * 0.5)) - fpos.x;
gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0, 0.0, 0.0);
gl_FragColor.a = smoothstep(-0.01, 0.01, d);
} else if (r < 0.56) { // 六邊形
fpos = fpos - vec2(0.5);
float d = polygon_distance2(
fpos,
6,
vec2(0.0, 0.4)
);
gl_FragColor.rgb = smoothstep(0.01, 0.0, d) * vec3(1.0, 0.0, 0.0);
gl_FragColor.a = smoothstep(0.01, 0.0, d);
} else if (r < 0.70) { // 五角星
fpos = fpos - vec2(0.5);
float d = star_distance(
fpos,
5,
vec2(0.15, 0.2)
);
gl_FragColor.rgb = smoothstep(0.01, 0.0, d) * vec3(1.0, 0.0, 0.0);
gl_FragColor.a = smoothstep(0.01, 0.0, d);
} else if (r < 0.84) { // 葫蘆
float u_k = 1.7;
float u_scale = 0.5;
float u_offset = 0.2;
fpos = fpos - vec2(0.5);
fpos = polar(fpos);
fpos.y += 3.14 / 2.0;
if (fpos.y > 3.14) fpos.y -= 3.14 * 2.0;
float d = u_scale * 0.5 * abs(cos(fpos.y * u_k * 0.5)) - fpos.x + u_offset;
gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0, 0.0, 0.0);
gl_FragColor.a = smoothstep(-0.01, 0.01, d);
} else { // 花苞
float u_k = 5.0;
float u_scale = 0.13;
float u_offset = 0.2;
fpos = fpos - vec2(0.5);
fpos = polar(fpos);
float d = smoothstep(-0.3, 1.0, u_scale * 0.5 * cos(fpos.y * u_k) + u_offset) - fpos.x;
gl_FragColor.rgb = smoothstep(-0.01, 0.01, d) * vec3(1.0, 0.0, 0.0);
gl_FragColor.a = smoothstep(-0.01, 0.01, d);
}
}
這樣我們就實現了一個類似於剪紙的圖案,實現的方式比較簡單粗暴,就是用了一堆if-else,看上去不太優雅。
相信大家一定都知道怎麼去實現更多圖形圖案了,接下去就是多動手多嘗試。
代碼參考:Canvas
代碼參考:WebGL