1. 程式人生 > >WebGL---3.繪製紋理

WebGL---3.繪製紋理

一、例項程式碼

<html>
	<canvas id='c' width='480' height='320'></canvas>
	<script type="x-shader/x-vertex" id="vertex-shader">
	  	attribute vec4 a_Position;
	  	attribute vec2 a_TextCoord;
	  	varying vec2 v_TexCoord;

	  	void main(){
	  		gl_Position = a_Position;
	  		v_TexCoord = a_TextCoord;
	  	}
	</script>
	<script type="x-shader/x-fragment" id="fragment-shader">
		//需要宣告浮點數精度,否則報錯No precision specified for (float)
	  	precision mediump float;
	  	
	  	// 紋理樣本,被用來進行取樣
	  	uniform sampler2D u_Sampler;
	  	varying vec2 v_TexCoord;

	  	void main(){
	  		//設定顏色,從紋理中取樣
	  		gl_FragColor = texture2D(u_Sampler,v_TexCoord);
	  	}
	</script>
	<script src="WebGLUtils.js"> </script>

	<script>
		var gl = createGLContext('c')

		//建立shader program
		var program = createProgramFromElementId(gl,'vertex-shader','fragment-shader');
		gl.useProgram(program);


		//-----1.將座標資料複製到GPU緩衝區-----//
		var vertices = new Float32Array([
			-1.0, 1.0,   0.0, 1.0,//左上角
			-1.0, -1.0,  0.0, 0.0,//左下角
			1.0,1.0,	 1.0,1.0,//右上角
			0.5, -0.5,   1.0, 0.0//右下角
		])
		
		// 建立緩衝區,將資料從CPU複製到GPU
		var vertexBuffer = gl.createBuffer()
		// 繫結緩衝區到目標
		gl.bindBuffer(gl.ARRAY_BUFFER,vertexBuffer);
		// 向緩衝區寫入資料
		gl.bufferData(gl.ARRAY_BUFFER,vertices,gl.STATIC_DRAW);


		//-----2.向著色器傳遞座標資料-----//
		var FSIZE = vertices.BYTES_PER_ELEMENT;

		// 1.傳遞頂點座標
		var a_Position = gl.getAttribLocation(program,"a_Position");
		gl.enableVertexAttribArray(a_Position);
		gl.vertexAttribPointer(a_Position,2,gl.FLOAT,false,4*FSIZE,0);

		// 2.傳遞紋理座標
		var a_TextCoord = gl.getAttribLocation(program,"a_TextCoord");
		gl.enableVertexAttribArray(a_TextCoord);
		gl.vertexAttribPointer(a_TextCoord, 2, gl.FLOAT, false, FSIZE*4, FSIZE*2);


		//-----向著色器傳遞紋理資料-----//
		// 瀏覽器載入圖片
		var image = new Image();
		image.src = "ttt.png";
		image.onload = function(){
			//-----3.將影象資料複製到GPU緩衝區的紋理中-----//
			var texture = gl.createTexture();
			gl.activeTexture(gl.TEXTURE0);
	        gl.bindTexture(gl.TEXTURE_2D, texture);
	        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
	        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
	        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);


			//-----4.向著色器傳遞紋理資料-----//
			var u_Sampler = gl.getUniformLocation(program, 'u_Sampler');
	        gl.uniform1i(u_Sampler, 0);


	        // 5.繪製
	        gl.clear(gl.COLOR_BUFFER_BIT);
	        gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

		}

	</script>
</html>

二、程式碼分解

1. 編寫著色器

我們使用了自定義的標籤來放置vertex shader和fragment shader.

因為html5的標準規定,如果script標籤裡面的type不是text/javascript或者src=”…”之類的話,瀏覽器會把這段script標籤裡面的內容解析成文字。

因此,我們自定義兩種type分別用來存放vertex shader和fragment shader:

2. 載入圖片

我們需要載入一個影象,建立一個紋理然後將影象資料從CPU複製到GPU紋理中。 由於瀏覽器中的圖片是非同步載入的,所以我們需要重新組織一下程式碼, 等待紋理載入,一旦載入完成就開始繪製。

var image = new Image();
image.src = "test.png";
image.onload = function() {
	render(image);
}

3. 複製影象資料到紋理

// 建立紋理
var texture = gl.createTexture();

// 啟用紋理單元
gl.activeTexture(gl.TEXTURE0);

// 繫結紋理
gl.bindTexture(gl.TEXTURE_2D, texture);

// 影象Y軸翻轉
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)

// 複製影象資料到紋理
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

第一個引數指定了紋理目標(Target)。TEXTURE_2D代表二維紋理,TEXTURE_CUBE_MAP 立方體紋理

第二個引數為紋理指定多級漸遠紋理的級別,如果你希望單獨手動設定每個多級漸遠紋理的級別的話。這裡我們填0,也就是基本級別。

第三個引數影象的內部格式 ,有:gl.RGB(紅綠藍)、gl.RGBA(紅綠藍透明度)、gl.ALPHA(0.0,0.0,0.0,透明度)等

第四個引數是紋理的資料格式,必須與第三個引數相同。

第五個引數紋理資料格式

UNSIGNED_BYTE:表示無符號整形,每一個顏色分量佔據1位元組
UNSIGNED_SHORT_5_6_5:表示RGB,每一個分量分別佔據佔據5,6,5位元
UNSIGNED_SHORT_4_4_4_4:表示RGBA,每一個分量分別佔據佔據4,4,4,4位元
UNSIGNED_SHORT_5_5_5_1:表示RGBA,每一個分量分別佔據佔據5位元,A分量佔據1位元

第六個引數是紋理影象的image物件

4. 紋理座標

紋理座標在x和y軸上,範圍為0到1之間(注意我們使用的是2D紋理影象)。使用紋理座標獲取紋理顏色叫做取樣(Sampling)。紋理座標起始於(0, 0),也就是紋理圖片的左下角,終始於(1, 1),即紋理圖片的右上角。

因為OpenGL要求y軸0.0座標是在圖片的底部的,但是圖片(圖片資料)的y軸0.0座標通常在頂部。這裡我們需要把紋理在Y軸翻轉一下

gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)

5. 設定紋理引數

紋理環繞方式

紋理座標的範圍通常是從(0, 0)到(1, 1),那如果我們把紋理座標設定在範圍之外會發生什麼?OpenGL預設的行為是重複這個紋理影象,但OpenGL提供了更多的選擇:

gl.REPEAT 對紋理的預設行為。重複紋理影象。
gl.MIRRORED_REPEAT 和GL_REPEAT一樣,但每次重複圖片是映象放置的
gl.CLAMP_TO_EDGE 紋理座標會被約束在0到1之間,超出的部分會重複紋理座標的邊緣,產生一種邊緣被拉伸的效果。
gl.CLAMP_TO_BORDER 超出的座標為使用者指定的邊緣顏色。

在這裡插入圖片描述

前面提到的每個選項都可以使用glTexParameter*函式對單獨的一個座標軸設定(s、t(如果是使用3D紋理那麼還有一個r)它們和x、y、z是等價的)

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.MIRRORED_REPEAT);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.MIRRORED_REPEAT);

紋理過濾

當進行放大(Magnify)和縮小(Minify)操作的時候可以設定紋理過濾的選項,比如你可以在紋理被縮小的時候使用鄰近過濾,被放大時使用線性過濾。我們需要使用glTexParameter*函式為放大和縮小指定過濾方式。這段程式碼看起來會和紋理環繞方式的設定很相似:

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

GL_NEAREST(也叫鄰近過濾,Nearest Neighbor Filtering)是OpenGL預設的紋理過濾方式。當設定為GL_NEAREST的時候,OpenGL會選擇中心點最接近紋理座標的那個畫素。
GL_LINEAR(也叫線性過濾,(Bi)linear Filtering)它會基於紋理座標附近的紋理畫素,計算出一個插值,近似出這些紋理畫素之間的顏色。一個紋理畫素的中心距離紋理座標越近,那麼這個紋理畫素的顏色對最終的樣本顏色的貢獻越大。

紋理單元

一個紋理的位置值通常稱為一個紋理單元(Texture Unit)
紋理單元的主要目的是讓我們在著色器中可以使用多於一個的紋理。通過把紋理單元賦值給取樣器,我們可以一次繫結多個紋理,只要我們首先啟用對應的紋理單元。就像glBindTexture一樣,我們可以使用glActiveTexture啟用紋理單元,傳入我們需要使用的紋理單元:

gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);

WebGL通過紋理單元的機制來同時使用多個紋理,gl.TEXTURE0~gl.TEXTURE7是管理紋理影象的8個紋理單元


6.向著色器傳遞紋理資料

var u_Sampler = gl.getUniformLocation(program, 'u_Sampler');
gl.uniform1i(u_Sampler, 0);

7.繪製

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

gl.TRIANGLES_STRIP:除了將前三個頂點之後的頂點當作第三個頂點與前兩個頂點共同構成一個新三角形外,其他都與 gl.TRIANGLES 相同。例如,如果陣列中包含 A、B、C、D 四個頂
點,則第一個三角形連線 ABC,而第二個三角形連線 BCD。


三、階段性總結

通過對三角形的繪製和紋理的繪製,可以把繪製過程大體分為以下幾個階段:

  1. 準備頂點和片段著色器,編譯著色器程式
  2. CPU中準備原始資料
  3. 資料從CPU到GPU快取
  4. 向著色器中傳遞快取資料
  5. 繪製