【零基礎】充分理解WebGL(四)

語言: CN / TW / HK

接上篇 http://juejin.cn/post/7103437998640857119

幾何圖形

通過前面兩節我們已經知道距離場構圖法的關鍵就是要構造距離場,並學習瞭如何構建圓、直線、線段的距離場,以及如果通過取樣來構建連續函式曲線的距離場。

接下來,我們來討論如何構建三角形、正多邊形和橢圓的距離場。

首先是三角形:

我們定義點到三角形的距離為點到三角形三條邊距離中最短的一條邊的距離。

根據上面的定義,在三角形內部,點到三角形的距離不會超過三角形內接圓半徑l,所以我們可以通過將距離除以半徑l,得到歸一化的距離場,如下圖所示:

image.png

還有一個關鍵是需要確定點在三角形內部還是外部,這一點,我們可以通過距離的符號來判斷,這是因為:

如果點P在三角形內部,對於P點的觀察者來說,三角形的三條邊的向量矩方向(或者說三條邊的旋轉方向)一致,反之,如果P點在三角形外部,三條邊向量矩方向不一致,這一點表現在三個距離的正負號上,如果P在三角形內部,三個距離的正負號情況一致,否則不一致。這個判斷方法不僅對三角形有效,對所有凸多邊形都有效。

因此我們得到三角形距離場函式:

```glsl float sdf_line(vec2 st, vec2 a, vec2 b) { vec2 ap = st - a; vec2 ab = b - a; return ((ap.x * ab.y) - (ab.x * ap.y)) / length(ab); }

float sdf_seg(vec2 st, vec2 a, vec2 b) { vec2 ap = st - a; vec2 ab = b - a; vec2 bp = st - b; float l = length(ab); float proj = dot(ap, ab) / l; if(proj >= 0.0 && proj <= l) { return sdf_line(st, a, b); } return min(length(ap), length(bp)); }

/* 三角形的 SDF / float sdf_triangle(vec2 st, vec2 a, vec2 b, vec2 c) { vec2 va = a - b; vec2 vb = b - c; vec2 vc = c - a;

float d1 = sdf_line(st, a, b); float d2 = sdf_line(st, b, c); float d3 = sdf_line(st, c, a);

// 三角形內切圓半徑 float l = abs(va.x * vb.y - va.y * vb.x) / (length(va) + length(vb) + length(vc));

// 點在三角形內部,定義距離為正 if(d1 >= 0.0 && d2 >= 0.0 && d3 >= 0.0 || d1 <= 0.0 && d2 <= 0.0 && d3 <= 0.0) { return min(abs(d1), min(abs(d2), abs(d3))) / l; }

// 點在三角形外部,定義距離為負 d1 = sdf_seg(st, a, b); d2 = sdf_seg(st, b, c); d3 = sdf_seg(st, c, a); return -min(abs(d1), min(abs(d2), abs(d3))) / l; } ```

注意:我們這裡對上一節的函式做了一點小調整,主要是更改了引數次序。因為有許多不同圖形的距離場函式,它們的引數不同,但至少都接受st,所以我們將st統一作為距離場函式的第一個引數。

最終我們可以通過這個距離場函式,給定三個頂點座標,繪製出三角形了。

http://code.juejin.cn/pen/7105944424164622350

接著是正多邊形

我們可以定義點P到正多邊形距離為P到正多邊形最近一條邊的距離,並將它除以圓心到邊的距離l進行歸一化。

image.png

具體的程式碼實現如下:

```glsl

ifndef PI

define PI 3.141592653589793

endif

ifndef FLT_EPSILON

define FLT_EPSILON 0.000001

endif

vec2 transform(vec2 v0, mat3 matrix) { return vec2(matrix * vec3(v0, 1.0)); }

vec2 rotate(vec2 v0, vec2 origin, float ang) { float sinA = sin(ang); float cosA = cos(ang); mat3 m = mat3(cosA, -sinA, 0, sinA, cosA, 0, 0, 0, 1); return transform(v0 - origin, m) + origin; }

vec2 rotate(vec2 v0, float ang) { return rotate(v0, vec2(0.0), ang); }

float atan2(float dy, float dx) { float ax = abs(dx); float ay = abs(dy); float a = min(ax, ay) / (max(ax, ay) + FLT_EPSILON); float s = a * a; float r = ((-0.0464964749 * s + 0.15931422) * s - 0.327622764) * s * a + a; if(ay > ax) r = 1.57079637 - r; if(dx < 0.0) r = PI - r; if(dy < 0.0) r = -r; return r; }

float atan2(vec2 v) { return atan2(v.y, v.x); }

/* 從 v1 相對 v2 的逆時針夾角, 0 ~ 2 * PI / float angle(vec2 v1, vec2 v2) { float ang = atan2(v1) - atan2(v2); if(ang < 0.0) ang += 2.0 * PI; return ang; }

/* 正多邊形 / float regular_polygon(vec2 st, vec2 center, float r, float rotation, const int edges) { vec2 p = st - center; vec2 v0 = vec2(0, r); // 第一個頂點 v0 = rotate(v0, -rotation);

float a = 2.0 * PI / float(edges); // 每條邊與中心點的夾角

float ang = angle(v0, p); // 取夾角 ang = floor(ang / a); // 在哪個區間

vec2 v1 = rotate(v0, a * ang); // 左頂點 vec2 v2 = rotate(v0, a * (ang + 1.0)); // 右頂點

float c_a = cos(0.5 * a);

float l = r * c_a; float d = sdf_line(p, v1, v2);

return d / l; } ```

注意這裡面有些細節,我們通過反正切來求向量夾角,通過線性變換rotate來旋轉向量,其中線性變換是非常重要的數學方法,在後續章節中會進一步詳細介紹。

這樣我們就可以繪製正多邊形了,比如下面的程式碼繪製了一個正7邊形。

http://code.juejin.cn/pen/7105959050231152647

橢圓和橢扇形

和圓類似,我們通過橢圓方程來定義橢圓的距離場。

$d = 1.0 - (x^2/a^2 + y^2/b^2)$

然後我們通過夾角來近似計算橢扇形距離場,這裡我們把距離場分成三個部分,兩個向量夾角中間的部分,以及左側、右側兩邊的部分。

image.png

具體實現程式碼如下,有很多細節,這裡不再贅述,有興趣的同學可以仔細研究,在碼上掘金上修改一些引數執行看看。

```glsl float sdf_ellipse(vec2 st, vec2 c, float a, float b) { vec2 p = st - c; return 1.0 - sqrt(pow(p.x / a, 2.0) + pow(p.y / b, 2.0)); }

float sdf_ellipse(vec2 st, vec2 c, float a, float b, float sAng, float eAng) { vec2 ua = vec2(cos(sAng), sin(sAng)); vec2 ub = vec2(cos(eAng), sin(eAng));

float d1 = sdf_line(st, c, ua + c); float d2 = sdf_line(st, c, ub + c);

float d3 = sdf_ellipse(st, c, a, b); float r = min(a, b);

vec2 v = st - c; float ang = angle(v, vec2(1.0, 0));

if(eAng - sAng > 2.0 * PI) { return d3; }

if(ang >= sAng && ang <= eAng) { // 兩個向量夾角中間的部分 float m = max(a, b); float d11 = sdf_seg(st, c, ua * m + c); float d12 = sdf_seg(st, c, ub * m + c); if(d3 >= 0.0) { return min(abs(d11 / r), min(abs(d12 / r), d3)); } return d3; }

float pa = dot(ua, v); // 求投影 float pb = dot(ub, v);

if(pa < 0.0 && pb < 0.0) { return -length(st - c) / r; }

if(d1 > 0.0 && pa >= 0.0) { vec2 va = pa * ua; float da = pow(va.x / a, 2.0) + pow(va.y / b, 2.0); if(d3 > 0.0 || da <= pow(1.0 + abs(d1 / r), 2.0)) { return -abs(d1 / r); } else { return d3; } }

if(d2 < 0.0 && pb >= 0.0) { vec2 vb = pb * ub; float db = pow(vb.x / a, 2.0) + pow(vb.y / b, 2.0); if(d3 >= 0.0 || db <= pow(1.0 + abs(d2 / r), 2.0)) { return -abs(d2 / r); } else { return d3; } } } ```

http://code.juejin.cn/pen/7105966227348488200

重複

如果要在畫布上繪製多個相同圖形,不必一一繪製每一個圖形,要我們有一些數學手段可以運用。

簡單來說,我們可以擴大st或d的值,然後對它取小數部分,比如下面的程式碼繪製多條直線:

glsl void main() { vec2 st = gl_FragCoord.xy / resolution; float d = sdf_line(st, vec2(0), vec2(0.5)); d = abs(d); d = fract(10.0 * d); FragColor.rgb = stroke(d, 0.5, 0.2, 0.3) * vec3(1.0); FragColor.a = 1.0; }

http://code.juejin.cn/pen/7105970707141492750

類似地,我們繪製多重菱形:

glsl void main() { vec2 st = gl_FragCoord.xy / resolution; float d = regular_polygon(st, vec2(0.5), 0.5, 0.0, 4); d = abs(d); d = fract(10.0 * d); FragColor.rgb = stroke(d, 0.5, 0.5, 0.5) * vec3(1.0); FragColor.a = 1.0; }

http://code.juejin.cn/pen/7105972595907887140

如果我們擴大的是st而非d,得到的是另一種重複:

glsl void main() { vec2 st = gl_FragCoord.xy / resolution; st = mix(vec2(-5.0), vec2(5.0), st); float d = regular_polygon(fract(st), vec2(0.5), 0.55, 0.0, 4); d = abs(d); FragColor.rgb = stroke(d, 0.5, 0.5, 0.5) * vec3(1.0); FragColor.a = 1.0; }

http://code.juejin.cn/pen/7105973697403420709

根據這個原理,我們可以繪製出類似中國傳統紋飾的圖案,例如:

http://code.juejin.cn/pen/7105975492284514334

我們看到,利用距離場加上重複,我們可以用非常簡單的程式碼繪製出看起來比較複雜的規律圖案,這個過程需要一些數學知識和想象力,但創造圖案是非常有趣的,到這一步,我們的WebGL渲染漸入佳境,在後續還有更多令人驚歎的效果可以通過寥寥幾行程式碼實現,而這正是GPU繪圖的魅力!