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

本篇博客了解一下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è)置(st(如果是使用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),并且指定ST軸。最后一個(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);
}

最終展示:

源碼Github

?著作權(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)容

  • 1、概述 前面幾篇文章OpenGL ES 3.0(一)綜述 、OpenGL ES 3.0(二)GLSL與著色器 討...
    高丕基閱讀 3,346評(píng)論 0 7
  • 本文首發(fā)于個(gè)人博客:Lam's Blog - 【OpenGL-ES】二維紋理,文章由MarkDown語法編寫,可能...
    格子林ll閱讀 3,958評(píng)論 0 9
  • 版本記錄 前言 OpenGL 圖形庫項(xiàng)目中一直也沒用過,最近也想學(xué)著使用這個(gè)圖形庫,感覺還是很有意思,也就自然想著...
    刀客傳奇閱讀 9,235評(píng)論 0 8
  • 紋理(Textures) 我們已經(jīng)了解到,我們可以為每個(gè)頂點(diǎn)使用顏色來增加圖形的細(xì)節(jié),從而創(chuàng)建出有趣的圖像。但是通...
    IceMJ閱讀 5,836評(píng)論 2 13
  • 前言 OpenGL的紋理實(shí)際上運(yùn)用十分廣泛,是OpenGL中的重點(diǎn)。如果你有看過Android底層的繪制原理,能夠...
    yjy239閱讀 4,214評(píng)論 5 7

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