視覺化學習:使用WebGL實現網格背景

beckyye發表於2024-03-05

前言

作為前端開發人員,我們最關注的就是應用的互動體驗,而元素背景是最基礎的互動體驗之一。一般而言,能夠使用程式碼實現的介面,我們都會盡可能減少圖片的使用,這主要是有幾方面的原因,第一,是圖片會消耗更多的頻寬,對於移動端或者網路訊號較差時的體驗不夠友好,第二是不夠便捷,在使用圖片的情況下即使有細微的調整也需要重新做圖上傳,第三是不夠靈活,這一點主要體現在根據不同的條件需要呈現不同的效果。

本文就使用網格背景作為例子,來透過程式碼實現,這是在視覺化課程中新學的內容,這裡我做一個總結和複習。網格背景是一種常見的設計元素,它可以為網頁增添一種現代感和動態感。要實現網格背景,我們可以使用CSS程式碼來實現,這類實現方式我在很多文章中都有看到過,但除了使用CSS,我們還可以透過WebGL實現,透過WebGL來實現,還能額外帶來一些好處,比如有更好的效能,也能與畫布上的其他元素更好地融合。

網格背景

在使用具體的程式碼實現之前,我們先簡單來分析一下網格背景。

視覺化學習:使用WebGL實現網格背景

從上面的圖片中我們能發現,網格背景其實可以看作是一個小的網格圖案重複了n遍而形成的一個背景,也就是說這是一個重複圖案的背景。

使用CSS實現

在使用WebGL來實現網格背景之前,我們還是先來看一下CSS程式碼是如何實現的。

在使用CSS程式碼實現網格背景時,需要用到CSS中的漸變函式,因為漸變函式最終形成的效果類似於影像,所以可以當作影像來使用,也就是說可以應用於background-image屬性。漸變函式有三種:線性漸變、徑向漸變和圓錐漸變,在這個例子中,由於網格是直線的,所以我們使用線性漸變函式就可以了。

線性漸變的語法比較簡單,以下是MDN給出的表示式

<linear-gradient()> = 
  linear-gradient( [ <linear-gradient-syntax> ] )  

<linear-gradient-syntax> = 
  [ <angle> | to <side-or-corner> ]? , <color-stop-list>  

<side-or-corner> = 
  [ left | right ]  ||
  [ top | bottom ]  

<color-stop-list> = 
  <linear-color-stop> , [ <linear-color-hint>? , <linear-color-stop> ]#  

<linear-color-stop> = 
  <color> <length-percentage>?  

<linear-color-hint> = 
  <length-percentage>  

<length-percentage> = 
  <length>      |
  <percentage>  

我們主要向線性漸變函式傳遞三類引數:第一是漸變的方向,第二是初始色值,第三是最終色值。如果要控制得更細緻,我們還可以透過上述公式中的length或者percentage來控制某個色值的範圍,或者設定多個階梯色值。

現在我們就可以透過以下CSS程式碼,來實現網格背景的設定。

.grid-bg {
  width: 300px;
  height: 300px;
  background-image: linear-gradient(to right, transparent 95%, #ccc 0),
    linear-gradient(to bottom, transparent 95%, #ccc 0);
  background-size: 8px 8px, 8px 8px;
}

以上程式碼中首先使用background-size屬性指定了一個網格圖案的大小,然後我們知道background-repeat屬性的預設值是repeat,所以會重複這個圖案來鋪滿整個元素的背景,最後我們來看background-image這個屬性,我們使用了線性漸變函式來給這個屬性賦值,可以看到這裡用了兩個漸變函式。

第一個漸變函式,設定的漸變方向是to right,也就是自左向右進行漸變,初始色值是transparent也就是透明色,這裡使用了百分數95%來控制透明色的範圍,也就是說從0%的位置開始到95%的位置,都是透明色,然後最終色值是#ccc也就是灰色,我們想要95%到100%的範圍都是#ccc的灰色,可以直接簡寫為0。

第二個漸變函式也是類似的。

在這兩個漸變函式的作用下,最終形成的圖案就是一個小網格,右邊5%的寬度和下邊5%的寬度由灰色填充;然後在background-repeat預設值repeat的作用下,就會鋪滿對應元素。

使用WebGL實現

那麼既然CSS已經可以實現網格來替代圖片了,為什麼又要使用WebGL來實現呢?所以那肯定是WebGL的實現有其他的優勢,首先是直接呼叫GPU的話,無論是有多少重複圖案都能一次完成,理論上沒有效能瓶頸,其次是能滿足更多型別的需求,比如能使網格隨著Canvas中的其他元素一起縮放。

那麼在WebGL中要如何去實現網格背景呢?我們可以透過圖案的使用來實現。

基礎頁面

首先最基礎的,我們先在頁面上放置一個Canvas。

<canvas width="512" height="512"></canvas>

操作WebGL

接著就可以開始操作WebGL去完成圖案的繪製。

在本次實現中,使用了一個基礎庫gl-renderer來簡化WebGL的操作,讓我們可以將重心放在資料提供和編寫shader上。

以下是操作WebGL的程式碼:

// 第一步:建立Renderer物件
const canvas = document.querySelector('canvas');
const renderer = new GlRenderer(canvas);
// 第二步:建立並啟用WebGL程式
const program = renderer.compileSync(fragment, vertex);
renderer.useProgram(program);
// 第三步:設定uniform變數
renderer.uniforms.rows = 32; // 64; // 每一行顯示多少個網格(在片元著色器中使用)
// 第四步:將頂點資料送入緩衝區
renderer.setMeshData([{
  positions: [ // 頂點(覆蓋整個畫布)
      [-1, -1], // 左下
      [-1, 1], // 左上
      [1, 1], // 右上
      [1, -1] // 右下
  ],
  attributes: {
    uv: [ // 紋理座標(座標系:左下角[0,0] 右上角[1,1])
        [0, 0], // 左下
        [0, 1], // 左上
        [1, 1], // 右上
        [1, 0] // 右下
    ]
  },
  cells: [ // 頂點索引(三角剖分):將矩形剖分成兩個三角形
      [0, 1, 2],
      [2, 0, 3]
  ]
}]);
// 第五步:執行渲染
renderer.render();

以上程式碼不難理解,就是向WebGL傳遞資料,並執行渲染。主要有以下幾個操作:

  • 首先在初始化階段,根據GLSL程式碼和Canvas的WebGL上下文建立WebGL程式;

  • 接著就是傳遞資料,包括uniform常量、頂點資料和紋理座標。其中紋理座標的使用與圖案有關。

  • 然後是設定三角剖分的頂點索引。三角剖分簡單來說就是將一個多邊形使用多個三角形組合拼接來表示,可以參考下圖來理解。

    視覺化學習:使用WebGL實現網格背景
  • 最後一步就是渲染。

GLSL程式碼

現在我們來看關鍵的GLSL程式碼。

// 頂點著色器
attribute vec2 a_vertexPosition;
attribute vec2 uv;
varying vec2 vUv;

void main() {
  gl_PointSize = 1.0;
  vUv = uv;
  gl_Position = vec4(a_vertexPosition, 1, 1);
}

以上是頂點著色器,這段程式碼比較基礎,找到要處理的畫素點,其餘的就是將紋理座標傳遞給片元著色器。

在網格背景的實現中,片元著色器是比較關鍵的一環,現在我們就來看片元著色器的實現程式碼:

#ifdef GL_ES
precision mediump float;
#endif

varying vec2 vUv; // 由頂點著色器傳來的uv屬性
uniform float rows;

void main() {
  // st:儲存畫素點對應紋理座標的小數部分
  vec2 st = fract(vUv * rows); // fract函式是一個用於獲取向量中小數部分的函式
  float d1 = step(st.x, 0.9); // step:階梯函式。當step(a,b)中的b < a時,返回0;當b >= a時,返回1。
  float d2 = step(0.1, st.y);

  // 根據d1*d2的值,決定使用哪個顏色來繪製當前畫素。
  // st.x <= 0.9 且 st.y >= 0.1時,d1*d2=1, 否則為0
  gl_FragColor.rgb = mix(vec3(0.8), vec3(1.0), d1 * d2);
  gl_FragColor.a = 1.0;
}

在編寫shader時,要理解其中的GLSL程式碼不是太容易,因為對於課程中給出的解釋也並不太能理解,所以我也是花了一點時間去思考。

首先就是這個fract函式的呼叫,課程中給出的說法是:它可以幫助我們獲得重複的 rows 行 rows 列的值 st

當一個數從 0~1 週期性變化的時候, 我們只要將它乘以整數 N,然後再用 fract 取小數,就能得到 N 個週期的數值。

但我感覺這種說法並不太直觀,至少我看下來覺得還是一頭霧水,當然也可能是我的理解能力有限,著色器程式理論上是批次執行,那麼這段程式碼針對某個待處理的畫素點是什麼含義呢?因為暫時我想先自己探索一番,所以我也沒有參考其他資料。

經過多日思考,以下是我目前的幾點理解:

  1. 首先是傳遞的紋理座標,我們可以理解是一個單元的座標,座標的範圍是一個正方形。

  2. 然後vUv * rows可以看作是相當於把紋理座標在畫布上的縱向放大rows倍,又因為紋理座標範圍的原因,同時相當於在橫向上也放大了rows倍。

  3. 接著就得到了當前待處理的畫素點對映到紋理座標上的位置,後續會根據這個畫素點的紋理座標去計算設定。

  4. st儲存了畫素點對應紋理座標的小數部分。

  5. step是階梯函式,接收兩個入參a和b,根據a和b的大小關係返回0或1;當b < a時,返回0;當b >= a時,返回1。

    兩個step函式的執行後,我們就可以得到:

    st.x <= 0.9 且 st.y >= 0.1時,d1*d2=1, 否則為0

  6. 最後我們呼叫mix得到最終的色值。

    mix(a, b, c)是一個線性插值函式。a和b是兩個色值,當c為0時,返回a;當c為1時,mix函式返回b。

    在這裡vec3(0.8)是一個灰色色值,vec3(1.0)是白色;顯而易見當d1或d2為0時,也就是st.x > 0.9 或 st.y < 0.1時,畫素點渲染為灰色,否則渲染為白色。

具體渲染結果可參考下圖(隨手畫的比較潦草),最終網格的數量由rows決定:

視覺化學習:使用WebGL實現網格背景

總之我理解的就是,在片元著色器中,我們主要去計算某個畫素點的色值,在這個例子中,就是對映到紋理座標並根據紋理座標計算得到一個色值,最終畫素點會被渲染為這個色值。

這段程式碼中,紋理座標的小數部分是週期性重複的,所以就可以得到重複的圖案,最終形成網格背景。

總結

最後總結一下吧,要理解WebGL程式碼,有時候還是需要轉換一下思路,其實我也並不太確定自己的理解是不是對的,但我覺得有時候學習新的東西,就是給自己一個開啟思路的機會,最重要的還是自己去思考理解,讓自己得到新的啟發。

相關文章