iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 8
0
Modern Web

WebGL 與 Three.js 初探系列 第 8

[Day8] webGL 修羅道 - 3D 紋理貼圖

Sampler

在 GSGL 中,有一個特殊的類別是 sampler,負責接收由 Javascript 中傳來的 texture。我們可以將圖像的數據放到一個特殊的紋理當中。這邊要特別注意的是,我們的紋理被映射之後一樣是矩形,並且座標系統只會在 -1 ~ 1 之間。

有了採樣器收集的紋理數據之後,我們就可以做計算,並且將值給 gl_FragColor。以下是 GSGL 中的 texture API:

  • vec4 texture2D(sampler2D, vec2)
  • vec4 texture2DProj(sampler2D, vec3)

以下用來綁定到採樣器的立方圖紋理

  • vec4 textureCube(samplerCube, vec3)
  • vec4 textureCubeLod(samplerCube, vec3)

demo-texture-animation

我們在 handleTextureLoaded 的函數當中加入以下的程式碼,跟建立 buffer 的時候很類似,我們來看一下:

function handleTextureLoaded(gl, texture,image) {
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); // 紋理座標垂直翻轉
  
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
  
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  
}

想要使用紋理,我們一樣需要先綁定 texture,texture 物件通常則會由 gl.createTexture 傳送過來。

  • gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true) 由於 webGL 的座標系統和 canvas 座標系統不一樣,如果直接映射過去會發現圖片是倒過來的,我們使用此函式讓圖片轉正
  • gl.texImage2D 我們透過這個函數將圖片傳入到顯示卡的紋理空間(gl.TEXTURE_2D) 。
  • gl.texPatameteri 則是設定一堆不同的選項,來告訴 webGL,像素的存儲方式
    • gl.TEXTURE_MAG_FILETER gl.LINEAR 放大過濾採用線性過濾
    • gl.TEXTURE_MIN_FILETER gl.LINEAR 縮小過濾採用線性過濾
    • gl.TEXTURE_WRAP_S gl.CLAMP_TO_EDGE 水平使用限制邊緣的方式設定紋理包裝
    • gl.TEXTURE_WRAP_T gl.CLAMP_TO_EDGE 垂直使用限制邊緣的方式設定紋理包裝

再來就是載入圖片了:

function initImage(gl) {
  var cubeTexture = gl.createTexture();
  var image = new Image();
  image.crossOrigin = '';
  
  // 找不到圖片素材,用自己的大頭貼當作範例
  image.src = 'https://scontent-tpe1-1.xx.fbcdn.net/v/t31.0-8/14305435_1044232042351132_2171601430048718173_o.jpg?oh=e5f4fd1ca577678383c574f2287a6d05&oe=58EACC82';
  
  image.onload = function() {
    handleTextureLoaded(gl, cubeTexture, image);
  }
}

我們設定了一個 onload 函數來確保圖片載入後才開始進行 texture 綁定的工作。

到這邊,基本上我們已經順利綁定了紋理了!現在要來修改我們的 GSGL 程式:

<script id="shader-vs" type="x-shader/x-vertex">
  attribute vec4 coords;
  attribute vec2 textureCoords;

  uniform mat4 u_transformMatrix;
  uniform mat4 u_perspectiveMatrix;
  attribute vec4 colors;
  varying vec4 varyingColors;
  
  varying vec2 v_textureCoords;

  void main(void) {
    v_textureCoords = textureCoords;
    gl_Position = u_perspectiveMatrix * u_transformMatrix  * coords;
    varyingColors = colors;
  }
  </script>
  
  <script id="shader-fs" type="x-shader/x-fragment">
  precision mediump float;
  uniform vec4 color;
  varying vec4 varyingColors;
  
  varying vec2 v_textureCoords;
  uniform sampler2D sampler;
  void main(void) {
    gl_FragColor = texture2D(sampler, v_textureCoords);
  }
  </script>

首先,先加入 attribute textureCoords 讓 Javascript 傳值進來,再透過 varying v_textureCoords 將紋理座標傳給片段著色器。最後透過 texture2D 計算出目前頂點的顏色。

加入 createTexture 函數

function createTexture(gl, program) {
  var textureCoords = [
    0.0, 0.0,
    1.0, 0.0,
    1.0, 1.0,
    0.0, 1.0,
    
    1.0, 0.0,
    1.0, 1.0,
    0.0, 1.0,
    0.0, 0.0,
    
    0.0, 1.0,
    0.0, 0.0,
    1.0, 0.0,
    1.0, 1.0,
    
    1.0, 1.0,
    0.0, 1.0,
    0.0, 0.0,
    1.0, 0.0,
    
    1.0, 0.0,
    1.0, 1.0,
    0.0, 1.0,
    1.0, 0.0,
    
    0.0, 0.0,
    1.0, 0.0,
    1.0, 1.0,
    0.0, 1.0,
    
    1.0, 0.0,
    1.0, 1.0,
    0.0, 0.0,
    1.0, 0.0,
    
    0.0, 0.0,
    1.0, 0.0,
    1.0, 1.0,
    0.0, 1.0,
  ];
  
  var buffer = gl.createBuffer();
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(textureCoords), gl.STATIC_DRAW);
  
  var textureCoords = gl.getAttribLocation(program, 'textureCoords');
  gl.vertexAttribPointer(textureCoords, 2, gl.FLOAT, false, 0,0);
  gl.enableVertexAttribArray(textureCoords);
  
  return buffer;
}

我們建立了紋理座標的頂點,並且透過 buffer 將頂點陣列傳入。跟之前傳入 buffer 的方式相同,大家可以參考看看。

最後,我們再將紋理座標傳入 GSGL 中。就可以看到成果了!不過很顯然地,我的文理座標中似乎有些地方沒有設定好,導致紋理的映射方式有點不同。這邊就交給各位讀者去調整參數了。

裡頭的程式碼有點複雜,大家可能需要時常翻翻 MDN 的文件 來確認每個 API 的用途。

更多可能性

舉例來說,我們可以在正方體的每一個面當中都放入不同的圖片,達到更精緻的展示效果,也可以透過混和的方式來調整圖片,也能夠在 GSGL 中撰寫更多的處理函數來修改圖片中的某個顏色。全部都是靠各位的創意來表現了!

影片也能夠載入到 canvas 當中,這代表著我們也能夠將影片的內容經過 webGL 的處理後再送到網頁當中!聽起來很令人興奮吧!

結論

今天是 webGL 修羅道的最後一天了,希望各位學得愉快。為了讓各位更了解 3D 背後的原理,我認為基本的 GSGL 理解是必要的。大家也一定發現到,裡頭有很多不必要的邏輯、初始化、綁定 buffer 等等,都不是我們應該要去煩惱的事情。

市面上當然也不乏一些優秀的 webGL 函式庫,其中最有名的就是 Three.js 了。明天開始,就是 three.js 系列囉!

雖然實際上的遊戲開發、建模上,我們絕對不會在 js 自己手動程式碼建模,雖然理論上做得到,但人生苦短,還是不要這樣折磨自己。不過其實用簡單的幾何形狀,還是可以打造出很可愛的圖形哦!

three.js 裡頭幫我們設置了 3D 開發中常見的幾種物件:

  • 光源
  • 幾何形狀
  • 簡單的模型
  • 材質
  • camera
  • loader(可以從檔案中讀取建模的數據)

明天我會介紹 webGL 中的一些實際應用來當做 webGL 修羅道的結尾。其實自己對 webGL 的詳細運作仍然有點不熟悉,要在 10 天內摸透全部的原理實在是太累人了。不過希望透過這些 demo 與教學,可以讓各位對 webGL 不再那麼陌生,如果遇到效能瓶頸、或是需要更精緻的特效時,我們能夠透過自己撰寫簡單的 GSGL 函式來讓動畫或特效更加逼真。

debug 小技巧
  • google extension 有 shader editor 跟 GL inspector 可以使用,但不知道有沒有可以監控或對 GSGL 本身做 debug 的程式呢?

上一篇
[Day7] webGL修羅道(4) - 3D 與動畫
下一篇
[Day9] webGL 修羅道 - 特效實戰及總結
系列文
WebGL 與 Three.js 初探30

尚未有邦友留言

立即登入留言