WebGL 的 Hello World

凹凸實驗室發表於2022-03-24

本文整理自 div 俠於 凹凸 2022 年技術分享,簡單介紹了 WebGL 畫一個基礎圖形的流程,希望你瞭解之後,在使用 3d 渲染庫的時候可以少點迷糊。

四種常用的頁面繪圖工具

關於h5頁面的圖形繪製,我們大多談及的是這四種工具:html+css,svg、canvas2d、webgl。

image-20220321180951763

html+css 是最常見的繪圖工具了,使用 css 繪圖跟平時寫頁面佈局一樣,在製作圖表的時候,我們可以用 css 把圖表的樣式定義好,其他的,就是根據資料的不同 ,給元素新增上不同的屬性。這樣的開發對於圖表元素簡單、資料結點少的場景非常友好。不僅可以減少開發的工具量,而且不用引入多餘的程式碼庫。但是,隨時需要繪製的圖形越來越多, css 程式碼做變得越來越複雜,加上 css 本來沒有邏輯語義,程式碼會變得不易閱讀和維護。

svg 是可縮放向量圖形,他跟 html , css 的結合很緊密,可以把 svg 當做 img 的 src ,也可以用 css 操控 svg 的屬性, svg 和 html 都是文字標記語言, svg 較 html 增加了對非線性圖形的支援,包括圓弧,貝塞爾曲線等。同時, svg 支援<g>, <defs> 等複用類的語法,這讓他就算繪製很多圖形,程式碼也保留一定可讀性。不過 svg 也有一些缺點。因為一個圖形都是一個元素結點。在資料多的時候,頁面重新整理引起的佈局、渲染計算的開銷就會非常大。而且,完整的 svg 把結構,樣式,複用邏輯都放在一起,跟 html + css + js 這種三者分開的模式比總少了一些整潔。

canvas2D 是 canvas 的 2d 繪圖上下文,他提供了一系列方法,用於對 canvas 區域的影像進行修改和繪製,相比於前兩者的開箱即用, canvas2d 很多圖形和顏色都需要自己實現和封裝使得這個工具上手的難度大了不少,但是,如果把這些基礎的事情做好,你將擁有一個功能完全覆蓋前面兩個工具,而且便於擴充套件的繪圖工具。

webGL 也是 canvas 的繪圖上下文,是 opengl es 的 web 實現。最大的特點,就是更低層,可以直接使用 gpu 的並行能力。在處理圖形數量非常多,畫素級處理和 3d 物體的場景下,擁有很高的效能優勢。

四種工具的選擇思路

image-20220321181126634

當我們拿到一個繪圖需求的時候,應該先看看這個需求用到的圖形是不是比較少,而且簡單。如果是的話,可以直接選擇 css 進行快速開發。如果圖形雖然簡單但比較多,或者圖形有一些曲線需求,這個時候 svg 還可以快速應付。如果圖形之間的結構複雜,數量比較多的時候選擇 canvas2d 。而當圖形的數量級大到一定的量,或者需要對每一個畫素進行處理,或者需要大量的 3d 展示的時候,我們得使用 webgl 了

image-20220321181247016

webgl的hello world

webgl 的 hello world 不像其他工具一樣可以一兩行程式碼就搞定,而是足足有四十多行程式碼。雖然這串程式碼在各個 3d 渲染庫裡都有對應封裝的方法,基本不用我們自己徒手去寫,但是學習這串程式碼可以讓我們對 webgl 繪圖過程有一個最基礎的瞭解。

webgl 繪圖一共有五個步驟:

  1. 建立 webgl 繪圖上下文
  2. 建立著色器程式設計,關聯到 gl 上下文中 (跟第3步並行)
  3. 建立資料,放入緩衝區並把緩衝區關聯到 gl 止下文中(跟第2步並行)
  4. gpu載入快取中的資料
  5. 繪製圖形

建立Webgl上下文

const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');

建立著色器程式

const program = gl.createProgram();
gl.attachShader(program, /*某個著色器(下文的vertexShader)*/);
gl.linkProgram(program);
gl.useProgram(program);

著色器是一段給 gpu 執行的程式,我們用 glCreateProgram 建立一個空的程式物件,然後使用 glAttachShader 給這個程式物件填充編譯後的著色器程式碼。著色器是什麼,怎麼編譯後面再說,這裡可以把他當成某一個函式編譯後的程式碼。把幾個這種編譯後的函式放入程式物件後, gpu 執行這個程式物件,就會把畫素資訊當做入參,依次執行程式物件中的函式。

填充完著色器程式碼後,呼叫 glLinkProgram 把程式關聯到 gl 上下文中,並用 glUseProgram 來啟用這個程式。

接下來,來看一下著色器程式碼怎麼搞出來。

const vertex = `
      attribute vec2 position;
      void main() {
        gl_Position = vec4(position, 1.0, 1.0);
      }
    `;
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertex);
gl.compileShader(vertexShader);

首先我們定義了一個變數 vertex 並給他賦值一串其他語言格式的程式碼字串,這個串程式碼是 glsl 程式碼,是一個跟 c 語言很相似的程式碼。程式碼接收一個傳入的二維向量 position ,然後把他執行環境中的全域性變數 gl_Position 設定成一個四維向量,這個四維向量前兩個維度的分量是傳入的二維向量。

接下來用 glCreateShader 建立一個著色器, VERTEX_SHADER 常量說明這個著色器是一個頂點著色器,跟頂點著色器對應的是片元著色器,頂點著色器處理做為確定點的位置。片元著色器則對頂點構成的圖形中的所有位置進行逐個處理,比如兩點畫一個直線,兩點是頂點著色器確定的,直線是片元著色器在確定了兩個點的位置之後畫的。

在我們建立了一個空的頂點著色器物件 vertexShader 之後,就可以用 glShaderSource 把前面的字串程式碼放入頂點著色器物件中,然後用 glCompileShader 把這段程式碼編譯成可執行檔案。這個過程跟c語言的編譯過程是相似的。

gl.attachShader(program, /*某個著色器(下文的vertexShader)*/);
gl.attachShader(program, vertexShader);

完成這一步之後,就要回到上面寫註釋那裡,把著色器物件關聯到程式物件裡。當然,你還得去寫一個片元著色器,用同樣的步驟把一個片元著色器也關聯到程式物件裡。

image-20220321181429987

將資料存入緩衝區

const points = new Float32Array([-1, -1, 0, 1, 1, -1]);
const bufferId = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, bufferId);
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);

經過上文的操作之後,我們已經有了一個裝載著著色器程式碼的程式物件,這個物件放到 gl 繪圖上下文中被啟用了。接下來,我們要定義的就是給這個程式用的資料。

在頂點著色器那一塊,程式碼裡面接受一個傳入的二維向量,就是我們現在要定義的。首先定義一個型別化陣列,初始化的時候放入6個數,這個6個數後面會被繪圖程式分成三組放到三次頂點著色器呼叫中。另外,使用型別化陣列是為了優化效能,讓大量資料的情況下,資料佔用的空間更小。

有了資料之後,呼叫 glCreateBuffer 建立一個緩衝區物件,用 glBindBuffer 把這個物件跟 gl 繪圖上下文關聯起來,最後呼叫 glBufferData 把 points 的資料放入緩衝區中。

gpu載入快取中的資料

const vPosition = gl.getAttribLocation(program, "position");
gl.vertexAttribPointer(vPosition, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(vPosition);

在這一步中,我們先呼叫 glGetAttribLocation 拿到程式物件中 position 這個變數的位置,呼叫 glVertexAttribPointer 把這個變數的長度設定為 2 ,型別設定成 glFLOAT ,並用 glEnableVertexAttribArray 啟用這個變數

繪製圖形

gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, points.length / 2);

到了最後一步,只要用 glClear 把顏色緩衝區清空,然後用 glDrawArrays 進行繪圖就行了。其中 gl.TRIANGLES 確定了片元著色器的繪圖範圍,當這個值是 gl.POINTS ,著色器會把點兩兩連線,而 gl.TRIANGLES 讓第三個點成一組繪製三角形

image-20220321181511519

這樣, webgl 的一個hello world就完成了,上面的三角形就是這40行程式碼輸出的影像。

總結

這段程式在 three.js 和其他的 3d 框架和工具庫裡都有一定的封裝,通過那些庫進行 webgl 的繪圖相對來說會方便很多,但如果不知道這些庫最根本的操作,就很容易在遇到問題的時候繞進去。所以希望本文能增加大家對 web 3d 底層方面的理解,給大家在學習這些3d工具庫的時候提供一些幫助。

參考資料

GPU與渲染管線:如何用WebGL繪製最簡單的幾何圖形?

歡迎關注凹凸實驗室部落格:aotu.io

或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章。