OpenGL ES入门05-OpenGL ES 纹理贴图
前言
本文是关于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_NEAREST|纹理过滤 | 描述|总结|
|:---:|:---:|:---:|:---:|
|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];
}