3. 紋理三角形
上章中的三角形雖然看起來比較鮮豔,但很多時候並不實用。我們想在三角形上顯示一幅圖片,或者更恰當點說,我們想讓一張圖片中和三角形對應的那一部分顯示出來。這可以通過實用紋理映像來實現。這是本章的目的。
為了讓WebGL可以訪問我們指定映像資料,我們需要將映像資料儲存到WebGL內部的紋理對象中。首先,我們要建立紋理對象,使用WebGL函數createTexture()。然後,將該紋理對象設定為相應紋理類型的當前操作對象;這通過WebGL函數bindTexture(target, textureObject)完成。該函數的第一個參數表示要綁定的紋理類型,有TEXTURE_2D和TEXTURE_CUBE_MAP兩種。 此處我們使用的是2D的映像,是2D的紋理,因此,第一個參數是TEXTURE_2D。之後,將我們指定的映像資料“拷貝”到該紋理對象對應的儲存區以使WebGL內部能夠訪問到它。執行“拷貝”的WebGL函數分為兩類,texImage2D系列和texSubImage2D系列(詳情參考《WebGL參考手冊》和《OpenGL
ES 2.0 編程指南》)。前者更新整個紋理儲存區的資料,後者可以更新部分儲存區的資料。我們以HTML的image元素指定的映像作為紋理映像,並且一次性“拷貝”到紋理對象中,所以,我們使用的WebGL函數為:
void texImage2D(GLenum target, GLint level, GLenum internalformat,
GLenum format, GLenum type, HTMLImageElement image)
該函數的第一個參數指定要設定的某個具體資料區塊,要麼是2D紋理的要麼就是立方體紋理的某個面的,是下面的枚舉之一:TEXTURE_2D、TEXTURE_CUBE_MAP_POSITIVE_X、 TEXTURE_CUBE_MAP_NEGATIVE_X、TEXTURE_CUBE_MAP_POSITIVE_Y、TEXTURE_CUBE_MAP_NEGATIVE_Y、TEXTURE_CUBE_MAP_POSITIVE_Z、TEXTURE_CUBE_MAP_NEGATIVE_Z。第二個參數指定要載入的紋理層級。它的意義將在消除鋸齒的章節中介紹。目前我們只是設定最原始的映像資料,對應的紋理層級為0。internalformat指定紋理儲存區的格式,可以為:RGBA、RGB、LUMINANCE_ALPHA、LUMINANCE、ALPHA。format指的是參數image代表的映像的像素的格式。要注意的是,在WebGL中,internalformat和format必須相同。type指定輸入的像素資料類型,可以是下者之一:UNSIGNED_BYTE、UNSIGNED_SHORT_4_4_4_4、UNSIGNED_SHORT_5_5_5_1、UNSIGNED_SHORT_5_6_5。它的取值和format
密切相關,你可以把它當成是format的子格式。比如,當format為RGBA時,你才能設定type為UNSIGNED_SHORT_4_4_4_4和UNSIGNED_SHORT_5_5_5_1(UNSIGNED_BYTE比較特殊,它表示像素的的每個組份都用一個BYTE表示)。在本章樣本中,使用的是一個24位的位元影像映像,RGB格式,UNSIGNED_BYTE類型(5位表示紅色,6位表示綠色,5位表示藍色)。最後的image參數,是我們在HTML中指定的image元素所代表的對象。
映像資料弄到WebGL內部之後,在使用之前,要給它打上一個標識。在片段著色器中,將通過這個標識進行紋理訪問。該標識我們稱之為紋理單元。打上標識分為兩步:首先使用WebGL函數activeTexture(textureUnit)啟用標識;然後再使用WebGL函數bindTexture(target, textureObject)將指定的紋理對象關聯到當前啟用的紋理單元(上面也用到了bindTexture,這點不要奇怪;上面的功能+此處的功能,才是bindTexture函數的完整功能)。紋理單元可以為TEXTURE0、TEXTURE1、...TEXTURE31。但這不代表著你可以使用它們的全部。你實際可以使用的紋理單元必須在WebGL函數getParameter(MAX_TEXTURE_IMAGE_UNITS)的範圍之內。必須,假設函數的傳回值為8,那麼你只能使用TEXTURE0、TEXTURE1、...TEXTURE7。
至此,已經完成了工作的一半。剩下的一半,是在頂點著色器中計算出頂點的紋理座標(換句話說,是要計算出該頂點要顯示的是映像上的哪個點;我本章樣本中,我們的映像是600px*450px,剛好和和視見區大小相同;而要顯示的內容也恰好是三角形所佔部分,因此,只要簡單地把頂點座標轉換為紋理座標即可)並把它通過varying變數傳遞給片段著色器。然後,讓片段著色器根據傳入的varying紋理座標從紋理中取樣,將取樣結果賦值給內建變數gl_FragColor。取樣,通過著色器的內建函數texture2D(s_texture,
v_texCoord)完成。注意,該函數僅在著色器中有效,它不是WebGL中的函數,別搞混了。該函數的第一個參數是一個取樣器。該取樣器和前段中提到的紋理單元對應,如,取樣器值為0,表示從紋理單元TEXTURE0取樣;若值為1則表示從紋理單元TEXTURE1取樣。通常,片段著色器是無法自己確定取樣器的(即,要使用的紋理單元)。解決的辦法是在片段著色器中定義一個unifrom取樣器,然後我們在應用中通過WebGL的uniform*系列函數指定它。uniform*系列函數需要知道uniform變數的關聯索引。在前面曾經提到過,uniform變數的管理索引由程式對象在連結時自動產生,我們只能通過WebGL函數getUniformLocation(programObject,
name)進行擷取。
注意,紋理座標和渲染座標不同。紋理座標的左下角為(0,0),右上方為(1,1)。超出這個範圍的紋理座標的行為,由紋理封裝模式決定。我們在頂點著色器中,總共只計算了三個紋理座標,分別和三角形的三個頂點對應。但要顯示的確實整個三角形對應的地區。不要擔心,WebGL會自動為我們進行插值計算,並輸出結果。很多時候,經過插值計算得出的紋理座標,無法唯一確定映像上的一個點。比如,在1和2之間按線性插值,得到結果1.5。但映像中的點是沒有1.5這個座標的。這個時候,我們就需要告訴WebGL要如何處理這種情況下。對於我們當前的這個樣本來說,可用的方法有兩種:一個是取離1.5最近的那個點(如果存在多個,就取第一個點好了);另外一種是線性插值;即將離得最近的前後兩點和上下兩點按線性公式計算出中間值作為最終值。這兩種方法在一些情況下會產生鋸齒,但對我們現在的這個樣本來說足夠了。具體設定的時候,還要區分放大和縮小這兩種情況;使用的WebGL函數為texParameterf(target,
pname, param)。第一個參數為TEXTURE_2D、TEXTURE_CUBE_MAP。第二個參數為TEXTURE_MAG_FILTER、TEXTURE_MIN_FILTER,分別表示放大和縮小。第三個參數,和我剛剛所講的兩種方法對應的是NEAREST、LINEAR。此函數還可以用來指定紋理的封裝模式。此時,第二個參數為TEXTURE_WRAP_S、EXTURE_WRAP_T,分別對應x和y方向。第三個參數為REPEAT、CLAMP_TO_EDGE、MIRRORED_REPEAT;它們的具體效果,參考《OpenGL
ES 2.0 編程指南》的《第九章 紋理/紋理座標封裝》。另外,當紋理映像的寬度和高度不是2的整數次方時,封裝模式只能是GL_CLAMP_TO_EDGE;並且縮小過濾只能是GL_NEAREST 或 GL_LINEAR(換個說法,就是不能是mip貼圖)。
下面是整合後的樣本:
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=gb2312">
<script type="text/javascript" src="glMatrix-0.9.5.js"></script>
<script id="shader-vs" type="x-shader/x-vertex">
attribute vec3 v3Position;
varying vec2 v_texCoord;
void main(void)
{
v_texCoord = vec2((v3Position.x+1.0)/2.0, 1.0-(v3Position.y+1.0)/2.0);
gl_Position = vec4(v3Position, 1.0);
}
</script>
<script id="shader-fs" type="x-shader/x-fragment">
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
uniform sampler2D s_texture;
varying vec2 v_texCoord;
void main(void)
{
gl_FragColor = texture2D(s_texture, v_texCoord);
}
</script>
<script>
function ShaderSourceFromScript(scriptID)
{
var shaderScript = document.getElementById(scriptID);
if (shaderScript == null) return "";
var sourceCode = "";
var child = shaderScript.firstChild;
while (child)
{
if (child.nodeType == child.TEXT_NODE ) sourceCode += child.textContent;
child = child.nextSibling;
}
return sourceCode;
}
var webgl = null;
var vertexShaderObject = null;
var fragmentShaderObject = null;
var programObject = null;
var triangleBuffer = null;
var v3PositionIndex = 0;
var textureObject = null;
var samplerIndex = -1;
function Init()
{
var myCanvasObject = document.getElementById('myCanvas');
webgl = myCanvasObject.getContext("experimental-webgl");
webgl.viewport(0, 0, myCanvasObject.clientWidth, myCanvasObject.clientHeight);
vertexShaderObject = webgl.createShader(webgl.VERTEX_SHADER);
fragmentShaderObject = webgl.createShader(webgl.FRAGMENT_SHADER);
webgl.shaderSource(vertexShaderObject, ShaderSourceFromScript("shader-vs"));
webgl.shaderSource(fragmentShaderObject, ShaderSourceFromScript("shader-fs"));
webgl.compileShader(vertexShaderObject);
webgl.compileShader(fragmentShaderObject);
if(!webgl.getShaderParameter(vertexShaderObject, webgl.COMPILE_STATUS)){alert(webgl.getShaderInfoLog(vertexShaderObject));return;}
if(!webgl.getShaderParameter(fragmentShaderObject, webgl.COMPILE_STATUS)){alert(webgl.getShaderInfoLog(fragmentShaderObject));return;}
programObject = webgl.createProgram();
webgl.attachShader(programObject, vertexShaderObject);
webgl.attachShader(programObject, fragmentShaderObject);
webgl.bindAttribLocation(programObject, v3PositionIndex, "v3Position");
webgl.linkProgram(programObject);
if(!webgl.getProgramParameter(programObject, webgl.LINK_STATUS)){alert(webgl.getProgramInfoLog(programObject));return;}
samplerIndex = webgl.getUniformLocation(programObject, "s_texture");
webgl.useProgram(programObject);
var jsArrayData = [
0.0, 1.0, 0.0,//上頂點
-1.0, -1.0, 0.0,//左頂點
1.0, 0.0, 0.0];//右頂點
triangleBuffer = webgl.createBuffer();
webgl.bindBuffer(webgl.ARRAY_BUFFER, triangleBuffer);
webgl.bufferData(webgl.ARRAY_BUFFER, new Float32Array(jsArrayData), webgl.STATIC_DRAW);
textureObject = webgl.createTexture();
webgl.bindTexture(webgl.TEXTURE_2D, textureObject);
var img = document.getElementById('myTexture');
webgl.texImage2D(webgl.TEXTURE_2D, 0, webgl.RGB, webgl.RGB, webgl.UNSIGNED_BYTE, img);
webgl.clearColor(0.0, 0.0, 0.0, 1.0);
webgl.clear(webgl.COLOR_BUFFER_BIT);
webgl.bindBuffer(webgl.ARRAY_BUFFER, triangleBuffer);
webgl.enableVertexAttribArray(v3PositionIndex);
webgl.vertexAttribPointer(v3PositionIndex, 3, webgl.FLOAT, false, 0, 0);
webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_MIN_FILTER, webgl.NEAREST);
webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_MAG_FILTER, webgl.NEAREST);
webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_WRAP_S, webgl.CLAMP_TO_EDGE);
webgl.texParameteri(webgl.TEXTURE_2D, webgl.TEXTURE_WRAP_T, webgl.CLAMP_TO_EDGE);
webgl.activeTexture(webgl.TEXTURE0);
webgl.bindTexture(webgl.TEXTURE_2D, textureObject);
webgl.uniform1i(samplerIndex, 0);
webgl.drawArrays(webgl.TRIANGLES, 0, 3);
}
</script>
</head>
<body onload='Init()'>
<canvas id="myCanvas" style="border:1px solid red;" width='600px' height='450px'></canvas>
<img id="myTexture" src='texture.bmp'>
</body>
</html>
運行結果如下: