本篇博客了解一下2D紋理,并完成一個(gè)繪制顯示一張圖片的Renderer。
2D紋理
2D紋理是OpenGL ES中最基本和常用的紋理形式。2D紋理本質(zhì)上其實(shí):是一個(gè)圖像數(shù)據(jù)的二維數(shù)組。一個(gè)紋理的單獨(dú)數(shù)據(jù)元素稱作"紋素(Texel,texture pixels)紋理像素簡寫"。用2D紋理渲染時(shí),紋理坐標(biāo)用作紋理圖像中的索引。2D紋理的紋理坐標(biāo)用一對(duì)2D坐標(biāo)(s,t)指定,有時(shí)也 稱作(u,v)坐標(biāo)。
紋理坐標(biāo)在x和y軸上,范圍為0到1之間(注意我們使用的是2D紋理圖像)。使用紋理坐標(biāo)獲取紋理顏色叫做采樣(Sampling)。紋理坐標(biāo)起始于(0, 0),也就是紋理圖片的左下角,終始于(1, 1),即紋理圖片的右上角。下面的圖片展示了我們是如何把紋理坐標(biāo)映射到三角形上的。

我們?yōu)槿切沃付?個(gè)紋理坐標(biāo)點(diǎn)。如上圖所示,我們希望三角形的左下角對(duì)應(yīng)紋理的左下角,因此我們把三角形左下角頂點(diǎn)的紋理坐標(biāo)設(shè)置為(0, 0);三角形的上頂點(diǎn)對(duì)應(yīng)于圖片的上中位置所以我們把它的紋理坐標(biāo)設(shè)置為(0.5, 1.0);同理右下方的頂點(diǎn)設(shè)置為(1, 0)。我們只要給頂點(diǎn)著色器傳遞這三個(gè)紋理坐標(biāo)就行了,接下來它們會(huì)被傳片段著色器中,它會(huì)為每個(gè)片段進(jìn)行紋理坐標(biāo)的插值。
紋理坐標(biāo)看起來就像這樣:
float texCoords[] = {
0.0f, 0.0f, // 左下角
1.0f, 0.0f, // 右下角
0.5f, 1.0f // 上中
};
對(duì)紋理采樣的解釋非常寬松,它可以采用幾種不同的插值方式。所以我們需要自己告訴OpenGL該怎樣對(duì)紋理采樣。
紋理環(huán)繞方式
紋理坐標(biāo)的范圍通常是從(0, 0)到(1, 1),那如果我們把紋理坐標(biāo)設(shè)置在范圍之外會(huì)發(fā)生什么?OpenGL默認(rèn)的行為是重復(fù)這個(gè)紋理圖像(我們基本上忽略浮點(diǎn)紋理坐標(biāo)的整數(shù)部分),但OpenGL提供了更多的選擇:
| 環(huán)繞方式 | 描述 |
|---|---|
| GL_REPEAT | 對(duì)紋理的默認(rèn)行為。重復(fù)紋理圖像。 |
| GL_MIRRORED_REPEAT | 和GL_REPEAT一樣,但每次重復(fù)圖片是鏡像放置的。 |
| GL_CLAMP_TO_EDGE | 紋理坐標(biāo)會(huì)被約束在0到1之間,超出的部分會(huì)重復(fù)紋理坐標(biāo)的邊緣,產(chǎn)生一種邊緣被拉伸的效果。 |
| GL_CLAMP_TO_BORDER | 超出的坐標(biāo)為用戶指定的邊緣顏色。 |

前面提到的每個(gè)選項(xiàng)都可以使用glTexParameter*函數(shù)對(duì)單獨(dú)的一個(gè)坐標(biāo)軸設(shè)置(s、t(如果是使用3D紋理那么還有一個(gè)r)它們和x、y、z是等價(jià)的):
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
第一個(gè)參數(shù)指定了紋理目標(biāo);我們使用的是2D紋理,因此紋理目標(biāo)是GL_TEXTURE_2D。第二個(gè)參數(shù)需要我們指定設(shè)置的選項(xiàng)與應(yīng)用的紋理軸。我們打算配置的是WRAP選項(xiàng),并且指定S和T軸。最后一個(gè)參數(shù)需要我們傳遞一個(gè)環(huán)繞方式(Wrapping),在這個(gè)例子中OpenGL會(huì)給當(dāng)前激活的紋理設(shè)定紋理環(huán)繞方式為GL_MIRRORED_REPEAT。
紋理過濾
紋理坐標(biāo)不依賴于分辨率(Resolution),它可以是任意浮點(diǎn)值,所以O(shè)penGL需要知道怎樣將紋理像素映射到紋理坐標(biāo)。當(dāng)你有一個(gè)很大的物體但是紋理的分辨率很低的時(shí)候這就變得很重要了。你可能已經(jīng)猜到了,OpenGL也有對(duì)于紋理過濾(Texture Filtering)的選項(xiàng)。紋理過濾有很多個(gè)選項(xiàng),但是現(xiàn)在我們只討論最重要的兩種:GL_NEAREST和GL_LINEAR。
GL_NEAREST(也叫鄰近過濾,Nearest Neighbor Filtering)是OpenGL默認(rèn)的紋理過濾方式。當(dāng)設(shè)置為GL_NEAREST的時(shí)候,OpenGL會(huì)選擇中心點(diǎn)最接近紋理坐標(biāo)的那個(gè)像素。下圖中你可以看到四個(gè)像素,加號(hào)代表紋理坐標(biāo)。左上角那個(gè)紋理像素的中心距離紋理坐標(biāo)最近,所以它會(huì)被選擇為樣本顏色:

GL_LINEAR(也叫線性過濾,(Bi)linear Filtering)它會(huì)基于紋理坐標(biāo)附近的紋理像素,計(jì)算出一個(gè)插值,近似出這些紋理像素之間的顏色。一個(gè)紋理像素的中心距離紋理坐標(biāo)越近,那么這個(gè)紋理像素的顏色對(duì)最終的樣本顏色的貢獻(xiàn)越大。下圖中你可以看到返回的顏色是鄰近像素的混合色:

那么這兩種紋理過濾方式有怎樣的視覺效果呢?讓我們看看在一個(gè)很大的物體上應(yīng)用一張低分辨率的紋理會(huì)發(fā)生什么吧(紋理被放大了,每個(gè)紋理像素都能看到):

GL_NEAREST產(chǎn)生了顆粒狀的圖案,我們能夠清晰看到組成紋理的像素,而GL_LINEAR能夠產(chǎn)生更平滑的圖案,很難看出單個(gè)的紋理像素。GL_LINEAR可以產(chǎn)生更真實(shí)的輸出,但有些開發(fā)者更喜歡8-bit風(fēng)格,所以他們會(huì)用GL_NEAREST選項(xiàng)。
當(dāng)進(jìn)行放大(Magnify)和縮小(Minify)操作的時(shí)候可以設(shè)置紋理過濾的選項(xiàng),比如你可以在紋理被縮小的時(shí)候使用鄰近過濾,被放大時(shí)使用線性過濾。我們需要使用glTexParameter*函數(shù)為放大和縮小指定過濾方式。這段代碼看起來會(huì)和紋理環(huán)繞方式的設(shè)置很相似:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
多級(jí)漸遠(yuǎn)紋理
想象一下,假設(shè)我們有一個(gè)包含著上千物體的大房間,每個(gè)物體上都有紋理。有些物體會(huì)很遠(yuǎn),但其紋理會(huì)擁有與近處物體同樣高的分辨率。由于遠(yuǎn)處的物體可能只產(chǎn)生很少的片段,OpenGL從高分辨率紋理中為這些片段獲取正確的顏色值就很困難,因?yàn)樗枰獙?duì)一個(gè)跨過紋理很大部分的片段只拾取一個(gè)紋理顏色。在小物體上這會(huì)產(chǎn)生不真實(shí)的感覺,更不用說對(duì)它們使用高分辨率紋理浪費(fèi)內(nèi)存的問題了。
OpenGL使用一種叫做多級(jí)漸遠(yuǎn)紋理(Mipmap)的概念來解決這個(gè)問題,它簡單來說就是一系列的紋理圖像,后一個(gè)紋理圖像是前一個(gè)的二分之一。多級(jí)漸遠(yuǎn)紋理背后的理念很簡單:距觀察者的距離超過一定的閾值,OpenGL會(huì)使用不同的多級(jí)漸遠(yuǎn)紋理,即最適合物體的距離的那個(gè)。由于距離遠(yuǎn),解析度不高也不會(huì)被用戶注意到。同時(shí),多級(jí)漸遠(yuǎn)紋理另一加分之處是它的性能非常好。讓我們看一下多級(jí)漸遠(yuǎn)紋理是什么樣子的:

手工為每個(gè)紋理圖像創(chuàng)建一系列多級(jí)漸遠(yuǎn)紋理很麻煩,幸好OpenGL有一個(gè)glGenerateMipmaps函數(shù),在創(chuàng)建完一個(gè)紋理后調(diào)用它OpenGL就會(huì)承擔(dān)接下來的所有工作了。
| 過濾方式 | 描述 |
|---|---|
| GL_NEAREST_MIPMAP_NEAREST | 使用最鄰近的多級(jí)漸遠(yuǎn)紋理來匹配像素大小,并使用鄰近插值進(jìn)行紋理采樣 |
| GL_LINEAR_MIPMAP_NEAREST | 使用最鄰近的多級(jí)漸遠(yuǎn)紋理級(jí)別,并使用線性插值進(jìn)行采樣 |
| GL_NEAREST_MIPMAP_LINEAR | 在兩個(gè)最匹配像素大小的多級(jí)漸遠(yuǎn)紋理之間進(jìn)行線性插值,使用鄰近插值進(jìn)行采樣 |
| GL_LINEAR_MIPMAP_LINEAR | 在兩個(gè)鄰近的多級(jí)漸遠(yuǎn)紋理之間使用線性插值,并使用線性插值進(jìn)行采樣 |
就像紋理過濾一樣,我們可以使用glTexParameteri將過濾方式設(shè)置為前面四種提到的方法之一:
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
代碼實(shí)現(xiàn)
首先,定義頂點(diǎn)坐標(biāo)和紋理坐標(biāo)
/**
* 頂點(diǎn)坐標(biāo)
* (x,y,z)
*/
private float[] POSITION_VERTEX = new float[]{
0f, 0f, 0f, //頂點(diǎn)坐標(biāo)V0
1f, 1f, 0f, //頂點(diǎn)坐標(biāo)V1
-1f, 1f, 0f, //頂點(diǎn)坐標(biāo)V2
-1f, -1f, 0f, //頂點(diǎn)坐標(biāo)V3
1f, -1f, 0f //頂點(diǎn)坐標(biāo)V4
};
/**
* 紋理坐標(biāo)
* (s,t)
*/
private static final float[] TEX_VERTEX = {
0.5f, 0.5f, //紋理坐標(biāo)V0
1f, 0f, //紋理坐標(biāo)V1
0f, 0f, //紋理坐標(biāo)V2
0f, 1.0f, //紋理坐標(biāo)V3
1f, 1.0f //紋理坐標(biāo)V4
};
這里頂點(diǎn)坐標(biāo)和紋理坐標(biāo)是一一對(duì)應(yīng)的,只是因?yàn)槎咦鴺?biāo)原點(diǎn)不同,坐標(biāo)值也不同,如下圖。

/**
* 索引,最終繪制時(shí)通過索引從頂點(diǎn)數(shù)據(jù)中取出對(duì)應(yīng)頂點(diǎn),再按照指定的方式進(jìn)行繪制
*/
private static final short[] VERTEX_INDEX = {
0, 1, 2, //V0,V1,V2 三個(gè)頂點(diǎn)組成一個(gè)三角形
0, 2, 3, //V0,V2,V3 三個(gè)頂點(diǎn)組成一個(gè)三角形
0, 3, 4, //V0,V3,V4 三個(gè)頂點(diǎn)組成一個(gè)三角形
0, 4, 1 //V0,V4,V1 三個(gè)頂點(diǎn)組成一個(gè)三角形
};
/**
* 頂點(diǎn)著色器
*/
private String vertextShader =
"#version 300 es\n" +
"layout (location = 0) in vec4 vPosition;\n" +
"layout (location = 1) in vec2 aTextureCoord;\n" +
"http://矩陣\n" +
"uniform mat4 u_Matrix;\n"+
"http://輸出紋理坐標(biāo)(s,t)\n" +
"out vec2 vTexCoord;\n" +
"void main() { \n" +
" gl_Position = u_Matrix * vPosition;\n" +
" gl_PointSize = 10.0;\n" +
" vTexCoord = aTextureCoord;\n" +
"}\n";
片段著色器應(yīng)該接下來會(huì)把輸出變量vTexCoord作為輸入變量。
片段著色器也應(yīng)該能訪問紋理對(duì)象,但是我們?cè)鯓幽馨鸭y理對(duì)象傳給片段著色器呢?GLSL有一個(gè)供紋理對(duì)象使用的內(nèi)建數(shù)據(jù)類型,叫做采樣器(Sampler),它以紋理類型作為后綴,比如sampler1D、sampler3D,或在我們的例子中的sampler2D。我們可以簡單聲明一個(gè)uniform sampler2D把一個(gè)紋理添加到片段著色器中,稍后我們會(huì)把紋理賦值給這個(gè)uniform。
/**
* 片段著色器
*/
private String fragmentShader =
"#version 300 es\n" +
"precision mediump float;\n" +
"uniform sampler2D uTextureUnit;\n" +
"http://接收剛才頂點(diǎn)著色器傳入的紋理坐標(biāo)(s,t)\n" +
"in vec2 vTexCoord;\n" +
"out vec4 vFragColor;\n" +
"void main() {\n" +
" vFragColor = texture(uTextureUnit,vTexCoord);\n" +
"}\n";
我們使用GLSL內(nèi)建的texture函數(shù)來采樣紋理的顏色,它第一個(gè)參數(shù)是紋理采樣器,第二個(gè)參數(shù)是對(duì)應(yīng)的紋理坐標(biāo)。texture函數(shù)會(huì)使用之前設(shè)置的紋理參數(shù)對(duì)相應(yīng)的顏色值進(jìn)行采樣。這個(gè)片段著色器的輸出就是紋理的(插值)紋理坐標(biāo)上的(過濾后的)顏色。
public static int loadTexture(Context context, int resourceId) {
final int[] textureIds = new int[1];
//創(chuàng)建一個(gè)紋理對(duì)象
GLES30.glGenTextures(1, textureIds, 0);
if (textureIds[0] == 0) {
Log.e(TAG, "Could not generate a new OpenGL textureId object.");
return 0;
}
final BitmapFactory.Options options = new BitmapFactory.Options();
//這里需要加載原圖未經(jīng)縮放的數(shù)據(jù)
options.inScaled = false;
final Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);
if (bitmap == null) {
Log.e(TAG, "Resource ID " + resourceId + " could not be decoded.");
GLES30.glDeleteTextures(1, textureIds, 0);
return 0;
}
// 綁定紋理到OpenGL
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureIds[0]);
//設(shè)置默認(rèn)的紋理過濾參數(shù)
GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR_MIPMAP_LINEAR);
GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
// 加載bitmap到紋理中
GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, bitmap, 0);
// 生成MIP貼圖
GLES30.glGenerateMipmap(GLES30.GL_TEXTURE_2D);
// 數(shù)據(jù)如果已經(jīng)被加載進(jìn)OpenGL,則可以回收該bitmap
bitmap.recycle();
// 取消綁定紋理
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
return textureIds[0];
}
繪制
@Override
public void onDrawFrame(GL10 gl) {
GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
//使用程序片段
GLES30.glUseProgram(mProgram);
GLES30.glUniformMatrix4fv(uMatrixLocation, 1, false, mMatrix, 0);
GLES30.glEnableVertexAttribArray(0);
GLES30.glVertexAttribPointer(0, 3, GLES30.GL_FLOAT, false, 0, vertexBuffer);
GLES30.glEnableVertexAttribArray(1);
GLES30.glVertexAttribPointer(1, 2, GLES30.GL_FLOAT, false, 0, mTexVertexBuffer);
GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
//綁定紋理
GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, textureId);
// 繪制
GLES20.glDrawElements(GLES20.GL_TRIANGLES, VERTEX_INDEX.length, GLES20.GL_UNSIGNED_SHORT, mVertexIndexBuffer);
}
最終展示:
