作者:星隕
來源:
OpenGL 學習系列 --- 紋理接下來探索紋理了。
紋理,簡單的了解就是一副圖像。而把一副圖像映射到圖形上的過程,叫做紋理映射。
比如有如下圖形和三角形,想要把圖形中的一部分映射到三角形上。

基本原理
要注意到,OpenGL 繪制的物體是 3D 的,而紋理是 2D 的,那麼紋理映射就是将 2D 的紋理映射到 3D 的物體上,可以想象成用一張紙裹着一個物體一樣,不過要按照一定規律來。
OpenGL 中繪制的物體是有坐标系的,每個點都對應 x、y、z 坐标,而紋理也有着它的坐标,隻要 3D 物體中的每個點都對應了 2D 紋理中的某個點,那麼就可以把紋理映射到 3D 物體上去了。
紋理的坐标,叫
做紋理坐标系
。它的範圍隻有﹝0,0﹞到﹝1,1﹞ 。
它的坐标原點位于左下角,水準向右為 S 軸,豎直向上為 Y 軸。不論實際的紋理圖檔尺寸大小如何,橫向、縱向坐标最大值都是 1 。
例如:實際圖為 512 x 256 像素分辨率,則橫向第 512 個像素對應紋理坐标為 1 ,縱向第 256 個像素對應紋理坐标為 1 。不過,紋理圖最好是采用像素為 2 的 n 次方的紋理圖。
紋理映射的基本思想就是:首先為圖元中的每個頂點指定恰當的紋理坐标,然後通過紋理坐标在紋理圖中可以确定選中的紋理區域,最後将選中紋理區域中的内容根據紋理坐标映射到指定的圖元上。
紋理映射在 OpenGL 的渲染管線上的展現:在渲染管線中,先進行頂點着色器,繪制出物體的大緻形狀,之後會進行
光栅化
,将物體光栅化為許多片段組成,然後再進行片段着色器,将圖形的每個片段進行着色。
那麼就需要在 頂點着色器 中将紋理的坐标傳入,在光栅化階段,紋理坐标将根據 頂點着色器 對它的處理以及 片段和各頂點的位置關系 插值産生,然後才是将插值計算後的結果傳入到片段着色器中。
着色器操作
相比直接繪制圖形,使用紋理後,着色器也要改變了。
頂點着色器:
1attribute vec4 a_Position;
2attribute vec2 a_TextureCoordinates;
3varying vec2 v_TextureCoordinates;
4uniform mat4 u_ModelMatrix;
5uniform mat4 u_ViewMatrix;
6uniform mat4 u_ProjectionMatrix;
7uniform mat4 u_Matrix;
8
9void main() {
10 v_TextureCoordinates = a_TextureCoordinates ;
11 gl_Position = u_ProjectionMatrix * u_ViewMatrix * u_ModelMatrix * a_Position;
12}
在頂點着色器中多了 v_TextureCoordinates 變量,它是 varying 類型,意思為可變類型,在光栅化處理時會對該變量進行處理,随後傳入到片段着色器中。
片段着色器
1precision mediump float;
2uniform sampler2D u_TextureUnit;
3varying vec2 v_TextureCoordinates;
4
5void main(){
6 // 未使用紋理的顔色指派 : gl_FragColor = u_Color;
7 gl_FragColor = texture2D(u_TextureUnit,v_TextureCoordinates);
8}
v_TextureCoordinates1
變量就是接受來自頂點着色器傳的值,
u_TextureUnit
變量就是使用的采樣器,類型是
sampler2D
。
使用紋理後的片段着色器要使用
texture2D
函數給顔色指派。
texture2D
函數的作用就是采樣,從紋理中采取像素指派給
gl_FragColor
變量,也就是最後的顔色。
上層代碼
大緻了解了着色器代碼,接着就是上層的 Java 代碼了。
和要建立一個 OpenGL ProgramId 類似,使用紋理也需要建立一個紋理 ID。
1 /**
2 * 傳回加載圖像後的 OpenGl 紋理的 ID
3 * @param context
4 * @param resourceId
5 * @return
6 */
7 public static int loadTexture(Context context, int resourceId) {
8 final int[] textureObjectIds = new int[1];
9 glGenTextures(1, textureObjectIds, 0);
10 if (textureObjectIds[0] == 0) {
11 Timber.d("Could not generate a new OpenGL texture object.");
12 return 0;
13 }
14 final BitmapFactory.Options options = new BitmapFactory.Options();
15 options.inScaled = false;
16 final Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), resourceId, options);
17
18 if (bitmap == null) {
19 Timber.d("resource Id could not be decoded");
20 glDeleteTextures(1, textureObjectIds, 0);
21 return 0;
22 }
23
24 glBindTexture(GL_TEXTURE_2D, textureObjectIds[0]);
25
26 // 設定縮小的情況下過濾方式
27 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
28 // 設定放大的情況下過濾方式
29 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
30
31 // 加載紋理到 OpenGL,讀入 Bitmap 定義的位圖資料,并把它複制到目前綁定的紋理對象
32 // 目前綁定的紋理對象就會被附加上紋理圖像。
33 texImage2D(GL_TEXTURE_2D, 0, bitmap, 0);
34
35 bitmap.recycle();
36
37 // 為目前綁定的紋理自動生成所有需要的多級漸遠紋理
38 // 生成 MIP 貼圖
39 glGenerateMipmap(GL_TEXTURE_2D);
40
41 // 解除與紋理的綁定,避免用其他的紋理方法意外地改變這個紋理
42 glBindTexture(GL_TEXTURE_2D, 0);
43
44 return textureObjectIds[0];
45 }
- 首先使用
建立紋理 ID。glGenTextures
- 如果建立失敗,則使用
删除并退出。glDeleteTextures
- 建立成功之後,使用
函數将紋理 ID 和紋理目标綁定。glBindTexture
- 之後會設定紋理在縮小和放大情況下的過濾方式。
- 再使用
将紋理目标和texImage2D
圖檔綁定。Bitmap
- 使用
函數生成多級漸遠紋理和glGenerateMipmap
紋理貼圖。MIP
-
函數解除綁定。glBindTexture
glBindTexture 函數
這裡要重點說一下 glBindTexture 函數。
它的作用是綁定紋理名到指定的目前活動紋理單元,當一個紋理綁定到一個目标時,目标紋理單元先前綁定的紋理對象将被自動斷開。紋理目标預設綁定的是 0 ,是以要斷開時,也再将紋理目标綁定到 0 就好了。
是以在代碼的最後調用了
glBindTexture(GL_TEXTURE_2D, 0) 來解除綁定
當一個紋理被綁定時,在綁定的目标上的 OpenGL 操作将作用到綁定的紋理上,并且,對綁定的目标的查詢也将傳回其上綁定的紋理的狀态。
也就是說,這個紋理目标成為了被綁定到它上面的紋理的别名,而紋理名稱為 0 則會引用到它的預設紋理。是以,當後續對紋理目标調用
glTexParameteri
函數設定過濾方式,其實也是對紋理設定的過濾方式。
綁定紋理中的值
建立并且設定了紋理着色器ID之後,就需要綁定并設定在着色器語言中的變量了。
1 // 綁定着色器腳本中的變量
2 uTextureUnitAttr = glGetUniformLocation(mProgram, U_TEXTURE_UNIT)
3 mTextureId = TextureHelper.loadTexture(mContext,R.drawable.texture)
4 // 激活紋理單元
5 glActiveTexture(GL_TEXTURE0)
6 // 綁定紋理目标
7 glBindTexture(GL_TEXTURE_2D, mTextureId)
8 // 給片段着色器中的采樣器變量 sample2D 指派
9 glUniform1i(uTextureUnitAttr, 0)
在着色器腳本中定義了
uniform
類型的采樣器變量
sampler2D
,在上層的應用代碼需要将它綁定并指派。而
varying
類型的變量由頂點着色器傳過去,不需要另外指派了。
接下來要使用 glActiveTexture 函數激活紋理單元。在一個系統中,紋理單元的資料是有限的,在源碼中從 GL_TEXTURE0 到 GL_TEXTURE31 共定義了三十二個紋理單元,但具體數量根據機型而定。
通過 GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS 常量可以查詢到。
1 var intBuffer:IntBuffer = IntBuffer.allocate(1)
2 glGetIntegerv(GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS,intBuffer)
3 LogUtil.d("max combined texture image units " + intBuffer[0])
激活了紋理單元,還需要再綁定紋理目标。
一個紋理單元包含了多個類型紋理目标,如:GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP 等等。
因為紋理單元是紋理的一個别名,是以對紋理單元所做的操作,都相當于對紋理操作的。把一些對紋理所做的操作提取到函數裡,最後再加載紋理,并綁定到紋理目标上。
glUniform1i
函數為采樣器進行指派為 0 ,這是和激活紋理單元相對應的。因為激活的紋理單元為 0 ,是以指派也是為 0 。如果這裡不一緻,直接就看不到任何東西了。
實際效果
當綁定并設定好片段着色器中的值之後,接下來的流程就和繪制基本圖形一樣了。
具體的繪制操作都在片段着色器裡面定義了,而在上層代碼中就不用花費很多心思了,在頂點着色器不變的情況下,甚至可以隻改變片段着色器的值來繪制不同的紋理效果。
總結 & 名詞混淆點
在上面既是紋理單元又是紋理目标的很容易搞混,梳理一下概念:
形如 GL_TEXTURE0、GL_TEXTURE1、GL_TEXTURE2 的就是紋理單元,一台機子上紋理單元數量是有限的,依具體機型而定。而 glActiveTexture 則是激活具體的紋理單元。
一個紋理單元又包含多個類型的紋理目标,有:GL_TEXTURE_1D、GL_TEXTURE_2D、CUBE_MAP 等等。
通過 glGenTextures 函數生成的 int 類型的值就是紋理,通過 glBindTexture 函數将紋理目标和紋理綁定後,對紋理目标所進行的操作都反映到對紋理上。
紋理目标需要通過 texImage2D 函數附加上 Bitmap 位圖。
參考
http://blog.csdn.net/opengl_es/article/details/19852277 http://blog.csdn.net/artisans/article/details/76695614具體代碼詳情,可以參考我的 Github 項目:
https://github.com/glumes/AndroidOpenGLTutorial「視訊雲技術」你最值得關注的音視訊技術公衆号,每周推送來自阿裡雲一線的實踐技術文章,在這裡與音視訊領域一流工程師交流切磋。