前面介绍《OpenGL 3D世界》,已经可渲染出具有一定真实感的场景,但是场景中物体的颜色比较单一,相比于绚丽多彩的现实世界显得有些乏味。Texture能够解决这个问题。
纹理的定义
A texture is an OpenGL Object that contains one or more images that all have the same image format.
直译:纹理是包含相同格式的图像的一个OpenGL对象。
说的直白一些,一个图像上传GPU后,就可以得到一张纹理。
纹理映射
如果想把一张纹理应用到相应的几何图元,就必须告诉渲染系统如何进行纹理的映射。告诉的方式就是图元中顶点指定恰当的纹理坐标。纹理坐标用浮点数来表示,范围一般从0.0 ~ 1.0。映射如下:
右侧是一幅纹理图,其位于纹理坐标系中。纹理坐标系原点在左上侧,向右为S轴,向下为T轴,两个轴的取值范围都是0.0 ~ 1.0。也就是说无论纹理图的尺寸如何,其横向、纵向坐标最大值都是1。例如若实际图为256x128,则横边第256像素对应纹理坐标为1,竖边第128个像素对应纹理坐标为1.
左侧是一个三角形图元,其3个顶点都可以指定纹理坐标。如果取右图的纹理坐标就是右图所示的三角形纹理区域。
结论
纹理映射的基本思想就是首先为图元中每个顶点指定恰当的纹理坐标,然后通过纹理坐标在纹理图中可以确定选中的纹理区域,最后将选中纹理区域中的内容根据纹理坐标映射到指定的图元上。
前面说渲染管线的时候,我们知道最终用户看到的是显示在屏幕上的像素,而像素是由片元产生的。因此,进行纹理映射的过程实际上就是为左侧三角形图元 中的每个片元着色 ,用于着色的颜色需要从右侧的纹理图中提取。
提取流程:
- 图元中的每个顶点都需要在顶点着色器中通过易变变量将纹理坐标传入片元着色器。
- 经过顶点着色器后渲染管线的固定功能部分会根据情况进行插值计算,产生对应到每个片元的用于记录纹理坐标的易变变量值。
- 最后每个片元在片元着色器中根据其接收到的记录纹理坐标的易变变量值到纹理图中提出对应位置的颜色即可,提取颜色的过程一般称为纹理采样。
着色器代码
uniform mat4 uMVPMatrix;
attribute vec3 aPosition;
attribute vec2 aTexCoord;
varying vec2 vTexCoord;
void main() {
gl_Position = uMVPMatrix * vec4(aPosition,1);
vTexCoor = aTexCoord;
}
precision mediump float;
varying vec2 vTexCoord;
uniform sampler2D sTexture;
void main() {
gl_FragColor = texture2D(sTexture,vTextureCoord);
}
宿主程序
public int createTexture() {
int [] = textures = new int[1];
GLES20.glGenTextures(1,textures,0);
int textureId = textures[0];
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D,textureId);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_WRAP_T,GLES20.GL_CLAMP_TO_EDGE);
return textureId;
}
纹理拉伸
上面createTexture代码中,对纹理进行了S轴与T轴的拉伸方式。纹理有两种不同拉伸方式:重复方式和截取方式。纹理的标准坐标是0.0~1.0,有没有想过,如果设置大于1的纹理坐标会是什么情况?
当纹理坐标大于1以后,设置的拉伸方式就会起作用。
重复拉伸方式
若设置的拉伸方式为重复,当顶点纹理坐标大于1时则实际起作用的纹理坐标为纹理坐标的小数部分。也就是若纹理坐标为3.3,则起作用的纹理坐标为0.3,这种情况下会产生重复的效果。
重复拉伸方式应用场景是产生重复纹理,例如窗帘的重复花纹,地板重复砖面,大块地面重复铺满的草皮纹理,大片水面重复铺满的水波纹理。如果没有重复拉伸方式,则开发人员只能将大块面积切割成一块一块的小面积矩形,对每一块矩形单独设置0.0~0.1内纹理坐标。这样开发不但烦琐,而且大大增加了顶点的数量,程序运行时的效率也会受到很大的影响。因此开发是,面对这样的场景,设置重复拉伸方式,就可以减少顶点数量,大大提升渲染效率。
截取拉伸方式
截取方式中当纹理坐标的值大于1时都看作1,因此会产生边缘拉伸的效果。
设置纹理拉伸的代码
GLES20.glTexParameterf(GLES20.TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_S,GLES20.GL_REPEAT);
GLES20.glTexParameterf(GLES20.TEXTURE_2D,GLES20.GL_TEXTURE_WRAP_T,
GLES20.GL_CLAMP_TO_EDGE);
拉伸方式汇总
- GL_REPEAT: 默认方案,重复纹理图片。
- GL_MIRRORED_REPEAT:类似于默认方案,不过每次重复的时候进行镜像重复(另一种重复方式)。
- GL_CLAMP_TO_EDGE:将坐标限制在0到1之间。超出的坐标会重复绘制边缘的像素,变成一种扩展边缘的图案。
- GL_CLAMP_TO_BORDER:超出的坐标将会被绘制成用户指定的边界颜色(截取方式变种)。
上图各种拉伸方式效果图。
纹理采样
上面说过,所谓纹理采样就是根据片元的纹理坐标到纹理图中提取对应位置颜色的过程。由于被渲染图元中的片元数量与其对应纹理区域中像素的数量并不一定相同,也就是说图元中的片元与纹理图中的像素并不总是一一对应的。
例如,将较小的纹理图映射到较大的图元或将较大的纹理映射到较小的图元时这种情况就会产生。因此通过纹理坐标在纹理图中并不一定能找到与这完全对应的像素,这时候就需要采用一些策略使得纹理采样可以顺利进行下去。通常采用的策略有最近点采样、线性采样两种。
最近点采样
最近点采样是最简单的一种采样算法,其速度在各种采样算法中也是最快的。就是直接取片元纹理坐标对应纹理区域中最近点的颜色值。
效果特点
最近点采样很简单,计算量小。但最近点采样也有一个明显的缺点,就是若把较小的纹理图映射到较大的图元上时容易产生很明显的锯齿。其实将较大的纹理映射到较小的图元时,也会有锯齿产生,但由于图元整体较小,视觉上不那么明显。
这张是ps 设置NEAREST后,有锯齿的纹理图。
加载纹理时,设置采用最近点采样方式的代码如下:
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_NEAREST);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_NEAREST);
线性纹理采样
最近点采样会有锯齿,不能满足高视觉效果的要求。我们有时需要图像更光滑,过渡更平滑,这时可以选择线性纹理采样算法。
线性采样时结果颜色并不一定仅来自于纹理图中的一个像素,其在采样时会考虑片元对应的纹理坐标点附近的几个像素。通过加权求平均计算出最终的采样结果。
效果特点
由于线性采样时对采样范围内的多个像素进行了加权求平均,因此在将较小的纹理映射到较大的图元上时,不再会有锯齿一现象,而是平滑过渡的。但是线条边缘会模糊。
加载纹理时,设置采用线性采样方式的代码:
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR);
下面是两种采样的原理图和对比图:
两种采样方式的对比
最近点采样,采到颜色是纹理中真实的数据。如果是大纹理,小图元时,由于纹理密度大,纹理中的像素点足以描述图元中的片元,也就是说不同片元的纹理坐标离散化后,能够对应纹理中不同的像素点。这个时候最近点采样比较合适。如果是小纹理,大图元,由于纹理密度小,纹理中像素点不足描述图元中的片元,不同片元的纹理坐标离散化后,可能对应纹理中相同的像素点。这个时候线性采样比较合适,纹理的像素点已经不能正确的表示片元了,就是使用多个像素点表示。
上面设置采样方式的代码可以看出,无论采用哪种采样方式,都需要对 GL_TEXTURE_MIN_FILTER与GL_TEXTURE_MAG_FILTER两种情况分别进行设置。其实OpenGL的设计已经考虑上述情况,采样器会根据纹理图与图元大小选择不同filter。当纹理图比需要映射的图元尺寸大时系统采用MIN对应的纹理采样算法进行采样,而当纹理图比需要映射的图元尺寸小时系统采用MAG对应的纹理采样算法进行采样。 当纹理图中一个像素对应到待映射图元上的多个片元时,采用MAG采样,反之采用MIN采样。
综上,在实际开发中一般情况往往采用将MIN情况设置为最近点采样,将MAG情况设置为线性采样的组合。
mipmap纹理
事实上,如果需要处理的场景很大时,需要一张大纹理来描述。例如一大片铺满相同纹理的丘陵地形。可能会出现远处地形视觉上更清楚,近处地形更模糊的反真实现象。这主要是由于透视投影有近大远小的效果,远处的地形投影到屏幕上尺寸比较小,近处的尺寸比较大,而整个场景使用的是同一幅纹理图。
对远处的山体而言纹理图被缩小进行映射,自然很清楚;而近处的山体可能纹理图需要被拉大进行映射,自然就发虚。为了解决这个问题,应该对远处地形采用尺寸较小分辨率低的纹理,近处的采用尺寸较大分辨率高的纹理进行映射。其实这就是mipmap基本思想。
不过在应用中若要自己开发根据场景视觉大小自动选择恰当分辨率的纹理进行映射的功能会非常复杂,幸运的是mipmap仅需要在加载纹理是进行一些设置即可,其他的工作由渲染管线自动完成。
开发人员只需要提供一幅原始纹理图,系统会在纹理加载时自动生成一系列由大到小的纹理图。每幅纹理图是前一幅尺寸的1/2,直至纹理图的尺寸缩小到1x1。一系列纹理图中的第一副就是原始纹理图,因此可以轻松地计算出,一系列的mipmap纹理占用的空间接近原始纹理图的2倍。如下图:
一系列的mipmap纹理图生成成功后,当应用程序运行时,渲染管线会首先根据情况计算出细节级别,然后根据细节级别决定使用系列中哪一个分辨率的纹理图。开发中将采样方式设置为mipmap并自动生成一系列的mipmap纹理图的基本代码如下:
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER,GLES20.GL_LINEAR_MIPMAP_NEAREST);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER,GLES20.GL_LINEAR_MIPMAP_LINEAR);
GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);