WebGL:使用著色器進行幾何造型

beckyye發表於2024-05-11

前言

本文將介紹如何使用著色器來進行幾何造型,說到幾何圖形大家一定都不陌生,比如說三角形、圓形,接觸過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),這是圖形渲染中的一個專有名詞,本質上就是利用空間中的距離分佈來著色,是著色器造型生成圖案的基礎方法。這種效果如果要用頂點來畫就沒這麼簡單了。

WebGL:使用著色器進行幾何造型

除了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:使用著色器進行幾何造型

總結

在剛開始著色器幾何造型的學習時,我也有點奇怪WebGL已經有頂點可以畫圖形了,為什麼還要有這個著色器造型,看到符號距離場SDF的時候,才感覺出來是有區別哦,好像還挺有用的。

繪製的步驟簡單來說就兩步,第一步,確定計算距離的方式,第二步,根據距離給片元上色。

繪製三角形和圓形都還算簡單的,我相信很多小夥伴看下來應該都能理解了,可以自己動手嘗試一下,有興趣的小夥伴還可以去嘗試更多的圖形,比如正多邊形、橢圓等等這些圖形,甚至是更復雜的圖形。

完整程式碼參考

相關文章