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

語言: CN / TW / HK

接上篇:https://juejin.cn/post/7100864973332545550

在上一篇中,我們瞭解了基本的距離場構圖法,在這一篇,我們來進一步利用這個方法來繪製更多圖形。

距離場構圖法,最核心的思路是要定義一個形狀的距離場,通俗來説,就是定義整個畫布空間中每個像素點的距離值。

最簡單的例子就是圓,定義圓的距離場,只需要定義像素點到圓心的距離,在上一篇中我們畫圓,就是用了這個定義。

https://code.juejin.cn/pen/7100853943923638280

與圓比較類似,也很簡單的距離場是矩形:

```glsl

version 300 es

precision highp float; out vec4 FragColor; uniform vec2 resolution; void main() { vec2 st = gl_FragCoord.xy / resolution; vec2 center = vec2(0.5); st = abs(st - center); float d = max(st.x, st.y); FragColor.rgb = smoothstep(d - 0.015, d, 0.2) * vec3(1.0); FragColor.a = 1.0; } ```

https://code.juejin.cn/pen/7103401255023673358

接下來我們來求稍微複雜一點的,任意直線的距離場。

點到直線的距離

要計算點到任意直線的距離。

最簡單的辦法是用向量來計算,我們用兩點A、B來確定一條直線AB,那麼任意一點P到直線AB的距離為:

$$d = \dfrac{|\overrightarrow{PA} \times \overrightarrow{AB}|}{|\overrightarrow{AB}|}$$

因為如圖所示:

image.png

向量$\overrightarrow{PA}$與向量$\overrightarrow{AB}$的叉積模的幾何意義是平行四邊形APP'B的面積,除以底邊AB的長度,就是P到直線AB的距離。

所以我們可以實現直線的距離場:

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

並用以繪製直線:

glsl void main() { vec2 st = gl_FragCoord.xy / resolution; float d = sdf_line(vec2(0), vec2(1.0), st); float d2 = sdf_line(vec2(1.0, 0.0), vec2(0.0, 1.0), st); FragColor.rgb = (stroke(d, 0.0, 0.03, 0.2) + stroke(d2, 0.0, 0.03, 0.2)) * vec3(1.0); FragColor.a = 1.0; }

https://code.juejin.cn/pen/7103412310877995039

點到線段的距離

前面我們計算的是點到直線的距離,稍微修改一下,考慮兩個端點,就可以計算點到線段的距離。

點到線段的距離,當點P在向量$\overrightarrow{AB}$的投影落在線段AB之間時(如下圖左邊),點P到線段AB的距離就等於P到AB所在直線的距離,而點P在向量$\overrightarrow{AB}$的投影落在線段AB之外時(如下圖右邊),那麼點P到線段AB的距離為線段AP和PB中較短的一條的長度。

image.png

所以我們得到如下的線段距離場計算函數:

```glsl

float sdf_seg(vec2 a, vec2 b, vec2 st) { 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(a, b, st); } return min(length(ap), length(bp)); } ```

注意這裏面我們用向量$\overrightarrow{AP}$和向量$\overrightarrow{AB}$的點乘除以線段AB的長度來得到點P在AB上的投影,通過投影的長度和方向來判斷點是否落在線段AB之間。

最終我們就可以用距離場繪製線段了:

https://code.juejin.cn/pen/7103420502697803790

採樣與曲線繪製

如果要繪製一條連續曲線,我們可以取相鄰的三個點A、B、C採樣,計算P點到這三個點構成的兩條線段AB和AC的距離,取距離短的作為P到曲線的距離。

```glsl float sdf_plot(vec2 a, vec2 b, vec2 c, vec2 st) { float d1 = sdf_seg(a, b, st); float d2 = sdf_seg(b, c, st);

return min(d1, d2); } ```

我們定義一個宏,來對曲線方程進行採樣:

```glsl

ifndef PLOT

define PLOT(f, st, step) sdf_plot(vec2(st.x - step, f(st.x - step)), vec2(st.x, f(st.x)), vec2(st.x + step, f(st.x + step)), st)

endif

```

有了採樣方法,我們就可以繪製各種曲線了:

```glsl float stroke(float d, float d0, float w, float smth) { float th = 0.5 * w; smth = smth * w; float start = d0 - th; float end = d0 + th; return smoothstep(start, start + smth, d) - smoothstep(end - smth, end, d); }

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

float sdf_seg(vec2 a, vec2 b, vec2 st) { 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(a, b, st); } return min(length(ap), length(bp)); }

float sdf_plot(vec2 a, vec2 b, vec2 c, vec2 st) { float d1 = sdf_seg(a, b, st); float d2 = sdf_seg(b, c, st);

return min(d1, d2); }

ifndef PLOT

define PLOT(f, st, step) sdf_plot(vec2(st.x - step, f(st.x - step)), vec2(st.x, f(st.x)), vec2(st.x + step, f(st.x + step)), st)

endif

float fx(in float x) { return 0.0; }

float fy(in float x) { return 9999999.99 * x; }

float f1(in float x) { return floor(x); }

float f2(in float x) { return sin(2.0 * x) / x; }

float f3(in float x) { return sqrt(1.0 - x * x); // return 0.0; }

float f4(in float x) { return -x - sin(x); }

float f5(in float x) { return log(x); }

void main() { vec2 st = gl_FragCoord.xy / resolution; st = mix(vec2(-10, -10), vec2(10, 10), st);

float stp = 0.1; float thick = 0.4; float smth = 0.2;

// PLOT func, field, step float px = PLOT(fx, st, stp); float py = PLOT(fy, st, stp);

float p1 = PLOT(f1, st, stp); float p2 = PLOT(f2, st, stp); float p3 = PLOT(f3, st, stp); float p4 = PLOT(f4, st, stp); float p5 = PLOT(f5, st, stp);

vec3 cx = stroke(px, 0.0, 0.2, 0.2) * vec3(1.0, 1.0, 1.0); vec3 cy = stroke(py, 0.0, 0.2, 0.2) * vec3(1.0, 1.0, 1.0);

vec3 c1 = stroke(p1, 0.0, thick, smth) * vec3(0, 1.0, 0); vec3 c2 = stroke(p2, 0.0, thick, smth) * vec3(0, 1.0, 1.0); vec3 c3 = stroke(p3, 0.0, thick, smth) * vec3(1.0, 1.0, 0); vec3 c4 = stroke(p4, 0.0, thick, smth) * vec3(1.0, 0, 1.0); vec3 c5 = stroke(p5, 0.0, thick, smth) * vec3(1.0, 0, 0);

FragColor.rgb = cx + cy + c1 + c2 + c3 + c4 + c5; FragColor.a = 1.0; } ```

https://code.juejin.cn/pen/7103428660606009374

注意,在上面代碼裏,我們通過st = mix(vec2(-10, -10), vec2(10, 10), st);來擴大座標系的區間,將座標系從(0,0),(1,1)擴大到了(-10,-10),(10,10),這也是一種常用的數學技巧,可以牢記。

在下一講裏面,我們將繼續深入,利用距離場構圖轉換不同座標系來構造出更加有趣的圖形。