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

語言: CN / TW / HK

接上一篇: https://juejin.cn/post/7098256201661546532

在繼續深入之前,我們先來解決上一篇中的遺留問題:

我們默認不設置頂點的時候,繪製的圖形是整個canvas範圍,但是WebGL並不支持四邊形圖元,那麼我們原本的繪製範圍是如何界定的呢?

因為三角形是基本圖元,而Canvas畫布本身是一個四邊形,所以我們需要使用兩個三角形的頂點進行繪製,這也是gl-renderer默認的頂點數據,它相當於:

js renderer.setMeshData([ { positions: [[-1, -1, 0], [1, -1, 0], [1, 1, 0], [-1, 1, 0]], cells: [[0, 1, 3], [3, 1, 2]], }, ]);

如下圖所示,我們用兩個三角形來完成四邊形的繪製。

image.png

實際上,任意二維簡單多邊形都可以剖分成若干個三角形,然後進行繪製,這個過程在數學上叫做三角剖分

c995d143ad4bd1133f8515e250afa40f4afb0591.png

用WebGL繪製平面圖形,三角剖分是一種基本的方法。不過這個問題我們可以留待後續的文章詳細講解。在這一講我們先回到片段着色器部分,來談談利用着色器或者説利用GPU進行造型繪圖的基本原理和方法。

與上一講一樣,我們通過動手代碼實踐來理解,先從簡單的開始。

最簡單的單色繪製,在上一講已經説過了,比如下面的代碼,將整個繪圖區繪製為黑色:

```js const canvas = document.querySelector('canvas'); const renderer = new GlRenderer(canvas, {webgl2: true});

const fragment = #version 300 es precision highp float; out vec4 FragColor; void main() { FragColor = vec4(0, 0, 0, 1); }; const program = renderer.compileSync(fragment); renderer.useProgram(program); renderer.render(); ```

下面我們稍微修改一下代碼:

```js const canvas = document.querySelector('canvas'); const renderer = new GlRenderer(canvas, {webgl2: true});

const fragment = #version 300 es precision highp float; out vec4 FragColor; uniform vec2 resolution; void main() { vec2 st = gl_FragCoord.xy / resolution; FragColor = vec4(0, 0, 0, 1); if(st.x > 0.5) { FragColor = vec4(1, 1, 1, 1); } }; const program = renderer.compileSync(fragment); renderer.useProgram(program); renderer.uniforms.resolution = [canvas.width, canvas.height]; renderer.render(); ```

上面的代碼很好理解,我們判斷x座標大於0.5時,輸出顏色白色,否則為黑色。

這樣我們得到如下的效果:

1653014317595.jpg

上面的代碼,我們用if(st.x > 0.5)來判斷黑白分界線,實際上我們有更簡單的辦法:

```glsl

version 300 es

precision highp float; out vec4 FragColor; uniform vec2 resolution; void main() { vec2 st = gl_FragCoord.xy / resolution; FragColor.rgb = step(0.5, st.x) * vec3(1.0); FragColor.a = 1.0; } ```

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

這裏我們用step函數來代替if語句,step(x, y)是階梯函數,當y小於x時值為0,y大於等於x時值為1。

在着色器中,step是一個非常好用的函數,可以使用它來繪製不同的圖形。

比如下面這個例子通過step繪製一個圓形:

```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); FragColor.rgb = step(length(st - center), 0.2) * vec3(1.0); FragColor.a = 1.0; } ```

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

消鋸齒

直接用step繪製曲線,容易產生鋸齒,我們可以通過smoothstep來消除鋸齒:

```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); float d = length(st - center); FragColor.rgb = smoothstep(d - 0.015, d, 0.2) * vec3(1.0); FragColor.a = 1.0; } ```

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

smoothstep 對階梯函數進行了平滑處理,它在範圍的上下限之間進行插值。

通過兩個step相減或者兩個smoothstep相減的技巧,可以用來畫線,例如我們修改一下上面的代碼:

```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); float d = length(st - center); FragColor.rgb = (smoothstep(d - 0.015, d, 0.2) - smoothstep(d, d + 0.015, 0.18)) * vec3(1.0); FragColor.a = 1.0; } ```

就可以繪製一個圓環:

1653297692576.jpg

我們可以將這個技巧封裝成一個通用函數:

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); }

它的第一個參數接受一個距離量,第二個參數在指定距離的等距線附近繪製,第三個參數表示繪製寬度,第四個參數是平滑比率。

這樣我們就可以用stroke來畫線了,只要我們能把距離定義出來,比如下面的代碼繪製了一條x=0.5的直線:

glsl void main() { vec2 st = gl_FragCoord.xy / resolution; float d = stroke(st.x, 0.5, 0.02, 0.1); FragColor.rgb = d * vec3(1.0); FragColor.a = 1.0; }

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

這種利用距離來構圖的思路叫做距離場構圖法。下面的代碼繪製了y=x的直線和y=4*(x-0.5)**2的拋物線:

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

在這一講的最後,留給大家一個作業,用距離場構圖法來繪製一條正弦曲線,要求至少繪製3個週期,你知道如何繪製嗎?如果你做出來了,可以把代碼分享到評論區。