前言
前面文章中,我们通过为顶点添加颜色,来创建有趣的图形,但是现实世界中的物体(例如砖墙,草坪等等)表面是有很多细节的,如果我们想要让图形看起来更加真实,必须要有足够的顶点,从而能指定足够多的颜色,描述图形的细节。但是绘制一个立方体的时候,我们就需要绘制六个面,已经感觉很繁琐,更不用说为每个面绘制不同的细节。
所以接下来我们介绍让图形看起来更加真实的技术------纹理。
纹理类型包含以下几种:2D纹理,立方图纹理,3D纹理,2D纹理数组,1D纹理,下面我们介绍最基本的2D纹理,来帮助我们了解纹理这个概念。
下面我们在正方形上加载一个纹理。
片段着色器:
#version 300 es
precision mediump float;
in vec2 oTextCoord;
uniform sampler2D texture1;
out vec4 fragColor;
void main() {
fragColor = texture(texture1,oTextCoord);
}
片段着色器中,我们用sampler2D 这个类表示纹理,texture函数加载纹理像素,需要传入两个对象:纹理对象,纹理坐标。
这里我们说明下纹理坐标的概念:
这里我们使用的是2D纹理,纹理坐标在X轴和Y轴,范围在0~1之间,纹理坐标标明从纹理图像的哪个部分采样,通过使用纹理坐标把纹理图像映射到屏幕的过程叫做纹理映射也叫纹理贴图。
代码如下:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
[super glkView:view drawInRect:rect];
_esContext.width = view.drawableWidth;
_esContext.height = view.drawableHeight;
GLfloat factor = (view.frame.size.width / view.frame.size.height) * 0.5;
//(x,y,z) (s,t)
float position[] = {
-0.5, factor, 0.0, 0.0, 0.0,
-0.5, -factor, 0.0, 0.0, 1.0,
0.5, -factor, 0.0, 1.0, 1.0,
0.5, factor, 0.0, 1.0, 0.0,
};
//开启深度测试,为了确定绘制的时候哪一个面绘制在上面
glClear(GL_COLOR_BUFFER_BIT);
GLuint vboIndex = 0;
glGenBuffers(1, &vboIndex);
glBindBuffer(GL_ARRAY_BUFFER, vboIndex);
glBufferData(GL_ARRAY_BUFFER, sizeof(position), position, GL_STATIC_DRAW);
GLuint positionIndex = glGetAttribLocation(_esContext.program, "vPosition");
GLuint textCoordIndex = glGetAttribLocation(_esContext.program, "textCoord");
GLuint textureLocation = glGetUniformLocation(_esContext.program, "texture1");
glEnableVertexAttribArray(positionIndex);
glEnableVertexAttribArray(textCoordIndex);
GLuint offset = 3 * sizeof(float);
glVertexAttribPointer(positionIndex, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), NULL);
glVertexAttribPointer(textCoordIndex, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (const void *)offset);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, self.baseTexture.name);
glUniform1i(textureLocation, 0);
_esContext.drawFunc(&_esContext);
//关闭顶点属性
glDisableVertexAttribArray(positionIndex);
glDisableVertexAttribArray(textCoordIndex);
glDeleteBuffers(1, &vboIndex);
}
代码中注意以下两点:
1.这里我们设置顶点坐标的时候用上了屏幕宽高比,这是因为手机屏幕宽高不一样,如果按照0.5的比例来渲染的话,图像会拉伸,所以需要乘以这个宽高比。
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, self.baseTexture.name);
glUniform1i(textureLocation, 0);
这里的baseTexture是通过GLkit库提供的GLKTextLoader类加载的纹理对象。得益于这个API,帮我们省去了很多工作,示例代码中注释的部分还含有自己生成纹理对象,并绑定使用的过程。
纹理环绕
上面我们说的纹理坐标范围是0~1如果我们超过了这个范围,怎么办呢,OpenGL ES默认的行为是重复这个纹理图像(但OpenGL ES提供了更多的选择:
#define GL_REPEAT 对纹理的默认行为。重复纹理图像。
#define GL_CLAMP_TO_EDGE 纹理坐标会被约束在0到1之间,超出的部分会重复纹理坐标的边缘,产生一种边缘被拉伸的效果。
#define GL_MIRRORED_REPEAT 和GL_REPEAT一样,但每次重复图片是镜像放置的。
纹理过滤:
纹理坐标不依赖于屏幕分辨率,它可以是任意浮点值,当你有一个很大的物体,但是纹理的分辨率很低的时候,ES需要知道怎样将纹理映射到屏幕,ES这里提供了对于纹理过滤的选项。这里我们介绍最重要的两项:
1.GL_NEARST(邻近过滤),这个是默认的纹理过滤方式,当设置为这个选项时,ES会选择中心点最接近纹理坐标的那个像素。如下图所示
2.GL_LINEAR,当设置为这个选项时,它会基于纹理坐标附近的纹理像素,计算一个差值。一个纹理像素的中心距离纹理坐标越近,那么它在插值的计算中,占比越大。如下图所示:
看一下两种方式对最终成像效果的影响:
我们可以看到GL_NEARST方式的纹理具有颗粒感,可以看清像素,但是GL_LINEAR效果比较真实,过渡比较自然。
mipmap贴图
想象一下,假设我们绘制一段城墙,这个城墙表面是由一个个砖块纹理拼接而成,近处的看起来效果很自然,但是距离很远的时候,由于远处的物体可能只产生很少的片段,OpenGL ES从高分辨率纹理中为这些片段获取正确的颜色值就很困难,因为它需要对一个跨过纹理很大部分的片段只拾取一个纹理颜色,在小物体上会产生不真实的感觉,而且对它们使用高分辨率的纹理也会浪费资源,造成性能问题。
OPenGL ES使用一种叫做多级渐远纹理(Mipmap)的概念来解决这个问题,它简单来说就是一系列的纹理图像,后一个纹理图像是前一个的二分之一。多级渐远纹理背后的理念很简单:距观察者的距离超过一定的阈值,OpenGL ES 会使用不同的多级渐远纹理,即最适合物体的距离的那个。由于距离远,解析度不高也不会被用户注意到。同时,多级渐远纹理另一加分之处是它的性能非常好。
mipmap也有几种过滤方式:
GL_NEAREST_MIPMAP_NEAREST 使用最邻近的多级渐远纹理来匹配像素大小,并使用邻近插值进行纹理采样
GL_LINEAR_MIPMAP_NEAREST 使用最邻近的多级渐远纹理级别,并使用线性插值进行采样
GL_NEAREST_MIPMAP_LINEAR 在两个最匹配像素大小的多级渐远纹理之间进行线性插值,使用邻近插值进行采样
GL_LINEAR_MIPMAP_LINEAR 在两个邻近的多级渐远纹理之间使用线性插值,并使用线性插值进行采样
1.生成和绑定纹理
GLuint textId = 0;
glGenTextures(1, &textId);
glBindTexture(GL_TEXTURE_2D, textId)
2.加载纹理数据
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 750, 750, 0, GL_RGBA, GL_UNSIGNED_BYTE, (unsigned char *)_imageData.bytes);
- 设置纹理过滤方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
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);
4.绑定纹理到着色器
glUniform1i(textureLocation, 0);
使用纹理大致分为上面几步,示例程序中有两种方式,第一种简易方式是借助GLKit.后面一种是实际的操作过程。