前言
本文將介紹如何使用著色器來進行幾何造型,說到幾何圖形大家一定都不陌生,比如說三角形、圓形,接觸過WebGL基礎使用的小夥伴一定都知道怎麼去在畫布上繪製一個三角形,只要傳入三個頂點座標,並選擇繪圖模式,我們就能在WebGL的畫布上畫出一個三角形。
但是除了這種形式之外,我們還可以直接使用片元著色器進行幾何造型,那麼具體要怎麼做呢,下面就以三角形作為例子來進行演示。
繪製三角形
要實現三角形的繪製,我們需要先判斷當前片元是否在三角形內部,也就是說在給片元上色之前,我們需要先計算出片元的色值。
假設我們已知三角形的三個頂點:
vec2(0.3),
vec2(0.5, 0.7),
vec2(0.7, 0.3)
那麼我們就可以根據當前片元的紋理座標和三角形三個頂點的座標,計算片元與三角形的距離。下面我們會透過使用向量的叉乘和點乘來進行計算,對向量操作不太熟悉的小夥伴可以去找些資料複習一下,或者參考我前面的文章。
首先在片元著色器中定義一個函式,叫做line_distance
,用於計算點到一條直線的距離,這裡我們使用了向量的叉乘來計算,程式碼應該比較容易理解,就簡單說一下,這裡的引數st表示我們要判斷的點,a和b分別表示直線上的兩個點,因為這三個點都是平面上的點,叉積本身也是向量,所以直接取叉積Z軸的分量就是二維向量叉乘的結果。這個函式我們在後續會用於判斷點是否在三角形內部。
// 點到直線的距離
float line_distance(in vec2 st, in vec2 a, in vec2 b) {
vec3 ab = vec3(b - a, 0);
vec3 p = vec3(st - a, 0);
return cross(p, normalize(ab)).z;
}
接著繼續定義一個函式,叫做seg_distance
,用於計算點到一條線段的距離,點與線段的距離我們分為兩類情況考慮,一類是點線上段的上方或下方,這個時候點到線段的距離,就等於點到這條線段所在直線的距離,第二類是點線上段的左右兩側,此時點到線段的距離就是點到線段兩個端點的距離中的較小值。
// 點到線段的距離
float seg_distance(in vec2 st, in vec2 a, in vec2 b) {
vec3 ab = vec3(b - a, 0);
vec3 p = vec3(st - a, 0);
float l = length(ab);
float d = abs(
cross(p, normalize(ab))
).z;
float proj = dot(p, ab) / l;
if (proj >= 0.0 && proj <=l) return d;
return min(distance(st, a), distance(st, b));
}
最後定義一個函式,叫做triangle_distance
,用於計算點到三角形的距離,這屬於一個自定義的距離概念,從程式碼上看準確來說應該是點到三條邊的距離中的最小值。在這段程式碼中,我們定義內部的距離取負數,外部的距離為正。
// 點與三角形的距離
float triangle_distance(in vec2 st, in vec2 a, in vec2 b, in vec2 c) {
float d1 = line_distance(st, a, b);
float d2 = line_distance(st, b, c);
float d3 = line_distance(st, c, a);
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))); // 內部距離為負
}
return min(seg_distance(st, a, b), min(seg_distance(st, b, c), seg_distance(st, c, a))); // 外部距離為正
}
首先我們判斷三組向量叉乘的結果,符號是否是一樣的,如果是一樣,說明當前點在三角形內部,那麼點必然是線上段的上方,所以直接用三個叉積中的最小值。
如果點在三角形外部,就取點到三個線段距離中的最小值。
到這裡我們就能得到片元與三角形的距離了。
float d = triangle_distance(
vUv,
vec2(0.3),
vec2(0.5, 0.7),
vec2(0.7, 0.3)
);
接著我們就利用這個距離來進行最簡單的填充,將三角形的內部直接填充為白色。
gl_FragColor.rgb = (1.0 - smoothstep(0.0, 0.01, d)) * vec3(1.0);
當然除了填充之外,我們可以進行三角形的描邊。
gl_FragColor.rgb = (smoothstep(-0.005, 0.0, d) - smoothstep(0.0, 0.005, d)) * vec3(1.0);
smoothstep(a, b, c)
函式可能有些小夥伴不瞭解,這裡簡單說一下,這個函式接收3個引數。
- 在a小於b的情況下
- 如果c小於a,會返回0
- 如果c大於b,就返回1
- 而在a大於b的情況下
- 如果c大於a,返回的是0
- 如果c小於b,就返回1
- 如果c在a和b之間,就返回一個過渡值
所以其實上面的計算也可以反過來寫。
應用場景
那麼看到這裡,有些小夥伴可能就有疑問了,這感覺好像和直接用頂點畫沒什麼區別,那麼我們為什麼要用著色器造型呢?答案其實很簡單,就是它能幫助我們實現更多的圖案效果。比如使用以下程式碼,可以實現三角環。
d = fract(20.0 * d);
gl_FragColor.rgb = (smoothstep(0.45, 0.5, d) - smoothstep(0.5, 0.55, d)) * vec3(1.0);
在這段程式碼中,將距離的值放大20倍,再取小數部分,就能實現重複的環。這種繪製方式叫做符號距離場渲染(SDF),這是圖形渲染中的一個專有名詞,本質上就是利用空間中的距離分佈來著色,是著色器造型生成圖案的基礎方法。這種效果如果要用頂點來畫就沒這麼簡單了。
除了SDF,我們注意到程式碼中使用的是紋理座標,所以著色器造型還可以配合紋理實現更多的效果,比如圖片裁剪。下面我們來看一個簡單的例子。
void main() {
vec4 color = texture2D(tMap, vUv);
float d = distance(vUv, vec2(0.5, 0.5));
gl_FragColor.rgb = (1.0 - smoothstep(0.4, 0.4005, d)) * color.rgb;
gl_FragColor.a = (1.0 - smoothstep(0.4, 0.4005, d));
}
這裡我們定義的距離是片元到一個點的距離,根據這個距離我們能按照上面的方式繪製出一個圓心在0.5, 0.5
的圓,同時我們現在還能將紋理按這個圓形裁剪出來,就還是蠻好用的效果。
總結
在剛開始著色器幾何造型的學習時,我也有點奇怪WebGL已經有頂點可以畫圖形了,為什麼還要有這個著色器造型,看到符號距離場SDF的時候,才感覺出來是有區別哦,好像還挺有用的。
繪製的步驟簡單來說就兩步,第一步,確定計算距離的方式,第二步,根據距離給片元上色。
繪製三角形和圓形都還算簡單的,我相信很多小夥伴看下來應該都能理解了,可以自己動手嘗試一下,有興趣的小夥伴還可以去嘗試更多的圖形,比如正多邊形、橢圓等等這些圖形,甚至是更復雜的圖形。
完整程式碼參考