OpenGL ES 3.0紋理映射-繪製一張圖片

本篇博客瞭解一下2D紋理,並完成一個繪製顯示一張圖片的Renderer。java

2D紋理

2D紋理是OpenGL ES中最基本和經常使用的紋理形式。2D紋理本質上其實:是一個圖像數據的二維數組。一個紋理的單獨數據元素稱做"紋素(Texel,texture pixels)紋理像素簡寫"。用2D紋理渲染時,紋理座標用做紋理圖像中的索引。2D紋理的紋理座標用一對2D座標(s,t)指定,有時也 稱做(u,v)座標。git

紋理座標在x和y軸上,範圍爲0到1之間(注意咱們使用的是2D紋理圖像)。使用紋理座標獲取紋理顏色叫作採樣(Sampling)。紋理座標起始於(0, 0),也就是紋理圖片的左下角,終始於(1, 1),即紋理圖片的右上角。下面的圖片展現了咱們是如何把紋理座標映射到三角形上的。github

咱們爲三角形指定了3個紋理座標點。如上圖所示,咱們但願三角形的左下角對應紋理的左下角,所以咱們把三角形左下角頂點的紋理座標設置爲(0, 0);三角形的上頂點對應於圖片的上中位置因此咱們把它的紋理座標設置爲(0.5, 1.0);同理右下方的頂點設置爲(1, 0)。咱們只要給頂點着色器傳遞這三個紋理座標就好了,接下來它們會被傳片斷着色器中,它會爲每一個片斷進行紋理座標的插值。數組

紋理座標看起來就像這樣:bash

float texCoords[] = {
    0.0f, 0.0f, // 左下角
    1.0f, 0.0f, // 右下角
    0.5f, 1.0f // 上中
};
複製代碼

對紋理採樣的解釋很是寬鬆,它能夠採用幾種不一樣的插值方式。因此咱們須要本身告訴OpenGL該怎樣對紋理採樣app

紋理環繞方式

紋理座標的範圍一般是從(0, 0)到(1, 1),那若是咱們把紋理座標設置在範圍以外會發生什麼?OpenGL默認的行爲是重複這個紋理圖像(咱們基本上忽略浮點紋理座標的整數部分),但OpenGL提供了更多的選擇:ide

環繞方式 描述
GL_REPEAT 對紋理的默認行爲。重複紋理圖像。
GL_MIRRORED_REPEAT 和GL_REPEAT同樣,但每次重複圖片是鏡像放置的。
GL_CLAMP_TO_EDGE 紋理座標會被約束在0到1之間,超出的部分會重複紋理座標的邊緣,產生一種邊緣被拉伸的效果。
GL_CLAMP_TO_BORDER 超出的座標爲用戶指定的邊緣顏色。

前面提到的每一個選項均可以使用glTexParameter*函數對單獨的一個座標軸設置(st(若是是使用3D紋理那麼還有一個r)它們和xyz是等價的):函數

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_MIRRORED_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_MIRRORED_REPEAT);
複製代碼

第一個參數指定了紋理目標;咱們使用的是2D紋理,所以紋理目標是GL_TEXTURE_2D。第二個參數須要咱們指定設置的選項與應用的紋理軸。咱們打算配置的是WRAP選項,而且指定ST軸。最後一個參數須要咱們傳遞一個環繞方式(Wrapping),在這個例子中OpenGL會給當前激活的紋理設定紋理環繞方式爲GL_MIRRORED_REPEAT。性能

紋理過濾

紋理座標不依賴於分辨率(Resolution),它能夠是任意浮點值,因此OpenGL須要知道怎樣將紋理像素映射到紋理座標。當你有一個很大的物體可是紋理的分辨率很低的時候這就變得很重要了。你可能已經猜到了,OpenGL也有對於紋理過濾(Texture Filtering)的選項。紋理過濾有不少個選項,可是如今咱們只討論最重要的兩種:GL_NEAREST和GL_LINEAR。ui

GL_NEAREST(也叫鄰近過濾,Nearest Neighbor Filtering)是OpenGL默認的紋理過濾方式。當設置爲GL_NEAREST的時候,OpenGL會選擇中心點最接近紋理座標的那個像素。下圖中你能夠看到四個像素,加號表明紋理座標。左上角那個紋理像素的中心距離紋理座標最近,因此它會被選擇爲樣本顏色:

GL_LINEAR(也叫線性過濾,(Bi)linear Filtering)它會基於紋理座標附近的紋理像素,計算出一個插值,近似出這些紋理像素之間的顏色。一個紋理像素的中心距離紋理座標越近,那麼這個紋理像素的顏色對最終的樣本顏色的貢獻越大。下圖中你能夠看到返回的顏色是鄰近像素的混合色:

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

GL_NEAREST產生了顆粒狀的圖案,咱們可以清晰看到組成紋理的像素,而GL_LINEAR可以產生更平滑的圖案,很難看出單個的紋理像素。GL_LINEAR能夠產生更真實的輸出,但有些開發者更喜歡8-bit風格,因此他們會用GL_NEAREST選項。

當進行放大(Magnify)和縮小(Minify)操做的時候能夠設置紋理過濾的選項,好比你能夠在紋理被縮小的時候使用鄰近過濾,被放大時使用線性過濾。咱們須要使用glTexParameter*函數爲放大和縮小指定過濾方式。這段代碼看起來會和紋理環繞方式的設置很類似:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
複製代碼

多級漸遠紋理

想象一下,假設咱們有一個包含着上千物體的大房間,每一個物體上都有紋理。有些物體會很遠,但其紋理會擁有與近處物體一樣高的分辨率。因爲遠處的物體可能只產生不多的片斷,OpenGL從高分辨率紋理中爲這些片斷獲取正確的顏色值就很困難,由於它須要對一個跨過紋理很大部分的片斷只拾取一個紋理顏色。在小物體上這會產生不真實的感受,更不用說對它們使用高分辨率紋理浪費內存的問題了。

OpenGL使用一種叫作多級漸遠紋理(Mipmap)的概念來解決這個問題,它簡單來講就是一系列的紋理圖像,後一個紋理圖像是前一個的二分之一。多級漸遠紋理背後的理念很簡單:距觀察者的距離超過必定的閾值,OpenGL會使用不一樣的多級漸遠紋理,即最適合物體的距離的那個。因爲距離遠,解析度不高也不會被用戶注意到。同時,多級漸遠紋理另外一加分之處是它的性能很是好。讓咱們看一下多級漸遠紋理是什麼樣子的:

手工爲每一個紋理圖像建立一系列多級漸遠紋理很麻煩,幸虧OpenGL有一個glGenerateMipmaps函數,在建立完一個紋理後調用它OpenGL就會承擔接下來的全部工做了。

過濾方式 描述
GL_NEAREST_MIPMAP_NEAREST 使用最鄰近的多級漸遠紋理來匹配像素大小,並使用鄰近插值進行紋理採樣
GL_LINEAR_MIPMAP_NEAREST 使用最鄰近的多級漸遠紋理級別,並使用線性插值進行採樣
GL_NEAREST_MIPMAP_LINEAR 在兩個最匹配像素大小的多級漸遠紋理之間進行線性插值,使用鄰近插值進行採樣
GL_LINEAR_MIPMAP_LINEAR 在兩個鄰近的多級漸遠紋理之間使用線性插值,並使用線性插值進行採樣

就像紋理過濾同樣,咱們可使用glTexParameteri將過濾方式設置爲前面四種提到的方法之一:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
複製代碼

代碼實現

首先,定義頂點座標和紋理座標

/** * 頂點座標 * (x,y,z) */
private float[] POSITION_VERTEX = new float[]{
        0f, 0f, 0f,     //頂點座標V0
        1f, 1f, 0f,     //頂點座標V1
        -1f, 1f, 0f,    //頂點座標V2
        -1f, -1f, 0f,   //頂點座標V3
        1f, -1f, 0f     //頂點座標V4
};

/** * 紋理座標 * (s,t) */
private static final float[] TEX_VERTEX = {
        0.5f, 0.5f, //紋理座標V0
        1f, 0f,     //紋理座標V1
        0f, 0f,     //紋理座標V2
        0f, 1.0f,   //紋理座標V3
        1f, 1.0f    //紋理座標V4
};
複製代碼

這裏頂點座標和紋理座標是一一對應的,只是由於兩者座標原點不一樣,座標值也不一樣,以下圖。

/** * 索引,最終繪製時經過索引從頂點數據中取出對應頂點,再按照指定的方式進行繪製 */
private static final short[] VERTEX_INDEX = {
        0, 1, 2,  //V0,V1,V2 三個頂點組成一個三角形
        0, 2, 3,  //V0,V2,V3 三個頂點組成一個三角形
        0, 3, 4,  //V0,V3,V4 三個頂點組成一個三角形
        0, 4, 1   //V0,V4,V1 三個頂點組成一個三角形
};
複製代碼
/** * 頂點着色器 */
private String vertextShader =
                "#version 300 es\n" +
                "layout (location = 0) in vec4 vPosition;\n" +
                "layout (location = 1) in vec2 aTextureCoord;\n" +
                "//矩陣\n" +
                "uniform mat4 u_Matrix;\n"+
                "//輸出紋理座標(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";


複製代碼

片斷着色器應該接下來會把輸出變量vTexCoord做爲輸入變量。

片斷着色器也應該能訪問紋理對象,可是咱們怎樣能把紋理對象傳給片斷着色器呢?GLSL有一個供紋理對象使用的內建數據類型,叫作採樣器(Sampler),它以紋理類型做爲後綴,好比sampler1Dsampler3D,或在咱們的例子中的sampler2D。咱們能夠簡單聲明一個uniform sampler2D把一個紋理添加到片斷着色器中,稍後咱們會把紋理賦值給這個uniform。

/** * 片斷着色器 */
private String fragmentShader =
                "#version 300 es\n" +
                "precision mediump float;\n" +
                "uniform sampler2D uTextureUnit;\n" +
                "//接收剛纔頂點着色器傳入的紋理座標(s,t)\n" +
                "in vec2 vTexCoord;\n" +
                "out vec4 vFragColor;\n" +
                "void main() {\n" +
                " vFragColor = texture(uTextureUnit,vTexCoord);\n" +
                "}\n";
複製代碼

咱們使用GLSL內建的texture函數來採樣紋理的顏色,它第一個參數是紋理採樣器,第二個參數是對應的紋理座標。texture函數會使用以前設置的紋理參數對相應的顏色值進行採樣。這個片斷着色器的輸出就是紋理的(插值)紋理座標上的(過濾後的)顏色。

public static int loadTexture(Context context, int resourceId) {
    final int[] textureIds = new int[1];
    //建立一個紋理對象
    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();
    //這裏須要加載原圖未經縮放的數據
    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]);

    //設置默認的紋理過濾參數
    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);

    // 數據若是已經被加載進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);
}
複製代碼

最終展現:

源碼Github

相關文章
相關標籤/搜索