iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 4
1
Modern Web

WebGL 與 Three.js 初探系列 第 4

[Day4] WebGL 修羅道(1) - 用 100行程式碼畫...一個點

WebGL 修羅道(1) - 用 100行程式碼畫點

前一個章節提到的 Shader,就是負責繪製每個像素的顏色跟位置。跟 canvas 本身的原理很像,我們先決定 context 的狀態(fillColor, lineTo, stroke) 等等,最後在調用 storke fill 等語法來繪製(在 webGL 中是 drawArrays)。shader 使用 GSGL 撰寫,一個 C/C++ like 的程式語言。webGL 的章節當中,我們會常常看到他。

Shader 如何接收資料

一般來說,我們有四種方式可以讓 shader 接收資料:

  1. buffers 跟 attributes

    我們可以將資料放進去 buffer 當中,當 GPU 需要使用時就從 buffer 取用,通常會存放像是頂點位置、紋理、顏色等等的資料;attribute 則代表我們如何從緩衝區中獲得資料

  2. uniforms 則是執行 program 之前,能夠設定的全域變數。

  3. varyings 是 vertex shader 將數據傳給 fragment shader 的一種方法,根據渲染內容不同,執行 fragment shader 的時候,會根據 vertex shader 提供的值進行計算

如果看完上面的解釋,還是覺得非常迷惘,別擔心,之後的幾天我們會常常遇到這幾種資料型態,也會慢慢解釋他們的用途。

歡迎來到修羅道,以下我們會用僅僅 100 行程式碼畫出一個...點(?

GSGL 概覽

識別字

  1. 變數名稱只能是英文字母、數字、底線
  2. 不能以數字開頭
  3. gl_ 開頭的變數為保留字

型別

  • attribute, uniform, varing
  • void
  • float, int, bool
  • vec2, vec3, vec4 (浮點向量)
  • ivec2, ivec3, ivec4 (整數向量)
  • bvec2, bvec3, bvec4
  • mat2, mat3, mat4 (矩陣)
  • sampler2D samlerCube (採樣器)

關於 GSGL 概覽,我們只介紹到這邊,GSGL 裡頭其實有蠻豐富的內建函數,我們之後遇到範例時再一起講解。

shader

終於可以開始寫 code 了!在這之前,先讓我們設定一下 webGL 環境。

我會將代碼放在 codepen 上,方便各位 fork 回來自己動手玩玩看!

因為自己也算是在摸索中,難免會有解釋錯誤的地方,如果各位有發現問題,歡迎在底下留言,我會趕快修正!

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>My first shader</title>
  <script id="shader-fs" type="x-shader/x-fragment">
    // shader program
    // gl_FragColor 是個 fragment shader 的全域變數
    void main(void) {
      gl_FragColor = vec4(0.0,0.0,0.0,1.0);
    }
  </script>
  
  <script id="shader-vs" type="x-shader/x-vertex">
    // 我們在這裡撰寫 GSGL
    // vec4 代表一個向量,接收四個參數(x,y,z,w)
    // 0,0,0 在 3d 座標中代表中心
    
    void main(void) {
      gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
      gl_PointSize = 10.0;
    }
  </script>
</head>
<body>
  <canvas width="600" height="600" id="canvas">
</body>
<script src="main.js"></script>
</html>

首先看到上面兩個 script tag,待會我們就要在裡頭撰寫我們的 vertex shaderfragment shader,再來為了使用 webGL,我們使用 canvas,待會我們就會取用 webgl context,這樣一來就能夠操作 webGL API 了!

script 的 type 當中,其實並沒有特別規範名稱,這邊的 x-shader 只是慣例上的使用方式,只要能夠讓 webGL 讀取到內容就好。不過如果是使用 IDE 的話,寫入 type 的好處是 IDE 會幫你做高亮處理。

撰寫 shader

<script id="shader-fs" type="x-shader/x-fragment">
  // shader program
  void main(void) {
    gl_FragColor = vec4(0.0,0.0,0.0,1.0);
  }
</script>

<script id="shader-vs" type="x-shader/x-vertex">
  // 我們在這裡撰寫 GSGL
  // vec4 代表一個向量,接收四個參數(x,y,z,w)
  // 0,0,0 在 3d 座標中代表中心

  void main(void) {
    gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
    gl_PointSize = 10.0;
  }
</script>

在 fragment shader 當中,我們做了一些事情:

  • gl_FragColor = vec4(0.0,0.0,0.0,1.0)

    宣告我們這個 fragment 的顏色為 0,0,0,第四個參數我們先暫時不理他。注意這邊規定的型別必須為浮點數,所以我們需要加入小數點。gl_FragColor 是一個全域變數,每一個 fragment 會依照 gl_FragColor 來繪圖。

  • gl_Position = vec4(0.0,0.0,0.0,1.0);

    宣告我們的頂點在 x=0, y=0, z=0 這個位置

  • gl_PointSize = 10.0 代表我們點的大小為 10

注意到 gl_ 開頭的變數都是保留字。

webGL context

// 1. 我們先用 canvas.getContext('webgl') 來獲得 webGL context
// 2. gl.viewport 定義了視窗大小
// 3. gl.clearColor 將視窗顏色清除為 (1(R),1(G),1(B),1(A))
// 注意 1 代表 255
function initGL() {
   var canvas = document.querySelector('#canvas');
   var gl = canvas.getContext('webgl');
  
   gl.viewport(0,0, canvas.width, canvas.height);
   gl.clearColor(1,1,1,1);
  
   return gl;
}

function createShaders(gl, type) {
  var shaderScript = '';
  var shader;

  switch(type) {
    case 'fragment':
      shaderScript = document.querySelector('#shader-fs').textContent;
      shader = gl.createShader(gl.FRAGMENT_SHADER);
      break;
    case 'vertex':
      shaderScript = document.querySelector('#shader-vs').textContent;
      shader = gl.createShader(gl.VERTEX_SHADER);
      break;
  }

  gl.shaderSource(shader, shaderScript);
  gl.compileShader(shader);
  
  return shader;
}

function initShaders(gl) {
  var vertexShader = createShaders(gl, 'vertex');
  var fragmentShader = createShaders(gl, 'fragment');
  
  var shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  
  var isSuccessful = gl.getProgramParameter(program, gl.LINK_STATUS);
  if(!isSuccessful) {
    throw Error();
  }
  gl.useProgram(shaderProgram);
  
}

// 我們畫圖的邏輯一律在 draw function 裡頭
function draw(gl) {
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.POINTS, 0,1);
}


function main() {
  var gl = initGL();
  initShaders(gl)
  draw(gl);
}

main();

比較重要的幾個部分在 createShader 跟 initShaders 當中。我們使用 gl.createShader(gl.FRAGMENT_SHADER)gl.VERTEX_SHADER 來建立 shader 並宣告型別。同時用 gl.shaderSource(shader, shaderScript)gl.compileShader 讓 shader 能夠被 webGL 使用。

initShaders 目的則是連結 shader 並且加入 program 裡頭,步驟是 gl.attachShader(fragment 跟 vertex shader 都要加入) > gl.linkProgram > gl.useProgram

draw 函式裡頭,我們調用了 gl.drawArrays(mode, first, count) 來畫出圖形。

費盡千辛萬苦,我們終於將一個正方形給畫出來了,嚴格來說也不算正方形,只是一個點而已。從以上的程式當中,我們知道,要讓 webGL 正確繪圖,我們需要 fragment shader 及 vertex shader,同時我們需要用 webGL 與 shader 做連結,最後再調用 drawArraysdrawElements 畫出圖形。

光是畫一個簡單的圖形,我們的程式碼就已經一長串了,很難想像如果開發更複雜的圖形,我們要寫的程式碼一定會用指數型增長吧!

不過了解他們的原理還是有好處的,所以在繼續之前,我想請各位讀者到 webGL API 瞧瞧,並且熟悉一下今天出現的 API。你也可以修改 script 中的 GSGL 程式碼,看看會發生什麼事。確定熟悉了基本操作之後,我們再往下前進!

這四天有關於 webGL 的章節,會是本系列文當中最艱難的幾篇,之後就會輕鬆許多了XD,還撐得下去的讀者們加油!

Debug 小技巧

  1. 如果發現程式碼不會動,第一件事情就是先打開 console,看看有沒有噴錯,依照錯誤訊息指示來除錯吧!
  2. 我們可以透過 gl.LINK_STATUS gl.COMPILE_STATUS 等全域變數來看看目前的狀態是否正確,需要更多資訊,可以參考這一頁 Constant

上一篇
[Day3] 影像、GPU、webGL
下一篇
[Day5] WebGL 修羅道(2) - 資料傳遞
系列文
WebGL 與 Three.js 初探30

1 則留言

0
Robby
iT邦新手 5 級 ‧ 2017-08-02 22:01:35

Hi 目前正在學習 three.js ,很高興有您的文章貢獻

目前發現文中程式碼部分:

var isSuccessful = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!isSuccessful) {
    throw Error();
}

「program」好像是錯誤的呢,會出現錯誤:

program is not defined

更改為:shaderProgram 就沒問題了

var isSuccessful = gl.getProgramParameter(shaderProgram, gl.LINK_STATUS);
if (!isSuccessful) {
    throw Error();
}
LeoYeh iT邦新手 5 級 ‧ 2018-01-23 11:16:04 檢舉

/images/emoticon/emoticon12.gif

我要留言

立即登入留言