基本原理
與漸變色接近,但有些區(qū)別:
漸變色:光柵化過程中,計(jì)算出顏色值,然后在片段著色器的時(shí)候可以直接賦值
紋理:光柵化過程中,計(jì)算出當(dāng)前片段在紋理上的坐標(biāo)位置,然后在片段著色器的中,根據(jù)這個(gè)紋理上的坐標(biāo),去紋理中取出相應(yīng)的顏色值。
紋理坐標(biāo)
OpenGL中,2D紋理也有自己的坐標(biāo)體系,取值范圍在(0,0)到(1,1)內(nèi),兩個(gè)維度分別是S、T,所以一般稱為ST紋理坐標(biāo)。而有些時(shí)候也叫UV坐標(biāo)。
紋理坐標(biāo)方向性在Android上與我們平時(shí)熟悉的Bitmap、canvas等一致,都是頂點(diǎn)在左上角,如下圖。

其實(shí)OpenGL中的紋理坐標(biāo)是沒有內(nèi)在方向性的,所以我們可以隨意定義。但由于大多數(shù)計(jì)算機(jī)圖像都有默認(rèn)方向,所以我們只要按照上圖取理解即可。
紋理尺寸
OpenGL ES 2.0中規(guī)定,紋理的每個(gè)維度必須是2次冪,也就是2、4、8 ... 128、256、512等等。而紋理也有最大值上限,通常比較大,比如2048*2048。
文件讀取
OpenGL不能直接加載JPG或者PNG這類被編碼為特定的壓縮格式,需要加載原始數(shù)據(jù),也就是Bitmap。我們?cè)趦?nèi)置圖片到工程中,應(yīng)該講圖片放在drawable-nodpi目錄下,避免讀取的時(shí)候被壓縮處理了。通過BtimapFactory解碼讀取圖片的時(shí)候,要設(shè)置為非縮放的方式,即options.isScaled = false。
紋理坐標(biāo)與頂點(diǎn)坐標(biāo)的關(guān)系
紋理上的每個(gè)頂點(diǎn)與定點(diǎn)坐標(biāo)上的頂點(diǎn)一一對(duì)應(yīng)。如下圖,左邊是頂點(diǎn)坐標(biāo),右邊是紋理坐標(biāo),只要兩個(gè)坐標(biāo)的ABCD定義順序一致,就可以正常地映射出對(duì)應(yīng)的圖形。頂點(diǎn)坐標(biāo)內(nèi)光柵化后的每個(gè)片段,都會(huì)在紋理坐標(biāo)內(nèi)取得對(duì)應(yīng)的顏色值。

紋理過濾
當(dāng)我們通過光柵化將圖形處理成一個(gè)個(gè)小片段的時(shí)候,再講紋理采樣,渲染到指定位置上時(shí),通常會(huì)遇到紋理元素和小片段并非一一對(duì)應(yīng)。這時(shí)候,會(huì)出現(xiàn)紋理的壓縮或者放大。那么在這兩種情況下,就會(huì)有不同的處理方案,這就是紋理過濾了。
之后再仔細(xì)補(bǔ)充每種過濾方式的效果,不過一般情況下,可以采取通用的方案即可滿足需求。
詳情可以參考LearnOpenGL-CN。
加載紋理
下面是一個(gè)工具類方法,相對(duì)通用,能解決大部分需求,只要調(diào)用TextureHelper.loadTexture(mContextContext context, int resourceId);這個(gè)方法即可將內(nèi)置的圖片資源加載出對(duì)應(yīng)的紋理ID。
/**
* 紋理加載助手類
*/
public class TextureHelper {
private static final String TAG = "TextureHelper";
/**
* 根據(jù)資源ID獲取相應(yīng)的OpenGL紋理ID,若加載失敗則返回0
* <br>必須在GL線程中調(diào)用
*/
public static TextureBean loadTexture(Context context, int resourceId) {
TextureBean bean = new TextureBean();
final int[] textureObjectIds = new int[1];
// 1\. 創(chuàng)建紋理對(duì)象
GLES20.glGenTextures(1, textureObjectIds, 0);
if (textureObjectIds[0] == 0) {
if (LoggerConfig.ON) {
Log.w(TAG, "Could not generate a new OpenGL texture object.");
}
return bean;
}
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inScaled = false;
final Bitmap bitmap = BitmapFactory.decodeResource(
context.getResources(), resourceId, options);
if (bitmap == null) {
if (LoggerConfig.ON) {
Log.w(TAG, "Resource ID " + resourceId + " could not be decoded.");
}
// 加載Bitmap資源失敗,刪除紋理Id
GLES20.glDeleteTextures(1, textureObjectIds, 0);
return bean;
}
// 2\. 將紋理綁定到OpenGL對(duì)象上
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, textureObjectIds[0]);
// 3\. 設(shè)置紋理過濾參數(shù):解決紋理縮放過程中的鋸齒問題。若不設(shè)置,則會(huì)導(dǎo)致紋理為黑色
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR_MIPMAP_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
// 4\. 通過OpenGL對(duì)象讀取Bitmap數(shù)據(jù),并且綁定到紋理對(duì)象上,之后就可以回收Bitmap對(duì)象
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, bitmap, 0);
// Note: Following code may cause an error to be reported in the
// ADB log as follows: E/IMGSRV(20095): :0: HardwareMipGen:
// Failed to generate texture mipmap levels (error=3)
// No OpenGL error will be encountered (glGetError() will return
// 0). If this happens, just squash the source image to be
// square. It will look the same because of texture coordinates,
// and mipmap generation will work.
// 5\. 生成Mip位圖
GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
// 6\. 回收Bitmap對(duì)象
bean.setWidth(bitmap.getWidth());
bean.setHeight(bitmap.getHeight());
bitmap.recycle();
// 7\. 將紋理從OpenGL對(duì)象上解綁
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
// 所以整個(gè)流程中,OpenGL對(duì)象類似一個(gè)容器或者中間者的方式,將Bitmap數(shù)據(jù)轉(zhuǎn)移到OpenGL紋理上
bean.setTextureId(textureObjectIds[0]);
return bean;
}
/**
* 紋理數(shù)據(jù)
*/
public static class TextureBean {
private int mTextureId;
private int mWidth;
private int mHeight;
public int getTextureId() {
return mTextureId;
}
void setTextureId(int textureId) {
mTextureId = textureId;
}
public int getWidth() {
return mWidth;
}
public void setWidth(int width) {
mWidth = width;
}
public int getHeight() {
return mHeight;
}
public void setHeight(int height) {
mHeight = height;
}
}
}
這個(gè)工具類要求圖片必須是2次冪的尺寸,不過大部分情況下不會(huì)有問題,兼容性沒經(jīng)過測試。若需要處理,則是在加載出Bitmap的時(shí)候,再生成對(duì)應(yīng)的最接近的2次冪Bitmap。
代碼實(shí)現(xiàn)
1. GLSL
private static final String VERTEX_SHADER = "" +
"uniform mat4 u_Matrix;\n" +
"attribute vec4 a_Position;\n" +
// 紋理坐標(biāo):2個(gè)分量,S和T坐標(biāo)
"attribute vec2 a_TexCoord;\n" +
"varying vec2 v_TexCoord;\n" +
"void main()\n" +
"{\n" +
" v_TexCoord = a_TexCoord;\n" +
" gl_Position = u_Matrix * a_Position;\n" +
"}";
private static final String FRAGMENT_SHADER = "" +
"precision mediump float;\n" +
"varying vec2 v_TexCoord;\n" +
"uniform sampler2D u_TextureUnit;\n" +
"void main()\n" +
"{\n" +
" gl_FragColor = texture2D(u_TextureUnit, v_TexCoord);\n" +
"}";
這里多了2個(gè)新的知識(shí)點(diǎn)。
一個(gè)是紋理坐標(biāo),varying類型,對(duì)應(yīng)到《漸變色》的章節(jié)去理解,非常簡單,漸變色在片段著色器中傳進(jìn)來的是每個(gè)片段的顏色,而本次傳進(jìn)來的是每個(gè)片段在紋理中的位置。
另外一個(gè)是sampler2D的類型,其實(shí)就是紋理數(shù)據(jù)。另外還有個(gè)方法texture2D,第一個(gè)參數(shù)是紋理數(shù)據(jù),第二個(gè)是紋理坐標(biāo),那么這個(gè)方法就是在紋理數(shù)據(jù)中取出當(dāng)前片段對(duì)應(yīng)在紋理數(shù)據(jù)中位置上的數(shù)據(jù)顏色。
2. 定義頂點(diǎn)坐標(biāo)
/**
* 頂點(diǎn)坐標(biāo)
*/
private static final float[] POINT_DATA = {
-0.5f, -0.5f,
-0.5f, 0.5f,
0.5f, 0.5f,
0.5f, -0.5f,
};
/**
* 紋理坐標(biāo)
*/
private static final float[] TEX_VERTEX = {
0, 1,
0, 0,
1, 0,
1, 1,
};
這兩個(gè)坐標(biāo)體系上的每個(gè)頂點(diǎn)是相互對(duì)應(yīng)的,改變兩個(gè)坐標(biāo)內(nèi)部的頂點(diǎn)順序,會(huì)發(fā)現(xiàn)繪制的效果和之前的不一樣。
根據(jù)這一點(diǎn),我們可以繪制多邊形的紋理。
3. 獲取紋理ID
mTextureBean = TextureHelper.loadTexture(mContext, R.drawable.tuzki);
4. 繪制紋理
@Override
public void onDrawFrame(GL10 glUnused) {
GLES20.glClear(GL10.GL_COLOR_BUFFER_BIT);
// 設(shè)置當(dāng)前活動(dòng)的紋理單元為紋理單元0
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
// 將紋理ID綁定到當(dāng)前活動(dòng)的紋理單元上
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTextureBean.getTextureId());
// 將紋理單元傳遞片段著色器的u_TextureUnit
GLES20.glUniform1i(uTextureUnitLocation, 0);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_FAN, 0, POINT_DATA.length / POSITION_COMPONENT_COUNT);
}

紋理、紋理ID和紋理單元、紋理目標(biāo)
紋理:在OpenGL中簡單理解就是一張圖片
紋理Id:紋理的直接引用
紋理單元:紋理的操作容器,有GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2等,紋理單元的數(shù)量是有限的,最多16個(gè)。所以在最多只能同時(shí)操作16個(gè)紋理。在切換使用紋理單元的時(shí)候,使用glActiveTexture方法。
-
紋理目標(biāo):一個(gè)紋理單元中包含了多個(gè)類型的紋理目標(biāo),有GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP等。本章中,將紋理ID綁定到紋理單元0的GL_TEXTURE_2D紋理目標(biāo)上,之后對(duì)紋理目標(biāo)的操作都是對(duì)紋理Id對(duì)應(yīng)的數(shù)據(jù)進(jìn)行操作。
紋理相關(guān)概念.jpg
紋理ID與紋理單元的關(guān)系
OpenGL要操作一個(gè)紋理,那么是將紋理ID裝進(jìn)紋理單元這個(gè)容器里,然后再通過操作紋理單元的方式去實(shí)現(xiàn)的。這樣的話,我們可以加載出很多很多個(gè)紋理ID(但要注意爆內(nèi)存問題),但只有16個(gè)紋理單元,在Fragment Shader里最多同時(shí)能操作16個(gè)單元。
讀取當(dāng)前的畫面
在OpenGL開發(fā)過程中,經(jīng)常會(huì)需要一些調(diào)試的場景,想知道當(dāng)前繪制的某一幀具體效果。那么可以通過下面的代碼+Debug模式下進(jìn)行處理。
static void sendImage(int width, int height) {
ByteBuffer rgbaBuf = ByteBuffer.allocateDirect(width * height * 4);
rgbaBuf.position(0);
long start = System.nanoTime();
GLES20.glReadPixels(0, 0, width, height, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE,
rgbaBuf);
long end = System.nanoTime();
Log.d("TryOpenGL", "glReadPixels: " + (end - start));
saveRgb2Bitmap(rgbaBuf, Environment.getExternalStorageDirectory().getAbsolutePath()
+ "/gl_dump_" + width + "_" + height + ".png", width, height);
}
static void saveRgb2Bitmap(Buffer buf, String filename, int width, int height) {
Log.d("TryOpenGL", "Creating " + filename);
BufferedOutputStream bos = null;
try {
bos = new BufferedOutputStream(new FileOutputStream(filename));
Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bmp.copyPixelsFromBuffer(buf);
bmp.compress(Bitmap.CompressFormat.PNG, 90, bos);
bmp.recycle();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bos != null) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
拓展閱讀
參考
見Android OpenGL ES學(xué)習(xí)資料所列舉的博客、資料。
GitHub代碼工程
本系列課程所有相關(guān)代碼請(qǐng)參考我的GitHub項(xiàng)目GLStudio。
課程目錄
本系列課程目錄詳見 簡書 - Android OpenGL ES教程規(guī)劃
作者:Benhero
鏈接:http://www.itdecent.cn/p/3659f4649f98
來源:簡書
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。
