Android OpenGL ES 六.紋理繪制(轉(zhuǎn)載)

基本原理

與漸變色接近,但有些區(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)在左上角,如下圖。

ST紋理坐標(biāo)

其實(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)的顏色值。

頂點(diǎn)坐標(biāo)與紋理坐標(biāo)映射

紋理過濾

當(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);
}

紋理繪制.png

紋理、紋理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)注明出處。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 基本原理 與漸變色接近,但有些區(qū)別:漸變色:光柵化過程中,計(jì)算出顏色值,然后在片段著色器的時(shí)候可以直接賦值紋理:光...
    Benhero閱讀 7,302評(píng)論 1 8
  • 1、背景 只記得有一次面試,面試官提到一個(gè)問題,既然你對(duì)OpenGL ES有一定的了解,那么可否寫一個(gè)紋理鏡像呢,...
    minhelloworld閱讀 1,189評(píng)論 0 0
  • 本篇博客了解一下2D紋理,并完成一個(gè)繪制顯示一張圖片的Renderer。 2D紋理 2D紋理是OpenGL ES中...
    MzDavid閱讀 914評(píng)論 0 0
  • 本文首發(fā)于個(gè)人博客:Lam's Blog - 【OpenGL-ES】二維紋理,文章由MarkDown語法編寫,可能...
    格子林ll閱讀 3,958評(píng)論 0 9
  • 1、概述 前面幾篇文章OpenGL ES 3.0(一)綜述 、OpenGL ES 3.0(二)GLSL與著色器 討...
    高丕基閱讀 3,346評(píng)論 0 7

友情鏈接更多精彩內(nèi)容