前言
本文是關于OpenGL ES的系統性學習過程,記錄了自己在學習OpenGL ES時的收獲。
這篇文章的目標是學習OpenGL ES 2.0中的紋理貼圖技術。
環境是Xcode8.1+OpenGL ES 2.0
目前代碼已經放到github上面,OpenGL ES入門05-OpenGL ES 紋理貼圖
歡迎關注我的 OpenGL ES入門專題
概述
紋理 是表示物體表面細節的一幅或幾幅二維圖形(甚至也有一維和三維的紋理),也稱紋理貼圖(texture mapping)當把紋理按照特定的方式映射到物體表面上的時候能使物體看上去更加真實。紋理映射是一種允許我們為三角形賦予圖象數據的技術;這讓我們能夠更細膩更真實地表現我們的場景。
實現效果
紋理坐標
紋理坐標在x和y軸上,范圍為0到1之間(當然也可以大于1)。使用紋理坐標獲取紋理顏色叫做采樣(Sampling)。紋理坐標起始于(0, 0),也就是紋理圖片的左下角,終始于(1, 1),即紋理圖片的右上角。
紋理環繞方式
紋理坐標的范圍通常是從(0, 0)到(1, 1)。但是如果紋理坐標不在該范圍里,OpenGL ES默認的行為是重復這個紋理圖像,但是我們也可以自己設置其它處理的方式。
環繞方式(Wrapping) | 描述 |
---|---|
GL_REPEAT | 對紋理的默認行為,重復紋理圖像。 |
GL_MIRRORED_REPEAT | 和GL_REPEAT一樣,但每次重復圖片是鏡像放置的。 |
GL_CLAMP_TO_EDGE | 紋理坐標會被約束在0到1之間,超出的部分會重復紋理坐標的邊緣,產生一種邊緣被拉伸的效果。 |
GL_CLAMP_TO_BORDER | 超出的坐標為用戶指定的邊緣顏色。 |
- 我們可以使用glTexParameter*函數對單獨的一個坐標軸設置(二維紋理為s、t坐標,三維紋理為s、t、r坐標)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
參數 target:紋理目標是;
參數 pname :指定坐標軸S軸、T軸、R軸;
參數 param :環繞方式;
如果我們選擇 GL_CLAMP_TO_BORDER 選項,我們還需要指定一個邊緣的顏色。這需要使用glTexParameterfv(表示float類型的數組)函數,用GL_TEXTURE_BORDER_COLOR作為它的選項,并且傳遞一個float數組作為邊緣的顏色值
float borderColor[] = { 0.5f, 0.5f, 0.5f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);
紋理過濾
紋理坐標不依賴于分辨率,它可以是任意浮點值,所以OpenGL ES需要知道怎樣將紋理像素映射到紋理坐標。OpenGL ES默認的紋理過濾方式是鄰近過濾。
|紋理過濾 | 描述|總結|
|:---:|:---:|:---:|:---:|
|GL_LINEAR 線性過濾| 它會基于紋理坐標附近的紋理像素,計算出一個插值,近似出這些紋理像素之間的顏色。一個紋理像素的中心距離紋理坐標越近,那么這個紋理像素的顏色對最終的樣本顏色的貢獻越大 | GL_LINEAR能夠產生更平滑的圖案,很難看出單個的紋理像素。
|GL_NEAREST 鄰近過濾|當設置為GL_NEAREST的時候,會選擇中心點最接近紋理坐標的那個像素。|GL_NEAREST產生了顆粒狀的圖案,我們能夠清晰看到組成紋理的像素|
- 使用glTexParameter*函數為放大和縮小指定過濾方式。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
加載紋理
在實例中使用的JPEG的圖片。因此需要將壓縮數據解碼成RGB像素數據。在iOS中可以使用UIImage和CGImage獲取到RGB數據。在實例中我使用了libjpeg庫來解碼jpeg圖片,因此跨平臺型好一些。(因為希望通過代碼累積最終得到一個跨平臺的工具)
struct my_error_mgr
{
struct jpeg_error_mgr pub; /* "public" fields */
jmp_buf setjmp_buffer; /* for return to caller */
};
typedef struct my_error_mgr* my_error_ptr;
void my_error_exit (j_common_ptr cinfo)
{
my_error_ptr myerr = (my_error_ptr) cinfo->err;
(*cinfo->err->output_message) (cinfo);
longjmp(myerr->setjmp_buffer, 1);
}
int read_jpeg_file(const char* jpegfile,
unsigned char** data,
int* size,
int* width,
int* height)
{
struct jpeg_decompress_struct cinfo;
struct my_error_mgr jerr;
FILE* fp;
JSAMPARRAY buffer;
int row_stride = 0;
unsigned char* tmp_buffer = NULL;
int rgb_size;
fp = fopen(jpegfile, "rb");
if (fp == NULL)
{
printf("open file %s failed.\n", jpegfile);
return -1;
}
cinfo.err = jpeg_std_error(&jerr.pub);
jerr.pub.error_exit = my_error_exit;
if (setjmp(jerr.setjmp_buffer))
{
jpeg_destroy_decompress(&cinfo);
fclose(fp);
return -1;
}
jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, fp);
jpeg_read_header(&cinfo, TRUE);
// we only support RGB or grayscale
if (cinfo.jpeg_color_space == JCS_GRAYSCALE) {
cinfo.out_color_space = JCS_GRAYSCALE;
}else {
cinfo.out_color_space = JCS_RGB;
}
jpeg_start_decompress(&cinfo);
row_stride = cinfo.output_width * cinfo.output_components;
*width = cinfo.output_width;
*height = cinfo.output_height;
rgb_size = row_stride * cinfo.output_height; // 總大小
*size = rgb_size;
buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
*data = (unsigned char *)malloc(sizeof(char) * rgb_size); // 分配總內存
printf("jpeg debug:\nrgb_size: %d, size: %d w: %d h: %d row_stride: %d \n",
rgb_size,
cinfo.image_width*cinfo.image_height*3,
cinfo.image_width,
cinfo.image_height,
row_stride);
tmp_buffer = *data;
while (cinfo.output_scanline < cinfo.output_height) // 解壓每一行
{
jpeg_read_scanlines(&cinfo, buffer, 1);
// 復制到內存
memcpy(tmp_buffer, buffer[0], row_stride);
tmp_buffer += row_stride;
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(fp);
return 0;
}
生成紋理
和之前生成的VBO、VAO對象一樣,紋理生成也類似之前緩存對象的生成,生成步驟也相差無幾。
- 創建紋理對象
void glGenTextures(GLsizei n, GLuint * textures);
參數 n : 表示需要創建紋理對象的個數
參數 textures :用于存儲創建好的紋理對象句柄
- 將紋理對象設置為當前紋理對象
void glBindTexture (GLenum target, GLuint texture);
參數 target :指定綁定的目標
參數 texture :紋理對象句柄
- 指定紋理
void glTexImage2D (GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid* pixels);
參數 target :指定紋理單元的類型,二維紋理需要指定為GL_TEXTURE_2D
參數 level:指定紋理單元的層次,非mipmap紋理level設置為0,mipmap紋理設置為紋理的層級
參數 internalFormat:指定OpenGL ES是如何管理紋理單元中數據格式的
參數 width:指定紋理單元的寬度
參數 height:指定紋理單元的高度
參數 border:指定紋理單元的邊框,如果包含邊框取值為1,不包含邊框取值為0
參數 format:指定data所指向的數據的格式
參數 type:指定data所指向的數據的類型
參數 data:實際指向的數據
- 激活紋理單元。GL_TEXTURE0默認激活,在使用其它紋理單元的時候需要手動激活。OpenGL ES支持的最小紋理單元與設備特性有關,通常情況下OpenGL ES2.0最少支持8個紋理單元,OpenGL ES3.0最少支持16個紋理單元。
void glActiveTexture (GLenum texture);
參數 texture :需要激活的紋理單元
- 釋放紋理對象
void glDeleteTextures (GLsizei n, const GLuint* textures);
參數 n : 表示紋理對象的個數
參數 textures :紋理對象句柄
GLuint createTexture2D(GLenum format, int width, int height, void *data)
{
GLuint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, 0);
return texture;
}
實例
1、創建頂點數據以及紋理坐標
- (void)setupVBO
{
_vertCount = 6;
// GLfloat vertices[] = {
// 0.5f, 0.5f, 0.0f, 1.0f, 1.0f, // 右上
// 0.5f, -0.5f, 0.0f, 1.0f, 0.0f, // 右下
// -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, // 左下
// -0.5f, 0.5f, 0.0f, 0.0f, 1.0f // 左上
// };
GLfloat vertices[] = {
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // 右上
0.5f, -0.5f, 0.0f, 1.0f, 1.0f, // 右下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // 左下
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // 左下
-0.5f, 0.5f, 0.0f, 0.0f, 0.0f, // 左上
0.5f, 0.5f, 0.0f, 1.0f, 0.0f, // 右上
};
// 創建VBO
_vbo = createVBO(GL_ARRAY_BUFFER, GL_STATIC_DRAW, sizeof(vertices), vertices);
glEnableVertexAttribArray(glGetAttribLocation(_program, "position"));
glVertexAttribPointer(glGetAttribLocation(_program, "position"), 3, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL);
glEnableVertexAttribArray(glGetAttribLocation(_program, "texcoord"));
glVertexAttribPointer(glGetAttribLocation(_program, "texcoord"), 2, GL_FLOAT, GL_FALSE, sizeof(GLfloat)*5, NULL+sizeof(GL_FLOAT)*3);
}
2、創建紋理對象
- (void)setupTexure
{
NSString *path = [[NSBundle mainBundle] pathForResource:@"wood" ofType:@"jpg"];
unsigned char *data;
int size;
int width;
int height;
// 加載紋理
if (read_jpeg_file(path.UTF8String, &data, &size, &width, &height) < 0) {
printf("%s\n", "decode fail");
}
// 創建紋理
_texture = createTexture2D(GL_RGB, width, height, data);
if (data) {
free(data);
data = NULL;
}
}
3、創建頂點著色器和片元著色器。在片元著色器中使用采樣器texture2D根據紋理坐標進行采樣。
attribute vec3 position;
attribute vec2 texcoord;
varying vec2 vTexcoord;
void main()
{
gl_Position = vec4(position, 1.0);
vTexcoord = texcoord;
}
precision mediump float;
uniform sampler2D image;
varying vec2 vTexcoord;
void main()
{
gl_FragColor = texture2D(image, vTexcoord);
}
4、激活紋理并渲染
- (void)render
{
glClearColor(1.0, 1.0, 0, 1.0);
glClear(GL_COLOR_BUFFER_BIT);
glLineWidth(2.0);
glViewport(0, 0, self.frame.size.width, self.frame.size.height);
// 激活紋理
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, _texture);
glUniform1i(glGetUniformLocation(_program, "image"), 0);
glDrawArrays(GL_TRIANGLES, 0, _vertCount);
// 索引數組
//unsigned int indices[] = {0,1,2,3,2,0};
//glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, indices);
//將指定 renderbuffer 呈現在屏幕上,在這里我們指定的是前面已經綁定為當前 renderbuffer 的那個,在 renderbuffer 可以被呈現之前,必須調用renderbufferStorage:fromDrawable: 為之分配存儲空間。
[_context presentRenderbuffer:GL_RENDERBUFFER];
}