Android OpenGL ES 2.0 手把手教學(6)- 紋理

程式設計師kenney發表於2019-05-03

大家好,下面和大學一起學習紋理,在我的github上有一個專案OpenGLES2.0SamplesForAndroid,我會不斷地編寫學習樣例,文章和程式碼同步更新,歡迎關注,連結:github.com/kenneycode/…

在前面的例子中,我們渲染的都是一些比較簡單的顏色,如果我們要渲染一張圖片,該怎麼做呢?這就需要用到紋理,我們需要建立一個紋理並把圖片載入到紋理中,然後在fragment shader中對紋理進行取樣,從而將紋理渲染出來。

我們先通過glGenTextures建立一個紋理,然後設定一些引數,這裡得到的紋理只是一個id

// 建立圖片紋理
// Create texture
val textures = IntArray(1)
GLES20.glGenTextures(textures.size, textures, 0)
val imageTexture = textures[0]

GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, imageTexture)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
複製程式碼

建立好紋理之後,它還是空的,我們要給這個紋理填充內容,我們先將一張圖片解碼成bitmap,並將畫素資料copy到一個ByteBuffer中,因為將bitmp載入到紋理中的方法接受的是一個ByteBuffer

val bitmap = Util.decodeBitmapFromAssets("image_0.jpg")
val b = ByteBuffer.allocate(bitmap.width * bitmap.height * 4)
bitmap.copyPixelsToBuffer(b)
b.position(0)
複製程式碼

下面通過glTexImage2D方法將上面得到的ByteBuffer載入到紋理中:

GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 
                    0, 
                    GLES20.GL_RGBA, 
                    bitmap.width, 
                    bitmap.height, 
                    0, 
                    GLES20.GL_RGBA, 
                    GLES20.GL_UNSIGNED_BYTE, 
                    b)
複製程式碼

這時紋理中才真正有了內容,接下來需要將紋理傳遞給fragment shader進行取樣,從而渲染出來,我們先來看看如何在fragment shader中使用紋理:

// vertex shader
precision mediump float;
attribute vec4 a_position;
attribute vec2 a_textureCoordinate;
varying vec2 v_textureCoordinate;
void main() {
    v_textureCoordinate = a_textureCoordinate;
    gl_Position = a_position;
}

// fragment shader
precision mediump float;
varying vec2 v_textureCoordinate;
uniform sampler2D u_texture;
void main() {
    gl_FragColor = texture2D(u_texture, v_textureCoordinate);
}
複製程式碼

關鍵點是uniform sampler2D u_texture;這句,它宣告瞭一個2D的取樣器用於取樣紋理。

varying vec2 v_textureCoordinate;則是從vertex shader中傳遞過來的一個經過插值的紋理座標值,關於varying變數,在之前的一個篇文章《Android OpenGL ES 2.0 手把手教學(4)- 片段著色器 fragment shader》中有涉及到。

gl_FragColor = texture2D(u_texture, v_textureCoordinate);就是從紋理中取樣出v_textureCoordinate座標所對應的顏色作為fragment shader的輸出,我們可以看到,fragment shader的輸出實際上就是一個顏色,在之前的文章中,是我們自己去控制這個顏色,而當這個顏色如果是來自紋理的取樣,那最終渲染出來就是紋理的樣子。

現在,紋理和shader都準備好了,如果把它們聯絡越來呢?首先需要像之前一樣先獲取shader中紋理變數的location

val uTextureLocation = GLES20.glGetAttribLocation(programId, "u_texture")
複製程式碼

然後給這個location指定對應哪個紋理單元,這裡我們使用0號紋理單元:

GLES20.glUniform1i(uTextureLocation, 0)
複製程式碼

看到這裡,可能有些朋友有點懵,紋理單元又是個什麼東西?是這樣的,紋理單元可以想像成是一種類似暫存器的東西,在OpenGL使用紋理前,我們先要把紋理放到某個紋理單元上,之後的操作OpenGL就會去我們指定的紋理單元上去取對應的紋理。

我們剛才讓location對應0號紋理單元,但是我們好像沒並沒有哪裡說我們把紋理放在了0號紋理單元,這是怎麼回事呢?因為預設情況下,OpenGL是使用0號紋理單元的,我們因為沒有更改過使用的紋理單元,因此預設就是0號了,我們如果想使用其它紋理單元,可以通過glActiveTexture來指定,例如:

GLES20.glActiveTexture(GLES20.GL_TEXTURE1)
複製程式碼

就會指定使用1號紋理單元,如果我們的例子改為使用1號紋理單元,那麼uTextureLocation就要相應地改為讓它對應1號紋理單元:

GLES20.glUniform1i(uTextureLocation, 1)
複製程式碼

下面我看來看看頂點座標和紋理座標:

private val vertexData = floatArrayOf(-1f, -1f, -1f, 1f, 1f, 1f, -1f, -1f, 1f, 1f, 1f, -1f)
private val textureCoordinateData = floatArrayOf(0f, 1f, 0f, 0f, 1f, 0f, 0f, 1f, 1f, 0f, 1f, 1f)
複製程式碼

如何給shader傳遞頂點座標,方法大家現在應該比較熟悉了,其實紋理座標的傳遞也幾乎是一樣的,只是值不一樣而已。

之前提到過,頂點座標的作用是告訴OpenGL要渲染到什麼區域,頂點座標的座標系是每個軸的範圍都是-1~1,其實也可以超出-11,只不過超出就不在渲染的範圍內了,就看不見了,並不是就算錯誤,頂點座標系的原點在中間。

紋理座標的座標原點在左下角,每個軸的範圍是0~1,同樣的也可以超出01,超出之後的表現會根據設定的紋理引數有所不同。

在這個例子中,我們使用GL_TRIANGLES的繪製模式進行渲染,關於渲染模式,可以參考我的上一篇文章《Android OpenGL ES 2.0 手把手教學(5)- 繪製模式》,在這種繪製模式下,每三個點構成一個獨立三形,因此紋理座標構成的兩個三角形會對應渲染到頂點座標指定的兩個三角形中,我們來看一下效果:

Android OpenGL ES 2.0 手把手教學(6)- 紋理

細心的朋友可能會發現,我們的頂點座標和紋理座標上下是顛倒的,比如頂點座標(-1,-1)對應紋理座標(0,1),也就是渲染區域的左下角對應紋理的左上角,這樣一樣,渲染出來的影象不是應該倒過來嗎?但我們看到的效果卻是正確的。

這是因為我們的紋理來自於bitmap,而bitmap的座標原點是左上角,也就是它和OpenGL中的紋理座標系是上下顛倒的,所以我們把紋理座標再顛倒一次,就是正的了。

我們剛才建立紋理時給紋理設定了幾個引數,我們來看一下:

GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_NEAREST)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_NEAREST)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE)
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE)
複製程式碼

其中GL_TEXTURE_MIN_FILTERGL_TEXTURE_MAG_FILTER是紋理過濾引數,指定當我們渲染出來的紋理比原來的紋理小或者大時要如何處理,這裡我們使用了GL_LINEAR的方式,這是一種線性插值的方式,得到的結果會更平滑,除此之外,還有其它很多選項,還有另一個比較常用的是GL_NEAREST,它會選擇和它最近的畫素,得到的結果鋸齒感比GL_LINEAR要大,我們將頂點座標擴大5倍,即變成-5~5,來得到放大的效果,可以來看看放大時的這兩種效果:

Android OpenGL ES 2.0 手把手教學(6)- 紋理

GL_TEXTURE_WRAP_SGL_TEXTURE_WRAP_T則是指定紋理座標超出了紋理範圍之後,該如何填充,比較常用的有GL_CLAMP_TO_EDGEGL_REPEAT,它們的效果分別是填充邊緣畫素和重複這個紋理,我們將紋理座標改為0~3,看看效果:

Android OpenGL ES 2.0 手把手教學(6)- 紋理

以上就是關於紋理的一些基礎知識,程式碼在我github的OpenGLES2.0SamplesForAndroid專案中,本文對應的是SampleTexture,專案連結:github.com/kenneycode/…

感謝閱讀!

相關文章