前言
在前面的文章里我們已經學習了OpenGL的基本流程和概念,并且成功的渲染了第一個三角形,這篇文章我們會貼上第一個紋理。
本文目標
學習并使用紋理。關于什么是紋理,大家可以直接看百度百科給出的解釋。這篇文章里的紋理主要是指圖片。
正文
1.解析圖片數據
本文中使用的圖片是bmp格式,因為這個格式解析起來比較簡單,如果大家看不懂可以自行了解一下bmp的結構,不過不懂也沒有關系,這不是本文的重點,iOS為我們封裝好了一些類,我們以后可以直接使用,這個在后面的教程里會仔細介紹,這里是基礎教程,iOS封裝的東西也是基于這些基礎Api,因此了解這些而不是直接使用那些類是有好處的。
解析bmp格式圖片的代碼如下。
+ (char *)decodeBMPWithBMPImageName:(NSString *)imageName width:(float *)width height:(float *)height
{
NSString *path = [[NSBundle mainBundle] pathForResource:imageName ofType:nil];
NSData *bmpContent = [NSData dataWithContentsOfFile:path];
Byte *bmpBytes = (Byte *)[bmpContent bytes];
if (*(unsigned short *)bmpBytes == 0x4D42) { // 判斷是否是bmp
int contentOffset = *((int *)(bmpBytes+10)); // 獲得圖像內容的偏移,即圖像從什么位置開始
*width = *((int *)(bmpBytes+18)); // bmp圖像的寬放在偏移18的位置,它是一個int型
*height = *((int *)(bmpBytes+22));// bmp圖像的放在寬的后面,它也是一個int型
Byte *pixelData = bmpBytes + contentOffset; // 指向內容開始
// 因為在bmp圖像中的儲存格式是bgr,我們需要改成rgb
for (int i = 0; i < *width * *height * 3; i+=3) {
Byte temp = pixelData[i];
pixelData[i] = pixelData[i+2];
pixelData[i+2] = temp;
}
return (char *)pixelData;
}
return NULL;
}
這個方法返回的就是修改完的bmp的像素,其中width跟height是外部傳進來的引用變量,因為接下來我們會用到,如果你有一定的C語言基礎,看懂這段代碼應該不難。
2.生成紋理對象
獲取了bmp的像素之后,我們就要開始真正的生成紋理對象了,具體的步驟是,先建立一個GLuint
類型的紋理,然后給它設置參數,綁定數據傳到GPU,代碼如下。
GLuint texture; // 聲明紋理對象
glGenTextures(1, &texture); // 創建紋理對象
glBindTexture(GL_TEXTURE_2D, texture); // 設置狀態機
// 設置紋理參數
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); // 設置紋理放大時采用線性插值處理
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); // 設置紋理縮小時采用線性插值處理
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); // 設置X方向的紋理環繞
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // 設置Y方向的紋理環繞
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, imagePixel); // 綁定數據并上傳到GPU
glBindTexture(GL_TEXTURE_2D, 0);
代碼里出現的GL_TEXTURE_2D
表明我們處理的是一個2D的紋理,同理還有GL_TEXTURE_3D
,處理紋理放大縮小的參數是GL_LINEAR
,即線性插值處理,它的具體表現為當取一個點上的像素的時候,是取的附近的一個點跟這個點的差值,這樣會顯得平滑一些,它還有一個參數是GL_NEAREST
,具體表現為當取一個點上的像素的時候,是取的離它最近的一個點的像素。
設置紋理環繞的意思是當你設置的值超過設定值的時候應該怎么處理,這里是截取紋理坐標到[1/2n,1-1/2n]。
如果大家對紋理參數部分不太了解,可以看這篇文章進行了解,這里著重的解釋一下glTexImage2D
方法。
target
:上面已經解釋了。
level
:執行細節,這個功能因為一般比較耗費內存所以使用很少,一般傳0,也就是最基本的圖像級別。
internalformat
:GPU里的像素格式,因為bmp里沒有Alpha通道所以這里是RGB。
width
:圖片的寬。
height
:圖片的高。
border
:border的寬度,必須為0。
format
:圖片的像素格式,解釋同internalformat
,當然這個參數不必一定跟internalformat
一致。
type
:每個數據的類型,unsigned byte
也就是Byte
。
pixels
:圖像的數據。
3.在Shader里添加變量
我們創建好的紋理怎么傳給Shader呢,答案就是獲取變量的位置,然后傳進去。
我們已經說過位置幾何信息是由Vertex Shader
來處理,而紋理實際上也有自己的坐標,比如說在一個圖形里,紋理究竟基于什么位置貼上去或者貼紋理的哪一部分,紋理的坐標以左下角為(0,0),以右上角為(1,1),只有兩個點,因此我們在Shader.vsh
里添加一個vec2
的變量。
attribute vec2 texcoord;
同時這個變量需要傳遞到Fragment Shader
來渲染顏色,所以我們要創建一個專門用來傳遞的變量,它的類型是varying
。
varying vec2 v_texcoord;
為了區分,一般我們是用v開頭的命名方式來說明它是一個varying
類型的變量。
然后在main
方法里,對v_texcoord
進行賦值。
v_texcoord = texcoord;
修改后Shader.vsh
里的代碼如下。
attribute vec4 position;
attribute vec2 texcoord;
varying vec2 v_texcoord;
void main()
{
v_texcoord = texcoord;
gl_Position = position;
}
然后打開Shader.fsh
,首先我們添加一下精度修飾語句
。
precision mediump float;
它的意思是,用中等精度來修飾float變量,可選的有highp
高等精度跟lowp
低等精度。在Vertex Shader
中,有默認精度語句,所以我們在無需更改的情況下無需設置,在Fragment Shader
中,只有int
,sampler2D
,samplerCube
的默認精度語句,所以我們需要自己添加float
的精度語句。
然后添加我們的紋理變量,它是uniform
修飾的,通常情況下uniform
用來修飾不可更改的。
uniform sampler2D u_texture;
然后添加我們從Vertex Shader
里傳遞過來的變量,大家要記住這是一條流水線,所以我們變量的名字要一致。
varying vec2 v_texcoord;
最后在main
方法里,對gl_FragColor
賦值。
gl_FragColor = vec4(1.0) * texture2D(u_texture,v_texcoord);
其中texture2D
是一個系統自帶函數,參數就是紋理跟紋理的坐標,vec4(1.0)
代表的是白色,任何紋理的顏色*白色都會顯示紋理本來的顏色。
修改后的全部代碼如下。
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_texcoord;
void main()
{
gl_FragColor = vec4(1.0) * texture2D(u_texture,v_texcoord);
}
4.使用紋理
上面的步驟做完,下面的就簡單了,因為我們之前的文章已經講過了,所以這里只貼代碼加上一些簡單的注釋。
首先修改我們的頂點數組,添加上我們的紋理坐標。
float positions[] =
{
// 頂點坐標 // 紋理坐標
-.5f,-.5f,0.f, 0.f,0.f,
.5f,-.5f,0.f, 1.f,0.f,
.5f,.5f,0.f, 1.f,1.f,
-.5f,.5f,0.f, 0.f,1.f,
};
注意因為我們的頂點數組改變了,所以以前的有些參數也需要修改一下。
_vbo = [OpenGLUtils createVertexBuffersObjectWithObjType:GL_ARRAY_BUFFER ObjSize:sizeof(float) * 5 * 4 Usage:GL_STATIC_DRAW Data:positions]; // size由3*3變成了5*4
然后獲取我們新建變量的"位置"。
GLint texcoordLocation = glGetAttribLocation(_gpuProgram, "texcoord"); // 獲取texcoord變量的位置
GLint textureLocation = glGetUniformLocation(_gpuProgram, "u_texture"); // 獲取u_texture變量的位置
綁定紋理。
glBindTexture(GL_TEXTURE_2D, _texture);
glUniform1i(_textureLocation, 0);
啟用紋理的"槽"并設置數據。
glEnableVertexAttribArray(_texcoordLocation);
glVertexAttribPointer(_texcoordLocation, 2, GL_FLOAT, GL_FALSE, sizeof(float) * 5, (void *)(sizeof(float) * 3));
同樣的,posLocation
的參數也需要改一下。
glVertexAttribPointer(_posLocation, 3, GL_FLOAT, GL_FALSE, sizeof(float) * 5, 0); // 由間隔3個點變成間隔5個點
最后,修改glDrawArrays
的參數。
glDrawArrays(GL_TRIANGLE_FAN, 0, 4); // 因為我們這次繪制的是四邊形,所以從3個點變成繪制4個點
關于GL_TRIANGLE_FAN
這個參數,以及其他的可選參數的解釋,可以通過這篇文章進行了解。
如果這個地方某個知識點忘了,可以回去看看上一篇文章。
結束
這篇文章我們已經了解了使用紋理的基本流程,到這里,基于濾鏡的OpenGL的基礎大家已經大致掌握,所以從下一篇開始我會逐漸的教大家做幾個簡單的濾鏡,相信大家會很容易理解。
本文Demo
點擊這里下載Demo,找到對應的名字即可。