前端也要懂圖形化: 淺談 WebGL 技術

WebGL 概述

WebGL 是一項結合了 HTML5 和 JavaScript,用來在網頁上繪製和渲染複雜三維圖形的技術。WebGL 通過 JavaScript 操作 OpenGL 接口的標準,把三維空間圖像顯示在二維的屏幕上。

WebGL 與 OpenGL

OpenGL 本身是一套規範,不是 API,通過 OpenGL 來統一各個顯卡廠家實現操作圖形、圖像的實現標準。WebGL 的技術規範繼承自 OpenGL ES,從 2.0 版本開始,OpenGL 支持可編程着色器方法,這個支持可以讓我們通過着色器語言編寫着色器程序,代表我們可以精確控制每個像素的位置和顏色。在 OpenGL2.0 規範中,GPU 可以執行着色器程序,根據着色器程序生成像素數據,最終顯示在屏幕上。

WebGL 程序的結構

相對於傳統網頁,支持 WebGL 的瀏覽器底層接入了 OpenGL/OpenGL ES 標準,WebGL 通過實現標準支持着色器語言編程語言 GLSL ES,在我們實際開發過程中,GLSL ES 通常是以字符串的形式存在 JavaScript 中,我們可以通過 JavaScript 修改 GLSL ES 字符串來改變着色器程序。

着色器

着色器是 WebGL 依賴的實現圖像渲染的一種繪圖機制。WebGL 在 GPU 中運行,因此需要使用能夠在 GPU 上運行的代碼,這樣的代碼需要提供成對的方法,他們分別是頂點着色器片元着色器

頂點着色器

頂點着色器的作用是計算頂點的位置,根據計算出的一系列頂點位置,WebGL 可以對點, 線和三角形在內的一些圖元進行光柵化處理。WebGL 中顯示的物體由一系列頂點組成,每個頂點都有位置和顏色等信息,在默認的情況下,所有像素的顏色都由線性插值計算得來,自動形成平滑漸變。

下面是頂點着色器的示例代碼,頂點着色器通過a_Positiona_PointSize分別接收並設置頂點的位置和大小,通過a_Color從程序獲取顏色並通過v_Color傳遞給片元着色器。其中,gl_Positiongl_PointSize 是着色器的內置變量,分別代表頂點的位置和大小,因此這段代碼的作用是設置頂點的位置和大小,同時接收程序傳入的顏色a_Color並把它傳遞給片元着色器。

在着色器內,一般命名以gl_開頭的變量是着色器的內置變量,除此之外webgl__webgl還是着色器保留字,自定義變量不能以webgl__webgl開頭。變量聲明一般包含 < 存儲限定符 >< 數據_類型_ >< _變量名稱_ >,以attribute vec4 a_Position爲例,attribute表示存儲限定符,vec是數據類型,a_Position爲變量名稱。

const vs_source = `

    attribute vec4 a_Position;

    attribute float a_PointSize;

    attribute vec4 a_Color;

    varying vec4 v_Color;

    void main() {

        gl_Position = a_Position;

        gl_PointSize = a_PointSize;

        v_Color = a_Color;

    }

`;

片元着色器

片元着色器的作用是計算出當前繪製圖元中每個像素的顏色值,逐片元控制片元的顏色和紋理等渲染。關於片元,片元包含顏色、深度和紋理等信息,片元相對像素多出許多信息,從直觀表現上看兩者都是像素點。關於圖元,圖元是指 WebGL 中可以直接繪製 7 種基本圖形,它們分別是:

下面是片元着色器的示例代碼,首先設置了float爲中等精度,然後通過v_Color接收來自頂點着色器的顏色並將其設置給內置變量 gl_FragColor,其中通過內置變量 gl_FragColor 來確定頂點像素顏色。

關於精度可以詳見 WebGL 着色器精度設置 [1]。

const fs_source = `

    precision mediump float;

    varying vec4 v_Color;

    void main() {

        gl_FragColor = v_Color;

    }

`;

存儲限定符

在上面例子中,我們在聲明變量時,除了指定變量類型外,還使用了存儲限定符。在 GLSL 中,應該根據變量用途使用存儲限定符修飾。一般常用到三種存儲限定符:

attribute 屬性

attribute 只能用於頂點着色器,用來存儲頂點着色器中每個頂點的輸入,包括頂點位置座標、紋理座標和顏色等信息。

通常情況下我們會使用緩衝,緩衝是程序發送給 GPU 的數據,attribute 用來從緩衝中獲取所需數據,並將它提供給頂點着色器。程序可以指定每次頂點着色器運行時讀取緩衝的規則。

使用緩衝設置頂點信息

生成緩衝代碼

// 創建緩衝對象

const vertexBuffer = gl.createBuffer();

將數據寫入緩衝

// 緩衝數據

const vertices = new Float32Array([

 -0.5, 0.5, 10.0, 1.0, 0.0, 0.0, 1.0,

 -0.5, -0.5, 20.0, 0.0, 1.0, 0.0, 1.0,

 0.5, 0.5, 30.0, 0.0, 0.0, 1.0, 1.0

]);

// 綁定緩衝對象

gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

// 將緩衝數據填充緩衝對象

gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

設置緩衝讀取規則和啓用緩衝對象

 // 調用頂點緩衝,將緩衝數據中一組7個數據中的前2個數據傳給a_Position

 gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, SIZE * 7, 0);

 // 調用頂點緩衝,將緩衝數據中一組7個數據中的第3(偏移2個數據取1一個)個數據傳給a_PointSize

 gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, SIZE * 7, SIZE * 2);

 // 調用頂點緩衝,將緩衝數據中一組7個數據中的第4-7(偏移3個數據取4個)個數據傳給a_Color

 gl.vertexAttribPointer(a_Color, 4, gl.FLOAT, false, SIZE * 7, SIZE * 3);

 // 激活a_Position使用緩衝數組,下同

 gl.enableVertexAttribArray(a_Position);

 gl.enableVertexAttribArray(a_PointSize);

 gl.enableVertexAttribArray(a_Color);

uniform 全局變量

uniform 可以存在頂點着色器和片元着色器,用來存儲圖元處理過程中保持不變的值,例如顏色。值得一提的是,頂點着色器和片元着色器共享了 uniform 變量的命名空間,如果在頂點着色器和片段着色器中都聲明瞭同名 uniform 變量,二者聲明的類型和着色器的精度必須一致。

varying 可變量

varying 一般同時存在頂點着色器和片元着色器中,它的作用是從頂點着色器向片元着色器傳輸數據,在圖元裝配後,WegGL 會對圖元光柵化,在光柵化過程中,varying 聲明的變量的值會進行內插,使 varying 變量的值線性(默認)變化。

一個簡單的例子:彩色三角形

在這段代碼中,指定三角形的三個點,分別位於畫布的左上角、左下角和右上角,他們的顏色分別爲紅色、綠色和藍色,大家可以運行示例代碼查看效果。

// 頂點着色器

const vs_source = `

  attribute vec4 a_Position;

  attribute float a_PointSize;

  attribute vec4 a_Color;

  varying vec4 v_Color;

  void main() {

    gl_Position = a_Position;

    gl_PointSize = a_PointSize;

    v_Color = a_Color;

  }

`;

 

// 片元着色器

const fs_source = `

  precision mediump float;

  varying vec4 v_Color;

  void main() {

    gl_FragColor = v_Color;

  }

`;

 const canvas = document.getElementById('app');

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

 

 function initShader() {

   // 創建shader

   const vs_shader = gl.createShader(gl.VERTEX_SHADER);

   gl.shaderSource(vs_shader, vs_source);

   gl.compileShader(vs_shader);

   if (!gl.getShaderParameter(vs_shader, gl.COMPILE_STATUS)) {

     const error = gl.getShaderInfoLog(vs_shader);

     console.log('Failed to compile vs_shader:' + error);

     gl.deleteShader(vs_shader);

     return;

   }

   const fs_shader = gl.createShader(gl.FRAGMENT_SHADER);

   gl.shaderSource(fs_shader, fs_source);

   gl.compileShader(fs_shader);

   if (!gl.getShaderParameter(fs_shader, gl.COMPILE_STATUS)) {

     const error = gl.getShaderInfoLog(fs_shader);

     console.log('Failed to compile fs_shader:' + error);

     gl.deleteShader(fs_shader);

     return;

   }

   // 創建program

   const program = gl.createProgram();

   gl.attachShader(program, vs_shader);

   gl.attachShader(program, fs_shader);

   gl.linkProgram(program);

   if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {

     const error = gl.getProgramInfoLog(program);

     console.log('無法鏈接程序對象:' + error);

     gl.deleteProgram(program);

     gl.deleteShader(fs_shader);

     gl.deleteShader(vs_shader);

     return;

   }

   gl.useProgram(program);

   gl.program = program;

   // 獲取着色器變量位置和賦值

   const a_Position = gl.getAttribLocation(gl.program, 'a_Position');

   if (a_Position < 0) {

     console.log('Failed to get the storage location of a_Position');

     return;

   }

   const a_Color = gl.getAttribLocation(gl.program, 'a_Color');

   if (a_Color < 0) {

     console.log('Failed to get the storage location of a_Color');

     return;

   }

   // 使用緩衝區表示多個值

  const vertices = new Float32Array([

    -0.5, 0.5, 1.0, 0.0, 0.0, 1.0,

    -0.5, -0.5, 0.0, 1.0, 0.0, 1.0,

    0.5, 0.5, 0.0, 0.0, 1.0, 1.0

  ])

  const SIZE = vertices.BYTES_PER_ELEMENT;

   const vertexBuffer = gl.createBuffer();

   gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

   gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

   gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, SIZE * 6, 0);

   gl.vertexAttribPointer(a_Color, 4, gl.FLOAT, false, SIZE * 6, SIZE * 2);

   gl.enableVertexAttribArray(a_Position);

   gl.enableVertexAttribArray(a_Color);

 }

 initShader();

 

 gl.clearColor(0.0, 0.0, 0.0, 1.0);

 gl.clear(gl.COLOR_BUFFER_BIT);

 

 gl.drawArrays(gl.TRIANGLES, 0, 3);

我們分別只設置了三個點的顏色,但是渲染出來的圖形卻是彩色的三角形,造成這樣的結果的原因是因爲v_Color在傳遞的過程中經過了內插。以左上角點和右上角點對比,代表rgb顏色三個值線性向目標值變化,內插過程將顏色分配給光柵化後每個片元(像素)顏色,造成了顏色漸變。

WebGL 渲染過程

以上面例子爲例,三角形的有 3 個點,需要執行 3 次頂點着色器,把 3 個點位置存儲在圖形裝配區域。頂點着色器執行完畢後,三個點的座標都已經處在圖形裝配區,開始裝配圖形,由於我們設置的是gl.TRIANGLES,圖形裝配出一個三角形。隨後會進行光柵化,將裝配好的圖像轉化成片元組合,也就是像素點,varying 變量的插值也在這一過程中進行。光柵化後,程序調用片元着色器,假定光柵化後有 10 個片元,那麼片元着色器將執行 10 次,每次調用處理一個片元,片元着色器計算該片元的顏色並寫入顏色緩衝區,當最後一個片元被處理完成,瀏覽器就會顯示出最終的結果。

圖片渲染

通過頂點信息繪製像素點功能很強大,但對於複雜圖形不夠好用,通常我們需要渲染圖片,這樣就需要紋理映射了,這就是爲什麼在 WebGL 中,我們更傾向把圖片描述爲紋理的原因。

紋理映射

紋理映射原理很簡單,就是將一張圖片映射到一個幾何圖形的表面。由於在 WebGL 中,WebGL 能直接繪製的圖形只有點、線和三角形,因此在紋理映射中,圖片被映射到由兩個三角形組成的矩形。

紋理映射具體步驟:

  1. 準備映射的紋理圖圖像,紋理圖像應該滿足 2 的冪次方

圖片分辨率爲非 2 的冪次方(104 * 104),圖片不能被渲染,並提示圖片分辨率可能不是 2 的冪次方

圖片分辨率爲 2 的冪次方(256 * 256),正常顯示

  1. 爲幾何圖形配置映射方式,頂點座標和紋理座標對應,需要注意,構建順序與新增頂點的奇偶性相關。(假設 v0 代表第一個頂點

下面頂點組成的三角形分別爲:T1 = [v0, v1, v2],T2 = [v2, v1, v3]

  // 頂點座標、紋理座標,繪製順序:左上->左下->右上->右下

  const vertices = new Float32Array([

    -0.5, 0.5, 0.0, 1.0,   // v0

    -0.5, -0.5, 0.0, 0.0,  // v1

    0.5, 0.5, 1.0, 1.0,    // v2

    0.5, -0.5, 1.0, 0.0,   // v3

  ]);

當把第一個座標和第二個座標互換時,繪製

  // 頂點座標、紋理座標,繪製順序:左下->左上->右上->右下

  const vertices = new Float32Array([

    -0.5, -0.5, 0.0, 0.0,  // v0

    -0.5, 0.5, 0.0, 1.0,   // v1

    0.5, 0.5, 1.0, 1.0,    // v2

    0.5, -0.5, 1.0, 0.0,   // v3

  ]);

  1. 加載紋理圖像,對其進行一些配置,以在 WebGL 中使用,分別有如下操作

  2. 創建紋理對象

  3. 圖片 Y 軸反轉,因爲圖片座標系 y 軸垂直向下,而紋理座標系(st 座標系)t 軸垂直向上,在我們的例子中,頂點座標和紋理座標是相反的,所以在紋理存儲像素中需要對 y 軸翻轉

假如未未進行 y 軸反轉

  1. 激活指定紋理單元

  2. 綁定紋理對象到紋理單元

  3. 設置紋理過濾,紋理過濾是指,當繪製範圍(頂點座標組成的矩形區域)與紋理本身大小不匹配時,如何獲取紋理顏色。需要注意的是,設置錯誤的過濾可能會導致渲染失敗

  4. gl.TEXTURE_MAP_FILTER放大方法,紋理撐滿至繪製範圍

  5. gl.TEXTURE_MIN_FILTER縮小方法,紋理縮小至繪製範圍

  6. gl.TEXTURE_WRAP_S水平填充,紋理大小不變,左右填充

  7. gl.TEXTURE_WRAP_T垂直填充,紋理大小不變,上下填充

過濾設置錯誤,假如畫布大小 400 * 400,頂點座標範圍 - 0.5~0.5 對應大小 200 * 200,圖片大小 256 * 256,紋理比繪製範圍大,但是過濾僅設置了gl.TEXTURE_MAG_FILTER,獲取紋理顏色失敗,控制檯又輸出了熟悉的錯誤:圖片不能被渲染,是否未設置合適紋理過濾

  1. 將圖像綁定到紋理

  2. 將紋理單元傳遞給着色器中的採樣器

  3. 在片元着色器中將相應的像素從紋理中抽取出來,並將像素的顏色賦給片元

以上操作相關的代碼片段如下

// 頂點着色器

 const vs_source = `

   attribute vec4 a_Position;

   attribute vec2 a_TexCoord;

   varying vec2 v_TexCoord;

   void main() {

    gl_Position = a_Position;

    v_TexCoord = a_TexCoord;

   }

`;

 

 // 片元着色器

const fs_source = `

  precision mediump float;

  uniform sampler2D u_Sampler;

  varying vec2 v_TexCoord;

  void main() {

    gl_FragColor = texture2D(u_Sampler, v_TexCoord);

  }

`;



...



function initShader() {

  ...

  

  // 頂點座標和紋理座標

  const vertices = new Float32Array([

    -0.5, 0.5, 0.0, 1.0,

    -0.5, -0.5, 0.0, 0.0,

    0.5, 0.5, 1.0, 1.0,

    0.5, -0.5, 1.0, 0.0,

  ]);

   

  ...

}



const texture = gl.createTexture();

const u_Sample = gl.getUniformLocation(gl.program, 'u_Sample');

 

const img = new Image();

img.onload = function () {

  // 加載紋理圖像,對其進行一些配置,以在WebGL中使用

  // 紋理像素存儲y軸反轉

  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);

  // 激活紋理單元0

  gl.activeTexture(gl.TEXTURE0);

  // 綁定紋理

  gl.bindTexture(gl.TEXTURE_2D, texture);

  // 設置過濾的過濾方式,這裏僅設置了gl.TEXTURE_MIN_FILTER,也可以設置多種過濾

  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

  // 生成紋理

  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);

  // 將紋理單元0傳遞給採樣器

  gl.uniform1i(u_Sample, 0);

 

  gl.clearColor(0.0, 1.0, 0.0, 1.0);

  gl.clear(gl.COLOR_BUFFER_BIT);

  

  // 繪圖順序跟點有關

  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

}

img.src = 'power-of-2-image';

參考資料

[1]

WebGL 着色器精度設置: https://blog.csdn.net/u014291990/article/details/103173077

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/k8MbHKyzSRlBWS53Ymw3uQ