前言
在前面的文章里我们已经学习了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,找到对应的名字即可。